/[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 2529 - (show annotations)
Thu Jan 20 17:55:23 2005 UTC (20 years, 1 month ago) by russell
Original Path: trunk/thuban/Thuban/UI/mainwindow.py
File MIME type: text/x-python
File size: 50742 byte(s)
Warn user about misprojected layers when their lat/lon bounding
box exceeds rational lat/lon values.

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

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26