/[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 430 - (show annotations)
Mon Feb 24 18:47:06 2003 UTC (22 years ago) by jonathan
File MIME type: text/x-python
File size: 19705 byte(s)
(SelectPropertiesDialog): Support
        for changing the stroke and fill colors and previewing the
        changes.

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

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26