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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 1142 - (hide annotations)
Tue Jun 10 09:41:57 2003 UTC (21 years, 8 months ago) by bh
File MIME type: text/x-python
File size: 15199 byte(s)
* Thuban/Model/messages.py (LAYER_SHAPESTORE_REPLACED): New
message.

* Thuban/Model/layer.py (Layer.SetShapeStore): Send
LAYER_SHAPESTORE_REPLACED when the shapestore changes. A
LAYER_CHANGED will still be sent if the classification changes.

* Thuban/UI/classifier.py (Classifier.__init__): Add the map as
parameter so we can subscribe to some of its messages
(Classifier.__init__): Subscribe to the map's MAP_LAYERS_REMOVED
and the layer's LAYER_SHAPESTORE_REPLACED
(Classifier.unsubscribe_messages): New. Unsubscribe from message
subscribed to in __init__
(Classifier.map_layers_removed)
(Classifier.layer_shapestore_replaced): receivers for the messages
subscribed to in __init__. Unsubscribe and close the dialog

* Thuban/UI/mainwindow.py (MainWindow.OpenLayerProperties): Pass
the map to the Classifier dialog

* test/test_layer.py (SetShapeStoreTests): Derive from
SubscriberMixin as well so we can test messages
(SetShapeStoreTests.setUp): Subscribe to some of the layer's
messages
(SetShapeStoreTests.tearDown): Clear the messages again
(SetShapeStoreTests.test_sanity): Expand the doc-string and check
for the modified flag too
(SetShapeStoreTests.test_set_shape_store_modified_flag): New test
to check whether SetShapeStore sets the modified flag
(SetShapeStoreTests.test_set_shape_store_different_field_name)
(SetShapeStoreTests.test_set_shape_store_same_field)
(SetShapeStoreTests.test_set_shape_store_same_field_different_type):
Add tests for the messages. This checks both the new
LAYER_SHAPESTORE_REPLACED and the older LAYER_CHANGED

