utils.py 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199
  1. """
  2. @package core.utils
  3. @brief Misc utilities for wxGUI
  4. (C) 2007-2015 by 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 Martin Landa <landa.martin gmail.com>
  8. @author Jachym Cepicky
  9. """
  10. import os
  11. import sys
  12. import platform
  13. import glob
  14. import shlex
  15. import re
  16. import inspect
  17. import operator
  18. import six
  19. from grass.script import core as grass
  20. from grass.script import task as gtask
  21. from core.gcmd import RunCommand
  22. from core.debug import Debug
  23. from core.globalvar import ETCDIR, wxPythonPhoenix
  24. def cmp(a, b):
  25. """cmp function"""
  26. return (a > b) - (a < b)
  27. def normalize_whitespace(text):
  28. """Remove redundant whitespace from a string"""
  29. return (" ").join(text.split())
  30. def split(s):
  31. """Platform spefic shlex.split"""
  32. try:
  33. if sys.platform == "win32":
  34. return shlex.split(s.replace("\\", r"\\"))
  35. else:
  36. return shlex.split(s)
  37. except ValueError as e:
  38. sys.stderr.write(_("Syntax error: %s") % e)
  39. return []
  40. def GetTempfile(pref=None):
  41. """Creates GRASS temporary file using defined prefix.
  42. .. todo::
  43. Fix path on MS Windows/MSYS
  44. :param pref: prefer the given path
  45. :return: Path to file name (string) or None
  46. """
  47. ret = RunCommand("g.tempfile", read=True, pid=os.getpid())
  48. tempfile = ret.splitlines()[0].strip()
  49. # FIXME
  50. # ugly hack for MSYS (MS Windows)
  51. if platform.system() == "Windows":
  52. tempfile = tempfile.replace("/", "\\")
  53. try:
  54. path, file = os.path.split(tempfile)
  55. if pref:
  56. return os.path.join(pref, file)
  57. else:
  58. return tempfile
  59. except:
  60. return None
  61. def GetLayerNameFromCmd(dcmd, fullyQualified=False, param=None, layerType=None):
  62. """Get map name from GRASS command
  63. Parameter dcmd can be modified when first parameter is not
  64. defined.
  65. :param dcmd: GRASS command (given as list)
  66. :param fullyQualified: change map name to be fully qualified
  67. :param param: params directory
  68. :param str layerType: check also layer type ('raster', 'vector',
  69. 'raster_3d', ...)
  70. :return: tuple (name, found)
  71. """
  72. mapname = ""
  73. found = True
  74. if len(dcmd) < 1:
  75. return mapname, False
  76. if "d.grid" == dcmd[0]:
  77. mapname = "grid"
  78. elif "d.geodesic" in dcmd[0]:
  79. mapname = "geodesic"
  80. elif "d.rhumbline" in dcmd[0]:
  81. mapname = "rhumb"
  82. elif "d.graph" in dcmd[0]:
  83. mapname = "graph"
  84. else:
  85. params = list()
  86. for idx in range(len(dcmd)):
  87. try:
  88. p, v = dcmd[idx].split("=", 1)
  89. except ValueError:
  90. continue
  91. if p == param:
  92. params = [(idx, p, v)]
  93. break
  94. # this does not use types, just some (incomplete subset of?) names
  95. if p in (
  96. "map",
  97. "input",
  98. "layer",
  99. "red",
  100. "blue",
  101. "green",
  102. "hue",
  103. "saturation",
  104. "intensity",
  105. "shade",
  106. "labels",
  107. ):
  108. params.append((idx, p, v))
  109. if len(params) < 1:
  110. if len(dcmd) > 1:
  111. i = 1
  112. while i < len(dcmd):
  113. if "=" not in dcmd[i] and not dcmd[i].startswith("-"):
  114. task = gtask.parse_interface(dcmd[0])
  115. # this expects the first parameter to be the right one
  116. p = task.get_options()["params"][0].get("name", "")
  117. params.append((i, p, dcmd[i]))
  118. break
  119. i += 1
  120. else:
  121. return mapname, False
  122. if len(params) < 1:
  123. return mapname, False
  124. # need to add mapset for all maps
  125. mapsets = {}
  126. for i, p, v in params:
  127. if p == "layer":
  128. continue
  129. mapname = v
  130. mapset = ""
  131. if fullyQualified and "@" not in mapname:
  132. if layerType in ("raster", "vector", "raster_3d", "rgb", "his"):
  133. try:
  134. if layerType in ("raster", "rgb", "his"):
  135. findType = "cell"
  136. elif layerType == "raster_3d":
  137. findType = "grid3"
  138. else:
  139. findType = layerType
  140. mapset = grass.find_file(mapname, element=findType)["mapset"]
  141. except AttributeError: # not found
  142. return "", False
  143. if not mapset:
  144. found = False
  145. else:
  146. mapset = "" # grass.gisenv()['MAPSET']
  147. mapsets[i] = mapset
  148. # update dcmd
  149. for i, p, v in params:
  150. if p == "layer":
  151. continue
  152. dcmd[i] = p + "=" + v
  153. if i in mapsets and mapsets[i]:
  154. dcmd[i] += "@" + mapsets[i]
  155. maps = list()
  156. ogr = False
  157. for i, p, v in params:
  158. if v.lower().rfind("@ogr") > -1:
  159. ogr = True
  160. if p == "layer" and not ogr:
  161. continue
  162. maps.append(dcmd[i].split("=", 1)[1])
  163. mapname = "\n".join(maps)
  164. return mapname, found
  165. def GetValidLayerName(name):
  166. """Make layer name SQL compliant, based on G_str_to_sql()
  167. .. todo::
  168. Better use directly Ctypes to reuse venerable libgis C fns...
  169. """
  170. retName = name.strip()
  171. # check if name is fully qualified
  172. if "@" in retName:
  173. retName, mapset = retName.split("@")
  174. else:
  175. mapset = None
  176. cIdx = 0
  177. retNameList = list(retName)
  178. for c in retNameList:
  179. if (
  180. not (c >= "A" and c <= "Z")
  181. and not (c >= "a" and c <= "z")
  182. and not (c >= "0" and c <= "9")
  183. ):
  184. retNameList[cIdx] = "_"
  185. cIdx += 1
  186. retName = "".join(retNameList)
  187. if not (retName[0] >= "A" and retName[0] <= "Z") and not (
  188. retName[0] >= "a" and retName[0] <= "z"
  189. ):
  190. retName = "x" + retName[1:]
  191. if mapset:
  192. retName = retName + "@" + mapset
  193. return retName
  194. def ListOfCatsToRange(cats):
  195. """Convert list of category number to range(s)
  196. Used for example for d.vect cats=[range]
  197. :param cats: category list
  198. :return: category range string
  199. :return: '' on error
  200. """
  201. catstr = ""
  202. try:
  203. cats = list(map(int, cats))
  204. except:
  205. return catstr
  206. i = 0
  207. while i < len(cats):
  208. next = 0
  209. j = i + 1
  210. while j < len(cats):
  211. if cats[i + next] == cats[j] - 1:
  212. next += 1
  213. else:
  214. break
  215. j += 1
  216. if next > 1:
  217. catstr += "%d-%d," % (cats[i], cats[i + next])
  218. i += next + 1
  219. else:
  220. catstr += "%d," % (cats[i])
  221. i += 1
  222. return catstr.strip(",")
  223. def ListOfMapsets(get="ordered"):
  224. """Get list of available/accessible mapsets
  225. :param str get: method ('all', 'accessible', 'ordered')
  226. :return: list of mapsets
  227. :return: None on error
  228. """
  229. mapsets = []
  230. if get == "all" or get == "ordered":
  231. ret = RunCommand("g.mapsets", read=True, quiet=True, flags="l", sep="newline")
  232. if ret:
  233. mapsets = ret.splitlines()
  234. ListSortLower(mapsets)
  235. else:
  236. return None
  237. if get == "accessible" or get == "ordered":
  238. ret = RunCommand("g.mapsets", read=True, quiet=True, flags="p", sep="newline")
  239. if ret:
  240. if get == "accessible":
  241. mapsets = ret.splitlines()
  242. else:
  243. mapsets_accessible = ret.splitlines()
  244. for mapset in mapsets_accessible:
  245. mapsets.remove(mapset)
  246. mapsets = mapsets_accessible + mapsets
  247. else:
  248. return None
  249. return mapsets
  250. def ListSortLower(list):
  251. """Sort list items (not case-sensitive)"""
  252. list.sort(key=lambda x: x.lower())
  253. def GetVectorNumberOfLayers(vector):
  254. """Get list of all vector layers"""
  255. layers = list()
  256. if not vector:
  257. return layers
  258. fullname = grass.find_file(name=vector, element="vector")["fullname"]
  259. if not fullname:
  260. Debug.msg(
  261. 5, "utils.GetVectorNumberOfLayers(): vector map '%s' not found" % vector
  262. )
  263. return layers
  264. ret, out, msg = RunCommand(
  265. "v.category", getErrorMsg=True, read=True, input=fullname, option="layers"
  266. )
  267. if ret != 0:
  268. sys.stderr.write(
  269. _("Vector map <%(map)s>: %(msg)s\n") % {"map": fullname, "msg": msg}
  270. )
  271. return layers
  272. else:
  273. Debug.msg(1, "GetVectorNumberOfLayers(): ret %s" % ret)
  274. for layer in out.splitlines():
  275. layers.append(layer)
  276. Debug.msg(
  277. 3,
  278. "utils.GetVectorNumberOfLayers(): vector=%s -> %s"
  279. % (fullname, ",".join(layers)),
  280. )
  281. return layers
  282. def Deg2DMS(lon, lat, string=True, hemisphere=True, precision=3):
  283. """Convert deg value to dms string
  284. :param lon: longitude (x)
  285. :param lat: latitude (y)
  286. :param string: True to return string otherwise tuple
  287. :param hemisphere: print hemisphere
  288. :param precision: seconds precision
  289. :return: DMS string or tuple of values
  290. :return: empty string on error
  291. """
  292. try:
  293. flat = float(lat)
  294. flon = float(lon)
  295. except ValueError:
  296. if string:
  297. return ""
  298. else:
  299. return None
  300. # fix longitude
  301. while flon > 180.0:
  302. flon -= 360.0
  303. while flon < -180.0:
  304. flon += 360.0
  305. # hemisphere
  306. if hemisphere:
  307. if flat < 0.0:
  308. flat = abs(flat)
  309. hlat = "S"
  310. else:
  311. hlat = "N"
  312. if flon < 0.0:
  313. hlon = "W"
  314. flon = abs(flon)
  315. else:
  316. hlon = "E"
  317. else:
  318. flat = abs(flat)
  319. flon = abs(flon)
  320. hlon = ""
  321. hlat = ""
  322. slat = __ll_parts(flat, precision=precision)
  323. slon = __ll_parts(flon, precision=precision)
  324. if string:
  325. return slon + hlon + "; " + slat + hlat
  326. return (slon + hlon, slat + hlat)
  327. def DMS2Deg(lon, lat):
  328. """Convert dms value to deg
  329. :param lon: longitude (x)
  330. :param lat: latitude (y)
  331. :return: tuple of converted values
  332. :return: ValueError on error
  333. """
  334. x = __ll_parts(lon, reverse=True)
  335. y = __ll_parts(lat, reverse=True)
  336. return (x, y)
  337. def __ll_parts(value, reverse=False, precision=3):
  338. """Converts deg to d:m:s string
  339. :param value: value to be converted
  340. :param reverse: True to convert from d:m:s to deg
  341. :param precision: seconds precision (ignored if reverse is True)
  342. :return: converted value (string/float)
  343. :return: ValueError on error (reverse == True)
  344. """
  345. if not reverse:
  346. if value == 0.0:
  347. return "%s%.*f" % ("00:00:0", precision, 0.0)
  348. d = int(int(value))
  349. m = int((value - d) * 60)
  350. s = ((value - d) * 60 - m) * 60
  351. if m < 0:
  352. m = "00"
  353. elif m < 10:
  354. m = "0" + str(m)
  355. else:
  356. m = str(m)
  357. if s < 0:
  358. s = "00.0000"
  359. elif s < 10.0:
  360. s = "0%.*f" % (precision, s)
  361. else:
  362. s = "%.*f" % (precision, s)
  363. return str(d) + ":" + m + ":" + s
  364. else: # -> reverse
  365. try:
  366. d, m, s = value.split(":")
  367. hs = s[-1]
  368. s = s[:-1]
  369. except ValueError:
  370. try:
  371. d, m = value.split(":")
  372. hs = m[-1]
  373. m = m[:-1]
  374. s = "0.0"
  375. except ValueError:
  376. try:
  377. d = value
  378. hs = d[-1]
  379. d = d[:-1]
  380. m = "0"
  381. s = "0.0"
  382. except ValueError:
  383. raise ValueError
  384. if hs not in ("N", "S", "E", "W"):
  385. raise ValueError
  386. coef = 1.0
  387. if hs in ("S", "W"):
  388. coef = -1.0
  389. fm = int(m) / 60.0
  390. fs = float(s) / (60 * 60)
  391. return coef * (float(d) + fm + fs)
  392. def GetCmdString(cmd):
  393. """Get GRASS command as string.
  394. :param cmd: GRASS command given as tuple
  395. :return: command string
  396. """
  397. return " ".join(gtask.cmdtuple_to_list(cmd))
  398. def PathJoin(*args):
  399. """Check path created by os.path.join"""
  400. path = os.path.join(*args)
  401. if platform.system() == "Windows" and "/" in path:
  402. return path[1].upper() + ":\\" + path[3:].replace("/", "\\")
  403. return path
  404. def ReadEpsgCodes():
  405. """Read EPSG codes with g.proj
  406. :return: dictionary of EPSG code
  407. """
  408. epsgCodeDict = dict()
  409. ret = RunCommand("g.proj", read=True, list_codes="EPSG")
  410. for line in ret.splitlines():
  411. code, descr, params = line.split("|")
  412. epsgCodeDict[int(code)] = (descr, params)
  413. return epsgCodeDict
  414. def ReprojectCoordinates(coord, projOut, projIn=None, flags=""):
  415. """Reproject coordinates
  416. :param coord: coordinates given as tuple
  417. :param projOut: output projection
  418. :param projIn: input projection (use location projection settings)
  419. :return: reprojected coordinates (returned as tuple)
  420. """
  421. coors = RunCommand(
  422. "m.proj",
  423. flags=flags,
  424. input="-",
  425. proj_in=projIn,
  426. proj_out=projOut,
  427. sep=";",
  428. stdin="%f;%f" % (coord[0], coord[1]),
  429. read=True,
  430. )
  431. if coors:
  432. coors = coors.split(";")
  433. e = coors[0]
  434. n = coors[1]
  435. try:
  436. proj = projOut.split(" ")[0].split("=")[1]
  437. except IndexError:
  438. proj = ""
  439. if proj in ("ll", "latlong", "longlat") and "d" not in flags:
  440. return (proj, (e, n))
  441. else:
  442. try:
  443. return (proj, (float(e), float(n)))
  444. except ValueError:
  445. return (None, None)
  446. return (None, None)
  447. def GetListOfLocations(dbase):
  448. """Get list of GRASS locations in given dbase
  449. :param dbase: GRASS database path
  450. :return: list of locations (sorted)
  451. """
  452. listOfLocations = list()
  453. try:
  454. for location in glob.glob(os.path.join(dbase, "*")):
  455. try:
  456. if os.path.join(location, "PERMANENT") in glob.glob(
  457. os.path.join(location, "*")
  458. ):
  459. listOfLocations.append(os.path.basename(location))
  460. except:
  461. pass
  462. except (UnicodeEncodeError, UnicodeDecodeError) as e:
  463. raise e
  464. ListSortLower(listOfLocations)
  465. return listOfLocations
  466. def GetListOfMapsets(dbase, location, selectable=False):
  467. """Get list of mapsets in given GRASS location
  468. :param dbase: GRASS database path
  469. :param location: GRASS location
  470. :param selectable: True to get list of selectable mapsets, otherwise all
  471. :return: list of mapsets - sorted (PERMANENT first)
  472. """
  473. listOfMapsets = list()
  474. if selectable:
  475. ret = RunCommand(
  476. "g.mapset", read=True, flags="l", location=location, dbase=dbase
  477. )
  478. if not ret:
  479. return listOfMapsets
  480. for line in ret.rstrip().splitlines():
  481. listOfMapsets += line.split(" ")
  482. else:
  483. for mapset in glob.glob(os.path.join(dbase, location, "*")):
  484. if os.path.isdir(mapset) and os.path.isfile(
  485. os.path.join(dbase, location, mapset, "WIND")
  486. ):
  487. listOfMapsets.append(os.path.basename(mapset))
  488. ListSortLower(listOfMapsets)
  489. return listOfMapsets
  490. def GetColorTables():
  491. """Get list of color tables"""
  492. ret = RunCommand("r.colors", read=True, flags="l")
  493. if not ret:
  494. return list()
  495. return ret.splitlines()
  496. def _getGDALFormats():
  497. """Get dictionary of available GDAL drivers"""
  498. try:
  499. ret = grass.read_command("r.in.gdal", quiet=True, flags="f")
  500. except:
  501. ret = None
  502. return _parseFormats(ret), _parseFormats(ret, writableOnly=True)
  503. def _getOGRFormats():
  504. """Get dictionary of available OGR drivers"""
  505. try:
  506. ret = grass.read_command("v.in.ogr", quiet=True, flags="f")
  507. except:
  508. ret = None
  509. return _parseFormats(ret), _parseFormats(ret, writableOnly=True)
  510. def _parseFormats(output, writableOnly=False):
  511. """Parse r.in.gdal/v.in.ogr -f output"""
  512. formats = {"file": {}, "database": {}, "protocol": {}}
  513. if not output:
  514. return formats
  515. patt = None
  516. if writableOnly:
  517. patt = re.compile("\(rw\+?\)$", re.IGNORECASE)
  518. for line in output.splitlines():
  519. key, name = map(lambda x: x.strip(), line.strip().split(":", 1))
  520. if writableOnly and not patt.search(key):
  521. continue
  522. if name in ("Memory", "Virtual Raster", "In Memory Raster"):
  523. continue
  524. if name in (
  525. "PostgreSQL",
  526. "SQLite",
  527. "ODBC",
  528. "ESRI Personal GeoDatabase",
  529. "Rasterlite",
  530. "PostGIS WKT Raster driver",
  531. "PostGIS Raster driver",
  532. "CouchDB",
  533. "MSSQLSpatial",
  534. "FileGDB",
  535. ):
  536. formats["database"][key.split(" ")[0]] = name
  537. elif name in (
  538. "GeoJSON",
  539. "OGC Web Coverage Service",
  540. "OGC Web Map Service",
  541. "WFS",
  542. "GeoRSS",
  543. "HTTP Fetching Wrapper",
  544. ):
  545. formats["protocol"][key.split(" ")[0]] = name
  546. else:
  547. formats["file"][key.split(" ")[0]] = name
  548. for k, v in formats.items():
  549. formats[k] = dict(sorted(v.items(), key=operator.itemgetter(1)))
  550. return formats
  551. formats = None
  552. def GetFormats(writableOnly=False):
  553. """Get GDAL/OGR formats"""
  554. global formats
  555. if not formats:
  556. gdalAll, gdalWritable = _getGDALFormats()
  557. ogrAll, ogrWritable = _getOGRFormats()
  558. formats = {
  559. "all": {
  560. "gdal": gdalAll,
  561. "ogr": ogrAll,
  562. },
  563. "writable": {
  564. "gdal": gdalWritable,
  565. "ogr": ogrWritable,
  566. },
  567. }
  568. if writableOnly:
  569. return formats["writable"]
  570. return formats["all"]
  571. rasterFormatExtension = {
  572. "GeoTIFF": "tif",
  573. "Erdas Imagine Images (.img)": "img",
  574. "Ground-based SAR Applications Testbed File Format (.gff)": "gff",
  575. "Arc/Info Binary Grid": "adf",
  576. "Portable Network Graphics": "png",
  577. "JPEG JFIF": "jpg",
  578. "Japanese DEM (.mem)": "mem",
  579. "Graphics Interchange Format (.gif)": "gif",
  580. "X11 PixMap Format": "xpm",
  581. "MS Windows Device Independent Bitmap": "bmp",
  582. "SPOT DIMAP": "dim",
  583. "RadarSat 2 XML Product": "xml",
  584. "EarthWatch .TIL": "til",
  585. "ERMapper .ers Labelled": "ers",
  586. "ERMapper Compressed Wavelets": "ecw",
  587. "GRIdded Binary (.grb)": "grb",
  588. "EUMETSAT Archive native (.nat)": "nat",
  589. "Idrisi Raster A.1": "rst",
  590. "Golden Software ASCII Grid (.grd)": "grd",
  591. "Golden Software Binary Grid (.grd)": "grd",
  592. "Golden Software 7 Binary Grid (.grd)": "grd",
  593. "R Object Data Store": "r",
  594. "USGS DOQ (Old Style)": "doq",
  595. "USGS DOQ (New Style)": "doq",
  596. "ENVI .hdr Labelled": "hdr",
  597. "ESRI .hdr Labelled": "hdr",
  598. "Generic Binary (.hdr Labelled)": "hdr",
  599. "PCI .aux Labelled": "aux",
  600. "EOSAT FAST Format": "fst",
  601. "VTP .bt (Binary Terrain) 1.3 Format": "bt",
  602. "FARSITE v.4 Landscape File (.lcp)": "lcp",
  603. "Swedish Grid RIK (.rik)": "rik",
  604. "USGS Optional ASCII DEM (and CDED)": "dem",
  605. "Northwood Numeric Grid Format .grd/.tab": "",
  606. "Northwood Classified Grid Format .grc/.tab": "",
  607. "ARC Digitized Raster Graphics": "arc",
  608. "Magellan topo (.blx)": "blx",
  609. "SAGA GIS Binary Grid (.sdat)": "sdat",
  610. "GeoPackage (.gpkg)": "gpkg",
  611. }
  612. vectorFormatExtension = {
  613. "ESRI Shapefile": "shp",
  614. "GeoPackage": "gpkg",
  615. "UK .NTF": "ntf",
  616. "SDTS": "ddf",
  617. "DGN": "dgn",
  618. "VRT": "vrt",
  619. "REC": "rec",
  620. "BNA": "bna",
  621. "CSV": "csv",
  622. "GML": "gml",
  623. "GPX": "gpx",
  624. "KML": "kml",
  625. "GMT": "gmt",
  626. "PGeo": "mdb",
  627. "XPlane": "dat",
  628. "AVCBin": "adf",
  629. "AVCE00": "e00",
  630. "DXF": "dxf",
  631. "Geoconcept": "gxt",
  632. "GeoRSS": "xml",
  633. "GPSTrackMaker": "gtm",
  634. "VFK": "vfk",
  635. "SVG": "svg",
  636. }
  637. def GetSettingsPath():
  638. """Get full path to the settings directory"""
  639. try:
  640. verFd = open(os.path.join(ETCDIR, "VERSIONNUMBER"))
  641. version = int(verFd.readlines()[0].split(" ")[0].split(".")[0])
  642. except (IOError, ValueError, TypeError, IndexError) as e:
  643. sys.exit(_("ERROR: Unable to determine GRASS version. Details: %s") % e)
  644. verFd.close()
  645. # keep location of settings files rc and wx in sync with lib/init/grass.py
  646. if sys.platform == "win32":
  647. return os.path.join(os.getenv("APPDATA"), "GRASS%d" % version)
  648. return os.path.join(os.getenv("HOME"), ".grass%d" % version)
  649. def StoreEnvVariable(key, value=None, envFile=None):
  650. """Store environmental variable
  651. If value is not given (is None) then environmental variable is
  652. unset.
  653. :param key: env key
  654. :param value: env value
  655. :param envFile: path to the environmental file (None for default location)
  656. """
  657. windows = sys.platform == "win32"
  658. if not envFile:
  659. gVersion = grass.version()["version"].split(".", 1)[0]
  660. if not windows:
  661. envFile = os.path.join(os.getenv("HOME"), ".grass%s" % gVersion, "bashrc")
  662. else:
  663. envFile = os.path.join(
  664. os.getenv("APPDATA"), "GRASS%s" % gVersion, "env.bat"
  665. )
  666. # read env file
  667. environ = dict()
  668. lineSkipped = list()
  669. if os.path.exists(envFile):
  670. try:
  671. fd = open(envFile)
  672. except IOError as e:
  673. sys.stderr.write(_("Unable to open file '%s'\n") % envFile)
  674. return
  675. for line in fd.readlines():
  676. line = line.rstrip(os.linesep)
  677. try:
  678. k, v = map(lambda x: x.strip(), line.split(" ", 1)[1].split("=", 1))
  679. except Exception as e:
  680. sys.stderr.write(
  681. _("%s: line skipped - unable to parse '%s'\n" "Reason: %s\n")
  682. % (envFile, line, e)
  683. )
  684. lineSkipped.append(line)
  685. continue
  686. if k in environ:
  687. sys.stderr.write(_("Duplicated key: %s\n") % k)
  688. environ[k] = v
  689. fd.close()
  690. # update environmental variables
  691. if value is None:
  692. if key in environ:
  693. del environ[key]
  694. else:
  695. environ[key] = value
  696. # write update env file
  697. try:
  698. fd = open(envFile, "w")
  699. except IOError as e:
  700. sys.stderr.write(_("Unable to create file '%s'\n") % envFile)
  701. return
  702. if windows:
  703. expCmd = "set"
  704. else:
  705. expCmd = "export"
  706. for key, value in six.iteritems(environ):
  707. fd.write("%s %s=%s\n" % (expCmd, key, value))
  708. # write also skipped lines
  709. for line in lineSkipped:
  710. fd.write(line + os.linesep)
  711. fd.close()
  712. def SetAddOnPath(addonPath=None, key="PATH"):
  713. """Set default AddOn path
  714. :param addonPath: path to addons (None for default)
  715. :param key: env key - 'PATH' or 'BASE'
  716. """
  717. gVersion = grass.version()["version"].split(".", 1)[0]
  718. # update env file
  719. if not addonPath:
  720. if sys.platform != "win32":
  721. addonPath = os.path.join(
  722. os.path.join(os.getenv("HOME"), ".grass%s" % gVersion, "addons")
  723. )
  724. else:
  725. addonPath = os.path.join(
  726. os.path.join(os.getenv("APPDATA"), "GRASS%s" % gVersion, "addons")
  727. )
  728. StoreEnvVariable(key="GRASS_ADDON_" + key, value=addonPath)
  729. os.environ["GRASS_ADDON_" + key] = addonPath
  730. # update path
  731. if addonPath not in os.environ["PATH"]:
  732. os.environ["PATH"] = addonPath + os.pathsep + os.environ["PATH"]
  733. # predefined colors and their names
  734. # must be in sync with lib/gis/color_str.c
  735. str2rgb = {
  736. "aqua": (100, 128, 255),
  737. "black": (0, 0, 0),
  738. "blue": (0, 0, 255),
  739. "brown": (180, 77, 25),
  740. "cyan": (0, 255, 255),
  741. "gray": (128, 128, 128),
  742. "grey": (128, 128, 128),
  743. "green": (0, 255, 0),
  744. "indigo": (0, 128, 255),
  745. "magenta": (255, 0, 255),
  746. "orange": (255, 128, 0),
  747. "red": (255, 0, 0),
  748. "violet": (128, 0, 255),
  749. "purple": (128, 0, 255),
  750. "white": (255, 255, 255),
  751. "yellow": (255, 255, 0),
  752. }
  753. rgb2str = {}
  754. for (s, r) in str2rgb.items():
  755. rgb2str[r] = s
  756. # ensure that gray value has 'gray' string and not 'grey'
  757. rgb2str[str2rgb["gray"]] = "gray"
  758. # purple is defined as nickname for violet in lib/gis
  759. # (although Wikipedia says that purple is (128, 0, 128))
  760. # we will prefer the defined color, not nickname
  761. rgb2str[str2rgb["violet"]] = "violet"
  762. def color_resolve(color):
  763. if len(color) > 0 and color[0] in "0123456789":
  764. rgb = tuple(map(int, color.split(":")))
  765. label = color
  766. else:
  767. # Convert color names to RGB
  768. try:
  769. rgb = str2rgb[color]
  770. label = color
  771. except KeyError:
  772. rgb = (200, 200, 200)
  773. label = _("Select Color")
  774. return (rgb, label)
  775. command2ltype = {
  776. "d.rast": "raster",
  777. "d.rast3d": "raster_3d",
  778. "d.rgb": "rgb",
  779. "d.his": "his",
  780. "d.shade": "shaded",
  781. "d.legend": "rastleg",
  782. "d.rast.arrow": "rastarrow",
  783. "d.rast.num": "rastnum",
  784. "d.rast.leg": "maplegend",
  785. "d.vect": "vector",
  786. "d.vect.thematic": "thememap",
  787. "d.vect.chart": "themechart",
  788. "d.grid": "grid",
  789. "d.geodesic": "geodesic",
  790. "d.rhumbline": "rhumb",
  791. "d.labels": "labels",
  792. "d.barscale": "barscale",
  793. "d.redraw": "redraw",
  794. "d.wms": "wms",
  795. "d.histogram": "histogram",
  796. "d.colortable": "colortable",
  797. "d.graph": "graph",
  798. "d.out.file": "export",
  799. "d.to.rast": "torast",
  800. "d.text": "text",
  801. "d.northarrow": "northarrow",
  802. "d.polar": "polar",
  803. "d.legend.vect": "vectleg",
  804. }
  805. ltype2command = {}
  806. for (cmd, ltype) in command2ltype.items():
  807. ltype2command[ltype] = cmd
  808. def GetGEventAttribsForHandler(method, event):
  809. """Get attributes from event, which can be used by handler method.
  810. Be aware of event class attributes.
  811. :param method: handler method (including self arg)
  812. :param event: event
  813. :return: (valid kwargs for method,
  814. list of method's args without default value
  815. which were not found among event attributes)
  816. """
  817. args_spec = inspect.getargspec(method)
  818. args = args_spec[0]
  819. defaults = []
  820. if args_spec[3]:
  821. defaults = args_spec[3]
  822. # number of arguments without def value
  823. req_args = len(args) - 1 - len(defaults)
  824. kwargs = {}
  825. missing_args = []
  826. for i, a in enumerate(args):
  827. if hasattr(event, a):
  828. kwargs[a] = getattr(event, a)
  829. elif i < req_args:
  830. missing_args.append(a)
  831. return kwargs, missing_args
  832. def PilImageToWxImage(pilImage, copyAlpha=True):
  833. """Convert PIL image to wx.Image
  834. Based on http://wiki.wxpython.org/WorkingWithImages
  835. """
  836. from gui_core.wrap import EmptyImage
  837. hasAlpha = pilImage.mode[-1] == "A"
  838. if copyAlpha and hasAlpha: # Make sure there is an alpha layer copy.
  839. wxImage = EmptyImage(*pilImage.size)
  840. pilImageCopyRGBA = pilImage.copy()
  841. pilImageCopyRGB = pilImageCopyRGBA.convert("RGB") # RGBA --> RGB
  842. wxImage.SetData(pilImageCopyRGB.tobytes())
  843. # Create layer and insert alpha values.
  844. if wxPythonPhoenix:
  845. wxImage.SetAlpha(pilImageCopyRGBA.tobytes()[3::4])
  846. else:
  847. wxImage.SetAlphaData(pilImageCopyRGBA.tobytes()[3::4])
  848. else: # The resulting image will not have alpha.
  849. wxImage = EmptyImage(*pilImage.size)
  850. pilImageCopy = pilImage.copy()
  851. # Discard any alpha from the PIL image.
  852. pilImageCopyRGB = pilImageCopy.convert("RGB")
  853. wxImage.SetData(pilImageCopyRGB.tobytes())
  854. return wxImage
  855. def autoCropImageFromFile(filename):
  856. """Loads image from file and crops it automatically.
  857. If PIL is not installed, it does not crop it.
  858. :param filename: path to file
  859. :return: wx.Image instance
  860. """
  861. try:
  862. from PIL import Image
  863. pilImage = Image.open(filename)
  864. imageBox = pilImage.getbbox()
  865. cropped = pilImage.crop(imageBox)
  866. return PilImageToWxImage(cropped, copyAlpha=True)
  867. except ImportError:
  868. import wx
  869. return wx.Image(filename)
  870. def isInRegion(regionA, regionB):
  871. """Tests if 'regionA' is inside of 'regionB'.
  872. For example, region A is a display region and region B is some reference
  873. region, e.g., a computational region.
  874. >>> displayRegion = {'n': 223900, 's': 217190, 'w': 630780, 'e': 640690}
  875. >>> compRegion = {'n': 228500, 's': 215000, 'w': 630000, 'e': 645000}
  876. >>> isInRegion(displayRegion, compRegion)
  877. True
  878. >>> displayRegion = {'n':226020, 's': 212610, 'w': 626510, 'e': 646330}
  879. >>> isInRegion(displayRegion, compRegion)
  880. False
  881. :param regionA: input region A as dictionary
  882. :param regionB: input region B as dictionary
  883. :return: True if region A is inside of region B
  884. :return: False othewise
  885. """
  886. if (
  887. regionA["s"] >= regionB["s"]
  888. and regionA["n"] <= regionB["n"]
  889. and regionA["w"] >= regionB["w"]
  890. and regionA["e"] <= regionB["e"]
  891. ):
  892. return True
  893. return False
  894. def do_doctest_gettext_workaround():
  895. """Setups environment for doing a doctest with gettext usage.
  896. When using gettext with dynamically defined underscore function
  897. (`_("For translation")`), doctest does not work properly. One option is to
  898. use `import as` instead of dynamically defined underscore function but this
  899. would require change all modules which are used by tested module. This
  900. should be considered for the future. The second option is to define dummy
  901. underscore function and one other function which creates the right
  902. environment to satisfy all. This is done by this function.
  903. """
  904. def new_displayhook(string):
  905. """A replacement for default `sys.displayhook`"""
  906. if string is not None:
  907. sys.stdout.write("%r\n" % (string,))
  908. def new_translator(string):
  909. """A fake gettext underscore function."""
  910. return string
  911. sys.displayhook = new_displayhook
  912. import __builtin__
  913. __builtin__._ = new_translator
  914. def doc_test():
  915. """Tests the module using doctest
  916. :return: a number of failed tests
  917. """
  918. import doctest
  919. do_doctest_gettext_workaround()
  920. return doctest.testmod().failed
  921. def registerPid(pid):
  922. """Register process id as GUI_PID GRASS variable
  923. :param: pid process id
  924. """
  925. env = grass.gisenv()
  926. guiPid = []
  927. if "GUI_PID" in env:
  928. guiPid = env["GUI_PID"].split(",")
  929. guiPid.append(str(pid))
  930. grass.run_command("g.gisenv", set="GUI_PID={0}".format(",".join(guiPid)))
  931. def unregisterPid(pid):
  932. """Unregister process id from GUI_PID GRASS variable
  933. :param: pid process id
  934. """
  935. env = grass.gisenv()
  936. if "GUI_PID" not in env:
  937. return
  938. guiPid = env["GUI_PID"].split(",")
  939. pid = str(os.getpid())
  940. if pid in guiPid:
  941. guiPid.remove(pid)
  942. grass.run_command("g.gisenv", set="GUI_PID={0}".format(",".join(guiPid)))
  943. def get_shell_pid(env=None):
  944. """Get shell PID from the GIS environment or None"""
  945. try:
  946. shell_pid = int(grass.gisenv(env=env)["PID"])
  947. return shell_pid
  948. except (KeyError, ValueError) as error:
  949. Debug.msg(
  950. 1, "No PID for GRASS shell (assuming no shell running): {}".format(error)
  951. )
  952. return None
  953. def is_shell_running():
  954. """Return True if a separate shell is registered in the GIS environment"""
  955. if get_shell_pid() is None:
  956. return False
  957. return True
  958. if __name__ == "__main__":
  959. sys.exit(doc_test())