/[thuban]/trunk/thuban/Thuban/UI/classifier.py
ViewVC logotype

Diff of /trunk/thuban/Thuban/UI/classifier.py

Parent Directory Parent Directory | Revision Log Revision Log | View Patch Patch

revision 441 by jonathan, Thu Feb 27 15:55:00 2003 UTC revision 2556 by bernhard, Mon Feb 7 13:46:53 2005 UTC
# Line 1  Line 1 
1  # Copyright (c) 2001 by Intevation GmbH  # Copyright (c) 2003-2004 by Intevation GmbH
2  # Authors:  # Authors:
3  # Jonathan Coles <[email protected]>  # Jan-Oliver Wagner <[email protected]> (2003-2004)
4    # Martin Schulze <[email protected]> (2004)
5    # Frank Koormann <[email protected]> (2003)
6    # Bernhard Herzog <[email protected]> (2003)
7    # Jonathan Coles <[email protected]> (2003)
8  #  #
9  # This program is free software under the GPL (>=v2)  # This program is free software under the GPL (>=v2)
10  # Read the file COPYING coming with Thuban for details.  # Read the file COPYING coming with Thuban for details.
# Line 8  Line 12 
12  """Dialog for classifying how layers are displayed"""  """Dialog for classifying how layers are displayed"""
13    
14  __version__ = "$Revision$"  __version__ = "$Revision$"
15    # $Source$
16    # $Id$
17    
18  import copy  import copy
19    
20    from Thuban.Model.table import FIELDTYPE_INT, FIELDTYPE_DOUBLE, \
21         FIELDTYPE_STRING
22    
23  from wxPython.wx import *  from wxPython.wx import *
24  from wxPython.grid import *  from wxPython.grid import *
25    
26  from Thuban import _  from Thuban import _
27  from Thuban.common import *  from Thuban.UI.common import Color2wxColour, wxColour2Color
28  from Thuban.UI.common import *  
29    from Thuban.Model.messages import MAP_LAYERS_REMOVED, LAYER_SHAPESTORE_REPLACED
30    from Thuban.Model.range import Range
31    from Thuban.Model.classification import \
32        Classification, ClassGroupDefault, \
33        ClassGroupSingleton, ClassGroupRange, ClassGroupMap, \
34        ClassGroupProperties
35    
36  from Thuban.Model.classification import * #Classification, ClassData  from Thuban.Model.color import Transparent
37    
38  from Thuban.Model.color import Color  from Thuban.Model.layer import Layer
39    from Thuban.Model.data import SHAPETYPE_ARC, SHAPETYPE_POLYGON, SHAPETYPE_POINT
40    
41  from Thuban.Model.layer import SHAPETYPE_ARC, SHAPETYPE_POLYGON, SHAPETYPE_POINT  from Thuban.UI.classgen import ClassGenDialog
42    from Thuban.UI.colordialog import ColorDialog
43    
44    from Thuban.UI.layerproperties import LayerProperties
45    from messages import MAP_REPLACED
46    
 ID_PROPERTY_SELECT = 4010  
47  ID_CLASS_TABLE = 40011  ID_CLASS_TABLE = 40011
48    
49  ID_CLASSIFY_OK = 4001  
50  ID_CLASSIFY_CANCEL = 4002  # table columns
51  ID_CLASSIFY_ADD = 4003  COL_VISIBLE = 0
52  ID_CLASSIFY_GENRANGE = 4004  COL_SYMBOL  = 1
53    COL_VALUE   = 2
54  COL_VISUAL = 0  COL_LABEL   = 3
55  COL_VALUE  = 1  NUM_COLS    = 4
56  COL_LABEL  = 2  
57    # indices into the client data lists in Classifier.fields
58    FIELD_CLASS = 0
59    FIELD_TYPE = 1
60    FIELD_NAME = 2
61    
62  #  #
63  # this is a silly work around to ensure that the table that is  # this is a silly work around to ensure that the table that is
# Line 43  COL_LABEL  = 2 Line 66  COL_LABEL  = 2
66  import weakref  import weakref
67  class ClassGrid(wxGrid):  class ClassGrid(wxGrid):
68    
     def __init__(self, parent, layer):  
         wxGrid.__init__(self, parent, ID_CLASS_TABLE, size = (300, 150))  
         self.SetTable(  
             ClassTable(layer.GetClassification(), layer.ShapeType(), self),  
             true)  
69    
70      def SetCellRenderer(self, row, col):      def __init__(self, parent, classifier):
71          raise ValueError(_("Must not allow setting of renderer in ClassGrid!"))          """Constructor.
72    
73            parent -- the parent window
74    
75            clazz -- the working classification that this grid should
76                     use for display.
77            """
78    
79            wxGrid.__init__(self, parent, ID_CLASS_TABLE, style = 0)
80    
81            self.classifier = classifier
82    
83            self.currentSelection = []
84    
85            EVT_GRID_CELL_LEFT_DCLICK(self, self._OnCellDClick)
86            EVT_GRID_RANGE_SELECT(self, self._OnSelectedRange)
87            EVT_GRID_SELECT_CELL(self, self._OnSelectedCell)
88            EVT_GRID_COL_SIZE(self, self._OnCellResize)
89            EVT_GRID_ROW_SIZE(self, self._OnCellResize)
90    
91        #def GetCellAttr(self, row, col):
92            #print "GetCellAttr ", row, col
93            #wxGrid.GetCellAttr(self, row, col)
94    
95        def CreateTable(self, clazz, fieldType, shapeType, group = None):
96    
97            assert isinstance(clazz, Classification)
98    
99            table = self.GetTable()
100            if table is None:
101                w = self.GetDefaultColSize() * NUM_COLS \
102                    + self.GetDefaultRowLabelSize()
103                h = self.GetDefaultRowSize() * 4 \
104                    + self.GetDefaultColLabelSize()
105    
106                self.SetDimensions(-1, -1, w, h)
107                self.SetSizeHints(w, h, -1, -1)
108                table = ClassTable(self)
109                self.SetTable(table, True)
110    
111    
112            self.SetSelectionMode(wxGrid.wxGridSelectRows)
113            self.ClearSelection()
114    
115            table.Reset(clazz, fieldType, shapeType, group)
116    
117        def GetCurrentSelection(self):
118            """Return the currently highlighted rows as an increasing list
119               of row numbers."""
120            sel = copy.copy(self.currentSelection)
121            sel.sort()
122            return sel
123    
124        def GetSelectedRows(self):
125            return self.GetCurrentSelection()
126    
127        #def SetCellRenderer(self, row, col, renderer):
128            #raise ValueError(_("Must not allow setting of renderer in ClassGrid!"))
129    
130        #
131        # [Set|Get]Table is taken from http://wiki.wxpython.org
132        # they are needed as a work around to ensure that the table
133        # that is passed to SetTable is the one that is returned
134        # by GetTable.
135        #
136      def SetTable(self, object, *attributes):      def SetTable(self, object, *attributes):
137          self.tableRef = weakref.ref(object)          self.tableRef = weakref.ref(object)
138          return wxGrid.SetTable(self, object, *attributes)          return wxGrid.SetTable(self, object, *attributes)
139    
140      def GetTable(self):      def GetTable(self):
141          return self.tableRef()          try:
142                return self.tableRef()
143            except:
144                return None
145    
146        def DeleteSelectedRows(self):
147            """Deletes all highlighted rows.
148      
149            If only one row is highlighted then after it is deleted the
150            row that was below the deleted row is highlighted."""
151    
152            sel = self.GetCurrentSelection()
153    
154            # nothing to do
155            if len(sel) == 0: return
156    
157            # if only one thing is selected check if it is the default
158            # data row, because we can't remove that
159            if len(sel) == 1:
160                #group = self.GetTable().GetValueAsCustom(sel[0], COL_SYMBOL, None)
161                group = self.GetTable().GetClassGroup(sel[0])
162                if isinstance(group, ClassGroupDefault):
163                    wxMessageDialog(self,
164                                    _("The Default group cannot be removed."),
165                                    style = wxOK | wxICON_EXCLAMATION).ShowModal()
166                    return
167            
168    
169            self.ClearSelection()
170    
171            # we need to remove things from the bottom up so we don't
172            # change the indexes of rows that will be deleted next
173            sel.reverse()
174    
175            #
176            # actually remove the rows
177            #
178            table = self.GetTable()
179            for row in sel:
180                table.DeleteRows(row)
181    
182            #
183            # if there was only one row selected highlight the row
184            # that was directly below it, or move up one if the
185            # deleted row was the last row.
186            #
187            if len(sel) == 1:
188                r = sel[0]
189                if r > self.GetNumberRows() - 1:
190                    r = self.GetNumberRows() - 1
191                self.SelectRow(r)
192            
193    
194        def SelectGroup(self, group, makeVisible = True):
195            if group is None: return
196    
197            assert isinstance(group, ClassGroup)
198    
199            table = self.GetTable()
200    
201            assert table is not None
202    
203            for i in range(table.GetNumberRows()):
204                g = table.GetClassGroup(i)
205                if g is group:
206                    self.SelectRow(i)
207                    if makeVisible:
208                        self.MakeCellVisible(i, 0)
209                    break
210    
211    #
212    # XXX: This isn't working, and there is no way to deselect rows wxPython!
213    #
214    #   def DeselectRow(self, row):
215    #       self.ProcessEvent(
216    #           wxGridRangeSelectEvent(-1,
217    #                                  wxEVT_GRID_RANGE_SELECT,
218    #                                  self,
219    #                                  (row, row), (row, row),
220    #                                  sel = False))
221    
222        def _OnCellDClick(self, event):
223            """Handle a double click on a cell."""
224    
225            r = event.GetRow()
226            c = event.GetCol()
227    
228            if c == COL_SYMBOL:
229                self.classifier.EditSymbol(r)
230            else:
231                event.Skip()
232    
233        #
234        # _OnSelectedRange() and _OnSelectedCell() were borrowed
235        # from http://wiki.wxpython.org to keep track of which
236        # cells are currently highlighted
237        #
238        def _OnSelectedRange(self, event):
239            """Internal update to the selection tracking list"""
240            if event.Selecting():
241                for index in range( event.GetTopRow(), event.GetBottomRow()+1):
242                    if index not in self.currentSelection:
243                        self.currentSelection.append( index )
244            else:
245                for index in range( event.GetTopRow(), event.GetBottomRow()+1):
246                    while index in self.currentSelection:
247                        self.currentSelection.remove( index )
248            #self.ConfigureForSelection()
249    
250            event.Skip()
251    
252        def _OnSelectedCell( self, event ):
253            """Internal update to the selection tracking list"""
254            self.currentSelection = [ event.GetRow() ]
255            #self.ConfigureForSelection()
256            event.Skip()
257    
258        def _OnCellResize(self, event):
259            self.FitInside()
260            event.Skip()
261    
262  class ClassTable(wxPyGridTableBase):  class ClassTable(wxPyGridTableBase):
263        """Represents the underlying data structure for the grid."""
264    
265        __col_labels = [_("Visible"), _("Symbol"), _("Value"), _("Label")]
266    
267    
268        def __init__(self, view = None):
269            """Constructor.
270    
271      NUM_COLS = 3          shapeType -- the type of shape that the layer uses
272    
273      __col_labels = [_("Visual"), _("Value"), _("Label")]          view -- a wxGrid object that uses this class for its table
274            """
275    
     def __init__(self, clazz, shapeType, view = None):  
