buffered.py 75 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294
  1. """
  2. @package mapwin.mapwindow
  3. @brief Map display canvas - buffered window.
  4. Classes:
  5. - mapwindow::BufferedWindow
  6. - mapwindow::GraphicsSet
  7. - mapwindow::GraphicsSetItem
  8. (C) 2006-2013 by the GRASS Development Team
  9. This program is free software under the GNU General Public License
  10. (>=v2). Read the file COPYING that comes with GRASS for details.
  11. @author Martin Landa <landa.martin gmail.com>
  12. @author Michael Barton
  13. @author Jachym Cepicky
  14. @author Stepan Turek <stepan.turek seznam.cz> (handlers support, GraphicsSet)
  15. @author Anna Petrasova <kratochanna gmail.com> (refactoring)
  16. @author Vaclav Petras <wenzeslaus gmail.com> (refactoring)
  17. """
  18. from __future__ import print_function
  19. import os
  20. import time
  21. import math
  22. import sys
  23. import wx
  24. from grass.pydispatch.signal import Signal
  25. from core.globalvar import wxPythonPhoenix
  26. import grass.script as grass
  27. from gui_core.dialogs import SavedRegion
  28. from gui_core.wrap import (
  29. DragImage,
  30. PseudoDC,
  31. EmptyBitmap,
  32. BitmapFromImage,
  33. Window,
  34. Menu,
  35. Rect,
  36. NewId,
  37. )
  38. from core.gcmd import RunCommand, GException, GError
  39. from core.debug import Debug
  40. from core.settings import UserSettings
  41. from mapwin.base import MapWindowBase
  42. import core.utils as utils
  43. from mapwin.graphics import GraphicsSet
  44. from core.gthread import gThread
  45. try:
  46. import grass.lib.gis as gislib
  47. haveCtypes = True
  48. except (ImportError, TypeError):
  49. haveCtypes = False
  50. class BufferedMapWindow(MapWindowBase, Window):
  51. """A Buffered window class (2D view mode)
  52. Superclass for VDigitWindow (vector digitizer).
  53. When the drawing needs to change, you app needs to call the
  54. UpdateMap() method. Since the drawing is stored in a bitmap, you
  55. can also save the drawing to file by calling the
  56. SaveToFile() method.
  57. """
  58. def __init__(
  59. self,
  60. parent,
  61. giface,
  62. Map,
  63. properties,
  64. id=wx.ID_ANY,
  65. overlays=None,
  66. style=wx.NO_FULL_REPAINT_ON_RESIZE,
  67. **kwargs,
  68. ):
  69. """
  70. :param parent: parent window
  71. :param giface: grass interface instance
  72. :param map: map instance
  73. :param properties: instance of MapWindowProperties
  74. :param id: wx window id
  75. :param style: wx window style
  76. :param kwargs: keyword arguments passed to MapWindow and wx.Window
  77. """
  78. MapWindowBase.__init__(self, parent=parent, giface=giface, Map=Map)
  79. wx.Window.__init__(self, parent=parent, id=id, style=style, **kwargs)
  80. # This is applied when no layers are rendered and thus the background
  81. # color is not applied in rendering itself (it would be applied always
  82. # if rendering would use transparent background).
  83. self.SetBackgroundColour(
  84. wx.Colour(*UserSettings.Get(group="display", key="bgcolor", subkey="color"))
  85. )
  86. self._properties = properties
  87. # this class should not ask for digit, this is a hack
  88. self.digit = None
  89. # flags
  90. self.resize = False # indicates whether or not a resize event has taken place
  91. self.dragimg = None # initialize variable for map panning
  92. self.alwaysRender = (
  93. False # if it always sets render to True in self.UpdateMap()
  94. )
  95. # variables for drawing on DC
  96. self.pen = None # pen for drawing zoom boxes, etc.
  97. # pen for drawing polylines (measurements, profiles, etc)
  98. self.polypen = None
  99. # List of wx.Point tuples defining a polyline (geographical
  100. # coordinates)
  101. self.polycoords = []
  102. # ID of rubber band line
  103. self.lineid = None
  104. # ID of poly line resulting from cumulative rubber band lines (e.g.
  105. # measurement)
  106. self.plineid = None
  107. # following class members deals with merging more updateMap request
  108. # into one UpdateMap process
  109. # thread where timer for measuring delay limit
  110. self.renderTimingThr = gThread()
  111. # relevant timer id given by the thread
  112. self.timerRunId = None
  113. # time, of last updateMap request
  114. self.lastUpdateMapReq = None
  115. # defines time limit for waiting for another update request
  116. self.updDelay = 0
  117. # holds information about level of rendering during the delay limit
  118. self.render = self.renderVector = False
  119. # Emitted when zoom of a window is changed
  120. self.zoomChanged = Signal("BufferedWindow.zoomChanged")
  121. # Emitted when map was queried, parameters x, y are mouse coordinates
  122. # TODO: change pixel coordinates to map coordinates (using Pixel2Cell)
  123. self.mapQueried = Signal("BufferedWindow.mapQueried")
  124. # Emitted when the zoom history stack is emptied
  125. self.zoomHistoryUnavailable = Signal("BufferedWindow.zoomHistoryUnavailable")
  126. # Emitted when the zoom history stack is not empty
  127. self.zoomHistoryAvailable = Signal("BufferedWindow.zoomHistoryAvailable")
  128. # Emitted when map enters the window
  129. self.mouseEntered = Signal("BufferedWindow.mouseEntered")
  130. # Emitted when left mouse button is released and mouse use is 'pointer'
  131. # Parameters are x and y of the mouse click in map (cell) units
  132. # new and experimental, if the concept would be used widely,
  133. # it could replace register and unregister mechanism
  134. # and partially maybe also internal mouse use dictionary
  135. self.mouseLeftUpPointer = Signal("BufferedWindow.mouseLeftUpPointer")
  136. # Emitted when left mouse button is released
  137. self.mouseLeftUp = Signal("BufferedWindow.mouseLeftUp")
  138. # Emitted when right mouse button is released
  139. self.mouseRightUp = Signal("BufferedWindow.mouseRightUp")
  140. # Emitted when left mouse button was pressed
  141. self.mouseLeftDown = Signal("BufferedWindow.mouseLeftDown")
  142. # Emitted after double-click
  143. self.mouseDClick = Signal("BufferedWindow.mouseDClick")
  144. # Emitted when mouse us moving (mouse motion event)
  145. # Parametres are x and y of the mouse position in map (cell) units
  146. self.mouseMoving = Signal("BufferedWindow.mouseMoving")
  147. # event bindings
  148. self.Bind(wx.EVT_PAINT, self.OnPaint)
  149. self.Bind(wx.EVT_SIZE, self.OnSize)
  150. self.Bind(wx.EVT_IDLE, self.OnIdle)
  151. self._bindMouseEvents()
  152. self.processMouse = True
  153. # render output objects
  154. self.img = None # wx.Image object (self.mapfile)
  155. # decoration overlays
  156. self.overlays = overlays
  157. # images and their PseudoDC ID's for painting and dragging
  158. self.imagedict = {}
  159. self.select = {} # selecting/unselecting decorations for dragging
  160. self.textdict = {} # text, font, and color indexed by id
  161. # zoom objects
  162. self.zoomhistory = [] # list of past zoom extents
  163. self.currzoom = 0 # current set of extents in zoom history being used
  164. self.zoomtype = 1 # 1 zoom in, 0 no zoom, -1 zoom out
  165. self.hitradius = 10 # distance for selecting map decorations
  166. # offset for dialog (e.g. DisplayAttributesDialog)
  167. self.dialogOffset = 5
  168. # OnSize called to make sure the buffer is initialized.
  169. # This might result in OnSize getting called twice on some
  170. # platforms at initialization, but little harm done.
  171. # self.OnSize(None)
  172. self._definePseudoDC()
  173. # redraw all pdc's, pdcTmp layer is redrawn always (speed issue)
  174. self.redrawAll = True
  175. # will store an off screen empty bitmap for saving to file
  176. self._buffer = EmptyBitmap(max(1, self.Map.width), max(1, self.Map.height))
  177. self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x: None)
  178. # rerender when Map reports change
  179. self.Map.layerChanged.connect(self.OnUpdateMap)
  180. self.Map.GetRenderMgr().renderDone.connect(self._updateMFinished)
  181. # vars for handling mouse clicks
  182. self.dragid = None
  183. self.lastpos = (0, 0)
  184. # list for registration of graphics to draw
  185. self.graphicsSetList = []
  186. def OnUpdateMap(self):
  187. # before lambda func was used, however it was problem
  188. # to disconnect it from signal
  189. self.UpdateMap()
  190. def DisactivateWin(self):
  191. """Use when the class instance is hidden in MapFrame."""
  192. self.Map.layerChanged.disconnect(self.OnUpdateMap)
  193. def ActivateWin(self):
  194. """Used when the class instance is activated in MapFrame."""
  195. self.Map.layerChanged.connect(self.OnUpdateMap)
  196. def _definePseudoDC(self):
  197. """Define PseudoDC objects to use"""
  198. # create PseudoDC used for background map, map decorations like scales
  199. # and legends
  200. self.pdc = PseudoDC()
  201. # used for digitization tool
  202. self.pdcVector = None
  203. # transparent objects (region box, raster digitizer)
  204. self.pdcTransparent = PseudoDC()
  205. # pseudoDC for temporal objects (select box, measurement tool, etc.)
  206. self.pdcTmp = PseudoDC()
  207. def _bindMouseEvents(self):
  208. self.Bind(wx.EVT_MOUSE_EVENTS, self.MouseActions)
  209. self.Bind(wx.EVT_MOTION, self.OnMotion)
  210. self.Bind(wx.EVT_CONTEXT_MENU, self.OnContextMenu)
  211. def OnContextMenu(self, event):
  212. """Show Map Display context menu"""
  213. if self.digit:
  214. event.Skip()
  215. return
  216. # generate popup-menu
  217. menu = Menu()
  218. if not hasattr(self, "popupCopyCoordinates"):
  219. self.popupCopyCoordinates = NewId()
  220. self.Bind(wx.EVT_MENU, self.OnCopyCoordinates, id=self.popupCopyCoordinates)
  221. menu.Append(self.popupCopyCoordinates, _("Copy coordinates to clipboard"))
  222. menu.AppendSeparator()
  223. if not hasattr(self, "popupShowAllToolbars"):
  224. self.popupShowAllToolbars = NewId()
  225. self.Bind(wx.EVT_MENU, self.OnShowAllToolbars, id=self.popupShowAllToolbars)
  226. menu.Append(
  227. self.popupShowAllToolbars,
  228. _("Hide toolbars")
  229. if self._giface.AreAllToolbarsShown()
  230. else _("Show toolbars"),
  231. )
  232. if not hasattr(self, "popupShowStatusbar"):
  233. self.popupShowStatusbar = NewId()
  234. self.Bind(wx.EVT_MENU, self.OnShowStatusbar, id=self.popupShowStatusbar)
  235. menu.Append(
  236. self.popupShowStatusbar,
  237. _("Hide statusbar")
  238. if self._giface.IsStatusbarShown()
  239. else _("Show statusbar"),
  240. )
  241. pos = self.ScreenToClient(event.GetPosition())
  242. idlist = self.pdc.FindObjects(pos[0], pos[1], self.hitradius)
  243. if (
  244. self.overlays
  245. and idlist
  246. and [i for i in idlist if i in list(self.overlays.keys())]
  247. ): # legend, scale bar, north arrow, dtext
  248. menu.AppendSeparator()
  249. removeId = NewId()
  250. self.Bind(
  251. wx.EVT_MENU,
  252. lambda evt: self.overlayRemoved.emit(overlayId=idlist[0]),
  253. id=removeId,
  254. )
  255. menu.Append(removeId, self.overlays[idlist[0]].removeLabel)
  256. # raster legend can be resized
  257. if self.overlays[idlist[0]].name == "legend":
  258. resizeLegendId = NewId()
  259. self.Bind(
  260. wx.EVT_MENU,
  261. lambda evt: self.overlays[idlist[0]].StartResizing(),
  262. id=resizeLegendId,
  263. )
  264. menu.Append(resizeLegendId, _("Resize and move legend"))
  265. activateId = NewId()
  266. self.Bind(
  267. wx.EVT_MENU,
  268. lambda evt: self.overlayActivated.emit(overlayId=idlist[0]),
  269. id=activateId,
  270. )
  271. menu.Append(activateId, self.overlays[idlist[0]].activateLabel)
  272. self.PopupMenu(menu)
  273. menu.Destroy()
  274. def Draw(
  275. self,
  276. pdc,
  277. img=None,
  278. drawid=None,
  279. pdctype="image",
  280. coords=[0, 0, 0, 0],
  281. pen=None,
  282. brush=None,
  283. ):
  284. """Draws map and overlay decorations"""
  285. if drawid is None:
  286. if pdctype == "image" and img:
  287. drawid = self.imagedict[img]
  288. elif pdctype == "clear":
  289. drawid = None
  290. else:
  291. drawid = NewId()
  292. # TODO: find better solution
  293. if not pen:
  294. if pdctype == "polyline":
  295. pen = self.polypen
  296. else:
  297. pen = self.pen
  298. if img and pdctype == "image":
  299. # self.imagedict[img]['coords'] = coords
  300. self.select[self.imagedict[img]["id"]] = False # ?
  301. pdc.BeginDrawing()
  302. if drawid != 99:
  303. bg = wx.TRANSPARENT_BRUSH
  304. else:
  305. bg = wx.Brush(self.GetBackgroundColour())
  306. pdc.SetBackground(bg)
  307. Debug.msg(
  308. 5,
  309. "BufferedWindow.Draw(): id=%s, pdctype = %s, coord=%s"
  310. % (drawid, pdctype, coords),
  311. )
  312. # set PseudoDC id
  313. if drawid is not None:
  314. pdc.SetId(drawid)
  315. if pdctype == "clear": # erase the display
  316. bg = wx.WHITE_BRUSH
  317. # bg = wx.Brush(self.GetBackgroundColour())
  318. pdc.SetBackground(bg)
  319. pdc.RemoveAll()
  320. pdc.Clear()
  321. pdc.EndDrawing()
  322. self.Refresh()
  323. return
  324. if pdctype == "image": # draw selected image
  325. bitmap = BitmapFromImage(img)
  326. w, h = bitmap.GetSize()
  327. pdc.DrawBitmap(bitmap, coords[0], coords[1], True) # draw the composite map
  328. pdc.SetIdBounds(drawid, Rect(coords[0], coords[1], w, h))
  329. elif pdctype == "box": # draw a box on top of the map
  330. if pen:
  331. if not brush:
  332. brush = wx.Brush(wx.CYAN, wx.TRANSPARENT)
  333. pdc.SetBrush(brush)
  334. pdc.SetPen(pen)
  335. x2 = max(coords[0], coords[2])
  336. x1 = min(coords[0], coords[2])
  337. y2 = max(coords[1], coords[3])
  338. y1 = min(coords[1], coords[3])
  339. rwidth = x2 - x1
  340. rheight = y2 - y1
  341. rect = Rect(x1, y1, rwidth, rheight)
  342. pdc.DrawRectangleRect(rect)
  343. pdc.SetIdBounds(drawid, rect)
  344. elif pdctype == "line": # draw a line on top of the map
  345. if pen:
  346. pdc.SetBrush(wx.Brush(wx.CYAN, wx.TRANSPARENT))
  347. pdc.SetPen(pen)
  348. pdc.DrawLinePoint(
  349. wx.Point(coords[0], coords[1]), wx.Point(coords[2], coords[3])
  350. )
  351. pdc.SetIdBounds(
  352. drawid, Rect(coords[0], coords[1], coords[2], coords[3])
  353. )
  354. # polyline is a series of connected lines defined as sequence of points
  355. # lines are individual, not connected lines which must be drawn as 1
  356. # object (e.g. cross)
  357. elif pdctype in ("polyline", "lines"):
  358. if pen:
  359. pdc.SetBrush(wx.Brush(wx.CYAN, wx.TRANSPARENT))
  360. pdc.SetPen(pen)
  361. if len(coords) < 2:
  362. return
  363. if pdctype == "polyline":
  364. i = 1
  365. while i < len(coords):
  366. pdc.DrawLinePoint(
  367. wx.Point(coords[i - 1][0], coords[i - 1][1]),
  368. wx.Point(coords[i][0], coords[i][1]),
  369. )
  370. i += 1
  371. else:
  372. for line in coords:
  373. pdc.DrawLine(line[0], line[1], line[2], line[3])
  374. # get bounding rectangle for polyline/lines
  375. xlist = []
  376. ylist = []
  377. if len(coords) > 0:
  378. if pdctype == "polyline":
  379. for point in coords:
  380. x, y = point
  381. xlist.append(x)
  382. ylist.append(y)
  383. else:
  384. for line in coords:
  385. x1, y1, x2, y2 = line
  386. xlist.extend([x1, x2])
  387. ylist.extend([y1, y2])
  388. x1 = min(xlist)
  389. x2 = max(xlist)
  390. y1 = min(ylist)
  391. y2 = max(ylist)
  392. pdc.SetIdBounds(drawid, Rect(x1, y1, x2, y2))
  393. elif pdctype == "polygon":
  394. if pen:
  395. pdc.SetPen(pen)
  396. if not brush:
  397. brush = wx.TRANSPARENT_BRUSH
  398. pdc.SetBrush(brush)
  399. pdc.DrawPolygon(points=coords)
  400. x = min(coords, key=lambda x: x[0])[0]
  401. y = min(coords, key=lambda x: x[1])[1]
  402. w = max(coords, key=lambda x: x[0])[0] - x
  403. h = max(coords, key=lambda x: x[1])[1] - y
  404. pdc.SetIdBounds(drawid, Rect(x, y, w, h))
  405. elif pdctype == "circle": # draw circle
  406. if pen:
  407. pdc.SetPen(pen)
  408. if not brush:
  409. brush = wx.TRANSPARENT_BRUSH
  410. pdc.SetBrush(brush)
  411. radius = abs(coords[2] - coords[0]) / 2
  412. pdc.DrawCircle(
  413. max(coords[0], coords[2]) - radius,
  414. max(coords[1], coords[3]) - radius,
  415. radius=radius,
  416. )
  417. pdc.SetIdBounds(
  418. drawid, Rect(coords[0], coords[1], coords[2], coords[3])
  419. )
  420. elif pdctype == "point": # draw point
  421. if pen:
  422. pdc.SetPen(pen)
  423. pdc.DrawPoint(coords[0], coords[1])
  424. coordsBound = (
  425. coords[0] - 5,
  426. coords[1] - 5,
  427. coords[0] + 5,
  428. coords[1] + 5,
  429. )
  430. pdc.SetIdBounds(drawid, Rect(coordsBound))
  431. elif pdctype == "text": # draw text on top of map
  432. if not img["active"]:
  433. return # only draw active text
  434. if "rotation" in img:
  435. rotation = float(img["rotation"])
  436. else:
  437. rotation = 0.0
  438. w, h = self.GetFullTextExtent(img["text"])[0:2]
  439. pdc.SetFont(img["font"])
  440. pdc.SetTextForeground(img["color"])
  441. if "background" in img:
  442. pdc.SetBackgroundMode(wx.SOLID)
  443. pdc.SetTextBackground(img["background"])
  444. coords, bbox = self.TextBounds(img)
  445. if rotation == 0:
  446. pdc.DrawText(img["text"], coords[0], coords[1])
  447. else:
  448. pdc.DrawRotatedText(img["text"], coords[0], coords[1], rotation)
  449. pdc.SetIdBounds(drawid, bbox)
  450. pdc.EndDrawing()
  451. self.Refresh()
  452. return drawid
  453. def TextBounds(self, textinfo, relcoords=False):
  454. """Return text boundary data
  455. :param textinfo: text metadata (text, font, color, rotation)
  456. :param coords: reference point
  457. :return: coords of nonrotated text bbox (TL corner)
  458. :return: bbox of rotated text bbox (wx.Rect)
  459. :return: relCoords are text coord inside bbox
  460. """
  461. if "rotation" in textinfo:
  462. rotation = float(textinfo["rotation"])
  463. else:
  464. rotation = 0.0
  465. coords = textinfo["coords"]
  466. bbox = Rect(coords[0], coords[1], 0, 0)
  467. relCoords = (0, 0)
  468. Debug.msg(
  469. 4,
  470. "BufferedWindow.TextBounds(): text=%s, rotation=%f"
  471. % (textinfo["text"], rotation),
  472. )
  473. self.Update()
  474. self.SetFont(textinfo["font"])
  475. w, h = self.GetTextExtent(textinfo["text"])
  476. if rotation == 0:
  477. bbox[2], bbox[3] = w, h
  478. if relcoords:
  479. return coords, bbox, relCoords
  480. else:
  481. return coords, bbox
  482. boxh = math.fabs(math.sin(math.radians(rotation)) * w) + h
  483. boxw = math.fabs(math.cos(math.radians(rotation)) * w) + h
  484. if rotation > 0 and rotation < 90:
  485. bbox[1] -= boxh
  486. relCoords = (0, boxh)
  487. elif rotation >= 90 and rotation < 180:
  488. bbox[0] -= boxw
  489. bbox[1] -= boxh
  490. relCoords = (boxw, boxh)
  491. elif rotation >= 180 and rotation < 270:
  492. bbox[0] -= boxw
  493. relCoords = (boxw, 0)
  494. bbox[2] = boxw
  495. bbox[3] = boxh
  496. bbox.Inflate(h, h)
  497. if relcoords:
  498. return coords, bbox, relCoords
  499. else:
  500. return coords, bbox
  501. def OnPaint(self, event):
  502. """Draw PseudoDC's to buffered paint DC
  503. If self.redrawAll is False on self.pdcTmp content is re-drawn
  504. """
  505. Debug.msg(5, "BufferedWindow.OnPaint(): redrawAll=%s" % self.redrawAll)
  506. dc = wx.BufferedPaintDC(self, self._buffer)
  507. dc.Clear()
  508. # use PrepareDC to set position correctly
  509. # probably does nothing, removed from wxPython 2.9
  510. # self.PrepareDC(dc)
  511. # create a clipping rect from our position and size
  512. # and update region
  513. rgn = self.GetUpdateRegion().GetBox()
  514. if wxPythonPhoenix:
  515. dc.SetClippingRegion(rgn)
  516. else:
  517. dc.SetClippingRect(rgn)
  518. switchDraw = False
  519. if self.redrawAll is None:
  520. self.redrawAll = True
  521. switchDraw = True
  522. if self.redrawAll: # redraw pdc and pdcVector
  523. # draw to the dc using the calculated clipping rect
  524. self.pdc.DrawToDCClipped(dc, rgn)
  525. # draw vector map layer
  526. if self.digit:
  527. # decorate with GDDC (transparency)
  528. try:
  529. gcdc = wx.GCDC(dc)
  530. self.pdcVector.DrawToDCClipped(gcdc, rgn)
  531. except NotImplementedError as e:
  532. print(e, file=sys.stderr)
  533. self.pdcVector.DrawToDCClipped(dc, rgn)
  534. self.bufferLast = None
  535. else: # do not redraw pdc and pdcVector
  536. if self.bufferLast is None:
  537. # draw to the dc
  538. self.pdc.DrawToDC(dc)
  539. if self.digit:
  540. # decorate with GDDC (transparency)
  541. try:
  542. gcdc = wx.GCDC(dc)
  543. self.pdcVector.DrawToDC(gcdc)
  544. except NotImplementedError as e:
  545. print(e, file=sys.stderr)
  546. self.pdcVector.DrawToDC(dc)
  547. # store buffered image
  548. # self.bufferLast = wx.BitmapFromImage(self.buffer.ConvertToImage())
  549. self.bufferLast = dc.GetAsBitmap(
  550. Rect(0, 0, self.Map.width, self.Map.height)
  551. )
  552. self.pdc.DrawBitmap(self.bufferLast, 0, 0, False)
  553. self.pdc.DrawToDC(dc)
  554. # draw semitransparent objects (e.g. region box, raster digitizer
  555. # objects)
  556. try:
  557. gcdc = wx.GCDC(dc)
  558. self.pdcTransparent.DrawToDC(gcdc)
  559. except NotImplementedError as e:
  560. print(e, file=sys.stderr)
  561. self.pdcTransparent.DrawToDC(dc)
  562. # draw temporary object on the foreground
  563. self.pdcTmp.DrawToDC(dc)
  564. if switchDraw:
  565. self.redrawAll = False
  566. def OnSize(self, event):
  567. """Scale map image so that it is the same size as the Window"""
  568. # re-render image on idle
  569. self.resize = grass.clock()
  570. def OnIdle(self, event):
  571. """Only re-render a composite map image from GRASS during
  572. idle time instead of multiple times during resizing.
  573. """
  574. # use OnInternalIdle() instead ?
  575. if self.resize and self.resize + 0.2 < grass.clock():
  576. Debug.msg(3, "BufferedWindow.OnSize():")
  577. # set size of the input image
  578. self.Map.ChangeMapSize(self.GetClientSize())
  579. # Make new off screen bitmap: this bitmap will always have the
  580. # current drawing in it, so it can be used to save the image to
  581. # a file, or whatever.
  582. self._buffer.Destroy()
  583. self._buffer = EmptyBitmap(max(1, self.Map.width), max(1, self.Map.height))
  584. # get the image to be rendered
  585. self.img = self.GetImage()
  586. # update map display
  587. updatemap = True
  588. if (
  589. self.img and self.Map.width + self.Map.height > 0
  590. ): # scale image after resize
  591. self.img = self.img.Scale(self.Map.width, self.Map.height)
  592. if len(self.Map.GetListOfLayers()) > 0:
  593. self.UpdateMap()
  594. updatemap = False
  595. if updatemap:
  596. self.UpdateMap(render=True)
  597. self.resize = False
  598. elif self.resize:
  599. event.RequestMore()
  600. event.Skip()
  601. def SaveToFile(self, FileName, FileType, width, height, callback=None):
  602. """This draws the pseudo DC to a buffer that can be saved to
  603. a file.
  604. :param filename: file name
  605. :param FileType: type of bitmap
  606. :param width: image width
  607. :param height: image height
  608. """
  609. Debug.msg(1, "MapWindow.SaveToFile(): %s (%dx%d)", FileName, width, height)
  610. self._fileName = FileName
  611. self._fileType = FileType
  612. self._saveToFileCallback = callback
  613. self._busy = wx.BusyInfo(_("Please wait, exporting image..."), parent=self)
  614. wx.GetApp().Yield()
  615. self.Map.ChangeMapSize((width, height))
  616. renderMgr = self.Map.GetRenderMgr()
  617. # this seems wrong, rendering should have callback
  618. # when callback present, rendering does not emit signal
  619. # just calls callback
  620. renderMgr.renderDone.disconnect(self._updateMFinished)
  621. renderMgr.renderDone.connect(self._saveToFileDone)
  622. self.Map.Render(force=True, windres=self._properties.resolution)
  623. def _saveToFileDone(self, callback=None):
  624. renderMgr = self.Map.GetRenderMgr()
  625. renderMgr.renderDone.disconnect(self._saveToFileDone)
  626. ibuffer = EmptyBitmap(max(1, self.Map.width), max(1, self.Map.height))
  627. img = self.GetImage()
  628. self.pdc.RemoveAll()
  629. self.Draw(self.pdc, img, drawid=99)
  630. # compute size ratio to move overlay accordingly
  631. cSize = self.GetClientSize()
  632. ratio = float(self.Map.width) / cSize[0], float(self.Map.height) / cSize[1]
  633. # redraw legend, scalebar
  634. for img in self.GetOverlay():
  635. # draw any active and defined overlays
  636. if self.imagedict[img]["layer"].IsActive():
  637. id = self.imagedict[img]["id"]
  638. coords = int(ratio[0] * self.overlays[id].coords[0]), int(
  639. ratio[1] * self.overlays[id].coords[1]
  640. )
  641. self.Draw(
  642. self.pdc,
  643. img=img,
  644. drawid=id,
  645. pdctype=self.overlays[id].pdcType,
  646. coords=coords,
  647. )
  648. # redraw text labels
  649. for id in list(self.textdict.keys()):
  650. textinfo = self.textdict[id]
  651. oldCoords = textinfo["coords"]
  652. textinfo["coords"] = (
  653. ratio[0] * textinfo["coords"][0],
  654. ratio[1] * textinfo["coords"][1],
  655. )
  656. self.Draw(self.pdc, img=self.textdict[id], drawid=id, pdctype="text")
  657. # set back old coordinates
  658. textinfo["coords"] = oldCoords
  659. dc = wx.BufferedDC(None, ibuffer)
  660. dc.Clear()
  661. # probably does nothing, removed from wxPython 2.9
  662. # self.PrepareDC(dc)
  663. self.pdc.DrawToDC(dc)
  664. if self.digit:
  665. self.pdcVector.DrawToDC(dc)
  666. ibuffer.SaveFile(self._fileName, self._fileType)
  667. del self._busy
  668. del self._fileName
  669. del self._fileType
  670. renderMgr.renderDone.connect(self._updateMFinished)
  671. self.UpdateMap(render=True)
  672. self.Refresh()
  673. if self._saveToFileCallback:
  674. self._saveToFileCallback()
  675. def GetOverlay(self):
  676. """Converts rendered overlay files to wx.Image
  677. Updates self.imagedict
  678. :return: list of images
  679. """
  680. imgs = []
  681. for overlay in self.Map.GetListOfLayers(ltype="overlay", active=True):
  682. if (
  683. overlay.mapfile is not None
  684. and os.path.isfile(overlay.mapfile)
  685. and os.path.getsize(overlay.mapfile)
  686. ):
  687. img = utils.autoCropImageFromFile(overlay.mapfile)
  688. for key in list(self.imagedict.keys()):
  689. if self.imagedict[key]["id"] == overlay.id:
  690. del self.imagedict[key]
  691. self.imagedict[img] = {"id": overlay.id, "layer": overlay}
  692. imgs.append(img)
  693. return imgs
  694. def GetImage(self):
  695. """Converts redered map files to wx.Image
  696. Updates self.imagedict (id=99)
  697. :return: wx.Image instance (map composition)
  698. """
  699. imgId = 99
  700. if (
  701. self.Map.mapfile
  702. and os.path.isfile(self.Map.mapfile)
  703. and os.path.getsize(self.Map.mapfile)
  704. ):
  705. img = wx.Image(self.Map.mapfile, wx.BITMAP_TYPE_ANY)
  706. else:
  707. img = None
  708. for key in list(self.imagedict.keys()):
  709. if self.imagedict[key]["id"] == imgId:
  710. del self.imagedict[key]
  711. self.imagedict[img] = {"id": imgId}
  712. return img
  713. def SetAlwaysRenderEnabled(self, alwaysRender=True):
  714. self.alwaysRender = alwaysRender
  715. def IsAlwaysRenderEnabled(self):
  716. return self.alwaysRender
  717. def UpdateMap(self, render=True, renderVector=True, delay=0.0):
  718. """Updates the canvas anytime there is a change to the
  719. underlaying images or to the geometry of the canvas.
  720. This method should not be called directly.
  721. .. todo::
  722. change direct calling of UpdateMap method to emitting grass
  723. interface updateMap signal
  724. .. todo::
  725. consider using strong/weak signal instead of delay limit in
  726. giface
  727. :param render: re-render map composition
  728. :param renderVector: re-render vector map layer enabled for editing (used for digitizer)
  729. :param delay: defines time threshold in seconds for postponing
  730. rendering to merge more update requests.
  731. If another request comes within the limit, rendering is delayed
  732. again. Next delay limit is chosen according to the smallest
  733. delay value of all requests which have come during waiting period.
  734. Let say that first UpdateMap request come with 5 second delay
  735. limit. After 4 seconds of waiting another UpdateMap request
  736. come with delay limit of 2.5 seconds. New waiting period is set
  737. to 2.5 seconds, because limit of the second request is the
  738. smallest. If no other request comes rendering will be done
  739. after 6.5 seconds from the first request.
  740. Arguments 'render' and 'renderVector' have priority for True.
  741. It means that if more UpdateMap requests come within waiting
  742. period and at least one request has argument set for True, map
  743. will be updated with the True value of the argument.
  744. """
  745. if self.timerRunId is None or delay < self.updDelay:
  746. self.updDelay = delay
  747. if render:
  748. self.render = render
  749. if renderVector:
  750. self.renderVector = renderVector
  751. updTime = time.time()
  752. self.lastUpdateMapReq = updTime
  753. if self.updDelay < 0.0:
  754. self._runUpdateMap()
  755. else:
  756. self.timerRunId = self.renderTimingThr.GetId()
  757. self.renderTimingThr.Run(
  758. callable=self._timingFunction,
  759. ondone=self._onUpdateMap,
  760. pid=self.timerRunId,
  761. )
  762. def _timingFunction(self, pid):
  763. """Timer measuring elapsed time, since last update request.
  764. It terminates, when delay limit is exceeded.
  765. :param pid: id which defines whether it is newest timer, or
  766. there is another one (representing newer Update map
  767. request). If it is not the newest, it is terminated.
  768. """
  769. while True:
  770. updTime = time.time()
  771. time.sleep(0.01)
  772. if (
  773. updTime > self.lastUpdateMapReq + self.updDelay
  774. or pid != self.timerRunId
  775. ):
  776. return
  777. def _onUpdateMap(self, event):
  778. if self and self.timerRunId == event.pid:
  779. self._runUpdateMap()
  780. def _runUpdateMap(self):
  781. """Update map when delay limit is over."""
  782. self.timerRunId = None
  783. self._updateM(self.render, self.renderVector)
  784. self.render = self.renderVector = False
  785. def _updateM(self, render=True, renderVector=True):
  786. """
  787. :func:`UpdateMap` for arguments description.
  788. """
  789. Debug.msg(
  790. 1,
  791. "BufferedWindow.UpdateMap(): started "
  792. "(render=%s, renderVector=%s)" % (render, renderVector),
  793. )
  794. # was if self.Map.cmdfile and ...
  795. if self.IsAlwaysRenderEnabled() and self.img is None:
  796. render = True
  797. try:
  798. if render:
  799. # update display size
  800. self.Map.ChangeMapSize(self.GetClientSize())
  801. self.Map.Render(force=render, windres=self._properties.resolution)
  802. except GException as e:
  803. GError(message=e.value)
  804. def _updateMFinished(self, renderVector=True):
  805. Debug.msg(1, "BufferedWindow.UpdateMap(): finished")
  806. self.img = self.GetImage() # id=99
  807. #
  808. # clear pseudoDcs
  809. #
  810. for pdc in (self.pdc, self.pdcTransparent, self.pdcTmp):
  811. pdc.Clear()
  812. pdc.RemoveAll()
  813. #
  814. # draw background map image to PseudoDC
  815. #
  816. if not self.img:
  817. self.Draw(self.pdc, pdctype="clear")
  818. else:
  819. try:
  820. id = self.imagedict[self.img]["id"]
  821. except Exception as e:
  822. Debug.mgs(1, "UpdateMap() failed: %s", e)
  823. return False
  824. self.Draw(self.pdc, self.img, drawid=id)
  825. #
  826. # render vector map layer
  827. #
  828. if renderVector and self.digit:
  829. self._updateMap()
  830. #
  831. # render overlays
  832. #
  833. for img in self.GetOverlay():
  834. # draw any active and defined overlays
  835. if self.imagedict[img]["layer"].IsActive():
  836. id = self.imagedict[img]["id"]
  837. self.Draw(
  838. self.pdc,
  839. img=img,
  840. drawid=id,
  841. pdctype=self.overlays[id].pdcType,
  842. coords=self.overlays[id].coords,
  843. )
  844. for id in list(self.textdict.keys()):
  845. self.Draw(
  846. self.pdc,
  847. img=self.textdict[id],
  848. drawid=id,
  849. pdctype="text",
  850. coords=[10, 10, 10, 10],
  851. )
  852. # optionally draw computational extent box
  853. self.DrawCompRegionExtent()
  854. #
  855. # redraw pdcTmp if needed
  856. #
  857. # draw registered graphics
  858. if len(self.graphicsSetList) > 0:
  859. penOrig = self.pen
  860. polypenOrig = self.polypen
  861. for item in self.graphicsSetList:
  862. try:
  863. item.Draw()
  864. except:
  865. GError(
  866. parent=self,
  867. message=_(
  868. "Unable to draw registered graphics. "
  869. "The graphics was unregistered."
  870. ),
  871. )
  872. self.UnregisterGraphicsToDraw(item)
  873. self.pen = penOrig
  874. self.polypen = polypenOrig
  875. if len(self.polycoords) > 0:
  876. self.DrawLines(self.pdcTmp)
  877. return True
  878. def DrawCompRegionExtent(self):
  879. """Draw computational region extent in the display
  880. Display region is drawn as a blue box inside the computational region,
  881. computational region inside a display region as a red box).
  882. """
  883. if self._properties.showRegion:
  884. compReg = self.Map.GetRegion()
  885. dispReg = self.Map.GetCurrentRegion()
  886. reg = dispReg if utils.isInRegion(dispReg, compReg) else compReg
  887. regionCoords = []
  888. regionCoords.append((reg["w"], reg["n"]))
  889. regionCoords.append((reg["e"], reg["n"]))
  890. regionCoords.append((reg["e"], reg["s"]))
  891. regionCoords.append((reg["w"], reg["s"]))
  892. regionCoords.append((reg["w"], reg["n"]))
  893. # draw region extent
  894. self.polypen = wx.Pen(
  895. colour=wx.Colour(255, 0, 0, 128), width=3, style=wx.SOLID
  896. )
  897. self.DrawLines(pdc=self.pdcTransparent, polycoords=regionCoords)
  898. def EraseMap(self):
  899. """Erase map canvas"""
  900. self.Draw(self.pdc, pdctype="clear")
  901. if self.digit:
  902. self.Draw(self.pdcVector, pdctype="clear")
  903. self.Draw(self.pdcTransparent, pdctype="clear")
  904. self.Draw(self.pdcTmp, pdctype="clear")
  905. self.Map.AbortAllThreads()
  906. def DragMap(self, moveto):
  907. """Drag the entire map image for panning.
  908. :param moveto: dx,dy
  909. """
  910. dc = wx.BufferedDC(wx.ClientDC(self))
  911. dc.SetBackground(wx.Brush("White"))
  912. dc.Clear()
  913. self.dragimg = DragImage(self._buffer)
  914. self.dragimg.BeginDrag((0, 0), self)
  915. self.dragimg.GetImageRect(moveto)
  916. self.dragimg.Move(moveto)
  917. self.dragimg.DoDrawImage(dc, moveto)
  918. self.dragimg.EndDrag()
  919. def DragItem(self, id, coords):
  920. """Drag an overlay decoration item"""
  921. if id == 99 or id == "" or id is None:
  922. return
  923. Debug.msg(5, "BufferedWindow.DragItem(): id=%d" % id)
  924. x, y = self.lastpos
  925. dx = coords[0] - x
  926. dy = coords[1] - y
  927. self.pdc.SetBackground(wx.Brush(self.GetBackgroundColour()))
  928. r = self.pdc.GetIdBounds(id)
  929. if isinstance(r, list):
  930. r = Rect(r[0], r[1], r[2], r[3])
  931. if id in self.textdict: # text dragging
  932. rtop = (r[0], r[1] - r[3], r[2], r[3])
  933. r = r.Union(rtop)
  934. rleft = (r[0] - r[2], r[1], r[2], r[3])
  935. r = r.Union(rleft)
  936. self.pdc.TranslateId(id, dx, dy)
  937. r2 = self.pdc.GetIdBounds(id)
  938. if isinstance(r2, list):
  939. r2 = Rect(r[0], r[1], r[2], r[3])
  940. if id in self.textdict: # text
  941. self.textdict[id]["bbox"] = r2
  942. self.textdict[id]["coords"][0] += dx
  943. self.textdict[id]["coords"][1] += dy
  944. r = r.Union(r2)
  945. r.Inflate(4, 4)
  946. self.RefreshRect(r, False)
  947. self.lastpos = (coords[0], coords[1])
  948. def MouseDraw(self, pdc=None, begin=None, end=None):
  949. """Mouse box or line from 'begin' to 'end'
  950. If not given from self.mouse['begin'] to self.mouse['end'].
  951. """
  952. if not pdc:
  953. return
  954. if begin is None:
  955. begin = self.mouse["begin"]
  956. if end is None:
  957. end = self.mouse["end"]
  958. Debug.msg(
  959. 5,
  960. "BufferedWindow.MouseDraw(): use=%s, box=%s, begin=%f,%f, end=%f,%f"
  961. % (
  962. self.mouse["use"],
  963. self.mouse["box"],
  964. begin[0],
  965. begin[1],
  966. end[0],
  967. end[1],
  968. ),
  969. )
  970. if self.mouse["box"] == "box":
  971. boxid = wx.ID_NEW
  972. mousecoords = [begin[0], begin[1], end[0], end[1]]
  973. r = pdc.GetIdBounds(boxid)
  974. if isinstance(r, list):
  975. r = Rect(r[0], r[1], r[2], r[3])
  976. r.Inflate(4, 4)
  977. try:
  978. pdc.ClearId(boxid)
  979. except:
  980. pass
  981. self.RefreshRect(r, False)
  982. pdc.SetId(boxid)
  983. self.Draw(pdc, drawid=boxid, pdctype="box", coords=mousecoords)
  984. elif self.mouse["box"] == "line":
  985. self.lineid = wx.ID_NEW
  986. mousecoords = [begin[0], begin[1], end[0], end[1]]
  987. x1 = min(begin[0], end[0])
  988. x2 = max(begin[0], end[0])
  989. y1 = min(begin[1], end[1])
  990. y2 = max(begin[1], end[1])
  991. r = Rect(x1, y1, x2 - x1, y2 - y1)
  992. r.Inflate(4, 4)
  993. try:
  994. pdc.ClearId(self.lineid)
  995. except:
  996. pass
  997. self.RefreshRect(r, False)
  998. pdc.SetId(self.lineid)
  999. self.Draw(pdc, drawid=self.lineid, pdctype="line", coords=mousecoords)
  1000. def DrawLines(self, pdc=None, polycoords=None):
  1001. """Draw polyline in PseudoDC
  1002. Set self.pline to wx.NEW_ID + 1
  1003. :param polycoords: list of polyline vertices, geographical
  1004. coordinates (if not given, self.polycoords
  1005. is used)
  1006. """
  1007. if not pdc:
  1008. pdc = self.pdcTmp
  1009. if not polycoords:
  1010. polycoords = self.polycoords
  1011. if len(polycoords) > 0:
  1012. self.plineid = wx.ID_NEW + 1
  1013. # convert from EN to XY
  1014. coords = []
  1015. for p in polycoords:
  1016. coords.append(self.Cell2Pixel(p))
  1017. self.Draw(pdc, drawid=self.plineid, pdctype="polyline", coords=coords)
  1018. Debug.msg(
  1019. 4,
  1020. "BufferedWindow.DrawLines(): coords=%s, id=%s" % (coords, self.plineid),
  1021. )
  1022. return self.plineid
  1023. return -1
  1024. def DrawPolylines(self, pdc, coords, pen, drawid=None):
  1025. """Draw polyline in PseudoDC.
  1026. This is similar to DrawLines but this is used with GraphicsSet,
  1027. coordinates should be always in pixels.
  1028. :param pdc: PseudoDC
  1029. :param coords: list of coordinates (pixel coordinates)
  1030. :param pen: pen to be used
  1031. :param drawid: id of the drawn object (used by PseudoDC)
  1032. """
  1033. Debug.msg(4, "BufferedWindow.DrawPolylines(): coords=%s" % coords)
  1034. self.lineId = self.Draw(
  1035. pdc, drawid=None, pdctype="polyline", coords=coords, pen=pen
  1036. )
  1037. return self.lineid
  1038. def DrawCross(
  1039. self,
  1040. pdc,
  1041. coords,
  1042. size,
  1043. rotation=0,
  1044. pen=None,
  1045. text=None,
  1046. textAlign="lr",
  1047. textOffset=(5, 5),
  1048. drawid=None,
  1049. ):
  1050. """Draw cross in PseudoDC
  1051. .. todo::
  1052. implement rotation
  1053. :param pdc: PseudoDC
  1054. :param coords: center coordinates (pixel coordinates)
  1055. :param rotation: rotate symbol
  1056. :param text: draw also text (text, font, color, rotation)
  1057. :param textAlign: alignment (default 'lower-right')
  1058. :param textOffset: offset for text (from center point)
  1059. :param drawid: id of the drawn object (used by PseudoDC)
  1060. """
  1061. Debug.msg(
  1062. 4,
  1063. "BufferedWindow.DrawCross(): pdc=%s, coords=%s, size=%d"
  1064. % (pdc, coords, size),
  1065. )
  1066. coordsCross = (
  1067. (coords[0], coords[1] - size, coords[0], coords[1] + size),
  1068. (coords[0] - size, coords[1], coords[0] + size, coords[1]),
  1069. )
  1070. self.lineid = self.Draw(
  1071. pdc, drawid=drawid, pdctype="lines", coords=coordsCross, pen=pen
  1072. )
  1073. if not text:
  1074. return self.lineid
  1075. if textAlign == "ul":
  1076. coord = [coords[0] - textOffset[0], coords[1] - textOffset[1], 0, 0]
  1077. elif textAlign == "ur":
  1078. coord = [coords[0] + textOffset[0], coords[1] - textOffset[1], 0, 0]
  1079. elif textAlign == "lr":
  1080. coord = [coords[0] + textOffset[0], coords[1] + textOffset[1], 0, 0]
  1081. else:
  1082. coord = [coords[0] - textOffset[0], coords[1] + textOffset[1], 0, 0]
  1083. self.Draw(pdc, img=text, pdctype="text", coords=coord, pen=pen)
  1084. return self.lineid
  1085. def DrawRectangle(self, pdc, point1, point2, pen, brush=None, drawid=None):
  1086. """Draw rectangle (not filled) in PseudoDC
  1087. :param pdc: PseudoDC
  1088. :param point1: top left corner (pixel coordinates)
  1089. :param point2: bottom right corner (pixel coordinates)
  1090. :param pen: pen
  1091. :param drawid: id of the drawn object (used by PseudoDC)
  1092. """
  1093. Debug.msg(
  1094. 4,
  1095. "BufferedWindow.DrawRectangle(): pdc=%s, point1=%s, point2=%s"
  1096. % (pdc, point1, point2),
  1097. )
  1098. coords = [point1[0], point1[1], point2[0], point2[1]]
  1099. self.lineid = self.Draw(
  1100. pdc, drawid=drawid, pdctype="box", coords=coords, pen=pen, brush=brush
  1101. )
  1102. return self.lineid
  1103. def DrawCircle(self, pdc, coords, radius, pen, brush=None, drawid=None):
  1104. """Draw circle (not filled) in PseudoDC
  1105. :param pdc: PseudoDC
  1106. :param coords: center (pixel coordinates)
  1107. :param radius: radius
  1108. :param pen: pen
  1109. :param drawid: id of the drawn object (used by PseudoDC)
  1110. """
  1111. Debug.msg(
  1112. 4,
  1113. "BufferedWindow.DrawCircle(): pdc=%s, coords=%s, radius=%s"
  1114. % (pdc, coords, radius),
  1115. )
  1116. newcoords = [
  1117. coords[0] - radius,
  1118. coords[1] - radius,
  1119. coords[0] + radius,
  1120. coords[1] + radius,
  1121. ]
  1122. self.lineid = self.Draw(
  1123. pdc, drawid=drawid, pdctype="circle", coords=newcoords, pen=pen, brush=brush
  1124. )
  1125. return self.lineid
  1126. def DrawPolygon(self, pdc, coords, pen, brush=None, drawid=None):
  1127. """Draws polygon from a list of points (do not append the first point)
  1128. :param pdc: PseudoDC
  1129. :param coords: list of coordinates (pixel coordinates)
  1130. :param pen: pen
  1131. :param drawid: id of the drawn object (used by PseudoDC)
  1132. """
  1133. # avid wx.GCDC assert
  1134. if len(coords) <= 1:
  1135. return None
  1136. self.lineid = self.Draw(
  1137. pdc, drawid=drawid, pdctype="polygon", coords=coords, pen=pen, brush=brush
  1138. )
  1139. return self.lineid
  1140. def _computeZoomToPointAndRecenter(self, position, zoomtype):
  1141. """Computes zoom parameters for recenter mode.
  1142. Computes begin and end parameters for Zoom() method.
  1143. Used for zooming by single click (not box)
  1144. and mouse wheel zooming (zoom and recenter mode).
  1145. """
  1146. if zoomtype > 0:
  1147. begin = (
  1148. position[0] - self.Map.width / 4,
  1149. position[1] - self.Map.height / 4,
  1150. )
  1151. end = (position[0] + self.Map.width / 4, position[1] + self.Map.height / 4)
  1152. else:
  1153. begin = (
  1154. (self.Map.width - position[0]) / 2,
  1155. (self.Map.height - position[1]) / 2,
  1156. )
  1157. end = (begin[0] + self.Map.width / 2, begin[1] + self.Map.height / 2)
  1158. return begin, end
  1159. def MouseActions(self, event):
  1160. """Mouse motion and button click notifier"""
  1161. if not self.processMouse:
  1162. return
  1163. # zoom with mouse wheel
  1164. if event.GetWheelRotation() != 0:
  1165. self.OnMouseWheel(event)
  1166. # left mouse button pressed
  1167. elif event.LeftDown():
  1168. self.OnLeftDown(event)
  1169. # left mouse button released
  1170. elif event.LeftUp():
  1171. self.OnLeftUp(event)
  1172. # dragging
  1173. elif event.Dragging():
  1174. self.OnDragging(event)
  1175. # double click
  1176. elif event.ButtonDClick():
  1177. self.OnButtonDClick(event)
  1178. # middle mouse button pressed
  1179. elif event.MiddleDown():
  1180. self.OnMiddleDown(event)
  1181. # middle mouse button relesed
  1182. elif event.MiddleUp():
  1183. self.OnMiddleUp(event)
  1184. # right mouse button pressed
  1185. elif event.RightDown():
  1186. self.OnRightDown(event)
  1187. # right mouse button released
  1188. elif event.RightUp():
  1189. self.OnRightUp(event)
  1190. elif event.Entering():
  1191. self.OnMouseEnter(event)
  1192. elif event.Moving():
  1193. pixelCoordinates = event.GetPosition()
  1194. coordinates = self.Pixel2Cell(pixelCoordinates)
  1195. self.mouseMoving.emit(x=coordinates[0], y=coordinates[1])
  1196. self.OnMouseMoving(event)
  1197. def OnMouseWheel(self, event):
  1198. """Mouse wheel moved"""
  1199. zoomBehaviour = UserSettings.Get(
  1200. group="display", key="mouseWheelZoom", subkey="selection"
  1201. )
  1202. if zoomBehaviour == 2:
  1203. event.Skip()
  1204. return
  1205. self.processMouse = False
  1206. current = event.GetPosition()
  1207. wheel = event.GetWheelRotation()
  1208. Debug.msg(5, "BufferedWindow.MouseAction(): wheel=%d" % wheel)
  1209. if wheel > 0:
  1210. zoomtype = 1
  1211. else:
  1212. zoomtype = -1
  1213. if UserSettings.Get(group="display", key="scrollDirection", subkey="selection"):
  1214. zoomtype *= -1
  1215. # zoom 1/2 of the screen (TODO: settings)
  1216. if zoomBehaviour == 0: # zoom and recenter
  1217. begin, end = self._computeZoomToPointAndRecenter(
  1218. position=current, zoomtype=zoomtype
  1219. )
  1220. elif zoomBehaviour == 1: # zoom to current cursor position
  1221. begin = (current[0] / 2, current[1] / 2)
  1222. end = (
  1223. (self.Map.width - current[0]) / 2 + current[0],
  1224. (self.Map.height - current[1]) / 2 + current[1],
  1225. )
  1226. # zoom
  1227. self.Zoom(begin, end, zoomtype)
  1228. # redraw map
  1229. self.UpdateMap(delay=0.2)
  1230. self.Refresh()
  1231. self.processMouse = True
  1232. def OnDragging(self, event):
  1233. """Mouse dragging"""
  1234. Debug.msg(5, "BufferedWindow.MouseAction(): Dragging")
  1235. current = event.GetPosition()
  1236. previous = self.mouse["begin"]
  1237. move = (current[0] - previous[0], current[1] - previous[1])
  1238. if self.digit:
  1239. digitToolbar = self.toolbar
  1240. else:
  1241. digitToolbar = None
  1242. # dragging or drawing box with left button
  1243. if self.mouse["use"] == "pan" or event.MiddleIsDown():
  1244. self.DragMap(move)
  1245. # dragging decoration overlay item
  1246. elif (
  1247. self.mouse["use"] == "pointer"
  1248. and not digitToolbar
  1249. and self.dragid is not None
  1250. ):
  1251. coords = event.GetPosition()
  1252. self.DragItem(self.dragid, coords)
  1253. # dragging anything else - rubber band box or line
  1254. else:
  1255. if self.mouse["use"] == "pointer" and not digitToolbar:
  1256. return
  1257. self.mouse["end"] = event.GetPosition()
  1258. if event.LeftIsDown() and not (
  1259. digitToolbar
  1260. and digitToolbar.GetAction() in ("moveLine",)
  1261. and len(self.digit.GetDisplay().GetSelected()) > 0
  1262. ):
  1263. self.MouseDraw(pdc=self.pdcTmp)
  1264. def OnLeftDown(self, event):
  1265. """Left mouse button pressed"""
  1266. Debug.msg(5, "BufferedWindow.OnLeftDown(): use=%s" % self.mouse["use"])
  1267. self.mouse["begin"] = event.GetPosition()
  1268. # vector digizer
  1269. if self.mouse["use"] == "pointer" and self.digit:
  1270. if event.ControlDown():
  1271. self.OnLeftDownUndo(event)
  1272. else:
  1273. self._onLeftDown(event)
  1274. elif self.mouse["use"] == "pointer":
  1275. # get decoration or text id
  1276. idlist = []
  1277. self.dragid = ""
  1278. self.lastpos = self.mouse["begin"]
  1279. idlist = self.pdc.FindObjects(
  1280. self.lastpos[0], self.lastpos[1], self.hitradius
  1281. )
  1282. if 99 in idlist:
  1283. idlist.remove(99)
  1284. if idlist != []:
  1285. self.dragid = idlist[0] # drag whatever is on top
  1286. else:
  1287. pass
  1288. coords = self.Pixel2Cell(self.mouse["begin"])
  1289. self.mouseLeftDown.emit(x=coords[0], y=coords[1])
  1290. event.Skip()
  1291. def OnLeftUp(self, event):
  1292. """Left mouse button released
  1293. Emits mapQueried signal when mouse use is 'query'.
  1294. """
  1295. Debug.msg(5, "BufferedWindow.OnLeftUp(): use=%s" % self.mouse["use"])
  1296. self.mouse["end"] = event.GetPosition()
  1297. coordinates = self.Pixel2Cell(self.mouse["end"])
  1298. if self.mouse["use"] in ["zoom", "pan"]:
  1299. # set region in zoom or pan
  1300. begin = self.mouse["begin"]
  1301. end = self.mouse["end"]
  1302. if self.mouse["use"] == "zoom":
  1303. # set region for click (zero-width box)
  1304. if begin[0] - end[0] == 0 or begin[1] - end[1] == 0:
  1305. begin, end = self._computeZoomToPointAndRecenter(
  1306. position=end, zoomtype=self.zoomtype
  1307. )
  1308. self.Zoom(begin, end, self.zoomtype)
  1309. # redraw map
  1310. self.UpdateMap(render=True)
  1311. elif self.mouse["use"] == "query":
  1312. self.mapQueried.emit(x=self.mouse["end"][0], y=self.mouse["end"][1])
  1313. elif self.mouse["use"] == "pointer" and self.digit:
  1314. self._onLeftUp(event)
  1315. elif self.mouse["use"] == "pointer":
  1316. if self.dragid:
  1317. # end drag of overlay decoration
  1318. if self.overlays and self.dragid in self.overlays:
  1319. self.overlays[self.dragid].coords = self.pdc.GetIdBounds(
  1320. self.dragid
  1321. )
  1322. elif self.dragid in self.textdict:
  1323. self.textdict[self.dragid]["bbox"] = self.pdc.GetIdBounds(
  1324. self.dragid
  1325. )
  1326. else:
  1327. pass
  1328. self.dragid = None
  1329. self.mouseLeftUpPointer.emit(x=coordinates[0], y=coordinates[1])
  1330. elif self.mouse["use"] == "drawRegion":
  1331. coordinatesBegin = self.Pixel2Cell(self.mouse["begin"])
  1332. if coordinatesBegin[0] < coordinates[0]:
  1333. west = coordinatesBegin[0]
  1334. east = coordinates[0]
  1335. else:
  1336. west = coordinates[0]
  1337. east = coordinatesBegin[0]
  1338. if coordinatesBegin[1] < coordinates[1]:
  1339. south = coordinatesBegin[1]
  1340. north = coordinates[1]
  1341. else:
  1342. south = coordinates[1]
  1343. north = coordinatesBegin[1]
  1344. region = self.Map.GetRegion()
  1345. RunCommand(
  1346. "g.region",
  1347. parent=self,
  1348. flags="a",
  1349. nsres=region["nsres"],
  1350. ewres=region["ewres"],
  1351. n=north,
  1352. s=south,
  1353. e=east,
  1354. w=west,
  1355. )
  1356. # redraw map
  1357. self.UpdateMap(render=False)
  1358. # TODO: decide which coordinates to send (e, n, mouse['begin'],
  1359. # mouse['end'])
  1360. self.mouseLeftUp.emit(x=coordinates[0], y=coordinates[1])
  1361. def OnButtonDClick(self, event):
  1362. """Mouse button double click"""
  1363. Debug.msg(5, "BufferedWindow.OnButtonDClick(): use=%s" % self.mouse["use"])
  1364. screenCoords = event.GetPosition()
  1365. if self.mouse["use"] == "pointer":
  1366. # select overlay decoration options dialog
  1367. idlist = self.pdc.FindObjects(
  1368. screenCoords[0], screenCoords[1], self.hitradius
  1369. )
  1370. if idlist and idlist[0] != 99:
  1371. self.dragid = idlist[0]
  1372. self.overlayActivated.emit(overlayId=self.dragid)
  1373. coords = self.Pixel2Cell(screenCoords)
  1374. self.mouseDClick.emit(x=coords[0], y=coords[1])
  1375. def OnRightDown(self, event):
  1376. """Right mouse button pressed"""
  1377. Debug.msg(5, "BufferedWindow.OnRightDown(): use=%s" % self.mouse["use"])
  1378. if self.digit:
  1379. self._onRightDown(event)
  1380. event.Skip()
  1381. def OnRightUp(self, event):
  1382. """Right mouse button released"""
  1383. Debug.msg(5, "BufferedWindow.OnRightUp(): use=%s" % self.mouse["use"])
  1384. if self.digit:
  1385. self._onRightUp(event)
  1386. self.redrawAll = True
  1387. self.Refresh()
  1388. coords = self.Pixel2Cell(event.GetPosition())
  1389. self.mouseRightUp.emit(x=coords[0], y=coords[1])
  1390. event.Skip()
  1391. def OnMiddleDown(self, event):
  1392. """Middle mouse button pressed"""
  1393. if not event:
  1394. return
  1395. self.mouse["begin"] = event.GetPosition()
  1396. def OnMiddleUp(self, event):
  1397. """Middle mouse button released"""
  1398. self.mouse["end"] = event.GetPosition()
  1399. # set region in zoom or pan
  1400. begin = self.mouse["begin"]
  1401. end = self.mouse["end"]
  1402. self.Zoom(begin, end, 0) # no zoom
  1403. # redraw map
  1404. self.UpdateMap(render=True)
  1405. def OnMouseEnter(self, event):
  1406. """Mouse entered window and no mouse buttons were pressed
  1407. Emits the mouseEntered signal.
  1408. """
  1409. self.mouseEntered.emit()
  1410. event.Skip()
  1411. def OnMouseMoving(self, event):
  1412. """Motion event and no mouse buttons were pressed"""
  1413. if self.mouse["use"] == "pointer" and self.digit:
  1414. self._onMouseMoving(event)
  1415. pos = event.GetPosition()
  1416. idlist = self.pdc.FindObjects(pos[0], pos[1], self.hitradius)
  1417. if (
  1418. self.overlays
  1419. and idlist
  1420. and [i for i in idlist if i in list(self.overlays.keys())]
  1421. ): # legend, scale bar, north arrow, dtext
  1422. self.SetToolTip("Right click to modify or remove")
  1423. else:
  1424. self.SetToolTip(None)
  1425. event.Skip()
  1426. def OnCopyCoordinates(self, event):
  1427. """Copy coordinates to cliboard"""
  1428. e, n = self.GetLastEN()
  1429. if wx.TheClipboard.Open():
  1430. do = wx.TextDataObject()
  1431. # TODO: put delimiter in settings and apply also for Go to in
  1432. # statusbar
  1433. delim = ","
  1434. do.SetText(str(e) + delim + str(n))
  1435. wx.TheClipboard.SetData(do)
  1436. wx.TheClipboard.Close()
  1437. def OnShowStatusbar(self, event):
  1438. """Show/hide statusbar"""
  1439. self._giface.ShowStatusbar(not self._giface.IsStatusbarShown())
  1440. def OnShowAllToolbars(self, event):
  1441. """Show/hide all toolbars"""
  1442. self._giface.ShowAllToolbars(not self._giface.AreAllToolbarsShown())
  1443. def ClearLines(self, pdc=None):
  1444. """Clears temporary drawn lines from PseudoDC"""
  1445. if not pdc:
  1446. pdc = self.pdcTmp
  1447. try:
  1448. pdc.ClearId(self.lineid)
  1449. pdc.RemoveId(self.lineid)
  1450. except:
  1451. pass
  1452. try:
  1453. pdc.ClearId(self.plineid)
  1454. pdc.RemoveId(self.plineid)
  1455. except:
  1456. pass
  1457. Debug.msg(
  1458. 4,
  1459. "BufferedWindow.ClearLines(): lineid=%s, plineid=%s"
  1460. % (self.lineid, self.plineid),
  1461. )
  1462. return True
  1463. def Pixel2Cell(self, xyCoords):
  1464. """Convert image coordinates to real word coordinates
  1465. :param xyCoords: image coordinates
  1466. :return: easting, northing
  1467. :return: None on error
  1468. """
  1469. try:
  1470. x = int(xyCoords[0])
  1471. y = int(xyCoords[1])
  1472. except:
  1473. return None
  1474. if self.Map.region["ewres"] > self.Map.region["nsres"]:
  1475. res = self.Map.region["ewres"]
  1476. else:
  1477. res = self.Map.region["nsres"]
  1478. w = self.Map.region["center_easting"] - (self.Map.width / 2) * res
  1479. n = self.Map.region["center_northing"] + (self.Map.height / 2) * res
  1480. east = w + x * res
  1481. north = n - y * res
  1482. return (east, north)
  1483. def Cell2Pixel(self, enCoords):
  1484. """Convert real word coordinates to image coordinates"""
  1485. try:
  1486. east = float(enCoords[0])
  1487. north = float(enCoords[1])
  1488. except:
  1489. return None
  1490. if self.Map.region["ewres"] > self.Map.region["nsres"]:
  1491. res = self.Map.region["ewres"]
  1492. else:
  1493. res = self.Map.region["nsres"]
  1494. w = self.Map.region["center_easting"] - (self.Map.width / 2) * res
  1495. n = self.Map.region["center_northing"] + (self.Map.height / 2) * res
  1496. x = (east - w) / res
  1497. y = (n - north) / res
  1498. return (x, y)
  1499. def Zoom(self, begin, end, zoomtype):
  1500. """Calculates new region while (un)zoom/pan-ing"""
  1501. x1, y1 = begin
  1502. x2, y2 = end
  1503. newreg = {}
  1504. # threshold - too small squares do not make sense
  1505. # can only zoom to windows of > 5x5 screen pixels
  1506. if abs(x2 - x1) > 5 and abs(y2 - y1) > 5 and zoomtype != 0:
  1507. if x1 > x2:
  1508. x1, x2 = x2, x1
  1509. if y1 > y2:
  1510. y1, y2 = y2, y1
  1511. # zoom in
  1512. if zoomtype > 0:
  1513. newreg["w"], newreg["n"] = self.Pixel2Cell((x1, y1))
  1514. newreg["e"], newreg["s"] = self.Pixel2Cell((x2, y2))
  1515. # zoom out
  1516. elif zoomtype < 0:
  1517. newreg["w"], newreg["n"] = self.Pixel2Cell((-x1 * 2, -y1 * 2))
  1518. newreg["e"], newreg["s"] = self.Pixel2Cell(
  1519. (
  1520. self.Map.width + 2 * (self.Map.width - x2),
  1521. self.Map.height + 2 * (self.Map.height - y2),
  1522. )
  1523. )
  1524. # pan
  1525. elif zoomtype == 0:
  1526. dx = x1 - x2
  1527. dy = y1 - y2
  1528. if dx == 0 and dy == 0:
  1529. dx = x1 - self.Map.width / 2
  1530. dy = y1 - self.Map.height / 2
  1531. newreg["w"], newreg["n"] = self.Pixel2Cell((dx, dy))
  1532. newreg["e"], newreg["s"] = self.Pixel2Cell(
  1533. (self.Map.width + dx, self.Map.height + dy)
  1534. )
  1535. # if new region has been calculated, set the values
  1536. if newreg != {}:
  1537. # LL locations
  1538. if self.Map.projinfo["proj"] == "ll":
  1539. self.Map.region["n"] = min(self.Map.region["n"], 90.0)
  1540. self.Map.region["s"] = max(self.Map.region["s"], -90.0)
  1541. ce = newreg["w"] + (newreg["e"] - newreg["w"]) / 2
  1542. cn = newreg["s"] + (newreg["n"] - newreg["s"]) / 2
  1543. # calculate new center point and display resolution
  1544. self.Map.region["center_easting"] = ce
  1545. self.Map.region["center_northing"] = cn
  1546. self.Map.region["ewres"] = (newreg["e"] - newreg["w"]) / self.Map.width
  1547. self.Map.region["nsres"] = (newreg["n"] - newreg["s"]) / self.Map.height
  1548. if self._properties.alignExtent:
  1549. self.Map.AlignExtentFromDisplay()
  1550. else:
  1551. for k in ("n", "s", "e", "w"):
  1552. self.Map.region[k] = newreg[k]
  1553. if self.digit and hasattr(self, "moveInfo"):
  1554. self._zoom(None)
  1555. self.ZoomHistory(
  1556. self.Map.region["n"],
  1557. self.Map.region["s"],
  1558. self.Map.region["e"],
  1559. self.Map.region["w"],
  1560. )
  1561. if self.redrawAll is False:
  1562. self.redrawAll = True
  1563. def ZoomBack(self):
  1564. """Zoom to previous extents in zoomhistory list
  1565. Emits zoomChanged signal.
  1566. Emits zoomHistoryUnavailable signal when stack is empty.
  1567. """
  1568. Debug.msg(4, "BufferedWindow.ZoomBack(): hist)=%s" % self.zoomhistory)
  1569. zoom = list()
  1570. if len(self.zoomhistory) > 1:
  1571. self.zoomhistory.pop()
  1572. zoom = self.zoomhistory[-1]
  1573. if len(self.zoomhistory) < 2:
  1574. self.zoomHistoryUnavailable.emit()
  1575. # zoom to selected region
  1576. self.Map.GetRegion(n=zoom[0], s=zoom[1], e=zoom[2], w=zoom[3], update=True)
  1577. # update map
  1578. self.UpdateMap()
  1579. self.zoomChanged.emit()
  1580. def ZoomHistory(self, n, s, e, w):
  1581. """Manages a list of last 10 zoom extents
  1582. Emits zoomChanged signal.
  1583. Emits zoomHistoryAvailable signal when stack is not empty.
  1584. Emits zoomHistoryUnavailable signal when stack is empty.
  1585. All methods which are changing zoom should call this method
  1586. to make a record in the history. The signal zoomChanged will be
  1587. then emitted automatically.
  1588. :param n,s,e,w: north, south, east, west
  1589. :return: removed history item if exists (or None)
  1590. """
  1591. removed = None
  1592. self.zoomhistory.append((n, s, e, w))
  1593. if len(self.zoomhistory) > 10:
  1594. removed = self.zoomhistory.pop(0)
  1595. if removed:
  1596. Debug.msg(
  1597. 4,
  1598. "BufferedWindow.ZoomHistory(): hist=%s, removed=%s"
  1599. % (self.zoomhistory, removed),
  1600. )
  1601. else:
  1602. Debug.msg(4, "BufferedWindow.ZoomHistory(): hist=%s" % (self.zoomhistory))
  1603. # update toolbar
  1604. if len(self.zoomhistory) > 1:
  1605. self.zoomHistoryAvailable.emit()
  1606. else:
  1607. self.zoomHistoryUnavailable.emit()
  1608. self.zoomChanged.emit()
  1609. return removed
  1610. def InitZoomHistory(self):
  1611. """Initializes zoom history.
  1612. .. todo::
  1613. First item is handled in some special way. Improve the
  1614. documentation or fix the code.
  1615. It does not emits any signals.
  1616. This method can be possibly removed when the history will solve the
  1617. fist item in different way or when GCP manager (and possibly others)
  1618. will handle Map variable in the way that it will be prepared for
  1619. MapWindow/BufferedWindow and thus usable to initialize history.
  1620. """
  1621. self.zoomhistory.append(
  1622. (
  1623. self.Map.region["n"],
  1624. self.Map.region["s"],
  1625. self.Map.region["e"],
  1626. self.Map.region["w"],
  1627. )
  1628. )
  1629. Debug.msg(4, "BufferedWindow.InitZoomHistory(): hist=%s" % (self.zoomhistory))
  1630. def ResetZoomHistory(self):
  1631. """Reset zoom history"""
  1632. self.zoomhistory = list()
  1633. def ZoomToMap(self, layers=None, ignoreNulls=False, render=True):
  1634. """Set display extents to match selected raster
  1635. or vector map(s).
  1636. :param layers: list of layers to be zoom to
  1637. :param ignoreNulls: True to ignore null-values (valid only for rasters)
  1638. :param render: True to re-render display
  1639. """
  1640. if not layers:
  1641. layers = self._giface.GetLayerList().GetSelectedLayers(checkedOnly=False)
  1642. layers = [layer.maplayer for layer in layers]
  1643. if not layers:
  1644. return
  1645. rast = []
  1646. rast3d = None
  1647. vect = []
  1648. updated = False
  1649. for layer in layers:
  1650. # only one raster is used: g.region does not support multiple
  1651. if layer.type == "raster":
  1652. rast.append(layer.GetName())
  1653. elif layer.type == "raster_3d":
  1654. rast3d = layer.GetName()
  1655. elif layer.type == "vector":
  1656. if self.digit and self.toolbar.GetLayer() == layer:
  1657. w, s, b, e, n, t = self.digit.GetDisplay().GetMapBoundingBox()
  1658. self.Map.GetRegion(n=n, s=s, w=w, e=e, update=True)
  1659. updated = True
  1660. else:
  1661. vect.append(layer.name)
  1662. elif layer.type == "rgb":
  1663. for rname in layer.GetName().splitlines():
  1664. rast.append(rname)
  1665. if not updated:
  1666. self.Map.GetRegion(
  1667. rast=rast, rast3d=rast3d, vect=vect, zoom=ignoreNulls, update=True
  1668. )
  1669. self.ZoomHistory(
  1670. self.Map.region["n"],
  1671. self.Map.region["s"],
  1672. self.Map.region["e"],
  1673. self.Map.region["w"],
  1674. )
  1675. if render:
  1676. self.UpdateMap()
  1677. def ZoomToWind(self):
  1678. """Set display geometry to match computational region
  1679. settings (set with g.region)
  1680. """
  1681. self.Map.region = self.Map.GetRegion()
  1682. self.ZoomHistory(
  1683. self.Map.region["n"],
  1684. self.Map.region["s"],
  1685. self.Map.region["e"],
  1686. self.Map.region["w"],
  1687. )
  1688. self.UpdateMap()
  1689. def ZoomToDefault(self):
  1690. """Set display geometry to match default region settings"""
  1691. self.Map.region = self.Map.GetRegion(default=True)
  1692. self.Map.AdjustRegion() # aling region extent to the display
  1693. self.ZoomHistory(
  1694. self.Map.region["n"],
  1695. self.Map.region["s"],
  1696. self.Map.region["e"],
  1697. self.Map.region["w"],
  1698. )
  1699. self.UpdateMap()
  1700. def GoTo(self, e, n):
  1701. region = self.Map.GetCurrentRegion()
  1702. region["center_easting"], region["center_northing"] = e, n
  1703. dn = (region["nsres"] * region["rows"]) / 2.0
  1704. region["n"] = region["center_northing"] + dn
  1705. region["s"] = region["center_northing"] - dn
  1706. de = (region["ewres"] * region["cols"]) / 2.0
  1707. region["e"] = region["center_easting"] + de
  1708. region["w"] = region["center_easting"] - de
  1709. self.Map.AdjustRegion()
  1710. # add to zoom history
  1711. self.ZoomHistory(region["n"], region["s"], region["e"], region["w"])
  1712. self.UpdateMap()
  1713. def DisplayToWind(self):
  1714. """Set computational region (WIND file) to match display
  1715. extents
  1716. """
  1717. tmpreg = os.getenv("GRASS_REGION")
  1718. if tmpreg:
  1719. del os.environ["GRASS_REGION"]
  1720. # We ONLY want to set extents here. Don't mess with resolution. Leave that
  1721. # for user to set explicitly with g.region
  1722. new = self.Map.AlignResolution()
  1723. RunCommand(
  1724. "g.region",
  1725. parent=self,
  1726. overwrite=True,
  1727. n=new["n"],
  1728. s=new["s"],
  1729. e=new["e"],
  1730. w=new["w"],
  1731. rows=int(new["rows"]),
  1732. cols=int(new["cols"]),
  1733. )
  1734. if tmpreg:
  1735. os.environ["GRASS_REGION"] = tmpreg
  1736. self.UpdateMap(render=False)
  1737. def SetRegion(self, zoomOnly=True):
  1738. """Set display extents/compulational region from named region
  1739. file.
  1740. :param zoomOnly: zoom to named region only (computational region is not saved)
  1741. """
  1742. if zoomOnly:
  1743. label = _("Zoom to saved region extents")
  1744. else:
  1745. label = _("Set compulational region from named region")
  1746. dlg = SavedRegion(parent=self, title=label, loadsave="load")
  1747. if dlg.ShowModal() == wx.ID_CANCEL or not dlg.GetName():
  1748. dlg.Destroy()
  1749. return
  1750. region = dlg.GetName()
  1751. if not grass.find_file(name=region, element="windows")["name"]:
  1752. GError(
  1753. parent=self,
  1754. message=_("Region <%s> not found. Operation canceled.") % region,
  1755. )
  1756. dlg.Destroy()
  1757. return
  1758. dlg.Destroy()
  1759. if zoomOnly:
  1760. self.Map.GetRegion(regionName=region, update=True)
  1761. self.ZoomHistory(
  1762. self.Map.region["n"],
  1763. self.Map.region["s"],
  1764. self.Map.region["e"],
  1765. self.Map.region["w"],
  1766. )
  1767. else:
  1768. # set computation region from named region file
  1769. RunCommand("g.region", parent=self, region=region)
  1770. self.UpdateMap()
  1771. def SaveRegion(self, display=True):
  1772. """Save display extents/compulational region to named region
  1773. file.
  1774. :param display: True for display extends otherwise computational region
  1775. """
  1776. if display:
  1777. title = _("Save display extents to region file")
  1778. else:
  1779. title = _("Save computational region to region file")
  1780. dlg = SavedRegion(parent=self, title=title, loadsave="save")
  1781. if dlg.ShowModal() == wx.ID_CANCEL or not dlg.GetName():
  1782. dlg.Destroy()
  1783. return
  1784. # test to see if it already exists and ask permission to overwrite
  1785. if grass.find_file(name=dlg.GetName(), element="windows")["name"]:
  1786. overwrite = wx.MessageBox(
  1787. parent=self,
  1788. message=_(
  1789. "Region file <%s> already exists. " "Do you want to overwrite it?"
  1790. )
  1791. % (dlg.GetName()),
  1792. caption=_("Warning"),
  1793. style=wx.YES_NO | wx.CENTRE,
  1794. )
  1795. if overwrite != wx.YES:
  1796. dlg.Destroy()
  1797. return
  1798. if display:
  1799. self._saveDisplayRegion(dlg.GetName())
  1800. else:
  1801. self._saveCompRegion(dlg.GetName())
  1802. dlg.Destroy()
  1803. def _saveCompRegion(self, name):
  1804. """Save region settings to region file
  1805. :param name: region name
  1806. """
  1807. RunCommand("g.region", overwrite=True, parent=self, flags="u", save=name)
  1808. def _saveDisplayRegion(self, name):
  1809. """Save display extents to region file
  1810. :param name: region name
  1811. """
  1812. new = self.Map.GetCurrentRegion()
  1813. tmpreg = os.getenv("GRASS_REGION")
  1814. if tmpreg:
  1815. del os.environ["GRASS_REGION"]
  1816. RunCommand(
  1817. "g.region",
  1818. overwrite=True,
  1819. parent=self,
  1820. flags="u",
  1821. n=new["n"],
  1822. s=new["s"],
  1823. e=new["e"],
  1824. w=new["w"],
  1825. rows=int(new["rows"]),
  1826. cols=int(new["cols"]),
  1827. save=name,
  1828. )
  1829. if tmpreg:
  1830. os.environ["GRASS_REGION"] = tmpreg
  1831. def Distance(self, beginpt, endpt, screen=True):
  1832. """Calculates distance
  1833. Ctypes required for LL-locations
  1834. :param beginpt: first point
  1835. :param endpt: second point
  1836. :param screen: True for screen coordinates otherwise EN
  1837. """
  1838. if screen:
  1839. e1, n1 = self.Pixel2Cell(beginpt)
  1840. e2, n2 = self.Pixel2Cell(endpt)
  1841. else:
  1842. e1, n1 = beginpt
  1843. e2, n2 = endpt
  1844. dEast = e2 - e1
  1845. dNorth = n2 - n1
  1846. if self.Map.projinfo["proj"] == "ll" and haveCtypes:
  1847. dist = gislib.G_distance(e1, n1, e2, n2)
  1848. else:
  1849. dist = math.sqrt(math.pow((dEast), 2) + math.pow((dNorth), 2))
  1850. return (dist, (dEast, dNorth))
  1851. def GetMap(self):
  1852. """Get render.Map() instance"""
  1853. return self.Map
  1854. def RegisterGraphicsToDraw(
  1855. self, graphicsType, pdc=None, setStatusFunc=None, drawFunc=None, mapCoords=True
  1856. ):
  1857. """This method registers graphics to draw.
  1858. :param type: (string) - graphics type: "point", "line" or "rectangle"
  1859. :param pdc: PseudoDC object, default is pdcTmp
  1860. :param setStatusFunc: function called before drawing each item
  1861. Status function should be in this form:
  1862. setStatusFunc(item, itemOrderNum)
  1863. item passes instance of GraphicsSetItem
  1864. which will be drawn itemOrderNum number of item
  1865. in drawing order (from O)
  1866. Hidden items are also counted in drawing order.
  1867. :type setStatusFunc: function
  1868. :param drawFunc: defines own function for drawing, if function
  1869. is not defined DrawCross method is used for
  1870. type "point", DrawLines method for type "line",
  1871. DrawRectangle for "rectangle".
  1872. :param mapCoords: True if map coordinates should be set by user, otherwise pixels
  1873. :return: reference to GraphicsSet, which was added.
  1874. """
  1875. if not pdc:
  1876. pdc = self.pdcTmp
  1877. item = GraphicsSet(
  1878. parentMapWin=self,
  1879. graphicsType=graphicsType,
  1880. pdc=pdc,
  1881. setStatusFunc=setStatusFunc,
  1882. drawFunc=drawFunc,
  1883. mapCoords=mapCoords,
  1884. )
  1885. self.graphicsSetList.append(item)
  1886. return item
  1887. def UnregisterGraphicsToDraw(self, item):
  1888. """Unregisteres GraphicsSet instance
  1889. :param item: (GraphicsSetItem) - item to unregister
  1890. :return: True - if item was unregistered
  1891. :return: False - if item was not found
  1892. """
  1893. if item in self.graphicsSetList:
  1894. self.graphicsSetList.remove(item)
  1895. return True
  1896. return False