ws.py 12 KB

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