├── requirements.txt ├── README.md ├── gui ├── gui.ui └── window.py └── gdal2mbtiles.py /requirements.txt: -------------------------------------------------------------------------------- 1 | GDAL==2.1.3 2 | Pillow==4.0.0 3 | PyQt4==4.11.4 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gdal2mbtiles: Python-based tools for creating OGC MBTiles. 2 | MBTiles Specification [[RU]](http://gis-lab.info/qa/mbtiles-spec.html), [[EN]](https://github.com/mapbox/mbtiles-spec) 3 | 4 | # Introduction 5 | 6 | * Combination of multiprocess [gdal2tiles](https://github.com/bolshoydi/gdal2tilesp) and [mbutil](https://github.com/mapbox/mbutil) 7 | in order to write tiles directly into Mbtiles database. 8 | * Writing tiles without transitional (temporary) storage on disk 9 | * Works in both ways: as CLI script and from interface. Just launch `window.py` from gui 10 | 11 | # Requirements 12 | 13 | * [GDAL 2.X.X](https://pypi.python.org/pypi/GDAL/) 14 | * [PIL\Pillow] (https://pypi.python.org/pypi/Pillow/4.0.0) 15 | * [PyQt4](https://pypi.python.org/pypi/PyQt4/4.11.4) 16 | 17 | # Basic Usage 18 | 19 | `python gdal2mbtiles.py input_file [options] -z min_zoom - maxzoom output.mbtiles` 20 | 21 | `gdal2mbtiles --help` to see list of available options: 22 | 23 | `--version` (Doesn't work) show program's version number and exit 24 | 25 | `-h, --help ` show this help message and exit 26 | 27 | `-p PROFILE, --profile=PROFILE` 28 | Tile cutting profile (mercator,geodetic,raster) - 29 | default 'mercator' (Google Maps compatible) 30 | 31 | `-r RESAMPLING, --resampling=RESAMPLING` 32 | Resampling method (average,near,bilinear,cubic,cubicsp 33 | line,lanczos,antialias) - default 'average' 34 | 35 | `-s SRS, --s_srs=SRS` The spatial reference system used for the source input 36 | data 37 | 38 | `-z ZOOM, --zoom=ZOOM` Zoom levels to render (format:'2-5' or '10'). 39 | 40 | `-e, --resume` Resume mode. Generate only missing files. 41 | 42 | `-a NODATA, --srcnodata=NODATA` 43 | NODATA transparency value to assign to the input data 44 | `--processes=PROCESSES` 45 | Number of concurrent processes (defaults to the number 46 | of cores in the system) 47 | 48 | `-v, --verbose` Print status messages to stdout 49 | 50 | 51 | ## Web viewer options: 52 | 53 | Options for generated HTML viewers a la Google Maps 54 | 55 | `-w WEBVIEWER, --webviewer=WEBVIEWER` 56 | Web viewer to generate 57 | (all,google,openlayers,leaflet,index,metadata,none) - 58 | default 'all' 59 | `-t TITLE, --title=TITLE` 60 | Title of the map 61 | `-c COPYRIGHT, --copyright=COPYRIGHT` 62 | Copyright for the map 63 | `-g GOOGLEKEY, --googlekey=GOOGLEKEY` 64 | Google Maps API key from 65 | http://code.google.com/apis/maps/signup.html 66 | `-y YAHOOKEY, --yahookey=YAHOOKEY` 67 | Yahoo Application ID from 68 | http://developer.yahoo.com/wsregapp/ 69 | ## Config options: 70 | 71 | Options for config parameters 72 | 73 | `-x, --auxfiles` Generate aux.xml files. 74 | 75 | `-f OUTPUT_FORMAT, --format=OUTPUT_FORMAT` 76 | Image format for output tiles. Just PNG and JPEG 77 | allowed. PNG is selected by default 78 | 79 | `-o OUTPUT_CACHE, --output=OUTPUT_CACHE` 80 | Format for output cache. Values allowed are tms and 81 | xyz, being xyz the default value 82 | 83 | 84 | # Example 85 | `gdal2mbtiles.py input.tif -z 12-14 -a 0 output.mbtiles` 86 | 87 | -------------------------------------------------------------------------------- /gui/gui.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 462 10 | 279 11 | 12 | 13 | 14 | MainWindow 15 | 16 | 17 | 18 | 19 | 20 | 10 21 | 20 22 | 111 23 | 21 24 | 25 | 26 | 27 | Input 28 | 29 | 30 | 31 | 32 | 33 | 10 34 | 50 35 | 111 36 | 21 37 | 38 | 39 | 40 | Output 41 | 42 | 43 | 44 | 45 | 46 | 170 47 | 90 48 | 42 49 | 22 50 | 51 | 52 | 53 | 54 | 55 | 56 | 110 57 | 90 58 | 42 59 | 22 60 | 61 | 62 | 63 | 64 | 65 | 66 | 10 67 | 150 68 | 111 69 | 22 70 | 71 | 72 | 73 | false 74 | 75 | 76 | 77 | 78 | 79 | 10 80 | 90 81 | 71 82 | 21 83 | 84 | 85 | 86 | Zoom levels 87 | 88 | 89 | 90 | 91 | 92 | 10 93 | 130 94 | 111 95 | 16 96 | 97 | 98 | 99 | Type of output tiles 100 | 101 | 102 | 103 | 104 | 105 | 70 106 | 190 107 | 191 108 | 31 109 | 110 | 111 | 112 | Start 113 | 114 | 115 | 116 | 117 | 118 | 200 119 | 150 120 | 131 121 | 22 122 | 123 | 124 | 125 | 126 | 127 | 128 | 200 129 | 130 130 | 121 131 | 16 132 | 133 | 134 | 135 | Type of output format 136 | 137 | 138 | 139 | 140 | 141 | 130 142 | 50 143 | 321 144 | 21 145 | 146 | 147 | 148 | QFrame::StyledPanel 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 130 158 | 20 159 | 321 160 | 21 161 | 162 | 163 | 164 | QFrame::StyledPanel 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 20 174 | 230 175 | 431 176 | 21 177 | 178 | 179 | 180 | 24 181 | 182 | 183 | 184 | 185 | 186 | 160 187 | 90 188 | 16 189 | 16 190 | 191 | 192 | 193 | - 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | -------------------------------------------------------------------------------- /gui/window.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import sys, os, time 3 | import logging 4 | from Queue import Queue 5 | from PyQt4 import uic as UI 6 | from PyQt4.QtCore import QThread 7 | from PyQt4.QtCore import pyqtSlot 8 | from PyQt4.QtCore import pyqtSignal 9 | from PyQt4 import QtCore, QtGui 10 | 11 | import gdal2mbtiles as g2m 12 | 13 | import gdal2mbtiles 14 | 15 | try: 16 | _fromUtf8 = QtCore.QString.fromUtf8 17 | except: 18 | _fromUtf8 = lambda s: s 19 | 20 | 21 | class ConvThread(QThread): 22 | """ 23 | Класс потока для выполнения операций конвертации. 24 | Методы: 25 | run() - запуск функции func 26 | stop() - принудительная остановка (использовать осторожно!) 27 | """ 28 | loop = pyqtSignal(object) 29 | 30 | def __init__(self, func): 31 | QThread.__init__(self) 32 | self.func = func 33 | 34 | def run(self): 35 | self.loop.emit(u'Петля') 36 | self.func() 37 | 38 | def stop(self): 39 | if self.isRunning(): 40 | self.exit() 41 | self.terminate() 42 | 43 | else: 44 | self.exit() 45 | self.quit() 46 | 47 | class MainWindow(): 48 | EXIT_CODE_REBOOT = -1234 49 | 50 | def __init__(self, version): 51 | self.version = version 52 | self.mw = UI.loadUi('gui.ui') 53 | 54 | self.mw.setWindowTitle(u'Tiler (Version ' + self.version + u')') 55 | self.mw.pushbutton_input.clicked.connect(self.set_input) 56 | self.mw.pushbutton_output.clicked.connect(self.set_output) 57 | # self.mw.pushbutton_start.clicked.connect(self.start) 58 | self.mw.pushbutton_start.clicked.connect(self.thread_start) 59 | 60 | self.native = QtGui.QCheckBox() 61 | self.native.setText("Use native file dialog.") 62 | self.native.setChecked(True) 63 | 64 | self.input_path = None 65 | self.output_path = None 66 | 67 | self.mw.spinbox_general.setRange(0, 21) 68 | 69 | # self.mw.combobox_type_input.addItems(['GEOTiff', 'VRT (Virtual Raster Table)']) 70 | self.mw.combobox_type_output_tiles.addItems(['JPG', 'PNG']) 71 | self.mw.combobox_type_output.addItems(['MBTILES', 'GEO Package', 'Tiles']) 72 | 73 | self.mw.progressBar.setMinimum(0) 74 | self.mw.progressBar.setMaximum(100) 75 | self.mw.progressBar.setValue(0) 76 | self.pbar_thread = QtCore.QThread() 77 | self.pbar = g2m.ProgressBar() 78 | 79 | self.pbar.pbar_signal.connect(self.handle_value_updated) 80 | 81 | # self.pbar.moveToThread(self.pbar_thread) 82 | # self.pbar_thread.connect(self.pbar, QtCore.SIGNAL('QtCore_QtObject()'), self.handle_value_updated) 83 | # self.pbar_thread.connect(self.pbar, QtCore.SIGNAL('2pbar_signal'), self.handle_value_updated) 84 | # self.pbar_thread.started.connect(self.handle_value_updated) 85 | # self.pbar_thread.start() 86 | 87 | def set_input(self): 88 | options = QtGui.QFileDialog.Options() 89 | if not self.native.isChecked(): 90 | options |= QtGui.QFileDialog.DontUseNativeDialog 91 | fileName = QtGui.QFileDialog.getOpenFileName(self.mw, 92 | "Input", 93 | str(self.mw.label_input.text().toUtf8()), 94 | ".tif (*.tif);;All Files (*)", "", options) 95 | 96 | if fileName: 97 | self.mw.label_input.setText(fileName) 98 | self.input_path = os.path.normpath(str(fileName.toUtf8())).decode('utf-8') 99 | 100 | def set_output(self): 101 | options = QtGui.QFileDialog.DontResolveSymlinks | QtGui.QFileDialog.DontUseNativeDialog 102 | fileName = QtGui.QFileDialog.getSaveFileName(self.mw, 103 | u"Output", 104 | # os.path.dirname(str(self.Label_RSC.text().toUtf8())), 105 | str(self.mw.label_output.text().toUtf8()), 106 | ".mbtiles(*.mbtiles);;All Files (*)", "",options) 107 | if fileName: 108 | self.mw.label_output.setText(fileName+'.mbtiles') 109 | self.output_path = os.path.normpath(str(fileName.toUtf8()+'.mbtiles')) 110 | 111 | @pyqtSlot() 112 | def start(self): 113 | self.mw.progressBar.setValue(0) 114 | general_zoom = self.mw.spinbox_general.value() 115 | overview_zoom = self.mw.spinbox_overview.value() 116 | # ans = 'y' if self.mw.checkbox_save_tiles.isChecked() else 'n' 117 | type_inputs = ['--resume --no-kml', '--s_srs ESPG:3857 --resume --no-kml'] 118 | # type_input = type_inputs[self.mw.combobox_type_input.currentIndex()] 119 | type_outputs_tiles = ['JPEG', 'PNG'] 120 | type_output_tiles = type_outputs_tiles[self.mw.combobox_type_output_tiles.currentIndex()] 121 | type_outputs = ['mbtiles', 'geopackage', 'tiles'] 122 | type_output = type_outputs_tiles[self.mw.combobox_type_output.currentIndex()] 123 | argv = 'gdal2mbtiles.py {} -z {} {}'.format(self.input_path, 124 | str(overview_zoom) + '-' + str(general_zoom), 125 | 126 | self.output_path).split(' ') 127 | 128 | g2m.main(self.pbar, argv) 129 | 130 | 131 | @pyqtSlot() 132 | def handle_value_updated(self, value): 133 | self.mw.progressBar.setValue(value) 134 | 135 | def thread_start(self): 136 | self.th = ConvThread(self.start) 137 | self.th.setTerminationEnabled(True) 138 | self.th.start() 139 | 140 | 141 | if __name__ == '__main__': 142 | app = QtGui.QApplication(sys.argv) 143 | mw = MainWindow('0.1.0') 144 | mw.mw.show() 145 | sys.exit(app.exec_()) -------------------------------------------------------------------------------- /gdal2mbtiles.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # ****************************************************************************** 4 | # $Id$ 5 | # 6 | # Project: Google Summer of Code 2007, 2008 (http://code.google.com/soc/) 7 | # Support: BRGM (http://www.brgm.fr) 8 | # Purpose: Convert a raster into TMS (Tile Map Service) tiles in a directory. 9 | # - generate Google Earth metadata (KML SuperOverlay) 10 | # - generate simple HTML viewer based on Google Maps and OpenLayers 11 | # - support of global tiles (Spherical Mercator) for compatibility 12 | # with interactive web maps a la Google Maps 13 | # Author: Klokan Petr Pridal, klokan at klokan dot cz 14 | # Web: http://www.klokan.cz/projects/GDAL2Mbtiles/ 15 | # GUI: http://www.maptiler.org/ 16 | # 17 | ############################################################################### 18 | # Copyright (c) 2008, Klokan Petr Pridal 19 | # 20 | # Permission is hereby granted, free of charge, to any person obtaining a 21 | # copy of this software and associated documentation files (the "Software"), 22 | # to deal in the Software without restriction, including without limitation 23 | # the rights to use, copy, modify, merge, publish, distribute, sublicense, 24 | # and/or sell copies of the Software, and to permit persons to whom the 25 | # Software is furnished to do so, subject to the following conditions: 26 | # 27 | # The above copyright notice and this permission notice shall be included 28 | # in all copies or substantial portions of the Software. 29 | # 30 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 31 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 32 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 33 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 34 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 35 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 36 | # DEALINGS IN THE SOFTWARE. 37 | # ****************************************************************************** 38 | import signal, sys 39 | import time 40 | import io 41 | import os 42 | import json 43 | from PyQt4.QtCore import pyqtSlot 44 | from PyQt4 import QtCore 45 | 46 | if getattr(sys, 'frozen', False): 47 | app_path = os.path.dirname(sys.executable) 48 | lib_dir = os.path.join(os.path.dirname(sys.executable), 'lib').decode('cp1251').encode('utf-8') 49 | elif __file__: 50 | app_path = os.path.dirname(__file__) 51 | lib_dir = os.path.normpath(os.path.join(os.path.abspath(os.path.dirname(__file__)), '..', 'lib')) 52 | 53 | # Uncomment to use local GDAL 54 | environ_list = os.environ['PATH'].split(';') 55 | environ_list.insert(0, os.path.join(lib_dir, 'GDAL')) 56 | environ_list.insert(0, os.path.join(os.getcwd(), 'lib/Python27/Scripts')) 57 | environ_list.insert(0, os.path.join(os.getcwd(), 'lib/Python27')) 58 | os.environ['PATH'] = ';'.join(environ_list) 59 | os.environ['GDAL_DRIVER_PATH'] = os.path.join(lib_dir, '/GDAL', 'gdalplugins') 60 | os.environ['GDAL_DATA'] = os.path.join(lib_dir, 'GDAL', 'gdal-data') 61 | os.environ['PROJ_LIB'] = os.path.join(lib_dir, 'GDAL', 'projlib') 62 | os.environ['PROJ_DEBUG'] = 'ON' 63 | 64 | try: 65 | from osgeo import gdal 66 | from osgeo import osr 67 | except: 68 | import gdal 69 | 70 | print('You are using "old gen" bindings. GDAL2Mbtiles needs "new gen" bindings.') 71 | sys.exit(1) 72 | 73 | import sqlite3 74 | import math 75 | 76 | try: 77 | from PIL import Image 78 | import numpy 79 | import osgeo.gdal_array as gdalarray 80 | except: 81 | # 'antialias' resampling is not available 82 | pass 83 | 84 | import multiprocessing 85 | import traceback 86 | import tempfile 87 | from optparse import OptionParser, OptionGroup 88 | 89 | __version__ = "$Id$" 90 | 91 | resampling_list = ('average', 'near', 'bilinear', 'cubic', 'cubicspline', 'lanczos', 'antialias') 92 | profile_list = ('mercator', 'geodetic', 'raster') # ,'zoomify') 93 | webviewer_list = ('all', 'google', 'openlayers', 'leaflet', 'index', 'metadata', 'none') 94 | tcount = 0 95 | # ============================================================================= 96 | # ============================================================================= 97 | # ============================================================================= 98 | 99 | __doc__globalmaptiles = """ 100 | globalmaptiles.py 101 | 102 | Global Map Tiles as defined in Tile Map Service (TMS) Profiles 103 | ============================================================== 104 | 105 | Functions necessary for generation of global tiles used on the web. 106 | It contains classes implementing coordinate conversions for: 107 | 108 | - GlobalMercator (based on EPSG:900913 = EPSG:3785) 109 | for Google Maps, Yahoo Maps, Microsoft Maps compatible tiles 110 | - GlobalGeodetic (based on EPSG:4326) 111 | for OpenLayers Base Map and Google Earth compatible tiles 112 | 113 | More info at: 114 | 115 | http://wiki.osgeo.org/wiki/Tile_Map_Service_Specification 116 | http://wiki.osgeo.org/wiki/WMS_Tiling_Client_Recommendation 117 | http://msdn.microsoft.com/en-us/library/bb259689.aspx 118 | http://code.google.com/apis/maps/documentation/overlays.html#Google_Maps_Coordinates 119 | 120 | Created by Klokan Petr Pridal on 2008-07-03. 121 | Google Summer of Code 2008, project GDAL2Mbtiles for OSGEO. 122 | 123 | In case you use this class in your product, translate it to another language 124 | or find it usefull for your project please let me know. 125 | My email: klokan at klokan dot cz. 126 | I would like to know where it was used. 127 | 128 | Class is available under the open-source GDAL license (www.gdal.org). 129 | """ 130 | 131 | MAXZOOMLEVEL = 32 132 | 133 | 134 | class GlobalMercator(object): 135 | """ 136 | TMS Global Mercator Profile 137 | --------------------------- 138 | 139 | Functions necessary for generation of tiles in Spherical Mercator projection, 140 | EPSG:900913 (EPSG:gOOglE, Google Maps Global Mercator), EPSG:3785, OSGEO:41001. 141 | 142 | Such tiles are compatible with Google Maps, Microsoft Virtual Earth, Yahoo Maps, 143 | UK Ordnance Survey OpenSpace API, ... 144 | and you can overlay them on top of base maps of those web mapping applications. 145 | 146 | Pixel and tile coordinates are in TMS notation (origin [0,0] in bottom-left). 147 | 148 | What coordinate conversions do we need for TMS Global Mercator tiles:: 149 | 150 | LatLon <-> Meters <-> Pixels <-> Tile 151 | 152 | WGS84 coordinates Spherical Mercator Pixels in pyramid Tiles in pyramid 153 | lat/lon XY in metres XY pixels Z zoom XYZ from TMS 154 | EPSG:4326 EPSG:900913 155 | .----. --------- -- TMS 156 | / \ <-> | | <-> /----/ <-> Google 157 | \ / | | /--------/ QuadTree 158 | ----- --------- /------------/ 159 | KML, public WebMapService Web Clients TileMapService 160 | 161 | What is the coordinate extent of Earth in EPSG:900913? 162 | 163 | [-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244] 164 | Constant 20037508.342789244 comes from the circumference of the Earth in meters, 165 | which is 40 thousand kilometers, the coordinate origin is in the middle of extent. 166 | In fact you can calculate the constant as: 2 * math.pi * 6378137 / 2.0 167 | $ echo 180 85 | gdaltransform -s_srs EPSG:4326 -t_srs EPSG:900913 168 | Polar areas with abs(latitude) bigger then 85.05112878 are clipped off. 169 | 170 | What are zoom level constants (pixels/meter) for pyramid with EPSG:900913? 171 | 172 | whole region is on top of pyramid (zoom=0) covered by 256x256 pixels tile, 173 | every lower zoom level resolution is always divided by two 174 | initialResolution = 20037508.342789244 * 2 / 256 = 156543.03392804062 175 | 176 | What is the difference between TMS and Google Maps/QuadTree tile name convention? 177 | 178 | The tile raster itself is the same (equal extent, projection, pixel size), 179 | there is just different identification of the same raster tile. 180 | Tiles in TMS are counted from [0,0] in the bottom-left corner, id is XYZ. 181 | Google placed the origin [0,0] to the top-left corner, reference is XYZ. 182 | Microsoft is referencing tiles by a QuadTree name, defined on the website: 183 | http://msdn2.microsoft.com/en-us/library/bb259689.aspx 184 | 185 | The lat/lon coordinates are using WGS84 datum, yeh? 186 | 187 | Yes, all lat/lon we are mentioning should use WGS84 Geodetic Datum. 188 | Well, the web clients like Google Maps are projecting those coordinates by 189 | Spherical Mercator, so in fact lat/lon coordinates on sphere are treated as if 190 | the were on the WGS84 ellipsoid. 191 | 192 | From MSDN documentation: 193 | To simplify the calculations, we use the spherical form of projection, not 194 | the ellipsoidal form. Since the projection is used only for map display, 195 | and not for displaying numeric coordinates, we don't need the extra precision 196 | of an ellipsoidal projection. The spherical projection causes approximately 197 | 0.33 percent scale distortion in the Y direction, which is not visually noticable. 198 | 199 | How do I create a raster in EPSG:900913 and convert coordinates with PROJ.4? 200 | 201 | You can use standard GIS tools like gdalwarp, cs2cs or gdaltransform. 202 | All of the tools supports -t_srs 'epsg:900913'. 203 | 204 | For other GIS programs check the exact definition of the projection: 205 | More info at http://spatialreference.org/ref/user/google-projection/ 206 | The same projection is degined as EPSG:3785. WKT definition is in the official 207 | EPSG database. 208 | 209 | Proj4 Text: 210 | +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 211 | +k=1.0 +units=m +nadgrids=@null +no_defs 212 | 213 | Human readable WKT format of EPGS:900913: 214 | PROJCS["Google Maps Global Mercator", 215 | GEOGCS["WGS 84", 216 | DATUM["WGS_1984", 217 | SPHEROID["WGS 84",6378137,298.257223563, 218 | AUTHORITY["EPSG","7030"]], 219 | AUTHORITY["EPSG","6326"]], 220 | PRIMEM["Greenwich",0], 221 | UNIT["degree",0.0174532925199433], 222 | AUTHORITY["EPSG","4326"]], 223 | PROJECTION["Mercator_1SP"], 224 | PARAMETER["central_meridian",0], 225 | PARAMETER["scale_factor",1], 226 | PARAMETER["false_easting",0], 227 | PARAMETER["false_northing",0], 228 | UNIT["metre",1, 229 | AUTHORITY["EPSG","9001"]]] 230 | """ 231 | 232 | def __init__(self, tileSize=256): 233 | "Initialize the TMS Global Mercator pyramid" 234 | self.tileSize = tileSize 235 | self.initialResolution = 2 * math.pi * 6378137 / self.tileSize 236 | # 156543.03392804062 for tileSize 256 pixels 237 | self.originShift = 2 * math.pi * 6378137 / 2.0 238 | 239 | # 20037508.342789244 240 | 241 | def LatLonToMeters(self, lat, lon): 242 | "Converts given lat/lon in WGS84 Datum to XY in Spherical Mercator EPSG:900913" 243 | 244 | mx = lon * self.originShift / 180.0 245 | my = math.log(math.tan((90 + lat) * math.pi / 360.0)) / (math.pi / 180.0) 246 | 247 | my = my * self.originShift / 180.0 248 | return mx, my 249 | 250 | def MetersToLatLon(self, mx, my): 251 | "Converts XY point from Spherical Mercator EPSG:900913 to lat/lon in WGS84 Datum" 252 | 253 | lon = (mx / self.originShift) * 180.0 254 | lat = (my / self.originShift) * 180.0 255 | 256 | lat = 180 / math.pi * (2 * math.atan(math.exp(lat * math.pi / 180.0)) - math.pi / 2.0) 257 | return lat, lon 258 | 259 | def PixelsToMeters(self, px, py, zoom): 260 | "Converts pixel coordinates in given zoom level of pyramid to EPSG:900913" 261 | 262 | res = self.Resolution(zoom) 263 | mx = px * res - self.originShift 264 | my = py * res - self.originShift 265 | return mx, my 266 | 267 | def MetersToPixels(self, mx, my, zoom): 268 | "Converts EPSG:900913 to pyramid pixel coordinates in given zoom level" 269 | 270 | res = self.Resolution(zoom) 271 | px = (mx + self.originShift) / res 272 | py = (my + self.originShift) / res 273 | return px, py 274 | 275 | def PixelsToTile(self, px, py): 276 | "Returns a tile covering region in given pixel coordinates" 277 | 278 | tx = int(math.ceil(px / float(self.tileSize)) - 1) 279 | ty = int(math.ceil(py / float(self.tileSize)) - 1) 280 | return tx, ty 281 | 282 | def PixelsToRaster(self, px, py, zoom): 283 | "Move the origin of pixel coordinates to top-left corner" 284 | 285 | mapSize = self.tileSize << zoom 286 | return px, mapSize - py 287 | 288 | def MetersToTile(self, mx, my, zoom): 289 | "Returns tile for given mercator coordinates" 290 | 291 | px, py = self.MetersToPixels(mx, my, zoom) 292 | return self.PixelsToTile(px, py) 293 | 294 | def TileBounds(self, tx, ty, zoom): 295 | "Returns bounds of the given tile in EPSG:900913 coordinates" 296 | 297 | minx, miny = self.PixelsToMeters(tx * self.tileSize, ty * self.tileSize, zoom) 298 | maxx, maxy = self.PixelsToMeters((tx + 1) * self.tileSize, (ty + 1) * self.tileSize, zoom) 299 | return (minx, miny, maxx, maxy) 300 | 301 | def TileLatLonBounds(self, tx, ty, zoom): 302 | "Returns bounds of the given tile in latutude/longitude using WGS84 datum" 303 | 304 | bounds = self.TileBounds(tx, ty, zoom) 305 | minLat, minLon = self.MetersToLatLon(bounds[0], bounds[1]) 306 | maxLat, maxLon = self.MetersToLatLon(bounds[2], bounds[3]) 307 | 308 | return (minLat, minLon, maxLat, maxLon) 309 | 310 | def Resolution(self, zoom): 311 | "Resolution (meters/pixel) for given zoom level (measured at Equator)" 312 | 313 | # return (2 * math.pi * 6378137) / (self.tileSize * 2**zoom) 314 | return self.initialResolution / (2 ** zoom) 315 | 316 | def ZoomForPixelSize(self, pixelSize): 317 | "Maximal scaledown zoom of the pyramid closest to the pixelSize." 318 | 319 | for i in range(MAXZOOMLEVEL): 320 | if pixelSize > self.Resolution(i): 321 | if i != 0: 322 | return i - 1 323 | else: 324 | return 0 # We don't want to scale up 325 | 326 | def GoogleTile(self, tx, ty, zoom): 327 | "Converts TMS tile coordinates to Google Tile coordinates" 328 | 329 | # coordinate origin is moved from bottom-left to top-left corner of the extent 330 | return tx, (2 ** zoom - 1) - ty 331 | 332 | def QuadTree(self, tx, ty, zoom): 333 | "Converts TMS tile coordinates to Microsoft QuadTree" 334 | 335 | quadKey = "" 336 | ty = (2 ** zoom - 1) - ty 337 | for i in range(zoom, 0, -1): 338 | digit = 0 339 | mask = 1 << (i - 1) 340 | if (tx & mask) != 0: 341 | digit += 1 342 | if (ty & mask) != 0: 343 | digit += 2 344 | quadKey += str(digit) 345 | 346 | return quadKey 347 | 348 | 349 | # --------------------- 350 | 351 | class GlobalGeodetic(object): 352 | """ 353 | TMS Global Geodetic Profile 354 | --------------------------- 355 | 356 | Functions necessary for generation of global tiles in Plate Carre projection, 357 | EPSG:4326, "unprojected profile". 358 | 359 | Such tiles are compatible with Google Earth (as any other EPSG:4326 rasters) 360 | and you can overlay the tiles on top of OpenLayers base map. 361 | 362 | Pixel and tile coordinates are in TMS notation (origin [0,0] in bottom-left). 363 | 364 | What coordinate conversions do we need for TMS Global Geodetic tiles? 365 | 366 | Global Geodetic tiles are using geodetic coordinates (latitude,longitude) 367 | directly as planar coordinates XY (it is also called Unprojected or Plate 368 | Carre). We need only scaling to pixel pyramid and cutting to tiles. 369 | Pyramid has on top level two tiles, so it is not square but rectangle. 370 | Area [-180,-90,180,90] is scaled to 512x256 pixels. 371 | TMS has coordinate origin (for pixels and tiles) in bottom-left corner. 372 | Rasters are in EPSG:4326 and therefore are compatible with Google Earth. 373 | 374 | LatLon <-> Pixels <-> Tiles 375 | 376 | WGS84 coordinates Pixels in pyramid Tiles in pyramid 377 | lat/lon XY pixels Z zoom XYZ from TMS 378 | EPSG:4326 379 | .----. ---- 380 | / \ <-> /--------/ <-> TMS 381 | \ / /--------------/ 382 | ----- /--------------------/ 383 | WMS, KML Web Clients, Google Earth TileMapService 384 | """ 385 | 386 | def __init__(self, tileSize=256): 387 | self.tileSize = tileSize 388 | 389 | def LatLonToPixels(self, lat, lon, zoom): 390 | "Converts lat/lon to pixel coordinates in given zoom of the EPSG:4326 pyramid" 391 | 392 | res = 180.0 / self.tileSize / 2 ** zoom 393 | px = (180 + lat) / res 394 | py = (90 + lon) / res 395 | return px, py 396 | 397 | def PixelsToTile(self, px, py): 398 | "Returns coordinates of the tile covering region in pixel coordinates" 399 | 400 | tx = int(math.ceil(px / float(self.tileSize)) - 1) 401 | ty = int(math.ceil(py / float(self.tileSize)) - 1) 402 | return tx, ty 403 | 404 | def LatLonToTile(self, lat, lon, zoom): 405 | "Returns the tile for zoom which covers given lat/lon coordinates" 406 | 407 | px, py = self.LatLonToPixels(lat, lon, zoom) 408 | return self.PixelsToTile(px, py) 409 | 410 | def Resolution(self, zoom): 411 | "Resolution (arc/pixel) for given zoom level (measured at Equator)" 412 | 413 | return 180.0 / self.tileSize / 2 ** zoom 414 | 415 | # return 180 / float( 1 << (8+zoom) ) 416 | 417 | def ZoomForPixelSize(self, pixelSize): 418 | "Maximal scaledown zoom of the pyramid closest to the pixelSize." 419 | 420 | for i in range(MAXZOOMLEVEL): 421 | if pixelSize > self.Resolution(i): 422 | if i != 0: 423 | return i - 1 424 | else: 425 | return 0 # We don't want to scale up 426 | 427 | def TileBounds(self, tx, ty, zoom): 428 | "Returns bounds of the given tile" 429 | res = 180.0 / self.tileSize / 2 ** zoom 430 | return ( 431 | tx * self.tileSize * res - 180, 432 | ty * self.tileSize * res - 90, 433 | (tx + 1) * self.tileSize * res - 180, 434 | (ty + 1) * self.tileSize * res - 90 435 | ) 436 | 437 | def TileLatLonBounds(self, tx, ty, zoom): 438 | "Returns bounds of the given tile in the SWNE form" 439 | b = self.TileBounds(tx, ty, zoom) 440 | return (b[1], b[0], b[3], b[2]) 441 | 442 | 443 | # --------------------- 444 | # TODO: Finish Zoomify implemtentation!!! 445 | class Zoomify(object): 446 | """ 447 | Tiles compatible with the Zoomify viewer 448 | ---------------------------------------- 449 | """ 450 | 451 | def __init__(self, width, height, tilesize=256, tileformat='jpg'): 452 | """Initialization of the Zoomify tile tree""" 453 | 454 | self.tilesize = tilesize 455 | self.tileformat = tileformat 456 | imagesize = (width, height) 457 | tiles = (math.ceil(width / tilesize), math.ceil(height / tilesize)) 458 | 459 | # Size (in tiles) for each tier of pyramid. 460 | self.tierSizeInTiles = [] 461 | self.tierSizeInTiles.push(tiles) 462 | 463 | # Image size in pixels for each pyramid tierself 464 | self.tierImageSize = [] 465 | self.tierImageSize.append(imagesize); 466 | 467 | while (imagesize[0] > tilesize or imagesize[1] > tilesize): 468 | imagesize = (math.floor(imagesize[0] / 2), math.floor(imagesize[1] / 2)) 469 | tiles = (math.ceil(imagesize[0] / tilesize), math.ceil(imagesize[1] / tilesize)) 470 | self.tierSizeInTiles.append(tiles) 471 | self.tierImageSize.append(imagesize) 472 | 473 | self.tierSizeInTiles.reverse() 474 | self.tierImageSize.reverse() 475 | 476 | # Depth of the Zoomify pyramid, number of tiers (zoom levels) 477 | self.numberOfTiers = len(self.tierSizeInTiles) 478 | 479 | # Number of tiles up to the given tier of pyramid. 480 | self.tileCountUpToTier = [] 481 | self.tileCountUpToTier[0] = 0 482 | for i in range(1, self.numberOfTiers + 1): 483 | self.tileCountUpToTier.append( 484 | self.tierSizeInTiles[i - 1][0] * self.tierSizeInTiles[i - 1][1] + self.tileCountUpToTier[i - 1] 485 | ) 486 | 487 | def tilefilename(self, x, y, z): 488 | """Returns filename for tile with given coordinates""" 489 | 490 | tileIndex = x + y * self.tierSizeInTiles[z][0] + self.tileCountUpToTier[z] 491 | return os.path.join("TileGroup%.0f" % math.floor(tileIndex / 256), 492 | "%s-%s-%s.%s" % (z, x, y, self.tileformat)) 493 | 494 | 495 | # ============================================================================= 496 | # ============================================================================= 497 | # ============================================================================= 498 | 499 | class GDAL2Mbtiles(object): 500 | """Class for generating .mbtiles form raster based on GDAl, sqlite3 501 | order of main steps: 502 | open_input() 503 | generate_metadata() 504 | generate_base_tiles() 505 | generate_overview_tiles()""" 506 | 507 | # ------------------------------------------------------------------------- 508 | def error(self, msg, details=""): 509 | """Print an error message and stop the processing""" 510 | 511 | if details: 512 | self.parser.error(msg + "\n\n" + details) 513 | else: 514 | self.parser.error(msg) 515 | 516 | # ------------------------------------------------------------------------- 517 | def progressbar(self, complete=0.0): 518 | """Print progressbar for float value 0..1""" 519 | 520 | gdal.TermProgress_nocb(complete) 521 | 522 | # ------------------------------------------------------------------------- 523 | 524 | 525 | # ------------------------------------------------------------------------- 526 | def stop(self): 527 | """Stop the rendering immediately""" 528 | self.stopped = True 529 | 530 | # ------------------------------------------------------------------------- 531 | def __init__(self, arguments): 532 | """Constructor function - initialization""" 533 | 534 | self.stopped = False 535 | self.input = None 536 | self.output = None 537 | 538 | # Tile format 539 | 540 | self.tilesize = 256 541 | 542 | # Should we read bigger window of the input raster and scale it down? 543 | # Note: Modified leter by open_input() 544 | # Not for 'near' resampling 545 | # Not for Wavelet based drivers (JPEG2000, ECW, MrSID) 546 | # Not for 'raster' profile 547 | self.scaledquery = True 548 | # How big should be query window be for scaling down 549 | # Later on reset according the chosen resampling algorightm 550 | self.querysize = 4 * self.tilesize 551 | 552 | # Should we use Read on the input file for generating overview tiles? 553 | # Note: Modified later by open_input() 554 | # Otherwise the overview tiles are generated from existing underlying tiles 555 | self.overviewquery = False 556 | 557 | # RUN THE ARGUMENT PARSER: 558 | 559 | self.optparse_init() 560 | self.options, self.args = self.parser.parse_args(args=arguments) 561 | if not self.args: 562 | self.error("No input file specified") 563 | 564 | # POSTPROCESSING OF PARSED ARGUMENTS: 565 | 566 | if self.options.output_format == 'JPEG': 567 | self.tiledriver = 'JPEG' 568 | self.tileext = 'jpg' 569 | 570 | elif self.options.output_format == 'PNG': 571 | self.tiledriver = 'PNG' 572 | self.tileext = 'png' 573 | 574 | else: 575 | self.error("Output formats allowed are PNG and JPEG") 576 | 577 | if self.options.output_cache not in ('tms', 'xyz'): 578 | self.error("Accepted formats for output cache are 'xyz' or 'tms'") 579 | 580 | # Workaround for old versions of GDAL 581 | try: 582 | if (self.options.verbose and self.options.resampling == 'near') or gdal.TermProgress_nocb: 583 | pass 584 | except: 585 | self.error("This version of GDAL is not supported. Please upgrade to 1.6+.") 586 | # ,"You can try run crippled version of GDAL2Mbtiles with parameters: -v -r 'near'") 587 | 588 | # Is output directory the last argument? 589 | 590 | # Test output directory, if it doesn't exist 591 | # if os.path.isdir(self.args[-1]) or (len(self.args) > 1 and not os.path.exists(self.args[-1])): 592 | self.output = self.args[-1] 593 | self.args = self.args[:-1] 594 | # More files on the input not directly supported yet 595 | 596 | if (len(self.args) > 1): 597 | self.error("Processing of several input files is not supported.", 598 | """Please first use a tool like gdal_vrtmerge.py or gdal_merge.py on the files: 599 | gdal_vrtmerge.py -o merged.vrt %s""" % " ".join(self.args)) 600 | # TODO: Call functions from gdal_vrtmerge.py directly 601 | 602 | self.input = self.args[0] 603 | 604 | # Default values for not given options 605 | 606 | if not self.output: 607 | # Directory with input filename without extension in actual directory 608 | self.output = os.path.splitext(os.path.basename(self.input))[0] 609 | 610 | if not self.options.title: 611 | self.options.title = os.path.basename(self.input) 612 | 613 | if self.options.url and not self.options.url.endswith('/'): 614 | self.options.url += '/' 615 | if self.options.url: 616 | self.options.url += os.path.basename(self.output) + '/' 617 | 618 | # Supported options 619 | 620 | self.resampling = None 621 | 622 | if self.options.resampling == 'average': 623 | try: 624 | if gdal.RegenerateOverview: 625 | pass 626 | except: 627 | self.error("'average' resampling algorithm is not available.", 628 | "Please use -r 'near' argument or upgrade to newer version of GDAL.") 629 | 630 | elif self.options.resampling == 'antialias': 631 | try: 632 | if numpy: 633 | pass 634 | except: 635 | self.error("'antialias' resampling algorithm is not available.", 636 | "Install PIL (Python Imaging Library) and numpy.") 637 | 638 | elif self.options.resampling == 'near': 639 | self.resampling = gdal.GRA_NearestNeighbour 640 | self.querysize = self.tilesize 641 | 642 | elif self.options.resampling == 'bilinear': 643 | self.resampling = gdal.GRA_Bilinear 644 | self.querysize = self.tilesize * 2 645 | 646 | elif self.options.resampling == 'cubic': 647 | self.resampling = gdal.GRA_Cubic 648 | 649 | elif self.options.resampling == 'cubicspline': 650 | self.resampling = gdal.GRA_CubicSpline 651 | 652 | elif self.options.resampling == 'lanczos': 653 | self.resampling = gdal.GRA_Lanczos 654 | 655 | # User specified zoom levels 656 | self.tminz = None 657 | self.tmaxz = None 658 | if self.options.zoom: 659 | minmax = self.options.zoom.split('-', 1) 660 | minmax.extend(['']) 661 | min, max = minmax[:2] 662 | self.tminz = int(min) 663 | if max: 664 | self.tmaxz = int(max) 665 | else: 666 | self.tmaxz = int(min) 667 | 668 | # KML generation 669 | self.kml = self.options.kml 670 | 671 | # Output the results 672 | 673 | if self.options.verbose: 674 | print("Options:", self.options) 675 | print("Input:", self.input) 676 | print("Output:", self.output) 677 | print("Cache: %s MB" % (gdal.GetCacheMax() / 1024 / 1024)) 678 | print('') 679 | 680 | # ------------------------------------------------------------------------- 681 | def optparse_init(self): 682 | """Prepare the option parser for input (argv)""" 683 | 684 | usage = "Usage: %prog [options] input_file(s) [output]" 685 | p = OptionParser(usage, version="%prog " + __version__) 686 | p.add_option("-p", "--profile", dest='profile', type='choice', choices=profile_list, 687 | help="Tile cutting profile (%s) - default 'mercator' (Google Maps compatible)" % ",".join( 688 | profile_list)) 689 | p.add_option("-r", "--resampling", dest="resampling", type='choice', choices=resampling_list, 690 | help="Resampling method (%s) - default 'average'" % ",".join(resampling_list)) 691 | p.add_option('-s', '--s_srs', dest="s_srs", metavar="SRS", 692 | help="The spatial reference system used for the source input data") 693 | p.add_option('-z', '--zoom', dest="zoom", 694 | help="Zoom levels to render (format:'2-5' or '10').") 695 | p.add_option('-e', '--resume', dest="resume", action="store_true", 696 | help="Resume mode. Generate only missing files.") 697 | p.add_option('-a', '--srcnodata', dest="srcnodata", metavar="NODATA", 698 | help="NODATA transparency value to assign to the input data") 699 | p.add_option('--processes', dest='processes', type='int', default=multiprocessing.cpu_count(), 700 | help='Number of concurrent processes (defaults to the number of cores in the system)') 701 | p.add_option("-v", "--verbose", 702 | action="store_true", dest="verbose", 703 | help="Print status messages to stdout") 704 | 705 | # KML options 706 | g = OptionGroup(p, "KML (Google Earth) options", "Options for generated Google Earth SuperOverlay metadata") 707 | g.add_option("-k", "--force-kml", dest='kml', action="store_true", 708 | help="Generate KML for Google Earth - default for 'geodetic' profile and 'raster' in EPSG:4326. For a dataset with different projection use with caution!") 709 | g.add_option("-n", "--no-kml", dest='kml', action="store_false", 710 | help="Avoid automatic generation of KML files for EPSG:4326") 711 | g.add_option("-u", "--url", dest='url', 712 | help="URL address where the generated tiles are going to be published") 713 | p.add_option_group(g) 714 | 715 | # HTML options 716 | g = OptionGroup(p, "Web viewer options", "Options for generated HTML viewers a la Google Maps") 717 | g.add_option("-w", "--webviewer", dest='webviewer', type='choice', choices=webviewer_list, 718 | help="Web viewer to generate (%s) - default 'all'" % ",".join(webviewer_list)) 719 | g.add_option("-t", "--title", dest='title', 720 | help="Title of the map") 721 | g.add_option("-c", "--copyright", dest='copyright', 722 | help="Copyright for the map") 723 | g.add_option("-g", "--googlekey", dest='googlekey', 724 | help="Google Maps API key from http://code.google.com/apis/maps/signup.html") 725 | g.add_option("-y", "--yahookey", dest='yahookey', 726 | help="Yahoo Application ID from http://developer.yahoo.com/wsregapp/") 727 | p.add_option_group(g) 728 | 729 | # Config options 730 | g = OptionGroup(p, "Config options", "Options for config parameters") 731 | g.add_option("-x", "--auxfiles", dest='aux_files', action='store_true', 732 | help="Generate aux.xml files.") 733 | g.add_option("-f", "--format", dest="output_format", 734 | help="Image format for output tiles. Just PNG and JPEG allowed. PNG is selected by default") 735 | g.add_option("-o", "--output", dest="output_cache", 736 | help="Format for output cache. Values allowed are tms and xyz, being xyz the default value") 737 | p.add_option_group(g) 738 | 739 | # TODO: MapFile + TileIndexes per zoom level for efficient MapServer WMS 740 | # g = OptionGroup(p, "WMS MapServer metadata", "Options for generated mapfile and tileindexes for MapServer") 741 | # g.add_option("-i", "--tileindex", dest='wms', action="store_true" 742 | # help="Generate tileindex and mapfile for MapServer (WMS)") 743 | # p.add_option_group(g) 744 | 745 | p.set_defaults(verbose=False, profile="mercator", kml=False, url='', 746 | webviewer='all', copyright='', resampling='average', resume=False, 747 | googlekey='INSERT_YOUR_KEY_HERE', yahookey='INSERT_YOUR_YAHOO_APP_ID_HERE', aux_files=False, 748 | output_format="PNG", output_cache="xyz") 749 | 750 | self.parser = p 751 | 752 | # ------------------------------------------------------------------------- 753 | def open_input(self): 754 | """Initialization of the input raster, reprojection if necessary""" 755 | 756 | gdal.UseExceptions() 757 | gdal.AllRegister() 758 | if not self.options.verbose: 759 | gdal.PushErrorHandler('CPLQuietErrorHandler') 760 | 761 | # Initialize necessary GDAL drivers 762 | 763 | self.out_drv = gdal.GetDriverByName(self.tiledriver) 764 | self.mem_drv = gdal.GetDriverByName('MEM') 765 | 766 | if not self.out_drv: 767 | raise Exception("The '%s' driver was not found, is it available in this GDAL build?", self.tiledriver) 768 | if not self.mem_drv: 769 | raise Exception("The 'MEM' driver was not found, is it available in this GDAL build?") 770 | 771 | # Open the input file 772 | 773 | if self.input: 774 | self.in_ds = gdal.Open(self.input, gdal.GA_ReadOnly) 775 | else: 776 | raise Exception("No input file was specified") 777 | 778 | if self.options.verbose: 779 | print("Input file:", 780 | "( %sP x %sL - %s bands)" % (self.in_ds.RasterXSize, self.in_ds.RasterYSize, self.in_ds.RasterCount)) 781 | 782 | if not self.in_ds: 783 | # Note: GDAL prints the ERROR message too 784 | self.error("It is not possible to open the input file '%s'." % self.input) 785 | 786 | # Read metadata from the input file 787 | if self.in_ds.RasterCount == 0: 788 | self.error("Input file '%s' has no raster band" % self.input) 789 | 790 | if self.in_ds.GetRasterBand(1).GetRasterColorTable(): 791 | # TODO: Process directly paletted dataset by generating VRT in memory 792 | self.error("Please convert this file to RGB/RGBA and run GDAL2Mbtiles on the result.", 793 | """From paletted file you can create RGBA file (temp.vrt) by: 794 | gdal_translate -of vrt -expand rgba %s temp.vrt 795 | then run: 796 | GDAL2Mbtiles temp.vrt""" % self.input) 797 | 798 | # Get NODATA value 799 | self.in_nodata = [] 800 | for i in range(1, self.in_ds.RasterCount + 1): 801 | if self.in_ds.GetRasterBand(i).GetNoDataValue() != None: 802 | self.in_nodata.append(self.in_ds.GetRasterBand(i).GetNoDataValue()) 803 | if self.options.srcnodata: 804 | nds = list(map(float, self.options.srcnodata.split(','))) 805 | if len(nds) < self.in_ds.RasterCount: 806 | self.in_nodata = (nds * self.in_ds.RasterCount)[:self.in_ds.RasterCount] 807 | else: 808 | self.in_nodata = nds 809 | 810 | if self.options.verbose: 811 | print("NODATA: %s" % self.in_nodata) 812 | 813 | # 814 | # Here we should have RGBA input dataset opened in self.in_ds 815 | # 816 | 817 | if self.options.verbose: 818 | print("Preprocessed file:", 819 | "( %sP x %sL - %s bands)" % (self.in_ds.RasterXSize, self.in_ds.RasterYSize, self.in_ds.RasterCount)) 820 | 821 | # Spatial Reference System of the input raster 822 | 823 | 824 | self.in_srs = None 825 | # self.in_srs = 'EPSG:32641' 826 | 827 | if self.options.s_srs: 828 | self.in_srs = osr.SpatialReference() 829 | self.in_srs.SetFromUserInput(self.options.s_srs) 830 | self.in_srs_wkt = self.in_srs.ExportToWkt() 831 | else: 832 | self.in_srs_wkt = self.in_ds.GetProjection() 833 | if not self.in_srs_wkt and self.in_ds.GetGCPCount() != 0: 834 | self.in_srs_wkt = self.in_ds.GetGCPProjection() 835 | if self.in_srs_wkt: 836 | self.in_srs = osr.SpatialReference() 837 | self.in_srs.ImportFromWkt(self.in_srs_wkt) 838 | # elif self.options.profile != 'raster': 839 | # self.error("There is no spatial reference system info included in the input file.","You should run GDAL2Mbtiles with --s_srs EPSG:XXXX or similar.") 840 | 841 | # Spatial Reference System of tiles 842 | 843 | self.out_srs = osr.SpatialReference() 844 | 845 | if self.options.profile == 'mercator': 846 | self.out_srs.ImportFromEPSG(3857) 847 | elif self.options.profile == 'geodetic': 848 | self.out_srs.ImportFromEPSG(4326) 849 | else: 850 | self.out_srs = self.in_srs 851 | 852 | # Are the reference systems the same? Reproject if necessary. 853 | 854 | self.out_ds = None 855 | 856 | if self.options.profile in ('mercator', 'geodetic'): 857 | 858 | if (self.in_ds.GetGeoTransform() == (0.0, 1.0, 0.0, 0.0, 0.0, 1.0)) and (self.in_ds.GetGCPCount() == 0): 859 | self.error( 860 | "There is no georeference - neither affine transformation (worldfile) nor GCPs. You can generate only 'raster' profile tiles.", 861 | "Either GDAL2Mbtiles with parameter -p 'raster' or use another GIS software for georeference e.g. gdal_transform -gcp / -a_ullr / -a_srs") 862 | 863 | if self.in_srs: 864 | 865 | if (self.in_srs.ExportToProj4() != self.out_srs.ExportToProj4()) or (self.in_ds.GetGCPCount() != 0): 866 | 867 | # Generation of VRT dataset in tile projection, default 'nearest neighbour' warping 868 | self.out_ds = gdal.AutoCreateWarpedVRT(self.in_ds, self.in_srs_wkt, self.out_srs.ExportToWkt()) 869 | 870 | # TODO: HIGH PRIORITY: Correction of AutoCreateWarpedVRT according the max zoomlevel for correct direct warping!!! 871 | 872 | if self.options.verbose: 873 | print("Warping of the raster by AutoCreateWarpedVRT (result saved into 'tiles.vrt')") 874 | self.out_ds.GetDriver().CreateCopy("tiles.vrt", self.out_ds) 875 | 876 | # Note: self.in_srs and self.in_srs_wkt contain still the non-warped reference system!!! 877 | 878 | # Correction of AutoCreateWarpedVRT for NODATA values 879 | if self.in_nodata != []: 880 | fd, tempfilename = tempfile.mkstemp('-GDAL2Mbtiles.vrt') 881 | fptr = os.fdopen(fd) 882 | self.out_ds.GetDriver().CreateCopy(tempfilename, self.out_ds) 883 | # open as a text file 884 | s = open(tempfilename).read() 885 | # Add the warping options 886 | s = s.replace("""""", """ 887 | 888 | """) 889 | # replace BandMapping tag for NODATA bands.... 890 | for i in range(len(self.in_nodata)): 891 | s = s.replace("""""" % ((i + 1), (i + 1)), """ 892 | %i 893 | 0 894 | %i 895 | 0 896 | """ % ( 897 | (i + 1), (i + 1), self.in_nodata[i], 898 | self.in_nodata[i])) # Or rewrite to white by: , 255 )) 899 | # save the corrected VRT 900 | open(tempfilename, "w").write(s) 901 | # open by GDAL as self.out_ds 902 | self.out_ds = gdal.Open(tempfilename) # , gdal.GA_ReadOnly) 903 | # delete the temporary file 904 | fptr.flush() 905 | fptr.close() 906 | os.unlink(tempfilename) 907 | 908 | # set NODATA_VALUE metadata 909 | self.out_ds.SetMetadataItem('NODATA_VALUES', '%i %i %i' % ( 910 | self.in_nodata[0], self.in_nodata[1], self.in_nodata[2])) 911 | 912 | if self.options.verbose: 913 | print("Modified warping result saved into 'tiles1.vrt'") 914 | open("tiles1.vrt", "w").write(s) 915 | 916 | # ----------------------------------- 917 | # Correction of AutoCreateWarpedVRT for Mono (1 band) and RGB (3 bands) files without NODATA: 918 | # equivalent of gdalwarp -dstalpha 919 | if self.in_nodata == [] and self.out_ds.RasterCount in [1, 3]: 920 | fd, tempfilename = tempfile.mkstemp('-GDAL2Mbtiles.vrt') 921 | fptr = os.fdopen(fd) 922 | self.out_ds.GetDriver().CreateCopy(tempfilename, self.out_ds) 923 | # open as a text file 924 | s = open(tempfilename).read() 925 | # Add the warping options 926 | s = s.replace("""""", """ 927 | Alpha 928 | 929 | """ % (self.out_ds.RasterCount + 1)) 930 | s = s.replace("""""", """%i 931 | """ % (self.out_ds.RasterCount + 1)) 932 | s = s.replace("""""", """ 933 | """) 934 | # save the corrected VRT 935 | open(tempfilename, "w").write(s) 936 | # open by GDAL as self.out_ds 937 | self.out_ds = gdal.Open(tempfilename) # , gdal.GA_ReadOnly) 938 | # delete the temporary file 939 | fptr.flush() 940 | fptr.close() 941 | os.unlink(tempfilename) 942 | 943 | if self.options.verbose: 944 | print("Modified -dstalpha warping result saved into 'tiles1.vrt'") 945 | open("tiles1.vrt", "w").write(s) 946 | s = ''' 947 | ''' 948 | 949 | else: 950 | self.error("Input file has unknown SRS.", 951 | "Use --s_srs ESPG:xyz (or similar) to provide source reference system.") 952 | 953 | if self.out_ds and self.options.verbose: 954 | print("Projected file:", "tiles.vrt", "( %sP x %sL - %s bands)" % ( 955 | self.out_ds.RasterXSize, self.out_ds.RasterYSize, self.out_ds.RasterCount)) 956 | 957 | if not self.out_ds: 958 | self.out_ds = self.in_ds 959 | 960 | # 961 | # Here we should have a raster (out_ds) in the correct Spatial Reference system 962 | # 963 | 964 | # Get alpha band (either directly or from NODATA value) 965 | self.alphaband = self.out_ds.GetRasterBand(1).GetMaskBand() 966 | if ( 967 | self.alphaband.GetMaskFlags() & gdal.GMF_ALPHA) or self.out_ds.RasterCount == 4 or self.out_ds.RasterCount == 2: 968 | # TODO: Better test for alpha band in the dataset 969 | self.dataBandsCount = self.out_ds.RasterCount - 1 970 | else: 971 | self.dataBandsCount = self.out_ds.RasterCount 972 | 973 | # KML test 974 | self.isepsg4326 = False 975 | srs4326 = osr.SpatialReference() 976 | srs4326.ImportFromEPSG(4326) 977 | if self.out_srs and srs4326.ExportToProj4() == self.out_srs.ExportToProj4(): 978 | # self.kml = True 979 | self.kml = False 980 | self.isepsg4326 = True 981 | if self.options.verbose: 982 | print("KML autotest OK!") 983 | 984 | # Read the georeference 985 | 986 | self.out_gt = self.out_ds.GetGeoTransform() 987 | 988 | # originX, originY = self.out_gt[0], self.out_gt[3] 989 | # pixelSize = self.out_gt[1] # = self.out_gt[5] 990 | 991 | # Test the size of the pixel 992 | 993 | # MAPTILER - COMMENTED 994 | # if self.out_gt[1] != (-1 * self.out_gt[5]) and self.options.profile != 'raster': 995 | # TODO: Process corectly coordinates with are have swichted Y axis (display in OpenLayers too) 996 | # self.error("Size of the pixel in the output differ for X and Y axes.") 997 | 998 | # Report error in case rotation/skew is in geotransform (possible only in 'raster' profile) 999 | if (self.out_gt[2], self.out_gt[4]) != (0, 0): 1000 | self.error( 1001 | "Georeference of the raster contains rotation or skew. Such raster is not supported. Please use gdalwarp first.") 1002 | # TODO: Do the warping in this case automaticaly 1003 | 1004 | # 1005 | # Here we expect: pixel is square, no rotation on the raster 1006 | # 1007 | 1008 | # Output Bounds - coordinates in the output SRS 1009 | self.ominx = self.out_gt[0] 1010 | self.omaxx = self.out_gt[0] + self.out_ds.RasterXSize * self.out_gt[1] 1011 | self.omaxy = self.out_gt[3] 1012 | self.ominy = self.out_gt[3] - self.out_ds.RasterYSize * self.out_gt[1] 1013 | # Note: maybe round(x, 14) to avoid the gdal_translate behaviour, when 0 becomes -1e-15 1014 | 1015 | if self.options.verbose: 1016 | print("Bounds (output srs):", round(self.ominx, 13), self.ominy, self.omaxx, self.omaxy) 1017 | 1018 | # 1019 | # Calculating ranges for tiles in different zoom levels 1020 | # 1021 | 1022 | if self.options.profile == 'mercator': 1023 | 1024 | self.mercator = GlobalMercator() # from globalmaptiles.py 1025 | 1026 | # Function which generates SWNE in LatLong for given tile 1027 | self.tileswne = self.mercator.TileLatLonBounds 1028 | 1029 | # Generate table with min max tile coordinates for all zoomlevels 1030 | self.tminmax = list(range(0, 32)) 1031 | for tz in range(0, 32): 1032 | tminx, tminy = self.mercator.MetersToTile(self.ominx, self.ominy, tz) 1033 | tmaxx, tmaxy = self.mercator.MetersToTile(self.omaxx, self.omaxy, tz) 1034 | # crop tiles extending world limits (+-180,+-90) 1035 | tminx, tminy = max(0, tminx), max(0, tminy) 1036 | tmaxx, tmaxy = min(2 ** tz - 1, tmaxx), min(2 ** tz - 1, tmaxy) 1037 | self.tminmax[tz] = (tminx, tminy, tmaxx, tmaxy) 1038 | 1039 | # TODO: Maps crossing 180E (Alaska?) 1040 | 1041 | # Get the minimal zoom level (map covers area equivalent to one tile) 1042 | if self.tminz == None: 1043 | self.tminz = self.mercator.ZoomForPixelSize( 1044 | self.out_gt[1] * max(self.out_ds.RasterXSize, self.out_ds.RasterYSize) / float(self.tilesize)) 1045 | 1046 | # Get the maximal zoom level (closest possible zoom level up on the resolution of raster) 1047 | if self.tmaxz == None: 1048 | self.tmaxz = self.mercator.ZoomForPixelSize(self.out_gt[1]) 1049 | 1050 | if self.options.verbose: 1051 | print("Bounds (latlong):", self.mercator.MetersToLatLon(self.ominx, self.ominy), 1052 | self.mercator.MetersToLatLon(self.omaxx, self.omaxy)) 1053 | print('MinZoomLevel:', self.tminz) 1054 | print("MaxZoomLevel:", self.tmaxz, "(", self.mercator.Resolution(self.tmaxz), ")") 1055 | 1056 | if self.options.profile == 'geodetic': 1057 | 1058 | self.geodetic = GlobalGeodetic() # from globalmaptiles.py 1059 | 1060 | # Function which generates SWNE in LatLong for given tile 1061 | self.tileswne = self.geodetic.TileLatLonBounds 1062 | 1063 | # Generate table with min max tile coordinates for all zoomlevels 1064 | self.tminmax = list(range(0, 32)) 1065 | for tz in range(0, 32): 1066 | tminx, tminy = self.geodetic.LatLonToTile(self.ominx, self.ominy, tz) 1067 | tmaxx, tmaxy = self.geodetic.LatLonToTile(self.omaxx, self.omaxy, tz) 1068 | # crop tiles extending world limits (+-180,+-90) 1069 | tminx, tminy = max(0, tminx), max(0, tminy) 1070 | tmaxx, tmaxy = min(2 ** (tz + 1) - 1, tmaxx), min(2 ** tz - 1, tmaxy) 1071 | self.tminmax[tz] = (tminx, tminy, tmaxx, tmaxy) 1072 | 1073 | # TODO: Maps crossing 180E (Alaska?) 1074 | 1075 | # Get the maximal zoom level (closest possible zoom level up on the resolution of raster) 1076 | if self.tminz == None: 1077 | self.tminz = self.geodetic.ZoomForPixelSize( 1078 | self.out_gt[1] * max(self.out_ds.RasterXSize, self.out_ds.RasterYSize) / float(self.tilesize)) 1079 | 1080 | # Get the maximal zoom level (closest possible zoom level up on the resolution of raster) 1081 | if self.tmaxz == None: 1082 | self.tmaxz = self.geodetic.ZoomForPixelSize(self.out_gt[1]) 1083 | 1084 | if self.options.verbose: 1085 | print("Bounds (latlong):", self.ominx, self.ominy, self.omaxx, self.omaxy) 1086 | 1087 | if self.options.profile == 'raster': 1088 | 1089 | log2 = lambda x: math.log10(x) / math.log10(2) # log2 (base 2 logarithm) 1090 | 1091 | self.nativezoom = int(max(math.ceil(log2(self.out_ds.RasterXSize / float(self.tilesize))), 1092 | math.ceil(log2(self.out_ds.RasterYSize / float(self.tilesize))))) 1093 | 1094 | if self.options.verbose: 1095 | print("Native zoom of the raster:", self.nativezoom) 1096 | 1097 | # Get the minimal zoom level (whole raster in one tile) 1098 | if self.tminz == None: 1099 | self.tminz = 0 1100 | 1101 | # Get the maximal zoom level (native resolution of the raster) 1102 | if self.tmaxz == None: 1103 | self.tmaxz = self.nativezoom 1104 | 1105 | # Generate table with min max tile coordinates for all zoomlevels 1106 | self.tminmax = list(range(0, self.tmaxz + 1)) 1107 | self.tsize = list(range(0, self.tmaxz + 1)) 1108 | for tz in range(0, self.tmaxz + 1): 1109 | tsize = 2.0 ** (self.nativezoom - tz) * self.tilesize 1110 | tminx, tminy = 0, 0 1111 | tmaxx = int(math.ceil(self.out_ds.RasterXSize / tsize)) - 1 1112 | tmaxy = int(math.ceil(self.out_ds.RasterYSize / tsize)) - 1 1113 | self.tsize[tz] = math.ceil(tsize) 1114 | self.tminmax[tz] = (tminx, tminy, tmaxx, tmaxy) 1115 | 1116 | # Function which generates SWNE in LatLong for given tile 1117 | if self.kml and self.in_srs_wkt: 1118 | self.ct = osr.CoordinateTransformation(self.in_srs, srs4326) 1119 | 1120 | def rastertileswne(x, y, z): 1121 | pixelsizex = (2 ** (self.tmaxz - z) * self.out_gt[1]) # X-pixel size in level 1122 | pixelsizey = ( 1123 | 2 ** (self.tmaxz - z) * self.out_gt[1]) # Y-pixel size in level (usually -1*pixelsizex) 1124 | west = self.out_gt[0] + x * self.tilesize * pixelsizex 1125 | east = west + self.tilesize * pixelsizex 1126 | south = self.ominy + y * self.tilesize * pixelsizex 1127 | north = south + self.tilesize * pixelsizex 1128 | if not self.isepsg4326: 1129 | # Transformation to EPSG:4326 (WGS84 datum) 1130 | west, south = self.ct.TransformPoint(west, south)[:2] 1131 | east, north = self.ct.TransformPoint(east, north)[:2] 1132 | return south, west, north, east 1133 | 1134 | self.tileswne = rastertileswne 1135 | else: 1136 | self.tileswne = lambda x, y, z: (0, 0, 0, 0) 1137 | # ------------------------------------------------------------------------- 1138 | def generate_metadata(self, cur): 1139 | """Generation of main metadata files and HTML viewers (metadata related to particular tiles are generated during the tile processing).""" 1140 | 1141 | output_dir = os.path.dirname(os.path.abspath(self.output)) 1142 | if not os.path.exists(output_dir): 1143 | os.makedirs(output_dir) 1144 | 1145 | if self.options.profile == 'mercator': 1146 | 1147 | south, west = self.mercator.MetersToLatLon(self.ominx, self.ominy) 1148 | north, east = self.mercator.MetersToLatLon(self.omaxx, self.omaxy) 1149 | south, west = max(-85.05112878, south), max(-180.0, west) 1150 | north, east = min(85.05112878, north), min(180.0, east) 1151 | self.swne = (south, west, north, east) 1152 | 1153 | # Generate googlemaps.html 1154 | if self.options.webviewer in ('all', 'google') and self.options.profile == 'mercator': 1155 | if not self.options.resume or not os.path.exists(os.path.join(output_dir, 'googlemaps.html')): 1156 | f = open(os.path.join(output_dir, 'googlemaps.html'), 'w') 1157 | f.write(self.generate_googlemaps()) 1158 | f.close() 1159 | 1160 | # Generate openlayers.html 1161 | if self.options.webviewer in ('all', 'openlayers'): 1162 | if not self.options.resume or not os.path.exists(os.path.join(output_dir, 'openlayers.html')): 1163 | f = open(os.path.join(output_dir, 'openlayers.html'), 'w') 1164 | f.write(self.generate_openlayers()) 1165 | f.close() 1166 | 1167 | # Generate leaflet.html 1168 | if self.options.webviewer in ('all', 'leaflet'): 1169 | if not self.options.resume or not os.path.exists(os.path.join(output_dir, 'leaflet.html')): 1170 | f = open(os.path.join(output_dir, 'leaflet.html'), 'w') 1171 | f.write(self.generate_leaflet()) 1172 | f.close() 1173 | 1174 | # Generate index.html 1175 | if self.options.webviewer in ('all', 'index'): 1176 | if not self.options.resume or not os.path.exists(os.path.join(output_dir, 'index.html')): 1177 | f = open(os.path.join(output_dir, 'index.html'), 'w') 1178 | f.write(self.generate_index()) 1179 | f.close() 1180 | 1181 | # Generate metadata.json 1182 | if self.options.webviewer in ('all', 'metadata'): 1183 | if not self.options.resume or not os.path.exists(os.path.join(output_dir, 'metadata.json')): 1184 | metadata_dict = self.generate_metadatajson() 1185 | f = open(os.path.join(output_dir, 'metadata.json'), 'w') 1186 | f.write(json.dumps(metadata_dict)) 1187 | f.close() 1188 | for n, v in metadata_dict.items(): 1189 | cur.execute("INSERT INTO metadata (name,value) values (?,?)", (n, v)) 1190 | 1191 | 1192 | elif self.options.profile == 'geodetic': 1193 | 1194 | west, south = self.ominx, self.ominy 1195 | east, north = self.omaxx, self.omaxy 1196 | south, west = max(-90.0, south), max(-180.0, west) 1197 | north, east = min(90.0, north), min(180.0, east) 1198 | self.swne = (south, west, north, east) 1199 | 1200 | # Generate openlayers.html 1201 | if self.options.webviewer in ('all', 'openlayers'): 1202 | if not self.options.resume or not os.path.exists(os.path.join(output_dir, 'openlayers.html')): 1203 | f = open(os.path.join(output_dir, 'openlayers.html'), 'w') 1204 | f.write(self.generate_openlayers()) 1205 | f.close() 1206 | 1207 | elif self.options.profile == 'raster': 1208 | 1209 | west, south = self.ominx, self.ominy 1210 | east, north = self.omaxx, self.omaxy 1211 | 1212 | self.swne = (south, west, north, east) 1213 | 1214 | # Generate openlayers.html 1215 | if self.options.webviewer in ('all', 'openlayers'): 1216 | if not self.options.resume or not os.path.exists(os.path.join(output_dir, 'openlayers.html')): 1217 | f = open(os.path.join(output_dir, 'openlayers.html'), 'w') 1218 | f.write(self.generate_openlayers()) 1219 | f.close() 1220 | 1221 | # Generate tilemapresource.xml. 1222 | if not self.options.resume or not os.path.exists(os.path.join(output_dir, 'tilemapresource.xml')): 1223 | f = open(os.path.join(output_dir, 'tilemapresource.xml'), 'w') 1224 | f.write(self.generate_tilemapresource()) 1225 | f.close() 1226 | 1227 | if self.kml: 1228 | # TODO: Maybe problem for not automatically generated tminz 1229 | # The root KML should contain links to all tiles in the tminz level 1230 | children = [] 1231 | xmin, ymin, xmax, ymax = self.tminmax[self.tminz] 1232 | for x in range(xmin, xmax + 1): 1233 | for y in range(ymin, ymax + 1): 1234 | children.append([x, y, self.tminz]) 1235 | # Generate Root KML 1236 | if self.kml: 1237 | if not self.options.resume or not os.path.exists(os.path.join(output_dir, 'doc.kml')): 1238 | f = open(os.path.join(output_dir, 'doc.kml'), 'w') 1239 | f.write(self.generate_kml(None, None, None, children)) 1240 | f.close() 1241 | 1242 | # ------------------------------------------------------------------------- 1243 | def generate_base_tiles(self, cpu, queue, con): 1244 | """Generation of the base tiles (the lowest in the pyramid) directly from the input raster""" 1245 | cur = con.cursor() 1246 | if self.options.verbose: 1247 | # mx, my = self.out_gt[0], self.out_gt[3] # OriginX, OriginY 1248 | # px, py = self.mercator.MetersToPixels( mx, my, self.tmaxz) 1249 | # print "Pixel coordinates:", px, py, (mx, my) 1250 | print('') 1251 | print("Tiles generated from the max zoom level:") 1252 | print("----------------------------------------") 1253 | print('') 1254 | 1255 | # Set the bounds 1256 | tminx, tminy, tmaxx, tmaxy = self.tminmax[self.tmaxz] 1257 | 1258 | # Just the center tile 1259 | # tminx = tminx+ (tmaxx - tminx)/2 1260 | # tminy = tminy+ (tmaxy - tminy)/2 1261 | # tmaxx = tminx 1262 | # tmaxy = tminy 1263 | 1264 | ds = self.out_ds 1265 | tilebands = self.dataBandsCount + 1 1266 | querysize = self.querysize 1267 | 1268 | if self.options.verbose: 1269 | print("dataBandsCount: ", self.dataBandsCount) 1270 | print("tilebands: ", tilebands) 1271 | 1272 | # print tminx, tminy, tmaxx, tmaxy 1273 | tcount = (1 + abs(tmaxx - tminx)) * (1 + abs(tmaxy - tminy)) 1274 | 1275 | queue.put(tcount) 1276 | 1277 | ti = 0 1278 | j = 0 1279 | msg = '' 1280 | tz = self.tmaxz 1281 | count = (tmaxy - tminy + 1) * (tmaxx + 1 - tminx) 1282 | 1283 | for ty in range(tmaxy, tminy - 1, -1): # range(tminy, tmaxy+1): 1284 | for tx in range(tminx, tmaxx + 1): 1285 | if self.stopped: 1286 | break 1287 | ti += 1 1288 | if (ti - 1) % self.options.processes != cpu: 1289 | continue 1290 | 1291 | if self.options.output_cache == 'xyz': 1292 | ty_final = (2 ** tz - 1) - ty 1293 | else: 1294 | ty_final = ty 1295 | # my addons 1296 | tilefilename = os.path.join(self.output, str(tz), str(tx), "%s.%s" % (ty_final, self.tileext)) 1297 | if os.path.exists(os.path.abspath(tilefilename)): 1298 | # already exist 1299 | continue 1300 | if self.options.verbose: 1301 | print(ti, '/', tcount, tilefilename) # , "( TileMapService: z / x / y )" 1302 | 1303 | if self.options.resume and os.path.exists(tilefilename): 1304 | if self.options.verbose: 1305 | print("Tile generation skiped because of --resume") 1306 | else: 1307 | queue.put(tcount) 1308 | continue 1309 | 1310 | if self.options.profile == 'mercator': 1311 | # Tile bounds in EPSG:900913 1312 | b = self.mercator.TileBounds(tx, ty, tz) 1313 | elif self.options.profile == 'geodetic': 1314 | b = self.geodetic.TileBounds(tx, ty, tz) 1315 | 1316 | # print "\tgdalwarp -ts 256 256 -te %s %s %s %s %s %s_%s_%s.tif" % ( b[0], b[1], b[2], b[3], "tiles.vrt", tz, tx, ty) 1317 | 1318 | # Don't scale up by nearest neighbour, better change the querysize 1319 | # to the native resolution (and return smaller query tile) for scaling 1320 | 1321 | if self.options.profile in ('mercator', 'geodetic'): 1322 | rb, wb = self.geo_query(ds, b[0], b[3], b[2], b[1]) 1323 | nativesize = wb[0] + wb[2] # Pixel size in the raster covering query geo extent 1324 | if self.options.verbose: 1325 | print("\tNative Extent (querysize", nativesize, "): ", rb, wb) 1326 | 1327 | # Tile bounds in raster coordinates for ReadRaster query 1328 | rb, wb = self.geo_query(ds, b[0], b[3], b[2], b[1], querysize=querysize) 1329 | 1330 | rx, ry, rxsize, rysize = rb 1331 | wx, wy, wxsize, wysize = wb 1332 | 1333 | else: # 'raster' profile: 1334 | 1335 | tsize = int(self.tsize[tz]) # tilesize in raster coordinates for actual zoom 1336 | xsize = self.out_ds.RasterXSize # size of the raster in pixels 1337 | ysize = self.out_ds.RasterYSize 1338 | if tz >= self.nativezoom: 1339 | querysize = self.tilesize # int(2**(self.nativezoom-tz) * self.tilesize) 1340 | 1341 | rx = (tx) * tsize 1342 | rxsize = 0 1343 | if tx == tmaxx: 1344 | rxsize = xsize % tsize 1345 | if rxsize == 0: 1346 | rxsize = tsize 1347 | 1348 | rysize = 0 1349 | if ty == tmaxy: 1350 | rysize = ysize % tsize 1351 | if rysize == 0: 1352 | rysize = tsize 1353 | ry = ysize - (ty * tsize) - rysize 1354 | 1355 | wx, wy = 0, 0 1356 | wxsize, wysize = int(rxsize / float(tsize) * self.tilesize), int( 1357 | rysize / float(tsize) * self.tilesize) 1358 | if wysize != self.tilesize: 1359 | wy = self.tilesize - wysize 1360 | 1361 | if self.options.verbose: 1362 | print("\tReadRaster Extent: ", (rx, ry, rxsize, rysize), (wx, wy, wxsize, wysize)) 1363 | 1364 | # Query is in 'nearest neighbour' but can be bigger in then the tilesize 1365 | # We scale down the query to the tilesize by supplied algorithm. 1366 | 1367 | # Tile dataset in memory 1368 | dstile = self.mem_drv.Create('', self.tilesize, self.tilesize, tilebands) 1369 | # print 'dest', dstile 1370 | data = ds.ReadRaster(rx, ry, rxsize, rysize, wxsize, wysize, 1371 | band_list=list(range(1, self.dataBandsCount + 1))) 1372 | alpha = self.alphaband.ReadRaster(rx, ry, rxsize, rysize, wxsize, wysize) 1373 | 1374 | if self.tilesize == querysize: 1375 | # Use the ReadRaster result directly in tiles ('nearest neighbour' query) 1376 | dstile.WriteRaster(wx, wy, wxsize, wysize, data, band_list=list(range(1, self.dataBandsCount + 1))) 1377 | dstile.WriteRaster(wx, wy, wxsize, wysize, alpha, band_list=[tilebands]) 1378 | 1379 | # Note: For source drivers based on WaveLet compression (JPEG2000, ECW, MrSID) 1380 | # the ReadRaster function returns high-quality raster (not ugly nearest neighbour) 1381 | # TODO: Use directly 'near' for WaveLet files 1382 | else: 1383 | # Big ReadRaster query in memory scaled to the tilesize - all but 'near' algo 1384 | dsquery = self.mem_drv.Create('', querysize, querysize, tilebands) 1385 | # TODO: fill the null value in case a tile without alpha is produced (now only png tiles are supported) 1386 | # for i in range(1, tilebands+1): 1387 | # dsquery.GetRasterBand(1).Fill(tilenodata) 1388 | dsquery.WriteRaster(wx, wy, wxsize, wysize, data, band_list=list(range(1, self.dataBandsCount + 1))) 1389 | dsquery.WriteRaster(wx, wy, wxsize, wysize, alpha, band_list=[tilebands]) 1390 | 1391 | self.scale_query_to_tile(dsquery, dstile, tilefilename) 1392 | del dsquery 1393 | 1394 | del data 1395 | 1396 | if self.options.resampling != 'antialias': 1397 | dstile_array = dstile.ReadAsArray() 1398 | binary = io.BytesIO() 1399 | img = Image.fromarray(numpy.rollaxis(dstile_array, 0, 3)) # rotate from (256,256,3) to (3,256,256) 1400 | img.save(binary, format=self.tiledriver) 1401 | 1402 | cur.execute("""insert into tiles (zoom_level, 1403 | tile_column, tile_row, tile_data) values 1404 | (?, ?, ?, ?);""", 1405 | (tz, tx, ty, sqlite3.Binary(binary.getvalue()))) 1406 | 1407 | del img 1408 | binary.flush() 1409 | binary.close() 1410 | del dstile_array 1411 | del dstile 1412 | if not self.options.verbose: 1413 | con.commit() 1414 | queue.put(tcount) 1415 | 1416 | # ------------------------------------------------------------------------- 1417 | def generate_overview_tiles(self, cpu, tz, queue, con): 1418 | """Generation of the overview tiles (higher in the pyramid) based on existing tiles""" 1419 | cur = con.cursor() 1420 | tilebands = self.dataBandsCount + 1 1421 | 1422 | # Usage of existing tiles: from 4 underlying tiles generate one as overview. 1423 | 1424 | tcount = 0 1425 | for z in range(self.tmaxz - 1, self.tminz - 1, -1): 1426 | tminx, tminy, tmaxx, tmaxy = self.tminmax[z] 1427 | tcount += (1 + abs(tmaxx - tminx)) * (1 + abs(tmaxy - tminy)) 1428 | 1429 | ti = 0 1430 | 1431 | # querysize = tilesize * 2 1432 | 1433 | msg = '' 1434 | tminx, tminy, tmaxx, tmaxy = self.tminmax[tz] 1435 | count = (tmaxy - tminy + 1) * (tmaxx + 1 - tminx) 1436 | for ty in range(tmaxy, tminy - 1, -1): # range(tminy, tmaxy+1): 1437 | for tx in range(tminx, tmaxx + 1): 1438 | 1439 | if self.stopped: 1440 | break 1441 | 1442 | ti += 1 1443 | if (ti - 1) % self.options.processes != cpu: 1444 | continue 1445 | 1446 | if self.options.output_cache == 'xyz': 1447 | ty_final = (2 ** tz - 1) - ty 1448 | else: 1449 | ty_final = ty 1450 | 1451 | tilefilename = os.path.join(self.output, str(tz), str(tx), "%s.%s" % (ty_final, self.tileext)) 1452 | 1453 | if self.options.verbose: 1454 | print(ti, '/', tcount, tilefilename) # , "( TileMapService: z / x / y )" 1455 | 1456 | if self.options.resume and os.path.exists(tilefilename): 1457 | if self.options.verbose: 1458 | print("Tile generation skipped because of --resume") 1459 | else: 1460 | queue.put(tcount) 1461 | continue 1462 | 1463 | # TODO: improve that 1464 | if self.out_drv.ShortName == 'JPEG' and tilebands == 4: 1465 | tilebands = 3 1466 | 1467 | dsquery = self.mem_drv.Create('', 2 * self.tilesize, 2 * self.tilesize, tilebands) 1468 | # TODO: fill the null value 1469 | # for i in range(1, tilebands+1): 1470 | # dsquery.GetRasterBand(1).Fill(tilenodata) 1471 | dstile = self.mem_drv.Create('', self.tilesize, self.tilesize, tilebands) 1472 | 1473 | # TODO: Implement more clever walking on the tiles with cache functionality 1474 | # probably walk should start with reading of four tiles from top left corner 1475 | # Hilbert curve... 1476 | 1477 | 1478 | # Read the tiles and write them to query window 1479 | for y in range(2 * ty, 2 * ty + 2): 1480 | for x in range(2 * tx, 2 * tx + 2): 1481 | minx, miny, maxx, maxy = self.tminmax[tz + 1] 1482 | if x >= minx and x <= maxx and y >= miny and y <= maxy: 1483 | 1484 | if self.options.output_cache == 'xyz': 1485 | y_final = (2 ** (tz + 1) - 1) - y 1486 | else: 1487 | y_final = y 1488 | 1489 | tiles = cur.execute('''select tile_data from tiles 1490 | where zoom_level = (?) AND tile_column = (?) AND tile_row = (?) ;''', [tz + 1, x, y]) 1491 | blob_tile = tiles.fetchone() 1492 | pil_tile = Image.open(io.BytesIO(blob_tile[0])) 1493 | np_tile = numpy.array(pil_tile) 1494 | 1495 | if (ty == 0 and y == 1) or (ty != 0 and (y % (2 * ty)) != 0): 1496 | tileposy = 0 1497 | else: 1498 | tileposy = self.tilesize 1499 | if tx: 1500 | tileposx = x % (2 * tx) * self.tilesize 1501 | elif tx == 0 and x == 1: 1502 | tileposx = self.tilesize 1503 | else: 1504 | tileposx = 0 1505 | # Write Array each band of size (256L,256L) 1506 | for i in range(tilebands): 1507 | dsquery.GetRasterBand(i + 1).WriteArray(np_tile[:, :, i], tileposx, tileposy) 1508 | 1509 | self.scale_query_to_tile(dsquery, dstile, tilefilename) 1510 | # Write a copy of tile to png/jpg 1511 | # 1512 | if self.options.resampling != 'antialias': 1513 | # Write a copy of tile to png/jpg 1514 | dstile_array = dstile.ReadAsArray() 1515 | binary = io.BytesIO() 1516 | img = Image.fromarray(numpy.rollaxis(dstile_array, 0, 3)) 1517 | img.save(binary, format=self.tiledriver) 1518 | cur.execute("""insert into tiles (zoom_level, 1519 | tile_column, tile_row, tile_data) values 1520 | (?, ?, ?, ?);""", 1521 | (tz, tx, ty, sqlite3.Binary(binary.getvalue()))) 1522 | 1523 | del binary 1524 | del img 1525 | del dstile 1526 | 1527 | if self.options.verbose: 1528 | print("\tbuild from zoom", tz + 1, " tiles:", (2 * tx, 2 * ty), (2 * tx + 1, 2 * ty), 1529 | (2 * tx, 2 * ty + 1), (2 * tx + 1, 2 * ty + 1)) 1530 | 1531 | if not self.options.verbose: 1532 | queue.put(tcount) 1533 | con.commit() 1534 | pass 1535 | 1536 | # ------------------------------------------------------------------------- 1537 | def geo_query(self, ds, ulx, uly, lrx, lry, querysize=0): 1538 | """For given dataset and query in cartographic coordinates 1539 | returns parameters for ReadRaster() in raster coordinates and 1540 | x/y shifts (for border tiles). If the querysize is not given, the 1541 | extent is returned in the native resolution of dataset ds.""" 1542 | 1543 | geotran = ds.GetGeoTransform() 1544 | rx = int((ulx - geotran[0]) / geotran[1] + 0.001) 1545 | ry = int((uly - geotran[3]) / geotran[5] + 0.001) 1546 | rxsize = int((lrx - ulx) / geotran[1] + 0.5) 1547 | rysize = int((lry - uly) / geotran[5] + 0.5) 1548 | 1549 | if not querysize: 1550 | wxsize, wysize = rxsize, rysize 1551 | else: 1552 | wxsize, wysize = querysize, querysize 1553 | 1554 | # Coordinates should not go out of the bounds of the raster 1555 | wx = 0 1556 | if rx < 0: 1557 | rxshift = abs(rx) 1558 | wx = int(wxsize * (float(rxshift) / rxsize)) 1559 | wxsize = wxsize - wx 1560 | rxsize = rxsize - int(rxsize * (float(rxshift) / rxsize)) 1561 | rx = 0 1562 | if rx + rxsize > ds.RasterXSize: 1563 | wxsize = int(wxsize * (float(ds.RasterXSize - rx) / rxsize)) 1564 | rxsize = ds.RasterXSize - rx 1565 | 1566 | wy = 0 1567 | if ry < 0: 1568 | ryshift = abs(ry) 1569 | wy = int(wysize * (float(ryshift) / rysize)) 1570 | wysize = wysize - wy 1571 | rysize = rysize - int(rysize * (float(ryshift) / rysize)) 1572 | ry = 0 1573 | if ry + rysize > ds.RasterYSize: 1574 | wysize = int(wysize * (float(ds.RasterYSize - ry) / rysize)) 1575 | rysize = ds.RasterYSize - ry 1576 | 1577 | return (rx, ry, rxsize, rysize), (wx, wy, wxsize, wysize) 1578 | 1579 | # ------------------------------------------------------------------------- 1580 | def scale_query_to_tile(self, dsquery, dstile, tilefilename=''): 1581 | """Scales down query dataset to the tile dataset""" 1582 | 1583 | querysize = dsquery.RasterXSize 1584 | tilesize = dstile.RasterXSize 1585 | tilebands = dstile.RasterCount 1586 | 1587 | if self.options.resampling == 'average': 1588 | 1589 | # Function: gdal.RegenerateOverview() 1590 | for i in range(1, tilebands + 1): 1591 | # Black border around NODATA 1592 | # if i != 4: 1593 | # dsquery.GetRasterBand(i).SetNoDataValue(0) 1594 | res = gdal.RegenerateOverview(dsquery.GetRasterBand(i), 1595 | dstile.GetRasterBand(i), 'average') 1596 | if res != 0: 1597 | self.error("RegenerateOverview() failed on %s, error %d" % (tilefilename, res)) 1598 | 1599 | elif self.options.resampling == 'antialias': 1600 | 1601 | # Scaling by PIL (Python Imaging Library) - improved Lanczos 1602 | array = numpy.zeros((querysize, querysize, tilebands), numpy.uint8) 1603 | for i in range(tilebands): 1604 | array[:, :, i] = gdalarray.BandReadAsArray(dsquery.GetRasterBand(i + 1), 0, 0, querysize, querysize) 1605 | im = Image.fromarray(array, 'RGBA') # Always four bands 1606 | im1 = im.resize((tilesize, tilesize), Image.ANTIALIAS) 1607 | if os.path.exists(tilefilename): 1608 | im0 = Image.open(tilefilename) 1609 | im1 = Image.composite(im1, im0, im1) 1610 | im1.save(tilefilename, self.tiledriver) 1611 | 1612 | else: 1613 | 1614 | # Other algorithms are implemented by gdal.ReprojectImage(). 1615 | dsquery.SetGeoTransform((0.0, tilesize / float(querysize), 0.0, 0.0, 0.0, tilesize / float(querysize))) 1616 | dstile.SetGeoTransform((0.0, 1.0, 0.0, 0.0, 0.0, 1.0)) 1617 | 1618 | res = gdal.ReprojectImage(dsquery, dstile, None, None, self.resampling) 1619 | if res != 0: 1620 | self.error("ReprojectImage() failed on %s, error %d" % (tilefilename, res)) 1621 | 1622 | # ------------------------------------------------------------------------- 1623 | def generate_tilemapresource(self): 1624 | """ 1625 | Template for tilemapresource.xml. Returns filled string. Expected variables: 1626 | title, north, south, east, west, isepsg4326, projection, publishurl, 1627 | zoompixels, tilesize, tileformat, profile 1628 | """ 1629 | 1630 | args = {} 1631 | args['title'] = self.options.title 1632 | args['south'], args['west'], args['north'], args['east'] = self.swne 1633 | args['tilesize'] = self.tilesize 1634 | args['tileformat'] = self.tileext 1635 | args['publishurl'] = self.options.url 1636 | args['profile'] = self.options.profile 1637 | 1638 | if self.options.profile == 'mercator': 1639 | args['srs'] = "EPSG:900913" 1640 | elif self.options.profile == 'geodetic': 1641 | args['srs'] = "EPSG:4326" 1642 | elif self.options.s_srs: 1643 | args['srs'] = self.options.s_srs 1644 | elif self.out_srs: 1645 | args['srs'] = self.out_srs.ExportToWkt() 1646 | else: 1647 | args['srs'] = "" 1648 | 1649 | s = """ 1650 | 1651 | %(title)s 1652 | 1653 | %(srs)s 1654 | 1655 | 1656 | 1657 | 1658 | """ % args 1659 | for z in range(self.tminz, self.tmaxz + 1): 1660 | if self.options.profile == 'raster': 1661 | s += """ \n""" % ( 1662 | args['publishurl'], z, (2 ** (self.nativezoom - z) * self.out_gt[1]), z) 1663 | elif self.options.profile == 'mercator': 1664 | s += """ \n""" % ( 1665 | args['publishurl'], z, 156543.0339 / 2 ** z, z) 1666 | elif self.options.profile == 'geodetic': 1667 | s += """ \n""" % ( 1668 | args['publishurl'], z, 0.703125 / 2 ** z, z) 1669 | s += """ 1670 | 1671 | """ 1672 | return s 1673 | 1674 | # ------------------------------------------------------------------------- 1675 | def generate_kml(self, tx, ty, tz, children=[], **args): 1676 | """ 1677 | Template for the KML. Returns filled string. 1678 | """ 1679 | args['tx'], args['ty'], args['tz'] = tx, ty, tz 1680 | args['tileformat'] = self.tileext 1681 | if 'tilesize' not in args: 1682 | args['tilesize'] = self.tilesize 1683 | 1684 | if 'minlodpixels' not in args: 1685 | args['minlodpixels'] = int(args['tilesize'] / 2) # / 2.56) # default 128 1686 | if 'maxlodpixels' not in args: 1687 | args['maxlodpixels'] = int(args['tilesize'] * 8) # 1.7) # default 2048 (used to be -1) 1688 | if children == []: 1689 | args['maxlodpixels'] = -1 1690 | 1691 | if tx == None: 1692 | tilekml = False 1693 | args['title'] = self.options.title 1694 | else: 1695 | tilekml = True 1696 | args['title'] = "%d/%d/%d.kml" % (tz, tx, ty) 1697 | args['south'], args['west'], args['north'], args['east'] = self.tileswne(tx, ty, tz) 1698 | 1699 | if tx == 0: 1700 | args['drawOrder'] = 2 * tz + 1 1701 | elif tx != None: 1702 | args['drawOrder'] = 2 * tz 1703 | else: 1704 | args['drawOrder'] = 0 1705 | 1706 | url = self.options.url 1707 | if not url: 1708 | if tilekml: 1709 | url = "../../" 1710 | else: 1711 | url = "" 1712 | 1713 | s = """ 1714 | 1715 | 1716 | %(title)s 1717 | 1718 | """ % args 1723 | if tilekml: 1724 | s += """ 1725 | 1726 | 1727 | %(minlodpixels)d 1728 | %(maxlodpixels)d 1729 | 1730 | 1731 | %(north).14f 1732 | %(south).14f 1733 | %(east).14f 1734 | %(west).14f 1735 | 1736 | 1737 | 1738 | %(drawOrder)d 1739 | 1740 | %(ty)d.%(tileformat)s 1741 | 1742 | 1743 | %(north).14f 1744 | %(south).14f 1745 | %(east).14f 1746 | %(west).14f 1747 | 1748 | 1749 | """ % args 1750 | 1751 | for cx, cy, cz in children: 1752 | csouth, cwest, cnorth, ceast = self.tileswne(cx, cy, cz) 1753 | s += """ 1754 | 1755 | %d/%d/%d.%s 1756 | 1757 | 1758 | %d 1759 | -1 1760 | 1761 | 1762 | %.14f 1763 | %.14f 1764 | %.14f 1765 | %.14f 1766 | 1767 | 1768 | 1769 | %s%d/%d/%d.kml 1770 | onRegion 1771 | 1772 | 1773 | 1774 | """ % (cz, cx, cy, args['tileformat'], args['minlodpixels'], cnorth, csouth, ceast, cwest, url, cz, cx, cy) 1775 | 1776 | s += """ 1777 | 1778 | """ 1779 | return s 1780 | 1781 | # ------------------------------------------------------------------------- 1782 | def generate_googlemaps(self): 1783 | """ 1784 | Template for googlemaps.html implementing Overlay of tiles for 'mercator' profile. 1785 | It returns filled string. Expected variables: 1786 | title, googlemapskey, north, south, east, west, minzoom, maxzoom, tilesize, tileformat, publishurl 1787 | """ 1788 | args = {} 1789 | args['title'] = self.options.title 1790 | args['googlemapskey'] = self.options.googlekey 1791 | args['south'], args['west'], args['north'], args['east'] = self.swne 1792 | args['minzoom'] = self.tminz 1793 | args['maxzoom'] = self.tmaxz 1794 | args['tilesize'] = self.tilesize 1795 | args['tileformat'] = self.tileext 1796 | args['publishurl'] = self.options.url 1797 | args['copyright'] = self.options.copyright 1798 | 1799 | s = """ 1800 | 1801 | 1802 | %(title)s 1803 | 1804 | 1805 | 1813 | 1814 | 2065 | 2066 | 2067 | 2068 |
Generated by MapTiler/GDAL2Mbtiles, Copyright © 2008 Klokan Petr Pridal, GDAL & OSGeo GSoC 2069 | 2070 |
2071 |
2072 | 2073 | 2074 | """ % args 2075 | 2076 | return s 2077 | 2078 | # ------------------------------------------------------------------------- 2079 | def generate_leaflet(self): 2080 | """ 2081 | Template for leaflet.html implementing overlay of tiles for 'mercator' profile. 2082 | It returns filled string. Expected variables: 2083 | title, north, south, east, west, minzoom, maxzoom, tilesize, tileformat, publishurl 2084 | """ 2085 | 2086 | args = {} 2087 | args['title'] = self.options.title.replace('"', '\\"') 2088 | args['htmltitle'] = self.options.title 2089 | args['south'], args['west'], args['north'], args['east'] = self.swne 2090 | args['centerlon'] = (args['north'] + args['south']) / 2. 2091 | args['centerlat'] = (args['west'] + args['east']) / 2. 2092 | args['minzoom'] = self.tminz 2093 | args['maxzoom'] = self.tmaxz 2094 | args['beginzoom'] = self.tmaxz 2095 | args['tilesize'] = self.tilesize # not used 2096 | args['tileformat'] = self.tileext 2097 | args['publishurl'] = self.options.url # not used 2098 | args['copyright'] = self.options.copyright.replace('"', '\\"') 2099 | 2100 | s = """ 2101 | 2102 | 2103 | 2104 | 2105 | %(htmltitle)s 2106 | 2107 | 2108 | 2109 | 2110 | 2111 | 2132 | 2133 | 2134 | 2135 | 2136 |
2137 | 2138 | 2202 | 2203 | 2204 | 2205 | 2206 | """ % args 2207 | 2208 | return s 2209 | 2210 | # ------------------------------------------------------------------------- 2211 | def generate_index(self): 2212 | """ 2213 | Template for leaflet.html implementing overlay of tiles for 'mercator' profile. 2214 | It returns filled string. Expected variables: 2215 | title, north, south, east, west, minzoom, maxzoom, tilesize, tileformat, publishurl 2216 | """ 2217 | 2218 | args = {} 2219 | args['title'] = self.options.title.replace('"', '\\"') 2220 | args['htmltitle'] = self.options.title 2221 | args['south'], args['west'], args['north'], args['east'] = self.swne 2222 | args['centerlat'] = (args['north'] + args['south']) / 2. 2223 | args['centerlon'] = (args['west'] + args['east']) / 2. 2224 | args['minzoom'] = self.tminz 2225 | args['maxzoom'] = self.tmaxz 2226 | args['beginzoom'] = self.tmaxz 2227 | args['tilesize'] = self.tilesize # not used 2228 | args['tileformat'] = self.tileext 2229 | args['publishurl'] = self.options.url # not used 2230 | args['copyright'] = self.options.copyright.replace('"', '\\"') 2231 | 2232 | s = """ 2233 | 2234 | 2235 | %(title)s 2236 | 2237 | 2238 | 2239 | 2240 | 2241 | 2266 | 2267 | 2268 | 2269 | """ % args 2270 | 2271 | return s 2272 | 2273 | # ------------------------------------------------------------------------- 2274 | def generate_metadatajson(self): 2275 | """ 2276 | Template for metadata.json implementing overlay of tiles for 'mercator' profile. 2277 | It returns filled string. Expected variables: 2278 | 2279 | """ 2280 | 2281 | args = {} 2282 | args['title'] = self.options.title.replace('"', '\\"') 2283 | args['htmltitle'] = self.options.title 2284 | args['south'], args['west'], args['north'], args['east'] = self.swne 2285 | args['centerlat'] = (args['north'] + args['south']) / 2. 2286 | args['centerlon'] = (args['west'] + args['east']) / 2. 2287 | args['minzoom'] = self.tminz 2288 | args['maxzoom'] = self.tmaxz 2289 | args['beginzoom'] = self.tmaxz 2290 | args['tilesize'] = self.tilesize # not used 2291 | args['tileformat'] = self.tileext 2292 | args['publishurl'] = self.options.url # not used 2293 | args['copyright'] = self.options.copyright.replace('"', '\\"') 2294 | 2295 | s = { 2296 | "name": args['title'], 2297 | "description": args['htmltitle'], 2298 | "version": "1.0.0", 2299 | "attribution": args['copyright'], 2300 | "type": "overlay", 2301 | "format": args['tileformat'], 2302 | "minzoom": args['minzoom'], 2303 | "maxzoom": args['maxzoom'], 2304 | "bounds": str(args['south']) + " " + str(args['west']) + " " + str(args['north']) + " " + str(args['east']), 2305 | "scale": "1", 2306 | "profile": "mercator" 2307 | } 2308 | return s 2309 | 2310 | # ------------------------------------------------------------------------- 2311 | def generate_openlayers(self): 2312 | """ 2313 | Template for openlayers.html implementing overlay of available Spherical Mercator layers. 2314 | 2315 | It returns filled string. Expected variables: 2316 | title, googlemapskey, yahooappid, north, south, east, west, minzoom, maxzoom, tilesize, tileformat, publishurl 2317 | """ 2318 | 2319 | args = {} 2320 | args['title'] = self.options.title 2321 | args['googlemapskey'] = self.options.googlekey 2322 | args['yahooappid'] = self.options.yahookey 2323 | args['south'], args['west'], args['north'], args['east'] = self.swne 2324 | args['minzoom'] = self.tminz 2325 | args['maxzoom'] = self.tmaxz 2326 | args['tilesize'] = self.tilesize 2327 | args['tileformat'] = self.tileext 2328 | args['publishurl'] = self.options.url 2329 | args['copyright'] = self.options.copyright 2330 | if self.options.profile == 'raster': 2331 | args['rasterzoomlevels'] = self.tmaxz + 1 2332 | args['rastermaxresolution'] = 2 ** (self.nativezoom) * self.out_gt[1] 2333 | 2334 | s = """ 2335 | 2337 | %(title)s 2338 | 2339 | """ % args 2347 | 2348 | if self.options.profile == 'mercator': 2349 | s += """ 2350 | 2351 | 2352 | """ % args 2353 | 2354 | s += """ 2355 | 2356 | 2593 | 2594 | 2595 | 2596 |
Generated by MapTiler/GDAL2Mbtiles, Copyright © 2008 Klokan Petr Pridal, GDAL & OSGeo GSoC 2597 | 2598 |
2599 |
2600 | 2601 | 2602 | """ % args 2603 | 2604 | return s 2605 | 2606 | # ------------------------------------------------------- 2607 | """Methods for work with mbtiles""" 2608 | 2609 | # ------------------------------------------------------- 2610 | 2611 | 2612 | def mbtiles_connect(self): 2613 | try: 2614 | con = sqlite3.connect(self.output, timeout=30) 2615 | self.optimize_connection(con.cursor()) 2616 | return con 2617 | except Exception as e: 2618 | sys.exit(1) 2619 | 2620 | def mbtiles_setup(self, cur): 2621 | cur.execute(""" 2622 | CREATE TABLE tiles ( 2623 | zoom_level integer, 2624 | tile_column integer, 2625 | tile_row integer, 2626 | tile_data blob); 2627 | """) 2628 | cur.execute("""CREATE TABLE metadata 2629 | (name text, value text);""") 2630 | cur.execute("""CREATE TABLE grids (zoom_level integer, tile_column integer, 2631 | tile_row integer, grid blob);""") 2632 | cur.execute("""CREATE TABLE grid_data (zoom_level integer, tile_column 2633 | integer, tile_row integer, key_name text, key_json text);""") 2634 | 2635 | def create_index(slef, cur): 2636 | cur.execute("""create unique index name on metadata (name);""") 2637 | cur.execute("""create unique index tile_index on tiles 2638 | (zoom_level, tile_column, tile_row);""") 2639 | 2640 | def optimize_connection(self, cur): 2641 | cur.execute("""PRAGMA synchronous=OFF;""") 2642 | # cur.execute("""PRAGMA journal_mode=DELETE""") 2643 | # cur.execute("""PRAGMA journal_mode=WAL""") 2644 | cur.execute("""PRAGMA journal_mode=OFF;""") 2645 | cur.execute("""PRAGMA cache_size=-2000;""") 2646 | cur.execute("""PRAGMA page_size=65536;""") 2647 | cur.execute("""PRAGMA foreign_keys=1;""") 2648 | 2649 | # ------------------------------------------------------- 2650 | """Methods for work with Progressbar""" 2651 | 2652 | # ------------------------------------------------------- 2653 | class ProgressBar(QtCore.QObject): 2654 | 2655 | # pbar_signal variable which will emit count of processed tiles to GUI 2656 | pbar_signal = QtCore.pyqtSignal(int) 2657 | 2658 | def progress_emiter(self, maxz, minz, processed_tiles, total, overview=False): 2659 | base_level = float(100) / (maxz - minz+1) 2660 | if not overview: 2661 | multiplyer = (base_level)/float(total) 2662 | self.pbar_signal.emit(int(processed_tiles * multiplyer)) 2663 | else: # overview tiles 2664 | level_percent =100- base_level 2665 | # level_percent = base_level 2666 | multiplyer = (level_percent)/float(total) 2667 | self.pbar_signal.emit(int(base_level + processed_tiles * multiplyer)) 2668 | 2669 | # ============================================================================= 2670 | # ============================================================================= 2671 | # ============================================================================= 2672 | 2673 | def worker_metadata(gdal2mbtiles): 2674 | gdal2mbtiles.open_input() 2675 | con = gdal2mbtiles.mbtiles_connect() 2676 | cur = con.cursor() 2677 | gdal2mbtiles.mbtiles_setup(cur) 2678 | gdal2mbtiles.generate_metadata(cur) 2679 | con.commit() 2680 | con.close() 2681 | sys.stdout.flush() 2682 | 2683 | 2684 | def worker_base_tiles(argv, cpu, queue): 2685 | gdal2mbtiles = GDAL2Mbtiles(argv[1:]) 2686 | gdal2mbtiles.open_input() 2687 | con = gdal2mbtiles.mbtiles_connect() 2688 | gdal2mbtiles.generate_base_tiles(cpu, queue, con) 2689 | con.close() 2690 | 2691 | 2692 | def worker_overview_tiles(argv, cpu, tz, queue): 2693 | gdal2mbtiles = GDAL2Mbtiles(argv[1:]) 2694 | gdal2mbtiles.open_input() 2695 | con = gdal2mbtiles.mbtiles_connect() 2696 | gdal2mbtiles.generate_overview_tiles(cpu, tz, queue, con) 2697 | con.close() 2698 | 2699 | 2700 | def timing_val(func): 2701 | def wrapper(*arg, **kw): 2702 | t1 = time.time() 2703 | func(*arg, **kw) 2704 | t2 = time.time() 2705 | return (t2 - t1) 2706 | 2707 | return wrapper 2708 | 2709 | 2710 | @timing_val 2711 | def main(progress,argv = None): 2712 | queue = multiprocessing.Queue() 2713 | # progress = ProgressBar() 2714 | if not argv: 2715 | argv = gdal.GeneralCmdLineProcessor(sys.argv) 2716 | # progress = ProgressBar() 2717 | 2718 | gdal2mbtiles = GDAL2Mbtiles(argv[1:]) # handle command line options 2719 | proc_count = gdal2mbtiles.options.processes 2720 | if gdal2mbtiles.options.aux_files: 2721 | gdal.SetConfigOption("GDAL_PAM_ENABLED", "YES") 2722 | else: 2723 | gdal.SetConfigOption("GDAL_PAM_ENABLED", "NO") 2724 | p = multiprocessing.Process(target=worker_metadata, args=[gdal2mbtiles]) 2725 | p.start() 2726 | p.join() 2727 | print("Generating Base Tiles:") 2728 | tminz = gdal2mbtiles.tminz 2729 | tmaxz = gdal2mbtiles.tmaxz 2730 | procs = [] 2731 | for cpu in range(proc_count): 2732 | proc = multiprocessing.Process(target=worker_base_tiles, args=(argv, cpu, queue,)) 2733 | proc.daemon = True 2734 | proc.start() 2735 | procs.append(proc) 2736 | processed_tiles = 0 2737 | while len(multiprocessing.active_children()): 2738 | try: 2739 | total = queue.get(timeout=1) 2740 | processed_tiles += 1 2741 | progress.progress_emiter(tmaxz,tminz,processed_tiles,total) 2742 | gdal2mbtiles.progressbar(processed_tiles / float(total)) 2743 | sys.stdout.flush() 2744 | except: 2745 | pass 2746 | [p.join(timeout=1) for p in procs] 2747 | print("\n") 2748 | print("Generating Overview Tiles:") 2749 | #  Values generated after base tiles creation 2750 | 2751 | processed_tiles = 0 2752 | for tz in range(tmaxz - 1, tminz - 1, -1): 2753 | for cpu in range(proc_count): 2754 | proc = multiprocessing.Process(target=worker_overview_tiles, args=(argv, cpu % proc_count, tz, queue)) 2755 | proc.daemon = True 2756 | proc.start() 2757 | procs.append(proc) 2758 | while len(multiprocessing.active_children()): 2759 | try: 2760 | total = queue.get(timeout=1) 2761 | processed_tiles += 1 2762 | progress.progress_emiter(tmaxz, tminz, processed_tiles, total, overview=True) 2763 | gdal2mbtiles.progressbar(processed_tiles / float(total)) 2764 | sys.stdout.flush() 2765 | except: 2766 | pass 2767 | [p.join(timeout=1) for p in procs] 2768 | print('Indexing tiles') 2769 | con = gdal2mbtiles.mbtiles_connect() 2770 | gdal2mbtiles.create_index(con.cursor()) 2771 | con.execute('''PRAGMA journal_mode=DELETE''') 2772 | 2773 | 2774 | if __name__ == '__main__': 2775 | progress = ProgressBar() 2776 | t = main(progress) 2777 | print ('Tiling took: {:.2f} seconds '.format(t)) 2778 | --------------------------------------------------------------------------------