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

Diff of /branches/WIP-pyshapelib-bramz/Thuban/Model/classification.py

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

revision 1249 by jonathan, Fri Jun 20 09:27:19 2003 UTC revision 2657 by jan, Wed Jul 27 21:49:25 2005 UTC
# Line 1  Line 1 
1  # Copyright (c) 2001, 2003 by Intevation GmbH  # Copyright (c) 2001, 2003, 2005 by Intevation GmbH
2  # Authors:  # Authors:
3  # Jonathan Coles <[email protected]>  # Jonathan Coles <[email protected]>
4    # Jan-Oliver Wagner <[email protected]> (2005)
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 27  from Thuban import _ Line 28  from Thuban import _
28  from messages import \  from messages import \
29      LAYER_PROJECTION_CHANGED, \      LAYER_PROJECTION_CHANGED, \
30      LAYER_LEGEND_CHANGED, \      LAYER_LEGEND_CHANGED, \
31      LAYER_VISIBILITY_CHANGED      LAYER_VISIBILITY_CHANGED,\
32        CLASS_CHANGED
33    
34  from Thuban.Model.color import Color  from Thuban.Model.color import Color, Transparent, Black
35  from Thuban.Model.range import Range  from Thuban.Model.range import Range
36    
37  import Thuban.Model.layer  import Thuban.Model.layer
38    
39  class Classification:  from Thuban.Lib.connector import Publisher
40    
41    class Classification(Publisher):
42      """Encapsulates the classification of layer.      """Encapsulates the classification of layer.
43            
44      The Classification divides some kind of data into Groups which      The Classification divides some kind of data into Groups which
# Line 42  class Classification: Line 46  class Classification:
46      retrieved by matching data values to the appropriate group.      retrieved by matching data values to the appropriate group.
47      """      """
48    
49      def __init__(self, layer = None, field = None):      def __init__(self):
50          """Initialize a classification.          """Initialize a classification."""
   
         layer -- the Layer object who owns this classification  
   
         field -- the name of the data table field that  
                  is to be used to classify layer properties  
         """  
51    
         self.layer = None  
         self.field = None  
         self.fieldType = None  
52          self.__groups = []          self.__groups = []
53    
         self.__setLayerLock = False  
   
54          self.SetDefaultGroup(ClassGroupDefault())          self.SetDefaultGroup(ClassGroupDefault())
55    
         self.SetLayer(layer)  
         self.SetField(field)  
   
56      def __iter__(self):      def __iter__(self):
57          return ClassIterator(self.__groups)          return ClassIterator(self.__groups)
58    
59      def __deepcopy__(self, memo):      def __deepcopy__(self, memo):
60          clazz = Classification()          clazz = Classification()
61    
         # note: the only thing that isn't copied is the layer reference  
         clazz.field = self.field  
         clazz.fieldType = self.fieldType  
