base.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  1. """
  2. @package wxplot.base
  3. @brief Base classes for iinteractive plotting using PyPlot
  4. Classes:
  5. - base::PlotIcons
  6. - base::BasePlotFrame
  7. (C) 2011 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 six
  14. import wx
  15. from random import randint
  16. import wx.lib.plot as plot
  17. from core.globalvar import ICONDIR
  18. from core.settings import UserSettings
  19. from wxplot.dialogs import TextDialog, OptDialog
  20. from core.render import Map
  21. from icons.icon import MetaIcon
  22. from gui_core.toolbars import BaseIcons
  23. from gui_core.wrap import Menu
  24. import grass.script as grass
  25. PlotIcons = {
  26. "draw": MetaIcon(img="show", label=_("Draw/re-draw plot")),
  27. "transect": MetaIcon(
  28. img="layer-raster-profile",
  29. label=_("Draw transect in map display window to profile"),
  30. ),
  31. "options": BaseIcons["settings"],
  32. "statistics": MetaIcon(img="stats", label=_("Plot statistics")),
  33. "save": MetaIcon(img="save", label=_("Save profile data to CSV file")),
  34. "quit": BaseIcons["quit"],
  35. }
  36. class BasePlotFrame(wx.Frame):
  37. """Abstract PyPlot display frame class"""
  38. def __init__(
  39. self,
  40. parent=None,
  41. giface=None,
  42. size=wx.Size(700, 400),
  43. style=wx.DEFAULT_FRAME_STYLE,
  44. rasterList=[],
  45. **kwargs,
  46. ):
  47. wx.Frame.__init__(self, parent, id=wx.ID_ANY, size=size, style=style, **kwargs)
  48. self.parent = parent # MapFrame for a plot type
  49. self._giface = giface
  50. self.Map = Map() # instance of render.Map to be associated with display
  51. self.rasterList = rasterList # list of rasters to plot
  52. self.raster = {} # dictionary of raster maps and their plotting parameters
  53. self.plottype = ""
  54. self.linestyledict = {
  55. "solid": wx.SOLID,
  56. "dot": wx.DOT,
  57. "long-dash": wx.LONG_DASH,
  58. "short-dash": wx.SHORT_DASH,
  59. "dot-dash": wx.DOT_DASH,
  60. }
  61. self.ptfilldict = {"transparent": wx.TRANSPARENT, "solid": wx.SOLID}
  62. #
  63. # Icon
  64. #
  65. self.SetIcon(wx.Icon(os.path.join(ICONDIR, "grass.ico"), wx.BITMAP_TYPE_ICO))
  66. #
  67. # Add statusbar
  68. #
  69. self.statusbar = self.CreateStatusBar(number=2, style=0)
  70. self.statusbar.SetStatusWidths([-2, -1])
  71. #
  72. # Define canvas and settings
  73. #
  74. #
  75. self.client = plot.PlotCanvas(self)
  76. # define the function for drawing pointLabels
  77. self.client.pointLabelFunc = self.DrawPointLabel
  78. # Create mouse event for showing cursor coords in status bar
  79. self.client.canvas.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
  80. # Show closest point when enabled
  81. self.client.canvas.Bind(wx.EVT_MOTION, self.OnMotion)
  82. self.plotlist = [] # list of things to plot
  83. self.plot = None # plot draw object
  84. self.ptitle = "" # title of window
  85. self.xlabel = "" # default X-axis label
  86. self.ylabel = "" # default Y-axis label
  87. self.CentreOnScreen()
  88. self._createColorDict()
  89. def _createColorDict(self):
  90. """Create color dictionary to return wx.Colour tuples
  91. for assigning colors to images in imagery groups"""
  92. self.colorDict = {}
  93. for clr in six.iterkeys(grass.named_colors):
  94. if clr == "white":
  95. continue
  96. r = int(grass.named_colors[clr][0] * 255)
  97. g = int(grass.named_colors[clr][1] * 255)
  98. b = int(grass.named_colors[clr][2] * 255)
  99. self.colorDict[clr] = (r, g, b, 255)
  100. def InitPlotOpts(self, plottype):
  101. """Initialize options for entire plot"""
  102. self.plottype = plottype # histogram, profile, or scatter
  103. self.properties = {} # plot properties
  104. self.properties["font"] = {}
  105. self.properties["font"]["prop"] = UserSettings.Get(
  106. group=self.plottype, key="font"
  107. )
  108. self.wx_font = wx.Font(
  109. self.properties["font"]["prop"]["defaultSize"],
  110. self.properties["font"]["prop"]["family"],
  111. self.properties["font"]["prop"]["style"],
  112. self.properties["font"]["prop"]["weight"],
  113. )
  114. self.properties["raster"] = {}
  115. self.properties["raster"] = UserSettings.Get(group=self.plottype, key="raster")
  116. colstr = str(self.properties["raster"]["pcolor"])
  117. self.properties["raster"]["pcolor"] = tuple(
  118. int(colval) for colval in colstr.strip("()").split(",")
  119. )
  120. if self.plottype == "profile":
  121. self.properties["marker"] = UserSettings.Get(
  122. group=self.plottype, key="marker"
  123. )
  124. # changing color string to tuple for markers/points
  125. colstr = str(self.properties["marker"]["color"])
  126. self.properties["marker"]["color"] = tuple(
  127. int(colval) for colval in colstr.strip("()").split(",")
  128. )
  129. self.properties["grid"] = UserSettings.Get(group=self.plottype, key="grid")
  130. # changing color string to tuple
  131. colstr = str(self.properties["grid"]["color"])
  132. self.properties["grid"]["color"] = tuple(
  133. int(colval) for colval in colstr.strip("()").split(",")
  134. )
  135. self.properties["x-axis"] = {}
  136. self.properties["x-axis"]["prop"] = UserSettings.Get(
  137. group=self.plottype, key="x-axis"
  138. )
  139. self.properties["x-axis"]["axis"] = None
  140. self.properties["y-axis"] = {}
  141. self.properties["y-axis"]["prop"] = UserSettings.Get(
  142. group=self.plottype, key="y-axis"
  143. )
  144. self.properties["y-axis"]["axis"] = None
  145. self.properties["legend"] = UserSettings.Get(group=self.plottype, key="legend")
  146. self.zoom = False # zooming disabled
  147. self.drag = False # draging disabled
  148. # vertical and horizontal scrollbars
  149. self.client.showScrollbars = True
  150. # x and y axis set to normal (non-log)
  151. self.client.logScale = (False, False)
  152. if self.properties["x-axis"]["prop"]["type"] == "custom":
  153. self.client.xSpec = "min"
  154. else:
  155. self.client.xSpec = self.properties["x-axis"]["prop"]["type"]
  156. if self.properties["y-axis"]["prop"]["type"] == "custom":
  157. self.client.ySpec = "min"
  158. else:
  159. self.client.ySpec = self.properties["y-axis"]["prop"]["type"]
  160. def InitRasterOpts(self, rasterList, plottype):
  161. """Initialize or update raster dictionary for plotting"""
  162. rdict = {} # initialize a dictionary
  163. self.properties["raster"] = UserSettings.Get(group=self.plottype, key="raster")
  164. for r in rasterList:
  165. idx = rasterList.index(r)
  166. try:
  167. ret = grass.raster_info(r)
  168. except:
  169. continue
  170. # if r.info cannot parse map, skip it
  171. self.raster[r] = self.properties["raster"] # some default settings
  172. rdict[r] = {} # initialize sub-dictionaries for each raster in the list
  173. rdict[r]["units"] = ""
  174. if ret["units"] not in ("(none)", '"none"', "", None):
  175. rdict[r]["units"] = ret["units"]
  176. rdict[r]["plegend"] = r # use fully-qualified names
  177. # list of cell value,frequency pairs for plotting histogram
  178. rdict[r]["datalist"] = []
  179. rdict[r]["pline"] = None
  180. rdict[r]["datatype"] = ret["datatype"]
  181. #
  182. # initialize with saved values
  183. #
  184. if self.properties["raster"]["pwidth"] is not None:
  185. rdict[r]["pwidth"] = self.properties["raster"]["pwidth"]
  186. else:
  187. rdict[r]["pwidth"] = 1
  188. if (
  189. self.properties["raster"]["pstyle"] is not None
  190. and self.properties["raster"]["pstyle"] != ""
  191. ):
  192. rdict[r]["pstyle"] = self.properties["raster"]["pstyle"]
  193. else:
  194. rdict[r]["pstyle"] = "solid"
  195. if idx < len(self.colorList):
  196. if idx == 0:
  197. # use saved color for first plot
  198. if self.properties["raster"]["pcolor"] is not None:
  199. rdict[r]["pcolor"] = self.properties["raster"]["pcolor"]
  200. else:
  201. rdict[r]["pcolor"] = self.colorDict[self.colorList[idx]]
  202. else:
  203. rdict[r]["pcolor"] = self.colorDict[self.colorList[idx]]
  204. else:
  205. r = randint(0, 255)
  206. b = randint(0, 255)
  207. g = randint(0, 255)
  208. rdict[r]["pcolor"] = (r, g, b, 255)
  209. return rdict
  210. def InitRasterPairs(self, rasterList, plottype):
  211. """Initialize or update raster dictionary with raster pairs for
  212. bivariate scatterplots
  213. """
  214. if len(rasterList) == 0:
  215. return
  216. rdict = {} # initialize a dictionary
  217. for rpair in rasterList:
  218. idx = rasterList.index(rpair)
  219. try:
  220. ret0 = grass.raster_info(rpair[0])
  221. ret1 = grass.raster_info(rpair[1])
  222. except:
  223. continue
  224. # if r.info cannot parse map, skip it
  225. self.raster[rpair] = UserSettings.Get(
  226. group=plottype, key="rasters"
  227. ) # some default settings
  228. # initialize sub-dictionaries for each raster in the list
  229. rdict[rpair] = {}
  230. rdict[rpair][0] = {}
  231. rdict[rpair][1] = {}
  232. rdict[rpair][0]["units"] = ""
  233. rdict[rpair][1]["units"] = ""
  234. if ret0["units"] not in ("(none)", '"none"', "", None):
  235. rdict[rpair][0]["units"] = ret0["units"]
  236. if ret1["units"] not in ("(none)", '"none"', "", None):
  237. rdict[rpair][1]["units"] = ret1["units"]
  238. rdict[rpair]["plegend"] = (
  239. rpair[0].split("@")[0] + " vs " + rpair[1].split("@")[0]
  240. )
  241. # list of cell value,frequency pairs for plotting histogram
  242. rdict[rpair]["datalist"] = []
  243. rdict[rpair][0]["datatype"] = ret0["datatype"]
  244. rdict[rpair][1]["datatype"] = ret1["datatype"]
  245. #
  246. # initialize with saved values
  247. #
  248. if (
  249. self.properties["raster"]["ptype"] is not None
  250. and self.properties["raster"]["ptype"] != ""
  251. ):
  252. rdict[rpair]["ptype"] = self.properties["raster"]["ptype"]
  253. else:
  254. rdict[rpair]["ptype"] = "dot"
  255. if self.properties["raster"]["psize"] is not None:
  256. rdict[rpair]["psize"] = self.properties["raster"]["psize"]
  257. else:
  258. rdict[rpair]["psize"] = 1
  259. if (
  260. self.properties["raster"]["pfill"] is not None
  261. and self.properties["raster"]["pfill"] != ""
  262. ):
  263. rdict[rpair]["pfill"] = self.properties["raster"]["pfill"]
  264. else:
  265. rdict[rpair]["pfill"] = "solid"
  266. if idx <= len(self.colorList):
  267. rdict[rpair]["pcolor"] = self.colorDict[self.colorList[idx]]
  268. else:
  269. r = randint(0, 255)
  270. b = randint(0, 255)
  271. g = randint(0, 255)
  272. rdict[rpair]["pcolor"] = (r, g, b, 255)
  273. return rdict
  274. def SetGraphStyle(self):
  275. """Set plot and text options"""
  276. self.client.SetFont(self.wx_font)
  277. self.client.fontSizeTitle = self.properties["font"]["prop"]["titleSize"]
  278. self.client.fontSizeAxis = self.properties["font"]["prop"]["axisSize"]
  279. self.client.enableZoom = self.zoom
  280. self.client.enableDrag = self.drag
  281. #
  282. # axis settings
  283. #
  284. if self.properties["x-axis"]["prop"]["type"] == "custom":
  285. self.client.xSpec = "min"
  286. else:
  287. self.client.xSpec = self.properties["x-axis"]["prop"]["type"]
  288. if self.properties["y-axis"]["prop"]["type"] == "custom":
  289. self.client.ySpec = "min"
  290. else:
  291. self.client.ySpec = self.properties["y-axis"]["prop"]["type"]
  292. if (
  293. self.properties["x-axis"]["prop"]["type"] == "custom"
  294. and self.properties["x-axis"]["prop"]["min"]
  295. < self.properties["x-axis"]["prop"]["max"]
  296. ):
  297. self.properties["x-axis"]["axis"] = (
  298. self.properties["x-axis"]["prop"]["min"],
  299. self.properties["x-axis"]["prop"]["max"],
  300. )
  301. else:
  302. self.properties["x-axis"]["axis"] = None
  303. if (
  304. self.properties["y-axis"]["prop"]["type"] == "custom"
  305. and self.properties["y-axis"]["prop"]["min"]
  306. < self.properties["y-axis"]["prop"]["max"]
  307. ):
  308. self.properties["y-axis"]["axis"] = (
  309. self.properties["y-axis"]["prop"]["min"],
  310. self.properties["y-axis"]["prop"]["max"],
  311. )
  312. else:
  313. self.properties["y-axis"]["axis"] = None
  314. if self.properties["x-axis"]["prop"]["log"]:
  315. self.properties["x-axis"]["axis"] = None
  316. self.client.xSpec = "min"
  317. if self.properties["y-axis"]["prop"]["log"]:
  318. self.properties["y-axis"]["axis"] = None
  319. self.client.ySpec = "min"
  320. self.client.logScale = (
  321. self.properties["x-axis"]["prop"]["log"],
  322. self.properties["y-axis"]["prop"]["log"],
  323. )
  324. #
  325. # grid settings
  326. #
  327. self.client.enableGrid = self.properties["grid"]["enabled"]
  328. gridpen = wx.Pen(
  329. colour=wx.Colour(
  330. self.properties["grid"]["color"][0],
  331. self.properties["grid"]["color"][1],
  332. self.properties["grid"]["color"][2],
  333. 255,
  334. )
  335. )
  336. self.client.gridPen = gridpen
  337. #
  338. # legend settings
  339. #
  340. self.client.fontSizeLegend = self.properties["font"]["prop"]["legendSize"]
  341. self.client.enableLegend = self.properties["legend"]["enabled"]
  342. def DrawPlot(self, plotlist):
  343. """Draw line and point plot from list plot elements."""
  344. xlabel, ylabel = self._getPlotLabels()
  345. self.plot = plot.PlotGraphics(plotlist, self.ptitle, xlabel, ylabel)
  346. if self.properties["x-axis"]["prop"]["type"] == "custom":
  347. self.client.xSpec = "min"
  348. else:
  349. self.client.xSpec = self.properties["x-axis"]["prop"]["type"]
  350. if self.properties["y-axis"]["prop"]["type"] == "custom":
  351. self.client.ySpec = "min"
  352. else:
  353. self.client.ySpec = self.properties["y-axis"]["prop"]["type"]
  354. self.client.Draw(
  355. self.plot,
  356. self.properties["x-axis"]["axis"],
  357. self.properties["y-axis"]["axis"],
  358. )
  359. def DrawPointLabel(self, dc, mDataDict):
  360. """This is the fuction that defines how the pointLabels are
  361. plotted dc - DC that will be passed mDataDict - Dictionary
  362. of data that you want to use for the pointLabel
  363. As an example I have decided I want a box at the curve
  364. point with some text information about the curve plotted
  365. below. Any wxDC method can be used.
  366. """
  367. dc.SetPen(wx.Pen(wx.BLACK))
  368. dc.SetBrush(wx.Brush(wx.BLACK, wx.SOLID))
  369. sx, sy = mDataDict["scaledXY"] # scaled x,y of closest point
  370. # 10by10 square centered on point
  371. dc.DrawRectangle(sx - 5, sy - 5, 10, 10)
  372. px, py = mDataDict["pointXY"]
  373. cNum = mDataDict["curveNum"]
  374. pntIn = mDataDict["pIndex"]
  375. legend = mDataDict["legend"]
  376. # make a string to display
  377. s = "Crv# %i, '%s', Pt. (%.2f,%.2f), PtInd %i" % (cNum, legend, px, py, pntIn)
  378. dc.DrawText(s, sx, sy + 1)
  379. def OnZoom(self, event):
  380. """Enable zooming and disable dragging"""
  381. self.zoom = True
  382. self.drag = False
  383. self.client.enableZoom = self.zoom
  384. self.client.enableDrag = self.drag
  385. def OnDrag(self, event):
  386. """Enable dragging and disable zooming"""
  387. self.zoom = False
  388. self.drag = True
  389. self.client.enableDrag = self.drag
  390. self.client.enableZoom = self.zoom
  391. def OnRedraw(self, event):
  392. """Redraw the plot window. Unzoom to original size"""
  393. self.UpdateLabels()
  394. self.client.Reset()
  395. self.client.Redraw()
  396. def OnErase(self, event):
  397. """Erase the plot window"""
  398. self.client.Clear()
  399. def SaveToFile(self, event):
  400. """Save plot to graphics file"""
  401. self.client.SaveFile()
  402. def OnMouseLeftDown(self, event):
  403. self.SetStatusText(
  404. _("Left Mouse Down at Point:") + " (%.4f, %.4f)" % self.client._getXY(event)
  405. )
  406. event.Skip() # allows plotCanvas OnMouseLeftDown to be called
  407. def OnMotion(self, event):
  408. """Indicate when mouse is outside the plot area"""
  409. if self.client.enablePointLabel:
  410. # make up dict with info for the pointLabel
  411. # I've decided to mark the closest point on the closest curve
  412. dlst = self.client.GetClosestPoint(
  413. self.client._getXY(event), pointScaled=True
  414. )
  415. if dlst != []: # returns [] if none
  416. curveNum, legend, pIndex, pointXY, scaledXY, distance = dlst
  417. # make up dictionary to pass to my user function (see
  418. # DrawPointLabel)
  419. mDataDict = {
  420. "curveNum": curveNum,
  421. "legend": legend,
  422. "pIndex": pIndex,
  423. "pointXY": pointXY,
  424. "scaledXY": scaledXY,
  425. }
  426. # pass dict to update the pointLabel
  427. self.client.UpdatePointLabel(mDataDict)
  428. event.Skip() # go to next handler
  429. def PlotOptionsMenu(self, event):
  430. """Popup menu for plot and text options"""
  431. point = wx.GetMousePosition()
  432. popt = Menu()
  433. # Add items to the menu
  434. settext = wx.MenuItem(popt, wx.ID_ANY, _("Text settings"))
  435. popt.AppendItem(settext)
  436. self.Bind(wx.EVT_MENU, self.PlotText, settext)
  437. setgrid = wx.MenuItem(popt, wx.ID_ANY, _("Plot settings"))
  438. popt.AppendItem(setgrid)
  439. self.Bind(wx.EVT_MENU, self.PlotOptions, setgrid)
  440. # Popup the menu. If an item is selected then its handler
  441. # will be called before PopupMenu returns.
  442. self.PopupMenu(popt)
  443. popt.Destroy()
  444. def NotFunctional(self):
  445. """Creates a 'not functional' message dialog"""
  446. dlg = wx.MessageDialog(
  447. parent=self,
  448. message=_("This feature is not yet functional"),
  449. caption=_("Under Construction"),
  450. style=wx.OK | wx.ICON_INFORMATION,
  451. )
  452. dlg.ShowModal()
  453. dlg.Destroy()
  454. def _getPlotLabels(self):
  455. def log(txt):
  456. return "log( " + txt + " )"
  457. x = self.xlabel
  458. if self.properties["x-axis"]["prop"]["log"]:
  459. x = log(x)
  460. y = self.ylabel
  461. if self.properties["y-axis"]["prop"]["log"]:
  462. y = log(y)
  463. return x, y
  464. def OnPlotText(self, dlg):
  465. """Custom text settings for histogram plot."""
  466. self.ptitle = dlg.ptitle
  467. self.xlabel = dlg.xlabel
  468. self.ylabel = dlg.ylabel
  469. if self.plot:
  470. self.plot.title = dlg.ptitle
  471. self.OnRedraw(event=None)
  472. def UpdateLabels(self):
  473. x, y = self._getPlotLabels()
  474. self.client.SetFont(self.wx_font)
  475. self.client.fontSizeTitle = self.properties["font"]["prop"]["titleSize"]
  476. self.client.fontSizeAxis = self.properties["font"]["prop"]["axisSize"]
  477. if self.plot:
  478. self.plot.xLabel = x
  479. self.plot.yLabel = y
  480. def PlotText(self, event):
  481. """Set custom text values for profile title and axis labels."""
  482. dlg = TextDialog(
  483. parent=self,
  484. giface=self._giface,
  485. id=wx.ID_ANY,
  486. plottype=self.plottype,
  487. title=_("Text settings"),
  488. )
  489. btnval = dlg.ShowModal()
  490. if btnval == wx.ID_SAVE or btnval == wx.ID_OK or btnval == wx.ID_CANCEL:
  491. dlg.Destroy()
  492. def PlotOptions(self, event):
  493. """Set various profile options, including: line width, color,
  494. style; marker size, color, fill, and style; grid and legend
  495. options. Calls OptDialog class.
  496. """
  497. dlg = OptDialog(
  498. parent=self,
  499. giface=self._giface,
  500. id=wx.ID_ANY,
  501. plottype=self.plottype,
  502. title=_("Plot settings"),
  503. )
  504. btnval = dlg.ShowModal()
  505. if btnval == wx.ID_SAVE or btnval == wx.ID_OK or btnval == wx.ID_CANCEL:
  506. dlg.Destroy()
  507. self.Update()
  508. def PrintMenu(self, event):
  509. """Print options and output menu"""
  510. point = wx.GetMousePosition()
  511. printmenu = Menu()
  512. for title, handler in (
  513. (_("Page setup"), self.OnPageSetup),
  514. (_("Print preview"), self.OnPrintPreview),
  515. (_("Print display"), self.OnDoPrint),
  516. ):
  517. item = wx.MenuItem(printmenu, wx.ID_ANY, title)
  518. printmenu.AppendItem(item)
  519. self.Bind(wx.EVT_MENU, handler, item)
  520. # Popup the menu. If an item is selected then its handler
  521. # will be called before PopupMenu returns.
  522. self.PopupMenu(printmenu)
  523. printmenu.Destroy()
  524. def OnPageSetup(self, event):
  525. self.client.PageSetup()
  526. def OnPrintPreview(self, event):
  527. self.client.PrintPreview()
  528. def OnDoPrint(self, event):
  529. self.client.Printout()
  530. def OnQuit(self, event):
  531. self.Close(True)