vclean.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618
  1. """
  2. @package modules.vclean
  3. @brief Dialog for interactive construction of vector cleaning
  4. operations
  5. Classes:
  6. - vclean::VectorCleaningFrame
  7. (C) 2010-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 Markus Metz
  11. """
  12. import os
  13. import wx
  14. import wx.lib.scrolledpanel as scrolled
  15. from core.gcmd import RunCommand, GError
  16. from core import globalvar
  17. from gui_core.gselect import Select
  18. from core.settings import UserSettings
  19. from grass.script import core as grass
  20. from gui_core.wrap import Button, StaticText, StaticBox, TextCtrl
  21. class VectorCleaningFrame(wx.Frame):
  22. def __init__(
  23. self,
  24. parent,
  25. id=wx.ID_ANY,
  26. title=_("Set up vector cleaning tools"),
  27. style=wx.DEFAULT_FRAME_STYLE | wx.RESIZE_BORDER,
  28. **kwargs,
  29. ):
  30. """
  31. Dialog for interactively defining vector cleaning tools
  32. """
  33. wx.Frame.__init__(self, parent, id, title, style=style, **kwargs)
  34. self.parent = parent # GMFrame
  35. if self.parent:
  36. self.log = self.parent.GetLogWindow()
  37. else:
  38. self.log = None
  39. # grass command
  40. self.cmd = "v.clean"
  41. # statusbar
  42. self.CreateStatusBar()
  43. # icon
  44. self.SetIcon(
  45. wx.Icon(os.path.join(globalvar.ICONDIR, "grass.ico"), wx.BITMAP_TYPE_ICO)
  46. )
  47. self.panel = wx.Panel(parent=self, id=wx.ID_ANY)
  48. # input map to clean
  49. self.inmap = ""
  50. # cleaned output map
  51. self.outmap = ""
  52. self.ftype = ""
  53. # cleaning tools
  54. self.toolslines = {}
  55. self.tool_desc_list = [
  56. _("break lines/boundaries"),
  57. _("remove duplicates"),
  58. _("remove dangles"),
  59. _("change boundary dangles to lines"),
  60. _("remove bridges"),
  61. _("change bridges to lines"),
  62. _("snap lines/boundaries"),
  63. _("remove duplicate area centroids"),
  64. _("break polygons"),
  65. _("prune lines/boundaries"),
  66. _("remove small areas"),
  67. _("remove lines/boundaries of zero length"),
  68. _("remove small angles at nodes"),
  69. ]
  70. self.tool_list = [
  71. "break",
  72. "rmdupl",
  73. "rmdangle",
  74. "chdangle",
  75. "rmbridge",
  76. "chbridge",
  77. "snap",
  78. "rmdac",
  79. "bpol",
  80. "prune",
  81. "rmarea",
  82. "rmline",
  83. "rmsa",
  84. ]
  85. self.ftype = ["point", "line", "boundary", "centroid", "area", "face"]
  86. self.n_ftypes = len(self.ftype)
  87. self.tools_string = ""
  88. self.thresh_string = ""
  89. self.ftype_string = ""
  90. self.SetStatusText(_("Set up vector cleaning tools"))
  91. self.elem = "vector"
  92. self.ctlabel = _("Choose cleaning tools and set thresholds")
  93. # top controls
  94. self.inmaplabel = StaticText(
  95. parent=self.panel, id=wx.ID_ANY, label=_("Select input vector map:")
  96. )
  97. self.selectionInput = Select(
  98. parent=self.panel,
  99. id=wx.ID_ANY,
  100. size=globalvar.DIALOG_GSELECT_SIZE,
  101. type="vector",
  102. )
  103. self.ftype_check = {}
  104. ftypeBox = StaticBox(
  105. parent=self.panel, id=wx.ID_ANY, label=_(" Feature type: ")
  106. )
  107. self.ftypeSizer = wx.StaticBoxSizer(ftypeBox, wx.HORIZONTAL)
  108. self.outmaplabel = StaticText(
  109. parent=self.panel, id=wx.ID_ANY, label=_("Select output vector map:")
  110. )
  111. self.selectionOutput = Select(
  112. parent=self.panel,
  113. id=wx.ID_ANY,
  114. size=globalvar.DIALOG_GSELECT_SIZE,
  115. mapsets=[
  116. grass.gisenv()["MAPSET"],
  117. ],
  118. fullyQualified=False,
  119. type="vector",
  120. )
  121. self.overwrite = wx.CheckBox(
  122. parent=self.panel,
  123. id=wx.ID_ANY,
  124. label=_("Allow output files to overwrite existing files"),
  125. )
  126. self.overwrite.SetValue(
  127. UserSettings.Get(group="cmd", key="overwrite", subkey="enabled")
  128. )
  129. # cleaning tools
  130. self.ct_label = StaticText(parent=self.panel, id=wx.ID_ANY, label=self.ctlabel)
  131. self.ct_panel = self._toolsPanel()
  132. # buttons to manage cleaning tools
  133. self.btn_add = Button(parent=self.panel, id=wx.ID_ADD)
  134. self.btn_remove = Button(parent=self.panel, id=wx.ID_REMOVE)
  135. self.btn_moveup = Button(parent=self.panel, id=wx.ID_UP)
  136. self.btn_movedown = Button(parent=self.panel, id=wx.ID_DOWN)
  137. # add one tool as default
  138. self.AddTool()
  139. self.selected = -1
  140. # Buttons
  141. self.btn_close = Button(parent=self.panel, id=wx.ID_CLOSE)
  142. self.btn_run = Button(parent=self.panel, id=wx.ID_ANY, label=_("&Run"))
  143. self.btn_run.SetDefault()
  144. self.btn_clipboard = Button(parent=self.panel, id=wx.ID_COPY)
  145. self.btn_clipboard.SetToolTip(
  146. _("Copy the current command string to the clipboard (Ctrl+C)")
  147. )
  148. self.btn_help = Button(parent=self.panel, id=wx.ID_HELP)
  149. # bindings
  150. self.btn_close.Bind(wx.EVT_BUTTON, self.OnClose)
  151. self.btn_run.Bind(wx.EVT_BUTTON, self.OnCleaningRun)
  152. self.btn_clipboard.Bind(wx.EVT_BUTTON, self.OnCopy)
  153. self.btn_help.Bind(wx.EVT_BUTTON, self.OnHelp)
  154. self.btn_add.Bind(wx.EVT_BUTTON, self.OnAddTool)
  155. self.btn_remove.Bind(wx.EVT_BUTTON, self.OnClearTool)
  156. self.btn_moveup.Bind(wx.EVT_BUTTON, self.OnMoveToolUp)
  157. self.btn_movedown.Bind(wx.EVT_BUTTON, self.OnMoveToolDown)
  158. # layout
  159. self._layout()
  160. self.SetMinSize(self.GetBestSize())
  161. self.CentreOnScreen()
  162. def _layout(self):
  163. sizer = wx.BoxSizer(wx.VERTICAL)
  164. #
  165. # input output
  166. #
  167. inSizer = wx.GridBagSizer(hgap=5, vgap=5)
  168. inSizer.Add(
  169. self.inmaplabel,
  170. pos=(0, 0),
  171. flag=wx.ALIGN_CENTER_VERTICAL | wx.ALL | wx.EXPAND,
  172. border=1,
  173. )
  174. inSizer.Add(
  175. self.selectionInput,
  176. pos=(1, 0),
  177. flag=wx.ALIGN_CENTER_VERTICAL | wx.ALL | wx.EXPAND,
  178. border=1,
  179. )
  180. self.ftype_check = [
  181. wx.CheckBox(parent=self.panel, id=wx.ID_ANY, label=_("point")),
  182. wx.CheckBox(parent=self.panel, id=wx.ID_ANY, label=_("line")),
  183. wx.CheckBox(parent=self.panel, id=wx.ID_ANY, label=_("boundary")),
  184. wx.CheckBox(parent=self.panel, id=wx.ID_ANY, label=_("centroid")),
  185. wx.CheckBox(parent=self.panel, id=wx.ID_ANY, label=_("area")),
  186. wx.CheckBox(parent=self.panel, id=wx.ID_ANY, label=_("face")),
  187. ]
  188. typeoptSizer = wx.BoxSizer(wx.HORIZONTAL)
  189. for num in range(0, self.n_ftypes):
  190. type_box = self.ftype_check[num]
  191. if self.ftype[num] in ("point", "line", "area"):
  192. type_box.SetValue(True)
  193. typeoptSizer.Add(type_box, flag=wx.ALIGN_LEFT, border=1)
  194. self.ftypeSizer.Add(
  195. typeoptSizer, flag=wx.ALIGN_CENTER_VERTICAL | wx.ALL, border=2
  196. )
  197. outSizer = wx.GridBagSizer(hgap=5, vgap=5)
  198. outSizer.Add(
  199. self.outmaplabel,
  200. pos=(0, 0),
  201. flag=wx.ALIGN_CENTER_VERTICAL | wx.ALL | wx.EXPAND,
  202. border=1,
  203. )
  204. outSizer.Add(
  205. self.selectionOutput,
  206. pos=(1, 0),
  207. flag=wx.ALIGN_CENTER_VERTICAL | wx.ALL | wx.EXPAND,
  208. border=1,
  209. )
  210. replaceSizer = wx.BoxSizer(wx.HORIZONTAL)
  211. replaceSizer.Add(
  212. self.overwrite, proportion=1, flag=wx.ALL | wx.EXPAND, border=1
  213. )
  214. outSizer.Add(replaceSizer, pos=(2, 0), flag=wx.ALL | wx.EXPAND, border=1)
  215. #
  216. # tools selection
  217. #
  218. bodySizer = wx.GridBagSizer(hgap=5, vgap=5)
  219. bodySizer.Add(self.ct_label, pos=(0, 0), span=(1, 2), flag=wx.ALL, border=5)
  220. bodySizer.Add(self.ct_panel, pos=(1, 0), span=(1, 2))
  221. manageBoxSizer = wx.GridBagSizer(hgap=10, vgap=1)
  222. # start with row 1 for nicer layout
  223. manageBoxSizer.Add(self.btn_add, pos=(1, 0), border=2, flag=wx.ALL | wx.EXPAND)
  224. manageBoxSizer.Add(
  225. self.btn_remove, pos=(2, 0), border=2, flag=wx.ALL | wx.EXPAND
  226. )
  227. manageBoxSizer.Add(
  228. self.btn_moveup, pos=(3, 0), border=2, flag=wx.ALL | wx.EXPAND
  229. )
  230. manageBoxSizer.Add(
  231. self.btn_movedown, pos=(4, 0), border=2, flag=wx.ALL | wx.EXPAND
  232. )
  233. bodySizer.Add(
  234. manageBoxSizer, pos=(1, 2), flag=wx.EXPAND | wx.LEFT | wx.RIGHT, border=5
  235. )
  236. bodySizer.AddGrowableCol(2)
  237. #
  238. # standard buttons
  239. #
  240. btnSizer = wx.BoxSizer(wx.HORIZONTAL)
  241. btnSizer.Add(self.btn_close, flag=wx.LEFT | wx.RIGHT, border=5)
  242. btnSizer.Add(self.btn_run, flag=wx.LEFT | wx.RIGHT, border=5)
  243. btnSizer.Add(self.btn_clipboard, flag=wx.LEFT | wx.RIGHT, border=5)
  244. btnSizer.Add(self.btn_help, flag=wx.LEFT | wx.RIGHT, border=5)
  245. #
  246. # put it all together
  247. #
  248. sizer.Add(inSizer, proportion=0, flag=wx.ALL | wx.EXPAND, border=5)
  249. sizer.Add(self.ftypeSizer, proportion=0, flag=wx.ALL | wx.EXPAND, border=5)
  250. sizer.Add(outSizer, proportion=0, flag=wx.ALL | wx.EXPAND, border=5)
  251. sizer.Add(
  252. wx.StaticLine(parent=self, id=wx.ID_ANY, style=wx.LI_HORIZONTAL),
  253. proportion=0,
  254. flag=wx.EXPAND | wx.ALL,
  255. border=5,
  256. )
  257. sizer.Add(bodySizer, proportion=1, flag=wx.ALL | wx.EXPAND, border=5)
  258. sizer.Add(
  259. wx.StaticLine(parent=self, id=wx.ID_ANY, style=wx.LI_HORIZONTAL),
  260. proportion=0,
  261. flag=wx.EXPAND | wx.ALL,
  262. border=5,
  263. )
  264. sizer.Add(btnSizer, proportion=0, flag=wx.ALL | wx.ALIGN_RIGHT, border=5)
  265. self.panel.SetAutoLayout(True)
  266. self.panel.SetSizer(sizer)
  267. sizer.Fit(self.panel)
  268. self.Layout()
  269. def _toolsPanel(self):
  270. ct_panel = scrolled.ScrolledPanel(
  271. parent=self.panel, id=wx.ID_ANY, size=(500, 240), style=wx.SUNKEN_BORDER
  272. )
  273. self.ct_sizer = wx.GridBagSizer(vgap=2, hgap=4)
  274. ct_panel.SetSizer(self.ct_sizer)
  275. ct_panel.SetAutoLayout(True)
  276. return ct_panel
  277. def OnAddTool(self, event):
  278. """Add tool button pressed"""
  279. self.AddTool()
  280. def AddTool(self):
  281. snum = len(self.toolslines.keys())
  282. num = snum + 1
  283. # tool
  284. tool_cbox = wx.ComboBox(
  285. parent=self.ct_panel,
  286. id=1000 + num,
  287. size=(300, -1),
  288. choices=self.tool_desc_list,
  289. style=wx.CB_DROPDOWN | wx.CB_READONLY | wx.TE_PROCESS_ENTER,
  290. )
  291. self.Bind(wx.EVT_COMBOBOX, self.OnSetTool, tool_cbox)
  292. # threshold
  293. txt_ctrl = TextCtrl(
  294. parent=self.ct_panel,
  295. id=2000 + num,
  296. value="0.00",
  297. size=(100, -1),
  298. style=wx.TE_NOHIDESEL,
  299. )
  300. self.Bind(wx.EVT_TEXT, self.OnThreshValue, txt_ctrl)
  301. # select with tool number
  302. select = wx.CheckBox(parent=self.ct_panel, id=num, label=str(num) + ".")
  303. select.SetValue(False)
  304. self.Bind(wx.EVT_CHECKBOX, self.OnSelect, select)
  305. # start with row 1 and col 1 for nicer layout
  306. self.ct_sizer.Add(select, pos=(num, 1), flag=wx.ALIGN_CENTER | wx.RIGHT)
  307. self.ct_sizer.Add(
  308. tool_cbox, pos=(num, 2), flag=wx.ALIGN_CENTER | wx.RIGHT, border=5
  309. )
  310. self.ct_sizer.Add(
  311. txt_ctrl, pos=(num, 3), flag=wx.ALIGN_CENTER | wx.RIGHT, border=5
  312. )
  313. self.toolslines[num] = {"tool_desc": "", "tool": "", "thresh": "0.00"}
  314. self.ct_panel.Layout()
  315. self.ct_panel.SetupScrolling()
  316. def OnClearTool(self, event):
  317. """Remove tool button pressed"""
  318. id = self.selected
  319. if id > 0:
  320. self.FindWindowById(id + 1000).SetValue("")
  321. self.toolslines[id]["tool_desc"] = ""
  322. self.toolslines[id]["tool"] = ""
  323. self.SetStatusText(_("%s. cleaning tool removed, will be ignored") % id)
  324. else:
  325. self.SetStatusText(_("Please select a cleaning tool to remove"))
  326. def OnMoveToolUp(self, event):
  327. """Move up tool button pressed"""
  328. id = self.selected
  329. if id > 1:
  330. id_up = id - 1
  331. this_toolline = self.toolslines[id]
  332. up_toolline = self.toolslines[id_up]
  333. self.FindWindowById(id_up).SetValue(True)
  334. self.FindWindowById(id_up + 1000).SetValue(this_toolline["tool_desc"])
  335. self.FindWindowById(id_up + 2000).SetValue(this_toolline["thresh"])
  336. self.toolslines[id_up] = this_toolline
  337. self.FindWindowById(id).SetValue(False)
  338. self.FindWindowById(id + 1000).SetValue(up_toolline["tool_desc"])
  339. self.FindWindowById(id + 2000).SetValue(up_toolline["thresh"])
  340. self.toolslines[id] = up_toolline
  341. self.selected = id_up
  342. self.SetStatusText(_("%s. cleaning tool moved up") % id)
  343. elif id == 1:
  344. self.SetStatusText(_("1. cleaning tool can not be moved up "))
  345. elif id == -1:
  346. self.SetStatusText(_("Please select a cleaning tool to move up"))
  347. def OnMoveToolDown(self, event):
  348. """Move down tool button pressed"""
  349. id = self.selected
  350. snum = len(self.toolslines.keys())
  351. if id > 0 and id < snum:
  352. id_down = id + 1
  353. this_toolline = self.toolslines[id]
  354. down_toolline = self.toolslines[id_down]
  355. self.FindWindowById(id_down).SetValue(True)
  356. self.FindWindowById(id_down + 1000).SetValue(this_toolline["tool_desc"])
  357. self.FindWindowById(id_down + 2000).SetValue(this_toolline["thresh"])
  358. self.toolslines[id_down] = this_toolline
  359. self.FindWindowById(id).SetValue(False)
  360. self.FindWindowById(id + 1000).SetValue(down_toolline["tool_desc"])
  361. self.FindWindowById(id + 2000).SetValue(down_toolline["thresh"])
  362. self.toolslines[id] = down_toolline
  363. self.selected = id_down
  364. self.SetStatusText(_("%s. cleaning tool moved down") % id)
  365. elif id == snum:
  366. self.SetStatusText(_("Last cleaning tool can not be moved down "))
  367. elif id == -1:
  368. self.SetStatusText(_("Please select a cleaning tool to move down"))
  369. def OnSetTool(self, event):
  370. """Tool was defined"""
  371. id = event.GetId()
  372. tool_no = id - 1000
  373. num = self.FindWindowById(id).GetCurrentSelection()
  374. self.toolslines[tool_no]["tool_desc"] = self.tool_desc_list[num]
  375. self.toolslines[tool_no]["tool"] = self.tool_list[num]
  376. self.SetStatusText(
  377. str(tool_no) + ". " + _("cleaning tool: '%s'") % (self.tool_list[num])
  378. )
  379. def OnThreshValue(self, event):
  380. """Threshold value was entered"""
  381. id = event.GetId()
  382. num = id - 2000
  383. self.toolslines[num]["thresh"] = self.FindWindowById(id).GetValue()
  384. self.SetStatusText(
  385. _("Threshold for %(num)s. tool '%(tool)s': %(thresh)s")
  386. % {
  387. "num": num,
  388. "tool": self.toolslines[num]["tool"],
  389. "thresh": self.toolslines[num]["thresh"],
  390. }
  391. )
  392. def OnSelect(self, event):
  393. """Tool was selected"""
  394. id = event.GetId()
  395. if self.selected > -1 and self.selected != id:
  396. win = self.FindWindowById(self.selected)
  397. win.SetValue(False)
  398. if self.selected != id:
  399. self.selected = id
  400. else:
  401. self.selected = -1
  402. def OnDone(self, event):
  403. """Command done"""
  404. self.SetStatusText("")
  405. def OnCleaningRun(self, event):
  406. """Builds options and runs v.clean"""
  407. self.GetCmdStrings()
  408. err = list()
  409. for p, name in (
  410. (self.inmap, _("Name of input vector map")),
  411. (self.outmap, _("Name for output vector map")),
  412. (self.tools_string, _("Tools")),
  413. (self.thresh_string, _("Threshold")),
  414. ):
  415. if not p:
  416. err.append(_("'%s' not defined") % name)
  417. if err:
  418. GError(
  419. _("Some parameters not defined. Operation " "canceled.\n\n%s")
  420. % "\n".join(err),
  421. parent=self,
  422. )
  423. return
  424. self.SetStatusText(_("Executing selected cleaning operations..."))
  425. snum = len(self.toolslines.keys())
  426. if self.log:
  427. cmd = [
  428. self.cmd,
  429. "input=%s" % self.inmap,
  430. "output=%s" % self.outmap,
  431. "tool=%s" % self.tools_string,
  432. "thres=%s" % self.thresh_string,
  433. ]
  434. if self.ftype_string:
  435. cmd.append("type=%s" % self.ftype_string)
  436. if self.overwrite.IsChecked():
  437. cmd.append("--overwrite")
  438. self.log.RunCmd(cmd, onDone=self.OnDone)
  439. self.parent.Raise()
  440. else:
  441. if self.overwrite.IsChecked():
  442. overwrite = True
  443. else:
  444. overwrite = False
  445. RunCommand(
  446. self.cmd,
  447. input=self.inmap,
  448. output=self.outmap,
  449. type=self.ftype_string,
  450. tool=self.tools_string,
  451. thresh=self.thresh_string,
  452. overwrite=overwrite,
  453. )
  454. def OnClose(self, event):
  455. self.Destroy()
  456. def OnHelp(self, event):
  457. """Show GRASS manual page"""
  458. RunCommand("g.manual", quiet=True, parent=self, entry=self.cmd)
  459. def OnCopy(self, event):
  460. """Copy the command"""
  461. cmddata = wx.TextDataObject()
  462. # get tool and thresh strings
  463. self.GetCmdStrings()
  464. cmdstring = "%s" % (self.cmd)
  465. # list -> string
  466. cmdstring += " input=%s output=%s type=%s tool=%s thres=%s" % (
  467. self.inmap,
  468. self.outmap,
  469. self.ftype_string,
  470. self.tools_string,
  471. self.thresh_string,
  472. )
  473. if self.overwrite.IsChecked():
  474. cmdstring += " --overwrite"
  475. cmddata.SetText(cmdstring)
  476. if wx.TheClipboard.Open():
  477. wx.TheClipboard.SetData(cmddata)
  478. wx.TheClipboard.Close()
  479. self.SetStatusText(_("Vector cleaning command copied to clipboard"))
  480. def GetCmdStrings(self):
  481. self.tools_string = ""
  482. self.thresh_string = ""
  483. self.ftype_string = ""
  484. # feature types
  485. first = 1
  486. for num in range(0, self.n_ftypes - 1):
  487. if self.ftype_check[num].IsChecked():
  488. if first:
  489. self.ftype_string = "%s" % self.ftype[num]
  490. first = 0
  491. else:
  492. self.ftype_string += ",%s" % self.ftype[num]
  493. # cleaning tools
  494. first = 1
  495. snum = len(self.toolslines.keys())
  496. for num in range(1, snum + 1):
  497. if self.toolslines[num]["tool"]:
  498. if first:
  499. self.tools_string = "%s" % self.toolslines[num]["tool"]
  500. self.thresh_string = "%s" % self.toolslines[num]["thresh"]
  501. first = 0
  502. else:
  503. self.tools_string += ",%s" % self.toolslines[num]["tool"]
  504. self.thresh_string += ",%s" % self.toolslines[num]["thresh"]
  505. self.inmap = self.selectionInput.GetValue()
  506. self.outmap = self.selectionOutput.GetValue()
  507. if __name__ == "__main__":
  508. app = wx.App()
  509. frame = VectorCleaningFrame(parent=None)
  510. frame.Show()
  511. app.MainLoop()