/[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 2552 - (hide annotations)
Fri Jan 28 15:54:00 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: 20941 byte(s)
Make layer's use_mask flag default to true. Support a bit array describing
the mask to use. Improve error handling in ProjectRasterFile (also addresses
RT #2947).

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 jonathan 2537 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 bh 1926
87     #
88     # Base Renderer
89     #
90    
91 bh 1554 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 bh 1866 def __init__(self, dc, map, scale, offset, region = None,
110     resolution = 72.0, honor_visibility = None):
111 bh 1554 """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 bh 1866 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 bh 1554 resolution -- the assumed resolution of the DC. Used to convert
123     absolute lengths like font sizes to DC coordinates. The
124 bh 1866 default is 72.0. If given, this parameter must be
125     provided as a keyword argument.
126 bh 1554
127     honor_visibility -- boolean. If true, honor the visibility flag
128     of the layers, otherwise draw all layers. If None (the
129 bh 1866 default), use the renderer's default. If given, this
130     parameter must be provided as a keyword argument.
131 bh 1554 """
132     # resolution in pixel/inch
133     self.dc = dc
134 bh 1866 self.map = map
135 bh 1554 self.scale = scale
136     self.offset = offset
137 bh 1866 self.region = region
138 bh 1554 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 bh 1866 def render_map(self):
153     """Render the map onto the DC.
154 bh 1554
155 bh 1866 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 bh 1554 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 bh 1866 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 bh 2278 if isinstance(layer, Layer):
193 jonathan 2537 for i in self.draw_shape_layer_incrementally(layer):
194     yield True
195 bh 1866 elif isinstance(layer, RasterLayer) \
196     and Thuban.Model.resource.has_gdal_support():
197     self.draw_raster_layer(layer)
198     yield True
199 bh 1926 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 bh 1554
212 bh 1866 self.draw_label_layer(self.map.LabelLayer())
213     yield False
214 bh 1554
215 bh 1866 def draw_shape_layer_incrementally(self, layer):
216     """Draw the shape layer layer onto the map incrementally.
217 bh 1554
218 bh 1866 This method is a generator which yields True after every 500
219     shapes.
220 bh 1554 """
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 bh 1879 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 bh 1554 # Determine which render function to use.
245 bh 1591 useraw, draw_func, draw_func_param = self.low_level_renderer(layer)
246 bh 1554
247 bh 1914 #
248 bh 1554 # Iterate through all shapes that have to be drawn.
249 bh 1914 #
250    
251     # Count the shapes drawn so that we can yield every few hundred
252     # shapes
253 bh 1866 count = 0
254 bh 1914
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 bh 1593 for shape in self.layer_shapes(layer):
261 bh 1866 count += 1
262 bh 1554 if field is None:
263     group = defaultGroup
264     else:
265 bh 1918 value = table.ReadValue(shape.ShapeID(), field)
266 bh 1914 group = lc.FindGroup(value)
267 bh 1554
268     if not group.IsVisible():
269     continue
270    
271 bh 1914 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 bh 1554
277 bh 1591 if useraw:
278     data = shape.RawData()
279     else:
280     data = shape.Points()
281 jan 2377 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 bh 1866 if count % 500 == 0:
287     yield True
288 bh 1554
289 bh 1593 def layer_shapes(self, layer):
290     """Return an iterable over the shapes to be drawn from the given layer.
291 bh 1554
292     The default implementation simply returns all ids in the layer.
293     Override in derived classes to be more precise.
294     """
295 bh 1593 return layer.ShapeStore().AllShapes()
296 bh 1554
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 bh 1591 (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 bh 1554
306 bh 1591 func(param, shapedata, pen, brush)
307 bh 1554
308 bh 1591 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 bh 1554
313     The default implementation returns one of
314     self.draw_polygon_shape, self.draw_arc_shape or
315 bh 1591 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 bh 1554 """
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 bh 1591 return False, func, layer
327 bh 1554
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 bh 1591 def projected_points(self, layer, points):
336     """Return the projected coordinates of the points taken from layer.
337 bh 1554
338 bh 1591 Transform all the points in the list of lists of coordinate
339     pairs in points.
340 bh 1554
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 bh 1591 result = []
359 bh 1554 scale = self.scale
360     offx, offy = self.offset
361     make_point = self.make_point
362 jonathan 2537
363 bh 1591 for part in points:
364     result.append([])
365 bh 1554 for x, y in part:
366     if inverse:
367     x, y = inverse(x, y)
368     if forward:
369     x, y = forward(x, y)
370 bh 1591 result[-1].append(make_point(x * scale + offx,
371 bh 1554 -y * scale + offy))
372 bh 1591 return result
373 bh 1554
374 bh 1591 def draw_polygon_shape(self, layer, points, pen, brush):
375     """Draw a polygon shape from layer with the given brush and pen
376 bh 1554
377 bh 1591 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 bh 1554 self.projected_points.
381 bernhard 2413
382     For a description of the algorithm look in wxproj.cpp.
383 bh 1554 """
384 bh 1591 points = self.projected_points(layer, points)
385 bh 1554
386     if brush is not self.TRANSPARENT_BRUSH:
387     polygon = []
388     for part in points:
389     polygon.extend(part)
390    
391 bernhard 2413 # missing back vertices for correct filling.
392 bh 1554 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 bh 1591 def draw_arc_shape(self, layer, points, pen, brush):
408     """Draw an arc shape from layer with the given brush and pen
409 bh 1554
410 bh 1591 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 bh 1554 self.projected_points.
414     """
415 bh 1591 points = self.projected_points(layer, points)
416 bh 1554 self.dc.SetBrush(brush)
417     self.dc.SetPen(pen)
418     for part in points:
419     self.dc.DrawLines(part)
420    
421 jan 2377 def draw_point_shape(self, layer, points, pen, brush, size = 5):
422 bh 1591 """Draw a point shape from layer with the given brush and pen
423 bh 1554
424 bh 1591 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 bh 1554 self.projected_points.
428    
429     The point is drawn as a circle centered on the point.
430     """
431 bh 1591 points = self.projected_points(layer, points)
432 bh 1554 if not points:
433     return
434    
435 jan 2377 radius = int(round(self.resolution * size))
436 bh 1554 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 jonathan 2537 in_proj = proj_params_to_str(layer.GetProjection())
456     out_proj = proj_params_to_str(self.map.GetProjection())
457 bh 1554
458 jonathan 2537 # 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 jonathan 2551 #if False:
462 jonathan 2537 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 bh 1554
469 jonathan 2551 #print bb
470     #print pmin, pmax
471 bh 1554
472 jonathan 2551 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 jonathan 2537 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 bh 1554 try:
487 jonathan 2552 options = 0
488     if layer.UseMask(): options = options | 1
489    
490 jonathan 2551 project_params = (layer.GetImageFilename(), in_proj, out_proj,
491     (xmin, ymin, xmax, ymax), "", (width, height),
492 jonathan 2552 options)
493 jonathan 2551
494     data = (width, height, apply(ProjectRasterFile, project_params))
495    
496 jonathan 2552 except (MemoryError, IOError, AttributeError, ValueError):
497 bh 1554 # Why does this catch AttributeError and ValueError?
498     # FIXME: The exception should be communicated to the user
499     # better.
500     traceback.print_exc()
501     else:
502 jonathan 2551 self.draw_raster_data(fmin[0]+offx, offy-fmax[1], data, "RAW")
503 jonathan 2537 data = None
504 bh 1554
505 jonathan 2551 def draw_raster_data(self, x, y, data, format="BMP"):
506 jonathan 2537 """Draw the raster image in data onto the DC with the top
507     left corner at (x,y)
508 bh 1554
509 jonathan 2551 The raster image data is a tuple of the form
510     (width, height, (image_data, mask_data))
511    
512     holding the image width, height, image data, and mask data.
513 jonathan 2552 mask_data may be None if a mask should not be used. If
514     format is 'RAW' the data will be RGB values and the mask
515     will be in XMB format. Otherwise, both kinds
516 jonathan 2551 of data are assumed to be in the format specified in format.
517 bh 1554
518 bh 1926 The format parameter is a string with the name of the format.
519     The following format names should be used:
520    
521 jonathan 2537 'RAW' -- an array of RGB values (len=3*width*height)
522     'BMP' -- Windows Bitmap
523     'JPEG' -- JPEG Image
524 bh 1926
525 jonathan 2537 The default format is 'BMP'.
526 bh 1926
527     This method has to be implemented by derived classes. The
528     implementation in the derived class should try to support at
529     least the formats specified above and may support more.
530 bh 1554 """
531     raise NotImplementedError
532    
533     def label_font(self):
534     """Return the font object for the label layer"""
535     raise NotImplementedError
536    
537     def draw_label_layer(self, layer):
538     """Draw the label layer
539    
540     All labels are draw in the font returned by self.label_font().
541     """
542     scale = self.scale
543     offx, offy = self.offset
544    
545     self.dc.SetFont(self.label_font())
546    
547     map_proj = self.map.projection
548     if map_proj is not None:
549     forward = map_proj.Forward
550     else:
551     forward = None
552    
553     for label in layer.Labels():
554     x = label.x
555     y = label.y
556     text = label.text
557     if forward:
558     x, y = forward(x, y)
559 bh 1935 x = int(round(x * scale + offx))
560     y = int(round(-y * scale + offy))
561 bh 1554 width, height = self.dc.GetTextExtent(text)
562     if label.halign == ALIGN_LEFT:
563     # nothing to be done
564     pass
565     elif label.halign == ALIGN_RIGHT:
566     x = x - width
567     elif label.halign == ALIGN_CENTER:
568     x = x - width/2
569     if label.valign == ALIGN_TOP:
570     # nothing to be done
571     pass
572     elif label.valign == ALIGN_BOTTOM:
573     y = y - height
574     elif label.valign == ALIGN_CENTER:
575     y = y - height/2
576     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