profile.py 18 KB

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