276          wxPyGridTableBase.__init__(self)          wxPyGridTableBase.__init__(self)
277    
278            assert len(ClassTable.__col_labels) == NUM_COLS
279    
280            self.clazz = None
281            self.__colAttr = {}
282    
283          self.SetView(view)          self.SetView(view)
         self.tdata = []  
284    
285          self.Reset(clazz, shapeType)      def Reset(self, clazz, fieldType, shapeType, group = None):
286            """Reset the table with the given data.
287    
288      def Reset(self, clazz, shapeType):          This is necessary because wxWindows does not allow a grid's
289            table to change once it has been intially set and so we
290            need a way of modifying the data.
291    
292            clazz -- the working classification that this table should
293                     use for display. This may be different from the
294                     classification in the layer.
295    
296            shapeType -- the type of shape that the layer uses
297            """
298    
299            assert isinstance(clazz, Classification)
300    
301          self.GetView().BeginBatch()          self.GetView().BeginBatch()
302    
303            self.fieldType = fieldType
304          self.shapeType = shapeType          self.shapeType = shapeType
         self.renderer = ClassRenderer(self.shapeType)  
305    
306          old_tdata = self.tdata          self.SetClassification(clazz, group)
307            self.__Modified(-1)
308    
309          self.tdata = []          self.__colAttr = {}
310    
311          if clazz is None:          attr = wxGridCellAttr()
312              clazz = Classification()          attr.SetEditor(wxGridCellBoolEditor())
313            attr.SetRenderer(wxGridCellBoolRenderer())
314            attr.SetAlignment(wxALIGN_CENTER, wxALIGN_CENTER)
315            self.__colAttr[COL_VISIBLE] = attr
316    
317  #       p = clazz.GetDefaultGroup()          attr = wxGridCellAttr()
318  #       np = ClassDataDefault(classData = p)          attr.SetRenderer(ClassRenderer(self.shapeType))
319  #       self.tdata.append([np, 'DEFAULT', np.GetLabel()])          attr.SetReadOnly()
320            self.__colAttr[COL_SYMBOL] = attr
 #       for p in clazz.points.values():  
 #           np = ClassDataPoint(p.GetValue(), classData = p)  
 #           self.tdata.append([np, np.GetValue(), np.GetLabel()])  
   
 #       for p in clazz.ranges:  
 #           np = ClassDataRange(p.GetMin(), p.GetMax(), classData = p)  
 #           self.tdata.append([np,  
 #                              '%s - %s' % (np.GetMin(), np.GetMax()),  
 #                              np.GetLabel()])  
   
         i = 0  
         for p in clazz:  
             np = copy.copy(p)  
             self.__SetRow(i, np)  
             i += 1  
321    
322            self.GetView().EndBatch()
323          self.modified = 0          self.GetView().FitInside()
324    
325        def GetClassification(self):
326            """Return the current classification."""
327            return self.clazz
328    
329        def SetClassification(self, clazz, group = None):
330            """Fill in the table with the given classification.
331            Select the given group if group is not None.
332            """
333    
334            self.GetView().BeginBatch()
335    
336            old_len = self.GetNumberRows()
337    
338            row = -1
339            self.clazz = clazz
340    
341            self.__NotifyRowChanges(old_len, self.GetNumberRows())
342    
343          #          #
344            # XXX: this is dead code at the moment
345            #
346            if row > -1:
347                self.GetView().ClearSelection()
348                self.GetView().SelectRow(row)
349                self.GetView().MakeCellVisible(row, 0)
350    
351            self.__Modified()
352    
353            self.GetView().EndBatch()
354            self.GetView().FitInside()
355    
356        def __NotifyRowChanges(self, curRows, newRows):
357            """Make sure table updates correctly if the number of
358            rows changes.
359            """
360            #
361          # silly message processing for updates to the number of          # silly message processing for updates to the number of
362          # rows and columns          # rows and columns
363          #          #
         curRows = len(old_tdata)  
         newRows = len(self.tdata)  
