1""" 2@package gui_core.mapdisp 3 4@brief Base classes for Map display window 5 6Classes: 7 - mapdisp::MapFrameBase 8 - mapdisp::SingleMapFrame 9 - mapdisp::DoubleMapFrame 10 11(C) 2009-2014 by the GRASS Development Team 12 13This program is free software under the GNU General Public License 14(>=v2). Read the file COPYING that comes with GRASS for details. 15 16@author Martin Landa <landa.martin gmail.com> 17@author Michael Barton <michael.barton@asu.edu> 18@author Vaclav Petras <wenzeslaus gmail.com> 19@author Anna Kratochvilova <kratochanna gmail.com> 20""" 21 22import os 23import sys 24import six 25 26import wx 27 28from core import globalvar 29from core.debug import Debug 30from gui_core.toolbars import ToolSwitcher 31from gui_core.wrap import NewId 32 33from grass.script import core as grass 34 35 36class MapFrameBase(wx.Frame): 37 """Base class for map display window 38 39 Derived class must use (create and initialize) \c statusbarManager 40 or override 41 GetProperty(), SetProperty() and HasProperty() methods. 42 43 Several methods has to be overriden or 44 \c NotImplementedError("MethodName") will be raised. 45 46 If derived class enables and disables auto-rendering, 47 it should override IsAutoRendered method. 48 49 It is expected that derived class will call _setUpMapWindow(). 50 51 Derived class can has one or more map windows (and map renderes) 52 but implementation of MapFrameBase expects that one window and 53 one map will be current. 54 Current instances of map window and map renderer should be returned 55 by methods GetWindow() and GetMap() respectively. 56 57 AUI manager is stored in \c self._mgr. 58 """ 59 60 def __init__(self, parent=None, id=wx.ID_ANY, title='', 61 style=wx.DEFAULT_FRAME_STYLE, 62 auimgr=None, name='', **kwargs): 63 """ 64 65 .. warning:: 66 Use \a auimgr parameter only if you know what you are doing. 67 68 :param parent: gui parent 69 :param id: wx id 70 :param title: window title 71 :param style: \c wx.Frame style 72 :param toolbars: array of activated toolbars, e.g. ['map', 'digit'] 73 :param auimgr: AUI manager (if \c None, wx.aui.AuiManager is used) 74 :param name: frame name 75 :param kwargs: arguments passed to \c wx.Frame 76 """ 77 78 self.parent = parent 79 80 wx.Frame.__init__( 81 self, 82 parent, 83 id, 84 title, 85 style=style, 86 name=name, 87 **kwargs) 88 89 # 90 # set the size & system icon 91 # 92 self.SetClientSize(self.GetSize()) 93 self.iconsize = (16, 16) 94 95 self.SetIcon( 96 wx.Icon( 97 os.path.join( 98 globalvar.ICONDIR, 99 'grass_map.ico'), 100 wx.BITMAP_TYPE_ICO)) 101 102 # toolbars 103 self.toolbars = {} 104 105 # 106 # Fancy gui 107 # 108 if auimgr is None: 109 from wx.aui import AuiManager 110 self._mgr = AuiManager(self) 111 else: 112 self._mgr = auimgr 113 114 # handles switching between tools in different toolbars 115 self._toolSwitcher = ToolSwitcher() 116 self._toolSwitcher.toggleToolChanged.connect(self._onToggleTool) 117 118 self._initShortcuts() 119 120 def _initShortcuts(self): 121 122 # set accelerator table (fullscreen, close window) 123 shortcuts_table = ( 124 (self.OnFullScreen, wx.ACCEL_NORMAL, wx.WXK_F11), 125 (self.OnCloseWindow, wx.ACCEL_CTRL, ord('W')), 126 (self.OnRender, wx.ACCEL_CTRL, ord('R')), 127 (self.OnRender, wx.ACCEL_NORMAL, wx.WXK_F5), 128 ) 129 accelTable = [] 130 for handler, entry, kdb in shortcuts_table: 131 wxId = NewId() 132 self.Bind(wx.EVT_MENU, handler, id=wxId) 133 accelTable.append((entry, kdb, wxId)) 134 135 self.SetAcceleratorTable(wx.AcceleratorTable(accelTable)) 136 137 def _initMap(self, Map): 138 """Initialize map display, set dimensions and map region 139 """ 140 if not grass.find_program('g.region', '--help'): 141 sys.exit(_("GRASS module '%s' not found. Unable to start map " 142 "display window.") % 'g.region') 143 144 Debug.msg(2, "MapFrame._initMap():") 145 Map.ChangeMapSize(self.GetClientSize()) 146 Map.region = Map.GetRegion() # g.region -upgc 147 # self.Map.SetRegion() # adjust region to match display window 148 149 def _resize(self): 150 Debug.msg(1, "MapFrame._resize():") 151 wm, hw = self.MapWindow.GetClientSize() 152 wf, hf = self.GetSize() 153 dw = wf - wm 154 dh = hf - hw 155 self.SetSize((wf + dw, hf + dh)) 156 157 def _onToggleTool(self, id): 158 if self._toolSwitcher.IsToolInGroup(id, 'mouseUse'): 159 self.GetWindow().UnregisterAllHandlers() 160 161 def OnSize(self, event): 162 """Adjust statusbar on changing size""" 163 # reposition checkbox in statusbar 164 self.StatusbarReposition() 165 166 # update statusbar 167 self.StatusbarUpdate() 168 169 def OnFullScreen(self, event): 170 """!Switch fullscreen mode, hides also toolbars""" 171 for toolbar in self.toolbars.keys(): 172 self._mgr.GetPane(self.toolbars[toolbar]).Show(self.IsFullScreen()) 173 self._mgr.Update() 174 self.ShowFullScreen(not self.IsFullScreen()) 175 event.Skip() 176 177 def OnCloseWindow(self, event): 178 self.Destroy() 179 180 def GetToolSwitcher(self): 181 return self._toolSwitcher 182 183 def SetProperty(self, name, value): 184 """Sets property""" 185 self.statusbarManager.SetProperty(name, value) 186 187 def GetProperty(self, name): 188 """Returns property""" 189 return self.statusbarManager.GetProperty(name) 190 191 def HasProperty(self, name): 192 """Checks whether object has property""" 193 return self.statusbarManager.HasProperty(name) 194 195 def GetPPM(self): 196 """Get pixel per meter 197 198 .. todo:: 199 now computed every time, is it necessary? 200 201 .. todo:: 202 enable user to specify ppm (and store it in UserSettings) 203 """ 204 # TODO: need to be fixed... 205 # screen X region problem 206 # user should specify ppm 207 dc = wx.ScreenDC() 208 dpSizePx = wx.DisplaySize() # display size in pixels 209 dpSizeMM = wx.DisplaySizeMM() # display size in mm (system) 210 dpSizeIn = (dpSizeMM[0] / 25.4, dpSizeMM[1] / 25.4) # inches 211 sysPpi = dc.GetPPI() 212 comPpi = (dpSizePx[0] / dpSizeIn[0], 213 dpSizePx[1] / dpSizeIn[1]) 214 215 ppi = comPpi # pixel per inch 216 ppm = ((ppi[0] / 2.54) * 100, # pixel per meter 217 (ppi[1] / 2.54) * 100) 218 219 Debug.msg(4, "MapFrameBase.GetPPM(): size: px=%d,%d mm=%f,%f " 220 "in=%f,%f ppi: sys=%d,%d com=%d,%d; ppm=%f,%f" % 221 (dpSizePx[0], dpSizePx[1], dpSizeMM[0], dpSizeMM[1], 222 dpSizeIn[0], dpSizeIn[1], 223 sysPpi[0], sysPpi[1], comPpi[0], comPpi[1], 224 ppm[0], ppm[1])) 225 226 return ppm 227 228 def SetMapScale(self, value, map=None): 229 """Set current map scale 230 231 :param value: scale value (n if scale is 1:n) 232 :param map: Map instance (if none self.Map is used) 233 """ 234 if not map: 235 map = self.Map 236 237 region = self.Map.region 238 dEW = value * (region['cols'] / self.GetPPM()[0]) 239 dNS = value * (region['rows'] / self.GetPPM()[1]) 240 region['n'] = region['center_northing'] + dNS / 2. 241 region['s'] = region['center_northing'] - dNS / 2. 242 region['w'] = region['center_easting'] - dEW / 2. 243 region['e'] = region['center_easting'] + dEW / 2. 244 245 # add to zoom history 246 self.GetWindow().ZoomHistory(region['n'], region['s'], 247 region['e'], region['w']) 248 249 def GetMapScale(self, map=None): 250 """Get current map scale 251 252 :param map: Map instance (if none self.Map is used) 253 """ 254 if not map: 255 map = self.GetMap() 256 257 region = map.region 258 ppm = self.GetPPM() 259 260 heightCm = region['rows'] / ppm[1] * 100 261 widthCm = region['cols'] / ppm[0] * 100 262 263 Debug.msg(4, "MapFrame.GetMapScale(): width_cm=%f, height_cm=%f" % 264 (widthCm, heightCm)) 265 266 xscale = (region['e'] - region['w']) / (region['cols'] / ppm[0]) 267 yscale = (region['n'] - region['s']) / (region['rows'] / ppm[1]) 268 scale = (xscale + yscale) / 2. 269 270 Debug.msg( 271 3, "MapFrame.GetMapScale(): xscale=%f, yscale=%f -> scale=%f" % 272 (xscale, yscale, scale)) 273 274 return scale 275 276 def GetProgressBar(self): 277 """Returns progress bar 278 279 Progress bar can be used by other classes. 280 """ 281 return self.statusbarManager.GetProgressBar() 282 283 def GetMap(self): 284 """Returns current map (renderer) instance""" 285 raise NotImplementedError("GetMap") 286 287 def GetWindow(self): 288 """Returns current map window""" 289 raise NotImplementedError("GetWindow") 290 291 def GetWindows(self): 292 """Returns list of map windows""" 293 raise NotImplementedError("GetWindows") 294 295 def GetMapToolbar(self): 296 """Returns toolbar with zooming tools""" 297 raise NotImplementedError("GetMapToolbar") 298 299 def GetToolbar(self, name): 300 """Returns toolbar if exists and is active, else None. 301 """ 302 if name in self.toolbars and self.toolbars[name].IsShown(): 303 return self.toolbars[name] 304 305 return None 306 307 def StatusbarUpdate(self): 308 """Update statusbar content""" 309 if self.statusbarManager: 310 Debug.msg(5, "MapFrameBase.StatusbarUpdate()") 311 self.statusbarManager.Update() 312 313 def IsAutoRendered(self): 314 """Check if auto-rendering is enabled""" 315 # TODO: this is now not the right place to access this attribute 316 # TODO: add mapWindowProperties to init parameters 317 # and pass the right object in the init of derived class? 318 # or do not use this method at all, let mapwindow decide 319 return self.mapWindowProperties.autoRender 320 321 def CoordinatesChanged(self): 322 """Shows current coordinates on statusbar. 323 """ 324 # assuming that the first mode is coordinates 325 # probably shold not be here but good solution is not available now 326 if self.statusbarManager: 327 if self.statusbarManager.GetMode() == 0: 328 self.statusbarManager.ShowItem('coordinates') 329 330 def StatusbarReposition(self): 331 """Reposition items in statusbar""" 332 if self.statusbarManager: 333 self.statusbarManager.Reposition() 334 335 def StatusbarEnableLongHelp(self, enable=True): 336 """Enable/disable toolbars long help""" 337 for toolbar in six.itervalues(self.toolbars): 338 if toolbar: 339 toolbar.EnableLongHelp(enable) 340 341 def IsStandalone(self): 342 """Check if map frame is standalone""" 343 raise NotImplementedError("IsStandalone") 344 345 def OnRender(self, event): 346 """Re-render map composition (each map layer) 347 """ 348 raise NotImplementedError("OnRender") 349 350 def OnDraw(self, event): 351 """Re-display current map composition 352 """ 353 self.MapWindow.UpdateMap(render=False) 354 355 def OnErase(self, event): 356 """Erase the canvas 357 """ 358 self.MapWindow.EraseMap() 359 360 def OnZoomIn(self, event): 361 """Zoom in the map.""" 362 self.MapWindow.SetModeZoomIn() 363 364 def OnZoomOut(self, event): 365 """Zoom out the map.""" 366 self.MapWindow.SetModeZoomOut() 367 368 def _setUpMapWindow(self, mapWindow): 369 """Binds map windows' zoom history signals to map toolbar.""" 370 # enable or disable zoom history tool 371 if self.GetMapToolbar(): 372 mapWindow.zoomHistoryAvailable.connect( 373 lambda: 374 self.GetMapToolbar().Enable('zoomBack', enable=True)) 375 mapWindow.zoomHistoryUnavailable.connect( 376 lambda: 377 self.GetMapToolbar().Enable('zoomBack', enable=False)) 378 mapWindow.mouseMoving.connect(self.CoordinatesChanged) 379 380 def OnPointer(self, event): 381 """Sets mouse mode to pointer.""" 382 self.MapWindow.SetModePointer() 383 384 def OnPan(self, event): 385 """Panning, set mouse to drag 386 """ 387 self.MapWindow.SetModePan() 388 389 def OnZoomBack(self, event): 390 """Zoom last (previously stored position) 391 """ 392 self.MapWindow.ZoomBack() 393 394 def OnZoomToMap(self, event): 395 """ 396 Set display extents to match selected raster (including NULLs) 397 or vector map. 398 """ 399 self.MapWindow.ZoomToMap(layers=self.Map.GetListOfLayers()) 400 401 def OnZoomToWind(self, event): 402 """Set display geometry to match computational region 403 settings (set with g.region) 404 """ 405 self.MapWindow.ZoomToWind() 406 407 def OnZoomToDefault(self, event): 408 """Set display geometry to match default region settings 409 """ 410 self.MapWindow.ZoomToDefault() 411 412 413class SingleMapFrame(MapFrameBase): 414 """Frame with one map window. 415 416 It is base class for frames which needs only one map. 417 418 Derived class should have \c self.MapWindow or 419 it has to override GetWindow() methods. 420 421 @note To access maps use getters only 422 (when using class or when writing class itself). 423 """ 424 425 def __init__(self, parent=None, giface=None, id=wx.ID_ANY, title='', 426 style=wx.DEFAULT_FRAME_STYLE, 427 Map=None, 428 auimgr=None, name='', **kwargs): 429 """ 430 431 :param parent: gui parent 432 :param id: wx id 433 :param title: window title 434 :param style: \c wx.Frame style 435 :param map: instance of render.Map 436 :param name: frame name 437 :param kwargs: arguments passed to MapFrameBase 438 """ 439 440 MapFrameBase.__init__(self, parent=parent, id=id, title=title, 441 style=style, 442 auimgr=auimgr, name=name, **kwargs) 443 444 self.Map = Map # instance of render.Map 445 446 # 447 # initialize region values 448 # 449 if self.Map: 450 self._initMap(Map=self.Map) 451 452 def GetMap(self): 453 """Returns map (renderer) instance""" 454 return self.Map 455 456 def GetWindow(self): 457 """Returns map window""" 458 return self.MapWindow 459 460 def GetWindows(self): 461 """Returns list of map windows""" 462 return [self.MapWindow] 463 464 def OnRender(self, event): 465 """Re-render map composition (each map layer) 466 """ 467 self.GetWindow().UpdateMap(render=True, renderVector=True) 468 469 # update statusbar 470 self.StatusbarUpdate() 471 472 473class DoubleMapFrame(MapFrameBase): 474 """Frame with two map windows. 475 476 It is base class for frames which needs two maps. 477 There is no primary and secondary map. Both maps are equal. 478 However, one map is current. 479 480 It is expected that derived class will call _bindWindowsActivation() 481 when both map windows will be initialized. 482 483 Drived class should have method GetMapToolbar() returns toolbar 484 which has methods SetActiveMap() and Enable(). 485 486 @note To access maps use getters only 487 (when using class or when writing class itself). 488 489 .. todo: 490 Use it in GCP manager (probably changes to both DoubleMapFrame 491 and GCP MapFrame will be neccessary). 492 """ 493 494 def __init__(self, parent=None, id=wx.ID_ANY, title=None, 495 style=wx.DEFAULT_FRAME_STYLE, 496 firstMap=None, secondMap=None, 497 auimgr=None, name=None, **kwargs): 498 """ 499 500 \a firstMap is set as active (by assign it to \c self.Map). 501 Derived class should assging to \c self.MapWindow to make one 502 map window current by dafault. 503 504 :param parent: gui parent 505 :param id: wx id 506 :param title: window title 507 :param style: \c wx.Frame style 508 :param name: frame name 509 :param kwargs: arguments passed to MapFrameBase 510 """ 511 512 MapFrameBase.__init__(self, parent=parent, id=id, title=title, 513 style=style, 514 auimgr=auimgr, name=name, **kwargs) 515 516 self.firstMap = firstMap 517 self.secondMap = secondMap 518 self.Map = firstMap 519 520 # 521 # initialize region values 522 # 523 self._initMap(Map=self.firstMap) 524 self._initMap(Map=self.secondMap) 525 self._bindRegions = False 526 527 def _bindWindowsActivation(self): 528 self.GetFirstWindow().Bind(wx.EVT_ENTER_WINDOW, self.ActivateFirstMap) 529 self.GetSecondWindow().Bind(wx.EVT_ENTER_WINDOW, self.ActivateSecondMap) 530 531 def _onToggleTool(self, id): 532 if self._toolSwitcher.IsToolInGroup(id, 'mouseUse'): 533 self.GetFirstWindow().UnregisterAllHandlers() 534 self.GetSecondWindow().UnregisterAllHandlers() 535 536 def GetFirstMap(self): 537 """Returns first Map instance 538 """ 539 return self.firstMap 540 541 def GetSecondMap(self): 542 """Returns second Map instance 543 """ 544 return self.secondMap 545 546 def GetFirstWindow(self): 547 """Get first map window""" 548 return self.firstMapWindow 549 550 def GetSecondWindow(self): 551 """Get second map window""" 552 return self.secondMapWindow 553 554 def GetMap(self): 555 """Returns current map (renderer) instance 556 557 @note Use this method to access current map renderer. 558 (It is not guarented that current map will be stored in 559 \c self.Map in future versions.) 560 """ 561 return self.Map 562 563 def GetWindow(self): 564 """Returns current map window 565 566 :func:`GetMap()` 567 """ 568 return self.MapWindow 569 570 def GetWindows(self): 571 """Return list of all windows""" 572 return [self.firstMapWindow, self.secondMapWindow] 573 574 def ActivateFirstMap(self, event=None): 575 """Make first Map and MapWindow active and (un)bind regions of the two Maps.""" 576 if self.MapWindow == self.firstMapWindow: 577 return 578 579 self.Map = self.firstMap 580 self.MapWindow = self.firstMapWindow 581 self.GetMapToolbar().SetActiveMap(0) 582 583 # bind/unbind regions 584 if self._bindRegions: 585 self.firstMapWindow.zoomChanged.connect(self.OnZoomChangedFirstMap) 586 self.secondMapWindow.zoomChanged.disconnect( 587 self.OnZoomChangedSecondMap) 588 589 def ActivateSecondMap(self, event=None): 590 """Make second Map and MapWindow active and (un)bind regions of the two Maps.""" 591 if self.MapWindow == self.secondMapWindow: 592 return 593 594 self.Map = self.secondMap 595 self.MapWindow = self.secondMapWindow 596 self.GetMapToolbar().SetActiveMap(1) 597 598 if self._bindRegions: 599 self.secondMapWindow.zoomChanged.connect( 600 self.OnZoomChangedSecondMap) 601 self.firstMapWindow.zoomChanged.disconnect( 602 self.OnZoomChangedFirstMap) 603 604 def SetBindRegions(self, on): 605 """Set or unset binding display regions.""" 606 self._bindRegions = on 607 608 if on: 609 if self.MapWindow == self.firstMapWindow: 610 self.firstMapWindow.zoomChanged.connect( 611 self.OnZoomChangedFirstMap) 612 else: 613 self.secondMapWindow.zoomChanged.connect( 614 self.OnZoomChangedSecondMap) 615 else: 616 if self.MapWindow == self.firstMapWindow: 617 self.firstMapWindow.zoomChanged.disconnect( 618 self.OnZoomChangedFirstMap) 619 else: 620 self.secondMapWindow.zoomChanged.disconnect( 621 self.OnZoomChangedSecondMap) 622 623 def OnZoomChangedFirstMap(self): 624 """Display region of the first window (Map) changed. 625 626 Synchronize the region of the second map and re-render it. 627 This is the default implementation which can be overridden. 628 """ 629 region = self.GetFirstMap().GetCurrentRegion() 630 self.GetSecondMap().region.update(region) 631 self.Render(mapToRender=self.GetSecondWindow()) 632 633 def OnZoomChangedSecondMap(self): 634 """Display region of the second window (Map) changed. 635 636 Synchronize the region of the second map and re-render it. 637 This is the default implementation which can be overridden. 638 """ 639 region = self.GetSecondMap().GetCurrentRegion() 640 self.GetFirstMap().region.update(region) 641 self.Render(mapToRender=self.GetFirstWindow()) 642 643 def OnZoomIn(self, event): 644 """Zoom in the map.""" 645 self.GetFirstWindow().SetModeZoomIn() 646 self.GetSecondWindow().SetModeZoomIn() 647 648 def OnZoomOut(self, event): 649 """Zoom out the map.""" 650 self.GetFirstWindow().SetModeZoomOut() 651 self.GetSecondWindow().SetModeZoomOut() 652 653 def OnPan(self, event): 654 """Panning, set mouse to pan""" 655 self.GetFirstWindow().SetModePan() 656 self.GetSecondWindow().SetModePan() 657 658 def OnPointer(self, event): 659 """Set pointer mode (dragging overlays)""" 660 self.GetFirstWindow().SetModePointer() 661 self.GetSecondWindow().SetModePointer() 662 663 def OnQuery(self, event): 664 """Set query mode""" 665 self.GetFirstWindow().SetModeQuery() 666 self.GetSecondWindow().SetModeQuery() 667 668 def OnRender(self, event): 669 """Re-render map composition (each map layer) 670 """ 671 self.Render(mapToRender=self.GetFirstWindow()) 672 self.Render(mapToRender=self.GetSecondWindow()) 673 674 def Render(self, mapToRender): 675 """Re-render map composition""" 676 mapToRender.UpdateMap( 677 render=True, 678 renderVector=mapToRender == self.GetFirstWindow()) 679 680 # update statusbar 681 self.StatusbarUpdate() 682 683 def OnErase(self, event): 684 """Erase the canvas 685 """ 686 self.Erase(mapToErase=self.GetFirstWindow()) 687 self.Erase(mapToErase=self.GetSecondWindow()) 688 689 def Erase(self, mapToErase): 690 """Erase the canvas 691 """ 692 mapToErase.EraseMap() 693 694 def OnDraw(self, event): 695 """Re-display current map composition 696 """ 697 self.Draw(mapToDraw=self.GetFirstWindow()) 698 self.Draw(mapToDraw=self.GetSecondWindow()) 699 700 def Draw(self, mapToDraw): 701 """Re-display current map composition 702 """ 703 mapToDraw.UpdateMap(render=False) 704