wms_drv.py 34 KB

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