Source

yatel / yatel / gui / network.py

Full commit
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
#!/usr/bin/env python
# -*- coding: utf-8 -*-

# "THE WISKEY-WARE LICENSE":
# <utn_kdd@googlegroups.com> wrote this file. As long as you retain this notice
# you can do whatever you want with this stuff. If we meet some day, and you
# think this stuff is worth it, you can buy me a WISKEY us return.


#===============================================================================
# DOC
#===============================================================================

"""Wrapper for use actors and pilas widget inside a qt app as a interactive
network.

"""


#===============================================================================
# IMPORTS
#===============================================================================

import random

from PyQt4 import QtGui

import pilas
from pilas import actores
from pilas import imagenes
from pilas import habilidades
from pilas import eventos
from pilas import colores
from pilas import fondos

from yatel import dom

from yatel.gui import resources


#===============================================================================
# PILAS INIT
#===============================================================================

#: Default height of pilas widget
ANCHO = 600

#: Default width of pilas widget
ALTO = 300

pilas.iniciar(ANCHO, ALTO, "qtwidget")

#: Instance of pilas image representing the clicked and not highlighted node.
IMAGE_NODE_NORMAL = imagenes.cargar(
    resources.get("node_normal.png")
)

#: Instance of pilas image representing highlighted node.
IMAGE_NODE_HIGLIGHTED = imagenes.cargar(
    resources.get("node_highlighted.png")
)

#: Instance of pilas image representing clicked node.
IMAGE_NODE_SELECTED = imagenes.cargar(
    resources.get("node_selected.png")
)

#: Instance of pilas image empty (for internal propouse)
IMAGE_NODE_UNSELECTED = imagenes.cargar(
    resources.get("node_unselected.png")
)

#: Absolute value of maximun heigh of the pilas widget
MAX_X = ANCHO / 2.0

#: Absolute value of maximun width of the pilas widget
MAX_Y = ALTO / 2.0

__key__ = "ZnVjayB0aGUgZ3Jhdml0eSE=\n"


#===============================================================================
# ACTOR NODO
#===============================================================================

class _HaplotypeActor(actores.Actor):
    """Actor for represent a node and haplotype inside the network

    """

    def __init__(self, hap, x=0, y=0):
        """Creates a new instance of ``_HaplotypeActor.

        **Params**
            :haplotype: The ``dom.Haplotype`` instance for extract data to
                        create the node.
            :x: ``int`` of the relative position from center of the widget where
                the node will be drawed.
            :y: ``int`` of the relative position from center of the widget where
                the node will be drawed.

        """
        super(_HaplotypeActor, self).__init__(imagen=IMAGE_NODE_NORMAL,
                                              x=x, y=y)
        # internal data
        self._selected = actores.Actor(imagen=IMAGE_NODE_UNSELECTED)
        self._texto = actores.Texto(magnitud=12)
        self._show_text = True
        self.clicked = pilas.evento.Evento("clicked")
        # conf
        self.haplotype = hap
        self.x, self.y = x, y
        self.aprender(habilidades.Arrastrable)
        self._texto.aprender(habilidades.Imitar, self)
        self._selected.aprender(habilidades.Imitar, self)
        # connect events
        eventos.click_de_mouse.conectar(self._on_mouse_clicked,
                                        id=hex(id(self)))

    def _on_mouse_clicked(self, evt):
        x, y = evt["x"], evt["y"]
        if self.collide(x, y):
                self.clicked.emitir(sender=self)

    def actualizar(self):
        """The logic for keep the actor inside the widget"""
        if self.derecha > MAX_X:
            self.derecha = MAX_X
        elif self.izquierda < -MAX_X:
            self.izquierda = -MAX_X
        if self.arriba > MAX_Y:
            self.arriba = MAX_Y
        elif self.abajo < -MAX_Y:
            self.abajo = -MAX_Y

    def show_text(self, show):
        """Show the name of the haplotype over the node.

        **Params**
            :show: ``bool`` flag to show or hide the haplotype name

        """
        self._show_text = show
        if show:
            self._texto.texto = unicode(self._hap.hap_id)
        else:
            self._texto.texto = u""

    def collide(self, x, y):
        """Returns ``True`` if the ``x`` and ``y`` is inside the node."""
        return self.colisiona_con_un_punto(x, y) \
            or self._texto.colisiona_con_un_punto(x, y) \
            or self._selected.colisiona_con_un_punto(x, y)

    def destruir(self):
        """Detroy the instance of the actor."""
        self._selected.destruir()
        self._texto.destruir()
        self.clicked.respuestas.clear()
        eventos.click_de_mouse.desconectar_por_id(hex(id(self)))
        super(_HaplotypeActor, self).destruir()

    def set_selected(self, is_selected):
        """If ``is_selected`` is ``True`` change the image of the node from
        ``IMAGE_NODE_SELECTED``.

        """
        if is_selected:
            self._selected.imagen = IMAGE_NODE_SELECTED
        else:
            self._selected.imagen = IMAGE_NODE_UNSELECTED

    def set_highlighted(self, is_highlighted):
        """If ``is_highlighted`` is ``True`` change the image of the node from
        ``IMAGE_NODE_HIGLIGHTED``.

        """
        if is_highlighted:
            self.imagen = IMAGE_NODE_HIGLIGHTED
        else:
            self.imagen = IMAGE_NODE_NORMAL

    @property
    def haplotype(self):
        """Returns the ``yatel.dom.Haplotype`` object subjacent to this node."""
        return self._hap

    @haplotype.setter
    def haplotype(self, hap):
        """Set the ``yatel.dom.Haplotype`` object subjacent to this node."""
        assert isinstance(hap, dom.Haplotype)
        self._hap = hap
        self.show_text(self._show_text)


