wms_base.py 24 KB

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