/[thuban]/branches/WIP-pyshapelib-bramz/Thuban/UI/baserenderer.py
ViewVC logotype

Annotation of /branches/WIP-pyshapelib-bramz/Thuban/UI/baserenderer.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2413 - (hide annotations)
Mon Nov 22 11:16:35 2004 UTC (20 years, 3 months ago) by bernhard
Original Path: trunk/thuban/Thuban/UI/baserenderer.py
File MIME type: text/x-python
File size: 20426 byte(s)
* Thuban/Model/base.py (UnsetModified):
Fixed some typos in docstring.

* Thuban/UI/baserenderer.py (BaseRenderer.draw_polygon_shape()):
Added hints on the used algorithm for handling holes.

1 bh 2224 # Copyright (c) 2001, 2002, 2003, 2004 by Intevation GmbH
2 bh 1554 # Authors:
3     # Bernhard Herzog <[email protected]>
4     # Jonathan Coles <[email protected]>
5     # Frank Koormann <[email protected]>
6     #
7     # This program is free software under the GPL (>=v2)
8     # Read the file COPYING coming with Thuban for details.
9    
10     """Basic rendering logic for Thuban maps
11    
12     The code in this module is completely independend of wx so that it can
13     be tested reasonably easily and it could make it easier to write non-wx
14     renderers.
15     """
16    
17 bh 1866 from __future__ import generators
18    
19 bh 1554 __version__ = "$Revision$"
20     # $Source$
21     # $Id$
22    
23 bh 1926 import sys
24 bh 1554 import traceback
25    
26     from Thuban.Model.layer import Layer, RasterLayer
27     from Thuban.Model.data import SHAPETYPE_ARC, SHAPETYPE_POINT
28     from Thuban.Model.label import ALIGN_CENTER, ALIGN_TOP, ALIGN_BOTTOM, \
29     ALIGN_LEFT, ALIGN_RIGHT
30    
31     import Thuban.Model.resource
32    
33     if Thuban.Model.resource.has_gdal_support():
34     from gdalwarp import ProjectRasterFile
35    
36    
37 bh 1926 #
38     # Renderer Extensions
39     #
40     # The renderer extensions provide a way to render layer types defined in
41     # Thuban extensions. The renderer extensions are stored as a list with
42     # (layer_class, draw_function) pairs. If the renderer has to draw a
43     # non-builtin layer type, i.e. a layer that is not a subclass of Layer
44     # or RasterLayer, it iterates through that list, tests whether the layer
45 bh 2224 # to be drawn is an instance of layer_class and if so calls
46 bh 1926 # draw_function with the renderer and the layer as arguments. Since
47     # drawing is done incrementally, the draw_function should return an
48     # iterable. The easiest way is to simply implement the draw_function as
49     # a generator and to yield in suitable places, or to return the empty
50     # tuple.
51     #
52     # New renderer extensions should be added with add_renderer_extension().
53     # If necessary the extensions list can be reset with
54     # init_renderer_extensions().
55    
56     _renderer_extensions = []
57    
58     def add_renderer_extension(layer_class, function):
59     """Add a renderer extension for the layer class layer_class
60    
61     When an instance of layer_class is to be drawn by the renderer the
62     renderer will call function with the renderer and the layer_class
63     instance as arguments. Since drawing is done incrementally, the
64     function should return an iterable. The easiest way is to simply
65     implement the draw_function as a generator and to yield True in
66     suitable places, or to return the empty tuple if it's not possible
67 bh 2224 to do the rendering incrementally.
68 bh 1926 """
69     _renderer_extensions.append((layer_class, function))
70    
71     def init_renderer_extensions():
72     """(Re)initialize the list of renderer extensions
73    
74     Calling this function outside of the test suite is probably not
75     useful.
76     """
77     del _renderer_extensions[:]
78    
79    
80     #
81     # Base Renderer
82     #
83    
84 bh 1554 class BaseRenderer:
85    
86     """Basic Renderer Infrastructure for Thuban Maps
87    
88     This class can't be used directly to render because it doesn't know
89     anything about real DCs such as how to create pens or brushes. That
90     functionality has to be provided by derived classes. The reason for
91     this is that it makes the BaseRenderer completely independend of wx
92     and thus it's quite easy to write test cases for it.
93     """
94     # If true the render honors the visibility flag of the layers
95     honor_visibility = 1
96    
97     # Transparent brushes and pens. Derived classes should define these
98     # as appropriate.
99     TRANSPARENT_PEN = None
100     TRANSPARENT_BRUSH = None
101    
102 bh 1866 def __init__(self, dc, map, scale, offset, region = None,
103     resolution = 72.0, honor_visibility = None):
104 bh 1554 """Inititalize the renderer.
105    
106     dc -- the device context to render on.
107    
108     scale, offset -- the scale factor and translation to convert
109     between projected coordinates and the DC coordinates
110    
111 bh 1866 region -- The region to draw as a (x, y, width, height) tuple in
112     the map's coordinate system. Default is None meaning
113     to draw everything.
114    
115 bh 1554 resolution -- the assumed resolution of the DC. Used to convert
116     absolute lengths like font sizes to DC coordinates. The
117 bh 1866 default is 72.0. If given, this parameter must be
118     provided as a keyword argument.
119 bh 1554
120     honor_visibility -- boolean. If true, honor the visibility flag
121     of the layers, otherwise draw all layers. If None (the
122 bh 1866 default), use the renderer's default. If given, this
123     parameter must be provided as a keyword argument.
124 bh 1554 """
125     # resolution in pixel/inch
126     self.dc = dc
127 bh 1866 self.map = map
128 bh 1554 self.scale = scale
129     self.offset = offset
130 bh 1866 self.region = region
131 bh 1554 if honor_visibility is not None:
132     self.honor_visibility = honor_visibility
133     # store the resolution in pixel/point because it's more useful
134     # later.
135     self.resolution = resolution / 72.0
136    
137     def tools_for_property(self, prop):
138     """Return a suitable pen and brush for the property
139    
140     This method must be implemented in derived classes. The return
141     value should be a tuple (pen, brush).
142     """
143     raise NotImplementedError
144    
145 bh 1866 def render_map(self):
146     """Render the map onto the DC.
147 bh 1554
148 bh 1866 Both map and DC are the ones the renderer was instantiated with.
149    
150     This method is just a front-end for render_map_incrementally
151     which does all rendering in one go. It also calls the dc's
152     BeginDrawing and EndDrawing methods before and after calling
153     render_map_incrementally.
154     """
155    
156     self.dc.BeginDrawing()
157     try:
158     for cont in self.render_map_incrementally():
159     pass
160     finally:
161     self.dc.EndDrawing()
162    
163     def render_map_incrementally(self):
164     """Render the map incrementally.
165    
166     Return an iterator whose next method should be called until it
167     returns False. After returning False it will raise StopIteration
168     so that you could also use it in a for loop (implementation
169     detail: this method is implemented as a generator).
170    
171 bh 1554 Iterate through all layers and draw them. Layers containing
172     vector data are drawn with the draw_shape_layer method, raster
173     layers are drawn with draw_raster_layer. The label layer is
174     drawn last with draw_label_layer.
175    
176     During execution of this method, the map is bound to self.map so
177     that methods especially in derived classes have access to the
178     map if necessary.
179     """
180     # Whether the raster layer has already been drawn. See below for
181     # the optimization this is used for
182     seenRaster = True
183    
184 bh 1866 #
185     # This is only a good optimization if there is only one
186     # raster layer and the image covers the entire window (as
187     # it currently does). We note if there is a raster layer
188     # and only begin drawing layers once we have drawn it.
189     # That way we avoid drawing layers that won't be seen.
190     #
191     if Thuban.Model.resource.has_gdal_support():
192     for layer in self.map.Layers():
193     if isinstance(layer, RasterLayer) and layer.Visible():
194     seenRaster = False
195     break
196 bh 1554
197 bh 1866 for layer in self.map.Layers():
198     # if honor_visibility is true, only draw visible layers,
199     # otherwise draw all layers
200     if not self.honor_visibility or layer.Visible():
201 bh 2278 if isinstance(layer, Layer):
202     if seenRaster:
203     for i in self.draw_shape_layer_incrementally(layer):
204     yield True
205 bh 1866 elif isinstance(layer, RasterLayer) \
206     and Thuban.Model.resource.has_gdal_support():
207     self.draw_raster_layer(layer)
208     seenRaster = True
209     yield True
210 bh 1926 else:
211     # look it up in the renderer extensions
212     for cls, func in _renderer_extensions:
213     if isinstance(layer, cls):
214     for i in func(self, layer):
215     yield True
216     break
217     else:
218     # No renderer found. Print a message about it
219     print >>sys.stderr, ("Drawing layer %r not supported"
220     % layer)
221     yield True
222 bh 1554
223 bh 1866 self.draw_label_layer(self.map.LabelLayer())
224     yield False
225 bh 1554
226 bh 1866 def draw_shape_layer_incrementally(self, layer):
227     """Draw the shape layer layer onto the map incrementally.
228 bh 1554
229 bh 1866 This method is a generator which yields True after every 500
230     shapes.
231 bh 1554 """
232     scale = self.scale
233     offx, offy = self.offset
234    
235     map_proj = self.map.projection
236     layer_proj = layer.projection
237    
238     brush = self.TRANSPARENT_BRUSH
239     pen = self.TRANSPARENT_PEN
240    
241     old_prop = None
242     old_group = None
243     lc = layer.GetClassification()
244     field = layer.GetClassificationColumn()
245     defaultGroup = lc.GetDefaultGroup()
246     table = layer.ShapeStore().Table()
247    
248 bh 1879 if lc.GetNumGroups() == 0:
249     # There's only the default group, so we can pretend that
250     # there is no field to classifiy on which makes things
251     # faster since we don't need the attribute information at
252     # all.
253     field = None
254    
255 bh 1554 # Determine which render function to use.
256 bh 1591 useraw, draw_func, draw_func_param = self.low_level_renderer(layer)
257 bh 1554
258 bh 1914 #
259 bh 1554 # Iterate through all shapes that have to be drawn.
260 bh 1914 #
261    
262     # Count the shapes drawn so that we can yield every few hundred
263     # shapes
264 bh 1866 count = 0
265 bh 1914
266     # Cache the tools (pens and brushes) for the classification
267     # groups. This is a mapping from the group's ids to the a tuple
268     # (pen, brush)
269     tool_cache = {}
270    
271 bh 1593 for shape in self.layer_shapes(layer):
272 bh 1866 count += 1
273 bh 1554 if field is None:
274     group = defaultGroup
275     else:
276 bh 1918 value = table.ReadValue(shape.ShapeID(), field)
277 bh 1914 group = lc.FindGroup(value)
278 bh 1554
279     if not group.IsVisible():
280     continue
281    
282 bh 1914 try:
283     pen, brush = tool_cache[id(group)]
284     except KeyError:
285     pen, brush = tool_cache[id(group)] \
286     = self.tools_for_property(group.GetProperties())
287 bh 1554
288 bh 1591 if useraw:
289     data = shape.RawData()
290     else:
291     data = shape.Points()
292 jan 2377 if draw_func == self.draw_point_shape:
293     draw_func(draw_func_param, data, pen, brush,
294     size = group.GetProperties().GetSize())
295     else:
296     draw_func(draw_func_param, data, pen, brush)
297 bh 1866 if count % 500 == 0:
298     yield True
299 bh 1554
300 bh 1593 def layer_shapes(self, layer):
301     """Return an iterable over the shapes to be drawn from the given layer.
302 bh 1554
303     The default implementation simply returns all ids in the layer.
304     Override in derived classes to be more precise.
305     """
306 bh 1593 return layer.ShapeStore().AllShapes()
307 bh 1554
308     def low_level_renderer(self, layer):
309     """Return the low-level renderer for the layer for draw_shape_layer
310    
311     The low level renderer to be returned by this method is a tuple
312 bh 1591 (useraw, func, param) where useraw is a boolean indicating
313     whether the function uses the raw shape data, func is a callable
314     object and param is passed as the first parameter to func. The
315     draw_shape_layer method will call func like this:
316 bh 1554
317 bh 1591 func(param, shapedata, pen, brush)
318 bh 1554
319 bh 1591 where shapedata is the return value of the RawData method of the
320     shape object if useraw is true or the return value of the Points
321     method if it's false. pen and brush are the pen and brush to use
322     to draw the shape on the dc.
323 bh 1554
324     The default implementation returns one of
325     self.draw_polygon_shape, self.draw_arc_shape or
326 bh 1591 self.draw_point_shape as func and layer as param. None of the
327     method use the raw shape data. Derived classes can override this
328     method to return more efficient low level renderers.
329 bh 1554 """
330     shapetype = layer.ShapeType()
331     if shapetype == SHAPETYPE_POINT:
332     func = self.draw_point_shape
333     elif shapetype == SHAPETYPE_ARC:
334     func = self.draw_arc_shape
335     else:
336     func = self.draw_polygon_shape
337 bh 1591 return False, func, layer
338 bh 1554
339     def make_point(self, x, y):
340     """Convert (x, y) to a point object.
341    
342     Derived classes must override this method.
343     """
344     raise NotImplementedError
345    
346 bh 1591 def projected_points(self, layer, points):
347     """Return the projected coordinates of the points taken from layer.
348 bh 1554
349 bh 1591 Transform all the points in the list of lists of coordinate
350     pairs in points.
351 bh 1554
352     The transformation applies the inverse of the layer's projection
353     if any, then the map's projection if any and finally applies
354     self.scale and self.offset.
355    
356     The returned list has the same structure as the one returned the
357     shape's Points method.
358     """
359     proj = self.map.GetProjection()
360     if proj is not None:
361     forward = proj.Forward
362     else:
363     forward = None
364     proj = layer.GetProjection()
365     if proj is not None:
366     inverse = proj.Inverse
367     else:
368     inverse = None
369 bh 1591 result = []
370 bh 1554 scale = self.scale
371     offx, offy = self.offset
372     make_point = self.make_point
373 bh 1591 for part in points:
374     result.append([])
375 bh 1554 for x, y in part:
376     if inverse:
377     x, y = inverse(x, y)
378     if forward:
379     x, y = forward(x, y)
380 bh 1591 result[-1].append(make_point(x * scale + offx,
381 bh 1554 -y * scale + offy))
382 bh 1591 return result
383 bh 1554
384 bh 1591 def draw_polygon_shape(self, layer, points, pen, brush):
385     """Draw a polygon shape from layer with the given brush and pen
386 bh 1554
387 bh 1591 The shape is given by points argument which is a the return
388     value of the shape's Points() method. The coordinates in the
389     DC's coordinate system are determined with
390 bh 1554 self.projected_points.
391 bernhard 2413
392     For a description of the algorithm look in wxproj.cpp.
393 bh 1554 """
394 bh 1591 points = self.projected_points(layer, points)
395 bh 1554
396     if brush is not self.TRANSPARENT_BRUSH:
397     polygon = []
398     for part in points:
399     polygon.extend(part)
400    
401 bernhard 2413 # missing back vertices for correct filling.
402 bh 1554 insert_index = len(polygon)
403     for part in points[:-1]:
404     polygon.insert(insert_index, part[0])
405    
406     self.dc.SetBrush(brush)
407     self.dc.SetPen(self.TRANSPARENT_PEN)
408     self.dc.DrawPolygon(polygon)
409    
410     if pen is not self.TRANSPARENT_PEN:
411     # At last draw the boundarys of the simple polygons
412     self.dc.SetBrush(self.TRANSPARENT_BRUSH)
413     self.dc.SetPen(pen)
414     for part in points:
415     self.dc.DrawLines(part)
416    
417 bh 1591 def draw_arc_shape(self, layer, points, pen, brush):
418     """Draw an arc shape from layer with the given brush and pen
419 bh 1554
420 bh 1591 The shape is given by points argument which is a the return
421     value of the shape's Points() method. The coordinates in the
422     DC's coordinate system are determined with
423 bh 1554 self.projected_points.
424     """
425 bh 1591 points = self.projected_points(layer, points)
426 bh 1554 self.dc.SetBrush(brush)
427     self.dc.SetPen(pen)
428     for part in points:
429     self.dc.DrawLines(part)
430    
431 jan 2377 def draw_point_shape(self, layer, points, pen, brush, size = 5):
432 bh 1591 """Draw a point shape from layer with the given brush and pen
433 bh 1554
434 bh 1591 The shape is given by points argument which is a the return
435     value of the shape's Points() method. The coordinates in the
436     DC's coordinate system are determined with
437 bh 1554 self.projected_points.
438    
439     The point is drawn as a circle centered on the point.
440     """
441 bh 1591 points = self.projected_points(layer, points)
442 bh 1554 if not points:
443     return
444    
445 jan 2377 radius = int(round(self.resolution * size))
446 bh 1554 self.dc.SetBrush(brush)
447     self.dc.SetPen(pen)
448     for part in points:
449     for p in part:
450     self.dc.DrawEllipse(p.x - radius, p.y - radius,
451     2 * radius, 2 * radius)
452    
453     def draw_raster_layer(self, layer):
454     """Draw the raster layer
455    
456     This implementation does the projection and scaling of the data
457     as required by the layer's and map's projections and the scale
458     and offset of the renderer and then hands the transformed data
459     to self.draw_raster_data() which has to be implemented in
460     derived classes.
461     """
462     offx, offy = self.offset
463     width, height = self.dc.GetSizeTuple()
464    
465     in_proj = ""
466     proj = layer.GetProjection()
467     if proj is not None:
468     for p in proj.GetAllParameters():
469     in_proj += "+" + p + " "
470    
471     out_proj = ""
472     proj = self.map.GetProjection()
473     if proj is not None:
474     for p in proj.GetAllParameters():
475     out_proj += "+" + p + " "
476    
477     xmin = (0 - offx) / self.scale
478     ymin = (offy - height) / self.scale
479     xmax = (width - offx) / self.scale
480     ymax = (offy - 0) / self.scale
481    
482     try:
483     data = ProjectRasterFile(layer.GetImageFilename(),
484     in_proj, out_proj,
485     (xmin, ymin, xmax, ymax), "",
486     (width, height))
487     except (IOError, AttributeError, ValueError):
488     # Why does this catch AttributeError and ValueError?
489     # FIXME: The exception should be communicated to the user
490     # better.
491     traceback.print_exc()
492     else:
493 bh 1926 self.draw_raster_data(data, "BMP")
494 bh 1554
495 bh 1926 def draw_raster_data(self, data, format="BMP"):
496 bh 1554 """Draw the raster image in data onto the DC
497    
498 bh 1926 The raster image data is a string holding the data in the format
499     indicated by the format parameter. The image is assumed to be
500     exactly the size of the dc and to cover it completely.
501 bh 1554
502 bh 1926 The format parameter is a string with the name of the format.
503     The following format names should be used:
504    
505     'BMP' -- Windows Bitmap
506     'JPEG' -- Jpeg
507    
508     The default format is 'bmp'.
509    
510     This method has to be implemented by derived classes. The
511     implementation in the derived class should try to support at
512     least the formats specified above and may support more.
513 bh 1554 """
514     raise NotImplementedError
515    
516     def label_font(self):
517     """Return the font object for the label layer"""
518     raise NotImplementedError
519    
520     def draw_label_layer(self, layer):
521     """Draw the label layer
522    
523     All labels are draw in the font returned by self.label_font().
524     """
525     scale = self.scale
526     offx, offy = self.offset
527    
528     self.dc.SetFont(self.label_font())
529    
530     map_proj = self.map.projection
531     if map_proj is not None:
532     forward = map_proj.Forward
533     else:
534     forward = None
535    
536     for label in layer.Labels():
537     x = label.x
538     y = label.y
539     text = label.text
540     if forward:
541     x, y = forward(x, y)
542 bh 1935 x = int(round(x * scale + offx))
543     y = int(round(-y * scale + offy))
544 bh 1554 width, height = self.dc.GetTextExtent(text)
545     if label.halign == ALIGN_LEFT:
546     # nothing to be done
547     pass
548     elif label.halign == ALIGN_RIGHT:
549     x = x - width
550     elif label.halign == ALIGN_CENTER:
551     x = x - width/2
552     if label.valign == ALIGN_TOP:
553     # nothing to be done
554     pass
555     elif label.valign == ALIGN_BOTTOM:
556     y = y - height
557     elif label.valign == ALIGN_CENTER:
558     y = y - height/2
559     self.dc.DrawText(text, x, y)

Properties

Name Value
svn:eol-style native
svn:keywords Author Date Id Revision

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26