ws.py 12 KB

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