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

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

Parent Directory Parent Directory | Revision Log Revision Log


Revision 2102 - (show annotations)
Thu Mar 11 21:04:30 2004 UTC (20 years, 11 months ago) by bh
Original Path: trunk/thuban/Thuban/UI/mainwindow.py
File MIME type: text/x-python
File size: 47647 byte(s)
* Thuban/UI/dbdialog.py (ChooseDBTableDialog.__init__): Rework how
the dialog is constructed. Add combo boxes to select id and
geometry column.  Rename some instance variables.
(ChooseDBTableDialog.GetTable): Return id and geometry column
names
(ChooseDBTableDialog.OnTableSelect): New. Event handler for
selections in the table list

* Thuban/UI/mainwindow.py (MainWindow.AddDBLayer): Use id_column
and geometry_column

* Thuban/Model/session.py (Session.OpenDBShapeStore): Add the new
parameters for id_column and geometry column of PostGISShapeStore
here as well.

* Thuban/Model/postgisdb.py (type_map): Add ROWID psycog type.
(_raw_type_map): New. Map raw PostgreSQL type ints to thuban types
(PostGISConnection.GeometryTables): Use a better query to
determine which relations in the database might be usable for
shapestores.  Now supports views as well but is more PostgreSQL
specific
(PostGISConnection.table_columns): New. Somewhat experimental
method to let the db dialogs provide lists of columns to users so
that they can select id and geometry columns.
(PostGISTable.__init__): The default value of the id_column
parameter is now None it still means "gid" effectively, though.
(PostGISTable.IDColumn): New introspection method to return a
column object for the id column
(PostGISShapeStore.GeometryColumn): New introspection method to
return a column object for the geometry column

* test/test_postgis_db.py
(TestPostGISConnection.test_gis_tables_non_empty):
Removed. Subsumed by the new:
(TestPostGISConnection.test_gis_tables_with_views_and_tables):
New. Tes the GeometryTables and table_columns methods with actual
tables and views.
(PointTests.test_id_column, PointTests.test_geometry_column):
New. tests for the new methods.
(TestPostGISShapestorePoint.setUp)
(TestPostGISShapestorePointSRID.setUp)
(TestPostGISShapestorePointExplicitGIDColumn.setUp): Fill the
instance variables needed by the new tests

