Coverage for /home/deng/Projects/ete4/hackathon/ete4/ete4/smartview/renderer/draw_helpers.py: 29%
109 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-03-21 09:19 +0100
« prev ^ index » next coverage.py v7.2.7, created at 2024-03-21 09:19 +0100
1from collections import OrderedDict, namedtuple
2from math import sin, cos, pi, sqrt, atan2
4Box = namedtuple('Box', 'x y dx dy') # corner and size of a 2D shape
6Padding = namedtuple('Padding', 'x y')
9def clip_angles(a1, a2):
10 "Return the angles such that a1 to a2 extend at maximum from -pi to pi"
11 EPSILON = 1e-8 # without it, p1 can be == p2 and svg arcs are not drawn
12 return max(-pi + EPSILON, a1), min(pi - EPSILON, a2)
15def cartesian(point):
16 r, a = point
17 return r * cos(a), r * sin(a)
20def summary(nodes, prop="name"):
21 "Return a list of names summarizing the given list of nodes"
22 return list(OrderedDict((first_value(node, prop), None) for node in nodes).keys())
25def first_value(tree, prop):
26 "Return the value of the requested property for the first node that has it"
27 return next((node.props.get(prop) for node in tree.traverse('preorder')
28 if node.props.get(prop)), '')
31def get_xs(box):
32 x, _, dx, _ = box
33 return x, x + dx
35def get_ys(box):
36 _, y, _, dy = box
37 return y, y + dy
39def intersects_box(b1, b2):
40 "Return True if the boxes b1 and b2 (of the same kind) intersect"
41 return (intersects_segment(get_xs(b1), get_xs(b2)) and
42 intersects_segment(get_ys(b1), get_ys(b2)))
45def intersects_segment(s1, s2):
46 "Return True if the segments s1 and s2 intersect"
47 s1min, s1max = s1
48 s2min, s2max = s2
49 return s1min <= s2max and s2min <= s1max
52def intersects_angles(rect, asec):
53 "Return True if any part of rect is contained within the angles of the asec"
54 return any(intersects_segment(get_ys(circumasec(r)), get_ys(asec))
55 for r in split_thru_negative_xaxis(rect))
56 # We divide rect in two if it passes thru the -x axis, because then its
57 # circumbscribing asec goes from -pi to +pi and (wrongly) always intersects.
60def split_thru_negative_xaxis(rect):
61 "Return a list of rectangles resulting from cutting the given one"
62 x, y, dx, dy = rect
63 if x >= 0 or y > 0 or y + dy < 0:
64 return [rect]
65 else:
66 EPSILON = 1e-8
67 return [Box(x, y, dx, -y-EPSILON), Box(x, EPSILON, dx, dy + y)]
70def circumrect(asec):
71 "Return the rectangle that circumscribes the given annular sector"
72 if asec is None:
73 return None
75 rmin, amin, dr, da = asec
76 rmax, amax = rmin + dr, amin + da
78 amin, amax = clip_angles(amin, amax)
80 points = [(rmin, amin), (rmin, amax), (rmax, amin), (rmax, amax)]
81 xs = [r * cos(a) for r,a in points]
82 ys = [r * sin(a) for r,a in points]
83 xmin, ymin = min(xs), min(ys)
84 xmax, ymax = max(xs), max(ys)
86 if amin < -pi/2 < amax: # asec traverses the -y axis
87 ymin = -rmax
88 if amin < 0 < amax: # asec traverses the +x axis
89 xmax = rmax
90 if amin < pi/2 < amax: # asec traverses the +y axis
91 ymax = rmax
92 # NOTE: the annular sectors we consider never traverse the -x axis.
94 return Box(xmin, ymin, xmax - xmin, ymax - ymin)
97def circumasec(rect):
98 "Return the annular sector that circumscribes the given rectangle"
99 if rect is None:
100 return None
101 x, y, dx, dy = rect
102 points = [(x, y), (x, y+dy), (x+dx, y), (x+dx, y+dy)]
103 radius2 = [x*x + y*y for x,y in points]
104 if x <= 0 and x+dx >= 0 and y <= 0 and y+dy >= 0:
105 return Box(0, -pi, sqrt(max(radius2)), 2*pi)
106 else:
107 angles = [atan2(y, x) for x,y in points]
108 rmin, amin = sqrt(min(radius2)), min(angles)
109 return Box(rmin, amin, sqrt(max(radius2)) - rmin, max(angles) - amin)
111# Basic drawing elements.
112def draw_nodebox(box, name='', properties=None,
113 node_id=None, searched_by=None, style=None):
114 properties = { k:v for k,v in (properties or {}).items() \
115 if not (k.startswith('_') or k == 'seq')}
116 return ['nodebox', box, name,
117 properties, node_id or [],
118 searched_by or [], style or {}]
120def draw_outline(box, style=None):
121 return ['outline', box, style or {}]
123def get_line_type(style):
124 types = ['solid', 'dotted', 'dashed']
125 if style.get('type'):
126 style['type'] = types[int(style['type'])]
127 else:
128 style['type'] = types[0]
129 return style
131def draw_line(p1, p2, line_type='', parent_of=None, style=None):
132 style = get_line_type(style or {})
133 return ['line', p1, p2, line_type, parent_of or [], style]
135def draw_arc(p1, p2, large=False, arc_type='', style=None):
136 style = get_line_type(style or {})
137 return ['arc', p1, p2, int(large), arc_type, style]
139def draw_circle(center, radius, circle_type='', style=None, tooltip=None):
140 return ['circle', center, radius, circle_type, style or {}, tooltip or '']
142def draw_ellipse(center, rx, ry, ellipse_type='', style=None, tooltip=None):
143 return ['ellipse', center, rx, ry, ellipse_type, style or {}, tooltip or '']
145def draw_slice(center, r, a, da, slice_type='', style=None, tooltip=None):
146 return ['slice', (center, r, a, da), slice_type, style or {}, tooltip or '']
148def draw_triangle(box, tip, triangle_type='', style=None, tooltip=None):
149 """Returns array with all the information needed to draw a triangle
150 in front end.
151 :box: bounds triangle
152 :tip: defines tip orientation 'top', 'left' or 'right'.
153 :triangle_type: will label triangle in front end (class)
154 """
155 return ['triangle', box, tip, triangle_type, style or {}, tooltip or '']
157def draw_text(box, text, text_type='', rotation=0, anchor=None, style=None):
158 return ['text', box, text, text_type, rotation, anchor or "", style or {}]
160def draw_rect(box, rect_type, style=None, tooltip=None):
161 return ['rect', box, rect_type, style or {}, tooltip or '']
163def draw_rhombus(box, rhombus_type='', style=None, tooltip=None):
164 """ Create rhombus provided a bounding box """
165 # Rotate the box to provide a rhombus (points) to drawing engine
166 x, y, dx, dy = box
167 rhombus = ((x + dx / 2, y), # top
168 (x + dx, y + dy / 2), # right
169 (x + dx / 2, y + dy), # bottom
170 (x, y + dy / 2)) # left
171 return ['rhombus', rhombus, rhombus_type, style or {}, tooltip or '']
173def draw_arrow(box, tip, orientation='right', arrow_type='',
174 style=None, tooltip=None):
175 """ Create arrow provided a bounding box """
176 x, y, dx, dy = box
178 if orientation == 'right':
179 arrow = ((x, y),
180 (x + dx - tip, y),
181 (x + dx, y + dy / 2),
182 (x + dx - tip, y + dy),
183 (x, y + dy))
184 elif orientation == 'left':
185 arrow = ((x, y + dy / 2),
186 (x + tip, y),
187 (x + dx, y),
188 (x + dx, y + dy),
189 (x + tip, y + dy))
190 return ['polygon', arrow, arrow_type, style or {}, tooltip or '']
192def draw_array(box, a, tooltip=None):
193 return ['array', box, a, tooltip or '']
195def draw_html(box, html, html_type='', style=None):
196 return ['html', box, html, html_type, style or {}]
198def draw_img(box, img, img_type='', style=None):
199 return ['img', box, img, img_type, style or {}]