r.tileset.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. #!/usr/bin/env python
  2. ############################################################################
  3. #
  4. # MODULE: r.tileset
  5. #
  6. # AUTHOR(S): Cedric Shock
  7. # Updated for GRASS7 by Martin Landa, 2009
  8. #
  9. # PURPOSE: To produce tilings of regions in other projections.
  10. #
  11. # COPYRIGHT: (C) 2006-2009 by Cedric Shoc, Martin Landa, and GRASS development team
  12. #
  13. # This program is free software under the GNU General
  14. # Public License (>=v2). Read the file COPYING that
  15. # comes with GRASS for details.
  16. #
  17. #############################################################################
  18. # Bugs:
  19. # Does not know about meridians in projections. However, unlike the usual
  20. # hack used to find meridians, this code is perfectly happy with arbitrary
  21. # rotations and flips
  22. # The following are planned to be fixed in a future version, if it turns out
  23. # to be necessary for someone:
  24. # Does not generate optimal tilings. This means that between an appropriate
  25. # projection for a region and latitude longitude projection, in the
  26. # degenerate case, it may create tiles demanding up to twice the necessary
  27. # information. Requesting data from cylindrical projections near their poles
  28. # results in divergence. You really don't want to use source data from
  29. # someone who put it in a cylindrical projection near a pole, do you?
  30. # Not generating "optimal" tilings has another side effect; the sanity
  31. # of the destination region will not carry over to generating tiles of
  32. # realistic aspect ratio. This might be a problem for some WMS servers
  33. # presenting data in a highly inappropriate projection. Do you really
  34. # want their data?
  35. #%module
  36. #% description: Produces tilings of the source projection for use in the destination region and projection.
  37. #% keyword: raster
  38. #% keyword: tiling
  39. #%end
  40. #%flag
  41. #% key: g
  42. #% description: Produces shell script output
  43. #%end
  44. #%flag
  45. #% key: w
  46. #% description: Produces web map server query string output
  47. #%end
  48. #%option
  49. #% key: region
  50. #% type: string
  51. #% description: Name of region to use instead of current region for bounds and resolution
  52. #%end
  53. #%option
  54. #% key: sourceproj
  55. #% type: string
  56. #% description: Source projection
  57. #% required : yes
  58. #%end
  59. #%option
  60. #% key: sourcescale
  61. #% type: string
  62. #% description: Conversion factor from units to meters in source projection
  63. #% answer : 1
  64. #%end
  65. #%option
  66. #% key: destproj
  67. #% type: string
  68. #% description: Destination projection, defaults to this location's projection
  69. #% required : no
  70. #%end
  71. #%option
  72. #% key: destscale
  73. #% type: string
  74. #% description: Conversion factor from units to meters in source projection
  75. #% required : no
  76. #%end
  77. #%option
  78. #% key: maxcols
  79. #% type: integer
  80. #% description: Maximum number of columns for a tile in the source projection
  81. #% answer: 1024
  82. #%end
  83. #%option
  84. #% key: maxrows
  85. #% type: integer
  86. #% description: Maximum number of rows for a tile in the source projection
  87. #% answer: 1024
  88. #%end
  89. #%option
  90. #% key: overlap
  91. #% type: integer
  92. #% description: Number of cells tiles should overlap in each direction
  93. #% answer: 0
  94. #%end
  95. #%option G_OPT_F_SEP
  96. #% description: Output field separator
  97. #%end
  98. # Data structures used in this program:
  99. # A bounding box:
  100. # 0 -> left, 1-> bottom, 2->right, 3-> top
  101. # A border:
  102. # An array of points indexed by 0 for "x" and 4 for "y" + by number 0, 1, 2, and 3
  103. # A reprojector [0] is name of source projection, [1] is name of destination
  104. # A projection - [0] is proj.4 text, [1] is scale
  105. from __future__ import print_function
  106. import sys
  107. import tempfile
  108. import math
  109. from grass.script.utils import separator
  110. from grass.script import core as gcore
  111. from grass.exceptions import CalledModuleError
  112. # i18N
  113. import os
  114. import gettext
  115. gettext.install('grassmods', os.path.join(os.getenv("GISBASE"), 'locale'))
  116. def bboxToPoints(bbox):
  117. """Make points that are the corners of a bounding box"""
  118. points = []
  119. points.append((bbox['w'], bbox['s']))
  120. points.append((bbox['w'], bbox['n']))
  121. points.append((bbox['e'], bbox['n']))
  122. points.append((bbox['e'], bbox['s']))
  123. return points
  124. def pointsToBbox(points):
  125. bbox = {}
  126. min_x = min_y = max_x = max_y = None
  127. for point in points:
  128. if not min_x:
  129. min_x = max_x = point[0]
  130. if not min_y:
  131. min_y = max_y = point[1]
  132. if min_x > point[0]:
  133. min_x = point[0]
  134. if max_x < point[0]:
  135. max_x = point[0]
  136. if min_y > point[1]:
  137. min_y = point[1]
  138. if max_y < point[1]:
  139. max_y = point[1]
  140. bbox['n'] = max_y
  141. bbox['s'] = min_y
  142. bbox['w'] = min_x
  143. bbox['e'] = max_x
  144. return bbox
  145. def project(file, source, dest):
  146. """Projects point (x, y) using projector"""
  147. errors = 0
  148. points = []
  149. try:
  150. ret = gcore.read_command('m.proj',
  151. quiet=True,
  152. flags='d',
  153. proj_in=source['proj'],
  154. proj_out=dest['proj'],
  155. sep=';',
  156. input=file)
  157. except CalledModuleError:
  158. gcore.fatal(cs2cs + ' failed')
  159. if not ret:
  160. gcore.fatal(cs2cs + ' failed')
  161. for line in ret.splitlines():
  162. if "*" in line:
  163. errors += 1
  164. else:
  165. p_x2, p_y2, p_z2 = list(map(float, line.split(';')))
  166. points.append((p_x2 / dest['scale'], p_y2 / dest['scale']))
  167. return points, errors
  168. def projectPoints(points, source, dest):
  169. """Projects a list of points"""
  170. dest_points = []
  171. input = tempfile.NamedTemporaryFile(mode="wt")
  172. for point in points:
  173. input.file.write('%f;%f\n' % (point[0] * source['scale'],
  174. point[1] * source['scale']))
  175. input.file.flush()
  176. dest_points, errors = project(input.name, source, dest)
  177. return dest_points, errors
  178. def sideLengths(points, xmetric, ymetric):
  179. """Find the length of sides of a set of points from one to the next"""
  180. ret = []
  181. for i in range(len(points)):
  182. x1, y1 = points[i]
  183. j = i + 1
  184. if j >= len(points):
  185. j = 0
  186. sl_x = (points[j][0] - points[i][0]) * xmetric
  187. sl_y = (points[j][1] - points[i][1]) * ymetric
  188. sl_d = math.sqrt(sl_x * sl_x + sl_y * sl_y)
  189. ret.append(sl_d)
  190. return {'x': (ret[1], ret[3]), 'y': (ret[0], ret[2])}
  191. def bboxesIntersect(bbox_1, bbox_2):
  192. """Determine if two bounding boxes intersect"""
  193. bi_a1 = (bbox_1['w'], bbox_1['s'])
  194. bi_a2 = (bbox_1['e'], bbox_1['n'])
  195. bi_b1 = (bbox_2['w'], bbox_2['s'])
  196. bi_b2 = (bbox_2['e'], bbox_2['n'])
  197. cin = [False, False]
  198. for i in (0, 1):
  199. if (bi_a1[i] <= bi_b1[i] and bi_a2[i] >= bi_b1[i]) or \
  200. (bi_a1[i] <= bi_b1[i] and bi_a2[i] >= bi_b2[i]) or \
  201. (bi_b1[i] <= bi_a1[i] and bi_b2[i] >= bi_a1[i]) or \
  202. (bi_b1[i] <= bi_a1[i] and bi_b2[i] >= bi_a2[i]):
  203. cin[i] = True
  204. if cin[0] and cin[1]:
  205. return True
  206. return False
  207. def main():
  208. # Take into account those extra pixels we'll be a addin'
  209. max_cols = int(options['maxcols']) - int(options['overlap'])
  210. max_rows = int(options['maxrows']) - int(options['overlap'])
  211. if max_cols == 0:
  212. gcore.fatal(_("It is not possible to set 'maxcols=%s' and "
  213. "'overlap=%s'. Please set maxcols>overlap" %
  214. (options['maxcols'], options['overlap'])))
  215. elif max_rows == 0:
  216. gcore.fatal(_("It is not possible to set 'maxrows=%s' and "
  217. "'overlap=%s'. Please set maxrows>overlap" %
  218. (options['maxrows'], options['overlap'])))
  219. # destination projection
  220. if not options['destproj']:
  221. dest_proj = gcore.read_command('g.proj',
  222. quiet=True,
  223. flags='jf').rstrip('\n')
  224. if not dest_proj:
  225. gcore.fatal(_('g.proj failed'))
  226. else:
  227. dest_proj = options['destproj']
  228. gcore.debug("Getting destination projection -> '%s'" % dest_proj)
  229. # projection scale
  230. if not options['destscale']:
  231. ret = gcore.parse_command('g.proj',
  232. quiet=True,
  233. flags='j')
  234. if not ret:
  235. gcore.fatal(_('g.proj failed'))
  236. if '+to_meter' in ret:
  237. dest_scale = ret['+to_meter'].strip()
  238. else:
  239. gcore.warning(
  240. _("Scale (%s) not found, assuming '1'") %
  241. '+to_meter')
  242. dest_scale = '1'
  243. else:
  244. dest_scale = options['destscale']
  245. gcore.debug('Getting destination projection scale -> %s' % dest_scale)
  246. # set up the projections
  247. srs_source = {'proj': options['sourceproj'],
  248. 'scale': float(options['sourcescale'])}
  249. srs_dest = {'proj': dest_proj, 'scale': float(dest_scale)}
  250. if options['region']:
  251. gcore.run_command('g.region',
  252. quiet=True,
  253. region=options['region'])
  254. dest_bbox = gcore.region()
  255. gcore.debug('Getting destination region')
  256. # output field separator
  257. fs = separator(options['separator'])
  258. # project the destination region into the source:
  259. gcore.verbose('Projecting destination region into source...')
  260. dest_bbox_points = bboxToPoints(dest_bbox)
  261. dest_bbox_source_points, errors_dest = projectPoints(dest_bbox_points,
  262. source=srs_dest,
  263. dest=srs_source)
  264. if len(dest_bbox_source_points) == 0:
  265. gcore.fatal(_("There are no tiles available. Probably the output "
  266. "projection system it is not compatible with the "
  267. "projection of the current location"))
  268. source_bbox = pointsToBbox(dest_bbox_source_points)
  269. gcore.verbose('Projecting source bounding box into destination...')
  270. source_bbox_points = bboxToPoints(source_bbox)
  271. source_bbox_dest_points, errors_source = projectPoints(source_bbox_points,
  272. source=srs_source,
  273. dest=srs_dest)
  274. x_metric = 1 / dest_bbox['ewres']
  275. y_metric = 1 / dest_bbox['nsres']
  276. gcore.verbose('Computing length of sides of source bounding box...')
  277. source_bbox_dest_lengths = sideLengths(source_bbox_dest_points,
  278. x_metric, y_metric)
  279. # Find the skewedness of the two directions.
  280. # Define it to be greater than one
  281. # In the direction (x or y) in which the world is least skewed (ie north south in lat long)
  282. # Divide the world into strips. These strips are as big as possible contrained by max_
  283. # In the other direction do the same thing.
  284. # There's some recomputation of the size of the world that's got to come in
  285. # here somewhere.
  286. # For now, however, we are going to go ahead and request more data than is necessary.
  287. # For small regions far from the critical areas of projections this makes very little difference
  288. # in the amount of data gotten.
  289. # We can make this efficient for big regions or regions near critical
  290. # points later.
  291. bigger = []
  292. bigger.append(max(source_bbox_dest_lengths['x']))
  293. bigger.append(max(source_bbox_dest_lengths['y']))
  294. maxdim = (max_cols, max_rows)
  295. # Compute the number and size of tiles to use in each direction
  296. # I'm making fairly even sized tiles
  297. # They differer from each other in height and width only by one cell
  298. # I'm going to make the numbers all simpler and add this extra cell to
  299. # every tile.
  300. gcore.message(_('Computing tiling...'))
  301. tiles = [-1, -1]
  302. tile_base_size = [-1, -1]
  303. tiles_extra_1 = [-1, -1]
  304. tile_size = [-1, -1]
  305. tileset_size = [-1, -1]
  306. tile_size_overlap = [-1, -1]
  307. for i in range(len(bigger)):
  308. # make these into integers.
  309. # round up
  310. bigger[i] = int(bigger[i] + 1)
  311. tiles[i] = int((bigger[i] / maxdim[i]) + 1)
  312. tile_size[i] = tile_base_size[i] = int(bigger[i] / tiles[i])
  313. tiles_extra_1[i] = int(bigger[i] % tiles[i])
  314. # This is adding the extra pixel (remainder) to all of the tiles:
  315. if tiles_extra_1[i] > 0:
  316. tile_size[i] = tile_base_size[i] + 1
  317. tileset_size[i] = int(tile_size[i] * tiles[i])
  318. # Add overlap to tiles (doesn't effect tileset_size
  319. tile_size_overlap[i] = tile_size[i] + int(options['overlap'])
  320. gcore.verbose("There will be %d by %d tiles each %d by %d cells" %
  321. (tiles[0], tiles[1], tile_size[0], tile_size[1]))
  322. ximax = tiles[0]
  323. yimax = tiles[1]
  324. min_x = source_bbox['w']
  325. min_y = source_bbox['s']
  326. max_x = source_bbox['e']
  327. max_y = source_bbox['n']
  328. span_x = (max_x - min_x)
  329. span_y = (max_y - min_y)
  330. xi = 0
  331. tile_bbox = {'w': -1, 's': -1, 'e': -1, 'n': -1}
  332. if errors_dest > 0:
  333. gcore.warning(_("During computation %i tiles could not be created" %
  334. errors_dest))
  335. while xi < ximax:
  336. tile_bbox['w'] = float(
  337. min_x) + (float(xi) * float(tile_size[0]) / float(tileset_size[0])) * float(span_x)
  338. tile_bbox['e'] = float(min_x) + (float(xi + 1) * float(tile_size_overlap[0]
  339. ) / float(tileset_size[0])) * float(span_x)
  340. yi = 0
  341. while yi < yimax:
  342. tile_bbox['s'] = float(
  343. min_y) + (float(yi) * float(tile_size[1]) / float(tileset_size[1])) * float(span_y)
  344. tile_bbox['n'] = float(min_y) + (
  345. float(yi + 1) * float(tile_size_overlap[1]) /
  346. float(tileset_size[1])) * float(span_y)
  347. tile_bbox_points = bboxToPoints(tile_bbox)
  348. tile_dest_bbox_points, errors = projectPoints(tile_bbox_points,
  349. source=srs_source,
  350. dest=srs_dest)
  351. tile_dest_bbox = pointsToBbox(tile_dest_bbox_points)
  352. if bboxesIntersect(tile_dest_bbox, dest_bbox):
  353. if flags['w']:
  354. print("bbox=%s,%s,%s,%s&width=%s&height=%s" %
  355. (tile_bbox['w'], tile_bbox['s'], tile_bbox['e'],
  356. tile_bbox['n'], tile_size_overlap[0],
  357. tile_size_overlap[1]))
  358. elif flags['g']:
  359. print("w=%s;s=%s;e=%s;n=%s;cols=%s;rows=%s" %
  360. (tile_bbox['w'], tile_bbox['s'], tile_bbox['e'],
  361. tile_bbox['n'], tile_size_overlap[0],
  362. tile_size_overlap[1]))
  363. else:
  364. print("%s%s%s%s%s%s%s%s%s%s%s" %
  365. (tile_bbox['w'], fs, tile_bbox['s'], fs,
  366. tile_bbox['e'], fs, tile_bbox['n'], fs,
  367. tile_size_overlap[0], fs, tile_size_overlap[1]))
  368. yi += 1
  369. xi += 1
  370. if __name__ == "__main__":
  371. cs2cs = 'cs2cs'
  372. options, flags = gcore.parser()
  373. sys.exit(main())