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