Browse code

Handle multiple search results

* Always pan to the first result
* Show the number of results
* Provide a button to pan to the next result

Christian Hattemer authored on 30/04/2015 16:50:26 • Jose Fonseca committed on 17/08/2020 13:06:53
Showing 1 changed files
... ...
@@ -92,6 +92,9 @@ class Shape:
92 92
                 ya, yb = min(ya, y0), max(yb, y1)
93 93
         return xa, ya, xb, yb
94 94
 
95
+    def get_text(self):
96
+        return None
97
+
95 98
 
96 99
 class TextShape(Shape):
97 100
 
... ...
@@ -211,6 +214,9 @@ class TextShape(Shape):
211 214
         x, w, j = self.x, self.w, self.j
212 215
         return x - 0.5 * (1 + j) * w, -_inf, x + 0.5 * (1 - j) * w, _inf
213 216
 
217
+    def get_text(self):
218
+        return self.t
219
+
214 220
 
215 221
 class ImageShape(Shape):
216 222
 
... ...
@@ -494,6 +500,13 @@ class CompoundShape(Shape):
494 500
                 return True
495 501
         return False
496 502
 
503
+    def get_text(self):
504
+        for shape in self.shapes:
505
+            text = shape.get_text()
506
+            if text is not None:
507
+                return text
508
+        return None
509
+
497 510
 
498 511
 class Url(object):
499 512
 
Browse code

