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

Legend:
Removed from v.276  
changed lines
  Added in v.1983

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26