workspace.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. """
  2. @package lmgr::workspace
  3. @brief Workspace manager class for creating, loading and saving workspaces
  4. Class:
  5. - lmgr::WorkspaceManager
  6. (C) 2021 by the GRASS Development Team
  7. This program is free software under the GNU General Public
  8. License (>=v2). Read the file COPYING that comes with GRASS
  9. for details.
  10. """
  11. import os
  12. import tempfile
  13. import xml.etree.ElementTree as etree
  14. import wx
  15. import wx.aui
  16. from core.settings import UserSettings
  17. from core.gcmd import RunCommand, GError, GMessage
  18. from core.workspace import ProcessWorkspaceFile, WriteWorkspaceFile
  19. from core.debug import Debug
  20. class WorkspaceManager:
  21. """Workspace Manager for creating, loading and saving workspaces."""
  22. def __init__(self, lmgr, giface):
  23. self.lmgr = lmgr
  24. self.workspaceFile = None
  25. self._giface = giface
  26. self.workspaceChanged = False # track changes in workspace
  27. self.loadingWorkspace = False
  28. Debug.msg(1, "WorkspaceManager.__init__()")
  29. self._giface.workspaceChanged.connect(self.WorkspaceChanged)
  30. def WorkspaceChanged(self):
  31. "Update window title"
  32. self.workspaceChanged = True
  33. def New(self):
  34. """Create new workspace file
  35. Erase current workspace settings first
  36. """
  37. Debug.msg(4, "WorkspaceManager.New():")
  38. # start new map display if no display is available
  39. if not self.lmgr.currentPage:
  40. self.lmgr.NewDisplay()
  41. maptrees = [
  42. self.lmgr.notebookLayers.GetPage(i).maptree
  43. for i in range(self.lmgr.notebookLayers.GetPageCount())
  44. ]
  45. # ask user to save current settings
  46. if self.workspaceFile and self.workspaceChanged:
  47. self.Save()
  48. elif self.workspaceFile is None and any(tree.GetCount() for tree in maptrees):
  49. dlg = wx.MessageDialog(
  50. self.lmgr,
  51. message=_(
  52. "Current workspace is not empty. "
  53. "Do you want to store current settings "
  54. "to workspace file?"
  55. ),
  56. caption=_("Create new workspace?"),
  57. style=wx.YES_NO | wx.YES_DEFAULT | wx.CANCEL | wx.ICON_QUESTION,
  58. )
  59. ret = dlg.ShowModal()
  60. if ret == wx.ID_YES:
  61. self.SaveAs()
  62. elif ret == wx.ID_CANCEL:
  63. dlg.Destroy()
  64. return
  65. dlg.Destroy()
  66. # delete all layers in map displays
  67. for maptree in maptrees:
  68. maptree.DeleteAllLayers()
  69. # delete all decorations
  70. for display in self.lmgr.GetAllMapDisplays():
  71. for overlayId in list(display.decorations):
  72. display.RemoveOverlay(overlayId)
  73. self.workspaceFile = None
  74. self.workspaceChanged = False
  75. self.lmgr._setTitle()
  76. def Open(self):
  77. """Open file with workspace definition"""
  78. dlg = wx.FileDialog(
  79. parent=self.lmgr,
  80. message=_("Choose workspace file"),
  81. defaultDir=os.getcwd(),
  82. wildcard=_("GRASS Workspace File (*.gxw)|*.gxw"),
  83. )
  84. filename = ""
  85. if dlg.ShowModal() == wx.ID_OK:
  86. filename = dlg.GetPath()
  87. if filename == "":
  88. return
  89. Debug.msg(4, "WorkspaceManager.Open(): filename=%s" % filename)
  90. # delete current layer tree content
  91. self.Close()
  92. self.loadingWorkspace = True
  93. self.Load(filename)
  94. self.loadingWorkspace = False
  95. self.lmgr._setTitle()
  96. def _tryToSwitchMapsetFromWorkspaceFile(self, gxwXml):
  97. returncode, errors = RunCommand(
  98. "g.mapset",
  99. dbase=gxwXml.database,
  100. location=gxwXml.location,
  101. mapset=gxwXml.mapset,
  102. getErrorMsg=True,
  103. )
  104. if returncode != 0:
  105. # TODO: use the function from grass.py
  106. reason = _("Most likely the database, location or mapset" " does not exist")
  107. details = errors
  108. message = _(
  109. "Unable to change to location and mapset"
  110. " specified in the workspace.\n"
  111. "Reason: {reason}\nDetails: {details}\n\n"
  112. "Do you want to proceed with opening"
  113. " the workspace anyway?"
  114. ).format(**locals())
  115. dlg = wx.MessageDialog(
  116. parent=self.lmgr,
  117. message=message,
  118. caption=_("Proceed with opening of the workspace?"),
  119. style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION,
  120. )
  121. dlg.CenterOnParent()
  122. if dlg.ShowModal() in [wx.ID_NO, wx.ID_CANCEL]:
  123. return False
  124. else:
  125. # TODO: copy from ChangeLocation function
  126. GMessage(
  127. parent=self.lmgr,
  128. message=_(
  129. "Current location is <%(loc)s>.\n" "Current mapset is <%(mapset)s>."
  130. )
  131. % {"loc": gxwXml.location, "mapset": gxwXml.mapset},
  132. )
  133. return True
  134. def Load(self, filename):
  135. """Load layer tree definition stored in GRASS Workspace XML file (gxw)
  136. .. todo::
  137. Validate against DTD
  138. :return: True on success
  139. :return: False on error
  140. """
  141. # parse workspace file
  142. try:
  143. gxwXml = ProcessWorkspaceFile(etree.parse(filename))
  144. except Exception as e:
  145. GError(
  146. parent=self.lmgr,
  147. message=_(
  148. "Reading workspace file <%s> failed.\n"
  149. "Invalid file, unable to parse XML document."
  150. )
  151. % filename,
  152. )
  153. return False
  154. if gxwXml.database and gxwXml.location and gxwXml.mapset:
  155. if not self._tryToSwitchMapsetFromWorkspaceFile(gxwXml):
  156. return False
  157. # the really busy part starts here (mapset change is fast)
  158. busy = wx.BusyInfo(_("Please wait, loading workspace..."), parent=self.lmgr)
  159. wx.GetApp().Yield()
  160. #
  161. # load layer manager window properties
  162. #
  163. if (
  164. UserSettings.Get(
  165. group="general", key="workspace", subkey=["posManager", "enabled"]
  166. )
  167. is False
  168. ):
  169. if gxwXml.layerManager["pos"]:
  170. self.lmgr.SetPosition(gxwXml.layerManager["pos"])
  171. if gxwXml.layerManager["size"]:
  172. self.lmgr.SetSize(gxwXml.layerManager["size"])
  173. if gxwXml.layerManager["cwd"]:
  174. self.lmgr.cwdPath = gxwXml.layerManager["cwd"]
  175. if os.path.isdir(self.lmgr.cwdPath):
  176. os.chdir(self.lmgr.cwdPath)
  177. #
  178. # start map displays first (list of layers can be empty)
  179. #
  180. displayId = 0
  181. mapdisplay = list()
  182. for display in gxwXml.displays:
  183. mapdisp = self.lmgr.NewDisplay(name=display["name"], show=False)
  184. mapdisplay.append(mapdisp)
  185. maptree = self.lmgr.notebookLayers.GetPage(displayId).maptree
  186. # set windows properties
  187. mapdisp.SetProperties(
  188. render=display["render"],
  189. mode=display["mode"],
  190. showCompExtent=display["showCompExtent"],
  191. alignExtent=display["alignExtent"],
  192. constrainRes=display["constrainRes"],
  193. projection=display["projection"]["enabled"],
  194. )
  195. if display["projection"]["enabled"]:
  196. if display["projection"]["epsg"]:
  197. UserSettings.Set(
  198. group="display",
  199. key="projection",
  200. subkey="epsg",
  201. value=display["projection"]["epsg"],
  202. )
  203. if display["projection"]["proj"]:
  204. UserSettings.Set(
  205. group="display",
  206. key="projection",
  207. subkey="proj4",
  208. value=display["projection"]["proj"],
  209. )
  210. # set position and size of map display
  211. if not UserSettings.Get(
  212. group="general", key="workspace", subkey=["posDisplay", "enabled"]
  213. ):
  214. if display["pos"]:
  215. mapdisp.SetPosition(display["pos"])
  216. if display["size"]:
  217. mapdisp.SetSize(display["size"])
  218. # set extent if defined
  219. if display["extent"]:
  220. w, s, e, n, b, t = display["extent"]
  221. region = maptree.Map.region = maptree.Map.GetRegion(w=w, s=s, e=e, n=n)
  222. mapdisp.GetWindow().ResetZoomHistory()
  223. mapdisp.GetWindow().ZoomHistory(
  224. region["n"], region["s"], region["e"], region["w"]
  225. )
  226. if "showStatusbar" in display and not display["showStatusbar"]:
  227. mapdisp.ShowStatusbar(False)
  228. if "showToolbars" in display and not display["showToolbars"]:
  229. for toolbar in mapdisp.GetToolbarNames():
  230. mapdisp.RemoveToolbar(toolbar)
  231. displayId += 1
  232. mapdisp.Show() # show mapdisplay
  233. # set render property to False to speed up loading layers
  234. mapdisp.mapWindowProperties.autoRender = False
  235. maptree = None
  236. selectList = [] # list of selected layers
  237. #
  238. # load list of map layers
  239. #
  240. for layer in gxwXml.layers:
  241. display = layer["display"]
  242. maptree = self.lmgr.notebookLayers.GetPage(display).maptree
  243. newItem = maptree.AddLayer(
  244. ltype=layer["type"],
  245. lname=layer["name"],
  246. lchecked=layer["checked"],
  247. lopacity=layer["opacity"],
  248. lcmd=layer["cmd"],
  249. lgroup=layer["group"],
  250. lnviz=layer["nviz"],
  251. lvdigit=layer["vdigit"],
  252. loadWorkspace=True,
  253. )
  254. if "selected" in layer:
  255. selectList.append((maptree, newItem, layer["selected"]))
  256. for maptree, layer, selected in selectList:
  257. if selected:
  258. if not layer.IsSelected():
  259. maptree.SelectItem(layer, select=True)
  260. else:
  261. maptree.SelectItem(layer, select=False)
  262. del busy
  263. # set render property again when all layers are loaded
  264. for i, display in enumerate(gxwXml.displays):
  265. mapdisplay[i].mapWindowProperties.autoRender = display["render"]
  266. for overlay in gxwXml.overlays:
  267. # overlay["cmd"][0] name of command e.g. d.barscale, d.legend
  268. # overlay["cmd"][1:] parameters and flags
  269. if overlay["display"] == i:
  270. if overlay["cmd"][0] == "d.legend.vect":
  271. mapdisplay[i].AddLegendVect(overlay["cmd"])
  272. if overlay["cmd"][0] == "d.legend":
  273. mapdisplay[i].AddLegendRast(overlay["cmd"])
  274. if overlay["cmd"][0] == "d.barscale":
  275. mapdisplay[i].AddBarscale(overlay["cmd"])
  276. if overlay["cmd"][0] == "d.northarrow":
  277. mapdisplay[i].AddArrow(overlay["cmd"])
  278. if overlay["cmd"][0] == "d.text":
  279. mapdisplay[i].AddDtext(overlay["cmd"])
  280. # avoid double-rendering when loading workspace
  281. # mdisp.MapWindow2D.UpdateMap()
  282. # nviz
  283. if gxwXml.displays[i]["viewMode"] == "3d":
  284. mapdisplay[i].AddNviz()
  285. self.lmgr.nvizUpdateState(
  286. view=gxwXml.nviz_state["view"],
  287. iview=gxwXml.nviz_state["iview"],
  288. light=gxwXml.nviz_state["light"],
  289. )
  290. mapdisplay[i].MapWindow3D.constants = gxwXml.nviz_state["constants"]
  291. for idx, constant in enumerate(mapdisplay[i].MapWindow3D.constants):
  292. mapdisplay[i].MapWindow3D.AddConstant(constant, i + 1)
  293. for page in ("view", "light", "fringe", "constant", "cplane"):
  294. self.lmgr.nvizUpdatePage(page)
  295. self.lmgr.nvizUpdateSettings()
  296. mapdisplay[i].toolbars["map"].combo.SetSelection(1)
  297. self.workspaceFile = filename
  298. return True
  299. def SaveAs(self):
  300. """Save workspace definition to selected file"""
  301. dlg = wx.FileDialog(
  302. parent=self.lmgr,
  303. message=_("Choose file to save current workspace"),
  304. defaultDir=os.getcwd(),
  305. wildcard=_("GRASS Workspace File (*.gxw)|*.gxw"),
  306. style=wx.FD_SAVE,
  307. )
  308. filename = ""
  309. if dlg.ShowModal() == wx.ID_OK:
  310. filename = dlg.GetPath()
  311. if filename == "":
  312. return False
  313. # check for extension
  314. if filename[-4:] != ".gxw":
  315. filename += ".gxw"
  316. if os.path.exists(filename):
  317. dlg = wx.MessageDialog(
  318. self.lmgr,
  319. message=_(
  320. "Workspace file <%s> already exists. "
  321. "Do you want to overwrite this file?"
  322. )
  323. % filename,
  324. caption=_("Save workspace"),
  325. style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION,
  326. )
  327. if dlg.ShowModal() != wx.ID_YES:
  328. dlg.Destroy()
  329. return False
  330. Debug.msg(4, "WorkspaceManager.SaveAs(): filename=%s" % filename)
  331. self.SaveToFile(filename)
  332. self.workspaceFile = filename
  333. self.lmgr._setTitle()
  334. def Save(self):
  335. """Save file with workspace definition"""
  336. if self.workspaceFile:
  337. dlg = wx.MessageDialog(
  338. self.lmgr,
  339. message=_(
  340. "Workspace file <%s> already exists. "
  341. "Do you want to overwrite this file?"
  342. )
  343. % self.workspaceFile,
  344. caption=_("Save workspace"),
  345. style=wx.YES_NO | wx.YES_DEFAULT | wx.ICON_QUESTION,
  346. )
  347. if dlg.ShowModal() == wx.ID_NO:
  348. dlg.Destroy()
  349. else:
  350. Debug.msg(
  351. 4, "WorkspaceManager.Save(): filename=%s" % self.workspaceFile
  352. )
  353. self.SaveToFile(self.workspaceFile)
  354. self.lmgr._setTitle()
  355. self.workspaceChanged = False
  356. else:
  357. self.SaveAs()
  358. def SaveToFile(self, filename):
  359. """Save layer tree layout to workspace file
  360. :return: True on success, False on error
  361. """
  362. tmpfile = tempfile.TemporaryFile(mode="w+b")
  363. try:
  364. WriteWorkspaceFile(lmgr=self.lmgr, file=tmpfile)
  365. except Exception as e:
  366. GError(
  367. parent=self.lmgr,
  368. message=_("Writing current settings to workspace file " "failed."),
  369. )
  370. return False
  371. try:
  372. mfile = open(filename, "wb")
  373. tmpfile.seek(0)
  374. for line in tmpfile.readlines():
  375. mfile.write(line)
  376. except IOError:
  377. GError(
  378. parent=self.lmgr,
  379. message=_("Unable to open file <%s> for writing.") % filename,
  380. )
  381. return False
  382. mfile.close()
  383. return True
  384. def CanClosePage(self, caption):
  385. """Ask if page with map display(s) can be closed"""
  386. # save changes in the workspace
  387. maptree = self._giface.GetLayerTree()
  388. if self.workspaceChanged and UserSettings.Get(
  389. group="manager", key="askOnQuit", subkey="enabled"
  390. ):
  391. if self.workspaceFile:
  392. message = _("Do you want to save changes in the workspace?")
  393. else:
  394. message = _(
  395. "Do you want to store current settings " "to workspace file?"
  396. )
  397. # ask user to save current settings
  398. if maptree.GetCount() > 0:
  399. dlg = wx.MessageDialog(
  400. self.lmgr,
  401. message=message,
  402. caption=caption,
  403. style=wx.YES_NO
  404. | wx.YES_DEFAULT
  405. | wx.CANCEL
  406. | wx.ICON_QUESTION
  407. | wx.CENTRE,
  408. )
  409. ret = dlg.ShowModal()
  410. dlg.Destroy()
  411. if ret == wx.ID_YES:
  412. if not self.workspaceFile:
  413. self.SaveAs()
  414. else:
  415. self.SaveToFile(self.workspaceFile)
  416. elif ret == wx.ID_CANCEL:
  417. return False
  418. return True
  419. def Close(self):
  420. """Close file with workspace definition
  421. If workspace has been modified ask user to save the changes.
  422. """
  423. Debug.msg(4, "WorkspaceManager.Close(): file=%s" % self.workspaceFile)
  424. self.lmgr.DisplayCloseAll()
  425. self.workspaceFile = None
  426. self.workspaceChanged = False
  427. self.lmgr._setTitle()
  428. self.lmgr.displayIndex = 0
  429. self.lmgr.currentPage = None