62          clazz.__groups[0] = copy.deepcopy(self.__groups[0])          clazz.__groups[0] = copy.deepcopy(self.__groups[0])
63    
64          for i in range(1, len(self.__groups)):          for i in range(1, len(self.__groups)):
# Line 81  class Classification: Line 68  class Classification:
68    
69      def __SendNotification(self):      def __SendNotification(self):
70          """Notify the layer that this class has changed."""          """Notify the layer that this class has changed."""
71          if self.layer is not None:          self.issue(CLASS_CHANGED)
             self.layer.ClassChanged()  
       
     def SetField(self, field):  
         """Set the name of the data table field to use.  
           
         If there is no layer then the field type is set to None,  
         otherwise the layer is queried to find the type of the  
         field data  
   
         field -- if None then all values map to the default data  
         """  
   
         if field == "":  
             field = None  
   
   
         if field is None:  
             if self.layer is not None:  
                 self.fieldType = None  
         else:  
             if self.layer is not None:  
                 fieldType = self.layer.GetFieldType(field)  
                 if fieldType is None:  
                     raise ValueError("'%s' was not found in the layer's table."  
                                      % self.field)  
   
                 #  
                 # unfortunately we cannot call SetFieldType() because it  
                 # requires the layer to be None  
                 #  
                 self.fieldType = fieldType  
                 #self.SetFieldType(fieldType)  
   
         self.field = field  
   
         self.__SendNotification()  
   
     def GetField(self):  
         """Return the name of the field."""  
         return self.field  
   
     def GetFieldType(self):  
         """Return the field type."""  
         return self.fieldType  
   
     def SetFieldType(self, type):  
         """Set the type of the field used by this classification.  
72    
73          A ValueError is raised if the owning layer is not None and      def __getattr__(self, attr):
74          'type' is different from the current field type.          """Generate the compiled classification on demand"""
75          """          if attr == "_compiled_classification":
76                self._compile_classification()
77          if type != self.fieldType:              return self._compiled_classification
78              if self.layer is not None:          raise AttributeError(attr)
79                  raise ValueError()  
80              else:      def _compile_classification(self):
81                  self.fieldType = type          """Generate the compiled classification
82                  self.__SendNotification()  
83            The compiled classification is a more compact representation of
84      def SetLayer(self, layer):          the classification groups that is also more efficient for
85          """Set the owning Layer of this classification.          performing the classification.
86    
87          A ValueError exception will be thrown either the field or          The compiled classification is a list of tuples. The first
88          field type mismatch the information in the layer's table.          element of the tuple is a string which describes the rest of the
89          """          tuple. There are two kinds of tuples:
90    
91          # prevent infinite recursion when calling SetClassification()            'singletons'
92          if self.__setLayerLock: return  
93                The second element of the tuple is a dictionary which
94          self.__setLayerLock = True              combines several consecutive ClassGroupSingleton instances.
95                The dictionary maps the values of the singletons (as
96          if layer is None:              returned by the GetValue() method) to the corresponding
97              if self.layer is not None:              group.
98                  l = self.layer  
99                  self.layer = None            'range'
100                  l.SetClassification(None)  
101          else:              The tuple describes a ClassGroupRange instance. The tuples
102              assert isinstance(layer, Thuban.Model.layer.Layer)              second element is a tuple fo the form (lfunc, min, max,
103                rfunc, group) where group is the original group object,
104              old_layer = self.layer              lfunc and rfuct are comparison functions and min and max are
105                lower and upper bounds of the range. Given a value and such
106              self.layer = layer              a tuple the group matches if and only if
107    
108              try:                  lfunc(min, value) and rfunc(max, value)
109                  self.SetField(self.GetField()) # this sync's the fieldType  
110              except ValueError:              is true.
111                  self.layer = old_layer  
112                  self.__setLayerLock = False          The compiled classification is bound to
113                  raise ValueError          self._compile_classification.
114            """
115            compiled = []
116            for group in self.__groups[1:]:
117                if isinstance(group, ClassGroupSingleton):
118                    if not compiled or compiled[-1][0] != "singletons":
119                        compiled.append(("singletons", {}))
120                    compiled[-1][1].setdefault(group.GetValue(), group)
121                elif isinstance(group, ClassGroupRange):
122                    left, min, max, right = group.GetRangeTuple()
123                    if left == "[":
124                        lfunc = operator.le
125                    elif left == "]":
126                        lfunc = operator.lt
127                    if right == "[":
128                        rfunc = operator.gt
129                    elif right == "]":
130                        rfunc = operator.ge
131                    compiled.append(("range", (lfunc, min, max, rfunc, group)))
132              else:              else:
133                  self.layer.SetClassification(self)                  raise TypeError("Unknown group type %s", group)
134            self._compiled_classification = compiled
135    
136          self.__setLayerLock = False      def _clear_compiled_classification(self):
137            """Reset the compiled classification.
138    
139      def GetLayer(self):          If will be created on demand when self._compiled_classification
140          """Return the parent layer."""          is accessed again.
         return self.layer  
141    
142            Call this method whenever self.__groups is modified.
143            """
144            try:
145                del self._compiled_classification
146            except:
147                pass
148    
149      #      #
150      # these SetDefault* methods are really only provided for      # these SetDefault* methods are really only provided for
# Line 238  class Classification: Line 203  class Classification:
203    
204          group -- group that the value maps to.          group -- group that the value maps to.
205          """          """
   
