decorations.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515
  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(
  64. (self._renderer.width, self._renderer.height))
  65. self._coords = [x, y]
  66. return self._coords
  67. coords = property(fset=SetCoords, fget=GetCoords)
  68. def GetPdcType(self):
  69. return self._pdcType
  70. pdcType = property(fget=GetPdcType)
  71. def GetName(self):
  72. return self._name
  73. name = property(fget=GetName)
  74. def GetId(self):
  75. return self._id
  76. id = property(fget=GetId)
  77. def GetPropwin(self):
  78. return self._propwin
  79. def SetPropwin(self, win):
  80. self._propwin = win
  81. propwin = property(fget=GetPropwin, fset=SetPropwin)
  82. def GetLayer(self):
  83. return self._overlay
  84. layer = property(fget=GetLayer)
  85. def GetDialog(self):
  86. return self._dialog
  87. def SetDialog(self, win):
  88. self._dialog = win
  89. dialog = property(fget=GetDialog, fset=SetDialog)
  90. def IsShown(self):
  91. if self._overlay and self._overlay.IsActive() and self._overlay.IsRendered():
  92. return True
  93. return False
  94. def Show(self, show=True):
  95. """Activate or deactivate overlay."""
  96. if show:
  97. if not self._overlay:
  98. self._add()
  99. self._overlay.SetActive(True)
  100. self._update()
  101. else:
  102. self.Hide()
  103. self.overlayChanged.emit()
  104. def Hide(self):
  105. if self._overlay:
  106. self._overlay.SetActive(False)
  107. self.overlayChanged.emit()
  108. def GetOptData(self, dcmd, layer, params, propwin):
  109. """Called after options are set through module dialog.
  110. :param dcmd: resulting command
  111. :param layer: not used
  112. :param params: module parameters (not used)
  113. :param propwin: dialog window
  114. """
  115. if not dcmd:
  116. return
  117. self._cmd = dcmd
  118. self._dialog = propwin
  119. self.Show()
  120. def _add(self):
  121. self._overlay = self._renderer.AddOverlay(
  122. id=self._id,
  123. ltype=self._name,
  124. command=self.cmd,
  125. active=False,
  126. render=True,
  127. hidden=True)
  128. # check if successful
  129. def _update(self):
  130. self._renderer.ChangeOverlay(id=self._id, command=self._cmd)
  131. def CmdIsValid(self):
  132. """If command is valid"""
  133. return True
  134. def GetPlacement(self, screensize):
  135. """Get coordinates where to place overlay in a reasonable way
  136. :param screensize: screen size
  137. """
  138. if not hasPIL:
  139. self._giface.WriteWarning(
  140. _(
  141. "Please install Python Imaging Library (PIL)\n"
  142. "for better control of legend and other decorations."))
  143. return 0, 0
  144. for param in self._cmd:
  145. if not param.startswith('at'):
  146. continue
  147. x, y = [float(number) for number in param.split('=')[1].split(',')]
  148. x = int((x / 100.) * screensize[0])
  149. y = int((1 - y / 100.) * screensize[1])
  150. return x, y
  151. class BarscaleController(OverlayController):
  152. def __init__(self, renderer, giface):
  153. OverlayController.__init__(self, renderer, giface)
  154. self._id = OverlayId.barscaleId
  155. self._name = 'barscale'
  156. # different from default because the reference point is not in the
  157. # middle
  158. self._defaultAt = 'at=0,98'
  159. self._cmd = ['d.barscale', self._defaultAt]
  160. class ArrowController(OverlayController):
  161. def __init__(self, renderer, giface):
  162. OverlayController.__init__(self, renderer, giface)
  163. self._id = OverlayId.arrowId
  164. self._name = 'arrow'
  165. # different from default because the reference point is not in the
  166. # middle
  167. self._defaultAt = 'at=85.0,25.0'
  168. self._cmd = ['d.northarrow', self._defaultAt]
  169. class LegendController(OverlayController):
  170. def __init__(self, renderer, giface):
  171. OverlayController.__init__(self, renderer, giface)
  172. self._id = OverlayId.legendId
  173. self._name = 'legend'
  174. # TODO: synchronize with d.legend?
  175. self._defaultAt = 'at=5,50,7,10'
  176. self._cmd = ['d.legend', self._defaultAt]
  177. def GetPlacement(self, screensize):
  178. if not hasPIL:
  179. self._giface.WriteWarning(
  180. _(
  181. "Please install Python Imaging Library (PIL)\n"
  182. "for better control of legend and other decorations."))
  183. return 0, 0
  184. for param in self._cmd:
  185. if not param.startswith('at'):
  186. continue
  187. b, t, l, r = [float(number) for number in param.split(
  188. '=')[1].split(',')] # pylint: disable-msg=W0612
  189. x = int((l / 100.) * screensize[0])
  190. y = int((1 - t / 100.) * screensize[1])
  191. return x, y
  192. def CmdIsValid(self):
  193. inputs = 0
  194. for param in self._cmd[1:]:
  195. param = param.split('=')
  196. if len(param) == 1:
  197. inputs += 1
  198. else:
  199. if param[0] == 'raster' and len(param) == 2:
  200. inputs += 1
  201. elif param[0] == 'raster_3d' and len(param) == 2:
  202. inputs += 1
  203. if inputs == 1:
  204. return True
  205. return False
  206. def ResizeLegend(self, begin, end, screenSize):
  207. """Resize legend according to given bbox coordinates."""
  208. w = abs(begin[0] - end[0])
  209. h = abs(begin[1] - end[1])
  210. if begin[0] < end[0]:
  211. x = begin[0]
  212. else:
  213. x = end[0]
  214. if begin[1] < end[1]:
  215. y = begin[1]
  216. else:
  217. y = end[1]
  218. at = [(screenSize[1] - (y + h)) / float(screenSize[1]) * 100,
  219. (screenSize[1] - y) / float(screenSize[1]) * 100,
  220. x / float(screenSize[0]) * 100,
  221. (x + w) / float(screenSize[0]) * 100]
  222. atStr = "at=%d,%d,%d,%d" % (at[0], at[1], at[2], at[3])
  223. for i, subcmd in enumerate(self._cmd):
  224. if subcmd.startswith('at='):
  225. self._cmd[i] = atStr
  226. break
  227. self._coords = None
  228. self.Show()
  229. def StartResizing(self):
  230. """Tool in toolbar or button itself were pressed"""
  231. # prepare for resizing
  232. window = self._giface.GetMapWindow()
  233. window.SetNamedCursor('cross')
  234. window.mouse['use'] = None
  235. window.mouse['box'] = 'box'
  236. window.pen = wx.Pen(colour='Black', width=2, style=wx.SHORT_DASH)
  237. window.mouseLeftUp.connect(self._finishResizing)
  238. def _finishResizing(self):
  239. window = self._giface.GetMapWindow()
  240. window.mouseLeftUp.disconnect(self._finishResizing)
  241. screenSize = window.GetClientSizeTuple()
  242. self.ResizeLegend(
  243. window.mouse["begin"],
  244. window.mouse["end"],
  245. screenSize)
  246. self._giface.GetMapDisplay().GetMapToolbar().SelectDefault()
  247. # redraw
  248. self.overlayChanged.emit()
  249. class TextLayerDialog(wx.Dialog):
  250. """!Controls setting options and displaying/hiding map overlay decorations
  251. """
  252. def __init__(self, parent, ovlId, title, name='text', size=wx.DefaultSize,
  253. style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER):
  254. wx.Dialog.__init__(
  255. self,
  256. parent=parent,
  257. id=wx.ID_ANY,
  258. title=title,
  259. style=style,
  260. size=size)
  261. self.ovlId = ovlId
  262. self.parent = parent
  263. if self.ovlId in self.parent.MapWindow.textdict.keys():
  264. self.currText = self.parent.MapWindow.textdict[self.ovlId]['text']
  265. self.currFont = self.parent.MapWindow.textdict[self.ovlId]['font']
  266. self.currClr = self.parent.MapWindow.textdict[self.ovlId]['color']
  267. self.currRot = self.parent.MapWindow.textdict[
  268. self.ovlId]['rotation']
  269. self.currCoords = self.parent.MapWindow.textdict[
  270. self.ovlId]['coords']
  271. self.currBB = self.parent.MapWindow.textdict[self.ovlId]['bbox']
  272. else:
  273. self.currClr = wx.BLACK
  274. self.currText = ''
  275. self.currFont = self.GetFont()
  276. self.currRot = 0.0
  277. self.currCoords = [10, 10]
  278. self.currBB = wx.Rect()
  279. self.sizer = wx.BoxSizer(wx.VERTICAL)
  280. box = wx.GridBagSizer(vgap=5, hgap=5)
  281. # show/hide
  282. self.chkbox = wx.CheckBox(parent=self, id=wx.ID_ANY,
  283. label=_('Show text object'))
  284. if self.parent.Map.GetOverlay(self.ovlId) is None:
  285. self.chkbox.SetValue(True)
  286. else:
  287. self.chkbox.SetValue(
  288. self.parent.MapWindow.overlays[
  289. self.ovlId]['layer'].IsActive())
  290. box.Add(item=self.chkbox, span=(1, 2),
  291. pos=(0, 0))
  292. # text entry
  293. box.Add(
  294. item=wx.StaticText(
  295. parent=self,
  296. id=wx.ID_ANY,
  297. label=_("Text:")),
  298. flag=wx.ALIGN_CENTER_VERTICAL,
  299. pos=(
  300. 1,
  301. 0))
  302. self.textentry = ExpandoTextCtrl(
  303. parent=self, id=wx.ID_ANY, value="", size=(300, -1))
  304. self.textentry.SetFont(self.currFont)
  305. self.textentry.SetForegroundColour(self.currClr)
  306. self.textentry.SetValue(self.currText)
  307. # get rid of unneeded scrollbar when text box first opened
  308. self.textentry.SetClientSize((300, -1))
  309. box.Add(item=self.textentry,
  310. flag=wx.EXPAND,
  311. pos=(1, 1))
  312. # rotation
  313. box.Add(
  314. item=wx.StaticText(
  315. parent=self,
  316. id=wx.ID_ANY,
  317. label=_("Rotation:")),
  318. flag=wx.ALIGN_CENTER_VERTICAL,
  319. pos=(
  320. 2,
  321. 0))
  322. self.rotation = wx.SpinCtrl(
  323. parent=self, id=wx.ID_ANY, value="", pos=(
  324. 30, 50), size=(
  325. 75, -1), style=wx.SP_ARROW_KEYS)
  326. self.rotation.SetRange(-360, 360)
  327. self.rotation.SetValue(int(self.currRot))
  328. box.Add(item=self.rotation,
  329. flag=wx.ALIGN_RIGHT,
  330. pos=(2, 1))
  331. # font
  332. box.Add(
  333. item=wx.StaticText(
  334. parent=self,
  335. id=wx.ID_ANY,
  336. label=_("Font:")),
  337. flag=wx.ALIGN_CENTER_VERTICAL,
  338. pos=(
  339. 3,
  340. 0))
  341. fontbtn = wx.Button(parent=self, id=wx.ID_ANY, label=_("Set font"))
  342. box.Add(item=fontbtn,
  343. flag=wx.ALIGN_RIGHT,
  344. pos=(3, 1))
  345. box.AddGrowableCol(1)
  346. box.AddGrowableRow(1)
  347. self.sizer.Add(item=box, proportion=1,
  348. flag=wx.ALL | wx.EXPAND, border=10)
  349. # note
  350. box = wx.BoxSizer(wx.HORIZONTAL)
  351. label = wx.StaticText(
  352. parent=self, id=wx.ID_ANY, label=_(
  353. "Drag text with mouse in pointer mode "
  354. "to position.\nDouble-click to change options"))
  355. box.Add(item=label, proportion=0,
  356. flag=wx.ALIGN_CENTRE | wx.ALL, border=5)
  357. self.sizer.Add(
  358. item=box, proportion=0, flag=wx.EXPAND | wx.ALIGN_CENTER_VERTICAL |
  359. wx.ALIGN_CENTER | wx.ALL, border=5)
  360. line = wx.StaticLine(parent=self, id=wx.ID_ANY,
  361. size=(20, -1), style=wx.LI_HORIZONTAL)
  362. self.sizer.Add(item=line, proportion=0,
  363. flag=wx.EXPAND | wx.ALIGN_CENTRE | wx.ALL, border=5)
  364. btnsizer = wx.StdDialogButtonSizer()
  365. btn = wx.Button(parent=self, id=wx.ID_OK)
  366. btn.SetDefault()
  367. btnsizer.AddButton(btn)
  368. btn = wx.Button(parent=self, id=wx.ID_CANCEL)
  369. btnsizer.AddButton(btn)
  370. btnsizer.Realize()
  371. self.sizer.Add(item=btnsizer, proportion=0,
  372. flag=wx.EXPAND | wx.ALL | wx.ALIGN_CENTER, border=5)
  373. self.SetSizer(self.sizer)
  374. self.sizer.Fit(self)
  375. # bindings
  376. self.Bind(EVT_ETC_LAYOUT_NEEDED, self.OnRefit, self.textentry)
  377. self.Bind(wx.EVT_BUTTON, self.OnSelectFont, fontbtn)
  378. self.Bind(wx.EVT_TEXT, self.OnText, self.textentry)
  379. self.Bind(wx.EVT_SPINCTRL, self.OnRotation, self.rotation)
  380. self.SetMinSize((400, 230))
  381. def OnRefit(self, event):
  382. """Resize text entry to match text"""
  383. self.sizer.Fit(self)
  384. def OnText(self, event):
  385. """Change text string"""
  386. self.currText = event.GetString()
  387. def OnRotation(self, event):
  388. """Change rotation"""
  389. self.currRot = event.GetInt()
  390. event.Skip()
  391. def OnSelectFont(self, event):
  392. """Change font"""
  393. data = wx.FontData()
  394. data.EnableEffects(True)
  395. data.SetColour(self.currClr) # set colour
  396. data.SetInitialFont(self.currFont)
  397. dlg = wx.FontDialog(self, data)
  398. if dlg.ShowModal() == wx.ID_OK:
  399. data = dlg.GetFontData()
  400. self.currFont = data.GetChosenFont()
  401. self.currClr = data.GetColour()
  402. self.textentry.SetFont(self.currFont)
  403. self.textentry.SetForegroundColour(self.currClr)
  404. self.Layout()
  405. dlg.Destroy()
  406. def GetValues(self):
  407. """Get text properties"""
  408. return {'text': self.currText,
  409. 'font': self.currFont,
  410. 'color': self.currClr,
  411. 'rotation': self.currRot,
  412. 'coords': self.currCoords,
  413. 'active': self.chkbox.IsChecked()}