/[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 1088 - (show annotations)
Wed May 28 12:42:23 2003 UTC (21 years, 9 months ago) by bh
File MIME type: text/x-python
File size: 15056 byte(s)
* Thuban/Model/layer.py (Layer.SetShapeStore): Set the
classification to "None" if the type of the field has changed.

* test/test_layer.py (SetShapeStoreTests): New. Class with a few
test for the Layer.SetShapeStore method

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 from math import log, ceil
12
13 from Thuban import _
14
15 import shapelib, shptree
16
17 import gdal
18 from gdalconst import GA_ReadOnly
19
20 from messages import LAYER_PROJECTION_CHANGED, LAYER_VISIBILITY_CHANGED, \
21 LAYER_CHANGED
22
23 from color import Color
24
25 import classification
26
27 from base import TitledObject, Modifiable
28
29
30 class Shape:
31
32 """Represent one shape"""
33
34 def __init__(self, points):
35 self.points = points
36 #self.compute_bbox()
37 self.bbox = None
38
39 def compute_bbox(self):
40 if self.bbox is not None:
41 return self.bbox
42
43 xs = []
44 ys = []
45 for x, y in self.points:
46 xs.append(x)
47 ys.append(y)
48 self.llx = min(xs)
49 self.lly = min(ys)
50 self.urx = max(xs)
51 self.ury = max(ys)
52
53 self.bbox = (self.llx, self.lly, self.urx, self.ury)
54
55 return self.bbox
56
57 def Points(self):
58 return self.points
59
60
61
62 # Shape type constants
63 SHAPETYPE_POLYGON = "polygon"
64 SHAPETYPE_ARC = "arc"
65 SHAPETYPE_POINT = "point"
66
67 # mapping from shapelib shapetype constants to our constants
68 shapelib_shapetypes = {shapelib.SHPT_POLYGON: SHAPETYPE_POLYGON,
69 shapelib.SHPT_ARC: SHAPETYPE_ARC,
70 shapelib.SHPT_POINT: SHAPETYPE_POINT}
71
72 shapetype_names = {SHAPETYPE_POINT: "Point",
73 SHAPETYPE_ARC: "Arc",
74 SHAPETYPE_POLYGON: "Polygon"}
75
76 class BaseLayer(TitledObject, Modifiable):
77
78 """Base class for the layers."""
79
80 def __init__(self, title, visible = True, projection = None):
81 """Initialize the layer.
82
83 title -- the title
84 visible -- boolean. If true the layer is visible.
85 """
86 TitledObject.__init__(self, title)
87 Modifiable.__init__(self)
88 self.visible = visible
89 self.projection = projection
90
91 def Visible(self):
92 """Return true if layer is visible"""
93 return self.visible
94
95 def SetVisible(self, visible):
96 """Set the layer's visibility."""
97 self.visible = visible
98 self.issue(LAYER_VISIBILITY_CHANGED, self)
99
100 def HasClassification(self):
101 """Determine if this layer support classifications."""
102 return False
103
104 def GetProjection(self):
105 """Return the layer's projection."""
106 return self.projection
107
108 def SetProjection(self, projection):
109 """Set the layer's projection"""
110 self.projection = projection
111 self.changed(LAYER_PROJECTION_CHANGED, self)
112
113 class Layer(BaseLayer):
114
115 """Represent the information of one geodata file (currently a shapefile)
116
117 All children of the layer have the same type.
118
119 A layer has fill and stroke colors. Colors should be instances of
120 Color. They can also be None, indicating no fill or no stroke.
121
122 The layer objects send the following events, all of which have the
123 layer object as parameter:
124
125 TITLE_CHANGED -- The title has changed.
126 LAYER_PROJECTION_CHANGED -- the projection has changed.
127 """
128
129 def __init__(self, title, data, projection = None,
130 fill = Color.Transparent,
131 stroke = Color.Black,
132 lineWidth = 1,
133 visible = True):
134 """Initialize the layer.
135
136 title -- the title
137 data -- datastore object for the shape data shown by the layer
138 projection -- the projection object. Its Inverse method is
139 assumed to map the layer's coordinates to lat/long
140 coordinates
141 fill -- the fill color or Color.Transparent if the shapes are
142 not filled
143 stroke -- the stroke color or Color.Transparent if the shapes
144 are not stroked
145 visible -- boolean. If true the layer is visible.
146
147 colors are expected to be instances of Color class
148 """
149 BaseLayer.__init__(self, title,
150 visible = visible,
151 projection = projection)
152
153 #
154 # this is really important so that when the classification class
155 # tries to set its parent layer the variable will exist
156 #
157 self.__classification = None
158 self.__setClassLock = False
159
160 self.SetShapeStore(data)
161
162 self.SetClassification(None)
163
164 self.__classification.SetDefaultLineColor(stroke)
165 self.__classification.SetDefaultLineWidth(lineWidth)
166 self.__classification.SetDefaultFill(fill)
167 self.__classification.SetLayer(self)
168
169 self.UnsetModified()
170
171
172 def SetShapeStore(self, store):
173 self.store = store
174 self.shapefile = self.store.Shapefile()
175 self.shapetable = self.store.Table()
176 if hasattr(self.store, "FileName"):
177 self.filename = self.store.FileName()
178 self.table = self.shapetable
179
180 numshapes, shapetype, mins, maxs = self.shapefile.info()
181 self.numshapes = numshapes
182 self.shapetype = shapelib_shapetypes[shapetype]
183
184 # if there are shapes, set the bbox accordingly. Otherwise
185 # set it to None.
186 if self.numshapes:
187 self.bbox = mins[:2] + maxs[:2]
188 else:
189 self.bbox = None
190
191 # estimate a good depth for the quad tree. Each depth
192 # multiplies the number of nodes by four, therefore we
193 # basically take the base 4 logarithm of the number of
194 # shapes.
195 if self.numshapes < 4:
196 maxdepth = 1
197 else:
198 maxdepth = int(ceil(log(self.numshapes / 4.0) / log(4)))
199
200 self.shapetree = shptree.SHPTree(self.shapefile.cobject(), 2,
201 maxdepth)
202 # Set the classification to None if there is a classification
203 # and the new shapestore doesn't have a table with a suitable
204 # column, i.e one with the same name and type as before
205 # FIXME: Maybe we should keep it the same if the type is
206 # compatible enough such as FIELDTYPE_DOUBLE and FIELDTYPE_INT
207 if self.__classification is not None:
208 fieldname = self.__classification.GetField()
209 fieldtype = self.__classification.GetFieldType()
210 table = self.store.Table()
211 if (fieldname is not None
212 and (not table.HasColumn(fieldname)
213 or table.Column(fieldname).type != fieldtype)):
214 self.SetClassification(None)
215 self.changed(LAYER_CHANGED, self)
216
217 def ShapeStore(self):
218 return self.store
219
220 def Destroy(self):
221 BaseLayer.Destroy(self)
222 self.SetClassification(None)
223
224 def BoundingBox(self):
225 """Return the layer's bounding box in the intrinsic coordinate system.
226
227 If the layer has no shapes, return None.
228 """
229 return self.bbox
230
231 def LatLongBoundingBox(self):
232 """Return the layer's bounding box in lat/long coordinates.
233
234 Return None, if the layer doesn't contain any shapes.
235 """
236 bbox = self.BoundingBox()
237 if bbox is not None:
238 llx, lly, urx, ury = bbox
239 if self.projection is not None:
240 llx, lly = self.projection.Inverse(llx, lly)
241 urx, ury = self.projection.Inverse(urx, ury)
242 return llx, lly, urx, ury
243 else:
244 return None
245
246 def ShapesBoundingBox(self, shapes):
247 """Return a bounding box in lat/long coordinates for the given
248 list of shape ids.
249
250 If shapes is None or empty, return None.
251 """
252
253 if shapes is None or len(shapes) == 0: return None
254
255 llx = []
256 lly = []
257 urx = []
258 ury = []
259
260 if self.projection is not None:
261 inverse = lambda x, y: self.projection.Inverse(x, y)
262 else:
263 inverse = lambda x, y: (x, y)
264
265 for id in shapes:
266 left, bottom, right, top = self.Shape(id).compute_bbox()
267
268 left, bottom = inverse(left, bottom)
269 right, top = inverse(right, top)
270
271 llx.append(left)
272 lly.append(bottom)
273 urx.append(right)
274 ury.append(top)
275
276 return (min(llx), min(lly), max(urx), max(ury))
277
278 def GetFieldType(self, fieldName):
279 if self.table.HasColumn(fieldName):
280 return self.table.Column(fieldName).type
281 return None
282
283 def NumShapes(self):
284 """Return the number of shapes in the layer"""
285 return self.numshapes
286
287 def ShapeType(self):
288 """Return the type of the shapes in the layer.
289 This is either SHAPETYPE_POINT, SHAPETYPE_ARC or SHAPETYPE_POLYGON.
290 """
291 return self.shapetype
292
293 def Shape(self, index):
294 """Return the shape with index index"""
295 shape = self.shapefile.read_object(index)
296
297 if self.shapetype == SHAPETYPE_POINT:
298 points = shape.vertices()
299 else:
300 #for poly in shape.vertices():
301 poly = shape.vertices()[0]
302 points = []
303 for x, y in poly:
304 points.append((x, y))
305
306 return Shape(points)
307
308 def ShapesInRegion(self, box):
309 """Return the ids of the shapes that overlap the box.
310
311 Box is a tuple (left, bottom, right, top) in unprojected coordinates.
312 """
313 left, bottom, right, top = box
314
315 if self.projection is not None:
316 left, bottom = self.projection.Forward(left, bottom)
317 right, top = self.projection.Forward(right, top)
318
319 return self.shapetree.find_shapes((left, bottom), (right, top))
320
321 def HasClassification(self):
322 return True
323
324 def GetClassification(self):
325 return self.__classification
326
327 def SetClassification(self, clazz):
328 """Set the classification to 'clazz'
329
330 If 'clazz' is None a default classification is created
331 """
332
333 # prevent infinite recursion when calling SetLayer()
334 if self.__setClassLock: return
335
336 self.__setClassLock = True
337
338 if clazz is None:
339 if self.__classification is not None:
340 self.__classification.SetLayer(None)
341 self.__classification = classification.Classification()
342 else:
343 self.__classification = clazz
344 try:
345 self.__classification.SetLayer(self)
346 except ValueError:
347 self.__setClassLock = False
348 raise ValueError
349
350 self.changed(LAYER_CHANGED, self)
351
352 self.__setClassLock = False
353
354 def ClassChanged(self):
355 """Called from the classification object when it has changed."""
356 self.changed(LAYER_CHANGED, self)
357
358 def TreeInfo(self):
359 items = []
360
361 if hasattr(self, 'filename'):
362 items.append(_("Filename: %s") % self.filename)
363
364 if self.Visible():
365 items.append(_("Shown"))
366 else:
367 items.append(_("Hidden"))
368 items.append(_("Shapes: %d") % self.NumShapes())
369
370 bbox = self.LatLongBoundingBox()
371 if bbox is not None:
372 items.append(_("Extent (lat-lon): (%g, %g, %g, %g)") % bbox)
373 else:
374 items.append(_("Extent (lat-lon):"))
375 items.append(_("Shapetype: %s") % shapetype_names[self.ShapeType()])
376
377 if self.projection and len(self.projection.params) > 0:
378 items.append((_("Projection"),
379 [str(param) for param in self.projection.params]))
380
381 items.append(self.__classification)
382
383 return (_("Layer '%s'") % self.Title(), items)
384
385
386 class RasterLayer(BaseLayer):
387
388 def __init__(self, title, filename, projection = None, visible = True):
389 """Initialize the Raster Layer.
390
391 title -- title for the layer.
392
393 filename -- file name of the source image.
394
395 projection -- Projection object describing the projection which
396 the source image is in.
397
398 visible -- True is the layer should initially be visible.
399
400 Throws IOError if the filename is invalid or points to a file that
401 is not in a format GDAL can use.
402 """
403
404 BaseLayer.__init__(self, title, visible = visible)
405
406 self.projection = projection
407 self.filename = filename
408
409 self.bbox = -1
410
411 #
412 # temporarily open the file so that GDAL can test if it's valid.
413 #
414 dataset = gdal.Open(self.filename, GA_ReadOnly)
415
416 if dataset is None:
417 raise IOError()
418
419 self.UnsetModified()
420
421 def BoundingBox(self):
422 """Return the layer's bounding box in the intrinsic coordinate system.
423
424 If the layer has no shapes, return None.
425 """
426 if self.bbox == -1:
427 dataset = gdal.Open(self.filename, GA_ReadOnly)
428 if dataset is None:
429 self.bbox = None
430 else:
431 geotransform = dataset.GetGeoTransform()
432 if geotransform is None:
433 return None
434
435 x = 0
436 y = dataset.RasterYSize
437 left = geotransform[0] + \
438 geotransform[1] * x + \
439 geotransform[2] * y
440
441 bottom = geotransform[3] + \
442 geotransform[4] * x + \
443 geotransform[5] * y
444
445 x = dataset.RasterXSize
446 y = 0
447 right = geotransform[0] + \
448 geotransform[1] * x + \
449 geotransform[2] * y
450
451 top = geotransform[3] + \
452 geotransform[4] * x + \
453 geotransform[5] * y
454
455 self.bbox = (left, bottom, right, top)
456
457 return self.bbox
458
459 def LatLongBoundingBox(self):
460 bbox = self.BoundingBox()
461 if bbox is None:
462 return None
463
464 llx, lly, urx, ury = bbox
465 if self.projection is not None:
466 llx, lly = self.projection.Inverse(llx, lly)
467 urx, ury = self.projection.Inverse(urx, ury)
468
469 return llx, lly, urx, ury
470
471 def GetImageFilename(self):
472 return self.filename
473
474 def TreeInfo(self):
475 items = []
476
477 if self.Visible():
478 items.append(_("Shown"))
479 else:
480 items.append(_("Hidden"))
481 items.append(_("Shapes: %d") % self.NumShapes())
482
483 bbox = self.LatLongBoundingBox()
484 if bbox is not None:
485 items.append(_("Extent (lat-lon): (%g, %g, %g, %g)") % bbox)
486 else:
487 items.append(_("Extent (lat-lon):"))
488
489 if self.projection and len(self.projection.params) > 0:
490 items.append((_("Projection"),
491 [str(param) for param in self.projection.params]))
492
493 return (_("Layer '%s'") % self.Title(), items)
494

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26