364          if newRows > curRows:          if newRows > curRows:
365              msg = wxGridTableMessage(self,              msg = wxGridTableMessage(self,
366                          wxGRIDTABLE_NOTIFY_ROWS_APPENDED,                          wxGRIDTABLE_NOTIFY_ROWS_APPENDED,
367                          newRows - curRows)    # how many                          newRows - curRows)    # how many
368              self.GetView().ProcessTableMessage(msg)              self.GetView().ProcessTableMessage(msg)
369                self.GetView().FitInside()
370          elif newRows < curRows:          elif newRows < curRows:
371              msg = wxGridTableMessage(self,              msg = wxGridTableMessage(self,
372                          wxGRIDTABLE_NOTIFY_ROWS_DELETED,                          wxGRIDTABLE_NOTIFY_ROWS_DELETED,
373                          curRows - newRows,    # position                          curRows,              # position
374                          curRows - newRows)    # how many                          curRows - newRows)    # how many
375              self.GetView().ProcessTableMessage(msg)              self.GetView().ProcessTableMessage(msg)
376                self.GetView().FitInside()
         self.GetView().EndBatch()  
377    
378      def __SetRow(self, row, group):      def __SetRow(self, row, group):
379            """Set a row's data to that of the group.
380    
381          if isinstance(group, ClassGroupDefault):          The table is considered modified after this operation.
382              data = [group, 'DEFAULT', group.GetLabel()]  
383          elif isinstance(group, ClassGroupSingleton):          row -- if row is < 0 'group' is inserted at the top of the table
384              data = [group, group.GetValue(), group.GetLabel()]                 if row is >= GetNumberRows() or None 'group' is append to
385          elif isinstance(group, ClassGroupRange):                      the end of the table.
386              data = [group,                 otherwise 'group' replaces row 'row'
387                      '%s - %s' % (group.GetMin(), group.GetMax()),          """
                     group.GetLabel()]  
388    
389          if row >= len(self.tdata):          # either append or replace
390              self.tdata.append(data)          if row is None or row >= self.GetNumberRows():
391                self.clazz.AppendGroup(group)
392            elif row < 0:
393                self.clazz.InsertGroup(0, group)
394          else:          else:
395              self.tdata[row] = data              if row == 0:
396                    self.clazz.SetDefaultGroup(group)
397                else:
398                    self.clazz.ReplaceGroup(row - 1, group)
399    
400            self.__Modified()
401    
402      def GetColLabelValue(self, col):      def GetColLabelValue(self, col):
403            """Return the label for the given column."""
404          return self.__col_labels[col]          return self.__col_labels[col]
405    
406      def GetRowLabelValue(self, row):      def GetRowLabelValue(self, row):
407          data = self.tdata[row][COL_VISUAL]          """Return the label for the given row."""
408          if isinstance(data, ClassGroupDefault): return _("Default")  
409          if isinstance(data, ClassGroupSingleton): return _("Singleton")          if row == 0:
410          if isinstance(data, ClassGroupRange): return _("Range")              return _("Default")
411          if isinstance(data, ClassGroupMap): return _("Map")          else:
412                group = self.clazz.GetGroup(row - 1)
413                if isinstance(group, ClassGroupDefault):   return _("Default")
414                if isinstance(group, ClassGroupSingleton): return _("Singleton")
415                if isinstance(group, ClassGroupRange):     return _("Range")
416                if isinstance(group, ClassGroupMap):       return _("Map")
417    
418            assert False # shouldn't get here
419            return ""
420    
421      def GetNumberRows(self):      def GetNumberRows(self):
422          return len(self.tdata)          """Return the number of rows."""
423            if self.clazz is None:
424                return 0
425    
426            return self.clazz.GetNumGroups() + 1 # +1 for default group
427    
428      def GetNumberCols(self):      def GetNumberCols(self):
429          return self.NUM_COLS          """Return the number of columns."""
430            return NUM_COLS
431    
432      def IsEmptyCell(self, row, col):      def IsEmptyCell(self, row, col):
433          return 0          """Determine if a cell is empty. This is always false."""
434            return False
435    
436      def GetValue(self, row, col):      def GetValue(self, row, col):
437          return self.GetValueAsCustom(row, col, "")          """Return the object that is used to represent the given
438               cell coordinates. This may not be a string."""
439            return self.GetValueAsCustom(row, col, None)
440    
441      def SetValue(self, row, col, value):      def SetValue(self, row, col, value):
442          self.SetValueAsCustom(row, col, "", value)          """Assign 'value' to the cell specified by 'row' and 'col'.
443          self.__Modified()  
444                  The table is considered modified after this operation.
445            """
446    
447            self.SetValueAsCustom(row, col, None, value)
448    
449      def GetValueAsCustom(self, row, col, typeName):      def GetValueAsCustom(self, row, col, typeName):
450          return self.tdata[row][col]          """Return the object that is used to represent the given
451               cell coordinates. This may not be a string.
452    
453            typeName -- unused, but needed to overload wxPyGridTableBase
454            """
455    
456            if row == 0:
457                group = self.clazz.GetDefaultGroup()
458            else:
459                group = self.clazz.GetGroup(row - 1)
460    
461    
462            if col == COL_VISIBLE:
463                return group.IsVisible()
464    
465            if col == COL_SYMBOL:
466                return group.GetProperties()
467    
468            if col == COL_LABEL:
469                return group.GetLabel()
470    
471            # col must be COL_VALUE
472            assert col == COL_VALUE
473    
474            if isinstance(group, ClassGroupDefault):
475                return _("DEFAULT")
476            elif isinstance(group, ClassGroupSingleton):
477                return group.GetValue()
478            elif isinstance(group, ClassGroupRange):
479                return group.GetRange()
480    
481            assert False # shouldn't get here
482            return None
483    
484      def __ParseInput(self, value):      def __ParseInput(self, value):
485          """Try to determine what kind of input value is          """Try to determine what kind of input value is
486             (a single number or a range)             (string, number, or range)
487    
488            Returns a tuple (type, data) where type is 0 if data is
489            a singleton value, or 1 if is a range
490          """          """
491    
492          #          type = self.fieldType
493          # first try to take the input as a single number  
494          # if there's an exception try to break it into          if type == FIELDTYPE_STRING:
495          # a range seperated by a '-'. take care to ignore              return (0, value)
496          # a leading '-' as that could be for a negative number.          elif type in (FIELDTYPE_INT, FIELDTYPE_DOUBLE):
497          # then try to parse the individual parts. if there              if type == FIELDTYPE_INT:
498          # is an exception here, let it pass up to the calling                  # the float call allows the user to enter 1.0 for 1
499          # function.                  conv = lambda p: int(float(p))
500          #              else:
501          try:                  conv = float
502              return (Str2Num(value))  
503          except:              #
504              i = value.find('-')              # first try to take the input as a single number
505              if i == 0:              # if there's an exception try to break it into
506                  i = value.find('-', 1)              # a range. if there is an exception here, let it
507                # pass up to the calling function.
508                #
509                try:
510                    return (0, conv(value))
511                except ValueError:
512                    return (1, Range(value))
513    
514              return (Str2Num(value[:i]), Str2Num(value[i+1:]))          assert False  # shouldn't get here
515                        return (0,None)
516    
517      def SetValueAsCustom(self, row, col, typeName, value):      def SetValueAsCustom(self, row, col, typeName, value):
518          data = self.tdata[row][COL_VISUAL]          """Set the cell specified by 'row' and 'col' to 'value'.
519    
520          if col == COL_VISUAL:          If column represents the value column, the input is parsed
521              self.tdata[row][COL_VISUAL] = value          to determine if a string, number, or range was entered.
522          elif col == COL_VALUE:          A new ClassGroup may be created if the type of data changes.
             if row != 0: # DefaultData row  
523    
524                  if isinstance(data, ClassGroupMap):          The table is considered modified after this operation.
                     # something special  
                     pass  
                 else: # POINT, RANGE  
                     try:  
                         dataInfo = self.__ParseInput(value)  
                     except: pass  
                         # bad input, ignore the request  
                     else:  
525    
526                          if len(dataInfo) == 1:          typeName -- unused, but needed to overload wxPyGridTableBase
527                              if not isinstance(data, ClassGroupSingleton):          """
                                 ndata = ClassGroupSingleton(prop = data)  
                             ndata.SetValue(dataInfo[1])  
                         elif len(dataInfo) == 2:  
                             if not isinstance(data, ClassGroupRange):  
                                 data = ClassDataRange(classData = data)  
                             data.SetRange(dataInfo[1], dataInfo[2])  
528    
529                          ndata.SetLabel(data.GetLabel())          assert 0 <= col < self.GetNumberCols()
530                          self.__SetRow(row, ndata)          assert 0 <= row < self.GetNumberRows()
531    
532                          #self.tdata[row][COL_VISUAL] = data          if row == 0:
533                group = self.clazz.GetDefaultGroup()
534            else:
535                group = self.clazz.GetGroup(row - 1)
536    
537                          self.GetView().Refresh()          mod = True # assume the data will change
538    
539            if col == COL_VISIBLE:
540                group.SetVisible(value)
541            elif col == COL_SYMBOL:
542                group.SetProperties(value)
543          elif col == COL_LABEL:          elif col == COL_LABEL:
544              data.SetLabel(value)              group.SetLabel(value)
545              self.tdata[row][COL_LABEL] = data.GetLabel()          elif col == COL_VALUE:
546                if isinstance(group, ClassGroupDefault):
547                    # not allowed to modify the default value
548                    pass
549                elif isinstance(group, ClassGroupMap):
550                    # something special
551                    pass
552                else: # SINGLETON, RANGE
553                    try:
554                        dataInfo = self.__ParseInput(value)
555                    except ValueError:
556                        # bad input, ignore the request
557                        mod = False
558                    else:
559    
560                        changed = False
561                        ngroup = group
562                        props = group.GetProperties()
563    
564                        #
565                        # try to update the values, which may include
566                        # changing the underlying group type if the
567                        # group was a singleton and a range was entered
568                        #
569                        if dataInfo[0] == 0:
570                            if not isinstance(group, ClassGroupSingleton):
571                                ngroup = ClassGroupSingleton(props = props)
572                                changed = True
573                            ngroup.SetValue(dataInfo[1])
574                        elif dataInfo[0] == 1:
575                            if not isinstance(group, ClassGroupRange):
576                                ngroup = ClassGroupRange(props = props)
577                                changed = True
578                            ngroup.SetRange(dataInfo[1])
579                        else:
580                            assert False
581                            pass
582    
583                        if changed:
584                            ngroup.SetLabel(group.GetLabel())
585                            self.SetClassGroup(row, ngroup)
586          else:          else:
587              raise ValueError(_("Invalid column request"))              assert False # shouldn't be here
588                pass
589    
590          self.__Modified()          if mod:
591                self.__Modified()
592                self.GetView().Refresh()
593    
594      def GetAttr(self, row, col, someExtraParameter):      def GetAttr(self, row, col, someExtraParameter):
595          attr = wxGridCellAttr()          """Returns the cell attributes"""
         #attr = wxPyGridTableBase.GetAttr(self, row, col, someExtraParameter)  
   
         if col == COL_VISUAL:  
             attr.SetRenderer(ClassRenderer(self.shapeType))  
             attr.SetReadOnly()  
596    
597          return attr          return self.__colAttr.get(col, wxGridCellAttr()).Clone()
598    
599      def GetClassGroup(self, row):      def GetClassGroup(self, row):
600          return self.tdata[row][COL_VISUAL]          """Return the ClassGroup object representing row 'row'."""
601    
602      def __Modified(self):          #return self.GetValueAsCustom(row, COL_SYMBOL, None)
603          self.modified = 1          if row == 0:
604                return self.clazz.GetDefaultGroup()
605            else:
606                return self.clazz.GetGroup(row - 1)
607    
608        def SetClassGroup(self, row, group):
609            """Set the given row to properties of group."""
610            self.__SetRow(row, group)
611            self.GetView().Refresh()
612    
613        def __Modified(self, mod = True):
614            """Adjust the modified flag.
615    
616            mod -- if -1 set the modified flag to False, otherwise perform
617                   an 'or' operation with the current value of the flag and
618                   'mod'
619            """
620    
621            if mod == -1:
622                self.modified = False
623            else:
624                self.modified = mod or self.modified
625    
626      def IsModified(self):      def IsModified(self):
627            """True if this table is considered modified."""
628          return self.modified          return self.modified
629    
630      def AddNewDataRow(self):      def DeleteRows(self, pos, numRows = 1):
631          np = ClassDataPoint()          """Deletes 'numRows' beginning at row 'pos'.
632          self.tdata.append([np, np.GetValue(), np.GetLabel()])  
633          msg = wxGridTableMessage(self, wxGRIDTABLE_NOTIFY_ROWS_APPENDED, 1)          The row representing the default group is not removed.
634          self.GetView().ProcessTableMessage(msg)  
635          self.GetView().Refresh()          The table is considered modified if any rows are removed.
636            """
637    
638  class Classifier(wxDialog):          assert pos >= 0
639            old_len = self.GetNumberRows()
640            for row in range(pos, pos - numRows, -1):
641                group = self.GetClassGroup(row)
642                if row != 0:
643                    self.clazz.RemoveGroup(row - 1)
644                    self.__Modified()
645            
646      def __init__(self, parent, layer):          if self.IsModified():
647          wxDialog.__init__(self, parent, -1, _("Classify"),              self.__NotifyRowChanges(old_len, self.GetNumberRows())
                           style = wxRESIZE_BORDER)  
648    
649          self.layer = layer      def AppendRows(self, numRows = 1):
650            """Append 'numRows' empty rows to the end of the table.
651    
652          topBox = wxBoxSizer(wxVERTICAL)          The table is considered modified if any rows are appended.
653            """
654    
655          topBox.Add(wxStaticText(self, -1, _("Layer: %s") % layer.Title()),          old_len = self.GetNumberRows()
656              0, wxALIGN_LEFT | wxBOTTOM, 4)          for i in range(numRows):
657          topBox.Add(wxStaticText(self, -1, _("Type: %s") % layer.ShapeType()),              np = ClassGroupSingleton()
658              0, wxALIGN_LEFT | wxBOTTOM, 4)              self.__SetRow(None, np)
659    
660          propertyBox = wxBoxSizer(wxHORIZONTAL)          if self.IsModified():
661          propertyBox.Add(wxStaticText(self, -1, _("Property: ")),              self.__NotifyRowChanges(old_len, self.GetNumberRows())
662              0, wxALIGN_CENTER | wxALL, 4)  
663    
664          self.properties = wxComboBox(self, ID_PROPERTY_SELECT, "",  ID_PROPERTY_REVERT = 4002
665                                       style = wxCB_READONLY)  ID_PROPERTY_ADD = 4003
666    ID_PROPERTY_GENCLASS = 4004
667          self.num_cols = layer.table.field_count()  ID_PROPERTY_REMOVE = 4005
668          # just assume the first field in case one hasn't been  ID_PROPERTY_MOVEUP = 4006
669          # specified in the file.  ID_PROPERTY_MOVEDOWN = 4007
670          self.__cur_prop = 0  ID_PROPERTY_TRY = 4008
671          field = layer.GetClassification().GetField()  ID_PROPERTY_EDITSYM = 4009
672          for i in range(self.num_cols):  ID_PROPERTY_SELECT = 4011
673              type, name, len, decc = layer.table.field_info(i)  ID_PROPERTY_TITLE = 4012
674              if name == field:  ID_PROPERTY_FIELDTEXT = 4013
675                  self.__cur_prop = i  
676              self.properties.Append(name)  BTN_ADD = 0
677              self.properties.SetClientData(i, None)  BTN_EDIT = 1
678    BTN_GEN = 2
679          self.properties.SetSelection(self.__cur_prop)  BTN_UP = 3
680          propertyBox.Add(self.properties, 1, wxGROW|wxALL, 0)  BTN_DOWN = 4
681          EVT_COMBOBOX(self, ID_PROPERTY_SELECT, self.OnPropertySelect)  BTN_RM = 5
682    
683    EB_LAYER_TITLE = 0
684    EB_SELECT_FIELD = 1
685    EB_GEN_CLASS = 2
686    
687    class Classifier(LayerProperties):
688    
689        type2string = {None:             _("None"),
690                       FIELDTYPE_STRING: _("Text"),
691                       FIELDTYPE_INT:    _("Integer"),
692                       FIELDTYPE_DOUBLE: _("Decimal")}
693    
694        def __init__(self, parent, name, layer, group = None):
695            """Create a Properties/Classification dialog for a layer.
696            The layer is part of map. If group is not None, select that
697            group in the classification table.
698            """
699    
700          topBox.Add(propertyBox, 0, wxGROW, 4)          LayerProperties.__init__(self, parent, name, layer)
701    
702          #          self.layer.Subscribe(LAYER_SHAPESTORE_REPLACED,
703          # Classification data table                               self.layer_shapestore_replaced)
         #  
704    
705          controlBox = wxBoxSizer(wxHORIZONTAL)          self.genDlg = None
706          self.classGrid = ClassGrid(self, layer)          self.group = group
707    
708          controlBox.Add(self.classGrid, 1, wxGROW, 0)          LayerProperties.dialog_layout(self)
709    
710        def dialog_layout(self, panel, panelBox):
711    
712            if self.layer.HasClassification():
713    
714                self.fieldTypeText = wxStaticText(panel, -1, "")
715    
716                self.originalClass = self.layer.GetClassification()
717                self.originalClassField = self.layer.GetClassificationColumn()
718                field = self.originalClassField
719                fieldType = self.layer.GetFieldType(field)
720    
721                table = self.layer.ShapeStore().Table()
722                #
723                # make field choice box
724                #
725                self.fields = wxChoice(panel, ID_PROPERTY_SELECT,)
726    
727                self.num_cols = table.NumColumns()
728                # just assume the first field in case one hasn't been
729                # specified in the file.
730                self.__cur_field = 0
731    
732                self.fields.Append("<None>")
733    
734                if fieldType is None:
735                    self.fields.SetClientData(0, copy.deepcopy(self.originalClass))
736                else:
737                    self.fields.SetClientData(0, None)
738    
739                for i in range(self.num_cols):
740                    name = table.Column(i).name
741                    self.fields.Append(name)
742    
743                    if name == field:
744                        self.__cur_field = i + 1
745                        self.fields.SetClientData(i + 1,
746                                                  copy.deepcopy(self.originalClass))
747                    else:
748                        self.fields.SetClientData(i + 1, None)
749    
750                button_gen = wxButton(panel, ID_PROPERTY_GENCLASS,
751                    _("Generate Class"))
752                button_add = wxButton(panel, ID_PROPERTY_ADD,
753                    _("Add"))
754                button_moveup = wxButton(panel, ID_PROPERTY_MOVEUP,
755                    _("Move Up"))
756                button_movedown = wxButton(panel, ID_PROPERTY_MOVEDOWN,
757                    _("Move Down"))
758                button_edit = wxButton(panel, ID_PROPERTY_EDITSYM,
759                    _("Edit Symbol"))
760                button_remove = wxButton(panel, ID_PROPERTY_REMOVE,
761                    _("Remove"))
762    
763                self.classGrid = ClassGrid(panel, self)
764    
765                # calling __SelectField after creating the classGrid fills in the
766                # grid with the correct information
767                self.fields.SetSelection(self.__cur_field)
768                self.__SelectField(self.__cur_field, group = self.group)
769    
770    
771                classBox = wxStaticBoxSizer(
772                            wxStaticBox(panel, -1, _("Classification")), wxVERTICAL)
773    
774    
775                sizer = wxBoxSizer(wxHORIZONTAL)
776                sizer.Add(wxStaticText(panel, ID_PROPERTY_FIELDTEXT, _("Field: ")),
777                    0, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL | wxALL, 4)
778                sizer.Add(self.fields, 1, wxGROW | wxALL, 4)
779    
780                classBox.Add(sizer, 0, wxGROW, 4)
781    
782                classBox.Add(self.fieldTypeText, 0,
783                            wxGROW | wxALIGN_LEFT | wxALL | wxADJUST_MINSIZE, 4)
784    
785                controlBox = wxBoxSizer(wxHORIZONTAL)
786                controlButtonBox = wxBoxSizer(wxVERTICAL)
787    
788                controlButtonBox.Add(button_gen, 0, wxGROW|wxALL, 4)
789                controlButtonBox.Add(button_add, 0, wxGROW|wxALL, 4)
790                controlButtonBox.Add(button_moveup, 0, wxGROW|wxALL, 4)
791                controlButtonBox.Add(button_movedown, 0, wxGROW|wxALL, 4)
792                controlButtonBox.Add(button_edit, 0, wxGROW|wxALL, 4)
793                controlButtonBox.Add(60, 20, 0, wxGROW|wxALL|wxALIGN_BOTTOM, 4)
794                controlButtonBox.Add(button_remove, 0,
795                                     wxGROW|wxALL|wxALIGN_BOTTOM, 4)
796    
797                controlBox.Add(self.classGrid, 1, wxGROW, 0)
798                controlBox.Add(controlButtonBox, 0, wxGROW, 10)
799    
800                classBox.Add(controlBox, 1, wxGROW, 10)
801                panelBox.Add(classBox, 1, wxGROW, 0)
802    
803    
804            EVT_CHOICE(self, ID_PROPERTY_SELECT, self._OnFieldSelect)
805            EVT_BUTTON(self, ID_PROPERTY_ADD, self._OnAdd)
806            EVT_BUTTON(self, ID_PROPERTY_EDITSYM, self._OnEditSymbol)
807            EVT_BUTTON(self, ID_PROPERTY_REMOVE, self._OnRemove)
808            EVT_BUTTON(self, ID_PROPERTY_GENCLASS, self._OnGenClass)
809            EVT_BUTTON(self, ID_PROPERTY_MOVEUP, self._OnMoveUp)
810            EVT_BUTTON(self, ID_PROPERTY_MOVEDOWN, self._OnMoveDown)
811    
812        def unsubscribe_messages(self):
813            """Unsubscribe from all messages."""
814            LayerProperties.unsubscribe_messages(self)
815            self.layer.Unsubscribe(LAYER_SHAPESTORE_REPLACED,
816                                   self.layer_shapestore_replaced)
817    
818        def layer_shapestore_replaced(self, *args):
819            """Subscribed to the map's LAYER_SHAPESTORE_REPLACED message.
820            Close self.
821            """
822            self.Close()
823    
824          controlButtonBox = wxBoxSizer(wxVERTICAL)      def EditSymbol(self, row):
825          controlButtonBox.Add(wxButton(self, ID_CLASSIFY_ADD,          """Open up a dialog where the user can select the properties
826              _("Add")), 0, wxGROW | wxALL, 4)          for a group.
827          controlButtonBox.Add(wxButton(self, ID_CLASSIFY_GENRANGE,          """
828              _("Generate Ranges")), 0, wxGROW | wxALL, 4)          table = self.classGrid.GetTable()
829            prop = table.GetValueAsCustom(row, COL_SYMBOL, None)
         controlBox.Add(controlButtonBox, 0, wxGROW, 10)  
         topBox.Add(controlBox, 1, wxGROW, 10)  
   
         EVT_BUTTON(self, ID_CLASSIFY_ADD, self.OnAdd)  
         EVT_BUTTON(self, ID_CLASSIFY_GENRANGE, self.OnGenRange)  
         EVT_GRID_CELL_LEFT_DCLICK(self.classGrid, self.OnCellDClick)  
830    
831          #          # get a new ClassGroupProperties object and copy the
832          # Control buttons:          # values over to our current object
833          #          propDlg = SelectPropertiesDialog(self, prop, self.layer.ShapeType())
834          buttonBox = wxBoxSizer(wxHORIZONTAL)  
835          buttonBox.Add(wxButton(self, ID_CLASSIFY_OK, _("OK")),          self.Enable(False)
836                        0, wxALL, 4)          if propDlg.ShowModal() == wxID_OK:
837          buttonBox.Add(wxButton(self, ID_CLASSIFY_CANCEL, _("Cancel")),              new_prop = propDlg.GetClassGroupProperties()
838                        0, wxALL, 4)              table.SetValueAsCustom(row, COL_SYMBOL, None, new_prop)
839          topBox.Add(buttonBox, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_BOTTOM, 10)          self.Enable(True)
840            propDlg.Destroy()
841    
842        def _SetClassification(self, clazz):
843            """Called from the ClassGen dialog when a new classification has
844            been created and should be set in the table.
845            """
846            # FIXME: This could be implemented using a message
847    
848          EVT_BUTTON(self, ID_CLASSIFY_OK, self.OnOK)          self.fields.SetClientData(self.__cur_field, clazz)
849          EVT_BUTTON(self, ID_CLASSIFY_CANCEL, self.OnCancel)          self.classGrid.GetTable().SetClassification(clazz)
850    
851          self.SetAutoLayout(true)      def __BuildClassification(self, fieldIndex, copyClass=False, force=False):
852          self.SetSizer(topBox)          """Pack the classification setting into a Classification object.
853          topBox.Fit(self)          Returns (Classification, fieldName) where fieldName is the selected
854          topBox.SetSizeHints(self)          field in the table that the classification should be used with.
855            """
856    
857      def __BuildClassification(self, prop):  #       numRows = self.classGrid.GetNumberRows()
858    #       assert numRows > 0  # there should always be a default row
859    
860          clazz = Classification()          if fieldIndex == 0:
861          clazz.SetField(self.properties.GetStringSelection())              fieldName = None
862                fieldType = None
863            else:
864                fieldName = self.fields.GetString(fieldIndex)
865                fieldType = self.layer.GetFieldType(fieldName)
866    
867            clazz = self.fields.GetClientData(fieldIndex)
868            if clazz is None or self.classGrid.GetTable().IsModified() or force:
869                clazz = self.classGrid.GetTable().GetClassification()
870                if copyClass:
871                    clazz = copy.deepcopy(clazz)
872    
873            return clazz, fieldName
874    
875        def __SetGridTable(self, fieldIndex, group = None):
876            """Set the table with the classification associated with the
877            selected field at fieldIndex. Select the specified group
878            if group is not None.
879            """
880    
881          numRows = self.classGrid.GetNumberRows()          clazz = self.fields.GetClientData(fieldIndex)
882    
883          if numRows > 0:          if clazz is None:
884              table = self.classGrid.GetTable()              clazz = Classification()
885              clazz.SetDefaultGroup(table.GetClassGroup(0))              clazz.SetDefaultGroup(
886                    ClassGroupDefault(
887                        self.layer.GetClassification().
888                                   GetDefaultGroup().GetProperties()))
889    
890            fieldName = self.fields.GetString(fieldIndex)
891            fieldType = self.layer.GetFieldType(fieldName)
892                    
893            self.classGrid.CreateTable(clazz, fieldType,
894                                       self.layer.ShapeType(), group)
895    
896        def __SetFieldTypeText(self, fieldIndex):
897            """Set the field type string using the data type of the field
898            at fieldIndex.
899            """
900            fieldName = self.fields.GetString(fieldIndex)
901            fieldType = self.layer.GetFieldType(fieldName)
902    
903              for i in range(1, numRows):          assert Classifier.type2string.has_key(fieldType)
                 clazz.AddGroup(table.GetClassGroup(i))  
904    
905          return clazz          text = Classifier.type2string[fieldType]
906    
907      def OnPropertySelect(self, event):          self.fieldTypeText.SetLabel(_("Data Type: %s") % text)
         self.properties.SetClientData(  
             self.__cur_prop, self.__BuildClassification(self.__cur_prop))  
908    
909          self.__cur_prop = self.properties.GetSelection()      def __SelectField(self, newIndex, oldIndex = -1, group = None):
910          clazz = self.properties.GetClientData(self.__cur_prop)          """This method assumes that the current selection for the
911          table = self.classGrid.GetTable()          combo has already been set by a call to SetSelection().
912            """
913    
914          table.Reset(clazz, self.layer.ShapeType())          assert oldIndex >= -1
915    
916      def OnOK(self, event):          if oldIndex != -1:
917                clazz, name = self.__BuildClassification(oldIndex, force = True)
918                self.fields.SetClientData(oldIndex, clazz)
919    
920            self.__SetGridTable(newIndex, group)
921    
922            self.__EnableButtons(EB_SELECT_FIELD)
923    
924            self.__SetFieldTypeText(newIndex)
925    
926        def __SetTitle(self, title):
927            """Set the title of the dialog."""
928            if title != "":
929                title = ": " + title
930    
931            self.SetTitle(_("Layer Properties") + title)
932    
933        def _OnEditSymbol(self, event):
934            """Open up a dialog for the user to select group properties."""
935            sel = self.classGrid.GetCurrentSelection()
936    
937            if len(sel) == 1:
938                self.EditSymbol(sel[0])
939    
940        def _OnFieldSelect(self, event):
941            index = self.fields.GetSelection()
942            self.__SelectField(index, self.__cur_field)
943            self.__cur_field = index
944    
945        def OnTry(self, event):
946          """Put the data from the table into a new Classification and hand          """Put the data from the table into a new Classification and hand
947             it to the layer.             it to the layer.
948          """          """
949    
950          clazz = self.properties.GetClientData(self.__cur_prop)          if self.layer.HasClassification():
951                clazz = self.fields.GetClientData(self.__cur_field)
952    
953          #              #
954          # only build the classification if there wasn't one to              # only build the classification if there wasn't one to
955          # to begin with or it has been modified              # to begin with or it has been modified
956          #              #
957          if clazz is None or self.classGrid.GetTable().IsModified():              self.classGrid.SaveEditControlValue()
958              clazz = self.__BuildClassification(self.__cur_prop)              clazz, name = self.__BuildClassification(self.__cur_field, True)
959    
960          clazz.SetLayer(self.layer)              self.layer.SetClassificationColumn(name)
961                self.layer.SetClassification(clazz)
962    
963          self.layer.SetClassification(clazz)          self.haveApplied = True
964    
965          self.EndModal(wxID_OK)      def OnOK(self, event):
966            self.OnTry(event)
967            self.Close()
968    
969      def OnCancel(self, event):      def OnRevert(self, event):
970          """Do nothing. The layer's current classification stays the same."""          """The layer's current classification stays the same."""
971          self.EndModal(wxID_CANCEL)          if self.haveApplied and self.layer.HasClassification():
972                self.layer.SetClassificationColumn(self.originalClassField)
973                self.layer.SetClassification(self.originalClass)
974    
975            #self.Close()
976    
977      def OnAdd(self, event):      def _OnAdd(self, event):
978          self.classGrid.GetTable().AddNewDataRow()          self.classGrid.AppendRows()
         print "Classifier.OnAdd()"  
979    
980      def OnGenRange(self, event):      def _OnRemove(self, event):
981          print "Classifier.OnGenRange()"          self.classGrid.DeleteSelectedRows()
982    
983      def OnCellDClick(self, event):      def _OnGenClass(self, event):
984          r = event.GetRow()          """Open up a dialog for the user to generate classifications."""
985          c = event.GetCol()  
986          if c == COL_VISUAL:          self.genDlg = ClassGenDialog(self, self.layer,
987              # XXX: getting the properties is only possible with non-Maps!!!                            self.fields.GetString(self.__cur_field))
988              group = self.classGrid.GetTable().GetValueAsCustom(r, c, None)  
989              prop = group.GetProperties()          EVT_CLOSE(self.genDlg, self._OnGenDialogClose)
990              propDlg = SelectPropertiesDialog(NULL, prop, self.layer.ShapeType())  
991              if propDlg.ShowModal() == wxID_OK:          self.__EnableButtons(EB_GEN_CLASS)
992                  new_prop = propDlg.GetClassGroupProperties()  
993                  prop.SetStroke(new_prop.GetStroke())          self.genDlg.Show()
994                  prop.SetStrokeWidth(new_prop.GetStrokeWidth())  
995                  prop.SetFill(new_prop.GetFill())      def _OnGenDialogClose(self, event):
996                  self.classGrid.Refresh()          """Reenable buttons after the generate classification
997              propDlg.Destroy()          dialog is closed.
998            """
999            self.genDlg.Destroy()
1000  ID_SELPROP_OK = 4001          self.genDlg = None
1001  ID_SELPROP_CANCEL = 4002          self.__EnableButtons(EB_GEN_CLASS)
1002  ID_SELPROP_SPINCTRL = 4002  
1003        def _OnMoveUp(self, event):
1004            """When the user clicks MoveUp, try to move a group up one row."""
1005            sel = self.classGrid.GetCurrentSelection()
1006    
1007            if len(sel) == 1:
1008                i = sel[0]
1009                if i > 1:
1010                    table = self.classGrid.GetTable()
1011                    x = table.GetClassGroup(i - 1)
1012                    y = table.GetClassGroup(i)
1013                    table.SetClassGroup(i - 1, y)
1014                    table.SetClassGroup(i, x)
1015                    self.classGrid.ClearSelection()
1016                    self.classGrid.SelectRow(i - 1)
1017                    self.classGrid.MakeCellVisible(i - 1, 0)
1018    
1019        def _OnMoveDown(self, event):
1020            """When the user clicks MoveDown, try to move a group down one row."""
1021            sel = self.classGrid.GetCurrentSelection()
1022    
1023            if len(sel) == 1:
1024                i = sel[0]
1025                table = self.classGrid.GetTable()
1026                if 0 < i < table.GetNumberRows() - 1:
1027                    x = table.GetClassGroup(i)
1028                    y = table.GetClassGroup(i + 1)
1029                    table.SetClassGroup(i, y)
1030                    table.SetClassGroup(i + 1, x)
1031                    self.classGrid.ClearSelection()
1032                    self.classGrid.SelectRow(i + 1)
1033                    self.classGrid.MakeCellVisible(i + 1, 0)
1034    
1035        def _OnTitleChanged(self, event):
1036            """Update the dialog title when the user changed the layer name."""
1037            obj = event.GetEventObject()
1038    
1039            self.layer.SetTitle(obj.GetValue())
1040            self.__SetTitle(self.layer.Title())
1041    
1042            self.__EnableButtons(EB_LAYER_TITLE)
1043    
1044        def __EnableButtons(self, case):
1045            """Helper method that enables/disables the appropriate buttons
1046            based on the case provided. Cases are constants beginning with EB_.
1047            """
1048    
1049            list = {wxID_OK                 : True,
1050                    wxID_CANCEL             : True,
1051                    ID_PROPERTY_ADD         : True,
1052                    ID_PROPERTY_MOVEUP      : True,
1053                    ID_PROPERTY_MOVEDOWN    : True,
1054                    ID_PROPERTY_REMOVE      : True,
1055                    ID_PROPERTY_SELECT      : True,
1056                    ID_PROPERTY_FIELDTEXT   : True,
1057                    ID_PROPERTY_GENCLASS    : True,
1058                    ID_PROPERTY_EDITSYM     : True}
1059    
1060            if case == EB_LAYER_TITLE:  
1061                if self.layer.Title() == "":
1062                    list[wxID_OK] = False
1063                    list[wxID_CANCEL] = False
1064    
1065            elif case == EB_SELECT_FIELD:
1066                if self.fields.GetSelection() == 0:
1067                    list[ID_PROPERTY_GENCLASS] = False
1068                    list[ID_PROPERTY_ADD] = False
1069                    list[ID_PROPERTY_MOVEUP] = False
1070                    list[ID_PROPERTY_MOVEDOWN] = False
1071                    list[ID_PROPERTY_REMOVE] = False
1072    
1073            elif case == EB_GEN_CLASS:
1074                if self.genDlg is not None:
1075                    list[ID_PROPERTY_SELECT] = False
1076                    list[ID_PROPERTY_FIELDTEXT] = False
1077                    list[ID_PROPERTY_GENCLASS] = False
1078    
1079            for id, enable in list.items():
1080                win = self.FindWindowById(id)
1081                if win:
1082                    win.Enable(enable)
1083    
1084    ID_SELPROP_SPINCTRL_LINEWIDTH = 4002
1085  ID_SELPROP_PREVIEW = 4003  ID_SELPROP_PREVIEW = 4003
1086  ID_SELPROP_STROKECLR = 4004  ID_SELPROP_STROKECLR = 4004
1087  ID_SELPROP_FILLCLR = 4005  ID_SELPROP_FILLCLR = 4005
1088    ID_SELPROP_STROKECLRTRANS = 4006
1089    ID_SELPROP_FILLCLRTRANS = 4007
1090    ID_SELPROP_SPINCTRL_SIZE = 4008
1091    
1092  class SelectPropertiesDialog(wxDialog):  class SelectPropertiesDialog(wxDialog):
1093        """Dialog that allows the user to select group properties."""
1094    
1095      def __init__(self, parent, prop, shapeType):      def __init__(self, parent, prop, shapeType):
1096            """Open the dialog with the initial prop properties and shapeType."""
1097    
1098          wxDialog.__init__(self, parent, -1, _("Select Properties"),          wxDialog.__init__(self, parent, -1, _("Select Properties"),
1099                            style = wxRESIZE_BORDER)                            style = wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
1100    
1101          self.prop = ClassGroupProperties(prop)          self.prop = ClassGroupProperties(prop)
1102    
# Line 445  class SelectPropertiesDialog(wxDialog): Line 1108  class SelectPropertiesDialog(wxDialog):
1108          previewBox = wxBoxSizer(wxVERTICAL)          previewBox = wxBoxSizer(wxVERTICAL)
1109          previewBox.Add(wxStaticText(self, -1, _("Preview:")),          previewBox.Add(wxStaticText(self, -1, _("Preview:")),
1110              0, wxALIGN_LEFT | wxALL, 4)              0, wxALIGN_LEFT | wxALL, 4)
1111          self.previewer = ClassDataPreviewer(None, self.prop, shapeType,  
1112                                              self, ID_SELPROP_PREVIEW, (40, 40))          self.previewWin = ClassGroupPropertiesCtrl(
1113          previewBox.Add(self.previewer, 1, wxGROW, 15)              self, ID_SELPROP_PREVIEW, self.prop, shapeType,
1114                (40, 40), wxSIMPLE_BORDER)
1115    
1116            self.previewWin.AllowEdit(False)
1117    
1118            previewBox.Add(self.previewWin, 1, wxGROW | wxALL, 4)
1119    
1120          itemBox.Add(previewBox, 1, wxALIGN_LEFT | wxALL | wxGROW, 0)          itemBox.Add(previewBox, 1, wxALIGN_LEFT | wxALL | wxGROW, 0)
1121    
1122          # control box          # control box
1123          ctrlBox = wxBoxSizer(wxVERTICAL)          ctrlBox = wxBoxSizer(wxVERTICAL)
1124          ctrlBox.Add(  
1125              wxButton(self, ID_SELPROP_STROKECLR, "Change Stroke Color"),          lineColorBox = wxBoxSizer(wxHORIZONTAL)
1126              0, wxALIGN_CENTER_HORIZONTAL | wxALL | wxGROW, 4)          button = wxButton(self, ID_SELPROP_STROKECLR, _("Change Line Color"))
1127          EVT_BUTTON(self, ID_SELPROP_STROKECLR, self.OnChangeStrokeColor)          button.SetFocus()
1128            lineColorBox.Add(button, 1, wxALL | wxGROW, 4)
1129            EVT_BUTTON(self, ID_SELPROP_STROKECLR, self._OnChangeLineColor)
1130    
1131            lineColorBox.Add(
1132                wxButton(self, ID_SELPROP_STROKECLRTRANS, _("Transparent")),
1133                1, wxALL | wxGROW, 4)
1134            EVT_BUTTON(self, ID_SELPROP_STROKECLRTRANS,
1135                       self._OnChangeLineColorTrans)
1136    
1137            ctrlBox.Add(lineColorBox, 0,
1138                        wxALIGN_CENTER_HORIZONTAL | wxALL | wxGROW, 4)
1139    
1140          if shapeType != SHAPETYPE_ARC:          if shapeType != SHAPETYPE_ARC:
1141              ctrlBox.Add(              fillColorBox = wxBoxSizer(wxHORIZONTAL)
1142                  wxButton(self, ID_SELPROP_FILLCLR, "Change Fill Color"),              fillColorBox.Add(
1143                  0, wxALIGN_LEFT | wxALL | wxGROW, 4)                  wxButton(self, ID_SELPROP_FILLCLR, _("Change Fill Color")),
1144              EVT_BUTTON(self, ID_SELPROP_FILLCLR, self.OnChangeFillColor)                  1, wxALL | wxGROW, 4)
1145                EVT_BUTTON(self, ID_SELPROP_FILLCLR, self._OnChangeFillColor)
1146                fillColorBox.Add(
1147                    wxButton(self, ID_SELPROP_FILLCLRTRANS, _("Transparent")),
1148                    1, wxALL | wxGROW, 4)
1149                EVT_BUTTON(self, ID_SELPROP_FILLCLRTRANS,
1150                           self._OnChangeFillColorTrans)
1151                ctrlBox.Add(fillColorBox, 0,
1152                            wxALIGN_CENTER_HORIZONTAL | wxALL | wxGROW, 4)
1153    
1154            # Line width selection
1155          spinBox = wxBoxSizer(wxHORIZONTAL)          spinBox = wxBoxSizer(wxHORIZONTAL)
1156          spinBox.Add(wxStaticText(self, -1, _("Stroke Width: ")),          spinBox.Add(wxStaticText(self, -1, _("Line Width: ")),
1157                  0, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL | wxALL, 4)                  0, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL | wxALL, 4)
1158          self.spinCtrl = wxSpinCtrl(self, ID_SELPROP_SPINCTRL,          self.spinCtrl_linewidth = wxSpinCtrl(self,
1159                                     min=1, max=10,                                               ID_SELPROP_SPINCTRL_LINEWIDTH,
1160                                     value=str(prop.GetStrokeWidth()),                                               min=1, max=10,
1161                                     initial=prop.GetStrokeWidth())                                               value=str(prop.GetLineWidth()),
1162                                                 initial=prop.GetLineWidth())
1163    
1164          EVT_SPINCTRL(self, ID_SELPROP_SPINCTRL, self.OnSpin)          EVT_SPINCTRL(self, ID_SELPROP_SPINCTRL_LINEWIDTH,
1165                         self._OnSpinLineWidth)
         spinBox.Add(self.spinCtrl, 0, wxALIGN_LEFT | wxALL, 4)  
1166    
1167            spinBox.Add(self.spinCtrl_linewidth, 0, wxALIGN_LEFT | wxALL, 4)
1168          ctrlBox.Add(spinBox, 0, wxALIGN_RIGHT | wxALL, 0)          ctrlBox.Add(spinBox, 0, wxALIGN_RIGHT | wxALL, 0)
1169    
1170            # Size selection
1171            if shapeType == SHAPETYPE_POINT:
1172                spinBox = wxBoxSizer(wxHORIZONTAL)
1173                spinBox.Add(wxStaticText(self, -1, _("Size: ")),
1174                            0, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL | wxALL, 4)
1175                self.spinCtrl_size = wxSpinCtrl(self, ID_SELPROP_SPINCTRL_SIZE,
1176                                                min=1, max=100,
1177                                                value=str(prop.GetSize()),
1178                                                initial=prop.GetSize())
1179    
1180                EVT_SPINCTRL(self, ID_SELPROP_SPINCTRL_SIZE, self._OnSpinSize)
1181    
1182                spinBox.Add(self.spinCtrl_size, 0, wxALIGN_LEFT | wxALL, 4)
1183                ctrlBox.Add(spinBox, 0, wxALIGN_RIGHT | wxALL, 0)
1184    
1185    
1186          itemBox.Add(ctrlBox, 0, wxALIGN_RIGHT | wxALL | wxGROW, 0)          itemBox.Add(ctrlBox, 0, wxALIGN_RIGHT | wxALL | wxGROW, 0)
1187          topBox.Add(itemBox, 1, wxALIGN_LEFT | wxALL | wxGROW, 0)          topBox.Add(itemBox, 1, wxALIGN_LEFT | wxALL | wxGROW, 0)
1188    
   
1189          #          #
1190          # Control buttons:          # Control buttons:
1191          #          #
1192          buttonBox = wxBoxSizer(wxHORIZONTAL)          buttonBox = wxBoxSizer(wxHORIZONTAL)
1193          buttonBox.Add(wxButton(self, ID_CLASSIFY_OK, _("OK")),          button_ok = wxButton(self, wxID_OK, _("OK"))
1194                        0, wxALL, 4)          buttonBox.Add(button_ok, 0, wxRIGHT|wxEXPAND, 10)
1195          buttonBox.Add(wxButton(self, ID_CLASSIFY_CANCEL, _("Cancel")),          buttonBox.Add(wxButton(self, wxID_CANCEL, _("Cancel")),
1196                        0, wxALL, 4)                        0, wxRIGHT|wxEXPAND, 10)
1197          topBox.Add(buttonBox, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_BOTTOM, 10)          topBox.Add(buttonBox, 0, wxALIGN_RIGHT|wxBOTTOM|wxTOP, 10)
1198                                                                                    
1199          EVT_BUTTON(self, ID_SELPROP_OK, self.OnOK)          button_ok.SetDefault()
1200          EVT_BUTTON(self, ID_SELPROP_CANCEL, self.OnCancel)  
1201                                                                                            #EVT_BUTTON(self, wxID_OK, self._OnOK)
1202          self.SetAutoLayout(true)          #EVT_BUTTON(self, ID_SELPROP_CANCEL, self._OnCancel)
1203    
1204            self.SetAutoLayout(True)
1205          self.SetSizer(topBox)          self.SetSizer(topBox)
1206          topBox.Fit(self)          topBox.Fit(self)
1207          topBox.SetSizeHints(self)          topBox.SetSizeHints(self)
# Line 505  class SelectPropertiesDialog(wxDialog): Line 1212  class SelectPropertiesDialog(wxDialog):
1212      def OnCancel(self, event):      def OnCancel(self, event):
1213          self.EndModal(wxID_CANCEL)          self.EndModal(wxID_CANCEL)
1214    
1215      def OnSpin(self, event):      def _OnSpinLineWidth(self, event):
1216          self.prop.SetStrokeWidth(self.spinCtrl.GetValue())          self.prop.SetLineWidth(self.spinCtrl_linewidth.GetValue())
1217          self.previewer.Refresh()          self.previewWin.Refresh()
1218    
1219        def _OnSpinSize(self, event):
1220            self.prop.SetSize(self.spinCtrl_size.GetValue())
1221            self.previewWin.Refresh()
1222    
1223      def __GetColor(self, cur):      def __GetColor(self, cur):
1224          dialog = wxColourDialog(self)          dialog = ColorDialog(self)
1225          dialog.GetColourData().SetColour(Color2wxColour(cur))          dialog.SetColor(cur)
1226    
1227          ret = None          ret = None
1228          if dialog.ShowModal() == wxID_OK:          if dialog.ShowModal() == wxID_OK:
1229              ret = wxColour2Color(dialog.GetColourData().GetColour())              ret = dialog.GetColor()
1230    
1231          dialog.Destroy()          dialog.Destroy()
1232    
1233          return ret          return ret
1234            
1235      def OnChangeStrokeColor(self, event):      def _OnChangeLineColor(self, event):
1236          clr = self.__GetColor(self.prop.GetStroke())          clr = self.__GetColor(self.prop.GetLineColor())
1237          if clr is not None:          if clr is not None:
1238              self.prop.SetStroke(clr)              self.prop.SetLineColor(clr)
1239          self.previewer.Refresh() # XXX: work around, see ClassDataPreviewer          self.previewWin.Refresh() # XXX: work around, see ClassDataPreviewer
1240    
1241        def _OnChangeLineColorTrans(self, event):
1242            self.prop.SetLineColor(Transparent)
1243            self.previewWin.Refresh() # XXX: work around, see ClassDataPreviewer
1244    
1245      def OnChangeFillColor(self, event):      def _OnChangeFillColor(self, event):
1246          clr = self.__GetColor(self.prop.GetFill())          clr = self.__GetColor(self.prop.GetFill())
1247          if clr is not None:          if clr is not None:
1248              self.prop.SetFill(clr)              self.prop.SetFill(clr)
1249          self.previewer.Refresh() # XXX: work around, see ClassDataPreviewer          self.previewWin.Refresh() # XXX: work around, see ClassDataPreviewer
1250    
1251        def _OnChangeFillColorTrans(self, event):
1252            self.prop.SetFill(Transparent)
1253            self.previewWin.Refresh() # XXX: work around, see ClassDataPreviewer
1254    
1255      def GetClassGroupProperties(self):      def GetClassGroupProperties(self):
1256          return self.prop          return self.prop
1257    
1258    
1259  class ClassDataPreviewer(wxWindow):  class ClassDataPreviewWindow(wxWindow):
1260        """A custom window that draws group properties using the correct shape."""
1261    
1262      def __init__(self, rect, prop, shapeType,      def __init__(self, rect, prop, shapeType,
1263                         parent = None, id = -1, size = wxDefaultSize):                         parent = None, id = -1, size = wxDefaultSize):
1264            """Draws the appropriate shape as specified with shapeType using
1265            prop properities.
1266            """
1267          if parent is not None:          if parent is not None:
1268              wxWindow.__init__(self, parent, id, size=size)              wxWindow.__init__(self, parent, id, (0, 0), size)
1269              EVT_PAINT(self, self.OnPaint)              EVT_PAINT(self, self._OnPaint)
1270    
1271          self.rect = rect          self.rect = rect
1272    
1273          self.prop = prop          self.prop = prop
1274          self.shapeType = shapeType          self.shapeType = shapeType
1275            self.previewer = ClassDataPreviewer()
1276    
1277        def GetProperties():
1278            return self.prop
1279    
1280      def OnPaint(self, event):      def _OnPaint(self, event):
1281          dc = wxPaintDC(self)          dc = wxPaintDC(self)
1282    
1283          # XXX: this doesn't seem to be having an effect:          # XXX: this doesn't seem to be having an effect:
1284          dc.DestroyClippingRegion()          dc.DestroyClippingRegion()
1285    
1286          self.Draw(dc, None)          if self.rect is None:
1287                w, h = self.GetSize()
1288                rect = wxRect(0, 0, w, h)
1289            else:
1290                rect = self.rect
1291    
1292            self.previewer.Draw(dc, rect, self.prop, self.shapeType)
1293    
1294    class ClassDataPreviewer:
1295        """Class that actually draws a group property preview."""
1296    
1297      def Draw(self, dc, rect, prop = None, shapeType = None):      def Draw(self, dc, rect, prop, shapeType):
1298            """Draw the property.
1299    
1300          if prop is None: prop = self.prop          returns: (w, h) as adapted extend if the drawing size
1301          if shapeType is None: shapeType = self.shapeType          exceeded the given rect. This can only be the case
1302            for point symbols. If the symbol fits the given rect,
1303            None is returned.
1304            """
1305    
1306            assert dc is not None
1307            assert isinstance(prop, ClassGroupProperties)
1308    
1309          if rect is None:          if rect is None:
1310              x = y = 0              x = 0
1311              w, h = self.GetClientSizeTuple()              y = 0
1312                w, h = dc.GetSize()
1313          else:          else:
1314              x = rect.GetX()              x = rect.GetX()
1315              y = rect.GetY()              y = rect.GetY()
1316              w = rect.GetWidth()              w = rect.GetWidth()
1317              h = rect.GetHeight()              h = rect.GetHeight()
1318    
1319          stroke = prop.GetStroke()          stroke = prop.GetLineColor()
1320          if stroke is Color.None:          if stroke is Transparent:
1321              pen = wxTRANSPARENT_PEN              pen = wxTRANSPARENT_PEN
1322          else:          else:
1323              pen = wxPen(Color2wxColour(stroke),              pen = wxPen(Color2wxColour(stroke),
1324                          prop.GetStrokeWidth(),                          prop.GetLineWidth(),
1325                          wxSOLID)                          wxSOLID)
1326    
1327          stroke = prop.GetFill()          stroke = prop.GetFill()
1328          if stroke is Color.None:          if stroke is Transparent:
1329              brush = wxTRANSPARENT_BRUSH              brush = wxTRANSPARENT_BRUSH
1330          else:          else:
1331              brush = wxBrush(Color2wxColour(stroke), wxSOLID)              brush = wxBrush(Color2wxColour(stroke), wxSOLID)
# Line 593  class ClassDataPreviewer(wxWindow): Line 1339  class ClassDataPreviewer(wxWindow):
1339                             wxPoint(x + w/2, y + h/4*3),                             wxPoint(x + w/2, y + h/4*3),
1340                             wxPoint(x + w, y)])                             wxPoint(x + w, y)])
1341    
1342          elif shapeType == SHAPETYPE_POINT or \          elif shapeType == SHAPETYPE_POINT:
1343               shapeType == SHAPETYPE_POLYGON:  
1344                dc.DrawCircle(x + w/2, y + h/2, prop.GetSize())
1345                circle_size =  prop.GetSize() * 2 + prop.GetLineWidth() * 2
1346                new_h = h
1347                new_w = w
1348                if h < circle_size: new_h = circle_size
1349                if w < circle_size: new_w = circle_size
1350                if new_h > h or new_w > w:
1351                    return (new_w, new_h)
1352    
1353            elif shapeType == SHAPETYPE_POLYGON:
1354                dc.DrawRectangle(x, y, w, h)
1355    
1356              dc.DrawCircle(x + w/2, y + h/2,          return None
                           (min(w, h) - prop.GetStrokeWidth())/2)  
