/[thuban]/branches/WIP-pyshapelib-bramz/Thuban/Model/layer.py
ViewVC logotype

Contents of /branches/WIP-pyshapelib-bramz/Thuban/Model/layer.py

Parent Directory Parent Directory | Revision Log Revision Log


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

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26