Added TODO and DEVELOPMENT.
[faces-project.git] / faces / charting / connector.py
blobbe4d7c19af773fa2360bf2bff2ecd810f455d5ca
1 #@+leo-ver=4
2 #@+node:@file charting/connector.py
3 #@@language python
4 """
5 Line connectors between two widgets.
6 """
7 #@<< Copyright >>
8 #@+node:<< Copyright >>
9 ############################################################################
10 # Copyright (C) 2005, 2006, 2007, 2008 by Reithinger GmbH
11 # mreithinger@web.de
13 # This file is part of faces.
15 # faces is free software; you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License as published by
17 # the Free Software Foundation; either version 2 of the License, or
18 # (at your option) any later version.
20 # faces is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
25 # You should have received a copy of the GNU General Public License
26 # along with this program; if not, write to the
27 # Free Software Foundation, Inc.,
28 # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
29 ############################################################################
31 #@-node:<< Copyright >>
32 #@nl
33 #@<< Imports >>
34 #@+node:<< Imports >>
35 import widgets
36 import matplotlib.transforms as mtrans
37 import matplotlib.colors as colors
38 import math
39 from tools import *
43 #@-node:<< Imports >>
44 #@nl
46 _is_source_ = True
47 _colorConverter = colors.colorConverter
49 #@+others
50 #@+node:get_arrow
51 def get_arrow(cos, sin, tx, ty, width, height):
52 h = Lazy(height)
53 wm = Lazy(-width * 0.5)
54 wp = Lazy(width * 0.5)
56 arrow = [ (h, wm), (0, 0), (h, wp) ]
58 m11 = HSEP * cos
59 m12 = -HSEP * sin
60 m21 = VSEP * sin
61 m22 = VSEP * cos
63 def multplus(vector):
64 return (m11 * vector[0] + m12 * vector[1] + tx,
65 m21 * vector[0] + m22 * vector[1] + ty)
67 return map(multplus, arrow)
68 #@-node:get_arrow
69 #@+node:intersect
70 def intersect(l1, l2):
71 """
72 Intersects two lines:
73 l = (p, d) == p + a * d
75 p1 + a * d1 = p2 + b * d2
77 returns a, b
78 """
79 #@ << extract coordinates >>
80 #@+node:<< extract coordinates >>
81 p1, d1 = l1
82 p2, d2 = l2
84 p1x, p1y = p1
85 d1x, d1y = d1
87 p2x, p2y = p2
88 d2x, d2y = d2
89 #@nonl
90 #@-node:<< extract coordinates >>
91 #@nl
92 l1x = p1x, d1x
93 l1y = p1y, d1y
94 l2x = p2x, d2x
95 l2y = p2y, d2y
97 #@ << define calc >>
98 #@+node:<< define calc >>
99 def calc(mx, my, nx, ny):
100 ax, bx = mx
101 ay, by = my
102 cx, dx = nx
103 cy, dy = ny
105 rf = bx / by
106 n = ((cx - ax - rf * (cy - ay)))
107 d = rf * dy - dx
108 beta = n / d
109 beta.get()
111 if bx.get():
112 alpha = (cx + beta * dx - ax) / bx
113 else:
114 alpha = (cy + beta * dy - ay) / by
116 return alpha, beta
117 #@nonl
118 #@-node:<< define calc >>
119 #@nl
121 try:
122 a, b = calc(l1x, l1y, l2x, l2y)
123 return a, b
124 except ZeroDivisionError: pass
126 try:
127 a, b = calc(l1y, l1x, l2y, l2x)
128 return a, b
129 except ZeroDivisionError: pass
131 try:
132 b, a = calc(l2x, l2y, l1x, l1y)
133 return a, b
134 except ZeroDivisionError: pass
136 try:
137 b, a = calc(l2y, l2x, l1y, l1x)
138 return a, b
139 except ZeroDivisionError:
140 #lines are parallel
141 return mtrans.Value(-1), mtrans.Value(-1)
142 #@-node:intersect
143 #@+node:GanttConnector
144 #@+node:Classes for calculating a line path
145 #@+node:class ConnectorPath
146 class ConnectorPath(object):
148 Base class for path calculation.
150 #@ @+others
151 #@+node:calc_start_end
152 def calc_start_end(cls, src, dest):
153 x_ends = cls.calc_x_ends(src, dest)
154 return min(x_ends), max(x_ends)
156 calc_start_end = classmethod(calc_start_end)
157 #@nonl
158 #@-node:calc_start_end
159 #@+node:get_lines
160 def get_lines(cls, src, dest, transform):
161 src_end, dest_end = cls.calc_x_ends(src, dest)
163 def nearest_x(to_find, verts):
164 return float(min(map(lambda v: (abs(float(v[0]) - to_find), v[0]),
165 verts))[1])
166 so = src_end
167 do = dest_end
168 src_end = nearest_x(src_end, src.shape[0].get_verts())
169 dest_end = nearest_x(dest_end, dest.shape[0].get_verts())
171 return cls.get_edges((src_end, src.row.y.get(), src),
172 (dest_end, dest.row.y.get(), dest))
174 get_lines = classmethod(get_lines)
175 #@nonl
176 #@-node:get_lines
177 #@+node:point_near
178 def point_near(cls, point_widget, wanted_y):
179 """find all possible connector ends for a given x, y"""
180 x, y, widget = point_widget
182 bb_shape = mtrans.bound_vertices(widget.shape[0].get_verts())
183 set_helpers(bb_shape, bb_shape)
184 verts = widget.shape[0].get_verts()
185 wy = wanted_y.get()
187 def dist_point(point):
188 #freeze points
189 px = float(point[0])
190 py = float(point[1])
191 dx = abs(px - x) / HSEP.get()
192 dy = abs(py - wy) / VSEP.get()
193 return (dx + dy, (point[0], point[1]))
195 return min([ dist_point(p) for p in verts ])[1]
197 point_near = classmethod(point_near)
198 #@nonl
199 #@-node:point_near
200 #@+node:find_y_pos
201 def find_y_pos(cls, src, dest):
202 if src[1] < dest[1]: return TOP, BOTTOM
203 if src[1] > dest[1]: return BOTTOM, TOP
204 return VCENTER, VCENTER
206 find_y_pos = classmethod(find_y_pos)
207 #@nonl
208 #@-node:find_y_pos
209 #@+node:get_edges
210 def get_edges(cls, src, dest):
211 src_y, dest_y = cls.find_y_pos(src, dest)
212 return (cls.point_near(src, src_y), cls.point_near(dest, dest_y))
214 get_edges = classmethod(get_edges)
215 #@nonl
216 #@-node:get_edges
217 #@-others
219 #@-node:class ConnectorPath
220 #@+node:class StartEndPath
221 class StartEndPath(ConnectorPath):
222 #@ @+others
223 #@+node:calc_x_ends
224 def calc_x_ends(src, dest):
225 return src.start, dest.end
227 calc_x_ends = staticmethod(calc_x_ends)
228 #@nonl
229 #@-node:calc_x_ends
230 #@+node:get_edges
231 def get_edges(cls, src, dest):
232 src_y, dest_y = cls.find_y_pos(src, dest)
234 if src[0] == dest[0]:
235 return (cls.point_near(src, src_y),
236 cls.point_near(dest, dest_y))
238 if dest[0] < src[0]:
239 s = cls.point_near(src, VCENTER)
240 d = cls.point_near(dest, dest_y)
241 return (s, (d[0], s[1]), d)
243 sp = cls.point_near(src, src_y)
244 dp = cls.point_near(dest, dest_y)
246 # src[0] > dest[0]
247 row = src[2].row
248 if src[1] < dest[1]:
249 next_floor = row.y + VSEP * row.top_sep / 2
250 else:
251 next_floor = row.y - row.height - row.bottom_sep / 2 * VSEP
253 return (sp, (sp[0], next_floor), (dp[0], next_floor), (dp))
255 get_edges = classmethod(get_edges)
256 #@nonl
257 #@-node:get_edges
258 #@-others
260 #@-node:class StartEndPath
261 #@+node:class StartStartPath
262 class StartStartPath(ConnectorPath):
263 #@ @+others
264 #@+node:calc_x_ends
265 def calc_x_ends(src, dest):
266 return src.start, dest.start
268 calc_x_ends = staticmethod(calc_x_ends)
269 #@nonl
270 #@-node:calc_x_ends
271 #@+node:get_edges
272 def get_edges(cls, src, dest):
273 src_y, dest_y = cls.find_y_pos(src, dest)
275 if src[0] == dest[0]:
276 src = cls.point_near(src, src_y)
277 dest = cls.point_near(dest, dest_y)
278 return (src, dest)
280 if dest[0] < src[0]:
281 src = cls.point_near(src, VCENTER)
282 dest = cls.point_near(dest, dest_y)
283 return (src, (dest[0], src[1]), dest)
285 src = cls.point_near(src, src_y)
286 dest = cls.point_near(dest, VCENTER)
288 # src[0] > dest[0]
289 return (src, (src[0], dest[1]), dest)
291 get_edges = classmethod(get_edges)
292 #@nonl
293 #@-node:get_edges
294 #@-others
296 #@-node:class StartStartPath
297 #@+node:class EndStartPath
298 class EndStartPath(ConnectorPath):
299 #@ @+others
300 #@+node:calc_x_ends
301 def calc_x_ends(src, dest):
302 return src.end, dest.start
304 calc_x_ends = staticmethod(calc_x_ends)
305 #@-node:calc_x_ends
306 #@+node:get_edges
307 def get_edges(cls, src, dest):
308 src_y, dest_y = cls.find_y_pos(src, dest)
310 if src[0] == dest[0]:
311 return (cls.point_near(src, src_y),
312 cls.point_near(dest, dest_y))
314 if src[0] < dest[0]:
315 s = cls.point_near(src, VCENTER)
316 d = cls.point_near(dest, dest_y)
317 return (s, (d[0], s[1]), d)
319 s = cls.point_near(src, src_y)
320 d = cls.point_near(dest, dest_y)
322 # src[0] > dest[0]
323 row = src[2].row
324 if src[1] < dest[1]:
325 next_floor = row.y + VSEP * row.top_sep / 2
326 else:
327 next_floor = row.y - row.height - row.bottom_sep / 2 * VSEP
329 return (s, (s[0], next_floor), (d[0], next_floor), d)
331 get_edges = classmethod(get_edges)
332 #@nonl
333 #@-node:get_edges
334 #@-others
335 #@-node:class EndStartPath
336 #@+node:class EndEndPath
337 class EndEndPath(ConnectorPath):
338 #@ @+others
339 #@+node:calc_x_ends
340 def calc_x_ends(src, dest):
341 return src.end, dest.end
343 calc_x_ends = staticmethod(calc_x_ends)
344 #@-node:calc_x_ends
345 #@+node:get_edges
346 def get_edges(cls, src, dest):
347 src_y, dest_y = cls.find_y_pos(src, dest)
349 if src[0] == dest[0]:
350 src = cls.point_near(src, src_y)
351 dest = cls.point_near(dest, dest_y)
352 return (src, dest)
354 if src[0] < dest[0]:
355 src = cls.point_near(src, VCENTER)
356 dest = cls.point_near(dest, dest_y)
357 return (src, (dest[0], src[1]), dest)
359 src = cls.point_near(src, src_y)
360 dest = cls.point_near(dest, VCENTER)
362 # src[0] > dest[0]
363 return (src, (src[0], dest[1]), dest)
365 get_edges = classmethod(get_edges)
366 #@nonl
367 #@-node:get_edges
368 #@-others
369 #@-node:class EndEndPath
370 #@+node:class ShortestPath
371 class ShortestPath(ConnectorPath):
372 #@ @+others
373 #@+node:calc_x_ends
374 def calc_x_ends(src, dest):
375 if src.start <= dest.end and dest.start <= src.end:
376 start = (min(src.end, dest.end) + max(src.start, dest.start)) / 2
377 return start, start
379 if src.start > dest.end: return src.start, dest.end
380 return src.end, dest.start
382 calc_x_ends = staticmethod(calc_x_ends)
383 #@nonl
384 #@-node:calc_x_ends
385 #@-others
386 #@-node:class ShortestPath
387 #@-node:Classes for calculating a line path
388 #@+node:class GanttConnector
389 class GanttConnector(widgets.Widget):
391 A connecor path between two gantt widgets.
394 #@ << declarations >>
395 #@+node:<< declarations >>
396 properties = {
397 "width" : 3,
398 "height" : 3,
399 "edgecolor" : "darkslategray",
400 "facecolor" : "darkslategray",
401 "open" : False
404 zorder = -2
406 #@-node:<< declarations >>
407 #@nl
408 #@ @+others
409 #@+node:__init__
410 def __init__(self, src, dest, path, properties=None):
411 widgets.Widget.__init__(self, properties)
412 self.src = src
413 self.dest = dest
414 self.set_path(path)
415 #@-node:__init__
416 #@+node:set_path
417 def set_path(self, path):
418 self.path = path
419 self.start, self.end = path.calc_start_end(self.src, self.dest)
420 #@-node:set_path
421 #@+node:set_property
422 def set_property(self, name, value):
423 if name.find("connector") < 0:
424 # a convinience hack
425 name = "connector." + name
427 widgets.Widget.set_property(self, name, value)
428 #@-node:set_property
429 #@+node:get_bounds
430 def get_bounds(self, renderer):
431 return self.bbox
432 #@-node:get_bounds
433 #@+node:contains
434 def contains(self, x, y):
435 return False
436 #@-node:contains
437 #@+node:prepare_draw
438 def prepare_draw(self, renderer, point_to_pixel, fig_point_to_pixel):
439 #fetch all properties
440 self.get_patch("connector")
441 self.get_patch("connector.end")
442 self.get_property("connector.end.open")
443 self.get_property("connector.end.width")
444 self.get_property("connector.end.height")
445 self.get_property("connector.end.facecolor")
447 self.set_font_factor(point_to_pixel, fig_point_to_pixel)
449 transform = self.get_transform()
450 lines = self.path.get_lines(self.src, self.dest, transform)
452 self.xs = map(lambda e: e[0], lines)
453 self.ys = map(lambda e: e[1], lines)
455 self.edx = (self.xs[-2] - self.xs[-1]) / HSEP
456 self.edy = (self.ys[-2] - self.ys[-1]) / VSEP
457 self.LL = self.edx * self.edx + self.edy * self.edy
458 self.cos = Lazy(1)
459 self.sin = Lazy(0)
461 prop = self.get_property
463 awidth = prop("connector.end.width")
464 aheight = prop("connector.end.height")
466 self.arrow = get_arrow(self.cos, self.sin,
467 self.xs[-1], self.ys[-1],
468 awidth, aheight)
470 Point = mtrans.Point
471 Value = mtrans.Value
472 Bbox = mtrans.Bbox
474 to_float = lambda v: (float(v), v)
475 xs = map(to_float, self.xs)
476 ys = map(to_float, self.ys)
478 left = min(xs)[1] - (awidth * 0.5) * HSEP
479 right = max(xs)[1] + (awidth * 0.5) * HSEP
480 bottom = min(ys)[1]
481 top = max(ys)[1]
482 self.bbox = Bbox(Point(left.val, bottom.val),
483 Point(right.val, top.val))
484 return True, True
485 #@-node:prepare_draw
486 #@+node:draw
487 def draw(self, renderer, data_box):
488 if not self.get_visible() or not self.overlaps(data_box): return False
489 transform = self.get_transform()
491 gc = renderer.new_gc()
492 if self.get_clip_on():
493 gc.set_clip_rectangle(self.clipbox.get_bounds())
495 self.set_gc(gc, "connector")
496 draw_lines(renderer, gc, self.xs, self.ys, transform)
498 l = math.sqrt(float(self.LL)) or 1.0
499 self.cos.set(float(self.edx) / l)
500 self.sin.set(float(self.edy) / l)
502 self.set_gc(gc, "connector.end")
503 if self.get_property("connector.end.open"):
504 draw_lines(renderer, gc,
505 map(lambda a: a[0], self.arrow),
506 map(lambda a: a[1], self.arrow),
507 transform)
508 else:
509 face = self.get_property("connector.end.facecolor")
510 face = _colorConverter.to_rgb(face)
511 renderer.draw_polygon(gc, face, transform.seq_xy_tups(self.arrow))
513 return False
514 #@-node:draw
515 #@-others
516 #@-node:class GanttConnector
517 #@-node:GanttConnector
518 #@+node:class WBKConnector
519 class WBKConnector(widgets.Widget):
521 A connector of widgets inside a workbreakdown chart
523 #@ << declarations >>
524 #@+node:<< declarations >>
525 properties = {
526 "connector.linewidth" : 1,
527 "connector.edgecolor" : "black",
530 zorder = -2
532 #@-node:<< declarations >>
533 #@nl
534 #@ @+others
535 #@+node:__init__
536 def __init__(self, src, dest, properties=None):
537 widgets.Widget.__init__(self, properties)
538 self.src = src
539 self.dest = dest
541 #fetch all properties
542 self.get_patch("connector")
543 #@-node:__init__
544 #@+node:get_bounds
545 def get_bounds(self, renderer):
546 return self.bbox
547 #@-node:get_bounds
548 #@+node:contains
549 def contains(self, x, y):
550 return False
551 #@-node:contains
552 #@+node:prepare_draw
553 def prepare_draw(self, renderer, point_to_pixel, fig_point_to_pixel):
554 self.set_font_factor(point_to_pixel, fig_point_to_pixel)
556 HSEP.set(VSEP.get())
557 Point = mtrans.Point
558 BBox = mtrans.Bbox
560 src_box = self.src.bbox
561 dst_box = self.dest.bbox
563 if self.src.row.y.get() > self.dest.row.y.get():
564 self.bbox = BBox(Point(src_box.ur().x(), dst_box.ll().y()),
565 Point(dst_box.ll().x(), src_box.ur().y()))
566 else:
567 self.bbox = BBox(Point(src_box.ur().x(), src_box.ll().y()),
568 Point(dst_box.ll().x(), dst_box.ur().y()))
570 hor_middle = float(self.src.col.x + self.src.col.full_width()\
571 - self.src.col.right_sep * HSEP.get() / 2)
572 src_middle = (src_box.ymin() + src_box.ymax()) / 2
573 dst_middle = (dst_box.ymin() + dst_box.ymax()) / 2
575 self.xs = (src_box.xmax(), hor_middle, hor_middle, dst_box.xmin())
576 self.ys = (src_middle, src_middle, dst_middle, dst_middle)
577 return True, True
578 #@-node:prepare_draw
579 #@+node:draw
580 def draw(self, renderer, data_box):
581 if not self.get_visible() or not self.overlaps(data_box): return False
582 transform = self.get_transform()
584 gc = renderer.new_gc()
585 if self.get_clip_on():
586 gc.set_clip_rectangle(self.clipbox.get_bounds())
588 self.set_gc(gc, "connector")
589 draw_lines(renderer, gc, self.xs, self.ys, transform)
590 return False
591 #@-node:draw
592 #@-others
593 #@-node:class WBKConnector
594 #@+node:class ShortConnector
595 class ShortConnector(widgets.Widget):
597 A connector that connects two arbitrary widgets on the shortes path
599 #@ << declarations >>
600 #@+node:<< declarations >>
601 properties = {
602 "connector.linewidth" : 1,
603 "connector.edgecolor" : "black",
604 "connector.arrow.width" : 3,
605 "connector.arrow.height" : 3,
606 "connector.arrow.edgecolor" : "darkslategray",
607 "connector.arrow.facecolor" : "darkslategray",
608 "connector.arrow.open" : False,
609 "connector.directed" : True
612 zorder = -100
614 #@-node:<< declarations >>
615 #@nl
616 #@ @+others
617 #@+node:__init__
618 def __init__(self, src, dest, properties=None):
619 widgets.Widget.__init__(self, properties)
620 self.src = src
621 self.dest = dest
623 #fetch all properties
624 self.get_patch("connector")
625 #@-node:__init__
626 #@+node:get_bounds
627 def get_bounds(self, renderer):
628 return self.bbox
629 #@-node:get_bounds
630 #@+node:contains
631 def contains(self, x, y):
632 return False
633 #@-node:contains
634 #@+node:prepare_draw
635 def prepare_draw(self, renderer, point_to_pixel, fig_point_to_pixel):
636 self.set_font_factor(point_to_pixel, fig_point_to_pixel)
638 HSEP.set(VSEP.get())
639 Point = mtrans.Point
640 BBox = mtrans.Bbox
641 Value = mtrans.Value
642 Half = Value(0.5)
644 src_box = self.src.bbox
645 dst_box = self.dest.bbox
647 src_x = (src_box.ur().x() + src_box.ll().x()) * Half
648 src_y = (src_box.ur().y() + src_box.ll().y()) * Half
649 dst_x = (dst_box.ur().x() + dst_box.ll().x()) * Half
650 dst_y = (dst_box.ur().y() + dst_box.ll().y()) * Half
652 self.bbox = BBox(Point(src_x, src_y), Point(dst_x, dst_y))
655 if self.get_property("connector.directed"):
656 #@ << calc arrow position >>
657 #@+node:<< calc arrow position >>
658 self.dx = dx = dst_x - src_x
659 self.dy = dy = dst_y - src_y
661 #@<< calc line equations >>
662 #@+node:<< calc line equations >>
663 connector = ((src_x, src_y), (dx, dy))
664 left = ((dst_box.ll().x(), dst_box.ll().y()),
665 (Value(0), dst_box.ur().y() - dst_box.ll().y()))
666 right = ((dst_box.ur().x(), dst_box.ll().y()),
667 (Value(0), dst_box.ur().y() - dst_box.ll().y()))
668 top = ((dst_box.ll().x(), dst_box.ur().y()),
669 (dst_box.ur().x() - dst_box.ll().x(), Value(0)))
670 bottom = ((dst_box.ll().x(), dst_box.ll().y()),
671 (dst_box.ur().x() - dst_box.ll().x(), Value(0)))
672 #@nonl
673 #@-node:<< calc line equations >>
674 #@nl
676 #find the intersection point
677 ips = [ intersect(connector, l) for l in (left, right, top, bottom) ]
678 min_a = min([ (a.get(), a) for a, b in ips if 0 <= b.get() <= 1.0 ])[1]
680 # arrow stuff
681 self.len = dx * dx + dy * dy
682 self.cos = Lazy(1)
683 self.sin = Lazy(0)
685 prop = self.get_property
686 awidth = prop("connector.arrow.width")
687 aheight = prop("connector.arrow.height")
689 self.arrow = get_arrow(self.cos, self.sin,
690 src_x + min_a * dx,
691 src_y + min_a * dy,
692 awidth, aheight)
693 #@nonl
694 #@-node:<< calc arrow position >>
695 #@nl
697 return True, True
698 #@nonl
699 #@-node:prepare_draw
700 #@+node:draw
701 def draw(self, renderer, data_box):
702 if not self.get_visible() or not self.overlaps(data_box): return False
703 transform = self.get_transform()
705 gc = renderer.new_gc()
706 if self.get_clip_on():
707 gc.set_clip_rectangle(self.clipbox.get_bounds())
709 self.set_gc(gc, "connector")
711 x, y, w, h = self.bbox.get_bounds()
712 draw_lines(renderer, gc, (x, x + w), (y, y + h), transform)
714 if self.get_property("connector.directed"):
715 #@ << draw arrow >>
716 #@+node:<< draw arrow >>
717 l = math.sqrt(self.len.get()) or 1.0
718 self.cos.set(-self.dx.get() / l)
719 self.sin.set(-self.dy.get() / l)
721 self.set_gc(gc, "connector.arrow")
722 if self.get_property("connector.arrow.open"):
723 draw_lines(renderer, gc,
724 map(lambda a: a[0], self.arrow),
725 map(lambda a: a[1], self.arrow),
726 transform)
727 else:
728 face = self.get_property("connector.arrow.facecolor")
729 face = _colorConverter.to_rgb(face)
730 try:
731 arrow = transform.seq_xy_tups(self.arrow)
732 except ZeroDivisionError:
733 print "ZeroDivisionError"
734 #for c, a in enumerate(self.arrow):
735 # print " ", c, a[0].get(), a[1].get()
736 else:
737 renderer.draw_polygon(gc, face, arrow)
738 #@nonl
739 #@-node:<< draw arrow >>
740 #@nl
742 return False
743 #@nonl
744 #@-node:draw
745 #@-others
746 #@-node:class ShortConnector
747 #@-others
748 #@-node:@file charting/connector.py
749 #@-leo