1357    
1358  class ClassRenderer(wxPyGridCellRenderer):  class ClassRenderer(wxPyGridCellRenderer):
1359        """A wrapper class that can be used to draw group properties in a
1360        grid table.
1361        """
1362    
1363      def __init__(self, shapeType):      def __init__(self, shapeType):
1364          wxPyGridCellRenderer.__init__(self)          wxPyGridCellRenderer.__init__(self)
1365          self.previewer = ClassDataPreviewer(None, None, shapeType)          self.shapeType = shapeType
1366            self.previewer = ClassDataPreviewer()
1367    
1368      def Draw(self, grid, attr, dc, rect, row, col, isSelected):      def Draw(self, grid, attr, dc, rect, row, col, isSelected):
1369          data = grid.GetTable().GetValueAsCustom(row, col, "")          data = grid.GetTable().GetClassGroup(row)
   
1370    
1371          dc.SetClippingRegion(rect.GetX(), rect.GetY(),          dc.SetClippingRegion(rect.GetX(), rect.GetY(),
1372                               rect.GetWidth(), rect.GetHeight())                               rect.GetWidth(), rect.GetHeight())
# Line 617  class ClassRenderer(wxPyGridCellRenderer Line 1376  class ClassRenderer(wxPyGridCellRenderer
1376                           rect.GetWidth(), rect.GetHeight())                           rect.GetWidth(), rect.GetHeight())
1377    
1378          if not isinstance(data, ClassGroupMap):          if not isinstance(data, ClassGroupMap):
1379              self.previewer.Draw(dc, rect, data.GetProperties())              new_size = self.previewer.Draw(dc, rect, data.GetProperties(),
1380                                               self.shapeType)
1381                if new_size is not None:
1382                    (new_w, new_h) = new_size
1383                    grid.SetRowSize(row, new_h)
1384                    grid.SetColSize(col, new_h)
1385                    grid.ForceRefresh()
1386    
1387                    # now that we know the height, redraw everything
1388                    rect.SetHeight(new_h)
1389                    rect.SetWidth(new_w)
1390                    dc.DestroyClippingRegion()
1391                    dc.SetClippingRegion(rect.GetX(), rect.GetY(),
1392                                         rect.GetWidth(), rect.GetHeight())
1393                    dc.SetPen(wxPen(wxLIGHT_GREY))
1394                    dc.SetBrush(wxBrush(wxLIGHT_GREY, wxSOLID))
1395                    dc.DrawRectangle(rect.GetX(), rect.GetY(),
1396                                     rect.GetWidth(), rect.GetHeight())
1397                    self.previewer.Draw(dc, rect, data.GetProperties(),
1398                                        self.shapeType)
1399    
1400          if isSelected:          if isSelected:
1401              dc.SetPen(wxPen(wxColour(0 * 255, 0 * 255, 0 * 255),              dc.SetPen(wxPen(wxBLACK, 1, wxSOLID))
                       4, wxSOLID))  
