Package Gnumed :: Package timelinelib :: Package export :: Module svg
[frames] | no frames]

Source Code for Module Gnumed.timelinelib.export.svg

  1  # Copyright (C) 2009, 2010, 2011  Rickard Lindberg, Roger Lindberg 
  2  # 
  3  # This file is part of Timeline. 
  4  # 
  5  # Timeline is free software: you can redistribute it and/or modify 
  6  # it under the terms of the GNU General Public License as published by 
  7  # the Free Software Foundation, either version 3 of the License, or 
  8  # (at your option) any later version. 
  9  # 
 10  # Timeline is distributed in the hope that it will be useful, 
 11  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 12  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 13  # GNU General Public License for more details. 
 14  # 
 15  # You should have received a copy of the GNU General Public License 
 16  # along with Timeline.  If not, see <http://www.gnu.org/licenses/>. 
 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      # Space between event boxes (pixels) 
 34  INNER_PADDING = 3      # Space inside event box to text (pixels) 
 35  BASELINE_PADDING = 15  # Extra space to move events away from baseline (pixels) 
 36  PERIOD_THRESHOLD = 20  # Periods smaller than this are drawn as events (pixels) 
 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):
44 svgDrawer = SVGDrawingAlgorithm(path, scene, view_properties, shadow=True) 45 svgDrawer.draw() 46 svgDrawer.write(path)
47 48
49 -class SVGDrawingAlgorithm(object):
50 # options: shadow=True|False
51 - def __init__(self, path, scene, view_properties, **kwargs):
52 # store important data references 53 self.path = path 54 self.scene = scene 55 self.view_properties = view_properties 56 # SVG document size, maybe TODO 57 self.metrics = dict({'widthpx':1052, 'heightpx':744}); 58 # SVG document handle 59 self.svg = svg(width="%dpx" % self.metrics['widthpx'], height="%dpx" % self.metrics['heightpx']) 60 # Fonts and pens we use when drawing 61 # SVG Text style 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) # here i get an error from python. It is not allowed to add a primitive filter 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 # local flags 95 self.shadowFlag = False 96 # flag handling 97 for key in kwargs: 98 if key == 'shadow': 99 self.shadowFlag = kwargs[key]
100
101 - def write(self, path):
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
108 - def draw(self):
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
117 - def _draw_period_selection(self):
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
128 - def _draw_bg(self):
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
146 - def _draw_minor_strips(self, group, style):
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
151 - def _draw_minor_strip_divider_line_at(self, group, time):
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 # self.dc.SetPen(self.black_dashed_pen) 157 # self.dc.DrawLine(x, 0, x, self.scene.height) 158
159 - def _draw_minor_strip_label(self, group, style, strip_period):
160 label = self.scene.minor_strip.label(strip_period.start_time) 161 # self.dc.SetFont(self.scene.minor_strip.get_font(strip_period)) 162 # (tw, th) = self.dc.GetTextExtent(label) 163 middle = self.scene.x_pos_for_time(strip_period.start_time) + INNER_PADDING 164 # check for negative values 165 if middle < INNER_PADDING: 166 return 167 middley = self.scene.divider_y 168 # self.dc.DrawText(label, middle - tw / 2, middley - th) 169 # Label 170 myText = self._text(label, middle, middley) 171 myText.set_style(style) 172 group.addElement(myText)
173
174 - def _draw_major_strips(self, group, style):
175 # self.dc.SetFont(self.header_font) 176 # self.dc.SetPen(self.grey_solid_pen) 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 # Divider line 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 # self.dc.DrawLine(x, 0, x, self.scene.height) 186 # Label 187 label = self.scene.major_strip.label(tp.start_time, True) 188 # (tw, th) = self.dc.GetTextExtent(label) 189 x = self.scene.x_pos_for_time(tp.start_time) + INNER_PADDING 190 # x = self.scene.x_pos_for_time(tp.mean_time()) - tw / 2 191 # If the label is not visible when it is positioned in the middle 192 # of the period, we move it so that as much of it as possible is 193 # visible without crossing strip borders. 194 # self.dc.DrawText(label, x, INNER_PADDING) 195 extra_vertical_padding = 0 196 if x - INNER_PADDING < 0: 197 x = INNER_PADDING 198 extra_vertical_padding = fontSize * 4 199 # since there is no function like textwidth() for SVG, just take into account that text can be overwritten 200 # do not perform a special handling for right border, SVG is unlimited 201 myText = self._text(label, x, fontSize*4+INNER_PADDING+extra_vertical_padding) 202 myText.set_style(style) 203 group.addElement(myText)
204
205 - def _draw_divider_line(self, group):
206 # self.dc.SetPen(self.black_solid_pen) 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 # self.dc.DrawLine(0, self.scene.divider_y, self.scene.width, 212 # self.scene.divider_y) 213
214 - def _draw_lines_to_non_period_events(self, group, view_properties):
215 # self.dc.SetBrush(self.black_solid_brush) 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 # self.dc.DrawLine(x, y, x, self.scene.divider_y) 230 # self.dc.DrawCircle(x, self.scene.divider_y, 2) 231
232 - def _draw_now_line(self, group):
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 # self.dc.SetPen(self.darkred_solid_pen) 239 # self.dc.DrawLine(x, 0, x, self.scene.height) 240
241 - def _get_base_color(self, event):
242 if event.category: 243 base_color = event.category.color 244 else: 245 base_color = (200, 200, 200) 246 return base_color
247
248 - def _get_border_color(self, event):
249 base_color = self._get_base_color(event) 250 border_color = darken_color(base_color) 251 return border_color
252
253 - def _map_svg_color(self, color):
254 """ 255 map (r,g,b) color to svg string 256 """ 257 sColor = "#%02X%02X%02X" % color 258 return sColor
259
260 - def _get_box_border_color(self, event):
261 border_color = self._get_border_color(event) 262 sColor = self._map_svg_color(border_color) 263 return sColor
264
265 - def _get_box_color(self, event):
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
271 - def _get_box_indicator_color(self, event):
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
277 - def _legend_should_be_drawn(self, view_properties, categories):
278 return view_properties.show_legend and len(categories) > 0
279
280 - def _extract_categories(self):
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
288 - def _draw_legend(self, view_properties, categories):
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 # reserve 15% for the legend 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 # Draw big box 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 # Draw text and color boxes 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
345 - def _draw_events(self, view_properties):
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 # Ensure that we can't draw outside rectangle 354 # have one group per event 355 svgGroup = g() 356 # Ensure that we can't draw content outside inner rectangle 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 # Draw the text (if there is room for it) 368 svgGroup.addElement(self._svg_clipped_text(event.text, rect.Get(), myStyle)) 369 # Draw data contents indicator 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 # TODO (low): Transparency ? 388 return indicator
389
390 - def _svg_clipped_text(self, myString, rectTuple, myStyle):
391 myString = self._encode_unicode_text(myString) 392 # Put text,clipping into a SVG group 393 group=g() 394 rx, ry, width, height = rectTuple 395 text_x = rx + INNER_PADDING 396 text_y = ry + height - INNER_PADDING 397 # TODO: in SVG, negative value should be OK, but they 398 # are not drawn in Firefox. So add a special handling here 399 # however the root cause is the layout function for the recs 400 # which should be fixed not to have values < 0 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