wms_drv.py 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877
  1. """!
  2. @brief WMS, WMTS and NASA OnEarth drivers implemented in GRASS using GDAL Python bindings.
  3. List of classes:
  4. - wms_drv::WMSDrv
  5. - wms_drv::BaseRequestMgr
  6. - wms_drv::WMSRequestMgr
  7. - wms_drv::WMTSRequestMgr
  8. - wms_drv::OnEarthRequestMgr
  9. (C) 2012 by the GRASS Development Team
  10. This program is free software under the GNU General Public License
  11. (>=v2). Read the file COPYING that comes with GRASS for details.
  12. @author Stepan Turek <stepan.turek seznam.cz> (Mentor: Martin Landa)
  13. """
  14. import socket
  15. import grass.script as grass
  16. from time import sleep
  17. try:
  18. from osgeo import gdal
  19. from osgeo import gdalconst
  20. except:
  21. grass.fatal(_("Unable to load GDAL python bindings"))
  22. import numpy as Numeric
  23. Numeric.arrayrange = Numeric.arange
  24. from math import pi, floor
  25. from urllib2 import HTTPError
  26. from httplib import HTTPException
  27. try:
  28. from xml.etree.ElementTree import ParseError
  29. except ImportError: # < Python 2.7
  30. from xml.parsers.expat import ExpatError as ParseError
  31. from wms_base import WMSBase
  32. from wms_cap_parsers import WMTSCapabilitiesTree, OnEarthCapabilitiesTree
  33. class WMSDrv(WMSBase):
  34. def _download(self):
  35. """!Downloads data from WMS server using own driver
  36. @return temp_map with downloaded data
  37. """
  38. grass.message(_("Downloading data from WMS server..."))
  39. if "?" in self.params["url"]:
  40. self.params["url"] += "&"
  41. else:
  42. self.params["url"] += "?"
  43. if not self.params['capfile']:
  44. self.cap_file = self._fetchCapabilities(self.params)
  45. else:
  46. self.cap_file = self.params['capfile']
  47. # initialize correct manager according to chosen OGC service
  48. if self.params['driver'] == 'WMTS_GRASS':
  49. req_mgr = WMTSRequestMgr(self.params, self.bbox, self.region, self.proj_srs, self.cap_file)
  50. elif self.params['driver'] == 'WMS_GRASS':
  51. req_mgr = WMSRequestMgr(self.params, self.bbox, self.region, self.tile_size, self.proj_srs)
  52. elif self.params['driver'] == 'OnEarth_GRASS':
  53. req_mgr = OnEarthRequestMgr(self.params, self.bbox, self.region, self.proj_srs, self.cap_file)
  54. # get information about size in pixels and bounding box of raster, where all tiles will be joined
  55. map_region = req_mgr.GetMapRegion()
  56. init = True
  57. temp_map = None
  58. fetch_try = 0
  59. # iterate through all tiles and download them
  60. while True:
  61. if fetch_try == 0:
  62. # get url for request the tile and information for placing the tile into raster with other tiles
  63. tile = req_mgr.GetNextTile()
  64. # if last tile has been already downloaded
  65. if not tile:
  66. break
  67. # url for request the tile
  68. query_url = tile[0]
  69. # the tile size and offset in pixels for placing it into raster where tiles are joined
  70. tile_ref = tile[1]
  71. grass.debug(query_url, 2)
  72. try:
  73. wms_data = self._fetchDataFromServer(query_url, self.params['username'], self.params['password'])
  74. except (IOError, HTTPException), e:
  75. if HTTPError == type(e) and e.code == 401:
  76. grass.fatal(_("Authorization failed to '%s' when fetching data.") % self.params['url'])
  77. else:
  78. grass.fatal(_("Unable to fetch data from: '%s'") % self.params['url'])
  79. temp_tile = self._tempfile()
  80. # download data into temporary file
  81. try:
  82. temp_tile_opened = open(temp_tile, 'w')
  83. temp_tile_opened.write(wms_data.read())
  84. except IOError, e:
  85. # some servers are not happy with many subsequent requests for tiles done immediately,
  86. # if immediate request was unsuccessful, try to repeat the request after 5s and 30s breaks
  87. # TODO probably servers can return more kinds of errors related to this problem (not only 104)
  88. if socket.error == type(e) and e[0] == 104 and fetch_try < 2:
  89. fetch_try += 1
  90. if fetch_try == 1:
  91. sleep_time = 5
  92. elif fetch_try == 2:
  93. sleep_time = 30
  94. grass.warning(_("Server refused to send data for a tile.\nRequest will be repeated after %d s.") % sleep_time)
  95. sleep(sleep_time)
  96. continue
  97. else:
  98. grass.fatal(_("Unable to write data into tempfile.\n%s") % str(e))
  99. finally:
  100. temp_tile_opened.close()
  101. fetch_try = 0
  102. tile_dataset_info = gdal.Open(temp_tile, gdal.GA_ReadOnly)
  103. if tile_dataset_info is None:
  104. # print error xml returned from server
  105. try:
  106. error_xml_opened = open(temp_tile, 'r')
  107. err_str = error_xml_opened.read()
  108. except IOError, e:
  109. grass.fatal(_("Unable to read data from tempfile.\n%s") % str(e))
  110. finally:
  111. error_xml_opened.close()
  112. if err_str is not None:
  113. grass.fatal(_("WMS server error: %s") % err_str)
  114. else:
  115. grass.fatal(_("WMS server unknown error") )
  116. temp_tile_pct2rgb = None
  117. if tile_dataset_info.RasterCount == 1 and \
  118. tile_dataset_info.GetRasterBand(1).GetRasterColorTable() is not None:
  119. # expansion of color table into bands
  120. temp_tile_pct2rgb = self._tempfile()
  121. tile_dataset = self._pct2rgb(temp_tile, temp_tile_pct2rgb)
  122. else:
  123. tile_dataset = tile_dataset_info
  124. # initialization of temp_map_dataset, where all tiles are merged
  125. if init:
  126. temp_map = self._tempfile()
  127. driver = gdal.GetDriverByName(self.gdal_drv_format)
  128. metadata = driver.GetMetadata()
  129. if not metadata.has_key(gdal.DCAP_CREATE) or \
  130. metadata[gdal.DCAP_CREATE] == 'NO':
  131. grass.fatal(_('Driver %s does not supports Create() method') % drv_format)
  132. self.temp_map_bands_num = tile_dataset.RasterCount
  133. temp_map_dataset = driver.Create(temp_map, map_region['cols'], map_region['rows'],
  134. self.temp_map_bands_num,
  135. tile_dataset.GetRasterBand(1).DataType)
  136. init = False
  137. # tile is written into temp_map
  138. tile_to_temp_map = tile_dataset.ReadRaster(0, 0, tile_ref['sizeX'], tile_ref['sizeY'],
  139. tile_ref['sizeX'], tile_ref['sizeY'])
  140. temp_map_dataset.WriteRaster(tile_ref['t_cols_offset'], tile_ref['t_rows_offset'],
  141. tile_ref['sizeX'], tile_ref['sizeY'], tile_to_temp_map)
  142. tile_dataset = None
  143. tile_dataset_info = None
  144. grass.try_remove(temp_tile)
  145. grass.try_remove(temp_tile_pct2rgb)
  146. if not temp_map:
  147. return temp_map
  148. # georeferencing and setting projection of temp_map
  149. projection = grass.read_command('g.proj',
  150. flags = 'wf',
  151. epsg =self.params['srs']).rstrip('\n')
  152. temp_map_dataset.SetProjection(projection)
  153. pixel_x_length = (map_region['maxx'] - map_region['minx']) / int(map_region['cols'])
  154. pixel_y_length = (map_region['miny'] - map_region['maxy']) / int(map_region['rows'])
  155. geo_transform = [map_region['minx'] , pixel_x_length , 0.0 , map_region['maxy'] , 0.0 , pixel_y_length]
  156. temp_map_dataset.SetGeoTransform(geo_transform )
  157. temp_map_dataset = None
  158. return temp_map
  159. def _pct2rgb(self, src_filename, dst_filename):
  160. """!Create new dataset with data in dst_filename with bands according to src_filename
  161. raster color table - modified code from gdal utility pct2rgb
  162. @return new dataset
  163. """
  164. out_bands = 4
  165. band_number = 1
  166. # open source file
  167. src_ds = gdal.Open(src_filename)
  168. if src_ds is None:
  169. grass.fatal(_('Unable to open %s ' % src_filename))
  170. src_band = src_ds.GetRasterBand(band_number)
  171. # Build color table
  172. lookup = [ Numeric.arrayrange(256),
  173. Numeric.arrayrange(256),
  174. Numeric.arrayrange(256),
  175. Numeric.ones(256)*255 ]
  176. ct = src_band.GetRasterColorTable()
  177. if ct is not None:
  178. for i in range(min(256,ct.GetCount())):
  179. entry = ct.GetColorEntry(i)
  180. for c in range(4):
  181. lookup[c][i] = entry[c]
  182. # create the working file
  183. gtiff_driver = gdal.GetDriverByName(self.gdal_drv_format)
  184. tif_ds = gtiff_driver.Create(dst_filename,
  185. src_ds.RasterXSize, src_ds.RasterYSize, out_bands)
  186. # do the processing one scanline at a time
  187. for iY in range(src_ds.RasterYSize):
  188. src_data = src_band.ReadAsArray(0,iY,src_ds.RasterXSize,1)
  189. for iBand in range(out_bands):
  190. band_lookup = lookup[iBand]
  191. dst_data = Numeric.take(band_lookup,src_data)
  192. tif_ds.GetRasterBand(iBand+1).WriteArray(dst_data,0,iY)
  193. return tif_ds
  194. class BaseRequestMgr:
  195. """!Base class for request managers.
  196. """
  197. def _computeRequestData(self, bbox, tl_corner, tile_span, tile_size, mat_num_bbox):
  198. """!Initialize data needed for iteration through tiles. Used by WMTS_GRASS and OnEarth_GRASS drivers.
  199. """
  200. epsilon = 1e-15
  201. # request data bbox specified in row and col number
  202. self.t_num_bbox = {}
  203. self.t_num_bbox['min_col'] = int(floor((bbox['minx'] - tl_corner['minx']) / tile_span['x'] + epsilon))
  204. self.t_num_bbox['max_col'] = int(floor((bbox['maxx'] - tl_corner['minx']) / tile_span['x'] - epsilon))
  205. self.t_num_bbox['min_row'] = int(floor((tl_corner['maxy'] - bbox['maxy']) / tile_span['y'] + epsilon))
  206. self.t_num_bbox['max_row'] = int(floor((tl_corner['maxy'] - bbox['miny']) / tile_span['y'] - epsilon))
  207. # Does required bbox intersects bbox of data available on server?
  208. self.intersects = False
  209. for col in ['min_col', 'max_col']:
  210. for row in ['min_row', 'max_row']:
  211. if (self.t_num_bbox['min_row'] <= self.t_num_bbox[row] and self.t_num_bbox[row] <= mat_num_bbox['max_row']) and \
  212. (self.t_num_bbox['min_col'] <= self.t_num_bbox[col] and self.t_num_bbox[col] <= mat_num_bbox['max_col']):
  213. self.intersects = True
  214. if not self.intersects:
  215. grass.warning(_('Region is out of server data extend.'))
  216. self.map_region = None
  217. return
  218. # crop request bbox to server data bbox extend
  219. if self.t_num_bbox['min_col'] < (mat_num_bbox['min_col']):
  220. self.t_num_bbox['min_col'] = int(mat_num_bbox['min_col'])
  221. if self.t_num_bbox['max_col'] > (mat_num_bbox['max_col']):
  222. self.t_num_bbox['max_col'] = int(mat_num_bbox['max_col'])
  223. if self.t_num_bbox['min_row'] < (mat_num_bbox['min_row']):
  224. self.t_num_bbox['min_row'] = int(mat_num_bbox['min_row'])
  225. if self.t_num_bbox['max_row'] > (mat_num_bbox['max_row']):
  226. self.t_num_bbox['max_row'] = int(mat_num_bbox['max_row'])
  227. num_tiles = (self.t_num_bbox['max_col'] - self.t_num_bbox['min_col'] + 1) * (self.t_num_bbox['max_row'] - self.t_num_bbox['min_row'] + 1)
  228. grass.message(_('Fetching %d tiles with %d x %d pixel size per tile...') % (num_tiles, tile_size['x'], tile_size['y']))
  229. # georeference of raster, where tiles will be merged
  230. self.map_region = {}
  231. self.map_region['minx'] = self.t_num_bbox['min_col'] * tile_span['x'] + tl_corner['minx']
  232. self.map_region['maxy'] = tl_corner['maxy'] - (self.t_num_bbox['min_row']) * tile_span['y']
  233. self.map_region['maxx'] = (self.t_num_bbox['max_col'] + 1) * tile_span['x'] + tl_corner['minx']
  234. self.map_region['miny'] = tl_corner['maxy'] - (self.t_num_bbox['max_row'] + 1) * tile_span['y']
  235. # size of raster, where tiles will be merged
  236. self.map_region['cols'] = int(tile_size['x'] * (self.t_num_bbox['max_col'] - self.t_num_bbox['min_col'] + 1))
  237. self.map_region['rows'] = int(tile_size['y'] * (self.t_num_bbox['max_row'] - self.t_num_bbox['min_row'] + 1))
  238. # hold information about current column and row during iteration
  239. self.i_col = self.t_num_bbox['min_col']
  240. self.i_row = self.t_num_bbox['min_row']
  241. # bbox for first tile request
  242. self.query_bbox = {
  243. 'minx' : tl_corner['minx'],
  244. 'maxy' : tl_corner['maxy'],
  245. 'maxx' : tl_corner['minx'] + tile_span['x'],
  246. 'miny' : tl_corner['maxy'] - tile_span['y'],
  247. }
  248. self.tile_ref = {
  249. 'sizeX' : tile_size['x'],
  250. 'sizeY' : tile_size['y']
  251. }
  252. def _isGeoProj(self, proj):
  253. """!Is it geographic projection?
  254. """
  255. if (proj.find("+proj=latlong") != -1 or \
  256. proj.find("+proj=longlat") != -1):
  257. return True
  258. return False
  259. class WMSRequestMgr(BaseRequestMgr):
  260. def __init__(self, params, bbox, region, tile_size, proj_srs, cap_file = None):
  261. """!Initialize data needed for iteration through tiles.
  262. """
  263. self.version = params['wms_version']
  264. proj = params['proj_name'] + "=EPSG:"+ str(params['srs'])
  265. self.url = params['url'] + ("SERVICE=WMS&REQUEST=GetMap&VERSION=%s&LAYERS=%s&WIDTH=%s&HEIGHT=%s&STYLES=%s&BGCOLOR=%s&TRANSPARENT=%s" % \
  266. (params['wms_version'], params['layers'], tile_size['cols'], tile_size['rows'], params['styles'], \
  267. params['bgcolor'], params['transparent']))
  268. self.url += "&" +proj+ "&" + "FORMAT=" + params['format']
  269. self.bbox = bbox
  270. self.proj_srs = proj_srs
  271. self.tile_rows = tile_size['rows']
  272. self.tile_cols = tile_size['cols']
  273. if params['urlparams'] != "":
  274. self.url += "&" + params['urlparams']
  275. cols = int(region['cols'])
  276. rows = int(region['rows'])
  277. # computes parameters of tiles
  278. self.num_tiles_x = cols / self.tile_cols
  279. self.last_tile_x_size = cols % self.tile_cols
  280. self.tile_length_x = float(self.tile_cols) / float(cols) * (self.bbox['maxx'] - self.bbox['minx'])
  281. self.last_tile_x = False
  282. if self.last_tile_x_size != 0:
  283. self.last_tile_x = True
  284. self.num_tiles_x = self.num_tiles_x + 1
  285. self.num_tiles_y = rows / self.tile_rows
  286. self.last_tile_y_size = rows % self.tile_rows
  287. self.tile_length_y = float(self.tile_rows) / float(rows) * (self.bbox['maxy'] - self.bbox['miny'])
  288. self.last_tile_y = False
  289. if self.last_tile_y_size != 0:
  290. self.last_tile_y = True
  291. self.num_tiles_y = self.num_tiles_y + 1
  292. self.tile_bbox = dict(self.bbox)
  293. self.tile_bbox['maxx'] = self.bbox['minx'] + self.tile_length_x
  294. self.i_x = 0
  295. self.i_y = 0
  296. self.map_region = self.bbox
  297. self.map_region['cols'] = cols
  298. self.map_region['rows'] = rows
  299. def GetMapRegion(self):
  300. """!Get size in pixels and bounding box of raster where all tiles will be merged.
  301. """
  302. return self.map_region
  303. def GetNextTile(self):
  304. """!Get url for tile request from server and information for merging the tile with other tiles
  305. """
  306. tile_ref = {}
  307. if self.i_x >= self.num_tiles_x:
  308. return None
  309. tile_ref['sizeX'] = self.tile_cols
  310. if self.i_x == self.num_tiles_x - 1 and self.last_tile_x:
  311. tile_ref['sizeX'] = self.last_tile_x_size
  312. # set bbox for tile (N, S)
  313. if self.i_y != 0:
  314. self.tile_bbox['miny'] -= self.tile_length_y
  315. self.tile_bbox['maxy'] -= self.tile_length_y
  316. else:
  317. self.tile_bbox['maxy'] = self.bbox['maxy']
  318. self.tile_bbox['miny'] = self.bbox['maxy'] - self.tile_length_y
  319. tile_ref['sizeY'] = self.tile_rows
  320. if self.i_y == self.num_tiles_y - 1 and self.last_tile_y:
  321. tile_ref['sizeY'] = self.last_tile_y_size
  322. if self._isGeoProj(self.proj_srs) and self.version == "1.3.0":
  323. query_bbox = self._flipBbox(self.tile_bbox, self.proj_srs, self.version)
  324. else:
  325. query_bbox = self.tile_bbox
  326. query_url = self.url + "&" + "BBOX=%s,%s,%s,%s" % ( query_bbox['minx'], query_bbox['miny'], query_bbox['maxx'], query_bbox['maxy'])
  327. tile_ref['t_cols_offset'] = int(self.tile_cols * self.i_x)
  328. tile_ref['t_rows_offset'] = int(self.tile_rows * self.i_y)
  329. if self.i_y >= self.num_tiles_y - 1:
  330. self.i_y = 0
  331. self.i_x += 1
  332. # set bbox for next tile (E, W)
  333. self.tile_bbox['maxx'] += self.tile_length_x
  334. self.tile_bbox['minx'] += self.tile_length_x
  335. else:
  336. self.i_y += 1
  337. return query_url, tile_ref
  338. def _flipBbox(self, bbox, proj, version):
  339. """
  340. Flips coordinates if WMS standard is 1.3.0 and
  341. projection is geographic.
  342. value flips between this keys:
  343. maxy -> maxx
  344. maxx -> maxy
  345. miny -> minx
  346. minx -> miny
  347. @return copy of bbox with flipped coordinates
  348. """
  349. temp_bbox = dict(bbox)
  350. new_bbox = {}
  351. new_bbox['maxy'] = temp_bbox['maxx']
  352. new_bbox['miny'] = temp_bbox['minx']
  353. new_bbox['maxx'] = temp_bbox['maxy']
  354. new_bbox['minx'] = temp_bbox['miny']
  355. return new_bbox
  356. class WMTSRequestMgr(BaseRequestMgr):
  357. def __init__(self, params, bbox, region, proj_srs, cap_file = None):
  358. """!Initializes data needed for iteration through tiles.
  359. """
  360. self.proj_srs = proj_srs
  361. self.meters_per_unit = None
  362. # constant defined in WMTS standard (in meters)
  363. self.pixel_size = 0.00028
  364. # parse capabilities file
  365. try:
  366. # checks all elements needed by this class,
  367. # invalid elements are removed
  368. cap_tree = WMTSCapabilitiesTree(cap_file)
  369. except ParseError as error:
  370. grass.fatal(_("Unable to parse tile service file.\n%s\n") % str(error))
  371. self.xml_ns = cap_tree.getxmlnshandler()
  372. root = cap_tree.getroot()
  373. # get layer tile matrix sets with required projection
  374. mat_sets = self._getMatSets(root, params['layers'], params['srs']) #[[TileMatrixSet, TileMatrixSetLink], ....]
  375. # TODO: what if more tile matrix sets have required srs (returned more than 1)?
  376. mat_set = mat_sets[0][0]
  377. mat_set_link = mat_sets[0][1]
  378. params['tile_matrix_set'] = mat_set.find(self.xml_ns.NsOws('Identifier')).text
  379. # find tile matrix with resolution closest and smaller to wanted resolution
  380. tile_mat = self._findTileMats(mat_set.findall(self.xml_ns.NsWmts('TileMatrix')), region, bbox)
  381. # get extend of data available on server expressed in max/min rows and cols of tile matrix
  382. mat_num_bbox = self._getMatSize(tile_mat, mat_set_link)
  383. # initialize data needed for iteration through tiles
  384. self._computeRequestData(tile_mat, params, bbox, mat_num_bbox)
  385. def GetMapRegion(self):
  386. """!Get size in pixels and bounding box of raster where all tiles will be merged.
  387. """
  388. return self.map_region
  389. def _getMatSets(self, root, layer_name, srs):
  390. """!Get matrix sets which are available for chosen layer and have required EPSG.
  391. """
  392. contents = root.find(self.xml_ns.NsWmts('Contents'))
  393. layers = contents.findall(self.xml_ns.NsWmts('Layer'))
  394. ch_layer = None
  395. for layer in layers:
  396. layer_id = layer.find(self.xml_ns.NsOws('Identifier')).text
  397. if layer_id == layer_name:
  398. ch_layer = layer
  399. break
  400. if ch_layer is None:
  401. grass.fatal(_("Layer '%s' was not found in capabilities file") % layer_name)
  402. mat_set_links = ch_layer.findall(self.xml_ns.NsWmts('TileMatrixSetLink'))
  403. suitable_mat_sets = []
  404. tileMatrixSets = contents.findall(self.xml_ns.NsWmts('TileMatrixSet'))
  405. for link in mat_set_links:
  406. mat_set_link_id = link.find(self.xml_ns.NsWmts('TileMatrixSet')).text
  407. for mat_set in tileMatrixSets:
  408. mat_set_id = mat_set.find(self.xml_ns.NsOws('Identifier')).text
  409. if mat_set_id != mat_set_link_id:
  410. continue
  411. mat_set_srs = mat_set.find(self.xml_ns.NsOws('SupportedCRS')).text
  412. if mat_set_srs.lower() == ("EPSG:"+ str(srs)).lower():
  413. suitable_mat_sets.append([mat_set, link])
  414. if not suitable_mat_sets:
  415. grass.fatal(_("Layer '%s' is not available with %s code.") % (layer_name, "EPSG:" + str(srs)))
  416. return suitable_mat_sets # [[TileMatrixSet, TileMatrixSetLink], ....]
  417. def _findTileMats(self, tile_mats, region, bbox):
  418. """!Find best tile matrix set for requested resolution.
  419. """
  420. scale_dens = []
  421. scale_dens.append((bbox['maxy'] - bbox['miny']) / region['rows'] * self._getMetersPerUnit() / self.pixel_size)
  422. scale_dens.append((bbox['maxx'] - bbox['minx']) / region['cols'] * self._getMetersPerUnit() / self.pixel_size)
  423. scale_den = min(scale_dens)
  424. first = True
  425. for t_mat in tile_mats:
  426. mat_scale_den = float(t_mat.find(self.xml_ns.NsWmts('ScaleDenominator')).text)
  427. if first:
  428. best_scale_den = mat_scale_den
  429. best_t_mat = t_mat
  430. first = False
  431. continue
  432. best_diff = best_scale_den - scale_den
  433. mat_diff = mat_scale_den - scale_den
  434. if (best_diff < mat_diff and mat_diff < 0) or \
  435. (best_diff > mat_diff and best_diff > 0):
  436. best_t_mat = t_mat
  437. best_scale_den = mat_scale_den
  438. return best_t_mat
  439. def _getMetersPerUnit(self):
  440. """!Get coefficient which allows to convert units of request projection into meters.
  441. """
  442. if self.meters_per_unit:
  443. return self.meters_per_unit
  444. # for geographic projection
  445. if self._isGeoProj(self.proj_srs):
  446. proj_params = self.proj_srs.split(' ')
  447. for param in proj_params:
  448. if '+a' in param:
  449. a = float(param.split('=')[1])
  450. break
  451. equator_perim = 2 * pi * a
  452. # meters per degree on equator
  453. self.meters_per_unit = equator_perim / 360
  454. # other units
  455. elif '+to_meter' in self.proj_srs:
  456. proj_params = self.proj_srs.split(' ')
  457. for param in proj_params:
  458. if '+to_meter' in param:
  459. self.meters_per_unit = 1/float(param.split('=')[1])
  460. break
  461. # coordinate system in meters
  462. else:
  463. self.meters_per_unit = 1
  464. return self.meters_per_unit
  465. def _getMatSize(self, tile_mat, mat_set_link):
  466. """!Get rows and cols extend of data available on server for chosen layer and tile matrix.
  467. """
  468. # general tile matrix size
  469. mat_num_bbox = {}
  470. mat_num_bbox['min_col'] = mat_num_bbox['min_row'] = 0
  471. mat_num_bbox['max_col'] = int(tile_mat.find(self.xml_ns.NsWmts('MatrixWidth')).text) - 1
  472. mat_num_bbox['max_row'] = int(tile_mat.find(self.xml_ns.NsWmts('MatrixHeight')).text) - 1
  473. # get extend restriction in TileMatrixSetLink for the tile matrix, if exists
  474. tile_mat_set_limits = mat_set_link.find((self.xml_ns.NsWmts('TileMatrixSetLimits')))
  475. if tile_mat_set_limits is None:
  476. return mat_num_bbox
  477. tile_mat_id = tile_mat.find(self.xml_ns.NsOws('Identifier')).text
  478. tile_mat_limits = tile_mat_set_limits.findall(self.xml_ns.NsWmts('TileMatrixLimits'))
  479. for limit in tile_mat_limits:
  480. limit_tile_mat = limit.find(self.xml_ns.NsWmts('TileMatrix'))
  481. limit_id = limit_tile_mat.text
  482. if limit_id == tile_mat_id:
  483. for i in [['min_row', 'MinTileRow'], ['max_row', 'MaxTileRow'], \
  484. ['min_col', 'MinTileCol'], ['max_col', 'MaxTileCol']]:
  485. i_tag = limit.find(self.xml_ns.NsWmts(i[1]))
  486. mat_num_bbox[i[0]] = int(i_tag.text)
  487. break
  488. return mat_num_bbox
  489. def _computeRequestData(self, tile_mat, params, bbox, mat_num_bbox):
  490. """!Initialize data needed for iteration through tiles.
  491. """
  492. scale_den = float(tile_mat.find(self.xml_ns.NsWmts('ScaleDenominator')).text)
  493. pixel_span = scale_den * self.pixel_size / self._getMetersPerUnit()
  494. tl_str = tile_mat.find(self.xml_ns.NsWmts('TopLeftCorner')).text.split(' ')
  495. tl_corner = {}
  496. tl_corner['minx'] = float(tl_str[0])
  497. tl_corner['maxy'] = float(tl_str[1])
  498. tile_span = {}
  499. self.tile_size = {}
  500. self.tile_size['x'] = int(tile_mat.find(self.xml_ns.NsWmts('TileWidth')).text)
  501. tile_span['x'] = pixel_span * self.tile_size['x']
  502. self.tile_size['y'] = int(tile_mat.find(self.xml_ns.NsWmts('TileHeight')).text)
  503. tile_span['y'] = pixel_span * self.tile_size['y']
  504. self.url = params['url'] + ("SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&" \
  505. "LAYER=%s&STYLE=%s&FORMAT=%s&TILEMATRIXSET=%s&TILEMATRIX=%s" % \
  506. (params['layers'], params['styles'], params['format'],
  507. params['tile_matrix_set'], tile_mat.find(self.xml_ns.NsOws('Identifier')).text ))
  508. BaseRequestMgr._computeRequestData(self, bbox, tl_corner, tile_span, self.tile_size, mat_num_bbox)
  509. def GetNextTile(self):
  510. """!Get url for tile request from server and information for merging the tile with other tiles.
  511. """
  512. if not self.intersects or self.i_col > self.t_num_bbox['max_col']:
  513. return None
  514. query_url = self.url + "&TILECOL=%i&TILEROW=%i" % (int(self.i_col), int(self.i_row))
  515. self.tile_ref['t_cols_offset'] = int(self.tile_size['x'] * (self.i_col - self.t_num_bbox['min_col']))
  516. self.tile_ref['t_rows_offset'] = int(self.tile_size['y'] * (self.i_row - self.t_num_bbox['min_row']))
  517. if self.i_row >= self.t_num_bbox['max_row']:
  518. self.i_row = self.t_num_bbox['min_row']
  519. self.i_col += 1
  520. else:
  521. self.i_row += 1
  522. return query_url, self.tile_ref
  523. class OnEarthRequestMgr(BaseRequestMgr):
  524. def __init__(self, params, bbox, region, proj_srs, tile_service):
  525. """!Initializes data needed for iteration through tiles.
  526. """
  527. try:
  528. # checks all elements needed by this class,
  529. # invalid elements are removed
  530. self.cap_tree = OnEarthCapabilitiesTree(tile_service)
  531. except ParseError as error:
  532. grass.fatal(_("Unable to parse tile service file.\n%s\n") % str(error))
  533. root = self.cap_tree.getroot()
  534. # parse tile service file and get needed data for making tile requests
  535. url, self.tile_span, t_patt_bbox, self.tile_size = self._parseTileService(root, bbox, region, params)
  536. self.url = url
  537. self.url[0] = params['url'] + url[0]
  538. # initialize data needed for iteration through tiles
  539. self._computeRequestData(bbox, t_patt_bbox, self.tile_span, self.tile_size)
  540. def GetMapRegion(self):
  541. """!Get size in pixels and bounding box of raster where all tiles will be merged.
  542. """
  543. return self.map_region
  544. def _parseTileService(self, root, bbox, region, params):
  545. """!Get data from tile service file
  546. """
  547. tiled_patterns = root.find('TiledPatterns')
  548. tile_groups = self._getAllTiledGroup(tiled_patterns)
  549. if not tile_groups:
  550. grass.fatal(_("Unable to parse tile service file. \n No tag '%s' was found.") % 'TiledGroup')
  551. req_group = None
  552. for group in tile_groups:
  553. name = group.find('Name')
  554. if name.text == params['layers']:
  555. req_group = group
  556. break
  557. if req_group is None:
  558. grass.fatal(_("Tiled group '%s' was not found in tile service file") % params['layers'])
  559. group_t_patts = req_group.findall('TilePattern')
  560. best_patt = self._parseTilePattern(group_t_patts, bbox, region)
  561. urls = best_patt.text.split('\n')
  562. if params['urlparams']:
  563. url = self._insTimeToTilePatternUrl(params['urlparams'], urls)
  564. else:
  565. url = urls[0]
  566. for u in urls:
  567. if not 'time=${' in u:
  568. url = u
  569. url, t_bbox, width, height = self.cap_tree.gettilepatternurldata(url)
  570. tile_span = {}
  571. tile_span['x'] = abs(t_bbox[0] - t_bbox[2])
  572. tile_span['y'] = abs(t_bbox[1] - t_bbox[3])
  573. tile_pattern_bbox = req_group.find('LatLonBoundingBox')
  574. t_patt_bbox = {}
  575. for s in ['minx', 'miny', 'maxx', 'maxy']:
  576. t_patt_bbox[s] = float(tile_pattern_bbox.get(s))
  577. tile_size = {}
  578. tile_size['x'] = width
  579. tile_size['y'] = height
  580. return url, tile_span, t_patt_bbox, tile_size
  581. def _getAllTiledGroup(self, parent, tiled_groups = None):
  582. """!Get all 'TileGroup' elements
  583. """
  584. if not tiled_groups:
  585. tiled_groups = []
  586. tiled_groups += parent.findall('TiledGroup')
  587. new_groups = parent.findall('TiledGroups')
  588. for group in new_groups:
  589. self._getAllTiledGroup(group, tiled_groups)
  590. return tiled_groups
  591. def _parseTilePattern(self, group_t_patts, bbox, region):
  592. """!Find best tile pattern for requested resolution.
  593. """
  594. res = {}
  595. res['y'] = (bbox['maxy'] - bbox['miny']) / region['rows']
  596. res['x'] = (bbox['maxx'] - bbox['minx']) / region['cols']
  597. if res['x'] < res['y']:
  598. comp_res = 'x'
  599. else:
  600. comp_res = 'y'
  601. t_res = {}
  602. best_patt = None
  603. for pattern in group_t_patts:
  604. url, t_bbox, width, height = self.cap_tree.gettilepatternurldata(pattern.text.split('\n')[0])
  605. t_res['x'] = abs(t_bbox[0] - t_bbox[2]) / width
  606. t_res['y'] = abs(t_bbox[1] - t_bbox[3]) / height
  607. if best_patt is None:
  608. best_res = t_res[comp_res]
  609. best_patt = pattern
  610. first = False
  611. continue
  612. best_diff = best_res - res[comp_res]
  613. tile_diff = t_res[comp_res] - res[comp_res]
  614. if (best_diff < tile_diff and tile_diff < 0) or \
  615. (best_diff > tile_diff and best_diff > 0):
  616. best_res = t_res[comp_res]
  617. best_patt = pattern
  618. return best_patt
  619. def _insTimeToTilePatternUrl(self, url_params, urls):
  620. """!Time can be variable in some urls in OnEarth TMS.
  621. Insert requested time from 'urlparams' into the variable if any url of urls contains the variable.
  622. """
  623. url = None
  624. not_sup_params = []
  625. url_params_list = url_params.split('&')
  626. for param in url_params_list:
  627. try:
  628. k, v = param.split('=')
  629. except ValueError:
  630. grass.warning(_("Wrong form of parameter '%s' in '%s'. \n \
  631. The parameter was ignored.") % (param, 'urlparams'))
  632. if k != 'time':
  633. not_sup_params.append(k)
  634. continue
  635. has_time_var = False
  636. for url in urls:
  637. url_p_idxs = self.geturlparamidxs(url, k)
  638. if not url_p_idxs:
  639. continue
  640. url_p_value = url[url_p_idxs[0] + len(k + '=') : url_p_idxs[1]]
  641. if url_p_value[:2] == '${' and \
  642. url_p_value[len(url_p_value) - 1] == '}':
  643. url = url[:url_p_idxs[0]] + param + url[url_p_idxs[1]:]
  644. has_time_var = True
  645. break
  646. if not has_time_var:
  647. grass.warning(_("Parameter '%s' in '%s' is not variable in tile pattern url.") % (k, 'urlparams'))
  648. if not_sup_params:
  649. grass.warning(_("%s driver supports only '%s' parameter in '%s'. Other parameters are ignored.") % \
  650. ('OnEarth GRASS', 'time', 'urlparams'))
  651. return url
  652. def _computeRequestData(self, bbox, t_patt_bbox, tile_span, tile_size):
  653. """!Initialize data needed for iteration through tiles.
  654. """
  655. epsilon = 1e-15
  656. mat_num_bbox = {}
  657. mat_num_bbox['min_row'] = mat_num_bbox['min_col'] = 0
  658. mat_num_bbox['max_row'] = floor((t_patt_bbox['maxy'] - t_patt_bbox['miny'])/ tile_span['y'] + epsilon)
  659. mat_num_bbox['max_col'] = floor((t_patt_bbox['maxx'] - t_patt_bbox['minx'])/ tile_span['x'] + epsilon)
  660. BaseRequestMgr._computeRequestData(self, bbox, t_patt_bbox, self.tile_span, self.tile_size, mat_num_bbox)
  661. def GetNextTile(self):
  662. """!Get url for tile request from server and information for merging the tile with other tiles
  663. """
  664. if self.i_col > self.t_num_bbox['max_col']:
  665. return None
  666. x_offset = self.tile_span['x'] * self.i_col
  667. y_offset = self.tile_span['y'] * self.i_row
  668. query_url = self.url[0] + "&" + "bbox=%s,%s,%s,%s" % (float(self.query_bbox['minx'] + x_offset),
  669. float(self.query_bbox['miny'] - y_offset),
  670. float(self.query_bbox['maxx'] + x_offset),
  671. float(self.query_bbox['maxy'] - y_offset)) + self.url[1]
  672. self.tile_ref['t_cols_offset'] = int(self.tile_size['y'] * (self.i_col - self.t_num_bbox['min_col']))
  673. self.tile_ref['t_rows_offset'] = int(self.tile_size['x'] * (self.i_row - self.t_num_bbox['min_row']))
  674. if self.i_row >= self.t_num_bbox['max_row']:
  675. self.i_row = self.t_num_bbox['min_row']
  676. self.i_col += 1
  677. else:
  678. self.i_row += 1
  679. return query_url, self.tile_ref