tilegrab 1.1.0__tar.gz → 1.2.0b1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/PKG-INFO +2 -2
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/pyproject.toml +2 -2
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab/__init__.py +1 -1
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab/cli.py +48 -118
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab/dataset.py +6 -5
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab/images.py +61 -7
- tilegrab-1.2.0b1/src/tilegrab/logs.py +84 -0
- tilegrab-1.2.0b1/src/tilegrab/tiles.py +261 -0
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab.egg-info/PKG-INFO +2 -2
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab.egg-info/SOURCES.txt +1 -1
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab.egg-info/requires.txt +1 -1
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/uv.lock +332 -61
- tilegrab-1.1.0/src/tilegrab/mosaic.py +0 -75
- tilegrab-1.1.0/src/tilegrab/tiles.py +0 -179
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/.github/workflows/test.yml +0 -0
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/.gitignore +0 -0
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/.python-version +0 -0
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/.vscode/settings.json +0 -0
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/LICENSE +0 -0
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/README.md +0 -0
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/setup.cfg +0 -0
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab/__main__.py +0 -0
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab/downloader.py +0 -0
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab/sources.py +0 -0
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab.egg-info/dependency_links.txt +0 -0
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab.egg-info/entry_points.txt +0 -0
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/src/tilegrab.egg-info/top_level.txt +0 -0
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/tests/data/T.geojson +0 -0
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/tests/test_dataset.py +0 -0
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/tests/test_downloader.py +0 -0
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/tests/test_sources.py +0 -0
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/tests/test_tiles.py +0 -0
- {tilegrab-1.1.0 → tilegrab-1.2.0b1}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tilegrab
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0b1
|
|
4
4
|
Summary: Fast geospatial map tile downloader and mosaicker
|
|
5
5
|
Author: Thiwanka Munasinghe
|
|
6
6
|
License-Expression: MIT
|
|
@@ -14,7 +14,7 @@ Requires-Python: >=3.9
|
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
16
|
Requires-Dist: requests
|
|
17
|
-
Requires-Dist:
|
|
17
|
+
Requires-Dist: rasterio
|
|
18
18
|
Requires-Dist: pillow
|
|
19
19
|
Requires-Dist: tqdm
|
|
20
20
|
Requires-Dist: python-box
|
|
@@ -7,7 +7,7 @@ requires = [
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "tilegrab"
|
|
10
|
-
version = "1.
|
|
10
|
+
version = "1.2.0b1"
|
|
11
11
|
description = "Fast geospatial map tile downloader and mosaicker"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
authors = [
|
|
@@ -25,7 +25,7 @@ keywords = [
|
|
|
25
25
|
|
|
26
26
|
dependencies = [
|
|
27
27
|
"requests",
|
|
28
|
-
"
|
|
28
|
+
"rasterio",
|
|
29
29
|
"pillow",
|
|
30
30
|
"tqdm",
|
|
31
31
|
"python-box",
|
|
@@ -1,91 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
import logging
|
|
3
3
|
import argparse
|
|
4
|
-
import random
|
|
5
|
-
import sys
|
|
6
4
|
from pathlib import Path
|
|
7
5
|
from tilegrab.downloader import Downloader
|
|
8
6
|
from tilegrab.images import TileImageCollection
|
|
7
|
+
from tilegrab.logs import setup_logging
|
|
9
8
|
from tilegrab.tiles import TilesByShape, TilesByBBox
|
|
10
9
|
from tilegrab.dataset import GeoDataset
|
|
11
|
-
from tilegrab import
|
|
12
|
-
|
|
13
|
-
# Configure root logger
|
|
14
|
-
# logging.basicConfig(
|
|
15
|
-
# level=logging.INFO,
|
|
16
|
-
# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
17
|
-
# handlers=[
|
|
18
|
-
# logging.StreamHandler(sys.stdout),
|
|
19
|
-
# logging.FileHandler('tilegrab.log')
|
|
20
|
-
# ]
|
|
21
|
-
# )
|
|
22
|
-
|
|
23
|
-
# Normal colors
|
|
24
|
-
BLACK = "\033[30m"
|
|
25
|
-
RED = "\033[31m"
|
|
26
|
-
GREEN = "\033[32m"
|
|
27
|
-
YELLOW = "\033[33m"
|
|
28
|
-
BLUE = "\033[34m"
|
|
29
|
-
MAGENTA = "\033[35m"
|
|
30
|
-
CYAN = "\033[36m"
|
|
31
|
-
GRAY = "\033[90m"
|
|
32
|
-
WHITE = "\033[37m"
|
|
33
|
-
|
|
34
|
-
# Bright colors
|
|
35
|
-
BBLACK = "\033[90m"
|
|
36
|
-
BRED = "\033[91m"
|
|
37
|
-
BGREEN = "\033[92m"
|
|
38
|
-
BYELLOW = "\033[93m"
|
|
39
|
-
BBLUE = "\033[94m"
|
|
40
|
-
BMAGENTA = "\033[95m"
|
|
41
|
-
BCYAN = "\033[96m"
|
|
42
|
-
BGRAY = "\033[97m"
|
|
43
|
-
BWHITE = "\033[97m"
|
|
44
|
-
|
|
45
|
-
RESET = "\033[0m"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
class LogFormatter(logging.Formatter):
|
|
49
|
-
NAME_WIDTH = 14
|
|
50
|
-
|
|
51
|
-
LEVEL_MAP = {
|
|
52
|
-
logging.CRITICAL: f'{RED}‼ {RESET}',
|
|
53
|
-
logging.ERROR: f'{RED}✖ {RESET}',
|
|
54
|
-
logging.WARNING: f'{YELLOW}⚠ {RESET}',
|
|
55
|
-
logging.INFO: f'{BLUE}• {RESET}',
|
|
56
|
-
logging.DEBUG: f'{GRAY}· {RESET}',
|
|
57
|
-
logging.NOTSET: f'{CYAN}- {RESET}',
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
def format(self, record):
|
|
61
|
-
record.level_letter = self.LEVEL_MAP.get(record.levelno, '?')
|
|
62
|
-
|
|
63
|
-
short = record.name.rsplit('.', 1)[-1]
|
|
64
|
-
record.short_name = f"{short:<{self.NAME_WIDTH}}"
|
|
65
|
-
|
|
66
|
-
return super().format(record)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
console_formatter = LogFormatter(
|
|
70
|
-
f' %(level_letter)s %(message)s'
|
|
71
|
-
)
|
|
72
|
-
file_formatter = logging.Formatter(
|
|
73
|
-
'%(asctime)s %(levelname)s %(name)s - %(message)s'
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
console = logging.StreamHandler(sys.stdout)
|
|
77
|
-
console.setFormatter(console_formatter )
|
|
78
|
-
|
|
79
|
-
file = logging.FileHandler('tilegrab.log')
|
|
80
|
-
file.setFormatter(file_formatter)
|
|
81
|
-
|
|
82
|
-
logging.basicConfig(
|
|
83
|
-
level=logging.INFO,
|
|
84
|
-
handlers=[console, file],
|
|
85
|
-
)
|
|
10
|
+
from tilegrab import __version__
|
|
86
11
|
|
|
87
|
-
logger = logging.getLogger(__name__)
|
|
88
12
|
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
89
14
|
|
|
90
15
|
def parse_args() -> argparse.Namespace:
|
|
91
16
|
p = argparse.ArgumentParser(
|
|
@@ -127,13 +52,22 @@ def parse_args() -> argparse.Namespace:
|
|
|
127
52
|
"--key", type=str, default=None, help="API key where required by source"
|
|
128
53
|
)
|
|
129
54
|
|
|
55
|
+
# Create a named group for merged output format
|
|
56
|
+
mosaic_out_group = p.add_argument_group(
|
|
57
|
+
title="Mosaic export formats", description="Formats for the output mosaic image"
|
|
58
|
+
)
|
|
59
|
+
mosaic_group = mosaic_out_group.add_mutually_exclusive_group(required=True)
|
|
60
|
+
mosaic_group.add_argument("--jpg", action="store_true", help="JPG image; no geo-reference")
|
|
61
|
+
mosaic_group.add_argument("--png", action="store_true", help="PNG image; no geo-reference")
|
|
62
|
+
mosaic_group.add_argument("--tiff", action="store_true", help="GeoTiff image; with geo-reference")
|
|
63
|
+
|
|
130
64
|
# other options
|
|
131
65
|
p.add_argument("--zoom", type=int, required=True, help="Zoom level (integer)")
|
|
132
66
|
p.add_argument(
|
|
133
|
-
"--out",
|
|
67
|
+
"--tiles-out",
|
|
134
68
|
type=Path,
|
|
135
69
|
default=Path.cwd() / "saved_tiles",
|
|
136
|
-
help="Output directory (default: ./saved_tiles)",
|
|
70
|
+
help="Output directory for downloaded tiles (default: ./saved_tiles)",
|
|
137
71
|
)
|
|
138
72
|
p.add_argument(
|
|
139
73
|
"--download-only",
|
|
@@ -148,56 +82,47 @@ def parse_args() -> argparse.Namespace:
|
|
|
148
82
|
p.add_argument(
|
|
149
83
|
"--no-progress", action="store_false", help="Hide download progress bar"
|
|
150
84
|
)
|
|
85
|
+
p.add_argument("--quiet", action="store_true", help="Hide all prints")
|
|
86
|
+
p.add_argument("--debug", action="store_true", help="Enable debug logging")
|
|
151
87
|
p.add_argument(
|
|
152
|
-
"--
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
"--debug", action="store_true", help="Enable debug logging"
|
|
156
|
-
)
|
|
157
|
-
p.add_argument(
|
|
158
|
-
"--test", action="store_true", help="Only for testing purposes, not for normal use"
|
|
88
|
+
"--test",
|
|
89
|
+
action="store_true",
|
|
90
|
+
help="Only for testing purposes, not for normal use",
|
|
159
91
|
)
|
|
160
92
|
|
|
161
93
|
return p.parse_args()
|
|
162
94
|
|
|
163
95
|
|
|
164
96
|
def main():
|
|
97
|
+
LOG_LEVEL = logging.INFO
|
|
98
|
+
ENABLE_CLI_LOG = True
|
|
99
|
+
ENABLE_FILE_LOG = True
|
|
100
|
+
|
|
165
101
|
args = parse_args()
|
|
166
|
-
|
|
167
|
-
# Adjust logging level
|
|
168
102
|
if args.debug:
|
|
169
|
-
logging.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
console.close()
|
|
173
|
-
|
|
174
|
-
|
|
103
|
+
LOG_LEVEL = logging.DEBUG
|
|
104
|
+
if args.quiet:
|
|
105
|
+
ENABLE_CLI_LOG = False
|
|
175
106
|
|
|
176
|
-
|
|
177
|
-
(RED, BRED),
|
|
178
|
-
(GREEN, BGREEN),
|
|
179
|
-
(YELLOW, BYELLOW),
|
|
180
|
-
(BLUE, BBLUE),
|
|
181
|
-
(MAGENTA, BMAGENTA),
|
|
182
|
-
(CYAN, BCYAN),
|
|
183
|
-
(GRAY, BGRAY),
|
|
184
|
-
])
|
|
107
|
+
setup_logging(ENABLE_CLI_LOG, ENABLE_FILE_LOG, LOG_LEVEL)
|
|
185
108
|
|
|
186
109
|
if not args.quiet:
|
|
187
110
|
print()
|
|
188
|
-
print(f"
|
|
189
|
-
print(f"
|
|
190
|
-
print(f"
|
|
191
|
-
|
|
111
|
+
print(f"\033[37m " + ("-" * 60) + "\033[0m")
|
|
112
|
+
print(f"\033[97m TileGrab v{__version__}\033[0m".rjust(50))
|
|
113
|
+
print(f"\033[37m " + ("-" * 60) + "\033[0m")
|
|
114
|
+
|
|
192
115
|
try:
|
|
193
116
|
dataset = GeoDataset(args.source)
|
|
194
117
|
logger.info(f"Dataset loaded successfully from {args.source}")
|
|
195
118
|
|
|
196
119
|
_tmp = "bbox" if args.bbox else "shape" if args.shape else "DnE"
|
|
197
|
-
logger.info(
|
|
120
|
+
logger.info(
|
|
121
|
+
f"""Downloading tiles using {_tmp}
|
|
198
122
|
- minX: {dataset.bbox.minx:.4f} - minY: {dataset.bbox.miny:.4f}
|
|
199
123
|
- maxX: {dataset.bbox.maxx:.4f} - maxY: {dataset.bbox.maxy:.4f}
|
|
200
|
-
- zoom: {args.zoom}"""
|
|
124
|
+
- zoom: {args.zoom}"""
|
|
125
|
+
)
|
|
201
126
|
|
|
202
127
|
if args.shape:
|
|
203
128
|
tiles = TilesByShape(dataset, zoom=args.zoom)
|
|
@@ -210,34 +135,39 @@ def main():
|
|
|
210
135
|
# Choose source provider
|
|
211
136
|
if args.osm:
|
|
212
137
|
from tilegrab.sources import OSM
|
|
138
|
+
|
|
213
139
|
logger.info("Using OpenStreetMap (OSM) as tile source")
|
|
214
140
|
source = OSM(api_key=args.key) if args.key else OSM()
|
|
215
141
|
elif args.google_sat:
|
|
216
142
|
from tilegrab.sources import GoogleSat
|
|
143
|
+
|
|
217
144
|
logger.info("Using Google Satellite as tile source")
|
|
218
145
|
source = GoogleSat(api_key=args.key) if args.key else GoogleSat()
|
|
219
146
|
elif args.esri_sat:
|
|
220
147
|
from tilegrab.sources import ESRIWorldImagery
|
|
148
|
+
|
|
221
149
|
logger.info("Using ESRI World Imagery as tile source")
|
|
222
|
-
source =
|
|
150
|
+
source = (
|
|
151
|
+
ESRIWorldImagery(api_key=args.key) if args.key else ESRIWorldImagery()
|
|
152
|
+
)
|
|
223
153
|
else:
|
|
224
154
|
logger.error("No tile source selected")
|
|
225
155
|
raise SystemExit("No tile source selected")
|
|
226
|
-
|
|
227
|
-
downloader = Downloader(tiles, source, args.
|
|
156
|
+
|
|
157
|
+
downloader = Downloader(tiles, source, args.tiles_out)
|
|
228
158
|
result: TileImageCollection
|
|
229
159
|
|
|
230
160
|
if args.mosaic_only:
|
|
231
|
-
result = TileImageCollection(args.
|
|
232
|
-
result.load(tiles)
|
|
161
|
+
result = TileImageCollection(args.tiles_out)
|
|
162
|
+
result.load(tiles)
|
|
233
163
|
else:
|
|
234
164
|
result = downloader.run(show_progress=args.no_progress)
|
|
235
165
|
logger.info(f"Download result: {result}")
|
|
236
|
-
|
|
237
|
-
if not args.download_only:
|
|
166
|
+
|
|
167
|
+
if not args.download_only:
|
|
168
|
+
result.mosaic(tiff=args.tiff, png=args.png)
|
|
238
169
|
logger.info("Done")
|
|
239
170
|
|
|
240
|
-
|
|
241
171
|
except Exception as e:
|
|
242
172
|
logger.exception("Fatal error during execution")
|
|
243
173
|
raise SystemExit(1)
|
|
@@ -6,6 +6,7 @@ from typing import Union
|
|
|
6
6
|
|
|
7
7
|
logger = logging.getLogger(__name__)
|
|
8
8
|
|
|
9
|
+
TILE_EPSG = 4326 #Web Mercator - 3857 | 4326 - WGS84
|
|
9
10
|
class GeoDataset:
|
|
10
11
|
|
|
11
12
|
@property
|
|
@@ -63,14 +64,14 @@ class GeoDataset:
|
|
|
63
64
|
logger.critical("Dataset has no CRS defined")
|
|
64
65
|
raise RuntimeError("Missing CRS")
|
|
65
66
|
|
|
66
|
-
if epsg !=
|
|
67
|
-
logger.info(f"Reprojecting dataset from EPSG:{epsg} to EPSG:
|
|
68
|
-
gdf = gdf.to_crs(epsg=
|
|
67
|
+
if epsg != TILE_EPSG: #Web Mercator
|
|
68
|
+
logger.info(f"Reprojecting dataset from EPSG:{epsg} to EPSG:{TILE_EPSG}")
|
|
69
|
+
gdf = gdf.to_crs(epsg=TILE_EPSG)
|
|
69
70
|
else:
|
|
70
|
-
logger.debug("Dataset already in EPSG:
|
|
71
|
+
logger.debug(f"Dataset already in EPSG:{TILE_EPSG}")
|
|
71
72
|
|
|
72
73
|
self.original_epsg = epsg
|
|
73
|
-
self.current_epsg =
|
|
74
|
+
self.current_epsg = TILE_EPSG
|
|
74
75
|
self.source = gdf
|
|
75
76
|
self.source_path = source_path
|
|
76
77
|
logger.info(f"GeoDataset initialized successfully: {len(gdf)} features")
|
|
@@ -3,7 +3,7 @@ from dataclasses import dataclass
|
|
|
3
3
|
from io import BytesIO
|
|
4
4
|
from pathlib import Path, PosixPath, WindowsPath
|
|
5
5
|
import re
|
|
6
|
-
from typing import Any, List,
|
|
6
|
+
from typing import Any, List, Union
|
|
7
7
|
from PIL import Image as PLIImage
|
|
8
8
|
from box import Box
|
|
9
9
|
from tilegrab.tiles import Tile, TileCollection
|
|
@@ -132,16 +132,30 @@ class TileImage:
|
|
|
132
132
|
def url(self) -> Union[str, None]:
|
|
133
133
|
return self._tile.url
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
WEB_MERCATOR_EXTENT = 20037508.342789244
|
|
136
|
+
EPSG = 3857
|
|
136
137
|
class TileImageCollection:
|
|
137
138
|
images: List[TileImage] = []
|
|
138
139
|
width: int = 0
|
|
139
140
|
height: int = 0
|
|
140
141
|
|
|
142
|
+
def mosaic_bounds(self, x_min, y_min, x_max, y_max, z):
|
|
143
|
+
n = 2 ** z
|
|
144
|
+
tile_size_m = 2 * WEB_MERCATOR_EXTENT / n
|
|
145
|
+
|
|
146
|
+
xmin = (WEB_MERCATOR_EXTENT*-1) + x_min * tile_size_m
|
|
147
|
+
xmax = (WEB_MERCATOR_EXTENT*-1) + (x_max + 1) * tile_size_m
|
|
148
|
+
|
|
149
|
+
ymax = WEB_MERCATOR_EXTENT - y_min * tile_size_m
|
|
150
|
+
ymin = WEB_MERCATOR_EXTENT - (y_max + 1) * tile_size_m
|
|
151
|
+
|
|
152
|
+
return xmin, ymin, xmax, ymax
|
|
153
|
+
|
|
141
154
|
def load(self, tile_collection:TileCollection):
|
|
142
155
|
logger.info("Start loading saved ImageTiles")
|
|
143
156
|
pat = re.compile(r'^([0-9]+)_([0-9]+)_([0-9]+)\.[A-Za-z0-9]+$')
|
|
144
157
|
image_col = [p for p in self.path.glob(f"*.*") if p.is_file()]
|
|
158
|
+
self.zoom = tile_collection.to_list[0].z
|
|
145
159
|
|
|
146
160
|
for tile in tile_collection.to_list:
|
|
147
161
|
found_matching_image = False
|
|
@@ -169,6 +183,8 @@ class TileImageCollection:
|
|
|
169
183
|
logger.debug(f"Image appended to collection: {img.name}")
|
|
170
184
|
img.save()
|
|
171
185
|
|
|
186
|
+
self.zoom = img.tile.z
|
|
187
|
+
|
|
172
188
|
def __init__(self, path: Union[Path, str]) -> None:
|
|
173
189
|
self.path = Path(path)
|
|
174
190
|
logger.info(f"TileImageCollection initialized at {self.path}")
|
|
@@ -195,12 +211,15 @@ class TileImageCollection:
|
|
|
195
211
|
|
|
196
212
|
self.width = int((maxx - minx + 1) * self.images[0].width)
|
|
197
213
|
self.height = int((maxy - miny + 1) * self.images[0].height)
|
|
214
|
+
self.minx, self.maxx = minx, maxx
|
|
215
|
+
self.miny, self.maxy = miny, maxy
|
|
216
|
+
|
|
198
217
|
logger.info(f"Collection dimensions calculated: {self.width}x{self.height}")
|
|
199
218
|
|
|
200
|
-
def mosaic(self):
|
|
219
|
+
def mosaic(self, tiff:bool=True, png:bool=False):
|
|
201
220
|
logger.info("Starting mosaic creation")
|
|
202
221
|
self._update_collection_dim()
|
|
203
|
-
|
|
222
|
+
|
|
204
223
|
logger.info(f"Mosaicking {len(self.images)} images into {self.width}x{self.height}")
|
|
205
224
|
merged_image = PLIImage.new("RGB", (self.width, self.height))
|
|
206
225
|
|
|
@@ -210,9 +229,44 @@ class TileImageCollection:
|
|
|
210
229
|
logger.debug(f"Pasting image at position ({px}, {py}): {image.name}")
|
|
211
230
|
merged_image.paste(image.image, (px, py))
|
|
212
231
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
232
|
+
# TODO: fix this monkey patch
|
|
233
|
+
if tiff:
|
|
234
|
+
output_path = "mosaic.tiff"
|
|
235
|
+
import numpy as np
|
|
236
|
+
from rasterio.transform import from_bounds
|
|
237
|
+
import rasterio
|
|
238
|
+
|
|
239
|
+
data = np.array(merged_image)
|
|
240
|
+
data = data.transpose(2, 0, 1)
|
|
241
|
+
|
|
242
|
+
width_px, height_px = merged_image.size
|
|
243
|
+
xmin, ymin, xmax, ymax = self.mosaic_bounds(
|
|
244
|
+
self.minx, self.miny, self.maxx, self.maxy, self.images[0].tile.z
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
transform = from_bounds(
|
|
248
|
+
xmin, ymin, xmax, ymax,
|
|
249
|
+
width_px, height_px
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
with rasterio.open(
|
|
253
|
+
output_path,
|
|
254
|
+
"w",
|
|
255
|
+
driver="GTiff",
|
|
256
|
+
height=height_px,
|
|
257
|
+
width=width_px,
|
|
258
|
+
count=data.shape[0],
|
|
259
|
+
dtype=data.dtype,
|
|
260
|
+
crs=f"EPSG:{EPSG}",
|
|
261
|
+
transform=transform,
|
|
262
|
+
) as dst:
|
|
263
|
+
dst.write(data)
|
|
264
|
+
|
|
265
|
+
logger.info(f"Mosaic saved to {output_path}")
|
|
266
|
+
if png:
|
|
267
|
+
output_path = "mosaic.png"
|
|
268
|
+
merged_image.save(output_path)
|
|
269
|
+
logger.info(f"Mosaic saved to {output_path}")
|
|
216
270
|
|
|
217
271
|
def export_collection(self, type: ExportType):
|
|
218
272
|
logger.info(f"Exporting collection as type {type}")
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
# Normal colors
|
|
5
|
+
BLACK = "\033[30m"
|
|
6
|
+
RED = "\033[31m"
|
|
7
|
+
GREEN = "\033[32m"
|
|
8
|
+
YELLOW = "\033[33m"
|
|
9
|
+
BLUE = "\033[34m"
|
|
10
|
+
MAGENTA = "\033[35m"
|
|
11
|
+
CYAN = "\033[36m"
|
|
12
|
+
GRAY = "\033[90m"
|
|
13
|
+
WHITE = "\033[37m"
|
|
14
|
+
|
|
15
|
+
# Bright colors
|
|
16
|
+
BBLACK = "\033[90m"
|
|
17
|
+
BRED = "\033[91m"
|
|
18
|
+
BGREEN = "\033[92m"
|
|
19
|
+
BYELLOW = "\033[93m"
|
|
20
|
+
BBLUE = "\033[94m"
|
|
21
|
+
BMAGENTA = "\033[95m"
|
|
22
|
+
BCYAN = "\033[96m"
|
|
23
|
+
BGRAY = "\033[97m"
|
|
24
|
+
BWHITE = "\033[97m"
|
|
25
|
+
|
|
26
|
+
RESET = "\033[0m"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CLILogFormatter(logging.Formatter):
|
|
30
|
+
NAME_WIDTH = 20
|
|
31
|
+
LEVEL_MAP = {
|
|
32
|
+
logging.CRITICAL: f'{RED}‼ {RESET}',
|
|
33
|
+
logging.ERROR: f'{RED}✖ {RESET}',
|
|
34
|
+
logging.WARNING: f'{YELLOW}⚠ {RESET}',
|
|
35
|
+
logging.INFO: f'{BLUE}• {RESET}',
|
|
36
|
+
logging.DEBUG: f'{GRAY}· {RESET}',
|
|
37
|
+
logging.NOTSET: f'{CYAN}- {RESET}',
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
def __init__(self, fmt=None):
|
|
41
|
+
super().__init__(fmt or ' %(level_icon)s %(message)s')
|
|
42
|
+
|
|
43
|
+
def format(self, record):
|
|
44
|
+
record.level_icon = self.LEVEL_MAP.get(record.levelno, '?')
|
|
45
|
+
short = record.name.rsplit('.', 1)[-1]
|
|
46
|
+
record.short_name = f"{short:<{self.NAME_WIDTH}}"
|
|
47
|
+
|
|
48
|
+
return super().format(record)
|
|
49
|
+
|
|
50
|
+
class FileLogFormatter(logging.Formatter):
|
|
51
|
+
def __init__(self):
|
|
52
|
+
super().__init__(
|
|
53
|
+
'%(asctime)s %(levelname)s %(name)s - %(message)s'
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
def create_cli_handler(level=logging.INFO):
|
|
57
|
+
handler = logging.StreamHandler(sys.stdout)
|
|
58
|
+
handler.setLevel(level)
|
|
59
|
+
handler.setFormatter(CLILogFormatter())
|
|
60
|
+
return handler
|
|
61
|
+
|
|
62
|
+
def create_file_handler(path="tilegrab.log", level=logging.DEBUG):
|
|
63
|
+
handler = logging.FileHandler(path)
|
|
64
|
+
handler.setLevel(level)
|
|
65
|
+
handler.setFormatter(FileLogFormatter())
|
|
66
|
+
return handler
|
|
67
|
+
|
|
68
|
+
def setup_logging(
|
|
69
|
+
enable_cli=True,
|
|
70
|
+
enable_file=True,
|
|
71
|
+
level=logging.INFO
|
|
72
|
+
):
|
|
73
|
+
handlers = []
|
|
74
|
+
|
|
75
|
+
if enable_cli:
|
|
76
|
+
handlers.append(create_cli_handler(level))
|
|
77
|
+
|
|
78
|
+
if enable_file:
|
|
79
|
+
handlers.append(create_file_handler(level=level))
|
|
80
|
+
|
|
81
|
+
logging.basicConfig(
|
|
82
|
+
level=level,
|
|
83
|
+
handlers=handlers,
|
|
84
|
+
)
|