206          assert isinstance(group, ClassGroupDefault)          assert isinstance(group, ClassGroupDefault)
207          if len(self.__groups) > 0:          if len(self.__groups) > 0:
208              self.__groups[0] = group              self.__groups[0] = group
209          else:          else:
210              self.__groups.append(group)              self.__groups.append(group)
211            self.__SendNotification()
212    
213      def GetDefaultGroup(self):      def GetDefaultGroup(self):
214          """Return the default group."""          """Return the default group."""
# Line 258  class Classification: Line 223  class Classification:
223          self.InsertGroup(self.GetNumGroups(), item)          self.InsertGroup(self.GetNumGroups(), item)
224    
225      def InsertGroup(self, index, group):      def InsertGroup(self, index, group):
           
226          assert isinstance(group, ClassGroup)          assert isinstance(group, ClassGroup)
   
227          self.__groups.insert(index + 1, group)          self.__groups.insert(index + 1, group)
228            self._clear_compiled_classification()
229          self.__SendNotification()          self.__SendNotification()
230    
231      def RemoveGroup(self, index):      def RemoveGroup(self, index):
232          return self.__groups.pop(index + 1)          """Remove the classification group with the given index"""
233            self.__groups.pop(index + 1)
234            self._clear_compiled_classification()
235            self.__SendNotification()
236    
237      def ReplaceGroup(self, index, group):      def ReplaceGroup(self, index, group):
238          assert isinstance(group, ClassGroup)          assert isinstance(group, ClassGroup)
   
239          self.__groups[index + 1] = group          self.__groups[index + 1] = group
240            self._clear_compiled_classification()
241          self.__SendNotification()          self.__SendNotification()
242    
243      def GetGroup(self, index):      def GetGroup(self, index):
# Line 282  class Classification: Line 247  class Classification:
247          """Return the number of non-default groups in the classification."""          """Return the number of non-default groups in the classification."""
248          return len(self.__groups) - 1          return len(self.__groups) - 1
249    
   
250      def FindGroup(self, value):      def FindGroup(self, value):
251          """Return the associated group, or the default group.          """Return the group that matches the value.
252    
253          Groups are checked in the order the were added to the          Groups are effectively checked in the order the were added to
254          Classification.          the Classification.
255    
256          value -- the value to classify. If there is no mapping,          value -- the value to classify. If there is no mapping or value
257                   the field is None or value is None,                   is None, return the default properties
                  return the default properties  
