/[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 2082 - (show annotations)
Fri Feb 20 14:33:22 2004 UTC (21 years ago) by bh
File MIME type: text/x-python
File size: 20956 byte(s)
Reorder the imports and
doc-string a bit.  The doc-string must come first, otherwise it's
not a doc-string.  The __future__ import must be the first thing
after the doc-string.  Use only double quotes in doc-strings.
Single quotes trip up emacs syntax highlighting if the text
contains apostrophes.

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