├── .gitignore ├── LICENSE ├── README.md ├── pyterrainmaker ├── FillRaster.py ├── GlobalGeodetic.py ├── TerrainBundle.py ├── TerrainTile.py ├── TileScheme.py ├── __init__.py ├── data │ ├── blank_heightmap.terrain │ ├── mesh000.terrain │ └── mesh010.terrain └── ecef.py ├── terrain_util.py └── terrainmaker.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | .DS_Store 107 | 108 | .idea/ 109 | .vscode/ 110 | tmp/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2018] [Jiang Yunpeng @vistawn] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pyterrainmaker 2 | 3 | A pure Python library for making [heightmap](https://github.com/CesiumGS/cesium/wiki/heightmap-1.0) and [quantized-mesh](https://github.com/CesiumGS/quantized-mesh) terrains for [CesiumJs](http://cesiumjs.org). 4 | 5 | Fully compatible with [Cesium Terrain Server](https://github.com/geo-data/cesium-terrain-server). 6 | 7 | 8 | ## Command Line Tools 9 | 10 | ### `python3 terrainmaker.py -o ./terrain_tiles dem.tif` 11 | 12 | ``` 13 | Usage: python3 terrainmaker.py [options] GDAL_DATASOURCE 14 | Options: 15 | -v, --version output program version 16 | -h, --help output help information 17 | -l, --fill fill nodata by another raster 18 | -o, --out_dir output directory for terrains 19 | -f, --format terrain format: heightmap/mesh, default is heightmap 20 | -e, --max_error maximum triangulation error (float [=0.001]) 21 | -m, --mode output storage mode: compact/single, default is single 22 | ``` 23 | #### Recommendations 24 | 25 | * Input GDAL_DATASOURCE elevation data should have only one band or elevation band is the first band. 26 | * Input GDAL_DATASOURCE band must create overviews if band's X-Size or Y-Size greater than 2000 pixel. 27 | 28 | ### terrain_util 29 | ```shell 30 | python3 terrain_util.py 31 | ``` 32 | 33 | 34 | 35 | 36 | ## Dependency 37 | * GDAL 38 | * numpy 39 | * quantized_mesh_encoder 40 | * pydelatin 41 | 42 | 43 | ## TODO 44 | 45 | * Bundle mode terrains for better storage and management. 46 | * Multi-threading support 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /pyterrainmaker/FillRaster.py: -------------------------------------------------------------------------------- 1 | from osgeo import gdal, gdalconst 2 | import numpy as np 3 | import struct 4 | 5 | 6 | class FillRaster(object): 7 | 8 | def __init__(self, raster_loc): 9 | self.ds = gdal.Open(raster_loc, 0) 10 | transf = self.ds.GetGeoTransform() 11 | self.cols = self.ds.RasterXSize 12 | self.rows = self.ds.RasterYSize 13 | self.left_x = transf[0] 14 | self.top_y = transf[3] 15 | self.res_x = transf[1] 16 | self.res_y = abs(transf[5]) 17 | self.right_x = self.left_x + self.cols * self.res_x 18 | self.bottom_y = self.top_y - self.rows * self.res_y 19 | self.band = self.ds.GetRasterBand(1) 20 | 21 | def get_height(self, lon, lat): 22 | 23 | if not self.left_x < lon < self.right_x or not self.bottom_y < lat < self.top_y: 24 | return 0 25 | 26 | query_x = int((lon - self.left_x) / self.res_x) 27 | query_y = int((self.top_y - lat) / self.res_y) 28 | val = self.band.ReadRaster(query_x, query_y, 1, 1, buf_type=self.band.DataType) 29 | 30 | if val is None: 31 | return 0 32 | else: 33 | intval = struct.unpack('h', val) 34 | ele = intval[0] 35 | if ele == -32767: 36 | return 0 37 | else: 38 | return ele 39 | 40 | 41 | def get_array(self, extent, out_x_count, out_y_count): 42 | 43 | file_ds = self.ds 44 | 45 | file_band = file_ds.GetRasterBand(1) 46 | ov_count = file_band.GetOverviewCount() 47 | 48 | file_trans = file_ds.GetGeoTransform() 49 | min_ori_x = file_trans[0] 50 | max_ori_y = file_trans[3] 51 | 52 | file_ori_res = file_trans[1] 53 | file_ori_x_size = file_band.XSize 54 | 55 | (b_real_min_x, b_real_min_y, b_real_max_x, b_real_max_y) = extent 56 | out_res = (extent[2] - extent[0]) / out_x_count 57 | 58 | read_band = file_band 59 | read_res = file_ori_res 60 | 61 | for r_i in range(ov_count): 62 | ov_band = file_band.GetOverview(r_i) 63 | ov_x_size = ov_band.XSize 64 | ov_res = (file_ori_x_size / ov_x_size) * file_ori_res 65 | 66 | if ov_res > out_res: 67 | break 68 | else: 69 | read_band = ov_band 70 | read_res = ov_res 71 | 72 | ## read array by extent 73 | read_min_px = int((b_real_min_x - min_ori_x) / read_res) 74 | read_max_px = int((b_real_max_x - min_ori_x) / read_res) 75 | read_min_py = int((max_ori_y - b_real_max_y) / read_res) 76 | read_max_py = int((max_ori_y - b_real_min_y) / read_res) 77 | 78 | read_x_count = read_max_px - read_min_px 79 | read_y_count = read_max_py - read_min_py 80 | 81 | read_array = read_band.ReadAsArray(read_min_px, read_min_py, read_x_count, read_y_count) 82 | if read_array is None: 83 | return None 84 | read_array = read_array.astype(np.float32) 85 | 86 | if read_x_count == out_x_count and read_y_count == out_y_count: 87 | return read_array 88 | else: 89 | mem_drv = gdal.GetDriverByName('MEM') 90 | prj_ds = mem_drv.Create('', read_x_count, read_y_count, 1, gdalconst.GDT_Float32) 91 | prj_band = prj_ds.GetRasterBand(1) 92 | prj_band.WriteArray(read_array, 0, 0) 93 | 94 | dst_ds = mem_drv.Create('', out_x_count, out_y_count, 1, gdalconst.GDT_Float32) 95 | dst_ds.SetGeoTransform((0.0, 1.0, 0.0, 0.0, 0.0, 1.0)) 96 | prj_ds.SetGeoTransform( 97 | (0.0, out_x_count / float(read_x_count), 0.0, 0.0, 0.0, out_y_count / float(read_y_count))) 98 | res = gdal.ReprojectImage(prj_ds, dst_ds, None, None, eResampleAlg=gdalconst.GRIORA_NearestNeighbour) 99 | if res != 0: 100 | del prj_ds 101 | del dst_ds 102 | self.error("ReprojectImage() failed on %s, error %d" % ('aa', res)) 103 | else: 104 | result = np.array(dst_ds.GetRasterBand(1).ReadAsArray(0, 0, out_x_count, out_y_count)) 105 | del prj_ds 106 | del dst_ds 107 | return result 108 | 109 | -------------------------------------------------------------------------------- /pyterrainmaker/GlobalGeodetic.py: -------------------------------------------------------------------------------- 1 | 2 | import math 3 | 4 | class GlobalGeodetic(object): 5 | r""" 6 | TMS Global Geodetic Profile 7 | --------------------------- 8 | 9 | Functions necessary for generation of global tiles in Plate Carre projection, 10 | EPSG:4326, "unprojected profile". 11 | 12 | Such tiles are compatible with Google Earth (as any other EPSG:4326 rasters) 13 | and you can overlay the tiles on top of OpenLayers base map. 14 | 15 | Pixel and tile coordinates are in TMS notation (origin [0,0] in bottom-left). 16 | 17 | What coordinate conversions do we need for TMS Global Geodetic tiles? 18 | 19 | Global Geodetic tiles are using geodetic coordinates (latitude,longitude) 20 | directly as planar coordinates XY (it is also called Unprojected or Plate 21 | Carre). We need only scaling to pixel pyramid and cutting to tiles. 22 | Pyramid has on top level two tiles, so it is not square but rectangle. 23 | Area [-180,-90,180,90] is scaled to 512x256 pixels. 24 | TMS has coordinate origin (for pixels and tiles) in bottom-left corner. 25 | Rasters are in EPSG:4326 and therefore are compatible with Google Earth. 26 | 27 | LatLon <-> Pixels <-> Tiles 28 | 29 | WGS84 coordinates Pixels in pyramid Tiles in pyramid 30 | lat/lon XY pixels Z zoom XYZ from TMS 31 | EPSG:4326 32 | .----. ---- 33 | / \ <-> /--------/ <-> TMS 34 | \ / /--------------/ 35 | ----- /--------------------/ 36 | WMS, KML Web Clients, Google Earth TileMapService 37 | """ 38 | 39 | def __init__(self, tmscompatible, tileSize=256): 40 | self.tileSize = tileSize 41 | if tmscompatible is not None: 42 | # Defaults the resolution factor to 0.703125 (2 tiles @ level 0) 43 | # Adhers to OSGeo TMS spec 44 | # http://wiki.osgeo.org/wiki/Tile_Map_Service_Specification#global-geodetic 45 | self.resFact = 180.0 / self.tileSize 46 | else: 47 | # Defaults the resolution factor to 1.40625 (1 tile @ level 0) 48 | # Adheres OpenLayers, MapProxy, etc default resolution for WMTS 49 | self.resFact = 360.0 / self.tileSize 50 | 51 | def LonLatToPixels(self, lon, lat, zoom): 52 | "Converts lon/lat to pixel coordinates in given zoom of the EPSG:4326 pyramid" 53 | 54 | res = self.resFact / 2**zoom 55 | px = (180 + lon) / res 56 | py = (90 + lat) / res 57 | return px, py 58 | 59 | def PixelsToTile(self, px, py): 60 | "Returns coordinates of the tile covering region in pixel coordinates" 61 | 62 | tx = int(math.ceil(px / float(self.tileSize)) - 1) 63 | ty = int(math.ceil(py / float(self.tileSize)) - 1) 64 | return tx, ty 65 | 66 | def LonLatToTile(self, lon, lat, zoom): 67 | "Returns the tile for zoom which covers given lon/lat coordinates" 68 | 69 | px, py = self.LonLatToPixels(lon, lat, zoom) 70 | return self.PixelsToTile(px, py) 71 | 72 | def Resolution(self, zoom): 73 | "Resolution (arc/pixel) for given zoom level (measured at Equator)" 74 | 75 | return self.resFact / 2**zoom 76 | 77 | def ZoomForPixelSize(self, pixelSize): 78 | "Maximal scaledown zoom of the pyramid closest to the pixelSize." 79 | 80 | for i in range(MAXZOOMLEVEL): 81 | if pixelSize > self.Resolution(i): 82 | if i != 0: 83 | return i-1 84 | else: 85 | return 0 # We don't want to scale up 86 | 87 | def TileBounds(self, tx, ty, zoom): 88 | "Returns bounds of the given tile" 89 | res = self.resFact / 2**zoom 90 | return ( 91 | tx*self.tileSize*res - 180, 92 | ty*self.tileSize*res - 90, 93 | (tx+1)*self.tileSize*res - 180, 94 | (ty+1)*self.tileSize*res - 90 95 | ) 96 | 97 | def TileLatLonBounds(self, tx, ty, zoom): 98 | "Returns bounds of the given tile in the SWNE form" 99 | b = self.TileBounds(tx, ty, zoom) 100 | return (b[1], b[0], b[3], b[2]) 101 | 102 | -------------------------------------------------------------------------------- /pyterrainmaker/TerrainBundle.py: -------------------------------------------------------------------------------- 1 | 2 | # 3 | # TerrainBundle Class 4 | # multiple terrain tiles as processing unit. 5 | # default is 128 * 128 6 | # 7 | 8 | import os 9 | import numpy as np 10 | from osgeo import gdalconst 11 | from osgeo import gdal 12 | import struct 13 | 14 | from .GlobalGeodetic import GlobalGeodetic 15 | from .TerrainTile import TerrainTile 16 | 17 | import sys 18 | if sys.version_info >= (3, 0): 19 | xrange = range 20 | 21 | 22 | def make_child_flags(N, S, E, W): 23 | # Cesium format neighbor tiles flags 24 | HAS_SW = 0x01 25 | HAS_SE = 0x02 26 | HAS_NW = 0x04 27 | HAS_NE = 0x08 28 | 29 | NB_FLAGS = 0x00 30 | 31 | if N & W: 32 | NB_FLAGS = NB_FLAGS | HAS_NW 33 | if N & E: 34 | NB_FLAGS = NB_FLAGS | HAS_NE 35 | if S & W: 36 | NB_FLAGS = NB_FLAGS | HAS_SW 37 | if S & E: 38 | NB_FLAGS = NB_FLAGS | HAS_SE 39 | 40 | return NB_FLAGS 41 | 42 | 43 | class TerrainBundle(object): 44 | 45 | def __init__(self, in_source_band, bundle_size, is_compact): 46 | self.bundle_size = bundle_size 47 | self.data_band = in_source_band 48 | self.is_compact = is_compact 49 | self.__tiles = [] 50 | self.has_next_level = True 51 | self.level = 0 52 | self.resolution = None 53 | self.from_tile = None 54 | self.source_range = None 55 | self.data_band = None 56 | self.no_data = None 57 | self.out_no_data = None 58 | self.bundle_array = None 59 | self.fill_raster = None 60 | 61 | def calculate_tiles(self): 62 | cols = self.data_band.XSize 63 | rows = self.data_band.YSize 64 | (data_min_x, data_min_y, data_max_x, data_max_y) = self.source_range 65 | band_res = (data_max_x - data_min_x) / cols 66 | gg = GlobalGeodetic(True, 64) 67 | tile_x = self.from_tile[0] 68 | tile_y = self.from_tile[1] 69 | tile_max_x = tile_x 70 | tile_min_y = tile_y 71 | (t_min_y, t_min_x, t_max_y, t_max_x) = gg.TileLatLonBounds(tile_x, tile_y, self.level) 72 | 73 | for index_x in xrange(0, self.bundle_size): 74 | tile_x = self.from_tile[0] + index_x 75 | for index_y in xrange(0, self.bundle_size): 76 | tile_y = self.from_tile[1] - index_y 77 | tile_range = (t_min_y, t_min_x, t_max_y, t_max_x) = gg.TileLatLonBounds(tile_x, tile_y, self.level) 78 | 79 | if self.is_in_source_range(t_min_x, t_min_y, t_max_x, t_max_y): 80 | tile_max_x = max(tile_max_x, tile_x) 81 | tile_min_y = min(tile_min_y, tile_y) 82 | if self.has_next_level: 83 | flag = self.calc_tile_flag(tile_range) 84 | else: 85 | flag = 0x00 86 | tile = TerrainTile(index_x * 64, index_y*64, flag, (t_min_x, t_min_y, t_max_x, t_max_y), self.resolution) 87 | tile.x = tile_x 88 | tile.y = tile_y 89 | 90 | if self.level < 4: 91 | tile.fake = True 92 | 93 | self.__tiles.append(tile) 94 | 95 | if self.level < 4: 96 | return 97 | 98 | bundle_tiles_x = tile_max_x - self.from_tile[0] + 1 99 | bundle_tiles_y = self.from_tile[1] - tile_min_y + 1 100 | bundle_px_width = bundle_tiles_x * 64 + 1 101 | bundle_px_height = bundle_tiles_y * 64 + 1 102 | 103 | # first range 104 | (f_min_y, f_min_x, f_max_y, f_max_x) = gg.TileLatLonBounds(self.from_tile[0], self.from_tile[1], self.level) 105 | # bundle_range 106 | (b_min_y, b_min_x, b_max_y, b_max_x) = ( 107 | f_min_y - (f_max_y - f_min_y) * (bundle_tiles_y - 1), 108 | f_min_x, 109 | f_max_y, 110 | f_max_x + (f_max_x - f_min_x) * (bundle_tiles_x - 1) 111 | ) 112 | 113 | b_px_min_x = int((b_min_x - data_min_x) / band_res) 114 | b_px_max_x = int((b_max_x - data_min_x) / band_res) + 1 115 | b_px_min_y = int((data_max_y - b_max_y) / band_res) 116 | b_px_max_y = int((data_max_y - b_min_y) / band_res) + 1 117 | 118 | b_real_min_x, b_real_max_x, b_real_min_y, b_real_max_y = b_min_x, b_max_x, b_min_y, b_max_y 119 | shift_left = shift_right = shift_top = shift_bottom = 0 120 | if b_px_min_x < 0: 121 | shift_left = abs(b_px_min_x) 122 | b_real_min_x += shift_left * band_res 123 | b_px_min_x = 0 124 | if b_px_max_x >= cols: 125 | shift_right = b_px_max_x - cols 126 | b_real_max_x -= shift_right * band_res 127 | b_px_max_x = cols 128 | if b_px_min_y < 0: 129 | shift_top = abs(b_px_min_y) 130 | b_real_min_y += shift_top * band_res 131 | b_px_min_y = 0 132 | if b_px_max_y >= rows: 133 | shift_bottom = b_px_max_y - rows 134 | b_real_max_y -= shift_bottom * band_res 135 | b_px_max_y = rows 136 | w_x = b_px_max_x - b_px_min_x 137 | w_y = b_px_max_y - b_px_min_y 138 | 139 | tile_array = self.data_band.ReadAsArray(b_px_min_x, b_px_min_y, w_x, w_y) 140 | 141 | fill_blank_value = self.out_no_data 142 | if self.out_no_data is not None and self.no_data is not None: 143 | fill_blank_value = self.out_no_data 144 | np.place(tile_array, tile_array == self.no_data, self.out_no_data) 145 | 146 | shift_obj = (shift_obj_v, shift_obj_h) = ((shift_top, shift_bottom), (shift_left, shift_right)) 147 | 148 | if shift_obj != ((0, 0), (0, 0)): 149 | tile_array = np.lib.pad(tile_array, shift_obj, 'constant', constant_values=[fill_blank_value]) 150 | 151 | if self.fill_raster: 152 | h,w = tile_array.shape 153 | fill_array = self.fill_raster.get_array((b_real_min_x, b_real_min_y, b_real_max_x, b_real_max_y), w, h) 154 | if fill_array is not None: 155 | tile_array = np.where(tile_array==0, fill_array, tile_array) 156 | 157 | 158 | (m_rows, m_cols) = tile_array.shape 159 | 160 | mem_drv = gdal.GetDriverByName('MEM') 161 | 162 | dst_bundle_ds = mem_drv.Create('', bundle_px_width, bundle_px_height, 1, gdalconst.GDT_Float32) 163 | 164 | if m_cols == bundle_px_width and m_rows == bundle_px_height: 165 | dst_bundle_ds.WriteRaster(0, 0, m_cols, m_rows, np.frombuffer(tile_array, tile_array.dtype).tostring()) 166 | self.bundle_array = np.array(dst_bundle_ds.GetRasterBand(1).ReadAsArray(0, 0, bundle_px_width, bundle_px_height)) 167 | else: 168 | prj_ds = mem_drv.Create('', m_cols, m_rows, 1, gdalconst.GDT_Float32) 169 | prj_band = prj_ds.GetRasterBand(1) 170 | prj_band.WriteArray(tile_array, 0, 0) 171 | dst_bundle_ds.SetGeoTransform((0.0, 1.0, 0.0, 0.0, 0.0, 1.0)) 172 | prj_ds.SetGeoTransform( 173 | (0.0, bundle_px_width / float(m_cols), 0.0, 0.0, 0.0, bundle_px_height / float(m_rows))) 174 | res = gdal.ReprojectImage(prj_ds, dst_bundle_ds, None, None, eResampleAlg=gdalconst.GRIORA_NearestNeighbour) 175 | if res != 0: 176 | self.error("ReprojectImage() failed on %s, error %d" % ('aa', res)) 177 | else: 178 | self.bundle_array = np.array(dst_bundle_ds.GetRasterBand(1).ReadAsArray(0, 0, bundle_px_width, bundle_px_height)) 179 | 180 | del prj_ds 181 | 182 | del tile_array 183 | del dst_bundle_ds 184 | 185 | def calc_tile_flag(self, bound): 186 | (t_min_y, t_min_x, t_max_y, t_max_x) = bound 187 | N = S = W = E = False 188 | mid_x = (t_min_x + t_max_x) / 2 189 | mid_y = (t_min_y + t_max_y) / 2 190 | s_min_x, s_min_y, s_max_x, s_max_y = self.source_range 191 | if s_min_x <= mid_x and s_max_x >= t_min_x: 192 | W = True 193 | if s_min_x <= t_max_x and s_max_x >= mid_x: 194 | E = True 195 | if s_min_y <= t_max_y and s_max_y >= mid_y: 196 | N = True 197 | if s_min_y <= mid_y and s_max_y >= t_min_y: 198 | S = True 199 | 200 | return make_child_flags(N, S, E, W) 201 | 202 | def is_in_source_range(self, min_x, min_y, max_x, max_y): 203 | s_min_x, s_min_y, s_max_x, s_max_y = self.source_range 204 | if min_x > s_max_x or max_x < s_min_x or min_y > s_max_y or max_y < s_min_y: 205 | return False 206 | return True 207 | 208 | def write_tiles(self, location, decode_type, mesh_max_error): 209 | self.calculate_tiles() 210 | terrain_level_loc = os.path.join(location, str(self.level)) 211 | if os.path.isdir(terrain_level_loc) is False: 212 | os.mkdir(terrain_level_loc) 213 | 214 | if self.is_compact is True: 215 | bundle_name = '{0}_{1}.bundle'.format(self.from_tile[0], self.from_tile[1]) 216 | bundle_file_path = os.path.join(terrain_level_loc, bundle_name) 217 | bundle_f = open(bundle_file_path, 'wb') 218 | while len(self.__tiles) > 0: 219 | tile = self.__tiles.pop(0) 220 | tile.encode(self.bundle_array, decode_type, mesh_max_error) 221 | header = struct.pack('<3i', tile.x, tile.y, len(tile.binary)) 222 | bundle_f.write(header) 223 | bundle_f.write(tile.binary) 224 | del tile 225 | bundle_f.close() 226 | else: 227 | while len(self.__tiles) > 0: 228 | tile = self.__tiles.pop(0) 229 | tile.encode_and_save(self.bundle_array, terrain_level_loc, decode_type, mesh_max_error) 230 | del tile 231 | -------------------------------------------------------------------------------- /pyterrainmaker/TerrainTile.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import struct 3 | import os 4 | import zlib 5 | from io import BytesIO 6 | 7 | import quantized_mesh_encoder 8 | from pydelatin import Delatin 9 | from pydelatin.util import rescale_positions 10 | 11 | 12 | class TerrainTile(object): 13 | 14 | def __init__(self, offset_x, offset_y, flag_child, tile_bounds, tile_resolution): 15 | self.child_flag = flag_child 16 | self.x_offset = offset_x 17 | self.y_offset = offset_y 18 | self.x = None 19 | self.y = None 20 | self.water_mask = 0x00 21 | self.source_array = None 22 | self.decode_type = 'heightmap' 23 | self.array = None 24 | self.fake = False 25 | self.binary = None 26 | # bounds: (minx, miny, maxx, maxy) 27 | self.bounds = tile_bounds 28 | self.resolution = tile_resolution 29 | 30 | def encode(self, in_buddle_array, decodetype='heightmap', mesh_max_error=0.01): 31 | self.source_array = in_buddle_array 32 | self.decode_type = decodetype 33 | 34 | if self.fake: 35 | if decodetype == 'heightmap': 36 | self.encode_fake_heightmap() 37 | else: 38 | self.encode_fake_mesh(mesh_max_error) 39 | else: 40 | self.array = self.source_array[self.y_offset:self.y_offset + 65, self.x_offset:self.x_offset + 65] 41 | if self.decode_type == 'heightmap': 42 | self.encode_heightmap() 43 | else: 44 | self.encode_mesh(mesh_max_error) 45 | 46 | def encode_heightmap(self): 47 | encode_array = (self.array + 1000) * 5 48 | encode_array_int = encode_array.astype(np.int16) 49 | encode_array_int = encode_array_int.flatten() 50 | 51 | encode_bytes = encode_array_int.tobytes(order='C') 52 | child_water_bytes = struct.pack(' self.__resolutions[0]: 86 | next_res = next_res / 2 87 | next_level += 1 88 | self.__levels[next_level] = next_res 89 | self.__max_level = next_level 90 | 91 | def __find_source_band(self, in_res): 92 | find_band_index = 0 93 | while find_band_index + 1 in self.__resolutions and self.__resolutions[find_band_index] < in_res: 94 | find_band_index += 1 95 | return self.__source_bands[find_band_index] 96 | 97 | def __write_bundle_info(self, loc): 98 | with open(os.path.join(loc, 'bundle.json'), 'w') as f: 99 | extent = { 100 | 'x_min': self.__minx, 101 | 'x_max': self.__maxx, 102 | 'y_min': self.__miny, 103 | 'y_max': self.__maxy 104 | } 105 | info = { 106 | 'width': 128, 107 | 'height': 128, 108 | 'extent': extent 109 | } 110 | json.dump(info, f) 111 | 112 | def __gen_avaliables(self): 113 | avaliables = [] 114 | for x in range(0, self.__max_level + 1): 115 | if x == 0: 116 | avaliables.append([{"startX": 0, "endX": 1, "startY": 0, "endY": 0}]) 117 | else: 118 | avaliables.append(self.__avaliables[x]) 119 | return avaliables 120 | 121 | def __write_config(self, loc, decode_type): 122 | layer_json = { 123 | "tilejson": "2.1.0", 124 | "version": "1.0.0", 125 | "scheme": "tms", 126 | "tiles": ["{z}/{x}/{y}.terrain"], 127 | "bounds": [self.__minx, self.__miny, self.__maxx, self.__maxy], 128 | "available": self.__gen_avaliables(), 129 | "minzoom": 0, 130 | "maxzoom": self.__max_level 131 | } 132 | if decode_type == 'heightmap': 133 | layer_json["format"] = "heightmap-1.0" 134 | else: 135 | layer_json["format"] = "quantized-mesh-1.0" 136 | layer_json["extensions"] = ["watermask", "octvertexnormals"] 137 | 138 | self.write_layer_json(loc, layer_json) 139 | 140 | def set_fill_raster(self, raster_loc): 141 | self.fill_raster = FillRaster(raster_loc) 142 | 143 | @staticmethod 144 | def write_layer_json(loc, layer_json): 145 | with open(os.path.join(loc, 'layer.json'), 'w') as f: 146 | f.write(json.dumps(layer_json, indent=4)) 147 | 148 | def generate_scheme(self): 149 | has_child = False 150 | for level in sorted(self.__levels.keys(), reverse=True): 151 | self.generate_bundles_by_level(level, has_child) 152 | has_child = True 153 | 154 | def generate_bundles_by_level(self, level, has_child): 155 | res = self.__levels[level] 156 | gg = GlobalGeodetic(True, 64) 157 | left_tx, top_ty = gg.LonLatToTile(self.__minx, self.__maxy, level) 158 | right_tx, bottom_ty = gg.LonLatToTile(self.__maxx, self.__miny, level) 159 | self.__avaliables[level] = [{"startX": left_tx, "endX": right_tx, "startY": bottom_ty, "endY": top_ty}] 160 | source_band = self.__find_source_band(res) 161 | top_ty1 = top_ty 162 | while left_tx <= right_tx: 163 | while top_ty >= bottom_ty: 164 | g_bundle = TerrainBundle(source_band, self.bundle_size, self.is_compact) 165 | g_bundle.level = level 166 | g_bundle.resolution = res 167 | g_bundle.from_tile = (left_tx, top_ty) 168 | g_bundle.no_data = self.source_no_data 169 | g_bundle.out_no_data = self.out_no_data 170 | g_bundle.has_next_level = has_child 171 | g_bundle.fill_raster = self.fill_raster 172 | g_bundle.data_band = source_band 173 | g_bundle.source_range = (self.__minx, self.__miny, self.__maxx, self.__maxy) 174 | self.bundles.append(g_bundle) 175 | top_ty -= self.bundle_size 176 | top_ty = top_ty1 177 | left_tx += self.bundle_size 178 | 179 | @staticmethod 180 | def fill_zero_level(out_loc, decode_type): 181 | base_path = os.path.join(out_loc, '0') 182 | first_path = os.path.join(base_path, '0') 183 | second_path = os.path.join(base_path, '1') 184 | first_t = os.path.join(base_path, '0', '0.terrain') 185 | second_t = os.path.join(base_path, '1', '0.terrain') 186 | 187 | module_dir = os.path.abspath(os.path.join(__file__, '..')) 188 | 189 | temp_0 = os.path.join(module_dir, 'data', 'mesh000.terrain') 190 | temp_1 = os.path.join(module_dir, 'data', 'mesh010.terrain') 191 | if decode_type == 'heightmap': 192 | temp_0 = os.path.join(module_dir, 'data', 'blank_heightmap.terrain') 193 | temp_1 = os.path.join(module_dir, 'data', 'blank_heightmap.terrain') 194 | if not os.path.exists(first_t): 195 | if not os.path.exists(first_path): 196 | os.mkdir(first_path) 197 | shutil.copyfile(temp_0, first_t) 198 | if not os.path.exists(second_t): 199 | if not os.path.exists(second_path): 200 | os.mkdir(second_path) 201 | shutil.copyfile(temp_1, second_t) 202 | 203 | def make_bundles(self, out_loc, decode_type='heightmap', mesh_max_error=0.01, thread_count=multiprocessing.cpu_count()): 204 | self.__write_config(out_loc, decode_type) 205 | if self.is_compact: 206 | self.__write_bundle_info(out_loc) 207 | 208 | print('Start generating tiles...') 209 | total = len(self.bundles) 210 | while len(self.bundles) > 0: 211 | bundle = self.bundles.pop(0) 212 | bundle.write_tiles(out_loc, decode_type, mesh_max_error) 213 | del bundle 214 | sys.stdout.flush() 215 | remains = len(self.bundles) + 0.0 216 | print(' {0:.0f}/{1} ({2:.0f}%)'.format(total - remains, total, (1.0 - remains/total)*100), end='\r') 217 | 218 | self.fill_zero_level(out_loc, decode_type) 219 | 220 | 221 | 222 | -------------------------------------------------------------------------------- /pyterrainmaker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vistawn/pyterrainmaker/29b741e67b76dfc1bf35b805271e15cab74c0b3f/pyterrainmaker/__init__.py -------------------------------------------------------------------------------- /pyterrainmaker/data/blank_heightmap.terrain: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vistawn/pyterrainmaker/29b741e67b76dfc1bf35b805271e15cab74c0b3f/pyterrainmaker/data/blank_heightmap.terrain -------------------------------------------------------------------------------- /pyterrainmaker/data/mesh000.terrain: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vistawn/pyterrainmaker/29b741e67b76dfc1bf35b805271e15cab74c0b3f/pyterrainmaker/data/mesh000.terrain -------------------------------------------------------------------------------- /pyterrainmaker/data/mesh010.terrain: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vistawn/pyterrainmaker/29b741e67b76dfc1bf35b805271e15cab74c0b3f/pyterrainmaker/data/mesh010.terrain -------------------------------------------------------------------------------- /pyterrainmaker/ecef.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | wgs84_a = 6378137.0 # Semi-major axis 4 | wgs84_b = 6356752.3142451793 # Semi-minor axis 5 | wgs84_e2 = 0.0066943799901975848 # First eccentricity squared 6 | wgs84_a2 = wgs84_a ** 2 7 | wgs84_b2 = wgs84_b ** 2 8 | radians_per_degree = math.pi / 180.0 9 | degree_per_radians = 180.0 / math.pi 10 | 11 | 12 | def LLH2ECEF(lon, lat, alt): 13 | lat *= radians_per_degree 14 | lon *= radians_per_degree 15 | 16 | def n(x): 17 | return wgs84_a / (math.sqrt(1 - wgs84_e2 * (math.sin(x) ** 2))) 18 | 19 | x = (n(lat) + alt) * math.cos(lat) * math.cos(lon) 20 | y = (n(lat) + alt) * math.cos(lat) * math.sin(lon) 21 | z = (n(lat) * (1 - wgs84_e2) + alt) * math.sin(lat) 22 | 23 | return [x, y, z] 24 | 25 | 26 | def ECEF2LLH(x, y, z): 27 | ep = math.sqrt((wgs84_a2 - wgs84_b2) / wgs84_b2) 28 | p = math.sqrt(x ** 2 + y ** 2) 29 | th = math.atan2(wgs84_a * z, wgs84_b * p) 30 | lon = math.atan2(y, x) 31 | lat = math.atan2( 32 | z + ep ** 2 * wgs84_b * math.sin(th) ** 3, 33 | p - wgs84_e2 * wgs84_a * math.cos(th) ** 3 34 | ) 35 | N = wgs84_a / math.sqrt(1 - wgs84_e2 * math.sin(lat) ** 2) 36 | alt = p / math.cos(lat) - N 37 | 38 | lon *= degree_per_radians 39 | lat *= degree_per_radians 40 | 41 | return [lon, lat, alt] 42 | 43 | # x = 4.2010e+06 44 | # y = 1.7246e+05 45 | # z = 4.7801e+06 46 | # ECEF2LLH(x, y, z) 47 | # 48 | # lat = 48.8567 49 | # lon = 2.3508 50 | # h = 80 51 | # LLH2ECEF(lon,lat,h) 52 | 53 | 54 | -------------------------------------------------------------------------------- /terrain_util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | import argparse 4 | import gzip 5 | import numpy 6 | import struct 7 | import sys 8 | import os 9 | from osgeo import gdal 10 | from osgeo import gdalconst 11 | from osgeo import osr 12 | 13 | from pyterrainmaker.GlobalGeodetic import GlobalGeodetic 14 | 15 | 16 | try: 17 | from osgeo import gdal 18 | except: 19 | print('gdal module is not found.') 20 | sys.exit(1) 21 | 22 | 23 | class Terrain_Util(object): 24 | 25 | def __init__(self): 26 | parser = argparse.ArgumentParser( 27 | description='utils for terrain file', 28 | usage='''terrain_util.py [] 29 | 30 | The most commonly used commands are: 31 | dt decode terrain file to GeoTiff 32 | da decode terrain file to ASCII 33 | ex explode a bundle file to single terrain files 34 | ''') 35 | parser.add_argument('command', help='Subcommand to run') 36 | args = parser.parse_args(sys.argv[1:2]) 37 | if not hasattr(self, args.command): 38 | print('Unrecognized command') 39 | parser.print_help() 40 | exit(1) 41 | getattr(self, args.command)() 42 | 43 | def dt(self): 44 | """decode as tif file""" 45 | parser = argparse.ArgumentParser( 46 | description='decode a terrain file to GeoTiff') 47 | parser.add_argument('z', action='store', type=int) 48 | parser.add_argument('x', action='store', type=int) 49 | parser.add_argument('y', action='store', type=int) 50 | parser.add_argument('in_file') 51 | parser.add_argument('out_loc', nargs='?', default='.') 52 | 53 | args = parser.parse_args(sys.argv[2:]) 54 | in_file = args.in_file 55 | out_loc = args.out_loc 56 | x = args.x 57 | y = args.y 58 | level = args.z 59 | 60 | with gzip.open(in_file, 'rb') as in_zip: 61 | terrain_buffer = in_zip.read() 62 | grid = self.decode_buffer(terrain_buffer) 63 | self.write_grid_to_tif(grid, out_loc, x, y, level) 64 | 65 | def da(self): 66 | """decode as ascii file""" 67 | parser = argparse.ArgumentParser( 68 | description='decode a terrain file to ASCII') 69 | parser.add_argument('in_terrain') 70 | parser.add_argument('out_ascii') 71 | 72 | args = parser.parse_args(sys.argv[2:]) 73 | in_file = args.in_terrain 74 | out_file = args.out_ascii 75 | 76 | with gzip.open(in_file, 'rb') as in_zip: 77 | terrain_buffer = in_zip.read() 78 | grid = self.decode_buffer(terrain_buffer) 79 | numpy.savetxt(out_file, grid, '%.1f') 80 | 81 | def ex(self): 82 | """explode a bundle file to single terrain files""" 83 | parser = argparse.ArgumentParser( 84 | description='explode a bundle file to single terrain files') 85 | parser.add_argument('in_bundle') 86 | parser.add_argument('-out_loc', help='output terrain files location', default='.') 87 | 88 | args = parser.parse_args(sys.argv[2:]) 89 | bundle_file = args.in_bundle 90 | out_loc = args.out_loc 91 | with open(bundle_file, 'rb') as b_f: 92 | header = b_f.read(12) 93 | while header: 94 | (tile_x, tile_y, tile_len) = struct.unpack('<3i', header) 95 | print('writing ', tile_x, tile_y) 96 | t_b = b_f.read(tile_len) 97 | filename = '{0}_{1}.terrain'.format(tile_x, tile_y) 98 | with open(os.path.join(out_loc, filename), 'wb') as t_f: 99 | t_f.write(t_b) 100 | header = b_f.read(12) 101 | 102 | def write_grid_to_tif(self, in_grid, out_loc, x, y, level): 103 | mem_drv = gdal.GetDriverByName('GTiff') 104 | out_tif_path = out_loc + '/' + str(x) + '_' + str(y) + '_' + str(level) + '.tif' 105 | out_ds = mem_drv.Create(out_tif_path, 65, 65, 1, gdalconst.GDT_Float32) 106 | out_band = out_ds.GetRasterBand(1) 107 | out_band.WriteArray(in_grid, 0, 0) 108 | trans = self.get_transfrom(x, y, level) 109 | out_ds.SetGeoTransform(trans) 110 | 111 | sr_84 = osr.SpatialReference() 112 | sr_84.ImportFromEPSG(4326) 113 | out_ds.SetProjection(sr_84.ExportToWkt()) 114 | out_ds = None 115 | 116 | def get_transfrom(self, tile_x, tile_y, level): 117 | gg = GlobalGeodetic(True, 64) 118 | (t_min_y, t_min_x, t_max_y, t_max_x) = gg.TileLatLonBounds(tile_x, tile_y, level) 119 | res = (t_max_x - t_min_x) / 64 120 | return t_min_x, res, 0.0, t_max_y, 0.0, -res 121 | 122 | def decode_buffer(self, terrain_buffer): 123 | n = numpy.frombuffer(terrain_buffer, dtype=numpy.int16) 124 | n1 = numpy.split(n, [4225]) 125 | des = n1[0].reshape(65, 65) 126 | des = (des / 5) - 1000 127 | return des 128 | 129 | 130 | if __name__ == '__main__': 131 | Terrain_Util() -------------------------------------------------------------------------------- /terrainmaker.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | import os 4 | import getopt 5 | 6 | from pyterrainmaker.TileScheme import TileScheme 7 | 8 | try: 9 | from osgeo import gdal 10 | except: 11 | print('gdal module is not found.') 12 | sys.exit(1) 13 | 14 | 15 | def check_tif(in_tif): 16 | if os.path.exists(in_tif) is False: 17 | return False, 'is not found.' 18 | 19 | in_ds = gdal.Open(in_tif, gdal.GA_ReadOnly) 20 | if in_ds.RasterXSize > 2000 or in_ds.RasterYSize > 2000: 21 | band = in_ds.GetRasterBand(1) 22 | ov_count = band.GetOverviewCount() 23 | if ov_count == 0: 24 | del band, in_ds 25 | return False, ' do not have any overview. ' 26 | del in_ds 27 | return True, None 28 | 29 | 30 | def check_loc(loc): 31 | if os.path.isdir(loc): 32 | if os.access(loc, os.W_OK): 33 | return True, None 34 | else: 35 | return False, ': no write permission' 36 | else: 37 | return False, 'is not a valid directory' 38 | 39 | 40 | def print_usage(): 41 | 42 | print(''' 43 | Usage: python terrainmaker.py [options] GDAL_DATASOURCE 44 | 45 | Options: 46 | -v, --version output program version 47 | -h, --help output help information 48 | -l, --fill fill nodata by another raster 49 | -o, --out_dir output directory for terrains 50 | -f, --format terrain format: heightmap/mesh, default is heightmap 51 | -e, --max_error maximum triangulation error (float [=0.001]) 52 | -m, --mode output storage mode: compact/single, default is single 53 | ''') 54 | 55 | 56 | def main(argv): 57 | 58 | try: 59 | opts, args = getopt.getopt(argv, "hvl:o:f:e:m:", ['help=', 'version=', 'fill=', 'out_dir=', 'format=', 'max_error=','mode=']) 60 | except getopt.GetoptError: 61 | print_usage() 62 | sys.exit(2) 63 | 64 | out_loc = '.' 65 | storage_mode = 'single' 66 | terrain_format = 'heightmap' 67 | fill_raster = None 68 | max_error = 0.01 69 | for opt, arg in opts: 70 | if opt == '-h': 71 | print_usage() 72 | sys.exit() 73 | elif opt in ('-o', '--out_dir'): 74 | out_loc = arg 75 | elif opt in ('-e', '--max_error'): 76 | max_error = arg 77 | elif opt in ('-v', '--verion'): 78 | print('1.0.0') 79 | sys.exit() 80 | elif opt in ('-l', '--fill'): 81 | status, msg = check_tif(arg) 82 | if status is False: 83 | print(arg, msg) 84 | print_usage() 85 | sys.exit() 86 | else: 87 | fill_raster = arg 88 | elif opt in ('-f', '--format'): 89 | if arg not in ['heightmap', 'mesh']: 90 | print('-f parameter is invalid.') 91 | print_usage() 92 | sys.exit() 93 | terrain_format = arg 94 | elif opt in ('-m', '--mode'): 95 | if arg not in ['single','compact']: 96 | print('-m parameter is invalid.') 97 | print_usage() 98 | sys.exit() 99 | storage_mode = arg 100 | 101 | if len(args) < 1: 102 | print('Error: The GDAL_DATASOURCE must be specified.') 103 | print('') 104 | print_usage() 105 | sys.exit(2) 106 | 107 | try: 108 | max_error = float(max_error) 109 | except Exception as identifier: 110 | print('max_error must be float type. [0 - 1]') 111 | sys.exit() 112 | 113 | in_tif = args[0] 114 | status, msg = check_tif(in_tif) 115 | if status is False: 116 | print(in_tif, msg) 117 | print_usage() 118 | sys.exit() 119 | 120 | status, msg = check_loc(out_loc) 121 | if status is False: 122 | print(out_loc, msg) 123 | print_usage() 124 | sys.exit() 125 | 126 | is_compact = True if storage_mode == 'compact' else False 127 | 128 | ts = TileScheme(in_tif, is_compact) 129 | ts.out_no_data = 0 130 | if fill_raster: 131 | ts.set_fill_raster(fill_raster) 132 | ts.generate_scheme() 133 | 134 | ts.make_bundles(out_loc, decode_type=terrain_format, mesh_max_error=max_error) 135 | print("\r\n done") 136 | 137 | 138 | if __name__ == '__main__': 139 | main(sys.argv[1:]) 140 | --------------------------------------------------------------------------------