ws.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. """
  2. @package core.ws
  3. @brief Fetching and preparation of web service data for rendering.
  4. Note: Currently only WMS is implemented.
  5. Classes:
  6. - ws::RenderWMSMgr
  7. - ws::GDALRasterMerger
  8. (C) 2012-2015 by the GRASS Development Team
  9. This program is free software under the GNU General Public License
  10. (>=v2). Read the file COPYING that comes with GRASS for details.
  11. @author Stepan Turek <stepan.turek seznam.cz> (mentor: Martin Landa)
  12. """
  13. import sys
  14. import copy
  15. import time
  16. import six
  17. import wx
  18. from grass.script.utils import try_remove
  19. from grass.script import core as grass
  20. from grass.exceptions import CalledModuleError
  21. from core.debug import Debug
  22. from core.gthread import gThread
  23. try:
  24. haveGdal = True
  25. from osgeo import gdal
  26. except ImportError:
  27. haveGdal = False
  28. from grass.pydispatch.signal import Signal
  29. class RenderWMSMgr(wx.EvtHandler):
  30. """Fetch and prepare WMS data for rendering."""
  31. def __init__(self, layer, env):
  32. if not haveGdal:
  33. sys.stderr.write(
  34. _(
  35. "Unable to load GDAL Python bindings.\n"
  36. "WMS layers can not be displayed without the bindings.\n"
  37. )
  38. )
  39. self.layer = layer
  40. wx.EvtHandler.__init__(self)
  41. # thread for d.wms commands
  42. self.thread = gThread()
  43. self._startTime = None
  44. self.downloading = False
  45. self.renderedRegion = None
  46. self.updateMap = True
  47. self.fetched_data_cmd = None
  48. self.tempMap = grass.tempfile()
  49. self.dstSize = {}
  50. self.dataFetched = Signal("RenderWMSMgr.dataFetched")
  51. self.updateProgress = Signal("RenderWMSMgr.updateProgress")
  52. self.renderingFailed = Signal("RenderWMSMgr.renderingFailed")
  53. def __del__(self):
  54. try_remove(self.tempMap)
  55. def Render(self, cmd, env):
  56. """If it is needed, download missing WMS data.
  57. .. todo::
  58. lmgr deletes mapfile and maskfile when order of layers
  59. was changed (drag and drop) - if deleted, fetch data again
  60. """
  61. if not haveGdal:
  62. return
  63. Debug.msg(
  64. 1,
  65. "RenderWMSMgr.Render(%s): force=%d img=%s"
  66. % (self.layer, self.layer.forceRender, self.layer.mapfile),
  67. )
  68. env = copy.copy(env)
  69. self.dstSize["cols"] = int(env["GRASS_RENDER_WIDTH"])
  70. self.dstSize["rows"] = int(env["GRASS_RENDER_HEIGHT"])
  71. region = self._getRegionDict(env)
  72. self._fitAspect(region, self.dstSize)
  73. self.updateMap = True
  74. fetchData = True # changed to True when calling Render()
  75. zoomChanged = False
  76. if self.renderedRegion is None or cmd != self.fetched_data_cmd:
  77. fetchData = True
  78. else:
  79. for c in ["north", "south", "east", "west"]:
  80. if self.renderedRegion and region[c] != self.renderedRegion[c]:
  81. fetchData = True
  82. break
  83. for c in ["e-w resol", "n-s resol"]:
  84. if self.renderedRegion and region[c] != self.renderedRegion[c]:
  85. zoomChanged = True
  86. break
  87. if fetchData:
  88. self.fetched_data_cmd = None
  89. self.renderedRegion = region
  90. try_remove(self.layer.mapfile)
  91. try_remove(self.tempMap)
  92. self.currentPid = self.thread.GetId()
  93. # self.thread.Terminate()
  94. self.downloading = True
  95. self.fetching_cmd = cmd
  96. env["GRASS_RENDER_FILE"] = self.tempMap
  97. env["GRASS_REGION"] = self._createRegionStr(region)
  98. cmd_render = copy.deepcopy(cmd)
  99. cmd_render[1]["quiet"] = True # be quiet
  100. self._startTime = time.time()
  101. self.thread.Run(
  102. callable=self._render,
  103. cmd=cmd_render,
  104. env=env,
  105. ondone=self.OnRenderDone,
  106. userdata={"env": env},
  107. )
  108. self.layer.forceRender = False
  109. self.updateProgress.emit(env=env, layer=self.layer)
  110. def _render(self, cmd, env):
  111. try:
  112. # TODO: use errors=status when working
  113. grass.run_command(cmd[0], env=env, **cmd[1])
  114. return 0
  115. except CalledModuleError as e:
  116. grass.error(e)
  117. return 1
  118. def OnRenderDone(self, event):
  119. """Fetch data"""
  120. if event.pid != self.currentPid:
  121. return
  122. self.downloading = False
  123. if not self.updateMap:
  124. self.updateProgress.emit(env=event.userdata["env"], layer=self.layer)
  125. self.renderedRegion = None
  126. self.fetched_data_cmd = None
  127. return
  128. self.mapMerger = GDALRasterMerger(
  129. targetFile=self.layer.mapfile,
  130. region=self.renderedRegion,
  131. bandsNum=3,
  132. gdalDriver="PNM",
  133. fillValue=0,
  134. )
  135. self.mapMerger.AddRasterBands(self.tempMap, {1: 1, 2: 2, 3: 3})
  136. del self.mapMerger
  137. add_alpha_channel = True
  138. mask_fill_value = 0
  139. if self.fetching_cmd[1]["format"] == "jpeg":
  140. mask_fill_value = (
  141. 255 # white color, g.pnmcomp doesn't apply mask (alpha channel)
  142. )
  143. add_alpha_channel = False
  144. self.maskMerger = GDALRasterMerger(
  145. targetFile=self.layer.maskfile,
  146. region=self.renderedRegion,
  147. bandsNum=1,
  148. gdalDriver="PNM",
  149. fillValue=mask_fill_value,
  150. )
  151. if add_alpha_channel:
  152. # {4 : 1} alpha channel (4) to first and only channel (1) in mask
  153. self.maskMerger.AddRasterBands(self.tempMap, {4: 1})
  154. del self.maskMerger
  155. self.fetched_data_cmd = self.fetching_cmd
  156. Debug.msg(
  157. 1,
  158. "RenderWMSMgr.OnRenderDone(%s): ret=%d time=%f"
  159. % (self.layer, event.ret, time.time() - self._startTime),
  160. )
  161. self.dataFetched.emit(env=event.userdata["env"], layer=self.layer)
  162. def _getRegionDict(self, env):
  163. """Parse string from GRASS_REGION env variable into dict."""
  164. region = {}
  165. parsedRegion = env["GRASS_REGION"].split(";")
  166. for r in parsedRegion:
  167. r = r.split(":")
  168. r[0] = r[0].strip()
  169. if len(r) < 2:
  170. continue
  171. try:
  172. if r[0] in ["e-w resol3", "n-s resol3", "rows3", "cols3", "depths"]:
  173. # ignore 3D region values (causing problems in latlong locations)
  174. continue
  175. if r[0] in ["cols", "rows", "zone", "proj"]:
  176. region[r[0]] = int(r[1])
  177. else:
  178. region[r[0]] = float(r[1])
  179. except ValueError:
  180. region[r[0]] = r[1]
  181. return region
  182. def _createRegionStr(self, region):
  183. """Create string for GRASS_REGION env variable from dict created by _getRegionDict."""
  184. regionStr = ""
  185. for k, v in six.iteritems(region):
  186. item = k + ": " + str(v)
  187. if regionStr:
  188. regionStr += "; "
  189. regionStr += item
  190. return regionStr
  191. def IsDownloading(self):
  192. """Is it downloading any data from WMS server?"""
  193. return self.downloading
  194. def _fitAspect(self, region, size):
  195. """Compute region parameters to have direction independent resolution."""
  196. if region["n-s resol"] > region["e-w resol"]:
  197. delta = region["n-s resol"] * size["cols"] / 2
  198. center = (region["east"] - region["west"]) / 2
  199. region["east"] = center + delta + region["west"]
  200. region["west"] = center - delta + region["west"]
  201. region["e-w resol"] = region["n-s resol"]
  202. else:
  203. delta = region["e-w resol"] * size["rows"] / 2
  204. center = (region["north"] - region["south"]) / 2
  205. region["north"] = center + delta + region["south"]
  206. region["south"] = center - delta + region["south"]
  207. region["n-s resol"] = region["e-w resol"]
  208. def Abort(self):
  209. """Abort rendering process"""
  210. Debug.msg(1, "RenderWMSMgr({0}).Abort()".format(self.layer))
  211. self.thread.Terminate()
  212. # force rendering layer next time
  213. self.layer.forceRender = True
  214. self.updateMap = False
  215. self.thread.Terminate(False)
  216. class GDALRasterMerger:
  217. """Merge rasters.
  218. Based on gdal_merge.py utility.
  219. """
  220. def __init__(self, targetFile, region, bandsNum, gdalDriver, fillValue=None):
  221. """Create raster for merging."""
  222. self.gdalDrvType = gdalDriver
  223. nsRes = (region["south"] - region["north"]) / region["rows"]
  224. ewRes = (region["east"] - region["west"]) / region["cols"]
  225. self.tGeotransform = [region["west"], ewRes, 0, region["north"], 0, nsRes]
  226. self.tUlx, self.tUly, self.tLrx, self.tLry = self._getCorners(
  227. self.tGeotransform, region
  228. )
  229. driver = gdal.GetDriverByName(self.gdalDrvType)
  230. self.tDataset = driver.Create(
  231. targetFile, region["cols"], region["rows"], bandsNum, gdal.GDT_Byte
  232. )
  233. if fillValue is not None:
  234. # fill raster bands with a constant value
  235. for iBand in range(1, self.tDataset.RasterCount + 1):
  236. self.tDataset.GetRasterBand(iBand).Fill(fillValue)
  237. def AddRasterBands(self, sourceFile, sTBands):
  238. """Add raster bands from sourceFile into the merging raster."""
  239. sDataset = gdal.Open(sourceFile, gdal.GA_ReadOnly)
  240. if sDataset is None:
  241. return
  242. sGeotransform = sDataset.GetGeoTransform()
  243. sSize = {"rows": sDataset.RasterYSize, "cols": sDataset.RasterXSize}
  244. sUlx, sUly, sLrx, sLry = self._getCorners(sGeotransform, sSize)
  245. # figure out intersection region
  246. tIntsctUlx = max(self.tUlx, sUlx)
  247. tIntsctLrx = min(self.tLrx, sLrx)
  248. if self.tGeotransform[5] < 0:
  249. tIntsctUly = min(self.tUly, sUly)
  250. tIntsctLry = max(self.tLry, sLry)
  251. else:
  252. tIntsctUly = max(self.tUly, sUly)
  253. tIntsctLry = min(self.tLry, sLry)
  254. # do they even intersect?
  255. if tIntsctUlx >= tIntsctLrx:
  256. return
  257. if self.tGeotransform[5] < 0 and tIntsctUly <= tIntsctLry:
  258. return
  259. if self.tGeotransform[5] > 0 and tIntsctUly >= tIntsctLry:
  260. return
  261. # compute target window in pixel coordinates.
  262. tXoff = int((tIntsctUlx - self.tGeotransform[0]) / self.tGeotransform[1] + 0.1)
  263. tYoff = int((tIntsctUly - self.tGeotransform[3]) / self.tGeotransform[5] + 0.1)
  264. tXsize = (
  265. int((tIntsctLrx - self.tGeotransform[0]) / self.tGeotransform[1] + 0.5)
  266. - tXoff
  267. )
  268. tYsize = (
  269. int((tIntsctLry - self.tGeotransform[3]) / self.tGeotransform[5] + 0.5)
  270. - tYoff
  271. )
  272. if tXsize < 1 or tYsize < 1:
  273. return
  274. # Compute source window in pixel coordinates.
  275. sXoff = int((tIntsctUlx - sGeotransform[0]) / sGeotransform[1])
  276. sYoff = int((tIntsctUly - sGeotransform[3]) / sGeotransform[5])
  277. sXsize = int((tIntsctLrx - sGeotransform[0]) / sGeotransform[1] + 0.5) - sXoff
  278. sYsize = int((tIntsctLry - sGeotransform[3]) / sGeotransform[5] + 0.5) - sYoff
  279. if sXsize < 1 or sYsize < 1:
  280. return
  281. for sBandNnum, tBandNum in six.iteritems(sTBands):
  282. bandData = sDataset.GetRasterBand(sBandNnum).ReadRaster(
  283. sXoff, sYoff, sXsize, sYsize, tXsize, tYsize, gdal.GDT_Byte
  284. )
  285. self.tDataset.GetRasterBand(tBandNum).WriteRaster(
  286. tXoff, tYoff, tXsize, tYsize, bandData, tXsize, tYsize, gdal.GDT_Byte
  287. )
  288. def _getCorners(self, geoTrans, size):
  289. ulx = geoTrans[0]
  290. uly = geoTrans[3]
  291. lrx = geoTrans[0] + size["cols"] * geoTrans[1]
  292. lry = geoTrans[3] + size["rows"] * geoTrans[5]
  293. return ulx, uly, lrx, lry
  294. def SetGeorefAndProj(self):
  295. """Set georeference and projection to target file"""
  296. projection = grass.read_command("g.proj", flags="wf")
  297. self.tDataset.SetProjection(projection)
  298. self.tDataset.SetGeoTransform(self.tGeotransform)
  299. def __del__(self):
  300. self.tDataset = None