/[thuban]/trunk/thuban/Extensions/svgexport/svgmapwriter.py
ViewVC logotype

Annotation of /trunk/thuban/Extensions/svgexport/svgmapwriter.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2344 - (hide annotations)
Tue Sep 21 21:30:36 2004 UTC (20 years, 5 months ago) by bernhard
File MIME type: text/x-python
File size: 21546 byte(s)
Improved the svgexport to only use unique ids. Will issues
an error message dialoge when two layer names are the same.
ShapeIDs are now added with a dash within the svg ids.

* Extensions/svgexport/svgmapwriter.py (SVGMapWriterError): New.
* Extensions/svgexport/test/test_svgmapwriter.py: Added imports
(TestSVGRenderer): New test class with test_make_in() and
test_check_for_layer_name_clash()
* Extensions/svgexport/svgmapwriter.py (SVGRenderer): Fixed __init__()
and draw_shape_layer_incrementally() to not use a baseid twice,
satisfying test_check_for_layer_name_clash()
(VirtualDC.make_id): Use a dash between baseit and id, satisfies
test_make_in().
* Extensions/svexport/svgsaver.py: Import SVGMapWriterError, wxOK
and wxICON_HAND.
(write_to_svg): Put dc and rendering in a try statement and on
catching SVGmapWriterError notify the user and delete the target file.
-------------------------------------------------------------------

