terrain_gen.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. #!/usr/bin/env python
  2. '''
  3. create ardupilot terrain database files
  4. '''
  5. import math, struct, os
  6. import crc16, time, struct
  7. from MAVProxy.modules.mavproxy_map import srtm
  8. # MAVLink sends 4x4 grids
  9. TERRAIN_GRID_MAVLINK_SIZE = 4
  10. # a 2k grid_block on disk contains 8x7 of the mavlink grids. Each
  11. # grid block overlaps by one with its neighbour. This ensures that
  12. # the altitude at any point can be calculated from a single grid
  13. # block
  14. TERRAIN_GRID_BLOCK_MUL_X = 7
  15. TERRAIN_GRID_BLOCK_MUL_Y = 8
  16. # this is the spacing between 32x28 grid blocks, in grid_spacing units
  17. TERRAIN_GRID_BLOCK_SPACING_X = ((TERRAIN_GRID_BLOCK_MUL_X-1)*TERRAIN_GRID_MAVLINK_SIZE)
  18. TERRAIN_GRID_BLOCK_SPACING_Y = ((TERRAIN_GRID_BLOCK_MUL_Y-1)*TERRAIN_GRID_MAVLINK_SIZE)
  19. # giving a total grid size of a disk grid_block of 32x28
  20. TERRAIN_GRID_BLOCK_SIZE_X = (TERRAIN_GRID_MAVLINK_SIZE*TERRAIN_GRID_BLOCK_MUL_X)
  21. TERRAIN_GRID_BLOCK_SIZE_Y = (TERRAIN_GRID_MAVLINK_SIZE*TERRAIN_GRID_BLOCK_MUL_Y)
  22. # format of grid on disk
  23. TERRAIN_GRID_FORMAT_VERSION = 1
  24. IO_BLOCK_SIZE = 2048
  25. #GRID_SPACING = 100
  26. def to_float32(f):
  27. '''emulate single precision float'''
  28. return struct.unpack('f', struct.pack('f',f))[0]
  29. LOCATION_SCALING_FACTOR = to_float32(0.011131884502145034)
  30. LOCATION_SCALING_FACTOR_INV = to_float32(89.83204953368922)
  31. def longitude_scale(lat):
  32. '''get longitude scale factor'''
  33. scale = to_float32(math.cos(to_float32(math.radians(lat))))
  34. return max(scale, 0.01)
  35. def get_distance_NE_e7(lat1, lon1, lat2, lon2):
  36. '''get distance tuple between two positions in 1e7 format'''
  37. return ((lat2 - lat1) * LOCATION_SCALING_FACTOR, (lon2 - lon1) * LOCATION_SCALING_FACTOR * longitude_scale(lat1*1.0e-7))
  38. def add_offset(lat_e7, lon_e7, ofs_north, ofs_east):
  39. '''add offset in meters to a position'''
  40. dlat = int(float(ofs_north) * LOCATION_SCALING_FACTOR_INV)
  41. dlng = int((float(ofs_east) * LOCATION_SCALING_FACTOR_INV) / longitude_scale(lat_e7*1.0e-7))
  42. return (int(lat_e7+dlat), int(lon_e7+dlng))
  43. def east_blocks(lat_e7, lon_e7, grid_spacing):
  44. '''work out how many blocks per stride on disk'''
  45. lat2_e7 = lat_e7
  46. lon2_e7 = lon_e7 + 10*1000*1000
  47. # shift another two blocks east to ensure room is available
  48. lat2_e7, lon2_e7 = add_offset(lat2_e7, lon2_e7, 0, 2*grid_spacing*TERRAIN_GRID_BLOCK_SIZE_Y)
  49. offset = get_distance_NE_e7(lat_e7, lon_e7, lat2_e7, lon2_e7)
  50. return int(offset[1] / (grid_spacing*TERRAIN_GRID_BLOCK_SPACING_Y))
  51. def pos_from_file_offset(lat_degrees, lon_degrees, file_offset, grid_spacing):
  52. '''return a lat/lon in 1e7 format given a file offset'''
  53. ref_lat = int(lat_degrees*10*1000*1000)
  54. ref_lon = int(lon_degrees*10*1000*1000)
  55. stride = east_blocks(ref_lat, ref_lon, grid_spacing)
  56. blocks = file_offset // IO_BLOCK_SIZE
  57. grid_idx_x = blocks // stride
  58. grid_idx_y = blocks % stride
  59. idx_x = grid_idx_x * TERRAIN_GRID_BLOCK_SPACING_X
  60. idx_y = grid_idx_y * TERRAIN_GRID_BLOCK_SPACING_Y
  61. offset = (idx_x * grid_spacing, idx_y * grid_spacing)
  62. (lat_e7, lon_e7) = add_offset(ref_lat, ref_lon, offset[0], offset[1])
  63. offset = get_distance_NE_e7(ref_lat, ref_lon, lat_e7, lon_e7)
  64. grid_idx_x = int(idx_x / TERRAIN_GRID_BLOCK_SPACING_X)
  65. grid_idx_y = int(idx_y / TERRAIN_GRID_BLOCK_SPACING_Y)
  66. (lat_e7, lon_e7) = add_offset(ref_lat, ref_lon,
  67. grid_idx_x * TERRAIN_GRID_BLOCK_SPACING_X * float(grid_spacing),
  68. grid_idx_y * TERRAIN_GRID_BLOCK_SPACING_Y * float(grid_spacing))
  69. return (lat_e7, lon_e7)
  70. class GridBlock(object):
  71. def __init__(self, lat_int, lon_int, lat, lon, grid_spacing):
  72. '''
  73. a grid block is a structure in a local file containing height
  74. information. Each grid block is 2048 bytes in size, to keep file IO to
  75. block oriented SD cards efficient
  76. '''
  77. # crc of whole block, taken with crc=0
  78. self.crc = 0
  79. # format version number
  80. self.version = TERRAIN_GRID_FORMAT_VERSION
  81. # grid spacing in meters
  82. self.spacing = grid_spacing
  83. # heights in meters over a 32*28 grid
  84. self.height = []
  85. for x in range(TERRAIN_GRID_BLOCK_SIZE_X):
  86. self.height.append([0]*TERRAIN_GRID_BLOCK_SIZE_Y)
  87. # bitmap of 4x4 grids filled in from GCS (56 bits are used)
  88. self.bitmap = (1<<56)-1
  89. lat_e7 = int(lat * 1.0e7)
  90. lon_e7 = int(lon * 1.0e7)
  91. # grids start on integer degrees. This makes storing terrain data on
  92. # the SD card a bit easier. Note that this relies on the python floor
  93. # behaviour with integer division
  94. self.lat_degrees = lat_int
  95. self.lon_degrees = lon_int
  96. # create reference position for this rounded degree position
  97. ref_lat = self.lat_degrees*10*1000*1000
  98. ref_lon = self.lon_degrees*10*1000*1000
  99. # find offset from reference
  100. offset = get_distance_NE_e7(ref_lat, ref_lon, lat_e7, lon_e7)
  101. offset = (round(offset[0]), round(offset[1]))
  102. # get indices in terms of grid_spacing elements
  103. idx_x = int(offset[0] / self.spacing)
  104. idx_y = int(offset[1] / self.spacing)
  105. # find indexes into 32*28 grids for this degree reference. Note
  106. # the use of TERRAIN_GRID_BLOCK_SPACING_{X,Y} which gives a one square
  107. # overlap between grids
  108. self.grid_idx_x = idx_x // TERRAIN_GRID_BLOCK_SPACING_X
  109. self.grid_idx_y = idx_y // TERRAIN_GRID_BLOCK_SPACING_Y
  110. # calculate lat/lon of SW corner of 32*28 grid_block
  111. (ref_lat, ref_lon) = add_offset(ref_lat, ref_lon,
  112. self.grid_idx_x * TERRAIN_GRID_BLOCK_SPACING_X * float(self.spacing),
  113. self.grid_idx_y * TERRAIN_GRID_BLOCK_SPACING_Y * float(self.spacing))
  114. self.lat = ref_lat
  115. self.lon = ref_lon
  116. def fill(self, gx, gy, altitude):
  117. '''fill a square'''
  118. self.height[gx][gy] = int(altitude)
  119. def blocknum(self):
  120. '''find IO block number'''
  121. stride = east_blocks(self.lat_degrees*1e7, self.lon_degrees*1e7, self.spacing)
  122. return stride * self.grid_idx_x + self.grid_idx_y
  123. class DataFile(object):
  124. def __init__(self, lat, lon, folder):
  125. if lat < 0:
  126. NS = 'S'
  127. else:
  128. NS = 'N'
  129. if lon < 0:
  130. EW = 'W'
  131. else:
  132. EW = 'E'
  133. self.name = folder + "/%c%02u%c%03u.DAT" % (NS, min(abs(int(lat)), 99),
  134. EW, min(abs(int(lon)), 999))
  135. self.tmpname = folder + "/%c%02u%c%03u.DAT.tmp" % (NS, min(abs(int(lat)), 99),
  136. EW, min(abs(int(lon)), 999))
  137. try:
  138. os.mkdir(folder)
  139. except Exception:
  140. pass
  141. if not os.path.exists(self.name):
  142. self.fh = open(self.tmpname, 'w+b')
  143. else:
  144. self.fh = open(self.name, 'r+b')
  145. def finalise(self):
  146. '''finalise file after writing'''
  147. self.fh.close()
  148. #and rename
  149. os.rename(self.tmpname, self.name)
  150. def seek_offset(self, block):
  151. '''seek to right offset'''
  152. # work out how many longitude blocks there are at this latitude
  153. file_offset = block.blocknum() * IO_BLOCK_SIZE
  154. self.fh.seek(file_offset)
  155. def pack(self, block):
  156. '''pack into a block'''
  157. buf = bytes()
  158. buf += struct.pack("<QiiHHH", block.bitmap, block.lat, block.lon, block.crc, block.version, block.spacing)
  159. for gx in range(TERRAIN_GRID_BLOCK_SIZE_X):
  160. buf += struct.pack("<%uh" % TERRAIN_GRID_BLOCK_SIZE_Y, *block.height[gx])
  161. buf += struct.pack("<HHhb", block.grid_idx_x, block.grid_idx_y, block.lon_degrees, block.lat_degrees)
  162. return buf
  163. def write(self, block):
  164. '''write a grid block'''
  165. self.seek_offset(block)
  166. block.crc = 0
  167. buf = self.pack(block)
  168. block.crc = crc16.crc16xmodem(buf)
  169. buf = self.pack(block)
  170. self.fh.write(buf)
  171. def check_filled(self, block, grid_spacing):
  172. '''read a grid block and check if already filled'''
  173. self.seek_offset(block)
  174. buf = self.fh.read(IO_BLOCK_SIZE)
  175. if len(buf) != IO_BLOCK_SIZE:
  176. return False
  177. (bitmap, lat, lon, crc, version, spacing) = struct.unpack("<QiiHHH", buf[:22])
  178. if (version != TERRAIN_GRID_FORMAT_VERSION or
  179. abs(lat - block.lat)>2 or
  180. abs(lon - block.lon)>2 or
  181. spacing != grid_spacing or
  182. bitmap != (1<<56)-1):
  183. return False
  184. buf = buf[:16] + struct.pack("<H", 0) + buf[18:]
  185. crc2 = crc16.crc16xmodem(buf[:1821])
  186. if crc2 != crc:
  187. return False
  188. return True
  189. def create_degree(downloader, lat, lon, folder, grid_spacing):
  190. '''create data file for one degree lat/lon'''
  191. lat_int = int(math.floor(lat))
  192. lon_int = int(math.floor((lon)))
  193. tiles = {}
  194. dfile = DataFile(lat_int, lon_int, folder)
  195. print("Creating for %d %d" % (lat_int, lon_int))
  196. total_blocks = east_blocks(lat_int*1e7, lon_int*1e7, grid_spacing) * TERRAIN_GRID_BLOCK_SIZE_Y
  197. for blocknum in range(total_blocks):
  198. (lat_e7, lon_e7) = pos_from_file_offset(lat_int, lon_int, blocknum * IO_BLOCK_SIZE, grid_spacing)
  199. lat = lat_e7 * 1.0e-7
  200. lon = lon_e7 * 1.0e-7
  201. grid = GridBlock(lat_int, lon_int, lat, lon, grid_spacing)
  202. if grid.blocknum() != blocknum:
  203. continue
  204. if dfile.check_filled(grid, grid_spacing):
  205. continue
  206. for gx in range(TERRAIN_GRID_BLOCK_SIZE_X):
  207. for gy in range(TERRAIN_GRID_BLOCK_SIZE_Y):
  208. lat_e7, lon_e7 = add_offset(lat*1.0e7, lon*1.0e7, gx*grid_spacing, gy*grid_spacing)
  209. lat2_int = int(math.floor(lat_e7*1.0e-7))
  210. lon2_int = int(math.floor(lon_e7*1.0e-7))
  211. tile_idx = (lat2_int, lon2_int)
  212. while not tile_idx in tiles:
  213. tile = downloader.getTile(lat2_int, lon2_int)
  214. waited = False
  215. if tile == 0:
  216. print("waiting on download of %d,%d" % (lat2_int, lon2_int))
  217. time.sleep(0.3)
  218. waited = True
  219. continue
  220. if waited:
  221. print("downloaded %d,%d" % (lat2_int, lon2_int))
  222. tiles[tile_idx] = tile
  223. if isinstance(tile, srtm.SRTMOceanTile):
  224. # if it's a blank ocean tile, there's a quicker way to generate the tile
  225. grid.fill(gx, gy, 0)
  226. else:
  227. altitude = tiles[tile_idx].getAltitudeFromLatLon(lat_e7*1.0e-7, lon_e7*1.0e-7)
  228. grid.fill(gx, gy, altitude)
  229. dfile.write(grid)
  230. dfile.finalise()
  231. def makeTerrain(downloader, radius, lat, lon, spacing, uuid, folder):
  232. #GRID_SPACING = spacing
  233. done = set()
  234. #create folder if required
  235. try:
  236. # Create target Directory
  237. os.mkdir(os.path.join(os.getcwd(), folder))
  238. except FileExistsError:
  239. pass
  240. try:
  241. os.mkdir(os.path.join(os.getcwd(), folder + "-tmp"))
  242. except FileExistsError:
  243. pass
  244. folderthis = os.path.join(os.getcwd(), folder + "-tmp", uuid)
  245. print("Doing " + folderthis)
  246. for dx in range(-radius, radius):
  247. for dy in range(-radius, radius):
  248. (lat2,lon2) = add_offset(lat*1e7, lon*1e7, dx*1000.0, dy*1000.0)
  249. lat_int = int(round(lat2 * 1.0e-7))
  250. lon_int = int(round(lon2 * 1.0e-7))
  251. tag = (lat_int, lon_int)
  252. if tag in done:
  253. #numpercent += 1
  254. continue
  255. done.add(tag)
  256. create_degree(downloader, lat_int, lon_int, folderthis, spacing)
  257. create_degree(downloader, lat, lon, folderthis, spacing)
  258. #numpercent += 1