/[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 364 by jonathan, Mon Jan 27 11:47:12 2003 UTC revision 2343 by bernhard, Mon Sep 20 08:13:32 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]>
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 8  Line 10 
10  __version__ = "$Revision$"  __version__ = "$Revision$"
11    
12  import os  import os
13  from math import log, ceil  import warnings
14    
15  import shapelib, shptree  from Thuban import _
16    
17  from messages import LAYER_PROJECTION_CHANGED, LAYER_LEGEND_CHANGED, \  from messages import LAYER_PROJECTION_CHANGED, LAYER_VISIBILITY_CHANGED, \
18       LAYER_VISIBILITY_CHANGED       LAYER_CHANGED, LAYER_SHAPESTORE_REPLACED, CLASS_CHANGED
19    
20  from color import Color  import classification
 # Some predefined colors for internal use  
 _black = Color(0, 0, 0)  
   
 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    
 # 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}  
28    
29  shapetype_names = {SHAPETYPE_POINT: "Point",  shapetype_names = {SHAPETYPE_POINT: "Point",
30                     SHAPETYPE_ARC: "Arc",                     SHAPETYPE_ARC: "Arc",
# Line 67  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 76  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 86  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 supports 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 94  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 = None, stroke = _black, stroke_width = 1, visible = 1):                   fill = Transparent,
92                     stroke = Black,
93                     lineWidth = 1,
94                     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          # Make the filename absolute. The filename will be                                   projection = projection)
113          # interpreted relative to that anyway, but when saving a  
114          # session we need to compare absolute paths and it's usually          self.__classification = None
115          # safer to always work with absolute paths.          self.store = None
116          self.filename = os.path.abspath(filename)  
117            self.SetShapeStore(data)
118    
119            self.classification_column = None
120            self.SetClassificationColumn(None)
121            self.SetClassification(None)
122    
123            self.__classification.SetDefaultLineColor(stroke)
124            self.__classification.SetDefaultLineWidth(lineWidth)
125            self.__classification.SetDefaultFill(fill)
126    
127            self.UnsetModified()
128    
129        def SetShapeStore(self, store):
130            # Set the classification to None if there is a classification
131            # and the new shapestore doesn't have a table with a suitable
132            # column, i.e one with the same name and type as before
133            # FIXME: Maybe we should keep it the same if the type is
134            # compatible enough such as FIELDTYPE_DOUBLE and FIELDTYPE_INT
135            if self.__classification is not None:
136                columnname = self.classification_column
137                columntype = self.GetFieldType(columnname)
138                table = store.Table()
139                if (columnname is not None
140                    and (not table.HasColumn(columnname)
141                         or table.Column(columnname).type != columntype)):
142                    self.SetClassification(None)
143    
144          self.projection = projection          self.store = store
         self.fill = fill  
         self.stroke = stroke  
         self.stroke_width = stroke_width  
         self.shapefile = None  
         self.shapetree = None  
         self.open_shapefile()  
         # shapetable is the table associated with the shapefile, while  
         # table is the default table used to look up attributes for  
         # display  
         self.shapetable = Table(filename)  
         self.table = self.shapetable  
   
         self.classification = Classification()  
         self.classification.setNull(  
             {'stroke':stroke, 'stroke_width':stroke_width, 'fill':fill})  
   
     def open_shapefile(self):  
         if self.shapefile is None:  
             self.shapefile = shapelib.ShapeFile(self.filename)  
             numshapes, shapetype, mins, maxs = self.shapefile.info()  
             self.numshapes = numshapes  
             self.shapetype = shapelib_shapetypes[shapetype]  
   
             # if there are shapes, set the bbox accordinly. Otherwise  
             # set it to None.  
             if self.numshapes:  
                 self.bbox = mins[:2] + maxs[:2]  
             else:  
                 self.bbox = None  
145    
146              # estimate a good depth for the quad tree. Each depth          self.changed(LAYER_SHAPESTORE_REPLACED, self)
             # 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)))  
147    
148              self.shapetree = shptree.SHPTree(self.shapefile.cobject(), 2,      def ShapeStore(self):
149                                               maxdepth)          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 195  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)
222          shape = self.shapefile.read_object(index)  
223        def ShapesInRegion(self, bbox):
224            """Return an iterable over the shapes that overlap the bounding box.
225    
226            The bbox parameter should be the bounding box as a tuple in the
227            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            if column:
245                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 HasClassification(self):
254            return True
255    
256        def GetClassification(self):
257            return self.__classification
258    
259        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
275            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):
285            items = []
286    
287            items.append(_("Filename: %s") % self.ShapeStore().FileName())
288    
289            if self.Visible():
290                items.append(_("Shown"))
291            else:
292                items.append(_("Hidden"))
293            items.append(_("Shapes: %d") % self.NumShapes())
294    
295          if self.shapetype == SHAPETYPE_POINT:          bbox = self.LatLongBoundingBox()
296              points = shape.vertices()          if bbox is not None:
297                items.append(_("Extent (lat-lon): (%g, %g, %g, %g)") % tuple(bbox))
298          else:          else:
299              #for poly in shape.vertices():              items.append(_("Extent (lat-lon):"))
300              poly = shape.vertices()[0]          items.append(_("Shapetype: %s") % shapetype_names[self.ShapeType()])
301              points = []  
302              for x, y in poly:          if self.projection and len(self.projection.params) > 0:
303                  points.append((x, y))              items.append((_("Projection"),
304                            [str(param) for param in self.projection.params]))
305          return Shape(points)  
306            items.append(self.__classification)
307    
308      def ShapesInRegion(self, box):          return (_("Layer '%s'") % self.Title(), items)
         """Return the ids of the shapes that overlap the box.  
309    
310          Box is a tuple (left, bottom, right, top) in the coordinate      def ClipBoundingBox(self, bbox):
311          system used by the layer's shapefile.          """ 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          left, bottom, right, top = box          bminx, bminy, bmaxx, bmaxy = bbox
319          return self.shapetree.find_shapes((left, bottom), (right, top))          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    
     def SetProjection(self, projection):  
         """Set the layer's projection"""  
358          self.projection = projection          self.projection = projection
359          self.changed(LAYER_PROJECTION_CHANGED, self)          self.filename = os.path.abspath(filename)
360    
361      def SetFill(self, fill):          self.bbox = -1
362          """Set the layer's fill color. None means the shapes are not filled"""  
363          self.fill = fill          if resource.has_gdal_support():
364          self.changed(LAYER_LEGEND_CHANGED, self)              #
365                # temporarily open the file so that GDAL can test if it's valid.
366      def SetStroke(self, stroke):              #
367          """Set the layer's stroke color. None means the shapes are not              dataset = gdal.Open(self.filename, GA_ReadOnly)
368          stroked."""  
369          self.stroke = stroke              if dataset is None:
370          self.changed(LAYER_LEGEND_CHANGED, self)                  raise IOError()
371    
372      def SetStrokeWidth(self, width):          self.UnsetModified()
373          """Set the layer's stroke width."""  
374          self.stroke_width = width      def BoundingBox(self):
375          self.changed(LAYER_LEGEND_CHANGED, self)          """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):      def TreeInfo(self):
430          items = []          items = []
431    
432            items.append(_("Filename: %s") % self.GetImageFilename())
433    
434          if self.Visible():          if self.Visible():
435              items.append("Shown")              items.append(_("Shown"))
436          else:          else:
437              items.append("Hidden")              items.append(_("Hidden"))
         items.append("Shapes: %d" % self.NumShapes())  
438    
439          bbox = self.LatLongBoundingBox()          bbox = self.LatLongBoundingBox()
440          if bbox is not None:          if bbox is not None:
441              items.append("Extent (lat-lon): (%g, %g, %g, %g)" % bbox)              items.append(_("Extent (lat-lon): (%g, %g, %g, %g)") % bbox)
442          else:          else:
443              items.append("Extent (lat-lon):")              items.append(_("Extent (lat-lon):"))
444          items.append("Shapetype: %s" % shapetype_names[self.ShapeType()])  
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          def color_string(color):          return (_("Layer '%s'") % self.Title(), items)
             if color is None:  
                 return "None"  
             return "(%.3f, %.3f, %.3f)" % (color.red, color.green, color.blue)  
         items.append("Fill: " + color_string(self.fill))  
         items.append("Outline: " + color_string(self.stroke))  
450    
         return ("Layer '%s'" % self.Title(), items)  

Legend:
Removed from v.364  
changed lines
  Added in v.2343

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26