1 # Copyright (C) 2001, 2002, 2003, 2004 by Intevation GmbH
2 # Authors:
3 # Jan-Oliver Wagner <[email protected]>
4 # Bernhard Herzog <[email protected]>
5 # Frank Koormann <[email protected]>
6 #
7 # This program is free software under the GPL (>=v2)
8 # Read the file COPYING coming with Thuban for details.
9
10 """
11 The main window
12 """
13
14 __version__ = "$Revision$"
15 # $Source$
16 # $Id$
17
18 import os
19 import copy
20
21 from wxPython.wx import *
22
23 import Thuban
24
25 from Thuban import _
26 from Thuban.Model.messages import TITLE_CHANGED
27 from Thuban.Model.session import create_empty_session
28 from Thuban.Model.layer import Layer, RasterLayer
29 from Thuban.Model.postgisdb import PostGISShapeStore, has_postgis_support
30 # XXX: replace this by
31 # from wxPython.lib.dialogs import wxMultipleChoiceDialog
32 # when Thuban does not support wxPython 2.4.0 any more.
33 from Thuban.UI.multiplechoicedialog import wxMultipleChoiceDialog
34
35 import view
36 import tree
37 import tableview, identifyview
38 from Thuban.UI.classifier import Classifier
39 import legend
40 from menu import Menu
41
42 from context import Context
43 from command import registry, Command, ToolCommand
44 from messages import LAYER_SELECTED, SHAPES_SELECTED, VIEW_POSITION, \
45 MAP_REPLACED
46 from about import About
47
48 from Thuban.UI.dock import DockFrame
49 from Thuban.UI.join import JoinDialog
50 from Thuban.UI.dbdialog import DBFrame, DBDialog, ChooseDBTableDialog
51 import resource
52 import Thuban.Model.resource
53
54 import projdialog
55
56
57 class MainWindow(DockFrame):
58
59 # Some messages that can be subscribed/unsubscribed directly through
60 # the MapCanvas come in fact from other objects. This is a map to
61 # map those messages to the names of the instance variables they
62 # actually come from. This delegation is implemented in the
63 # Subscribe and unsubscribed methods
64 delegated_messages = {LAYER_SELECTED: "canvas",
65 SHAPES_SELECTED: "canvas",
66 MAP_REPLACED: "canvas"}
67
68 # Methods delegated to some instance variables. The delegation is
69 # implemented in the __getattr__ method.
70 delegated_methods = {"SelectLayer": "canvas",
71 "SelectShapes": "canvas",
72 "SelectedLayer": "canvas",
73 "SelectedShapes": "canvas",
74 }
75
76 def __init__(self, parent, ID, title, application, interactor,
77 initial_message = None, size = wxSize(-1, -1)):
78 DockFrame.__init__(self, parent, ID, title, wxDefaultPosition, size)
79 #wxFrame.__init__(self, parent, ID, title, wxDefaultPosition, size)
80
81 self.application = application
82
83 self.CreateStatusBar()
84 if initial_message:
85 self.SetStatusText(initial_message)
86
87 self.identify_view = None
88
89 self.init_ids()
90
91 # creat the menubar from the main_menu description
92 self.SetMenuBar(self.build_menu_bar(main_menu))
93
94 # Similarly, create the toolbar from main_toolbar
95 toolbar = self.build_toolbar(main_toolbar)
96 # call Realize to make sure that the tools appear.
97 toolbar.Realize()
98
99
100 # Create the map canvas
101 canvas = view.MapCanvas(self, -1)
102 canvas.Subscribe(VIEW_POSITION, self.view_position_changed)
103 canvas.Subscribe(SHAPES_SELECTED, self.identify_view_on_demand)
104 self.canvas = canvas
105 self.canvas.Subscribe(TITLE_CHANGED, self.title_changed)
106
107 self.SetMainWindow(self.canvas)
108
109 self.SetAutoLayout(True)
110
111 self.init_dialogs()
112
113 self.ShowLegend()
114
115 EVT_CLOSE(self, self.OnClose)
116
117 def Subscribe(self, channel, *args):
118 """Subscribe a function to a message channel.
119
120 If channel is one of the delegated messages call the appropriate
121 object's Subscribe method. Otherwise do nothing.
122 """
123 if channel in self.delegated_messages:
124 object = getattr(self, self.delegated_messages[channel])
125 object.Subscribe(channel, *args)
126 else:
127 print "Trying to subscribe to unsupported channel %s" % channel
128
129 def Unsubscribe(self, channel, *args):
130 """Unsubscribe a function from a message channel.
131
132 If channel is one of the delegated messages call the appropriate
133 object's Unsubscribe method. Otherwise do nothing.
134 """
135 if channel in self.delegated_messages:
136 object = getattr(self, self.delegated_messages[channel])
137 try:
138 object.Unsubscribe(channel, *args)
139 except wxPyDeadObjectError:
140 # The object was a wxObject and has already been
141 # destroyed. Hopefully it has unsubscribed all its
142 # subscribers already so that it's OK if we do nothing
143 # here
144 pass
145
146 def __getattr__(self, attr):
147 """If attr is one of the delegated methods return that method
148
149 Otherwise raise AttributeError.
150 """
151 if attr in self.delegated_methods:
152 return getattr(getattr(self, self.delegated_methods[attr]), attr)
153 raise AttributeError(attr)
154
155 def init_ids(self):
156 """Initialize the ids"""
157 self.current_id = 6000
158 self.id_to_name = {}
159 self.name_to_id = {}
160 self.events_bound = {}
161
162 def get_id(self, name):
163 """Return the wxWindows id for the command named name.
164
165 Create a new one if there isn't one yet"""
166 ID = self.name_to_id.get(name)
167 if ID is None:
168 ID = self.current_id
169 self.current_id = self.current_id + 1
170 self.name_to_id[name] = ID
171 self.id_to_name[ID] = name
172 return ID
173
174 def bind_command_events(self, command, ID):
175 """Bind the necessary events for the given command and ID"""
176 if not self.events_bound.has_key(ID):
177 # the events haven't been bound yet
178 EVT_MENU(self, ID, self.invoke_command)
179 if command.IsDynamic():
180 EVT_UPDATE_UI(self, ID, self.update_command_ui)
181
182 def build_menu_bar(self, menudesc):
183 """Build and return the menu bar from the menu description"""
184 menu_bar = wxMenuBar()
185
186 for item in menudesc.items:
187 # here the items must all be Menu instances themselves
188 menu_bar.Append(self.build_menu(item), item.title)
189
190 return menu_bar
191
192 def build_menu(self, menudesc):
193 """Return a wxMenu built from the menu description menudesc"""
194 wxmenu = wxMenu()
195 last = None
196 for item in menudesc.items:
197 if item is None:
198 # a separator. Only add one if the last item was not a
199 # separator
200 if last is not None:
201 wxmenu.AppendSeparator()
202 elif isinstance(item, Menu):
203 # a submenu
204 wxmenu.AppendMenu(wxNewId(), item.title, self.build_menu(item))
205 else:
206 # must the name the name of a command
207 self.add_menu_command(wxmenu, item)
208 last = item
209 return wxmenu
210
211 def build_toolbar(self, toolbardesc):
212 """Build and return the main toolbar window from a toolbar description
213
214 The parameter should be an instance of the Menu class but it
215 should not contain submenus.
216 """
217 toolbar = self.CreateToolBar(wxTB_3DBUTTONS)
218
219 # set the size of the tools' bitmaps. Not needed on wxGTK, but
220 # on Windows, although it doesn't work very well there. It seems
221 # that only 16x16 icons are really supported on windows.
222 # We probably shouldn't hardwire the bitmap size here.
223 toolbar.SetToolBitmapSize(wxSize(24, 24))
224
225 for item in toolbardesc.items:
226 if item is None:
227 toolbar.AddSeparator()
228 else:
229 # assume it's a string.
230 self.add_toolbar_command(toolbar, item)
231
232 return toolbar
233
234 def add_menu_command(self, menu, name):
235 """Add the command with name name to the menu menu.
236
237 If name is None, add a separator.
238 """
239 if name is None:
240 menu.AppendSeparator()
241 else:
242 command = registry.Command(name)
243 if command is not None:
244 ID = self.get_id(name)
245 menu.Append(ID, command.Title(), command.HelpText(),
246 command.IsCheckCommand())
247 self.bind_command_events(command, ID)
248 else:
249 print _("Unknown command %s") % name
250
251 def add_toolbar_command(self, toolbar, name):
252 """Add the command with name name to the toolbar toolbar.
253
254 If name is None, add a separator.
255 """
256 # Assume that all toolbar commands are also menu commmands so
257 # that we don't have to add the event handlers here
258 if name is None:
259 toolbar.AddSeparator()
260 else:
261 command = registry.Command(name)
262 if command is not None:
263 ID = self.get_id(name)
264 bitmap = resource.GetBitmapResource(command.Icon(),
265 wxBITMAP_TYPE_XPM)
266 toolbar.AddTool(ID, bitmap,
267 shortHelpString = command.HelpText(),
268 isToggle = command.IsCheckCommand())
269 self.bind_command_events(command, ID)
270 else:
271 print _("Unknown command %s") % name
272
273 def Context(self):
274 """Return the context object for a command invoked from this window
275 """
276 return Context(self.application, self.application.Session(), self)
277
278 def invoke_command(self, event):
279 name = self.id_to_name.get(event.GetId())
280 if name is not None:
281 command = registry.Command(name)
282 command.Execute(self.Context())
283 else:
284 print _("Unknown command ID %d") % event.GetId()
285
286 def update_command_ui(self, event):
287 #print "update_command_ui", self.id_to_name[event.GetId()]
288 context = self.Context()
289 command = registry.Command(self.id_to_name[event.GetId()])
290 if command is not None:
291 sensitive = command.Sensitive(context)
292 event.Enable(sensitive)
293 if command.IsTool() and not sensitive and command.Checked(context):
294 # When a checked tool command is disabled deselect all
295 # tools. Otherwise the tool would remain active but it
296 # might lead to errors if the tools stays active. This
297 # problem occurred in GREAT-ER and this fixes it, but
298 # it's not clear to me whether this is really the best
299 # way to do it (BH, 20021206).
300 self.canvas.SelectTool(None)
301 event.SetText(command.DynText(context))
302 if command.IsCheckCommand():
303 event.Check(command.Checked(context))
304
305 def RunMessageBox(self, title, text, flags = wxOK | wxICON_INFORMATION):
306 """Run a modal message box with the given text, title and flags
307 and return the result"""
308 dlg = wxMessageDialog(self, text, title, flags)
309 dlg.CenterOnParent()
310 result = dlg.ShowModal()
311 dlg.Destroy()
312 return result
313
314 def init_dialogs(self):
315 """Initialize the dialog handling"""
316 # The mainwindow maintains a dict mapping names to open
317 # non-modal dialogs. The dialogs are put into this dict when
318 # they're created and removed when they're closed
319 self.dialogs = {}
320
321 def add_dialog(self, name, dialog):
322 if self.dialogs.has_key(name):
323 raise RuntimeError(_("The Dialog named %s is already open") % name)
324 self.dialogs[name] = dialog
325
326 def dialog_open(self, name):
327 return self.dialogs.has_key(name)
328
329 def remove_dialog(self, name):
330 del self.dialogs[name]
331
332 def get_open_dialog(self, name):
333 return self.dialogs.get(name)
334
335 def view_position_changed(self):
336 pos = self.canvas.CurrentPosition()
337 if pos is not None:
338 text = "(%10.10g, %10.10g)" % pos
339 else:
340 text = ""
341 self.set_position_text(text)
342
343 def set_position_text(self, text):
344 """Set the statusbar text showing the current position.
345
346 By default the text is shown in field 0 of the status bar.
347 Override this method in derived classes to put it into a
348 different field of the statusbar.
349 """
350 self.SetStatusText(text)
351
352 def save_modified_session(self, can_veto = 1):
353 """If the current session has been modified, ask the user
354 whether to save it and do so if requested. Return the outcome of
355 the dialog (either wxID_OK, wxID_CANCEL or wxID_NO). If the
356 dialog wasn't run return wxID_NO.
357
358 If the can_veto parameter is true (default) the dialog includes
359 a cancel button, otherwise not.
360 """
361 if self.application.session.WasModified():
362 flags = wxYES_NO | wxICON_QUESTION
363 if can_veto:
364 flags = flags | wxCANCEL
365 result = self.RunMessageBox(_("Exit"),
366 _("The session has been modified."
367 " Do you want to save it?"),
368 flags)
369 if result == wxID_YES:
370 self.SaveSession()
371 else:
372 result = wxID_NO
373 return result
374
375 def NewSession(self):
376 if self.save_modified_session() != wxID_CANCEL:
377 self.application.SetSession(create_empty_session())
378
379 def OpenSession(self):
380 if self.save_modified_session() != wxID_CANCEL:
381 dlg = wxFileDialog(self, _("Open Session"),
382 self.application.Path("data"), "",
383 "Thuban Session File (*.thuban)|*.thuban",
384 wxOPEN)
385 if dlg.ShowModal() == wxID_OK:
386 self.application.OpenSession(dlg.GetPath(),
387 self.run_db_param_dialog)
388 self.application.SetPath("data", dlg.GetPath())
389 dlg.Destroy()
390
391 def run_db_param_dialog(self, parameters, message):
392 dlg = DBDialog(self, _("DB Connection Parameters"), parameters,
393 message)
394 return dlg.RunDialog()
395
396 def SaveSession(self):
397 if self.application.session.filename == None:
398 self.SaveSessionAs()
399 else:
400 self.application.SaveSession()
401
402 def SaveSessionAs(self):
403 dlg = wxFileDialog(self, _("Save Session As"),
404 self.application.Path("data"), "",
405 "Thuban Session File (*.thuban)|*.thuban",
406 wxSAVE|wxOVERWRITE_PROMPT)
407 if dlg.ShowModal() == wxID_OK:
408 self.application.session.SetFilename(dlg.GetPath())
409 self.application.SaveSession()
410 self.application.SetPath("data",dlg.GetPath())
411 dlg.Destroy()
412
413 def Exit(self):
414 self.Close(False)
415
416 def OnClose(self, event):
417 result = self.save_modified_session(can_veto = event.CanVeto())
418 if result == wxID_CANCEL:
419 event.Veto()
420 else:
421 # FIXME: it would be better to tie the unsubscription to
422 # wx's destroy event, but that isn't implemented for wxGTK
423 # yet.
424 self.canvas.Unsubscribe(VIEW_POSITION, self.view_position_changed)
425 DockFrame.OnClose(self, event)
426 for dlg in self.dialogs.values():
427 dlg.Destroy()
428 self.canvas.Destroy()
429 self.Destroy()
430
431 def SetMap(self, map):
432 self.canvas.SetMap(map)
433 self.update_title()
434
435 dialog = self.FindRegisteredDock("legend")
436 if dialog is not None:
437 dialog.GetPanel().SetMap(self.Map())
438
439 def Map(self):
440 """Return the map displayed by this mainwindow"""
441
442 return self.canvas.Map()
443
444 def ToggleSessionTree(self):
445 """If the session tree is shown close it otherwise create a new tree"""
446 name = "session_tree"
447 dialog = self.get_open_dialog(name)
448 if dialog is None:
449 dialog = tree.SessionTreeView(self, self.application, name)
450 self.add_dialog(name, dialog)
451 dialog.Show(True)
452 else:
453 dialog.Close()
454
455 def SessionTreeShown(self):
456 """Return true iff the session tree is currently shown"""
457 return self.get_open_dialog("session_tree") is not None
458
459 def About(self):
460 dlg = About(self)
461 dlg.ShowModal()
462 dlg.Destroy()
463
464 def DatabaseManagement(self):
465 name = "dbmanagement"
466 dialog = self.get_open_dialog(name)
467 if dialog is None:
468 map = self.canvas.Map()
469 dialog = DBFrame(self, name, self.application.Session())
470 self.add_dialog(name, dialog)
471 dialog.Show()
472 dialog.Raise()
473
474 def AddLayer(self):
475 dlg = wxFileDialog(self, _("Select one or more data files"),
476 self.application.Path("data"), "",
477 _("Shapefiles (*.shp)") + "|*.shp;*.SHP|" +
478 _("All Files (*.*)") + "|*.*",
479 wxOPEN | wxMULTIPLE)
480 if dlg.ShowModal() == wxID_OK:
481 filenames = dlg.GetPaths()
482 for filename in filenames:
483 title = os.path.splitext(os.path.basename(filename))[0]
484 map = self.canvas.Map()
485 has_layers = map.HasLayers()
486 try:
487 store = self.application.Session().OpenShapefile(filename)
488 except IOError:
489 # the layer couldn't be opened
490 self.RunMessageBox(_("Add Layer"),
491 _("Can't open the file '%s'.")%filename)
492 else:
493 layer = Layer(title, store)
494 map.AddLayer(layer)
495 if not has_layers:
496 # if we're adding a layer to an empty map, fit the
497 # new map to the window
498 self.canvas.FitMapToWindow()
499 self.application.SetPath("data",filename)
500 dlg.Destroy()
501
502 def AddRasterLayer(self):
503 dlg = wxFileDialog(self, _("Select an image file"),
504 self.application.Path("data"), "", "*.*",
505 wxOPEN)
506 if dlg.ShowModal() == wxID_OK:
507 filename = dlg.GetPath()
508 title = os.path.splitext(os.path.basename(filename))[0]
509 map = self.canvas.Map()
510 has_layers = map.HasLayers()
511 try:
512 layer = RasterLayer(title, filename)
513 except IOError:
514 # the layer couldn't be opened
515 self.RunMessageBox(_("Add Image Layer"),
516 _("Can't open the file '%s'.") % filename)
517 else:
518 map.AddLayer(layer)
519 if not has_layers:
520 # if we're adding a layer to an empty map, fit the
521 # new map to the window
522 self.canvas.FitMapToWindow()
523 self.application.SetPath("data", filename)
524 dlg.Destroy()
525
526 def AddDBLayer(self):
527 """Add a layer read from a database"""
528 session = self.application.Session()
529 dlg = ChooseDBTableDialog(self, self.application.Session())
530
531 if dlg.ShowModal() == wxID_OK:
532 dbconn, dbtable, id_column, geo_column = dlg.GetTable()
533 try:
534 title = str(dbtable)
535
536 # Chose the correct Interface for the database type
537 store = session.OpenDBShapeStore(dbconn, dbtable,
538 id_column = id_column,
539 geometry_column = geo_column)
540 layer = Layer(title, store)
541 except:
542 # Some error occured while initializing the layer
543 self.RunMessageBox(_("Add Layer from database"),
544 _("Can't open the database table '%s'")
545 % dbtable)
546 return
547
548 map = self.canvas.Map()
549
550 has_layers = map.HasLayers()
551 map.AddLayer(layer)
552 if not has_layers:
553 self.canvas.FitMapToWindow()
554
555 dlg.Destroy()
556
557 def RemoveLayer(self):
558 layer = self.current_layer()
559 if layer is not None:
560 self.canvas.Map().RemoveLayer(layer)
561
562 def CanRemoveLayer(self):
563 """Return true if the currently selected layer can be deleted.
564
565 If no layer is selected return False.
566
567 The return value of this method determines whether the remove
568 layer command is sensitive in menu.
569 """
570 layer = self.current_layer()
571 if layer is not None:
572 return self.canvas.Map().CanRemoveLayer(layer)
573 return False
574
575 def RaiseLayer(self):
576 layer = self.current_layer()
577 if layer is not None:
578 self.canvas.Map().RaiseLayer(layer)
579
580 def LowerLayer(self):
581 layer = self.current_layer()
582 if layer is not None:
583 self.canvas.Map().LowerLayer(layer)
584
585 def current_layer(self):
586 """Return the currently selected layer.
587
588 If no layer is selected, return None
589 """
590 return self.canvas.SelectedLayer()
591
592 def has_selected_layer(self):
593 """Return true if a layer is currently selected"""
594 return self.canvas.HasSelectedLayer()
595
596 def has_selected_shape_layer(self):
597 """Return true if a shape layer is currently selected"""
598 return isinstance(self.current_layer(), Layer)
599
600 def has_selected_shapes(self):
601 """Return true if a shape is currently selected"""
602 return self.canvas.HasSelectedShapes()
603
604 def HideLayer(self):
605 layer = self.current_layer()
606 if layer is not None:
607 layer.SetVisible(0)
608
609 def ShowLayer(self):
610 layer = self.current_layer()
611 if layer is not None:
612 layer.SetVisible(1)
613
614 def DuplicateLayer(self):
615 """Ceate a new layer above the selected layer with the same shapestore
616 """
617 layer = self.current_layer()
618 if layer is not None and hasattr(layer, "ShapeStore"):
619 new_layer = Layer(_("Copy of `%s'") % layer.Title(),
620 layer.ShapeStore(),
621 projection = layer.GetProjection())
622 new_classification = copy.deepcopy(layer.GetClassification())
623 new_layer.SetClassification(new_classification)
624 self.Map().AddLayer(new_layer)
625
626 def CanDuplicateLayer(self):
627 """Return whether the DuplicateLayer method can create a duplicate"""
628 layer = self.current_layer()
629 return layer is not None and hasattr(layer, "ShapeStore")
630
631 def LayerShowTable(self):
632 layer = self.current_layer()
633 if layer is not None:
634 table = layer.ShapeStore().Table()
635 name = "table_view" + str(id(table))
636 dialog = self.get_open_dialog(name)
637 if dialog is None:
638 dialog = tableview.LayerTableFrame(self, name,
639 _("Layer Table: %s") % layer.Title(),
640 layer, table)
641 self.add_dialog(name, dialog)
642 dialog.Show(True)
643 else:
644 # FIXME: bring dialog to front here
645 pass
646
647 def MapProjection(self):
648
649 name = "map_projection"
650 dialog = self.get_open_dialog(name)
651
652 if dialog is None:
653 map = self.canvas.Map()
654 dialog = projdialog.ProjFrame(self, name,
655 _("Map Projection: %s") % map.Title(), map)
656 self.add_dialog(name, dialog)
657 dialog.Show()
658 dialog.Raise()
659
660 def LayerProjection(self):
661
662 layer = self.current_layer()
663
664 name = "layer_projection" + str(id(layer))
665 dialog = self.get_open_dialog(name)
666
667 if dialog is None:
668 map = self.canvas.Map()
669 dialog = projdialog.ProjFrame(self, name,
670 _("Layer Projection: %s") % layer.Title(), layer)
671 self.add_dialog(name, dialog)
672 dialog.Show()
673 dialog.Raise()
674
675 def LayerEditProperties(self):
676
677 #
678 # the menu option for this should only be available if there
679 # is a current layer, so we don't need to check if the
680 # current layer is None
681 #
682
683 layer = self.current_layer()
684 self.OpenLayerProperties(layer)
685
686 def OpenLayerProperties(self, layer, group = None):
687 name = "layer_properties" + str(id(layer))
688 dialog = self.get_open_dialog(name)
689
690 if dialog is None:
691 dialog = Classifier(self, name, self.Map(), layer, group)
692 self.add_dialog(name, dialog)
693 dialog.Show()
694 dialog.Raise()
695
696 def LayerJoinTable(self):
697 layer = self.canvas.SelectedLayer()
698 if layer is not None:
699 dlg = JoinDialog(self, _("Join Layer with Table"),
700 self.application.session,
701 layer = layer)
702 dlg.ShowModal()
703
704 def LayerUnjoinTable(self):
705 layer = self.canvas.SelectedLayer()
706 if layer is not None:
707 orig_store = layer.ShapeStore().OrigShapeStore()
708 if orig_store:
709 layer.SetShapeStore(orig_store)
710
711 def ShowLegend(self):
712 if not self.LegendShown():
713 self.ToggleLegend()
714
715 def ToggleLegend(self):
716 """Show the legend if it's not shown otherwise hide it again"""
717 name = "legend"
718 dialog = self.FindRegisteredDock(name)
719
720 if dialog is None:
721 dialog = self.CreateDock(name, -1, _("Legend"), wxLAYOUT_LEFT)
722 legend.LegendPanel(dialog, None, self)
723 dialog.Dock()
724 dialog.GetPanel().SetMap(self.Map())
725 dialog.Show()
726 else:
727 dialog.Show(not dialog.IsShown())
728
729 def LegendShown(self):
730 """Return true iff the legend is currently open"""
731 dialog = self.FindRegisteredDock("legend")
732 return dialog is not None and dialog.IsShown()
733
734 def TableOpen(self):
735 dlg = wxFileDialog(self, _("Open Table"),
736 self.application.Path("data"), "",
737 _("DBF Files (*.dbf)") + "|*.dbf|" +
738 #_("CSV Files (*.csv)") + "|*.csv|" +
739 _("All Files (*.*)") + "|*.*",
740 wxOPEN)
741 if dlg.ShowModal() == wxID_OK:
742 filename = dlg.GetPath()
743 dlg.Destroy()
744 try:
745 table = self.application.session.OpenTableFile(filename)
746 except IOError:
747 # the layer couldn't be opened
748 self.RunMessageBox(_("Open Table"),
749 _("Can't open the file '%s'.") % filename)
750 else:
751 self.ShowTableView(table)
752 self.application.SetPath("data",filename)
753
754 def TableClose(self):
755 tables = self.application.session.UnreferencedTables()
756
757 lst = [(t.Title(), t) for t in tables]
758 lst.sort()
759 titles = [i[0] for i in lst]
760 dlg = wxMultipleChoiceDialog(self, _("Pick the tables to close:"),
761 _("Close Table"), titles,
762 size = (400, 300),
763 style = wxDEFAULT_DIALOG_STYLE |
764 wxRESIZE_BORDER)
765 if dlg.ShowModal() == wxID_OK:
766 for i in dlg.GetValue():
767 self.application.session.RemoveTable(lst[i][1])
768
769
770 def TableShow(self):
771 """Offer a multi-selection dialog for tables to be displayed
772
773 The windows for the selected tables are opened or brought to
774 the front.
775 """
776 tables = self.application.session.Tables()
777
778 lst = [(t.Title(), t) for t in tables]
779 lst.sort()
780 titles = [i[0] for i in lst]
781 dlg = wxMultipleChoiceDialog(self, _("Pick the table to show:"),
782 _("Show Table"), titles,
783 size = (400,300),
784 style = wxDEFAULT_DIALOG_STYLE |
785 wxRESIZE_BORDER)
786 if (dlg.ShowModal() == wxID_OK):
787 for i in dlg.GetValue():
788 # XXX: if the table belongs to a layer, open a
789 # LayerTableFrame instead of QueryTableFrame
790 self.ShowTableView(lst[i][1])
791
792 def TableJoin(self):
793 dlg = JoinDialog(self, _("Join Tables"), self.application.session)
794 dlg.ShowModal()
795
796 def ShowTableView(self, table):
797 """Open a table view for the table and optionally"""
798 name = "table_view%d" % id(table)
799 dialog = self.get_open_dialog(name)
800 if dialog is None:
801 dialog = tableview.QueryTableFrame(self, name,
802 _("Table: %s") % table.Title(),
803 table)
804 self.add_dialog(name, dialog)
805 dialog.Show(True)
806 dialog.Raise()
807
808 def TableRename(self):
809 """Let the user rename a table"""
810
811 # First, let the user select a table
812 tables = self.application.session.Tables()
813 lst = [(t.Title(), t) for t in tables]
814 lst.sort()
815 titles = [i[0] for i in lst]
816 dlg = wxMultipleChoiceDialog(self, _("Pick the table to rename:"),
817 _("Rename Table"), titles,
818 size = (400,300),
819 style = wxDEFAULT_DIALOG_STYLE |
820 wxRESIZE_BORDER)
821 if (dlg.ShowModal() == wxID_OK):
822 to_rename = [lst[i][1] for i in dlg.GetValue()]
823 dlg.Destroy()
824 else:
825 to_rename = []
826
827 # Second, let the user rename the layers
828 for table in to_rename:
829 dlg = wxTextEntryDialog(self, _("Table Title:"), _("Rename Table"),
830 table.Title())
831 try:
832 if dlg.ShowModal() == wxID_OK:
833 title = dlg.GetValue()
834 if title != "":
835 table.SetTitle(title)
836
837 # Make sure the session is marked as modified.
838 # FIXME: This should be handled automatically,
839 # but that requires more changes to the tables
840 # than I have time for currently.
841 self.application.session.changed()
842 finally:
843 dlg.Destroy()
844
845
846 def ZoomInTool(self):
847 self.canvas.ZoomInTool()
848
849 def ZoomOutTool(self):
850 self.canvas.ZoomOutTool()
851
852 def PanTool(self):
853 self.canvas.PanTool()
854
855 def IdentifyTool(self):
856 self.canvas.IdentifyTool()
857 self.identify_view_on_demand(None, None)
858
859 def LabelTool(self):
860 self.canvas.LabelTool()
861
862 def FullExtent(self):
863 self.canvas.FitMapToWindow()
864
865 def FullLayerExtent(self):
866 self.canvas.FitLayerToWindow(self.current_layer())
867
868 def FullSelectionExtent(self):
869 self.canvas.FitSelectedToWindow()
870
871 def ExportMap(self):
872 self.canvas.Export()
873
874 def PrintMap(self):
875 self.canvas.Print()
876
877 def RenameMap(self):
878 dlg = wxTextEntryDialog(self, _("Map Title:"), _("Rename Map"),
879 self.Map().Title())
880 if dlg.ShowModal() == wxID_OK:
881 title = dlg.GetValue()
882 if title != "":
883 self.Map().SetTitle(title)
884
885 dlg.Destroy()
886
887 def RenameLayer(self):
888 """Let the user rename the currently selected layer"""
889 layer = self.current_layer()
890 if layer is not None:
891 dlg = wxTextEntryDialog(self, _("Layer Title:"), _("Rename Layer"),
892 layer.Title())
893 try:
894 if dlg.ShowModal() == wxID_OK:
895 title = dlg.GetValue()
896 if title != "":
897 layer.SetTitle(title)
898 finally:
899 dlg.Destroy()
900
901 def identify_view_on_demand(self, layer, shapes):
902 """Subscribed to the canvas' SHAPES_SELECTED message
903
904 If the current tool is the identify tool, at least one shape is
905 selected and the identify dialog is not shown, show the dialog.
906 """
907 # If the selection has become empty we don't need to do
908 # anything. Otherwise it could happen that the dialog was popped
909 # up when the selection became empty, e.g. when a new selection
910 # is opened while the identify tool is active and dialog had
911 # been closed
912 if not shapes:
913 return
914
915 name = "identify_view"
916 if self.canvas.CurrentTool() == "IdentifyTool":
917 if not self.dialog_open(name):
918 dialog = identifyview.IdentifyView(self, name)
919 self.add_dialog(name, dialog)
920 dialog.Show(True)
921 else:
922 # FIXME: bring dialog to front?
923 pass
924
925 def title_changed(self, map):
926 """Subscribed to the canvas' TITLE_CHANGED messages"""
927 self.update_title()
928
929 def update_title(self):
930 """Update the window's title according to it's current state.
931
932 In this default implementation the title is 'Thuban - ' followed
933 by the map's title or simply 'Thuban' if there is not map.
934 Derived classes should override this method to get different
935 titles.
936
937 This method is called automatically by other methods when the
938 title may have to change. For the methods implemented in this
939 class this usually only means that a different map has been set
940 or the current map's title has changed.
941 """
942 map = self.Map()
943 if map is not None:
944 title = _("Thuban - %s") % (map.Title(),)
945 else:
946 title = _("Thuban")
947 self.SetTitle(title)
948
949
950 #
951 # Define all the commands available in the main window
952 #
953
954
955 # Helper functions to define common command implementations
956 def call_method(context, methodname, *args):
957 """Call the mainwindow's method methodname with args *args"""
958 apply(getattr(context.mainwindow, methodname), args)
959
960 def _method_command(name, title, method, helptext = "",
961 icon = "", sensitive = None, checked = None):
962 """Add a command implemented by a method of the mainwindow object"""
963 registry.Add(Command(name, title, call_method, args=(method,),
964 helptext = helptext, icon = icon,
965 sensitive = sensitive, checked = checked))
966
967 def make_check_current_tool(toolname):
968 """Return a function that tests if the currently active tool is toolname
969
970 The returned function can be called with the context and returns
971 true iff the currently active tool's name is toolname. It's directly
972 usable as the 'checked' callback of a command.
973 """
974 def check_current_tool(context, name=toolname):
975 return context.mainwindow.canvas.CurrentTool() == name
976 return check_current_tool
977
978 def _tool_command(name, title, method, toolname, helptext = "",
979 icon = "", sensitive = None):
980 """Add a tool command"""
981 registry.Add(ToolCommand(name, title, call_method, args=(method,),
982 helptext = helptext, icon = icon,
983 checked = make_check_current_tool(toolname),
984 sensitive = sensitive))
985
986 def _has_selected_layer(context):
987 """Return true if a layer is selected in the context"""
988 return context.mainwindow.has_selected_layer()
989
990 def _has_selected_shape_layer(context):
991 """Return true if a shape layer is selected in the context"""
992 return context.mainwindow.has_selected_shape_layer()
993
994 def _has_selected_shapes(context):
995 """Return true if a layer is selected in the context"""
996 return context.mainwindow.has_selected_shapes()
997
998 def _can_remove_layer(context):
999 return context.mainwindow.CanRemoveLayer()
1000
1001 def _has_tree_window_shown(context):
1002 """Return true if the tree window is shown"""
1003 return context.mainwindow.SessionTreeShown()
1004
1005 def _has_visible_map(context):
1006 """Return true iff theres a visible map in the mainwindow.
1007
1008 A visible map is a map with at least one visible layer."""
1009 map = context.mainwindow.Map()
1010 if map is not None:
1011 for layer in map.Layers():
1012 if layer.Visible():
1013 return 1
1014 return 0
1015
1016 def _has_legend_shown(context):
1017 """Return true if the legend window is shown"""
1018 return context.mainwindow.LegendShown()
1019
1020 def _has_gdal_support(context):
1021 """Return True if the GDAL is available"""
1022 return Thuban.Model.resource.has_gdal_support()
1023
1024 def _has_dbconnections(context):
1025 """Return whether the the session has database connections"""
1026 return context.session.HasDBConnections()
1027
1028 def _has_postgis_support(context):
1029 return has_postgis_support()
1030
1031
1032 # File menu
1033 _method_command("new_session", _("&New Session"), "NewSession",
1034 helptext = _("Start a new session"))
1035 _method_command("open_session", _("&Open Session..."), "OpenSession",
1036 helptext = _("Open a session file"))
1037 _method_command("save_session", _("&Save Session"), "SaveSession",
1038 helptext =_("Save this session to the file it was opened from"))
1039 _method_command("save_session_as", _("Save Session &As..."), "SaveSessionAs",
1040 helptext = _("Save this session to a new file"))
1041 _method_command("toggle_session_tree", _("Session &Tree"), "ToggleSessionTree",
1042 checked = _has_tree_window_shown,
1043 helptext = _("Toggle on/off the session tree analysis window"))
1044 _method_command("toggle_legend", _("Legend"), "ToggleLegend",
1045 checked = _has_legend_shown,
1046 helptext = _("Toggle Legend on/off"))
1047 _method_command("database_management", _("&Database Connections..."),
1048 "DatabaseManagement",
1049 sensitive = _has_postgis_support)
1050 _method_command("exit", _("E&xit"), "Exit",
1051 helptext = _("Finish working with Thuban"))
1052
1053 # Help menu
1054 _method_command("help_about", _("&About..."), "About",
1055 helptext = _("Info about Thuban authors, version and modules"))
1056
1057
1058 # Map menu
1059 _method_command("map_projection", _("Pro&jection..."), "MapProjection",
1060 helptext = _("Set or change the map projection"))
1061
1062 _tool_command("map_zoom_in_tool", _("&Zoom in"), "ZoomInTool", "ZoomInTool",
1063 helptext = _("Switch to map-mode 'zoom-in'"), icon = "zoom_in",
1064 sensitive = _has_visible_map)
1065 _tool_command("map_zoom_out_tool", _("Zoom &out"), "ZoomOutTool", "ZoomOutTool",
1066 helptext = _("Switch to map-mode 'zoom-out'"), icon = "zoom_out",
1067 sensitive = _has_visible_map)
1068 _tool_command("map_pan_tool", _("&Pan"), "PanTool", "PanTool",
1069 helptext = _("Switch to map-mode 'pan'"), icon = "pan",
1070 sensitive = _has_visible_map)
1071 _tool_command("map_identify_tool", _("&Identify"), "IdentifyTool",
1072 "IdentifyTool",
1073 helptext = _("Switch to map-mode 'identify'"), icon = "identify",
1074 sensitive = _has_visible_map)
1075 _tool_command("map_label_tool", _("&Label"), "LabelTool", "LabelTool",
1076 helptext = _("Add/Remove labels"), icon = "label",
1077 sensitive = _has_visible_map)
1078 _method_command("map_full_extent", _("&Full extent"), "FullExtent",
1079 helptext = _("Zoom to the full map extent"), icon = "fullextent",
1080 sensitive = _has_visible_map)
1081 _method_command("layer_full_extent", _("&Full layer extent"), "FullLayerExtent",
1082 helptext = _("Zoom to the full layer extent"),
1083 icon = "fulllayerextent", sensitive = _has_selected_layer)
1084 _method_command("selected_full_extent", _("&Full selection extent"),
1085 "FullSelectionExtent",
1086 helptext = _("Zoom to the full selection extent"),
1087 icon = "fullselextent", sensitive = _has_selected_shapes)
1088 _method_command("map_export", _("E&xport"), "ExportMap",
1089 helptext = _("Export the map to file"))
1090 _method_command("map_print", _("Prin&t"), "PrintMap",
1091 helptext = _("Print the map"))
1092 _method_command("map_rename", _("&Rename..."), "RenameMap",
1093 helptext = _("Rename the map"))
1094 _method_command("layer_add", _("&Add Layer..."), "AddLayer",
1095 helptext = _("Add a new layer to the map"))
1096 _method_command("rasterlayer_add", _("&Add Image Layer..."), "AddRasterLayer",
1097 helptext = _("Add a new image layer to the map"),
1098 sensitive = _has_gdal_support)
1099 _method_command("layer_add_db", _("Add &Database Layer..."), "AddDBLayer",
1100 helptext = _("Add a new database layer to active map"),
1101 sensitive = _has_dbconnections)
1102 _method_command("layer_remove", _("&Remove Layer"), "RemoveLayer",
1103 helptext = _("Remove selected layer"),
1104 sensitive = _can_remove_layer)
1105
1106 # Layer menu
1107 _method_command("layer_projection", _("Pro&jection..."), "LayerProjection",
1108 sensitive = _has_selected_layer,
1109 helptext = _("Specify projection for selected layer"))
1110 _method_command("layer_duplicate", _("&Duplicate"), "DuplicateLayer",
1111 helptext = _("Duplicate selected layer"),
1112 sensitive = lambda context: context.mainwindow.CanDuplicateLayer())
1113 _method_command("layer_rename", _("Re&name ..."), "RenameLayer",
1114 helptext = _("Rename selected layer"),
1115 sensitive = _has_selected_layer)
1116 _method_command("layer_raise", _("&Raise"), "RaiseLayer",
1117 helptext = _("Raise selected layer"),
1118 sensitive = _has_selected_layer)
1119 _method_command("layer_lower", _("&Lower"), "LowerLayer",
1120 helptext = _("Lower selected layer"),
1121 sensitive = _has_selected_layer)
1122 _method_command("layer_show", _("&Show"), "ShowLayer",
1123 helptext = _("Make selected layer visible"),
1124 sensitive = _has_selected_layer)
1125 _method_command("layer_hide", _("&Hide"), "HideLayer",
1126 helptext = _("Make selected layer unvisible"),
1127 sensitive = _has_selected_layer)
1128 _method_command("layer_show_table", _("Show Ta&ble"), "LayerShowTable",
1129 helptext = _("Show the selected layer's table"),
1130 sensitive = _has_selected_shape_layer)
1131 _method_command("layer_properties", _("&Properties..."), "LayerEditProperties",
1132 sensitive = _has_selected_layer,
1133 helptext = _("Edit the properties of the selected layer"))
1134 _method_command("layer_jointable", _("&Join Table..."), "LayerJoinTable",
1135 sensitive = _has_selected_shape_layer,
1136 helptext = _("Join and attach a table to the selected layer"))
1137
1138 def _can_unjoin(context):
1139 """Return whether the Layer/Unjoin command can be executed.
1140
1141 This is the case if a layer is selected and that layer has a
1142 shapestore that has an original shapestore.
1143 """
1144 layer = context.mainwindow.SelectedLayer()
1145 if layer is None:
1146 return 0
1147 getstore = getattr(layer, "ShapeStore", None)
1148 if getstore is not None:
1149 return getstore().OrigShapeStore() is not None
1150 else:
1151 return 0
1152 _method_command("layer_unjointable", _("&Unjoin Table..."), "LayerUnjoinTable",
1153 sensitive = _can_unjoin,
1154 helptext = _("Undo the last join operation"))
1155
1156
1157 def _has_tables(context):
1158 return bool(context.session.Tables())
1159
1160 # Table menu
1161 _method_command("table_open", _("&Open..."), "TableOpen",
1162 helptext = _("Open a DBF-table from a file"))
1163 _method_command("table_close", _("&Close..."), "TableClose",
1164 sensitive = lambda context: bool(context.session.UnreferencedTables()),
1165 helptext = _("Close one or more tables from a list"))
1166 _method_command("table_rename", _("&Rename..."), "TableRename",
1167 sensitive = _has_tables,
1168 helptext = _("Rename one or more tables"))
1169 _method_command("table_show", _("&Show..."), "TableShow",
1170 sensitive = _has_tables,
1171 helptext = _("Show one or more tables in a dialog"))
1172 _method_command("table_join", _("&Join..."), "TableJoin",
1173 sensitive = _has_tables,
1174 helptext = _("Join two tables creating a new one"))
1175
1176 # Export only under Windows ...
1177 map_menu = ["layer_add", "layer_add_db", "rasterlayer_add", "layer_remove",
1178 None,
1179 "map_rename",
1180 "map_projection",
1181 None,
1182 "map_zoom_in_tool", "map_zoom_out_tool",
1183 "map_pan_tool",
1184 "map_full_extent",
1185 "layer_full_extent",
1186 "selected_full_extent",
1187 None,
1188 "map_identify_tool", "map_label_tool",
1189 None,
1190 "toggle_legend",
1191 None]
1192 if wxPlatform == '__WXMSW__':
1193 map_menu.append("map_export")
1194 map_menu.append("map_print")
1195
1196 # the menu structure
1197 main_menu = Menu("<main>", "<main>",
1198 [Menu("file", _("&File"),
1199 ["new_session", "open_session", None,
1200 "save_session", "save_session_as", None,
1201 "database_management", None,
1202 "toggle_session_tree", None,
1203 "exit"]),
1204 Menu("map", _("&Map"), map_menu),
1205 Menu("layer", _("&Layer"),
1206 ["layer_rename", "layer_duplicate",
1207 None,
1208 "layer_raise", "layer_lower",
1209 None,
1210 "layer_show", "layer_hide",
1211 None,
1212 "layer_projection",
1213 None,
1214 "layer_show_table",
1215 "layer_jointable",
1216 "layer_unjointable",
1217 None,
1218 "layer_properties"]),
1219 Menu("table", _("&Table"),
1220 ["table_open", "table_close", "table_rename",
1221 None,
1222 "table_show",
1223 None,
1224 "table_join"]),
1225 Menu("help", _("&Help"),
1226 ["help_about"])])
1227
1228 # the main toolbar
1229
1230 main_toolbar = Menu("<toolbar>", "<toolbar>",
1231 ["map_zoom_in_tool", "map_zoom_out_tool", "map_pan_tool",
1232 "map_full_extent",
1233 "layer_full_extent",
1234 "selected_full_extent",
1235 None,
1236 "map_identify_tool", "map_label_tool"])
1237

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26