/[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 2551 - (show annotations)
Thu Jan 27 14:19:41 2005 UTC (20 years, 1 month ago) by jonathan
Original Path: trunk/thuban/Thuban/UI/baserenderer.py
File MIME type: text/x-python
File size: 20746 byte(s)
Add a new dialog box for raster layers. The dialog box allows
the user to toggle a mask that is generated by ProjectRasterFile
and is used to only draw the real parts of the projected image.

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