1 | #!/usr/bin/python2.7 |
||
2 | """ |
||
3 | Copyright (C) 2014 Reinventing Geospatial, Inc. |
||
4 | |||
5 | This program is free software: you can redistribute it and/or modify |
||
6 | it under the terms of the GNU General Public License as published by |
||
7 | the Free Software Foundation, either version 3 of the License, or |
||
8 | (at your option) any later version. |
||
9 | |||
10 | This program is distributed in the hope that it will be useful, |
||
11 | but WITHOUT ANY WARRANTY; without even the implied warranty of |
||
12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||
13 | GNU General Public License for more details. |
||
14 | |||
15 | You should have received a copy of the GNU General Public License |
||
16 | along with this program. If not, see <http://www.gnu.org/licenses/>, |
||
17 | or write to the Free Software Foundation, Inc., 59 Temple Place - |
||
18 | Suite 330, Boston, MA 02111-1307, USA. |
||
19 | |||
20 | Author: Steven D. Lander, Reinventing Geospatial Inc (RGi) |
||
21 | Date: 2013-07-12 |
||
22 | Requires: sqlite3, argparse |
||
23 | Optional: Python Imaging Library (PIL or Pillow) |
||
24 | Description: Converts a TMS folder into a geopackage with |
||
25 | PNGs for images with transparency and JPEGs for those |
||
26 | without. |
||
27 | Credits: |
||
28 | MapProxy imaging functions: http://mapproxy.org |
||
29 | gdal2mb on github: https://github.com/developmentseed/gdal2mb |
||
30 | |||
31 | Version: |
||
32 | """ |
||
33 | |||
34 | from glob import glob |
||
35 | try: |
||
36 | from cStringIO import StringIO as ioBuffer |
||
37 | except ImportError: |
||
38 | from io import BytesIO as ioBuffer |
||
39 | from time import sleep |
||
40 | from uuid import uuid4 |
||
41 | from sys import stdout |
||
42 | from sys import version_info |
||
43 | if version_info[0] == 3: |
||
44 | xrange = range |
||
45 | from operator import attrgetter |
||
46 | from sqlite3 import connect, Error |
||
47 | from argparse import ArgumentParser |
||
48 | from sqlite3 import Binary as sbinary |
||
49 | from os import walk, remove |
||
50 | from os.path import split, join, exists |
||
51 | from multiprocessing import cpu_count, Pool |
||
52 | from math import pi, sin, log, tan, atan, sinh, degrees |
||
53 | try: |
||
54 | from PIL.Image import open as IOPEN |
||
55 | except ImportError: |
||
56 | IOPEN = None |
||
57 | |||
58 | # JPEGs @ 75% provide good quality images with low footprint, use as a default |
||
59 | # PNGs should be used sparingly (mixed mode) due to their high disk usage RGBA |
||
60 | # Options are mixed, jpeg, and png |
||
61 | IMAGE_TYPES = '.png', '.jpeg', '.jpg' |
||
62 | |||
63 | |||
64 | class Mercator(object): |
||
65 | """ |
||
66 | Mercator projection class that holds specific calculations and formulas |
||
67 | for EPSG3857. |
||
68 | """ |
||
69 | |||
70 | def __init__(self, tile_size=256): |
||
71 | """ |
||
72 | Constructor |
||
73 | """ |
||
74 | self.tile_size = tile_size |
||
75 | self.radius = 6378137 |
||
76 | self.origin_shift = pi * self.radius |
||
77 | self.initial_resolution = 2 * self.origin_shift / self.tile_size |
||
78 | |||
79 | @staticmethod |
||
80 | def invert_y(z, y): |
||
81 | """ |
||
82 | Inverts the Y tile value. |
||
83 | |||
84 | Inputs: |
||
85 | z -- the zoom level associated with the tile |
||
86 | y -- the Y tile number |
||
87 | |||
88 | Returns: |
||
89 | The flipped tile value |
||
90 | """ |
||
91 | return (1 << z) - y - 1 |
||
92 | |||
93 | @staticmethod |
||
94 | def tile_to_lat_lon(z, x, y): |
||
95 | """ |
||
96 | Returns the lat/lon coordinates of the bottom-left corner of the input |
||
97 | tile. |
||
98 | |||
99 | Inputs: |
||
100 | z -- zoom level value for input tile |
||
101 | x -- tile column (longitude) value for input tile |
||
102 | y -- tile row (latitude) value for input tile |
||
103 | """ |
||
104 | n = 2.0**z |
||
105 | lon = x / n * 360.0 - 180.0 |
||
106 | lat_rad = atan(sinh(pi * (2 * y / n - 1))) |
||
107 | #lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * y / n))) |
||
108 | lat = degrees(lat_rad) |
||
109 | return lat, lon |
||
110 | |||
111 | def tile_to_meters(self, z, x, y): |
||
112 | """ |
||
113 | Returns the meter coordinates of the bottom-left corner of the input |
||
114 | tile. |
||
115 | |||
116 | Inputs: |
||
117 | z -- zoom level value for input tile |
||
118 | x -- tile column (longitude) value for input tile |
||
119 | y -- tile row (latitude) value for input tile |
||
120 | """ |
||
121 | # Mercator Upper left, add 1 to both x and y to get Lower right |
||
122 | lat, lon = self.tile_to_lat_lon(z, x, y) |
||
123 | meters_x = lon * self.origin_shift / 180.0 |
||
124 | meters_y = log(tan((90 + lat) * pi / 360.0)) / \ |
||
125 | (pi / 180.0) |
||
126 | meters_y = meters_y * self.origin_shift / 180.0 |
||
127 | return meters_x, meters_y |
||
128 | |||
129 | @staticmethod |
||
130 | def pixel_size(z): |
||
131 | """ |
||
132 | Returns the pixel resolution of the input zoom level. |
||
133 | |||
134 | Inputs: |
||
135 | z -- zoom level value for the input tile |
||
136 | """ |
||
137 | return 156543.033928041 / 2**z |
||
138 | |||
139 | def get_coord(self, z, x, y): |
||
140 | """ |
||
141 | Returns the coordinates (in meters) of the bottom-left corner of the |
||
142 | input tile. |
||
143 | |||
144 | Inputs: |
||
145 | z -- zoom level value for input tile |
||
146 | x -- tile column (longitude) value for input tile |
||
147 | y -- tile row (latitude) value for input tile |
||
148 | """ |
||
149 | return self.tile_to_meters(z, x, y) |
||
150 | |||
151 | @staticmethod |
||
152 | def truncate(coord): |
||
153 | """ |
||
154 | Formats a coordinate to within an acceptable degree of accuracy (2 |
||
155 | decimal places for mercator). |
||
156 | """ |
||
157 | return '%.2f' % (int(coord * 100) / float(100)) |
||
158 | |||
159 | |||
160 | class Geodetic(object): |
||
161 | """ |
||
162 | Geodetic projection class that holds specific calculations and formulas for |
||
163 | EPSG4326. |
||
164 | """ |
||
165 | |||
166 | def __init__(self, tile_size=256): |
||
167 | """ |
||
168 | Constructor |
||
169 | """ |
||
170 | self.tile_size = tile_size |
||
171 | self.resolution_factor = 360.0 / self.tile_size |
||
172 | |||
173 | def pixel_size(self, zoom): |
||
174 | """ |
||
175 | Return the size of a pixel in lat/long at the given zoom level |
||
176 | |||
177 | z -- zoom level of the tile |
||
178 | """ |
||
179 | return self.resolution_factor / 2**zoom |
||
180 | |||
181 | def get_coord(self, z, x, y): |
||
182 | """ |
||
183 | Return the coordinates (in lat/long) of the bottom left corner of |
||
184 | the tile |
||
185 | |||
186 | z -- zoom level for input tile |
||
187 | x -- tile column |
||
188 | y -- tile row |
||
189 | """ |
||
190 | res = self.resolution_factor / 2**z |
||
191 | return x * self.tile_size * res - 180, y * self.tile_size * res - 90 |
||
192 | |||
193 | @staticmethod |
||
194 | def invert_y(z, y): |
||
195 | """ |
||
196 | Return the inverted Y value of the tile |
||
197 | |||
198 | z -- zoom level |
||
199 | """ |
||
200 | if z == 0: |
||
201 | return 0 |
||
202 | else: |
||
203 | return (1 << (z - 1)) - y - 1 |
||
204 | |||
205 | @staticmethod |
||
206 | def truncate(coord): |
||
207 | """ |
||
208 | Formats a coordinate to an acceptable degree of accuracy (7 decimal |
||
209 | places for Geodetic). |
||
210 | """ |
||
211 | return '%.7f' % (int(coord * 10000000) / float(10000000)) |
||
212 | |||
213 | |||
214 | View Code Duplication | class EllipsoidalMercator(Mercator): |
|
0 ignored issues
–
show
Duplication
introduced
by
![]() |
|||
215 | """ |
||
216 | Ellipsoidal Mercator projection class that holds specific calculations and |
||
217 | formulas for EPSG3395. |
||
218 | """ |
||
219 | |||
220 | def __init__(self): |
||
221 | """ |
||
222 | Constructor |
||
223 | """ |
||
224 | super(EllipsoidalMercator, self).__init__() |
||
225 | |||
226 | @staticmethod |
||
227 | def lat_to_northing(lat): |
||
228 | """ |
||
229 | Convert a latitude to a northing |
||
230 | / / pi phi \ / 1 - e sin(phi) \ e/2 \ |
||
231 | y(phi) = R ln| tan| --- + --- | | -------------- | | |
||
232 | \ \ 4 2 / \ 1 + e sin(phi) / / |
||
233 | """ |
||
234 | r = 6378137.0 |
||
235 | e = 0.081819190842621 |
||
236 | return r * log(tan((pi / 2 + lat) / 2) * ((1 - e * sin(lat)) / |
||
237 | (1 + e * sin(lat)))**(e / 2)) |
||
238 | |||
239 | @staticmethod |
||
240 | def tile_to_lat_lon(z, x, y): |
||
241 | """ |
||
242 | Returns the lat/lon coordinates of the bottom-left corner of the input |
||
243 | tile. Finds the value numerically (using the secant method). |
||
244 | |||
245 | Inputs: |
||
246 | z -- zoom level value for input tile |
||
247 | x -- tile column value for input tile |
||
248 | y -- tile row value for input tile |
||
249 | """ |
||
250 | n = 2.0**z |
||
251 | lon = x / n * 360.0 - 180.0 |
||
252 | my = (y - 2**(z - 1)) * 6378137 * pi * 2 / 2**z |
||
253 | |||
254 | def f(phi): |
||
255 | return EllipsoidalMercator.lat_to_northing(phi) - my |
||
256 | |||
257 | lat = 0.0 |
||
258 | oldLat = 1.0 |
||
259 | diff = 1.0 |
||
260 | while abs(diff) > 0.0001: |
||
261 | newLat = lat - f(lat) * (lat - oldLat) / (f(lat) - f(oldLat)) |
||
262 | if newLat > 1.48499697138: |
||
263 | newLat = 1.48499697138 |
||
264 | elif newLat < -1.48499697138: |
||
265 | newLat = -1.48499697138 |
||
266 | oldLat = lat |
||
267 | lat = newLat |
||
268 | diff = lat - oldLat |
||
269 | lat = lat * 180.0 / pi |
||
270 | return lat, lon |
||
271 | |||
272 | def tile_to_meters(self, z, x, y): |
||
273 | """ |
||
274 | Returns the meter coordinates of the bottom-left corner of the input |
||
275 | tile. |
||
276 | |||
277 | Inputs: |
||
278 | z -- zoom level value for input tile |
||
279 | x -- tile column (longitude) value for input tile |
||
280 | y -- tile row (latitude) value for input tile |
||
281 | """ |
||
282 | lat, lon = self.tile_to_lat_lon(z, x, y) |
||
283 | meters_x = lon * self.origin_shift / 180.0 |
||
284 | meters_y = self.lat_to_northing(lat * pi / 180.0) |
||
285 | return meters_x, meters_y |
||
286 | |||
287 | |||
288 | View Code Duplication | class ScaledWorldMercator(EllipsoidalMercator): |
|
0 ignored issues
–
show
|
|||
289 | """ |
||
290 | Scaled World Mercator projection class that holds specific calculations |
||
291 | and formulas for EPSG9804/9805 projection proposed by NGA Craig Rollins. |
||
292 | """ |
||
293 | |||
294 | def __init__(self): |
||
295 | """ |
||
296 | Constructor |
||
297 | """ |
||
298 | super(ScaledWorldMercator, self).__init__() |
||
299 | |||
300 | @staticmethod |
||
301 | def pixel_size(z): |
||
302 | """ |
||
303 | Calculates the pixel size for a given zoom level. |
||
304 | """ |
||
305 | return 125829.12 / 2**z |
||
306 | |||
307 | @staticmethod |
||
308 | def lat_to_northing(lat): |
||
309 | """ |
||
310 | Convert a latitude to a northing |
||
311 | / / pi phi \ / 1 - e sin(phi) \ e/2 \ |
||
312 | y(phi) = R ln| tan| --- + --- | | -------------- | | |
||
313 | \ \ 4 2 / \ 1 + e sin(phi) / / |
||
314 | """ |
||
315 | r = 6378137.0 * 0.857385503731176 |
||
316 | e = 0.081819190842621 |
||
317 | return r * log(tan((pi / 2 + lat) / 2) * ((1 - e * sin(lat)) / |
||
318 | (1 + e * sin(lat)))**(e / 2)) |
||
319 | |||
320 | @staticmethod |
||
321 | def tile_to_lat_lon(z, x, y): |
||
322 | """ |
||
323 | Returns the lat/lon coordinates of the bottom-left corner of the input |
||
324 | tile. Finds the value numerically (using the secant method). A scale |
||
325 | factor has been added specifically for scaled world mercator. |
||
326 | |||
327 | Inputs: |
||
328 | z -- zoom level value for input tile |
||
329 | x -- tile column value for input tile |
||
330 | y -- tile row value for input tile |
||
331 | """ |
||
332 | n = 2.0**z |
||
333 | r = 6378137.0 * 0.857385503731176 |
||
334 | lon = x / n * 360.0 - 180.0 |
||
335 | my = (y - 2**(z - 1)) * r * pi * 2 / 2**z |
||
336 | |||
337 | def f(phi): |
||
338 | return ScaledWorldMercator.lat_to_northing(phi) - my |
||
339 | |||
340 | lat = 0.0 |
||
341 | oldLat = 1.0 |
||
342 | diff = 1.0 |
||
343 | while abs(diff) > 0.0001: |
||
344 | newLat = lat - f(lat) * (lat - oldLat) / (f(lat) - f(oldLat)) |
||
345 | if newLat > 1.4849969713855238: |
||
346 | newLat = 1.4849969713855238 |
||
347 | elif newLat < -1.4849969713855238: |
||
348 | newLat = -1.4849969713855238 |
||
349 | oldLat = lat |
||
350 | lat = newLat |
||
351 | diff = lat - oldLat |
||
352 | lat = lat * 180.0 / pi |
||
353 | return lat, lon |
||
354 | |||
355 | def tile_to_meters(self, z, x, y): |
||
356 | """ |
||
357 | Returns the meter coordinates of the bottom-left corner of the input |
||
358 | tile. A scale factor has been added to the longitude meters |
||
359 | calculation. |
||
360 | |||
361 | Inputs: |
||
362 | z -- zoom level value for input tile |
||
363 | x -- tile column (longitude) value for input tile |
||
364 | y -- tile row (latitude) value for input tile |
||
365 | """ |
||
366 | lat, lon = self.tile_to_lat_lon(z, x, y) |
||
367 | meters_x = lon * (pi * (6378137.0 * 0.857385503731176)) / 180.0 |
||
368 | meters_y = self.lat_to_northing(lat * pi / 180.0) |
||
369 | # Instituting a 2 decimal place round to ensure accuracy |
||
370 | return meters_x, round(meters_y, 2) |
||
371 | |||
372 | |||
373 | class ZoomMetadata(object): |
||
374 | """Return an object containing metadata about a given zoom level.""" |
||
375 | |||
376 | @property |
||
377 | def zoom(self): |
||
378 | """Return the zoom level of this metadata object.""" |
||
379 | return self.__zoom |
||
380 | |||
381 | @zoom.setter |
||
382 | def zoom(self, value): |
||
383 | """Set the zoom level of this metadata object.""" |
||
384 | self.__zoom = value |
||
385 | |||
386 | @property |
||
387 | def min_tile_col(self): |
||
388 | """Return the minimum tile column of this metadata object.""" |
||
389 | return self.__min_tile_col |
||
390 | |||
391 | @min_tile_col.setter |
||
392 | def min_tile_col(self, value): |
||
393 | """Set the minimum tile column of this metadata object.""" |
||
394 | self.__min_tile_col = value |
||
395 | |||
396 | @property |
||
397 | def max_tile_col(self): |
||
398 | """Return the maximum tile column of this metadata object.""" |
||
399 | return self.__max_tile_col |
||
400 | |||
401 | @max_tile_col.setter |
||
402 | def max_tile_col(self, value): |
||
403 | """Set the maximum tile column of this metadata object.""" |
||
404 | self.__max_tile_col = value |
||
405 | |||
406 | @property |
||
407 | def min_tile_row(self): |
||
408 | """Return the minimum tile row of this metadata object.""" |
||
409 | return self.__min_tile_row |
||
410 | |||
411 | @min_tile_row.setter |
||
412 | def min_tile_row(self, value): |
||
413 | """Set the minimum tile row of this metadata object.""" |
||
414 | self.__min_tile_row = value |
||
415 | |||
416 | @property |
||
417 | def max_tile_row(self): |
||
418 | """Return the maximum tile row of this metadata object.""" |
||
419 | return self.__max_tile_row |
||
420 | |||
421 | @max_tile_row.setter |
||
422 | def max_tile_row(self, value): |
||
423 | """Set the maximum tile row of this metadata object.""" |
||
424 | self.__max_tile_row = value |
||
425 | |||
426 | @property |
||
427 | def min_x(self): |
||
428 | """Return the minimum x coordinate of the bounding box.""" |
||
429 | return self.__min_x |
||
430 | |||
431 | @min_x.setter |
||
432 | def min_x(self, value): |
||
433 | """Set the minimum x coordinate of the bounding box.""" |
||
434 | self.__min_x = value |
||
435 | |||
436 | @property |
||
437 | def max_x(self): |
||
438 | """Return the maximum x coordinate of the bounding box.""" |
||
439 | return self.__max_x |
||
440 | |||
441 | @max_x.setter |
||
442 | def max_x(self, value): |
||
443 | """Set the maximum x coordinate of the bounding box.""" |
||
444 | self.__max_x = value |
||
445 | |||
446 | @property |
||
447 | def min_y(self): |
||
448 | """Return the minimum y coordinate of the bounding box.""" |
||
449 | return self.__min_y |
||
450 | |||
451 | @min_y.setter |
||
452 | def min_y(self, value): |
||
453 | """Set the minimum y coordinate of the bounding box.""" |
||
454 | self.__min_y = value |
||
455 | |||
456 | @property |
||
457 | def max_y(self): |
||
458 | """Return the maximum y coordinate of the bounding box.""" |
||
459 | return self.__max_y |
||
460 | |||
461 | @max_y.setter |
||
462 | def max_y(self, value): |
||
463 | """Set the maximum y coordinate of the bounding box.""" |
||
464 | self.__max_y = value |
||
465 | |||
466 | @property |
||
467 | def matrix_width(self): |
||
468 | """Number of tiles wide this matrix should be.""" |
||
469 | #return (self.__matrix_width if hasattr(self, 'matrix_width') else None) |
||
470 | return self.__matrix_width or None |
||
471 | |||
472 | @matrix_width.setter |
||
473 | def matrix_width(self, value): |
||
474 | """Set the number of tiles wide this matrix should be.""" |
||
475 | self.__matrix_width = value |
||
476 | |||
477 | @property |
||
478 | def matrix_height(self): |
||
479 | """Number of tiles high this matrix should be.""" |
||
480 | return self.__matrix_height or None |
||
481 | |||
482 | @matrix_height.setter |
||
483 | def matrix_height(self, value): |
||
484 | """Set the number of tiles high this matrix should be.""" |
||
485 | self.__matrix_height = value |
||
486 | |||
487 | |||
488 | class Geopackage(object): |
||
489 | """Object representing a GeoPackage container.""" |
||
490 | |||
491 | def __enter__(self): |
||
492 | """With-statement caller""" |
||
493 | return self |
||
494 | |||
495 | def __init__(self, file_path, srs): |
||
496 | """Constructor.""" |
||
497 | self.__file_path = file_path |
||
498 | self.__srs = srs |
||
499 | if self.__srs == 3857: |
||
500 | self.__projection = Mercator() |
||
501 | elif self.__srs == 3395: |
||
502 | self.__projection = EllipsoidalMercator() |
||
503 | elif self.__srs == 9804: |
||
504 | self.__projection = ScaledWorldMercator() |
||
505 | else: |
||
506 | self.__projection = Geodetic() |
||
507 | self.__db_con = connect(self.__file_path) |
||
508 | self.__create_schema() |
||
509 | |||
510 | def __create_schema(self): |
||
511 | """Create default geopackage schema on the database.""" |
||
512 | with self.__db_con as db_con: |
||
513 | cursor = db_con.cursor() |
||
514 | cursor.execute(""" |
||
515 | CREATE TABLE gpkg_contents ( |
||
516 | table_name TEXT NOT NULL PRIMARY KEY, |
||
517 | data_type TEXT NOT NULL, |
||
518 | identifier TEXT UNIQUE, |
||
519 | description TEXT DEFAULT '', |
||
520 | last_change DATETIME NOT NULL DEFAULT |
||
521 | (strftime('%Y-%m-%dT%H:%M:%fZ','now')), |
||
522 | min_x DOUBLE, |
||
523 | min_y DOUBLE, |
||
524 | max_x DOUBLE, |
||
525 | max_y DOUBLE, |
||
526 | srs_id INTEGER, |
||
527 | CONSTRAINT fk_gc_r_srs_id FOREIGN KEY (srs_id) |
||
528 | REFERENCES gpkg_spatial_ref_sys(srs_id)); |
||
529 | """) |
||
530 | cursor.execute(""" |
||
531 | CREATE TABLE gpkg_spatial_ref_sys ( |
||
532 | srs_name TEXT NOT NULL, |
||
533 | srs_id INTEGER NOT NULL PRIMARY KEY, |
||
534 | organization TEXT NOT NULL, |
||
535 | organization_coordsys_id INTEGER NOT NULL, |
||
536 | definition TEXT NOT NULL, |
||
537 | description TEXT); |
||
538 | """) |
||
539 | cursor.execute(""" |
||
540 | CREATE TABLE gpkg_tile_matrix ( |
||
541 | table_name TEXT NOT NULL, |
||
542 | zoom_level INTEGER NOT NULL, |
||
543 | matrix_width INTEGER NOT NULL, |
||
544 | matrix_height INTEGER NOT NULL, |
||
545 | tile_width INTEGER NOT NULL, |
||
546 | tile_height INTEGER NOT NULL, |
||
547 | pixel_x_size DOUBLE NOT NULL, |
||
548 | pixel_y_size DOUBLE NOT NULL, |
||
549 | CONSTRAINT pk_ttm PRIMARY KEY (table_name, zoom_level), |
||
550 | CONSTRAINT fk_ttm_table_name FOREIGN KEY (table_name) |
||
551 | REFERENCES gpkg_contents(table_name)); |
||
552 | """) |
||
553 | cursor.execute(""" |
||
554 | CREATE TABLE gpkg_tile_matrix_set ( |
||
555 | table_name TEXT NOT NULL PRIMARY KEY, |
||
556 | srs_id INTEGER NOT NULL, |
||
557 | min_x DOUBLE NOT NULL, |
||
558 | min_y DOUBLE NOT NULL, |
||
559 | max_x DOUBLE NOT NULL, |
||
560 | max_y DOUBLE NOT NULL, |
||
561 | CONSTRAINT fk_gtms_table_name FOREIGN KEY (table_name) |
||
562 | REFERENCES gpkg_contents(table_name), |
||
563 | CONSTRAINT fk_gtms_srs FOREIGN KEY (srs_id) |
||
564 | REFERENCES gpkg_spatial_ref_sys(srs_id)); |
||
565 | """) |
||
566 | cursor.execute(""" |
||
567 | CREATE TABLE tiles ( |
||
568 | id INTEGER PRIMARY KEY AUTOINCREMENT, |
||
569 | zoom_level INTEGER NOT NULL, |
||
570 | tile_column INTEGER NOT NULL, |
||
571 | tile_row INTEGER NOT NULL, |
||
572 | tile_data BLOB NOT NULL, |
||
573 | UNIQUE (zoom_level, tile_column, tile_row)); |
||
574 | """) |
||
575 | cursor.execute("pragma foreign_keys = 1;") |
||
576 | # Insert EPSG values for tiles table |
||
577 | wkt = """ |
||
578 | PROJCS["WGS 84 / Pseudo-Mercator",GEOGCS["WGS 84",DATUM["WGS_1984" |
||
579 | ,SPHEROID["WGS 84",6378137,298.257223563,AUTHORITY["EPSG","7030"]] |
||
580 | ,AUTHORITY["EPSG","6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG", |
||
581 | "8901"]],UNIT["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]] |
||
582 | ,AUTHORITY["EPSG","9122"]]AUTHORITY["EPSG","4326"]],PROJECTION[ |
||
583 | "Mercator_1SP"],PARAMETER["central_meridian",0],PARAMETER[ |
||
584 | "scale_factor",1],PARAMETER["false_easting",0],PARAMETER[ |
||
585 | "false_northing",0],UNIT["metre",1,AUTHORITY["EPSG","9001"]],AXIS[ |
||
586 | "X",EAST],AXIS["Y",NORTH] |
||
587 | """ |
||
588 | |||
589 | cursor.execute(""" |
||
590 | INSERT INTO gpkg_spatial_ref_sys ( |
||
591 | srs_id, |
||
592 | organization, |
||
593 | organization_coordsys_id, |
||
594 | srs_name, |
||
595 | definition) |
||
596 | VALUES (3857, ?, 3857, ?, ?) |
||
597 | """, ("epsg", "WGS 84 / Pseudo-Mercator", wkt)) |
||
598 | wkt = """ |
||
599 | GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137, |
||
600 | 298.257223563,AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG", |
||
601 | "6326"]],PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]],UNIT |
||
602 | ["degree",0.0174532925199433,AUTHORITY["EPSG","9122"]], |
||
603 | AUTHORITY["EPSG","4326"]] |
||
604 | """ |
||
605 | |||
606 | cursor.execute(""" |
||
607 | INSERT INTO gpkg_spatial_ref_sys ( |
||
608 | srs_id, |
||
609 | organization, |
||
610 | organization_coordsys_id, |
||
611 | srs_name, |
||
612 | definition) |
||
613 | VALUES (4326, ?, 4326, ?, ?) |
||
614 | """, ("epsg", "WGS 84", wkt)) |
||
615 | wkt = """ |
||
616 | PROJCS["WGS 84 / World Mercator",GEOGCS["WGS 84", |
||
617 | DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563, |
||
618 | AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]], |
||
619 | PRIMEM["Greenwich",0,AUTHORITY["EPSG","8901"]], |
||
620 | UNIT["degree",0.01745329251994328,AUTHORITY["EPSG","9122"]], |
||
621 | AUTHORITY["EPSG","4326"]],UNIT["metre",1,AUTHORITY["EPSG","9001"]], |
||
622 | PROJECTION["Mercator_1SP"],PARAMETER["central_meridian",0], |
||
623 | PARAMETER["scale_factor",1],PARAMETER["false_easting",0], |
||
624 | PARAMETER["false_northing",0],AUTHORITY["EPSG","3395"], |
||
625 | AXIS["Easting",EAST],AXIS["Northing",NORTH]] |
||
626 | """ |
||
627 | |||
628 | cursor.execute(""" |
||
629 | INSERT INTO gpkg_spatial_ref_sys ( |
||
630 | srs_id, |
||
631 | organization, |
||
632 | organization_coordsys_id, |
||
633 | srs_name, |
||
634 | definition) |
||
635 | VALUES (3395, ?, 3395, ?, ?) |
||
636 | """, ("epsg", "WGS 84 / World Mercator", wkt)) |
||
637 | wkt = """ |
||
638 | PROJCS["unnamed",GEOGCS["WGS 84", |
||
639 | DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563, |
||
640 | AUTHORITY["EPSG","7030"]],AUTHORITY["EPSG","6326"]], |
||
641 | PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433], |
||
642 | AUTHORITY["EPSG","4326"]],PROJECTION["Mercator_1SP"], |
||
643 | PARAMETER["central_meridian",0], |
||
644 | PARAMETER["scale_factor",0.803798909747978], |
||
645 | PARAMETER["false_easting",0], |
||
646 | PARAMETER["false_northing",0], |
||
647 | UNIT["metre",1,AUTHORITY["EPSG","9001"]]] |
||
648 | """ |
||
649 | |||
650 | cursor.execute(""" |
||
651 | INSERT INTO gpkg_spatial_ref_sys ( |
||
652 | srs_id, |
||
653 | organization, |
||
654 | organization_coordsys_id, |
||
655 | srs_name, |
||
656 | definition) |
||
657 | VALUES (9804, ?, 9804, ?, ?) |
||
658 | """, ("epsg", "WGS 84 / Scaled World Mercator", wkt)) |
||
659 | wkt = """undefined""" |
||
660 | cursor.execute(""" |
||
661 | INSERT INTO gpkg_spatial_ref_sys ( |
||
662 | srs_id, |
||
663 | organization, |
||
664 | organization_coordsys_id, |
||
665 | srs_name, |
||
666 | definition) |
||
667 | VALUES (-1, ?, -1, ?, ?) |
||
668 | """, ("NONE", " ", wkt)) |
||
669 | cursor.execute(""" |
||
670 | INSERT INTO gpkg_spatial_ref_sys ( |
||
671 | srs_id, |
||
672 | organization, |
||
673 | organization_coordsys_id, |
||
674 | srs_name, |
||
675 | definition) |
||
676 | VALUES (0, ?, 0, ?, ?) |
||
677 | """, ("NONE", " ", wkt)) |
||
678 | cursor.execute(""" |
||
679 | INSERT INTO gpkg_contents ( |
||
680 | table_name, |
||
681 | data_type, |
||
682 | identifier, |
||
683 | description, |
||
684 | min_x, |
||
685 | max_x, |
||
686 | min_y, |
||
687 | max_y, |
||
688 | srs_id) |
||
689 | VALUES (?, ?, ?, ?, 0, 0, 0, 0, ?); |
||
690 | """, ("tiles", "tiles", "Raster Tiles", |
||
691 | "Created by tiles2gpkg_parallel.py, written by S. Lander", |
||
692 | self.__srs)) |
||
693 | # Add GP10 to the Sqlite header |
||
694 | cursor.execute("pragma application_id = 1196437808;") |
||
695 | |||
696 | @property |
||
697 | def file_path(self): |
||
698 | """Return the path of the geopackage database on the file system.""" |
||
699 | return self.__file_path |
||
700 | |||
701 | def update_metadata(self, metadata): |
||
702 | """Update the metadata of the geopackage database after tile merge.""" |
||
703 | # initialize a new projection |
||
704 | with self.__db_con as db_con: |
||
705 | cursor = db_con.cursor() |
||
706 | tile_matrix_stmt = """ |
||
707 | INSERT OR REPLACE INTO gpkg_tile_matrix ( |
||
708 | table_name, |
||
709 | zoom_level, |
||
710 | matrix_width, |
||
711 | matrix_height, |
||
712 | tile_width, |
||
713 | tile_height, |
||
714 | pixel_x_size, |
||
715 | pixel_y_size) |
||
716 | VALUES (?, ?, ?, ?, ?, ?, ?, ?); |
||
717 | """ |
||
718 | |||
719 | # iterate through each zoom level object and assign |
||
720 | # matrix data to table |
||
721 | for level in metadata: |
||
722 | cursor.execute( |
||
723 | tile_matrix_stmt, |
||
724 | ("tiles", level.zoom, level.matrix_width, |
||
725 | level.matrix_height, self.__projection.tile_size, |
||
726 | self.__projection.tile_size, |
||
727 | self.__projection.pixel_size(level.zoom), |
||
728 | self.__projection.pixel_size(level.zoom))) |
||
729 | contents_stmt = """ |
||
730 | UPDATE gpkg_contents SET |
||
731 | min_x = ?, |
||
732 | min_y = ?, |
||
733 | max_x = ?, |
||
734 | max_y = ? |
||
735 | WHERE table_name = 'tiles'; |
||
736 | """ |
||
737 | |||
738 | tile_matrix_set_stmt = """ |
||
739 | INSERT OR REPLACE INTO gpkg_tile_matrix_set ( |
||
740 | table_name, |
||
741 | srs_id, |
||
742 | min_x, |
||
743 | min_y, |
||
744 | max_x, |
||
745 | max_y) |
||
746 | VALUES (?, ?, ?, ?, ?, ?); |
||
747 | """ |
||
748 | |||
749 | # get bounding box info based on |
||
750 | top_level = min(metadata, key=attrgetter('zoom')) |
||
751 | #top_level.min_x = self.__projection.truncate(top_level.min_x) |
||
752 | #top_level.min_y = self.__projection.truncate(top_level.min_y) |
||
753 | #top_level.max_x = self.__projection.truncate(top_level.max_x) |
||
754 | #top_level.max_y = self.__projection.truncate(top_level.max_y) |
||
755 | top_level.min_x = top_level.min_x |
||
756 | top_level.min_y = top_level.min_y |
||
757 | top_level.max_x = top_level.max_x |
||
758 | top_level.max_y = top_level.max_y |
||
759 | # write bounds and matrix set info to table |
||
760 | cursor.execute(contents_stmt, (top_level.min_x, top_level.min_y, |
||
761 | top_level.max_x, top_level.max_y)) |
||
762 | cursor.execute(tile_matrix_set_stmt, |
||
763 | ('tiles', self.__srs, top_level.min_x, |
||
764 | top_level.min_y, top_level.max_x, top_level.max_y)) |
||
765 | |||
766 | def execute(self, statement, inputs=None): |
||
767 | """Execute a prepared SQL statement on this geopackage database.""" |
||
768 | with self.__db_con as db_con: |
||
769 | cursor = db_con.cursor() |
||
770 | if inputs is not None: |
||
771 | result_cursor = cursor.execute(statement, inputs) |
||
772 | else: |
||
773 | result_cursor = cursor.execute(statement) |
||
774 | return result_cursor |
||
775 | |||
776 | def assimilate(self, source): |
||
777 | """Assimilate .gpkg.part tiles into this geopackage database.""" |
||
778 | if not exists(source): |
||
779 | raise IOError |
||
780 | with self.__db_con as db_con: |
||
781 | cursor = db_con.cursor() |
||
782 | cursor.execute("pragma synchronous = off;") |
||
783 | cursor.execute("pragma journal_mode = off;") |
||
784 | cursor.execute("pragma page_size = 65536;") |
||
785 | #print "Merging", source, "into", self.__file_path, "..." |
||
786 | query = "attach '" + source + "' as source;" |
||
787 | cursor.execute(query) |
||
788 | try: |
||
789 | cursor.execute("""INSERT OR REPLACE INTO tiles |
||
790 | (zoom_level, tile_column, tile_row, tile_data) |
||
791 | SELECT zoom_level, tile_column, tile_row, tile_data |
||
792 | FROM source.tiles;""") |
||
793 | cursor.execute("detach source;") |
||
794 | except Error as err: |
||
795 | print("Error: {}".format(type(err))) |
||
796 | print("Error msg:".format(err)) |
||
797 | raise |
||
798 | remove(source) |
||
799 | |||
800 | def __exit__(self, type, value, traceback): |
||
801 | """Resource cleanup on destruction.""" |
||
802 | self.__db_con.close() |
||
803 | |||
804 | |||
805 | class TempDB(object): |
||
806 | """ |
||
807 | Returns a temporary sqlite database to hold tiles for async workers. |
||
808 | Has a <filename>.gpkg.part file format. |
||
809 | """ |
||
810 | |||
811 | def __enter__(self): |
||
812 | """With-statement caller.""" |
||
813 | return self |
||
814 | |||
815 | def __init__(self, filename): |
||
816 | """ |
||
817 | Constructor. |
||
818 | |||
819 | Inputs: |
||
820 | filename -- the filename this database will be created with |
||
821 | """ |
||
822 | uid = uuid4() |
||
823 | self.name = uid.hex + '.gpkg.part' |
||
824 | self.__file_path = join(filename, self.name) |
||
825 | self.__db_con = connect(self.__file_path) |
||
826 | with self.__db_con as db_con: |
||
827 | cursor = db_con.cursor() |
||
828 | stmt = """ |
||
829 | CREATE TABLE tiles ( |
||
830 | id INTEGER PRIMARY KEY AUTOINCREMENT, |
||
831 | zoom_level INTEGER NOT NULL, |
||
832 | tile_column INTEGER NOT NULL, |
||
833 | tile_row INTEGER NOT NULL, |
||
834 | tile_data BLOB NOT NULL, |
||
835 | UNIQUE (zoom_level, tile_column, tile_row)); |
||
836 | """ |
||
837 | |||
838 | cursor.execute(stmt) |
||
839 | # Enable pragma for fast sqlite creation |
||
840 | cursor.execute("pragma synchronous = off;") |
||
841 | cursor.execute("pragma journal_mode = off;") |
||
842 | cursor.execute("pragma page_size = 80000;") |
||
843 | cursor.execute("pragma foreign_keys = 1;") |
||
844 | self.image_blob_stmt = """ |
||
845 | INSERT INTO tiles |
||
846 | (zoom_level, tile_column, tile_row, tile_data) |
||
847 | VALUES (?,?,?,?) |
||
848 | """ |
||
849 | |||
850 | def execute(self, statement, inputs=None): |
||
851 | with self.__db_con as db_con: |
||
852 | cursor = db_con.cursor() |
||
853 | if inputs is not None: |
||
854 | result_cursor = cursor.execute(statement, inputs) |
||
855 | else: |
||
856 | result_cursor = cursor.execute(statement) |
||
857 | return result_cursor |
||
858 | |||
859 | def insert_image_blob(self, z, x, y, data): |
||
860 | """ |
||
861 | Inserts a binary data array containing an image into a sqlite3 |
||
862 | database. |
||
863 | |||
864 | Inputs: |
||
865 | z -- the zoom level of the binary data |
||
866 | x -- the row number of the data |
||
867 | y -- the column number of the data |
||
868 | data -- the image data containing in a binary array |
||
869 | """ |
||
870 | with self.__db_con as db_con: |
||
871 | cursor = db_con.cursor() |
||
872 | cursor.execute(self.image_blob_stmt, (z, x, y, data)) |
||
873 | |||
874 | def __exit__(self, type, value, traceback): |
||
875 | """Resource cleanup on destruction.""" |
||
876 | self.__db_con.close() |
||
877 | |||
878 | |||
879 | def img_to_buf(img, img_type, jpeg_quality=75): |
||
880 | """ |
||
881 | Returns a buffer array with image binary data for the input image. |
||
882 | This code is based on logic implemented in MapProxy to convert PNG |
||
883 | images to JPEG then return the buffer. |
||
884 | |||
885 | Inputs: |
||
886 | img -- an image on the filesystem to be converted to binary |
||
887 | img_type -- the MIME type of the image (JPG, PNG) |
||
888 | """ |
||
889 | defaults = {} |
||
890 | buf = ioBuffer() |
||
891 | if img_type == 'jpeg': |
||
892 | img.convert('RGB') |
||
893 | # Hardcoding a default compression of 75% for JPEGs |
||
894 | defaults['quality'] = jpeg_quality |
||
895 | elif img_type == 'source': |
||
896 | img_type = img.format |
||
897 | img.save(buf, img_type, **defaults) |
||
898 | buf.seek(0) |
||
899 | return buf |
||
900 | |||
901 | |||
902 | def img_has_transparency(img): |
||
903 | """ |
||
904 | Returns a 0 if the input image has no transparency, 1 if it has some, |
||
905 | and -1 if the image is fully transparent. Tiles *should be a perfect |
||
906 | square (e.g, 256x256), so it can be safe to assume the first dimension |
||
907 | will match the second. This will ensure compatibility with different |
||
908 | tile sizes other than 256x256. This code is based on logic implemented |
||
909 | in MapProxy to check for images that have transparency. |
||
910 | |||
911 | Inputs: |
||
912 | img -- an Image object from the PIL library |
||
913 | """ |
||
914 | size = img.size[0] |
||
915 | if img.mode == 'P': |
||
916 | # For paletted images |
||
917 | if img.info.get('transparency', False): |
||
918 | return True |
||
919 | # Convert to RGBA to check alpha |
||
920 | img = img.convert('RGBA') |
||
921 | if img.mode == 'RGBA': |
||
922 | # Returns the number of pixels in this image that are transparent |
||
923 | # Assuming a tile size of 256, 65536 would be fully transparent |
||
924 | transparent_pixels = img.histogram()[-size] |
||
925 | if transparent_pixels == 0: |
||
926 | # No transparency |
||
927 | return 0 |
||
928 | elif 0 < transparent_pixels < (size * size): |
||
929 | # Image has some transparency |
||
930 | return 1 |
||
931 | else: |
||
932 | # Image is fully transparent, and can be discarded |
||
933 | return -1 |
||
934 | #return img.histogram()[-size] |
||
935 | return False |
||
936 | |||
937 | |||
938 | def file_count(base_dir): |
||
939 | """ |
||
940 | A function that finds all image tiles in a base directory. The base |
||
941 | directory should be arranged in TMS format, i.e. z/x/y. |
||
942 | |||
943 | Inputs: |
||
944 | base_dir -- the name of the TMS folder containing tiles. |
||
945 | |||
946 | Returns: |
||
947 | A list of dictionary objects containing the full file path and TMS |
||
948 | coordinates of the image tile. |
||
949 | """ |
||
950 | print("Calculating number of tiles, this could take a while...") |
||
951 | file_list = [] |
||
952 | # Avoiding dots (functional references) will increase performance of |
||
953 | # the loop because they will not be reevaluated each iteration. |
||
954 | for root, sub_folders, files in walk(base_dir): |
||
955 | temp_list = [join(root, f) for f in files if f.endswith(IMAGE_TYPES)] |
||
956 | file_list += temp_list |
||
957 | print("Found {} total tiles.".format(len(file_list))) |
||
958 | return [split_all(item) for item in file_list] |
||
959 | |||
960 | |||
961 | def split_all(path): |
||
962 | """ |
||
963 | Function that parses TMS coordinates from a full images file path. |
||
964 | |||
965 | Inputs: |
||
966 | path -- a full file path to an image tile. |
||
967 | |||
968 | Returns: |
||
969 | A dictionary containing the TMS coordinates of the tile and its full |
||
970 | file path. |
||
971 | """ |
||
972 | parts = [] |
||
973 | full_path = path |
||
974 | # Parse out the tms coordinates |
||
975 | for i in xrange(3): |
||
976 | head, tail = split(path) |
||
977 | parts.append(tail) |
||
978 | path = head |
||
979 | file_dict = dict(y=int(parts[0].split('.')[0]), |
||
980 | x=int(parts[1]), |
||
981 | z=int(parts[2]), |
||
982 | path=full_path) |
||
983 | return file_dict |
||
984 | |||
985 | |||
986 | def worker_map(temp_db, tile_dict, extra_args, invert_y): |
||
987 | """ |
||
988 | Function responsible for sending the correct oriented tile data to a |
||
989 | temporary sqlite3 database. |
||
990 | |||
991 | Inputs: |
||
992 | temp_db -- a temporary sqlite3 database that will hold this worker's tiles |
||
993 | tile_dict -- a dictionary with TMS coordinates and file path for a tile |
||
994 | tile_info -- a list of ZoomMetadata objects pre-generated for this tile set |
||
995 | imagery -- the type of image format to send to the sqlite3 database |
||
996 | invert_y -- a function that will flip the Y axis of the tile if present |
||
997 | """ |
||
998 | tile_info = extra_args['tile_info'] |
||
999 | imagery = extra_args['imagery'] |
||
1000 | jpeg_quality = extra_args['jpeg_quality'] |
||
1001 | zoom = tile_dict['z'] |
||
1002 | level = next((item for item in tile_info if item.zoom == zoom), None) |
||
1003 | x_row = tile_dict['x'] - level.min_tile_row |
||
1004 | if invert_y is not None: |
||
1005 | y_offset = invert_y(tile_dict['z'], level.max_tile_col) |
||
1006 | y_column = invert_y(tile_dict['z'], tile_dict['y']) |
||
1007 | y_column -= y_offset |
||
1008 | else: |
||
1009 | y_column = tile_dict['y'] - level.min_tile_col |
||
1010 | if IOPEN is not None: |
||
1011 | img = IOPEN(tile_dict['path'], 'r') |
||
1012 | data = ioBuffer() |
||
1013 | if imagery == 'mixed': |
||
1014 | if img_has_transparency(img): |
||
1015 | data = img_to_buf(img, 'png', jpeg_quality).read() |
||
1016 | else: |
||
1017 | data = img_to_buf(img, 'jpeg', jpeg_quality).read() |
||
1018 | else: |
||
1019 | data = img_to_buf(img, imagery, jpeg_quality).read() |
||
1020 | temp_db.insert_image_blob(zoom, x_row, y_column, sbinary(data)) |
||
1021 | else: |
||
1022 | file_handle = open(tile_dict['path'], 'rb') |
||
1023 | data = buffer(file_handle.read()) |
||
1024 | temp_db.insert_image_blob(zoom, x_row, y_column, data) |
||
1025 | file_handle.close() |
||
1026 | |||
1027 | |||
1028 | def sqlite_worker(file_list, extra_args): |
||
1029 | """ |
||
1030 | Worker function called by asynchronous processes. This function |
||
1031 | iterates through a set of tiles to process them into a TempDB object. |
||
1032 | |||
1033 | Inputs: |
||
1034 | file_list -- an array containing a subset of tiles that will be processed |
||
1035 | by this function into a TempDB object |
||
1036 | base_dir -- the directory in which the geopackage will be created, |
||
1037 | .gpkg.part files will be generated here |
||
1038 | metadata -- a ZoomLevelMetadata object containing information about |
||
1039 | the tiles in the TMS directory |
||
1040 | """ |
||
1041 | temp_db = TempDB(extra_args['root_dir']) |
||
1042 | with TempDB(extra_args['root_dir']) as temp_db: |
||
1043 | invert_y = None |
||
1044 | if extra_args['lower_left']: |
||
1045 | if extra_args['srs'] == 3857: |
||
1046 | invert_y = Mercator.invert_y |
||
1047 | elif extra_args['srs'] == 4326: |
||
1048 | invert_y = Geodetic.invert_y |
||
1049 | elif extra_args['srs'] == 3395: |
||
1050 | invert_y = EllipsoidalMercator.invert_y |
||
1051 | elif extra_args['srs'] == 9804: |
||
1052 | invert_y = ScaledWorldMercator.invert_y |
||
1053 | [worker_map(temp_db, item, extra_args, invert_y) for item in file_list] |
||
1054 | |||
1055 | |||
1056 | def allocate(cores, pool, file_list, extra_args): |
||
1057 | """ |
||
1058 | Recursive function that fairly distributes tiles to asynchronous worker |
||
1059 | processes. For N processes and C cores, N=C if C is divisible by 2. If |
||
1060 | not, then N is the largest factor of 8 that is still less than C. |
||
1061 | """ |
||
1062 | if cores is 1: |
||
1063 | print("Spawning worker with {} files".format(len(file_list))) |
||
1064 | return [pool.apply_async(sqlite_worker, [file_list, extra_args])] |
||
1065 | else: |
||
1066 | files = len(file_list) |
||
1067 | head = allocate( |
||
1068 | int(cores / 2), pool, file_list[:int(files / 2)], extra_args) |
||
1069 | tail = allocate( |
||
1070 | int(cores / 2), pool, file_list[int(files / 2):], extra_args) |
||
1071 | return head + tail |
||
1072 | |||
1073 | |||
1074 | def build_lut(file_list, lower_left, srs): |
||
1075 | """ |
||
1076 | Build a lookup table that aids in metadata generation. |
||
1077 | |||
1078 | Inputs: |
||
1079 | file_list -- the file_list dict made with file_count() |
||
1080 | lower_left -- bool indicating tile grid numbering scheme (tms or wmts) |
||
1081 | srs -- the spatial reference system of the tile grid |
||
1082 | |||
1083 | Returns: |
||
1084 | An array of ZoomLevelMetadata objects that describe each zoom level of the |
||
1085 | tile grid. |
||
1086 | """ |
||
1087 | # Initialize a projection class |
||
1088 | if srs == 3857: |
||
1089 | projection = Mercator() |
||
1090 | elif srs == 4326: |
||
1091 | projection = Geodetic() |
||
1092 | elif srs == 9804: |
||
1093 | projection = ScaledWorldMercator() |
||
1094 | else: |
||
1095 | projection = EllipsoidalMercator() |
||
1096 | # Create a list of zoom levels from the base directory |
||
1097 | zoom_levels = list(set([int(item['z']) for item in file_list])) |
||
1098 | zoom_levels.sort() |
||
1099 | matrix = [] |
||
1100 | # For every zoom in the list... |
||
1101 | for zoom in zoom_levels: |
||
1102 | # create a new ZoomMetadata object... |
||
1103 | level = ZoomMetadata() |
||
1104 | level.zoom = zoom |
||
1105 | # Sometimes, tiling programs do not generate the folders responsible |
||
1106 | # for the X axis if no tiles are being made within them. This results |
||
1107 | # in tiles "shifting" because of the way they are renumbered when |
||
1108 | # placed into a geopackage. |
||
1109 | # To fix, is there a zoom level preceding this one... |
||
1110 | if zoom - 1 in [item for item in zoom_levels if item == (zoom - 1)]: |
||
1111 | # there is, now retrieve it.... |
||
1112 | (prev, ) = ([item for item in matrix if item.zoom == (zoom - 1)]) |
||
1113 | # and fix the grid alignment values |
||
1114 | level.min_tile_row = 2 * prev.min_tile_row |
||
1115 | level.min_tile_col = 2 * prev.min_tile_col |
||
1116 | level.max_tile_row = 2 * prev.max_tile_row + 1 |
||
1117 | level.max_tile_col = 2 * prev.max_tile_col + 1 |
||
1118 | # Calculate the width and height |
||
1119 | level.matrix_width = prev.matrix_width * 2 |
||
1120 | level.matrix_height = prev.matrix_height * 2 |
||
1121 | else: |
||
1122 | # Get all possible x and y values... |
||
1123 | x_vals = [int(item['x']) |
||
1124 | for item in file_list if int(item['z']) == zoom] |
||
1125 | y_vals = [int(item['y']) |
||
1126 | for item in file_list if int(item['z']) == zoom] |
||
1127 | # then get the min/max values for each. |
||
1128 | level.min_tile_row, level.max_tile_row = min(x_vals), max(x_vals) |
||
1129 | level.min_tile_col, level.max_tile_col = min(y_vals), max(y_vals) |
||
1130 | # Fill in the matrix width and height for this top level |
||
1131 | x_width_max = max([item[ |
||
1132 | 'x'] for item in file_list if item['z'] == level.zoom]) |
||
1133 | x_width_min = min([item[ |
||
1134 | 'x'] for item in file_list if item['z'] == level.zoom]) |
||
1135 | level.matrix_width = (x_width_max - x_width_min) + 1 |
||
1136 | y_height_max = max([item[ |
||
1137 | 'y'] for item in file_list if item['z'] == level.zoom]) |
||
1138 | y_height_min = min([item[ |
||
1139 | 'y'] for item in file_list if item['z'] == level.zoom]) |
||
1140 | level.matrix_height = (y_height_max - y_height_min) + 1 |
||
1141 | if lower_left: |
||
1142 | # TMS-style tile grid, so to calc the top left corner of the grid, |
||
1143 | # you must get the min x (row) value and the max y (col) value + 1. |
||
1144 | # You are adding 1 to the y value because the math to calc the |
||
1145 | # coord assumes you want the bottom left corner, not the top left. |
||
1146 | # Similarly, to get the bottom right corner, add 1 to x value. |
||
1147 | level.min_x, level.max_y = projection.get_coord( |
||
1148 | level.zoom, level.min_tile_row, level.max_tile_col + 1) |
||
1149 | level.max_x, level.min_y = projection.get_coord( |
||
1150 | level.zoom, level.max_tile_row + 1, level.min_tile_col) |
||
1151 | else: |
||
1152 | # WMTS-style tile grid, so to calc the top left corner of the grid, |
||
1153 | # you must get the min x (row value and the min y (col) value + 1. |
||
1154 | # You are adding 1 to the y value because the math to calc the |
||
1155 | # coord assumes you want the bottom left corner, not the top left. |
||
1156 | # Similarly, to get the bottom right corner, add 1 to x value. |
||
1157 | # -- Since this is WMTS, we must invert the Y axis before we calc |
||
1158 | inv_min_y = projection.invert_y(level.zoom, level.min_tile_col) |
||
1159 | inv_max_y = projection.invert_y(level.zoom, level.max_tile_col) |
||
1160 | level.min_x, level.max_y = projection.get_coord( |
||
1161 | level.zoom, level.min_tile_row, inv_min_y + 1) |
||
1162 | level.max_x, level.min_y = projection.get_coord( |
||
1163 | level.zoom, level.max_tile_row + 1, inv_max_y) |
||
1164 | # Finally, add this ZoomMetadata object to the list |
||
1165 | matrix.append(level) |
||
1166 | return matrix |
||
1167 | |||
1168 | |||
1169 | def combine_worker_dbs(out_geopackage): |
||
1170 | """ |
||
1171 | Searches for .gpkg.part files in the base directory and merges them |
||
1172 | into one Geopackage file |
||
1173 | |||
1174 | Inputs: |
||
1175 | out_geopackage -- the final output geopackage file |
||
1176 | """ |
||
1177 | base_dir = split(out_geopackage.file_path)[0] |
||
1178 | if base_dir == "": |
||
1179 | base_dir = "." |
||
1180 | glob_path = join(base_dir + '/*.gpkg.part') |
||
1181 | file_list = glob(glob_path) |
||
1182 | print("Merging temporary databases...") |
||
1183 | #[out_geopackage.assimilate(f) for f in file_list] |
||
1184 | itr = len(file_list) |
||
1185 | status = ["|", "/", "-", "\\"] |
||
1186 | counter = 0 |
||
1187 | for tdb in file_list: |
||
1188 | comp = len(file_list) - itr |
||
1189 | itr -= 1 |
||
1190 | out_geopackage.assimilate(tdb) |
||
1191 | if tdb == file_list[-1]: |
||
1192 | stdout.write("\r[X] Progress: [" + "==" * comp + " " * itr + "]") |
||
1193 | else: |
||
1194 | stdout.write("\r[" + status[counter] + "] Progress: [" + "==" * |
||
1195 | comp + " " * itr + "]") |
||
1196 | stdout.flush() |
||
1197 | if counter != len(status) - 1: |
||
1198 | counter += 1 |
||
1199 | else: |
||
1200 | counter = 0 |
||
1201 | print(" All geopackages merged!") |
||
1202 | |||
1203 | |||
1204 | def main(arg_list): |
||
1205 | """ |
||
1206 | Create a geopackage from a directory of tiles arranged in TMS or WMTS |
||
1207 | format. |
||
1208 | |||
1209 | Inputs: |
||
1210 | arg_list -- an ArgumentParser object containing command-line options and |
||
1211 | flags |
||
1212 | """ |
||
1213 | # Build the file dictionary |
||
1214 | files = file_count(arg_list.source_folder) |
||
1215 | if len(files) == 0: |
||
1216 | # If there are no files, exit the script |
||
1217 | print(" Ensure the correct source tile directory was specified.") |
||
1218 | exit(1) |
||
1219 | # Is the input tile grid aligned to lower-left or not? |
||
1220 | lower_left = arg_list.tileorigin == 'll' or arg_list.tileorigin == 'sw' |
||
1221 | # Get the output file destination directory |
||
1222 | root_dir, _ = split(arg_list.output_file) |
||
1223 | # Build the tile matrix info object |
||
1224 | tile_info = build_lut(files, lower_left, arg_list.srs) |
||
1225 | # Initialize the output file |
||
1226 | if arg_list.threading: |
||
1227 | # Enable tiling on multiple CPU cores |
||
1228 | cores = cpu_count() |
||
1229 | pool = Pool(cores) |
||
1230 | # Build allocate dictionary |
||
1231 | extra_args = dict(root_dir=root_dir, |
||
1232 | tile_info=tile_info, |
||
1233 | lower_left=lower_left, |
||
1234 | srs=arg_list.srs, |
||
1235 | imagery=arg_list.imagery, |
||
1236 | jpeg_quality=arg_list.q) |
||
1237 | results = allocate(cores, pool, files, extra_args) |
||
1238 | status = ["|", "/", "-", "\\"] |
||
1239 | counter = 0 |
||
1240 | try: |
||
1241 | while True: |
||
1242 | rem = sum([1 for item in results if not item.ready()]) |
||
1243 | if rem == 0: |
||
1244 | stdout.write("\r[X] Progress: [" + "==" * (cores - rem) + |
||
1245 | " " * rem + "]") |
||
1246 | stdout.flush() |
||
1247 | print(" All Done!") |
||
1248 | break |
||
1249 | else: |
||
1250 | stdout.write("\r[" + status[counter] + "] Progress: [" + |
||
1251 | "==" * (cores - rem) + " " * rem + "]") |
||
1252 | stdout.flush() |
||
1253 | if counter != len(status) - 1: |
||
1254 | counter += 1 |
||
1255 | else: |
||
1256 | counter = 0 |
||
1257 | sleep(.25) |
||
1258 | pool.close() |
||
1259 | pool.join() |
||
1260 | except KeyboardInterrupt: |
||
1261 | print(" Interrupted!") |
||
1262 | pool.terminate() |
||
1263 | exit(1) |
||
1264 | else: |
||
1265 | # Debugging call to bypass multiprocessing (-T) |
||
1266 | extra_args = dict(root_dir=root_dir, |
||
1267 | tile_info=tile_info, |
||
1268 | lower_left=lower_left, |
||
1269 | srs=arg_list.srs, |
||
1270 | imagery=arg_list.imagery, |
||
1271 | jpeg_quality=arg_list.q) |
||
1272 | sqlite_worker(files, extra_args) |
||
1273 | # Combine the individual temp databases into the output file |
||
1274 | with Geopackage(arg_list.output_file, arg_list.srs) as gpkg: |
||
1275 | combine_worker_dbs(gpkg) |
||
1276 | # Using the data in the output file, create the metadata for it |
||
1277 | gpkg.update_metadata(tile_info) |
||
1278 | print("Complete") |
||
1279 | |||
1280 | |||
1281 | if __name__ == '__main__': |
||
1282 | print(""" |
||
1283 | tiles2gpkg_parallel.py Copyright (C) 2014 Reinventing Geospatial, Inc |
||
1284 | This program comes with ABSOLUTELY NO WARRANTY. |
||
1285 | This is free software, and you are welcome to redistribute it |
||
1286 | under certain conditions. |
||
1287 | """) |
||
1288 | PARSER = ArgumentParser(description="Convert TMS folder into geopackage") |
||
1289 | PARSER.add_argument("source_folder", |
||
1290 | metavar="source", |
||
1291 | help="Source folder of TMS files.") |
||
1292 | PARSER.add_argument("output_file", |
||
1293 | metavar="dest", |
||
1294 | help="Destination file path.") |
||
1295 | PARSER.add_argument("-tileorigin", |
||
1296 | metavar="tile_origin", |
||
1297 | help="Tile point of origin location. Valid options " + |
||
1298 | "are ll, ul, nw, or sw.", |
||
1299 | choices=["ll", "ul", "sw", "nw"], |
||
1300 | default="ll") |
||
1301 | PARSER.add_argument("-srs", |
||
1302 | metavar="srs", |
||
1303 | help="Spatial reference " + "system. Valid options are" |
||
1304 | + "3857, 4326, 3395, and 9804.", |
||
1305 | type=int, |
||
1306 | choices=[3857, 4326, 3395, 9804], |
||
1307 | default=3857) |
||
1308 | PARSER.add_argument("-imagery", |
||
1309 | metavar="imagery", |
||
1310 | help="Imagery type. Valid options are mixed, " + |
||
1311 | "jpeg, png, or source.", |
||
1312 | choices=["mixed", "jpeg", "png", "source"], |
||
1313 | default="source") |
||
1314 | PARSER.add_argument("-q", |
||
1315 | metavar="quality", |
||
1316 | type=int, |
||
1317 | default=75, |
||
1318 | help="Quality for jpeg images, 0-100. Default is 75", |
||
1319 | choices=list(range(100))) |
||
1320 | PARSER.add_argument("-a", |
||
1321 | dest="append", |
||
1322 | action="store_true", |
||
1323 | default=False, |
||
1324 | help="Append tile set to existing geopackage") |
||
1325 | PARSER.add_argument("-T", |
||
1326 | dest="threading", |
||
1327 | action="store_false", |
||
1328 | default=True, |
||
1329 | help="Disable multiprocessing.") |
||
1330 | ARG_LIST = PARSER.parse_args() |
||
1331 | if not exists(ARG_LIST.source_folder) or exists(ARG_LIST.output_file): |
||
1332 | PARSER.print_usage() |
||
1333 | print("Ensure that TMS directory exists and out file does not.") |
||
1334 | exit(1) |
||
1335 | if ARG_LIST.q is not None and ARG_LIST.imagery == 'png': |
||
1336 | PARSER.print_usage() |
||
1337 | print("-q cannot be used with png") |
||
1338 | exit(1) |
||
1339 | main(ARG_LIST) |
||
1340 |