tilegrab 1.1.0__py3-none-any.whl → 1.2.0b2__py3-none-any.whl
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/__init__.py +1 -1
- tilegrab/cli.py +95 -121
- tilegrab/dataset.py +12 -9
- tilegrab/downloader.py +32 -28
- tilegrab/images.py +178 -57
- tilegrab/logs.py +84 -0
- tilegrab/sources.py +14 -8
- tilegrab/tiles.py +166 -80
- {tilegrab-1.1.0.dist-info → tilegrab-1.2.0b2.dist-info}/METADATA +65 -100
- tilegrab-1.2.0b2.dist-info/RECORD +15 -0
- tilegrab/mosaic.py +0 -75
- tilegrab-1.1.0.dist-info/RECORD +0 -15
- {tilegrab-1.1.0.dist-info → tilegrab-1.2.0b2.dist-info}/WHEEL +0 -0
- {tilegrab-1.1.0.dist-info → tilegrab-1.2.0b2.dist-info}/entry_points.txt +0 -0
- {tilegrab-1.1.0.dist-info → tilegrab-1.2.0b2.dist-info}/licenses/LICENSE +0 -0
- {tilegrab-1.1.0.dist-info → tilegrab-1.2.0b2.dist-info}/top_level.txt +0 -0
tilegrab/images.py
CHANGED
|
@@ -1,35 +1,44 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from functools import cache
|
|
1
3
|
import logging
|
|
2
4
|
from dataclasses import dataclass
|
|
3
|
-
from io import BytesIO
|
|
4
5
|
from pathlib import Path, PosixPath, WindowsPath
|
|
5
|
-
import
|
|
6
|
-
from
|
|
7
|
-
from PIL import Image as PLIImage
|
|
6
|
+
from typing import Any, List, Union
|
|
7
|
+
from PIL import Image as PILImage
|
|
8
8
|
from box import Box
|
|
9
9
|
from tilegrab.tiles import Tile, TileCollection
|
|
10
10
|
import os
|
|
11
|
+
import numpy as np
|
|
12
|
+
from numpy.lib.stride_tricks import sliding_window_view
|
|
11
13
|
|
|
12
14
|
logger = logging.getLogger(__name__)
|
|
13
15
|
|
|
16
|
+
|
|
14
17
|
class ExportType:
|
|
15
18
|
PNG: int = 1
|
|
16
19
|
JPG: int = 2
|
|
17
20
|
TIFF: int = 3
|
|
18
21
|
|
|
22
|
+
|
|
19
23
|
@dataclass
|
|
20
24
|
class TileImage:
|
|
21
25
|
width: int = 256
|
|
22
26
|
height: int = 256
|
|
23
27
|
|
|
24
28
|
def __init__(self, tile: Tile, image: Union[bytes, bytearray]) -> None:
|
|
29
|
+
from io import BytesIO
|
|
30
|
+
|
|
25
31
|
self._tile = tile
|
|
26
32
|
try:
|
|
27
|
-
self._img =
|
|
33
|
+
self._img = PILImage.open(BytesIO(image))
|
|
28
34
|
logger.debug(f"TileImage created for z={tile.z},x={tile.x},y={tile.y}")
|
|
29
35
|
except Exception as e:
|
|
30
|
-
logger.error(
|
|
36
|
+
logger.error(
|
|
37
|
+
f"Failed to open image for tile z={tile.z},x={tile.x},y={tile.y}",
|
|
38
|
+
exc_info=True,
|
|
39
|
+
)
|
|
31
40
|
raise
|
|
32
|
-
|
|
41
|
+
|
|
33
42
|
self._path: Union[Path, None] = None
|
|
34
43
|
self._ext: str = self._get_image_type(image)
|
|
35
44
|
|
|
@@ -40,23 +49,23 @@ class TileImage:
|
|
|
40
49
|
b = bytes(data)
|
|
41
50
|
|
|
42
51
|
# PNG: 8 bytes
|
|
43
|
-
if b.startswith(b
|
|
52
|
+
if b.startswith(b"\x89PNG\r\n\x1a\n"):
|
|
44
53
|
logger.debug(f"Image detected as PNG")
|
|
45
|
-
return
|
|
54
|
+
return "png"
|
|
46
55
|
|
|
47
56
|
# JPEG / JPG: files start with FF D8 and end with FF D9
|
|
48
|
-
if len(b) >= 2 and b[0:2] == b
|
|
57
|
+
if len(b) >= 2 and b[0:2] == b"\xff\xd8":
|
|
49
58
|
logger.debug(f"Image detected as JPG")
|
|
50
|
-
return
|
|
59
|
+
return "jpg"
|
|
51
60
|
|
|
52
61
|
# BMP: starts with 'BM' (0x42 0x4D)
|
|
53
|
-
if len(b) >= 2 and b[0:2] == b
|
|
62
|
+
if len(b) >= 2 and b[0:2] == b"BM":
|
|
54
63
|
logger.debug(f"Image detected as BMP")
|
|
55
|
-
return
|
|
64
|
+
return "bmp"
|
|
56
65
|
|
|
57
66
|
logger.warning(f"Unknown image format, defaulting to PNG")
|
|
58
67
|
return "png"
|
|
59
|
-
|
|
68
|
+
|
|
60
69
|
def save(self):
|
|
61
70
|
try:
|
|
62
71
|
self._img.save(self.path)
|
|
@@ -68,16 +77,16 @@ class TileImage:
|
|
|
68
77
|
@property
|
|
69
78
|
def name(self) -> str:
|
|
70
79
|
return f"{self._tile.z}_{self._tile.x}_{self._tile.y}.{self._ext}"
|
|
71
|
-
|
|
80
|
+
|
|
72
81
|
@property
|
|
73
82
|
def tile(self) -> Tile:
|
|
74
83
|
return self._tile
|
|
75
|
-
|
|
84
|
+
|
|
76
85
|
@property
|
|
77
|
-
def image(self) ->
|
|
86
|
+
def image(self) -> PILImage.Image:
|
|
78
87
|
self._img.load()
|
|
79
88
|
return self._img
|
|
80
|
-
|
|
89
|
+
|
|
81
90
|
@property
|
|
82
91
|
def path(self) -> Path:
|
|
83
92
|
if self._path is None:
|
|
@@ -133,15 +142,68 @@ class TileImage:
|
|
|
133
142
|
return self._tile.url
|
|
134
143
|
|
|
135
144
|
|
|
145
|
+
WEB_MERCATOR_EXTENT = 20037508.342789244
|
|
146
|
+
EPSG = 3857
|
|
147
|
+
|
|
148
|
+
|
|
136
149
|
class TileImageCollection:
|
|
137
150
|
images: List[TileImage] = []
|
|
138
151
|
width: int = 0
|
|
139
152
|
height: int = 0
|
|
140
153
|
|
|
141
|
-
def
|
|
154
|
+
def __init__(self, path: Union[Path, str]) -> None:
|
|
155
|
+
self.path = Path(path)
|
|
156
|
+
logger.info(f"TileImageCollection initialized at {self.path}")
|
|
157
|
+
|
|
158
|
+
def __len__(self):
|
|
159
|
+
return sum(1 for _ in self)
|
|
160
|
+
|
|
161
|
+
def __iter__(self):
|
|
162
|
+
for i in self.images:
|
|
163
|
+
yield i
|
|
164
|
+
|
|
165
|
+
def __repr__(self) -> str:
|
|
166
|
+
return f"ImageCollection; len={len(self)}"
|
|
167
|
+
|
|
168
|
+
def _update_collection_dim(self):
|
|
169
|
+
if not self.images:
|
|
170
|
+
logger.warning("Attempting to update collection dimensions with no images")
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
x = [img.tile.x for img in self.images]
|
|
174
|
+
y = [img.tile.y for img in self.images]
|
|
175
|
+
minx, maxx = min(x), max(x)
|
|
176
|
+
miny, maxy = min(y), max(y)
|
|
177
|
+
self.minx, self.maxx = minx, maxx
|
|
178
|
+
self.miny, self.maxy = miny, maxy
|
|
179
|
+
logger.debug(f"Tile range x=({self.minx}, {self.maxx}); y=({self.miny}, {self.maxy})")
|
|
180
|
+
|
|
181
|
+
self.width = int((maxx - minx + 1) * self.images[0].width)
|
|
182
|
+
self.height = int((maxy - miny + 1) * self.images[0].height)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
logger.info(f"Collection dimensions calculated: {self.width}x{self.height}")
|
|
186
|
+
|
|
187
|
+
def mosaic_bounds(self, x_min, y_min, x_max, y_max, z):
|
|
188
|
+
n = 2**z
|
|
189
|
+
tile_size_m = 2 * WEB_MERCATOR_EXTENT / n
|
|
190
|
+
|
|
191
|
+
xmin = (WEB_MERCATOR_EXTENT * -1) + x_min * tile_size_m
|
|
192
|
+
xmax = (WEB_MERCATOR_EXTENT * -1) + (x_max + 1) * tile_size_m
|
|
193
|
+
|
|
194
|
+
ymax = WEB_MERCATOR_EXTENT - y_min * tile_size_m
|
|
195
|
+
ymin = WEB_MERCATOR_EXTENT - (y_max + 1) * tile_size_m
|
|
196
|
+
|
|
197
|
+
return xmin, ymin, xmax, ymax
|
|
198
|
+
|
|
199
|
+
def load(self, tile_collection: TileCollection):
|
|
200
|
+
import re
|
|
201
|
+
|
|
142
202
|
logger.info("Start loading saved ImageTiles")
|
|
143
|
-
pat = re.compile(r
|
|
203
|
+
pat = re.compile(r"^([0-9]+)_([0-9]+)_([0-9]+)\.[A-Za-z0-9]+$")
|
|
144
204
|
image_col = [p for p in self.path.glob(f"*.*") if p.is_file()]
|
|
205
|
+
self.zoom = tile_collection.to_list[0].z
|
|
206
|
+
logger.info(f"Found {len(image_col)} images at {self.path}")
|
|
145
207
|
|
|
146
208
|
for tile in tile_collection.to_list:
|
|
147
209
|
found_matching_image = False
|
|
@@ -154,66 +216,125 @@ class TileImageCollection:
|
|
|
154
216
|
|
|
155
217
|
if tile.x == x and tile.y == y and tile.z == z:
|
|
156
218
|
logger.debug(f"Processing ImageTile x={x} y={y} z={z}")
|
|
157
|
-
with open(image_path,
|
|
219
|
+
with open(image_path, "rb") as f:
|
|
158
220
|
tile_image = TileImage(tile, f.read())
|
|
159
221
|
tile_image.path = image_path
|
|
160
222
|
self.images.append(tile_image)
|
|
161
223
|
found_matching_image = True
|
|
162
224
|
continue
|
|
163
225
|
|
|
164
|
-
if not found_matching_image:
|
|
226
|
+
if not found_matching_image:
|
|
227
|
+
logger.warning(f"Missing ImageTile x={tile.x} y={tile.y} z={tile.z}")
|
|
165
228
|
|
|
229
|
+
logger.info(f"{len(self.images)} images loaded, {len(image_col) - len(self.images)} skipped")
|
|
230
|
+
|
|
231
|
+
self._update_collection_dim()
|
|
232
|
+
|
|
166
233
|
def append(self, img: TileImage):
|
|
167
234
|
img.path = os.path.join(self.path, img.name)
|
|
168
235
|
self.images.append(img)
|
|
169
236
|
logger.debug(f"Image appended to collection: {img.name}")
|
|
170
237
|
img.save()
|
|
171
238
|
|
|
172
|
-
|
|
173
|
-
self.path = Path(path)
|
|
174
|
-
logger.info(f"TileImageCollection initialized at {self.path}")
|
|
239
|
+
self.zoom = img.tile.z
|
|
175
240
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
def __iter__(self):
|
|
180
|
-
for i in self.images:
|
|
181
|
-
yield i
|
|
182
|
-
|
|
183
|
-
def __repr__(self) -> str:
|
|
184
|
-
return f"ImageCollection; len={len(self)}"
|
|
185
|
-
|
|
186
|
-
def _update_collection_dim(self):
|
|
187
|
-
if not self.images:
|
|
188
|
-
logger.warning("Attempting to update collection dimensions with no images")
|
|
189
|
-
return
|
|
190
|
-
|
|
191
|
-
x = [img.tile.x for img in self.images]
|
|
192
|
-
y = [img.tile.y for img in self.images]
|
|
193
|
-
minx, maxx = min(x), max(x)
|
|
194
|
-
miny, maxy = min(y), max(y)
|
|
195
|
-
|
|
196
|
-
self.width = int((maxx - minx + 1) * self.images[0].width)
|
|
197
|
-
self.height = int((maxy - miny + 1) * self.images[0].height)
|
|
198
|
-
logger.info(f"Collection dimensions calculated: {self.width}x{self.height}")
|
|
199
|
-
|
|
200
|
-
def mosaic(self):
|
|
201
|
-
logger.info("Starting mosaic creation")
|
|
241
|
+
@cache
|
|
242
|
+
def _create_mosaic(self) -> PILImage.Image:
|
|
243
|
+
logger.info("Start mosaicing TileImageCollection")
|
|
202
244
|
self._update_collection_dim()
|
|
203
245
|
|
|
204
|
-
logger.info(
|
|
205
|
-
|
|
246
|
+
logger.info(
|
|
247
|
+
f"Mosaicking {len(self.images)} images into {self.width}x{self.height}"
|
|
248
|
+
)
|
|
249
|
+
merged_image = PILImage.new("RGB", (self.width, self.height))
|
|
206
250
|
|
|
207
251
|
for image in self.images:
|
|
208
252
|
px = int((image.position.x) * image.width)
|
|
209
253
|
py = int((image.position.y) * image.height)
|
|
210
254
|
logger.debug(f"Pasting image at position ({px}, {py}): {image.name}")
|
|
211
255
|
merged_image.paste(image.image, (px, py))
|
|
256
|
+
|
|
257
|
+
return merged_image
|
|
212
258
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
259
|
+
def mosaic(self, export_types: List[int]):
|
|
260
|
+
|
|
261
|
+
merged_image = self._create_mosaic()
|
|
262
|
+
# TODO: fix this monkey patch
|
|
263
|
+
if ExportType.TIFF in export_types:
|
|
264
|
+
output_path = "mosaic.tiff"
|
|
265
|
+
import numpy as np
|
|
266
|
+
from rasterio.transform import from_bounds
|
|
267
|
+
import rasterio
|
|
268
|
+
|
|
269
|
+
data = np.array(merged_image)
|
|
270
|
+
data = data.transpose(2, 0, 1)
|
|
271
|
+
|
|
272
|
+
width_px, height_px = merged_image.size
|
|
273
|
+
xmin, ymin, xmax, ymax = self.mosaic_bounds(
|
|
274
|
+
self.minx, self.miny, self.maxx, self.maxy, self.images[0].tile.z
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
transform = from_bounds(xmin, ymin, xmax, ymax, width_px, height_px)
|
|
278
|
+
|
|
279
|
+
with rasterio.open(
|
|
280
|
+
output_path,
|
|
281
|
+
"w",
|
|
282
|
+
driver="GTiff",
|
|
283
|
+
height=height_px,
|
|
284
|
+
width=width_px,
|
|
285
|
+
count=data.shape[0],
|
|
286
|
+
dtype=data.dtype,
|
|
287
|
+
crs=f"EPSG:{EPSG}",
|
|
288
|
+
transform=transform,
|
|
289
|
+
) as dst:
|
|
290
|
+
dst.write(data)
|
|
291
|
+
|
|
292
|
+
logger.info(f"Mosaic saved to {output_path}")
|
|
293
|
+
if ExportType.PNG in export_types:
|
|
294
|
+
output_path = "mosaic.png"
|
|
295
|
+
merged_image.save(output_path)
|
|
296
|
+
logger.info(f"Mosaic saved to {output_path}")
|
|
297
|
+
if ExportType.JPG in export_types:
|
|
298
|
+
output_path = "mosaic.jpg"
|
|
299
|
+
merged_image.save(output_path)
|
|
300
|
+
logger.info(f"Mosaic saved to {output_path}")
|
|
301
|
+
|
|
302
|
+
def group(self, width: int, height: int, export_types:List[int], overlap: bool):
|
|
303
|
+
if width <= 0 or height <= 0:
|
|
304
|
+
raise ValueError("width and height must be positive integers")
|
|
305
|
+
|
|
306
|
+
out_dir = "grouped_tiles"
|
|
307
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
308
|
+
logger.info(f"Start grouping {len(self.images)} TileImages into {width}x{height} groups")
|
|
309
|
+
|
|
310
|
+
merged_image= self._create_mosaic()
|
|
311
|
+
merged_image_arr = np.asanyarray(merged_image)
|
|
312
|
+
|
|
313
|
+
image_px_width = 256
|
|
314
|
+
image_px_height = 256
|
|
315
|
+
kh, kw = image_px_height*height, image_px_width*width
|
|
316
|
+
view = sliding_window_view(merged_image_arr, (kh, kw, merged_image_arr.shape[2]))
|
|
317
|
+
|
|
318
|
+
view = view[..., 0, :, :, :]
|
|
319
|
+
OH, OW = view.shape[:2]
|
|
320
|
+
logger.info(f"Saving groups into ./{out_dir}")
|
|
321
|
+
for i in range(0, OH, height*image_px_height):
|
|
322
|
+
for j in range(0, OW, width*image_px_width):
|
|
323
|
+
patch = view[i, j]
|
|
324
|
+
if not (patch == 0).all():
|
|
325
|
+
img = PILImage.fromarray(patch)
|
|
326
|
+
if ExportType.PNG in export_types:
|
|
327
|
+
logger.debug(f"Saving group {i}{j} as {i}{j}.png")
|
|
328
|
+
img.save(os.path.join(out_dir, f"{i}{j}.png"))
|
|
329
|
+
if ExportType.JPG in export_types:
|
|
330
|
+
logger.debug(f"Saving group {i}{j} as {i}{j}.jpg")
|
|
331
|
+
img.save(os.path.join(out_dir, f"{i}{j}.jpg"))
|
|
332
|
+
if ExportType.TIFF in export_types:
|
|
333
|
+
logger.error(f"TIFF exports for tile groups not implemented yet")
|
|
334
|
+
raise NotImplementedError
|
|
335
|
+
else:
|
|
336
|
+
logger.debug(f"Skip no-data group {i}{j}")
|
|
216
337
|
|
|
217
338
|
def export_collection(self, type: ExportType):
|
|
218
339
|
logger.info(f"Exporting collection as type {type}")
|
|
219
|
-
|
|
340
|
+
raise NotImplementedError
|
tilegrab/logs.py
ADDED
|
@@ -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
|
+
)
|
tilegrab/sources.py
CHANGED
|
@@ -4,8 +4,10 @@ from typing import Dict, Optional
|
|
|
4
4
|
logger = logging.getLogger(__name__)
|
|
5
5
|
|
|
6
6
|
class TileSource:
|
|
7
|
-
|
|
8
|
-
name
|
|
7
|
+
url_template: str
|
|
8
|
+
name: str
|
|
9
|
+
description: str
|
|
10
|
+
output_dir: str
|
|
9
11
|
|
|
10
12
|
def __init__(
|
|
11
13
|
self,
|
|
@@ -17,7 +19,7 @@ class TileSource:
|
|
|
17
19
|
logger.debug(f"Initializing TileSource: {self.name}, has_api_key={api_key is not None}")
|
|
18
20
|
|
|
19
21
|
def get_url(self, z: int, x: int, y: int) -> str:
|
|
20
|
-
url = self.
|
|
22
|
+
url = self.url_template.format(x=x, y=y, z=z)
|
|
21
23
|
logger.debug(f"Generated URL for {self.name}: z={z}, x={x}, y={y}")
|
|
22
24
|
return url
|
|
23
25
|
|
|
@@ -33,30 +35,34 @@ class TileSource:
|
|
|
33
35
|
|
|
34
36
|
|
|
35
37
|
class GoogleSat(TileSource):
|
|
38
|
+
output_dir = "ggl_sat"
|
|
36
39
|
name = "GoogleSat"
|
|
37
40
|
description = "Google satellite imageries"
|
|
38
|
-
|
|
41
|
+
url_template = "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}"
|
|
39
42
|
|
|
40
43
|
|
|
41
44
|
class OSM(TileSource):
|
|
45
|
+
output_dir = "osm"
|
|
42
46
|
name = "OSM"
|
|
43
47
|
description = "OpenStreetMap imageries"
|
|
44
|
-
|
|
48
|
+
url_template = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
45
49
|
|
|
46
50
|
class ESRIWorldImagery(TileSource):
|
|
51
|
+
output_dir = "esri_world"
|
|
47
52
|
name = "ESRIWorldImagery"
|
|
48
53
|
description = "ESRI satellite imageries"
|
|
49
|
-
|
|
54
|
+
url_template = "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
|
|
50
55
|
|
|
51
56
|
class Nearmap(TileSource):
|
|
57
|
+
output_dir = "nearmap_sat"
|
|
52
58
|
name = "NearmapSat"
|
|
53
59
|
description = "Nearmap satellite imageries"
|
|
54
|
-
|
|
60
|
+
url_template = "https://api.nearmap.com/tiles/v3/Vert/{z}/{x}/{y}.png?apikey={token}"
|
|
55
61
|
|
|
56
62
|
def get_url(self, z: int, x: int, y: int) -> str:
|
|
57
63
|
if not self.api_key:
|
|
58
64
|
logger.error("Nearmap API key is required but not provided")
|
|
59
65
|
raise AssertionError("API key required for Nearmap")
|
|
60
|
-
url = self.
|
|
66
|
+
url = self.url_template.format(x=x, y=y, z=z, token=self.api_key)
|
|
61
67
|
logger.debug(f"Generated Nearmap URL: z={z}, x={x}, y={y}")
|
|
62
68
|
return url
|