ws.py 12 KB

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