1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 from types import UnicodeType
20 from xml.sax.saxutils import escape as xmlescape
21
22 from pysvg.filter import feGaussianBlur
23 from pysvg.filter import feOffset
24 from pysvg.filter import feMerge
25 from pysvg.filter import feMergeNode
26 from pysvg.filter import filter
27 from pysvg.builders import StyleBuilder
28 from pysvg.builders import ShapeBuilder
29 from pysvg.structure import g
30 from pysvg.structure import svg
31 from pysvg.structure import defs
32 from pysvg.shape import path
33 from pysvg.structure import clipPath
34 from pysvg.text import text
35
36 from timelinelib.canvas.drawing.utils import darken_color
37 from timelinelib.canvas.data import sort_categories
38 from timelinelib.features.experimental.experimentalfeatures import EXTENDED_CONTAINER_HEIGHT
39
40
41 OUTER_PADDING = 5
42 INNER_PADDING = 3
43 DATA_INDICATOR_SIZE = 10
44 SMALL_FONT_SIZE_PX = 11
45 LARGER_FONT_SIZE_PX = 14
46 Y_RECT_OFFSET = 12
47 Y_TEXT_OFFSET = 18
48 ENCODING = "utf-8"
49
50
51 -def export(path, timeline, scene, view_properties, appearence):
55
56
58
59
60
61 - def __init__(self, timeline, scene, view_properties, appearence, **kwargs):
62 self._timeline = timeline
63 self._scene = scene
64 self._appearence = appearence
65 self._view_properties = view_properties
66 self._svg = svg(width=scene.width, height=scene.height)
67 self._small_font_style = self._get_small_font_style()
68 self._small_centered_font_style = self._get_small_centered_font_style()
69 self._larger_font_style = self._get_larger_font_style()
70 try:
71 self._shadow_flag = kwargs["shadow"]
72 except KeyError:
73 self._shadow_flag = False
74
76 """
77 write the SVG code into the file with filename path. No
78 checking is done if file/path exists
79 """
80 self._svg.save(path, encoding=ENCODING)
81
83 for element in self._get_elements():
84 self._svg.addElement(element)
85
87 elements = [self._define_shadow_filter(), self._get_bg()]
88 elements.extend(self._get_events())
89 elements.extend(self._get_legend())
90 return elements
91
94
99
101 """
102 Draw background color
103 Draw background Era strips and labels
104 Draw major and minor strips, lines to all event boxes and baseline.
105 Both major and minor strips have divider lines and labels.
106 Draw now line if it is visible
107 """
108 group = g()
109 group.addElement(self._draw_background())
110 for era in self._timeline.get_all_periods():
111 group.addElement(self._draw_era_strip(era))
112 group.addElement(self._draw_era_text(era))
113 for strip in self._scene.minor_strip_data:
114 group.addElement(self._draw_minor_strip_divider_line(strip.end_time))
115 group.addElement(self._draw_minor_strip_label(strip))
116 for strip in self._scene.major_strip_data:
117 group.addElement(self._draw_major_strip_divider_line(strip.end_time))
118 group.addElement(self._draw_major_strip_label(strip))
119 group.addElement(self._draw_divider_line())
120 self._draw_lines_to_non_period_events(group, self._view_properties)
121 if self._now_line_is_visible():
122 group.addElement(self._draw_now_line())
123 return group
124
126 svg_color = self._map_svg_color(self._appearence.get_bg_colour()[:3])
127 return ShapeBuilder().createRect(0, 0, self._scene.width, self._scene.height, fill=svg_color)
128
135
136 - def _draw_era_text(self, era):
137 x, y = self._calc_era_text_metrics(era)
138 return self._draw_label(era.get_name(), x, y, self._small_centered_font_style)
139
145
147 period = era.get_time_period()
148 _, width = self._calc_era_strip_metrics(era)
149 x = self._scene.x_pos_for_time(period.start_time) + width / 2
150 y = self._scene.height - OUTER_PADDING
151 return x, y
152
155
157 label = self._scene.minor_strip.label(strip_period.start_time)
158 x = self._calc_x_for_minor_strip_label(strip_period)
159 y = self._calc_y_for_minor_strip_label()
160 return self._draw_label(label, x, y, self._small_font_style)
161
165
168
173
176
178 return ShapeBuilder().createLine(x, 0, x, self._scene.height, strokewidth=0.5, stroke=colour)
179
191
193 return ShapeBuilder().createLine(0, self._scene.divider_y, self._scene.width,
194 self._scene.divider_y, strokewidth=0.5, stroke="grey")
195
197 for (event, rect) in self._scene.event_data:
198 if rect.Y < self._scene.divider_y:
199 line, circle = self._draw_line_to_non_period_event(view_properties, event, rect)
200 group.addElement(line)
201 group.addElement(circle)
202
204 x = self._scene.x_pos_for_time(event.mean_time())
205 y = rect.Y + rect.Height / 2
206 stroke = {True: "red", False: "black"}[view_properties.is_selected(event)]
207 line = ShapeBuilder().createLine(x, y, x, self._scene.divider_y, stroke=stroke)
208 circle = ShapeBuilder().createCircle(x, self._scene.divider_y, 2)
209 return line, circle
210
212 return self._draw_vertical_line(self._scene.x_pos_for_now(), "darkred")
213
217
220
222 return self._map_svg_color(self._get_event_color(event))
223
226
232
234 """
235 map (r,g,b) color to svg string
236 """
237 return "#%02X%02X%02X" % color
238
240 return self._appearence.get_legend_visible() and len(categories) > 0
241
246
248 """
249 Draw legend for the given categories.
250
251 Box in lower right corner
252 Motivation for positioning in right corner:
253 SVG text cannot be centered since the text width cannot be calculated
254 and the first part of each event text is important.
255 ergo: text needs to be left aligned.
256 But then the probability is high that a lot of text is at the left
257 bottom
258 ergo: put the legend to the right.
259
260 +----------+
261 | Name O |
262 | Name O |
263 +----------+
264 """
265 group = g()
266 group.addElement(self._draw_categories_box(len(categories)))
267 cur_y = self._get_categories_box_y(len(categories)) + OUTER_PADDING
268 for cat in categories:
269 color_box, label = self._draw_category(self._get_categories_box_width(),
270 self._get_categories_item_height(),
271 self._get_categories_box_x(), cur_y, cat)
272 group.addElement(color_box)
273 group.addElement(label)
274 cur_y = cur_y + self._get_categories_item_height() + INNER_PADDING
275 return group
276
278 return ShapeBuilder().createRect(self._get_categories_box_x(),
279 self._get_categories_box_y(nbr_of_categories),
280 self._get_categories_box_width(),
281 self._get_categories_box_height(nbr_of_categories),
282 fill='white')
283
285
286 return int(self._scene.width * 0.15)
287
290
293
296
298 return self._scene.height - self._get_categories_box_height(nbr_of_categories) - OUTER_PADDING
299
301 return (self._draw_category_color_box(item_height, x, y, cat),
302 self._draw_category_label(width, item_height, x, y, cat))
303
305 base_color = self._map_svg_color(cat.color)
306 border_color = self._map_svg_color(darken_color(cat.color))
307 return ShapeBuilder().createRect(x + OUTER_PADDING,
308 y, item_height, item_height, fill=base_color,
309 stroke=border_color)
310
317
334
336 boxBorderColor = self._get_event_border_color(event)
337 if event.is_container() and EXTENDED_CONTAINER_HEIGHT.enabled():
338 svg_rect = ShapeBuilder().createRect(rect.X, rect.Y - Y_RECT_OFFSET, rect.GetWidth(),
339 rect.GetHeight() + Y_RECT_OFFSET,
340 stroke=boxBorderColor,
341 fill=self._get_event_box_color(event))
342 else:
343 svg_rect = ShapeBuilder().createRect(rect.X, rect.Y, rect.GetWidth(), rect.GetHeight(),
344 stroke=boxBorderColor, fill=self._get_event_box_color(event))
345 if self._shadow_flag:
346 svg_rect.set_filter("url(#filterShadow)")
347 return svg_rect
348
349 - def _draw_contents_indicator(self, event, rect):
350 """
351 The data contents indicator is a small triangle drawn in the upper
352 right corner of the event rectangle.
353 """
354 corner_x = rect.X + rect.Width
355 points = "%d,%d %d,%d %d,%d" % \
356 (corner_x - DATA_INDICATOR_SIZE, rect.Y,
357 corner_x, rect.Y,
358 corner_x, rect.Y + DATA_INDICATOR_SIZE)
359 color = self._get_box_indicator_color(event)
360 indicator = ShapeBuilder().createPolygon(points, fill=color, stroke=color)
361
362 return indicator
363
364 - def _svg_clipped_text(self, text, rect, style, center_text=False):
365 group = g()
366 group.set_clip_path("url(#%s)" % self._create_clip_path(rect))
367 group.addElement(self._draw_text(text, rect, style, center_text))
368 return group
369
371 path_id, path = self._calc_clip_path(rect)
372 clip = clipPath()
373 clip.addElement(path)
374 clip.set_id(path_id)
375 self._svg.addElement(self._create_defs(clip))
376 return path_id
377
379 rx, ry, width, height = rect
380 if rx < 0:
381 width += rx
382 rx = 0
383 pathId = "path%d_%d_%d" % (rx, ry, width)
384 p = path(pathData="M %d %d H %d V %d H %d" %
385 (rx, ry + height, rx + width, ry, rx))
386 return pathId, p
387
388 - def _draw_text(self, my_text, rect, style, center_text=False):
389 my_text = self._encode_text(my_text)
390 x, y = self._calc_text_pos(rect, center_text)
391 label = text(my_text, x, y)
392 label.set_style(style.getStyle())
393 label.set_lengthAdjust("spacingAndGlyphs")
394 return label
395
396 - def _calc_text_pos(self, rect, center_text=False):
397 rx, ry, width, height = rect
398
399
400 if rx < 0:
401 width += rx
402 x = 0
403 else:
404 x = rx + INNER_PADDING
405 if center_text:
406 x += (width - 2 * INNER_PADDING) / 2
407 y = ry + height - INNER_PADDING
408 return x, y
409
410 - def _text(self, the_text, x, y):
411 encoded_text = self._encode_text(the_text)
412 return text(encoded_text, x, y)
413
414 - def _encode_text(self, text):
415 return self._encode_unicode_text(xmlescape(text))
416
417 - def _encode_unicode_text(self, text):
418 if type(text) is UnicodeType:
419 return text.encode(ENCODING)
420 else:
421 return text
422
424 return self._create_defs(self._get_shadow_filter())
425
427 d = defs()
428 d.addElement(definition)
429 return d
430
433
436
439
441 style = StyleBuilder()
442 style.setStrokeDashArray(dash_array)
443 style.setFontFamily(fontfamily="Verdana")
444 style.setFontSize("%dpx" % size)
445 style.setTextAnchor(anchor)
446 return style
447
449 filterShadow = filter(x="-.3", y="-.5", width=1.9, height=1.9)
450 filtBlur = feGaussianBlur(stdDeviation="4")
451 filtBlur.set_in("SourceAlpha")
452 filtBlur.set_result("out1")
453 filtOffset = feOffset()
454 filtOffset.set_in("out1")
455 filtOffset.set_dx(4)
456 filtOffset.set_dy(-4)
457 filtOffset.set_result("out2")
458 filtMergeNode1 = feMergeNode()
459 filtMergeNode1.set_in("out2")
460 filtMergeNode2 = feMergeNode()
461 filtMergeNode2.set_in("SourceGraphic")
462 filtMerge = feMerge()
463 filtMerge.addElement(filtMergeNode1)
464 filtMerge.addElement(filtMergeNode2)
465 filterShadow.addElement(filtBlur)
466 filterShadow.addElement(filtOffset)
467 filterShadow.addElement(filtMerge)
468 filterShadow.set_id("filterShadow")
469 return filterShadow
470