/[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 363 - (show annotations)
Mon Jan 27 11:40:13 2003 UTC (22 years, 1 month ago) by jonathan
Original Path: trunk/thuban/Thuban/UI/mainwindow.py
File MIME type: text/x-python
File size: 26218 byte(s)
added 'Classify' option in 'Layer' menu

1 # Copyright (C) 2001, 2002 by Intevation GmbH
2 # Authors:
3 # Jan-Oliver Wagner <[email protected]>
4 # Bernhard Herzog <[email protected]>
5 #
6 # This program is free software under the GPL (>=v2)
7 # Read the file COPYING coming with Thuban for details.
8
9 """
10 The main window
11 """
12
13 __version__ = "$Revision$"
14
15 import os
16
17 from wxPython.wx import *
18
19 import Thuban
20 from Thuban.Model.session import create_empty_session
21 from Thuban.Model.layer import Layer
22 from Thuban.Model.color import Color
23 from Thuban.Model.proj import Projection
24
25 import view
26 import tree
27 import proj4dialog
28 import tableview, identifyview
29 import classifier
30 from menu import Menu
31
32 from context import Context
33 from command import registry, Command, ToolCommand
34 from messages import SELECTED_SHAPE, VIEW_POSITION
35
36
37 # the directory where the toolbar icons are stored
38 bitmapdir = os.path.join(Thuban.__path__[0], os.pardir, "Resources", "Bitmaps")
39 bitmapext = ".xpm"
40
41
42 class MainWindow(wxFrame):
43
44 def __init__(self, parent, ID, title, application, interactor,
45 initial_message = None, size = wxSize(-1, -1)):
46 wxFrame.__init__(self, parent, ID, title, wxDefaultPosition, size)
47
48 self.application = application
49 self.interactor = interactor
50
51 self.CreateStatusBar()
52 if initial_message:
53 self.SetStatusText(initial_message)
54
55 self.identify_view = None
56
57 self.init_ids()
58
59 # creat the menubar from the main_menu description
60 self.SetMenuBar(self.build_menu_bar(main_menu))
61
62 # Similarly, create the toolbar from main_toolbar
63 toolbar = self.build_toolbar(main_toolbar)
64 # call Realize to make sure that the tools appear.
65 toolbar.Realize()
66
67 # Create the map canvas
68 canvas = view.MapCanvas(self, -1, interactor)
69 canvas.Subscribe(VIEW_POSITION, self.view_position_changed)
70 self.canvas = canvas
71
72 self.init_dialogs()
73
74 interactor.Subscribe(SELECTED_SHAPE, self.identify_view_on_demand)
75
76 EVT_CLOSE(self, self.OnClose)
77
78 def init_ids(self):
79 """Initialize the ids"""
80 self.current_id = 6000
81 self.id_to_name = {}
82 self.name_to_id = {}
83 self.events_bound = {}
84
85 def get_id(self, name):
86 """Return the wxWindows id for the command named name.
87
88 Create a new one if there isn't one yet"""
89 ID = self.name_to_id.get(name)
90 if ID is None:
91 ID = self.current_id
92 self.current_id = self.current_id + 1
93 self.name_to_id[name] = ID
94 self.id_to_name[ID] = name
95 return ID
96
97 def bind_command_events(self, command, ID):
98 """Bind the necessary events for the given command and ID"""
99 if not self.events_bound.has_key(ID):
100 # the events haven't been bound yet
101 EVT_MENU(self, ID, self.invoke_command)
102 if command.IsDynamic():
103 EVT_UPDATE_UI(self, ID, self.update_command_ui)
104
105 def build_menu_bar(self, menudesc):
106 """Build and return the menu bar from the menu description"""
107 menu_bar = wxMenuBar()
108
109 for item in menudesc.items:
110 # here the items must all be Menu instances themselves
111 menu_bar.Append(self.build_menu(item), item.title)
112
113 return menu_bar
114
115 def build_menu(self, menudesc):
116 """Return a wxMenu built from the menu description menudesc"""
117 wxmenu = wxMenu()
118 last = None
119 for item in menudesc.items:
120 if item is None:
121 # a separator. Only add one if the last item was not a
122 # separator
123 if last is not None:
124 wxmenu.AppendSeparator()
125 elif isinstance(item, Menu):
126 # a submenu
127 wxmenu.AppendMenu(wxNewId(), item.title, self.build_menu(item))
128 else:
129 # must the name the name of a command
130 self.add_menu_command(wxmenu, item)
131 last = item
132 return wxmenu
133
134 def build_toolbar(self, toolbardesc):
135 """Build and return the main toolbar window from a toolbar description
136
137 The parameter should be an instance of the Menu class but it
138 should not contain submenus.
139 """
140 toolbar = self.CreateToolBar(wxTB_3DBUTTONS)
141
142 # set the size of the tools' bitmaps. Not needed on wxGTK, but
143 # on Windows, although it doesn't work very well there. It seems
144 # that only 16x16 icons are really supported on windows.
145 # We probably shouldn't hardwire the bitmap size here.
146 toolbar.SetToolBitmapSize(wxSize(24, 24))
147
148 for item in toolbardesc.items:
149 if item is None:
150 toolbar.AddSeparator()
151 else:
152 # assume it's a string.
153 self.add_toolbar_command(toolbar, item)
154
155 return toolbar
156
157 def add_menu_command(self, menu, name):
158 """Add the command with name name to the menu menu.
159
160 If name is None, add a separator.
161 """
162 if name is None:
163 menu.AppendSeparator()
164 else:
165 command = registry.Command(name)
166 if command is not None:
167 ID = self.get_id(name)
168 menu.Append(ID, command.Title(), command.HelpText(),
169 command.IsCheckCommand())
170 self.bind_command_events(command, ID)
171 else:
172 print "Unknown command %s" % name
173
174 def add_toolbar_command(self, toolbar, name):
175 """Add the command with name name to the toolbar toolbar.
176
177 If name is None, add a separator.
178 """
179 # Assume that all toolbar commands are also menu commmands so
180 # that we don't have to add the event handlers here
181 if name is None:
182 toolbar.AddSeparator()
183 else:
184 command = registry.Command(name)
185 if command is not None:
186 ID = self.get_id(name)
187 filename = os.path.join(bitmapdir, command.Icon()) + bitmapext
188 bitmap = wxBitmap(filename, wxBITMAP_TYPE_XPM)
189 toolbar.AddTool(ID, bitmap,
190 shortHelpString = command.HelpText(),
191 isToggle = command.IsCheckCommand())
192 self.bind_command_events(command, ID)
193 else:
194 print "Unknown command %s" % name
195
196 def Context(self):
197 """Return the context object for a command invoked from this window
198 """
199 return Context(self.application, self.application.Session(), self)
200
201 def invoke_command(self, event):
202 name = self.id_to_name.get(event.GetId())
203 if name is not None:
204 command = registry.Command(name)
205 command.Execute(self.Context())
206 else:
207 print "Unknown command ID %d" % event.GetId()
208
209 def update_command_ui(self, event):
210 #print "update_command_ui", self.id_to_name[event.GetId()]
211 context = self.Context()
212 command = registry.Command(self.id_to_name[event.GetId()])
213 if command is not None:
214 sensitive = command.Sensitive(context)
215 event.Enable(sensitive)
216 if command.IsTool() and not sensitive and command.Checked(context):
217 # When a checked tool command is disabled deselect all
218 # tools. Otherwise the tool would remain active but it
219 # might lead to errors if the tools stays active. This
220 # problem occurred in GREAT-ER and this fixes it, but
221 # it's not clear to me whether this is really the best
222 # way to do it (BH, 20021206).
223 self.canvas.SelectTool(None)
224 event.SetText(command.DynText(context))
225 if command.IsCheckCommand():
226 event.Check(command.Checked(context))
227
228 def RunMessageBox(self, title, text, flags = wxOK | wxICON_INFORMATION):
229 """Run a modal message box with the given text, title and flags
230 and return the result"""
231 dlg = wxMessageDialog(self, text, title, flags)
232 dlg.CenterOnParent()
233 result = dlg.ShowModal()
234 dlg.Destroy()
235 return result
236
237 def init_dialogs(self):
238 """Initialize the dialog handling"""
239 # The mainwindow maintains a dict mapping names to open
240 # non-modal dialogs. The dialogs are put into this dict when
241 # they're created and removed when they're closed
242 self.dialogs = {}
243
244 def add_dialog(self, name, dialog):
245 if self.dialogs.has_key(name):
246 raise RuntimeError("The Dialog named %s is already open" % name)
247 self.dialogs[name] = dialog
248
249 def dialog_open(self, name):
250 return self.dialogs.has_key(name)
251
252 def remove_dialog(self, name):
253 del self.dialogs[name]
254
255 def get_open_dialog(self, name):
256 return self.dialogs.get(name)
257
258 def view_position_changed(self):
259 pos = self.canvas.CurrentPosition()
260 if pos is not None:
261 text = "(%10.10g, %10.10g)" % pos
262 else:
263 text = ""
264 self.set_position_text(text)
265
266 def set_position_text(self, text):
267 """Set the statusbar text showing the current position.
268
269 By default the text is shown in field 0 of the status bar.
270 Override this method in derived classes to put it into a
271 different field of the statusbar.
272 """
273 self.SetStatusText(text)
274
275 def save_modified_session(self, can_veto = 1):
276 """If the current session has been modified, ask the user
277 whether to save it and do so if requested. Return the outcome of
278 the dialog (either wxID_OK, wxID_CANCEL or wxID_NO). If the
279 dialog wasn't run return wxID_NO.
280
281 If the can_veto parameter is true (default) the dialog includes
282 a cancel button, otherwise not.
283 """
284 if self.application.session.WasModified():
285 flags = wxYES_NO | wxICON_QUESTION
286 if can_veto:
287 flags = flags | wxCANCEL
288 result = self.RunMessageBox("Exit",
289 ("The session has been modified."
290 " Do you want to save it?"),
291 flags)
292 if result == wxID_YES:
293 self.SaveSession()
294 else:
295 result = wxID_NO
296 return result
297
298 def NewSession(self):
299 self.save_modified_session()
300 self.application.SetSession(create_empty_session())
301
302 def OpenSession(self):
303 self.save_modified_session()
304 dlg = wxFileDialog(self, "Select a session file", ".", "",
305 "*.thuban", wxOPEN)
306 if dlg.ShowModal() == wxID_OK:
307 self.application.OpenSession(dlg.GetPath())
308 dlg.Destroy()
309
310 def SaveSession(self):
311 if self.application.session.filename == None:
312 self.SaveSessionAs()
313 self.application.SaveSession()
314
315 def SaveSessionAs(self):
316 dlg = wxFileDialog(self, "Enter a filename for session", ".", "",
317 "*.thuban", wxOPEN)
318 if dlg.ShowModal() == wxID_OK:
319 self.application.session.SetFilename(dlg.GetPath())
320 self.application.SaveSession()
321 dlg.Destroy()
322
323 def Exit(self):
324 self.Close(false)
325
326 def OnClose(self, event):
327 result = self.save_modified_session(can_veto = event.CanVeto())
328 if result == wxID_CANCEL:
329 event.Veto()
330 else:
331 # FIXME: it would be better to tie the unsubscription to
332 # wx's destroy event, but that isn't implemented for wxGTK
333 # yet.
334 self.canvas.Unsubscribe(VIEW_POSITION, self.view_position_changed)
335 self.Destroy()
336
337 def SetMap(self, map):
338 self.canvas.SetMap(map)
339
340 def Map(self):
341 """Return the map displayed by this mainwindow"""
342 return self.canvas.Map()
343
344 def ShowSessionTree(self):
345 name = "session_tree"
346 dialog = self.get_open_dialog(name)
347 if dialog is None:
348 dialog = tree.SessionTreeView(self, self.application, name)
349 self.add_dialog(name, dialog)
350 dialog.Show(true)
351 else:
352 # FIXME: bring dialog to front here
353 pass
354
355 def About(self):
356 self.RunMessageBox("About",
357 ("Thuban is a program for\n"
358 "exploring geographic data.\n"
359 "Copyright (C) 2001, 2002 Intevation GmbH.\n"
360 "Thuban is licensed under the GPL"),
361 wxOK | wxICON_INFORMATION)
362
363 def AddLayer(self):
364 dlg = wxFileDialog(self, "Select a data file", ".", "", "*.*",
365 wxOPEN)
366 if dlg.ShowModal() == wxID_OK:
367 filename = dlg.GetPath()
368 title = os.path.splitext(os.path.basename(filename))[0]
369 layer = Layer(title, filename)
370 map = self.canvas.Map()
371 has_layers = map.HasLayers()
372 try:
373 map.AddLayer(layer)
374 except IOError:
375 # the layer couldn't be opened
376 self.RunMessageBox("Add Layer",
377 "Can't open the file '%s'." % filename)
378 else:
379 if not has_layers:
380 # if we're adding a layer to an empty map, for the
381 # new map to the window
382 self.canvas.FitMapToWindow()
383 dlg.Destroy()
384
385 def RemoveLayer(self):
386 layer = self.current_layer()
387 if layer is not None:
388 self.canvas.Map().RemoveLayer(layer)
389
390 def CanRemoveLayer(self):
391 """Return true if the currently selected layer can be deleted.
392
393 If no layer is selected return false.
394
395 The return value of this method determines whether the remove
396 layer command is sensitive in menu.
397 """
398 layer = self.current_layer()
399 if layer is not None:
400 return self.canvas.Map().CanRemoveLayer(layer)
401 return 0
402
403 def RaiseLayer(self):
404 layer = self.current_layer()
405 if layer is not None:
406 self.canvas.Map().RaiseLayer(layer)
407
408 def LowerLayer(self):
409 layer = self.current_layer()
410 if layer is not None:
411 self.canvas.Map().LowerLayer(layer)
412
413 def current_layer(self):
414 """Return the currently selected layer.
415
416 If no layer is selected, return None
417 """
418 return self.interactor.SelectedLayer()
419
420 def has_selected_layer(self):
421 """Return true if a layer is currently selected"""
422 return self.interactor.HasSelectedLayer()
423
424 def choose_color(self):
425 """Run the color selection dialog and return the selected color.
426
427 If the user cancels, return None.
428 """
429 dlg = wxColourDialog(self)
430 color = None
431 if dlg.ShowModal() == wxID_OK:
432 data = dlg.GetColourData()
433 wxc = data.GetColour()
434 color = Color(wxc.Red() / 255.0,
435 wxc.Green() / 255.0,
436 wxc.Blue() / 255.0)
437 dlg.Destroy()
438 return color
439
440 def LayerFillColor(self):
441 layer = self.current_layer()
442 if layer is not None:
443 color = self.choose_color()
444 if color is not None:
445 layer.SetFill(color)
446
447 def LayerTransparentFill(self):
448 layer = self.current_layer()
449 if layer is not None:
450 layer.SetFill(None)
451
452 def LayerOutlineColor(self):
453 layer = self.current_layer()
454 if layer is not None:
455 color = self.choose_color()
456 if color is not None:
457 layer.SetStroke(color)
458
459 def LayerNoOutline(self):
460 layer = self.current_layer()
461 if layer is not None:
462 layer.SetStroke(None)
463
464 def HideLayer(self):
465 layer = self.current_layer()
466 if layer is not None:
467 layer.SetVisible(0)
468
469 def ShowLayer(self):
470 layer = self.current_layer()
471 if layer is not None:
472 layer.SetVisible(1)
473
474 def LayerShowTable(self):
475 layer = self.current_layer()
476 if layer is not None:
477 table = layer.table
478 name = "table_view" + str(id(table))
479 dialog = self.get_open_dialog(name)
480 if dialog is None:
481 dialog = tableview.LayerTableFrame(self, self.interactor, name,
482 "Table: %s" % layer.Title(),
483 layer, table)
484 self.add_dialog(name, dialog)
485 dialog.Show(true)
486 else:
487 # FIXME: bring dialog to front here
488 pass
489
490 def Projection(self):
491 map = self.canvas.Map()
492 proj = map.projection
493 if proj is None:
494 proj4Dlg = proj4dialog.Proj4Dialog(NULL, None, map.BoundingBox())
495 else:
496 proj4Dlg = proj4dialog.Proj4Dialog(NULL, map.projection.params,
497 map.BoundingBox())
498 if proj4Dlg.ShowModal() == wxID_OK:
499 params = proj4Dlg.GetParams()
500 if params is not None:
501 proj = Projection(params)
502 else:
503 proj = None
504 map.SetProjection(proj)
505 proj4Dlg.Destroy()
506
507 def Classify(self):
508 classifyDlg = classifier.Classifier(NULL, self.current_layer())
509
510 classifyDlg.ShowModal()
511 classifyDlg.Destroy()
512
513 def ZoomInTool(self):
514 self.canvas.ZoomInTool()
515
516 def ZoomOutTool(self):
517 self.canvas.ZoomOutTool()
518
519 def PanTool(self):
520 self.canvas.PanTool()
521
522 def IdentifyTool(self):
523 self.canvas.IdentifyTool()
524 self.identify_view_on_demand(None, None)
525
526 def LabelTool(self):
527 self.canvas.LabelTool()
528
529 def FullExtent(self):
530 self.canvas.FitMapToWindow()
531
532 def PrintMap(self):
533 self.canvas.Print()
534
535 def identify_view_on_demand(self, layer, shape):
536 name = "identify_view"
537 if self.canvas.CurrentTool() == "IdentifyTool":
538 if not self.dialog_open(name):
539 dialog = identifyview.IdentifyView(self, self.interactor, name)
540 self.add_dialog(name, dialog)
541 dialog.Show(true)
542 else:
543 # FIXME: bring dialog to front?
544 pass
545
546 #
547 # Define all the commands available in the main window
548 #
549
550
551 # Helper functions to define common command implementations
552 def call_method(context, methodname, *args):
553 """Call the mainwindow's method methodname with args *args"""
554 apply(getattr(context.mainwindow, methodname), args)
555
556 def _method_command(name, title, method, helptext = "",
557 icon = "", sensitive = None):
558 """Add a command implemented by a method of the mainwindow object"""
559 registry.Add(Command(name, title, call_method, args=(method,),
560 helptext = helptext, icon = icon,
561 sensitive = sensitive))
562
563 def make_check_current_tool(toolname):
564 """Return a function that tests if the currently active tool is toolname
565
566 The returned function can be called with the context and returns
567 true iff the currently active tool's name is toolname. It's directly
568 usable as the 'checked' callback of a command.
569 """
570 def check_current_tool(context, name=toolname):
571 return context.mainwindow.canvas.CurrentTool() == name
572 return check_current_tool
573
574 def _tool_command(name, title, method, toolname, helptext = "",
575 icon = "", sensitive = None):
576 """Add a tool command"""
577 registry.Add(ToolCommand(name, title, call_method, args=(method,),
578 helptext = helptext, icon = icon,
579 checked = make_check_current_tool(toolname),
580 sensitive = sensitive))
581
582 def _has_selected_layer(context):
583 """Return true if a layer is selected in the context"""
584 return context.mainwindow.has_selected_layer()
585
586 def _can_remove_layer(context):
587 return context.mainwindow.CanRemoveLayer()
588
589 def _has_tree_window_shown(context):
590 """Return true if the tree window is shown"""
591 return context.mainwindow.get_open_dialog("session_tree") is None
592
593 def _has_visible_map(context):
594 """Return true iff theres a visible map in the mainwindow.
595
596 A visible map is a map with at least one visible layer."""
597 map = context.mainwindow.Map()
598 if map is not None:
599 for layer in map.Layers():
600 if layer.Visible():
601 return 1
602 return 0
603
604
605 # File menu
606 _method_command("new_session", "&New Session", "NewSession")
607 _method_command("open_session", "&Open Session", "OpenSession")
608 _method_command("save_session", "&Save Session", "SaveSession")
609 _method_command("save_session_as", "Save Session &As", "SaveSessionAs")
610 _method_command("show_session_tree", "Show Session &Tree", "ShowSessionTree",
611 sensitive = _has_tree_window_shown)
612 _method_command("exit", "E&xit", "Exit")
613
614 # Help menu
615 _method_command("help_about", "&About", "About")
616
617
618 # Map menu
619 _method_command("map_projection", "Pro&jection", "Projection")
620
621
622 _tool_command("map_zoom_in_tool", "&Zoom in", "ZoomInTool", "ZoomInTool",
623 helptext = "Switch to map-mode 'zoom-in'", icon = "zoom_in",
624 sensitive = _has_visible_map)
625 _tool_command("map_zoom_out_tool", "Zoom &out", "ZoomOutTool", "ZoomOutTool",
626 helptext = "Switch to map-mode 'zoom-out'", icon = "zoom_out",
627 sensitive = _has_visible_map)
628 _tool_command("map_pan_tool", "&Pan", "PanTool", "PanTool",
629 helptext = "Switch to map-mode 'pan'", icon = "pan",
630 sensitive = _has_visible_map)
631 _tool_command("map_identify_tool", "&Identify", "IdentifyTool", "IdentifyTool",
632 helptext = "Switch to map-mode 'identify'", icon = "identify",
633 sensitive = _has_visible_map)
634 _tool_command("map_label_tool", "&Label", "LabelTool", "LabelTool",
635 helptext = "Add/Remove labels", icon = "label",
636 sensitive = _has_visible_map)
637 _method_command("map_full_extent", "&Full extent", "FullExtent",
638 helptext = "Full Extent", icon = "fullextent",
639 sensitive = _has_visible_map)
640 _method_command("map_print", "Prin&t", "PrintMap", helptext = "Print the map")
641
642 # Layer menu
643 _method_command("layer_add", "&Add Layer", "AddLayer",
644 helptext = "Add a new layer to active map")
645 _method_command("layer_remove", "&Remove Layer", "RemoveLayer",
646 helptext = "Remove selected layer(s)",
647 sensitive = _can_remove_layer)
648 _method_command("layer_fill_color", "&Fill Color", "LayerFillColor",
649 helptext = "Set the fill color of selected layer(s)",
650 sensitive = _has_selected_layer)
651 _method_command("layer_transparent_fill", "&Transparent Fill",
652 "LayerTransparentFill",
653 helptext = "Do not fill the selected layer(s)",
654 sensitive = _has_selected_layer)
655 _method_command("layer_outline_color", "&Outline Color", "LayerOutlineColor",
656 helptext = "Set the outline color of selected layer(s)",
657 sensitive = _has_selected_layer)
658 _method_command("layer_no_outline", "&No Outline", "LayerNoOutline",
659 helptext = "Do not draw the outline of the selected layer(s)",
660 sensitive = _has_selected_layer)
661 _method_command("layer_raise", "&Raise", "RaiseLayer",
662 helptext = "Raise selected layer(s)",
663 sensitive = _has_selected_layer)
664 _method_command("layer_lower", "&Lower", "LowerLayer",
665 helptext = "Lower selected layer(s)",
666 sensitive = _has_selected_layer)
667 _method_command("layer_show", "&Show", "ShowLayer",
668 helptext = "Make selected layer(s) visible",
669 sensitive = _has_selected_layer)
670 _method_command("layer_hide", "&Hide", "HideLayer",
671 helptext = "Make selected layer(s) unvisible",
672 sensitive = _has_selected_layer)
673 _method_command("layer_show_table", "Show Ta&ble", "LayerShowTable",
674 helptext = "Show the selected layer's table",
675 sensitive = _has_selected_layer)
676
677 _method_command("layer_classifier", "Classify", "Classify",
678 sensitive = _has_selected_layer)
679
680 # the menu structure
681 main_menu = Menu("<main>", "<main>",
682 [Menu("file", "&File",
683 ["new_session", "open_session", None,
684 "save_session", "save_session_as", None,
685 "show_session_tree", None,
686 "exit"]),
687 Menu("map", "&Map",
688 ["layer_add", "layer_remove",
689 None,
690 "map_projection",
691 None,
692 "map_zoom_in_tool", "map_zoom_out_tool",
693 "map_pan_tool", "map_identify_tool", "map_label_tool",
694 None,
695 "map_full_extent",
696 None,
697 "map_print"]),
698 Menu("layer", "&Layer",
699 ["layer_fill_color", "layer_transparent_fill",
700 "layer_outline_color", "layer_no_outline",
701 None,
702 "layer_raise", "layer_lower",
703 None,
704 "layer_show", "layer_hide",
705 None,
706 "layer_show_table",
707 None,
708 "layer_classifier"]),
709 Menu("help", "&Help",
710 ["help_about"])])
711
712 # the main toolbar
713
714 main_toolbar = Menu("<toolbar>", "<toolbar>",
715 ["map_zoom_in_tool", "map_zoom_out_tool", "map_pan_tool",
716 "map_full_extent", None,
717 "map_identify_tool", "map_label_tool"])

Properties

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

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26