Gnumed.timelinelib.export.svg

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