/[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 2534 - (show annotations)
Fri Jan 21 08:31:16 2005 UTC (20 years, 1 month ago) by jan
Original Path: trunk/thuban/Thuban/UI/mainwindow.py
File MIME type: text/x-python
File size: 51784 byte(s)
(view_position_changed): Made string available for i18n.

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 """Put current position in status bar after a mouse move."""
339 pos = self.canvas.CurrentPosition()
340 if pos is not None:
341 text = "(%10.10g, %10.10g)" % pos
342 else:
343 text = ""
344 # XXX This is a hack until we find a better place for this code.
345 # (BER 20050120)
346 # BH wrote (20050120):
347 # this branch is only executed when the mouse
348 # leaves the canvas window, so it's not that often [..]
349 # [Here] not the right place to put this code.
350 # I don't have a better solution at hand,
351 # but the view_position_changed is only there to update
352 # the current position. If other information is to
353 # be shown in the status bar it should
354 # be handled in a different way and
355 # by other methods.
356 #
357 # The status bar widget supports some kind of stack of texts.
358 # maybe that can be used to distinguis
359 # between short-lived information such as the mouse position
360 # and more permanent information such as the hint
361 # about the projections.
362 map = self.canvas.Map()
363 for layer in map.layers:
364 bbox = layer.LatLongBoundingBox()
365 if bbox:
366 left, bottom, right, top = bbox
367 if not (-180 <= left <= 180 and
368 -180 <= right <= 180 and
369 -90 <= top <= 90 and
370 -90 <= bottom <= 90):
371 text = _("Select layer '%s' and pick a projection "
372 "using Layer/Projection...") % layer.title
373 break
374
375 self.set_position_text(text)
376
377 def set_position_text(self, text):
378 """Set the statusbar text showing the current position.
379
380 By default the text is shown in field 0 of the status bar.
381 Override this method in derived classes to put it into a
382 different field of the statusbar.
383 """
384 self.SetStatusText(text)
385
386 def OpenOrRaiseDialog(self, name, dialog_class, *args, **kw):
387 """
388 Open or raise a dialog.
389
390 If a dialog with the denoted name does already exist it is
391 raised. Otherwise a new dialog, an instance of dialog_class,
392 is created, inserted into the main list and displayed.
393 """
394 dialog = self.get_open_dialog(name)
395
396 if dialog is None:
397 dialog = dialog_class(self, name, *args, **kw)
398 self.add_dialog(name, dialog)
399 dialog.Show(True)
400 else:
401 dialog.Raise()
402
403 def save_modified_session(self, can_veto = 1):
404 """If the current session has been modified, ask the user
405 whether to save it and do so if requested. Return the outcome of
406 the dialog (either wxID_OK, wxID_CANCEL or wxID_NO). If the
407 dialog wasn't run return wxID_NO.
408
409 If the can_veto parameter is true (default) the dialog includes
410 a cancel button, otherwise not.
411 """
412 if self.application.session.WasModified():
413 flags = wxYES_NO | wxICON_QUESTION
414 if can_veto:
415 flags = flags | wxCANCEL
416 result = self.RunMessageBox(_("Exit"),
417 _("The session has been modified."
418 " Do you want to save it?"),
419 flags)
420 if result == wxID_YES:
421 self.SaveSession()
422 else:
423 result = wxID_NO
424 return result
425
426 def NewSession(self):
427 if self.save_modified_session() != wxID_CANCEL:
428 self.application.SetSession(create_empty_session())
429
430 def OpenSession(self):
431 if self.save_modified_session() != wxID_CANCEL:
432 dlg = wxFileDialog(self, _("Open Session"),
433 self.application.Path("data"), "",
434 "Thuban Session File (*.thuban)|*.thuban",
435 wxOPEN)
436 if dlg.ShowModal() == wxID_OK:
437 self.application.OpenSession(dlg.GetPath(),
438 self.run_db_param_dialog)
439 self.application.SetPath("data", dlg.GetPath())
440 dlg.Destroy()
441
442 def run_db_param_dialog(self, parameters, message):
443 dlg = DBDialog(self, _("DB Connection Parameters"), parameters,
444 message)
445 return dlg.RunDialog()
446
447 def SaveSession(self):
448 if self.application.session.filename == None:
449 self.SaveSessionAs()
450 else:
451 self.application.SaveSession()
452
453 def SaveSessionAs(self):
454 dlg = wxFileDialog(self, _("Save Session As"),
455 self.application.Path("data"), "",
456 "Thuban Session File (*.thuban)|*.thuban",
457 wxSAVE|wxOVERWRITE_PROMPT)
458 if dlg.ShowModal() == wxID_OK:
459 self.application.session.SetFilename(dlg.GetPath())
460 self.application.SaveSession()
461 self.application.SetPath("data",dlg.GetPath())
462 dlg.Destroy()
463
464 def Exit(self):
465 self.Close(False)
466
467 def OnClose(self, event):
468 result = self.save_modified_session(can_veto = event.CanVeto())
469 if result == wxID_CANCEL:
470 event.Veto()
471 else:
472 # FIXME: it would be better to tie the unsubscription to
473 # wx's destroy event, but that isn't implemented for wxGTK
474 # yet.
475 self.canvas.Unsubscribe(VIEW_POSITION, self.view_position_changed)
476 DockFrame.OnClose(self, event)
477 for dlg in self.dialogs.values():
478 dlg.Destroy()
479 self.canvas.Destroy()
480 self.Destroy()
481
482 def SetMap(self, map):
483 self.canvas.SetMap(map)
484 self.update_title()
485
486 dialog = self.FindRegisteredDock("legend")
487 if dialog is not None:
488 dialog.GetPanel().SetMap(self.Map())
489
490 def Map(self):
491 """Return the map displayed by this mainwindow"""
492
493 return self.canvas.Map()
494
495 def ToggleSessionTree(self):
496 """If the session tree is shown close it otherwise create a new tree"""
497 name = "session_tree"
498 dialog = self.get_open_dialog(name)
499 if dialog is None:
500 dialog = tree.SessionTreeView(self, self.application, name)
501 self.add_dialog(name, dialog)
502 dialog.Show(True)
503 else:
504 dialog.Close()
505
506 def SessionTreeShown(self):
507 """Return true iff the session tree is currently shown"""
508 return self.get_open_dialog("session_tree") is not None
509
510 def About(self):
511 dlg = About(self)
512 dlg.ShowModal()
513 dlg.Destroy()
514
515 def DatabaseManagement(self):
516 name = "dbmanagement"
517 dialog = self.get_open_dialog(name)
518 if dialog is None:
519 map = self.canvas.Map()
520 dialog = DBFrame(self, name, self.application.Session())
521 self.add_dialog(name, dialog)
522 dialog.Show()
523 dialog.Raise()
524
525 def AddLayer(self):
526 dlg = wxFileDialog(self, _("Select one or more data files"),
527 self.application.Path("data"), "",
528 _("Shapefiles (*.shp)") + "|*.shp;*.SHP|" +
529 _("All Files (*.*)") + "|*.*",
530 wxOPEN | wxMULTIPLE)
531 if dlg.ShowModal() == wxID_OK:
532 filenames = dlg.GetPaths()
533 for filename in filenames:
534 title = os.path.splitext(os.path.basename(filename))[0]
535 map = self.canvas.Map()
536 has_layers = map.HasLayers()
537 try:
538 store = self.application.Session().OpenShapefile(filename)
539 except IOError:
540 # the layer couldn't be opened
541 self.RunMessageBox(_("Add Layer"),
542 _("Can't open the file '%s'.")%filename)
543 else:
544 layer = Layer(title, store)
545 map.AddLayer(layer)
546 if not has_layers:
547 # if we're adding a layer to an empty map, fit the
548 # new map to the window
549 self.canvas.FitMapToWindow()
550 self.application.SetPath("data",filename)
551 dlg.Destroy()
552
553 def AddRasterLayer(self):
554 dlg = wxFileDialog(self, _("Select an image file"),
555 self.application.Path("data"), "", "*.*",
556 wxOPEN)
557 if dlg.ShowModal() == wxID_OK:
558 filename = dlg.GetPath()
559 title = os.path.splitext(os.path.basename(filename))[0]
560 map = self.canvas.Map()
561 has_layers = map.HasLayers()
562 try:
563 layer = RasterLayer(title, filename)
564 except IOError:
565 # the layer couldn't be opened
566 self.RunMessageBox(_("Add Image Layer"),
567 _("Can't open the file '%s'.") % filename)
568 else:
569 map.AddLayer(layer)
570 if not has_layers:
571 # if we're adding a layer to an empty map, fit the
572 # new map to the window
573 self.canvas.FitMapToWindow()
574 self.application.SetPath("data", filename)
575 dlg.Destroy()
576
577 def AddDBLayer(self):
578 """Add a layer read from a database"""
579 session = self.application.Session()
580 dlg = ChooseDBTableDialog(self, self.application.Session())
581
582 if dlg.ShowModal() == wxID_OK:
583 dbconn, dbtable, id_column, geo_column = dlg.GetTable()
584 try:
585 title = str(dbtable)
586
587 # Chose the correct Interface for the database type
588 store = session.OpenDBShapeStore(dbconn, dbtable,
589 id_column = id_column,
590 geometry_column = geo_column)
591 layer = Layer(title, store)
592 except:
593 # Some error occured while initializing the layer
594 self.RunMessageBox(_("Add Layer from database"),
595 _("Can't open the database table '%s'")
596 % dbtable)
597 return
598
599 map = self.canvas.Map()
600
601 has_layers = map.HasLayers()
602 map.AddLayer(layer)
603 if not has_layers:
604 self.canvas.FitMapToWindow()
605
606 dlg.Destroy()
607
608 def RemoveLayer(self):
609 layer = self.current_layer()
610 if layer is not None:
611 self.canvas.Map().RemoveLayer(layer)
612
613 def CanRemoveLayer(self):
614 """Return true if the currently selected layer can be deleted.
615
616 If no layer is selected return False.
617
618 The return value of this method determines whether the remove
619 layer command is sensitive in menu.
620 """
621 layer = self.current_layer()
622 if layer is not None:
623 return self.canvas.Map().CanRemoveLayer(layer)
624 return False
625
626 def LayerToTop(self):
627 layer = self.current_layer()
628 if layer is not None:
629 self.canvas.Map().MoveLayerToTop(layer)
630
631 def RaiseLayer(self):
632 layer = self.current_layer()
633 if layer is not None:
634 self.canvas.Map().RaiseLayer(layer)
635
636 def LowerLayer(self):
637 layer = self.current_layer()
638 if layer is not None:
639 self.canvas.Map().LowerLayer(layer)
640
641 def LayerToBottom(self):
642 layer = self.current_layer()
643 if layer is not None:
644 self.canvas.Map().MoveLayerToBottom(layer)
645
646 def current_layer(self):
647 """Return the currently selected layer.
648
649 If no layer is selected, return None
650 """
651 return self.canvas.SelectedLayer()
652
653 def has_selected_layer(self):
654 """Return true if a layer is currently selected"""
655 return self.canvas.HasSelectedLayer()
656
657 def has_selected_shape_layer(self):
658 """Return true if a shape layer is currently selected"""
659 return isinstance(self.current_layer(), Layer)
660
661 def has_selected_shapes(self):
662 """Return true if a shape is currently selected"""
663 return self.canvas.HasSelectedShapes()
664
665 def HideLayer(self):
666 layer = self.current_layer()
667 if layer is not None:
668 layer.SetVisible(False)
669
670 def ShowLayer(self):
671 layer = self.current_layer()
672 if layer is not None:
673 layer.SetVisible(True)
674
675 def ToggleLayerVisibility(self):
676 layer = self.current_layer()
677 layer.SetVisible(not layer.Visible())
678
679 def DuplicateLayer(self):
680 """Ceate a new layer above the selected layer with the same shapestore
681 """
682 layer = self.current_layer()
683 if layer is not None and hasattr(layer, "ShapeStore"):
684 new_layer = Layer(_("Copy of `%s'") % layer.Title(),
685 layer.ShapeStore(),
686 projection = layer.GetProjection())
687 new_classification = copy.deepcopy(layer.GetClassification())
688 new_layer.SetClassificationColumn(
689 layer.GetClassificationColumn())
690 new_layer.SetClassification(new_classification)
691 self.Map().AddLayer(new_layer)
692
693 def CanDuplicateLayer(self):
694 """Return whether the DuplicateLayer method can create a duplicate"""
695 layer = self.current_layer()
696 return layer is not None and hasattr(layer, "ShapeStore")
697
698 def LayerShowTable(self):
699 """
700 Present a TableView Window for the current layer.
701 In case the window is already open, bring it to the front.
702 In case, there is no active layer, do nothing.
703 In case, the layer has no ShapeStore, do nothing.
704 """
705 layer = self.current_layer()
706 if layer is not None:
707 if not hasattr(layer, "ShapeStore"):
708 return
709 table = layer.ShapeStore().Table()
710 name = "table_view" + str(id(table))
711 dialog = self.get_open_dialog(name)
712 if dialog is None:
713 dialog = tableview.LayerTableFrame(self, name,
714 _("Layer Table: %s") % layer.Title(),
715 layer, table)
716 self.add_dialog(name, dialog)
717 dialog.Show(True)
718 else:
719 dialog.Raise()
720
721 def MapProjection(self):
722
723 name = "map_projection"
724 dialog = self.get_open_dialog(name)
725
726 if dialog is None:
727 map = self.canvas.Map()
728 dialog = projdialog.ProjFrame(self, name,
729 _("Map Projection: %s") % map.Title(), map)
730 self.add_dialog(name, dialog)
731 dialog.Show()
732 dialog.Raise()
733
734 def LayerProjection(self):
735
736 layer = self.current_layer()
737
738 name = "layer_projection" + str(id(layer))
739 dialog = self.get_open_dialog(name)
740
741 if dialog is None:
742 map = self.canvas.Map()
743 dialog = projdialog.ProjFrame(self, name,
744 _("Layer Projection: %s") % layer.Title(), layer)
745 self.add_dialog(name, dialog)
746 dialog.Show()
747 dialog.Raise()
748
749 def LayerEditProperties(self):
750
751 #
752 # the menu option for this should only be available if there
753 # is a current layer, so we don't need to check if the
754 # current layer is None
755 #
756
757 layer = self.current_layer()
758 self.OpenLayerProperties(layer)
759
760 def OpenLayerProperties(self, layer, group = None):
761 """
762 Open or raise the properties dialog.
763
764 This method opens or raises the properties dialog for the
765 currently selected layer if one is defined for this layer
766 type.
767 """
768 dialog_class = layer_properties_dialogs.get(layer)
769
770 if dialog_class is not None:
771 name = "layer_properties" + str(id(layer))
772 self.OpenOrRaiseDialog(name, dialog_class, layer, group = group)
773
774 def LayerJoinTable(self):
775 layer = self.canvas.SelectedLayer()
776 if layer is not None:
777 dlg = JoinDialog(self, _("Join Layer with Table"),
778 self.application.session,
779 layer = layer)
780 dlg.ShowModal()
781
782 def LayerUnjoinTable(self):
783 layer = self.canvas.SelectedLayer()
784 if layer is not None:
785 orig_store = layer.ShapeStore().OrigShapeStore()
786 if orig_store:
787 layer.SetShapeStore(orig_store)
788
789 def ShowLegend(self):
790 if not self.LegendShown():
791 self.ToggleLegend()
792
793 def ToggleLegend(self):
794 """Show the legend if it's not shown otherwise hide it again"""
795 name = "legend"
796 dialog = self.FindRegisteredDock(name)
797
798 if dialog is None:
799 dialog = self.CreateDock(name, -1, _("Legend"), wxLAYOUT_LEFT)
800 legend.LegendPanel(dialog, None, self)
801 dialog.Dock()
802 dialog.GetPanel().SetMap(self.Map())
803 dialog.Show()
804 else:
805 dialog.Show(not dialog.IsShown())
806
807 def LegendShown(self):
808 """Return true iff the legend is currently open"""
809 dialog = self.FindRegisteredDock("legend")
810 return dialog is not None and dialog.IsShown()
811
812 def TableOpen(self):
813 dlg = wxFileDialog(self, _("Open Table"),
814 self.application.Path("data"), "",
815 _("DBF Files (*.dbf)") + "|*.dbf|" +
816 #_("CSV Files (*.csv)") + "|*.csv|" +
817 _("All Files (*.*)") + "|*.*",
818 wxOPEN)
819 if dlg.ShowModal() == wxID_OK:
820 filename = dlg.GetPath()
821 dlg.Destroy()
822 try:
823 table = self.application.session.OpenTableFile(filename)
824 except IOError:
825 # the layer couldn't be opened
826 self.RunMessageBox(_("Open Table"),
827 _("Can't open the file '%s'.") % filename)
828 else:
829 self.ShowTableView(table)
830 self.application.SetPath("data",filename)
831
832 def TableClose(self):
833 tables = self.application.session.UnreferencedTables()
834
835 lst = [(t.Title(), t) for t in tables]
836 lst.sort()
837 titles = [i[0] for i in lst]
838 dlg = wxMultipleChoiceDialog(self, _("Pick the tables to close:"),
839 _("Close Table"), titles,
840 size = (400, 300),
841 style = wxDEFAULT_DIALOG_STYLE |
842 wxRESIZE_BORDER)
843 if dlg.ShowModal() == wxID_OK:
844 for i in dlg.GetValue():
845 self.application.session.RemoveTable(lst[i][1])
846
847
848 def TableShow(self):
849 """Offer a multi-selection dialog for tables to be displayed
850
851 The windows for the selected tables are opened or brought to
852 the front.
853 """
854 tables = self.application.session.Tables()
855
856 lst = [(t.Title(), t) for t in tables]
857 lst.sort()
858 titles = [i[0] for i in lst]
859 dlg = wxMultipleChoiceDialog(self, _("Pick the table to show:"),
860 _("Show Table"), titles,
861 size = (400,300),
862 style = wxDEFAULT_DIALOG_STYLE |
863 wxRESIZE_BORDER)
864 if (dlg.ShowModal() == wxID_OK):
865 for i in dlg.GetValue():
866 # XXX: if the table belongs to a layer, open a
867 # LayerTableFrame instead of QueryTableFrame
868 self.ShowTableView(lst[i][1])
869
870 def TableJoin(self):
871 dlg = JoinDialog(self, _("Join Tables"), self.application.session)
872 dlg.ShowModal()
873
874 def ShowTableView(self, table):
875 """Open a table view for the table and optionally"""
876 name = "table_view%d" % id(table)
877 dialog = self.get_open_dialog(name)
878 if dialog is None:
879 dialog = tableview.QueryTableFrame(self, name,
880 _("Table: %s") % table.Title(),
881 table)
882 self.add_dialog(name, dialog)
883 dialog.Show(True)
884 dialog.Raise()
885
886 def TableRename(self):
887 """Let the user rename a table"""
888
889 # First, let the user select a table
890 tables = self.application.session.Tables()
891 lst = [(t.Title(), t) for t in tables]
892 lst.sort()
893 titles = [i[0] for i in lst]
894 dlg = wxMultipleChoiceDialog(self, _("Pick the table to rename:"),
895 _("Rename Table"), titles,
896 size = (400,300),
897 style = wxDEFAULT_DIALOG_STYLE |
898 wxRESIZE_BORDER)
899 if (dlg.ShowModal() == wxID_OK):
900 to_rename = [lst[i][1] for i in dlg.GetValue()]
901 dlg.Destroy()
902 else:
903 to_rename = []
904
905 # Second, let the user rename the layers
906 for table in to_rename:
907 dlg = wxTextEntryDialog(self, _("Table Title:"), _("Rename Table"),
908 table.Title())
909 try:
910 if dlg.ShowModal() == wxID_OK:
911 title = dlg.GetValue()
912 if title != "":
913 table.SetTitle(title)
914
915 # Make sure the session is marked as modified.
916 # FIXME: This should be handled automatically,
917 # but that requires more changes to the tables
918 # than I have time for currently.
919 self.application.session.changed()
920 finally:
921 dlg.Destroy()
922
923
924 def ZoomInTool(self):
925 self.canvas.ZoomInTool()
926
927 def ZoomOutTool(self):
928 self.canvas.ZoomOutTool()
929
930 def PanTool(self):
931 self.canvas.PanTool()
932
933 def IdentifyTool(self):
934 self.canvas.IdentifyTool()
935 self.identify_view_on_demand(None, None)
936
937 def LabelTool(self):
938 self.canvas.LabelTool()
939
940 def FullExtent(self):
941 self.canvas.FitMapToWindow()
942
943 def FullLayerExtent(self):
944 self.canvas.FitLayerToWindow(self.current_layer())
945
946 def FullSelectionExtent(self):
947 self.canvas.FitSelectedToWindow()
948
949 def ExportMap(self):
950 self.canvas.Export()
951
952 def PrintMap(self):
953 self.canvas.Print()
954
955 def RenameMap(self):
956 dlg = wxTextEntryDialog(self, _("Map Title:"), _("Rename Map"),
957 self.Map().Title())
958 if dlg.ShowModal() == wxID_OK:
959 title = dlg.GetValue()
960 if title != "":
961 self.Map().SetTitle(title)
962
963 dlg.Destroy()
964
965 def RenameLayer(self):
966 """Let the user rename the currently selected layer"""
967 layer = self.current_layer()
968 if layer is not None:
969 dlg = wxTextEntryDialog(self, _("Layer Title:"), _("Rename Layer"),
970 layer.Title())
971 try:
972 if dlg.ShowModal() == wxID_OK:
973 title = dlg.GetValue()
974 if title != "":
975 layer.SetTitle(title)
976 finally:
977 dlg.Destroy()
978
979 def identify_view_on_demand(self, layer, shapes):
980 """Subscribed to the canvas' SHAPES_SELECTED message
981
982 If the current tool is the identify tool, at least one shape is
983 selected and the identify dialog is not shown, show the dialog.
984 """
985 # If the selection has become empty we don't need to do
986 # anything. Otherwise it could happen that the dialog was popped
987 # up when the selection became empty, e.g. when a new selection
988 # is opened while the identify tool is active and dialog had
989 # been closed
990 if not shapes:
991 return
992
993 name = "identify_view"
994 if self.canvas.CurrentTool() == "IdentifyTool":
995 if not self.dialog_open(name):
996 dialog = identifyview.IdentifyView(self, name)
997 self.add_dialog(name, dialog)
998 dialog.Show(True)
999 else:
1000 # FIXME: bring dialog to front?
1001 pass
1002
1003 def title_changed(self, map):
1004 """Subscribed to the canvas' TITLE_CHANGED messages"""
1005 self.update_title()
1006
1007 def update_title(self):
1008 """Update the window's title according to it's current state.
1009
1010 In this default implementation the title is 'Thuban - ' followed
1011 by the map's title or simply 'Thuban' if there is not map.
1012 Derived classes should override this method to get different
1013 titles.
1014
1015 This method is called automatically by other methods when the
1016 title may have to change. For the methods implemented in this
1017 class this usually only means that a different map has been set
1018 or the current map's title has changed.
1019 """
1020 map = self.Map()
1021 if map is not None:
1022 title = _("Thuban - %s") % (map.Title(),)
1023 else:
1024 title = _("Thuban")
1025 self.SetTitle(title)
1026
1027
1028 #
1029 # Define all the commands available in the main window
1030 #
1031
1032
1033 # Helper functions to define common command implementations
1034 def call_method(context, methodname, *args):
1035 """Call the mainwindow's method methodname with args *args"""
1036 apply(getattr(context.mainwindow, methodname), args)
1037
1038 def _method_command(name, title, method, helptext = "",
1039 icon = "", sensitive = None, checked = None):
1040 """Add a command implemented by a method of the mainwindow object"""
1041 registry.Add(Command(name, title, call_method, args=(method,),
1042 helptext = helptext, icon = icon,
1043 sensitive = sensitive, checked = checked))
1044
1045 def make_check_current_tool(toolname):
1046 """Return a function that tests if the currently active tool is toolname
1047
1048 The returned function can be called with the context and returns
1049 true iff the currently active tool's name is toolname. It's directly
1050 usable as the 'checked' callback of a command.
1051 """
1052 def check_current_tool(context, name=toolname):
1053 return context.mainwindow.canvas.CurrentTool() == name
1054 return check_current_tool
1055
1056 def _tool_command(name, title, method, toolname, helptext = "",
1057 icon = "", sensitive = None):
1058 """Add a tool command"""
1059 registry.Add(ToolCommand(name, title, call_method, args=(method,),
1060 helptext = helptext, icon = icon,
1061 checked = make_check_current_tool(toolname),
1062 sensitive = sensitive))
1063
1064 def _has_selected_layer(context):
1065 """Return true if a layer is selected in the context"""
1066 return context.mainwindow.has_selected_layer()
1067
1068 def _has_selected_layer_visible(context):
1069 """Return true if a layer is selected in the context which is
1070 visible."""
1071 if context.mainwindow.has_selected_layer():
1072 layer = context.mainwindow.current_layer()
1073 if layer.Visible(): return True
1074 return False
1075
1076 def _has_selected_shape_layer(context):
1077 """Return true if a shape layer is selected in the context"""
1078 return context.mainwindow.has_selected_shape_layer()
1079
1080 def _has_selected_shapes(context):
1081 """Return true if a layer is selected in the context"""
1082 return context.mainwindow.has_selected_shapes()
1083
1084 def _can_remove_layer(context):
1085 return context.mainwindow.CanRemoveLayer()
1086
1087 def _has_tree_window_shown(context):
1088 """Return true if the tree window is shown"""
1089 return context.mainwindow.SessionTreeShown()
1090
1091 def _has_visible_map(context):
1092 """Return true iff theres a visible map in the mainwindow.
1093
1094 A visible map is a map with at least one visible layer."""
1095 map = context.mainwindow.Map()
1096 if map is not None:
1097 for layer in map.Layers():
1098 if layer.Visible():
1099 return True
1100 return False
1101
1102 def _has_legend_shown(context):
1103 """Return true if the legend window is shown"""
1104 return context.mainwindow.LegendShown()
1105
1106 def _has_gdal_support(context):
1107 """Return True if the GDAL is available"""
1108 return Thuban.Model.resource.has_gdal_support()
1109
1110 def _has_dbconnections(context):
1111 """Return whether the the session has database connections"""
1112 return context.session.HasDBConnections()
1113
1114 def _has_postgis_support(context):
1115 return has_postgis_support()
1116
1117
1118 # File menu
1119 _method_command("new_session", _("&New Session"), "NewSession",
1120 helptext = _("Start a new session"))
1121 _method_command("open_session", _("&Open Session..."), "OpenSession",
1122 helptext = _("Open a session file"))
1123 _method_command("save_session", _("&Save Session"), "SaveSession",
1124 helptext =_("Save this session to the file it was opened from"))
1125 _method_command("save_session_as", _("Save Session &As..."), "SaveSessionAs",
1126 helptext = _("Save this session to a new file"))
1127 _method_command("toggle_session_tree", _("Session &Tree"), "ToggleSessionTree",
1128 checked = _has_tree_window_shown,
1129 helptext = _("Toggle on/off the session tree analysis window"))
1130 _method_command("toggle_legend", _("Legend"), "ToggleLegend",
1131 checked = _has_legend_shown,
1132 helptext = _("Toggle Legend on/off"))
1133 _method_command("database_management", _("&Database Connections..."),
1134 "DatabaseManagement",
1135 sensitive = _has_postgis_support)
1136 _method_command("exit", _("E&xit"), "Exit",
1137 helptext = _("Finish working with Thuban"))
1138
1139 # Help menu
1140 _method_command("help_about", _("&About..."), "About",
1141 helptext = _("Info about Thuban authors, version and modules"))
1142
1143
1144 # Map menu
1145 _method_command("map_projection", _("Pro&jection..."), "MapProjection",
1146 helptext = _("Set or change the map projection"))
1147
1148 _tool_command("map_zoom_in_tool", _("&Zoom in"), "ZoomInTool", "ZoomInTool",
1149 helptext = _("Switch to map-mode 'zoom-in'"), icon = "zoom_in",
1150 sensitive = _has_visible_map)
1151 _tool_command("map_zoom_out_tool", _("Zoom &out"), "ZoomOutTool", "ZoomOutTool",
1152 helptext = _("Switch to map-mode 'zoom-out'"), icon = "zoom_out",
1153 sensitive = _has_visible_map)
1154 _tool_command("map_pan_tool", _("&Pan"), "PanTool", "PanTool",
1155 helptext = _("Switch to map-mode 'pan'"), icon = "pan",
1156 sensitive = _has_visible_map)
1157 _tool_command("map_identify_tool", _("&Identify"), "IdentifyTool",
1158 "IdentifyTool",
1159 helptext = _("Switch to map-mode 'identify'"), icon = "identify",
1160 sensitive = _has_visible_map)
1161 _tool_command("map_label_tool", _("&Label"), "LabelTool", "LabelTool",
1162 helptext = _("Add/Remove labels"), icon = "label",
1163 sensitive = _has_visible_map)
1164 _method_command("map_full_extent", _("&Full extent"), "FullExtent",
1165 helptext = _("Zoom to the full map extent"), icon = "fullextent",
1166 sensitive = _has_visible_map)
1167 _method_command("layer_full_extent", _("&Full layer extent"), "FullLayerExtent",
1168 helptext = _("Zoom to the full layer extent"),
1169 icon = "fulllayerextent", sensitive = _has_selected_layer)
1170 _method_command("selected_full_extent", _("&Full selection extent"),
1171 "FullSelectionExtent",
1172 helptext = _("Zoom to the full selection extent"),
1173 icon = "fullselextent", sensitive = _has_selected_shapes)
1174 _method_command("map_export", _("E&xport"), "ExportMap",
1175 helptext = _("Export the map to file"))
1176 _method_command("map_print", _("Prin&t"), "PrintMap",
1177 helptext = _("Print the map"))
1178 _method_command("map_rename", _("&Rename..."), "RenameMap",
1179 helptext = _("Rename the map"))
1180 _method_command("layer_add", _("&Add Layer..."), "AddLayer",
1181 helptext = _("Add a new layer to the map"))
1182 _method_command("rasterlayer_add", _("&Add Image Layer..."), "AddRasterLayer",
1183 helptext = _("Add a new image layer to the map"),
1184 sensitive = _has_gdal_support)
1185 _method_command("layer_add_db", _("Add &Database Layer..."), "AddDBLayer",
1186 helptext = _("Add a new database layer to active map"),
1187 sensitive = _has_dbconnections)
1188 _method_command("layer_remove", _("&Remove Layer"), "RemoveLayer",
1189 helptext = _("Remove selected layer"),
1190 sensitive = _can_remove_layer)
1191
1192 # Layer menu
1193 _method_command("layer_projection", _("Pro&jection..."), "LayerProjection",
1194 sensitive = _has_selected_layer,
1195 helptext = _("Specify projection for selected layer"))
1196 _method_command("layer_duplicate", _("&Duplicate"), "DuplicateLayer",
1197 helptext = _("Duplicate selected layer"),
1198 sensitive = lambda context: context.mainwindow.CanDuplicateLayer())
1199 _method_command("layer_rename", _("Re&name ..."), "RenameLayer",
1200 helptext = _("Rename selected layer"),
1201 sensitive = _has_selected_layer)
1202 _method_command("layer_raise", _("&Raise"), "RaiseLayer",
1203 helptext = _("Raise selected layer"),
1204 sensitive = _has_selected_layer)
1205 _method_command("layer_lower", _("&Lower"), "LowerLayer",
1206 helptext = _("Lower selected layer"),
1207 sensitive = _has_selected_layer)
1208 _method_command("layer_show", _("&Show"), "ShowLayer",
1209 helptext = _("Make selected layer visible"),
1210 sensitive = _has_selected_layer)
1211 _method_command("layer_hide", _("&Hide"), "HideLayer",
1212 helptext = _("Make selected layer unvisible"),
1213 sensitive = _has_selected_layer)
1214 _method_command("layer_show_table", _("Show Ta&ble"), "LayerShowTable",
1215 helptext = _("Show the selected layer's table"),
1216 sensitive = _has_selected_shape_layer)
1217 _method_command("layer_properties", _("&Properties..."), "LayerEditProperties",
1218 sensitive = _has_selected_layer,
1219 helptext = _("Edit the properties of the selected layer"))
1220 _method_command("layer_jointable", _("&Join Table..."), "LayerJoinTable",
1221 sensitive = _has_selected_shape_layer,
1222 helptext = _("Join and attach a table to the selected layer"))
1223
1224 # further layer methods:
1225 _method_command("layer_to_top", _("&Top"), "LayerToTop",
1226 helptext = _("Put selected layer to the top"),
1227 sensitive = _has_selected_layer)
1228 _method_command("layer_to_bottom", _("&Bottom"), "LayerToBottom",
1229 helptext = _("Put selected layer to the bottom"),
1230 sensitive = _has_selected_layer)
1231 _method_command("layer_visibility", _("&Visible"), "ToggleLayerVisibility",
1232 checked = _has_selected_layer_visible,
1233 helptext = _("Toggle visibility of selected layer"),
1234 sensitive = _has_selected_layer)
1235
1236 def _can_unjoin(context):
1237 """Return whether the Layer/Unjoin command can be executed.
1238
1239 This is the case if a layer is selected and that layer has a
1240 shapestore that has an original shapestore.
1241 """
1242 layer = context.mainwindow.SelectedLayer()
1243 if layer is None:
1244 return 0
1245 getstore = getattr(layer, "ShapeStore", None)
1246 if getstore is not None:
1247 return getstore().OrigShapeStore() is not None
1248 else:
1249 return 0
1250 _method_command("layer_unjointable", _("&Unjoin Table..."), "LayerUnjoinTable",
1251 sensitive = _can_unjoin,
1252 helptext = _("Undo the last join operation"))
1253
1254
1255 def _has_tables(context):
1256 return bool(context.session.Tables())
1257
1258 # Table menu
1259 _method_command("table_open", _("&Open..."), "TableOpen",
1260 helptext = _("Open a DBF-table from a file"))
1261 _method_command("table_close", _("&Close..."), "TableClose",
1262 sensitive = lambda context: bool(context.session.UnreferencedTables()),
1263 helptext = _("Close one or more tables from a list"))
1264 _method_command("table_rename", _("&Rename..."), "TableRename",
1265 sensitive = _has_tables,
1266 helptext = _("Rename one or more tables"))
1267 _method_command("table_show", _("&Show..."), "TableShow",
1268 sensitive = _has_tables,
1269 helptext = _("Show one or more tables in a dialog"))
1270 _method_command("table_join", _("&Join..."), "TableJoin",
1271 sensitive = _has_tables,
1272 helptext = _("Join two tables creating a new one"))
1273
1274 # Export only under Windows ...
1275 map_menu = ["layer_add", "layer_add_db", "rasterlayer_add", "layer_remove",
1276 None,
1277 "map_rename",
1278 "map_projection",
1279 None,
1280 "map_zoom_in_tool", "map_zoom_out_tool",
1281 "map_pan_tool",
1282 "map_full_extent",
1283 "layer_full_extent",
1284 "selected_full_extent",
1285 None,
1286 "map_identify_tool", "map_label_tool",
1287 None,
1288 "toggle_legend",
1289 None]
1290 if wxPlatform == '__WXMSW__':
1291 map_menu.append("map_export")
1292 map_menu.append("map_print")
1293
1294 # the menu structure
1295 main_menu = Menu("<main>", "<main>",
1296 [Menu("file", _("&File"),
1297 ["new_session", "open_session", None,
1298 "save_session", "save_session_as", None,
1299 "database_management", None,
1300 "toggle_session_tree", None,
1301 "exit"]),
1302 Menu("map", _("&Map"), map_menu),
1303 Menu("layer", _("&Layer"),
1304 ["layer_rename", "layer_duplicate",
1305 None,
1306 "layer_raise", "layer_lower",
1307 None,
1308 "layer_show", "layer_hide",
1309 None,
1310 "layer_projection",
1311 None,
1312 "layer_show_table",
1313 "layer_jointable",
1314 "layer_unjointable",
1315 None,
1316 "layer_properties"]),
1317 Menu("table", _("&Table"),
1318 ["table_open", "table_close", "table_rename",
1319 None,
1320 "table_show",
1321 None,
1322 "table_join"]),
1323 Menu("help", _("&Help"),
1324 ["help_about"])])
1325
1326 # the main toolbar
1327
1328 main_toolbar = Menu("<toolbar>", "<toolbar>",
1329 ["map_zoom_in_tool", "map_zoom_out_tool", "map_pan_tool",
1330 "map_full_extent",
1331 "layer_full_extent",
1332 "selected_full_extent",
1333 None,
1334 "map_identify_tool", "map_label_tool"])
1335

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26