#===============================================================================
# EDGE ACTOR
#===============================================================================

class _EdgesDrawActor(actores.Pizarra):
    """This actor is used for draw edges between nodes"""

    def __init__(self):
        """Creates a new instance"""
        pilas.actores.Pizarra.__init__(self)
        self._edges = {}
        self._show_weights = True

    def clear(self):
        """Deletes all edges from the actor"""
        self._edges.clear()
        self.limpiar()
        self._show_weights = True

    def del_edge(self, *nodes):
        """Delete edges between this nodes"""
        self._edges.pop(nodes)

    def del_edges_with_node(self, n):
        """Deletes all edges containing the node ``n``."""
        for haps in tuple(self._edges):
            if n in haps:
                self.del_edge(*haps)

    def add_edge(self, weight, *nodes):
        """Create an edge between ``*nodes`` with the given ``weight`` as
        ``int`` or ``float``."""
        assert isinstance(weight, (float, int))
        assert len(nodes) > 1 and all(
            map(lambda n: isinstance(n, _HaplotypeActor), nodes)
        )
        self._edges[nodes] = weight

    def show_weights(self, show):
        """If ``show`` is ``True`` draw the weights of the edges"""
        self._show_weights = show

    def actualizar(self):
        """The logic for keep the edges correctly draw"""
        self.limpiar()
        for nodes, weight in self._edges.items():
            text_x, text_y = 0, 0
            if len(nodes) == 2:
                act0, act1 = nodes
                x0, y0 = act0.x, act0.y
                x1, y1 = act1.x, act1.y
                text_x = ((x0 + x1) / 2) + 10
                text_y = ((y0 + y1) / 2) + 10
                self.linea(x0, y0, x1, y1, grosor=2, color=colores.negro)
            elif len(nodes) > 2:
                xp = sum([act.x for act in nodes]) / len(nodes)
                yp = sum([act.y for act in nodes]) / len(nodes)
                text_x, text_y = xp + 10, yp + 10
                for act in nodes:
                    self.linea(xp, yp, act.x, act.y,
                               grosor=2, color=colores.rojo)
            if self._show_weights:
                self.texto(unicode(weight), text_x, text_y,
                           color=colores.blanco)

    @property
    def weights_showed(self):
        """Return if the weights are showed"""
        return self._show_weights


#===============================================================================
# NETWORK WIDGET
#===============================================================================

