base.py 22 KB

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