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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2734 - (show annotations)
Thu Mar 1 12:42:59 2007 UTC (18 years ago) by bramz
File MIME type: text/x-python
File size: 26440 byte(s)
made a copy
1 # Copyright (c) 2001, 2003, 2005, 2006 by Intevation GmbH
2 # Authors:
3 # Jonathan Coles <[email protected]>
4 # Jan-Oliver Wagner <[email protected]> (2005)
5 # Frank Koormann <[email protected]> (2006)
6 #
7 # This program is free software under the GPL (>=v2)
8 # Read the file COPYING coming with Thuban for details.
9
10 __version__ = "$Revision$"
11
12 """
13 A Classification provides a mapping from an input value
14 to data. This mapping can be specified in two ways.
15 First, specific values can be associated with data.
16 Second, ranges can be associated with data such that if
17 an input value falls with a range that data is returned.
18 If no mapping can be found then default data will
19 be returned. Input values must be hashable objects
20
21 See the description of FindGroup() for more information
22 on the mapping algorithm.
23 """
24
25 import copy, operator, types
26 import re
27
28 from Thuban import _
29
30 from messages import \
31 LAYER_PROJECTION_CHANGED, \
32 LAYER_LEGEND_CHANGED, \
33 LAYER_VISIBILITY_CHANGED,\
34 CLASS_CHANGED
35
36 from Thuban.Model.color import Color, Transparent, Black
37 from Thuban.Model.range import Range
38
39 import Thuban.Model.layer
40
41 from Thuban.Lib.connector import Publisher
42
43 class Classification(Publisher):
44 """Encapsulates the classification of layer.
45
46 The Classification divides some kind of data into Groups which
47 are associated with properties. Later the properties can be
48 retrieved by matching data values to the appropriate group.
49 """
50
51 def __init__(self):
52 """Initialize a classification."""
53
54 self.__groups = []
55
56 self.SetDefaultGroup(ClassGroupDefault())
57
58 def __iter__(self):
59 return ClassIterator(self.__groups)
60
61 def __deepcopy__(self, memo):
62 clazz = Classification()
63
64 clazz.__groups[0] = copy.deepcopy(self.__groups[0])
65
66 for i in range(1, len(self.__groups)):
67 clazz.__groups.append(copy.deepcopy(self.__groups[i]))
68
69 return clazz
70
71 def __SendNotification(self):
72 """Notify the layer that this class has changed."""
73 self.issue(CLASS_CHANGED)
74
75 def __getattr__(self, attr):
76 """Generate the compiled classification on demand"""
77 if attr == "_compiled_classification":
78 self._compile_classification()
79 return self._compiled_classification
80 raise AttributeError(attr)
81
82 def _compile_classification(self):
83 """Generate the compiled classification
84
85 The compiled classification is a more compact representation of
86 the classification groups that is also more efficient for
87 performing the classification.
88
89 The compiled classification is a list of tuples. The first
90 element of the tuple is a string which describes the rest of the
91 tuple. There are two kinds of tuples:
92
93 'singletons'
94
95 The second element of the tuple is a dictionary which
96 combines several consecutive ClassGroupSingleton instances.
97 The dictionary maps the values of the singletons (as
98 returned by the GetValue() method) to the corresponding
99 group.
100
101 'range'
102
103 The tuple describes a ClassGroupRange instance. The tuples
104 second element is a tuple fo the form (lfunc, min, max,
105 rfunc, group) where group is the original group object,
106 lfunc and rfuct are comparison functions and min and max are
107 lower and upper bounds of the range. Given a value and such
108 a tuple the group matches if and only if
109
110 lfunc(min, value) and rfunc(max, value)
111
112 is true.
113
114 'pattern'
115
116 The tuple contains the compiled regular expression object and
117 the original group object.
118
119 The compiled classification is bound to
120 self._compile_classification.
121 """
122 compiled = []
123 for group in self.__groups[1:]:
124 if isinstance(group, ClassGroupSingleton):
125 if not compiled or compiled[-1][0] != "singletons":
126 compiled.append(("singletons", {}))
127 compiled[-1][1].setdefault(group.GetValue(), group)
128 elif isinstance(group, ClassGroupPattern):
129 pattern = re.compile(group.GetPattern())
130 compiled.append(("pattern", (pattern, group)))
131 elif isinstance(group, ClassGroupRange):
132 left, min, max, right = group.GetRangeTuple()
133 if left == "[":
134 lfunc = operator.le
135 elif left == "]":
136 lfunc = operator.lt
137 if right == "[":
138 rfunc = operator.gt
139 elif right == "]":
140 rfunc = operator.ge
141 compiled.append(("range", (lfunc, min, max, rfunc, group)))
142 else:
143 raise TypeError("Unknown group type %s", group)
144 self._compiled_classification = compiled
145
146 def _clear_compiled_classification(self):
147 """Reset the compiled classification.
148
149 If will be created on demand when self._compiled_classification
150 is accessed again.
151
152 Call this method whenever self.__groups is modified.
153 """
154 try:
155 del self._compiled_classification
156 except:
157 pass
158
159 #
160 # these SetDefault* methods are really only provided for
161 # some backward compatibility. they should be considered
162 # for removal once all the classification code is finished.
163 #
164
165 def SetDefaultFill(self, fill):
166 """Set the default fill color.
167
168 fill -- a Color object.
169 """
170 self.GetDefaultGroup().GetProperties().SetFill(fill)
171 self.__SendNotification()
172
173 def GetDefaultFill(self):
174 """Return the default fill color."""
175 return self.GetDefaultGroup().GetProperties().GetFill()
176
177 def SetDefaultLineColor(self, color):
178 """Set the default line color.
179
180 color -- a Color object.
181 """
182 self.GetDefaultGroup().GetProperties().SetLineColor(color)
183 self.__SendNotification()
184
185 def GetDefaultLineColor(self):
186 """Return the default line color."""
187 return self.GetDefaultGroup().GetProperties().GetLineColor()
188
189 def SetDefaultLineWidth(self, lineWidth):
190 """Set the default line width.
191
192 lineWidth -- an integer > 0.
193 """
194 assert isinstance(lineWidth, types.IntType)
195 self.GetDefaultGroup().GetProperties().SetLineWidth(lineWidth)
196 self.__SendNotification()
197
198 def GetDefaultLineWidth(self):
199 """Return the default line width."""
200 return self.GetDefaultGroup().GetProperties().GetLineWidth()
201
202
203 #
204 # The methods that manipulate self.__groups have to be kept in
205 # sync. We store the default group in index 0 to make it
206 # convienent to iterate over the classification's groups, but
207 # from the user's perspective the first (non-default) group is
208 # at index 0 and the DefaultGroup is a special entity.
209 #
210
211 def SetDefaultGroup(self, group):
212 """Set the group to be used when a value can't be classified.
213
214 group -- group that the value maps to.
215 """
216 assert isinstance(group, ClassGroupDefault)
217 if len(self.__groups) > 0:
218 self.__groups[0] = group
219 else:
220 self.__groups.append(group)
221 self.__SendNotification()
222
223 def GetDefaultGroup(self):
224 """Return the default group."""
225 return self.__groups[0]
226
227 def AppendGroup(self, item):
228 """Append a new ClassGroup item to the classification.
229
230 item -- this must be a valid ClassGroup object
231 """
232
233 self.InsertGroup(self.GetNumGroups(), item)
234
235 def InsertGroup(self, index, group):
236 assert isinstance(group, ClassGroup)
237 self.__groups.insert(index + 1, group)
238 self._clear_compiled_classification()
239 self.__SendNotification()
240
241 def RemoveGroup(self, index):
242 """Remove the classification group with the given index"""
243 self.__groups.pop(index + 1)
244 self._clear_compiled_classification()
245 self.__SendNotification()
246
247 def ReplaceGroup(self, index, group):
248 assert isinstance(group, ClassGroup)
249 self.__groups[index + 1] = group
250 self._clear_compiled_classification()
251 self.__SendNotification()
252
253 def GetGroup(self, index):
254 return self.__groups[index + 1]
255
256 def GetNumGroups(self):
257 """Return the number of non-default groups in the classification."""
258 return len(self.__groups) - 1
259
260 def FindGroup(self, value):
261 """Return the group that matches the value.
262
263 Groups are effectively checked in the order the were added to
264 the Classification.
265
266 value -- the value to classify. If there is no mapping or value
267 is None, return the default properties
268 """
269
270 if value is not None:
271 for typ, params in self._compiled_classification:
272 if typ == "singletons":
273 group = params.get(value)
274 if group is not None:
275 return group
276 elif typ == "range":
277 lfunc, min, max, rfunc, g = params
278 if lfunc(min, value) and rfunc(max, value):
279 return g
280 elif typ == "pattern":
281 # TODO: make pattern more robust. The following chrashes
282 # if accidently be applied on non-string columns.
283 # Usually the UI prevents this.
284 p, g = params
285 if p.match(value):
286 return g
287
288 return self.GetDefaultGroup()
289
290 def GetProperties(self, value):
291 """Return the properties associated with the given value.
292
293 Use this function rather than Classification.FindGroup().GetProperties()
294 since the returned group may be a ClassGroupMap which doesn't support
295 a call to GetProperties().
296 """
297
298 group = self.FindGroup(value)
299 if isinstance(group, ClassGroupMap):
300 return group.GetPropertiesFromValue(value)
301 else:
302 return group.GetProperties()
303
304 def TreeInfo(self):
305 items = []
306
307 def build_color_item(text, color):
308 if color is Transparent:
309 return ("%s: %s" % (text, _("None")), None)
310
311 return ("%s: (%.3f, %.3f, %.3f)" %
312 (text, color.red, color.green, color.blue),
313 color)
314
315 def build_item(group, string):
316 label = group.GetLabel()
317 if label == "":
318 label = string
319 else:
320 label += " (%s)" % string
321
322 props = group.GetProperties()
323 i = []
324 v = props.GetLineColor()
325 i.append(build_color_item(_("Line Color"), v))
326 v = props.GetLineWidth()
327 i.append(_("Line Width: %s") % v)
328
329 # Note: Size is owned by all properties, so
330 # a size will also appear where it does not
331 # make sense like for lines and polygons.
332 v = props.GetSize()
333 i.append(_("Size: %s") % v)
334
335 v = props.GetFill()
336 i.append(build_color_item(_("Fill"), v))
337 return (label, i)
338
339 for p in self:
340 items.append(build_item(p, p.GetDisplayText()))
341
342 return (_("Classification"), items)
343
344 class ClassIterator:
345 """Allows the Groups in a Classifcation to be interated over.
346
347 The items are returned in the following order:
348 default data, singletons, ranges, maps
349 """
350
351 def __init__(self, data): #default, points, ranges, maps):
352 """Constructor.
353
354 default -- the default group
355
356 points -- a list of singleton groups
357
358 ranges -- a list of range groups
359
360 maps -- a list of map groups
361 """
362
363 self.data = data
364 self.data_index = 0
365
366 def __iter__(self):
367 return self
368
369 def next(self):
370 """Return the next item."""
371
372 if self.data_index >= len(self.data):
373 raise StopIteration
374 else:
375 d = self.data[self.data_index]
376 self.data_index += 1
377 return d
378
379 class ClassGroupProperties:
380 """Represents the properties of a single Classification Group.
381
382 These are used when rendering a layer."""
383
384 # TODO: Actually, size is only relevant for point objects.
385 # Eventually it should be spearated, e.g. when introducing symbols.
386
387 def __init__(self, props = None):
388 """Constructor.
389
390 props -- a ClassGroupProperties object. The class is copied if
391 prop is not None. Otherwise, a default set of properties
392 is created such that: line color = Black, line width = 1,
393 size = 5 and fill color = Transparent
394 """
395
396 if props is not None:
397 self.SetProperties(props)
398 else:
399 self.SetLineColor(Black)
400 self.SetLineWidth(1)
401 self.SetSize(5)
402 self.SetFill(Transparent)
403
404 def SetProperties(self, props):
405 """Set this class's properties to those in class props."""
406
407 assert isinstance(props, ClassGroupProperties)
408 self.SetLineColor(props.GetLineColor())
409 self.SetLineWidth(props.GetLineWidth())
410 self.SetSize(props.GetSize())
411 self.SetFill(props.GetFill())
412
413 def GetLineColor(self):
414 """Return the line color as a Color object."""
415 return self.__stroke
416
417 def SetLineColor(self, color):
418 """Set the line color.
419
420 color -- the color of the line. This must be a Color object.
421 """
422
423 self.__stroke = color
424
425 def GetLineWidth(self):
426 """Return the line width."""
427 return self.__strokeWidth
428
429 def SetLineWidth(self, lineWidth):
430 """Set the line width.
431
432 lineWidth -- the new line width. This must be > 0.
433 """
434 assert isinstance(lineWidth, types.IntType)
435 if (lineWidth < 1):
436 raise ValueError(_("lineWidth < 1"))
437
438 self.__strokeWidth = lineWidth
439
440 def GetSize(self):
441 """Return the size."""
442 return self.__size
443
444 def SetSize(self, size):
445 """Set the size.
446
447 size -- the new size. This must be > 0.
448 """
449 assert isinstance(size, types.IntType)
450 if (size < 1):
451 raise ValueError(_("size < 1"))
452
453 self.__size = size
454
455 def GetFill(self):
456 """Return the fill color as a Color object."""
457 return self.__fill
458
459 def SetFill(self, fill):
460 """Set the fill color.
461
462 fill -- the color of the fill. This must be a Color object.
463 """
464
465 self.__fill = fill
466
467 def __eq__(self, other):
468 """Return true if 'props' has the same attributes as this class"""
469
470 #
471 # using 'is' over '==' results in a huge performance gain
472 # in the renderer
473 #
474 return isinstance(other, ClassGroupProperties) \
475 and (self.__stroke is other.__stroke or \
476 self.__stroke == other.__stroke) \
477 and (self.__fill is other.__fill or \
478 self.__fill == other.__fill) \
479 and self.__strokeWidth == other.__strokeWidth\
480 and self.__size == other.__size
481
482 def __ne__(self, other):
483 return not self.__eq__(other)
484
485 def __copy__(self):
486 return ClassGroupProperties(self)
487
488 def __deepcopy__(self):
489 return ClassGroupProperties(self)
490
491 def __repr__(self):
492 return repr((self.__stroke, self.__strokeWidth, self.__size,
493 self.__fill))
494
495 class ClassGroup:
496 """A base class for all Groups within a Classification"""
497
498 def __init__(self, label = "", props = None, group = None):
499 """Constructor.
500
501 label -- A string representing the Group's label
502 """
503
504 if group is not None:
505 self.SetLabel(copy.copy(group.GetLabel()))
506 self.SetProperties(copy.copy(group.GetProperties()))
507 self.SetVisible(group.IsVisible())
508 else:
509 self.SetLabel(label)
510 self.SetProperties(props)
511 self.SetVisible(True)
512
513 def GetLabel(self):
514 """Return the Group's label."""
515 return self.label
516
517 def SetLabel(self, label):
518 """Set the Group's label.
519
520 label -- a string representing the Group's label. This must
521 not be None.
522 """
523 assert isinstance(label, types.StringTypes)
524 self.label = label
525
526 def GetDisplayText(self):
527 assert False, "GetDisplay must be overridden by subclass!"
528 return ""
529
530 def Matches(self, value):
531 """Determines if this Group is associated with the given value.
532
533 Returns False. This needs to be overridden by all subclasses.
534 """
535 assert False, "GetMatches must be overridden by subclass!"
536 return False
537
538 def GetProperties(self):
539 """Return the properties associated with the given value."""
540
541 return self.prop
542
543 def SetProperties(self, prop):
544 """Set the properties associated with this Group.
545
546 prop -- a ClassGroupProperties object. if prop is None,
547 a default set of properties is created.
548 """
549
550 if prop is None: prop = ClassGroupProperties()
551 assert isinstance(prop, ClassGroupProperties)
552 self.prop = prop
553
554 def IsVisible(self):
555 return self.visible
556
557 def SetVisible(self, visible):
558 self.visible = visible
559
560 def __eq__(self, other):
561 return isinstance(other, ClassGroup) \
562 and self.label == other.label \
563 and self.GetProperties() == other.GetProperties()
564
565 def __ne__(self, other):
566 return not self.__eq__(other)
567
568 def __repr__(self):
569 return repr(self.label) + ", " + repr(self.GetProperties())
570
571 class ClassGroupSingleton(ClassGroup):
572 """A Group that is associated with a single value."""
573
574 def __init__(self, value = 0, props = None, label = "", group = None):
575 """Constructor.
576
577 value -- the associated value.
578
579 prop -- a ClassGroupProperites object. If prop is None a default
580 set of properties is created.
581
582 label -- a label for this group.
583 """
584 ClassGroup.__init__(self, label, props, group)
585
586 self.SetValue(value)
587
588 def __copy__(self):
589 return ClassGroupSingleton(self.GetValue(),
590 self.GetProperties(),
591 self.GetLabel())
592
593 def __deepcopy__(self, memo):
594 return ClassGroupSingleton(self.GetValue(), group = self)
595
596 def GetValue(self):
597 """Return the associated value."""
598 return self.__value
599
600 def SetValue(self, value):
601 """Associate this Group with the given value."""
602 self.__value = value
603
604 def Matches(self, value):
605 """Determine if the given value matches the associated Group value."""
606
607 """Returns True if the value matches, False otherwise."""
608
609 return self.__value == value
610
611 def GetDisplayText(self):
612 label = self.GetLabel()
613
614 if label != "": return label
615
616 return str(self.GetValue())
617
618 def __eq__(self, other):
619 return ClassGroup.__eq__(self, other) \
620 and isinstance(other, ClassGroupSingleton) \
621 and self.__value == other.__value
622
623 def __repr__(self):
624 return "(" + repr(self.__value) + ", " + ClassGroup.__repr__(self) + ")"
625
626 class ClassGroupDefault(ClassGroup):
627 """The default Group. When values do not match any other
628 Group within a Classification, the properties from this
629 class are used."""
630
631 def __init__(self, props = None, label = "", group = None):
632 """Constructor.
633
634 prop -- a ClassGroupProperites object. If prop is None a default
635 set of properties is created.
636
637 label -- a label for this group.
638 """
639
640 ClassGroup.__init__(self, label, props, group)
641
642 def __copy__(self):
643 return ClassGroupDefault(self.GetProperties(), self.GetLabel())
644
645 def __deepcopy__(self, memo):
646 return ClassGroupDefault(label = self.GetLabel(), group = self)
647
648 def Matches(self, value):
649 return True
650
651 def GetDisplayText(self):
652 label = self.GetLabel()
653
654 if label != "": return label
655
656 return _("DEFAULT")
657
658 def __eq__(self, other):
659 return ClassGroup.__eq__(self, other) \
660 and isinstance(other, ClassGroupDefault) \
661 and self.GetProperties() == other.GetProperties()
662
663 def __repr__(self):
664 return "(" + ClassGroup.__repr__(self) + ")"
665
666 class ClassGroupRange(ClassGroup):
667 """A Group that represents a range of values that map to the same
668 set of properties."""
669
670 def __init__(self, _range = (0,1), props = None, label = "", group=None):
671 """Constructor.
672
673 The minumum value must be strictly less than the maximum.
674
675 _range -- either a tuple (min, max) where min < max or
676 a Range object
677
678 prop -- a ClassGroupProperites object. If prop is None a default
679 set of properties is created.
680
681 label -- a label for this group.
682 """
683
684 ClassGroup.__init__(self, label, props, group)
685 self.SetRange(_range)
686
687 def __copy__(self):
688 return ClassGroupRange(self.__range,
689 props = self.GetProperties(),
690 label = self.GetLabel())
691
692 def __deepcopy__(self, memo):
693 return ClassGroupRange(copy.copy(self.__range),
694 group = self)
695
696 def GetMin(self):
697 """Return the range's minimum value."""
698 return self.__range.GetRange()[1]
699
700 def SetMin(self, min):
701 """Set the range's minimum value.
702
703 min -- the new minimum. Note that this must be less than the current
704 maximum value. Use SetRange() to change both min and max values.
705 """
706
707 self.SetRange((min, self.__range.GetRange()[2]))
708
709 def GetMax(self):
710 """Return the range's maximum value."""
711 return self.__range.GetRange()[2]
712
713 def SetMax(self, max):
714 """Set the range's maximum value.
715
716 max -- the new maximum. Note that this must be greater than the current
717 minimum value. Use SetRange() to change both min and max values.
718 """
719 self.SetRange((self.__range.GetRange()[1], max))
720
721 def SetRange(self, _range):
722 """Set a new range.
723
724 _range -- Either a tuple (min, max) where min < max or
725 a Range object.
726
727 Raises ValueError on error.
728 """
729
730 if isinstance(_range, Range):
731 self.__range = _range
732 elif isinstance(_range, types.TupleType) and len(_range) == 2:
733 self.__range = Range(("[", _range[0], _range[1], "["))
734 else:
735 raise ValueError()
736
737 def GetRange(self):
738 """Return the range as a string"""
739 return self.__range.string(self.__range.GetRange())
740
741 def GetRangeTuple(self):
742 return self.__range.GetRange()
743
744 def Matches(self, value):
745 """Determine if the given value lies with the current range.
746
747 The following check is used: min <= value < max.
748 """
749
750 return operator.contains(self.__range, value)
751
752 def GetDisplayText(self):
753 label = self.GetLabel()
754
755 if label != "": return label
756
757 return self.__range.string(self.__range.GetRange())
758
759 def __eq__(self, other):
760 return ClassGroup.__eq__(self, other) \
761 and isinstance(other, ClassGroupRange) \
762 and self.__range == other.__range
763
764 def __repr__(self):
765 return "(" + str(self.__range) + ClassGroup.__repr__(self) + ")"
766
767 class ClassGroupPattern(ClassGroup):
768 """A Group that is associated with a reg exp pattern."""
769
770 def __init__(self, pattern = "", props = None, label = "", group = None):
771 """Constructor.
772
773 pattern -- the associated pattern.
774
775 props -- a ClassGroupProperites object. If props is None a default
776 set of properties is created.
777
778 label -- a label for this group.
779 """
780 ClassGroup.__init__(self, label, props, group)
781
782 self.SetPattern(pattern)
783
784 def __copy__(self):
785 return ClassGroupPattern(self.GetPattern(),
786 self.GetProperties(),
787 self.GetLabel())
788
789 def __deepcopy__(self, memo):
790 return ClassGroupPattern(self.GetPattern(), group = self)
791
792 def GetPattern(self):
793 """Return the associated pattern."""
794 return self.__pattern
795
796 def SetPattern(self, pattern):
797 """Associate this Group with the given pattern."""
798 self.__pattern = pattern
799
800 def Matches(self, pattern):
801 """Check if the given pattern matches the associated Group pattern."""
802
803 """Returns True if the value matches, False otherwise."""
804
805 if re.match(self.__pattern, pattern):
806 return True
807 else:
808 return False
809
810 def GetDisplayText(self):
811 label = self.GetLabel()
812
813 if label != "": return label
814
815 return str(self.GetPattern())
816
817 def __eq__(self, other):
818 return ClassGroup.__eq__(self, other) \
819 and isinstance(other, ClassGroupPattern) \
820 and self.__pattern == other.__pattern
821
822 def __repr__(self):
823 return "(" + repr(self.__pattern) + ", " + ClassGroup.__repr__(self) + ")"
824
825 class ClassGroupMap(ClassGroup):
826 """Currently, this class is not used."""
827
828 FUNC_ID = "id"
829
830 def __init__(self, map_type = FUNC_ID, func = None, prop = None, label=""):
831 ClassGroup.__init__(self, label)
832
833 self.map_type = map_type
834 self.func = func
835
836 if self.func is None:
837 self.func = func_id
838
839 def Map(self, value):
840 return self.func(value)
841
842 def GetProperties(self):
843 return None
844
845 def GetPropertiesFromValue(self, value):
846 pass
847
848 def GetDisplayText(self):
849 return "Map: " + self.map_type
850
851 #
852 # built-in mappings
853 #
854 def func_id(value):
855 return value
856

Properties

Name Value
svn:eol-style native
svn:keywords Author Date Id Revision

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26