258          """          """
259    
260          if self.GetField() is not None and value is not None:          if value is not None:
261                for typ, params in self._compiled_classification:
262              for i in range(1, len(self.__groups)):                  if typ == "singletons":
263                  group = self.__groups[i]                      group = params.get(value)
264                  if group.Matches(value):                      if group is not None:
265                      return group                          return group
266                    elif typ == "range":
267                        lfunc, min, max, rfunc, g = params
268                        if lfunc(min, value) and rfunc(max, value):
269                            return g
270    
271          return self.GetDefaultGroup()          return self.GetDefaultGroup()
272    
# Line 321  class Classification: Line 288  class Classification:
288          items = []          items = []
289    
290          def build_color_item(text, color):          def build_color_item(text, color):
291              if color is Color.Transparent:              if color is Transparent:
292                  return ("%s: %s" % (text, _("None")), None)                  return ("%s: %s" % (text, _("None")), None)
293    
294              return ("%s: (%.3f, %.3f, %.3f)" %              return ("%s: (%.3f, %.3f, %.3f)" %
# Line 341  class Classification: Line 308  class Classification:
308              i.append(build_color_item(_("Line Color"), v))              i.append(build_color_item(_("Line Color"), v))
309              v = props.GetLineWidth()              v = props.GetLineWidth()
310              i.append(_("Line Width: %s") % v)              i.append(_("Line Width: %s") % v)
311    
312                # Note: Size is owned by all properties, so
313                # a size will also appear where it does not
314                # make sense like for lines and polygons.
315                v = props.GetSize()
316                i.append(_("Size: %s") % v)
317    
318              v = props.GetFill()              v = props.GetFill()
319              i.append(build_color_item(_("Fill"), v))              i.append(build_color_item(_("Fill"), v))
320              return (label, i)              return (label, i)
# Line 348  class Classification: Line 322  class Classification:
322          for p in self:          for p in self:
323              items.append(build_item(p, p.GetDisplayText()))              items.append(build_item(p, p.GetDisplayText()))
324    
 #           if isinstance(p, ClassGroupDefault):  
 #               items.append(build_item(self.GetDefaultGroup(), _("'DEFAULT'")))  
 #           elif isinstance(p, ClassGroupSingleton):  
 #               items.append(build_item(p, str(p.GetValue())))  
 #           elif isinstance(p, ClassGroupRange):  
 #               items.append(build_item(p, "%s - %s" %  
 #                                          (p.GetMin(), p.GetMax())))  
   
325          return (_("Classification"), items)          return (_("Classification"), items)
326    
327  class ClassIterator:  class ClassIterator:
328      """Allows the Groups in a Classifcation to be interated over.      """Allows the Groups in a Classifcation to be interated over.
329    
# Line 369  class ClassIterator: Line 335  class ClassIterator:
335          """Constructor.          """Constructor.
336    
337          default -- the default group          default -- the default group
338    
339          points -- a list of singleton groups          points -- a list of singleton groups
340    
341          ranges -- a list of range groups          ranges -- a list of range groups
342    
343          maps -- a list of map groups          maps -- a list of map groups
344          """          """
345    
346          self.data = data #[default, points, ranges, maps]          self.data = data
347          self.data_index = 0          self.data_index = 0
         #self.data_iter = iter(self.data)  
         #self.iter = None  
348    
349      def __iter__(self):      def __iter__(self):
350          return self          return self
# Line 394  class ClassIterator: Line 358  class ClassIterator:
358              d = self.data[self.data_index]              d = self.data[self.data_index]
359              self.data_index += 1              self.data_index += 1
360              return d              return d
361            
 #       if self.iter is None:  
 #           try:  
 #               self.data_item = self.data_iter.next()  
 #               self.iter = iter(self.data_item)  
 #           except TypeError:  
 #               return self.data_item  
   
 #       try:  
 #           return self.iter.next()  
 #       except StopIteration:  
 #           self.iter = None  
 #           return self.next()  
         
362  class ClassGroupProperties:  class ClassGroupProperties:
363      """Represents the properties of a single Classification Group.      """Represents the properties of a single Classification Group.
364      
365      These are used when rendering a layer."""      These are used when rendering a layer."""
366    
367        # TODO: Actually, size is only relevant for point objects.
368        # Eventually it should be spearated, e.g. when introducing symbols.
369    
370      def __init__(self, props = None):      def __init__(self, props = None):
371          """Constructor.          """Constructor.
372    
373          props -- a ClassGroupProperties object. The class is copied if          props -- a ClassGroupProperties object. The class is copied if
374                   prop is not None. Otherwise, a default set of properties                   prop is not None. Otherwise, a default set of properties
375                   is created such that: line color = Color.Black, line width = 1,                   is created such that: line color = Black, line width = 1,
376                   and fill color = Color.Transparent                   size = 5 and fill color = Transparent
377          """          """
378    
         #self.stroke = None  
         #self.strokeWidth = 0  
         #self.fill = None  
   
379          if props is not None:          if props is not None:
380              self.SetProperties(props)              self.SetProperties(props)
381          else:          else:
382              self.SetLineColor(Color.Black)              self.SetLineColor(Black)
383              self.SetLineWidth(1)              self.SetLineWidth(1)
384              self.SetFill(Color.Transparent)              self.SetSize(5)
385                self.SetFill(Transparent)
386    
387      def SetProperties(self, props):      def SetProperties(self, props):
388          """Set this class's properties to those in class props."""          """Set this class's properties to those in class props."""
# Line 439  class ClassGroupProperties: Line 390  class ClassGroupProperties:
390          assert isinstance(props, ClassGroupProperties)          assert isinstance(props, ClassGroupProperties)
391          self.SetLineColor(props.GetLineColor())          self.SetLineColor(props.GetLineColor())
392          self.SetLineWidth(props.GetLineWidth())          self.SetLineWidth(props.GetLineWidth())
393            self.SetSize(props.GetSize())
394          self.SetFill(props.GetFill())          self.SetFill(props.GetFill())
395            
396      def GetLineColor(self):      def GetLineColor(self):
397          """Return the line color as a Color object."""          """Return the line color as a Color object."""
398          return self.__stroke          return self.__stroke
# Line 468  class ClassGroupProperties: Line 420  class ClassGroupProperties:
420    
421          self.__strokeWidth = lineWidth          self.__strokeWidth = lineWidth
422    
423        def GetSize(self):
424            """Return the size."""
425            return self.__size
426    
427        def SetSize(self, size):
428            """Set the size.
429    
430            size -- the new size. This must be > 0.
431            """
432            assert isinstance(size, types.IntType)
433            if (size < 1):
434                raise ValueError(_("size < 1"))
435    
436            self.__size = size
437    
438      def GetFill(self):      def GetFill(self):
439          """Return the fill color as a Color object."""          """Return the fill color as a Color object."""
440          return self.__fill          return self.__fill
441    
442      def SetFill(self, fill):      def SetFill(self, fill):
443          """Set the fill color.          """Set the fill color.
444    
# Line 492  class ClassGroupProperties: Line 459  class ClassGroupProperties:
459                   self.__stroke == other.__stroke)        \                   self.__stroke == other.__stroke)        \
460              and (self.__fill is other.__fill or          \              and (self.__fill is other.__fill or          \
461                   self.__fill == other.__fill)            \                   self.__fill == other.__fill)            \
462              and self.__strokeWidth == other.__strokeWidth              and self.__strokeWidth == other.__strokeWidth\
463                and self.__size == other.__size
464    
465      def __ne__(self, other):      def __ne__(self, other):
466          return not self.__eq__(other)          return not self.__eq__(other)
# Line 504  class ClassGroupProperties: Line 472  class ClassGroupProperties:
472          return ClassGroupProperties(self)          return ClassGroupProperties(self)
473    
474      def __repr__(self):      def __repr__(self):
475          return repr((self.__stroke, self.__strokeWidth, self.__fill))          return repr((self.__stroke, self.__strokeWidth, self.__size,
476                        self.__fill))
477    
478  class ClassGroup:  class ClassGroup:
479      """A base class for all Groups within a Classification"""      """A base class for all Groups within a Classification"""
# Line 527  class ClassGroup: Line 496  class ClassGroup:
496      def GetLabel(self):      def GetLabel(self):
497          """Return the Group's label."""          """Return the Group's label."""
498          return self.label          return self.label
499    
500      def SetLabel(self, label):      def SetLabel(self, label):
501          """Set the Group's label.          """Set the Group's label.
502    
# Line 681  class ClassGroupRange(ClassGroup): Line 650  class ClassGroupRange(ClassGroup):
650      """A Group that represents a range of values that map to the same      """A Group that represents a range of values that map to the same
651         set of properties."""         set of properties."""
652    
653      def __init__(self, min = 0, max = 1, props = None, label = "", group=None):      def __init__(self, _range = (0,1), props = None, label = "", group=None):
654          """Constructor.          """Constructor.
655    
656          The minumum value must be strictly less than the maximum.          The minumum value must be strictly less than the maximum.
657    
658          min -- the minimum range value          _range -- either a tuple (min, max) where min < max or
659                      a Range object
         max -- the maximum range value  
