profile.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. """
  2. @package wxplot.profile
  3. @brief Profiling using PyPlot
  4. Classes:
  5. - profile::ProfileFrame
  6. - profile::ProfileToolbar
  7. (C) 2011-2014 by the GRASS Development Team
  8. This program is free software under the GNU General Public License
  9. (>=v2). Read the file COPYING that comes with GRASS for details.
  10. @author Michael Barton, Arizona State University
  11. """
  12. import os
  13. import sys
  14. import six
  15. import math
  16. import numpy
  17. import wx
  18. import gui_core.wxlibplot as plot
  19. import grass.script as grass
  20. from core.utils import _
  21. from wxplot.base import BasePlotFrame, PlotIcons
  22. from gui_core.toolbars import BaseToolbar, BaseIcons
  23. from gui_core.wrap import StockCursor
  24. from wxplot.dialogs import ProfileRasterDialog, PlotStatsFrame
  25. from core.gcmd import RunCommand, GWarning, GError, GMessage
  26. class ProfileFrame(BasePlotFrame):
  27. """Mainframe for displaying profile of one or more raster maps. Uses wx.lib.plot.
  28. """
  29. def __init__(self, parent, controller, units, size=wx.Size(700, 400),
  30. rasterList=None, **kwargs):
  31. BasePlotFrame.__init__(self, parent=parent, size=size, **kwargs)
  32. self.controller = controller
  33. self.controller.transectChanged.connect(self.SetTransect)
  34. self.transect = []
  35. self.toolbar = ProfileToolbar(parent=self)
  36. # workaround for http://trac.wxwidgets.org/ticket/13888
  37. if sys.platform != 'darwin':
  38. self.SetToolBar(self.toolbar)
  39. self.SetTitle(_("GRASS Profile Analysis Tool"))
  40. # in case of degrees, we want to use meters
  41. self._units = units if 'degree' not in units else 'meters'
  42. #
  43. # Init variables
  44. #
  45. if rasterList is None:
  46. self.rasterList = []
  47. else:
  48. self.rasterList = rasterList
  49. self.plottype = 'profile'
  50. self.coordstr = '' # string of coordinates for r.profile
  51. self.seglist = [] # segment endpoint list
  52. self.ppoints = '' # segment endpoints data
  53. self.transect_length = 0.0 # total transect length
  54. self.ptitle = _('Profile of') # title of window
  55. self.colorList = [
  56. "blue",
  57. "red",
  58. "green",
  59. "yellow",
  60. "magenta",
  61. "cyan",
  62. "aqua",
  63. "black",
  64. "grey",
  65. "orange",
  66. "brown",
  67. "purple",
  68. "violet",
  69. "indigo"]
  70. self._initOpts()
  71. if len(
  72. self.rasterList) > 0: # set raster name(s) from layer manager if a map is selected
  73. self.raster = self.InitRasterOpts(self.rasterList, self.plottype)
  74. else:
  75. self.raster = {}
  76. # determine units (axis labels)
  77. # maybe, we should not accept these invalid units
  78. # but ok, trying to handle it here
  79. if self._units is not None and self._units != '':
  80. self.xlabel = _('Distance (%s)') % self._units
  81. else:
  82. self.xlabel = _("Distance along transect")
  83. self.ylabel = _("Cell values")
  84. # Bind events
  85. self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
  86. self.SetGraphStyle()
  87. def _initOpts(self):
  88. """Initialize plot options
  89. """
  90. self.InitPlotOpts('profile')
  91. def SetTransect(self, coords):
  92. self.transect = coords
  93. if coords:
  94. self.OnCreateProfile(None)
  95. else:
  96. self.OnErase(None)
  97. def OnDrawTransect(self, event):
  98. """Draws transect to profile in map display
  99. """
  100. if self.controller.IsActive():
  101. self.controller.Stop()
  102. self.controller.Start()
  103. self.parent.SetFocus()
  104. self.parent.Raise()
  105. def OnSelectRaster(self, event):
  106. """Select raster map(s) to profile
  107. """
  108. dlg = ProfileRasterDialog(parent=self)
  109. dlg.CenterOnParent()
  110. if dlg.ShowModal() == wx.ID_OK:
  111. self.rasterList = dlg.rasterList
  112. self.raster = self.InitRasterOpts(self.rasterList, self.plottype)
  113. # plot profile
  114. if len(self.transect) > 0 and len(self.rasterList) > 0:
  115. self.OnCreateProfile(event=None)
  116. dlg.Destroy()
  117. def SetupProfile(self):
  118. """Create coordinate string for profiling. Create segment
  119. list for transect segment markers.
  120. """
  121. # create list of coordinate points for r.profile
  122. dist = 0
  123. cumdist = 0
  124. self.coordstr = ''
  125. lasteast = lastnorth = None
  126. region = grass.region()
  127. insideRegion = True
  128. if len(self.transect) > 0:
  129. for point in self.transect:
  130. if not (region['w'] <= point[0] <= region['e']
  131. and region['s'] <= point[1] <= region['n']):
  132. insideRegion = False
  133. # build string of coordinate points for r.profile
  134. if self.coordstr == '':
  135. self.coordstr = '%f,%f' % (point[0], point[1])
  136. else:
  137. self.coordstr = '%s,%f,%f' % (
  138. self.coordstr, point[0], point[1])
  139. if not insideRegion:
  140. GWarning(
  141. message=_(
  142. "Not all points of profile lie inside computational region."),
  143. parent=self)
  144. if len(self.rasterList) == 0:
  145. return
  146. # title of window
  147. self.ptitle = _('Profile of')
  148. # create list of coordinates for transect segment markers
  149. if len(self.transect) > 0:
  150. self.seglist = []
  151. for point in self.transect:
  152. # get value of raster cell at coordinate point
  153. ret = RunCommand('r.what',
  154. parent=self,
  155. read=True,
  156. map=self.rasterList[0],
  157. coordinates='%f,%f' % (point[0], point[1]))
  158. val = ret.splitlines()[0].split('|')[3]
  159. if val is None or val == '*':
  160. continue
  161. val = float(val)
  162. # calculate distance between coordinate points
  163. if lasteast and lastnorth:
  164. dist = math.sqrt(
  165. math.pow((lasteast - point[0]),
  166. 2) + math.pow((lastnorth - point[1]),
  167. 2))
  168. cumdist += dist
  169. # store total transect length
  170. self.transect_length = cumdist
  171. # build a list of distance,value pairs for each segment of
  172. # transect
  173. self.seglist.append((cumdist, val))
  174. lasteast = point[0]
  175. lastnorth = point[1]
  176. # delete extra first segment point
  177. try:
  178. self.seglist.pop(0)
  179. except:
  180. pass
  181. #
  182. # create datalist of dist/value pairs and y labels for each raster map
  183. #
  184. self.ylabel = ''
  185. i = 0
  186. for r in six.iterkeys(self.raster):
  187. self.raster[r]['datalist'] = []
  188. datalist = self.CreateDatalist(r, self.coordstr)
  189. if len(datalist) > 0:
  190. self.raster[r]['datalist'] = datalist
  191. # update ylabel to match units if they exist
  192. if self.raster[r]['units'] != '':
  193. self.ylabel += '%s (%d),' % (self.raster[r]['units'], i)
  194. i += 1
  195. # update title
  196. self.ptitle += ' %s ,' % r.split('@')[0]
  197. self.ptitle = self.ptitle.rstrip(',')
  198. if self.ylabel == '':
  199. self.ylabel = _('Raster values')
  200. else:
  201. self.ylabel = self.ylabel.rstrip(',')
  202. def CreateDatalist(self, raster, coords):
  203. """Build a list of distance, value pairs for points along transect using r.profile
  204. """
  205. datalist = []
  206. # keep total number of transect points to 500 or less to avoid
  207. # freezing with large, high resolution maps
  208. region = grass.region()
  209. curr_res = min(float(region['nsres']), float(region['ewres']))
  210. transect_rec = 0
  211. if self.transect_length / curr_res > 500:
  212. transect_res = self.transect_length / 500
  213. else:
  214. transect_res = curr_res
  215. ret = RunCommand("r.profile",
  216. parent=self,
  217. input=raster,
  218. coordinates=coords,
  219. resolution=transect_res,
  220. null="nan",
  221. quiet=True,
  222. read=True)
  223. if not ret:
  224. return []
  225. for line in ret.splitlines():
  226. dist, elev = line.strip().split(' ')
  227. if dist is None or dist == '' or dist == 'nan' or \
  228. elev is None or elev == '' or elev == 'nan':
  229. continue
  230. dist = float(dist)
  231. elev = float(elev)
  232. datalist.append((dist, elev))
  233. return datalist
  234. def OnCreateProfile(self, event):
  235. """Main routine for creating a profile. Uses r.profile to
  236. create a list of distance,cell value pairs. This is passed to
  237. plot to create a line graph of the profile. If the profile
  238. transect is in multiple segments, these are drawn as
  239. points. Profile transect is drawn, using methods in mapdisp.py
  240. """
  241. if len(self.transect) == 0 or len(self.rasterList) == 0:
  242. dlg = wx.MessageDialog(
  243. parent=self,
  244. message=_('You must draw a transect to profile in the map display window.'),
  245. caption=_('Nothing to profile'),
  246. style=wx.OK | wx.ICON_INFORMATION | wx.CENTRE)
  247. dlg.ShowModal()
  248. dlg.Destroy()
  249. return
  250. self.SetCursor(StockCursor(wx.CURSOR_ARROW))
  251. self.SetupProfile()
  252. p = self.CreatePlotList()
  253. self.DrawPlot(p)
  254. def CreatePlotList(self):
  255. """Create a plot data list from transect datalist and
  256. transect segment endpoint coordinates.
  257. """
  258. # graph the distance, value pairs for the transect
  259. self.plotlist = []
  260. # Add segment marker points to plot data list
  261. if len(self.seglist) > 0:
  262. self.ppoints = plot.PolyMarker(
  263. self.seglist,
  264. legend=' ' + self.properties['marker']['legend'],
  265. colour=wx.Colour(
  266. self.properties['marker']['color'][0],
  267. self.properties['marker']['color'][1],
  268. self.properties['marker']['color'][2],
  269. 255),
  270. size=self.properties['marker']['size'],
  271. fillstyle=self.ptfilldict[
  272. self.properties['marker']['fill']],
  273. marker=self.properties['marker']['type'])
  274. self.plotlist.append(self.ppoints)
  275. # Add profile distance/elevation pairs to plot data list for each
  276. # raster profiled
  277. for r in self.rasterList:
  278. col = wx.Colour(self.raster[r]['pcolor'][0],
  279. self.raster[r]['pcolor'][1],
  280. self.raster[r]['pcolor'][2],
  281. 255)
  282. self.raster[r]['pline'] = plot.PolyLine(
  283. self.raster[r]['datalist'],
  284. colour=col, width=self.raster[r]['pwidth'],
  285. style=self.linestyledict[self.raster[r]['pstyle']],
  286. legend=self.raster[r]['plegend'])
  287. self.plotlist.append(self.raster[r]['pline'])
  288. if len(self.plotlist) > 0:
  289. return self.plotlist
  290. else:
  291. return None
  292. def Update(self):
  293. """Update profile after changing options
  294. """
  295. self.SetGraphStyle()
  296. p = self.CreatePlotList()
  297. self.DrawPlot(p)
  298. def SaveProfileToFile(self, event):
  299. """Save r.profile data to a csv file
  300. """
  301. dlg = wx.FileDialog(
  302. parent=self,
  303. message=_(
  304. "Choose prefix for file(s) where to save profile values..."),
  305. defaultDir=os.getcwd(),
  306. wildcard=_("Comma separated value (*.csv)|*.csv"),
  307. style=wx.FD_SAVE)
  308. pfile = []
  309. if dlg.ShowModal() == wx.ID_OK:
  310. path = dlg.GetPath()
  311. for r in self.rasterList:
  312. pfile.append(path + '_' + str(r.replace('@', '_')) + '.csv')
  313. if os.path.exists(pfile[-1]):
  314. dlgOv = wx.MessageDialog(
  315. self,
  316. message=_(
  317. "File <%s> already exists. "
  318. "Do you want to overwrite this file?") % pfile
  319. [-1],
  320. caption=_("Overwrite file?"),
  321. style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION)
  322. if dlgOv.ShowModal() != wx.ID_YES:
  323. pfile.pop()
  324. dlgOv.Destroy()
  325. continue
  326. try:
  327. fd = open(pfile[-1], "w")
  328. except IOError as e:
  329. GError(parent=self,
  330. message=_("Unable to open file <%s> for writing.\n"
  331. "Reason: %s") % (pfile[-1], e))
  332. dlg.Destroy()
  333. return
  334. for datapair in self.raster[r]['datalist']:
  335. fd.write(
  336. '%.6f,%.6f\n' %
  337. (float(
  338. datapair[0]), float(
  339. datapair[1])))
  340. fd.close()
  341. dlg.Destroy()
  342. if pfile:
  343. message = _("%d files created:\n%s") % (
  344. len(pfile), '\n'.join(pfile))
  345. else:
  346. message = _("No files generated.")
  347. GMessage(parent=self, message=message)
  348. def OnStats(self, event):
  349. """Displays regression information in messagebox
  350. """
  351. message = []
  352. title = _('Statistics for Profile(s)')
  353. for r in six.iterkeys(self.raster):
  354. try:
  355. rast = r.split('@')[0]
  356. statstr = 'Profile of %s\n\n' % rast
  357. iterable = (i[1] for i in self.raster[r]['datalist'])
  358. a = numpy.fromiter(iterable, numpy.float)
  359. statstr += 'n: %f\n' % a.size
  360. statstr += 'minimum: %f\n' % numpy.amin(a)
  361. statstr += 'maximum: %f\n' % numpy.amax(a)
  362. statstr += 'range: %f\n' % numpy.ptp(a)
  363. statstr += 'mean: %f\n' % numpy.mean(a)
  364. statstr += 'standard deviation: %f\n' % numpy.std(a)
  365. statstr += 'variance: %f\n' % numpy.var(a)
  366. cv = numpy.std(a) / numpy.mean(a)
  367. statstr += 'coefficient of variation: %f\n' % cv
  368. statstr += 'sum: %f\n' % numpy.sum(a)
  369. statstr += 'median: %f\n' % numpy.median(a)
  370. statstr += 'distance along transect: %f\n\n' % self.transect_length
  371. message.append(statstr)
  372. except:
  373. pass
  374. stats = PlotStatsFrame(self, id=wx.ID_ANY, message=message,
  375. title=title)
  376. if stats.Show() == wx.ID_CLOSE:
  377. stats.Destroy()
  378. def OnCloseWindow(self, event):
  379. if self.controller.IsActive():
  380. self.controller.Stop()
  381. self.Destroy()
  382. class ProfileToolbar(BaseToolbar):
  383. """Toolbar for profiling raster map
  384. """
  385. def __init__(self, parent):
  386. BaseToolbar.__init__(self, parent)
  387. # workaround for http://trac.wxwidgets.org/ticket/13888
  388. if sys.platform == 'darwin':
  389. parent.SetToolBar(self)
  390. self.InitToolbar(self._toolbarData())
  391. # realize the toolbar
  392. self.Realize()
  393. def _toolbarData(self):
  394. """Toolbar data"""
  395. return self._getToolbarData((('addraster', BaseIcons["addRast"],
  396. self.parent.OnSelectRaster),
  397. ('transect', PlotIcons["transect"],
  398. self.parent.OnDrawTransect),
  399. (None, ),
  400. ('draw', PlotIcons["draw"],
  401. self.parent.OnCreateProfile),
  402. ('erase', BaseIcons["erase"],
  403. self.parent.OnErase),
  404. ('drag', BaseIcons['pan'],
  405. self.parent.OnDrag),
  406. ('zoom', BaseIcons['zoomIn'],
  407. self.parent.OnZoom),
  408. ('unzoom', BaseIcons['zoomBack'],
  409. self.parent.OnRedraw),
  410. (None, ),
  411. ('statistics', PlotIcons['statistics'],
  412. self.parent.OnStats),
  413. ('datasave', PlotIcons["save"],
  414. self.parent.SaveProfileToFile),
  415. ('image', BaseIcons["saveFile"],
  416. self.parent.SaveToFile),
  417. ('print', BaseIcons["print"],
  418. self.parent.PrintMenu),
  419. (None, ),
  420. ('settings', PlotIcons["options"],
  421. self.parent.PlotOptionsMenu),
  422. ('quit', PlotIcons["quit"],
  423. self.parent.OnQuit),
  424. ))