/[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 2734 - (show annotations)
Thu Mar 1 12:42:59 2007 UTC (18 years ago) by bramz
File MIME type: text/x-python
File size: 22109 byte(s)
made a copy
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
34
35 #
36 # Renderer Extensions
37 #
38 # The renderer extensions provide a way to render layer types defined in
39 # Thuban extensions. The renderer extensions are stored as a list with
40 # (layer_class, draw_function) pairs. If the renderer has to draw a
41 # non-builtin layer type, i.e. a layer that is not a subclass of Layer
42 # or RasterLayer, it iterates through that list, tests whether the layer
43 # to be drawn is an instance of layer_class and if so calls
44 # draw_function with the renderer and the layer as arguments. Since
45 # drawing is done incrementally, the draw_function should return an
46 # iterable. The easiest way is to simply implement the draw_function as
47 # a generator and to yield in suitable places, or to return the empty
48 # tuple.
49 #
50 # New renderer extensions should be added with add_renderer_extension().
51 # If necessary the extensions list can be reset with
52 # init_renderer_extensions().
53
54 _renderer_extensions = []
55
56 def add_renderer_extension(layer_class, function):
57 """Add a renderer extension for the layer class layer_class
58
59 When an instance of layer_class is to be drawn by the renderer the
60 renderer will call function with the renderer and the layer_class
61 instance as arguments. Since drawing is done incrementally, the
62 function should return an iterable. The easiest way is to simply
63 implement the draw_function as a generator and to yield True in
64 suitable places, or to return the empty tuple if it's not possible
65 to do the rendering incrementally.
66 """
67 _renderer_extensions.append((layer_class, function))
68
69 def init_renderer_extensions():
70 """(Re)initialize the list of renderer extensions
71
72 Calling this function outside of the test suite is probably not
73 useful.
74 """
75 del _renderer_extensions[:]
76
77 def proj_params_to_str(proj):
78 "Build a string suitable for GDAL describing the given projection"
79 str = ""
80 if proj is not None:
81 for p in proj.GetAllParameters():
82 str += "+" + p + " "
83 return str
84
85 #
86 # Base Renderer
87 #
88
89 class BaseRenderer:
90
91 """Basic Renderer Infrastructure for Thuban Maps
92
93 This class can't be used directly to render because it doesn't know
94 anything about real DCs such as how to create pens or brushes. That
95 functionality has to be provided by derived classes. The reason for
96 this is that it makes the BaseRenderer completely independend of wx
97 and thus it's quite easy to write test cases for it.
98 """
99 # If true the render honors the visibility flag of the layers
100 honor_visibility = 1
101
102 # Transparent brushes and pens. Derived classes should define these
103 # as appropriate.
104 TRANSPARENT_PEN = None
105 TRANSPARENT_BRUSH = None
106
107 def __init__(self, dc, map, scale, offset, region = None,
108 resolution = 72.0, honor_visibility = None):
109 """Inititalize the renderer.
110
111 dc -- the device context to render on.
112
113 scale, offset -- the scale factor and translation to convert
114 between projected coordinates and the DC coordinates
115
116 region -- The region to draw as a (x, y, width, height) tuple in
117 the map's coordinate system. Default is None meaning
118 to draw everything.
119
120 resolution -- the assumed resolution of the DC. Used to convert
121 absolute lengths like font sizes to DC coordinates. The
122 default is 72.0. If given, this parameter must be
123 provided as a keyword argument.
124
125 honor_visibility -- boolean. If true, honor the visibility flag
126 of the layers, otherwise draw all layers. If None (the
127 default), use the renderer's default. If given, this
128 parameter must be provided as a keyword argument.
129 """
130 # resolution in pixel/inch
131 self.dc = dc
132 self.map = map
133 self.scale = scale
134 self.offset = offset
135 self.region = region
136 if honor_visibility is not None:
137 self.honor_visibility = honor_visibility
138 # store the resolution in pixel/point because it's more useful
139 # later.
140 self.resolution = resolution / 72.0
141
142 def tools_for_property(self, prop):
143 """Return a suitable pen and brush for the property
144
145 This method must be implemented in derived classes. The return
146 value should be a tuple (pen, brush).
147 """
148 raise NotImplementedError
149
150 def render_map(self):
151 """Render the map onto the DC.
152
153 Both map and DC are the ones the renderer was instantiated with.
154
155 This method is just a front-end for render_map_incrementally
156 which does all rendering in one go. It also calls the dc's
157 BeginDrawing and EndDrawing methods before and after calling
158 render_map_incrementally.
159 """
160
161 self.dc.BeginDrawing()
162 try:
163 for cont in self.render_map_incrementally():
164 pass
165 finally:
166 self.dc.EndDrawing()
167
168 def render_map_incrementally(self):
169 """Render the map incrementally.
170
171 Return an iterator whose next method should be called until it
172 returns False. After returning False it will raise StopIteration
173 so that you could also use it in a for loop (implementation
174 detail: this method is implemented as a generator).
175
176 Iterate through all layers and draw them. Layers containing
177 vector data are drawn with the draw_shape_layer method, raster
178 layers are drawn with draw_raster_layer. The label layer is
179 drawn last with draw_label_layer.
180
181 During execution of this method, the map is bound to self.map so
182 that methods especially in derived classes have access to the
183 map if necessary.
184 """
185
186 for layer in self.map.Layers():
187 # if honor_visibility is true, only draw visible layers,
188 # otherwise draw all layers
189 if not self.honor_visibility or layer.Visible():
190 if isinstance(layer, Layer):
191 for i in self.draw_shape_layer_incrementally(layer):
192 yield True
193 elif isinstance(layer, RasterLayer):
194 self.draw_raster_layer(layer)
195 yield True
196 else:
197 # look it up in the renderer extensions
198 for cls, func in _renderer_extensions:
199 if isinstance(layer, cls):
200 for i in func(self, layer):
201 yield True
202 break
203 else:
204 # No renderer found. Print a message about it
205 print >>sys.stderr, ("Drawing layer %r not supported"
206 % layer)
207 yield True
208
209 self.draw_label_layer(self.map.LabelLayer())
210 yield False
211
212 def draw_shape_layer_incrementally(self, layer):
213 """Draw the shape layer layer onto the map incrementally.
214
215 This method is a generator which yields True after every 500
216 shapes.
217 """
218 scale = self.scale
219 offx, offy = self.offset
220
221 map_proj = self.map.projection
222 layer_proj = layer.projection
223
224 brush = self.TRANSPARENT_BRUSH
225 pen = self.TRANSPARENT_PEN
226
227 old_prop = None
228 old_group = None
229 lc = layer.GetClassification()
230 field = layer.GetClassificationColumn()
231 defaultGroup = lc.GetDefaultGroup()
232 table = layer.ShapeStore().Table()
233
234 if lc.GetNumGroups() == 0:
235 # There's only the default group, so we can pretend that
236 # there is no field to classifiy on which makes things
237 # faster since we don't need the attribute information at
238 # all.
239 field = None
240
241 # Determine which render function to use.
242 useraw, draw_func, draw_func_param = self.low_level_renderer(layer)
243
244 #
245 # Iterate through all shapes that have to be drawn.
246 #
247
248 # Count the shapes drawn so that we can yield every few hundred
249 # shapes
250 count = 0
251
252 # Cache the tools (pens and brushes) for the classification
253 # groups. This is a mapping from the group's ids to the a tuple
254 # (pen, brush)
255 tool_cache = {}
256
257 for shape in self.layer_shapes(layer):
258 count += 1
259 if field is None:
260 group = defaultGroup
261 else:
262 value = table.ReadValue(shape.ShapeID(), field)
263 group = lc.FindGroup(value)
264
265 if not group.IsVisible():
266 continue
267
268 try:
269 pen, brush = tool_cache[id(group)]
270 except KeyError:
271 pen, brush = tool_cache[id(group)] \
272 = self.tools_for_property(group.GetProperties())
273
274 if useraw:
275 data = shape.RawData()
276 else:
277 data = shape.Points()
278 if draw_func == self.draw_point_shape:
279 draw_func(draw_func_param, data, pen, brush,
280 size = group.GetProperties().GetSize())
281 else:
282 draw_func(draw_func_param, data, pen, brush)
283 if count % 500 == 0:
284 yield True
285
286 def layer_shapes(self, layer):
287 """Return an iterable over the shapes to be drawn from the given layer.
288
289 The default implementation simply returns all ids in the layer.
290 Override in derived classes to be more precise.
291 """
292 return layer.ShapeStore().AllShapes()
293
294 def low_level_renderer(self, layer):
295 """Return the low-level renderer for the layer for draw_shape_layer
296
297 The low level renderer to be returned by this method is a tuple
298 (useraw, func, param) where useraw is a boolean indicating
299 whether the function uses the raw shape data, func is a callable
300 object and param is passed as the first parameter to func. The
301 draw_shape_layer method will call func like this:
302
303 func(param, shapedata, pen, brush)
304
305 where shapedata is the return value of the RawData method of the
306 shape object if useraw is true or the return value of the Points
307 method if it's false. pen and brush are the pen and brush to use
308 to draw the shape on the dc.
309
310 The default implementation returns one of
311 self.draw_polygon_shape, self.draw_arc_shape or
312 self.draw_point_shape as func and layer as param. None of the
313 method use the raw shape data. Derived classes can override this
314 method to return more efficient low level renderers.
315 """
316 shapetype = layer.ShapeType()
317 if shapetype == SHAPETYPE_POINT:
318 func = self.draw_point_shape
319 elif shapetype == SHAPETYPE_ARC:
320 func = self.draw_arc_shape
321 else:
322 func = self.draw_polygon_shape
323 return False, func, layer
324
325 def make_point(self, x, y):
326 """Convert (x, y) to a point object.
327
328 Derived classes must override this method.
329 """
330 raise NotImplementedError
331
332 def projected_points(self, layer, points):
333 """Return the projected coordinates of the points taken from layer.
334
335 Transform all the points in the list of lists of coordinate
336 pairs in points.
337
338 The transformation applies the inverse of the layer's projection
339 if any, then the map's projection if any and finally applies
340 self.scale and self.offset.
341
342 The returned list has the same structure as the one returned the
343 shape's Points method.
344 """
345 proj = self.map.GetProjection()
346 if proj is not None:
347 forward = proj.Forward
348 else:
349 forward = None
350 proj = layer.GetProjection()
351 if proj is not None:
352 inverse = proj.Inverse
353 else:
354 inverse = None
355 result = []
356 scale = self.scale
357 offx, offy = self.offset
358 make_point = self.make_point
359
360 for part in points:
361 result.append([])
362 for x, y in part:
363 if inverse:
364 x, y = inverse(x, y)
365 if forward:
366 x, y = forward(x, y)
367 result[-1].append(make_point(x * scale + offx,
368 -y * scale + offy))
369 return result
370
371 def draw_polygon_shape(self, layer, points, pen, brush):
372 """Draw a polygon shape from layer with the given brush and pen
373
374 The shape is given by points argument which is a the return
375 value of the shape's Points() method. The coordinates in the
376 DC's coordinate system are determined with
377 self.projected_points.
378
379 For a description of the algorithm look in wxproj.cpp.
380 """
381 points = self.projected_points(layer, points)
382
383 if brush is not self.TRANSPARENT_BRUSH:
384 polygon = []
385 for part in points:
386 polygon.extend(part)
387
388 # missing back vertices for correct filling.
389 insert_index = len(polygon)
390 for part in points[:-1]:
391 polygon.insert(insert_index, part[0])
392
393 self.dc.SetBrush(brush)
394 self.dc.SetPen(self.TRANSPARENT_PEN)
395 self.dc.DrawPolygon(polygon)
396
397 if pen is not self.TRANSPARENT_PEN:
398 # At last draw the boundarys of the simple polygons
399 self.dc.SetBrush(self.TRANSPARENT_BRUSH)
400 self.dc.SetPen(pen)
401 for part in points:
402 self.dc.DrawLines(part)
403
404 def draw_arc_shape(self, layer, points, pen, brush):
405 """Draw an arc shape from layer with the given brush and pen
406
407 The shape is given by points argument which is a the return
408 value of the shape's Points() method. The coordinates in the
409 DC's coordinate system are determined with
410 self.projected_points.
411 """
412 points = self.projected_points(layer, points)
413 self.dc.SetBrush(brush)
414 self.dc.SetPen(pen)
415 for part in points:
416 self.dc.DrawLines(part)
417
418 def draw_point_shape(self, layer, points, pen, brush, size = 5):
419 """Draw a point shape from layer with the given brush and pen
420
421 The shape is given by points argument which is a the return
422 value of the shape's Points() method. The coordinates in the
423 DC's coordinate system are determined with
424 self.projected_points.
425
426 The point is drawn as a circle centered on the point.
427 """
428 points = self.projected_points(layer, points)
429 if not points:
430 return
431
432 radius = int(round(self.resolution * size))
433 self.dc.SetBrush(brush)
434 self.dc.SetPen(pen)
435 for part in points:
436 for p in part:
437 self.dc.DrawEllipse(p.x - radius, p.y - radius,
438 2 * radius, 2 * radius)
439
440 def draw_raster_layer(self, layer):
441 """Draw the raster layer
442
443 This implementation uses self.projected_raster_layer() to project
444 and scale the data as required by the layer's and map's projections
445 and the scale and offset of the renderer and then hands the transformed
446 data to self.draw_raster_data() which has to be implemented in
447 derived classes.
448 """
449 offx, offy = self.offset
450 width, height = self.dc.GetSizeTuple()
451
452 in_proj = proj_params_to_str(layer.GetProjection())
453 out_proj = proj_params_to_str(self.map.GetProjection())
454
455 # True -- warp the image to the size of the whole screen
456 # False -- only use the bound box of the layer (currently inaccurate)
457 if True:
458 #if False:
459 pmin = [0,height]
460 pmax = [width, 0]
461 else:
462 bb = layer.LatLongBoundingBox()
463 bb = [[[bb[0], bb[1]], [bb[2], bb[3]],],]
464 pmin, pmax = self.projected_points(layer, bb)[0]
465
466 #print bb
467 #print pmin, pmax
468
469 fmin = [max(0, min(pmin[0], pmax[0])) - offx,
470 offy - min(height, max(pmin[1], pmax[1]))]
471
472 fmax = [min(width, max(pmin[0], pmax[0])) - offx,
473 offy - max(0, min(pmin[1], pmax[1]))]
474
475 xmin = fmin[0]/self.scale
476 ymin = fmin[1]/self.scale
477 xmax = fmax[0]/self.scale
478 ymax = fmax[1]/self.scale
479
480 width = int(min(width, round(fmax[0] - fmin[0] + 1)))
481 height = int(min(height, round(fmax[1] - fmin[1] + 1)))
482
483 options = 0
484 options = options | layer.MaskType()
485
486 img_data = self.projected_raster_layer(layer, in_proj, out_proj,
487 (xmin,ymin,xmax,ymax), [0,0], (width, height), options)
488
489 if img_data is not None:
490 data = (width, height, img_data)
491 self.draw_raster_data(fmin[0]+offx, offy-fmax[1],
492 data, format="RAW", opacity=layer.Opacity())
493 data = None
494
495 def projected_raster_layer(self, layer, srcProj, dstProj, extents,
496 resolution, dimensions, options):
497 """Return the projected raster image associated with the layer.
498
499 The returned value will be a tuple of the form
500
501 (image_data, mask_data, alpha_data)
502
503 suitable for the data parameter to draw_raster_data.
504
505 The return value may be None if raster projections are not supported.
506
507 srcProj -- a string describing the source projection
508 dstProj -- a string describing the destination projection
509 extents -- a tuple of the region to project in map coordinates
510 resolution -- (currently not used, defaults to [0,0])
511 dimensions -- a tuple (width, height) for the output image
512 options -- bit-wise options to pass to the renderer
513
514 the currently supported values for options are
515
516 OPTS_MASK = 1 -- generate a mask
517 OPTS_ALPHA = 2 -- generate an alpha channel
518 OPTS_INVERT_MASK = 4 -- invert the values in the mask
519 (if generated)
520
521 This method has to be implemented by derived classes.
522 """
523
524 raise NotImplementedError
525
526 def draw_raster_data(self, x, y, data, format="BMP", opacity=1.0):
527 """Draw a raster image held in data onto the DC with the top
528 left corner at (x,y)
529
530 The raster image data is a tuple of the form
531 (width, height, (image_data, mask_data, alpha_data))
532
533 holding the image width, height, image data, mask data, and alpha data.
534 mask_data may be None if a mask should not be used. alpha_data may
535 also be None. If both are not None mask overrides alpha. If
536 format is 'RAW' the data will be RGB values and the mask
537 will be in XMB format. Otherwise, both kinds
538 of data are assumed to be in the format specified in format.
539
540 The format parameter is a string with the name of the format.
541 The following format names should be used:
542
543 'RAW' -- an array of RGB values (len=3*width*height)
544 'PNG' -- Portable Network Graphic (transparency supported)
545 'BMP' -- Windows Bitmap
546 'TIFF' -- Tagged Image File Format
547 'GIF' -- GIF Image
548 'JPEG' -- JPEG Image
549
550 The default format is 'BMP'.
551
552 This method has to be implemented by derived classes. The
553 implementation in the derived class should try to support at
554 least the formats specified above and may support more.
555 """
556 raise NotImplementedError
557
558 def label_font(self):
559 """Return the font object for the label layer"""
560 raise NotImplementedError
561
562 def draw_label_layer(self, layer):
563 """Draw the label layer
564
565 All labels are draw in the font returned by self.label_font().
566 """
567 scale = self.scale
568 offx, offy = self.offset
569
570 self.dc.SetFont(self.label_font())
571
572 map_proj = self.map.projection
573 if map_proj is not None:
574 forward = map_proj.Forward
575 else:
576 forward = None
577
578 for label in layer.Labels():
579 x = label.x
580 y = label.y
581 text = label.text
582 if forward:
583 x, y = forward(x, y)
584 x = int(round(x * scale + offx))
585 y = int(round(-y * scale + offy))
586 width, height = self.dc.GetTextExtent(text.decode('iso-8859-1'))
587 if label.halign == ALIGN_LEFT:
588 # nothing to be done
589 pass
590 elif label.halign == ALIGN_RIGHT:
591 x = x - width
592 elif label.halign == ALIGN_CENTER:
593 x = x - width/2
594 if label.valign == ALIGN_TOP:
595 # nothing to be done
596 pass
597 elif label.valign == ALIGN_BOTTOM:
598 y = y - height
599 elif label.valign == ALIGN_CENTER:
600 y = y - height/2
601 self.dc.DrawText(text.decode('iso-8859-1'), 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