profile.py 18 KB

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