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

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

Parent Directory Parent Directory | Revision Log Revision Log | View Patch Patch

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

Legend:
Removed from v.412  
changed lines
  Added in v.2339

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26