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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2344 - (show 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 # Copyright (c) 2001, 2002, 2003, 2004 by Intevation GmbH
2 # 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 # For compatibility with python 2.2
14 from __future__ import generators
15
16
17 __version__ = "$Revision$"
18 # $Source$
19 # $Id$
20
21
22 # 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 from types import ListType
28
29 from Thuban import _
30 # VirtualDC extends XMLWriter
31 from Thuban.Model.xmlwriter import XMLWriter, escape
32 # Color related classes from the model of thuban
33 from Thuban.Model.color import Transparent, Black
34 # 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 """A simple Point class."""
53 def __init__(self, xp=0, yp=0):
54 """Init the point object."""
55 self.x = xp
56 self.y = yp
57
58 class Trafo:
59 """Class for tranformation properties transfer."""
60 def __init__(self):
61 """Initialize the class."""
62 self.trafos = []
63
64 def Append(self, type, coeffs):
65 """Append a transformation to the list."""
66 self.trafos.append((type, coeffs))
67
68 def Count(self):
69 """Get the number of transformations in list."""
70 return len(self.trafos)
71
72 def Pop(self):
73 """Pop and return a transformation from the end of the list."""
74 if len(self.trafos) > 0:
75 return self.trafos.pop()
76 else: return None
77
78 class Pattern:
79 """Pattern object """
80 def __init__(self, solid=1):
81 """Init the Pattern object."""
82 self.solid = solid
83
84 class Pen:
85 """Pen object for property transfer."""
86 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 """Return the pen's color."""
95 return self.color
96
97 def GetWidth(self):
98 """Return the pen's width."""
99 return self.width
100
101 def GetJoin(self):
102 """Return the pen's join type."""
103 return self.join
104
105 def GetCap(self):
106 """Return the pen's cap type."""
107 return self.cap
108
109 def GetDashes(self):
110 """Return the pen's dashes."""
111 if self.dashes is None or self.dashes is SOLID:
112 return []
113 else: return self.dashes
114
115 class Brush:
116 """Brush property class."""
117 def __init__(self, bfill=Black, bpattern=None):
118 """Init the brush with the given values."""
119 self.fill = bfill
120 self.pattern = bpattern
121
122 def GetColor(self):
123 """Return the brush color."""
124 return self.fill
125
126 def GetPattern(self):
127 """Return the Brush pattern object."""
128 return self.pattern
129
130 class Font:
131 """Font class that accts as property object."""
132 def __init__(self, ffamily='Helvetica', fsize=12):
133 """Init the font with the given values."""
134 self.family = ffamily
135 self.size = fsize
136
137 def GetFaceName(self):
138 """Return the fontfamily the font belongs to."""
139 return self.family
140
141 def GetPointSize(self):
142 """Return the size of the font in points."""
143 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 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 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 """Init SVGRenderer and call superclass init."""
172 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 self.used_baseids=[] # needed for name clash check
177
178 def make_point(self, x, y):
179 """Return a Point object from two values."""
180 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 """Return a pen/brush tuple build from a property object."""
188 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
246 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 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 dc.SetBaseID(layer.title)
270 self.used_baseids.append(layer.title)
271 # 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 """Draw the raster layer"""
317 # 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 """Overriden to avoid automatic rendering of legend,
327 scalbar and frame.
328 """
329 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 """This class imitates a DC and writes SVG instead.
349
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 """
355 def __init__(self, file, dim=(0,0), units=''):
356 """Setup some variables and objects for property collection."""
357 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 """Write a string to the file with the current indention level.
373 """
374 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 """Append some metadata to the array that will be
379 written with the next svg-element
380 """
381 if key is '' or val is '':
382 return
383 self.meta[key] = val
384
385 def SetMeta(self, pairs, flush_after=1):
386 """Delete old meta informations and set the new ones."""
387 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 """Drop collected metadata."""
394 self.meta = {}
395
396 def BeginGroup(self, **args):
397 """Begin a group of elements.
398
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 """
405 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 """Examine a trafo object for asigned transformations details."""
430 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 """End a group of elements"""
443 self.indent_level -= 1
444 self.write_indent('</g>\n')
445 self.FlushMeta()
446
447 def BeginExport(self):
448 """Start the export process and write basic document
449 informations to the file.
450 """
451 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 """End the export process with closing the SVG tag and close
459 the file accessor"""
460 self.indent_level -= 1
461 self.write_indent('</svg>\n')
462 self.close()
463
464 def Close(self):
465 """Close the file."""
466 self.close()
467
468 def BeginDrawing(self):
469 """Dummy function to work with the Thuban renderers."""
470 pass
471
472 def EndDrawing(self):
473 """Dummy function to work with the Thuban renderers."""
474 pass
475
476 def GetSizeTuple(self):
477 """Return the dimension of this virtual canvas."""
478 return self.dim
479
480 def GetTextExtent(self, text):
481 """Return the dimension of the given text."""
482 # 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 """Set the ID stored by the svg elements."""
493 self.id = id
494
495 def SetBaseID(self, id):
496 """Set the ID stored by the svg elements."""
497 self.baseid = id
498
499 def SetFont(self, font):
500 """Set the fontproperties to use with text elements."""
501 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 """Set the style of the pen used to draw graphics."""
517 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 """Set the fill properties."""
528 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 """Set the color of the text foreground."""
537 self.font['fill'] = color.hex()
538
539 def make_style(self, line=0, fill=0, font=0):
540 """Build the style attribute including desired properties
541 such as fill, forground, stroke, etc."""
542 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 """Build the meta attribute."""
563 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 """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 if self.id < 0:
581 return ''
582 return 'id="%s-%s"' % (self.baseid, self.id)
583
584 def DrawEllipse(self, x, y, dx, dy):
585 """Draw an ellipse."""
586 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 """Draw a circle onto the virtual dc."""
592 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 """Draw a rectangle with the given parameters."""
598 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 """Draw Text at the given position."""
604 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 """Draw some points into a Buffer that will be
612 written before the next object.
613 """
614 self.DrawPolygon(points,0)
615
616 def DrawPolygon(self, polygon, closed=1):
617 """Draw a polygon onto the virtual dc."""
618 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 """Draw a spline object.
636 """
637 self.DrawPolygon(points, 0)
638 print "TODO: DrawSpline(..)"
639 return # TODO: Implement
640
641 def BeginClipPath(self, id):
642 """Build a clipping region to draw in."""
643 self.write_indent('<clipPath id="%s">\n' % id)
644 self.indent_level += 1
645
646 def EndClipPath(self):
647 """End a clip path."""
648 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