├── .gitignore
├── test.png
├── test
├── karta.jpg
├── images
│ ├── marker-shadow.png
│ ├── marker-icon-red.png
│ └── marker-icon-red-2x.png
├── png2jpg.sh
├── createtiles.sh
├── index.html
├── rastercoords.js
├── geojson.js
└── index.js
├── setup.py
├── LICENSE
├── README.md
└── gdal2tiles.py
/.gitignore:
--------------------------------------------------------------------------------
1 | /test/tiles
2 | /tmp
3 |
--------------------------------------------------------------------------------
/test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/commenthol/gdal2tiles-leaflet/HEAD/test.png
--------------------------------------------------------------------------------
/test/karta.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/commenthol/gdal2tiles-leaflet/HEAD/test/karta.jpg
--------------------------------------------------------------------------------
/test/images/marker-shadow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/commenthol/gdal2tiles-leaflet/HEAD/test/images/marker-shadow.png
--------------------------------------------------------------------------------
/test/images/marker-icon-red.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/commenthol/gdal2tiles-leaflet/HEAD/test/images/marker-icon-red.png
--------------------------------------------------------------------------------
/test/images/marker-icon-red-2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/commenthol/gdal2tiles-leaflet/HEAD/test/images/marker-icon-red-2x.png
--------------------------------------------------------------------------------
/test/png2jpg.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ####
4 | # sample file to convert from png to jpg using imagemagick convert
5 | #
6 |
7 | for png in `(find tiles -name "*.png")`
8 | do
9 | jpg=`echo $png | sed "s/png$/jpg/"`
10 | echo "--- $png"
11 | convert $png -quality 80 $jpg
12 | rm $png
13 | done
14 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | from setuptools import setup
2 |
3 | setup(
4 | name='gdal2tiles_leaflet',
5 | version='0.2.0',
6 | description='Generate raster image tiles for use with leaflet',
7 | url='https://github.com/commenthol/gdal2tiles-leaflet',
8 | license='MIT',
9 | py_modules=['gdal2tiles-multiprocess', 'gdal2tiles'],
10 | install_requires=[
11 | 'gdal',
12 | ],
13 | )
14 |
--------------------------------------------------------------------------------
/test/createtiles.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | rm -rf tiles
4 |
5 | export GDAL_ALLOW_LARGE_LIBJPEG_MEM_ALLOC=1
6 |
7 | python=python3
8 |
9 | case $1 in
10 | mpz)
11 | $python ../gdal2tiles-multiprocess.py -l -p raster -z 0-5 -w none karta.jpg tiles
12 | ;;
13 | mp)
14 | $python ../gdal2tiles-multiprocess.py -l -p raster -w none karta.jpg tiles
15 | ;;
16 | z)
17 | $python ../gdal2tiles.py -l -p raster -w none karta.jpg -z 0-5 tiles
18 | ;;
19 | *)
20 | $python ../gdal2tiles.py -l -p raster -w none karta.jpg tiles
21 | ;;
22 | esac
23 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | example for leaflet-rastercoords
5 |
6 |
7 |
8 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016, commenthol
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/test/rastercoords.js:
--------------------------------------------------------------------------------
1 | /**
2 | * leaflet plugin for plain image map projection
3 | * @copyright 2016- commenthol
4 | * @license MIT
5 | */
6 | /* globals define */
7 |
8 | ;(function (factory) {
9 | var L
10 | if (typeof define === 'function' && define.amd) {
11 | // AMD
12 | define(['leaflet'], factory)
13 | } else if (typeof module !== 'undefined') {
14 | // Node/CommonJS
15 | L = require('leaflet')
16 | module.exports = factory(L)
17 | } else {
18 | // Browser globals
19 | if (typeof window.L === 'undefined') {
20 | throw new Error('Leaflet must be loaded first')
21 | }
22 | factory(window.L)
23 | }
24 | }(function (L) {
25 | /**
26 | * L.RasterCoords
27 | * @param {L.map} map - the map used
28 | * @param {Array} imgsize - [ width, height ] image dimensions
29 | * @param {Number} [tilesize] - tilesize in pixels. Default=256
30 | */
31 | L.RasterCoords = function (map, imgsize, tilesize) {
32 | this.map = map
33 | this.width = imgsize[0]
34 | this.height = imgsize[1]
35 | this.tilesize = tilesize || 256
36 | this.zoom = this.zoomLevel()
37 | if (this.width && this.height) {
38 | this.setMaxBounds()
39 | }
40 | }
41 |
42 | L.RasterCoords.prototype = {
43 | /**
44 | * calculate accurate zoom level for the given image size
45 | */
46 | zoomLevel: function () {
47 | return Math.ceil(
48 | Math.log(
49 | Math.max(this.width, this.height) /
50 | this.tilesize
51 | ) / Math.log(2)
52 | )
53 | },
54 | /**
55 | * unproject `coords` to the raster coordinates used by the raster image projection
56 | * @param {Array} coords - [ x, y ]
57 | * @return {L.LatLng} - internal coordinates
58 | */
59 | unproject: function (coords) {
60 | return this.map.unproject(coords, this.zoom)
61 | },
62 | /**
63 | * project `coords` back to image coordinates
64 | * @param {Array} coords - [ x, y ]
65 | * @return {L.LatLng} - image coordinates
66 | */
67 | project: function (coords) {
68 | return this.map.project(coords, this.zoom)
69 | },
70 | /**
71 | * sets the max bounds on map
72 | */
73 | setMaxBounds: function () {
74 | var southWest = this.unproject([0, this.height])
75 | var northEast = this.unproject([this.width, 0])
76 | this.map.setMaxBounds(new L.LatLngBounds(southWest, northEast))
77 | }
78 | }
79 |
80 | return L.RasterCoords
81 | }))
82 | ; // eslint-disable-line semi
83 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # gdal2tiles-leaflet
2 |
3 | > Generate raster image tiles for use with leaflet.
4 |
5 | [Example][example] in action.
6 |
7 | This is a modified version of [gdal2tiles.py][] which adds support for raster images as plain 2D maps in [leafletjs][].
8 |
9 | It adds the option `-l` or `--leaflet` to generate the resulting images with the reference point `[0,0]` in the upper-left (North-West) corner, opposed to the standard behaviour for TMS tiles using the lower-left (South-East) corner.
10 |
11 | Together with the small [leafletjs][] plugin [leaflet-rastercoords][] you'll be able to add markers at the correct position using the (x, y) coordinates of the full-size image.
12 |
13 | ## Prerequisites
14 |
15 | On Debian style OS:
16 |
17 | sudo apt install python-gdal
18 |
19 | for python3
20 |
21 | sudo apt install python3-gdal
22 |
23 | for others give your search-engine a try...
24 |
25 | ## Basic usage
26 |
27 | ````
28 | $ gdal2tiles.py -l -p raster -z 0-5 -w none
29 | ````
30 |
31 | Check [test/createtiles.sh](test/createtiles.sh) for usage.
32 |
33 | If the `-z` option is omitted then the tool considers the min. zoom level otherwise note...
34 |
35 | **Note:** The min zoom level for tile generation must be greater or
36 | equal to `log2(max(width, height)/tilesize)`
37 |
38 | Assuming an image with 2000x3000 pixels:
39 |
40 | ````
41 | # take the larger dimension -> here height = 3000px
42 | $ echo "l(3000/256)/l(2)" | bc -l
43 | # 3.55 --> min zoomlevel for tile generation is 4
44 | # means: `gdal2tiles.py -l -p raster -z 0-2 ...`
45 | # \__ is not allowed
46 | # correct usage
47 | $ gdal2tiles -l -p raster -z 0-4 ...
48 | ````
49 |
50 | ## Multicore usage
51 |
52 | The same works with multicore support, thanks to [gdal2tiles-Ticket-4379][].
53 |
54 | ````
55 | $ gdal2tiles-multiprocess.py -l -p raster -z 0-5 -w none
56 | ````
57 |
58 | ## Usage with Leaflet
59 |
60 | To use the generated tiles with Leaflet there is a small plugin to correctly set the required projection for raster images.
61 | Please refer to the documention at [leaflet-rastercoords][].
62 |
63 | ## Example
64 |
65 | To run the example you'll need to generate the tiles for the large image first.
66 |
67 | ````
68 | $ cd test
69 | $ ./createtiles.sh
70 | $ open index.html
71 | ````
72 |
73 | Then open `index.html` in a browser.
74 |
75 | [][example]
76 |
77 | Or see it [here][example] in action.
78 |
79 | ## Contribution and License Agreement
80 |
81 | If you contribute code to this project, you are implicitly allowing your
82 | code to be distributed under the respective license. You are also implicitly
83 | verifying that all code is your original work or correctly attributed
84 | with the source of its origin and licence.
85 |
86 | ## License
87 |
88 | Modifications and samples are [MIT licensed][LICENSE].
89 |
90 | [gdal2tiles.py][]: (MIT licensed)
91 | * Copyright (c) 2008, Klokan Petr Pridal
92 | * Copyright (c) 2010-2013, Even Rouault
93 |
94 | ## References
95 |
96 |
97 |
98 | * [/gdal-1.11.1/swig/python/scripts/gdal2tiles.py][gdal2tiles.py]
99 | * [example][example]
100 | * [gdal2tiles-Ticket-4379][gdal2tiles-Ticket-4379]
101 | * [leaflet-rastercoords][leaflet-rastercoords]
102 | * [leafletjs][leafletjs]
103 | * [LICENSE][LICENSE]
104 |
105 |
106 |
107 | [LICENSE]: ./LICENSE
108 | [leafletjs]: http://leafletjs.com
109 | [leaflet-rastercoords]: https://github.com/commenthol/leaflet-rastercoords
110 | [gdal2tiles.py]: http://download.osgeo.org/gdal/1.11.1/gdal-1.11.1.tar.gz "/gdal-1.11.1/swig/python/scripts/gdal2tiles.py"
111 | [gdal2tiles-Ticket-4379]: http://trac.osgeo.org/gdal/ticket/4379
112 | [example]: https://commenthol.github.io/leaflet-rastercoords/
113 |
--------------------------------------------------------------------------------
/test/geojson.js:
--------------------------------------------------------------------------------
1 | ;(function (window) {
2 | // geoJson definitions for country
3 | window.countries = [{
4 | type: 'Feature',
5 | properties: {
6 | name: 'Iceland'
7 | },
8 | geometry: {
9 | type: 'Point',
10 | coordinates: [1258, 911]
11 | }
12 | }, {
13 | type: 'Feature',
14 | properties: {
15 | name: 'Ireland'
16 | },
17 | geometry: {
18 | type: 'Point',
19 | coordinates: [1324, 1580]
20 | }
21 | }, {
22 | type: 'Feature',
23 | properties: {
24 | name: 'England'
25 | },
26 | geometry: {
27 | type: 'Point',
28 | coordinates: [1498, 1662]
29 | }
30 | }, {
31 | type: 'Feature',
32 | properties: {
33 | name: 'France'
34 | },
35 | geometry: {
36 | type: 'Point',
37 | coordinates: [1608, 1918]
38 | }
39 | }, {
40 | type: 'Feature',
41 | properties: {
42 | name: 'Italia'
43 | },
44 | geometry: {
45 | type: 'Point',
46 | coordinates: [1923, 2093]
47 | }
48 | }, {
49 | type: 'Feature',
50 | properties: {
51 | name: 'Hispania'
52 | },
53 | geometry: {
54 | type: 'Point',
55 | coordinates: [1374, 2148]
56 | }
57 | }]
58 |
59 | // geoJson definitions
60 | window.geoInfo = [{
61 | 'type': 'Feature',
62 | 'properties': {
63 | 'name': 'Mare Germanicum'
64 | },
65 | 'geometry': {
66 | 'type': 'Point',
67 | 'coordinates': [1589, 1447]
68 | }
69 | }, {
70 | 'type': 'Feature',
71 | 'properties': {
72 | 'name': 'Mare Balticum'
73 | },
74 | 'geometry': {
75 | 'type': 'Point',
76 | 'coordinates': [2090, 1407]
77 | }
78 | }, {
79 | 'type': 'Feature',
80 | 'properties': {
81 | 'name': 'Mare Mediteraneum'
82 | },
83 | 'geometry': {
84 | 'type': 'Point',
85 | 'coordinates': [2028, 2453]
86 | }
87 | }, {
88 | 'type': 'Feature',
89 | 'properties': {
90 | 'name': 'Mare Maggiore'
91 | },
92 | 'geometry': {
93 | 'type': 'Point',
94 | 'coordinates': [2623, 1918]
95 | }
96 | }]
97 |
98 | // polygon
99 | window.polygon = [{
100 | x: 1528.5,
101 | y: 1524
102 | }, {
103 | x: 1532,
104 | y: 1571
105 | }, {
106 | x: 1559.5,
107 | y: 1620.5
108 | }, {
109 | x: 1541,
110 | y: 1612
111 | }, {
112 | x: 1562.5,
113 | y: 1634.5
114 | }, {
115 | x: 1548.5,
116 | y: 1655
117 | }, {
118 | x: 1567.5,
119 | y: 1651
120 | }, {
121 | x: 1598.5,
122 | y: 1666.5
123 | }, {
124 | x: 1576,
125 | y: 1713
126 | }, {
127 | x: 1562,
128 | y: 1718
129 | }, {
130 | x: 1584,
131 | y: 1722.5
132 | }, {
133 | x: 1586.5,
134 | y: 1736.5
135 | }, {
136 | x: 1550.5,
137 | y: 1750.5
138 | }, {
139 | x: 1505,
140 | y: 1743.5
141 | }, {
142 | x: 1505.5,
143 | y: 1752
144 | }, {
145 | x: 1493.5,
146 | y: 1754.5
147 | }, {
148 | x: 1480,
149 | y: 1745.5
150 | }, {
151 | x: 1462,
152 | y: 1742
153 | }, {
154 | x: 1448.5,
155 | y: 1733
156 | }, {
157 | x: 1431.5,
158 | y: 1737.5
159 | }, {
160 | x: 1421,
161 | y: 1755
162 | }, {
163 | x: 1403,
164 | y: 1740.5
165 | }, {
166 | x: 1377.5,
167 | y: 1741.5
168 | }, {
169 | x: 1370.5,
170 | y: 1751
171 | }, {
172 | x: 1349.5,
173 | y: 1733
174 | }, {
175 | x: 1367.5,
176 | y: 1725
177 | }, {
178 | x: 1400,
179 | y: 1715
180 | }, {
181 | x: 1417.5,
182 | y: 1704.5
183 | }, {
184 | x: 1445,
185 | y: 1709
186 | }, {
187 | x: 1463,
188 | y: 1696
189 | }, {
190 | x: 1436.5,
191 | y: 1703
192 | }, {
193 | x: 1427,
194 | y: 1690
195 | }, {
196 | x: 1415,
197 | y: 1679
198 | }, {
199 | x: 1397.5,
200 | y: 1683
201 | }, {
202 | x: 1386.5,
203 | y: 1667.5
204 | }, {
205 | x: 1424.5,
206 | y: 1655
207 | }, {
208 | x: 1428,
209 | y: 1632
210 | }, {
211 | x: 1406.5,
212 | y: 1636
213 | }, {
214 | x: 1435.5,
215 | y: 1613
216 | }, {
217 | x: 1457,
218 | y: 1614
219 | }, {
220 | x: 1467,
221 | y: 1593.5
222 | }, {
223 | x: 1457.5,
224 | y: 1572.5
225 | }, {
226 | x: 1467.5,
227 | y: 1551
228 | }, {
229 | x: 1528.5,
230 | y: 1524
231 | }]
232 | }(window))
233 |
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | /* global L */
2 | ;(function (window) {
3 | function init (mapid) {
4 | var minZoom = 0
5 | var maxZoom = 5
6 | var img = [
7 | 3831, // original width of image `karta.jpg`
8 | 3101 // original height of image
9 | ]
10 |
11 | // create the map
12 | var map = L.map(mapid, {
13 | minZoom: minZoom,
14 | maxZoom: maxZoom
15 | })
16 |
17 | // assign map and image dimensions
18 | var rc = new L.RasterCoords(map, img)
19 |
20 | // set the view on a marker ...
21 | map.setView(rc.unproject([1589, 1447]), 4)
22 |
23 | // add layer control object
24 | L.control.layers({}, {
25 | 'Polygon': layerPolygon(map, rc),
26 | 'Countries': layerCountries(map, rc),
27 | 'Bounds': layerBounds(map, rc, img),
28 | 'Info': layerGeo(map, rc),
29 | 'Circles': layerCircles(map, rc)
30 | }).addTo(map)
31 |
32 | // the tile layer containing the image generated with gdal2tiles --leaflet ...
33 | L.tileLayer('./tiles/{z}/{x}/{y}.png', {
34 | noWrap: true,
35 | attribution: 'Map ' +
37 | 'Karta över Europa, 1672 - Skoklosters under ' +
38 | 'CC0'
39 | }).addTo(map)
40 | }
41 |
42 | /**
43 | * layer with markers
44 | */
45 | function layerBounds (map, rc, img) {
46 | // set marker at the image bound edges
47 | var layerBounds = L.layerGroup([
48 | L.marker(rc.unproject([0, 0])).bindPopup('[0,0]'),
49 | L.marker(rc.unproject(img)).bindPopup(JSON.stringify(img))
50 | ])
51 | map.addLayer(layerBounds)
52 |
53 | // set markers on click events in the map
54 | map.on('click', function (event) {
55 | // to obtain raster coordinates from the map use `project`
56 | var coord = rc.project(event.latlng)
57 | // to set a marker, ... in raster coordinates in the map use `unproject`
58 | var marker = L.marker(rc.unproject(coord))
59 | .addTo(layerBounds)
60 | marker.bindPopup('[' + Math.floor(coord.x) + ',' + Math.floor(coord.y) + ']')
61 | .openPopup()
62 | })
63 |
64 | return layerBounds
65 | }
66 |
67 | /**
68 | * layer using geoJson data for countries adding a circle marker
69 | */
70 | function layerCountries (map, rc) {
71 | var layerCountries = L.geoJson(window.countries, {
72 | // correctly map the geojson coordinates on the image
73 | coordsToLatLng: function (coords) {
74 | return rc.unproject(coords)
75 | },
76 | // add a popup content to the marker
77 | onEachFeature: function (feature, layer) {
78 | if (feature.properties && feature.properties.name) {
79 | layer.bindPopup(feature.properties.name)
80 | }
81 | },
82 | pointToLayer: function (feature, latlng) {
83 | return L.circleMarker(latlng, {
84 | radius: 8,
85 | fillColor: '#800080',
86 | color: '#D107D1',
87 | weight: 1,
88 | opacity: 1,
89 | fillOpacity: 0.8
90 | })
91 | }
92 | })
93 | map.addLayer(layerCountries)
94 | return layerCountries
95 | }
96 |
97 | /**
98 | * layer with red markers
99 | */
100 | function layerGeo (map, rc) {
101 | var imgDir = 'images/'
102 | var redMarker = L.icon({
103 | iconUrl: imgDir + 'marker-icon-red.png',
104 | iconRetinaUrl: imgDir + 'marker-icon-red-2x.png',
105 | iconSize: [25, 41],
106 | iconAnchor: [12, 41],
107 | popupAnchor: [-0, -31],
108 | shadowUrl: imgDir + 'marker-shadow.png',
109 | shadowSize: [41, 41],
110 | shadowAnchor: [14, 41]
111 | })
112 | var layerGeo = L.geoJson(window.geoInfo, {
113 | // correctly map the geojson coordinates on the image
114 | coordsToLatLng: function (coords) {
115 | return rc.unproject(coords)
116 | },
117 | // add a popup content to the marker
118 | onEachFeature: function (feature, layer) {
119 | if (feature.properties && feature.properties.name) {
120 | layer.bindPopup(feature.properties.name)
121 | }
122 | },
123 | pointToLayer: function (feature, latlng) {
124 | return L.marker(latlng, {
125 | icon: redMarker
126 | })
127 | }
128 | })
129 | map.addLayer(layerGeo)
130 | return layerGeo
131 | }
132 |
133 | /**
134 | * layer drawing a polygon
135 | */
136 | function layerPolygon (map, rc) {
137 | var points = window.polygon.map(function (point) {
138 | return rc.unproject([point.x, point.y])
139 | })
140 | var layerPolygon = L.polygon([points])
141 | map.addLayer(layerPolygon)
142 | return layerPolygon
143 | }
144 |
145 | /**
146 | * layer drawing some cicles
147 | */
148 | function layerCircles (map, rc) {
149 | /*
150 | // using circle may cause displaying a ellipse at the edges of the image
151 | // radius is painful to adjust - simply don't use
152 | const circle = L.circle(rc.unproject([200, 1000]), { radius: 1e6 })
153 | */
154 |
155 | /*
156 | // drawing a circle with a polyline
157 | // Not so nice because of the visible steps
158 | function circlePoints ([x, y], r, steps = 360) {
159 | var p = []
160 | for (var i = 0; i < steps; i++) {
161 | p.push(rc.unproject([
162 | (x + r * Math.cos(2 * Math.PI * i / steps)),
163 | (y + r * Math.sin(2 * Math.PI * i / steps))
164 | ]))
165 | }
166 | return p
167 | }
168 | const polyline = L.polygon([circlePoints([200, 200], 200)], {
169 | fillColor: '#3388ff',
170 | color: '#fb0000'
171 | })
172 | */
173 |
174 | // Custom marker prototype - credits to Arkensor
175 | L.CircleMarkerScaling = L.CircleMarker.extend({
176 | _project: function () {
177 | this._point = this._map.latLngToLayerPoint(this._latlng);
178 | this._radius = 2 * this.options.radius * this._map.getZoomScale(this._map.getZoom(), this._map.getMaxZoom());
179 | this._updateBounds();
180 | }
181 | })
182 | L.circleMarkerScaling = function (latlng, options) {
183 | return new L.CircleMarkerScaling(latlng, options);
184 | }
185 |
186 | const custom = L.circleMarkerScaling(rc.unproject([200, 200]), {
187 | radius: 200,
188 | fillColor: '#3388ff',
189 | color: '#fbff2c',
190 | })
191 |
192 | const layer = L.featureGroup([/*circle, polyline,*/ custom])
193 | map.addLayer(layer)
194 | return layer
195 | }
196 |
197 | init('map')
198 | }(window))
199 |
--------------------------------------------------------------------------------
/gdal2tiles.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 |
4 | # ******************************************************************************
5 | # $Id: gdal2tiles.py 27349 2014-05-16 18:58:51Z rouault $
6 | #
7 | # Project: Google Summer of Code 2007, 2008 (http://code.google.com/soc/)
8 | # Support: BRGM (http://www.brgm.fr)
9 | # Purpose: Convert a raster into TMS (Tile Map Service) tiles in a directory.
10 | # - generate Google Earth metadata (KML SuperOverlay)
11 | # - generate simple HTML viewer based on Google Maps and OpenLayers
12 | # - support of global tiles (Spherical Mercator) for compatibility
13 | # with interactive web maps a la Google Maps
14 | # Author: Klokan Petr Pridal, klokan at klokan dot cz
15 | # Web: http://www.klokan.cz/projects/gdal2tiles/
16 | # GUI: http://www.maptiler.org/
17 | #
18 | ###############################################################################
19 | # Copyright (c) 2008, Klokan Petr Pridal
20 | # Copyright (c) 2010-2013, Even Rouault
21 | #
22 | # Permission is hereby granted, free of charge, to any person obtaining a
23 | # copy of this software and associated documentation files (the "Software"),
24 | # to deal in the Software without restriction, including without limitation
25 | # the rights to use, copy, modify, merge, publish, distribute, sublicense,
26 | # and/or sell copies of the Software, and to permit persons to whom the
27 | # Software is furnished to do so, subject to the following conditions:
28 | #
29 | # The above copyright notice and this permission notice shall be included
30 | # in all copies or substantial portions of the Software.
31 | #
32 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
33 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
34 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
35 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
36 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
37 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
38 | # DEALINGS IN THE SOFTWARE.
39 | # ******************************************************************************
40 |
41 | import sys
42 |
43 | try:
44 | from osgeo import gdal
45 | from osgeo import osr
46 | except:
47 | import gdal
48 | print('You are using "old gen" bindings. gdal2tiles needs "new gen" bindings.')
49 | sys.exit(1)
50 |
51 | import os
52 | import math
53 |
54 | try:
55 | from PIL import Image
56 | import numpy
57 | import osgeo.gdal_array as gdalarray
58 | except:
59 |
60 | # 'antialias' resampling is not available
61 |
62 | pass
63 |
64 | __version__ = '$Id: gdal2tiles.py 27349 2014-05-16 18:58:51Z rouault $'
65 |
66 | resampling_list = (
67 | 'average',
68 | 'near',
69 | 'bilinear',
70 | 'cubic',
71 | 'cubicspline',
72 | 'lanczos',
73 | 'antialias',
74 | )
75 | profile_list = ('mercator', 'geodetic', 'raster') # ,'zoomify')
76 | webviewer_list = ('all', 'google', 'openlayers', 'none')
77 |
78 | # =============================================================================
79 | # =============================================================================
80 | # =============================================================================
81 |
82 | __doc__globalmaptiles = \
83 | """
84 | globalmaptiles.py
85 |
86 | Global Map Tiles as defined in Tile Map Service (TMS) Profiles
87 | ==============================================================
88 |
89 | Functions necessary for generation of global tiles used on the web.
90 | It contains classes implementing coordinate conversions for:
91 |
92 | - GlobalMercator (based on EPSG:900913 = EPSG:3785)
93 | for Google Maps, Yahoo Maps, Bing Maps compatible tiles
94 | - GlobalGeodetic (based on EPSG:4326)
95 | for OpenLayers Base Map and Google Earth compatible tiles
96 |
97 | More info at:
98 |
99 | http://wiki.osgeo.org/wiki/Tile_Map_Service_Specification
100 | http://wiki.osgeo.org/wiki/WMS_Tiling_Client_Recommendation
101 | http://msdn.microsoft.com/en-us/library/bb259689.aspx
102 | http://code.google.com/apis/maps/documentation/overlays.html#Google_Maps_Coordinates
103 |
104 | Created by Klokan Petr Pridal on 2008-07-03.
105 | Google Summer of Code 2008, project GDAL2Tiles for OSGEO.
106 |
107 | In case you use this class in your product, translate it to another language
108 | or find it usefull for your project please let me know.
109 | My email: klokan at klokan dot cz.
110 | I would like to know where it was used.
111 |
112 | Class is available under the open-source GDAL license (www.gdal.org).
113 | """
114 |
115 | import math
116 |
117 | MAXZOOMLEVEL = 32
118 |
119 |
120 | class GlobalMercator(object):
121 |
122 | """
123 | TMS Global Mercator Profile
124 | ---------------------------
125 |
126 | Functions necessary for generation of tiles in Spherical Mercator projection,
127 | EPSG:900913 (EPSG:gOOglE, Google Maps Global Mercator), EPSG:3785, OSGEO:41001.
128 |
129 | Such tiles are compatible with Google Maps, Bing Maps, Yahoo Maps,
130 | UK Ordnance Survey OpenSpace API, ...
131 | and you can overlay them on top of base maps of those web mapping applications.
132 |
133 | Pixel and tile coordinates are in TMS notation (origin [0,0] in bottom-left).
134 |
135 | What coordinate conversions do we need for TMS Global Mercator tiles::
136 |
137 | LatLon <-> Meters <-> Pixels <-> Tile
138 |
139 | WGS84 coordinates Spherical Mercator Pixels in pyramid Tiles in pyramid
140 | lat/lon XY in metres XY pixels Z zoom XYZ from TMS
141 | EPSG:4326 EPSG:900913
142 | .----. --------- -- TMS
143 | / \ <-> | | <-> /----/ <-> Google
144 | \ / | | /--------/ QuadTree
145 | ----- --------- /------------/
146 | KML, public WebMapService Web Clients TileMapService
147 |
148 | What is the coordinate extent of Earth in EPSG:900913?
149 |
150 | [-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244]
151 | Constant 20037508.342789244 comes from the circumference of the Earth in meters,
152 | which is 40 thousand kilometers, the coordinate origin is in the middle of extent.
153 | In fact you can calculate the constant as: 2 * math.pi * 6378137 / 2.0
154 | $ echo 180 85 | gdaltransform -s_srs EPSG:4326 -t_srs EPSG:900913
155 | Polar areas with abs(latitude) bigger then 85.05112878 are clipped off.
156 |
157 | What are zoom level constants (pixels/meter) for pyramid with EPSG:900913?
158 |
159 | whole region is on top of pyramid (zoom=0) covered by 256x256 pixels tile,
160 | every lower zoom level resolution is always divided by two
161 | initialResolution = 20037508.342789244 * 2 / 256 = 156543.03392804062
162 |
163 | What is the difference between TMS and Google Maps/QuadTree tile name convention?
164 |
165 | The tile raster itself is the same (equal extent, projection, pixel size),
166 | there is just different identification of the same raster tile.
167 | Tiles in TMS are counted from [0,0] in the bottom-left corner, id is XYZ.
168 | Google placed the origin [0,0] to the top-left corner, reference is XYZ.
169 | Microsoft is referencing tiles by a QuadTree name, defined on the website:
170 | http://msdn2.microsoft.com/en-us/library/bb259689.aspx
171 |
172 | The lat/lon coordinates are using WGS84 datum, yeh?
173 |
174 | Yes, all lat/lon we are mentioning should use WGS84 Geodetic Datum.
175 | Well, the web clients like Google Maps are projecting those coordinates by
176 | Spherical Mercator, so in fact lat/lon coordinates on sphere are treated as if
177 | the were on the WGS84 ellipsoid.
178 |
179 | From MSDN documentation:
180 | To simplify the calculations, we use the spherical form of projection, not
181 | the ellipsoidal form. Since the projection is used only for map display,
182 | and not for displaying numeric coordinates, we don't need the extra precision
183 | of an ellipsoidal projection. The spherical projection causes approximately
184 | 0.33 percent scale distortion in the Y direction, which is not visually noticable.
185 |
186 | How do I create a raster in EPSG:900913 and convert coordinates with PROJ.4?
187 |
188 | You can use standard GIS tools like gdalwarp, cs2cs or gdaltransform.
189 | All of the tools supports -t_srs 'epsg:900913'.
190 |
191 | For other GIS programs check the exact definition of the projection:
192 | More info at http://spatialreference.org/ref/user/google-projection/
193 | The same projection is degined as EPSG:3785. WKT definition is in the official
194 | EPSG database.
195 |
196 | Proj4 Text:
197 | +proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0
198 | +k=1.0 +units=m +nadgrids=@null +no_defs
199 |
200 | Human readable WKT format of EPGS:900913:
201 | PROJCS["Google Maps Global Mercator",
202 | GEOGCS["WGS 84",
203 | DATUM["WGS_1984",
204 | SPHEROID["WGS 84",6378137,298.257223563,
205 | AUTHORITY["EPSG","7030"]],
206 | AUTHORITY["EPSG","6326"]],
207 | PRIMEM["Greenwich",0],
208 | UNIT["degree",0.0174532925199433],
209 | AUTHORITY["EPSG","4326"]],
210 | PROJECTION["Mercator_1SP"],
211 | PARAMETER["central_meridian",0],
212 | PARAMETER["scale_factor",1],
213 | PARAMETER["false_easting",0],
214 | PARAMETER["false_northing",0],
215 | UNIT["metre",1,
216 | AUTHORITY["EPSG","9001"]]]
217 | """
218 |
219 | def __init__(self, tileSize=256):
220 | '''Initialize the TMS Global Mercator pyramid'''
221 |
222 | self.tileSize = tileSize
223 | self.initialResolution = 2 * math.pi * 6378137 / self.tileSize
224 |
225 | # 156543.03392804062 for tileSize 256 pixels
226 |
227 | self.originShift = 2 * math.pi * 6378137 / 2.0
228 |
229 | # 20037508.342789244
230 |
231 | def LatLonToMeters(self, lat, lon):
232 | '''Converts given lat/lon in WGS84 Datum to XY in Spherical Mercator EPSG:900913'''
233 |
234 | mx = lon * self.originShift / 180.0
235 | my = math.log(math.tan((90 + lat) * math.pi / 360.0)) \
236 | / (math.pi / 180.0)
237 |
238 | my = my * self.originShift / 180.0
239 | return (mx, my)
240 |
241 | def MetersToLatLon(self, mx, my):
242 | '''Converts XY point from Spherical Mercator EPSG:900913 to lat/lon in WGS84 Datum'''
243 |
244 | lon = mx / self.originShift * 180.0
245 | lat = my / self.originShift * 180.0
246 |
247 | lat = 180 / math.pi * (2 * math.atan(math.exp(lat * math.pi
248 | / 180.0)) - math.pi / 2.0)
249 | return (lat, lon)
250 |
251 | def PixelsToMeters(
252 | self,
253 | px,
254 | py,
255 | zoom,
256 | ):
257 | '''Converts pixel coordinates in given zoom level of pyramid to EPSG:900913'''
258 |
259 | res = self.Resolution(zoom)
260 | mx = px * res - self.originShift
261 | my = py * res - self.originShift
262 | return (mx, my)
263 |
264 | def MetersToPixels(
265 | self,
266 | mx,
267 | my,
268 | zoom,
269 | ):
270 | '''Converts EPSG:900913 to pyramid pixel coordinates in given zoom level'''
271 |
272 | res = self.Resolution(zoom)
273 | px = (mx + self.originShift) / res
274 | py = (my + self.originShift) / res
275 | return (px, py)
276 |
277 | def PixelsToTile(self, px, py):
278 | '''Returns a tile covering region in given pixel coordinates'''
279 |
280 | tx = int(math.ceil(px / float(self.tileSize)) - 1)
281 | ty = int(math.ceil(py / float(self.tileSize)) - 1)
282 | return (tx, ty)
283 |
284 | def PixelsToRaster(
285 | self,
286 | px,
287 | py,
288 | zoom,
289 | ):
290 | '''Move the origin of pixel coordinates to top-left corner'''
291 |
292 | mapSize = self.tileSize << zoom
293 | return (px, mapSize - py)
294 |
295 | def MetersToTile(
296 | self,
297 | mx,
298 | my,
299 | zoom,
300 | ):
301 | '''Returns tile for given mercator coordinates'''
302 |
303 | (px, py) = self.MetersToPixels(mx, my, zoom)
304 | return self.PixelsToTile(px, py)
305 |
306 | def TileBounds(
307 | self,
308 | tx,
309 | ty,
310 | zoom,
311 | ):
312 | '''Returns bounds of the given tile in EPSG:900913 coordinates'''
313 |
314 | (minx, miny) = self.PixelsToMeters(tx * self.tileSize, ty
315 | * self.tileSize, zoom)
316 | (maxx, maxy) = self.PixelsToMeters((tx + 1) * self.tileSize,
317 | (ty + 1) * self.tileSize, zoom)
318 | return (minx, miny, maxx, maxy)
319 |
320 | def TileLatLonBounds(
321 | self,
322 | tx,
323 | ty,
324 | zoom,
325 | ):
326 | '''Returns bounds of the given tile in latutude/longitude using WGS84 datum'''
327 |
328 | bounds = self.TileBounds(tx, ty, zoom)
329 | (minLat, minLon) = self.MetersToLatLon(bounds[0], bounds[1])
330 | (maxLat, maxLon) = self.MetersToLatLon(bounds[2], bounds[3])
331 |
332 | return (minLat, minLon, maxLat, maxLon)
333 |
334 | def Resolution(self, zoom):
335 | '''Resolution (meters/pixel) for given zoom level (measured at Equator)'''
336 |
337 | # return (2 * math.pi * 6378137) / (self.tileSize * 2**zoom)
338 |
339 | return self.initialResolution / 2 ** zoom
340 |
341 | def ZoomForPixelSize(self, pixelSize):
342 | '''Maximal scaledown zoom of the pyramid closest to the pixelSize.'''
343 |
344 | for i in range(MAXZOOMLEVEL):
345 | if pixelSize > self.Resolution(i):
346 | if i != 0:
347 | return i - 1
348 | else:
349 | return 0 # We don't want to scale up
350 |
351 | def GoogleTile(
352 | self,
353 | tx,
354 | ty,
355 | zoom,
356 | ):
357 | '''Converts TMS tile coordinates to Google Tile coordinates'''
358 |
359 | # coordinate origin is moved from bottom-left to top-left corner of the extent
360 |
361 | return (tx, 2 ** zoom - 1 - ty)
362 |
363 | def QuadTree(
364 | self,
365 | tx,
366 | ty,
367 | zoom,
368 | ):
369 | '''Converts TMS tile coordinates to Microsoft QuadTree'''
370 |
371 | quadKey = ''
372 | ty = 2 ** zoom - 1 - ty
373 | for i in range(zoom, 0, -1):
374 | digit = 0
375 | mask = 1 << i - 1
376 | if tx & mask != 0:
377 | digit += 1
378 | if ty & mask != 0:
379 | digit += 2
380 | quadKey += str(digit)
381 |
382 | return quadKey
383 |
384 |
385 | # ---------------------
386 |
387 | class GlobalGeodetic(object):
388 |
389 | """
390 | TMS Global Geodetic Profile
391 | ---------------------------
392 |
393 | Functions necessary for generation of global tiles in Plate Carre projection,
394 | EPSG:4326, "unprojected profile".
395 |
396 | Such tiles are compatible with Google Earth (as any other EPSG:4326 rasters)
397 | and you can overlay the tiles on top of OpenLayers base map.
398 |
399 | Pixel and tile coordinates are in TMS notation (origin [0,0] in bottom-left).
400 |
401 | What coordinate conversions do we need for TMS Global Geodetic tiles?
402 |
403 | Global Geodetic tiles are using geodetic coordinates (latitude,longitude)
404 | directly as planar coordinates XY (it is also called Unprojected or Plate
405 | Carre). We need only scaling to pixel pyramid and cutting to tiles.
406 | Pyramid has on top level two tiles, so it is not square but rectangle.
407 | Area [-180,-90,180,90] is scaled to 512x256 pixels.
408 | TMS has coordinate origin (for pixels and tiles) in bottom-left corner.
409 | Rasters are in EPSG:4326 and therefore are compatible with Google Earth.
410 |
411 | LatLon <-> Pixels <-> Tiles
412 |
413 | WGS84 coordinates Pixels in pyramid Tiles in pyramid
414 | lat/lon XY pixels Z zoom XYZ from TMS
415 | EPSG:4326
416 | .----. ----
417 | / \ <-> /--------/ <-> TMS
418 | \ / /--------------/
419 | ----- /--------------------/
420 | WMS, KML Web Clients, Google Earth TileMapService
421 | """
422 |
423 | def __init__(self, tmscompatible, tileSize=256):
424 | self.tileSize = tileSize
425 | if tmscompatible is not None:
426 |
427 | # Defaults the resolution factor to 0.703125 (2 tiles @ level 0)
428 | # Adhers to OSGeo TMS spec http://wiki.osgeo.org/wiki/Tile_Map_Service_Specification#global-geodetic
429 |
430 | self.resFact = 180.0 / self.tileSize
431 | else:
432 |
433 | # Defaults the resolution factor to 1.40625 (1 tile @ level 0)
434 | # Adheres OpenLayers, MapProxy, etc default resolution for WMTS
435 |
436 | self.resFact = 360.0 / self.tileSize
437 |
438 | def LonLatToPixels(
439 | self,
440 | lon,
441 | lat,
442 | zoom,
443 | ):
444 | '''Converts lon/lat to pixel coordinates in given zoom of the EPSG:4326 pyramid'''
445 |
446 | res = self.resFact / 2 ** zoom
447 | px = (180 + lon) / res
448 | py = (90 + lat) / res
449 | return (px, py)
450 |
451 | def PixelsToTile(self, px, py):
452 | '''Returns coordinates of the tile covering region in pixel coordinates'''
453 |
454 | tx = int(math.ceil(px / float(self.tileSize)) - 1)
455 | ty = int(math.ceil(py / float(self.tileSize)) - 1)
456 | return (tx, ty)
457 |
458 | def LonLatToTile(
459 | self,
460 | lon,
461 | lat,
462 | zoom,
463 | ):
464 | '''Returns the tile for zoom which covers given lon/lat coordinates'''
465 |
466 | (px, py) = self.LonLatToPixels(lon, lat, zoom)
467 | return self.PixelsToTile(px, py)
468 |
469 | def Resolution(self, zoom):
470 | '''Resolution (arc/pixel) for given zoom level (measured at Equator)'''
471 |
472 | return self.resFact / 2 ** zoom
473 |
474 | # return 180 / float( 1 << (8+zoom) )
475 |
476 | def ZoomForPixelSize(self, pixelSize):
477 | '''Maximal scaledown zoom of the pyramid closest to the pixelSize.'''
478 |
479 | for i in range(MAXZOOMLEVEL):
480 | if pixelSize > self.Resolution(i):
481 | if i != 0:
482 | return i - 1
483 | else:
484 | return 0 # We don't want to scale up
485 |
486 | def TileBounds(
487 | self,
488 | tx,
489 | ty,
490 | zoom,
491 | ):
492 | '''Returns bounds of the given tile'''
493 |
494 | res = self.resFact / 2 ** zoom
495 | return (tx * self.tileSize * res - 180, ty * self.tileSize
496 | * res - 90, (tx + 1) * self.tileSize * res - 180, (ty
497 | + 1) * self.tileSize * res - 90)
498 |
499 | def TileLatLonBounds(
500 | self,
501 | tx,
502 | ty,
503 | zoom,
504 | ):
505 | '''Returns bounds of the given tile in the SWNE form'''
506 |
507 | b = self.TileBounds(tx, ty, zoom)
508 | return (b[1], b[0], b[3], b[2])
509 |
510 |
511 | # ---------------------
512 | # TODO: Finish Zoomify implemtentation!!!
513 |
514 | class Zoomify(object):
515 |
516 | """
517 | Tiles compatible with the Zoomify viewer
518 | ----------------------------------------
519 | """
520 |
521 | def __init__(
522 | self,
523 | width,
524 | height,
525 | tilesize=256,
526 | tileformat='jpg',
527 | ):
528 | """Initialization of the Zoomify tile tree"""
529 |
530 | self.tilesize = tilesize
531 | self.tileformat = tileformat
532 | imagesize = (width, height)
533 | tiles = (math.ceil(width / tilesize), math.ceil(height
534 | / tilesize))
535 |
536 | # Size (in tiles) for each tier of pyramid.
537 |
538 | self.tierSizeInTiles = []
539 | self.tierSizeInTiles.push(tiles)
540 |
541 | # Image size in pixels for each pyramid tierself
542 |
543 | self.tierImageSize = []
544 | self.tierImageSize.append(imagesize)
545 |
546 | while imagesize[0] > tilesize or imageSize[1] > tilesize:
547 | imagesize = (math.floor(imagesize[0] / 2),
548 | math.floor(imagesize[1] / 2))
549 | tiles = (math.ceil(imagesize[0] / tilesize),
550 | math.ceil(imagesize[1] / tilesize))
551 | self.tierSizeInTiles.append(tiles)
552 | self.tierImageSize.append(imagesize)
553 |
554 | self.tierSizeInTiles.reverse()
555 | self.tierImageSize.reverse()
556 |
557 | # Depth of the Zoomify pyramid, number of tiers (zoom levels)
558 |
559 | self.numberOfTiers = len(self.tierSizeInTiles)
560 |
561 | # Number of tiles up to the given tier of pyramid.
562 |
563 | self.tileCountUpToTier = []
564 | self.tileCountUpToTier[0] = 0
565 | for i in range(1, self.numberOfTiers + 1):
566 | self.tileCountUpToTier.append(self.tierSizeInTiles[i
567 | - 1][0] * self.tierSizeInTiles[i - 1][1]
568 | + self.tileCountUpToTier[i - 1])
569 |
570 | def tilefilename(
571 | self,
572 | x,
573 | y,
574 | z,
575 | ):
576 | """Returns filename for tile with given coordinates"""
577 |
578 | tileIndex = x + y * self.tierSizeInTiles[z][0] \
579 | + self.tileCountUpToTier[z]
580 | return os.path.join('TileGroup%.0f' % math.floor(tileIndex
581 | / 256), '%s-%s-%s.%s' % (z, x, y,
582 | self.tileformat))
583 |
584 |
585 | # =============================================================================
586 | # =============================================================================
587 | # =============================================================================
588 |
589 | class GDAL2Tiles(object):
590 |
591 | # -------------------------------------------------------------------------
592 |
593 | def process(self):
594 | """The main processing function, runs all the main steps of processing"""
595 |
596 | # Opening and preprocessing of the input file
597 |
598 | self.open_input()
599 |
600 | # Generation of main metadata files and HTML viewers
601 |
602 | self.generate_metadata()
603 |
604 | # Generation of the lowest tiles
605 |
606 | self.generate_base_tiles()
607 |
608 | # Generation of the overview tiles (higher in the pyramid)
609 |
610 | self.generate_overview_tiles()
611 |
612 | # -------------------------------------------------------------------------
613 |
614 | def error(self, msg, details=''):
615 | """Print an error message and stop the processing"""
616 |
617 | if details:
618 | self.parser.error(msg + '''
619 |
620 | ''' + details)
621 | else:
622 | self.parser.error(msg)
623 |
624 | # -------------------------------------------------------------------------
625 |
626 | def progressbar(self, complete=0.0):
627 | """Print progressbar for float value 0..1"""
628 |
629 | gdal.TermProgress_nocb(complete)
630 |
631 | # -------------------------------------------------------------------------
632 |
633 | def stop(self):
634 | """Stop the rendering immediately"""
635 |
636 | self.stopped = True
637 |
638 | # -------------------------------------------------------------------------
639 |
640 | def __init__(self, arguments):
641 | """Constructor function - initialization"""
642 |
643 | self.stopped = False
644 | self.input = None
645 | self.output = None
646 |
647 | # Tile format
648 |
649 | self.tilesize = 256
650 | self.tiledriver = 'PNG'
651 | self.tileext = 'png'
652 |
653 | # Should we read bigger window of the input raster and scale it down?
654 | # Note: Modified leter by open_input()
655 | # Not for 'near' resampling
656 | # Not for Wavelet based drivers (JPEG2000, ECW, MrSID)
657 | # Not for 'raster' profile
658 |
659 | self.scaledquery = True
660 |
661 | # How big should be query window be for scaling down
662 | # Later on reset according the chosen resampling algorightm
663 |
664 | self.querysize = 4 * self.tilesize
665 |
666 | # Should we use Read on the input file for generating overview tiles?
667 | # Note: Modified later by open_input()
668 | # Otherwise the overview tiles are generated from existing underlying tiles
669 |
670 | self.overviewquery = False
671 |
672 | # RUN THE ARGUMENT PARSER:
673 |
674 | self.optparse_init()
675 | (self.options, self.args) = \
676 | self.parser.parse_args(args=arguments)
677 | if not self.args:
678 | self.error('No input file specified')
679 |
680 | # POSTPROCESSING OF PARSED ARGUMENTS:
681 |
682 | # Workaround for old versions of GDAL
683 |
684 | try:
685 | if self.options.verbose and self.options.resampling \
686 | == 'near' or gdal.TermProgress_nocb:
687 | pass
688 | except:
689 | self.error('This version of GDAL is not supported. Please upgrade to 1.6+.'
690 | )
691 |
692 | # ,"You can try run crippled version of gdal2tiles with parameters: -v -r 'near'")
693 |
694 | # Is output directory the last argument?
695 |
696 | # Test output directory, if it doesn't exist
697 |
698 | if os.path.isdir(self.args[-1]) or len(self.args) > 1 \
699 | and not os.path.exists(self.args[-1]):
700 | self.output = self.args[-1]
701 | self.args = self.args[:-1]
702 |
703 | # More files on the input not directly supported yet
704 |
705 | if len(self.args) > 1:
706 | self.error('Processing of several input files is not supported.'
707 | ,
708 | """Please first use a tool like gdal_vrtmerge.py or gdal_merge.py on the files:
709 | gdal_vrtmerge.py -o merged.vrt %s"""
710 | % ' '.join(self.args))
711 |
712 | # TODO: Call functions from gdal_vrtmerge.py directly
713 |
714 | self.input = self.args[0]
715 |
716 | # Default values for not given options
717 |
718 | if not self.output:
719 |
720 | # Directory with input filename without extension in actual directory
721 |
722 | self.output = \
723 | os.path.splitext(os.path.basename(self.input))[0]
724 |
725 | if not self.options.title:
726 | self.options.title = os.path.basename(self.input)
727 |
728 | if self.options.url and not self.options.url.endswith('/'):
729 | self.options.url += '/'
730 | if self.options.url:
731 | self.options.url += os.path.basename(self.output) + '/'
732 |
733 | # Supported options
734 |
735 | self.resampling = None
736 |
737 | if self.options.resampling == 'average':
738 | try:
739 | if gdal.RegenerateOverview:
740 | pass
741 | except:
742 | self.error("'average' resampling algorithm is not available."
743 | ,
744 | "Please use -r 'near' argument or upgrade to newer version of GDAL."
745 | )
746 | elif self.options.resampling == 'antialias':
747 |
748 | try:
749 | if numpy:
750 | pass
751 | except:
752 | self.error("'antialias' resampling algorithm is not available."
753 | ,
754 | 'Install PIL (Python Imaging Library) and numpy.'
755 | )
756 | elif self.options.resampling == 'near':
757 |
758 | self.resampling = gdal.GRA_NearestNeighbour
759 | self.querysize = self.tilesize
760 | elif self.options.resampling == 'bilinear':
761 |
762 | self.resampling = gdal.GRA_Bilinear
763 | self.querysize = self.tilesize * 2
764 | elif self.options.resampling == 'cubic':
765 |
766 | self.resampling = gdal.GRA_Cubic
767 | elif self.options.resampling == 'cubicspline':
768 |
769 | self.resampling = gdal.GRA_CubicSpline
770 | elif self.options.resampling == 'lanczos':
771 |
772 | self.resampling = gdal.GRA_Lanczos
773 |
774 | # User specified zoom levels
775 |
776 | self.tminz = None
777 | self.tmaxz = None
778 | if self.options.zoom:
779 | minmax = self.options.zoom.split('-', 1)
780 | minmax.extend([''])
781 | (min, max) = minmax[:2]
782 | self.tminz = int(min)
783 | if max:
784 | self.tmaxz = int(max)
785 | else:
786 | self.tmaxz = int(min)
787 |
788 | # KML generation
789 |
790 | self.kml = self.options.kml
791 |
792 | # Output the results
793 |
794 | if self.options.verbose:
795 | print('Options:', self.options)
796 | print('Input:', self.input)
797 | print('Output:', self.output)
798 | print('Cache: %s MB' % (gdal.GetCacheMax() / 1024 / 1024))
799 | print('')
800 |
801 | # -------------------------------------------------------------------------
802 |
803 | def optparse_init(self):
804 | """Prepare the option parser for input (argv)"""
805 |
806 | from optparse import OptionParser, OptionGroup
807 | usage = 'Usage: %prog [options] input_file(s) [output]'
808 | p = OptionParser(usage, version='%prog ' + __version__)
809 | p.add_option(
810 | '-p',
811 | '--profile',
812 | dest='profile',
813 | type='choice',
814 | choices=profile_list,
815 | help="Tile cutting profile (%s) - default 'mercator' (Google Maps compatible)"
816 | % ','.join(profile_list),
817 | )
818 | p.add_option(
819 | '-r',
820 | '--resampling',
821 | dest='resampling',
822 | type='choice',
823 | choices=resampling_list,
824 | help="Resampling method (%s) - default 'average'"
825 | % ','.join(resampling_list),
826 | )
827 | p.add_option('-s', '--s_srs', dest='s_srs', metavar='SRS',
828 | help='The spatial reference system used for the source input data'
829 | )
830 | p.add_option('-z', '--zoom', dest='zoom',
831 | help="Zoom levels to render (format:'2-5' or '10')."
832 | )
833 | p.add_option('-e', '--resume', dest='resume',
834 | action='store_true',
835 | help='Resume mode. Generate only missing files.')
836 | p.add_option('-a', '--srcnodata', dest='srcnodata',
837 | metavar='NODATA',
838 | help='NODATA transparency value to assign to the input data'
839 | )
840 | p.add_option('-d', '--tmscompatible', dest='tmscompatible',
841 | action='store_true',
842 | help='When using the geodetic profile, specifies the base resolution as 0.703125 or 2 tiles at zoom level 0.'
843 | )
844 | p.add_option('-l', '--leaflet', action='store_true',
845 | dest='leaflet',
846 | help="Set 0,0 point to north. For use with 'leaflet'. Requires -p raster. "
847 | )
848 | p.add_option('-v', '--verbose', action='store_true',
849 | dest='verbose',
850 | help='Print status messages to stdout')
851 |
852 | # KML options
853 |
854 | g = OptionGroup(p, 'KML (Google Earth) options',
855 | 'Options for generated Google Earth SuperOverlay metadata'
856 | )
857 | g.add_option('-k', '--force-kml', dest='kml',
858 | action='store_true',
859 | help="Generate KML for Google Earth - default for 'geodetic' profile and 'raster' in EPSG:4326. For a dataset with different projection use with caution!"
860 | )
861 | g.add_option('-n', '--no-kml', dest='kml', action='store_false'
862 | ,
863 | help='Avoid automatic generation of KML files for EPSG:4326'
864 | )
865 | g.add_option('-u', '--url', dest='url',
866 | help='URL address where the generated tiles are going to be published'
867 | )
868 | p.add_option_group(g)
869 |
870 | # HTML options
871 |
872 | g = OptionGroup(p, 'Web viewer options',
873 | 'Options for generated HTML viewers a la Google Maps'
874 | )
875 | g.add_option(
876 | '-w',
877 | '--webviewer',
878 | dest='webviewer',
879 | type='choice',
880 | choices=webviewer_list,
881 | help="Web viewer to generate (%s) - default 'all'"
882 | % ','.join(webviewer_list),
883 | )
884 | g.add_option('-t', '--title', dest='title',
885 | help='Title of the map')
886 | g.add_option('-c', '--copyright', dest='copyright',
887 | help='Copyright for the map')
888 | g.add_option('-g', '--googlekey', dest='googlekey',
889 | help='Google Maps API key from http://code.google.com/apis/maps/signup.html'
890 | )
891 |
892 | (g.add_option('-b', '--bingkey', dest='bingkey',
893 | help='Bing Maps API key from https://www.bingmapsportal.com/'
894 | ), )
895 | p.add_option_group(g)
896 |
897 | # TODO: MapFile + TileIndexes per zoom level for efficient MapServer WMS
898 | # g = OptionGroup(p, "WMS MapServer metadata", "Options for generated mapfile and tileindexes for MapServer")
899 | # g.add_option("-i", "--tileindex", dest='wms', action="store_true"
900 | # help="Generate tileindex and mapfile for MapServer (WMS)")
901 | # p.add_option_group(g)
902 |
903 | p.set_defaults(
904 | verbose=False,
905 | profile='mercator',
906 | kml=False,
907 | url='',
908 | webviewer='all',
909 | copyright='',
910 | resampling='average',
911 | resume=False,
912 | googlekey='INSERT_YOUR_KEY_HERE',
913 | bingkey='INSERT_YOUR_KEY_HERE',
914 | )
915 |
916 | self.parser = p
917 |
918 | # -------------------------------------------------------------------------
919 |
920 | def open_input(self):
921 | """Initialization of the input raster, reprojection if necessary"""
922 |
923 | gdal.AllRegister()
924 |
925 | # Initialize necessary GDAL drivers
926 |
927 | self.out_drv = gdal.GetDriverByName(self.tiledriver)
928 | self.mem_drv = gdal.GetDriverByName('MEM')
929 |
930 | if not self.out_drv:
931 | raise Exception("The '%s' driver was not found, is it available in this GDAL build?"
932 | , self.tiledriver)
933 | if not self.mem_drv:
934 | raise Exception("The 'MEM' driver was not found, is it available in this GDAL build?"
935 | )
936 |
937 | # Open the input file
938 |
939 | if self.input:
940 | self.in_ds = gdal.Open(self.input, gdal.GA_ReadOnly)
941 | else:
942 | raise Exception('No input file was specified')
943 |
944 | if self.options.verbose:
945 | print('Input file:', '( %sP x %sL - %s bands)'
946 | % (self.in_ds.RasterXSize, self.in_ds.RasterYSize,
947 | self.in_ds.RasterCount))
948 |
949 | if not self.in_ds:
950 |
951 | # Note: GDAL prints the ERROR message too
952 |
953 | self.error("It is not possible to open the input file '%s'."
954 | % self.input)
955 |
956 | # Read metadata from the input file
957 |
958 | if self.in_ds.RasterCount == 0:
959 | self.error("Input file '%s' has no raster band"
960 | % self.input)
961 |
962 | if self.in_ds.GetRasterBand(1).GetRasterColorTable():
963 |
964 | # TODO: Process directly paletted dataset by generating VRT in memory
965 |
966 | self.error('Please convert this file to RGB/RGBA and run gdal2tiles on the result.'
967 | ,
968 | """From paletted file you can create RGBA file (temp.vrt) by:
969 | gdal_translate -of vrt -expand rgba %s temp.vrt
970 | then run:
971 | gdal2tiles temp.vrt"""
972 | % self.input)
973 |
974 | # Get NODATA value
975 |
976 | self.in_nodata = []
977 | for i in range(1, self.in_ds.RasterCount + 1):
978 | if self.in_ds.GetRasterBand(i).GetNoDataValue() != None:
979 | self.in_nodata.append(self.in_ds.GetRasterBand(i).GetNoDataValue())
980 | if self.options.srcnodata:
981 | nds = list(map(float, self.options.srcnodata.split(',')))
982 | if len(nds) < self.in_ds.RasterCount:
983 | self.in_nodata = (nds
984 | * self.in_ds.RasterCount)[:self.in_ds.RasterCount]
985 | else:
986 | self.in_nodata = nds
987 |
988 | if self.options.verbose:
989 | print('NODATA: %s' % self.in_nodata)
990 |
991 | #
992 | # Here we should have RGBA input dataset opened in self.in_ds
993 | #
994 |
995 | if self.options.verbose:
996 | print ('Preprocessed file:', '( %sP x %sL - %s bands)'
997 | % (self.in_ds.RasterXSize, self.in_ds.RasterYSize,
998 | self.in_ds.RasterCount))
999 |
1000 | # Spatial Reference System of the input raster
1001 |
1002 | self.in_srs = None
1003 |
1004 | if self.options.s_srs:
1005 | self.in_srs = osr.SpatialReference()
1006 | self.in_srs.SetFromUserInput(self.options.s_srs)
1007 | self.in_srs_wkt = self.in_srs.ExportToWkt()
1008 | else:
1009 | self.in_srs_wkt = self.in_ds.GetProjection()
1010 | if not self.in_srs_wkt and self.in_ds.GetGCPCount() != 0:
1011 | self.in_srs_wkt = self.in_ds.GetGCPProjection()
1012 | if self.in_srs_wkt:
1013 | self.in_srs = osr.SpatialReference()
1014 | self.in_srs.ImportFromWkt(self.in_srs_wkt)
1015 |
1016 | # elif self.options.profile != 'raster':
1017 | # self.error("There is no spatial reference system info included in the input file.","You should run gdal2tiles with --s_srs EPSG:XXXX or similar.")
1018 |
1019 | # Spatial Reference System of tiles
1020 |
1021 | self.out_srs = osr.SpatialReference()
1022 |
1023 | if self.options.profile == 'mercator':
1024 | self.out_srs.ImportFromEPSG(900913)
1025 | elif self.options.profile == 'geodetic':
1026 | self.out_srs.ImportFromEPSG(4326)
1027 | else:
1028 | self.out_srs = self.in_srs
1029 |
1030 | # Are the reference systems the same? Reproject if necessary.
1031 |
1032 | self.out_ds = None
1033 |
1034 | if self.options.profile in ('mercator', 'geodetic'):
1035 |
1036 | if self.in_ds.GetGeoTransform() == (
1037 | 0.0,
1038 | 1.0,
1039 | 0.0,
1040 | 0.0,
1041 | 0.0,
1042 | 1.0,
1043 | ) and self.in_ds.GetGCPCount() == 0:
1044 | self.error("There is no georeference - neither affine transformation (worldfile) nor GCPs. You can generate only 'raster' profile tiles."
1045 | ,
1046 | "Either gdal2tiles with parameter -p 'raster' or use another GIS software for georeference e.g. gdal_transform -gcp / -a_ullr / -a_srs"
1047 | )
1048 |
1049 | if self.in_srs:
1050 |
1051 | if self.in_srs.ExportToProj4() \
1052 | != self.out_srs.ExportToProj4() \
1053 | or self.in_ds.GetGCPCount() != 0:
1054 |
1055 | # Generation of VRT dataset in tile projection, default 'nearest neighbour' warping
1056 |
1057 | self.out_ds = gdal.AutoCreateWarpedVRT(self.in_ds,
1058 | self.in_srs_wkt, self.out_srs.ExportToWkt())
1059 |
1060 | # TODO: HIGH PRIORITY: Correction of AutoCreateWarpedVRT according the max zoomlevel for correct direct warping!!!
1061 |
1062 | if self.options.verbose:
1063 | print("Warping of the raster by AutoCreateWarpedVRT (result saved into 'tiles.vrt')")
1064 | self.out_ds.GetDriver().CreateCopy('tiles.vrt',
1065 | self.out_ds)
1066 |
1067 | # Note: self.in_srs and self.in_srs_wkt contain still the non-warped reference system!!!
1068 |
1069 | # Correction of AutoCreateWarpedVRT for NODATA values
1070 |
1071 | if self.in_nodata != []:
1072 | import tempfile
1073 | tempfilename = tempfile.mktemp('-gdal2tiles.vrt'
1074 | )
1075 | self.out_ds.GetDriver().CreateCopy(tempfilename,
1076 | self.out_ds)
1077 |
1078 | # open as a text file
1079 |
1080 | s = open(tempfilename).read()
1081 |
1082 | # Add the warping options
1083 |
1084 | s = s.replace("""""",
1085 | """
1086 |
1087 | """)
1088 |
1089 | # replace BandMapping tag for NODATA bands....
1090 |
1091 | for i in range(len(self.in_nodata)):
1092 | s = \
1093 | s.replace(""""""
1094 | % (i + 1, i + 1),
1095 | """
1096 | %i
1097 | 0
1098 | %i
1099 | 0
1100 | """
1101 | % (i + 1, i + 1, self.in_nodata[i],
1102 | self.in_nodata[i])) # Or rewrite to white by: , 255 ))
1103 |
1104 | # save the corrected VRT
1105 |
1106 | open(tempfilename, 'w').write(s)
1107 |
1108 | # open by GDAL as self.out_ds
1109 |
1110 | self.out_ds = gdal.Open(tempfilename) # , gdal.GA_ReadOnly)
1111 |
1112 | # delete the temporary file
1113 |
1114 | os.unlink(tempfilename)
1115 |
1116 | # set NODATA_VALUE metadata
1117 |
1118 | self.out_ds.SetMetadataItem('NODATA_VALUES',
1119 | '%i %i %i' % (self.in_nodata[0],
1120 | self.in_nodata[1], self.in_nodata[2]))
1121 |
1122 | if self.options.verbose:
1123 | print("Modified warping result saved into 'tiles1.vrt'")
1124 | open('tiles1.vrt', 'w').write(s)
1125 |
1126 | # -----------------------------------
1127 | # Correction of AutoCreateWarpedVRT for Mono (1 band) and RGB (3 bands) files without NODATA:
1128 | # equivalent of gdalwarp -dstalpha
1129 |
1130 | if self.in_nodata == [] and self.out_ds.RasterCount \
1131 | in [1, 3]:
1132 | import tempfile
1133 | tempfilename = tempfile.mktemp('-gdal2tiles.vrt'
1134 | )
1135 | self.out_ds.GetDriver().CreateCopy(tempfilename,
1136 | self.out_ds)
1137 |
1138 | # open as a text file
1139 |
1140 | s = open(tempfilename).read()
1141 |
1142 | # Add the warping options
1143 |
1144 | s = s.replace("""""",
1145 | """
1146 | Alpha
1147 |
1148 | """
1149 | % (self.out_ds.RasterCount + 1))
1150 | s = s.replace("""""",
1151 | """%i
1152 | """
1153 | % (self.out_ds.RasterCount + 1))
1154 | s = s.replace("""""",
1155 | """
1156 | """
1157 | )
1158 |
1159 | # save the corrected VRT
1160 |
1161 | open(tempfilename, 'w').write(s)
1162 |
1163 | # open by GDAL as self.out_ds
1164 |
1165 | self.out_ds = gdal.Open(tempfilename) # , gdal.GA_ReadOnly)
1166 |
1167 | # delete the temporary file
1168 |
1169 | os.unlink(tempfilename)
1170 |
1171 | if self.options.verbose:
1172 | print("Modified -dstalpha warping result saved into 'tiles1.vrt'")
1173 | open('tiles1.vrt', 'w').write(s)
1174 | s = '''
1175 | '''
1176 | else:
1177 |
1178 | self.error('Input file has unknown SRS.',
1179 | 'Use --s_srs ESPG:xyz (or similar) to provide source reference system.'
1180 | )
1181 |
1182 | if self.out_ds and self.options.verbose:
1183 | print ('Projected file:', 'tiles.vrt',
1184 | '( %sP x %sL - %s bands)'
1185 | % (self.out_ds.RasterXSize,
1186 | self.out_ds.RasterYSize,
1187 | self.out_ds.RasterCount))
1188 |
1189 | if not self.out_ds:
1190 | self.out_ds = self.in_ds
1191 |
1192 | #
1193 | # Here we should have a raster (out_ds) in the correct Spatial Reference system
1194 | #
1195 |
1196 | # Get alpha band (either directly or from NODATA value)
1197 |
1198 | self.alphaband = self.out_ds.GetRasterBand(1).GetMaskBand()
1199 | if self.alphaband.GetMaskFlags() & gdal.GMF_ALPHA \
1200 | or self.out_ds.RasterCount == 4 or self.out_ds.RasterCount \
1201 | == 2:
1202 |
1203 | # TODO: Better test for alpha band in the dataset
1204 |
1205 | self.dataBandsCount = self.out_ds.RasterCount - 1
1206 | else:
1207 | self.dataBandsCount = self.out_ds.RasterCount
1208 |
1209 | # KML test
1210 |
1211 | self.isepsg4326 = False
1212 | srs4326 = osr.SpatialReference()
1213 | srs4326.ImportFromEPSG(4326)
1214 | if self.out_srs and srs4326.ExportToProj4() \
1215 | == self.out_srs.ExportToProj4():
1216 | self.kml = True
1217 | self.isepsg4326 = True
1218 | if self.options.verbose:
1219 | print('KML autotest OK!')
1220 |
1221 | # Read the georeference
1222 |
1223 | self.out_gt = self.out_ds.GetGeoTransform()
1224 |
1225 | # originX, originY = self.out_gt[0], self.out_gt[3]
1226 | # pixelSize = self.out_gt[1] # = self.out_gt[5]
1227 |
1228 | # Test the size of the pixel
1229 |
1230 | # MAPTILER - COMMENTED
1231 | # if self.out_gt[1] != (-1 * self.out_gt[5]) and self.options.profile != 'raster':
1232 | # TODO: Process corectly coordinates with are have swichted Y axis (display in OpenLayers too)
1233 | # self.error("Size of the pixel in the output differ for X and Y axes.")
1234 |
1235 | # Report error in case rotation/skew is in geotransform (possible only in 'raster' profile)
1236 |
1237 | if (self.out_gt[2], self.out_gt[4]) != (0, 0):
1238 | self.error('Georeference of the raster contains rotation or skew. Such raster is not supported. Please use gdalwarp first.'
1239 | )
1240 |
1241 | # TODO: Do the warping in this case automaticaly
1242 |
1243 | #
1244 | # Here we expect: pixel is square, no rotation on the raster
1245 | #
1246 |
1247 | # Output Bounds - coordinates in the output SRS
1248 |
1249 | self.ominx = self.out_gt[0]
1250 | self.omaxx = self.out_gt[0] + self.out_ds.RasterXSize \
1251 | * self.out_gt[1]
1252 | self.omaxy = self.out_gt[3]
1253 | self.ominy = self.out_gt[3] - self.out_ds.RasterYSize \
1254 | * self.out_gt[1]
1255 |
1256 | # Note: maybe round(x, 14) to avoid the gdal_translate behaviour, when 0 becomes -1e-15
1257 |
1258 | if self.options.verbose:
1259 | print ('Bounds (output srs):', round(self.ominx, 13),
1260 | self.ominy, self.omaxx, self.omaxy)
1261 |
1262 | #
1263 | # Calculating ranges for tiles in different zoom levels
1264 | #
1265 |
1266 | if self.options.profile == 'mercator':
1267 |
1268 | self.mercator = GlobalMercator() # from globalmaptiles.py
1269 |
1270 | # Function which generates SWNE in LatLong for given tile
1271 |
1272 | self.tileswne = self.mercator.TileLatLonBounds
1273 |
1274 | # Generate table with min max tile coordinates for all zoomlevels
1275 |
1276 | self.tminmax = list(range(0, 32))
1277 | for tz in range(0, 32):
1278 | (tminx, tminy) = self.mercator.MetersToTile(self.ominx,
1279 | self.ominy, tz)
1280 | (tmaxx, tmaxy) = self.mercator.MetersToTile(self.omaxx,
1281 | self.omaxy, tz)
1282 |
1283 | # crop tiles extending world limits (+-180,+-90)
1284 |
1285 | (tminx, tminy) = (max(0, tminx), max(0, tminy))
1286 | (tmaxx, tmaxy) = (min(2 ** tz - 1, tmaxx), min(2 ** tz
1287 | - 1, tmaxy))
1288 | self.tminmax[tz] = (tminx, tminy, tmaxx, tmaxy)
1289 |
1290 | # TODO: Maps crossing 180E (Alaska?)
1291 |
1292 | # Get the minimal zoom level (map covers area equivalent to one tile)
1293 |
1294 | if self.tminz == None:
1295 | self.tminz = \
1296 | self.mercator.ZoomForPixelSize(self.out_gt[1]
1297 | * max(self.out_ds.RasterXSize,
1298 | self.out_ds.RasterYSize) / float(self.tilesize))
1299 |
1300 | # Get the maximal zoom level (closest possible zoom level up on the resolution of raster)
1301 |
1302 | if self.tmaxz == None:
1303 | self.tmaxz = \
1304 | self.mercator.ZoomForPixelSize(self.out_gt[1])
1305 |
1306 | if self.options.verbose:
1307 | print ('Bounds (latlong):',
1308 | self.mercator.MetersToLatLon(self.ominx,
1309 | self.ominy),
1310 | self.mercator.MetersToLatLon(self.omaxx,
1311 | self.omaxy))
1312 | print ('MinZoomLevel:', self.tminz)
1313 | print ('MaxZoomLevel:', self.tmaxz, '(',
1314 | self.mercator.Resolution(self.tmaxz), ')')
1315 |
1316 | if self.options.profile == 'geodetic':
1317 |
1318 | self.geodetic = GlobalGeodetic(self.options.tmscompatible) # from globalmaptiles.py
1319 |
1320 | # Function which generates SWNE in LatLong for given tile
1321 |
1322 | self.tileswne = self.geodetic.TileLatLonBounds
1323 |
1324 | # Generate table with min max tile coordinates for all zoomlevels
1325 |
1326 | self.tminmax = list(range(0, 32))
1327 | for tz in range(0, 32):
1328 | (tminx, tminy) = self.geodetic.LonLatToTile(self.ominx,
1329 | self.ominy, tz)
1330 | (tmaxx, tmaxy) = self.geodetic.LonLatToTile(self.omaxx,
1331 | self.omaxy, tz)
1332 |
1333 | # crop tiles extending world limits (+-180,+-90)
1334 |
1335 | (tminx, tminy) = (max(0, tminx), max(0, tminy))
1336 | (tmaxx, tmaxy) = (min(2 ** (tz + 1) - 1, tmaxx), min(2
1337 | ** tz - 1, tmaxy))
1338 | self.tminmax[tz] = (tminx, tminy, tmaxx, tmaxy)
1339 |
1340 | # TODO: Maps crossing 180E (Alaska?)
1341 |
1342 | # Get the maximal zoom level (closest possible zoom level up on the resolution of raster)
1343 |
1344 | if self.tminz == None:
1345 | self.tminz = \
1346 | self.geodetic.ZoomForPixelSize(self.out_gt[1]
1347 | * max(self.out_ds.RasterXSize,
1348 | self.out_ds.RasterYSize) / float(self.tilesize))
1349 |
1350 | # Get the maximal zoom level (closest possible zoom level up on the resolution of raster)
1351 |
1352 | if self.tmaxz == None:
1353 | self.tmaxz = \
1354 | self.geodetic.ZoomForPixelSize(self.out_gt[1])
1355 |
1356 | if self.options.verbose:
1357 | print ('Bounds (latlong):', self.ominx, self.ominy,
1358 | self.omaxx, self.omaxy)
1359 |
1360 | if self.options.profile == 'raster':
1361 |
1362 | log2 = lambda x: math.log10(x) / math.log10(2) # log2 (base 2 logarithm)
1363 |
1364 | self.nativezoom = \
1365 | int(max(math.ceil(log2(self.out_ds.RasterXSize
1366 | / float(self.tilesize))),
1367 | math.ceil(log2(self.out_ds.RasterYSize
1368 | / float(self.tilesize)))))
1369 |
1370 | if int(self.tmaxz or 0) < self.nativezoom:
1371 | self.tmaxz = self.nativezoom
1372 |
1373 | if self.options.verbose:
1374 | print ('Native zoom of the raster:', self.nativezoom)
1375 |
1376 | # Get the minimal zoom level (whole raster in one tile)
1377 |
1378 | if self.tminz == None:
1379 | self.tminz = 0
1380 |
1381 | # Get the maximal zoom level (native resolution of the raster)
1382 |
1383 | if self.tmaxz == None:
1384 | self.tmaxz = self.nativezoom
1385 |
1386 | # Generate table with min max tile coordinates for all zoomlevels
1387 |
1388 | self.tminmax = list(range(0, self.tmaxz + 1))
1389 | self.tsize = list(range(0, self.tmaxz + 1))
1390 | for tz in range(0, self.tmaxz + 1):
1391 | tsize = 2.0 ** (self.nativezoom - tz) * self.tilesize
1392 | (tminx, tminy) = (0, 0)
1393 | tmaxx = int(math.ceil(self.out_ds.RasterXSize / tsize)) \
1394 | - 1
1395 | tmaxy = int(math.ceil(self.out_ds.RasterYSize / tsize)) \
1396 | - 1
1397 | self.tsize[tz] = math.ceil(tsize)
1398 | self.tminmax[tz] = (tminx, tminy, tmaxx, tmaxy)
1399 |
1400 | # Function which generates SWNE in LatLong for given tile
1401 |
1402 | if self.kml and self.in_srs_wkt:
1403 | self.ct = osr.CoordinateTransformation(self.in_srs,
1404 | srs4326)
1405 |
1406 | def rastertileswne(x, y, z):
1407 | pixelsizex = 2 ** (self.tmaxz - z) * self.out_gt[1] # X-pixel size in level
1408 | pixelsizey = 2 ** (self.tmaxz - z) * self.out_gt[1] # Y-pixel size in level (usually -1*pixelsizex)
1409 | west = self.out_gt[0] + x * self.tilesize \
1410 | * pixelsizex
1411 | east = west + self.tilesize * pixelsizex
1412 | south = self.ominy + y * self.tilesize * pixelsizex
1413 | north = south + self.tilesize * pixelsizex
1414 | if not self.isepsg4326:
1415 |
1416 | # Transformation to EPSG:4326 (WGS84 datum)
1417 |
1418 | (west, south) = self.ct.TransformPoint(west,
1419 | south)[:2]
1420 | (east, north) = self.ct.TransformPoint(east,
1421 | north)[:2]
1422 | return (south, west, north, east)
1423 |
1424 | self.tileswne = rastertileswne
1425 | else:
1426 | self.tileswne = lambda x, y, z: (0, 0, 0, 0)
1427 |
1428 | # -------------------------------------------------------------------------
1429 |
1430 | def generate_metadata(self):
1431 | """Generation of main metadata files and HTML viewers (metadata related to particular tiles are generated during the tile processing)."""
1432 |
1433 | if not os.path.exists(self.output):
1434 | os.makedirs(self.output)
1435 |
1436 | if self.options.profile == 'mercator':
1437 |
1438 | (south, west) = self.mercator.MetersToLatLon(self.ominx,
1439 | self.ominy)
1440 | (north, east) = self.mercator.MetersToLatLon(self.omaxx,
1441 | self.omaxy)
1442 | (south, west) = (max(-85.05112878, south), max(-180.0,
1443 | west))
1444 | (north, east) = (min(85.05112878, north), min(180.0, east))
1445 | self.swne = (south, west, north, east)
1446 |
1447 | # Generate googlemaps.html
1448 |
1449 | if self.options.webviewer in ('all', 'google') \
1450 | and self.options.profile == 'mercator':
1451 | if not self.options.resume \
1452 | or not os.path.exists(os.path.join(self.output,
1453 | 'googlemaps.html')):
1454 | f = open(os.path.join(self.output, 'googlemaps.html'
1455 | ), 'w')
1456 | f.write(self.generate_googlemaps())
1457 | f.close()
1458 |
1459 | # Generate openlayers.html
1460 |
1461 | if self.options.webviewer in ('all', 'openlayers'):
1462 | if not self.options.resume \
1463 | or not os.path.exists(os.path.join(self.output,
1464 | 'openlayers.html')):
1465 | f = open(os.path.join(self.output, 'openlayers.html'
1466 | ), 'w')
1467 | f.write(self.generate_openlayers())
1468 | f.close()
1469 | elif self.options.profile == 'geodetic':
1470 |
1471 | (west, south) = (self.ominx, self.ominy)
1472 | (east, north) = (self.omaxx, self.omaxy)
1473 | (south, west) = (max(-90.0, south), max(-180.0, west))
1474 | (north, east) = (min(90.0, north), min(180.0, east))
1475 | self.swne = (south, west, north, east)
1476 |
1477 | # Generate openlayers.html
1478 |
1479 | if self.options.webviewer in ('all', 'openlayers'):
1480 | if not self.options.resume \
1481 | or not os.path.exists(os.path.join(self.output,
1482 | 'openlayers.html')):
1483 | f = open(os.path.join(self.output, 'openlayers.html'
1484 | ), 'w')
1485 | f.write(self.generate_openlayers())
1486 | f.close()
1487 | elif self.options.profile == 'raster':
1488 |
1489 | (west, south) = (self.ominx, self.ominy)
1490 | (east, north) = (self.omaxx, self.omaxy)
1491 |
1492 | self.swne = (south, west, north, east)
1493 |
1494 | # Generate openlayers.html
1495 |
1496 | if self.options.webviewer in ('all', 'openlayers'):
1497 | if not self.options.resume \
1498 | or not os.path.exists(os.path.join(self.output,
1499 | 'openlayers.html')):
1500 | f = open(os.path.join(self.output, 'openlayers.html'
1501 | ), 'w')
1502 | f.write(self.generate_openlayers())
1503 | f.close()
1504 |
1505 | # Generate tilemapresource.xml.
1506 |
1507 | if not self.options.resume \
1508 | or not os.path.exists(os.path.join(self.output,
1509 | 'tilemapresource.xml')):
1510 | f = open(os.path.join(self.output, 'tilemapresource.xml'),
1511 | 'w')
1512 | f.write(self.generate_tilemapresource())
1513 | f.close()
1514 |
1515 | if self.kml:
1516 |
1517 | # TODO: Maybe problem for not automatically generated tminz
1518 | # The root KML should contain links to all tiles in the tminz level
1519 |
1520 | children = []
1521 | (xmin, ymin, xmax, ymax) = self.tminmax[self.tminz]
1522 | for x in range(xmin, xmax + 1):
1523 | for y in range(ymin, ymax + 1):
1524 | children.append([x, y, self.tminz])
1525 |
1526 | # Generate Root KML
1527 |
1528 | if self.kml:
1529 | if not self.options.resume \
1530 | or not os.path.exists(os.path.join(self.output,
1531 | 'doc.kml')):
1532 | f = open(os.path.join(self.output, 'doc.kml'), 'w')
1533 | f.write(self.generate_kml(None, None, None,
1534 | children))
1535 | f.close()
1536 |
1537 | # -------------------------------------------------------------------------
1538 |
1539 | def generate_base_tiles(self):
1540 | """Generation of the base tiles (the lowest in the pyramid) directly from the input raster"""
1541 |
1542 | print('Generating Base Tiles:')
1543 |
1544 | if self.options.verbose:
1545 |
1546 | # mx, my = self.out_gt[0], self.out_gt[3] # OriginX, OriginY
1547 | # px, py = self.mercator.MetersToPixels( mx, my, self.tmaxz)
1548 | # print("Pixel coordinates:", px, py, (mx, my))
1549 |
1550 | print('')
1551 | print('Tiles generated from the max zoom level:')
1552 | print('----------------------------------------')
1553 | print('')
1554 |
1555 | # Set the bounds
1556 |
1557 | (tminx, tminy, tmaxx, tmaxy) = self.tminmax[self.tmaxz]
1558 |
1559 | # Just the center tile
1560 | # tminx = tminx+ (tmaxx - tminx)/2
1561 | # tminy = tminy+ (tmaxy - tminy)/2
1562 | # tmaxx = tminx
1563 | # tmaxy = tminy
1564 |
1565 | ds = self.out_ds
1566 | tilebands = self.dataBandsCount + 1
1567 | querysize = self.querysize
1568 |
1569 | if self.options.verbose:
1570 | print ('dataBandsCount: ', self.dataBandsCount)
1571 | print ('tilebands: ', tilebands)
1572 |
1573 | # print(tminx, tminy, tmaxx, tmaxy)
1574 |
1575 | tcount = (1 + abs(tmaxx - tminx)) * (1 + abs(tmaxy - tminy))
1576 |
1577 | # print(tcount)
1578 |
1579 | ti = 0
1580 |
1581 | tz = self.tmaxz
1582 | yrange = range(tmaxy, tminy - 1, -1)
1583 | if self.options.leaflet:
1584 | yrange = range(tminy, tmaxy + 1)
1585 |
1586 | for ty in yrange:
1587 | for tx in range(tminx, tmaxx + 1):
1588 |
1589 | if self.stopped:
1590 | break
1591 | ti += 1
1592 | tilefilename = os.path.join(self.output, str(tz),
1593 | str(tx), '%s.%s' % (ty, self.tileext))
1594 | if self.options.verbose:
1595 | print (ti, '/', tcount, tilefilename) # , "( TileMapService: z / x / y )"
1596 |
1597 | if self.options.resume and os.path.exists(tilefilename):
1598 | if self.options.verbose:
1599 | print('Tile generation skiped because of --resume')
1600 | else:
1601 | self.progressbar(ti / float(tcount))
1602 | continue
1603 |
1604 | # Create directories for the tile
1605 |
1606 | if not os.path.exists(os.path.dirname(tilefilename)):
1607 | os.makedirs(os.path.dirname(tilefilename))
1608 |
1609 | if self.options.profile == 'mercator':
1610 |
1611 | # Tile bounds in EPSG:900913
1612 |
1613 | b = self.mercator.TileBounds(tx, ty, tz)
1614 | elif self.options.profile == 'geodetic':
1615 | b = self.geodetic.TileBounds(tx, ty, tz)
1616 |
1617 | # 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))
1618 |
1619 | # Don't scale up by nearest neighbour, better change the querysize
1620 | # to the native resolution (and return smaller query tile) for scaling
1621 |
1622 | if self.options.profile in ('mercator', 'geodetic'):
1623 | (rb, wb) = self.geo_query(ds, b[0], b[3], b[2],
1624 | b[1])
1625 | nativesize = wb[0] + wb[2] # Pixel size in the raster covering query geo extent
1626 | if self.options.verbose:
1627 | print ('\tNative Extent (querysize',
1628 | nativesize, '): ', rb, wb)
1629 |
1630 | # Tile bounds in raster coordinates for ReadRaster query
1631 |
1632 | (rb, wb) = self.geo_query(
1633 | ds,
1634 | b[0],
1635 | b[3],
1636 | b[2],
1637 | b[1],
1638 | querysize=querysize,
1639 | )
1640 |
1641 | (rx, ry, rxsize, rysize) = rb
1642 | (wx, wy, wxsize, wysize) = wb
1643 | else:
1644 |
1645 | # 'raster' profile:
1646 |
1647 | tsize = int(self.tsize[tz]) # tilesize in raster coordinates for actual zoom
1648 | xsize = self.out_ds.RasterXSize # size of the raster in pixels
1649 | ysize = self.out_ds.RasterYSize
1650 | if tz >= self.nativezoom:
1651 | querysize = self.tilesize # int(2**(self.nativezoom-tz) * self.tilesize)
1652 |
1653 | rx = tx * tsize
1654 | rxsize = 0
1655 | if tx == tmaxx:
1656 | rxsize = xsize % tsize
1657 | if rxsize == 0:
1658 | rxsize = tsize
1659 |
1660 | rysize = 0
1661 | if ty == tmaxy:
1662 | rysize = ysize % tsize
1663 | if rysize == 0:
1664 | rysize = tsize
1665 | if self.options.leaflet:
1666 | ry = ty * tsize
1667 | else:
1668 | ry = ysize - ty * tsize - rysize
1669 |
1670 | (wx, wy) = (0, 0)
1671 | (wxsize, wysize) = (int(rxsize / float(tsize)
1672 | * self.tilesize), int(rysize / float(tsize)
1673 | * self.tilesize))
1674 | if not self.options.leaflet:
1675 | if wysize != self.tilesize:
1676 | wy = self.tilesize - wysize
1677 |
1678 | if self.options.verbose:
1679 | print ('\tReadRaster Extent: ', (rx, ry, rxsize,
1680 | rysize), (wx, wy, wxsize, wysize))
1681 |
1682 | # Query is in 'nearest neighbour' but can be bigger in then the tilesize
1683 | # We scale down the query to the tilesize by supplied algorithm.
1684 |
1685 | # Tile dataset in memory
1686 |
1687 | dstile = self.mem_drv.Create('', self.tilesize,
1688 | self.tilesize, tilebands)
1689 | data = ds.ReadRaster(
1690 | rx,
1691 | ry,
1692 | rxsize,
1693 | rysize,
1694 | wxsize,
1695 | wysize,
1696 | band_list=list(range(1, self.dataBandsCount + 1)),
1697 | )
1698 | alpha = self.alphaband.ReadRaster(
1699 | rx,
1700 | ry,
1701 | rxsize,
1702 | rysize,
1703 | wxsize,
1704 | wysize,
1705 | )
1706 |
1707 | if self.tilesize == querysize:
1708 |
1709 | # Use the ReadRaster result directly in tiles ('nearest neighbour' query)
1710 |
1711 | dstile.WriteRaster(
1712 | wx,
1713 | wy,
1714 | wxsize,
1715 | wysize,
1716 | data,
1717 | band_list=list(range(1, self.dataBandsCount
1718 | + 1)),
1719 | )
1720 | dstile.WriteRaster(
1721 | wx,
1722 | wy,
1723 | wxsize,
1724 | wysize,
1725 | alpha,
1726 | band_list=[tilebands],
1727 | )
1728 | else:
1729 |
1730 | # Note: For source drivers based on WaveLet compression (JPEG2000, ECW, MrSID)
1731 | # the ReadRaster function returns high-quality raster (not ugly nearest neighbour)
1732 | # TODO: Use directly 'near' for WaveLet files
1733 | # Big ReadRaster query in memory scaled to the tilesize - all but 'near' algo
1734 |
1735 | dsquery = self.mem_drv.Create('', querysize,
1736 | querysize, tilebands)
1737 |
1738 | # TODO: fill the null value in case a tile without alpha is produced (now only png tiles are supported)
1739 | # for i in range(1, tilebands+1):
1740 | # dsquery.GetRasterBand(1).Fill(tilenodata)
1741 |
1742 | dsquery.WriteRaster(
1743 | wx,
1744 | wy,
1745 | wxsize,
1746 | wysize,
1747 | data,
1748 | band_list=list(range(1, self.dataBandsCount
1749 | + 1)),
1750 | )
1751 | dsquery.WriteRaster(
1752 | wx,
1753 | wy,
1754 | wxsize,
1755 | wysize,
1756 | alpha,
1757 | band_list=[tilebands],
1758 | )
1759 |
1760 | self.scale_query_to_tile(dsquery, dstile,
1761 | tilefilename)
1762 | del dsquery
1763 |
1764 | del data
1765 |
1766 | if self.options.resampling != 'antialias':
1767 |
1768 | # Write a copy of tile to png/jpg
1769 |
1770 | self.out_drv.CreateCopy(tilefilename, dstile,
1771 | strict=0)
1772 |
1773 | del dstile
1774 |
1775 | # Create a KML file for this tile.
1776 |
1777 | if self.kml:
1778 | kmlfilename = os.path.join(self.output, str(tz),
1779 | str(tx), '%d.kml' % ty)
1780 | if not self.options.resume \
1781 | or not os.path.exists(kmlfilename):
1782 | f = open(kmlfilename, 'w')
1783 | f.write(self.generate_kml(tx, ty, tz))
1784 | f.close()
1785 |
1786 | if not self.options.verbose:
1787 | self.progressbar(ti / float(tcount))
1788 |
1789 | # -------------------------------------------------------------------------
1790 |
1791 | def generate_overview_tiles(self):
1792 | """Generation of the overview tiles (higher in the pyramid) based on existing tiles"""
1793 |
1794 | print('Generating Overview Tiles:')
1795 |
1796 | tilebands = self.dataBandsCount + 1
1797 |
1798 | # Usage of existing tiles: from 4 underlying tiles generate one as overview.
1799 |
1800 | tcount = 0
1801 | for tz in range(self.tmaxz - 1, self.tminz - 1, -1):
1802 | (tminx, tminy, tmaxx, tmaxy) = self.tminmax[tz]
1803 | tcount += (1 + abs(tmaxx - tminx)) * (1 + abs(tmaxy
1804 | - tminy))
1805 |
1806 | ti = 0
1807 |
1808 | # querysize = tilesize * 2
1809 |
1810 | for tz in range(self.tmaxz - 1, self.tminz - 1, -1):
1811 | (tminx, tminy, tmaxx, tmaxy) = self.tminmax[tz]
1812 | yrange = range(tmaxy, tminy - 1, -1)
1813 | if self.options.leaflet:
1814 | yrange = range(tminy, tmaxy + 1)
1815 | for ty in yrange:
1816 | for tx in range(tminx, tmaxx + 1):
1817 |
1818 | if self.stopped:
1819 | break
1820 |
1821 | ti += 1
1822 | tilefilename = os.path.join(self.output, str(tz),
1823 | str(tx), '%s.%s' % (ty, self.tileext))
1824 |
1825 | if self.options.verbose:
1826 | print (ti, '/', tcount, tilefilename) # , "( TileMapService: z / x / y )"
1827 |
1828 | if self.options.resume \
1829 | and os.path.exists(tilefilename):
1830 | if self.options.verbose:
1831 | print('Tile generation skiped because of --resume')
1832 | else:
1833 | self.progressbar(ti / float(tcount))
1834 | continue
1835 |
1836 | # Create directories for the tile
1837 |
1838 | if not os.path.exists(os.path.dirname(tilefilename)):
1839 | os.makedirs(os.path.dirname(tilefilename))
1840 |
1841 | dsquery = self.mem_drv.Create('', 2
1842 | * self.tilesize, 2 * self.tilesize,
1843 | tilebands)
1844 |
1845 | # TODO: fill the null value
1846 | # for i in range(1, tilebands+1):
1847 | # dsquery.GetRasterBand(1).Fill(tilenodata)
1848 |
1849 | dstile = self.mem_drv.Create('', self.tilesize,
1850 | self.tilesize, tilebands)
1851 |
1852 | # TODO: Implement more clever walking on the tiles with cache functionality
1853 | # probably walk should start with reading of four tiles from top left corner
1854 | # Hilbert curve
1855 |
1856 | children = []
1857 |
1858 | # Read the tiles and write them to query window
1859 |
1860 | for y in range(2 * ty, 2 * ty + 2):
1861 | for x in range(2 * tx, 2 * tx + 2):
1862 | (minx, miny, maxx, maxy) = self.tminmax[tz
1863 | + 1]
1864 | if x >= minx and x <= maxx and y >= miny \
1865 | and y <= maxy:
1866 | dsquerytile = \
1867 | gdal.Open(os.path.join(self.output,
1868 | str(tz + 1), str(x), '%s.%s'
1869 | % (y, self.tileext)),
1870 | gdal.GA_ReadOnly)
1871 |
1872 | if self.options.leaflet:
1873 | if ty:
1874 | tileposy = y % (2 * ty) \
1875 | * self.tilesize
1876 | elif ty == 0 and y == 1:
1877 | tileposy = self.tilesize
1878 | else:
1879 | tileposy = 0
1880 | else:
1881 | if ty == 0 and y == 1 or ty != 0 \
1882 | and y % (2 * ty) != 0:
1883 | tileposy = 0
1884 | else:
1885 | tileposy = self.tilesize
1886 |
1887 | if tx:
1888 | tileposx = x % (2 * tx) \
1889 | * self.tilesize
1890 | elif tx == 0 and x == 1:
1891 | tileposx = self.tilesize
1892 | else:
1893 | tileposx = 0
1894 | dsquery.WriteRaster(
1895 | tileposx,
1896 | tileposy,
1897 | self.tilesize,
1898 | self.tilesize,
1899 | dsquerytile.ReadRaster(0, 0,
1900 | self.tilesize, self.tilesize),
1901 | band_list=list(range(1, tilebands
1902 | + 1)),
1903 | )
1904 | children.append([x, y, tz + 1])
1905 |
1906 | self.scale_query_to_tile(dsquery, dstile,
1907 | tilefilename)
1908 |
1909 | # Write a copy of tile to png/jpg
1910 |
1911 | if self.options.resampling != 'antialias':
1912 |
1913 | # Write a copy of tile to png/jpg
1914 |
1915 | self.out_drv.CreateCopy(tilefilename, dstile,
1916 | strict=0)
1917 |
1918 | if self.options.verbose:
1919 | print (
1920 | '\tbuild from zoom',
1921 | tz + 1,
1922 | ' tiles:',
1923 | (2 * tx, 2 * ty),
1924 | (2 * tx + 1, 2 * ty),
1925 | (2 * tx, 2 * ty + 1),
1926 | (2 * tx + 1, 2 * ty + 1),
1927 | )
1928 |
1929 | # Create a KML file for this tile.
1930 |
1931 | if self.kml:
1932 | f = open(os.path.join(self.output,
1933 | '%d/%d/%d.kml' % (tz, tx, ty)), 'w')
1934 | f.write(self.generate_kml(tx, ty, tz, children))
1935 | f.close()
1936 |
1937 | if not self.options.verbose:
1938 | self.progressbar(ti / float(tcount))
1939 |
1940 | # -------------------------------------------------------------------------
1941 |
1942 | def geo_query(
1943 | self,
1944 | ds,
1945 | ulx,
1946 | uly,
1947 | lrx,
1948 | lry,
1949 | querysize=0,
1950 | ):
1951 | """For given dataset and query in cartographic coordinates
1952 | returns parameters for ReadRaster() in raster coordinates and
1953 | x/y shifts (for border tiles). If the querysize is not given, the
1954 | extent is returned in the native resolution of dataset ds."""
1955 |
1956 | geotran = ds.GetGeoTransform()
1957 | rx = int((ulx - geotran[0]) / geotran[1] + 0.001)
1958 | ry = int((uly - geotran[3]) / geotran[5] + 0.001)
1959 | rxsize = int((lrx - ulx) / geotran[1] + 0.5)
1960 | rysize = int((lry - uly) / geotran[5] + 0.5)
1961 |
1962 | if not querysize:
1963 | (wxsize, wysize) = (rxsize, rysize)
1964 | else:
1965 | (wxsize, wysize) = (querysize, querysize)
1966 |
1967 | # Coordinates should not go out of the bounds of the raster
1968 |
1969 | wx = 0
1970 | if rx < 0:
1971 | rxshift = abs(rx)
1972 | wx = int(wxsize * (float(rxshift) / rxsize))
1973 | wxsize = wxsize - wx
1974 | rxsize = rxsize - int(rxsize * (float(rxshift) / rxsize))
1975 | rx = 0
1976 | if rx + rxsize > ds.RasterXSize:
1977 | wxsize = int(wxsize * (float(ds.RasterXSize - rx) / rxsize))
1978 | rxsize = ds.RasterXSize - rx
1979 |
1980 | wy = 0
1981 | if ry < 0:
1982 | ryshift = abs(ry)
1983 | wy = int(wysize * (float(ryshift) / rysize))
1984 | wysize = wysize - wy
1985 | rysize = rysize - int(rysize * (float(ryshift) / rysize))
1986 | ry = 0
1987 | if ry + rysize > ds.RasterYSize:
1988 | wysize = int(wysize * (float(ds.RasterYSize - ry) / rysize))
1989 | rysize = ds.RasterYSize - ry
1990 |
1991 | return ((rx, ry, rxsize, rysize), (wx, wy, wxsize, wysize))
1992 |
1993 | # -------------------------------------------------------------------------
1994 |
1995 | def scale_query_to_tile(
1996 | self,
1997 | dsquery,
1998 | dstile,
1999 | tilefilename='',
2000 | ):
2001 | """Scales down query dataset to the tile dataset"""
2002 |
2003 | querysize = dsquery.RasterXSize
2004 | tilesize = dstile.RasterXSize
2005 | tilebands = dstile.RasterCount
2006 |
2007 | if self.options.resampling == 'average':
2008 |
2009 | # Function: gdal.RegenerateOverview()
2010 |
2011 | for i in range(1, tilebands + 1):
2012 |
2013 | # Black border around NODATA
2014 | # if i != 4:
2015 | # dsquery.GetRasterBand(i).SetNoDataValue(0)
2016 |
2017 | res = gdal.RegenerateOverview(dsquery.GetRasterBand(i),
2018 | dstile.GetRasterBand(i), 'average')
2019 | if res != 0:
2020 | self.error('RegenerateOverview() failed on %s, error %d'
2021 | % (tilefilename, res))
2022 | elif self.options.resampling == 'antialias':
2023 |
2024 | # Scaling by PIL (Python Imaging Library) - improved Lanczos
2025 |
2026 | array = numpy.zeros((querysize, querysize, tilebands),
2027 | numpy.uint8)
2028 | for i in range(tilebands):
2029 | array[:, :, i] = \
2030 | gdalarray.BandReadAsArray(dsquery.GetRasterBand(i
2031 | + 1), 0, 0, querysize, querysize)
2032 | im = Image.fromarray(array, 'RGBA') # Always four bands
2033 | im1 = im.resize((tilesize, tilesize), Image.ANTIALIAS)
2034 | if os.path.exists(tilefilename):
2035 | im0 = Image.open(tilefilename)
2036 | im1 = Image.composite(im1, im0, im1)
2037 | im1.save(tilefilename, self.tiledriver)
2038 | else:
2039 |
2040 | # Other algorithms are implemented by gdal.ReprojectImage().
2041 |
2042 | dsquery.SetGeoTransform((
2043 | 0.0,
2044 | tilesize / float(querysize),
2045 | 0.0,
2046 | 0.0,
2047 | 0.0,
2048 | tilesize / float(querysize),
2049 | ))
2050 | dstile.SetGeoTransform((
2051 | 0.0,
2052 | 1.0,
2053 | 0.0,
2054 | 0.0,
2055 | 0.0,
2056 | 1.0,
2057 | ))
2058 |
2059 | res = gdal.ReprojectImage(dsquery, dstile, None, None,
2060 | self.resampling)
2061 | if res != 0:
2062 | self.error('ReprojectImage() failed on %s, error %d'
2063 | % (tilefilename, res))
2064 |
2065 | # -------------------------------------------------------------------------
2066 |
2067 | def generate_tilemapresource(self):
2068 | """
2069 | Template for tilemapresource.xml. Returns filled string. Expected variables:
2070 | title, north, south, east, west, isepsg4326, projection, publishurl,
2071 | zoompixels, tilesize, tileformat, profile
2072 | """
2073 |
2074 | args = {}
2075 | args['title'] = self.options.title
2076 | (args['south'], args['west'], args['north'], args['east']) = \
2077 | self.swne
2078 | args['tilesize'] = self.tilesize
2079 | args['tileformat'] = self.tileext
2080 | args['publishurl'] = self.options.url
2081 | args['profile'] = self.options.profile
2082 |
2083 | if self.options.profile == 'mercator':
2084 | args['srs'] = 'EPSG:900913'
2085 | elif self.options.profile == 'geodetic':
2086 | args['srs'] = 'EPSG:4326'
2087 | elif self.options.s_srs:
2088 | args['srs'] = self.options.s_srs
2089 | elif self.out_srs:
2090 | args['srs'] = self.out_srs.ExportToWkt()
2091 | else:
2092 | args['srs'] = ''
2093 |
2094 | s = \
2095 | """
2096 |
2097 | %(title)s
2098 |
2099 | %(srs)s
2100 |
2101 |
2102 |
2103 |
2104 | """ \
2105 | % args
2106 | for z in range(self.tminz, self.tmaxz + 1):
2107 | if self.options.profile == 'raster':
2108 | s += \
2109 | """ \n""" \
2110 | % (args['publishurl'], z, 2 ** (self.nativezoom
2111 | - z) * self.out_gt[1], z)
2112 | elif self.options.profile == 'mercator':
2113 | s += \
2114 | """ \n""" \
2115 | % (args['publishurl'], z, 156543.0339 / 2 ** z, z)
2116 | elif self.options.profile == 'geodetic':
2117 | s += \
2118 | """ \n""" \
2119 | % (args['publishurl'], z, 0.703125 / 2 ** z, z)
2120 | s += """
2121 |
2122 | """
2123 | return s
2124 |
2125 | # -------------------------------------------------------------------------
2126 |
2127 | def generate_kml(
2128 | self,
2129 | tx,
2130 | ty,
2131 | tz,
2132 | children=[],
2133 | **args
2134 | ):
2135 | """
2136 | Template for the KML. Returns filled string.
2137 | """
2138 |
2139 | (args['tx'], args['ty'], args['tz']) = (tx, ty, tz)
2140 | args['tileformat'] = self.tileext
2141 | if 'tilesize' not in args:
2142 | args['tilesize'] = self.tilesize
2143 |
2144 | if 'minlodpixels' not in args:
2145 | args['minlodpixels'] = int(args['tilesize'] / 2) # / 2.56) # default 128
2146 | if 'maxlodpixels' not in args:
2147 | args['maxlodpixels'] = int(args['tilesize'] * 8) # 1.7) # default 2048 (used to be -1)
2148 | if children == []:
2149 | args['maxlodpixels'] = -1
2150 |
2151 | if tx == None:
2152 | tilekml = False
2153 | args['title'] = self.options.title
2154 | else:
2155 | tilekml = True
2156 | args['title'] = '%d/%d/%d.kml' % (tz, tx, ty)
2157 | (args['south'], args['west'], args['north'], args['east'
2158 | ]) = self.tileswne(tx, ty, tz)
2159 |
2160 | if tx == 0:
2161 | args['drawOrder'] = 2 * tz + 1
2162 | elif tx != None:
2163 | args['drawOrder'] = 2 * tz
2164 | else:
2165 | args['drawOrder'] = 0
2166 |
2167 | url = self.options.url
2168 | if not url:
2169 | if tilekml:
2170 | url = '../../'
2171 | else:
2172 | url = ''
2173 |
2174 | s = \
2175 | """
2176 |
2177 |
2178 | %(title)s
2179 |
2180 | """ \
2185 | % args
2186 | if tilekml:
2187 | s += \
2188 | """
2189 |
2190 |
2191 | %(north).14f
2192 | %(south).14f
2193 | %(east).14f
2194 | %(west).14f
2195 |
2196 |
2197 | %(minlodpixels)d
2198 | %(maxlodpixels)d
2199 |
2200 |
2201 |
2202 | %(drawOrder)d
2203 |
2204 | %(ty)d.%(tileformat)s
2205 |
2206 |
2207 | %(north).14f
2208 | %(south).14f
2209 | %(east).14f
2210 | %(west).14f
2211 |
2212 |
2213 | """ \
2214 | % args
2215 |
2216 | for (cx, cy, cz) in children:
2217 | (csouth, cwest, cnorth, ceast) = self.tileswne(cx, cy, cz)
2218 | s += \
2219 | """
2220 |
2221 | %d/%d/%d.%s
2222 |
2223 |
2224 | %.14f
2225 | %.14f
2226 | %.14f
2227 | %.14f
2228 |
2229 |
2230 | %d
2231 | -1
2232 |
2233 |
2234 |
2235 | %s%d/%d/%d.kml
2236 | onRegion
2237 |
2238 |
2239 |
2240 | """ \
2241 | % (
2242 | cz,
2243 | cx,
2244 | cy,
2245 | args['tileformat'],
2246 | cnorth,
2247 | csouth,
2248 | ceast,
2249 | cwest,
2250 | args['minlodpixels'],
2251 | url,
2252 | cz,
2253 | cx,
2254 | cy,
2255 | )
2256 |
2257 | s += """
2258 |
2259 | """
2260 | return s
2261 |
2262 | # -------------------------------------------------------------------------
2263 |
2264 | def generate_googlemaps(self):
2265 | """
2266 | Template for googlemaps.html implementing Overlay of tiles for 'mercator' profile.
2267 | It returns filled string. Expected variables:
2268 | title, googlemapskey, north, south, east, west, minzoom, maxzoom, tilesize, tileformat, publishurl
2269 | """
2270 |
2271 | args = {}
2272 | args['title'] = self.options.title
2273 | args['googlemapskey'] = self.options.googlekey
2274 | (args['south'], args['west'], args['north'], args['east']) = \
2275 | self.swne
2276 | args['minzoom'] = self.tminz
2277 | args['maxzoom'] = self.tmaxz
2278 | args['tilesize'] = self.tilesize
2279 | args['tileformat'] = self.tileext
2280 | args['publishurl'] = self.options.url
2281 | args['copyright'] = self.options.copyright
2282 |
2283 | s = \
2284 | """
2285 |
2286 |
2287 | %(title)s
2288 |
2289 |
2290 |
2298 |
2299 |
2556 |
2557 |
2558 |
2559 |
2562 |
2563 |
2564 |
2565 | """ \
2566 | % args
2567 |
2568 | return s
2569 |
2570 | # -------------------------------------------------------------------------
2571 |
2572 | def generate_openlayers(self):
2573 | """
2574 | Template for openlayers.html implementing overlay of available Spherical Mercator layers.
2575 |
2576 | It returns filled string. Expected variables:
2577 | title, bingkey, north, south, east, west, minzoom, maxzoom, tilesize, tileformat, publishurl
2578 | """
2579 |
2580 | args = {}
2581 | args['title'] = self.options.title
2582 | args['bingkey'] = self.options.bingkey
2583 | (args['south'], args['west'], args['north'], args['east']) = \
2584 | self.swne
2585 | args['minzoom'] = self.tminz
2586 | args['maxzoom'] = self.tmaxz
2587 | args['tilesize'] = self.tilesize
2588 | args['tileformat'] = self.tileext
2589 | args['publishurl'] = self.options.url
2590 | args['copyright'] = self.options.copyright
2591 | if self.options.tmscompatible:
2592 | args['tmsoffset'] = '-1'
2593 | else:
2594 | args['tmsoffset'] = ''
2595 | if self.options.profile == 'raster':
2596 | args['rasterzoomlevels'] = self.tmaxz + 1
2597 | args['rastermaxresolution'] = 2 ** self.nativezoom \
2598 | * self.out_gt[1]
2599 |
2600 | s = \
2601 | """
2602 |
2604 | %(title)s
2605 |
2606 | """ \
2616 | % args
2617 |
2618 | if self.options.profile == 'mercator':
2619 | s += \
2620 | """
2621 | """ \
2622 | % args
2623 |
2624 | s += \
2625 | """
2626 |
2627 |
2902 |
2903 |
2904 |
2905 |
2908 |
2909 |
2910 |
2911 |