GdalUtils.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. """
  2. ***************************************************************************
  3. GdalUtils.py
  4. ---------------------
  5. Date : August 2012
  6. Copyright : (C) 2012 by Victor Olaya
  7. Email : volayaf at gmail dot com
  8. ***************************************************************************
  9. * *
  10. * This program is free software; you can redistribute it and/or modify *
  11. * it under the terms of the GNU General Public License as published by *
  12. * the Free Software Foundation; either version 2 of the License, or *
  13. * (at your option) any later version. *
  14. * *
  15. ***************************************************************************
  16. """
  17. __author__ = 'Victor Olaya'
  18. __date__ = 'August 2012'
  19. __copyright__ = '(C) 2012, Victor Olaya'
  20. import os
  21. import subprocess
  22. import platform
  23. import re
  24. import warnings
  25. import psycopg2
  26. with warnings.catch_warnings():
  27. warnings.filterwarnings("ignore", category=DeprecationWarning)
  28. from osgeo import ogr
  29. from qgis.core import (Qgis,
  30. QgsBlockingProcess,
  31. QgsRunProcess,
  32. QgsApplication,
  33. QgsVectorFileWriter,
  34. QgsProcessingFeedback,
  35. QgsProcessingUtils,
  36. QgsMessageLog,
  37. QgsSettings,
  38. QgsCredentials,
  39. QgsDataSourceUri,
  40. QgsProjUtils,
  41. QgsCoordinateReferenceSystem,
  42. QgsProcessingException)
  43. from qgis.PyQt.QtCore import (
  44. QCoreApplication,
  45. QProcess
  46. )
  47. from processing.core.ProcessingConfig import ProcessingConfig
  48. from processing.tools.system import isWindows, isMac
  49. try:
  50. with warnings.catch_warnings():
  51. warnings.filterwarnings("ignore", category=DeprecationWarning)
  52. from osgeo import gdal # NOQA
  53. gdalAvailable = True
  54. except:
  55. gdalAvailable = False
  56. class GdalUtils:
  57. GDAL_HELP_PATH = 'GDAL_HELP_PATH'
  58. supportedRasters = None
  59. supportedOutputRasters = None
  60. @staticmethod
  61. def runGdal(commands, feedback=None):
  62. if feedback is None:
  63. feedback = QgsProcessingFeedback()
  64. envval = os.getenv('PATH')
  65. # We need to give some extra hints to get things picked up on OS X
  66. isDarwin = False
  67. try:
  68. isDarwin = platform.system() == 'Darwin'
  69. except OSError: # https://travis-ci.org/m-kuhn/QGIS#L1493-L1526
  70. pass
  71. if isDarwin and os.path.isfile(os.path.join(QgsApplication.prefixPath(), "bin", "gdalinfo")):
  72. # Looks like there's a bundled gdal. Let's use it.
  73. os.environ['PATH'] = "{}{}{}".format(os.path.join(QgsApplication.prefixPath(), "bin"), os.pathsep, envval)
  74. os.environ['DYLD_LIBRARY_PATH'] = os.path.join(QgsApplication.prefixPath(), "lib")
  75. else:
  76. # Other platforms should use default gdal finder codepath
  77. settings = QgsSettings()
  78. path = settings.value('/GdalTools/gdalPath', '')
  79. if not path.lower() in envval.lower().split(os.pathsep):
  80. envval += f'{os.pathsep}{path}'
  81. os.putenv('PATH', envval)
  82. fused_command = ' '.join([str(c) for c in commands])
  83. QgsMessageLog.logMessage(fused_command, 'Processing', Qgis.Info)
  84. feedback.pushInfo(GdalUtils.tr('GDAL command:'))
  85. feedback.pushCommandInfo(fused_command)
  86. feedback.pushInfo(GdalUtils.tr('GDAL command output:'))
  87. loglines = [GdalUtils.tr('GDAL execution console output')]
  88. # create string list of number from 0 to 99
  89. progress_string_list = [str(a) for a in range(0, 100)]
  90. def on_stdout(ba):
  91. val = ba.data().decode('UTF-8')
  92. # catch progress reports
  93. if val == '100 - done.':
  94. on_stdout.progress = 100
  95. feedback.setProgress(on_stdout.progress)
  96. else:
  97. # remove any number of trailing "." or ".." strings
  98. match = re.match(r'.*?(\d+)\.+\s*$', val)
  99. found_number = False
  100. if match:
  101. int_val = match.group(1)
  102. if int_val in progress_string_list:
  103. on_stdout.progress = int(int_val)
  104. feedback.setProgress(on_stdout.progress)
  105. found_number = True
  106. if not found_number and val == '.':
  107. on_stdout.progress += 2.5
  108. feedback.setProgress(on_stdout.progress)
  109. on_stdout.buffer += val
  110. if on_stdout.buffer.endswith('\n') or on_stdout.buffer.endswith('\r'):
  111. # flush buffer
  112. feedback.pushConsoleInfo(on_stdout.buffer.rstrip())
  113. loglines.append(on_stdout.buffer.rstrip())
  114. on_stdout.buffer = ''
  115. on_stdout.progress = 0
  116. on_stdout.buffer = ''
  117. def on_stderr(ba):
  118. val = ba.data().decode('UTF-8')
  119. on_stderr.buffer += val
  120. if on_stderr.buffer.endswith('\n') or on_stderr.buffer.endswith('\r'):
  121. # flush buffer
  122. feedback.reportError(on_stderr.buffer.rstrip())
  123. loglines.append(on_stderr.buffer.rstrip())
  124. on_stderr.buffer = ''
  125. on_stderr.buffer = ''
  126. print(fused_command)
  127. command, *arguments = QgsRunProcess.splitCommand(fused_command)
  128. proc = QgsBlockingProcess(command, arguments)
  129. proc.setStdOutHandler(on_stdout)
  130. proc.setStdErrHandler(on_stderr)
  131. res = proc.run(feedback)
  132. if feedback.isCanceled() and res != 0:
  133. feedback.pushInfo(GdalUtils.tr('Process was canceled and did not complete'))
  134. elif not feedback.isCanceled() and proc.exitStatus() == QProcess.CrashExit:
  135. raise QgsProcessingException(GdalUtils.tr('Process was unexpectedly terminated'))
  136. elif res == 0:
  137. feedback.pushInfo(GdalUtils.tr('Process completed successfully'))
  138. elif proc.processError() == QProcess.FailedToStart:
  139. raise QgsProcessingException(GdalUtils.tr('Process {} failed to start. Either {} is missing, or you may have insufficient permissions to run the program.').format(command, command))
  140. else:
  141. feedback.reportError(GdalUtils.tr('Process returned error code {}').format(res))
  142. return loglines
  143. @staticmethod
  144. def getSupportedRasters():
  145. if not gdalAvailable:
  146. return {}
  147. if GdalUtils.supportedRasters is not None:
  148. return GdalUtils.supportedRasters
  149. if gdal.GetDriverCount() == 0:
  150. gdal.AllRegister()
  151. GdalUtils.supportedRasters = {}
  152. GdalUtils.supportedOutputRasters = {}
  153. GdalUtils.supportedRasters['GTiff'] = ['tif', 'tiff']
  154. GdalUtils.supportedOutputRasters['GTiff'] = ['tif', 'tiff']
  155. for i in range(gdal.GetDriverCount()):
  156. driver = gdal.GetDriver(i)
  157. if driver is None:
  158. continue
  159. shortName = driver.ShortName
  160. metadata = driver.GetMetadata()
  161. if gdal.DCAP_RASTER not in metadata \
  162. or metadata[gdal.DCAP_RASTER] != 'YES':
  163. continue
  164. if gdal.DMD_EXTENSIONS in metadata:
  165. extensions = metadata[gdal.DMD_EXTENSIONS].split(' ')
  166. if extensions:
  167. GdalUtils.supportedRasters[shortName] = extensions
  168. # Only creatable rasters can be referenced in output rasters
  169. if ((gdal.DCAP_CREATE in metadata and
  170. metadata[gdal.DCAP_CREATE] == 'YES') or
  171. (gdal.DCAP_CREATECOPY in metadata and
  172. metadata[gdal.DCAP_CREATECOPY] == 'YES')):
  173. GdalUtils.supportedOutputRasters[shortName] = extensions
  174. return GdalUtils.supportedRasters
  175. @staticmethod
  176. def getSupportedOutputRasters():
  177. if not gdalAvailable:
  178. return {}
  179. if GdalUtils.supportedOutputRasters is not None:
  180. return GdalUtils.supportedOutputRasters
  181. else:
  182. GdalUtils.getSupportedRasters()
  183. return GdalUtils.supportedOutputRasters
  184. @staticmethod
  185. def getSupportedRasterExtensions():
  186. allexts = []
  187. for exts in list(GdalUtils.getSupportedRasters().values()):
  188. for ext in exts:
  189. if ext not in allexts and ext not in ['', 'tif', 'tiff']:
  190. allexts.append(ext)
  191. allexts.sort()
  192. allexts[0:0] = ['tif', 'tiff']
  193. return allexts
  194. @staticmethod
  195. def getSupportedOutputRasterExtensions():
  196. allexts = []
  197. for exts in list(GdalUtils.getSupportedOutputRasters().values()):
  198. for ext in exts:
  199. if ext not in allexts and ext not in ['', 'tif', 'tiff']:
  200. allexts.append(ext)
  201. allexts.sort()
  202. allexts[0:0] = ['tif', 'tiff']
  203. return allexts
  204. @staticmethod
  205. def getVectorDriverFromFileName(filename):
  206. ext = os.path.splitext(filename)[1]
  207. if ext == '':
  208. return 'ESRI Shapefile'
  209. formats = QgsVectorFileWriter.supportedFiltersAndFormats()
  210. for format in formats:
  211. if ext in format.filterString:
  212. return format.driverName
  213. return 'ESRI Shapefile'
  214. @staticmethod
  215. def getFormatShortNameFromFilename(filename):
  216. ext = filename[filename.rfind('.') + 1:]
  217. supported = GdalUtils.getSupportedRasters()
  218. for name in list(supported.keys()):
  219. exts = supported[name]
  220. if ext in exts:
  221. return name
  222. return 'GTiff'
  223. @staticmethod
  224. def escapeAndJoin(strList):
  225. escChars = [' ', '&', '(', ')', '"', ';']
  226. joined = ''
  227. for s in strList:
  228. if not isinstance(s, str):
  229. s = str(s)
  230. # don't escape if command starts with - and isn't a negative number, e.g. -9999
  231. if s and re.match(r'^([^-]|-\d)', s) and any(c in s for c in escChars):
  232. escaped = '"' + s.replace('\\', '\\\\').replace('"', '"""') \
  233. + '"'
  234. else:
  235. escaped = s
  236. if escaped is not None:
  237. joined += escaped + ' '
  238. return joined.strip()
  239. @staticmethod
  240. def version():
  241. return int(gdal.VersionInfo('VERSION_NUM'))
  242. @staticmethod
  243. def readableVersion():
  244. return gdal.VersionInfo('RELEASE_NAME')
  245. @staticmethod
  246. def ogrConnectionStringFromLayer(layer):
  247. """Generates OGR connection string from a layer
  248. """
  249. return GdalUtils.ogrConnectionStringAndFormatFromLayer(layer)[0]
  250. @staticmethod
  251. def ogrConnectionStringAndFormat(uri, context):
  252. """Generates OGR connection string and format string from layer source
  253. Returned values are a tuple of the connection string and format string
  254. """
  255. ogrstr = None
  256. format = None
  257. layer = QgsProcessingUtils.mapLayerFromString(uri, context, False)
  258. if layer is None:
  259. path, ext = os.path.splitext(uri)
  260. format = QgsVectorFileWriter.driverForExtension(ext)
  261. return uri, '"' + format + '"'
  262. return GdalUtils.ogrConnectionStringAndFormatFromLayer(layer)
  263. @staticmethod
  264. def ogrConnectionStringAndFormatFromLayer(layer):
  265. provider = layer.dataProvider().name()
  266. if provider == 'spatialite':
  267. # dbname='/geodata/osm_ch.sqlite' table="places" (Geometry) sql=
  268. regex = re.compile("dbname='(.+)'")
  269. r = regex.search(str(layer.source()))
  270. ogrstr = r.groups()[0]
  271. format = 'SQLite'
  272. elif provider == 'postgres':
  273. # dbname='ktryjh_iuuqef' host=spacialdb.com port=9999
  274. # user='ktryjh_iuuqef' password='xyqwer' sslmode=disable
  275. # key='gid' estimatedmetadata=true srid=4326 type=MULTIPOLYGON
  276. # table="t4" (geom) sql=
  277. dsUri = QgsDataSourceUri(layer.dataProvider().dataSourceUri())
  278. conninfo = dsUri.connectionInfo()
  279. conn = None
  280. ok = False
  281. while not conn:
  282. try:
  283. conn = psycopg2.connect(dsUri.connectionInfo())
  284. except psycopg2.OperationalError:
  285. (ok, user, passwd) = QgsCredentials.instance().get(conninfo, dsUri.username(), dsUri.password())
  286. if not ok:
  287. break
  288. dsUri.setUsername(user)
  289. dsUri.setPassword(passwd)
  290. if not conn:
  291. raise RuntimeError('Could not connect to PostgreSQL database - check connection info')
  292. if ok:
  293. QgsCredentials.instance().put(conninfo, user, passwd)
  294. ogrstr = "PG:%s" % dsUri.connectionInfo()
  295. format = 'PostgreSQL'
  296. elif provider == 'mssql':
  297. # 'dbname=\'db_name\' host=myHost estimatedmetadata=true
  298. # srid=27700 type=MultiPolygon table="dbo"."my_table"
  299. # #(Shape) sql='
  300. dsUri = layer.dataProvider().uri()
  301. ogrstr = 'MSSQL:'
  302. ogrstr += f'database={dsUri.database()};'
  303. ogrstr += f'server={dsUri.host()};'
  304. if dsUri.username() != "":
  305. ogrstr += f'uid={dsUri.username()};'
  306. else:
  307. ogrstr += 'trusted_connection=yes;'
  308. if dsUri.password() != '':
  309. ogrstr += f'pwd={dsUri.password()};'
  310. ogrstr += f'tables={dsUri.table()}'
  311. format = 'MSSQL'
  312. elif provider == "oracle":
  313. # OCI:user/password@host:port/service:table
  314. dsUri = QgsDataSourceUri(layer.dataProvider().dataSourceUri())
  315. ogrstr = "OCI:"
  316. if dsUri.username() != "":
  317. ogrstr += dsUri.username()
  318. if dsUri.password() != "":
  319. ogrstr += "/" + dsUri.password()
  320. delim = "@"
  321. if dsUri.host() != "":
  322. ogrstr += delim + dsUri.host()
  323. delim = ""
  324. if dsUri.port() not in ["", '1521']:
  325. ogrstr += ":" + dsUri.port()
  326. ogrstr += "/"
  327. if dsUri.database() != "":
  328. ogrstr += dsUri.database()
  329. elif dsUri.database() != "":
  330. ogrstr += delim + dsUri.database()
  331. if ogrstr == "OCI:":
  332. raise RuntimeError('Invalid oracle data source - check connection info')
  333. ogrstr += ":"
  334. if dsUri.schema() != "":
  335. ogrstr += dsUri.schema() + "."
  336. ogrstr += dsUri.table()
  337. format = 'OCI'
  338. elif provider.lower() == "wfs":
  339. uri = QgsDataSourceUri(layer.source())
  340. baseUrl = uri.param('url').split('?')[0]
  341. ogrstr = f"WFS:{baseUrl}"
  342. format = 'WFS'
  343. else:
  344. ogrstr = str(layer.source()).split("|")[0]
  345. path, ext = os.path.splitext(ogrstr)
  346. format = QgsVectorFileWriter.driverForExtension(ext)
  347. return ogrstr, '"' + format + '"'
  348. @staticmethod
  349. def ogrOutputLayerName(uri):
  350. uri = uri.strip('"')
  351. return os.path.basename(os.path.splitext(uri)[0])
  352. @staticmethod
  353. def ogrLayerName(uri):
  354. uri = uri.strip('"')
  355. if ' table=' in uri:
  356. # table="schema"."table"
  357. re_table_schema = re.compile(' table="([^"]*)"\\."([^"]*)"')
  358. r = re_table_schema.search(uri)
  359. if r:
  360. return r.groups()[0] + '.' + r.groups()[1]
  361. # table="table"
  362. re_table = re.compile(' table="([^"]*)"')
  363. r = re_table.search(uri)
  364. if r:
  365. return r.groups()[0]
  366. elif 'layername' in uri:
  367. regex = re.compile('(layername=)([^|]*)')
  368. r = regex.search(uri)
  369. return r.groups()[1]
  370. fields = uri.split('|')
  371. basePath = fields[0]
  372. fields = fields[1:]
  373. layerid = 0
  374. for f in fields:
  375. if f.startswith('layername='):
  376. return f.split('=')[1]
  377. if f.startswith('layerid='):
  378. layerid = int(f.split('=')[1])
  379. ds = ogr.Open(basePath)
  380. if not ds:
  381. return None
  382. ly = ds.GetLayer(layerid)
  383. if not ly:
  384. return None
  385. name = ly.GetName()
  386. ds = None
  387. return name
  388. @staticmethod
  389. def parseCreationOptions(value):
  390. parts = value.split('|')
  391. options = []
  392. for p in parts:
  393. options.extend(['-co', p])
  394. return options
  395. @staticmethod
  396. def writeLayerParameterToTextFile(filename, alg, parameters, parameter_name, context, quote=True, executing=False):
  397. listFile = QgsProcessingUtils.generateTempFilename(filename, context)
  398. if executing:
  399. layers = []
  400. for l in alg.parameterAsLayerList(parameters, parameter_name, context):
  401. if quote:
  402. layers.append('"' + l.source() + '"')
  403. else:
  404. layers.append(l.source())
  405. with open(listFile, 'w') as f:
  406. f.write('\n'.join(layers))
  407. return listFile
  408. @staticmethod
  409. def gdal_crs_string(crs):
  410. """
  411. Converts a QgsCoordinateReferenceSystem to a string understandable
  412. by GDAL
  413. :param crs: crs to convert
  414. :return: gdal friendly string
  415. """
  416. if crs.authid().upper().startswith('EPSG:') or crs.authid().upper().startswith('IGNF:') or crs.authid().upper().startswith('ESRI:'):
  417. return crs.authid()
  418. return crs.toWkt(QgsCoordinateReferenceSystem.WKT_PREFERRED_GDAL)
  419. @classmethod
  420. def tr(cls, string, context=''):
  421. if context == '':
  422. context = cls.__name__
  423. return QCoreApplication.translate(context, string)