#!/usr/bin/env python3 ############################################################################ # # MODULE: d.rast.edit # AUTHOR(S): Glynn Clements # COPYRIGHT: (C) 2007,2008,2009 Glynn Clements # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # / # %module # % description: Edits cell values in a raster map. # % keyword: display # % keyword: editing # % keyword: raster # %end # %option G_OPT_R_INPUT # %end # %option G_OPT_R_OUTPUT # %end # %option G_OPT_R_INPUT # % key: aspect # % required: no # % description: Name of input aspect raster map # %end # %option # % key: width # % type: integer # % required: no # % multiple: no # % description: Width of display canvas # % answer: 640 # %end # %option # % key: height # % type: integer # % required: no # % multiple: no # % description: Height of display canvas # % answer: 480 # %end # %option # % key: size # % type: integer # % required: no # % multiple: no # % description: Minimum size of each cell # % answer: 12 # %end # %option # % key: rows # % type: integer # % required: no # % multiple: no # % description: Maximum number of rows to load # % answer: 100 # %end # %option # % key: cols # % type: integer # % required: no # % multiple: no # % description: Maximum number of columns to load # % answer: 100 # %end import sys import math import atexit import grass.script as grass from grass.script.setup import set_gui_path set_gui_path() try: import wx except ImportError: # ensure that --help, --interface-description etc work even without wx if __name__ == "__main__": if len(sys.argv) == 2: arg = sys.argv[1] if arg[0:2] == "--" or arg in ["help", "-help"]: grass.parser() # Either we didn't call g.parser, or it returned # At this point, there's nothing to be done except re-raise the exception raise wind_keys = { "north": ("n", float), "south": ("s", float), "east": ("e", float), "west": ("w", float), "nsres": ("nsres", float), "ewres": ("ewres", float), "rows": ("rows", int), "cols": ("cols", int), } gray12_bits = b"\x00\x00\x22\x22\x00\x00\x88\x88\x00\x00\x22\x22\x00\x00\x88\x88\x00\x00\x22\x22\x00\x00\x88\x88\x00\x00\x22\x22\x00\x00\x88\x88" def run(cmd, **kwargs): grass.run_command(cmd, quiet=True, **kwargs) def wxGUI(): class OverviewCanvas(wx.ScrolledWindow): def __init__(self, app, parent): wx.ScrolledWindow.__init__(self, parent) self.app = app self.width = app.total["cols"] self.height = app.total["rows"] self.SetVirtualSize((self.width, self.height)) self.SetScrollRate(1, 1) self.Bind(wx.EVT_LEFT_DOWN, self.OnMouse) self.Bind(wx.EVT_MOTION, self.OnMouse) self.Bind(wx.EVT_LEFT_UP, self.OnMouse) self.Bind(wx.EVT_PAINT, self.OnPaint) run("r.out.ppm", input=app.inmap, output=app.tempfile) self.image = wx.Bitmap(wx.Image(app.tempfile)) grass.try_remove(app.tempfile) app.force_window() def OnPaint(self, ev): x0 = self.app.origin_x y0 = self.app.origin_y dx = self.app.cols dy = self.app.rows dc = wx.PaintDC(self) self.DoPrepareDC(dc) src = wx.MemoryDC() src.SelectObjectAsSource(self.image) dc.Blit(0, 0, self.width, self.height, src, 0, 0) src.SelectObjectAsSource(wx.NullBitmap) dc.SetPen(wx.Pen("red", style=wx.LONG_DASH)) dc.SetBrush(wx.Brush("black", style=wx.TRANSPARENT)) dc.DrawRectangle(x0, y0, dx, dy) dc.SetBrush(wx.NullBrush) dc.SetPen(wx.NullPen) def OnMouse(self, ev): if ev.Moving(): return x = ev.GetX() y = ev.GetY() (x, y) = self.CalcUnscrolledPosition(x, y) self.set_window(x, y) if ev.ButtonUp(): self.app.change_window() def set_window(self, x, y): self.app.origin_x = x - app.cols / 2 self.app.origin_y = y - app.rows / 2 self.app.force_window() self.Refresh() class OverviewWindow(wx.Frame): def __init__(self, app): wx.Frame.__init__(self, None, title="d.rast.edit overview (%s)" % app.inmap) self.app = app self.canvas = OverviewCanvas(app, parent=self) self.Bind(wx.EVT_CLOSE, self.OnClose) def OnClose(self, ev): self.app.finalize() class Canvas(wx.ScrolledWindow): def __init__(self, app, parent): wx.ScrolledWindow.__init__(self, parent) self.app = app self.size = app.size w = app.cols * self.size h = app.rows * self.size self.SetVirtualSize((w, h)) self.SetScrollRate(1, 1) self.Bind(wx.EVT_LEFT_DOWN, self.OnMouse) self.Bind(wx.EVT_RIGHT_DOWN, self.OnMouse) self.Bind(wx.EVT_MOTION, self.OnMouse) self.Bind(wx.EVT_PAINT, self.OnPaint2) self.row = 0 self.col = 0 self.gray12 = wx.Bitmap(bits=gray12_bits, width=16, height=16) def OnMouse(self, ev): oldrow = self.row oldcol = self.col x = ev.GetX() y = ev.GetY() (x, y) = self.CalcUnscrolledPosition(x, y) col = x / self.size row = y / self.size self.row = int(row) self.col = int(col) if ev.Moving(): self.refresh_cell(oldrow, oldcol) self.refresh_cell(row, col) elif ev.ButtonDown(wx.MOUSE_BTN_LEFT): self.cell_set() elif ev.ButtonDown(wx.MOUSE_BTN_RIGHT): self.cell_get() def paint_cell(self, dc, r, c): if r < 0 or r >= self.app.rows or c < 0 or c >= self.app.cols: return val = self.app.values[r][c] if val is None: fill = "black" stipple = self.gray12 else: fill = self.app.get_color(val) stipple = None if r == self.row and c == self.col: outline = "red" elif self.app.changed[r][c]: outline = "white" else: outline = "black" dc.SetPen(wx.Pen(outline)) if stipple: brush = wx.Brush(fill, style=wx.STIPPLE) brush.SetStipple(stipple) else: brush = wx.Brush(fill, style=wx.SOLID) x0 = c * self.size + 1 x1 = x0 + self.size - 1 y0 = r * self.size + 1 y1 = y0 + self.size - 1 dc.SetBrush(brush) dc.DrawRectangle(x0, y0, x1 - x0, y1 - y0) dc.SetPen(wx.NullPen) dc.SetBrush(wx.NullBrush) if not self.app.angles: return if self.app.angles[r][c] == "*": return cx = (x0 + x1) / 2 cy = (y0 + y1) / 2 a = math.radians(float(self.app.angles[r][c])) dx = math.cos(a) * self.size / 2 dy = -math.sin(a) * self.size / 2 x0 = cx - dx y0 = cy - dy x1 = cx + dx y1 = cy + dy dx, dy = x1 - x0, y1 - y0 px, py = -dy, dx r, g, b, a = wx.Colour(fill).Get() if r + g + b > 384: line = "black" else: line = "white" dc.SetPen(wx.Pen(line)) dc.DrawLine(x0, y0, x1, y1) dc.DrawLine(x1, y1, x1 + px / 6 - dx / 3, y1 + py / 6 - dy / 3) dc.DrawLine(x1, y1, x1 - px / 6 - dx / 3, y1 - py / 6 - dy / 3) dc.SetPen(wx.NullPen) def paint_rect(self, dc, x, y, w, h): c0 = int((x + 0) / self.size) c1 = int((x + w + 1) / self.size) r0 = int((y + 0) / self.size) r1 = int((y + h + 1) / self.size) for r in range(r0, r1 + 1): for c in range(c0, c1 + 1): self.paint_cell(dc, r, c) def OnPaint(self, ev): dc = wx.PaintDC(self) self.DoPrepareDC(dc) for r in range(self.app.rows): for c in range(self.app.cols): self.paint_cell(dc, r, c) def OnPaint2(self, ev): dc = wx.PaintDC(self) self.DoPrepareDC(dc) it = wx.RegionIterator(self.GetUpdateRegion()) while it.HaveRects(): x = it.GetX() y = it.GetY() w = it.GetW() h = it.GetH() (x, y) = self.CalcUnscrolledPosition(x, y) self.paint_rect(dc, x, y, w, h) it.Next() def cell_enter(self): if not self.row or not self.col: return self.app.update_status(self.row, self.col) def cell_leave(self): self.app.clear_status() def cell_get(self): self.app.brush = self.app.values[self.row][self.col] self.app.frame.brush_update() def cell_set(self): self.app.values[self.row][self.col] = self.app.brush self.app.changed[self.row][self.col] = True self.refresh_cell(self.row, self.col) def refresh_cell(self, row, col): x = col * self.size y = row * self.size (x, y) = self.CalcScrolledPosition(x, y) self.RefreshRect((x, y, self.size, self.size)) class ColorPanel(Panel): def __init__(self, **kwargs): Panel.__init__(self, **kwargs) self.stipple = wx.Bitmap(bits=gray12_bits, width=16, height=16) self.null_bg = True self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnErase) def OnErase(self, ev): self.ClearBackground() if not self.null_bg: return dc = ev.GetDC() if not dc: dc = ClientDC(self) brush = wx.Brush("black", style=wx.STIPPLE) brush.SetStipple(self.stipple) dc.SetBackground(brush) dc.Clear() dc.SetBackground(wx.NullBrush) def SetNullBackgroundColour(self): Panel.SetBackgroundColour(self, "gray") self.null_bg = True def SetBackgroundColour(self, color): Panel.SetBackgroundColour(self, color) self.null_bg = False class MainWindow(wx.Frame): def __init__(self, app): wx.Frame.__init__(self, None, title="d.rast.edit (%s)" % app.inmap) self.app = app self.Bind(wx.EVT_CLOSE, self.OnClose) filemenu = Menu() filemenu.Append(wx.ID_SAVE, "&Save", "Save changes") filemenu.Append(wx.ID_EXIT, "E&xit", "Terminate the program") menubar = wx.MenuBar() menubar.Append(filemenu, "&File") self.SetMenuBar(menubar) self.Bind(wx.EVT_MENU, self.OnSave) self.Bind(wx.EVT_MENU, self.OnExit) sizer = wx.BoxSizer(orient=wx.VERTICAL) self.canvas = Canvas(app, parent=self) sizer.Add(self.canvas, proportion=1, flag=wx.EXPAND) tools = wx.BoxSizer(wx.HORIZONTAL) label = wx.StaticText(parent=self, label="New Value:") tools.Add(label, flag=wx.ALIGN_CENTER_VERTICAL) tools.AddSpacer(5) self.newval = wx.TextCtrl(parent=self, style=wx.TE_PROCESS_ENTER) tools.Add(self.newval, flag=wx.ALIGN_CENTER_VERTICAL) label = wx.StaticText(parent=self, label="Color:") tools.Add(label, flag=wx.ALIGN_CENTER_VERTICAL) tools.AddSpacer(5) self.color = ColorPanel(parent=self, size=(30, 5)) tools.Add(self.color, flag=wx.ALIGN_CENTER_VERTICAL | wx.EXPAND) self.Bind(wx.EVT_TEXT_ENTER, self.OnReturn, source=self.newval) sizer.Add(tools, proportion=0, flag=wx.EXPAND) self.SetSizerAndFit(sizer) self.SetSize((app.width, app.height)) self.status = self.CreateStatusBar(6) self.brush_update() def OnSave(self, ev): self.app.save_map() def OnExit(self, ev): self.Close(True) def OnClose(self, ev): self.app.finalize() def OnReturn(self, ev): self.app.brush = self.newval.GetValue() if self.app.brush != "*" and self.app.brush.strip("0123456789") != "": self.app.brush = "*" self.brush_update() def update_status(self): for i, key in enumerate(["row", "col", "x", "y", "value", "aspect"]): s = "%s%s: %s" % (key[0].upper(), key[1:], self.app.status[key]) self.status.SetStatusText(s, i) def clear_status(self): for key in self.status: self.status[key] = "" def brush_update(self): self.newval.ChangeValue(self.app.brush) if self.app.brush == "*": self.color.SetNullBackgroundColour() else: self.color.SetBackgroundColour(self.app.get_color(self.app.brush)) self.color.Refresh() class Application(wx.App): def __init__(self, options): self.options = options wx.App.__init__(self) def initialize(self): grass.use_temp_region() run("g.region", raster=self.inmap) reg = grass.region() for k, f in wind_keys.values(): self.total[k] = (f)(reg[k]) if self.cols > self.total["cols"]: self.cols = self.total["cols"] if self.rows > self.total["rows"]: self.rows = self.total["rows"] tempbase = grass.tempfile() grass.try_remove(tempbase) self.tempfile = tempbase + ".ppm" self.tempmap = "tmp.d.rast.edit" atexit.register(self.cleanup) run("g.copy", raster=(self.inmap, self.outmap), overwrite=True) run("r.colors", map=self.outmap, rast=self.inmap) def cleanup(self): grass.try_remove(self.tempfile) run("g.remove", flags="f", type="raster", name=self.tempmap) def finalize(self): self.save_map() sys.exit(0) def save_map(self): p = grass.feed_command( "r.in.ascii", input="-", output=self.tempmap, quiet=True, overwrite=True ) outf = p.stdin outf.write(grass.encode("north: %f\n" % self.wind["n"])) outf.write(grass.encode("south: %f\n" % self.wind["s"])) outf.write(grass.encode("east: %f\n" % self.wind["e"])) outf.write(grass.encode("west: %f\n" % self.wind["w"])) outf.write(grass.encode("rows: %d\n" % self.wind["rows"])) outf.write(grass.encode("cols: %d\n" % self.wind["cols"])) outf.write(grass.encode("null: *\n")) for row in range(self.wind["rows"]): for col in range(self.wind["cols"]): if col > 0: outf.write(grass.encode(" ")) val = self.values[row][col] if val and self.changed[row][col]: outf.write(grass.encode("%s" % val)) else: outf.write(grass.encode("*")) outf.write(grass.encode("\n")) outf.close() p.wait() run("g.region", raster=self.inmap) run( "r.patch", input=(self.tempmap, self.outmap), output=self.outmap, overwrite=True, ) run("r.colors", map=self.outmap, rast=self.inmap) run("g.remove", flags="f", type="raster", name=self.tempmap) def read_header(self, infile): wind = {} for i in range(6): line = grass.decode(infile.readline()).rstrip("\r\n") f = line.split(":") key = f[0] val = f[1].strip() (k, f) = wind_keys[key] wind[k] = (f)(val) return wind def read_data(self, infile): values = [] for row in range(self.wind["rows"]): line = grass.decode(infile.readline()).rstrip("\r\n") values.append(line.split()) return values def load_map(self): run("g.region", **self.wind) p = grass.pipe_command("r.out.ascii", input=self.inmap, quiet=True) self.wind = self.read_header(p.stdout) self.values = self.read_data(p.stdout) self.changed = [[False for c in row] for row in self.values] p.wait() self.clear_changes() run("r.out.ppm", input=self.inmap, output=self.tempfile) colorimg = wx.Image(self.tempfile) grass.try_remove(self.tempfile) for row in range(self.wind["rows"]): for col in range(self.wind["cols"]): val = self.values[row][col] if val in self.colors: continue r = colorimg.GetRed(col, row) g = colorimg.GetGreen(col, row) b = colorimg.GetBlue(col, row) color = "#%02x%02x%02x" % (r, g, b) self.colors[val] = color colorimg.Destroy() def load_aspect(self): if not self.aspect: return p = grass.pipe_command("r.out.ascii", input=self.aspect, quiet=True) self.read_header(p.stdout) self.angles = self.read_data(p.stdout) p.wait() def clear_changes(self): for row in range(self.wind["rows"]): for col in range(self.wind["cols"]): self.changed[row][col] = 0 def update_window(self): x0 = self.origin_x y0 = self.origin_y x1 = x0 + self.cols y1 = y0 + self.rows self.wind["n"] = self.total["n"] - y0 * self.total["nsres"] self.wind["s"] = self.total["n"] - y1 * self.total["nsres"] self.wind["w"] = self.total["w"] + x0 * self.total["ewres"] self.wind["e"] = self.total["w"] + x1 * self.total["ewres"] self.wind["rows"] = self.rows self.wind["cols"] = self.cols def change_window(self): wait = wx.BusyCursor() self.save_map() self.update_window() self.load_map() self.load_aspect() self.refresh_canvas() del wait def force_window(self): if self.origin_x < 0: self.origin_x = 0 if self.origin_x > self.total["cols"] - self.cols: self.origin_x = self.total["cols"] - self.cols if self.origin_y < 0: self.origin_y = 0 if self.origin_y > self.total["rows"] - self.rows: self.origin_y = self.total["rows"] - self.rows def update_status(self, row, col): self.status["row"] = row self.status["col"] = col self.status["x"] = ( self.wind["e"] + (col + 0.5) * (self.wind["e"] - self.wind["w"]) / self.wind["cols"] ) self.status["y"] = ( self.wind["n"] - (row + 0.5) * (self.wind["n"] - self.wind["s"]) / self.wind["rows"] ) self.status["value"] = self.values[row][col] if self.angles: self.status["aspect"] = self.angles[row][col] def force_color(self, val): run("g.region", rows=1, cols=1) run("r.mapcalc", expression="%s = %d" % (self.tempmap, val)) run("r.colors", map=self.tempmap, rast=self.inmap) run("r.out.ppm", input=self.tempmap, out=self.tempfile) run("g.remove", flags="f", type="raster", name=self.tempmap) tempimg = wx.Image(self.tempfile) grass.try_remove(self.tempfile) rgb = tempimg.get(0, 0) color = "#%02x%02x%02x" % rgb self.colors[val] = color tempimg.delete() def get_color(self, val): if val not in self.colors: try: self.force_color(val) except: self.colors[val] = "#ffffff" return self.colors[val] def refresh_canvas(self): self.frame.canvas.Refresh() def make_ui(self): self.frame = MainWindow(self) self.SetTopWindow(self.frame) self.frame.Show() self.overview = OverviewWindow(self) self.overview.Show() def OnInit(self): self.outmap = self.options["output"] self.inmap = self.options["input"] self.aspect = self.options["aspect"] self.width = int(self.options["width"]) self.height = int(self.options["height"]) self.size = int(self.options["size"]) self.rows = int(self.options["rows"]) self.cols = int(self.options["cols"]) self.status = { "row": "", "col": "", "x": "", "y": "", "value": "", "aspect": "", } self.values = None self.changed = None self.angles = None self.colors = {} self.brush = "*" self.origin_x = 0 self.origin_y = 0 self.wind = {} self.total = {} self.initialize() self.update_window() self.make_ui() self.load_map() self.load_aspect() self.refresh_canvas() return True return Application if __name__ == "__main__": options, flags = grass.parser() from gui_core.wrap import ClientDC, Menu, Panel Application = wxGUI() app = Application(options) app.MainLoop()