660    
661          prop -- a ClassGroupProperites object. If prop is None a default          prop -- a ClassGroupProperites object. If prop is None a default
662                   set of properties is created.                   set of properties is created.
# Line 697  class ClassGroupRange(ClassGroup): Line 665  class ClassGroupRange(ClassGroup):
665          """          """
666    
667          ClassGroup.__init__(self, label, props, group)          ClassGroup.__init__(self, label, props, group)
668            self.SetRange(_range)
         #self.__min = self.__max = 0  
         #self.__range = Range("[" + repr(float(min)) + ";" +  
                                    #repr(float(max)) + "[")  
         self.SetRange(min, max)  
669    
670      def __copy__(self):      def __copy__(self):
671          return ClassGroupRange(min = self.__range,          return ClassGroupRange(self.__range,
                                max = None,  
672                                 props = self.GetProperties(),                                 props = self.GetProperties(),
673                                 label = self.GetLabel())                                 label = self.GetLabel())
674    
675      def __deepcopy__(self, memo):      def __deepcopy__(self, memo):
676          return ClassGroupRange(min = copy.copy(self.__range),          return ClassGroupRange(copy.copy(self.__range),
                                max = copy.copy(self.GetMax()),  
677                                 group = self)                                 group = self)
678    
679      def GetMin(self):      def GetMin(self):
# Line 725  class ClassGroupRange(ClassGroup): Line 687  class ClassGroupRange(ClassGroup):
687                 maximum value. Use SetRange() to change both min and max values.                 maximum value. Use SetRange() to change both min and max values.
688          """          """
689            
690          self.SetRange(min, self.__range.GetRange()[2])          self.SetRange((min, self.__range.GetRange()[2]))
691    
692      def GetMax(self):      def GetMax(self):
693          """Return the range's maximum value."""          """Return the range's maximum value."""
# Line 737  class ClassGroupRange(ClassGroup): Line 699  class ClassGroupRange(ClassGroup):
699          max -- the new maximum. Note that this must be greater than the current          max -- the new maximum. Note that this must be greater than the current
700                 minimum value. Use SetRange() to change both min and max values.                 minimum value. Use SetRange() to change both min and max values.
701          """          """
702          self.SetRange(self.__range.GetRange()[1], max)          self.SetRange((self.__range.GetRange()[1], max))
703    
704      def SetRange(self, min, max = None):      def SetRange(self, _range):
705          """Set a new range.          """Set a new range.
706    
707          Note that min must be strictly less than max.          _range -- Either a tuple (min, max) where min < max or
708                      a Range object.
709    
710          min -- the new minimum value          Raises ValueError on error.
         min -- the new maximum value  
