mapdisp_window.py 68 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943
  1. """!
  2. @package mapdisp_window.py
  3. @brief Map display canvas - buffered window.
  4. Classes:
  5. - MapWindow
  6. - BufferedWindow
  7. (C) 2006-2011 by the GRASS Development Team
  8. This program is free software under the GNU General Public
  9. License (>=v2). Read the file COPYING that comes with GRASS
  10. for details.
  11. @author Martin Landa <landa.martin gmail.com>
  12. @author Michael Barton
  13. @author Jachym Cepicky
  14. """
  15. import os
  16. import time
  17. import math
  18. import sys
  19. import tempfile
  20. import traceback
  21. import wx
  22. import grass.script as grass
  23. import dbm
  24. import gdialogs
  25. import gcmd
  26. import utils
  27. import globalvar
  28. import gselect
  29. from debug import Debug
  30. from preferences import globalSettings as UserSettings
  31. from units import ConvertValue as UnitsConvertValue
  32. try:
  33. import grass.lib.gis as gislib
  34. haveCtypes = True
  35. except ImportError:
  36. haveCtypes = False
  37. class MapWindow(object):
  38. """!Abstract map display window class
  39. Superclass for BufferedWindow class (2D display mode), and GLWindow
  40. (3D display mode).
  41. Subclasses have to define _bindMouseEvents() method which binds
  42. MouseEvent handlers.
  43. """
  44. def __init__(self, parent, id = wx.ID_ANY,
  45. Map = None, tree = None, lmgr = None, **kwargs):
  46. self.parent = parent # MapFrame
  47. self.Map = Map
  48. self.tree = tree
  49. self.lmgr = lmgr
  50. # mouse attributes -- position on the screen, begin and end of
  51. # dragging, and type of drawing
  52. self.mouse = {
  53. 'begin': [0, 0], # screen coordinates
  54. 'end' : [0, 0],
  55. 'use' : "pointer",
  56. 'box' : "point"
  57. }
  58. # stores overriden cursor
  59. self._overridenCursor = None
  60. def RegisterMouseEventHandler(self, event, handler, cursor = None):
  61. """!Binds event handler
  62. Call event.Skip() in handler to allow default processing in MapWindow.
  63. \code
  64. # your class methods
  65. def OnButton(self, event):
  66. # current map display's map window
  67. # expects LayerManager to be the parent
  68. self.mapwin = self.parent.GetLayerTree().GetMapDisplay().GetWindow()
  69. if self.mapwin.RegisterMouseEventHandler(wx.EVT_LEFT_DOWN, self.OnMouseAction,
  70. wx.StockCursor(wx.CURSOR_CROSS)):
  71. self.parent.GetLayerTree().GetMapDisplay().Raise()
  72. else:
  73. # handle that you cannot get coordinates
  74. def OnMouseAction(self, event):
  75. # get real world coordinates of mouse click
  76. coor = self.mapwin.Pixel2Cell(event.GetPositionTuple()[:])
  77. self.text.SetLabel('Coor: ' + str(coor))
  78. self.mapwin.UnregisterMouseEventHandler(wx.EVT_LEFT_DOWN)
  79. event.Skip()
  80. \endcode
  81. @param event one of mouse events
  82. @param handler function to handle event
  83. @param cursor cursor which temporary overrides current cursor
  84. @return True if successfull
  85. @return False if event cannot be bind
  86. """
  87. # if it is a VDigitWindow it cannot be used
  88. # hasattr is ugly
  89. if hasattr(self, "digit"):
  90. return False
  91. self.Bind(event, handler)
  92. self.mouse['useBeforeGenericEvent'] = self.mouse['use']
  93. self.mouse['use'] = 'genericEvent'
  94. if cursor:
  95. self._overridenCursor = self.GetCursor()
  96. self.SetCursor(cursor)
  97. return True
  98. def UnregisterMouseEventHandler(self, event):
  99. """!Unbinds event handler a restores previous state
  100. You should unbind to restore normal MapWindow behaviour.
  101. Note that this operation will unbind any other external (non-MapWindow) handlers.
  102. @param event event to unbind
  103. @return True if successfull
  104. @return False if event cannot be unbind
  105. """
  106. if hasattr(self, "digit"):
  107. return False
  108. # it is not yet posible in wxPython to unbind exact event
  109. ret = self.Unbind(event)
  110. # restore bind state
  111. self._bindMouseEvents()
  112. # restore mouse use (previous state)
  113. self.mouse['use'] = self.mouse['useBeforeGenericEvent']
  114. # restore overriden cursor
  115. if self._overridenCursor:
  116. self.SetCursor(self._overridenCursor)
  117. return ret
  118. def OnMotion(self, event):
  119. """!Track mouse motion and update statusbar
  120. """
  121. if self.parent.statusbarWin['toggle'].GetSelection() == 0: # Coordinates
  122. precision = int(UserSettings.Get(group = 'projection', key = 'format',
  123. subkey = 'precision'))
  124. format = UserSettings.Get(group = 'projection', key = 'format',
  125. subkey = 'll')
  126. try:
  127. e, n = self.Pixel2Cell(event.GetPositionTuple())
  128. except (TypeError, ValueError):
  129. self.parent.statusbar.SetStatusText("", 0)
  130. event.Skip()
  131. return
  132. updated = False
  133. if hasattr(self, "digit"):
  134. updated = self._onMotion((e, n), precision)
  135. if not updated:
  136. if self.parent.statusbarWin['projection'].IsChecked():
  137. if not UserSettings.Get(group = 'projection', key = 'statusbar', subkey = 'proj4'):
  138. self.parent.statusbar.SetStatusText(_("Projection not defined (check the settings)"), 0)
  139. else:
  140. proj, coord = utils.ReprojectCoordinates(coord = (e, n),
  141. projOut = UserSettings.Get(group = 'projection',
  142. key = 'statusbar',
  143. subkey = 'proj4'),
  144. flags = 'd')
  145. if coord:
  146. e, n = coord
  147. if proj in ('ll', 'latlong', 'longlat') and format == 'DMS':
  148. self.parent.statusbar.SetStatusText(utils.Deg2DMS(e, n, precision = precision),
  149. 0)
  150. else:
  151. self.parent.statusbar.SetStatusText("%.*f; %.*f" % \
  152. (precision, e, precision, n), 0)
  153. else:
  154. self.parent.statusbar.SetStatusText(_("Error in projection (check the settings)"), 0)
  155. else:
  156. if self.parent.Map.projinfo['proj'] == 'll' and format == 'DMS':
  157. self.parent.statusbar.SetStatusText(utils.Deg2DMS(e, n, precision = precision),
  158. 0)
  159. else:
  160. self.parent.statusbar.SetStatusText("%.*f; %.*f" % \
  161. (precision, e, precision, n), 0)
  162. event.Skip()
  163. def GetLayerByName(self, name, mapType, dataType = 'layer'):
  164. """!Get layer from layer tree by nam
  165. @param name layer name
  166. @param type 'item' / 'layer' / 'nviz'
  167. @return layer / map layer properties / nviz properties
  168. @return None
  169. """
  170. if not self.tree:
  171. return None
  172. try:
  173. mapLayer = self.Map.GetListOfLayers(l_type = mapType, l_name = name)[0]
  174. except IndexError:
  175. return None
  176. if dataType == 'layer':
  177. return mapLayer
  178. item = self.tree.FindItemByData('maplayer', mapLayer)
  179. if not item:
  180. return None
  181. if dataType == 'nviz':
  182. return self.tree.GetPyData(item)[0]['nviz']
  183. return item
  184. def GetSelectedLayer(self, type = 'layer', multi = False):
  185. """!Get selected layer from layer tree
  186. @param type 'item' / 'layer' / 'nviz'
  187. @param multi return first selected layer or all
  188. @return layer / map layer properties / nviz properties
  189. @return None / [] on failure
  190. """
  191. ret = []
  192. if not self.tree or \
  193. not self.tree.GetSelection():
  194. if multi:
  195. return []
  196. else:
  197. return None
  198. if multi and \
  199. type == 'item':
  200. return self.tree.GetSelections()
  201. for item in self.tree.GetSelections():
  202. if not item.IsChecked():
  203. if multi:
  204. continue
  205. else:
  206. return None
  207. if type == 'item': # -> multi = False
  208. return item
  209. try:
  210. if type == 'nviz':
  211. layer = self.tree.GetPyData(item)[0]['nviz']
  212. else:
  213. layer = self.tree.GetPyData(item)[0]['maplayer']
  214. except:
  215. layer = None
  216. if multi:
  217. ret.append(layer)
  218. else:
  219. return layer
  220. return ret
  221. class BufferedWindow(MapWindow, wx.Window):
  222. """!A Buffered window class (2D view mode)
  223. Superclass for VDigitWindow (vector digitizer).
  224. When the drawing needs to change, you app needs to call the
  225. UpdateMap() method. Since the drawing is stored in a bitmap, you
  226. can also save the drawing to file by calling the
  227. SaveToFile() method.
  228. """
  229. def __init__(self, parent, id = wx.ID_ANY,
  230. Map = None, tree = None, lmgr = None,
  231. style = wx.NO_FULL_REPAINT_ON_RESIZE, **kwargs):
  232. MapWindow.__init__(self, parent, id, Map, tree, lmgr, **kwargs)
  233. wx.Window.__init__(self, parent, id, style = style, **kwargs)
  234. # flags
  235. self.resize = False # indicates whether or not a resize event has taken place
  236. self.dragimg = None # initialize variable for map panning
  237. # variables for drawing on DC
  238. self.pen = None # pen for drawing zoom boxes, etc.
  239. self.polypen = None # pen for drawing polylines (measurements, profiles, etc)
  240. # List of wx.Point tuples defining a polyline (geographical coordinates)
  241. self.polycoords = []
  242. # ID of rubber band line
  243. self.lineid = None
  244. # ID of poly line resulting from cumulative rubber band lines (e.g. measurement)
  245. self.plineid = None
  246. # event bindings
  247. self.Bind(wx.EVT_PAINT, self.OnPaint)
  248. self.Bind(wx.EVT_SIZE, self.OnSize)
  249. self.Bind(wx.EVT_IDLE, self.OnIdle)
  250. self._bindMouseEvents()
  251. self.processMouse = True
  252. # render output objects
  253. self.mapfile = None # image file to be rendered
  254. self.img = None # wx.Image object (self.mapfile)
  255. # decoration overlays
  256. self.overlays = {}
  257. # images and their PseudoDC ID's for painting and dragging
  258. self.imagedict = {}
  259. self.select = {} # selecting/unselecting decorations for dragging
  260. self.textdict = {} # text, font, and color indexed by id
  261. self.currtxtid = None # PseudoDC id for currently selected text
  262. # zoom objects
  263. self.zoomhistory = [] # list of past zoom extents
  264. self.currzoom = 0 # current set of extents in zoom history being used
  265. self.zoomtype = 1 # 1 zoom in, 0 no zoom, -1 zoom out
  266. self.hitradius = 10 # distance for selecting map decorations
  267. self.dialogOffset = 5 # offset for dialog (e.g. DisplayAttributesDialog)
  268. # OnSize called to make sure the buffer is initialized.
  269. # This might result in OnSize getting called twice on some
  270. # platforms at initialization, but little harm done.
  271. ### self.OnSize(None)
  272. self._definePseudoDC()
  273. # redraw all pdc's, pdcTmp layer is redrawn always (speed issue)
  274. self.redrawAll = True
  275. # will store an off screen empty bitmap for saving to file
  276. self._buffer = None
  277. self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x:None)
  278. # vars for handling mouse clicks
  279. self.dragid = -1
  280. self.lastpos = (0, 0)
  281. def _definePseudoDC(self):
  282. """!Define PseudoDC objects to use
  283. """
  284. # create PseudoDC used for background map, map decorations like scales and legends
  285. self.pdc = wx.PseudoDC()
  286. # used for digitization tool
  287. self.pdcVector = None
  288. # decorations (region box, etc.)
  289. self.pdcDec = wx.PseudoDC()
  290. # pseudoDC for temporal objects (select box, measurement tool, etc.)
  291. self.pdcTmp = wx.PseudoDC()
  292. def _bindMouseEvents(self):
  293. self.Bind(wx.EVT_MOUSE_EVENTS, self.MouseActions)
  294. self.Bind(wx.EVT_MOTION, self.OnMotion)
  295. def Draw(self, pdc, img = None, drawid = None, pdctype = 'image', coords = [0, 0, 0, 0]):
  296. """!Draws map and overlay decorations
  297. """
  298. if drawid == None:
  299. if pdctype == 'image' and img:
  300. drawid = self.imagedict[img]
  301. elif pdctype == 'clear':
  302. drawid == None
  303. else:
  304. drawid = wx.NewId()
  305. if img and pdctype == 'image':
  306. # self.imagedict[img]['coords'] = coords
  307. self.select[self.imagedict[img]['id']] = False # ?
  308. pdc.BeginDrawing()
  309. if drawid != 99:
  310. bg = wx.TRANSPARENT_BRUSH
  311. else:
  312. bg = wx.Brush(self.GetBackgroundColour())
  313. pdc.SetBackground(bg)
  314. Debug.msg (5, "BufferedWindow.Draw(): id=%s, pdctype = %s, coord=%s" % \
  315. (drawid, pdctype, coords))
  316. # set PseudoDC id
  317. if drawid is not None:
  318. pdc.SetId(drawid)
  319. if pdctype == 'clear': # erase the display
  320. bg = wx.WHITE_BRUSH
  321. # bg = wx.Brush(self.GetBackgroundColour())
  322. pdc.SetBackground(bg)
  323. pdc.RemoveAll()
  324. pdc.Clear()
  325. pdc.EndDrawing()
  326. self.Refresh()
  327. return
  328. if pdctype == 'image': # draw selected image
  329. bitmap = wx.BitmapFromImage(img)
  330. w,h = bitmap.GetSize()
  331. pdc.DrawBitmap(bitmap, coords[0], coords[1], True) # draw the composite map
  332. pdc.SetIdBounds(drawid, wx.Rect(coords[0],coords[1], w, h))
  333. elif pdctype == 'box': # draw a box on top of the map
  334. if self.pen:
  335. pdc.SetBrush(wx.Brush(wx.CYAN, wx.TRANSPARENT))
  336. pdc.SetPen(self.pen)
  337. x2 = max(coords[0],coords[2])
  338. x1 = min(coords[0],coords[2])
  339. y2 = max(coords[1],coords[3])
  340. y1 = min(coords[1],coords[3])
  341. rwidth = x2-x1
  342. rheight = y2-y1
  343. rect = wx.Rect(x1, y1, rwidth, rheight)
  344. pdc.DrawRectangleRect(rect)
  345. pdc.SetIdBounds(drawid, rect)
  346. elif pdctype == 'line': # draw a line on top of the map
  347. if self.pen:
  348. pdc.SetBrush(wx.Brush(wx.CYAN, wx.TRANSPARENT))
  349. pdc.SetPen(self.pen)
  350. pdc.DrawLinePoint(wx.Point(coords[0], coords[1]),wx.Point(coords[2], coords[3]))
  351. pdc.SetIdBounds(drawid, wx.Rect(coords[0], coords[1], coords[2], coords[3]))
  352. elif pdctype == 'polyline': # draw a polyline on top of the map
  353. if self.polypen:
  354. pdc.SetBrush(wx.Brush(wx.CYAN, wx.TRANSPARENT))
  355. pdc.SetPen(self.polypen)
  356. if (len(coords) < 2):
  357. return
  358. i = 1
  359. while i < len(coords):
  360. pdc.DrawLinePoint(wx.Point(coords[i-1][0], coords[i-1][1]),
  361. wx.Point(coords[i][0], coords[i][1]))
  362. i += 1
  363. # get bounding rectangle for polyline
  364. xlist = []
  365. ylist = []
  366. if len(coords) > 0:
  367. for point in coords:
  368. x,y = point
  369. xlist.append(x)
  370. ylist.append(y)
  371. x1 = min(xlist)
  372. x2 = max(xlist)
  373. y1 = min(ylist)
  374. y2 = max(ylist)
  375. pdc.SetIdBounds(drawid, wx.Rect(x1,y1,x2,y2))
  376. # self.ovlcoords[drawid] = [x1,y1,x2,y2]
  377. elif pdctype == 'point': # draw point
  378. if self.pen:
  379. pdc.SetPen(self.pen)
  380. pdc.DrawPoint(coords[0], coords[1])
  381. coordsBound = (coords[0] - 5,
  382. coords[1] - 5,
  383. coords[0] + 5,
  384. coords[1] + 5)
  385. pdc.SetIdBounds(drawid, wx.Rect(coordsBound))
  386. elif pdctype == 'text': # draw text on top of map
  387. if not img['active']:
  388. return # only draw active text
  389. if 'rotation' in img:
  390. rotation = float(img['rotation'])
  391. else:
  392. rotation = 0.0
  393. w, h = self.GetFullTextExtent(img['text'])[0:2]
  394. pdc.SetFont(img['font'])
  395. pdc.SetTextForeground(img['color'])
  396. coords, bbox = self.TextBounds(img)
  397. if rotation == 0:
  398. pdc.DrawText(img['text'], coords[0], coords[1])
  399. else:
  400. pdc.DrawRotatedText(img['text'], coords[0], coords[1], rotation)
  401. pdc.SetIdBounds(drawid, bbox)
  402. pdc.EndDrawing()
  403. self.Refresh()
  404. return drawid
  405. def TextBounds(self, textinfo, relcoords = False):
  406. """!Return text boundary data
  407. @param textinfo text metadata (text, font, color, rotation)
  408. @param coords reference point
  409. @return coords of nonrotated text bbox (TL corner)
  410. @return bbox of rotated text bbox (wx.Rect)
  411. @return relCoords are text coord inside bbox
  412. """
  413. if 'rotation' in textinfo:
  414. rotation = float(textinfo['rotation'])
  415. else:
  416. rotation = 0.0
  417. coords = textinfo['coords']
  418. bbox = wx.Rect(coords[0], coords[1], 0, 0)
  419. relCoords = (0, 0)
  420. Debug.msg (4, "BufferedWindow.TextBounds(): text=%s, rotation=%f" % \
  421. (textinfo['text'], rotation))
  422. self.Update()
  423. self.SetFont(textinfo['font'])
  424. w, h = self.GetTextExtent(textinfo['text'])
  425. if rotation == 0:
  426. bbox[2], bbox[3] = w, h
  427. if relcoords:
  428. return coords, bbox, relCoords
  429. else:
  430. return coords, bbox
  431. boxh = math.fabs(math.sin(math.radians(rotation)) * w) + h
  432. boxw = math.fabs(math.cos(math.radians(rotation)) * w) + h
  433. if rotation > 0 and rotation < 90:
  434. bbox[1] -= boxh
  435. relCoords = (0, boxh)
  436. elif rotation >= 90 and rotation < 180:
  437. bbox[0] -= boxw
  438. bbox[1] -= boxh
  439. relCoords = (boxw, boxh)
  440. elif rotation >= 180 and rotation < 270:
  441. bbox[0] -= boxw
  442. relCoords = (boxw, 0)
  443. bbox[2] = boxw
  444. bbox[3] = boxh
  445. bbox.Inflate(h,h)
  446. if relcoords:
  447. return coords, bbox, relCoords
  448. else:
  449. return coords, bbox
  450. def OnPaint(self, event):
  451. """!Draw PseudoDC's to buffered paint DC
  452. If self.redrawAll is False on self.pdcTmp content is re-drawn
  453. """
  454. Debug.msg(4, "BufferedWindow.OnPaint(): redrawAll=%s" % self.redrawAll)
  455. dc = wx.BufferedPaintDC(self, self.buffer)
  456. dc.Clear()
  457. # use PrepareDC to set position correctly
  458. self.PrepareDC(dc)
  459. # create a clipping rect from our position and size
  460. # and update region
  461. rgn = self.GetUpdateRegion().GetBox()
  462. dc.SetClippingRect(rgn)
  463. switchDraw = False
  464. if self.redrawAll is None:
  465. self.redrawAll = True
  466. switchDraw = True
  467. if self.redrawAll: # redraw pdc and pdcVector
  468. # draw to the dc using the calculated clipping rect
  469. self.pdc.DrawToDCClipped(dc, rgn)
  470. # draw vector map layer
  471. if hasattr(self, "digit"):
  472. # decorate with GDDC (transparency)
  473. try:
  474. gcdc = wx.GCDC(dc)
  475. self.pdcVector.DrawToDCClipped(gcdc, rgn)
  476. except NotImplementedError, e:
  477. print >> sys.stderr, e
  478. self.pdcVector.DrawToDCClipped(dc, rgn)
  479. self.bufferLast = None
  480. else: # do not redraw pdc and pdcVector
  481. if self.bufferLast is None:
  482. # draw to the dc
  483. self.pdc.DrawToDC(dc)
  484. if hasattr(self, "digit"):
  485. # decorate with GDDC (transparency)
  486. try:
  487. gcdc = wx.GCDC(dc)
  488. self.pdcVector.DrawToDC(gcdc)
  489. except NotImplementedError, e:
  490. print >> sys.stderr, e
  491. self.pdcVector.DrawToDC(dc)
  492. # store buffered image
  493. # self.bufferLast = wx.BitmapFromImage(self.buffer.ConvertToImage())
  494. self.bufferLast = dc.GetAsBitmap(wx.Rect(0, 0, self.Map.width, self.Map.height))
  495. self.pdc.DrawBitmap(self.bufferLast, 0, 0, False)
  496. self.pdc.DrawToDC(dc)
  497. # draw decorations (e.g. region box)
  498. try:
  499. gcdc = wx.GCDC(dc)
  500. self.pdcDec.DrawToDC(gcdc)
  501. except NotImplementedError, e:
  502. print >> sys.stderr, e
  503. self.pdcDec.DrawToDC(dc)
  504. # draw temporary object on the foreground
  505. ### self.pdcTmp.DrawToDCClipped(dc, rgn)
  506. self.pdcTmp.DrawToDC(dc)
  507. if switchDraw:
  508. self.redrawAll = False
  509. def OnSize(self, event):
  510. """!Scale map image so that it is the same size as the Window
  511. """
  512. Debug.msg(3, "BufferedWindow.OnSize():")
  513. # set size of the input image
  514. self.Map.ChangeMapSize(self.GetClientSize())
  515. # align extent based on center point and display resolution
  516. # this causes that image is not resized when display windows is resized
  517. ### self.Map.AlignExtentFromDisplay()
  518. # Make new off screen bitmap: this bitmap will always have the
  519. # current drawing in it, so it can be used to save the image to
  520. # a file, or whatever.
  521. self.buffer = wx.EmptyBitmap(max(1, self.Map.width), max(1, self.Map.height))
  522. # get the image to be rendered
  523. self.img = self.GetImage()
  524. # update map display
  525. if self.img and self.Map.width + self.Map.height > 0: # scale image during resize
  526. self.img = self.img.Scale(self.Map.width, self.Map.height)
  527. if len(self.Map.GetListOfLayers()) > 0:
  528. self.UpdateMap()
  529. # re-render image on idle
  530. self.resize = True
  531. # reposition checkbox in statusbar
  532. self.parent.StatusbarReposition()
  533. # update statusbar
  534. self.parent.StatusbarUpdate()
  535. def OnIdle(self, event):
  536. """!Only re-render a composite map image from GRASS during
  537. idle time instead of multiple times during resizing.
  538. """
  539. if self.resize:
  540. self.UpdateMap(render = True)
  541. event.Skip()
  542. def SaveToFile(self, FileName, FileType, width, height):
  543. """!This draws the pseudo DC to a buffer that can be saved to
  544. a file.
  545. @param FileName file name
  546. @param FileType type of bitmap
  547. @param width image width
  548. @param height image height
  549. """
  550. busy = wx.BusyInfo(message = _("Please wait, exporting image..."),
  551. parent = self)
  552. wx.Yield()
  553. self.Map.ChangeMapSize((width, height))
  554. ibuffer = wx.EmptyBitmap(max(1, width), max(1, height))
  555. self.Map.Render(force = True, windres = True)
  556. img = self.GetImage()
  557. self.Draw(self.pdc, img, drawid = 99)
  558. dc = wx.BufferedPaintDC(self, ibuffer)
  559. dc.Clear()
  560. self.PrepareDC(dc)
  561. self.pdc.DrawToDC(dc)
  562. if self.pdcVector:
  563. self.pdcVector.DrawToDC(dc)
  564. ibuffer.SaveFile(FileName, FileType)
  565. busy.Destroy()
  566. self.UpdateMap(render = True)
  567. self.Refresh()
  568. def GetOverlay(self):
  569. """!Converts rendered overlay files to wx.Image
  570. Updates self.imagedict
  571. @return list of images
  572. """
  573. imgs = []
  574. for overlay in self.Map.GetListOfLayers(l_type = "overlay", l_active = True):
  575. if os.path.isfile(overlay.mapfile) and os.path.getsize(overlay.mapfile):
  576. img = wx.Image(overlay.mapfile, wx.BITMAP_TYPE_ANY)
  577. self.imagedict[img] = { 'id' : overlay.id,
  578. 'layer' : overlay }
  579. imgs.append(img)
  580. return imgs
  581. def GetImage(self):
  582. """!Converts redered map files to wx.Image
  583. Updates self.imagedict (id=99)
  584. @return wx.Image instance (map composition)
  585. """
  586. imgId = 99
  587. if self.mapfile and self.Map.mapfile and os.path.isfile(self.Map.mapfile) and \
  588. os.path.getsize(self.Map.mapfile):
  589. img = wx.Image(self.Map.mapfile, wx.BITMAP_TYPE_ANY)
  590. else:
  591. img = None
  592. self.imagedict[img] = { 'id': imgId }
  593. return img
  594. def UpdateMap(self, render = True, renderVector = True):
  595. """!Updates the canvas anytime there is a change to the
  596. underlaying images or to the geometry of the canvas.
  597. @param render re-render map composition
  598. @param renderVector re-render vector map layer enabled for editing (used for digitizer)
  599. """
  600. start = time.clock()
  601. self.resize = False
  602. if not self.Map.cmdfile and self.img is None:
  603. render = True
  604. #
  605. # initialize process bar (only on 'render')
  606. #
  607. if render or renderVector:
  608. self.parent.statusbarWin['progress'].Show()
  609. if self.parent.statusbarWin['progress'].GetRange() > 0:
  610. self.parent.statusbarWin['progress'].SetValue(1)
  611. #
  612. # render background image if needed
  613. #
  614. # update layer dictionary if there has been a change in layers
  615. if self.tree and self.tree.reorder:
  616. self.tree.ReorderLayers()
  617. # reset flag for auto-rendering
  618. if self.tree:
  619. self.tree.rerender = False
  620. try:
  621. if render:
  622. # update display size
  623. self.Map.ChangeMapSize(self.GetClientSize())
  624. if self.parent.statusbarWin['resolution'].IsChecked():
  625. # use computation region resolution for rendering
  626. windres = True
  627. else:
  628. windres = False
  629. self.mapfile = self.Map.Render(force = True, mapWindow = self.parent,
  630. windres = windres)
  631. else:
  632. self.mapfile = self.Map.Render(force = False, mapWindow = self.parent)
  633. except gcmd.GException, e:
  634. gcmd.GError(message = e.value)
  635. self.mapfile = None
  636. self.img = self.GetImage() # id=99
  637. #
  638. # clear pseudoDcs
  639. #
  640. for pdc in (self.pdc,
  641. self.pdcDec,
  642. self.pdcTmp):
  643. pdc.Clear()
  644. pdc.RemoveAll()
  645. #
  646. # draw background map image to PseudoDC
  647. #
  648. if not self.img:
  649. self.Draw(self.pdc, pdctype = 'clear')
  650. else:
  651. try:
  652. id = self.imagedict[self.img]['id']
  653. except:
  654. return False
  655. self.Draw(self.pdc, self.img, drawid = id)
  656. #
  657. # render vector map layer
  658. #
  659. if renderVector and hasattr(self, "digit"):
  660. self._updateMap()
  661. #
  662. # render overlays
  663. #
  664. for img in self.GetOverlay():
  665. # draw any active and defined overlays
  666. if self.imagedict[img]['layer'].IsActive():
  667. id = self.imagedict[img]['id']
  668. self.Draw(self.pdc, img = img, drawid = id,
  669. pdctype = self.overlays[id]['pdcType'], coords = self.overlays[id]['coords'])
  670. for id in self.textdict.keys():
  671. self.Draw(self.pdc, img = self.textdict[id], drawid = id,
  672. pdctype = 'text', coords = [10, 10, 10, 10])
  673. # optionally draw computational extent box
  674. self.DrawCompRegionExtent()
  675. #
  676. # redraw pdcTmp if needed
  677. #
  678. if len(self.polycoords) > 0:
  679. self.DrawLines(self.pdcTmp)
  680. if not self.parent.IsStandalone() and \
  681. self.parent.GetLayerManager().georectifying:
  682. # -> georectifier (redraw GCPs)
  683. if self.parent.toolbars['georect']:
  684. coordtype = 'gcpcoord'
  685. else:
  686. coordtype = 'mapcoord'
  687. self.parent.GetLayerManager().georectifying.DrawGCP(coordtype)
  688. if not self.parent.IsStandalone() and \
  689. self.parent.GetLayerManager().gcpmanagement:
  690. # -> georectifier (redraw GCPs)
  691. if self.parent.toolbars['gcpdisp']:
  692. if self == self.parent.TgtMapWindow:
  693. coordtype = 'target'
  694. else:
  695. coordtype = 'source'
  696. self.parent.DrawGCP(coordtype)
  697. #
  698. # clear measurement
  699. #
  700. if self.mouse["use"] == "measure":
  701. self.ClearLines(pdc = self.pdcTmp)
  702. self.polycoords = []
  703. self.mouse['use'] = 'pointer'
  704. self.mouse['box'] = 'point'
  705. self.mouse['end'] = [0, 0]
  706. self.SetCursor(self.parent.cursors["default"])
  707. stop = time.clock()
  708. #
  709. # hide process bar
  710. #
  711. self.parent.statusbarWin['progress'].Hide()
  712. #
  713. # update statusbar
  714. #
  715. ### self.Map.SetRegion()
  716. self.parent.StatusbarUpdate()
  717. if grass.find_file(name = 'MASK', element = 'cell')['name']:
  718. # mask found
  719. self.parent.statusbarWin['mask'].SetLabel(_('MASK'))
  720. else:
  721. self.parent.statusbarWin['mask'].SetLabel('')
  722. Debug.msg (1, "BufferedWindow.UpdateMap(): render=%s, renderVector=%s -> time=%g" % \
  723. (render, renderVector, (stop-start)))
  724. return True
  725. def DrawCompRegionExtent(self):
  726. """!Draw computational region extent in the display
  727. Display region is drawn as a blue box inside the computational region,
  728. computational region inside a display region as a red box).
  729. """
  730. if hasattr(self, "regionCoords"):
  731. compReg = self.Map.GetRegion()
  732. dispReg = self.Map.GetCurrentRegion()
  733. reg = None
  734. if self.IsInRegion(dispReg, compReg):
  735. self.polypen = wx.Pen(colour = wx.Colour(0, 0, 255, 128), width = 3, style = wx.SOLID)
  736. reg = dispReg
  737. else:
  738. self.polypen = wx.Pen(colour = wx.Colour(255, 0, 0, 128),
  739. width = 3, style = wx.SOLID)
  740. reg = compReg
  741. self.regionCoords = []
  742. self.regionCoords.append((reg['w'], reg['n']))
  743. self.regionCoords.append((reg['e'], reg['n']))
  744. self.regionCoords.append((reg['e'], reg['s']))
  745. self.regionCoords.append((reg['w'], reg['s']))
  746. self.regionCoords.append((reg['w'], reg['n']))
  747. # draw region extent
  748. self.DrawLines(pdc = self.pdcDec, polycoords = self.regionCoords)
  749. def IsInRegion(self, region, refRegion):
  750. """!
  751. Test if 'region' is inside of 'refRegion'
  752. @param region input region
  753. @param refRegion reference region (e.g. computational region)
  754. @return True if region is inside of refRegion
  755. @return False
  756. """
  757. if region['s'] >= refRegion['s'] and \
  758. region['n'] <= refRegion['n'] and \
  759. region['w'] >= refRegion['w'] and \
  760. region['e'] <= refRegion['e']:
  761. return True
  762. return False
  763. def EraseMap(self):
  764. """!Erase map canvas
  765. """
  766. self.Draw(self.pdc, pdctype = 'clear')
  767. if hasattr(self, "digit"):
  768. self.Draw(self.pdcVector, pdctype = 'clear')
  769. self.Draw(self.pdcDec, pdctype = 'clear')
  770. self.Draw(self.pdcTmp, pdctype = 'clear')
  771. def DragMap(self, moveto):
  772. """!Drag the entire map image for panning.
  773. @param moveto dx,dy
  774. """
  775. dc = wx.BufferedDC(wx.ClientDC(self))
  776. dc.SetBackground(wx.Brush("White"))
  777. dc.Clear()
  778. self.dragimg = wx.DragImage(self.buffer)
  779. self.dragimg.BeginDrag((0, 0), self)
  780. self.dragimg.GetImageRect(moveto)
  781. self.dragimg.Move(moveto)
  782. self.dragimg.DoDrawImage(dc, moveto)
  783. self.dragimg.EndDrag()
  784. def DragItem(self, id, event):
  785. """!Drag an overlay decoration item
  786. """
  787. if id == 99 or id == '' or id == None: return
  788. Debug.msg (5, "BufferedWindow.DragItem(): id=%d" % id)
  789. x, y = self.lastpos
  790. dx = event.GetX() - x
  791. dy = event.GetY() - y
  792. self.pdc.SetBackground(wx.Brush(self.GetBackgroundColour()))
  793. r = self.pdc.GetIdBounds(id)
  794. if type(r) is list:
  795. r = wx.Rect(r[0], r[1], r[2], r[3])
  796. if id > 100: # text dragging
  797. rtop = (r[0],r[1]-r[3],r[2],r[3])
  798. r = r.Union(rtop)
  799. rleft = (r[0]-r[2],r[1],r[2],r[3])
  800. r = r.Union(rleft)
  801. self.pdc.TranslateId(id, dx, dy)
  802. r2 = self.pdc.GetIdBounds(id)
  803. if type(r2) is list:
  804. r2 = wx.Rect(r[0], r[1], r[2], r[3])
  805. if id > 100: # text
  806. self.textdict[id]['bbox'] = r2
  807. self.textdict[id]['coords'][0] += dx
  808. self.textdict[id]['coords'][1] += dy
  809. r = r.Union(r2)
  810. r.Inflate(4,4)
  811. self.RefreshRect(r, False)
  812. self.lastpos = (event.GetX(), event.GetY())
  813. def MouseDraw(self, pdc = None, begin = None, end = None):
  814. """!Mouse box or line from 'begin' to 'end'
  815. If not given from self.mouse['begin'] to self.mouse['end'].
  816. """
  817. if not pdc:
  818. return
  819. if begin is None:
  820. begin = self.mouse['begin']
  821. if end is None:
  822. end = self.mouse['end']
  823. Debug.msg (5, "BufferedWindow.MouseDraw(): use=%s, box=%s, begin=%f,%f, end=%f,%f" % \
  824. (self.mouse['use'], self.mouse['box'],
  825. begin[0], begin[1], end[0], end[1]))
  826. if self.mouse['box'] == "box":
  827. boxid = wx.ID_NEW
  828. mousecoords = [begin[0], begin[1],
  829. end[0], end[1]]
  830. r = pdc.GetIdBounds(boxid)
  831. if type(r) is list:
  832. r = wx.Rect(r[0], r[1], r[2], r[3])
  833. r.Inflate(4, 4)
  834. try:
  835. pdc.ClearId(boxid)
  836. except:
  837. pass
  838. self.RefreshRect(r, False)
  839. pdc.SetId(boxid)
  840. self.Draw(pdc, drawid = boxid, pdctype = 'box', coords = mousecoords)
  841. elif self.mouse['box'] == "line" or self.mouse['box'] == 'point':
  842. self.lineid = wx.ID_NEW
  843. mousecoords = [begin[0], begin[1], \
  844. end[0], end[1]]
  845. x1 = min(begin[0],end[0])
  846. x2 = max(begin[0],end[0])
  847. y1 = min(begin[1],end[1])
  848. y2 = max(begin[1],end[1])
  849. r = wx.Rect(x1,y1,x2-x1,y2-y1)
  850. r.Inflate(4,4)
  851. try:
  852. pdc.ClearId(self.lineid)
  853. except:
  854. pass
  855. self.RefreshRect(r, False)
  856. pdc.SetId(self.lineid)
  857. self.Draw(pdc, drawid = self.lineid, pdctype = 'line', coords = mousecoords)
  858. def DrawLines(self, pdc = None, polycoords = None):
  859. """!Draw polyline in PseudoDC
  860. Set self.pline to wx.NEW_ID + 1
  861. polycoords - list of polyline vertices, geographical coordinates
  862. (if not given, self.polycoords is used)
  863. """
  864. if not pdc:
  865. pdc = self.pdcTmp
  866. if not polycoords:
  867. polycoords = self.polycoords
  868. if len(polycoords) > 0:
  869. self.plineid = wx.ID_NEW + 1
  870. # convert from EN to XY
  871. coords = []
  872. for p in polycoords:
  873. coords.append(self.Cell2Pixel(p))
  874. self.Draw(pdc, drawid = self.plineid, pdctype = 'polyline', coords = coords)
  875. Debug.msg (4, "BufferedWindow.DrawLines(): coords=%s, id=%s" % \
  876. (coords, self.plineid))
  877. return self.plineid
  878. return -1
  879. def DrawCross(self, pdc, coords, size, rotation = 0,
  880. text = None, textAlign = 'lr', textOffset = (5, 5)):
  881. """!Draw cross in PseudoDC
  882. @todo implement rotation
  883. @param pdc PseudoDC
  884. @param coord center coordinates
  885. @param rotation rotate symbol
  886. @param text draw also text (text, font, color, rotation)
  887. @param textAlign alignment (default 'lower-right')
  888. @textOffset offset for text (from center point)
  889. """
  890. Debug.msg(4, "BufferedWindow.DrawCross(): pdc=%s, coords=%s, size=%d" % \
  891. (pdc, coords, size))
  892. coordsCross = ((coords[0] - size, coords[1], coords[0] + size, coords[1]),
  893. (coords[0], coords[1] - size, coords[0], coords[1] + size))
  894. self.lineid = wx.NewId()
  895. for lineCoords in coordsCross:
  896. self.Draw(pdc, drawid = self.lineid, pdctype = 'line', coords = lineCoords)
  897. if not text:
  898. return self.lineid
  899. if textAlign == 'ul':
  900. coord = [coords[0] - textOffset[0], coords[1] - textOffset[1], 0, 0]
  901. elif textAlign == 'ur':
  902. coord = [coords[0] + textOffset[0], coords[1] - textOffset[1], 0, 0]
  903. elif textAlign == 'lr':
  904. coord = [coords[0] + textOffset[0], coords[1] + textOffset[1], 0, 0]
  905. else:
  906. coord = [coords[0] - textOffset[0], coords[1] + textOffset[1], 0, 0]
  907. self.Draw(pdc, img = text,
  908. pdctype = 'text', coords = coord)
  909. return self.lineid
  910. def MouseActions(self, event):
  911. """!Mouse motion and button click notifier
  912. """
  913. if not self.processMouse:
  914. return
  915. # zoom with mouse wheel
  916. if event.GetWheelRotation() != 0:
  917. self.OnMouseWheel(event)
  918. # left mouse button pressed
  919. elif event.LeftDown():
  920. self.OnLeftDown(event)
  921. # left mouse button released
  922. elif event.LeftUp():
  923. self.OnLeftUp(event)
  924. # dragging
  925. elif event.Dragging():
  926. self.OnDragging(event)
  927. # double click
  928. elif event.ButtonDClick():
  929. self.OnButtonDClick(event)
  930. # middle mouse button pressed
  931. elif event.MiddleDown():
  932. self.OnMiddleDown(event)
  933. # middle mouse button relesed
  934. elif event.MiddleUp():
  935. self.OnMiddleUp(event)
  936. # right mouse button pressed
  937. elif event.RightDown():
  938. self.OnRightDown(event)
  939. # right mouse button released
  940. elif event.RightUp():
  941. self.OnRightUp(event)
  942. elif event.Entering():
  943. self.OnMouseEnter(event)
  944. elif event.Moving():
  945. self.OnMouseMoving(event)
  946. def OnMouseWheel(self, event):
  947. """!Mouse wheel moved
  948. """
  949. self.processMouse = False
  950. current = event.GetPositionTuple()[:]
  951. wheel = event.GetWheelRotation()
  952. Debug.msg (5, "BufferedWindow.MouseAction(): wheel=%d" % wheel)
  953. # zoom 1/2 of the screen, centered to current mouse position (TODO: settings)
  954. begin = (current[0] - self.Map.width / 4,
  955. current[1] - self.Map.height / 4)
  956. end = (current[0] + self.Map.width / 4,
  957. current[1] + self.Map.height / 4)
  958. if wheel > 0:
  959. zoomtype = 1
  960. else:
  961. zoomtype = -1
  962. # zoom
  963. self.Zoom(begin, end, zoomtype)
  964. # redraw map
  965. self.UpdateMap()
  966. # update statusbar
  967. self.parent.StatusbarUpdate()
  968. self.Refresh()
  969. self.processMouse = True
  970. def OnDragging(self, event):
  971. """!Mouse dragging
  972. """
  973. Debug.msg (5, "BufferedWindow.MouseAction(): Dragging")
  974. current = event.GetPositionTuple()[:]
  975. previous = self.mouse['begin']
  976. move = (current[0] - previous[0],
  977. current[1] - previous[1])
  978. if hasattr(self, "digit"):
  979. digitToolbar = self.toolbar
  980. else:
  981. digitToolbar = None
  982. # dragging or drawing box with left button
  983. if self.mouse['use'] == 'pan' or \
  984. event.MiddleIsDown():
  985. self.DragMap(move)
  986. # dragging decoration overlay item
  987. elif (self.mouse['use'] == 'pointer' and
  988. not digitToolbar and
  989. self.dragid != None):
  990. self.DragItem(self.dragid, event)
  991. # dragging anything else - rubber band box or line
  992. else:
  993. if (self.mouse['use'] == 'pointer' and
  994. not digitToolbar):
  995. return
  996. self.mouse['end'] = event.GetPositionTuple()[:]
  997. if (event.LeftIsDown() and
  998. not (digitToolbar and
  999. digitToolbar.GetAction() in ("moveLine",) and
  1000. self.digit.GetDisplay().GetSelected() > 0)):
  1001. # draw box only when left mouse button is pressed
  1002. self.MouseDraw(pdc = self.pdcTmp)
  1003. def OnLeftDown(self, event):
  1004. """!Left mouse button pressed
  1005. """
  1006. Debug.msg (5, "BufferedWindow.OnLeftDown(): use=%s" % \
  1007. self.mouse["use"])
  1008. self.mouse['begin'] = event.GetPositionTuple()[:]
  1009. if self.mouse["use"] in ["measure", "profile"]:
  1010. # measure or profile
  1011. if len(self.polycoords) == 0:
  1012. self.mouse['end'] = self.mouse['begin']
  1013. self.polycoords.append(self.Pixel2Cell(self.mouse['begin']))
  1014. self.ClearLines(pdc=self.pdcTmp)
  1015. else:
  1016. self.mouse['begin'] = self.mouse['end']
  1017. elif self.mouse['use'] in ('zoom', 'legend'):
  1018. pass
  1019. # vector digizer
  1020. elif self.mouse["use"] == "pointer" and \
  1021. hasattr(self, "digit"):
  1022. if event.ControlDown():
  1023. self.OnLeftDownUndo(event)
  1024. else:
  1025. self._onLeftDown(event)
  1026. elif self.mouse['use'] == 'pointer':
  1027. # get decoration or text id
  1028. self.idlist = []
  1029. self.dragid = ''
  1030. self.lastpos = self.mouse['begin']
  1031. idlist = self.pdc.FindObjects(self.lastpos[0], self.lastpos[1],
  1032. self.hitradius)
  1033. if 99 in idlist:
  1034. idlist.remove(99)
  1035. if idlist != []:
  1036. self.dragid = idlist[0] #drag whatever is on top
  1037. else:
  1038. pass
  1039. event.Skip()
  1040. def OnLeftUp(self, event):
  1041. """!Left mouse button released
  1042. """
  1043. Debug.msg (5, "BufferedWindow.OnLeftUp(): use=%s" % \
  1044. self.mouse["use"])
  1045. self.mouse['end'] = event.GetPositionTuple()[:]
  1046. if self.mouse['use'] in ["zoom", "pan"]:
  1047. # set region in zoom or pan
  1048. begin = self.mouse['begin']
  1049. end = self.mouse['end']
  1050. if self.mouse['use'] == 'zoom':
  1051. # set region for click (zero-width box)
  1052. if begin[0] - end[0] == 0 or \
  1053. begin[1] - end[1] == 0:
  1054. # zoom 1/2 of the screen (TODO: settings)
  1055. begin = (end[0] - self.Map.width / 4,
  1056. end[1] - self.Map.height / 4)
  1057. end = (end[0] + self.Map.width / 4,
  1058. end[1] + self.Map.height / 4)
  1059. self.Zoom(begin, end, self.zoomtype)
  1060. # redraw map
  1061. self.UpdateMap(render = True)
  1062. # update statusbar
  1063. self.parent.StatusbarUpdate()
  1064. elif self.mouse["use"] == "query":
  1065. # querying
  1066. layers = self.GetSelectedLayer(multi = True)
  1067. isRaster = False
  1068. nVectors = 0
  1069. for l in layers:
  1070. if l.GetType() == 'raster':
  1071. isRaster = True
  1072. break
  1073. if l.GetType() == 'vector':
  1074. nVectors += 1
  1075. if isRaster or nVectors > 1:
  1076. self.parent.QueryMap(self.mouse['begin'][0],self.mouse['begin'][1])
  1077. else:
  1078. self.parent.QueryVector(self.mouse['begin'][0], self.mouse['begin'][1])
  1079. # clear temp canvas
  1080. self.UpdateMap(render = False, renderVector = False)
  1081. elif self.mouse["use"] == "queryVector":
  1082. # editable mode for vector map layers
  1083. self.parent.QueryVector(self.mouse['begin'][0], self.mouse['begin'][1])
  1084. # clear temp canvas
  1085. self.UpdateMap(render = False, renderVector = False)
  1086. elif self.mouse["use"] in ["measure", "profile"]:
  1087. # measure or profile
  1088. if self.mouse["use"] == "measure":
  1089. self.parent.MeasureDist(self.mouse['begin'], self.mouse['end'])
  1090. self.polycoords.append(self.Pixel2Cell(self.mouse['end']))
  1091. self.ClearLines(pdc = self.pdcTmp)
  1092. self.DrawLines(pdc = self.pdcTmp)
  1093. elif self.mouse["use"] == "pointer" and \
  1094. not self.parent.IsStandalone() and \
  1095. self.parent.GetLayerManager().gcpmanagement:
  1096. # -> GCP manager
  1097. if self.parent.toolbars['gcpdisp']:
  1098. coord = self.Pixel2Cell(self.mouse['end'])
  1099. if self.parent.MapWindow == self.parent.SrcMapWindow:
  1100. coordtype = 'source'
  1101. else:
  1102. coordtype = 'target'
  1103. self.parent.GetLayerManager().gcpmanagement.SetGCPData(coordtype, coord, self, confirm = True)
  1104. self.UpdateMap(render = False, renderVector = False)
  1105. elif self.mouse["use"] == "pointer" and \
  1106. not self.parent.IsStandalone() and \
  1107. self.parent.GetLayerManager().georectifying:
  1108. # -> georectifying
  1109. coord = self.Pixel2Cell(self.mouse['end'])
  1110. if self.parent.toolbars['georect']:
  1111. coordtype = 'gcpcoord'
  1112. else:
  1113. coordtype = 'mapcoord'
  1114. self.parent.GetLayerManager().georectifying.SetGCPData(coordtype, coord, self)
  1115. self.UpdateMap(render = False, renderVector = False)
  1116. elif self.mouse["use"] == "pointer" and \
  1117. hasattr(self, "digit"):
  1118. self._onLeftUp(event)
  1119. elif (self.mouse['use'] == 'pointer' and
  1120. self.dragid >= 0):
  1121. # end drag of overlay decoration
  1122. if self.dragid < 99 and self.dragid in self.overlays:
  1123. self.overlays[self.dragid]['coords'] = self.pdc.GetIdBounds(self.dragid)
  1124. elif self.dragid > 100 and self.dragid in self.textdict:
  1125. self.textdict[self.dragid]['bbox'] = self.pdc.GetIdBounds(self.dragid)
  1126. else:
  1127. pass
  1128. self.dragid = None
  1129. self.currtxtid = None
  1130. elif self.mouse['use'] == 'legend':
  1131. self.ResizeLegend(self.mouse["begin"], self.mouse["end"])
  1132. self.parent.dialogs['legend'].FindWindowByName("resize").SetValue(False)
  1133. self.Map.GetOverlay(1).SetActive(True)
  1134. self.parent.MapWindow.SetCursor(self.parent.cursors["default"])
  1135. self.parent.MapWindow.mouse['use'] = 'pointer'
  1136. self.UpdateMap()
  1137. def OnButtonDClick(self, event):
  1138. """!Mouse button double click
  1139. """
  1140. Debug.msg (5, "BufferedWindow.OnButtonDClick(): use=%s" % \
  1141. self.mouse["use"])
  1142. if self.mouse["use"] == "measure":
  1143. # measure
  1144. self.ClearLines(pdc=self.pdcTmp)
  1145. self.polycoords = []
  1146. self.mouse['use'] = 'pointer'
  1147. self.mouse['box'] = 'point'
  1148. self.mouse['end'] = [0, 0]
  1149. self.Refresh()
  1150. self.SetCursor(self.parent.cursors["default"])
  1151. elif self.mouse["use"] != "profile" or \
  1152. (self.mouse['use'] != 'pointer' and \
  1153. hasattr(self, "digit")):
  1154. # select overlay decoration options dialog
  1155. clickposition = event.GetPositionTuple()[:]
  1156. idlist = self.pdc.FindObjects(clickposition[0], clickposition[1], self.hitradius)
  1157. if idlist == []:
  1158. return
  1159. self.dragid = idlist[0]
  1160. # self.ovlcoords[self.dragid] = self.pdc.GetIdBounds(self.dragid)
  1161. if self.dragid > 100:
  1162. self.currtxtid = self.dragid
  1163. self.parent.OnAddText(None)
  1164. elif self.dragid == 0:
  1165. self.parent.OnAddBarscale(None)
  1166. elif self.dragid == 1:
  1167. self.parent.OnAddLegend(None)
  1168. def OnRightDown(self, event):
  1169. """!Right mouse button pressed
  1170. """
  1171. Debug.msg (5, "BufferedWindow.OnRightDown(): use=%s" % \
  1172. self.mouse["use"])
  1173. if hasattr(self, "digit"):
  1174. self._onRightDown(event)
  1175. event.Skip()
  1176. def OnRightUp(self, event):
  1177. """!Right mouse button released
  1178. """
  1179. Debug.msg (5, "BufferedWindow.OnRightUp(): use=%s" % \
  1180. self.mouse["use"])
  1181. if hasattr(self, "digit"):
  1182. self._onRightUp(event)
  1183. self.redrawAll = True
  1184. self.Refresh()
  1185. event.Skip()
  1186. def OnMiddleDown(self, event):
  1187. """!Middle mouse button pressed
  1188. """
  1189. if not event:
  1190. return
  1191. self.mouse['begin'] = event.GetPositionTuple()[:]
  1192. def OnMiddleUp(self, event):
  1193. """!Middle mouse button released
  1194. """
  1195. self.mouse['end'] = event.GetPositionTuple()[:]
  1196. # set region in zoom or pan
  1197. begin = self.mouse['begin']
  1198. end = self.mouse['end']
  1199. self.Zoom(begin, end, 0) # no zoom
  1200. # redraw map
  1201. self.UpdateMap(render = True)
  1202. # update statusbar
  1203. self.parent.StatusbarUpdate()
  1204. def OnMouseEnter(self, event):
  1205. """!Mouse entered window and no mouse buttons were pressed
  1206. """
  1207. if not self.parent.IsStandalone() and \
  1208. self.parent.GetLayerManager().gcpmanagement:
  1209. if self.parent.toolbars['gcpdisp']:
  1210. if not self.parent.MapWindow == self:
  1211. self.parent.MapWindow = self
  1212. self.parent.Map = self.Map
  1213. self.parent.UpdateActive(self)
  1214. # needed for wingrass
  1215. self.SetFocus()
  1216. else:
  1217. event.Skip()
  1218. def OnMouseMoving(self, event):
  1219. """!Motion event and no mouse buttons were pressed
  1220. """
  1221. if self.mouse["use"] == "pointer" and \
  1222. hasattr(self, "digit"):
  1223. self._onMouseMoving(event)
  1224. event.Skip()
  1225. def ClearLines(self, pdc = None):
  1226. """!Clears temporary drawn lines from PseudoDC
  1227. """
  1228. if not pdc:
  1229. pdc = self.pdcTmp
  1230. try:
  1231. pdc.ClearId(self.lineid)
  1232. pdc.RemoveId(self.lineid)
  1233. except:
  1234. pass
  1235. try:
  1236. pdc.ClearId(self.plineid)
  1237. pdc.RemoveId(self.plineid)
  1238. except:
  1239. pass
  1240. Debug.msg(4, "BufferedWindow.ClearLines(): lineid=%s, plineid=%s" %
  1241. (self.lineid, self.plineid))
  1242. return True
  1243. def Pixel2Cell(self, (x, y)):
  1244. """!Convert image coordinates to real word coordinates
  1245. @param x, y image coordinates
  1246. @return easting, northing
  1247. @return None on error
  1248. """
  1249. try:
  1250. x = int(x)
  1251. y = int(y)
  1252. except:
  1253. return None
  1254. if self.Map.region["ewres"] > self.Map.region["nsres"]:
  1255. res = self.Map.region["ewres"]
  1256. else:
  1257. res = self.Map.region["nsres"]
  1258. w = self.Map.region["center_easting"] - (self.Map.width / 2) * res
  1259. n = self.Map.region["center_northing"] + (self.Map.height / 2) * res
  1260. east = w + x * res
  1261. north = n - y * res
  1262. return (east, north)
  1263. def Cell2Pixel(self, (east, north)):
  1264. """!Convert real word coordinates to image coordinates
  1265. """
  1266. try:
  1267. east = float(east)
  1268. north = float(north)
  1269. except:
  1270. return None
  1271. if self.Map.region["ewres"] > self.Map.region["nsres"]:
  1272. res = self.Map.region["ewres"]
  1273. else:
  1274. res = self.Map.region["nsres"]
  1275. w = self.Map.region["center_easting"] - (self.Map.width / 2) * res
  1276. n = self.Map.region["center_northing"] + (self.Map.height / 2) * res
  1277. x = (east - w) / res
  1278. y = (n - north) / res
  1279. return (x, y)
  1280. def ResizeLegend(self, begin, end):
  1281. w = abs(begin[0] - end[0])
  1282. h = abs(begin[1] - end[1])
  1283. if begin[0] < end[0]:
  1284. x = begin[0]
  1285. else:
  1286. x = end[0]
  1287. if begin[1] < end[1]:
  1288. y = begin[1]
  1289. else:
  1290. y = end[1]
  1291. screenRect = wx.Rect(x, y, w, h)
  1292. screenSize = self.GetClientSizeTuple()
  1293. at = [(screenSize[1] - (y + h)) / float(screenSize[1]) * 100,
  1294. (screenSize[1] - y) / float(screenSize[1]) * 100,
  1295. x / float(screenSize[0]) * 100,
  1296. (x + w) / float(screenSize[0]) * 100]
  1297. for i, subcmd in enumerate(self.overlays[1]['cmd']):
  1298. if subcmd.startswith('at='):
  1299. self.overlays[1]['cmd'][i] = "at=%d,%d,%d,%d" % (at[0], at[1], at[2], at[3])
  1300. self.Map.ChangeOverlay(1, True, command = self.overlays[1]['cmd'])
  1301. self.overlays[1]['coords'] = (0,0)
  1302. def Zoom(self, begin, end, zoomtype):
  1303. """!Calculates new region while (un)zoom/pan-ing
  1304. """
  1305. x1, y1 = begin
  1306. x2, y2 = end
  1307. newreg = {}
  1308. # threshold - too small squares do not make sense
  1309. # can only zoom to windows of > 5x5 screen pixels
  1310. if abs(x2-x1) > 5 and abs(y2-y1) > 5 and zoomtype != 0:
  1311. if x1 > x2:
  1312. x1, x2 = x2, x1
  1313. if y1 > y2:
  1314. y1, y2 = y2, y1
  1315. # zoom in
  1316. if zoomtype > 0:
  1317. newreg['w'], newreg['n'] = self.Pixel2Cell((x1, y1))
  1318. newreg['e'], newreg['s'] = self.Pixel2Cell((x2, y2))
  1319. # zoom out
  1320. elif zoomtype < 0:
  1321. newreg['w'], newreg['n'] = self.Pixel2Cell((-x1 * 2, -y1 * 2))
  1322. newreg['e'], newreg['s'] = self.Pixel2Cell((self.Map.width + 2 * \
  1323. (self.Map.width - x2),
  1324. self.Map.height + 2 * \
  1325. (self.Map.height - y2)))
  1326. # pan
  1327. elif zoomtype == 0:
  1328. dx = x1 - x2
  1329. dy = y1 - y2
  1330. if dx == 0 and dy == 0:
  1331. dx = x1 - self.Map.width / 2
  1332. dy = y1 - self.Map.height / 2
  1333. newreg['w'], newreg['n'] = self.Pixel2Cell((dx, dy))
  1334. newreg['e'], newreg['s'] = self.Pixel2Cell((self.Map.width + dx,
  1335. self.Map.height + dy))
  1336. # if new region has been calculated, set the values
  1337. if newreg != {}:
  1338. # LL locations
  1339. if self.parent.Map.projinfo['proj'] == 'll':
  1340. self.region['n'] = min(self.region['n'], 90.0)
  1341. self.region['s'] = max(self.region['s'], -90.0)
  1342. ce = newreg['w'] + (newreg['e'] - newreg['w']) / 2
  1343. cn = newreg['s'] + (newreg['n'] - newreg['s']) / 2
  1344. # calculate new center point and display resolution
  1345. self.Map.region['center_easting'] = ce
  1346. self.Map.region['center_northing'] = cn
  1347. self.Map.region['ewres'] = (newreg['e'] - newreg['w']) / self.Map.width
  1348. self.Map.region['nsres'] = (newreg['n'] - newreg['s']) / self.Map.height
  1349. if self.parent.statusbarWin['alignExtent'].IsChecked():
  1350. self.Map.AlignExtentFromDisplay()
  1351. else:
  1352. for k in ('n', 's', 'e', 'w'):
  1353. self.Map.region[k] = newreg[k]
  1354. if hasattr(self, "digit") and \
  1355. hasattr(self, "moveInfo"):
  1356. self._zoom(event)
  1357. self.ZoomHistory(self.Map.region['n'], self.Map.region['s'],
  1358. self.Map.region['e'], self.Map.region['w'])
  1359. if self.redrawAll is False:
  1360. self.redrawAll = True
  1361. def ZoomBack(self):
  1362. """!Zoom to previous extents in zoomhistory list
  1363. """
  1364. zoom = list()
  1365. if len(self.zoomhistory) > 1:
  1366. self.zoomhistory.pop()
  1367. zoom = self.zoomhistory[-1]
  1368. # disable tool if stack is empty
  1369. if len(self.zoomhistory) < 2: # disable tool
  1370. if self.parent.GetName() == 'MapWindow':
  1371. toolbar = self.parent.toolbars['map']
  1372. elif self.parent.GetName() == 'GRMapWindow':
  1373. toolbar = self.parent.toolbars['georect']
  1374. elif self.parent.GetName() == 'GCPMapWindow':
  1375. toolbar = self.parent.toolbars['gcpdisp']
  1376. toolbar.Enable('zoomback', enable = False)
  1377. # zoom to selected region
  1378. self.Map.GetRegion(n = zoom[0], s = zoom[1],
  1379. e = zoom[2], w = zoom[3],
  1380. update = True)
  1381. # update map
  1382. self.UpdateMap()
  1383. # update statusbar
  1384. self.parent.StatusbarUpdate()
  1385. def ZoomHistory(self, n, s, e, w):
  1386. """!Manages a list of last 10 zoom extents
  1387. @param n,s,e,w north, south, east, west
  1388. @return removed history item if exists (or None)
  1389. """
  1390. removed = None
  1391. self.zoomhistory.append((n,s,e,w))
  1392. if len(self.zoomhistory) > 10:
  1393. removed = self.zoomhistory.pop(0)
  1394. if removed:
  1395. Debug.msg(4, "BufferedWindow.ZoomHistory(): hist=%s, removed=%s" %
  1396. (self.zoomhistory, removed))
  1397. else:
  1398. Debug.msg(4, "BufferedWindow.ZoomHistory(): hist=%s" %
  1399. (self.zoomhistory))
  1400. # update toolbar
  1401. if len(self.zoomhistory) > 1:
  1402. enable = True
  1403. else:
  1404. enable = False
  1405. if self.parent.GetName() == 'MapWindow':
  1406. toolbar = self.parent.toolbars['map']
  1407. elif self.parent.GetName() == 'GRMapWindow':
  1408. toolbar = self.parent.toolbars['georect']
  1409. elif self.parent.GetName() == 'GCPMapWindow':
  1410. toolbar = self.parent.toolbars['gcpdisp']
  1411. toolbar.Enable('zoomback', enable)
  1412. return removed
  1413. def ResetZoomHistory(self):
  1414. """!Reset zoom history"""
  1415. self.zoomhistory = list()
  1416. def ZoomToMap(self, layers = None, ignoreNulls = False, render = True):
  1417. """!Set display extents to match selected raster
  1418. or vector map(s).
  1419. @param layers list of layers to be zoom to
  1420. @param ignoreNulls True to ignore null-values (valid only for rasters)
  1421. @param render True to re-render display
  1422. """
  1423. zoomreg = {}
  1424. if not layers:
  1425. layers = self.GetSelectedLayer(multi = True)
  1426. if not layers:
  1427. return
  1428. rast = []
  1429. vect = []
  1430. updated = False
  1431. for l in layers:
  1432. # only raster/vector layers are currently supported
  1433. if l.type == 'raster':
  1434. rast.append(l.GetName())
  1435. elif l.type == 'vector':
  1436. if hasattr(self, "digit") and \
  1437. self.toolbar.GetLayer() == l:
  1438. w, s, b, e, n, t = self.digit.GetDisplay().GetMapBoundingBox()
  1439. self.Map.GetRegion(n = n, s = s, w = w, e = e,
  1440. update = True)
  1441. updated = True
  1442. else:
  1443. vect.append(l.name)
  1444. elif l.type == 'rgb':
  1445. for rname in l.GetName().splitlines():
  1446. rast.append(rname)
  1447. if not updated:
  1448. self.Map.GetRegion(rast = rast,
  1449. vect = vect,
  1450. update = True)
  1451. self.ZoomHistory(self.Map.region['n'], self.Map.region['s'],
  1452. self.Map.region['e'], self.Map.region['w'])
  1453. if render:
  1454. self.UpdateMap()
  1455. self.parent.StatusbarUpdate()
  1456. def ZoomToWind(self):
  1457. """!Set display geometry to match computational region
  1458. settings (set with g.region)
  1459. """
  1460. self.Map.region = self.Map.GetRegion()
  1461. self.ZoomHistory(self.Map.region['n'], self.Map.region['s'],
  1462. self.Map.region['e'], self.Map.region['w'])
  1463. self.UpdateMap()
  1464. self.parent.StatusbarUpdate()
  1465. def ZoomToDefault(self):
  1466. """!Set display geometry to match default region settings
  1467. """
  1468. self.Map.region = self.Map.GetRegion(default = True)
  1469. self.Map.AdjustRegion() # aling region extent to the display
  1470. self.ZoomHistory(self.Map.region['n'], self.Map.region['s'],
  1471. self.Map.region['e'], self.Map.region['w'])
  1472. self.UpdateMap()
  1473. self.parent.StatusbarUpdate()
  1474. def DisplayToWind(self):
  1475. """!Set computational region (WIND file) to match display
  1476. extents
  1477. """
  1478. tmpreg = os.getenv("GRASS_REGION")
  1479. if tmpreg:
  1480. del os.environ["GRASS_REGION"]
  1481. # We ONLY want to set extents here. Don't mess with resolution. Leave that
  1482. # for user to set explicitly with g.region
  1483. new = self.Map.AlignResolution()
  1484. gcmd.RunCommand('g.region',
  1485. parent = self,
  1486. overwrite = True,
  1487. n = new['n'],
  1488. s = new['s'],
  1489. e = new['e'],
  1490. w = new['w'],
  1491. rows = int(new['rows']),
  1492. cols = int(new['cols']))
  1493. if tmpreg:
  1494. os.environ["GRASS_REGION"] = tmpreg
  1495. def ZoomToSaved(self):
  1496. """!Set display geometry to match extents in
  1497. saved region file
  1498. """
  1499. dlg = gdialogs.SavedRegion(parent = self,
  1500. title = _("Zoom to saved region extents"),
  1501. loadsave='load')
  1502. if dlg.ShowModal() == wx.ID_CANCEL or not dlg.wind:
  1503. dlg.Destroy()
  1504. return
  1505. if not grass.find_file(name = dlg.wind, element = 'windows')['name']:
  1506. wx.MessageBox(parent = self,
  1507. message = _("Region <%s> not found. Operation canceled.") % dlg.wind,
  1508. caption = _("Error"), style = wx.ICON_ERROR | wx.OK | wx.CENTRE)
  1509. dlg.Destroy()
  1510. return
  1511. self.Map.GetRegion(regionName = dlg.wind,
  1512. update = True)
  1513. dlg.Destroy()
  1514. self.ZoomHistory(self.Map.region['n'],
  1515. self.Map.region['s'],
  1516. self.Map.region['e'],
  1517. self.Map.region['w'])
  1518. self.UpdateMap()
  1519. def SaveDisplayRegion(self):
  1520. """!Save display extents to named region file.
  1521. """
  1522. dlg = gdialogs.SavedRegion(parent = self,
  1523. title = _("Save display extents to region file"),
  1524. loadsave='save')
  1525. if dlg.ShowModal() == wx.ID_CANCEL or not dlg.wind:
  1526. dlg.Destroy()
  1527. return
  1528. # test to see if it already exists and ask permission to overwrite
  1529. if grass.find_file(name = dlg.wind, element = 'windows')['name']:
  1530. overwrite = wx.MessageBox(parent = self,
  1531. message = _("Region file <%s> already exists. "
  1532. "Do you want to overwrite it?") % (dlg.wind),
  1533. caption = _("Warning"), style = wx.YES_NO | wx.CENTRE)
  1534. if (overwrite == wx.YES):
  1535. self.SaveRegion(dlg.wind)
  1536. else:
  1537. self.SaveRegion(dlg.wind)
  1538. dlg.Destroy()
  1539. def SaveRegion(self, wind):
  1540. """!Save region settings
  1541. @param wind region name
  1542. """
  1543. new = self.Map.GetCurrentRegion()
  1544. tmpreg = os.getenv("GRASS_REGION")
  1545. if tmpreg:
  1546. del os.environ["GRASS_REGION"]
  1547. gcmd.RunCommand('g.region',
  1548. overwrite = True,
  1549. parent = self,
  1550. flags = 'u',
  1551. n = new['n'],
  1552. s = new['s'],
  1553. e = new['e'],
  1554. w = new['w'],
  1555. rows = int(new['rows']),
  1556. cols = int(new['cols']),
  1557. save = wind)
  1558. if tmpreg:
  1559. os.environ["GRASS_REGION"] = tmpreg
  1560. def Distance(self, beginpt, endpt, screen = True):
  1561. """!Calculete distance
  1562. Ctypes required for LL-locations
  1563. @param beginpt first point
  1564. @param endpt second point
  1565. @param screen True for screen coordinates otherwise EN
  1566. """
  1567. if screen:
  1568. e1, n1 = self.Pixel2Cell(beginpt)
  1569. e2, n2 = self.Pixel2Cell(endpt)
  1570. else:
  1571. e1, n1 = beginpt
  1572. e2, n2 = endpt
  1573. dEast = (e2 - e1)
  1574. dNorth = (n2 - n1)
  1575. if self.parent.Map.projinfo['proj'] == 'll' and haveCtypes:
  1576. dist = gislib.G_distance(e1, n1, e2, n2)
  1577. else:
  1578. dist = math.sqrt(math.pow((dEast), 2) + math.pow((dNorth), 2))
  1579. return (dist, (dEast, dNorth))