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