/[thuban]/trunk/thuban/Thuban/Model/layer.py
ViewVC logotype

Contents of /trunk/thuban/Thuban/Model/layer.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2644 - (show annotations)
Tue Jul 5 16:30:51 2005 UTC (19 years, 8 months ago) by bh
File MIME type: text/x-python
File size: 17024 byte(s)
* Thuban/Model/layer.py (Layer.__mangle_bounding_box)
(Layer.ClipBoundingBox): Rename ClipBoundingBox to
__mangle_bounding_box.  See the comments in the code and RT #2845

* test/test_layer.py (TestLayer.test_arc_layer_with_projection):
Remove the explicit test of ClipBoundingBox.  The method isn't
public anymore and the direct call in the test wasn't necessary in
the first place.  If ClipBoundingBox (now __mangle_bounding_box)
isn't called, the return value of ShapesInRegion will be
different.

1 # Copyright (c) 2001, 2002, 2003, 2004, 2005 by Intevation GmbH
2 # Authors:
3 # Bernhard Herzog <[email protected]>
4 # Jonathan Coles <[email protected]>
5 # Silke Reimer <[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 __version__ = "$Revision$"
11
12 import os
13 import warnings
14
15 from Thuban import _
16
17 from messages import LAYER_PROJECTION_CHANGED, LAYER_VISIBILITY_CHANGED, \
18 LAYER_CHANGED, LAYER_SHAPESTORE_REPLACED, CLASS_CHANGED
19
20 import classification
21
22 from color import Transparent, Black
23 from base import TitledObject, Modifiable
24 from data import SHAPETYPE_POLYGON, SHAPETYPE_ARC, SHAPETYPE_POINT
25
26 import resource
27
28 from color import Color
29
30 shapetype_names = {SHAPETYPE_POINT: "Point",
31 SHAPETYPE_ARC: "Arc",
32 SHAPETYPE_POLYGON: "Polygon"}
33
34 class BaseLayer(TitledObject, Modifiable):
35
36 """Base class for the layers."""
37
38 def __init__(self, title, visible = True, projection = None):
39 """Initialize the layer.
40
41 title -- the title
42 visible -- boolean. If true the layer is visible.
43 """
44 TitledObject.__init__(self, title)
45 Modifiable.__init__(self)
46 self.visible = visible
47 self.projection = projection
48
49 def Visible(self):
50 """Return true if layer is visible"""
51 return self.visible
52
53 def SetVisible(self, visible):
54 """Set the layer's visibility."""
55 self.visible = visible
56 self.issue(LAYER_VISIBILITY_CHANGED, self)
57
58 def HasClassification(self):
59 """Determine if this layer supports classifications."""
60 return False
61
62 def HasShapes(self):
63 """Determine if this layer supports shapes."""
64 return False
65
66 def GetProjection(self):
67 """Return the layer's projection."""
68 return self.projection
69
70 def SetProjection(self, projection):
71 """Set the layer's projection."""
72 self.projection = projection
73 self.changed(LAYER_PROJECTION_CHANGED, self)
74
75 def Type(self):
76 return "Unknown"
77
78 class Layer(BaseLayer):
79
80 """Represent the information of one geodata file (currently a shapefile)
81
82 All children of the layer have the same type.
83
84 A layer has fill and stroke colors. Colors should be instances of
85 Color. They can also be Transparent, indicating no fill or no stroke.
86
87 The layer objects send the following events, all of which have the
88 layer object as parameter:
89
90 TITLE_CHANGED -- The title has changed.
91 LAYER_PROJECTION_CHANGED -- the projection has changed.
92 """
93
94 def __init__(self, title, data, projection = None,
95 fill = Transparent,
96 stroke = Black,
97 lineWidth = 1,
98 visible = True):
99 """Initialize the layer.
100
101 title -- the title
102 data -- datastore object for the shape data shown by the layer
103 projection -- the projection object. Its Inverse method is
104 assumed to map the layer's coordinates to lat/long
105 coordinates
106 fill -- the fill color or Transparent if the shapes are
107 not filled
108 stroke -- the stroke color or Transparent if the shapes
109 are not stroked
110 visible -- boolean. If true the layer is visible.
111
112 colors are expected to be instances of Color class
113 """
114 BaseLayer.__init__(self, title,
115 visible = visible,
116 projection = projection)
117
118 self.__classification = None
119 self.store = None
120
121 self.SetShapeStore(data)
122
123 self.classification_column = None
124 self.SetClassificationColumn(None)
125 self.SetClassification(None)
126
127 self.__classification.SetDefaultLineColor(stroke)
128 self.__classification.SetDefaultLineWidth(lineWidth)
129 self.__classification.SetDefaultFill(fill)
130
131 self.UnsetModified()
132
133 def SetShapeStore(self, store):
134 # Set the classification to None if there is a classification
135 # and the new shapestore doesn't have a table with a suitable
136 # column, i.e one with the same name and type as before
137 # FIXME: Maybe we should keep it the same if the type is
138 # compatible enough such as FIELDTYPE_DOUBLE and FIELDTYPE_INT
139 if self.__classification is not None:
140 columnname = self.classification_column
141 columntype = self.GetFieldType(columnname)
142 table = store.Table()
143 if (columnname is not None
144 and (not table.HasColumn(columnname)
145 or table.Column(columnname).type != columntype)):
146 self.SetClassification(None)
147
148 self.store = store
149
150 self.changed(LAYER_SHAPESTORE_REPLACED, self)
151
152 def ShapeStore(self):
153 return self.store
154
155 def Destroy(self):
156 BaseLayer.Destroy(self)
157 if self.__classification is not None:
158 self.__classification.Unsubscribe(CLASS_CHANGED,
159 self._classification_changed)
160
161 def BoundingBox(self):
162 """Return the layer's bounding box in the intrinsic coordinate system.
163
164 If the layer has no shapes, return None.
165 """
166 return self.store.BoundingBox()
167
168 def LatLongBoundingBox(self):
169 """Return the layer's bounding box in lat/long coordinates.
170
171 Return None, if the layer doesn't contain any shapes.
172 """
173 bbox = self.BoundingBox()
174 if bbox is not None and self.projection is not None:
175 bbox = self.projection.InverseBBox(bbox)
176 return bbox
177
178 def Type(self):
179 return self.ShapeType();
180
181 def ShapesBoundingBox(self, shapes):
182 """Return a bounding box in lat/long coordinates for the given
183 list of shape ids.
184
185 If shapes is None or empty, return None.
186 """
187
188 if shapes is None or len(shapes) == 0: return None
189
190 xs = []
191 ys = []
192
193 for id in shapes:
194 bbox = self.Shape(id).compute_bbox()
195 if self.projection is not None:
196 bbox = self.projection.InverseBBox(bbox)
197 left, bottom, right, top = bbox
198 xs.append(left); xs.append(right)
199 ys.append(bottom); ys.append(top)
200
201 return (min(xs), min(ys), max(xs), max(ys))
202
203
204 def GetFieldType(self, fieldName):
205 if self.store:
206 table = self.store.Table()
207 if table.HasColumn(fieldName):
208 return table.Column(fieldName).type
209 return None
210
211 def HasShapes(self):
212 return True
213
214 def NumShapes(self):
215 """Return the number of shapes in the layer"""
216 return self.store.NumShapes()
217
218 def ShapeType(self):
219 """Return the type of the shapes in the layer.
220
221 The return value is one of the SHAPETYPE_* constants defined in
222 Thuban.Model.data.
223 """
224 return self.store.ShapeType()
225
226 def Shape(self, index):
227 """Return the shape with index index"""
228 return self.store.Shape(index)
229
230 def ShapesInRegion(self, bbox):
231 """Return an iterable over the shapes that overlap the bounding box.
232
233 The bbox parameter should be the bounding box as a tuple in the
234 form (minx, miny, maxx, maxy) in unprojected coordinates.
235 """
236 if self.projection is not None:
237 # Ensure that region lies within the layer's bounding box
238 # Otherwise projection of the region would lead to incorrect
239 # values.
240 clipbbox = self.__mangle_bounding_box(bbox)
241 bbox = self.projection.ForwardBBox(clipbbox)
242 return self.store.ShapesInRegion(bbox)
243
244 def GetClassificationColumn(self):
245 return self.classification_column
246
247 def SetClassificationColumn(self, column):
248 """Set the column to classifiy on, or None. If column is not None
249 and the column does not exist in the table, raise a ValueError.
250 """
251 if column:
252 columnType = self.GetFieldType(column)
253 if columnType is None:
254 raise ValueError()
255 changed = self.classification_column != column
256 self.classification_column = column
257 if changed:
258 self.changed(LAYER_CHANGED, self)
259
260 def HasClassification(self):
261 return True
262
263 def GetClassification(self):
264 return self.__classification
265
266 def SetClassification(self, clazz):
267 """Set the classification used by this layer to 'clazz'
268
269 If 'clazz' is None a default classification is created.
270
271 This issues a LAYER_CHANGED event.
272 """
273
274 if self.__classification is not None:
275 self.__classification.Unsubscribe(CLASS_CHANGED,
276 self._classification_changed)
277
278 if clazz is None:
279 clazz = classification.Classification()
280
281 self.__classification = clazz
282 self.__classification.Subscribe(CLASS_CHANGED,
283 self._classification_changed)
284
285 self._classification_changed()
286
287 def _classification_changed(self):
288 """Called from the classification object when it has changed."""
289 self.changed(LAYER_CHANGED, self)
290
291 def TreeInfo(self):
292 items = []
293
294 items.append(_("Filename: %s") % self.ShapeStore().FileName())
295
296 if self.Visible():
297 items.append(_("Shown"))
298 else:
299 items.append(_("Hidden"))
300 items.append(_("Shapes: %d") % self.NumShapes())
301
302 bbox = self.LatLongBoundingBox()
303 if bbox is not None:
304 items.append(_("Extent (lat-lon): (%g, %g, %g, %g)") % tuple(bbox))
305 else:
306 items.append(_("Extent (lat-lon):"))
307 items.append(_("Shapetype: %s") % shapetype_names[self.ShapeType()])
308
309 if self.projection and len(self.projection.params) > 0:
310 items.append((_("Projection"),
311 [str(param) for param in self.projection.params]))
312
313 items.append(self.__classification)
314
315 return (_("Layer '%s'") % self.Title(), items)
316
317 def __mangle_bounding_box(self, bbox):
318 # FIXME: This method doesn't make much sense.
319 # See RT #2845 which effectively says:
320 #
321 # If this method, which was originally called ClipBoundingBox,
322 # is supposed to do clipping it shouldn't return the parameter
323 # unchanged when it lies completely outside of the bounding box.
324 # It would be better to return None and return an empty list in
325 # ShapesInRegion (the only caller) in that case.
326 #
327 # This method was introduced to fix a bug that IIRC had
328 # something todo with projections and bounding boxes containing
329 # NaN or INF when the parameter to ShapesInRegion covered the
330 # entire earth or something similarly large).
331 bminx, bminy, bmaxx, bmaxy = bbox
332 lminx, lminy, lmaxx, lmaxy = self.LatLongBoundingBox()
333 if bminx > lmaxx or bmaxx < lminx:
334 left, right = bminx, bmaxx
335 else:
336 left = max(lminx, bminx)
337 right = min(lmaxx, bmaxx)
338 if bminy > lmaxy or bmaxy < lminy:
339 bottom, top = bminy, bmaxy
340 else:
341 bottom = max(lminy, bminy)
342 top = min(lmaxy, bmaxy)
343
344 return (left, bottom, right, top)
345
346
347 if resource.has_gdal_support():
348 import gdal
349 from gdalconst import GA_ReadOnly
350
351 class RasterLayer(BaseLayer):
352
353 MASK_NONE = 0
354 MASK_BIT = 1
355 MASK_ALPHA = 2
356
357 def __init__(self, title, filename, projection = None,
358 visible = True, opacity = 1, masktype = MASK_BIT):
359 """Initialize the Raster Layer.
360
361 title -- title for the layer.
362
363 filename -- file name of the source image.
364
365 projection -- Projection object describing the projection which
366 the source image is in.
367
368 visible -- True is the layer should initially be visible.
369
370 Throws IOError if the filename is invalid or points to a file that
371 is not in a format GDAL can use.
372 """
373
374 BaseLayer.__init__(self, title, visible = visible)
375
376 self.projection = projection
377 self.filename = os.path.abspath(filename)
378
379 self.bbox = -1
380
381 self.mask_type = masktype
382 self.opacity = opacity
383
384 self.image_info = None
385
386 if resource.has_gdal_support():
387 #
388 # temporarily open the file so that GDAL can test if it's valid.
389 #
390 dataset = gdal.Open(self.filename, GA_ReadOnly)
391
392 if dataset is None:
393 raise IOError()
394
395 #
396 # while we have the file, extract some basic information
397 # that we can display later
398 #
399 self.image_info = {}
400
401 self.image_info["nBands"] = dataset.RasterCount
402 self.image_info["Size"] = (dataset.RasterXSize, dataset.RasterYSize)
403 self.image_info["Driver"] = dataset.GetDriver().ShortName
404
405 # store some information about the individual bands
406 # [min_value, max_value]
407 a = self.image_info["BandData"] = []
408
409 for i in range(1, dataset.RasterCount+1):
410 band = dataset.GetRasterBand(i)
411 a.append(band.ComputeRasterMinMax())
412
413 self.UnsetModified()
414
415 def BoundingBox(self):
416 """Return the layer's bounding box in the intrinsic coordinate system.
417
418 If the there is no support for images, or the file cannot
419 be read, or there is no geographics information available, return None.
420 """
421 if not resource.has_gdal_support():
422 return None
423
424 if self.bbox == -1:
425 dataset = gdal.Open(self.filename, GA_ReadOnly)
426 if dataset is None:
427 self.bbox = None
428 else:
429 geotransform = dataset.GetGeoTransform()
430 if geotransform is None:
431 return None
432
433 x = 0
434 y = dataset.RasterYSize
435 left = geotransform[0] + \
436 geotransform[1] * x + \
437 geotransform[2] * y
438
439 bottom = geotransform[3] + \
440 geotransform[4] * x + \
441 geotransform[5] * y
442
443 x = dataset.RasterXSize
444 y = 0
445 right = geotransform[0] + \
446 geotransform[1] * x + \
447 geotransform[2] * y
448
449 top = geotransform[3] + \
450 geotransform[4] * x + \
451 geotransform[5] * y
452
453 self.bbox = (left, bottom, right, top)
454
455 return self.bbox
456
457 def LatLongBoundingBox(self):
458 bbox = self.BoundingBox()
459 if bbox is None:
460 return None
461
462 if self.projection is not None:
463 bbox = self.projection.InverseBBox(bbox)
464
465 return bbox
466
467 def Type(self):
468 return "Image"
469
470 def GetImageFilename(self):
471 return self.filename
472
473 def MaskType(self):
474 """Return True if the mask should be used when rendering the layer."""
475 return self.mask_type
476
477 def SetMaskType(self, type):
478 """Set the type of mask to use.
479
480 type can be one of MASK_NONE, MASK_BIT, MASK_ALPHA
481
482 If the state changes, a LAYER_CHANGED message is sent.
483 """
484 if type not in (self.MASK_NONE, self.MASK_BIT, self.MASK_ALPHA):
485 raise ValueError("type is invalid")
486
487 if type != self.mask_type:
488 self.mask_type = type
489 self.changed(LAYER_CHANGED, self)
490
491 def Opacity(self):
492 """Return the level of opacity used in alpha blending.
493 """
494 return self.opacity
495
496 def SetOpacity(self, op):
497 """Set the level of alpha opacity.
498
499 0 <= op <= 1.
500
501 The layer is fully opaque when op = 1.
502 """
503 if not (0 <= op <= 1):
504 raise ValueError("op out of range")
505
506 if op != self.opacity:
507 self.opacity = op
508 self.changed(LAYER_CHANGED, self)
509
510 def ImageInfo(self):
511 return self.image_info
512
513 def TreeInfo(self):
514 items = []
515
516 items.append(_("Filename: %s") % self.GetImageFilename())
517
518 if self.Visible():
519 items.append(_("Shown"))
520 else:
521 items.append(_("Hidden"))
522
523 bbox = self.LatLongBoundingBox()
524 if bbox is not None:
525 items.append(_("Extent (lat-lon): (%g, %g, %g, %g)") % bbox)
526 else:
527 items.append(_("Extent (lat-lon):"))
528
529 if self.projection and len(self.projection.params) > 0:
530 items.append((_("Projection"),
531 [str(param) for param in self.projection.params]))
532
533 return (_("Layer '%s'") % self.Title(), items)
534

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26