wms_drv.py 37 KB

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