/[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 1587 - (show annotations)
Fri Aug 15 10:31:07 2003 UTC (21 years, 6 months ago) by bh
File MIME type: text/x-python
File size: 14831 byte(s)
* Thuban/Model/layer.py (Layer.ShapesInRegion): Apply the layer
projection to all corners of the bounding box to get a better
approximation of the projected bounding box

* test/test_layer.py (TestLayer.test_point_layer_with_projection):
New. Test coordinate handling of a layer with a projection.
Catches the bug fixed in Layer.ShapesInRegion

1 # Copyright (c) 2001, 2002, 2003 by Intevation GmbH
2 # Authors:
3 # Bernhard Herzog <[email protected]>
4 # Jonathan Coles <[email protected]>
5 #
6 # This program is free software under the GPL (>=v2)
7 # Read the file COPYING coming with Thuban for details.
8
9 __version__ = "$Revision$"
10
11 import warnings
12
13 from Thuban import _
14
15 from messages import LAYER_PROJECTION_CHANGED, LAYER_VISIBILITY_CHANGED, \
16 LAYER_CHANGED, LAYER_SHAPESTORE_REPLACED, CLASS_CHANGED
17
18 import classification
19
20 from color import Transparent, Black
21 from base import TitledObject, Modifiable
22 from data import SHAPETYPE_POLYGON, SHAPETYPE_ARC, SHAPETYPE_POINT
23
24 import resource
25
26
27 shapetype_names = {SHAPETYPE_POINT: "Point",
28 SHAPETYPE_ARC: "Arc",
29 SHAPETYPE_POLYGON: "Polygon"}
30
31 class BaseLayer(TitledObject, Modifiable):
32
33 """Base class for the layers."""
34
35 def __init__(self, title, visible = True, projection = None):
36 """Initialize the layer.
37
38 title -- the title
39 visible -- boolean. If true the layer is visible.
40 """
41 TitledObject.__init__(self, title)
42 Modifiable.__init__(self)
43 self.visible = visible
44 self.projection = projection
45
46 def Visible(self):
47 """Return true if layer is visible"""
48 return self.visible
49
50 def SetVisible(self, visible):
51 """Set the layer's visibility."""
52 self.visible = visible
53 self.issue(LAYER_VISIBILITY_CHANGED, self)
54
55 def HasClassification(self):
56 """Determine if this layer support classifications."""
57 return False
58
59 def HasShapes(self):
60 """Determine if this layer supports shapes."""
61 return False
62
63 def GetProjection(self):
64 """Return the layer's projection."""
65 return self.projection
66
67 def SetProjection(self, projection):
68 """Set the layer's projection"""
69 self.projection = projection
70 self.changed(LAYER_PROJECTION_CHANGED, self)
71
72 class Layer(BaseLayer):
73
74 """Represent the information of one geodata file (currently a shapefile)
75
76 All children of the layer have the same type.
77
78 A layer has fill and stroke colors. Colors should be instances of
79 Color. They can also be Transparent, indicating no fill or no stroke.
80
81 The layer objects send the following events, all of which have the
82 layer object as parameter:
83
84 TITLE_CHANGED -- The title has changed.
85 LAYER_PROJECTION_CHANGED -- the projection has changed.
86 """
87
88 def __init__(self, title, data, projection = None,
89 fill = Transparent,
90 stroke = Black,
91 lineWidth = 1,
92 visible = True):
93 """Initialize the layer.
94
95 title -- the title
96 data -- datastore object for the shape data shown by the layer
97 projection -- the projection object. Its Inverse method is
98 assumed to map the layer's coordinates to lat/long
99 coordinates
100 fill -- the fill color or Transparent if the shapes are
101 not filled
102 stroke -- the stroke color or Transparent if the shapes
103 are not stroked
104 visible -- boolean. If true the layer is visible.
105
106 colors are expected to be instances of Color class
107 """
108 BaseLayer.__init__(self, title,
109 visible = visible,
110 projection = projection)
111
112 self.__classification = None
113 self.store = None
114
115 self.SetShapeStore(data)
116
117 self.classification_column = None
118 self.SetClassificationColumn(None)
119 self.SetClassification(None)
120
121 self.__classification.SetDefaultLineColor(stroke)
122 self.__classification.SetDefaultLineWidth(lineWidth)
123 self.__classification.SetDefaultFill(fill)
124
125 self.UnsetModified()
126
127 def __getattr__(self, attr):
128 """Access to some attributes for backwards compatibility
129
130 The attributes implemented here are now held by the shapestore
131 if at all. For backwards compatibility pretend that they are
132 still there but issue a DeprecationWarning when they are
133 accessed.
134 """
135 if attr in ("table", "shapetable"):
136 value = self.store.Table()
137 elif attr == "shapefile":
138 value = self.store.Shapefile()
139 elif attr == "filename":
140 value = self.store.FileName()
141 else:
142 raise AttributeError, attr
143 warnings.warn("The Layer attribute %r is deprecated."
144 " It's value can be accessed through the shapestore"
145 % attr, DeprecationWarning, stacklevel = 2)
146 return value
147
148 def SetShapeStore(self, store):
149 # Set the classification to None if there is a classification
150 # and the new shapestore doesn't have a table with a suitable
151 # column, i.e one with the same name and type as before
152 # FIXME: Maybe we should keep it the same if the type is
153 # compatible enough such as FIELDTYPE_DOUBLE and FIELDTYPE_INT
154 if self.__classification is not None:
155 columnname = self.classification_column
156 columntype = self.GetFieldType(columnname)
157 table = store.Table()
158 if (columnname is not None
159 and (not table.HasColumn(columnname)
160 or table.Column(columnname).type != columntype)):
161 self.SetClassification(None)
162
163 self.store = store
164
165 self.changed(LAYER_SHAPESTORE_REPLACED, self)
166
167 def ShapeStore(self):
168 return self.store
169
170 def Destroy(self):
171 BaseLayer.Destroy(self)
172 if self.__classification is not None:
173 self.__classification.Unsubscribe(CLASS_CHANGED,
174 self._classification_changed)
175
176 def BoundingBox(self):
177 """Return the layer's bounding box in the intrinsic coordinate system.
178
179 If the layer has no shapes, return None.
180 """
181 return self.store.BoundingBox()
182
183 def LatLongBoundingBox(self):
184 """Return the layer's bounding box in lat/long coordinates.
185
186 Return None, if the layer doesn't contain any shapes.
187 """
188 bbox = self.BoundingBox()
189 if bbox is not None:
190 llx, lly, urx, ury = bbox
191 if self.projection is not None:
192 llx, lly = self.projection.Inverse(llx, lly)
193 urx, ury = self.projection.Inverse(urx, ury)
194 return llx, lly, urx, ury
195 else:
196 return None
197
198 def ShapesBoundingBox(self, shapes):
199 """Return a bounding box in lat/long coordinates for the given
200 list of shape ids.
201
202 If shapes is None or empty, return None.
203 """
204
205 if shapes is None or len(shapes) == 0: return None
206
207 llx = []
208 lly = []
209 urx = []
210 ury = []
211
212 if self.projection is not None:
213 inverse = lambda x, y: self.projection.Inverse(x, y)
214 else:
215 inverse = lambda x, y: (x, y)
216
217 for id in shapes:
218 left, bottom, right, top = self.Shape(id).compute_bbox()
219
220 left, bottom = inverse(left, bottom)
221 right, top = inverse(right, top)
222
223 llx.append(left)
224 lly.append(bottom)
225 urx.append(right)
226 ury.append(top)
227
228 return (min(llx), min(lly), max(urx), max(ury))
229
230 def GetFieldType(self, fieldName):
231 if self.store:
232 table = self.store.Table()
233 if table.HasColumn(fieldName):
234 return table.Column(fieldName).type
235 return None
236
237 def HasShapes(self):
238 return True
239
240 def NumShapes(self):
241 """Return the number of shapes in the layer"""
242 return self.store.NumShapes()
243
244 def ShapeType(self):
245 """Return the type of the shapes in the layer.
246
247 The return value is one of the SHAPETYPE_* constants defined in
248 Thuban.Model.data.
249 """
250 return self.store.ShapeType()
251
252 def Shape(self, index):
253 """Return the shape with index index"""
254 return self.store.Shape(index)
255
256 def ShapesInRegion(self, bbox):
257 """Return the ids of the shapes that overlap the box.
258
259 Box is a tuple (left, bottom, right, top) in unprojected coordinates.
260 """
261 if self.projection is not None:
262 left, bottom, right, top = bbox
263 xs = []; ys = []
264 for x, y in [(left, bottom), (left, top), (right, top),
265 (right, bottom)]:
266 x, y = self.projection.Forward(x, y)
267 xs.append(x)
268 ys.append(y)
269 bbox = (min(xs), min(ys), max(xs), max(ys))
270
271 return self.store.ShapesInRegion(bbox)
272
273 def GetClassificationColumn(self):
274 return self.classification_column
275
276 def SetClassificationColumn(self, column):
277 """Set the column to classifiy on, or None. If column is not None
278 and the column does not exist in the table, raise a ValueError.
279 """
280 if column:
281 columnType = self.GetFieldType(column)
282 if columnType is None:
283 raise ValueError()
284 changed = self.classification_column != column
285 self.classification_column = column
286 if changed:
287 self.changed(LAYER_CHANGED, self)
288
289 def HasClassification(self):
290 return True
291
292 def GetClassification(self):
293 return self.__classification
294
295 def SetClassification(self, clazz):
296 """Set the classification used by this layer to 'clazz'
297
298 If 'clazz' is None a default classification is created.
299
300 This issues a LAYER_CHANGED event.
301 """
302
303 if self.__classification is not None:
304 self.__classification.Unsubscribe(CLASS_CHANGED,
305 self._classification_changed)
306
307 if clazz is None:
308 clazz = classification.Classification()
309
310 self.__classification = clazz
311 self.__classification.Subscribe(CLASS_CHANGED,
312 self._classification_changed)
313
314 self._classification_changed()
315
316 def _classification_changed(self):
317 """Called from the classification object when it has changed."""
318 self.changed(LAYER_CHANGED, self)
319
320 def TreeInfo(self):
321 items = []
322
323 items.append(_("Filename: %s") % self.ShapeStore().FileName())
324
325 if self.Visible():
326 items.append(_("Shown"))
327 else:
328 items.append(_("Hidden"))
329 items.append(_("Shapes: %d") % self.NumShapes())
330
331 bbox = self.LatLongBoundingBox()
332 if bbox is not None:
333 items.append(_("Extent (lat-lon): (%g, %g, %g, %g)") % bbox)
334 else:
335 items.append(_("Extent (lat-lon):"))
336 items.append(_("Shapetype: %s") % shapetype_names[self.ShapeType()])
337
338 if self.projection and len(self.projection.params) > 0:
339 items.append((_("Projection"),
340 [str(param) for param in self.projection.params]))
341
342 items.append(self.__classification)
343
344 return (_("Layer '%s'") % self.Title(), items)
345
346
347 if resource.has_gdal_support():
348 import gdal
349 from gdalconst import GA_ReadOnly
350
351 class RasterLayer(BaseLayer):
352
353 def __init__(self, title, filename, projection = None, visible = True):
354 """Initialize the Raster Layer.
355
356 title -- title for the layer.
357
358 filename -- file name of the source image.
359
360 projection -- Projection object describing the projection which
361 the source image is in.
362
363 visible -- True is the layer should initially be visible.
364
365 Throws IOError if the filename is invalid or points to a file that
366 is not in a format GDAL can use.
367 """
368
369 BaseLayer.__init__(self, title, visible = visible)
370
371 self.projection = projection
372 self.filename = filename
373
374 self.bbox = -1
375
376 if resource.has_gdal_support():
377 #
378 # temporarily open the file so that GDAL can test if it's valid.
379 #
380 dataset = gdal.Open(self.filename, GA_ReadOnly)
381
382 if dataset is None:
383 raise IOError()
384
385 self.UnsetModified()
386
387 def BoundingBox(self):
388 """Return the layer's bounding box in the intrinsic coordinate system.
389
390 If the there is no support for images, or the file cannot
391 be read, or there is no geographics information available, return None.
392 """
393 if not resource.has_gdal_support():
394 return None
395
396 if self.bbox == -1:
397 dataset = gdal.Open(self.filename, GA_ReadOnly)
398 if dataset is None:
399 self.bbox = None
400 else:
401 geotransform = dataset.GetGeoTransform()
402 if geotransform is None:
403 return None
404
405 x = 0
406 y = dataset.RasterYSize
407 left = geotransform[0] + \
408 geotransform[1] * x + \
409 geotransform[2] * y
410
411 bottom = geotransform[3] + \
412 geotransform[4] * x + \
413 geotransform[5] * y
414
415 x = dataset.RasterXSize
416 y = 0
417 right = geotransform[0] + \
418 geotransform[1] * x + \
419 geotransform[2] * y
420
421 top = geotransform[3] + \
422 geotransform[4] * x + \
423 geotransform[5] * y
424
425 self.bbox = (left, bottom, right, top)
426
427 return self.bbox
428
429 def LatLongBoundingBox(self):
430 bbox = self.BoundingBox()
431 if bbox is None:
432 return None
433
434 llx, lly, urx, ury = bbox
435 if self.projection is not None:
436 llx, lly = self.projection.Inverse(llx, lly)
437 urx, ury = self.projection.Inverse(urx, ury)
438
439 return llx, lly, urx, ury
440
441 def GetImageFilename(self):
442 return self.filename
443
444 def TreeInfo(self):
445 items = []
446
447 items.append(_("Filename: %s") % self.GetImageFilename())
448
449 if self.Visible():
450 items.append(_("Shown"))
451 else:
452 items.append(_("Hidden"))
453
454 bbox = self.LatLongBoundingBox()
455 if bbox is not None:
456 items.append(_("Extent (lat-lon): (%g, %g, %g, %g)") % bbox)
457 else:
458 items.append(_("Extent (lat-lon):"))
459
460 if self.projection and len(self.projection.params) > 0:
461 items.append((_("Projection"),
462 [str(param) for param in self.projection.params]))
463
464 return (_("Layer '%s'") % self.Title(), items)
465

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26