1402              dc.SetBrush(wxTRANSPARENT_BRUSH)              dc.SetBrush(wxTRANSPARENT_BRUSH)
1403    
1404              dc.DrawRectangle(rect.GetX(), rect.GetY(),              dc.DrawRectangle(rect.GetX(), rect.GetY(),
1405                               rect.GetWidth(), rect.GetHeight())                               rect.GetWidth(), rect.GetHeight())
1406    
1407          dc.DestroyClippingRegion()          dc.DestroyClippingRegion()
1408    
1409    
1410    class ClassGroupPropertiesCtrl(wxWindow, wxControl):
1411        """A custom window and control that draw a preview of group properties
1412        and can open a dialog to modify the properties if the user double-clicks
1413        it.
1414        """
1415    
1416        def __init__(self, parent, id, props, shapeType,
1417                     size = wxDefaultSize, style = 0):
1418    
1419            wxWindow.__init__(self, parent, id, size = size, style = style)
1420    
1421            self.parent = parent
1422    
1423            self.SetProperties(props)
1424            self.SetShapeType(shapeType)
1425            self.AllowEdit(True)
1426    
1427            EVT_PAINT(self, self._OnPaint)
1428            EVT_LEFT_DCLICK(self, self._OnLeftDClick)
1429    
1430            self.previewer = ClassDataPreviewer()
1431    
1432        def _OnPaint(self, event):
1433            dc = wxPaintDC(self)
1434    
1435            # XXX: this doesn't seem to be having an effect:
1436            dc.DestroyClippingRegion()
1437    
1438            w, h = self.GetClientSize()
1439    
1440            self.previewer.Draw(dc,
1441                                wxRect(0, 0, w, h),
1442                                self.GetProperties(),
1443                                self.GetShapeType())
1444    
1445    
1446        def GetProperties(self):
1447            return self.props
1448    
1449        def SetProperties(self, props):
1450            self.props = props
1451            self.Refresh()
1452    
1453        def GetShapeType(self):
1454            return self.shapeType
1455    
1456        def SetShapeType(self, shapeType):
1457            self.shapeType = shapeType
1458            self.Refresh()
1459    
1460        def AllowEdit(self, allow):
1461            """Allow/Disallow double-clicking on the control."""
1462            self.allowEdit = allow
1463    
1464        def DoEdit(self):
1465            """Open the properties selector dialog."""
1466    
1467            if not self.allowEdit: return
1468    
1469            propDlg = SelectPropertiesDialog(self.parent,
1470                                             self.GetProperties(),
1471                                             self.GetShapeType())
1472    
1473            if propDlg.ShowModal() == wxID_OK:
1474                new_prop = propDlg.GetClassGroupProperties()
1475                self.SetProperties(new_prop)
1476                self.Refresh()
1477    
1478            propDlg.Destroy()
1479    
1480        def _OnLeftDClick(self, event):
1481            self.DoEdit()
1482    

Legend:
Removed from v.441  
changed lines
  Added in v.2556

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26