histogram.py 19 KB

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