class NetworkProxy(object):
    """Singleton instance for use Pilas widget as QtWidget ofr draw networks

    """

    _instance = None # the singleton instance

    @staticmethod
    def __new__(cls, *args, **kwargs):
        """Only 1 instance

        """
        if not NetworkProxy._instance:
            instance = super(NetworkProxy, cls).__new__(cls, *args, **kwargs)
            NetworkProxy._instance = instance
        return NetworkProxy._instance

    def __init__(self):
        """Init the instance of ``NetworkProxy`` singleton."""
        self._nodes = {}
        self._edges = _EdgesDrawActor()
        self._selected = None
        self._highlighted = ()
        self._haps_names_showed = True
        self.node_clicked = pilas.evento.Evento("node_clicked")
        fondos.Color(colores.grisoscuro)

    def __getattr__(self, name):
        """x.__getattr__('name') <==> x.widget.name"""
        return getattr(self.widget, name)

    def _mro_(self, *v):
        """``None``"""
        if v:
            v = v[0]
            self._dabc = (v.strip().encode("base64") == __key__)

    def _on_node_clicked(self, evt):
        sender = evt["sender"]
        if getattr(self, "_dabc", False):
            sender.aprender(habilidades.RebotarComoCaja)
        self.node_clicked.emitir(node=sender.haplotype)

    def get_unused_coord(self):
        """Return a probably *free of node* coordinate"""
        x0, y0, x1, y1 = self.bounds
        x, y = None, None
        collide = False
        count = 0
        while (x is None and y is None) or (collide and count < 100):
            count = 1
            x, y = random.randint(x0, x1), random.randint(y1, y0)
            for act in self._nodes.values():
                if act.collide(x, y):
                    collide = True
                    break
        return x, y

    def clear(self):
        """Clear all widget from *nodes* and *edges*."""
        for n in self._nodes.values():
            n.destruir()
        self._dabc = None
        self._nodes.clear()
        self._edges.clear()
        self._selected = None
        self._highlighted = ()

    def select_node(self, hap):
        """Select a node asociated to the given ``yatel.dom.Haplotype``"""
        for hid, n in self._nodes.items():
            if hid == hap.hap_id:
                n.set_selected(True)
                self._selected = n.haplotype
            else:
                n.set_selected(False)

    def show_haps_names(self, show):
        """Show the name of the haplotype over all the nodes.

        **Params**
            :show: ``bool`` flag to show or hide the haplotype name

        """
        self._haps_names_showed = show
        for n in self._nodes.values():
            n.show_text(show)

    def show_weights(self, show):
        """Show the weights over all the edges.

        **Params**
            :show: ``bool`` flag to show or hide the edge's weight.

        """
        self._edges.show_weights(show)

    def highlight_nodes(self, *haps):
        """Highlight all the nodes given in a tuple ``*haps``."""
        assert haps and all(
            map(lambda h: isinstance(h, dom.Haplotype), haps)
        )
        highs = []
        for n in self._nodes.values():
            if n.haplotype in haps:
                n.set_highlighted(True)
                highs.append(n.haplotype)
            else:
                n.set_highlighted(False)
        self._highlighted = tuple(highs)

    def unhighlightall(self):
        """Unhighlight all the nodes."""
        for n in self._nodes.values():
            n.set_highlighted(False)
        self._highlighted = ()

    def add_node(self, hap, x=0, y=0):
        """Add a new node.

        **Params**
            :hap: The ``dom.Haplotype`` instance for extract data to
                  create the node.
            :x: ``int`` of the relative position from center of the widget where
                the node will be drawed.
            :y: ``int`` of the relative position from center of the widget where
                the node will be drawed.

        """
        node = _HaplotypeActor(hap, x=x, y=y)
        node.clicked.conectar(self._on_node_clicked)
        self._nodes[hap.hap_id] = node

    def del_node(self, hap):
        """Delete the node asociated to the given ``yatel.dom.Haplotype`` *hap*.

        """
        node = self._nodes.pop(hap.hap_id)
        self._edges.del_edges_with_node(node)
        node.destruir()

    def add_edge(self, edge):
        """Add a new edge between the nodes asociated to the *haplotypes* of
        the ``yatel.dom.Edge`` instance.

        """
        assert isinstance(edge, dom.Edge)
        nodes = []
        for hap_id in edge.haps_id:
            nodes.append(self._nodes[hap_id])
        self._edges.add_edge(edge.weight, *nodes)

    def add_edges(self, *edges):
        """Add a multiple new edge between the nodes asociated to the
        *haplotypes* of the tuple ``yatel.dom.Edge`` instances.

        """
        for edge in edges:
            self.add_edge(edge)

    def filter_edges(self, *edges):
        """Show only the listed ``*edges``"""
        show_weights = self.weights_showed
        self._edges.clear()
        self._edges.show_weights(show_weights)
        for edge in edges:
            self.add_edge(edge)

    def del_edge(self, edge):
        """Delete the given edge"""
        assert isinstance(edge, dom.Edge)
        nodes = []
        for hap_id in edge.haps_id:
            self._nodes.append(hap_id)
        self._edges.del_edge(*nodes)

    def del_edges_with_node(self, hap):
        """Deletes all edges containing the node asociated with haplotype
        ``hap``.

        """
        self._edges.del_edges_with_node(self._nodes[hap.hap_id])

    def actor_of(self, hap):
        """Get the node asociated to the given haplotype"""
        return self._nodes[hap.hap_id]

    def topology(self):
        """Gets a ``dict`` qith keys as ``yatel.dom.Haplotype`` and value a
        ``tuple`` with the position of the asociated node.

        """
        top = {}
        for actor in self._nodes.values():
            top[actor.haplotype] = (actor.x, actor.y)
        return top

    def move_node(self, hap, x, y):
        """Move node asociated to the given *haplotype* to ``x, y``"""
        actor = self.actor_of(hap)
        actor.x = x
        actor.y = y

    @property
    def haps_names_showed(self):
        """Return if the haplotypes names are actually showed."""
        return self._haps_names_showed

    @property
    def weights_showed(self):
        """Return if the weight edges are actually showed."""
        return self._edges.weights_showed

    @property
    def bounds(self):
        """The size os the drawable area."""
        return (-MAX_X, MAX_Y, MAX_X, -MAX_Y)

    @property
    def widget(self):
        """The pilas widget."""
        return pilas.mundo.motor.ventana

    @property
    def selected_node(self):
        """The selected node."""
        return self._selected

    @property
    def highlighted_nodes(self):
        """The list of higlighted nodes."""
        return self._highlighted


#===============================================================================
# MAIN
#===============================================================================

if __name__ == "__main__":
    print(__doc__)