wms_base.py 22 KB

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