1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 from types import UnicodeType
20
21 import wx
22 from pysvg.structure import *
23 from pysvg.core import *
24 from pysvg.text import *
25 from pysvg.shape import *
26 from pysvg.builders import *
27 from pysvg.filter import *
28
29 from timelinelib.db.objects.category import sort_categories
30 from timelinelib.drawing.utils import darken_color
31
32
33 OUTER_PADDING = 5
34 INNER_PADDING = 3
35 BASELINE_PADDING = 15
36 PERIOD_THRESHOLD = 20
37 BALLOON_RADIUS = 12
38 DATA_INDICATOR_SIZE = 10
39 SMALL_FONT_SIZE = 9
40 MAJOR_STRIP_FONT_SIZE = 6
41
42
43 -def export(path, scene, view_properties):
47
48
50
51 - def __init__(self, path, scene, view_properties, **kwargs):
52
53 self.path = path
54 self.scene = scene
55 self.view_properties = view_properties
56
57 self.metrics = dict({'widthpx':1052, 'heightpx':744});
58
59 self.svg = svg(width="%dpx" % self.metrics['widthpx'], height="%dpx" % self.metrics['heightpx'])
60
61
62 self.myHeaderStyle = StyleBuilder()
63 self.mySmallTextStyle = StyleBuilder()
64 self.myHeaderStyle.setFontFamily(fontfamily="Verdana")
65 self.mySmallTextStyle.setFontFamily(fontfamily="Verdana")
66 self.mySmallTextStyle.setFontSize('3')
67 self.myHeaderStyle.setFontSize('7')
68 self.mySmallTextStyle.setFilling("black")
69 self.myHeaderStyle.setFilling("black")
70 filterShadow = filter(x="-.3",y="-.5", width=1.9, height=1.9)
71 filtBlur = feGaussianBlur(stdDeviation="4")
72 filtBlur.set_in("SourceAlpha")
73 filtBlur.set_result("out1")
74 filtOffset = feOffset()
75 filtOffset.set_in("out1")
76 filtOffset.set_dx(4)
77 filtOffset.set_dy(-4)
78 filtOffset.set_result("out2")
79 filtMergeNode1 = feMergeNode()
80 filtMergeNode1.set_in("out2")
81 filtMergeNode2 = feMergeNode()
82 filtMergeNode2.set_in("SourceGraphic")
83 filtMerge = feMerge()
84 filtMerge.addElement(filtMergeNode1)
85 filtMerge.addElement(filtMergeNode2)
86 filterShadow.addElement(filtBlur)
87 filterShadow.addElement(filtOffset)
88 filterShadow.addElement(filtMerge)
89 filterShadow.set_id("filterShadow")
90 d=defs()
91 d.addElement(filterShadow)
92 self.svg.addElement(d)
93 self.DATA_ICON_WIDTH = 5
94
95 self.shadowFlag = False
96
97 for key in kwargs:
98 if key == 'shadow':
99 self.shadowFlag = kwargs[key]
100
102 """
103 write the SVG code into the file with filename path. No
104 checking is done if file/path exists
105 """
106 self.svg.save(path)
107
109 """
110 Implement the drawing interface.
111 """
112 self._draw_period_selection()
113 self._draw_bg()
114 self._draw_events(self.view_properties)
115 self._draw_legend(self.view_properties, self._extract_categories())
116
118 if not self.view_properties.period_selection:
119 return
120 start, end = self.view_properties.period_selection
121 start_x = self.scene.x_pos_for_time(start)
122 end_x = self.scene.x_pos_for_time(end)
123 self.dc.SetBrush(self.lightgrey_solid_brush)
124 self.dc.SetPen(wx.TRANSPARENT_PEN)
125 self.dc.DrawRectangle(start_x, 0,
126 end_x - start_x + 1, self.scene.height)
127
129 """
130 Draw major and minor strips, lines to all event boxes and baseline.
131 Both major and minor strips have divider lines and labels.
132 """
133 myStyle = StyleBuilder()
134 myStyle.setStrokeDashArray((2,2))
135 myStyle.setFontFamily(fontfamily="Verdana")
136 myStyle.setFontSize("2em")
137 myStyle.setTextAnchor('left')
138 svgGroup = g()
139 self._draw_minor_strips(svgGroup, myStyle)
140 self._draw_major_strips(svgGroup, myStyle)
141 self._draw_divider_line(svgGroup)
142 self._draw_lines_to_non_period_events(svgGroup, self.view_properties)
143 self._draw_now_line(svgGroup)
144 self.svg.addElement(svgGroup)
145
147 for strip_period in self.scene.minor_strip_data:
148 self._draw_minor_strip_divider_line_at(group,strip_period.end_time)
149 self._draw_minor_strip_label(group, style, strip_period)
150
152 x = self.scene.x_pos_for_time(time)
153 oh = ShapeBuilder()
154 line = oh.createLine(x,0,x,self.scene.height, strokewidth=0.5, stroke="lightgrey")
155 group.addElement(line)
156
157
158
173
175
176
177 oh = ShapeBuilder()
178 style.setStrokeDashArray("")
179 fontSize = MAJOR_STRIP_FONT_SIZE
180 style.setFontSize("%dem" % fontSize)
181 for tp in self.scene.major_strip_data:
182
183 x = self.scene.x_pos_for_time(tp.end_time)
184 line = oh.createLine(x,0,x,self.scene.height,strokewidth=0.5, stroke="grey")
185
186
187 label = self.scene.major_strip.label(tp.start_time, True)
188
189 x = self.scene.x_pos_for_time(tp.start_time) + INNER_PADDING
190
191
192
193
194
195 extra_vertical_padding = 0
196 if x - INNER_PADDING < 0:
197 x = INNER_PADDING
198 extra_vertical_padding = fontSize * 4
199
200
201 myText = self._text(label, x, fontSize*4+INNER_PADDING+extra_vertical_padding)
202 myText.set_style(style)
203 group.addElement(myText)
204
206
207 oh = ShapeBuilder()
208 line = oh.createLine(0, self.scene.divider_y, self.scene.width,
209 self.scene.divider_y, strokewidth=0.5, stroke="grey")
210 group.addElement(line)
211
212
213
215
216 for (event, rect) in self.scene.event_data:
217 if rect.Y < self.scene.divider_y:
218 x = self.scene.x_pos_for_time(event.mean_time())
219 y = rect.Y + rect.Height / 2
220 if view_properties.is_selected(event):
221 myStroke="red"
222 else:
223 myStroke="black"
224 oh = ShapeBuilder()
225 line = oh.createLine(x, y, x, self.scene.divider_y, stroke=myStroke)
226 group.addElement(line)
227 circle = oh.createCircle(x, self.scene.divider_y, 2)
228 group.addElement(circle)
229
230
231
233 x = self.scene.x_pos_for_now()
234 if x > 0 and x < self.scene.width:
235 oh = ShapeBuilder()
236 line = oh.createLine(x, 0, x, self.scene.height, stroke="darkred")
237 group.addElement(line)
238
239
240
247
249 base_color = self._get_base_color(event)
250 border_color = darken_color(base_color)
251 return border_color
252
254 """
255 map (r,g,b) color to svg string
256 """
257 sColor = "#%02X%02X%02X" % color
258 return sColor
259
261 border_color = self._get_border_color(event)
262 sColor = self._map_svg_color(border_color)
263 return sColor
264
266 """ get the color of the event box """
267 base_color = self._get_base_color(event)
268 sColor = self._map_svg_color(base_color)
269 return sColor
270
272 base_color = self._get_base_color(event)
273 darker_color = darken_color(base_color, 0.6)
274 sColor = self._map_svg_color(darker_color)
275 return sColor
276
278 return view_properties.show_legend and len(categories) > 0
279
281 categories = []
282 for (event, rect) in self.scene.event_data:
283 cat = event.category
284 if cat and not cat in categories:
285 categories.append(cat)
286 return sort_categories(categories)
287
289 """
290 Draw legend for the given categories.
291
292 Box in lower right corner
293 Motivation for positioning in right corner:
294 SVG text cannot be centered since the text width cannot be calculated
295 and the first part of each event text is important.
296 ergo: text needs to be left aligned.
297 But then the probability is high that a lot of text is at the left
298 bottom
299 ergo: put the legend to the right.
300
301 +----------+
302 | Name O |
303 | Name O |
304 +----------+
305 """
306 if self._legend_should_be_drawn(view_properties, categories):
307 num_categories = len(categories)
308 if num_categories == 0:
309 return
310 font_size = SMALL_FONT_SIZE
311 myStyle = StyleBuilder()
312 myStyle.setFontFamily(fontfamily="Verdana")
313 myStyle.setFontSize(font_size)
314 myStyle.setTextAnchor('left')
315
316 width = int(self.metrics['widthpx'] * 0.15)
317 item_height = font_size + OUTER_PADDING
318 height = num_categories *(item_height+INNER_PADDING)+2*OUTER_PADDING
319
320 builder = ShapeBuilder()
321 x = self.metrics['widthpx'] - width - OUTER_PADDING
322 svgGroup = g()
323 box_rect = builder.createRect(x,
324 self.metrics['heightpx'] - height - OUTER_PADDING,
325 width, height,fill='white')
326 svgGroup.addElement(box_rect)
327
328 cur_y = self.metrics['heightpx'] - height - OUTER_PADDING + INNER_PADDING
329 for cat in categories:
330 base_color = self._map_svg_color(cat.color)
331 border_color = self._map_svg_color(darken_color(cat.color))
332 color_box_rect = builder.createRect(x + OUTER_PADDING,
333 cur_y, item_height, item_height, fill=base_color,
334 stroke=border_color)
335 svgGroup.addElement(color_box_rect)
336 myText = self._svg_clipped_text(cat.name,
337 (x + OUTER_PADDING + INNER_PADDING+item_height,
338 cur_y, width-OUTER_PADDING-INNER_PADDING-item_height,
339 item_height ),
340 myStyle)
341 svgGroup.addElement(myText)
342 cur_y = cur_y + item_height + INNER_PADDING
343 self.svg.addElement(svgGroup)
344
346 """Draw all event boxes and the text inside them."""
347 myStyle = StyleBuilder()
348 myStyle.setFontFamily(fontfamily="Verdana")
349 myStyle.setFontSize("%d" % SMALL_FONT_SIZE)
350 myStyle.setTextAnchor('left')
351 oh = ShapeBuilder()
352 for (event, rect) in self.scene.event_data:
353
354
355 svgGroup = g()
356
357 boxColor = self._get_box_color(event)
358 boxBorderColor = self._get_box_border_color(event)
359 svgRect = oh.createRect(rect.X, rect.Y,
360 rect.GetWidth(), rect.GetHeight(),
361 stroke=boxBorderColor,
362 fill=boxColor )
363 if self.shadowFlag:
364 svgRect.set_filter("url(#filterShadow)")
365 svgGroup.addElement(svgRect)
366 if rect.Width > 0:
367
368 svgGroup.addElement(self._svg_clipped_text(event.text, rect.Get(), myStyle))
369
370 if event.has_data():
371 svgGroup.addElement(self._draw_contents_indicator(event, rect))
372 self.svg.addElement(svgGroup)
373
374 - def _draw_contents_indicator(self, event, rect):
375 """
376 The data contents indicator is a small triangle drawn in the upper
377 right corner of the event rectangle.
378 """
379 corner_x = rect.X + rect.Width
380 polyPoints = "%d,%d %d,%d %d,%d" % \
381 (corner_x - DATA_INDICATOR_SIZE, rect.Y,
382 corner_x, rect.Y,
383 corner_x, rect.Y + DATA_INDICATOR_SIZE)
384 polyColor = self._get_box_indicator_color(event)
385 oh = ShapeBuilder()
386 indicator = oh.createPolygon(polyPoints,fill=polyColor,stroke=polyColor)
387
388 return indicator
389
390 - def _svg_clipped_text(self, myString, rectTuple, myStyle):
391 myString = self._encode_unicode_text(myString)
392
393 group=g()
394 rx, ry, width, height = rectTuple
395 text_x = rx + INNER_PADDING
396 text_y = ry + height - INNER_PADDING
397
398
399
400
401 if text_x < INNER_PADDING:
402 width = width - (INNER_PADDING-text_x)
403 text_x = INNER_PADDING
404 pathId = "path%d_%d" % (text_x, text_y)
405 p = path(pathData= "M %d %d H %d V %d H %d" % \
406 (rx, ry + height,
407 text_x+width-INNER_PADDING,
408 ry, rx))
409 clip = clipPath()
410 clip.addElement(p)
411 clip.set_id(pathId)
412 d=defs()
413 d.addElement(clip)
414 self.svg.addElement(d)
415 myText = text( myString, text_x, text_y )
416 myText.set_style(myStyle.getStyle())
417 myText.set_textLength(width-2*INNER_PADDING)
418 myText.set_lengthAdjust("spacingAndGlyphs")
419 group.set_clip_path("url(#%s)" % pathId)
420
421 group.addElement(myText)
422 return group
423
424 - def _text(self, the_text, x, y):
425 encoded_text = self._encode_unicode_text(the_text)
426 return text(encoded_text, x, y)
427
428 - def _encode_unicode_text(self, text):
429 if type(text) is UnicodeType:
430 return text.encode('ISO-8859-1')
431 else:
432 return text
433