wms_drv.py 39 KB

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