r.tileset.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
  1. #!/usr/bin/env python3
  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.script.utils import decode
  112. from grass.exceptions import CalledModuleError
  113. def bboxToPoints(bbox):
  114. """Make points that are the corners of a bounding box"""
  115. points = []
  116. points.append((bbox['w'], bbox['s']))
  117. points.append((bbox['w'], bbox['n']))
  118. points.append((bbox['e'], bbox['n']))
  119. points.append((bbox['e'], bbox['s']))
  120. return points
  121. def pointsToBbox(points):
  122. bbox = {}
  123. min_x = min_y = max_x = max_y = None
  124. for point in points:
  125. if not min_x:
  126. min_x = max_x = point[0]
  127. if not min_y:
  128. min_y = max_y = point[1]
  129. if min_x > point[0]:
  130. min_x = point[0]
  131. if max_x < point[0]:
  132. max_x = point[0]
  133. if min_y > point[1]:
  134. min_y = point[1]
  135. if max_y < point[1]:
  136. max_y = point[1]
  137. bbox['n'] = max_y
  138. bbox['s'] = min_y
  139. bbox['w'] = min_x
  140. bbox['e'] = max_x
  141. return bbox
  142. def project(file, source, dest):
  143. """Projects point (x, y) using projector"""
  144. errors = 0
  145. points = []
  146. try:
  147. ret = gcore.read_command('m.proj',
  148. quiet=True,
  149. flags='d',
  150. proj_in=source['proj'],
  151. proj_out=dest['proj'],
  152. sep=';',
  153. input=file)
  154. ret = decode(ret)
  155. except CalledModuleError:
  156. gcore.fatal(cs2cs + ' failed')
  157. if not ret:
  158. gcore.fatal(cs2cs + ' failed')
  159. for line in ret.splitlines():
  160. if "*" in line:
  161. errors += 1
  162. else:
  163. p_x2, p_y2, p_z2 = list(map(float, line.split(';')))
  164. points.append((p_x2 / dest['scale'], p_y2 / dest['scale']))
  165. return points, errors
  166. def projectPoints(points, source, dest):
  167. """Projects a list of points"""
  168. dest_points = []
  169. input = tempfile.NamedTemporaryFile(mode="wt")
  170. for point in points:
  171. input.file.write('%f;%f\n' % (point[0] * source['scale'],
  172. point[1] * source['scale']))
  173. input.file.flush()
  174. dest_points, errors = project(input.name, source, dest)
  175. return dest_points, errors
  176. def sideLengths(points, xmetric, ymetric):
  177. """Find the length of sides of a set of points from one to the next"""
  178. ret = []
  179. for i in range(len(points)):
  180. x1, y1 = points[i]
  181. j = i + 1
  182. if j >= len(points):
  183. j = 0
  184. sl_x = (points[j][0] - points[i][0]) * xmetric
  185. sl_y = (points[j][1] - points[i][1]) * ymetric
  186. sl_d = math.sqrt(sl_x * sl_x + sl_y * sl_y)
  187. ret.append(sl_d)
  188. return {'x': (ret[1], ret[3]), 'y': (ret[0], ret[2])}
  189. def bboxesIntersect(bbox_1, bbox_2):
  190. """Determine if two bounding boxes intersect"""
  191. bi_a1 = (bbox_1['w'], bbox_1['s'])
  192. bi_a2 = (bbox_1['e'], bbox_1['n'])
  193. bi_b1 = (bbox_2['w'], bbox_2['s'])
  194. bi_b2 = (bbox_2['e'], bbox_2['n'])
  195. cin = [False, False]
  196. for i in (0, 1):
  197. if (bi_a1[i] <= bi_b1[i] and bi_a2[i] >= bi_b1[i]) or \
  198. (bi_a1[i] <= bi_b1[i] and bi_a2[i] >= bi_b2[i]) or \
  199. (bi_b1[i] <= bi_a1[i] and bi_b2[i] >= bi_a1[i]) or \
  200. (bi_b1[i] <= bi_a1[i] and bi_b2[i] >= bi_a2[i]):
  201. cin[i] = True
  202. if cin[0] and cin[1]:
  203. return True
  204. return False
  205. def main():
  206. # Take into account those extra pixels we'll be a addin'
  207. max_cols = int(options['maxcols']) - int(options['overlap'])
  208. max_rows = int(options['maxrows']) - int(options['overlap'])
  209. if max_cols == 0:
  210. gcore.fatal(_("It is not possible to set 'maxcols=%s' and "
  211. "'overlap=%s'. Please set maxcols>overlap" %
  212. (options['maxcols'], options['overlap'])))
  213. elif max_rows == 0:
  214. gcore.fatal(_("It is not possible to set 'maxrows=%s' and "
  215. "'overlap=%s'. Please set maxrows>overlap" %
  216. (options['maxrows'], options['overlap'])))
  217. # destination projection
  218. if not options['destproj']:
  219. dest_proj = gcore.read_command('g.proj',
  220. quiet=True,
  221. flags='jf')
  222. dest_proj = decode(dest_proj).rstrip('\n')
  223. if not dest_proj:
  224. gcore.fatal(_('g.proj failed'))
  225. else:
  226. dest_proj = options['destproj']
  227. gcore.debug("Getting destination projection -> '%s'" % dest_proj)
  228. # projection scale
  229. if not options['destscale']:
  230. ret = gcore.parse_command('g.proj',
  231. quiet=True,
  232. flags='j')
  233. if not ret:
  234. gcore.fatal(_('g.proj failed'))
  235. if '+to_meter' in ret:
  236. dest_scale = ret['+to_meter'].strip()
  237. else:
  238. gcore.warning(
  239. _("Scale (%s) not found, assuming '1'") %
  240. '+to_meter')
  241. dest_scale = '1'
  242. else:
  243. dest_scale = options['destscale']
  244. gcore.debug('Getting destination projection scale -> %s' % dest_scale)
  245. # set up the projections
  246. srs_source = {'proj': options['sourceproj'],
  247. 'scale': float(options['sourcescale'])}
  248. srs_dest = {'proj': dest_proj, 'scale': float(dest_scale)}
  249. if options['region']:
  250. gcore.run_command('g.region',
  251. quiet=True,
  252. region=options['region'])
  253. dest_bbox = gcore.region()
  254. gcore.debug('Getting destination region')
  255. # output field separator
  256. fs = separator(options['separator'])
  257. # project the destination region into the source:
  258. gcore.verbose('Projecting destination region into source...')
  259. dest_bbox_points = bboxToPoints(dest_bbox)
  260. dest_bbox_source_points, errors_dest = projectPoints(dest_bbox_points,
  261. source=srs_dest,
  262. dest=srs_source)
  263. if len(dest_bbox_source_points) == 0:
  264. gcore.fatal(_("There are no tiles available. Probably the output "
  265. "projection system it is not compatible with the "
  266. "projection of the current location"))
  267. source_bbox = pointsToBbox(dest_bbox_source_points)
  268. gcore.verbose('Projecting source bounding box into destination...')
  269. source_bbox_points = bboxToPoints(source_bbox)
  270. source_bbox_dest_points, errors_source = projectPoints(source_bbox_points,
  271. source=srs_source,
  272. dest=srs_dest)
  273. x_metric = 1 / dest_bbox['ewres']
  274. y_metric = 1 / dest_bbox['nsres']
  275. gcore.verbose('Computing length of sides of source bounding box...')
  276. source_bbox_dest_lengths = sideLengths(source_bbox_dest_points,
  277. x_metric, y_metric)
  278. # Find the skewedness of the two directions.
  279. # Define it to be greater than one
  280. # In the direction (x or y) in which the world is least skewed (ie north south in lat long)
  281. # Divide the world into strips. These strips are as big as possible contrained by max_
  282. # In the other direction do the same thing.
  283. # There's some recomputation of the size of the world that's got to come in
  284. # here somewhere.
  285. # For now, however, we are going to go ahead and request more data than is necessary.
  286. # For small regions far from the critical areas of projections this makes very little difference
  287. # in the amount of data gotten.
  288. # We can make this efficient for big regions or regions near critical
  289. # points later.
  290. bigger = []
  291. bigger.append(max(source_bbox_dest_lengths['x']))
  292. bigger.append(max(source_bbox_dest_lengths['y']))
  293. maxdim = (max_cols, max_rows)
  294. # Compute the number and size of tiles to use in each direction
  295. # I'm making fairly even sized tiles
  296. # They differer from each other in height and width only by one cell
  297. # I'm going to make the numbers all simpler and add this extra cell to
  298. # every tile.
  299. gcore.message(_('Computing tiling...'))
  300. tiles = [-1, -1]
  301. tile_base_size = [-1, -1]
  302. tiles_extra_1 = [-1, -1]
  303. tile_size = [-1, -1]
  304. tileset_size = [-1, -1]
  305. tile_size_overlap = [-1, -1]
  306. for i in range(len(bigger)):
  307. # make these into integers.
  308. # round up
  309. bigger[i] = int(bigger[i] + 1)
  310. tiles[i] = int((bigger[i] / maxdim[i]) + 1)
  311. tile_size[i] = tile_base_size[i] = int(bigger[i] / tiles[i])
  312. tiles_extra_1[i] = int(bigger[i] % tiles[i])
  313. # This is adding the extra pixel (remainder) to all of the tiles:
  314. if tiles_extra_1[i] > 0:
  315. tile_size[i] = tile_base_size[i] + 1
  316. tileset_size[i] = int(tile_size[i] * tiles[i])
  317. # Add overlap to tiles (doesn't effect tileset_size
  318. tile_size_overlap[i] = tile_size[i] + int(options['overlap'])
  319. gcore.verbose("There will be %d by %d tiles each %d by %d cells" %
  320. (tiles[0], tiles[1], tile_size[0], tile_size[1]))
  321. ximax = tiles[0]
  322. yimax = tiles[1]
  323. min_x = source_bbox['w']
  324. min_y = source_bbox['s']
  325. max_x = source_bbox['e']
  326. max_y = source_bbox['n']
  327. span_x = (max_x - min_x)
  328. span_y = (max_y - min_y)
  329. xi = 0
  330. tile_bbox = {'w': -1, 's': -1, 'e': -1, 'n': -1}
  331. if errors_dest > 0:
  332. gcore.warning(_("During computation %i tiles could not be created" %
  333. errors_dest))
  334. while xi < ximax:
  335. tile_bbox['w'] = float(
  336. min_x) + (float(xi) * float(tile_size[0]) / float(tileset_size[0])) * float(span_x)
  337. tile_bbox['e'] = float(min_x) + (float(xi + 1) * float(tile_size_overlap[0]
  338. ) / float(tileset_size[0])) * float(span_x)
  339. yi = 0
  340. while yi < yimax:
  341. tile_bbox['s'] = float(
  342. min_y) + (float(yi) * float(tile_size[1]) / float(tileset_size[1])) * float(span_y)
  343. tile_bbox['n'] = float(min_y) + (
  344. float(yi + 1) * float(tile_size_overlap[1]) /
  345. float(tileset_size[1])) * float(span_y)
  346. tile_bbox_points = bboxToPoints(tile_bbox)
  347. tile_dest_bbox_points, errors = projectPoints(tile_bbox_points,
  348. source=srs_source,
  349. dest=srs_dest)
  350. tile_dest_bbox = pointsToBbox(tile_dest_bbox_points)
  351. if bboxesIntersect(tile_dest_bbox, dest_bbox):
  352. if flags['w']:
  353. print("bbox=%s,%s,%s,%s&width=%s&height=%s" %
  354. (tile_bbox['w'], tile_bbox['s'], tile_bbox['e'],
  355. tile_bbox['n'], tile_size_overlap[0],
  356. tile_size_overlap[1]))
  357. elif flags['g']:
  358. print("w=%s;s=%s;e=%s;n=%s;cols=%s;rows=%s" %
  359. (tile_bbox['w'], tile_bbox['s'], tile_bbox['e'],
  360. tile_bbox['n'], tile_size_overlap[0],
  361. tile_size_overlap[1]))
  362. else:
  363. print("%s%s%s%s%s%s%s%s%s%s%s" %
  364. (tile_bbox['w'], fs, tile_bbox['s'], fs,
  365. tile_bbox['e'], fs, tile_bbox['n'], fs,
  366. tile_size_overlap[0], fs, tile_size_overlap[1]))
  367. yi += 1
  368. xi += 1
  369. if __name__ == "__main__":
  370. cs2cs = 'cs2cs'
  371. options, flags = gcore.parser()
  372. sys.exit(main())