guiutils.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. """
  2. @package startup.guiutils
  3. @brief General GUI-dependent utilities for GUI startup of GRASS GIS
  4. (C) 2018 by Vaclav Petras the GRASS Development Team
  5. This program is free software under the GNU General Public License
  6. (>=v2). Read the file COPYING that comes with GRASS for details.
  7. @author Vaclav Petras <wenzeslaus gmail com>
  8. @author Linda Kladivova <l.kladivova@seznam.cz>
  9. This is for code which depend on something from GUI (wx or wxGUI).
  10. """
  11. import os
  12. import sys
  13. import wx
  14. import grass.script as gs
  15. from grass.script import gisenv
  16. from core import globalvar
  17. from core.gcmd import GError, GMessage, DecodeString, RunCommand
  18. from gui_core.dialogs import TextEntryDialog
  19. from location_wizard.dialogs import RegionDef
  20. from gui_core.widgets import GenericMultiValidator
  21. from startup.utils import (create_mapset, delete_mapset, delete_location,
  22. rename_mapset, rename_location, mapset_exists,
  23. location_exists, get_default_mapset_name)
  24. def SetSessionMapset(database, location, mapset):
  25. """Sets database, location and mapset for the current session"""
  26. RunCommand("g.gisenv", set="GISDBASE=%s" % database)
  27. RunCommand("g.gisenv", set="LOCATION_NAME=%s" % location)
  28. RunCommand("g.gisenv", set="MAPSET=%s" % mapset)
  29. class MapsetDialog(TextEntryDialog):
  30. def __init__(self, parent=None, default=None, message=None, caption=None,
  31. database=None, location=None):
  32. self.database = database
  33. self.location = location
  34. # list of tuples consisting of conditions and callbacks
  35. checks = [(gs.legal_name, self._nameValidationFailed),
  36. (self._checkMapsetNotExists, self._mapsetAlreadyExists),
  37. (self._checkOGR, self._reservedMapsetName)]
  38. validator = GenericMultiValidator(checks)
  39. TextEntryDialog.__init__(
  40. self, parent=parent,
  41. message=message,
  42. caption=caption,
  43. defaultValue=default,
  44. validator=validator,
  45. )
  46. def _nameValidationFailed(self, ctrl):
  47. message = _(
  48. "Name '{}' is not a valid name for location or mapset. "
  49. "Please use only ASCII characters excluding characters {} "
  50. "and space.").format(ctrl.GetValue(), '/"\'@,=*~')
  51. GError(parent=self, message=message, caption=_("Invalid name"))
  52. def _checkOGR(self, text):
  53. """Check user's input for reserved mapset name."""
  54. if text.lower() == 'ogr':
  55. return False
  56. return True
  57. def _reservedMapsetName(self, ctrl):
  58. message = _(
  59. "Name '{}' is reserved for direct "
  60. "read access to OGR layers. Please use "
  61. "another name for your mapset.").format(ctrl.GetValue())
  62. GError(parent=self, message=message,
  63. caption=_("Reserved mapset name"))
  64. def _checkMapsetNotExists(self, text):
  65. """Check whether user's input mapset exists or not."""
  66. if mapset_exists(self.database, self.location, text):
  67. return False
  68. return True
  69. def _mapsetAlreadyExists(self, ctrl):
  70. message = _(
  71. "Mapset '{}' already exists. Please consider using "
  72. "another name for your mapset.").format(ctrl.GetValue())
  73. GError(parent=self, message=message,
  74. caption=_("Existing mapset path"))
  75. class LocationDialog(TextEntryDialog):
  76. def __init__(self, parent=None, default=None, message=None, caption=None,
  77. database=None):
  78. self.database = database
  79. # list of tuples consisting of conditions and callbacks
  80. checks = [(gs.legal_name, self._nameValidationFailed),
  81. (self._checkLocationNotExists, self._locationAlreadyExists)]
  82. validator = GenericMultiValidator(checks)
  83. TextEntryDialog.__init__(
  84. self, parent=parent,
  85. message=message,
  86. caption=caption,
  87. defaultValue=default,
  88. validator=validator,
  89. )
  90. def _nameValidationFailed(self, ctrl):
  91. message = _(
  92. "Name '{}' is not a valid name for location or mapset. "
  93. "Please use only ASCII characters excluding characters {} "
  94. "and space.").format(ctrl.GetValue(), '/"\'@,=*~')
  95. GError(parent=self, message=message, caption=_("Invalid name"))
  96. def _checkLocationNotExists(self, text):
  97. """Check whether user's input location exists or not."""
  98. if location_exists(self.database, text):
  99. return False
  100. return True
  101. def _locationAlreadyExists(self, ctrl):
  102. message = _(
  103. "Location '{}' already exists. Please consider using "
  104. "another name for your location.").format(ctrl.GetValue())
  105. GError(parent=self, message=message,
  106. caption=_("Existing location path"))
  107. # TODO: similar to (but not the same as) read_gisrc function in grass.py
  108. def read_gisrc():
  109. """Read variables from a current GISRC file
  110. Returns a dictionary representation of the file content.
  111. """
  112. grassrc = {}
  113. gisrc = os.getenv("GISRC")
  114. if gisrc and os.path.isfile(gisrc):
  115. try:
  116. rc = open(gisrc, "r")
  117. for line in rc.readlines():
  118. try:
  119. key, val = line.split(":", 1)
  120. except ValueError as e:
  121. sys.stderr.write(
  122. _('Invalid line in GISRC file (%s):%s\n' % (e, line)))
  123. grassrc[key.strip()] = DecodeString(val.strip())
  124. finally:
  125. rc.close()
  126. return grassrc
  127. def GetVersion():
  128. """Gets version and revision
  129. Returns tuple `(version, revision)`. For standard releases revision
  130. is an empty string.
  131. Revision string is currently wrapped in parentheses with added
  132. leading space. This is an implementation detail and legacy and may
  133. change anytime.
  134. """
  135. versionFile = open(os.path.join(globalvar.ETCDIR, "VERSIONNUMBER"))
  136. versionLine = versionFile.readline().rstrip('\n')
  137. versionFile.close()
  138. try:
  139. grassVersion, grassRevision = versionLine.split(' ', 1)
  140. if grassVersion.endswith('dev'):
  141. grassRevisionStr = ' (%s)' % grassRevision
  142. else:
  143. grassRevisionStr = ''
  144. except ValueError:
  145. grassVersion = versionLine
  146. grassRevisionStr = ''
  147. return (grassVersion, grassRevisionStr)
  148. def create_mapset_interactively(guiparent, grassdb, location):
  149. """
  150. Create new mapset
  151. """
  152. dlg = MapsetDialog(
  153. parent=guiparent,
  154. default=get_default_mapset_name(),
  155. message=_("Name for the new mapset:"),
  156. caption=_("Create new mapset"),
  157. database=grassdb,
  158. location=location,
  159. )
  160. mapset = None
  161. if dlg.ShowModal() == wx.ID_OK:
  162. mapset = dlg.GetValue()
  163. try:
  164. create_mapset(grassdb, location, mapset)
  165. except OSError as err:
  166. mapset = None
  167. GError(
  168. parent=guiparent,
  169. message=_("Unable to create new mapset: {}").format(err),
  170. showTraceback=False,
  171. )
  172. dlg.Destroy()
  173. return mapset
  174. def create_location_interactively(guiparent, grassdb):
  175. """
  176. Create new location using Location Wizard.
  177. Returns tuple (database, location, mapset) where mapset is "PERMANENT"
  178. by default or another mapset a user created and may want to switch to.
  179. """
  180. from location_wizard.wizard import LocationWizard
  181. gWizard = LocationWizard(parent=guiparent,
  182. grassdatabase=grassdb)
  183. if gWizard.location is None:
  184. gWizard_output = (None, None, None)
  185. # Returns Nones after Cancel
  186. return gWizard_output
  187. if gWizard.georeffile:
  188. message = _(
  189. "Do you want to import {}"
  190. "to the newly created location?"
  191. ).format(gWizard.georeffile)
  192. dlg = wx.MessageDialog(parent=guiparent,
  193. message=message,
  194. caption=_("Import data?"),
  195. style=wx.YES_NO | wx.YES_DEFAULT |
  196. wx.ICON_QUESTION)
  197. dlg.CenterOnParent()
  198. if dlg.ShowModal() == wx.ID_YES:
  199. import_file(guiparent, gWizard.georeffile)
  200. dlg.Destroy()
  201. if gWizard.default_region:
  202. defineRegion = RegionDef(guiparent, location=gWizard.location)
  203. defineRegion.CenterOnParent()
  204. defineRegion.ShowModal()
  205. defineRegion.Destroy()
  206. if gWizard.user_mapset:
  207. mapset = create_mapset_interactively(guiparent,
  208. gWizard.grassdatabase,
  209. gWizard.location)
  210. # Returns database and location created by user
  211. # and a mapset user may want to switch to
  212. gWizard_output = (gWizard.grassdatabase, gWizard.location,
  213. mapset)
  214. else:
  215. # Returns PERMANENT mapset when user mapset not defined
  216. gWizard_output = (gWizard.grassdatabase, gWizard.location,
  217. "PERMANENT")
  218. return gWizard_output
  219. def rename_mapset_interactively(guiparent, grassdb, location, mapset):
  220. """
  221. Rename selected mapset
  222. """
  223. newmapset = None
  224. if mapset == "PERMANENT":
  225. GMessage(
  226. parent=guiparent,
  227. message=_(
  228. "Mapset <PERMANENT> is required for valid GRASS location.\n\n"
  229. "This mapset cannot be renamed."
  230. ),
  231. )
  232. return newmapset
  233. dlg = MapsetDialog(
  234. parent=guiparent,
  235. default=mapset,
  236. message=_("Current name: {}\n\nEnter new name:").format(mapset),
  237. caption=_("Rename selected mapset"),
  238. database=grassdb,
  239. location=location,
  240. )
  241. if dlg.ShowModal() == wx.ID_OK:
  242. newmapset = dlg.GetValue()
  243. try:
  244. rename_mapset(grassdb, location, mapset, newmapset)
  245. except OSError as err:
  246. newmapset = None
  247. wx.MessageBox(
  248. parent=guiparent,
  249. caption=_("Error"),
  250. message=_("Unable to rename mapset.\n\n{}").format(err),
  251. style=wx.OK | wx.ICON_ERROR | wx.CENTRE,
  252. )
  253. dlg.Destroy()
  254. return newmapset
  255. def rename_location_interactively(guiparent, grassdb, location):
  256. """
  257. Rename selected location
  258. """
  259. dlg = LocationDialog(
  260. parent=guiparent,
  261. default=location,
  262. message=_("Current name: {}\n\nEnter new name:").format(location),
  263. caption=_("Rename selected location"),
  264. database=grassdb,
  265. )
  266. if dlg.ShowModal() == wx.ID_OK:
  267. newlocation = dlg.GetValue()
  268. try:
  269. rename_location(grassdb, location, newlocation)
  270. except OSError as err:
  271. newlocation = None
  272. wx.MessageBox(
  273. parent=guiparent,
  274. caption=_("Error"),
  275. message=_("Unable to rename location.\n\n{}").format(err),
  276. style=wx.OK | wx.ICON_ERROR | wx.CENTRE,
  277. )
  278. else:
  279. newlocation = None
  280. dlg.Destroy()
  281. return newlocation
  282. def download_location_interactively(guiparent, grassdb):
  283. """
  284. Download new location using Location Wizard.
  285. Returns tuple (database, location, mapset) where mapset is "PERMANENT"
  286. by default or in future it could be the mapset the user may want to
  287. switch to.
  288. """
  289. from startup.locdownload import LocationDownloadDialog
  290. result = (None, None, None)
  291. loc_download = LocationDownloadDialog(parent=guiparent,
  292. database=grassdb)
  293. loc_download.ShowModal()
  294. if loc_download.GetLocation() is not None:
  295. # Returns database and location created by user
  296. # and a mapset user may want to switch to
  297. result = (grassdb, loc_download.GetLocation(), "PERMANENT")
  298. loc_download.Destroy()
  299. return result
  300. def delete_mapset_interactively(guiparent, grassdb, location, mapset):
  301. """Delete one mapset with user interaction.
  302. This is currently just a convenience wrapper for delete_mapsets_interactively().
  303. """
  304. mapsets = [(grassdb, location, mapset)]
  305. return delete_mapsets_interactively(guiparent, mapsets)
  306. def delete_mapsets_interactively(guiparent, mapsets):
  307. """Delete multiple mapsets with user interaction.
  308. Parameter *mapsets* is a list of tuples (database, location, mapset).
  309. If PERMANENT or current mapset found, delete operation is not performed.
  310. Exceptions during deletation are handled in this function.
  311. Retuns True if there was a change, i.e., all mapsets were successfuly deleted
  312. or at least one mapset was deleted. Returns False if one or more mapsets cannot be
  313. deleted (see above the possible reasons) or if an error was encountered when
  314. deleting the first mapset in the list.
  315. """
  316. genv = gisenv()
  317. issues = []
  318. deletes = []
  319. # Check selected mapsets and remember issue.
  320. # Each error is reported only once (using elif).
  321. for grassdb, location, mapset in mapsets:
  322. mapset_path = os.path.join(grassdb, location, mapset)
  323. # Check for permanent mapsets
  324. if mapset == "PERMANENT":
  325. issue = _("<{}> is required for a valid location.").format(mapset_path)
  326. issues.append(issue)
  327. # Check for current mapset
  328. elif (
  329. grassdb == genv['GISDBASE'] and
  330. location == genv['LOCATION_NAME'] and
  331. mapset == genv['MAPSET']
  332. ):
  333. issue = _("<{}> is the current mapset.").format(mapset_path)
  334. issues.append(issue)
  335. # No issue detected
  336. else:
  337. deletes.append(mapset_path)
  338. modified = False # True after first successful delete
  339. # If any issues, display the warning message and do not delete anything
  340. if issues:
  341. issues = "\n".join(issues)
  342. dlg = wx.MessageDialog(
  343. parent=guiparent,
  344. message=_(
  345. "Cannot delete one or more mapsets for the following reasons:\n\n"
  346. "{}\n\n"
  347. "No mapsets will be deleted."
  348. ).format(issues),
  349. caption=_("Unable to delete selected mapsets"),
  350. style=wx.OK | wx.ICON_WARNING
  351. )
  352. dlg.ShowModal()
  353. else:
  354. deletes = "\n".join(deletes)
  355. dlg = wx.MessageDialog(
  356. parent=guiparent,
  357. message=_(
  358. "Do you want to continue with deleting"
  359. " one or more of the following mapsets?\n\n"
  360. "{}\n\n"
  361. "All maps included in these mapsets will be permanently deleted!"
  362. ).format(deletes),
  363. caption=_("Delete selected mapsets"),
  364. style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION,
  365. )
  366. if dlg.ShowModal() == wx.ID_YES:
  367. try:
  368. for grassdb, location, mapset in mapsets:
  369. delete_mapset(grassdb, location, mapset)
  370. modified = True
  371. dlg.Destroy()
  372. return modified
  373. except OSError as error:
  374. wx.MessageBox(
  375. parent=guiparent,
  376. caption=_("Error when deleting mapsets"),
  377. message=_(
  378. "The following error occured when deleting mapset <{path}>:"
  379. "\n\n{error}\n\n"
  380. "Deleting of mapsets was interrupted."
  381. ).format(
  382. path=os.path.join(grassdb, location, mapset),
  383. error=error,
  384. ),
  385. style=wx.OK | wx.ICON_ERROR | wx.CENTRE,
  386. )
  387. dlg.Destroy()
  388. return modified
  389. def delete_location_interactively(guiparent, grassdb, location):
  390. """
  391. Delete selected location
  392. """
  393. dlg = wx.MessageDialog(
  394. parent=guiparent,
  395. message=_(
  396. "Do you want to continue with deleting "
  397. "location {}?\n\n"
  398. "ALL MAPS included in this location will be "
  399. "PERMANENTLY DELETED!"
  400. ).format(location),
  401. caption=_("Delete selected location"),
  402. style=wx.YES_NO | wx.NO_DEFAULT | wx.ICON_QUESTION,
  403. )
  404. if dlg.ShowModal() == wx.ID_YES:
  405. try:
  406. delete_location(grassdb, location)
  407. dlg.Destroy()
  408. return True
  409. except OSError as err:
  410. wx.MessageBox(
  411. parent=guiparent,
  412. caption=_("Error"),
  413. message=_("Unable to delete location.\n\n{}").format(err),
  414. style=wx.OK | wx.ICON_ERROR | wx.CENTRE,
  415. )
  416. dlg.Destroy()
  417. return False
  418. def import_file(guiparent, filePath):
  419. """Tries to import file as vector or raster.
  420. If successfull sets default region from imported map.
  421. """
  422. RunCommand('db.connect', flags='c')
  423. mapName = os.path.splitext(os.path.basename(filePath))[0]
  424. vectors = RunCommand('v.in.ogr', input=filePath, flags='l',
  425. read=True)
  426. wx.BeginBusyCursor()
  427. wx.GetApp().Yield()
  428. if vectors:
  429. # vector detected
  430. returncode, error = RunCommand(
  431. 'v.in.ogr', input=filePath, output=mapName, flags='e',
  432. getErrorMsg=True)
  433. else:
  434. returncode, error = RunCommand(
  435. 'r.in.gdal', input=filePath, output=mapName, flags='e',
  436. getErrorMsg=True)
  437. wx.EndBusyCursor()
  438. if returncode != 0:
  439. GError(
  440. parent=guiparent,
  441. message=_(
  442. "Import of <%(name)s> failed.\n"
  443. "Reason: %(msg)s") % ({
  444. 'name': filePath,
  445. 'msg': error}))
  446. else:
  447. GMessage(
  448. message=_(
  449. "Data file <%(name)s> imported successfully. "
  450. "The location's default region was set from "
  451. "this imported map.") % {
  452. 'name': filePath},
  453. parent=guiparent)