buffered.py 75 KB

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