/[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 1158 - (show annotations)
Thu Jun 12 12:40:11 2003 UTC (21 years, 8 months ago) by jonathan
File MIME type: text/x-python
File size: 15387 byte(s)
Import gdal only if it available.
(RasterLayer): Handle the case where the gdal library is unavailable.
        Addresses RTbug #1877.

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

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26