711          """          """
712    
713          if isinstance(min, Range):          if isinstance(_range, Range):
714              self.__range = min              self.__range = _range
715            elif isinstance(_range, types.TupleType) and len(_range) == 2:
716                self.__range = Range(("[", _range[0], _range[1], "["))
717          else:          else:
718              if max is None:              raise ValueError()
                 raise ValueError()  
   
             self.__range = Range(("[", min, max, "["))  
719    
720      def GetRange(self):      def GetRange(self):
721          """Return the range as a string"""          """Return the range as a string"""
         #return (self.__min, self.__max)  
722          return self.__range.string(self.__range.GetRange())          return self.__range.string(self.__range.GetRange())
723    
724        def GetRangeTuple(self):
725            return self.__range.GetRange()
726    
727      def Matches(self, value):      def Matches(self, value):
728          """Determine if the given value lies with the current range.          """Determine if the given value lies with the current range.
729    
# Line 768  class ClassGroupRange(ClassGroup): Line 731  class ClassGroupRange(ClassGroup):
731          """          """
732    
733          return operator.contains(self.__range, value)          return operator.contains(self.__range, value)
         #return self.__min <= value < self.__max  
734    
735      def GetDisplayText(self):      def GetDisplayText(self):
736          label = self.GetLabel()          label = self.GetLabel()
737    
738          if label != "": return label          if label != "": return label
739    
         #return _("%s - %s") % (self.GetMin(), self.GetMax())  
         #return repr(self.__range)  
740          return self.__range.string(self.__range.GetRange())          return self.__range.string(self.__range.GetRange())
741    
742      def __eq__(self, other):      def __eq__(self, other):
743          return ClassGroup.__eq__(self, other) \          return ClassGroup.__eq__(self, other) \
744              and isinstance(other, ClassGroupRange) \              and isinstance(other, ClassGroupRange) \
745              and self.__range == other.__range              and self.__range == other.__range
             #and self.__min == other.__min \  
             #and self.__max == other.__max  
746    
747      def __repr__(self):      def __repr__(self):
748          return "(" + str(self.__range) + ClassGroup.__repr__(self) + ")"          return "(" + str(self.__range) + ClassGroup.__repr__(self) + ")"
         #return "(" + repr(self.__min) + ", " + repr(self.__max) + ", " + \  
                #ClassGroup.__repr__(self) + ")"  
749    
750  class ClassGroupMap(ClassGroup):  class ClassGroupMap(ClassGroup):
751      """Currently, this class is not used."""      """Currently, this class is not used."""

Legend:
Removed from v.1249  
changed lines
  Added in v.2657

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26