build_lut()   F
last analyzed

Complexity

Conditions 22

Size

Total Lines 93

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 22
c 3
b 0
f 0
dl 0
loc 93
rs 2

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like build_lut() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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
This code seems to be duplicated in your project.
Loading history...
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
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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