/[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 2657 - (show annotations)
Wed Jul 27 21:49:25 2005 UTC (19 years, 7 months ago) by jan
Original Path: trunk/thuban/Thuban/Model/classification.py
File MIME type: text/x-python
File size: 23951 byte(s)
(Classification.TreeItem.build_info): Added output of size.

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

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26