base.py 22 KB

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