base.py 22 KB

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