/[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 1912 - (show annotations)
Mon Nov 3 13:55:41 2003 UTC (21 years, 4 months ago) by bh
Original Path: trunk/thuban/Thuban/Model/classification.py
File MIME type: text/x-python
File size: 23073 byte(s)
(Classification.__getattr__)
(Classification._compile_classification)
(Classification._clear_compiled_classification): New. Methods to
manage a 'compiled' representation of the classification groups
which is created on demand
(Classification.InsertGroup, Classification.RemoveGroup)
(Classification.ReplaceGroup): reset the compiled representation
(Classification.FindGroup): Use the compiled representation to
find the matching group
(ClassGroupRange.GetRangeTuple): New. Return the range as a tuple

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

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26