1 bh 2082 # Copyright (c) 2001, 2002, 2003, 2004 by Intevation GmbH
2 bh 2074 # Authors:
3     # Markus Rechtien <[email protected]>
4     #
5     # This program is free software under the GPL (>=v2)
6     # Read the file COPYING coming with Thuban for details.
7    
8    
9     """
10     Classes needed to write a session in SVG format
11     """
12    
13 bh 2082 # For compatibility with python 2.2
14     from __future__ import generators
15 bh 2074
16 bh 2082
17     __version__ = "$Revision$"
18     # $Source$
19     # $Id$
20    
21    
22 bh 2074 # Regular expressions used with Fontnames
23     import re
24     # Combining strings
25     from string import join
26     # We need to determine some object types
27 bh 2082 from types import ListType
28 bernhard 2344
29     from Thuban import _
30 bh 2074 # VirtualDC extends XMLWriter
31     from Thuban.Model.xmlwriter import XMLWriter, escape
32     # Color related classes from the model of thuban
33 bh 2082 from Thuban.Model.color import Transparent, Black
34 bh 2074 # The SVGRenderer is subclass of BaseRenderer
35     from Thuban.UI.baserenderer import BaseRenderer
36    
37     # Basic font map.
38     fontMap = { "Times" : re.compile("Times-Roman.*"),
39     "Helvetica" : re.compile("Helvetica.*"),
40     "Courier" : re.compile("Courier.*"),
41     }
42    
43     # Possible values for svg line joins.
44     svg_joins = {'miter':'miter', 'round':'round', 'bevel':'bevel'}
45     # Possible values for svg line caps.
46     svg_caps = {'':'', 'butt':'butt', 'round':'round', 'square':'square'}
47    
48     #
49     # Some pseudo classes to be compatible with the Baserenderer-class.
50     #
51     class Point:
52 bh 2082 """A simple Point class."""
53 bh 2074 def __init__(self, xp=0, yp=0):
54 bh 2082 """Init the point object."""
55 bh 2074 self.x = xp
56     self.y = yp
57    
58     class Trafo:
59 bh 2082 """Class for tranformation properties transfer."""
60 bh 2074 def __init__(self):
61 bh 2082 """Initialize the class."""
62 bh 2074 self.trafos = []
63    
64     def Append(self, type, coeffs):
65 bh 2082 """Append a transformation to the list."""
66 bh 2074 self.trafos.append((type, coeffs))
67    
68     def Count(self):
69 bh 2082 """Get the number of transformations in list."""
70 bh 2074 return len(self.trafos)
71    
72     def Pop(self):
73 bh 2082 """Pop and return a transformation from the end of the list."""
74 bh 2074 if len(self.trafos) > 0:
75     return self.trafos.pop()
76     else: return None
77    
78     class Pattern:
79 bh 2082 """Pattern object """
80 bh 2074 def __init__(self, solid=1):
81 bh 2082 """Init the Pattern object."""
82 bh 2074 self.solid = solid
83    
84     class Pen:
85 bh 2082 """Pen object for property transfer."""
86 bh 2074 def __init__(self, pcolor = Black, pwidth = 1, pdashes = None):
87     self.color = pcolor
88     self.width = pwidth
89     self.dashes = pdashes
90     self.join = 'round'
91     self.cap = 'round'
92    
93     def GetColor(self):
94 bh 2082 """Return the pen's color."""
95 bh 2074 return self.color
96    
97     def GetWidth(self):
98 bh 2082 """Return the pen's width."""
99 bh 2074 return self.width
100    
101     def GetJoin(self):
102 bh 2082 """Return the pen's join type."""
103 bh 2074 return self.join
104    
105     def GetCap(self):
106 bh 2082 """Return the pen's cap type."""
107 bh 2074 return self.cap
108    
109     def GetDashes(self):
110 bh 2082 """Return the pen's dashes."""
111 bh 2074 if self.dashes is None or self.dashes is SOLID:
112     return []
113     else: return self.dashes
114    
115     class Brush:
116 bh 2082 """Brush property class."""
117 bh 2074 def __init__(self, bfill=Black, bpattern=None):
118 bh 2082 """Init the brush with the given values."""
119 bh 2074 self.fill = bfill
120     self.pattern = bpattern
121    
122     def GetColor(self):
123 bh 2082 """Return the brush color."""
124 bh 2074 return self.fill
125    
126     def GetPattern(self):
127 bh 2082 """Return the Brush pattern object."""
128 bh 2074 return self.pattern
129    
130     class Font:
131 bh 2082 """Font class that accts as property object."""
132 bh 2074 def __init__(self, ffamily='Helvetica', fsize=12):
133 bh 2082 """Init the font with the given values."""
134 bh 2074 self.family = ffamily
135     self.size = fsize
136    
137     def GetFaceName(self):
138 bh 2082 """Return the fontfamily the font belongs to."""
139 bh 2074 return self.family
140    
141     def GetPointSize(self):
142 bh 2082 """Return the size of the font in points."""
143 bh 2074 return self.size
144    
145     # Instantiate an empty pen.
146     TRANSPARENT_PEN = Pen(None, 0, None)
147     # Instantiate an empty brush.
148     TRANSPARENT_BRUSH = Brush(None, None)
149     # Instantiate a solid pattern.
150     SOLID = Pattern()
151    
152 bernhard 2344 class SVGMapWriterError(Exception):
153     """Get raised for problems when writing map-svg files.
154    
155     Occasion when this exception is raised:
156     Two layers have the same name to be used as BaseId: Name Clash
157     """
158    
159    
160 bh 2074 class SVGRenderer(BaseRenderer):
161     """Class to render a map onto a VirtualDC.
162    
163     This class, derived from BaseRenderer, will render a hole
164     session onto the VirtualDC to write all shapes as SVG code
165     to a file.
166     In opposite to other renderers it includes metadata, such as
167     shape ids and classification, when rendering the shapes.
168     """
169     def __init__(self, dc, map, scale, offset, region,
170     resolution = 1.0, honor_visibility = 1):
171 bh 2082 """Init SVGRenderer and call superclass init."""
172 bh 2074 BaseRenderer.__init__(self, dc, map, scale, offset, region,
173     resolution, honor_visibility)
174     #
175     self.factor = (abs(region[2]) + abs(region[3])) / (2.0 * 1000.0)
176 bernhard 2344 self.used_baseids=[] # needed for name clash check
177    
178 bh 2074 def make_point(self, x, y):
179 bh 2082 """Return a Point object from two values."""
180 bh 2074 return Point(x, y)
181    
182     def label_font(self):
183     """Return the font object for the label layer"""
184     return Font()
185    
186     def tools_for_property(self, prop):
187 bh 2082 """Return a pen/brush tuple build from a property object."""
188 bh 2074 fill = prop.GetFill()
189     if fill is Transparent:
190     brush = TRANSPARENT_BRUSH
191     else:
192     brush = Brush(fill, SOLID)
193    
194     stroke = prop.GetLineColor()
195     if stroke is Transparent:
196     pen = TRANSPARENT_PEN
197     else:
198     pen = Pen(stroke, prop.GetLineWidth() * self.factor, SOLID)
199     return pen, brush
200    
201     def draw_polygon_shape(self, layer, points, pen, brush):
202     """Draw a polygon shape from layer with the given brush and pen
203    
204     The shape is given by points argument which is a the return
205     value of the shape's Points() method. The coordinates in the
206     DC's coordinate system are determined with
207     self.projected_points.
208     """
209     points = self.projected_points(layer, points)
210    
211     if brush is not TRANSPARENT_BRUSH:
212     self.dc.SetBrush(brush)
213     self.dc.SetPen(pen)
214     for part in points:
215     self.dc.DrawLines(part)
216    
217     def draw_point_shape(self, layer, points, pen, brush):
218     """Draw a point shape from layer with the given brush and pen
219    
220     The shape is given by points argument which is a the return
221     value of the shape's Points() method. The coordinates in the
222     DC's coordinate system are determined with
223     self.projected_points.
224    
225     The point is drawn as a circle centered on the point.
226     """
227     points = self.projected_points(layer, points)
228     if not points:
229     return
230    
231     radius = self.factor * 2.0
232     self.dc.SetBrush(brush)
233     self.dc.SetPen(pen)
234     for part in points:
235     for p in part:
236     self.dc.DrawCircle(p.x - radius, p.y - radius,
237     2.0 * radius)
238    
239     def draw_shape_layer_incrementally(self, layer):
240     """Draw a shapelayer incrementally.
241     """
242     dc = self.dc
243     brush = TRANSPARENT_BRUSH
244     pen = TRANSPARENT_PEN
245 bernhard 2344
246 bh 2074 value = None
247     field = None
248     lc = layer.GetClassification()
249     field = layer.GetClassificationColumn()
250     defaultGroup = lc.GetDefaultGroup()
251     table = layer.ShapeStore().Table()
252    
253     if lc.GetNumGroups() == 0:
254     # There's only the default group, so we can pretend that
255     # there is no field to classifiy on which makes things
256     # faster since we don't need the attribute information at
257     # all.
258     field = None
259    
260     # Determine which render function to use.
261     useraw, draw_func, draw_func_param = \
262     self.low_level_renderer(layer)
263     tool_cache = {}
264    
265 bernhard 2344 if layer.title in self.used_baseids:
266     raise SVGMapWriterError(_("Clash of layer names!\n")+ \
267     _("Two layers probably have the same name, try renaming one."))
268     # prefix of a shape id to be unique
269 bh 2074 dc.SetBaseID(layer.title)
270 bernhard 2344 self.used_baseids.append(layer.title)
271 bh 2074 # Titel of current layer to the groups meta informations
272     dc.BeginGroup(meta={'Layer':layer.Title(), })
273     # Delete all MetaData
274     dc.FlushMeta()
275     for shape in self.layer_shapes(layer):
276     if field is None:
277     group = defaultGroup
278     value = group.GetDisplayText()
279     else:
280     value = table.ReadValue(shape.ShapeID(), field)
281     group = lc.FindGroup(value)
282    
283     if not group.IsVisible():
284     continue
285    
286     # Render classification
287     shapeType = layer.ShapeType()
288     props = group.GetProperties()
289    
290     # put meta infos into DC
291     if field and value:
292     dc.SetMeta({field:value, })
293     # set current shape id
294     dc.SetID(shape.ShapeID())
295    
296     try:
297     pen, brush = tool_cache[id(group)]
298     except KeyError:
299     pen, brush = tool_cache[id(group)] \
300     = self.tools_for_property(group.GetProperties())
301    
302     if useraw:
303     data = shape.RawData()
304     else:
305     data = shape.Points()
306     draw_func(draw_func_param, data, pen, brush)
307     # compatibility
308     if 0:
309     yield True
310     # reset shape id
311     dc.SetID(-1)
312     dc.SetBaseID("")
313     dc.EndGroup()
314    
315     def draw_raster_layer(self, layer):
316 bh 2082 """Draw the raster layer"""
317 bh 2074 # For now we cannot draw raster layers onto our VirtualDC
318     pass
319    
320     def draw_raster_data(self, data, format="BMP"):
321     """Draw the raster image in data onto the DC"""
322     # For now we cannot draw raster data onto our VirtualDC
323     pass
324    
325     def RenderMap(self, selected_layer, selected_shapes):
326 bh 2082 """Overriden to avoid automatic rendering of legend,
327 bh 2074 scalbar and frame.
328 bh 2082 """
329 bh 2074 dc = self.dc
330     self.selected_layer = selected_layer
331     self.selected_shapes = selected_shapes
332     minx, miny, width, height = self.region
333     # scale down to a size of 1000
334     trans = Trafo()
335     trans.Append('scale', (1000.0 / ((width + height) / 2.0)))
336     #
337     dc.BeginClipPath('mapclip')
338     dc.DrawRectangle(0, 0, width, height)
339     dc.EndClipPath()
340     #
341     dc.BeginGroup(meta={'Object':'map', }, clipid='mapclip', \
342     transform=trans)
343     self.render_map()
344     dc.EndGroup()
345    
346    
347     class VirtualDC(XMLWriter):
348 bh 2082 """This class imitates a DC and writes SVG instead.
349 bh 2074
350     All shapes and graphic objects will be turned into
351     SVG elements and will be written into a file.
352     Any properties, such as stroke width or stroke color,
353     will be written together with the SVG elementents.
354 bh 2082 """
355 bh 2074 def __init__(self, file, dim=(0,0), units=''):
356 bh 2082 """Setup some variables and objects for property collection."""
357 bh 2074 XMLWriter.__init__(self)
358     self.dim = dim
359     self.units = units
360     self.pen = {}
361     self.brush = {}
362     self.font = {}
363     self.meta = {}
364     self.style = {}
365     # Some buffers
366     self.points = []
367     self.id = -1
368     self.flush_meta = 1
369     self.write(file)
370    
371     def write_indent(self, str):
372 bh 2082 """Write a string to the file with the current indention level.
373     """
374 bh 2074 from Thuban.Model.xmlwriter import TAB
375     self.file.write("%s%s" % (TAB*self.indent_level, str))
376    
377     def AddMeta(self, key, val):
378 bh 2082 """Append some metadata to the array that will be
379 bh 2074 written with the next svg-element
380 bh 2082 """
381 bh 2074 if key is '' or val is '':
382     return
383     self.meta[key] = val
384    
385     def SetMeta(self, pairs, flush_after=1):
386 bh 2082 """Delete old meta informations and set the new ones."""
387 bh 2074 self.meta = {}
388     self.flush_meta = flush_after
389     for key, val in pairs.items():
390     self.AddMeta(key, val)
391    
392     def FlushMeta(self):
393 bh 2082 """Drop collected metadata."""
394 bh 2074 self.meta = {}
395    
396     def BeginGroup(self, **args):
397 bh 2082 """Begin a group of elements.
398 bh 2074
399     Possible arguments:
400     meta A list of key, value metadata pairs
401     style A list of key, value style attributes
402     clipid The ID of a clipPath definition to be
403     applied to this group
404 bh 2082 """
405 bh 2074 self.FlushMeta()
406     # adding meta data
407     if args.has_key('meta'):
408     for key, val in args['meta'].items():
409     self.AddMeta(key, val)
410     attribs = " "
411     # adding style attributes
412     if args.has_key('style'):
413     for key, val in args['style'].items():
414     attribs += '%s="%s" ' % (key, val)
415     # adding clip informations
416     if args.has_key("clipid"):
417     attribs += ' clip-path="url(#%s)"' % (args['clipid'],)
418     # FIXME: this shouldn't be static
419     attribs += ' clip-rule="evenodd"'
420     if args.has_key('transform'):
421     trafostr = self.parse_trafo(args['transform'])
422     if trafostr:
423     attribs += ' transform="%s"' % (trafostr)
424     # put everything together
425     self.write_indent('<g %s%s>\n' % (self.make_meta(), attribs))
426     self.indent_level += 1
427    
428     def parse_trafo(self, trafo):
429 bh 2082 """Examine a trafo object for asigned transformations details."""
430 bh 2074 if not trafo:
431     return ''
432     retval = ''
433     while trafo.Count() > 0:
434     trans, coeffs = tuple(trafo.Pop())
435     if isinstance(coeffs, ListType):
436     retval += " %s%s" % (trans, join(coeffs, ', '))
437     else: retval += " %s(%s)" % (trans, coeffs)
438     # return the string
439     return retval
440    
441     def EndGroup(self):
442 bh 2082 """End a group of elements"""
443 bh 2074 self.indent_level -= 1
444     self.write_indent('</g>\n')
445     self.FlushMeta()
446    
447     def BeginExport(self):
448 bh 2082 """Start the export process and write basic document
449 bh 2074 informations to the file.
450 bh 2082 """
451 bh 2074 self.write_indent('<?xml version="1.0" encoding="ISO-8859-1" '
452     'standalone="yes"?>\n')
453     width, height = self.dim
454     self.write_indent('<svg>\n')
455     self.indent_level += 1
456    
457     def EndExport(self):
458 bh 2082 """End the export process with closing the SVG tag and close
459     the file accessor"""
460 bh 2074 self.indent_level -= 1
461     self.write_indent('</svg>\n')
462     self.close()
463    
464     def Close(self):
465 bh 2082 """Close the file."""
466 bh 2074 self.close()
467    
468     def BeginDrawing(self):
469 bh 2082 """Dummy function to work with the Thuban renderers."""
470 bh 2074 pass
471    
472     def EndDrawing(self):
473 bh 2082 """Dummy function to work with the Thuban renderers."""
474 bh 2074 pass
475    
476     def GetSizeTuple(self):
477 bh 2082 """Return the dimension of this virtual canvas."""
478 bh 2074 return self.dim
479    
480     def GetTextExtent(self, text):
481 bh 2082 """Return the dimension of the given text."""
482 bh 2074 # FIXME: find something more appropriate
483     try:
484     if self.font:
485     return (int(self.font["font-size"]),
486     len(text) * int(self.font["font-size"]))
487     else: return (12,len(text) * 10)
488     except ValueError:
489     return (12,len(text) * 10)
490    
491     def SetID(self, id):
492 bh 2082 """Set the ID stored by the svg elements."""
493 bh 2074 self.id = id
494    
495     def SetBaseID(self, id):
496 bh 2082 """Set the ID stored by the svg elements."""
497 bh 2074 self.baseid = id
498    
499     def SetFont(self, font):
500 bh 2082 """Set the fontproperties to use with text elements."""
501 bh 2074 if font is not None:
502     fontname = font.GetFaceName()
503     size = font.GetPointSize()
504     for svgfont, pattern in fontMap.items():
505     if pattern.match(fontname):
506     fontname = svgfont
507     break
508     if fontname:
509     self.font["font-family"] = fontname
510     else: self.font["font-family"] = None
511     if size:
512     self.font["font-size"] = str(size)
513     else: self.font["font-size"] = None
514    
515     def SetPen(self, pen):
516 bh 2082 """Set the style of the pen used to draw graphics."""
517 bh 2074 if pen is TRANSPARENT_PEN:
518     self.pen = {}
519     else:
520     self.pen["stroke"] = pen.GetColor().hex()
521     self.pen["stroke-dasharray"] = join(pen.GetDashes(), ',')
522     self.pen["stroke-width"] = pen.GetWidth()
523     self.pen["stroke-linejoin"] = svg_joins[pen.GetJoin()]
524     self.pen["stroke-linecap"] = svg_caps[pen.GetCap()]
525    
526     def SetBrush(self, brush):
527 bh 2082 """Set the fill properties."""
528 bh 2074 if brush is TRANSPARENT_BRUSH:
529     self.brush['fill'] = 'none'
530     elif brush.GetPattern() is SOLID:
531     self.brush['fill'] = brush.GetColor().hex()
532     else: # TODO Handle Patterns
533     pass
534    
535     def SetTextForeground(self, color):
536 bh 2082 """Set the color of the text foreground."""
537 bh 2074 self.font['fill'] = color.hex()
538    
539     def make_style(self, line=0, fill=0, font=0):
540 bh 2082 """Build the style attribute including desired properties
541     such as fill, forground, stroke, etc."""
542 bh 2074 result = []
543     # little helper function
544     def append(pairs):
545     for key, val in pairs.items():
546     if not val in [None, '']:
547     result.append('%s:%s' % (key, val))
548     #
549     if line and len(self.pen) > 0:
550     append(self.pen)
551     if fill and len(self.brush) > 0:
552     append(self.brush)
553     if font and len(self.font) > 0:
554     append(self.font)
555     style = join(result, '; ')
556     if style:
557     return 'style="%s"' % (style, )
558     else:
559     return ''
560    
561     def make_meta(self, meta=None):
562 bh 2082 """Build the meta attribute."""
563 bh 2074 result = []
564     if not meta:
565     meta = self.meta
566     if len(meta) is 0:
567     return ''
568     for key, val in meta.items():
569     if not val in [None, '', 'none']:
570     result.append('%s:%s' % (key, val))
571     if self.flush_meta:
572     self.meta = {}
573     return 'meta="%s"' % (join(result, '; '))
574    
575     def make_id(self):
576 bernhard 2344 """Return id= string for object out of currently set baseid and id.
577    
578     Return the empty string if no id was set.
579     """
580 bh 2074 if self.id < 0:
581     return ''
582 bernhard 2344 return 'id="%s-%s"' % (self.baseid, self.id)
583 bh 2074
584     def DrawEllipse(self, x, y, dx, dy):
585 bh 2082 """Draw an ellipse."""
586 bh 2074 elips = '<ellipse cx="%s" cy="%s" rx="%s" ry="%s" %s %s %s/>\n'
587     self.write_indent(elips % (x, y, dx, dy, self.make_id(),
588     self.make_style(1,1,0), self.make_meta()) )
589    
590     def DrawCircle(self, x, y, radius):
591 bh 2082 """Draw a circle onto the virtual dc."""
592 bh 2074 self.write_indent('<circle cx="%s" cy="%s" r="%s" %s %s %s/>\n' %
593     (x, y, radius, self.make_id(), self.make_style(1,1,0),
594     self.make_meta()) )
595    
596     def DrawRectangle(self, x, y, width, height):
597 bh 2082 """Draw a rectangle with the given parameters."""
598 bh 2074 rect = '<rect x="%s" y="%s" width="%s" height="%s" %s %s %s/>\n'
599     self.write_indent(rect % ( x, y, width, height, self.make_id(),
600     self.make_style(1,1,0), self.make_meta()) )
601    
602     def DrawText(self, text, x, y):
603 bh 2082 """Draw Text at the given position."""
604 bh 2074 beginText = '<text x="%s" y="%s" %s %s %s>'
605     self.write_indent(beginText % ( x, y, self.make_id(),
606     self.make_style(0,0,1), self.make_meta()) )
607     self.file.write(escape(text))
608     self.file.write('</text>\n')
609    
610     def DrawLines(self, points):
611 bh 2082 """Draw some points into a Buffer that will be
612 bh 2074 written before the next object.
613 bh 2082 """
614 bh 2074 self.DrawPolygon(points,0)
615    
616     def DrawPolygon(self, polygon, closed=1):
617 bh 2082 """Draw a polygon onto the virtual dc."""
618 bh 2074 self.write_indent('<path %s ' % (self.make_style(1,1,0)))
619     data = []
620     i = 0
621     for point in polygon:
622     if i is 0:
623     data.append('M %s %s ' % (point.x, point.y))
624     else:
625     data.append('L %s %s ' % (point.x, point.y))
626     i+=1
627     # FIXME: Determine if path is closed
628     if closed:
629     data.append('Z')
630     # Put everything together and write it to the file
631     self.file.write('%s %s d="%s"/>\n' % (self.make_id(),
632     self.make_meta(), join(data, '') ) )
633    
634     def DrawSpline(self, points, closed=0):
635 bh 2082 """Draw a spline object.
636     """
637 bh 2074 self.DrawPolygon(points, 0)
638     print "TODO: DrawSpline(..)"
639     return # TODO: Implement
640    
641     def BeginClipPath(self, id):
642 bh 2082 """Build a clipping region to draw in."""
643 bh 2074 self.write_indent('<clipPath id="%s">\n' % id)
644     self.indent_level += 1
645    
646     def EndClipPath(self):
647 bh 2082 """End a clip path."""
648 bh 2074 self.indent_level -= 1
649     self.write_indent("</clipPath>\n")

Properties

Name Value
svn:eol-style native
svn:keywords Author Date Id Revision

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26