base.py 22 KB

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