wms_base.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658
  1. """!
  2. @brief Preparation of parameters for drivers, which download it, and managing downloaded data.
  3. List of classes:
  4. - wms_base::WMSBase
  5. - wms_base::GRASSImporter
  6. - wms_base::WMSDriversInfo
  7. (C) 2012-2013 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 Stepan Turek <stepan.turek seznam.cz> (Mentor: Martin Landa)
  11. """
  12. import os
  13. from math import ceil
  14. import base64
  15. try:
  16. from urllib2 import Request, urlopen, HTTPError
  17. from httplib import HTTPException
  18. except ImportError:
  19. from urllib.request import Request, urlopen
  20. from urllib.error import HTTPError
  21. from http.client import HTTPException
  22. import grass.script as grass
  23. from grass.exceptions import CalledModuleError
  24. class WMSBase:
  25. def __init__(self):
  26. # these variables are information for destructor
  27. self.temp_files_to_cleanup = []
  28. self.params = {}
  29. self.tile_size = {'bbox' : None}
  30. self.temp_map = None
  31. self.temp_warpmap = None
  32. def __del__(self):
  33. # tries to remove temporary files, all files should be
  34. # removed before, implemented just in case of unexpected
  35. # stop of module
  36. for temp_file in self.temp_files_to_cleanup:
  37. grass.try_remove(temp_file)
  38. def _debug(self, fn, msg):
  39. grass.debug("%s.%s: %s" %
  40. (self.__class__.__name__, fn, msg))
  41. def _initializeParameters(self, options, flags):
  42. self._debug("_initialize_parameters", "started")
  43. # initialization of module parameters (options, flags)
  44. self.params['driver'] = options['driver']
  45. drv_info = WMSDriversInfo()
  46. driver_props = drv_info.GetDrvProperties(options['driver'])
  47. self._checkIgnoeredParams(options, flags, driver_props)
  48. self.params['capfile'] = options['capfile'].strip()
  49. for key in ['url', 'layers', 'styles', 'method']:
  50. self.params[key] = options[key].strip()
  51. self.flags = flags
  52. if self.flags['o']:
  53. self.params['transparent'] = 'FALSE'
  54. else:
  55. self.params['transparent'] = 'TRUE'
  56. for key in ['password', 'username', 'urlparams']:
  57. self.params[key] = options[key]
  58. if (self.params ['password'] and self.params ['username'] == '') or \
  59. (self.params ['password'] == '' and self.params ['username']):
  60. grass.fatal(_("Please insert both %s and %s parameters or none of them." % ('password', 'username')))
  61. self.params['bgcolor'] = options['bgcolor'].strip()
  62. if options['format'] == "jpeg" and \
  63. not 'format' in driver_props['ignored_params']:
  64. if not flags['o'] and \
  65. 'WMS' in self.params['driver']:
  66. grass.warning(_("JPEG format does not support transparency"))
  67. self.params['format'] = drv_info.GetFormat(options['format'])
  68. if not self.params['format']:
  69. self.params['format'] = self.params['format']
  70. #TODO: get srs from Tile Service file in OnEarth_GRASS driver
  71. self.params['srs'] = int(options['srs'])
  72. if self.params['srs'] <= 0 and not 'srs' in driver_props['ignored_params']:
  73. grass.fatal(_("Invalid EPSG code %d") % self.params['srs'])
  74. self.params['wms_version'] = options['wms_version']
  75. if "CRS" in GetSRSParamVal(self.params['srs']) and self.params['wms_version'] == "1.1.1":
  76. self.params['wms_version'] = "1.3.0"
  77. grass.warning(_("WMS version <1.3.0> will be used, because version <1.1.1> does not support <%s>projection")
  78. % GetSRSParamVal(self.params['srs']))
  79. if self.params['wms_version'] == "1.3.0":
  80. self.params['proj_name'] = "CRS"
  81. else:
  82. self.params['proj_name'] = "SRS"
  83. # read projection info
  84. self.proj_location = grass.read_command('g.proj',
  85. flags ='jf').rstrip('\n')
  86. self.proj_location = self._modifyProj(self.proj_location)
  87. if self.params['srs'] in [3857, 900913]:
  88. # HACK: epsg 3857 def: http://spatialreference.org/ref/sr-org/7483/
  89. # g.proj can return: ...+a=6378137 +rf=298.257223563... (WGS84 elipsoid def instead of sphere), it can make 20km shift in Y, when raster is transformed
  90. # needed to be tested on more servers
  91. self.proj_srs = '+proj=merc +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +no_defs +a=6378137 +b=6378137 +nadgrids=@null +to_meter=1'
  92. else:
  93. self.proj_srs = grass.read_command('g.proj',
  94. flags = 'jf',
  95. epsg = str(GetEpsg(self.params['srs']))).rstrip('\n')
  96. self.proj_srs = self._modifyProj(self.proj_srs)
  97. if not self.proj_srs or not self.proj_location:
  98. grass.fatal(_("Unable to get projection info"))
  99. self.region = options['region']
  100. min_tile_size = 100
  101. maxcols = int(options['maxcols'])
  102. if maxcols <= min_tile_size:
  103. grass.fatal(_("Maxcols must be greater than 100"))
  104. maxrows = int(options['maxrows'])
  105. if maxrows <= min_tile_size:
  106. grass.fatal(_("Maxrows must be greater than 100"))
  107. # setting optimal tile size according to maxcols and maxrows constraint and region cols and rows
  108. self.tile_size['cols'] = int(self.region['cols'] / ceil(self.region['cols'] / float(maxcols)))
  109. self.tile_size['rows'] = int(self.region['rows'] / ceil(self.region['rows'] / float(maxrows)))
  110. # default format for GDAL library
  111. self.gdal_drv_format = "GTiff"
  112. self._debug("_initialize_parameters", "finished")
  113. def _modifyProj(self, proj):
  114. """!Modify proj.4 string for usage in this module"""
  115. # add +wktext parameter to avoid droping of +nadgrids parameter (if presented) in gdalwarp
  116. if '+nadgrids=' in proj and ' +wktext' not in proj:
  117. proj += ' +wktext'
  118. return proj
  119. def _checkIgnoeredParams(self, options, flags, driver_props):
  120. """!Write warnings for set parameters and flags, which chosen driver does not use."""
  121. not_relevant_params = []
  122. for i_param in driver_props['ignored_params']:
  123. if options.has_key(i_param) and \
  124. options[i_param] and \
  125. i_param not in ['srs', 'wms_version', 'format']: # params with default value
  126. not_relevant_params.append('<' + i_param + '>')
  127. if len(not_relevant_params) > 0:
  128. grass.warning(_("These parameter are ignored: %s\n\
  129. %s driver does not support the parameters." %\
  130. (','.join(not_relevant_params), options['driver'])))
  131. not_relevant_flags = []
  132. for i_flag in driver_props['ignored_flags']:
  133. if flags[i_flag]:
  134. not_relevant_flags.append('<' + i_flag + '>')
  135. if len(not_relevant_flags) > 0:
  136. grass.warning(_("These flags are ignored: %s\n\
  137. %s driver does not support the flags." %\
  138. (','.join(not_relevant_flags), options['driver'])))
  139. def GetMap(self, options, flags):
  140. """!Download data from WMS server."""
  141. self._initializeParameters(options, flags)
  142. self.bbox = self._computeBbox()
  143. self.temp_map = self._download()
  144. if not self.temp_map:
  145. return
  146. self._reprojectMap()
  147. return self.temp_warpmap
  148. def _fetchCapabilities(self, options):
  149. """!Download capabilities from WMS server
  150. """
  151. cap_url = options['url'].strip()
  152. if "?" in cap_url:
  153. cap_url += "&"
  154. else:
  155. cap_url += "?"
  156. if 'WMTS' in options['driver']:
  157. cap_url += "SERVICE=WMTS&REQUEST=GetCapabilities&VERSION=1.0.0"
  158. elif 'OnEarth' in options['driver']:
  159. cap_url += "REQUEST=GetTileService"
  160. else:
  161. cap_url += "SERVICE=WMS&REQUEST=GetCapabilities&VERSION=" + options['wms_version']
  162. if options['urlparams']:
  163. cap_url += "&" + options['urlparams']
  164. grass.debug('Fetching capabilities file.\n%s' % cap_url)
  165. try:
  166. cap = self._fetchDataFromServer(cap_url, options['username'], options['password'])
  167. except (IOError, HTTPException) as e:
  168. if isinstance(e, HTTPError) and e.code == 401:
  169. grass.fatal(
  170. _("Authorization failed to <%s> when fetching capabilities") %
  171. options['url'])
  172. else:
  173. msg = _("Unable to fetch capabilities from <%s>: %s") % (options['url'], e)
  174. if hasattr(e, 'reason'):
  175. msg += _("\nReason: ") + e.reason
  176. grass.fatal(msg)
  177. grass.debug('Fetching capabilities OK')
  178. return cap
  179. def _fetchDataFromServer(self, url, username = None, password = None):
  180. """!Fetch data from server
  181. """
  182. request = Request(url)
  183. if username and password:
  184. base64string = base64.encodestring('%s:%s' % (username, password)).replace('\n', '')
  185. request.add_header("Authorization", "Basic %s" % base64string)
  186. try:
  187. return urlopen(request)
  188. except ValueError as error:
  189. grass.fatal("%s" % error)
  190. def GetCapabilities(self, options):
  191. """!Get capabilities from WMS server
  192. """
  193. cap = self._fetchCapabilities(options)
  194. capfile_output = options['capfile_output'].strip()
  195. # save to file
  196. if capfile_output:
  197. try:
  198. temp = open(capfile_output, "w")
  199. temp.write(cap.read())
  200. temp.close()
  201. return
  202. except IOError as error:
  203. grass.fatal(_("Unabble to open file '%s'.\n%s\n" % (cap_file, error)))
  204. # print to output
  205. cap_lines = cap.readlines()
  206. for line in cap_lines:
  207. print(line.rstrip())
  208. def _computeBbox(self):
  209. """!Get region extent for WMS query (bbox)
  210. """
  211. self._debug("_computeBbox", "started")
  212. bbox_region_items = {'maxy' : 'n', 'miny' : 's', 'maxx' : 'e', 'minx' : 'w'}
  213. bbox = {}
  214. if self.proj_srs == self.proj_location: # TODO: do it better
  215. for bbox_item, region_item in bbox_region_items.items():
  216. bbox[bbox_item] = self.region[region_item]
  217. # if location projection and wms query projection are
  218. # different, corner points of region are transformed into wms
  219. # projection and then bbox is created from extreme coordinates
  220. # of the transformed points
  221. else:
  222. for bbox_item, region_item in bbox_region_items.items():
  223. bbox[bbox_item] = None
  224. temp_region = self._tempfile()
  225. try:
  226. temp_region_opened = open(temp_region, 'w')
  227. temp_region_opened.write("%f %f\n%f %f\n%f %f\n%f %f\n" %\
  228. (self.region['e'], self.region['n'],\
  229. self.region['w'], self.region['n'],\
  230. self.region['w'], self.region['s'],\
  231. self.region['e'], self.region['s'] ))
  232. except IOError:
  233. grass.fatal(_("Unable to write data into tempfile"))
  234. finally:
  235. temp_region_opened.close()
  236. points = grass.read_command('m.proj', flags = 'd',
  237. proj_out = self.proj_srs,
  238. proj_in = self.proj_location,
  239. input = temp_region,
  240. quiet = True) # TODO: stdin
  241. grass.try_remove(temp_region)
  242. if not points:
  243. grass.fatal(_("Unable to determine region, %s failed") % 'm.proj')
  244. points = points.splitlines()
  245. if len(points) != 4:
  246. grass.fatal(_("Region definition: 4 points required"))
  247. for point in points:
  248. try:
  249. point = list(map(float, point.split("|")))
  250. except ValueError:
  251. grass.fatal(_('Reprojection of region using m.proj failed.'))
  252. if not bbox['maxy']:
  253. bbox['maxy'] = point[1]
  254. bbox['miny'] = point[1]
  255. bbox['maxx'] = point[0]
  256. bbox['minx'] = point[0]
  257. continue
  258. if bbox['maxy'] < point[1]:
  259. bbox['maxy'] = point[1]
  260. elif bbox['miny'] > point[1]:
  261. bbox['miny'] = point[1]
  262. if bbox['maxx'] < point[0]:
  263. bbox['maxx'] = point[0]
  264. elif bbox['minx'] > point[0]:
  265. bbox['minx'] = point[0]
  266. self._debug("_computeBbox", "finished -> %s" % bbox)
  267. # Ordering of coordinates axis of geographic coordinate
  268. # systems in WMS 1.3.0 is flipped. If self.tile_size['flip_coords'] is
  269. # True, coords in bbox need to be flipped in WMS query.
  270. return bbox
  271. def _reprojectMap(self):
  272. """!Reproject data using gdalwarp if needed
  273. """
  274. # reprojection of raster
  275. if self.proj_srs != self.proj_location: # TODO: do it better
  276. grass.message(_("Reprojecting raster..."))
  277. self.temp_warpmap = grass.tempfile()
  278. if int(os.getenv('GRASS_VERBOSE', '2')) <= 2:
  279. nuldev = file(os.devnull, 'w+')
  280. else:
  281. nuldev = None
  282. if self.params['method'] == "nearest":
  283. gdal_method = "near"
  284. elif self.params['method'] == "linear":
  285. gdal_method = "bilinear"
  286. else:
  287. gdal_method = self.params['method']
  288. #"+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +no_defs"
  289. # RGB rasters - alpha layer is added for cropping edges of projected raster
  290. try:
  291. if self.temp_map_bands_num == 3:
  292. ps = grass.Popen(['gdalwarp',
  293. '-s_srs', '%s' % self.proj_srs,
  294. '-t_srs', '%s' % self.proj_location,
  295. '-r', gdal_method, '-dstalpha',
  296. self.temp_map, self.temp_warpmap], stdout = nuldev)
  297. # RGBA rasters
  298. else:
  299. ps = grass.Popen(['gdalwarp',
  300. '-s_srs', '%s' % self.proj_srs,
  301. '-t_srs', '%s' % self.proj_location,
  302. '-r', gdal_method,
  303. self.temp_map, self.temp_warpmap], stdout = nuldev)
  304. ps.wait()
  305. except OSError as e:
  306. grass.fatal('%s \nThis can be caused by missing %s utility. ' % (e, 'gdalwarp'))
  307. if nuldev:
  308. nuldev.close()
  309. if ps.returncode != 0:
  310. grass.fatal(_('%s failed') % 'gdalwarp')
  311. grass.try_remove(self.temp_map)
  312. # raster projection is same as projection of location
  313. else:
  314. self.temp_warpmap = self.temp_map
  315. self.temp_files_to_cleanup.remove(self.temp_map)
  316. return self.temp_warpmap
  317. def _tempfile(self):
  318. """!Create temp_file and append list self.temp_files_to_cleanup
  319. with path of file
  320. @return string path to temp_file
  321. """
  322. temp_file = grass.tempfile()
  323. if temp_file is None:
  324. grass.fatal(_("Unable to create temporary files"))
  325. # list of created tempfiles for destructor
  326. self.temp_files_to_cleanup.append(temp_file)
  327. return temp_file
  328. class GRASSImporter:
  329. def __init__(self, opt_output):
  330. self.cleanup_mask = False
  331. self.cleanup_layers = False
  332. # output map name
  333. self.opt_output = opt_output
  334. # suffix for existing mask (during overriding will be saved
  335. # into raster named:self.opt_output + this suffix)
  336. self.original_mask_suffix = "_temp_MASK"
  337. # check names of temporary rasters, which module may create
  338. maps = []
  339. for suffix in ('.red', '.green', '.blue', '.alpha', self.original_mask_suffix ):
  340. rast = self.opt_output + suffix
  341. if grass.find_file(rast, element = 'cell', mapset = '.')['file']:
  342. maps.append(rast)
  343. if len(maps) != 0:
  344. grass.fatal(_("Please change output name, or change names of these rasters: %s, "
  345. "module needs to create this temporary maps during execution.") % ",".join(maps))
  346. def __del__(self):
  347. # removes temporary mask, used for import transparent or warped temp_map
  348. if self.cleanup_mask:
  349. # clear temporary mask, which was set by module
  350. try:
  351. grass.run_command('r.mask', quiet=True, flags='r')
  352. except CalledModuleError:
  353. grass.fatal(_('%s failed') % 'r.mask')
  354. # restore original mask, if exists
  355. if grass.find_file(self.opt_output + self.original_mask_suffix, element = 'cell', mapset = '.' )['name']:
  356. try:
  357. mask_copy = self.opt_output + self.original_mask_suffix
  358. grass.run_command('g.copy', quiet=True,
  359. raster=mask_copy + ',MASK')
  360. except CalledModuleError:
  361. grass.fatal(_('%s failed') % 'g.copy')
  362. # remove temporary created rasters
  363. if self.cleanup_layers:
  364. maps = []
  365. for suffix in ('.red', '.green', '.blue', '.alpha', self.original_mask_suffix):
  366. rast = self.opt_output + suffix
  367. if grass.find_file(rast, element = 'cell', mapset = '.')['file']:
  368. maps.append(rast)
  369. if maps:
  370. grass.run_command('g.remove',
  371. quiet = True,
  372. flags = 'fb',
  373. type = 'raster',
  374. name = ','.join(maps))
  375. # delete environmental variable which overrides region
  376. if 'GRASS_REGION' in os.environ.keys():
  377. os.environ.pop('GRASS_REGION')
  378. def ImportMapIntoGRASS(self, raster):
  379. """!Import raster into GRASS.
  380. """
  381. # importing temp_map into GRASS
  382. try:
  383. grass.run_command('r.in.gdal',
  384. quiet=True, overwrite=True,
  385. input=raster, output=self.opt_output)
  386. except CalledModuleError:
  387. grass.fatal(_('%s failed') % 'r.in.gdal')
  388. # information for destructor to cleanup temp_layers, created
  389. # with r.in.gdal
  390. self.cleanup_layers = True
  391. # setting region for full extend of imported raster
  392. if grass.find_file(self.opt_output + '.red', element = 'cell', mapset = '.')['file']:
  393. region_map = self.opt_output + '.red'
  394. else:
  395. region_map = self.opt_output
  396. os.environ['GRASS_REGION'] = grass.region_env(rast = region_map)
  397. # mask created from alpha layer, which describes real extend
  398. # of warped layer (may not be a rectangle), also mask contains
  399. # transparent parts of raster
  400. if grass.find_file( self.opt_output + '.alpha', element = 'cell', mapset = '.' )['name']:
  401. # saving current mask (if exists) into temp raster
  402. if grass.find_file('MASK', element = 'cell', mapset = '.' )['name']:
  403. try:
  404. mask_copy = self.opt_output + self.original_mask_suffix
  405. grass.run_command('g.copy', quiet=True,
  406. raster='MASK,' + mask_copy)
  407. except CalledModuleError:
  408. grass.fatal(_('%s failed') % 'g.copy')
  409. # info for destructor
  410. self.cleanup_mask = True
  411. try:
  412. grass.run_command('r.mask',
  413. quiet=True, overwrite=True,
  414. maskcats="0",
  415. flags='i',
  416. raster=self.opt_output + '.alpha')
  417. except CalledModuleError:
  418. grass.fatal(_('%s failed') % 'r.mask')
  419. #TODO one band + alpha band?
  420. if grass.find_file(self.opt_output + '.red', element = 'cell', mapset = '.')['file']:
  421. try:
  422. grass.run_command('r.composite',
  423. quiet=True, overwrite=True,
  424. red=self.opt_output + '.red',
  425. green=self.opt_output + '.green',
  426. blue=self.opt_output + '.blue',
  427. output=self.opt_output)
  428. except CalledModuleError:
  429. grass.fatal(_('%s failed') % 'r.composite')
  430. grass.message(_('<%s> created.') % self.opt_output)
  431. class WMSDriversInfo:
  432. def __init__(self):
  433. """!Provides information about driver parameters.
  434. """
  435. # format labels
  436. self.f_labels = ["geotiff", "tiff", "png", "jpeg", "gif", "png8"]
  437. # form for request
  438. self.formats = ["image/geotiff", "image/tiff", "image/png", "image/jpeg", "image/gif", "image/png8"]
  439. self.srs = ("epsg", "crs")
  440. def GetDrvProperties(self, driver):
  441. """!Get information about driver parameters.
  442. """
  443. if driver == 'WMS_GDAL':
  444. return self._GDALDrvProperties()
  445. if 'WMS' in driver:
  446. return self._WMSProperties()
  447. if 'WMTS' in driver:
  448. return self._WMTSProperties()
  449. if 'OnEarth' in driver:
  450. return self._OnEarthProperties()
  451. def _OnEarthProperties(self):
  452. props = {}
  453. props['ignored_flags'] = ['o']
  454. props['ignored_params'] = ['bgcolor', 'styles', 'capfile_output',
  455. 'format', 'srs', 'wms_version']
  456. props['req_multiple_layers'] = False
  457. return props
  458. def _WMSProperties(self):
  459. props = {}
  460. props['ignored_params'] = ['capfile']
  461. props['ignored_flags'] = []
  462. props['req_multiple_layers'] = True
  463. return props
  464. def _WMTSProperties(self):
  465. props = {}
  466. props['ignored_flags'] = ['o']
  467. props['ignored_params'] = ['urlparams', 'bgcolor', 'wms_version']
  468. props['req_multiple_layers'] = False
  469. return props
  470. def _GDALDrvProperties(self):
  471. props = {}
  472. props['ignored_flags'] = []
  473. props['ignored_params'] = ['urlparams', 'bgcolor', 'capfile', 'capfile_output',
  474. 'username', 'password']
  475. props['req_multiple_layers'] = True
  476. return props
  477. def GetFormatLabel(self, format):
  478. """!Convert format request form to value in parameter 'format'.
  479. """
  480. if format in self.formats:
  481. return self.f_labels[self.formats.index(format)]
  482. return None
  483. def GetFormat(self, label):
  484. """!Convert value in parameter 'format' to format request form.
  485. """
  486. if label in self.f_labels:
  487. return self.formats[self.f_labels.index(label)]
  488. return None
  489. def GetSrs(self):
  490. """!Get supported srs prefixes (e.g. epsg/crs)
  491. @todo filter according to version and driver params
  492. """
  493. return self.srs
  494. #TODO move to utils?
  495. def GetSRSParamVal(srs):
  496. """!Decides whether to use CRS or EPSG prefix according to srs number.
  497. """
  498. if srs in [84, 83, 27]:
  499. return "CRS:%d" % srs
  500. else:
  501. return "EPSG:%d" % srs
  502. def GetEpsg(srs):
  503. """
  504. @return EPSG number
  505. If srs is CRS number, return EPSG number which corresponds to CRS number.
  506. """
  507. if srs == 84:
  508. return 4326
  509. if srs == 83:
  510. return 4269
  511. if srs == 27:
  512. return 4267
  513. return srs