1 bh 701 # Copyright (c) 2001, 2002, 2003 by Intevation GmbH
2 bh 6 # Authors:
3     # Bernhard Herzog <[email protected]>
4 jonathan 412 # Jonathan Coles <[email protected]>
5 bh 6 #
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 bh 171 from math import log, ceil
12    
13 jan 374 from Thuban import _
14    
15 bh 143 import shapelib, shptree
16 bh 6
17 jonathan 929 import gdal
18     from gdalconst import GA_ReadOnly
19    
20 bh 701 from messages import LAYER_PROJECTION_CHANGED, LAYER_VISIBILITY_CHANGED, \
21 bh 1142 LAYER_CHANGED, LAYER_SHAPESTORE_REPLACED
22 bh 6
23     from color import Color
24    
25 jonathan 437 import classification
26 bh 6
27     from base import TitledObject, Modifiable
28    
29 bh 723
30 bh 6 class Shape:
31    
32     """Represent one shape"""
33    
34     def __init__(self, points):
35     self.points = points
36     #self.compute_bbox()
37 jonathan 828 self.bbox = None
38 bh 6
39     def compute_bbox(self):
40 jonathan 828 if self.bbox is not None:
41     return self.bbox
42    
43 bh 6 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 jonathan 828 self.bbox = (self.llx, self.lly, self.urx, self.ury)
54    
55     return self.bbox
56    
57 bh 6 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 jonathan 929 def __init__(self, title, visible = True, projection = None):
81 bh 6 """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 jonathan 929 self.projection = projection
90 bh 6
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 bh 276
100 jonathan 929 def HasClassification(self):
101     """Determine if this layer support classifications."""
102     return False
103 bh 276
104 jonathan 929 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 bh 6 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 bh 723 def __init__(self, title, data, projection = None,
130 jonathan 610 fill = Color.Transparent,
131 jonathan 412 stroke = Color.Black,
132 jonathan 464 lineWidth = 1,
133 jonathan 771 visible = True):
134 bh 6 """Initialize the layer.
135    
136     title -- the title
137 bh 723 data -- datastore object for the shape data shown by the layer
138 bh 6 projection -- the projection object. Its Inverse method is
139     assumed to map the layer's coordinates to lat/long
140     coordinates
141 jonathan 610 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 bh 6 visible -- boolean. If true the layer is visible.
146    
147     colors are expected to be instances of Color class
148     """
149 jonathan 929 BaseLayer.__init__(self, title,
150     visible = visible,
151     projection = projection)
152 bh 276
153 jonathan 481 #
154     # this is really important so that when the classification class
155     # tries to set its parent layer the variable will exist
156     #
157 bh 723 self.__classification = None
158 jonathan 529 self.__setClassLock = False
159 jonathan 481
160 bh 723 self.SetShapeStore(data)
161 jonathan 492
162     self.SetClassification(None)
163    
164 jonathan 464 self.__classification.SetDefaultLineColor(stroke)
165     self.__classification.SetDefaultLineWidth(lineWidth)
166 jonathan 412 self.__classification.SetDefaultFill(fill)
167 jonathan 492 self.__classification.SetLayer(self)
168 jonathan 364
169 jonathan 389 self.UnsetModified()
170    
171 bh 6
172 bh 723 def SetShapeStore(self, store):
173     self.store = store
174     self.shapefile = self.store.Shapefile()
175     self.shapetable = self.store.Table()
176 bh 996 if hasattr(self.store, "FileName"):
177     self.filename = self.store.FileName()
178 bh 723 self.table = self.shapetable
179 bh 179
180 bh 723 numshapes, shapetype, mins, maxs = self.shapefile.info()
181     self.numshapes = numshapes
182     self.shapetype = shapelib_shapetypes[shapetype]
183 bh 171
184 bh 723 # 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 bh 171
191 bh 723 # 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 bh 1088 # 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 bh 723 if self.__classification is not None:
208     fieldname = self.__classification.GetField()
209 bh 1088 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 bh 723 self.SetClassification(None)
215 bh 1142 self.changed(LAYER_SHAPESTORE_REPLACED, self)
216 bh 723
217     def ShapeStore(self):
218     return self.store
219    
220 bh 258 def Destroy(self):
221 bh 260 BaseLayer.Destroy(self)
222 jonathan 529 self.SetClassification(None)
223 bh 1117 self.store = self.shapetree = None
224     self.table = self.shapefile = self.shapetable = None
225 bh 258
226 bh 6 def BoundingBox(self):
227 bh 179 """Return the layer's bounding box in the intrinsic coordinate system.
228    
229     If the layer has no shapes, return None.
230     """
231 bh 6 return self.bbox
232    
233     def LatLongBoundingBox(self):
234 bh 179 """Return the layer's bounding box in lat/long coordinates.
235 bh 6
236 bh 179 Return None, if the layer doesn't contain any shapes.
237     """
238     bbox = self.BoundingBox()
239     if bbox is not None:
240     llx, lly, urx, ury = bbox
241     if self.projection is not None:
242     llx, lly = self.projection.Inverse(llx, lly)
243     urx, ury = self.projection.Inverse(urx, ury)
244     return llx, lly, urx, ury
245     else:
246     return None
247    
248 jonathan 828 def ShapesBoundingBox(self, shapes):
249     """Return a bounding box in lat/long coordinates for the given
250     list of shape ids.
251    
252     If shapes is None or empty, return None.
253     """
254    
255     if shapes is None or len(shapes) == 0: return None
256    
257     llx = []
258     lly = []
259     urx = []
260     ury = []
261    
262     if self.projection is not None:
263     inverse = lambda x, y: self.projection.Inverse(x, y)
264     else:
265     inverse = lambda x, y: (x, y)
266    
267     for id in shapes:
268     left, bottom, right, top = self.Shape(id).compute_bbox()
269    
270     left, bottom = inverse(left, bottom)
271     right, top = inverse(right, top)
272    
273     llx.append(left)
274     lly.append(bottom)
275     urx.append(right)
276     ury.append(top)
277    
278     return (min(llx), min(lly), max(urx), max(ury))
279    
280 jonathan 464 def GetFieldType(self, fieldName):
281 bh 839 if self.table.HasColumn(fieldName):
282     return self.table.Column(fieldName).type
283     return None
284 jonathan 464
285 bh 6 def NumShapes(self):
286     """Return the number of shapes in the layer"""
287     return self.numshapes
288    
289     def ShapeType(self):
290     """Return the type of the shapes in the layer.
291     This is either SHAPETYPE_POINT, SHAPETYPE_ARC or SHAPETYPE_POLYGON.
292     """
293     return self.shapetype
294    
295     def Shape(self, index):
296     """Return the shape with index index"""
297     shape = self.shapefile.read_object(index)
298 jonathan 364
299 bh 6 if self.shapetype == SHAPETYPE_POINT:
300     points = shape.vertices()
301     else:
302     #for poly in shape.vertices():
303     poly = shape.vertices()[0]
304     points = []
305     for x, y in poly:
306 bh 82 points.append((x, y))
307 jonathan 364
308 bh 6 return Shape(points)
309    
310 bh 143 def ShapesInRegion(self, box):
311     """Return the ids of the shapes that overlap the box.
312    
313 jonathan 794 Box is a tuple (left, bottom, right, top) in unprojected coordinates.
314 bh 143 """
315     left, bottom, right, top = box
316 jonathan 794
317     if self.projection is not None:
318     left, bottom = self.projection.Forward(left, bottom)
319     right, top = self.projection.Forward(right, top)
320    
321 bh 147 return self.shapetree.find_shapes((left, bottom), (right, top))
322 bh 143
323 jonathan 929 def HasClassification(self):
324     return True
325 jonathan 725
326 jonathan 412 def GetClassification(self):
327     return self.__classification
328    
329     def SetClassification(self, clazz):
330 jonathan 492 """Set the classification to 'clazz'
331 jonathan 481
332 jonathan 492 If 'clazz' is None a default classification is created
333     """
334 jonathan 481
335 jonathan 529 # prevent infinite recursion when calling SetLayer()
336     if self.__setClassLock: return
337    
338     self.__setClassLock = True
339    
340 jonathan 492 if clazz is None:
341 jonathan 529 if self.__classification is not None:
342     self.__classification.SetLayer(None)
343 jonathan 492 self.__classification = classification.Classification()
344     else:
345     self.__classification = clazz
346 jonathan 529 try:
347     self.__classification.SetLayer(self)
348     except ValueError:
349     self.__setClassLock = False
350     raise ValueError
351 jonathan 492
352 jonathan 545 self.changed(LAYER_CHANGED, self)
353 jonathan 412
354 jonathan 529 self.__setClassLock = False
355    
356 jonathan 492 def ClassChanged(self):
357     """Called from the classification object when it has changed."""
358 jonathan 558 self.changed(LAYER_CHANGED, self)
359 jonathan 492
360 bh 217 def TreeInfo(self):
361     items = []
362    
363 jan 1086 if hasattr(self, 'filename'):
364     items.append(_("Filename: %s") % self.filename)
365 jan 1012
366 bh 217 if self.Visible():
367 jan 374 items.append(_("Shown"))
368 bh 217 else:
369 jan 374 items.append(_("Hidden"))
370     items.append(_("Shapes: %d") % self.NumShapes())
371 bh 217
372     bbox = self.LatLongBoundingBox()
373     if bbox is not None:
374 jan 374 items.append(_("Extent (lat-lon): (%g, %g, %g, %g)") % bbox)
375 bh 217 else:
376 jan 374 items.append(_("Extent (lat-lon):"))
377     items.append(_("Shapetype: %s") % shapetype_names[self.ShapeType()])
378 bh 217
379 jonathan 736 if self.projection and len(self.projection.params) > 0:
380     items.append((_("Projection"),
381     [str(param) for param in self.projection.params]))
382    
383 jonathan 412 items.append(self.__classification)
384 bh 217
385 jan 374 return (_("Layer '%s'") % self.Title(), items)
386 jonathan 382
387 jonathan 736
388 jonathan 929 class RasterLayer(BaseLayer):
389    
390     def __init__(self, title, filename, projection = None, visible = True):
391     """Initialize the Raster Layer.
392    
393     title -- title for the layer.
394    
395     filename -- file name of the source image.
396    
397     projection -- Projection object describing the projection which
398     the source image is in.
399    
400     visible -- True is the layer should initially be visible.
401 jonathan 961
402     Throws IOError if the filename is invalid or points to a file that
403     is not in a format GDAL can use.
404 jonathan 929 """
405    
406     BaseLayer.__init__(self, title, visible = visible)
407    
408     self.projection = projection
409     self.filename = filename
410    
411     self.bbox = -1
412    
413 jonathan 961 #
414     # temporarily open the file so that GDAL can test if it's valid.
415     #
416     dataset = gdal.Open(self.filename, GA_ReadOnly)
417    
418     if dataset is None:
419     raise IOError()
420    
421 jonathan 929 self.UnsetModified()
422    
423     def BoundingBox(self):
424     """Return the layer's bounding box in the intrinsic coordinate system.
425    
426     If the layer has no shapes, return None.
427     """
428     if self.bbox == -1:
429     dataset = gdal.Open(self.filename, GA_ReadOnly)
430     if dataset is None:
431     self.bbox = None
432     else:
433 jonathan 961 geotransform = dataset.GetGeoTransform()
434     if geotransform is None:
435     return None
436    
437     x = 0
438     y = dataset.RasterYSize
439     left = geotransform[0] + \
440     geotransform[1] * x + \
441     geotransform[2] * y
442    
443     bottom = geotransform[3] + \
444     geotransform[4] * x + \
445     geotransform[5] * y
446    
447     x = dataset.RasterXSize
448     y = 0
449     right = geotransform[0] + \
450     geotransform[1] * x + \
451     geotransform[2] * y
452    
453     top = geotransform[3] + \
454     geotransform[4] * x + \
455     geotransform[5] * y
456    
457 jonathan 929 self.bbox = (left, bottom, right, top)
458    
459     return self.bbox
460    
461     def LatLongBoundingBox(self):
462     bbox = self.BoundingBox()
463 jonathan 961 if bbox is None:
464 jonathan 929 return None
465    
466 jonathan 961 llx, lly, urx, ury = bbox
467     if self.projection is not None:
468     llx, lly = self.projection.Inverse(llx, lly)
469     urx, ury = self.projection.Inverse(urx, ury)
470    
471     return llx, lly, urx, ury
472    
473 jonathan 929 def GetImageFilename(self):
474     return self.filename
475    
476     def TreeInfo(self):
477     items = []
478    
479     if self.Visible():
480     items.append(_("Shown"))
481     else:
482     items.append(_("Hidden"))
483     items.append(_("Shapes: %d") % self.NumShapes())
484    
485     bbox = self.LatLongBoundingBox()
486     if bbox is not None:
487     items.append(_("Extent (lat-lon): (%g, %g, %g, %g)") % bbox)
488     else:
489     items.append(_("Extent (lat-lon):"))
490    
491     if self.projection and len(self.projection.params) > 0:
492     items.append((_("Projection"),
493     [str(param) for param in self.projection.params]))
494    
495     return (_("Layer '%s'") % self.Title(), items)
496    

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26