/[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 444 - (show annotations)
Thu Feb 27 16:02:59 2003 UTC (22 years ago) by jonathan
File MIME type: text/x-python
File size: 20635 byte(s)
Fix to create a tuple with a single value instead of simply returning the value.

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 ndata = data
221 if len(dataInfo) == 1:
222 if not isinstance(data, ClassGroupSingleton):
223 ndata = ClassGroupSingleton(prop = data)
224 ndata.SetValue(dataInfo[0])
225 elif len(dataInfo) == 2:
226 if not isinstance(data, ClassGroupRange):
227 data = ClassDataRange(classData = data)
228 data.SetRange(dataInfo[0], dataInfo[1])
229
230 ndata.SetLabel(data.GetLabel())
231 self.__SetRow(row, ndata)
232
233 #self.tdata[row][COL_VISUAL] = data
234
235 self.GetView().Refresh()
236
237 elif col == COL_LABEL:
238 data.SetLabel(value)
239 self.tdata[row][COL_LABEL] = data.GetLabel()
240 else:
241 raise ValueError(_("Invalid column request"))
242
243 self.__Modified()
244
245 def GetAttr(self, row, col, someExtraParameter):
246 attr = wxGridCellAttr()
247 #attr = wxPyGridTableBase.GetAttr(self, row, col, someExtraParameter)
248
249 if col == COL_VISUAL:
250 attr.SetRenderer(ClassRenderer(self.shapeType))
251 attr.SetReadOnly()
252
253 return attr
254
255 def GetClassGroup(self, row):
256 return self.tdata[row][COL_VISUAL]
257
258 def __Modified(self):
259 self.modified = 1
260
261 def IsModified(self):
262 return self.modified
263
264 def AddNewDataRow(self):
265 np = ClassDataPoint()
266 self.tdata.append([np, np.GetValue(), np.GetLabel()])
267 msg = wxGridTableMessage(self, wxGRIDTABLE_NOTIFY_ROWS_APPENDED, 1)
268 self.GetView().ProcessTableMessage(msg)
269 self.GetView().Refresh()
270
271 class Classifier(wxDialog):
272
273 def __init__(self, parent, layer):
274 wxDialog.__init__(self, parent, -1, _("Classify"),
275 style = wxRESIZE_BORDER)
276
277 self.layer = layer
278
279 topBox = wxBoxSizer(wxVERTICAL)
280
281 topBox.Add(wxStaticText(self, -1, _("Layer: %s") % layer.Title()),
282 0, wxALIGN_LEFT | wxBOTTOM, 4)
283 topBox.Add(wxStaticText(self, -1, _("Type: %s") % layer.ShapeType()),
284 0, wxALIGN_LEFT | wxBOTTOM, 4)
285
286 propertyBox = wxBoxSizer(wxHORIZONTAL)
287 propertyBox.Add(wxStaticText(self, -1, _("Property: ")),
288 0, wxALIGN_CENTER | wxALL, 4)
289
290 self.properties = wxComboBox(self, ID_PROPERTY_SELECT, "",
291 style = wxCB_READONLY)
292
293 self.num_cols = layer.table.field_count()
294 # just assume the first field in case one hasn't been
295 # specified in the file.
296 self.__cur_prop = 0
297 field = layer.GetClassification().GetField()
298 for i in range(self.num_cols):
299 type, name, len, decc = layer.table.field_info(i)
300 if name == field:
301 self.__cur_prop = i
302 self.properties.Append(name)
303 self.properties.SetClientData(i, None)
304
305 self.properties.SetSelection(self.__cur_prop)
306 propertyBox.Add(self.properties, 1, wxGROW|wxALL, 0)
307 EVT_COMBOBOX(self, ID_PROPERTY_SELECT, self.OnPropertySelect)
308
309 topBox.Add(propertyBox, 0, wxGROW, 4)
310
311 #
312 # Classification data table
313 #
314
315 controlBox = wxBoxSizer(wxHORIZONTAL)
316 self.classGrid = ClassGrid(self, layer)
317
318 controlBox.Add(self.classGrid, 1, wxGROW, 0)
319
320 controlButtonBox = wxBoxSizer(wxVERTICAL)
321 controlButtonBox.Add(wxButton(self, ID_CLASSIFY_ADD,
322 _("Add")), 0, wxGROW | wxALL, 4)
323 controlButtonBox.Add(wxButton(self, ID_CLASSIFY_GENRANGE,
324 _("Generate Ranges")), 0, wxGROW | wxALL, 4)
325
326 controlBox.Add(controlButtonBox, 0, wxGROW, 10)
327 topBox.Add(controlBox, 1, wxGROW, 10)
328
329 EVT_BUTTON(self, ID_CLASSIFY_ADD, self.OnAdd)
330 EVT_BUTTON(self, ID_CLASSIFY_GENRANGE, self.OnGenRange)
331 EVT_GRID_CELL_LEFT_DCLICK(self.classGrid, self.OnCellDClick)
332
333 #
334 # Control buttons:
335 #
336 buttonBox = wxBoxSizer(wxHORIZONTAL)
337 buttonBox.Add(wxButton(self, ID_CLASSIFY_OK, _("OK")),
338 0, wxALL, 4)
339 buttonBox.Add(wxButton(self, ID_CLASSIFY_CANCEL, _("Cancel")),
340 0, wxALL, 4)
341 topBox.Add(buttonBox, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_BOTTOM, 10)
342
343 EVT_BUTTON(self, ID_CLASSIFY_OK, self.OnOK)
344 EVT_BUTTON(self, ID_CLASSIFY_CANCEL, self.OnCancel)
345
346 self.SetAutoLayout(true)
347 self.SetSizer(topBox)
348 topBox.Fit(self)
349 topBox.SetSizeHints(self)
350
351 def __BuildClassification(self, prop):
352
353 clazz = Classification()
354 clazz.SetField(self.properties.GetStringSelection())
355
356 numRows = self.classGrid.GetNumberRows()
357
358 if numRows > 0:
359 table = self.classGrid.GetTable()
360 clazz.SetDefaultGroup(table.GetClassGroup(0))
361
362 for i in range(1, numRows):
363 clazz.AddGroup(table.GetClassGroup(i))
364
365 return clazz
366
367 def OnPropertySelect(self, event):
368 self.properties.SetClientData(
369 self.__cur_prop, self.__BuildClassification(self.__cur_prop))
370
371 self.__cur_prop = self.properties.GetSelection()
372 clazz = self.properties.GetClientData(self.__cur_prop)
373 table = self.classGrid.GetTable()
374
375 table.Reset(clazz, self.layer.ShapeType())
376
377 def OnOK(self, event):
378 """Put the data from the table into a new Classification and hand
379 it to the layer.
380 """
381
382 clazz = self.properties.GetClientData(self.__cur_prop)
383
384 #
385 # only build the classification if there wasn't one to
386 # to begin with or it has been modified
387 #
388 if clazz is None or self.classGrid.GetTable().IsModified():
389 clazz = self.__BuildClassification(self.__cur_prop)
390
391 clazz.SetLayer(self.layer)
392
393 self.layer.SetClassification(clazz)
394
395 self.EndModal(wxID_OK)
396
397 def OnCancel(self, event):
398 """Do nothing. The layer's current classification stays the same."""
399 self.EndModal(wxID_CANCEL)
400
401
402 def OnAdd(self, event):
403 self.classGrid.GetTable().AddNewDataRow()
404 print "Classifier.OnAdd()"
405
406 def OnGenRange(self, event):
407 print "Classifier.OnGenRange()"
408
409 def OnCellDClick(self, event):
410 r = event.GetRow()
411 c = event.GetCol()
412 if c == COL_VISUAL:
413 # XXX: getting the properties is only possible with non-Maps!!!
414 group = self.classGrid.GetTable().GetValueAsCustom(r, c, None)
415 prop = group.GetProperties()
416 propDlg = SelectPropertiesDialog(NULL, prop, self.layer.ShapeType())
417 if propDlg.ShowModal() == wxID_OK:
418 new_prop = propDlg.GetClassGroupProperties()
419 prop.SetStroke(new_prop.GetStroke())
420 prop.SetStrokeWidth(new_prop.GetStrokeWidth())
421 prop.SetFill(new_prop.GetFill())
422 self.classGrid.Refresh()
423 propDlg.Destroy()
424
425
426 ID_SELPROP_OK = 4001
427 ID_SELPROP_CANCEL = 4002
428 ID_SELPROP_SPINCTRL = 4002
429 ID_SELPROP_PREVIEW = 4003
430 ID_SELPROP_STROKECLR = 4004
431 ID_SELPROP_FILLCLR = 4005
432
433 class SelectPropertiesDialog(wxDialog):
434
435 def __init__(self, parent, prop, shapeType):
436 wxDialog.__init__(self, parent, -1, _("Select Properties"),
437 style = wxRESIZE_BORDER)
438
439 self.prop = ClassGroupProperties(prop)
440
441 topBox = wxBoxSizer(wxVERTICAL)
442
443 itemBox = wxBoxSizer(wxHORIZONTAL)
444
445 # preview box
446 previewBox = wxBoxSizer(wxVERTICAL)
447 previewBox.Add(wxStaticText(self, -1, _("Preview:")),
448 0, wxALIGN_LEFT | wxALL, 4)
449 self.previewer = ClassDataPreviewer(None, self.prop, shapeType,
450 self, ID_SELPROP_PREVIEW, (40, 40))
451 previewBox.Add(self.previewer, 1, wxGROW, 15)
452
453 itemBox.Add(previewBox, 1, wxALIGN_LEFT | wxALL | wxGROW, 0)
454
455 # control box
456 ctrlBox = wxBoxSizer(wxVERTICAL)
457 ctrlBox.Add(
458 wxButton(self, ID_SELPROP_STROKECLR, "Change Stroke Color"),
459 0, wxALIGN_CENTER_HORIZONTAL | wxALL | wxGROW, 4)
460 EVT_BUTTON(self, ID_SELPROP_STROKECLR, self.OnChangeStrokeColor)
461
462 if shapeType != SHAPETYPE_ARC:
463 ctrlBox.Add(
464 wxButton(self, ID_SELPROP_FILLCLR, "Change Fill Color"),
465 0, wxALIGN_LEFT | wxALL | wxGROW, 4)
466 EVT_BUTTON(self, ID_SELPROP_FILLCLR, self.OnChangeFillColor)
467
468 spinBox = wxBoxSizer(wxHORIZONTAL)
469 spinBox.Add(wxStaticText(self, -1, _("Stroke Width: ")),
470 0, wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL | wxALL, 4)
471 self.spinCtrl = wxSpinCtrl(self, ID_SELPROP_SPINCTRL,
472 min=1, max=10,
473 value=str(prop.GetStrokeWidth()),
474 initial=prop.GetStrokeWidth())
475
476 EVT_SPINCTRL(self, ID_SELPROP_SPINCTRL, self.OnSpin)
477
478 spinBox.Add(self.spinCtrl, 0, wxALIGN_LEFT | wxALL, 4)
479
480 ctrlBox.Add(spinBox, 0, wxALIGN_RIGHT | wxALL, 0)
481 itemBox.Add(ctrlBox, 0, wxALIGN_RIGHT | wxALL | wxGROW, 0)
482 topBox.Add(itemBox, 1, wxALIGN_LEFT | wxALL | wxGROW, 0)
483
484
485 #
486 # Control buttons:
487 #
488 buttonBox = wxBoxSizer(wxHORIZONTAL)
489 buttonBox.Add(wxButton(self, ID_CLASSIFY_OK, _("OK")),
490 0, wxALL, 4)
491 buttonBox.Add(wxButton(self, ID_CLASSIFY_CANCEL, _("Cancel")),
492 0, wxALL, 4)
493 topBox.Add(buttonBox, 0, wxALIGN_CENTER_HORIZONTAL|wxALIGN_BOTTOM, 10)
494
495 EVT_BUTTON(self, ID_SELPROP_OK, self.OnOK)
496 EVT_BUTTON(self, ID_SELPROP_CANCEL, self.OnCancel)
497
498 self.SetAutoLayout(true)
499 self.SetSizer(topBox)
500 topBox.Fit(self)
501 topBox.SetSizeHints(self)
502
503 def OnOK(self, event):
504 self.EndModal(wxID_OK)
505
506 def OnCancel(self, event):
507 self.EndModal(wxID_CANCEL)
508
509 def OnSpin(self, event):
510 self.prop.SetStrokeWidth(self.spinCtrl.GetValue())
511 self.previewer.Refresh()
512
513 def __GetColor(self, cur):
514 dialog = wxColourDialog(self)
515 dialog.GetColourData().SetColour(Color2wxColour(cur))
516 ret = None
517 if dialog.ShowModal() == wxID_OK:
518 ret = wxColour2Color(dialog.GetColourData().GetColour())
519
520 dialog.Destroy()
521
522 return ret
523
524 def OnChangeStrokeColor(self, event):
525 clr = self.__GetColor(self.prop.GetStroke())
526 if clr is not None:
527 self.prop.SetStroke(clr)
528 self.previewer.Refresh() # XXX: work around, see ClassDataPreviewer
529
530 def OnChangeFillColor(self, event):
531 clr = self.__GetColor(self.prop.GetFill())
532 if clr is not None:
533 self.prop.SetFill(clr)
534 self.previewer.Refresh() # XXX: work around, see ClassDataPreviewer
535
536 def GetClassGroupProperties(self):
537 return self.prop
538
539
540 class ClassDataPreviewer(wxWindow):
541
542 def __init__(self, rect, prop, shapeType,
543 parent = None, id = -1, size = wxDefaultSize):
544 if parent is not None:
545 wxWindow.__init__(self, parent, id, size=size)
546 EVT_PAINT(self, self.OnPaint)
547
548 self.rect = rect
549 self.prop = prop
550 self.shapeType = shapeType
551
552 def OnPaint(self, event):
553 dc = wxPaintDC(self)
554
555 # XXX: this doesn't seem to be having an effect:
556 dc.DestroyClippingRegion()
557
558 self.Draw(dc, None)
559
560 def Draw(self, dc, rect, prop = None, shapeType = None):
561
562 if prop is None: prop = self.prop
563 if shapeType is None: shapeType = self.shapeType
564
565 if rect is None:
566 x = y = 0
567 w, h = self.GetClientSizeTuple()
568 else:
569 x = rect.GetX()
570 y = rect.GetY()
571 w = rect.GetWidth()
572 h = rect.GetHeight()
573
574 stroke = prop.GetStroke()
575 if stroke is Color.None:
576 pen = wxTRANSPARENT_PEN
577 else:
578 pen = wxPen(Color2wxColour(stroke),
579 prop.GetStrokeWidth(),
580 wxSOLID)
581
582 stroke = prop.GetFill()
583 if stroke is Color.None:
584 brush = wxTRANSPARENT_BRUSH
585 else:
586 brush = wxBrush(Color2wxColour(stroke), wxSOLID)
587
588 dc.SetPen(pen)
589 dc.SetBrush(brush)
590
591 if shapeType == SHAPETYPE_ARC:
592 dc.DrawSpline([wxPoint(x, y + h),
593 wxPoint(x + w/2, y + h/4),
594 wxPoint(x + w/2, y + h/4*3),
595 wxPoint(x + w, y)])
596
597 elif shapeType == SHAPETYPE_POINT or \
598 shapeType == SHAPETYPE_POLYGON:
599
600 dc.DrawCircle(x + w/2, y + h/2,
601 (min(w, h) - prop.GetStrokeWidth())/2)
602
603 class ClassRenderer(wxPyGridCellRenderer):
604
605 def __init__(self, shapeType):
606 wxPyGridCellRenderer.__init__(self)
607 self.previewer = ClassDataPreviewer(None, None, shapeType)
608
609 def Draw(self, grid, attr, dc, rect, row, col, isSelected):
610 data = grid.GetTable().GetValueAsCustom(row, col, "")
611
612
613 dc.SetClippingRegion(rect.GetX(), rect.GetY(),
614 rect.GetWidth(), rect.GetHeight())
615 dc.SetPen(wxPen(wxLIGHT_GREY))
616 dc.SetBrush(wxBrush(wxLIGHT_GREY, wxSOLID))
617 dc.DrawRectangle(rect.GetX(), rect.GetY(),
618 rect.GetWidth(), rect.GetHeight())
619
620 if not isinstance(data, ClassGroupMap):
621 self.previewer.Draw(dc, rect, data.GetProperties())
622
623 if isSelected:
624 dc.SetPen(wxPen(wxColour(0 * 255, 0 * 255, 0 * 255),
625 4, wxSOLID))
626 dc.SetBrush(wxTRANSPARENT_BRUSH)
627 dc.DrawRectangle(rect.GetX(), rect.GetY(),
628 rect.GetWidth(), rect.GetHeight())
629
630 dc.DestroyClippingRegion()
631

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26