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