histogram.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. """!
  2. @package modules.histogram
  3. Plotting histogram based on d.histogram
  4. Classes:
  5. - histogram::BufferedWindow
  6. - histogram::HistogramFrame
  7. - histogram::HistogramToolbar
  8. (C) 2007, 2010-2011 by the GRASS Development Team
  9. This program is free software under the GNU General Public License
  10. (>=v2). Read the file COPYING that comes with GRASS for details.
  11. @author Michael Barton
  12. @author Various updates by Martin Landa <landa.martin gmail.com>
  13. """
  14. import os
  15. import wx
  16. from core import globalvar
  17. from core.render import Map
  18. from gui_core.forms import GUI
  19. from mapdisp.gprint import PrintOptions
  20. from core.utils import GetLayerNameFromCmd, _
  21. from gui_core.dialogs import GetImageHandlers, ImageSizeDialog
  22. from gui_core.preferences import DefaultFontDialog
  23. from core.debug import Debug
  24. from core.gcmd import GError
  25. from gui_core.toolbars import BaseToolbar, BaseIcons
  26. class BufferedWindow(wx.Window):
  27. """!A Buffered window class.
  28. When the drawing needs to change, you app needs to call the
  29. UpdateHist() method. Since the drawing is stored in a bitmap, you
  30. can also save the drawing to file by calling the
  31. SaveToFile(self,file_name,file_type) method.
  32. """
  33. def __init__(self, parent, id = wx.ID_ANY,
  34. style = wx.NO_FULL_REPAINT_ON_RESIZE,
  35. Map = None, **kwargs):
  36. wx.Window.__init__(self, parent, id = id, style = style, **kwargs)
  37. self.parent = parent
  38. self.Map = Map
  39. self.mapname = self.parent.mapname
  40. #
  41. # Flags
  42. #
  43. self.render = True # re-render the map from GRASS or just redraw image
  44. self.resize = False # indicates whether or not a resize event has taken place
  45. self.dragimg = None # initialize variable for map panning
  46. self.pen = None # pen for drawing zoom boxes, etc.
  47. #
  48. # Event bindings
  49. #
  50. self.Bind(wx.EVT_PAINT, self.OnPaint)
  51. self.Bind(wx.EVT_SIZE, self.OnSize)
  52. self.Bind(wx.EVT_IDLE, self.OnIdle)
  53. #
  54. # Render output objects
  55. #
  56. self.mapfile = None # image file to be rendered
  57. self.img = "" # wx.Image object (self.mapfile)
  58. self.imagedict = {} # images and their PseudoDC ID's for painting and dragging
  59. self.pdc = wx.PseudoDC()
  60. # will store an off screen empty bitmap for saving to file
  61. self._buffer = wx.EmptyBitmap(max(1, self.Map.width), max(1, self.Map.height))
  62. # make sure that extents are updated at init
  63. self.Map.region = self.Map.GetRegion()
  64. self.Map.SetRegion()
  65. self.Bind(wx.EVT_ERASE_BACKGROUND, lambda x:None)
  66. def Draw(self, pdc, img = None, drawid = None, pdctype = 'image', coords = [0,0,0,0]):
  67. """!Draws histogram or clears window
  68. """
  69. if drawid == None:
  70. if pdctype == 'image' :
  71. drawid = imagedict[img]
  72. elif pdctype == 'clear':
  73. drawid == None
  74. else:
  75. drawid = wx.NewId()
  76. else:
  77. pdc.SetId(drawid)
  78. pdc.BeginDrawing()
  79. Debug.msg (3, "BufferedWindow.Draw(): id=%s, pdctype=%s, coord=%s" % (drawid, pdctype, coords))
  80. if pdctype == 'clear': # erase the display
  81. bg = wx.WHITE_BRUSH
  82. pdc.SetBackground(bg)
  83. pdc.Clear()
  84. self.Refresh()
  85. pdc.EndDrawing()
  86. return
  87. if pdctype == 'image':
  88. bg = wx.TRANSPARENT_BRUSH
  89. pdc.SetBackground(bg)
  90. bitmap = wx.BitmapFromImage(img)
  91. w,h = bitmap.GetSize()
  92. pdc.DrawBitmap(bitmap, coords[0], coords[1], True) # draw the composite map
  93. pdc.SetIdBounds(drawid, (coords[0],coords[1],w,h))
  94. pdc.EndDrawing()
  95. self.Refresh()
  96. def OnPaint(self, event):
  97. """!Draw psuedo DC to buffer
  98. """
  99. dc = wx.BufferedPaintDC(self, self._buffer)
  100. # use PrepareDC to set position correctly
  101. # probably does nothing, removed from wxPython 2.9
  102. # self.PrepareDC(dc)
  103. # we need to clear the dc BEFORE calling PrepareDC
  104. bg = wx.Brush(self.GetBackgroundColour())
  105. dc.SetBackground(bg)
  106. dc.Clear()
  107. # create a clipping rect from our position and size
  108. # and the Update Region
  109. rgn = self.GetUpdateRegion()
  110. r = rgn.GetBox()
  111. # draw to the dc using the calculated clipping rect
  112. self.pdc.DrawToDCClipped(dc,r)
  113. def OnSize(self, event):
  114. """!Init image size to match window size
  115. """
  116. # set size of the input image
  117. self.Map.width, self.Map.height = self.GetClientSize()
  118. # Make new off screen bitmap: this bitmap will always have the
  119. # current drawing in it, so it can be used to save the image to
  120. # a file, or whatever.
  121. self._buffer = wx.EmptyBitmap(self.Map.width, self.Map.height)
  122. # get the image to be rendered
  123. self.img = self.GetImage()
  124. # update map display
  125. if self.img and self.Map.width + self.Map.height > 0: # scale image during resize
  126. self.img = self.img.Scale(self.Map.width, self.Map.height)
  127. self.render = False
  128. self.UpdateHist()
  129. # re-render image on idle
  130. self.resize = True
  131. def OnIdle(self, event):
  132. """!Only re-render a histogram image from GRASS during idle
  133. time instead of multiple times during resizing.
  134. """
  135. if self.resize:
  136. self.render = True
  137. self.UpdateHist()
  138. event.Skip()
  139. def SaveToFile(self, FileName, FileType, width, height):
  140. """!This will save the contents of the buffer to the specified
  141. file. See the wx.Windows docs for wx.Bitmap::SaveFile for the
  142. details
  143. """
  144. busy = wx.BusyInfo(message=_("Please wait, exporting image..."),
  145. parent=self)
  146. wx.Yield()
  147. self.Map.ChangeMapSize((width, height))
  148. ibuffer = wx.EmptyBitmap(max(1, width), max(1, height))
  149. self.Map.Render(force=True, windres = True)
  150. img = self.GetImage()
  151. self.Draw(self.pdc, img, drawid = 99)
  152. dc = wx.BufferedDC(None, ibuffer)
  153. dc.Clear()
  154. # probably does nothing, removed from wxPython 2.9
  155. # self.PrepareDC(dc)
  156. self.pdc.DrawToDC(dc)
  157. ibuffer.SaveFile(FileName, FileType)
  158. busy.Destroy()
  159. def GetImage(self):
  160. """!Converts files to wx.Image
  161. """
  162. if self.Map.mapfile and os.path.isfile(self.Map.mapfile) and \
  163. os.path.getsize(self.Map.mapfile):
  164. img = wx.Image(self.Map.mapfile, wx.BITMAP_TYPE_ANY)
  165. else:
  166. img = None
  167. self.imagedict[img] = 99 # set image PeudoDC ID
  168. return img
  169. def UpdateHist(self, img = None):
  170. """!Update canvas if histogram options changes or window
  171. changes geometry
  172. """
  173. Debug.msg (2, "BufferedWindow.UpdateHist(%s): render=%s" % (img, self.render))
  174. oldfont = ""
  175. oldencoding = ""
  176. if self.render:
  177. # render new map images
  178. # set default font and encoding environmental variables
  179. if "GRASS_FONT" in os.environ:
  180. oldfont = os.environ["GRASS_FONT"]
  181. if self.parent.font != "": os.environ["GRASS_FONT"] = self.parent.font
  182. if "GRASS_ENCODING" in os.environ:
  183. oldencoding = os.environ["GRASS_ENCODING"]
  184. if self.parent.encoding != None and self.parent.encoding != "ISO-8859-1":
  185. os.environ[GRASS_ENCODING] = self.parent.encoding
  186. # using active comp region
  187. self.Map.GetRegion(update = True)
  188. self.Map.width, self.Map.height = self.GetClientSize()
  189. self.mapfile = self.Map.Render(force = self.render)
  190. self.img = self.GetImage()
  191. self.resize = False
  192. if not self.img: return
  193. try:
  194. id = self.imagedict[self.img]
  195. except:
  196. return
  197. # paint images to PseudoDC
  198. self.pdc.Clear()
  199. self.pdc.RemoveAll()
  200. self.Draw(self.pdc, self.img, drawid = id) # draw map image background
  201. self.resize = False
  202. # update statusbar
  203. # Debug.msg (3, "BufferedWindow.UpdateHist(%s): region=%s" % self.Map.region)
  204. self.Map.SetRegion()
  205. self.parent.statusbar.SetStatusText("Image/Raster map <%s>" % self.parent.mapname)
  206. # set default font and encoding environmental variables
  207. if oldfont != "":
  208. os.environ["GRASS_FONT"] = oldfont
  209. if oldencoding != "":
  210. os.environ["GRASS_ENCODING"] = oldencoding
  211. def EraseMap(self):
  212. """!Erase the map display
  213. """
  214. self.Draw(self.pdc, pdctype = 'clear')
  215. class HistogramFrame(wx.Frame):
  216. """!Main frame for hisgram display window. Uses d.histogram
  217. rendered onto canvas
  218. """
  219. def __init__(self, parent, giface, id=wx.ID_ANY,
  220. title = _("GRASS GIS Histogramming Tool (d.histogram)"),
  221. size = wx.Size(500, 350),
  222. style = wx.DEFAULT_FRAME_STYLE, **kwargs):
  223. wx.Frame.__init__(self, parent, id, title, size = size, style = style, **kwargs)
  224. self.SetIcon(wx.Icon(os.path.join(globalvar.ICONDIR, 'grass.ico'), wx.BITMAP_TYPE_ICO))
  225. self._giface = giface
  226. self.Map = Map() # instance of render.Map to be associated with display
  227. self.layer = None # reference to layer with histogram
  228. # Init variables
  229. self.params = {} # previously set histogram parameters
  230. self.propwin = '' # ID of properties dialog
  231. self.font = ""
  232. self.encoding = 'ISO-8859-1' # default encoding for display fonts
  233. self.toolbar = HistogramToolbar(parent = self)
  234. self.SetToolBar(self.toolbar)
  235. # find selected map
  236. # might by moved outside this class
  237. # setting to None but honestly we do not handle no map case
  238. # TODO: when self.mapname is None content of map window is showed
  239. self.mapname = None
  240. layers = self._giface.GetLayerList().GetSelectedLayers(checkedOnly=False)
  241. if len(layers) > 0:
  242. self.mapname = layers[0].maplayer.name
  243. # Add statusbar
  244. self.statusbar = self.CreateStatusBar(number = 1, style = 0)
  245. # self.statusbar.SetStatusWidths([-2, -1])
  246. hist_frame_statusbar_fields = ["Histogramming %s" % self.mapname]
  247. for i in range(len(hist_frame_statusbar_fields)):
  248. self.statusbar.SetStatusText(hist_frame_statusbar_fields[i], i)
  249. # Init map display
  250. self.InitDisplay() # initialize region values
  251. # initialize buffered DC
  252. self.HistWindow = BufferedWindow(self, id = wx.ID_ANY, Map = self.Map) # initialize buffered DC
  253. # Bind various events
  254. self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
  255. # Init print module and classes
  256. self.printopt = PrintOptions(self, self.HistWindow)
  257. # Add layer to the map
  258. self.layer = self.Map.AddLayer(ltype = "command", name = 'histogram', command = [['d.histogram']],
  259. active = False, hidden = False, opacity = 1, render = False)
  260. if self.mapname:
  261. self.SetHistLayer(self.mapname, None)
  262. else:
  263. self.OnErase(None)
  264. wx.CallAfter(self.OnOptions, None)
  265. def InitDisplay(self):
  266. """!Initialize histogram display, set dimensions and region
  267. """
  268. self.width, self.height = self.GetClientSize()
  269. self.Map.geom = self.width, self.height
  270. def OnOptions(self, event):
  271. """!Change histogram settings"""
  272. cmd = ['d.histogram']
  273. if self.mapname != '':
  274. cmd.append('map=%s' % self.mapname)
  275. module = GUI(parent = self)
  276. module.ParseCommand(cmd, completed = (self.GetOptData, None, self.params))
  277. def GetOptData(self, dcmd, layer, params, propwin):
  278. """!Callback method for histogram command generated by dialog
  279. created in menuform.py
  280. """
  281. if dcmd:
  282. name, found = GetLayerNameFromCmd(dcmd, fullyQualified = True,
  283. layerType = 'raster')
  284. if not found:
  285. GError(parent = propwin,
  286. message = _("Raster map <%s> not found") % name)
  287. return
  288. self.SetHistLayer(name, dcmd)
  289. self.params = params
  290. self.propwin = propwin
  291. self.HistWindow.UpdateHist()
  292. def SetHistLayer(self, name, cmd = None):
  293. """!Set histogram layer
  294. """
  295. self.mapname = name
  296. if not cmd:
  297. cmd = ['d.histogram',('map=%s' % self.mapname)]
  298. self.layer = self.Map.ChangeLayer(layer = self.layer,
  299. command = [cmd],
  300. active = True)
  301. return self.layer
  302. def SetHistFont(self, event):
  303. """!Set font for histogram. If not set, font will be default
  304. display font.
  305. """
  306. dlg = DefaultFontDialog(parent = self, id = wx.ID_ANY,
  307. title = _('Select font for histogram text'))
  308. dlg.fontlb.SetStringSelection(self.font, True)
  309. if dlg.ShowModal() == wx.ID_CANCEL:
  310. dlg.Destroy()
  311. return
  312. # set default font type, font, and encoding to whatever selected in dialog
  313. if dlg.font != None:
  314. self.font = dlg.font
  315. if dlg.encoding != None:
  316. self.encoding = dlg.encoding
  317. dlg.Destroy()
  318. self.HistWindow.UpdateHist()
  319. def OnErase(self, event):
  320. """!Erase the histogram display
  321. """
  322. self.HistWindow.Draw(self.HistWindow.pdc, pdctype = 'clear')
  323. def OnRender(self, event):
  324. """!Re-render histogram
  325. """
  326. self.HistWindow.UpdateHist()
  327. def GetWindow(self):
  328. """!Get buffered window"""
  329. return self.HistWindow
  330. def SaveToFile(self, event):
  331. """!Save to file
  332. """
  333. filetype, ltype = GetImageHandlers(self.HistWindow.img)
  334. # get size
  335. dlg = ImageSizeDialog(self)
  336. dlg.CentreOnParent()
  337. if dlg.ShowModal() != wx.ID_OK:
  338. dlg.Destroy()
  339. return
  340. width, height = dlg.GetValues()
  341. dlg.Destroy()
  342. # get filename
  343. dlg = wx.FileDialog(parent = self,
  344. message = _("Choose a file name to save the image "
  345. "(no need to add extension)"),
  346. wildcard = filetype,
  347. style=wx.SAVE | wx.FD_OVERWRITE_PROMPT)
  348. if dlg.ShowModal() == wx.ID_OK:
  349. path = dlg.GetPath()
  350. if not path:
  351. dlg.Destroy()
  352. return
  353. base, ext = os.path.splitext(path)
  354. fileType = ltype[dlg.GetFilterIndex()]['type']
  355. extType = ltype[dlg.GetFilterIndex()]['ext']
  356. if ext != extType:
  357. path = base + '.' + extType
  358. self.HistWindow.SaveToFile(path, fileType,
  359. width, height)
  360. self.HistWindow.UpdateHist()
  361. dlg.Destroy()
  362. def PrintMenu(self, event):
  363. """!Print options and output menu
  364. """
  365. point = wx.GetMousePosition()
  366. printmenu = wx.Menu()
  367. # Add items to the menu
  368. setup = wx.MenuItem(printmenu, id = wx.ID_ANY, text = _('Page setup'))
  369. printmenu.AppendItem(setup)
  370. self.Bind(wx.EVT_MENU, self.printopt.OnPageSetup, setup)
  371. preview = wx.MenuItem(printmenu, id = wx.ID_ANY, text = _('Print preview'))
  372. printmenu.AppendItem(preview)
  373. self.Bind(wx.EVT_MENU, self.printopt.OnPrintPreview, preview)
  374. doprint = wx.MenuItem(printmenu, id = wx.ID_ANY, text = _('Print display'))
  375. printmenu.AppendItem(doprint)
  376. self.Bind(wx.EVT_MENU, self.printopt.OnDoPrint, doprint)
  377. # Popup the menu. If an item is selected then its handler
  378. # will be called before PopupMenu returns.
  379. self.PopupMenu(printmenu)
  380. printmenu.Destroy()
  381. def OnQuit(self, event):
  382. self.Close(True)
  383. def OnCloseWindow(self, event):
  384. """!Window closed
  385. Also remove associated rendered images
  386. """
  387. try:
  388. self.propwin.Close(True)
  389. except:
  390. pass
  391. self.Map.Clean()
  392. self.Destroy()
  393. class HistogramToolbar(BaseToolbar):
  394. """!Histogram toolbar (see histogram.py)
  395. """
  396. def __init__(self, parent):
  397. BaseToolbar.__init__(self, parent)
  398. self.InitToolbar(self._toolbarData())
  399. # realize the toolbar
  400. self.Realize()
  401. def _toolbarData(self):
  402. """!Toolbar data"""
  403. return self._getToolbarData((('histogram', BaseIcons["histogramD"],
  404. self.parent.OnOptions),
  405. ('render', BaseIcons["display"],
  406. self.parent.OnRender),
  407. ('erase', BaseIcons["erase"],
  408. self.parent.OnErase),
  409. ('font', BaseIcons["font"],
  410. self.parent.SetHistFont),
  411. (None, ),
  412. ('save', BaseIcons["saveFile"],
  413. self.parent.SaveToFile),
  414. ('hprint', BaseIcons["print"],
  415. self.parent.PrintMenu),
  416. (None, ),
  417. ('quit', BaseIcons["quit"],
  418. self.parent.OnQuit))
  419. )