├── 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 |
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 |
2599 |
2600 |
2601 |
2602 |