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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 441 - (show annotations)
Thu Feb 27 15:55:00 2003 UTC (22 years ago) by jonathan
File MIME type: text/x-python
File size: 20597 byte(s)
Changes to use new Classification and Group functions.

1 # Copyright (c) 2001 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 """Dialog for classifying how layers are displayed"""
9
10 __version__ = "$Revision$"
11
12 import copy
13
14 from wxPython.wx import *
15 from wxPython.grid import *
16
17 from Thuban import _
18 from Thuban.common import *
19 from Thuban.UI.common import *
20
21 from Thuban.Model.classification import * #Classification, ClassData
22
23 from Thuban.Model.color import Color
24
25 from Thuban.Model.layer import SHAPETYPE_ARC, SHAPETYPE_POLYGON, SHAPETYPE_POINT
26
27 ID_PROPERTY_SELECT = 4010
28 ID_CLASS_TABLE = 40011
29
30 ID_CLASSIFY_OK = 4001
31 ID_CLASSIFY_CANCEL = 4002
32 ID_CLASSIFY_ADD = 4003
33 ID_CLASSIFY_GENRANGE = 4004
34
35 COL_VISUAL = 0
36 COL_VALUE = 1
37 COL_LABEL = 2
38
39 #
40 # this is a silly work around to ensure that the table that is
41 # passed into SetTable is the same that is returned by GetTable
42 #
43 import weakref
44 class ClassGrid(wxGrid):
45
46 def __init__(self, parent, layer):
47 wxGrid.__init__(self, parent, ID_CLASS_TABLE, size = (300, 150))
48 self.SetTable(
49 ClassTable(layer.GetClassification(), layer.ShapeType(), self),
50 true)
51
52 def SetCellRenderer(self, row, col):
53 raise ValueError(_("Must not allow setting of renderer in ClassGrid!"))
54
55 def SetTable(self, object, *attributes):
56 self.tableRef = weakref.ref(object)
57 return wxGrid.SetTable(self, object, *attributes)
58
59 def GetTable(self):
60 return self.tableRef()
61
62
63 class ClassTable(wxPyGridTableBase):
64
65 NUM_COLS = 3
66
67 __col_labels = [_("Visual"), _("Value"), _("Label")]
68
69 def __init__(self, clazz, shapeType, view = None):
70 wxPyGridTableBase.__init__(self)
71 self.SetView(view)
72 self.tdata = []
73
74 self.Reset(clazz, shapeType)
75
76 def Reset(self, clazz, shapeType):
77
78 self.GetView().BeginBatch()
79
80 self.shapeType = shapeType
81 self.renderer = ClassRenderer(self.shapeType)
82
83 old_tdata = self.tdata
84
85 self.tdata = []
86
87 if clazz is None:
88 clazz = Classification()
89
90 # p = clazz.GetDefaultGroup()
91 # np = ClassDataDefault(classData = p)
92 # self.tdata.append([np, 'DEFAULT', np.GetLabel()])
93
94 # for p in clazz.points.values():
95 # np = ClassDataPoint(p.GetValue(), classData = p)
96 # self.tdata.append([np, np.GetValue(), np.GetLabel()])
97
98 # for p in clazz.ranges:
99 # np = ClassDataRange(p.GetMin(), p.GetMax(), classData = p)
100 # self.tdata.append([np,
101 # '%s - %s' % (np.GetMin(), np.GetMax()),
102 # np.GetLabel()])
103
104 i = 0
105 for p in clazz:
106 np = copy.copy(p)
107 self.__SetRow(i, np)
108 i += 1
109
110
111 self.modified = 0
112
113 #
114 # silly message processing for updates to the number of
115 # rows and columns
116 #
117 curRows = len(old_tdata)
118 newRows = len(self.tdata)
119 if newRows > curRows:
120 msg = wxGridTableMessage(self,
121 wxGRIDTABLE_NOTIFY_ROWS_APPENDED,
122 newRows - curRows) # how many
123 self.GetView().ProcessTableMessage(msg)
124 elif newRows < curRows:
125 msg = wxGridTableMessage(self,
126 wxGRIDTABLE_NOTIFY_ROWS_DELETED,
127 curRows - newRows, # position
128 curRows - newRows) # how many
129 self.GetView().ProcessTableMessage(msg)
130
131 self.GetView().EndBatch()
132
133 def __SetRow(self, row, group):
134
135 if isinstance(group, ClassGroupDefault):
136 data = [group, 'DEFAULT', group.GetLabel()]
137 elif isinstance(group, ClassGroupSingleton):
138 data = [group, group.GetValue(), group.GetLabel()]
139 elif isinstance(group, ClassGroupRange):
140 data = [group,
141 '%s - %s' % (group.GetMin(), group.GetMax()),
142 group.GetLabel()]
143
144 if row >= len(self.tdata):
145 self.tdata.append(data)
146 else:
147 self.tdata[row] = data
148
149 def GetColLabelValue(self, col):
150 return self.__col_labels[col]
151
152 def GetRowLabelValue(self, row):
153 data = self.tdata[row][COL_VISUAL]
154 if isinstance(data, ClassGroupDefault): return _("Default")
155 if isinstance(data, ClassGroupSingleton): return _("Singleton")
156 if isinstance(data, ClassGroupRange): return _("Range")
157 if isinstance(data, ClassGroupMap): return _("Map")
158
159 def GetNumberRows(self):
160 return len(self.tdata)
161
162 def GetNumberCols(self):
163 return self.NUM_COLS
164
165 def IsEmptyCell(self, row, col):
166 return 0
167
168 def GetValue(self, row, col):
169 return self.GetValueAsCustom(row, col, "")
170
171 def SetValue(self, row, col, value):
172 self.SetValueAsCustom(row, col, "", value)
173 self.__Modified()
174
175 def GetValueAsCustom(self, row, col, typeName):
176 return self.tdata[row][col]
177
178 def __ParseInput(self, value):
179 """Try to determine what kind of input value is
180 (a single number or a range)
181 """
182
183 #
184 # first try to take the input as a single number
185 # if there's an exception try to break it into
186 # a range seperated by a '-'. take care to ignore
187 # a leading '-' as that could be for a negative number.
188 # then try to parse the individual parts. if there
189 # is an exception here, let it pass up to the calling
190 # function.
191 #
192 try:
193 return (Str2Num(value))
194 except:
195 i = value.find('-')
196 if i == 0:
197 i = value.find('-', 1)
198
199 return (Str2Num(value[:i]), Str2Num(value[i+1:]))
200
201
202 def SetValueAsCustom(self, row, col, typeName, value):
203 data = self.tdata[row][COL_VISUAL]
204
205 if col == COL_VISUAL:
206 self.tdata[row][COL_VISUAL] = value
207 elif col == COL_VALUE:
208 if row != 0: # DefaultData row
209
210 if isinstance(data, ClassGroupMap):
211 # something special
212 pass
213 else: # POINT, RANGE
214 try:
215 dataInfo = self.__ParseInput(value)
216 except: pass
217 # bad input, ignore the request
218 else:
219
220 if len(dataInfo) == 1:
221 if not isinstance(data, ClassGroupSingleton):
222 ndata = ClassGroupSingleton(prop = data)
223 ndata.SetValue(dataInfo[1])
224 elif len(dataInfo) == 2:
225 if not isinstance(data, ClassGroupRange):
226 data = ClassDataRange(classData = data)
227 data.SetRange(dataInfo[1], dataInfo[2])
228
229 ndata.SetLabel(data.GetLabel())
230 self.__SetRow(row, ndata)
231
232 #self.tdata[row][COL_VISUAL] = data
233
234 self.GetView().Refresh()
235
236 elif col == COL_LABEL:
237 data.SetLabel(value)
238 self.tdata[row][COL_LABEL] = data.GetLabel()
239 else:
240 raise ValueError(_("Invalid column request"))
241
242 self.__Modified()
243
244 def GetAttr(self, row, col, someExtraParameter):
245 attr = wxGridCellAttr()
246 #attr = wxPyGridTableBase.GetAttr(self, row, col, someExtraParameter)
247
248 if col == COL_VISUAL:
249 attr.SetRenderer(ClassRenderer(self.shapeType))
250 attr.SetReadOnly()
251
252 return attr
253
254 def GetClassGroup(self, row):
255 return self.tdata[row][COL_VISUAL]
256
257 def __Modified(self):
258 self.modified = 1
259
260 def IsModified(self):
261 return self.modified
262
263 def AddNewDataRow(self):
264 np = ClassDataPoint()
265 self.tdata.append([np, np.GetValue(), np.GetLabel()])
266 msg = wxGridTableMessage(self, wxGRIDTABLE_NOTIFY_ROWS_APPENDED, 1)
267 self.GetView().ProcessTableMessage(msg)
268 self.GetView().Refresh()
269
270 class Classifier(wxDialog):
271
272 def __init__(self, parent, layer):
273 wxDialog.__init__(self, parent, -1, _("Classify"),
274 style = wxRESIZE_BORDER)
275
276 self.layer = layer
277
278 topBox = wxBoxSizer(wxVERTICAL)
279
280 topBox.Add(wxStaticText(self, -1, _("Layer: %s") % layer.Title()),
281 0, wxALIGN_LEFT | wxBOTTOM, 4)
282 topBox.Add(wxStaticText(self, -1, _("Type: %s") % layer.ShapeType()),
283 0, wxALIGN_LEFT | wxBOTTOM, 4)
284
285 propertyBox = wxBoxSizer(wxHORIZONTAL)
286 propertyBox.Add(wxStaticText(self, -1, _("Property: ")),
287 0, wxALIGN_CENTER | wxALL, 4)
288
289 self.properties = wxComboBox(self, ID_PROPERTY_SELECT, "",
290 style = wxCB_READONLY)
291
292 self.num_cols = layer.table.field_count()
293 # just assume the first field in case one hasn't been
294 # specified in the file.
295 self.__cur_prop = 0
296 field = layer.GetClassification().GetField()
297 for i in range(self.num_cols):
298 type, name, len, decc = layer.table.field_info(i)
299 if name == field:
300 self.__cur_prop = i
301 self.properties.Append(name)
302 self.properties.SetClientData(i, None)
303
304 self.properties.SetSelection(self.__cur_prop)
305 propertyBox.Add(self.properties, 1, wxGROW|wxALL, 0)
306 EVT_COMBOBOX(self, ID_PROPERTY_SELECT, self.OnPropertySelect)
307
308 topBox.Add(propertyBox, 0, wxGROW, 4)
309
310 #
311 # Classification data table
312 #
313
314 controlBox = wxBoxSizer(wxHORIZONTAL)
315 self.classGrid = ClassGrid(self, layer)
316
317 controlBox.Add(self.classGrid, 1, wxGROW, 0)
318
319 controlButtonBox = wxBoxSizer(wxVERTICAL)
320 controlButtonBox.Add(wxButton(self, ID_CLASSIFY_ADD,
321 _("Add")), 0, wxGROW | wxALL, 4)
322 controlButtonBox.Add(wxButton(self, ID_CLASSIFY_GENRANGE,
323 _("Generate Ranges")), 0, wxGROW | wxALL, 4)
324
325 controlBox.Add(controlButtonBox, 0, wxGROW, 10)
326 topBox.Add(controlBox, 1, wxGROW, 10)
327
328 EVT_BUTTON(self, ID_CLASSIFY_ADD, self.OnAdd)
329 EVT_BUTTON(self, ID_CLASSIFY_GENRANGE, self.OnGenRange)
330 EVT_GRID_CELL_LEFT_DCLICK(self.classGrid, self.OnCellDClick)
331
332 #
333 # Control buttons:
334 #
335 buttonBox = wxBoxSizer(wxHORIZONTAL)
336 buttonBox.Add(wxButton(self, ID_CLASSIFY_OK, _("OK")),
337 0, wxALL, 4)
338 buttonBox.Add(wxButton(self, ID_CLASSIFY_CANCEL, _("Cancel")),
339 0, wxALL, 4)
340 topBox.Add(buttonBox, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_BOTTOM, 10)
341
342 EVT_BUTTON(self, ID_CLASSIFY_OK, self.OnOK)
343 EVT_BUTTON(self, ID_CLASSIFY_CANCEL, self.OnCancel)
344
345 self.SetAutoLayout(true)
346 self.SetSizer(topBox)
347 topBox.Fit(self)
348 topBox.SetSizeHints(self)
349
350 def __BuildClassification(self, prop):
351
352 clazz = Classification()
353 clazz.SetField(self.properties.GetStringSelection())
354
355 numRows = self.classGrid.GetNumberRows()
356
357 if numRows > 0:
358 table = self.classGrid.GetTable()
359 clazz.SetDefaultGroup(table.GetClassGroup(0))
360
361 for i in range(1, numRows):
362 clazz.AddGroup(table.GetClassGroup(i))
363
364 return clazz
365
366 def OnPropertySelect(self, event):
367 self.properties.SetClientData(
368 self.__cur_prop, self.__BuildClassification(self.__cur_prop))
369
370 self.__cur_prop = self.properties.GetSelection()
371 clazz = self.properties.GetClientData(self.__cur_prop)
372 table = self.classGrid.GetTable()
373
374 table.Reset(clazz, self.layer.ShapeType())
375
376 def OnOK(self, event):
377 """Put the data from the table into a new Classification and hand
378 it to the layer.
379 """
380
381 clazz = self.properties.GetClientData(self.__cur_prop)
382
383 #
384 # only build the classification if there wasn't one to
385 # to begin with or it has been modified
386 #
387 if clazz is None or self.classGrid.GetTable().IsModified():
388 clazz = self.__BuildClassification(self.__cur_prop)
389
390 clazz.SetLayer(self.layer)
391
392 self.layer.SetClassification(clazz)
393
394 self.EndModal(wxID_OK)
395
396 def OnCancel(self, event):
397 """Do nothing. The layer's current classification stays the same."""
398 self.EndModal(wxID_CANCEL)
399
400
401 def OnAdd(self, event):
402 self.classGrid.GetTable().AddNewDataRow()
403 print "Classifier.OnAdd()"
404
405 def OnGenRange(self, event):
406 print "Classifier.OnGenRange()"
407
408 def OnCellDClick(self, event):
409 r = event.GetRow()
410 c = event.GetCol()
411 if c == COL_VISUAL:
412 # XXX: getting the properties is only possible with non-Maps!!!
413 group = self.classGrid.GetTable().GetValueAsCustom(r, c, None)
414 prop = group.GetProperties()
415 propDlg = SelectPropertiesDialog(NULL, prop, self.layer.ShapeType())
416 if propDlg.ShowModal() == wxID_OK:
417 new_prop = propDlg.GetClassGroupProperties()
418 prop.SetStroke(new_prop.GetStroke())
419 prop.SetStrokeWidth(new_prop.GetStrokeWidth())
420 prop.SetFill(new_prop.GetFill())
421 self.classGrid.Refresh()
422 propDlg.Destroy()
423
424
425 ID_SELPROP_OK = 4001
426 ID_SELPROP_CANCEL = 4002
427 ID_SELPROP_SPINCTRL = 4002
428 ID_SELPROP_PREVIEW = 4003
429 ID_SELPROP_STROKECLR = 4004
430 ID_SELPROP_FILLCLR = 4005
431
432 class SelectPropertiesDialog(wxDialog):
433
434 def __init__(self, parent, prop, shapeType):
435 wxDialog.__init__(self, parent, -1, _("Select Properties"),
436 style = wxRESIZE_BORDER)
437
438 self.prop = ClassGroupProperties(prop)
439
440 topBox = wxBoxSizer(wxVERTICAL)
441
442 itemBox = wxBoxSizer(wxHORIZONTAL)
443
444 # preview box
445 previewBox = wxBoxSizer(wxVERTICAL)
446 previewBox.Add(wxStaticText(self, -1, _("Preview:")),
447 0, wxALIGN_LEFT | wxALL, 4)
448 self.previewer = ClassDataPreviewer(None, self.prop, shapeType,
449 self, ID_SELPROP_PREVIEW, (40, 40))
450 previewBox.Add(self.previewer, 1, wxGROW, 15)
451
452 itemBox.Add(previewBox, 1, wxALIGN_LEFT | wxALL | wxGROW, 0)
453
454 # control box
455 ctrlBox = wxBoxSizer(wxVERTICAL)
456 ctrlBox.Add(
457 wxButton(self, ID_SELPROP_STROKECLR, "Change Stroke Color"),
458 0, wxALIGN_CENTER_HORIZONTAL | wxALL | wxGROW, 4)
459 EVT_BUTTON(self, ID_SELPROP_STROKECLR, self.OnChangeStrokeColor)
460
461 if shapeType != SHAPETYPE_ARC:
462 ctrlBox.Add(
463 wxButton(self, ID_SELPROP_FILLCLR, "Change Fill Color"),
464 0, wxALIGN_LEFT | wxALL | wxGROW, 4)
465 EVT_BUTTON(self, ID_SELPROP_FILLCLR, self.OnChangeFillColor)
466
467 spinBox = wxBoxSizer(wxHORIZONTAL)
468 spinBox.Add(wxStaticText(self, -1, _("Stroke Width: ")),
469 0, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL | wxALL, 4)
470 self.spinCtrl = wxSpinCtrl(self, ID_SELPROP_SPINCTRL,
471 min=1, max=10,
472 value=str(prop.GetStrokeWidth()),
473 initial=prop.GetStrokeWidth())
474
475 EVT_SPINCTRL(self, ID_SELPROP_SPINCTRL, self.OnSpin)
476
477 spinBox.Add(self.spinCtrl, 0, wxALIGN_LEFT | wxALL, 4)
478
479 ctrlBox.Add(spinBox, 0, wxALIGN_RIGHT | wxALL, 0)
480 itemBox.Add(ctrlBox, 0, wxALIGN_RIGHT | wxALL | wxGROW, 0)
481 topBox.Add(itemBox, 1, wxALIGN_LEFT | wxALL | wxGROW, 0)
482
483
484 #
485 # Control buttons:
486 #
487 buttonBox = wxBoxSizer(wxHORIZONTAL)
488 buttonBox.Add(wxButton(self, ID_CLASSIFY_OK, _("OK")),
489 0, wxALL, 4)
490 buttonBox.Add(wxButton(self, ID_CLASSIFY_CANCEL, _("Cancel")),
491 0, wxALL, 4)
492 topBox.Add(buttonBox, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_BOTTOM, 10)
493
494 EVT_BUTTON(self, ID_SELPROP_OK, self.OnOK)
495 EVT_BUTTON(self, ID_SELPROP_CANCEL, self.OnCancel)
496
497 self.SetAutoLayout(true)
498 self.SetSizer(topBox)
499 topBox.Fit(self)
500 topBox.SetSizeHints(self)
501
502 def OnOK(self, event):
503 self.EndModal(wxID_OK)
504
505 def OnCancel(self, event):
506 self.EndModal(wxID_CANCEL)
507
508 def OnSpin(self, event):
509 self.prop.SetStrokeWidth(self.spinCtrl.GetValue())
510 self.previewer.Refresh()
511
512 def __GetColor(self, cur):
513 dialog = wxColourDialog(self)
514 dialog.GetColourData().SetColour(Color2wxColour(cur))
515 ret = None
516 if dialog.ShowModal() == wxID_OK:
517 ret = wxColour2Color(dialog.GetColourData().GetColour())
518
519 dialog.Destroy()
520
521 return ret
522
523 def OnChangeStrokeColor(self, event):
524 clr = self.__GetColor(self.prop.GetStroke())
525 if clr is not None:
526 self.prop.SetStroke(clr)
527 self.previewer.Refresh() # XXX: work around, see ClassDataPreviewer
528
529 def OnChangeFillColor(self, event):
530 clr = self.__GetColor(self.prop.GetFill())
531 if clr is not None:
532 self.prop.SetFill(clr)
533 self.previewer.Refresh() # XXX: work around, see ClassDataPreviewer
534
535 def GetClassGroupProperties(self):
536 return self.prop
537
538
539 class ClassDataPreviewer(wxWindow):
540
541 def __init__(self, rect, prop, shapeType,
542 parent = None, id = -1, size = wxDefaultSize):
543 if parent is not None:
544 wxWindow.__init__(self, parent, id, size=size)
545 EVT_PAINT(self, self.OnPaint)
546
547 self.rect = rect
548 self.prop = prop
549 self.shapeType = shapeType
550
551 def OnPaint(self, event):
552 dc = wxPaintDC(self)
553
554 # XXX: this doesn't seem to be having an effect:
555 dc.DestroyClippingRegion()
556
557 self.Draw(dc, None)
558
559 def Draw(self, dc, rect, prop = None, shapeType = None):
560
561 if prop is None: prop = self.prop
562 if shapeType is None: shapeType = self.shapeType
563
564 if rect is None:
565 x = y = 0
566 w, h = self.GetClientSizeTuple()
567 else:
568 x = rect.GetX()
569 y = rect.GetY()
570 w = rect.GetWidth()
571 h = rect.GetHeight()
572
573 stroke = prop.GetStroke()
574 if stroke is Color.None:
575 pen = wxTRANSPARENT_PEN
576 else:
577 pen = wxPen(Color2wxColour(stroke),
578 prop.GetStrokeWidth(),
579 wxSOLID)
580
581 stroke = prop.GetFill()
582 if stroke is Color.None:
583 brush = wxTRANSPARENT_BRUSH
584 else:
585 brush = wxBrush(Color2wxColour(stroke), wxSOLID)
586
587 dc.SetPen(pen)
588 dc.SetBrush(brush)
589
590 if shapeType == SHAPETYPE_ARC:
591 dc.DrawSpline([wxPoint(x, y + h),
592 wxPoint(x + w/2, y + h/4),
593 wxPoint(x + w/2, y + h/4*3),
594 wxPoint(x + w, y)])
595
596 elif shapeType == SHAPETYPE_POINT or \
597 shapeType == SHAPETYPE_POLYGON:
598
599 dc.DrawCircle(x + w/2, y + h/2,
600 (min(w, h) - prop.GetStrokeWidth())/2)
601
602 class ClassRenderer(wxPyGridCellRenderer):
603
604 def __init__(self, shapeType):
605 wxPyGridCellRenderer.__init__(self)
606 self.previewer = ClassDataPreviewer(None, None, shapeType)
607
608 def Draw(self, grid, attr, dc, rect, row, col, isSelected):
609 data = grid.GetTable().GetValueAsCustom(row, col, "")
610
611
612 dc.SetClippingRegion(rect.GetX(), rect.GetY(),
613 rect.GetWidth(), rect.GetHeight())
614 dc.SetPen(wxPen(wxLIGHT_GREY))
615 dc.SetBrush(wxBrush(wxLIGHT_GREY, wxSOLID))
616 dc.DrawRectangle(rect.GetX(), rect.GetY(),
617 rect.GetWidth(), rect.GetHeight())
618
619 if not isinstance(data, ClassGroupMap):
620 self.previewer.Draw(dc, rect, data.GetProperties())
621
622 if isSelected:
623 dc.SetPen(wxPen(wxColour(0 * 255, 0 * 255, 0 * 255),
624 4, wxSOLID))
625 dc.SetBrush(wxTRANSPARENT_BRUSH)
626 dc.DrawRectangle(rect.GetX(), rect.GetY(),
627 rect.GetWidth(), rect.GetHeight())
628
629 dc.DestroyClippingRegion()
630

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26