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

Legend:
Removed from v.385  
changed lines
  Added in v.1426

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26