decorations.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. """
  2. @package mapwin.decorations
  3. @brief Map display decorations (overlays) - text, barscale and legend
  4. Classes:
  5. - decorations::OverlayController
  6. - decorations::BarscaleController
  7. - decorations::ArrowController
  8. - decorations::LegendController
  9. - decorations::TextLayerDialog
  10. (C) 2006-2014 by the GRASS Development Team
  11. This program is free software under the GNU General Public License
  12. (>=v2). Read the file COPYING that comes with GRASS for details.
  13. @author Anna Kratochvilova <kratochanna gmail.com>
  14. """
  15. import os
  16. from core.utils import _
  17. import wx
  18. from wx.lib.expando import ExpandoTextCtrl, EVT_ETC_LAYOUT_NEEDED
  19. from grass.pydispatch.signal import Signal
  20. try:
  21. from PIL import Image
  22. hasPIL = True
  23. except ImportError:
  24. hasPIL = False
  25. class OverlayId:
  26. legendId = 0
  27. barscaleId = 1
  28. arrowId = 2
  29. class OverlayController(object):
  30. """Base class for decorations (barscale, legend) controller."""
  31. def __init__(self, renderer, giface):
  32. self._giface = giface
  33. self._renderer = renderer
  34. self._overlay = None
  35. self._coords = None
  36. self._pdcType = 'image'
  37. self._propwin = None
  38. self._defaultAt = ''
  39. self._cmd = None # to be set by user
  40. self._name = None # to be defined by subclass
  41. self._id = None # to be defined by subclass
  42. self._dialog = None
  43. # signals that overlay or its visibility changed
  44. self.overlayChanged = Signal('OverlayController::overlayChanged')
  45. def SetCmd(self, cmd):
  46. hasAt = False
  47. for i in cmd:
  48. if i.startswith("at="):
  49. hasAt = True
  50. # reset coordinates, 'at' values will be used, see GetCoords
  51. self._coords = None
  52. break
  53. if not hasAt:
  54. cmd.append(self._defaultAt)
  55. self._cmd = cmd
  56. def GetCmd(self):
  57. return self._cmd
  58. cmd = property(fset=SetCmd, fget=GetCmd)
  59. def SetCoords(self, coords):
  60. self._coords = list(coords)
  61. def GetCoords(self):
  62. if self._coords is None: # initial position
  63. x, y = self.GetPlacement((self._renderer.width, self._renderer.height))
  64. self._coords = [x, y]
  65. return self._coords
  66. coords = property(fset=SetCoords, fget=GetCoords)
  67. def GetPdcType(self):
  68. return self._pdcType
  69. pdcType = property(fget=GetPdcType)
  70. def GetName(self):
  71. return self._name
  72. name = property(fget=GetName)
  73. def GetId(self):
  74. return self._id
  75. id = property(fget=GetId)
  76. def GetPropwin(self):
  77. return self._propwin
  78. def SetPropwin(self, win):
  79. self._propwin = win
  80. propwin = property(fget=GetPropwin, fset=SetPropwin)
  81. def GetLayer(self):
  82. return self._overlay
  83. layer = property(fget=GetLayer)
  84. def GetDialog(self):
  85. return self._dialog
  86. def SetDialog(self, win):
  87. self._dialog = win
  88. dialog = property(fget=GetDialog, fset=SetDialog)
  89. def IsShown(self):
  90. if self._overlay and self._overlay.IsActive() and self._overlay.IsRendered():
  91. return True
  92. return False
  93. def Show(self, show=True):
  94. """Activate or deactivate overlay."""
  95. if show:
  96. if not self._overlay:
  97. self._add()
  98. self._overlay.SetActive(True)
  99. self._update()
  100. else:
  101. self.Hide()
  102. self.overlayChanged.emit()
  103. def Hide(self):
  104. if self._overlay:
  105. self._overlay.SetActive(False)
  106. self.overlayChanged.emit()
  107. def GetOptData(self, dcmd, layer, params, propwin):
  108. """Called after options are set through module dialog.
  109. :param dcmd: resulting command
  110. :param layer: not used
  111. :param params: module parameters (not used)
  112. :param propwin: dialog window
  113. """
  114. if not dcmd:
  115. return
  116. self._cmd = dcmd
  117. self._dialog = propwin
  118. self.Show()
  119. def _add(self):
  120. self._overlay = self._renderer.AddOverlay(id=self._id, ltype=self._name,
  121. command=self.cmd, active=False,
  122. render=False, hidden=True)
  123. # check if successful
  124. def _update(self):
  125. self._renderer.ChangeOverlay(id=self._id, command=self._cmd,
  126. render=False)
  127. def CmdIsValid(self):
  128. """If command is valid"""
  129. return True
  130. def GetPlacement(self, screensize):
  131. """Get coordinates where to place overlay in a reasonable way
  132. :param screensize: screen size
  133. """
  134. if not hasPIL:
  135. self._giface.WriteWarning(_("Please install Python Imaging Library (PIL)\n"
  136. "for better control of legend and other decorations."))
  137. return 0, 0
  138. for param in self._cmd:
  139. if not param.startswith('at'):
  140. continue
  141. x, y = [float(number) for number in param.split('=')[1].split(',')]
  142. x = int((x / 100.) * screensize[0])
  143. y = int((1 - y / 100.) * screensize[1])
  144. return x, y
  145. class BarscaleController(OverlayController):
  146. def __init__(self, renderer, giface):
  147. OverlayController.__init__(self, renderer, giface)
  148. self._id = OverlayId.barscaleId
  149. self._name = 'barscale'
  150. # different from default because the reference point is not in the middle
  151. self._defaultAt = 'at=0,98'
  152. self._cmd = ['d.barscale', self._defaultAt]
  153. class ArrowController(OverlayController):
  154. def __init__(self, renderer, giface):
  155. OverlayController.__init__(self, renderer, giface)
  156. self._id = OverlayId.arrowId
  157. self._name = 'arrow'
  158. # different from default because the reference point is not in the middle
  159. self._defaultAt = 'at=85.0,25.0'
  160. self._cmd = ['d.northarrow', self._defaultAt]
  161. class LegendController(OverlayController):
  162. def __init__(self, renderer, giface):
  163. OverlayController.__init__(self, renderer, giface)
  164. self._id = OverlayId.legendId
  165. self._name = 'legend'
  166. # TODO: synchronize with d.legend?
  167. self._defaultAt = 'at=5,50,7,10'
  168. self._cmd = ['d.legend', self._defaultAt]
  169. def GetPlacement(self, screensize):
  170. if not hasPIL:
  171. self._giface.WriteWarning(_("Please install Python Imaging Library (PIL)\n"
  172. "for better control of legend and other decorations."))
  173. return 0, 0
  174. for param in self._cmd:
  175. if not param.startswith('at'):
  176. continue
  177. b, t, l, r = [float(number) for number in param.split('=')[1].split(',')] # pylint: disable-msg=W0612
  178. x = int((l / 100.) * screensize[0])
  179. y = int((1 - t / 100.) * screensize[1])
  180. return x, y
  181. def CmdIsValid(self):
  182. inputs = 0
  183. for param in self._cmd[1:]:
  184. param = param.split('=')
  185. if len(param) == 1:
  186. inputs += 1
  187. else:
  188. if param[0] == 'raster' and len(param) == 2:
  189. inputs += 1
  190. elif param[0] == 'raster_3d' and len(param) == 2:
  191. inputs += 1
  192. if inputs == 1:
  193. return True
  194. return False
  195. def ResizeLegend(self, begin, end, screenSize):
  196. """Resize legend according to given bbox coordinates."""
  197. w = abs(begin[0] - end[0])
  198. h = abs(begin[1] - end[1])
  199. if begin[0] < end[0]:
  200. x = begin[0]
  201. else:
  202. x = end[0]
  203. if begin[1] < end[1]:
  204. y = begin[1]
  205. else:
  206. y = end[1]
  207. at = [(screenSize[1] - (y + h)) / float(screenSize[1]) * 100,
  208. (screenSize[1] - y) / float(screenSize[1]) * 100,
  209. x / float(screenSize[0]) * 100,
  210. (x + w) / float(screenSize[0]) * 100]
  211. atStr = "at=%d,%d,%d,%d" % (at[0], at[1], at[2], at[3])
  212. for i, subcmd in enumerate(self._cmd):
  213. if subcmd.startswith('at='):
  214. self._cmd[i] = atStr
  215. break
  216. self._coords = None
  217. self.Show()
  218. def StartResizing(self):
  219. """Tool in toolbar or button itself were pressed"""
  220. # prepare for resizing
  221. window = self._giface.GetMapWindow()
  222. window.SetNamedCursor('cross')
  223. window.mouse['use'] = None
  224. window.mouse['box'] = 'box'
  225. window.pen = wx.Pen(colour='Black', width=2, style=wx.SHORT_DASH)
  226. window.mouseLeftUp.connect(self._finishResizing)
  227. def _finishResizing(self):
  228. window = self._giface.GetMapWindow()
  229. window.mouseLeftUp.disconnect(self._finishResizing)
  230. screenSize = window.GetClientSizeTuple()
  231. self.ResizeLegend(window.mouse["begin"], window.mouse["end"], screenSize)
  232. self._giface.GetMapDisplay().GetMapToolbar().SelectDefault()
  233. # redraw
  234. self.overlayChanged.emit()
  235. class TextLayerDialog(wx.Dialog):
  236. """!Controls setting options and displaying/hiding map overlay decorations
  237. """
  238. def __init__(self, parent, ovlId, title, name='text', size=wx.DefaultSize,
  239. style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER):
  240. wx.Dialog.__init__(self, parent=parent, id=wx.ID_ANY, title=title, style=style, size=size)
  241. self.ovlId = ovlId
  242. self.parent = parent
  243. if self.ovlId in self.parent.MapWindow.textdict.keys():
  244. self.currText = self.parent.MapWindow.textdict[self.ovlId]['text']
  245. self.currFont = self.parent.MapWindow.textdict[self.ovlId]['font']
  246. self.currClr = self.parent.MapWindow.textdict[self.ovlId]['color']
  247. self.currRot = self.parent.MapWindow.textdict[self.ovlId]['rotation']
  248. self.currCoords = self.parent.MapWindow.textdict[self.ovlId]['coords']
  249. self.currBB = self.parent.MapWindow.textdict[self.ovlId]['bbox']
  250. else:
  251. self.currClr = wx.BLACK
  252. self.currText = ''
  253. self.currFont = self.GetFont()
  254. self.currRot = 0.0
  255. self.currCoords = [10, 10]
  256. self.currBB = wx.Rect()
  257. self.sizer = wx.BoxSizer(wx.VERTICAL)
  258. box = wx.GridBagSizer(vgap=5, hgap=5)
  259. # show/hide
  260. self.chkbox = wx.CheckBox(parent=self, id=wx.ID_ANY,
  261. label=_('Show text object'))
  262. if self.parent.Map.GetOverlay(self.ovlId) is None:
  263. self.chkbox.SetValue(True)
  264. else:
  265. self.chkbox.SetValue(self.parent.MapWindow.overlays[self.ovlId]['layer'].IsActive())
  266. box.Add(item=self.chkbox, span=(1, 2),
  267. pos=(0, 0))
  268. # text entry
  269. box.Add(item=wx.StaticText(parent=self, id=wx.ID_ANY, label=_("Text:")),
  270. flag=wx.ALIGN_CENTER_VERTICAL,
  271. pos=(1, 0))
  272. self.textentry = ExpandoTextCtrl(parent=self, id=wx.ID_ANY, value="", size=(300, -1))
  273. self.textentry.SetFont(self.currFont)
  274. self.textentry.SetForegroundColour(self.currClr)
  275. self.textentry.SetValue(self.currText)
  276. # get rid of unneeded scrollbar when text box first opened
  277. self.textentry.SetClientSize((300, -1))
  278. box.Add(item=self.textentry,
  279. flag=wx.EXPAND,
  280. pos=(1, 1))
  281. # rotation
  282. box.Add(item=wx.StaticText(parent=self, id=wx.ID_ANY, label=_("Rotation:")),
  283. flag=wx.ALIGN_CENTER_VERTICAL,
  284. pos=(2, 0))
  285. self.rotation = wx.SpinCtrl(parent=self, id=wx.ID_ANY, value="", pos=(30, 50),
  286. size=(75, -1), style=wx.SP_ARROW_KEYS)
  287. self.rotation.SetRange(-360, 360)
  288. self.rotation.SetValue(int(self.currRot))
  289. box.Add(item=self.rotation,
  290. flag=wx.ALIGN_RIGHT,
  291. pos=(2, 1))
  292. # font
  293. box.Add(item=wx.StaticText(parent=self, id=wx.ID_ANY, label=_("Font:")),
  294. flag=wx.ALIGN_CENTER_VERTICAL,
  295. pos=(3, 0))
  296. fontbtn = wx.Button(parent=self, id=wx.ID_ANY, label=_("Set font"))
  297. box.Add(item=fontbtn,
  298. flag=wx.ALIGN_RIGHT,
  299. pos=(3, 1))
  300. box.AddGrowableCol(1)
  301. box.AddGrowableRow(1)
  302. self.sizer.Add(item=box, proportion=1,
  303. flag=wx.ALL | wx.EXPAND, border=10)
  304. # note
  305. box = wx.BoxSizer(wx.HORIZONTAL)
  306. label = wx.StaticText(parent=self, id=wx.ID_ANY,
  307. label=_("Drag text with mouse in pointer mode "
  308. "to position.\nDouble-click to change options"))
  309. box.Add(item=label, proportion=0,
  310. flag=wx.ALIGN_CENTRE | wx.ALL, border=5)
  311. self.sizer.Add(item=box, proportion=0,
  312. flag=wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.ALIGN_CENTER | wx.ALL, border=5)
  313. line = wx.StaticLine(parent=self, id=wx.ID_ANY,
  314. size=(20, -1), style=wx.LI_HORIZONTAL)
  315. self.sizer.Add(item=line, proportion=0,
  316. flag=wx.EXPAND | wx.ALIGN_CENTRE | wx.ALL, border=5)
  317. btnsizer = wx.StdDialogButtonSizer()
  318. btn = wx.Button(parent=self, id=wx.ID_OK)
  319. btn.SetDefault()
  320. btnsizer.AddButton(btn)
  321. btn = wx.Button(parent=self, id=wx.ID_CANCEL)
  322. btnsizer.AddButton(btn)
  323. btnsizer.Realize()
  324. self.sizer.Add(item=btnsizer, proportion=0,
  325. flag=wx.EXPAND | wx.ALL | wx.ALIGN_CENTER, border=5)
  326. self.SetSizer(self.sizer)
  327. self.sizer.Fit(self)
  328. # bindings
  329. self.Bind(EVT_ETC_LAYOUT_NEEDED, self.OnRefit, self.textentry)
  330. self.Bind(wx.EVT_BUTTON, self.OnSelectFont, fontbtn)
  331. self.Bind(wx.EVT_TEXT, self.OnText, self.textentry)
  332. self.Bind(wx.EVT_SPINCTRL, self.OnRotation, self.rotation)
  333. self.SetMinSize((400, 230))
  334. def OnRefit(self, event):
  335. """Resize text entry to match text"""
  336. self.sizer.Fit(self)
  337. def OnText(self, event):
  338. """Change text string"""
  339. self.currText = event.GetString()
  340. def OnRotation(self, event):
  341. """Change rotation"""
  342. self.currRot = event.GetInt()
  343. event.Skip()
  344. def OnSelectFont(self, event):
  345. """Change font"""
  346. data = wx.FontData()
  347. data.EnableEffects(True)
  348. data.SetColour(self.currClr) # set colour
  349. data.SetInitialFont(self.currFont)
  350. dlg = wx.FontDialog(self, data)
  351. if dlg.ShowModal() == wx.ID_OK:
  352. data = dlg.GetFontData()
  353. self.currFont = data.GetChosenFont()
  354. self.currClr = data.GetColour()
  355. self.textentry.SetFont(self.currFont)
  356. self.textentry.SetForegroundColour(self.currClr)
  357. self.Layout()
  358. dlg.Destroy()
  359. def GetValues(self):
  360. """Get text properties"""
  361. return {'text': self.currText,
  362. 'font': self.currFont,
  363. 'color': self.currClr,
  364. 'rotation': self.currRot,
  365. 'coords': self.currCoords,
  366. 'active': self.chkbox.IsChecked()}