ws.py 11 KB

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