/[thuban]/branches/WIP-pyshapelib-bramz/Thuban/UI/tableview.py
ViewVC logotype

Contents of /branches/WIP-pyshapelib-bramz/Thuban/UI/tableview.py

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2755 - (show annotations)
Thu Apr 12 09:21:58 2007 UTC (17 years, 10 months ago) by dpinte
File MIME type: text/x-python
File size: 22367 byte(s)
2007-04-12 Didrik Pinte <dpinte@itae.be>

   * Removed workaround for file encoding in the Thuban code


1 # Copyright (c) 2001, 2002, 2003 by Intevation GmbH
2 # Authors:
3 # Bernhard Herzog <[email protected]>
4 #
5 # This program is free software under the GPL (>=v2)
6 # Read the file COPYING coming with Thuban for details.
7
8 __version__ = "$Revision$"
9
10 import os.path
11
12 from Thuban import _
13
14 import wx
15 from wx import grid
16
17 from Thuban.Lib.connector import Publisher
18 from Thuban.Model.table import FIELDTYPE_INT, FIELDTYPE_DOUBLE, \
19 FIELDTYPE_STRING, table_to_dbf, table_to_csv
20 from dialogs import ThubanFrame
21
22 from messages import SHAPES_SELECTED, SESSION_REPLACED
23 from Thuban.Model.messages import TABLE_REMOVED, MAP_LAYERS_REMOVED
24 from Thuban.UI.common import ThubanBeginBusyCursor, ThubanEndBusyCursor
25
26 wx_value_type_map = {FIELDTYPE_INT: grid.GRID_VALUE_NUMBER,
27 FIELDTYPE_DOUBLE: grid.GRID_VALUE_FLOAT,
28 FIELDTYPE_STRING: grid.GRID_VALUE_STRING}
29
30 ROW_SELECTED = "ROW_SELECTED"
31
32 QUERY_KEY = 'S'
33
34 class DataTable(grid.PyGridTableBase):
35
36 """Wrapper around a Thuban table object suitable for a wxGrid"""
37
38 def __init__(self, table = None):
39 grid.PyGridTableBase.__init__(self)
40 self.num_cols = 0
41 self.num_rows = 0
42 self.columns = []
43 self.table = None
44 self.SetTable(table)
45
46 def SetTable(self, table):
47 self.table = table
48 self.num_cols = table.NumColumns()
49 self.num_rows = table.NumRows()
50
51 self.columns = []
52 for i in range(self.num_cols):
53 col = table.Column(i)
54 self.columns.append((col.name, wx_value_type_map[col.type]))
55
56 #
57 # required methods for the wxPyGridTableBase interface
58 #
59
60 def GetNumberRows(self):
61 return self.num_rows
62
63 def GetNumberCols(self):
64 return self.num_cols
65
66 def IsEmptyCell(self, row, col):
67 return 0
68
69 # Get/Set values in the table. The Python version of these
70 # methods can handle any data-type, (as long as the Editor and
71 # Renderer understands the type too,) not just strings as in the
72 # C++ version.
73 def GetValue(self, row, col):
74 try:
75 record = self.table.ReadRowAsDict(row, row_is_ordinal = 1)
76 except UnicodeError:
77 record = dict()
78 for (key, val) in self.table.ReadRowAsDict(row, \
79 row_is_ordinal = 1).items():
80 record[key] = val
81 return record[self.columns[col][0]]
82
83 def SetValue(self, row, col, value):
84 pass
85
86 #
87 # Some optional methods
88 #
89
90 # Called when the grid needs to display labels
91 def GetColLabelValue(self, col):
92 return self.columns[col][0]
93
94 # Called to determine the kind of editor/renderer to use by
95 # default, doesn't necessarily have to be the same type used
96 # nativly by the editor/renderer if they know how to convert.
97 def GetTypeName(self, row, col):
98 return self.columns[col][1]
99
100 # Called to determine how the data can be fetched and stored by the
101 # editor and renderer. This allows you to enforce some type-safety
102 # in the grid.
103 def CanGetValueAs(self, row, col, typeName):
104 # perhaps we should allow conversion int->double?
105 return self.GetTypeName(row, col) == typeName
106
107 def CanSetValueAs(self, row, col, typeName):
108 return self.CanGetValueAs(row, col, typeName)
109
110
111 #
112 def RowIdToOrdinal(self, rowid):
113 """Return the ordinal of the row given by its id"""
114 return self.table.RowIdToOrdinal(rowid)
115
116 def RowOrdinalToId(self, ordinal):
117 """Return the id of the row given by its ordinal"""
118 return self.table.RowOrdinalToId(ordinal)
119
120
121 class NullRenderer(grid.PyGridCellRenderer):
122
123 """Renderer that draws NULL as a gray rectangle
124
125 Other values are delegated to a normal renderer which is given as
126 the parameter to the constructor.
127 """
128
129 def __init__(self, non_null_renderer):
130 grid.PyGridCellRenderer.__init__(self)
131 self.non_null_renderer = non_null_renderer
132
133 def Draw(self, grid, attr, dc, rect, row, col, isSelected):
134 value = grid.table.GetValue(row, col)
135 if value is None:
136 dc.SetBackgroundMode(wx.SOLID)
137 dc.SetBrush(wx.Brush(wx.Colour(192, 192, 192), wx.SOLID))
138 dc.SetPen(wx.TRANSPARENT_PEN)
139 dc.DrawRectangle(rect.x, rect.y, rect.width, rect.height)
140 else:
141 self.non_null_renderer.Draw(grid, attr, dc, rect, row, col,
142 isSelected)
143
144 def GetBestSize(self, grid, attr, dc, row, col):
145 self.non_null_renderer.GetBestSize(grid, attr, dc, row, col)
146
147 def Clone(self):
148 return NullRenderer(self.non_null_renderer)
149
150
151 class TableGrid(grid.Grid, Publisher):
152
153 """A grid view for a Thuban table
154
155 When rows are selected by the user the table issues ROW_SELECTED
156 messages. wx sends selection events even when the selection is
157 manipulated by code (instead of by the user) which usually lead to
158 ROW_SELECTED messages being sent in turn. Therefore sending messages
159 can be switched on and off with the allow_messages and
160 disallow_messages methods.
161 """
162
163 def __init__(self, parent, table = None):
164 grid.Grid.__init__(self, parent, -1)
165
166 self.allow_messages_count = 0
167
168 # keep track of which rows are selected.
169 self.rows = {}
170
171 self.table = DataTable(table)
172
173 # The second parameter means that the grid is to take ownership
174 # of the table and will destroy it when done. Otherwise you
175 # would need to keep a reference to it and call its Destroy
176 # method later.
177 self.SetTable(self.table, True)
178
179 #self.SetMargins(0,0)
180
181 # AutoSizeColumns would allow us to make the grid have optimal
182 # column widths automatically but it would cause a traversal of
183 # the entire table which for large .dbf files can take a very
184 # long time.
185 #self.AutoSizeColumns(False)
186
187 self.SetSelectionMode(grid.Grid.wxGridSelectRows)
188
189 self.ToggleEventListeners(True)
190 self.Bind(grid.EVT_GRID_RANGE_SELECT, self.OnRangeSelect)
191 self.Bind(grid.EVT_GRID_SELECT_CELL, self.OnSelectCell)
192
193 # Replace the normal renderers with our own versions which
194 # render NULL/None values specially
195 self.RegisterDataType(grid.GRID_VALUE_STRING,
196 NullRenderer(grid.GridCellStringRenderer()), None)
197 self.RegisterDataType(grid.GRID_VALUE_NUMBER,
198 NullRenderer(grid.GridCellNumberRenderer()), None)
199 self.RegisterDataType(grid.GRID_VALUE_FLOAT,
200 NullRenderer(grid.GridCellFloatRenderer()), None)
201
202 self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)
203
204 def OnDestroy(self, event):
205 Publisher.Destroy(self)
206
207 def SetTableObject(self, table):
208 self.table.SetTable(table)
209
210 def OnRangeSelect(self, event):
211 to_id = self.table.RowOrdinalToId
212 if self.handleSelectEvents:
213 self.rows = dict([(to_id(i), 0) for i in self.GetSelectedRows()])
214
215 # if we're selecting we need to include the selected range and
216 # make sure that the current row is also included, which may
217 # not be the case if you just click on a single row!
218 if event.Selecting():
219 for i in range(event.GetTopRow(), event.GetBottomRow() + 1):
220 self.rows[to_id(i)] = 0
221 self.rows[to_id(event.GetTopLeftCoords().GetRow())] = 0
222
223 self.issue(ROW_SELECTED, self.rows.keys())
224
225 event.Skip()
226
227 def OnSelectCell(self, event):
228 to_id = self.table.RowOrdinalToId
229 if self.handleSelectEvents:
230 self.issue(ROW_SELECTED,
231 [to_id(i) for i in self.GetSelectedRows()])
232 event.Skip()
233
234 def ToggleEventListeners(self, on):
235 self.handleSelectEvents = on
236
237 def GetNumberSelected(self):
238 return len(self.rows)
239
240 def disallow_messages(self):
241 """Disallow messages to be send.
242
243 This method only increases a counter so that calls to
244 disallow_messages and allow_messages can be nested. Only the
245 outermost calls will actually switch message sending on and off.
246 """
247 self.allow_messages_count += 1
248
249 def allow_messages(self):
250 """Allow messages to be send.
251
252 This method only decreases a counter so that calls to
253 disallow_messages and allow_messages can be nested. Only the
254 outermost calls will actually switch message sending on and off.
255 """
256 self.allow_messages_count -= 1
257
258 def issue(self, *args):
259 """Issue a message unless disallowed.
260
261 See the allow_messages and disallow_messages methods.
262 """
263 if self.allow_messages_count == 0:
264 Publisher.issue(self, *args)
265
266 def SelectRowById(self, rowid, do_select):
267 """Select row with the id rowid"""
268 self.SelectRow(self.table.RowIdToOrdinal(rowid), do_select)
269
270
271 class LayerTableGrid(TableGrid):
272
273 """Table grid for the layer tables.
274
275 The LayerTableGrid is basically the same as a TableGrid but it's
276 selection is usually coupled to the selected object in the map.
277 """
278
279 def select_shapes(self, layer, shapes):
280 """Select the row corresponding to the specified shape and layer
281
282 If layer is not the layer the table is associated with do
283 nothing. If shape or layer is None also do nothing.
284 """
285 if layer is not None \
286 and layer.ShapeStore().Table() is self.table.table:
287
288 self.disallow_messages()
289 try:
290 self.ClearSelection()
291 if len(shapes) > 0:
292 # keep track of the lowest id so we can make it the
293 # first visible item
294 first = -1
295
296 to_ordinal = self.table.RowIdToOrdinal
297 for shape in shapes:
298 row = to_ordinal(shape)
299 self.SelectRow(row, True)
300 if row < first:
301 first = row
302
303 self.SetGridCursor(first, 0)
304 self.MakeCellVisible(first, 0)
305 finally:
306 self.allow_messages()
307
308
309 class TableFrame(ThubanFrame):
310
311 """Frame that displays a Thuban table in a grid view"""
312
313 def __init__(self, parent, name, title, table):
314 ThubanFrame.__init__(self, parent, name, title)
315 self.panel = wx.Panel(self, -1)
316
317 self.table = table
318 self.grid = self.make_grid(self.table)
319 self.app = self.parent.application
320 self.app.Subscribe(SESSION_REPLACED, self.close_on_session_replaced)
321 self.session = self.app.Session()
322 self.session.Subscribe(TABLE_REMOVED, self.close_on_table_removed)
323
324 def OnDestroy(self, event):
325 """Extend inherited method to unsubscribe messages"""
326 self.app.Unsubscribe(SESSION_REPLACED, self.close_on_session_replaced)
327 self.session.Unsubscribe(TABLE_REMOVED, self.close_on_table_removed)
328 ThubanFrame.OnDestroy(self, event)
329
330 def make_grid(self, table):
331 """Return the table grid to use in the frame.
332
333 The default implementation returns a TableGrid instance.
334 Override in derived classes to use different grid classes.
335 """
336 return TableGrid(self, table)
337
338 def close_on_session_replaced(self, *args):
339 """Subscriber for the SESSION_REPLACED messages.
340
341 The table frame is tied to a session so close the window when
342 the session changes.
343 """
344 self.Close()
345
346 def close_on_table_removed(self, table):
347 """Subscriber for the TABLE_REMOVED messages.
348
349 The table frame is tied to a particular table so close the
350 window when the table is removed.
351 """
352 if table is self.table:
353 self.Close()
354
355
356 ID_QUERY = 4001
357 ID_EXPORT = 4002
358 ID_COMBOVALUE = 4003
359 ID_EXPORTSEL = 4004
360
361 class QueryTableFrame(TableFrame):
362
363 """Frame that displays a table in a grid view and offers user actions
364 selection and export
365
366 A QueryTableFrame is TableFrame whose selection is connected to the
367 selected object in a map.
368 """
369
370 def __init__(self, parent, name, title, table):
371 TableFrame.__init__(self, parent, name, title, table)
372
373 self.combo_fields = wx.ComboBox(self.panel, -1, style=wx.CB_READONLY)
374 self.choice_comp = wx.Choice(self.panel, -1,
375 choices=["<", "<=", "==", "!=", ">=", ">"])
376 self.combo_value = wx.ComboBox(self.panel, ID_COMBOVALUE)
377 self.choice_action = wx.Choice(self.panel, -1,
378 choices=[_("Replace Selection"),
379 _("Refine Selection"),
380 _("Add to Selection")])
381
382 button_query = wx.Button(self.panel, ID_QUERY, _("Query"))
383 button_export = wx.Button(self.panel, ID_EXPORT, _("Export"))
384 button_exportSel = wx.Button(self.panel, ID_EXPORTSEL, _("Export Selection"))
385 button_close = wx.Button(self.panel, wx.ID_CLOSE, _("Close"))
386
387 self.CreateStatusBar()
388
389 self.grid.SetSize((400, 200))
390
391 self.combo_value.Append("")
392 for i in range(table.NumColumns()):
393 name = table.Column(i).name
394 self.combo_fields.Append(name)
395 self.combo_value.Append(name)
396
397 # assume at least one field?
398 self.combo_fields.SetSelection(0)
399 self.combo_value.SetSelection(0)
400 self.choice_action.SetSelection(0)
401 self.choice_comp.SetSelection(0)
402
403 self.grid.Reparent(self.panel)
404
405 self.UpdateStatusText()
406
407 topBox = wx.BoxSizer(wx.VERTICAL)
408
409 sizer = wx.StaticBoxSizer(wx.StaticBox(self.panel, -1,
410 _("Selection")),
411 wx.HORIZONTAL)
412 sizer.Add(self.combo_fields, 1, wx.EXPAND|wx.ALL, 4)
413 sizer.Add(self.choice_comp, 0, wx.ALL, 4)
414 sizer.Add(self.combo_value, 1, wx.EXPAND|wx.ALL, 4)
415 sizer.Add(self.choice_action, 0, wx.ALL, 4)
416 sizer.Add(button_query, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 4)
417 sizer.Add( (40, 20), 0, wx.ALL, 4)
418
419 topBox.Add(sizer, 0, wx.EXPAND|wx.ALL, 4)
420 topBox.Add(self.grid, 1, wx.EXPAND|wx.ALL, 0)
421
422 sizer = wx.BoxSizer(wx.HORIZONTAL)
423 sizer.Add(button_export, 0, wx.ALL, 4)
424 sizer.Add(button_exportSel, 0, wx.ALL, 4)
425 sizer.Add( (60, 20), 1, wx.ALL|wx.EXPAND, 4)
426 sizer.Add(button_close, 0, wx.ALL|wx.ALIGN_RIGHT, 4)
427 topBox.Add(sizer, 0, wx.ALL | wx.EXPAND, 4)
428
429 self.panel.SetAutoLayout(True)
430 self.panel.SetSizer(topBox)
431 topBox.Fit(self.panel)
432 topBox.SetSizeHints(self.panel)
433
434 panelSizer = wx.BoxSizer(wx.VERTICAL)
435 panelSizer.Add(self.panel, 1, wx.EXPAND, 0)
436 self.SetAutoLayout(True)
437 self.SetSizer(panelSizer)
438 panelSizer.Fit(self)
439 panelSizer.SetSizeHints(self)
440
441 self.grid.SetFocus()
442
443 self.Bind(wx.EVT_BUTTON, self.OnQuery, id=ID_QUERY)
444 self.Bind(wx.EVT_BUTTON, self.OnExport, id=ID_EXPORT)
445 self.Bind(wx.EVT_BUTTON, self.OnExportSel, id=ID_EXPORTSEL)
446 self.Bind(wx.EVT_BUTTON, self.OnClose, id=wx.ID_CLOSE)
447 self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown, self.grid)
448
449 self.grid.Subscribe(ROW_SELECTED, self.UpdateStatusText)
450
451 def UpdateStatusText(self, rows=None):
452 self.SetStatusText(_("%i rows (%i selected), %i columns")
453 % (self.grid.GetNumberRows(),
454 self.grid.GetNumberSelected(),
455 self.grid.GetNumberCols()))
456
457 def OnKeyDown(self, event):
458 """Catch query key from grid"""
459 if event.AltDown() and event.GetKeyCode() == ord(QUERY_KEY):
460 self.combo_fields.SetFocus()
461 self.combo_fields.refocus = True
462 else:
463 event.Skip()
464
465 def OnQuery(self, event):
466 ThubanBeginBusyCursor()
467 try:
468
469 text = self.combo_value.GetValue()
470 if self.combo_value.GetSelection() < 1 \
471 or self.combo_value.FindString(text) == -1:
472 value = text
473 else:
474 value = self.table.Column(text)
475
476 ids = self.table.SimpleQuery(
477 self.table.Column(self.combo_fields.GetStringSelection()),
478 self.choice_comp.GetStringSelection(),
479 value)
480
481 choice = self.choice_action.GetSelection()
482
483 #
484 # what used to be nice code got became a bit ugly because
485 # each time we select a row a message is sent to the grid
486 # which we are listening for and then we send further
487 # messages.
488 #
489 # now, we disable those listeners select everything but
490 # the first item, reenable the listeners, and select
491 # the first element, which causes everything to be
492 # updated properly.
493 #
494 if ids:
495 self.grid.ToggleEventListeners(False)
496
497 if choice == 0:
498 # Replace Selection
499 self.grid.ClearSelection()
500 elif choice == 1:
501 # Refine Selection
502 sel = self.get_selected()
503 self.grid.ClearSelection()
504 ids = filter(sel.has_key, ids)
505 elif choice == 2:
506 # Add to Selection
507 pass
508
509 #
510 # select the rows (all but the first)
511 #
512 firsttime = True
513 for id in ids:
514 if firsttime:
515 firsttime = False
516 else:
517 self.grid.SelectRowById(id, True)
518
519 self.grid.ToggleEventListeners(True)
520
521 #
522 # select the first row
523 #
524 if ids:
525 self.grid.SelectRowById(ids[0], True)
526
527 finally:
528 ThubanEndBusyCursor()
529
530 def doExport(self, onlySelected):
531
532 dlg = wx.FileDialog(self, _("Export Table To"), ".", "",
533 _("DBF Files (*.dbf)|*.dbf|") +
534 _("CSV Files (*.csv)|*.csv|") +
535 _("All Files (*.*)|*.*"),
536 wx.SAVE|wx.OVERWRITE_PROMPT)
537 if dlg.ShowModal() == wx.ID_OK:
538 filename = dlg.GetPath()
539 type = os.path.basename(filename).split('.')[-1:][0]
540 dlg.Destroy()
541
542 if onlySelected:
543 records = self.grid.GetSelectedRows()
544 else:
545 records = None
546
547 if type.upper() == "DBF":
548 table_to_dbf(self.table, filename, records)
549 elif type.upper() == 'CSV':
550 table_to_csv(self.table, filename, records)
551 else:
552 dlg = wx.MessageDialog(None, "Unsupported format: %s" % type,
553 "Table Export", wx.OK|wx.ICON_WARNING)
554 dlg.ShowModal()
555 dlg.Destroy()
556 else:
557 dlg.Destroy()
558
559 def OnExport(self, event):
560 self.doExport(False)
561
562 def OnExportSel(self, event):
563 self.doExport(True)
564
565 def OnClose(self, event):
566 TableFrame.OnClose(self, event)
567
568 def get_selected(self):
569 """Return a dictionary of the selected rows.
570
571 The dictionary has the indexes as keys."""
572 to_id = self.table.RowOrdinalToId
573 return dict([(to_id(i), 0) for i in self.grid.GetSelectedRows()])
574
575
576 class LayerTableFrame(QueryTableFrame):
577
578 """Frame that displays a layer table in a grid view
579
580 A LayerTableFrame is a QueryTableFrame whose selection is connected to the
581 selected object in a map.
582 """
583
584 def __init__(self, parent, name, title, layer, table):
585 QueryTableFrame.__init__(self, parent, name, title, table)
586 self.layer = layer
587 self.grid.Subscribe(ROW_SELECTED, self.rows_selected)
588 self.parent.Subscribe(SHAPES_SELECTED, self.select_shapes)
589 self.map = self.parent.Map()
590 self.map.Subscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
591
592 # if there is already a selection present, update the grid
593 # accordingly
594 sel = self.get_selected().keys()
595 for i in sel:
596 self.grid.SelectRowById(i, True)
597
598 def OnDestroy(self, event):
599 """Extend inherited method to unsubscribe messages"""
600 # There's no need to unsubscribe from self.grid's messages
601 # because it will get a DESTROY event too (since destroying the
602 # frame basically means that all child windows are also
603 # destroyed) and this it will clear all subscriptions
604 # automatically. It may even have been destroyed already (this
605 # does happen on w2000 for instance) so calling any of its
606 # methods here would be an error.
607 self.parent.Unsubscribe(SHAPES_SELECTED, self.select_shapes)
608 self.map.Unsubscribe(MAP_LAYERS_REMOVED, self.map_layers_removed)
609 QueryTableFrame.OnDestroy(self, event)
610
611 def make_grid(self, table):
612 """Override the derived method to return a LayerTableGrid.
613 """
614 return LayerTableGrid(self, table)
615
616 def get_selected(self):
617 """Override the derived method to return a dictionary of the selected
618 rows.
619 """
620 return dict([(i, 0) for i in self.parent.SelectedShapes()])
621
622 def select_shapes(self, layer, shapes):
623 """Subscribed to the SHAPES_SELECTED message.
624
625 If shapes contains exactly one shape id, select that shape in
626 the grid. Otherwise deselect all.
627 """
628 self.grid.select_shapes(layer, shapes)
629
630 def rows_selected(self, rows):
631 """Return the selected rows of the layer as they are returned
632 by Layer.SelectShapes().
633 """
634 if self.layer is not None:
635 self.parent.SelectShapes(self.layer, rows)
636
637 def map_layers_removed(self, *args):
638 """Receiver for the map's MAP_LAYERS_REMOVED message
639
640 Close the dialog if the layer whose table we're showing is not
641 in the map anymore.
642 """
643 if self.layer not in self.map.Layers():
644 self.Close()
645

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26