/[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 381 by jonathan, Tue Jan 28 18:37:05 2003 UTC revision 479 by jonathan, Thu Mar 6 16:46:07 2003 UTC
# Line 1  Line 1 
1  # Copyright (c) 2001 by Intevation GmbH  # Copyright (c) 2001, 2003 by Intevation GmbH
2  # Authors:  # Authors:
3  # Jonathan Coles <[email protected]>  # Jonathan Coles <[email protected]>
4  #  #
# Line 13  to data. This mapping can be specified i Line 13  to data. This mapping can be specified i
13  First, specific values can be associated with data.  First, specific values can be associated with data.
14  Second, ranges can be associated with data such that if  Second, ranges can be associated with data such that if
15  an input value falls with a range that data is returned.  an input value falls with a range that data is returned.
16  If no mapping can be found then a NullData data will  If no mapping can be found then default data will
17  be returned. Input values must be hashable objects  be returned. Input values must be hashable objects
18    
19  See the description of getProperties() for more information  See the description of GetGroup() for more information
20  on the mapping algorithm.  on the mapping algorithm.
21  """  """
22        
23    # fix for people using python2.1
24    from __future__ import nested_scopes
25    
26    from types import *
27    
28    from messages import LAYER_PROJECTION_CHANGED, LAYER_LEGEND_CHANGED, \
29         LAYER_VISIBILITY_CHANGED
30    
31  from Thuban import _  from Thuban import _
32  from Thuban.Model.color import Color  from Thuban.Model.color import Color
33    
34  from wxPython.wx import *  import Thuban.Model.layer
35    
36  # constants  # constants
37  RANGE_MIN  = 0  RANGE_MIN  = 0
# Line 31  RANGE_MAX  = 1 Line 39  RANGE_MAX  = 1
39  RANGE_DATA = 2  RANGE_DATA = 2
40    
41  class Classification:  class Classification:
42        """Encapsulates the classification of layer. The Classification
43        divides some kind of data into Groups which are associated with
44        properties. Later the properties can be retrieved by matching
45        data values to the appropriate group."""
46    
47        def __init__(self, layer = None, field = None):
     def __init__(self, field = None):  
48          """Initialize a classification.          """Initialize a classification.
49    
50               layer -- the Layer object who owns this classification
51    
52             field -- the name of the data table field that             field -- the name of the data table field that
53                      is to be used to classify layer properties                      is to be used to classify layer properties
54          """          """
55    
56          self.points = {}          self.layer = None
57          self.ranges = []          self.field = None
58          self.setField(field)          self.fieldType = None
59          self.setNull(None)          self.groups = []
60                    self.__sendMessages = False
61      def setField(self, field):  
62            self.__ToggleMessages(False)
63            self.SetDefaultGroup(ClassGroupDefault())
64            self.SetLayer(layer)
65            self.SetField(field)
66    
67            self.__ToggleMessages(True)
68    
69        def __iter__(self):
70            return ClassIterator(self.groups)
71    
72        def __ToggleMessages(self, on):
73            self.__sendMessages = on
74    
75        def __SendMessage(self, message):
76            """Send the message 'message' to the parent layer."""
77            if self.__sendMessages and self.layer is not None:
78                self.layer.changed(message, self.layer)
79        
80        def SetField(self, field = None):
81          """Set the name of the data table field to use.          """Set the name of the data table field to use.
82                    
83             field -- if None then all values map to NullData             If there is no layer then the field type is set to None,
84               otherwise the layer is queried to find the type of the
85               field data
86    
87               field -- if None then all values map to the default data
88          """          """
89    
90            if field == "":
91                field = None
92    
93          self.field = field          self.field = field
94    
95      def setNull(self, data):          if self.layer is not None:
96          """Set the data to be used when a value can't be classified.              fieldType = self.layer.GetFieldType(field)
97            else:
98                fieldType = None
99    
100             data -- data that the value maps to. See class description.          self.SetFieldType(fieldType)
         """  
101    
102          self.NullData = data          # XXX: if fieldType comes back None then field isn't in the table!
103    
104      def addRange(self, min, max, data):          self.__SendMessage(LAYER_LEGEND_CHANGED)
         """Add a new range to the classification.  
105    
106             A range allows a value to be classified if it falls between      def GetField(self):
107             min and max. Specifically, min <= value < max          """Return the name of the field."""
108                    return self.field
            min -- the lower bound.  
109    
110             max -- the upper bound.      def GetFieldType(self):
111            """Return the field type."""
112            return self.fieldType
113    
114             data -- data that the value maps to. See class description.      def SetFieldType(self, type):
115          """          self.fieldType = type
116    
117          if min >= max:      def SetLayer(self, layer):
118              raise ValueError(_("Range minimum >= maximum!"))          """Set the owning Layer of this classification."""
119          self.ranges.append([min, max, data])  
120            if __debug__:
121                if layer is not None:
122                    assert(isinstance(layer, Thuban.Model.layer.Layer))
123    
124            # prevent infinite recursion when calling SetClassification()
125            if self.layer is not None and layer == self.layer:
126                return
127    
128            self.layer = layer
129            self.SetField(self.GetField()) # XXX: this sync's the fieldType
130    
131      def addPoint(self, value, data):          if self.layer is not None:
132          """Associate a single value with data.              self.layer.SetClassification(self)
133    
134             When this value is to be classified data will be returned.          #self.__SendMessage(LAYER_LEGEND_CHANGED)
135    
136             value -- classification value.      def GetLayer(self):
137            """Return the parent layer."""
138            return self.layer
139    
140             data  -- data that the value maps to. See class description.      def SetDefaultGroup(self, group):
141            """Set the group to be used when a value can't be classified.
142    
143               group -- group that the value maps to.
144          """          """
145    
146          self.points[value] = data          assert(isinstance(group, ClassGroupDefault))
147            self.AddGroup(group)
148    
149      def getProperties(self, value):      def GetDefaultGroup(self):
150          """Return the associated data, or the NullData.          """Return the default group."""
151            return self.groups[0]
152    
153        #
154        # these SetDefault* methods are really only provided for
155        # some backward compatibility. they should be considered
156        # for removal once all the classification code is finished.
157        #
158    
159             The following search technique is used:      def SetDefaultFill(self, fill):
160                 (1) if the field is None, return NullData          """Set the default fill color.
                (2) check if the value exists as a single value  
                (3) check if the value falls within a range. Ranges  
                    are checked in the order they were added to  
                    the classification.  
161    
162             value -- the value to classify. If there is no mapping          fill -- a Color object.
                     return the NullData (which may be None)  
163          """          """
164            assert(isinstance(fill, Color))
165            self.GetDefaultGroup().GetProperties().SetFill(fill)
166            self.__SendMessage(LAYER_LEGEND_CHANGED)
167            
168        def GetDefaultFill(self):
169            """Return the default fill color."""
170            return self.GetDefaultGroup().GetProperties().GetFill()
171            
172        def SetDefaultLineColor(self, color):
173            """Set the default line color.
174    
175          if self.field is not None:          color -- a Color object.
176              #          """
177              # first check the discrete values          assert(isinstance(color, Color))
178              #          self.GetDefaultGroup().GetProperties().SetLineColor(color)
179              if self.points.has_key(value):          self.__SendMessage(LAYER_LEGEND_CHANGED)
180                  return self.points[value]          
181        def GetDefaultLineColor(self):
182            """Return the default line color."""
183            return self.GetDefaultGroup().GetProperties().GetLineColor()
184            
185        def SetDefaultLineWidth(self, lineWidth):
186            """Set the default line width.
187    
188            lineWidth -- an integer > 0.
189            """
190            assert(isinstance(lineWidth, IntType))
191            self.GetDefaultGroup().GetProperties().SetLineWidth(lineWidth)
192            self.__SendMessage(LAYER_LEGEND_CHANGED)
193            
194        def GetDefaultLineWidth(self):
195            """Return the default line width."""
196            return self.GetDefaultGroup().GetProperties().GetLineWidth()
197            
198        def AddGroup(self, item):
199            """Add a new ClassGroup item to the classification.
200    
201              #          item -- this must be a valid ClassGroup object
202              # now check the ranges          """
203              #  
204              for p in self.ranges:          assert(isinstance(item, ClassGroup))
205                  if (p[RANGE_MIN] <= value) and (value < p[RANGE_MAX]):  
206                      return p[RANGE_DATA]          if len(self.groups) > 0 and isinstance(item, ClassGroupDefault):
207                self.groups[0] = item
208                #self.SetDefaultGroup(item)
209            else:
210                self.groups.append(item)
211    
212            self.__SendMessage(LAYER_LEGEND_CHANGED)
213    
214          return self.NullData      def GetGroup(self, value):
215            """Return the associated group, or the default group.
216    
217               Groups are checked in the order the were added to the
218               Classification.
219    
220               value -- the value to classify. If there is no mapping,
221                        the field is None or value is None,
222                        return the default properties
223            """
224    
225            if self.GetField() is not None and value is not None:
226    
227                for i in range(1, len(self.groups)):
228                    group = self.groups[i]
229                    if group.Matches(value):
230                        return group
231    
232            return self.GetDefaultGroup()
233    
234        def GetProperties(self, value):
235            """Return the properties associated with the given value."""
236    
237            group = self.GetGroup(value)
238            if isinstance(group, ClassGroupMap):
239                return group.GetPropertiesFromValue(value)
240            else:
241                return group.GetProperties()
242    
243      def TreeInfo(self):      def TreeInfo(self):
244          items = []          items = []
245    
246          #          def build_color_item(text, color):
247          # shouldn't print anything if there are no classifications              if color is Color.None:
248          #                  return ("%s: %s" % (text, _("None")), None)
249    
250                return ("%s: (%.3f, %.3f, %.3f)" %
251                        (text, color.red, color.green, color.blue),
252                        color)
253    
254            def build_item(group, string):
255                label = group.GetLabel()
256                if label == "":
257                    label = string
258                else:
259                    label += " (%s)" % string
260    
261                props = group.GetProperties()
262                i = []
263                v = props.GetLineColor()
264                i.append(build_color_item(_("Line Color"), v))
265                v = props.GetLineWidth()
266                i.append(_("Line Width: %s") % v)
267                v = props.GetFill()
268                i.append(build_color_item(_("Fill"), v))
269                return (label, i)
270    
271            for p in self:
272                if isinstance(p, ClassGroupDefault):
273                    items.append(build_item(self.GetDefaultGroup(), _("'DEFAULT'")))
274                elif isinstance(p, ClassGroupSingleton):
275                    items.append(build_item(p, str(p.GetValue())))
276                elif isinstance(p, ClassGroupRange):
277                    items.append(build_item(p, "%s - %s" %
278                                               (p.GetMin(), p.GetMax())))
279    
280            return (_("Classification"), items)
281    
282    class ClassIterator:
283        """Allows the Groups in a Classifcation to be interated over.
284    
285        The items are returned in the following order:
286            default data, singletons, ranges, maps
287        """
288    
289        def __init__(self, data): #default, points, ranges, maps):
290            """Constructor.
291    
292            default -- the default group
293    
294            points -- a list of singleton groups
295    
296            ranges -- a list of range groups
297    
298            maps -- a list of map groups
299            """
300    
301            self.data = data #[default, points, ranges, maps]
302            self.data_index = 0
303            #self.data_iter = iter(self.data)
304            #self.iter = None
305    
306        def __iter__(self):
307            return self
308    
309        def next(self):
310            """Return the next item."""
311    
312            if self.data_index >= len(self.data):
313                raise StopIteration
314            else:
315                d = self.data[self.data_index]
316                self.data_index += 1
317                return d
318                    
319          def color_string(color):  #       if self.iter is None:
320              if color is None:  #           try:
321                  return "None"  #               self.data_item = self.data_iter.next()
322              return "(%.3f, %.3f, %.3f)" % (color.red, color.green, color.blue)  #               self.iter = iter(self.data_item)
323    #           except TypeError:
324    #               return self.data_item
325    
326    #       try:
327    #           return self.iter.next()
328    #       except StopIteration:
329    #           self.iter = None
330    #           return self.next()
331          
332    class ClassGroupProperties:
333        """Represents the properties of a single Classification Group.
334      
335        These are used when rendering a layer."""
336    
337          if self.NullData is not None:      def __init__(self, props = None):
338              i = []          """Constructor.
             for key, value in self.NullData.items():  
                 if isinstance(value, Color):  
                     i.append((_("%s: %s") % (key, color_string(value)), value))  
                 else:  
                     i.append(_("%s: %s") % (key, value))  
             items.append((_("'NULL'"), i))  
339    
340          for name, data in self.points.items():          props -- a ClassGroupProperties object. The class is copied if
341              i = []                   prop is not None. Otherwise, a default set of properties
342              for key, value in data.items():                   is created such that: line color = Color.Black, line width = 1,
343                  if isinstance(value, Color):                   and fill color = Color.None
344                      i.append((_("%s: %s") % (key, color_string(value)), value))          """
                 else:  
                     i.append(_("%s: %s") % (key, value))  
             items.append((_("%s") % name, i))  
345    
346          for p in self.ranges:          self.stroke = None
347              i = []          self.strokeWidth = 0
348              data = p[RANGE_DATA]          self.fill = None
349              for key, value in data.items():  
350                  if isinstance(value, Color):          if props is not None:
351                      i.append((_("%s: %s") % (key, color_string(value)), value))              self.SetProperties(props)
352                  else:          else:
353                      i.append(_("%s: %s") % (key, value))              self.SetLineColor(Color.None)
354              items.append((_("%s-%s") % (p[RANGE_MIN], p[RANGE_MAX], i)))              self.SetLineWidth(1)
355                      self.SetFill(Color.None)
356          return (_("Classifications"), items)  
357        def SetProperties(self, props):
358            """Set this class's properties to those in class props."""
359    
360            assert(isinstance(props, ClassGroupProperties))
361            self.SetLineColor(props.GetLineColor())
362            self.SetLineWidth(props.GetLineWidth())
363            self.SetFill(props.GetFill())
364            
365        def GetLineColor(self):
366            """Return the line color as a Color object."""
367            return self.stroke
368    
369        def SetLineColor(self, color):
370            """Set the line color.
371    
372            color -- the color of the line. This must be a Color object.
373            """
374    
375            assert(isinstance(color, Color))
376            self.stroke = color
377    
378        def GetLineWidth(self):
379            """Return the line width."""
380            return self.strokeWidth
381    
382        def SetLineWidth(self, lineWidth):
383            """Set the line width.
384    
385            lineWidth -- the new line width. This must be > 0.
386            """
387            assert(isinstance(lineWidth, IntType))
388            if (lineWidth < 1):
389                raise ValueError(_("lineWidth < 1"))
390    
391            self.strokeWidth = lineWidth
392    
393        def GetFill(self):
394            """Return the fill color as a Color object."""
395            return self.fill
396    
397        def SetFill(self, fill):
398            """Set the fill color.
399    
400            fill -- the color of the fill. This must be a Color object.
401            """
402    
403            assert(isinstance(fill, Color))
404            self.fill = fill
405    
406        def __eq__(self, other):
407            """Return true if 'props' has the same attributes as this class"""
408    
409            return isinstance(other, ClassGroupProperties)   \
410                and self.stroke      == other.GetLineColor() \
411                and self.strokeWidth == other.GetLineWidth() \
412                and self.fill        == other.GetFill()
413    
414        def __ne__(self, other):
415            return not self.__eq__(other)
416    
417    class ClassGroup:
418        """A base class for all Groups within a Classification"""
419    
420        def __init__(self, label = ""):
421            """Constructor.
422    
423            label -- A string representing the Group's label
424            """
425    
426            self.label = None
427    
428            self.SetLabel(label)
429    
430        def GetLabel(self):
431            """Return the Group's label."""
432            return self.label
433    
434        def SetLabel(self, label):
435            """Set the Group's label.
436    
437            label -- a string representing the Group's label. This must
438                     not be None.
439            """
440            assert(isinstance(label, StringType))
441            self.label = label
442    
443        def Matches(self, value):
444            """Determines if this Group is associated with the given value.
445    
446            Returns False. This needs to be overridden by all subclasses.
447            """
448            return False
449    
450        def GetProperties(self):
451            """Return the properties associated with the given value.
452    
453            Returns None. This needs to be overridden by all subclasses.
454            """
455            return None
456    
457        
458    class ClassGroupSingleton(ClassGroup):
459        """A Group that is associated with a single value."""
460    
461        def __init__(self, value = 0, prop = None, label = ""):
462            """Constructor.
463    
464            value -- the associated value.
465    
466            prop -- a ClassGroupProperites object. If prop is None a default
467                     set of properties is created.
468    
469            label -- a label for this group.
470            """
471            ClassGroup.__init__(self, label)
472    
473            self.prop = None
474            self.value = None
475    
476            self.SetValue(value)
477            self.SetProperties(prop)
478    
479        def __copy__(self):
480            return ClassGroupSingleton(self.GetValue(),
481                                       self.GetProperties(),
482                                       self.GetLabel())
483    
484        def GetValue(self):
485            """Return the associated value."""
486            return self.value
487    
488        def SetValue(self, value):
489            """Associate this Group with the given value."""
490            self.value = value
491    
492        def Matches(self, value):
493            """Determine if the given value matches the associated Group value."""
494    
495            """Returns True if the value matches, False otherwise."""
496    
497            return self.value == value
498    
499        def GetProperties(self):
500            """Return the Properties associated with this Group."""
501    
502            return self.prop
503    
504        def SetProperties(self, prop):
505            """Set the properties associated with this Group.
506    
507            prop -- a ClassGroupProperties object. if prop is None,
508                    a default set of properties is created.
509            """
510    
511            if prop is None: prop = ClassGroupProperties()
512            assert(isinstance(prop, ClassGroupProperties))
513            self.prop = prop
514    
515        def __eq__(self, other):
516            return isinstance(other, ClassGroupSingleton) \
517                and self.GetProperties() == other.GetProperties() \
518                and self.GetValue() == other.GetValue()
519    
520        def __ne__(self, other):
521            return not self.__eq__(other)
522    
523    class ClassGroupDefault(ClassGroup):
524        """The default Group. When values do not match any other
525           Group within a Classification, the properties from this
526           class are used."""
527    
528        def __init__(self, prop = None, label = ""):
529            """Constructor.
530    
531            prop -- a ClassGroupProperites object. If prop is None a default
532                     set of properties is created.
533    
534            label -- a label for this group.
535            """
536    
537            ClassGroup.__init__(self, label)
538            self.SetProperties(prop)
539    
540        def __copy__(self):
541            return ClassGroupDefault(self.GetProperties(), self.GetLabel())
542    
543        def Matches(self, value):
544            return True
545    
546        def GetProperties(self):
547            """Return the Properties associated with this Group."""
548            return self.prop
549    
550        def SetProperties(self, prop):
551            """Set the properties associated with this Group.
552    
553            prop -- a ClassGroupProperties object. if prop is None,
554                    a default set of properties is created.
555            """
556    
557            if prop is None: prop = ClassGroupProperties()
558            assert(isinstance(prop, ClassGroupProperties))
559            self.prop = prop
560    
561        def __eq__(self, other):
562            return isinstance(other, ClassGroupDefault) \
563                and self.GetProperties() == other.GetProperties()
564    
565        def __ne__(self, other):
566            return not self.__eq__(other)
567    
568    class ClassGroupRange(ClassGroup):
569        """A Group that represents a range of values that map to the same
570           set of properties."""
571    
572        def __init__(self, min = 0, max = 1, prop = None, label = ""):
573            """Constructor.
574    
575            The minumum value must be strictly less than the maximum.
576    
577            min -- the minimum range value
578    
579            max -- the maximum range value
580    
581            prop -- a ClassGroupProperites object. If prop is None a default
582                     set of properties is created.
583    
584            label -- a label for this group.
585            """
586    
587            ClassGroup.__init__(self, label)
588    
589            self.min = self.max = 0
590            self.prop = None
591    
592            self.SetRange(min, max)
593            self.SetProperties(prop)
594    
595        def __copy__(self):
596            return ClassGroupRange(self.GetMin(),
597                                   self.GetMax(),
598                                   self.GetProperties(),
599                                   self.GetLabel())
600    
601        def GetMin(self):
602            """Return the range's minimum value."""
603            return self.min
604    
605        def SetMin(self, min):
606            """Set the range's minimum value.
607        
608            min -- the new minimum. Note that this must be less than the current
609                   maximum value. Use SetRange() to change both min and max values.
610            """
611        
612            self.SetRange(min, self.max)
613    
614        def GetMax(self):
615            """Return the range's maximum value."""
616            return self.max
617    
618        def SetMax(self, max):
619            """Set the range's maximum value.
620        
621            max -- the new maximum. Note that this must be greater than the current
622                   minimum value. Use SetRange() to change both min and max values.
623            """
624            self.SetRange(self.min, max)
625    
626        def SetRange(self, min, max):
627            """Set a new range.
628    
629            Note that min must be strictly less than max.
630    
631            min -- the new minimum value
632            min -- the new maximum value
633            """
634    
635            if min >= max:
636                raise ValueError(_("ClassGroupRange: %i(min) >= %i(max)!") %
637                                 (min, max))
638            self.min = min
639            self.max = max
640    
641        def GetRange(self):
642            """Return the range as a tuple (min, max)"""
643            return (self.min, self.max)
644    
645        def Matches(self, value):
646            """Determine if the given value lies with the current range.
647    
648            The following check is used: min <= value < max.
649            """
650    
651            return self.min <= value < self.max
652    
653        def GetProperties(self):
654            """Return the Properties associated with this Group."""
655            return self.prop
656    
657        def SetProperties(self, prop):
658            """Set the properties associated with this Group.
659    
660            prop -- a ClassGroupProperties object. if prop is None,
661                    a default set of properties is created.
662            """
663            if prop is None: prop = ClassGroupProperties()
664            assert(isinstance(prop, ClassGroupProperties))
665            self.prop = prop
666    
667        def __eq__(self, other):
668            return isinstance(other, ClassGroupRange) \
669                and self.GetProperties() == other.GetProperties() \
670                and self.GetRange() == other.GetRange()
671    
672        def __ne__(self, other):
673            return not self.__eq__(other)
674    
675    class ClassGroupMap(ClassGroup):
676        """Currently, this class is not used."""
677    
678        FUNC_ID = "id"
679    
680        def __init__(self, map_type = FUNC_ID, func = None, prop = None, label=""):
681            ClassGroup.__init__(self, label)
682    
683            self.map_type = map_type
684            self.func = func
685    
686            if self.func is None:
687                self.func = func_id
688    
689        def Map(self, value):
690            return self.func(value)
691    
692        def GetProperties(self):
693            return None
694    
695        def GetPropertiesFromValue(self, value):
696            pass
697    
698        #
699        # built-in mappings
700        #
701        def func_id(value):
702            return value
703    

Legend:
Removed from v.381  
changed lines
  Added in v.479

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26