1"""
2@package mapwin.decorations
3
4@brief Map display decorations (overlays) - text, barscale and legend
5
6Classes:
7 - decorations::OverlayController
8 - decorations::BarscaleController
9 - decorations::ArrowController
10 - decorations::LegendController
11 - decorations::TextLayerDialog
12
13(C) 2006-2014 by the GRASS Development Team
14
15This program is free software under the GNU General Public License
16(>=v2). Read the file COPYING that comes with GRASS for details.
17
18@author Anna Kratochvilova <kratochanna gmail.com>
19"""
20
21import os
22
23import wx
24
25from grass.pydispatch.signal import Signal
26try:
27    from PIL import Image
28    hasPIL = True
29except ImportError:
30    hasPIL = False
31from gui_core.wrap import NewId
32
33
34class OverlayController(object):
35
36    """Base class for decorations (barscale, legend) controller."""
37
38    def __init__(self, renderer, giface):
39        self._giface = giface
40        self._renderer = renderer
41        self._overlay = None
42        self._coords = None
43        self._pdcType = 'image'
44        self._propwin = None
45        self._defaultAt = ''
46        self._cmd = None   # to be set by user
47        self._name = None  # to be defined by subclass
48        self._removeLabel = None  # to be defined by subclass
49        self._id = NewId()
50        self._dialog = None
51
52        # signals that overlay or its visibility changed
53        self.overlayChanged = Signal('OverlayController::overlayChanged')
54
55    def SetCmd(self, cmd):
56        hasAt = False
57        for i in cmd:
58            if i.startswith("at="):
59                hasAt = True
60                # reset coordinates, 'at' values will be used, see GetCoords
61                self._coords = None
62                break
63        if not hasAt:
64            cmd.append(self._defaultAt)
65        self._cmd = cmd
66
67    def GetCmd(self):
68        return self._cmd
69
70    cmd = property(fset=SetCmd, fget=GetCmd)
71
72    def SetCoords(self, coords):
73        self._coords = list(coords)
74
75    def GetCoords(self):
76        if self._coords is None:  # initial position
77            x, y = self.GetPlacement(
78                (self._renderer.width, self._renderer.height))
79            self._coords = [x, y]
80        return self._coords
81
82    coords = property(fset=SetCoords, fget=GetCoords)
83
84    def GetPdcType(self):
85        return self._pdcType
86
87    pdcType = property(fget=GetPdcType)
88
89    def GetName(self):
90        return self._name
91
92    name = property(fget=GetName)
93
94    def GetRemoveLabel(self):
95        return self._removeLabel
96
97    removeLabel = property(fget=GetRemoveLabel)
98
99    def GetId(self):
100        return self._id
101
102    id = property(fget=GetId)
103
104    def GetPropwin(self):
105        return self._propwin
106
107    def SetPropwin(self, win):
108        self._propwin = win
109
110    propwin = property(fget=GetPropwin, fset=SetPropwin)
111
112    def GetLayer(self):
113        return self._overlay
114
115    layer = property(fget=GetLayer)
116
117    def GetDialog(self):
118        return self._dialog
119
120    def SetDialog(self, win):
121        self._dialog = win
122
123    dialog = property(fget=GetDialog, fset=SetDialog)
124
125    def IsShown(self):
126        if self._overlay and self._overlay.IsActive() and self._overlay.IsRendered():
127            return True
128        return False
129
130    def Show(self, show=True):
131        """Activate or deactivate overlay."""
132        if show:
133            if not self._overlay:
134                self._add()
135            self._overlay.SetActive(True)
136            self._update()
137        else:
138            self.Hide()
139
140        self.overlayChanged.emit()
141
142    def Hide(self):
143        if self._overlay:
144            self._overlay.SetActive(False)
145        self.overlayChanged.emit()
146
147    def Remove(self):
148        if self._dialog:
149            self._dialog.Destroy()
150        self._renderer.DeleteOverlay(self._overlay)
151        self.overlayChanged.emit()
152
153    def _add(self):
154        self._overlay = self._renderer.AddOverlay(
155            id=self._id,
156            ltype=self._name,
157            command=self.cmd,
158            active=False,
159            render=True,
160            hidden=True)
161        # check if successful
162
163    def _update(self):
164        self._renderer.ChangeOverlay(id=self._id, command=self._cmd)
165
166    def CmdIsValid(self):
167        """If command is valid"""
168        return True
169
170    def GetPlacement(self, screensize):
171        """Get coordinates where to place overlay in a reasonable way
172
173        :param screensize: screen size
174        """
175        if not hasPIL:
176            self._giface.WriteWarning(
177                _(
178                    "Please install Python Imaging Library (PIL)\n"
179                    "for better control of legend and other decorations."))
180            return 0, 0
181
182        for param in self._cmd:
183            if not param.startswith('at'):
184                continue
185            x, y = [float(number) for number in param.split('=')[1].split(',')]
186            x = int((x / 100.) * screensize[0])
187            y = int((1 - y / 100.) * screensize[1])
188
189            return x, y
190
191
192class DtextController(OverlayController):
193
194    def __init__(self, renderer, giface):
195        OverlayController.__init__(self, renderer, giface)
196        self._name = 'text'
197        self._removeLabel = _("Remove text")
198        self._defaultAt = 'at=50,50'
199        self._cmd = ['d.text', self._defaultAt]
200
201    def CmdIsValid(self):
202        inputs = 0
203        for param in self._cmd[1:]:
204            param = param.split('=')
205            if len(param) == 1:
206                inputs += 1
207            else:
208                if param[0] == 'text' and len(param) == 2:
209                    inputs += 1
210        if inputs >= 1:
211            return True
212        return False
213
214
215class BarscaleController(OverlayController):
216
217    def __init__(self, renderer, giface):
218        OverlayController.__init__(self, renderer, giface)
219        self._name = 'barscale'
220        self._removeLabel = _("Remove scale bar")
221        # different from default because the reference point is not in the
222        # middle
223        self._defaultAt = 'at=0,98'
224        self._cmd = ['d.barscale', self._defaultAt]
225
226
227class ArrowController(OverlayController):
228
229    def __init__(self, renderer, giface):
230        OverlayController.__init__(self, renderer, giface)
231        self._name = 'arrow'
232        self._removeLabel = _("Remove north arrow")
233        # different from default because the reference point is not in the
234        # middle
235        self._defaultAt = 'at=85.0,25.0'
236        self._cmd = ['d.northarrow', self._defaultAt]
237
238
239class LegendVectController(OverlayController):
240
241    def __init__(self, renderer, giface):
242        OverlayController.__init__(self, renderer, giface)
243        self._name = 'vectleg'
244        self._removeLabel = _("Remove vector legend")
245        # different from default because the reference point is not in the
246        # middle
247        self._defaultAt = 'at=20.0,80.0'
248        self._cmd = ['d.legend.vect', self._defaultAt]
249
250
251class LegendController(OverlayController):
252
253    def __init__(self, renderer, giface):
254        OverlayController.__init__(self, renderer, giface)
255        self._name = 'legend'
256        self._removeLabel = _("Remove legend")
257        # default is in the center to avoid trimmed legend on the edge
258        self._defaultAt = 'at=5,50,47,50'
259        self._actualAt = self._defaultAt
260        self._cmd = ['d.legend', self._defaultAt]
261
262    def SetCmd(self, cmd):
263        """Overriden method
264
265        Required for setting default or actual raster legend position.
266        """
267        hasAt = False
268        for i in cmd:
269            if i.startswith("at="):
270                hasAt = True
271                # reset coordinates, 'at' values will be used, see GetCoords
272                self._coords = None
273                break
274        if not hasAt:
275            if self._actualAt != self._defaultAt:
276                cmd.append(self._actualAt)
277            else:
278                cmd.append(self._defaultAt)
279        self._cmd = cmd
280
281    cmd = property(fset=SetCmd, fget=OverlayController.GetCmd)
282
283    def GetPlacement(self, screensize):
284        if not hasPIL:
285            self._giface.WriteWarning(
286                _(
287                    "Please install Python Imaging Library (PIL)\n"
288                    "for better control of legend and other decorations."))
289            return 0, 0
290        for param in self._cmd:
291            if not param.startswith('at'):
292                continue
293            # if the at= is the default, we will move the legend from the center to bottom left
294            if param == self._defaultAt:
295                b, t, l, r = 5, 50, 7, 10
296            else:
297                b, t, l, r = [float(number) for number in param.split(
298                    '=')[1].split(',')]  # pylint: disable-msg=W0612
299            x = int((l / 100.) * screensize[0])
300            y = int((1 - t / 100.) * screensize[1])
301
302            return x, y
303
304    def CmdIsValid(self):
305        inputs = 0
306        for param in self._cmd[1:]:
307            param = param.split('=')
308            if len(param) == 1:
309                inputs += 1
310            else:
311                if param[0] == 'raster' and len(param) == 2:
312                    inputs += 1
313                elif param[0] == 'raster_3d' and len(param) == 2:
314                    inputs += 1
315        if inputs == 1:
316            return True
317        return False
318
319    def ResizeLegend(self, begin, end, screenSize):
320        """Resize legend according to given bbox coordinates."""
321        w = abs(begin[0] - end[0])
322        h = abs(begin[1] - end[1])
323        if begin[0] < end[0]:
324            x = begin[0]
325        else:
326            x = end[0]
327        if begin[1] < end[1]:
328            y = begin[1]
329        else:
330            y = end[1]
331
332        at = [(screenSize[1] - (y + h)) / float(screenSize[1]) * 100,
333              (screenSize[1] - y) / float(screenSize[1]) * 100,
334              x / float(screenSize[0]) * 100,
335              (x + w) / float(screenSize[0]) * 100]
336        atStr = "at=%d,%d,%d,%d" % (at[0], at[1], at[2], at[3])
337
338        for i, subcmd in enumerate(self._cmd):
339            if subcmd.startswith('at='):
340                self._cmd[i] = atStr
341                break
342
343        self._coords = None
344        self._actualAt = atStr
345        self.Show()
346
347    def StartResizing(self):
348        """Tool in toolbar or button itself were pressed"""
349        # prepare for resizing
350        window = self._giface.GetMapWindow()
351        window.SetNamedCursor('cross')
352        window.mouse['use'] = None
353        window.mouse['box'] = 'box'
354        window.pen = wx.Pen(colour='Black', width=2, style=wx.SHORT_DASH)
355        window.mouseLeftUp.connect(self._finishResizing)
356
357    def _finishResizing(self):
358        window = self._giface.GetMapWindow()
359        window.mouseLeftUp.disconnect(self._finishResizing)
360        screenSize = window.GetClientSize()
361        self.ResizeLegend(
362            window.mouse["begin"],
363            window.mouse["end"],
364            screenSize)
365        self._giface.GetMapDisplay().GetMapToolbar().SelectDefault()
366        # redraw
367        self.overlayChanged.emit()
368