/[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 2388 - (show annotations)
Mon Nov 15 16:27:41 2004 UTC (20 years, 3 months ago) by bernhard
File MIME type: text/x-python
File size: 23618 byte(s)
* Extensions/svgexport/: Minor improvements to doc strings.

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