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") |