Edge highlight along curve (#82)

- changed Edge to yield Jump everywhere along the curve
- changed Edge and Graph such that edges are highlighted only when directly connected to element under cursor
- fixed issue related to non-Bezier shapes in Edge
- replaced precomputation of points by distance to curve computation
- added requirement numpy to travis configuration
- added method get_smallest_distance to Shape
- moved Edge._get_distance to BezierShape.get_smallest_distance
- changed BezierShape.get_smallest_distance to include endpoints

notEvil authored on 17/08/2020 12:34:02 • GitHub committed on 17/08/2020 12:34:02
Showing 1 changed files
... ...
@@ -26,6 +26,7 @@ from gi.repository import GdkPixbuf
26 26
 from gi.repository import Pango
27 27
 from gi.repository import PangoCairo
28 28
 import cairo
29
+import numpy
29 30
 
30 31
 _inf = float('inf')
31 32
 _get_bounding = operator.attrgetter('bounding')
... ...
@@ -68,6 +69,9 @@ class Shape:
68 69
     def search_text(self, regexp):
69 70
         return False
70 71
 
72
+    def get_smallest_distance(self, x, y):
73
+        return None
74
+
71 75
     @staticmethod
72 76
     def _bounds_from_points(points):
73 77
         x0, y0 = points[0]
... ...
@@ -408,6 +412,67 @@ class BezierShape(Shape):
408 412
             cr.set_source_rgba(*pen.color)
409 413
             cr.stroke()
410 414
 
415
+    def get_smallest_distance(self, x, y):
416
+        min_squared_distance = float('inf')
417
+
418
+        points_iter = iter(self.points)
419
+        x0, y0 = next(points_iter)
420
+
421
+        while True:
422
+            try:
423
+                x1, y1 = next(points_iter)
424
+
425
+            except StopIteration:
426
+                break
427
+
428
+            x2, y2 = next(points_iter)
429
+            x3, y3 = next(points_iter)
430
+
431
+            _e1 = -5
432
+            _e2 = (x0 - 3 * x1 + 3 * x2 - x3)
433
+            _e3 = (y0 - 3 * y1 + 3 * y2 - y3)
434
+            _e4 = 2 * x1
435
+            _e5 = 2 * y1
436
+            _e6 = x0**2
437
+            _e7 = y0**2
438
+            _e8 = -2
439
+            _e9 = (x0 - _e4 + x2)
440
+            _e10 = (y0 - _e5 + y2)
441
+            _e11 = 2 * x0
442
+            _e12 = 2 * y0
443
+            _e13 = 5 * _e6
444
+            _e14 = 5 * _e7
445
+            _e15 = x1**2
446
+            _e16 = y1**2
447
+            coefficients = [
448
+                (x - x0) * (x0 - x1) + (y - y0) * (y0 - y1),
449
+                _e13 + 3 * _e15 + _e11 * (_e1 * x1 + x2) - 2 * x * _e9 + _e14 + 3 * _e16 + _e12 * (_e1 * y1 + y2) - 2 * y * _e10,
450
+                -10 * _e6 + 9 * x1 * (_e8 * x1 + x2) + x * _e2 + x0 * (30 * x1 - 12 * x2 + x3) - 10 * _e7 + 9 * y1 * (_e8 * y1 + y2) + y * _e3 + y0 * (30 * y1 - 12 * y2 + y3),
451
+                2 * (_e13 + 18 * _e15 + 3 * x2**2 + _e4 * (-9 * x2 + x3) - _e11 * (10 * x1 - 6 * x2 + x3) + _e14 + 3 * (6 * _e16 - 6 * y1 * y2 + y2**2) + _e5 * y3 - _e12 * (10 * y1 - 6 * y2 + y3)),
452
+                _e1 * _e9 * _e2 - 5 * _e10 * _e3,
453
+                _e2**2 + _e3**2
454
+            ]
455
+            coefficients.reverse()
456
+
457
+            for t in numpy.roots(coefficients):
458
+                if 1e-6 < abs(t.imag):
459
+                    continue
460
+
461
+                t = t.real
462
+                if t < 0:
463
+                    t = 0
464
+                elif 1 < t:
465
+                    t = 1
466
+
467
+                squared_distance = ((1 - t)**3 * x0 + 3 * (1 - t)**2 * t * x1 + 3 * (1 - t) * t**2 * x2 + t**3 * x3 - x)**2 + ((1 - t)**3 * y0 + 3 * (1 - t)**2 * t * y1 + 3 * (1 - t) * t**2 * y2 + t**3 * y3 - y)**2
468
+                if squared_distance < min_squared_distance:
469
+                    min_squared_distance = squared_distance
470
+
471
+            x0 = x3
472
+            y0 = y3
473
+
474
+        return math.sqrt(min_squared_distance)
475
+
411 476
 
412 477
 class CompoundShape(Shape):
413 478
 
... ...
@@ -534,10 +599,19 @@ class Edge(Element):
534 599
         return False
535 600
 
536 601
     def get_jump(self, x, y):
537
-        if self.is_inside_begin(x, y):
538
-            return Jump(self, self.dst.x, self.dst.y, highlight=set([self, self.dst]))
539
-        if self.is_inside_end(x, y):
540
-            return Jump(self, self.src.x, self.src.y, highlight=set([self, self.src]))
602
+        for shape in self.shapes:
603
+            x1, y1, x2, y2 = shape.bounding
604
+            if (x1 - self.RADIUS) <= x and x <= (x2 + self.RADIUS) and (y1 - self.RADIUS) <= y and y <= (y2 + self.RADIUS):
605
+                break
606
+
607
+        else:
608
+            return None
609
+
610
+        for shape in self.shapes:
611
+            distance = shape.get_smallest_distance(x, y)
612
+            if distance is not None and distance <= self.RADIUS:
613
+                return Jump(self, self.src.x, self.src.y)
614
+
541 615
         return None
542 616
 
543 617
     def __repr__(self):
... ...
@@ -570,9 +644,17 @@ class Graph(Shape):
570 644
                 shape._draw(cr, highlight=False, bounding=bounding)
571 645
 
572 646
     def _draw_nodes(self, cr, bounding, highlight_items):
647
+        highlight_nodes = []
648
+        for element in highlight_items:
649
+            if isinstance(element, Edge):
650
+                highlight_nodes.append(element.src)
651
+                highlight_nodes.append(element.dst)
652
+            else:
653
+                highlight_nodes.append(element)
654
+
573 655
         for node in self.nodes:
574 656
             if bounding is None or node._intersects(bounding):
575
-                node._draw(cr, highlight=(node in highlight_items), bounding=bounding)
657
+                node._draw(cr, highlight=(node in highlight_nodes), bounding=bounding)
576 658
 
577 659
     def _draw_edges(self, cr, bounding, highlight_items):
578 660
         for edge in self.edges:
Browse code

Add support for tooltips.

- added support for tooltips
- fixed issue related to residual tooltip windows
- added tooltip window resize to label size

See https://github.com/jrfonseca/xdot.py/pull/81

Fixes https://github.com/jrfonseca/xdot.py/issues/71

notEvil authored on 06/07/2020 09:29:39 • Jose Fonseca committed on 20/07/2020 11:07:52
Showing 1 changed files
... ...
@@ -469,7 +469,7 @@ class Element(CompoundShape):
469 469
 
470 470
 class Node(Element):
471 471
 
472
-    def __init__(self, id, x, y, w, h, shapes, url):
472
+    def __init__(self, id, x, y, w, h, shapes, url, tooltip):
473 473
         Element.__init__(self, shapes)
474 474
 
475 475
         self.id = id
... ...
@@ -482,6 +482,7 @@ class Node(Element):
482 482
         self.y2 = y + 0.5*h
483 483
 
484 484
         self.url = url
485
+        self.tooltip = tooltip
485 486
 
486 487
     def is_inside(self, x, y):
487 488
         return self.x1 <= x and x <= self.x2 and self.y1 <= y and y <= self.y2
... ...
@@ -510,11 +511,12 @@ def square_distance(x1, y1, x2, y2):
510 511
 
511 512
 class Edge(Element):
512 513
 
513
-    def __init__(self, src, dst, points, shapes):
514
+    def __init__(self, src, dst, points, shapes, tooltip):
514 515
         Element.__init__(self, shapes)
515 516
         self.src = src
516 517
         self.dst = dst
517 518
         self.points = points
519
+        self.tooltip = tooltip
518 520
 
519 521
     RADIUS = 10
520 522
 
Browse code

Support "outputorder" attribute when drawing.

https://github.com/jrfonseca/xdot.py/pull/68

Tal Ben-Nun authored on 10/03/2019 16:55:23 • Jose Fonseca committed on 05/04/2019 12:56:33
Showing 1 changed files
... ...
@@ -544,7 +544,7 @@ class Edge(Element):
544 544
 
545 545
 class Graph(Shape):
546 546
 
547
-    def __init__(self, width=1, height=1, shapes=(), nodes=(), edges=()):
547
+    def __init__(self, width=1, height=1, shapes=(), nodes=(), edges=(), outputorder='breadthfirst'):
548 548
         Shape.__init__(self)
549 549
 
550 550
         self.width = width
... ...
@@ -552,6 +552,7 @@ class Graph(Shape):
552 552
         self.shapes = shapes
553 553
         self.nodes = nodes
554 554
         self.edges = edges
555
+        self.outputorder = outputorder
555 556
 
556 557
         self.bounding = Shape._envelope_bounds(
557 558
             map(_get_bounding, self.shapes),
... ...
@@ -561,6 +562,23 @@ class Graph(Shape):
561 562
     def get_size(self):
562 563
         return self.width, self.height
563 564
 
565
+    def _draw_shapes(self, cr, bounding):
566
+        for shape in self.shapes:
567
+            if bounding is None or shape._intersects(bounding):
568
+                shape._draw(cr, highlight=False, bounding=bounding)
569
+
570
+    def _draw_nodes(self, cr, bounding, highlight_items):
571
+        for node in self.nodes:
572
+            if bounding is None or node._intersects(bounding):
573
+                node._draw(cr, highlight=(node in highlight_items), bounding=bounding)
574
+
575
+    def _draw_edges(self, cr, bounding, highlight_items):
576
+        for edge in self.edges:
577
+            if bounding is None or edge._intersects(bounding):
578
+                should_highlight = any(e in highlight_items
579
+                                       for e in (edge, edge.src, edge.dst))
580
+                edge._draw(cr, highlight=should_highlight, bounding=bounding)
581
+
564 582
     def draw(self, cr, highlight_items=None, bounding=None):
565 583
         if bounding is not None:
566 584
             if not self._intersects(bounding):
... ...
@@ -575,17 +593,13 @@ class Graph(Shape):
575 593
         cr.set_line_cap(cairo.LINE_CAP_BUTT)
576 594
         cr.set_line_join(cairo.LINE_JOIN_MITER)
577 595
 
578
-        for shape in self.shapes:
579
-            if bounding is None or shape._intersects(bounding):
580
-                shape._draw(cr, highlight=False, bounding=bounding)
581
-        for edge in self.edges:
582
-            if bounding is None or edge._intersects(bounding):
583
-                should_highlight = any(e in highlight_items
584
-                                       for e in (edge, edge.src, edge.dst))
585
-                edge._draw(cr, highlight=should_highlight, bounding=bounding)
586
-        for node in self.nodes:
587
-            if bounding is None or node._intersects(bounding):
588
-                node._draw(cr, highlight=(node in highlight_items), bounding=bounding)
596
+        self._draw_shapes(cr, bounding)
597
+        if self.outputorder == 'edgesfirst':
598
+            self._draw_edges(cr, bounding, highlight_items)
599
+            self._draw_nodes(cr, bounding, highlight_items)
600
+        else:
601
+            self._draw_nodes(cr, bounding, highlight_items)
602
+            self._draw_edges(cr, bounding, highlight_items)
589 603
 
590 604
     def get_element(self, x, y):
591 605
         for node in self.nodes:
Browse code

Faster rendering of large graphs

iamahuman authored on 23/11/2018 10:47:03 • José Fonseca committed on 01/12/2018 16:57:56
Showing 1 changed files
... ...
@@ -14,6 +14,7 @@
14 14
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
15 15
 #
16 16
 import math
17
+import operator
17 18
 
18 19
 import gi
19 20
 gi.require_version('Gtk', '3.0')
... ...
@@ -26,17 +27,36 @@ from gi.repository import Pango
26 27
 from gi.repository import PangoCairo
27 28
 import cairo
28 29
 
30
+_inf = float('inf')
31
+_get_bounding = operator.attrgetter('bounding')
32
+
29 33
 
30 34
 class Shape:
31 35
     """Abstract base class for all the drawing shapes."""
36
+    bounding = (-_inf, -_inf, _inf, _inf)
32 37
 
33 38
     def __init__(self):
34 39
         pass
35 40
 
36
-    def draw(self, cr, highlight=False):
37
-        """Draw this shape with the given cairo context"""
41
+    def _intersects(self, bounding):
42
+        x0, y0, x1, y1 = bounding
43
+        x2, y2, x3, y3 = self.bounding
44
+        return x2 <= x1 and x0 <= x3 and y2 <= y1 and y0 <= y3
45
+
46
+    def _fully_in(self, bounding):
47
+        x0, y0, x1, y1 = bounding
48
+        x2, y2, x3, y3 = self.bounding
49
+        return x0 <= x2 and x3 <= x1 and y0 <= y2 and y3 <= y1
50
+
51
+    def _draw(self, cr, highlight, bounding):
52
+        """Actual draw implementation"""
38 53
         raise NotImplementedError
39 54
 
55
+    def draw(self, cr, highlight=False, bounding=None):
56
+        """Draw this shape with the given cairo context"""
57
+        if bounding is None or self._intersects(bounding):
58
+            self._draw(cr, highlight, bounding)
59
+
40 60
     def select_pen(self, highlight):
41 61
         if highlight:
42 62
             if not hasattr(self, 'highlight_pen'):
... ...
@@ -48,6 +68,26 @@ class Shape:
48 68
     def search_text(self, regexp):
49 69
         return False
50 70
 
71
+    @staticmethod
72
+    def _bounds_from_points(points):
73
+        x0, y0 = points[0]
74
+        x1, y1 = x0, y0
75
+        for i in range(1, len(points)):
76
+            x, y = points[i]
77
+            x0, x1 = min(x0, x), max(x1, x)
78
+            y0, y1 = min(y0, y), max(y1, y)
79
+        return x0, y0, x1, y1
80
+
81
+    @staticmethod
82
+    def _envelope_bounds(*args):
83
+        xa = ya = _inf
84
+        xb = yb = -_inf
85
+        for bounds in args:
86
+            for x0, y0, x1, y1 in bounds:
87
+                xa, xb = min(xa, x0), max(xb, x1)
88
+                ya, yb = min(ya, y0), max(yb, y1)
89
+        return xa, ya, xb, yb
90
+
51 91
 
52 92
 class TextShape(Shape):
53 93
 
... ...
@@ -62,7 +102,7 @@ class TextShape(Shape):
62 102
         self.w = w  # width
63 103
         self.t = t  # text
64 104
 
65
-    def draw(self, cr, highlight=False):
105
+    def _draw(self, cr, highlight, bounding):
66 106
 
67 107
         try:
68 108
             layout = self.layout
... ...
@@ -139,34 +179,22 @@ class TextShape(Shape):
139 179
         else:
140 180
             f = 1.0
141 181
 
142
-        if self.j == self.LEFT:
143
-            x = self.x
144
-        elif self.j == self.CENTER:
145
-            x = self.x - 0.5*width
146
-        elif self.j == self.RIGHT:
147
-            x = self.x - width
148
-        else:
149
-            assert 0
150
-
151 182
         y = self.y - height + descent
152 183
 
153
-        cr.move_to(x, y)
184
+        if bounding is None or (y <= bounding[3] and bounding[1] <= y + height):
185
+            x = self.x - 0.5 * (1 + self.j) * width
186
+            cr.move_to(x, y)
154 187
 
155
-        cr.save()
156
-        cr.scale(f, f)
157
-        cr.set_source_rgba(*self.select_pen(highlight).color)
158
-        PangoCairo.show_layout(cr, layout)
159
-        cr.restore()
188
+            cr.save()
189
+            cr.scale(f, f)
190
+            cr.set_source_rgba(*self.select_pen(highlight).color)
191
+            PangoCairo.show_layout(cr, layout)
192
+            cr.restore()
160 193
 
161 194
         if 0:  # DEBUG
162 195
             # show where dot thinks the text should appear
163 196
             cr.set_source_rgba(1, 0, 0, .9)
164
-            if self.j == self.LEFT:
165
-                x = self.x
166
-            elif self.j == self.CENTER:
167
-                x = self.x - 0.5*self.w
168
-            elif self.j == self.RIGHT:
169
-                x = self.x - self.w
197
+            x = self.x - 0.5 * (1 + self.j) * width
170 198
             cr.move_to(x, self.y)
171 199
             cr.line_to(x+self.w, self.y)
172 200
             cr.stroke()
... ...
@@ -174,6 +202,11 @@ class TextShape(Shape):
174 202
     def search_text(self, regexp):
175 203
         return regexp.search(self.t) is not None
176 204
 
205
+    @property
206
+    def bounding(self):
207
+        x, w, j = self.x, self.w, self.j
208
+        return x - 0.5 * (1 + j) * w, -_inf, x + 0.5 * (1 - j) * w, _inf
209
+
177 210
 
178 211
 class ImageShape(Shape):
179 212
 
... ...
@@ -186,7 +219,7 @@ class ImageShape(Shape):
186 219
         self.h = h
187 220
         self.path = path
188 221
 
189
-    def draw(self, cr, highlight=False):
222
+    def _draw(self, cr, highlight, bounding):
190 223
         pixbuf = GdkPixbuf.Pixbuf.new_from_file(self.path)
191 224
         sx = float(self.w)/float(pixbuf.get_width())
192 225
         sy = float(self.h)/float(pixbuf.get_height())
... ...
@@ -197,6 +230,11 @@ class ImageShape(Shape):
197 230
         cr.paint()
198 231
         cr.restore()
199 232
 
233
+    @property
234
+    def bounding(self):
235
+        x0, y0 = self.x0, self.y0
236
+        return x0, y0 - self.h, x0 + self.w, y0
237
+
200 238
 
201 239
 class EllipseShape(Shape):
202 240
 
... ...
@@ -209,7 +247,7 @@ class EllipseShape(Shape):
209 247
         self.h = h
210 248
         self.filled = filled
211 249
 
212
-    def draw(self, cr, highlight=False):
250
+    def _draw(self, cr, highlight, bounding):
213 251
         cr.save()
214 252
         cr.translate(self.x0, self.y0)
215 253
         cr.scale(self.w, self.h)
... ...
@@ -226,6 +264,13 @@ class EllipseShape(Shape):
226 264
             cr.set_source_rgba(*pen.color)
227 265
             cr.stroke()
228 266
 
267
+    @property
268
+    def bounding(self):
269
+        x0, y0, w, h = self.x0, self.y0, self.w, self.h
270
+        bt = 0 if self.filled else self.pen.linewidth / 2.
271
+        w, h = w + bt, h + bt
272
+        return x0 - w, y0 - h, x0 + w, y0 + h
273
+
229 274
 
230 275
 class PolygonShape(Shape):
231 276
 
... ...
@@ -235,7 +280,11 @@ class PolygonShape(Shape):
235 280
         self.points = points
236 281
         self.filled = filled
237 282
 
238
-    def draw(self, cr, highlight=False):
283
+        x0, y0, x1, y1 = Shape._bounds_from_points(self.points)
284
+        bt = 0 if self.filled else self.pen.linewidth / 2.
285
+        self.bounding = x0 - bt, y0 - bt, x1 + bt, y1 + bt
286
+
287
+    def _draw(self, cr, highlight, bounding):
239 288
         x0, y0 = self.points[-1]
240 289
         cr.move_to(x0, y0)
241 290
         for x, y in self.points:
... ...
@@ -260,7 +309,11 @@ class LineShape(Shape):
260 309
         self.pen = pen.copy()
261 310
         self.points = points
262 311
 
263
-    def draw(self, cr, highlight=False):
312
+        x0, y0, x1, y1 = Shape._bounds_from_points(self.points)
313
+        bt = self.pen.linewidth / 2.
314
+        self.bounding = x0 - bt, y0 - bt, x1 + bt, y1 + bt
315
+
316
+    def _draw(self, cr, highlight, bounding):
264 317
         x0, y0 = self.points[0]
265 318
         cr.move_to(x0, y0)
266 319
         for x1, y1 in self.points[1:]:
... ...
@@ -280,13 +333,69 @@ class BezierShape(Shape):
280 333
         self.points = points
281 334
         self.filled = filled
282 335
 
283
-    def draw(self, cr, highlight=False):
336
+        x0, y0 = self.points[0]
337
+        xa = xb = x0
338
+        ya = yb = y0
339
+        for i in range(1, len(self.points), 3):
340
+            (x1, y1), (x2, y2), (x3, y3) = self.points[i:i+3]
341
+            for t in self._cubic_bernstein_extrema(x0, x1, x2, x3):
342
+                if 0 < t < 1:  # We're dealing only with Bezier curves
343
+                    v = self._cubic_bernstein(x0, x1, x2, x3, t)
344
+                    xa, xb = min(xa, v), max(xb, v)
345
+            xa, xb = min(xa, x3), max(xb, x3)  # t=0 / t=1
346
+            for t in self._cubic_bernstein_extrema(y0, y1, y2, y3):
347
+                if 0 < t < 1:  # We're dealing only with Bezier curves
348
+                    v = self._cubic_bernstein(y0, y1, y2, y3, t)
349
+                    ya, yb = min(ya, v), max(yb, v)
350
+            ya, yb = min(ya, y3), max(yb, y3)  # t=0 / t=1
351
+            x0, y0 = x3, y3
352
+
353
+        bt = 0 if self.filled else self.pen.linewidth / 2.
354
+        self.bounding = xa - bt, ya - bt, xb + bt, yb + bt
355
+
356
+    @staticmethod
357
+    def _cubic_bernstein_extrema(p0, p1, p2, p3):
358
+        """
359
+        Find extremas of a function of real domain defined by evaluating
360
+        a cubic bernstein polynomial of given bernstein coefficients.
361
+        """
362
+        # compute coefficients of derivative
363
+        a = 3.*(p3-p0+3.*(p1-p2))
364
+        b = 6.*(p0+p2-2.*p1)
365
+        c = 3.*(p1-p0)
366
+
367
+        if a == 0:
368
+            if b == 0:
369
+                return ()  # constant
370
+            return (-c / b,)  # linear
371
+
372
+        # quadratic
373
+        # compute discriminant
374
+        d = b*b - 4.*a*c
375
+        if d < 0:
376
+            return ()
377
+
378
+        k = -2. * a
379
+        if d == 0:
380
+            return (b / k,)
381
+
382
+        r = math.sqrt(d)
383
+        return ((b + r) / k, (b - r) / k)
384
+
385
+    @staticmethod
386
+    def _cubic_bernstein(p0, p1, p2, p3, t):
387
+        """
388
+        Evaluate polynomial of given bernstein coefficients
389
+        using de Casteljau's algorithm.
390
+        """
391
+        u = 1 - t
392
+        return p0*(u**3) + 3*t*u*(p1*u + p2*t) + p3*(t**3)
393
+
394
+    def _draw(self, cr, highlight, bounding):
284 395
         x0, y0 = self.points[0]
285 396
         cr.move_to(x0, y0)
286 397
         for i in range(1, len(self.points), 3):
287
-            x1, y1 = self.points[i]
288
-            x2, y2 = self.points[i + 1]
289
-            x3, y3 = self.points[i + 2]
398
+            (x1, y1), (x2, y2), (x3, y3) = self.points[i:i+3]
290 399
             cr.curve_to(x1, y1, x2, y2, x3, y3)
291 400
         pen = self.select_pen(highlight)
292 401
         if self.filled:
... ...
@@ -305,10 +414,14 @@ class CompoundShape(Shape):
305 414
     def __init__(self, shapes):
306 415
         Shape.__init__(self)
307 416
         self.shapes = shapes
417
+        self.bounding = Shape._envelope_bounds(map(_get_bounding, self.shapes))
308 418
 
309
-    def draw(self, cr, highlight=False):
419
+    def _draw(self, cr, highlight, bounding):
420
+        if bounding is not None and self._fully_in(bounding):
421
+            bounding = None
310 422
         for shape in self.shapes:
311
-            shape.draw(cr, highlight=highlight)
423
+            if bounding is None or shape._intersects(bounding):
424
+                shape._draw(cr, highlight, bounding)
312 425
 
313 426
     def search_text(self, regexp):
314 427
         for shape in self.shapes:
... ...
@@ -440,10 +553,21 @@ class Graph(Shape):
440 553
         self.nodes = nodes
441 554
         self.edges = edges
442 555
 
556
+        self.bounding = Shape._envelope_bounds(
557
+            map(_get_bounding, self.shapes),
558
+            map(_get_bounding, self.nodes),
559
+            map(_get_bounding, self.edges))
560
+
443 561
     def get_size(self):
444 562
         return self.width, self.height
445 563
 
446
-    def draw(self, cr, highlight_items=None):
564
+    def draw(self, cr, highlight_items=None, bounding=None):
565
+        if bounding is not None:
566
+            if not self._intersects(bounding):
567
+                return
568
+            if self._fully_in(bounding):
569
+                bounding = None
570
+
447 571
         if highlight_items is None:
448 572
             highlight_items = ()
449 573
         cr.set_source_rgba(0.0, 0.0, 0.0, 1.0)
... ...
@@ -452,13 +576,16 @@ class Graph(Shape):
452 576
         cr.set_line_join(cairo.LINE_JOIN_MITER)
453 577
 
454 578
         for shape in self.shapes:
455
-            shape.draw(cr)
579
+            if bounding is None or shape._intersects(bounding):
580
+                shape._draw(cr, highlight=False, bounding=bounding)
456 581
         for edge in self.edges:
457
-            should_highlight = any(e in highlight_items
458
-                                   for e in (edge, edge.src, edge.dst))
459
-            edge.draw(cr, highlight=should_highlight)
582
+            if bounding is None or edge._intersects(bounding):
583
+                should_highlight = any(e in highlight_items
584
+                                       for e in (edge, edge.src, edge.dst))
585
+                edge._draw(cr, highlight=should_highlight, bounding=bounding)
460 586
         for node in self.nodes:
461
-            node.draw(cr, highlight=(node in highlight_items))
587
+            if bounding is None or node._intersects(bounding):
588
+                node._draw(cr, highlight=(node in highlight_items), bounding=bounding)
462 589
 
463 590
     def get_element(self, x, y):
464 591
         for node in self.nodes:
Browse code

Fix most flake8 errors

Peter Hill authored on 02/07/2016 10:27:27 • Jose Fonseca committed on 10/07/2016 08:40:15
Showing 1 changed files
... ...
@@ -83,7 +83,8 @@ class TextShape(Shape):
83 83
                 # 'TypeError: font_options must be a cairo.FontOptions or None'
84 84
                 pass
85 85
             except KeyError:
86
-                # cairo.FontOptions is not registered as a foreign struct in older PyGObject versions.
86
+                # cairo.FontOptions is not registered as a foreign
87
+                # struct in older PyGObject versions.
87 88
                 # https://git.gnome.org/browse/pygobject/commit/?id=b21f66d2a399b8c9a36a1758107b7bdff0ec8eaa
88 89
                 pass
89 90
 
... ...
@@ -121,7 +122,7 @@ class TextShape(Shape):
121 122
         else:
122 123
             PangoCairo.update_layout(cr, layout)
123 124
 
124
-        descent = 2 # XXX get descender from font metrics
125
+        descent = 2  # XXX get descender from font metrics
125 126
 
126 127
         width, height = layout.get_size()
127 128
         width = float(width)/Pango.SCALE
... ...
@@ -132,7 +133,7 @@ class TextShape(Shape):
132 133
         # scale it so that the text fits inside its box
133 134
         if width > self.w:
134 135
             f = self.w / width
135
-            width = self.w # equivalent to width *= f
136
+            width = self.w  # equivalent to width *= f
136 137
             height *= f
137 138
             descent *= f
138 139
         else:
... ...
@@ -157,7 +158,7 @@ class TextShape(Shape):
157 158
         PangoCairo.show_layout(cr, layout)
158 159
         cr.restore()
159 160
 
160
-        if 0: # DEBUG
161
+        if 0:  # DEBUG
161 162
             # show where dot thinks the text should appear
162 163
             cr.set_source_rgba(1, 0, 0, .9)
163 164
             if self.j == self.LEFT:
Browse code

Cleaner splitting into separate modules

Peter Hill authored on 02/07/2016 09:45:05 • Jose Fonseca committed on 10/07/2016 08:40:15
Showing 1 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,486 @@
1
+# Copyright 2008-2015 Jose Fonseca
2
+#
3
+# This program is free software: you can redistribute it and/or modify it
4
+# under the terms of the GNU Lesser General Public License as published
5
+# by the Free Software Foundation, either version 3 of the License, or
6
+# (at your option) any later version.
7
+#
8
+# This program is distributed in the hope that it will be useful,
9
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
+# GNU Lesser General Public License for more details.
12
+#
13
+# You should have received a copy of the GNU Lesser General Public License
14
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15
+#
16
+import math
17
+
18
+import gi
19
+gi.require_version('Gtk', '3.0')
20
+gi.require_version('PangoCairo', '1.0')
21
+
22
+from gi.repository import GObject
23
+from gi.repository import Gdk
24
+from gi.repository import GdkPixbuf
25
+from gi.repository import Pango
26
+from gi.repository import PangoCairo
27
+import cairo
28
+
29
+
30
+class Shape:
31
+    """Abstract base class for all the drawing shapes."""
32
+
33
+    def __init__(self):
34
+        pass
35
+
36
+    def draw(self, cr, highlight=False):
37
+        """Draw this shape with the given cairo context"""
38
+        raise NotImplementedError
39
+
40
+    def select_pen(self, highlight):
41
+        if highlight:
42
+            if not hasattr(self, 'highlight_pen'):
43
+                self.highlight_pen = self.pen.highlighted()
44
+            return self.highlight_pen
45
+        else:
46
+            return self.pen
47
+
48
+    def search_text(self, regexp):
49
+        return False
50
+
51
+
52
+class TextShape(Shape):
53
+
54
+    LEFT, CENTER, RIGHT = -1, 0, 1
55
+
56
+    def __init__(self, pen, x, y, j, w, t):
57
+        Shape.__init__(self)
58
+        self.pen = pen.copy()
59
+        self.x = x
60
+        self.y = y
61
+        self.j = j  # Centering
62
+        self.w = w  # width
63
+        self.t = t  # text
64
+
65
+    def draw(self, cr, highlight=False):
66
+
67
+        try:
68
+            layout = self.layout
69
+        except AttributeError:
70
+            layout = PangoCairo.create_layout(cr)
71
+
72
+            # set font options
73
+            # see http://lists.freedesktop.org/archives/cairo/2007-February/009688.html
74
+            context = layout.get_context()
75
+            fo = cairo.FontOptions()
76
+            fo.set_antialias(cairo.ANTIALIAS_DEFAULT)
77
+            fo.set_hint_style(cairo.HINT_STYLE_NONE)
78
+            fo.set_hint_metrics(cairo.HINT_METRICS_OFF)
79
+            try:
80
+                PangoCairo.context_set_font_options(context, fo)
81
+            except TypeError:
82
+                # XXX: Some broken pangocairo bindings show the error
83
+                # 'TypeError: font_options must be a cairo.FontOptions or None'
84
+                pass
85
+            except KeyError:
86
+                # cairo.FontOptions is not registered as a foreign struct in older PyGObject versions.
87
+                # https://git.gnome.org/browse/pygobject/commit/?id=b21f66d2a399b8c9a36a1758107b7bdff0ec8eaa
88
+                pass
89
+
90
+            # set font
91
+            font = Pango.FontDescription()
92
+
93
+            # https://developer.gnome.org/pango/stable/PangoMarkupFormat.html
94
+            markup = GObject.markup_escape_text(self.t)
95
+            if self.pen.bold:
96
+                markup = '<b>' + markup + '</b>'
97
+            if self.pen.italic:
98
+                markup = '<i>' + markup + '</i>'
99
+            if self.pen.underline:
100
+                markup = '<span underline="single">' + markup + '</span>'
101
+            if self.pen.strikethrough:
102
+                markup = '<s>' + markup + '</s>'
103
+            if self.pen.superscript:
104
+                markup = '<sup><small>' + markup + '</small></sup>'
105
+            if self.pen.subscript:
106
+                markup = '<sub><small>' + markup + '</small></sub>'
107
+
108
+            success, attrs, text, accel_char = Pango.parse_markup(markup, -1, '\x00')
109
+            assert success
110
+            layout.set_attributes(attrs)
111
+
112
+            font.set_family(self.pen.fontname)
113
+            font.set_absolute_size(self.pen.fontsize*Pango.SCALE)
114
+            layout.set_font_description(font)
115
+
116
+            # set text
117
+            layout.set_text(text, -1)
118
+
119
+            # cache it
120
+            self.layout = layout
121
+        else:
122
+            PangoCairo.update_layout(cr, layout)
123
+
124
+        descent = 2 # XXX get descender from font metrics
125
+
126
+        width, height = layout.get_size()
127
+        width = float(width)/Pango.SCALE
128
+        height = float(height)/Pango.SCALE
129
+
130
+        # we know the width that dot thinks this text should have
131
+        # we do not necessarily have a font with the same metrics
132
+        # scale it so that the text fits inside its box
133
+        if width > self.w:
134
+            f = self.w / width
135
+            width = self.w # equivalent to width *= f
136
+            height *= f
137
+            descent *= f
138
+        else:
139
+            f = 1.0
140
+
141
+        if self.j == self.LEFT:
142
+            x = self.x
143
+        elif self.j == self.CENTER:
144
+            x = self.x - 0.5*width
145
+        elif self.j == self.RIGHT:
146
+            x = self.x - width
147
+        else:
148
+            assert 0
149
+
150
+        y = self.y - height + descent
151
+
152
+        cr.move_to(x, y)
153
+
154
+        cr.save()
155
+        cr.scale(f, f)
156
+        cr.set_source_rgba(*self.select_pen(highlight).color)
157
+        PangoCairo.show_layout(cr, layout)
158
+        cr.restore()
159
+
160
+        if 0: # DEBUG
161
+            # show where dot thinks the text should appear
162
+            cr.set_source_rgba(1, 0, 0, .9)
163
+            if self.j == self.LEFT:
164
+                x = self.x
165
+            elif self.j == self.CENTER:
166
+                x = self.x - 0.5*self.w
167
+            elif self.j == self.RIGHT:
168
+                x = self.x - self.w
169
+            cr.move_to(x, self.y)
170
+            cr.line_to(x+self.w, self.y)
171
+            cr.stroke()
172
+
173
+    def search_text(self, regexp):
174
+        return regexp.search(self.t) is not None
175
+
176
+
177
+class ImageShape(Shape):
178
+
179
+    def __init__(self, pen, x0, y0, w, h, path):
180
+        Shape.__init__(self)
181
+        self.pen = pen.copy()
182
+        self.x0 = x0
183
+        self.y0 = y0
184
+        self.w = w
185
+        self.h = h
186
+        self.path = path
187
+
188
+    def draw(self, cr, highlight=False):
189
+        pixbuf = GdkPixbuf.Pixbuf.new_from_file(self.path)
190
+        sx = float(self.w)/float(pixbuf.get_width())
191
+        sy = float(self.h)/float(pixbuf.get_height())
192
+        cr.save()
193
+        cr.translate(self.x0, self.y0 - self.h)
194
+        cr.scale(sx, sy)
195
+        Gdk.cairo_set_source_pixbuf(cr, pixbuf, 0, 0)
196
+        cr.paint()
197
+        cr.restore()
198
+
199
+
200
+class EllipseShape(Shape):
201
+
202
+    def __init__(self, pen, x0, y0, w, h, filled=False):
203
+        Shape.__init__(self)
204
+        self.pen = pen.copy()
205
+        self.x0 = x0
206
+        self.y0 = y0
207
+        self.w = w
208
+        self.h = h
209
+        self.filled = filled
210
+
211
+    def draw(self, cr, highlight=False):
212
+        cr.save()
213
+        cr.translate(self.x0, self.y0)
214
+        cr.scale(self.w, self.h)
215
+        cr.move_to(1.0, 0.0)
216
+        cr.arc(0.0, 0.0, 1.0, 0, 2.0*math.pi)
217
+        cr.restore()
218
+        pen = self.select_pen(highlight)
219
+        if self.filled:
220
+            cr.set_source_rgba(*pen.fillcolor)
221
+            cr.fill()
222
+        else:
223
+            cr.set_dash(pen.dash)
224
+            cr.set_line_width(pen.linewidth)
225
+            cr.set_source_rgba(*pen.color)
226
+            cr.stroke()
227
+
228
+
229
+class PolygonShape(Shape):
230
+
231
+    def __init__(self, pen, points, filled=False):
232
+        Shape.__init__(self)
233
+        self.pen = pen.copy()
234
+        self.points = points
235
+        self.filled = filled
236
+
237
+    def draw(self, cr, highlight=False):
238
+        x0, y0 = self.points[-1]
239
+        cr.move_to(x0, y0)
240
+        for x, y in self.points:
241
+            cr.line_to(x, y)
242
+        cr.close_path()
243
+        pen = self.select_pen(highlight)
244
+        if self.filled:
245
+            cr.set_source_rgba(*pen.fillcolor)
246
+            cr.fill_preserve()
247
+            cr.fill()
248
+        else:
249
+            cr.set_dash(pen.dash)
250
+            cr.set_line_width(pen.linewidth)
251
+            cr.set_source_rgba(*pen.color)
252
+            cr.stroke()
253
+
254
+
255
+class LineShape(Shape):
256
+
257
+    def __init__(self, pen, points):
258
+        Shape.__init__(self)
259
+        self.pen = pen.copy()
260
+        self.points = points
261
+
262
+    def draw(self, cr, highlight=False):
263
+        x0, y0 = self.points[0]
264
+        cr.move_to(x0, y0)
265
+        for x1, y1 in self.points[1:]:
266
+            cr.line_to(x1, y1)
267
+        pen = self.select_pen(highlight)
268
+        cr.set_dash(pen.dash)
269
+        cr.set_line_width(pen.linewidth)
270
+        cr.set_source_rgba(*pen.color)
271
+        cr.stroke()
272
+
273
+
274
+class BezierShape(Shape):
275
+
276
+    def __init__(self, pen, points, filled=False):
277
+        Shape.__init__(self)
278
+        self.pen = pen.copy()
279
+        self.points = points
280
+        self.filled = filled
281
+
282
+    def draw(self, cr, highlight=False):
283
+        x0, y0 = self.points[0]
284
+        cr.move_to(x0, y0)
285
+        for i in range(1, len(self.points), 3):
286
+            x1, y1 = self.points[i]
287
+            x2, y2 = self.points[i + 1]
288
+            x3, y3 = self.points[i + 2]
289
+            cr.curve_to(x1, y1, x2, y2, x3, y3)
290
+        pen = self.select_pen(highlight)
291
+        if self.filled:
292
+            cr.set_source_rgba(*pen.fillcolor)
293
+            cr.fill_preserve()
294
+            cr.fill()
295
+        else:
296
+            cr.set_dash(pen.dash)
297
+            cr.set_line_width(pen.linewidth)
298
+            cr.set_source_rgba(*pen.color)
299
+            cr.stroke()
300
+
301
+
302
+class CompoundShape(Shape):
303
+
304
+    def __init__(self, shapes):
305
+        Shape.__init__(self)
306
+        self.shapes = shapes
307
+
308
+    def draw(self, cr, highlight=False):
309
+        for shape in self.shapes:
310
+            shape.draw(cr, highlight=highlight)
311
+
312
+    def search_text(self, regexp):
313
+        for shape in self.shapes:
314
+            if shape.search_text(regexp):
315
+                return True
316
+        return False
317
+
318
+
319
+class Url(object):
320
+
321
+    def __init__(self, item, url, highlight=None):
322
+        self.item = item
323
+        self.url = url
324
+        if highlight is None:
325
+            highlight = set([item])
326
+        self.highlight = highlight
327
+
328
+
329
+class Jump(object):
330
+
331
+    def __init__(self, item, x, y, highlight=None):
332
+        self.item = item
333
+        self.x = x
334
+        self.y = y
335
+        if highlight is None:
336
+            highlight = set([item])
337
+        self.highlight = highlight
338
+
339
+
340
+class Element(CompoundShape):
341
+    """Base class for graph nodes and edges."""
342
+
343
+    def __init__(self, shapes):
344
+        CompoundShape.__init__(self, shapes)
345
+
346
+    def is_inside(self, x, y):
347
+        return False
348
+
349
+    def get_url(self, x, y):
350
+        return None
351
+
352
+    def get_jump(self, x, y):
353
+        return None
354
+
355
+
356
+class Node(Element):
357
+
358
+    def __init__(self, id, x, y, w, h, shapes, url):
359
+        Element.__init__(self, shapes)
360
+
361
+        self.id = id
362
+        self.x = x
363
+        self.y = y
364
+
365
+        self.x1 = x - 0.5*w
366
+        self.y1 = y - 0.5*h
367
+        self.x2 = x + 0.5*w
368
+        self.y2 = y + 0.5*h
369
+
370
+        self.url = url
371
+
372
+    def is_inside(self, x, y):
373
+        return self.x1 <= x and x <= self.x2 and self.y1 <= y and y <= self.y2
374
+
375
+    def get_url(self, x, y):
376
+        if self.url is None:
377
+            return None
378
+        if self.is_inside(x, y):
379
+            return Url(self, self.url)
380
+        return None
381
+
382
+    def get_jump(self, x, y):
383
+        if self.is_inside(x, y):
384
+            return Jump(self, self.x, self.y)
385
+        return None
386
+
387
+    def __repr__(self):
388
+        return "<Node %s>" % self.id
389
+
390
+
391
+def square_distance(x1, y1, x2, y2):
392
+    deltax = x2 - x1
393
+    deltay = y2 - y1
394
+    return deltax*deltax + deltay*deltay
395
+
396
+
397
+class Edge(Element):
398
+
399
+    def __init__(self, src, dst, points, shapes):
400
+        Element.__init__(self, shapes)
401
+        self.src = src
402
+        self.dst = dst
403
+        self.points = points
404
+
405
+    RADIUS = 10
406
+
407
+    def is_inside_begin(self, x, y):
408
+        return square_distance(x, y, *self.points[0]) <= self.RADIUS*self.RADIUS
409
+
410
+    def is_inside_end(self, x, y):
411
+        return square_distance(x, y, *self.points[-1]) <= self.RADIUS*self.RADIUS
412
+
413
+    def is_inside(self, x, y):
414
+        if self.is_inside_begin(x, y):
415
+            return True
416
+        if self.is_inside_end(x, y):
417
+            return True
418
+        return False
419
+
420
+    def get_jump(self, x, y):
421
+        if self.is_inside_begin(x, y):
422
+            return Jump(self, self.dst.x, self.dst.y, highlight=set([self, self.dst]))
423
+        if self.is_inside_end(x, y):
424
+            return Jump(self, self.src.x, self.src.y, highlight=set([self, self.src]))
425
+        return None
426
+
427
+    def __repr__(self):
428
+        return "<Edge %s -> %s>" % (self.src, self.dst)
429
+
430
+
431
+class Graph(Shape):
432
+
433
+    def __init__(self, width=1, height=1, shapes=(), nodes=(), edges=()):
434
+        Shape.__init__(self)
435
+
436
+        self.width = width
437
+        self.height = height
438
+        self.shapes = shapes
439
+        self.nodes = nodes
440
+        self.edges = edges
441
+
442
+    def get_size(self):
443
+        return self.width, self.height
444
+
445
+    def draw(self, cr, highlight_items=None):
446
+        if highlight_items is None:
447
+            highlight_items = ()
448
+        cr.set_source_rgba(0.0, 0.0, 0.0, 1.0)
449
+
450
+        cr.set_line_cap(cairo.LINE_CAP_BUTT)
451
+        cr.set_line_join(cairo.LINE_JOIN_MITER)
452
+
453
+        for shape in self.shapes:
454
+            shape.draw(cr)
455
+        for edge in self.edges:
456
+            should_highlight = any(e in highlight_items
457
+                                   for e in (edge, edge.src, edge.dst))
458
+            edge.draw(cr, highlight=should_highlight)
459
+        for node in self.nodes:
460
+            node.draw(cr, highlight=(node in highlight_items))
461
+
462
+    def get_element(self, x, y):
463
+        for node in self.nodes:
464
+            if node.is_inside(x, y):
465
+                return node
466
+        for edge in self.edges:
467
+            if edge.is_inside(x, y):
468
+                return edge
469
+
470
+    def get_url(self, x, y):
471
+        for node in self.nodes:
472
+            url = node.get_url(x, y)
473
+            if url is not None:
474
+                return url
475
+        return None
476
+
477
+    def get_jump(self, x, y):
478
+        for edge in self.edges:
479
+            jump = edge.get_jump(x, y)
480
+            if jump is not None:
481
+                return jump
482
+        for node in self.nodes:
483
+            jump = node.get_jump(x, y)
484
+            if jump is not None:
485
+                return jump
486
+        return None