profile.py 18 KB

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