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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2413 - (show 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 # Copyright (c) 2001, 2002, 2003, 2004 by Intevation GmbH
2 # 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 from __future__ import generators
18
19 __version__ = "$Revision$"
20 # $Source$
21 # $Id$
22
23 import sys
24 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 #
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 # to be drawn is an instance of layer_class and if so calls
46 # 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 to do the rendering incrementally.
68 """
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 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 def __init__(self, dc, map, scale, offset, region = None,
103 resolution = 72.0, honor_visibility = None):
104 """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 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 resolution -- the assumed resolution of the DC. Used to convert
116 absolute lengths like font sizes to DC coordinates. The
117 default is 72.0. If given, this parameter must be
118 provided as a keyword argument.
119
120 honor_visibility -- boolean. If true, honor the visibility flag
121 of the layers, otherwise draw all layers. If None (the
122 default), use the renderer's default. If given, this
123 parameter must be provided as a keyword argument.
124 """
125 # resolution in pixel/inch
126 self.dc = dc
127 self.map = map
128 self.scale = scale
129 self.offset = offset
130 self.region = region
131 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 def render_map(self):
146 """Render the map onto the DC.
147
148 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 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 #
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
197 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 if isinstance(layer, Layer):
202 if seenRaster:
203 for i in self.draw_shape_layer_incrementally(layer):
204 yield True
205 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 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
223 self.draw_label_layer(self.map.LabelLayer())
224 yield False
225
226 def draw_shape_layer_incrementally(self, layer):
227 """Draw the shape layer layer onto the map incrementally.
228
229 This method is a generator which yields True after every 500
230 shapes.
231 """
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 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 # Determine which render function to use.
256 useraw, draw_func, draw_func_param = self.low_level_renderer(layer)
257
258 #
259 # Iterate through all shapes that have to be drawn.
260 #
261
262 # Count the shapes drawn so that we can yield every few hundred
263 # shapes
264 count = 0
265
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 for shape in self.layer_shapes(layer):
272 count += 1
273 if field is None:
274 group = defaultGroup
275 else:
276 value = table.ReadValue(shape.ShapeID(), field)
277 group = lc.FindGroup(value)
278
279 if not group.IsVisible():
280 continue
281
282 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
288 if useraw:
289 data = shape.RawData()
290 else:
291 data = shape.Points()
292 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 if count % 500 == 0:
298 yield True
299
300 def layer_shapes(self, layer):
301 """Return an iterable over the shapes to be drawn from the given layer.
302
303 The default implementation simply returns all ids in the layer.
304 Override in derived classes to be more precise.
305 """
306 return layer.ShapeStore().AllShapes()
307
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 (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
317 func(param, shapedata, pen, brush)
318
319 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
324 The default implementation returns one of
325 self.draw_polygon_shape, self.draw_arc_shape or
326 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 """
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 return False, func, layer
338
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 def projected_points(self, layer, points):
347 """Return the projected coordinates of the points taken from layer.
348
349 Transform all the points in the list of lists of coordinate
350 pairs in points.
351
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 result = []
370 scale = self.scale
371 offx, offy = self.offset
372 make_point = self.make_point
373 for part in points:
374 result.append([])
375 for x, y in part:
376 if inverse:
377 x, y = inverse(x, y)
378 if forward:
379 x, y = forward(x, y)
380 result[-1].append(make_point(x * scale + offx,
381 -y * scale + offy))
382 return result
383
384 def draw_polygon_shape(self, layer, points, pen, brush):
385 """Draw a polygon shape from layer with the given brush and pen
386
387 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 self.projected_points.
391
392 For a description of the algorithm look in wxproj.cpp.
393 """
394 points = self.projected_points(layer, points)
395
396 if brush is not self.TRANSPARENT_BRUSH:
397 polygon = []
398 for part in points:
399 polygon.extend(part)
400
401 # missing back vertices for correct filling.
402 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 def draw_arc_shape(self, layer, points, pen, brush):
418 """Draw an arc shape from layer with the given brush and pen
419
420 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 self.projected_points.
424 """
425 points = self.projected_points(layer, points)
426 self.dc.SetBrush(brush)
427 self.dc.SetPen(pen)
428 for part in points:
429 self.dc.DrawLines(part)
430
431 def draw_point_shape(self, layer, points, pen, brush, size = 5):
432 """Draw a point shape from layer with the given brush and pen
433
434 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 self.projected_points.
438
439 The point is drawn as a circle centered on the point.
440 """
441 points = self.projected_points(layer, points)
442 if not points:
443 return
444
445 radius = int(round(self.resolution * size))
446 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 self.draw_raster_data(data, "BMP")
494
495 def draw_raster_data(self, data, format="BMP"):
496 """Draw the raster image in data onto the DC
497
498 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
502 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 """
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 x = int(round(x * scale + offx))
543 y = int(round(-y * scale + offy))
544 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