2 #@+node:@file charting/connector.py
5 Line connectors between two widgets.
8 #@+node:<< Copyright >>
9 ############################################################################
10 # Copyright (C) 2005, 2006, 2007, 2008 by Reithinger GmbH
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 >>
36 import matplotlib
.transforms
as mtrans
37 import matplotlib
.colors
as colors
47 _colorConverter
= colors
.colorConverter
51 def get_arrow(cos
, sin
, tx
, ty
, width
, height
):
53 wm
= Lazy(-width
* 0.5)
54 wp
= Lazy(width
* 0.5)
56 arrow
= [ (h
, wm
), (0, 0), (h
, wp
) ]
64 return (m11
* vector
[0] + m12
* vector
[1] + tx
,
65 m21
* vector
[0] + m22
* vector
[1] + ty
)
67 return map(multplus
, arrow
)
70 def intersect(l1
, l2
):
73 l = (p, d) == p + a * d
75 p1 + a * d1 = p2 + b * d2
79 #@ << extract coordinates >>
80 #@+node:<< extract coordinates >>
90 #@-node:<< extract coordinates >>
98 #@+node:<< define calc >>
99 def calc(mx
, my
, nx
, ny
):
106 n
= ((cx
- ax
- rf
* (cy
- ay
)))
112 alpha
= (cx
+ beta
* dx
- ax
) / bx
114 alpha
= (cy
+ beta
* dy
- ay
) / by
118 #@-node:<< define calc >>
122 a
, b
= calc(l1x
, l1y
, l2x
, l2y
)
124 except ZeroDivisionError: pass
127 a
, b
= calc(l1y
, l1x
, l2y
, l2x
)
129 except ZeroDivisionError: pass
132 b
, a
= calc(l2x
, l2y
, l1x
, l1y
)
134 except ZeroDivisionError: pass
137 b
, a
= calc(l2y
, l2x
, l1y
, l1x
)
139 except ZeroDivisionError:
141 return mtrans
.Value(-1), mtrans
.Value(-1)
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.
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
)
158 #@-node:calc_start_end
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]),
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
)
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()
187 def dist_point(point
):
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
)
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
)
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
)
219 #@-node:class ConnectorPath
220 #@+node:class StartEndPath
221 class StartEndPath(ConnectorPath
):
224 def calc_x_ends(src
, dest
):
225 return src
.start
, dest
.end
227 calc_x_ends
= staticmethod(calc_x_ends
)
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
))
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
)
249 next_floor
= row
.y
+ VSEP
* row
.top_sep
/ 2
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
)
260 #@-node:class StartEndPath
261 #@+node:class StartStartPath
262 class StartStartPath(ConnectorPath
):
265 def calc_x_ends(src
, dest
):
266 return src
.start
, dest
.start
268 calc_x_ends
= staticmethod(calc_x_ends
)
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
)
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
)
289 return (src
, (src
[0], dest
[1]), dest
)
291 get_edges
= classmethod(get_edges
)
296 #@-node:class StartStartPath
297 #@+node:class EndStartPath
298 class EndStartPath(ConnectorPath
):
301 def calc_x_ends(src
, dest
):
302 return src
.end
, dest
.start
304 calc_x_ends
= staticmethod(calc_x_ends
)
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
))
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
)
325 next_floor
= row
.y
+ VSEP
* row
.top_sep
/ 2
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
)
335 #@-node:class EndStartPath
336 #@+node:class EndEndPath
337 class EndEndPath(ConnectorPath
):
340 def calc_x_ends(src
, dest
):
341 return src
.end
, dest
.end
343 calc_x_ends
= staticmethod(calc_x_ends
)
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
)
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
)
363 return (src
, (src
[0], dest
[1]), dest
)
365 get_edges
= classmethod(get_edges
)
369 #@-node:class EndEndPath
370 #@+node:class ShortestPath
371 class ShortestPath(ConnectorPath
):
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
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
)
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 >>
399 "edgecolor" : "darkslategray",
400 "facecolor" : "darkslategray",
406 #@-node:<< declarations >>
410 def __init__(self
, src
, dest
, path
, properties
=None):
411 widgets
.Widget
.__init
__(self
, properties
)
417 def set_path(self
, path
):
419 self
.start
, self
.end
= path
.calc_start_end(self
.src
, self
.dest
)
422 def set_property(self
, name
, value
):
423 if name
.find("connector") < 0:
425 name
= "connector." + name
427 widgets
.Widget
.set_property(self
, name
, value
)
430 def get_bounds(self
, renderer
):
434 def contains(self
, x
, y
):
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
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],
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
482 self
.bbox
= Bbox(Point(left
.val
, bottom
.val
),
483 Point(right
.val
, top
.val
))
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
),
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
))
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 >>
526 "connector.linewidth" : 1,
527 "connector.edgecolor" : "black",
532 #@-node:<< declarations >>
536 def __init__(self
, src
, dest
, properties
=None):
537 widgets
.Widget
.__init
__(self
, properties
)
541 #fetch all properties
542 self
.get_patch("connector")
545 def get_bounds(self
, renderer
):
549 def contains(self
, x
, y
):
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
)
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()))
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
)
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
)
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 >>
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
614 #@-node:<< declarations >>
618 def __init__(self
, src
, dest
, properties
=None):
619 widgets
.Widget
.__init
__(self
, properties
)
623 #fetch all properties
624 self
.get_patch("connector")
627 def get_bounds(self
, renderer
):
631 def contains(self
, x
, y
):
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
)
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)))
673 #@-node:<< calc line equations >>
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]
681 self
.len = dx
* dx
+ dy
* dy
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
,
694 #@-node:<< calc arrow position >>
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"):
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
),
728 face
= self
.get_property("connector.arrow.facecolor")
729 face
= _colorConverter
.to_rgb(face
)
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()
737 renderer
.draw_polygon(gc
, face
, arrow
)
739 #@-node:<< draw arrow >>
746 #@-node:class ShortConnector
748 #@-node:@file charting/connector.py