tilegrab 1.1.0__py3-none-any.whl → 1.2.0b1__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 +48 -118
- tilegrab/dataset.py +6 -5
- tilegrab/images.py +61 -7
- tilegrab/logs.py +84 -0
- tilegrab/tiles.py +154 -72
- {tilegrab-1.1.0.dist-info → tilegrab-1.2.0b1.dist-info}/METADATA +2 -2
- tilegrab-1.2.0b1.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.0b1.dist-info}/WHEEL +0 -0
- {tilegrab-1.1.0.dist-info → tilegrab-1.2.0b1.dist-info}/entry_points.txt +0 -0
- {tilegrab-1.1.0.dist-info → tilegrab-1.2.0b1.dist-info}/licenses/LICENSE +0 -0
- {tilegrab-1.1.0.dist-info → tilegrab-1.2.0b1.dist-info}/top_level.txt +0 -0
tilegrab/__init__.py
CHANGED
tilegrab/cli.py
CHANGED
|
@@ -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)
|
tilegrab/dataset.py
CHANGED
|
@@ -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")
|
tilegrab/images.py
CHANGED
|
@@ -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}")
|
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/tiles.py
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import math
|
|
3
|
-
from typing import Any, List, Tuple
|
|
4
|
-
import mercantile
|
|
3
|
+
from typing import Any, Dict, Generator, Iterator, List, Tuple
|
|
5
4
|
from abc import ABC, abstractmethod
|
|
6
5
|
from dataclasses import dataclass
|
|
7
6
|
from typing import Union
|
|
8
7
|
from .dataset import GeoDataset
|
|
9
8
|
from box import Box
|
|
9
|
+
from shapely.geometry import box
|
|
10
|
+
from shapely import intersects
|
|
10
11
|
|
|
11
12
|
logger = logging.getLogger(__name__)
|
|
12
13
|
|
|
14
|
+
EPSILON = 1e-14
|
|
15
|
+
LL_EPSILON = 1e-11
|
|
16
|
+
|
|
17
|
+
|
|
13
18
|
@dataclass
|
|
14
19
|
class Tile:
|
|
15
20
|
x: int = 0
|
|
@@ -24,7 +29,7 @@ class Tile:
|
|
|
24
29
|
# def from_tuple(cls, t: tuple[int, int, int]) -> "Tile":
|
|
25
30
|
# logger.debug(f"Creating Tile from tuple: {t}")
|
|
26
31
|
# return cls(*t)
|
|
27
|
-
|
|
32
|
+
|
|
28
33
|
@property
|
|
29
34
|
def url(self) -> Union[str, None]:
|
|
30
35
|
return self._url
|
|
@@ -38,59 +43,63 @@ class Tile:
|
|
|
38
43
|
def position(self) -> Box:
|
|
39
44
|
if self._position is None:
|
|
40
45
|
logger.error(f"Tile position not set: z={self.z}, x={self.x}, y={self.y}")
|
|
41
|
-
raise RuntimeError("Image does not have
|
|
46
|
+
raise RuntimeError("Image does not have a position")
|
|
42
47
|
return self._position
|
|
43
48
|
|
|
44
49
|
@position.setter
|
|
45
50
|
def position(self, value: Tuple[float, float]):
|
|
46
51
|
x, y = self.x - value[0], self.y - value[1]
|
|
47
|
-
self._position = Box({
|
|
52
|
+
self._position = Box({"x": x, "y": y})
|
|
48
53
|
logger.debug(f"Tile position calculated: x={x}, y={y}")
|
|
49
54
|
|
|
55
|
+
|
|
50
56
|
class TileCollection(ABC):
|
|
51
|
-
|
|
57
|
+
|
|
52
58
|
MIN_X: float = 0
|
|
53
59
|
MAX_X: float = 0
|
|
54
60
|
MIN_Y: float = 0
|
|
55
61
|
MAX_Y: float = 0
|
|
56
|
-
_cache:
|
|
62
|
+
_cache: List[Tile]
|
|
63
|
+
_tile_count: int = 0
|
|
57
64
|
|
|
58
65
|
@abstractmethod
|
|
59
|
-
def _build_tile_cache(self) ->
|
|
66
|
+
def _build_tile_cache(self) -> List[Tile]:
|
|
60
67
|
raise NotImplementedError
|
|
61
68
|
|
|
62
69
|
def __len__(self):
|
|
63
|
-
return
|
|
70
|
+
return self._tile_count
|
|
64
71
|
|
|
65
72
|
def __iter__(self):
|
|
66
|
-
for t in self._cache:
|
|
67
|
-
yield t
|
|
73
|
+
for t in self._cache:
|
|
74
|
+
yield t
|
|
68
75
|
|
|
69
76
|
def __init__(self, feature: GeoDataset, zoom: int, SAFE_LIMIT: int = 250):
|
|
70
77
|
self.zoom = zoom
|
|
71
78
|
self.SAFE_LIMIT = SAFE_LIMIT
|
|
72
79
|
self.feature = feature
|
|
73
80
|
|
|
74
|
-
logger.info(
|
|
81
|
+
logger.info(
|
|
82
|
+
f"Initializing TileCollection: zoom={zoom}, safe_limit={SAFE_LIMIT}"
|
|
83
|
+
)
|
|
75
84
|
|
|
76
85
|
assert feature.bbox.minx < feature.bbox.maxx
|
|
77
86
|
assert feature.bbox.miny < feature.bbox.maxy
|
|
78
87
|
|
|
79
88
|
self._build_tile_cache()
|
|
80
|
-
|
|
81
|
-
|
|
89
|
+
|
|
82
90
|
if len(self) > SAFE_LIMIT:
|
|
83
91
|
logger.error(f"Tile count exceeds safe limit: {len(self)} > {SAFE_LIMIT}")
|
|
84
92
|
raise ValueError(
|
|
85
93
|
f"Your query excedes the hard limit {len(self)} > {SAFE_LIMIT}"
|
|
86
94
|
)
|
|
87
|
-
|
|
95
|
+
|
|
88
96
|
logger.info(f"TileCollection initialized with {len(self)} tiles")
|
|
89
97
|
|
|
90
98
|
def __repr__(self) -> str:
|
|
91
99
|
return f"TileCollection; len={len(self)}; x-extent=({self.feature.bbox.minx}-{self.feature.bbox.maxx}); y-extent=({self.feature.bbox.miny}-{self.feature.bbox.maxy})"
|
|
92
100
|
|
|
93
|
-
def tile_bounds(self,
|
|
101
|
+
def tile_bounds(self, tile: Union[Tile, Box]) -> Tuple[float, float, float, float]:
|
|
102
|
+
x, y, z = tile.x, tile.y, tile.z
|
|
94
103
|
n = 2**z
|
|
95
104
|
|
|
96
105
|
lon_min = x / n * 360.0 - 180.0
|
|
@@ -99,81 +108,154 @@ class TileCollection(ABC):
|
|
|
99
108
|
lat_min = math.degrees(math.atan(math.sinh(math.pi * (1 - 2 * (y + 1) / n))))
|
|
100
109
|
lat_max = math.degrees(math.atan(math.sinh(math.pi * (1 - 2 * y / n))))
|
|
101
110
|
|
|
102
|
-
logger.debug(
|
|
111
|
+
logger.debug(
|
|
112
|
+
f"Tile bounds calculated for z={z},x={x},y={y}: ({lon_min},{lat_min},{lon_max},{lat_max})"
|
|
113
|
+
)
|
|
103
114
|
return lon_min, lat_min, lon_max, lat_max
|
|
104
115
|
|
|
105
116
|
@property
|
|
106
|
-
def to_list(self) ->
|
|
107
|
-
|
|
117
|
+
def to_list(self) -> List[Tile]:
|
|
118
|
+
cache = list(self._cache)
|
|
119
|
+
if cache is None or len(cache) < 1:
|
|
108
120
|
logger.debug("Building tile cache from to_list property")
|
|
109
121
|
self._build_tile_cache()
|
|
110
|
-
self._update_min_max()
|
|
111
122
|
if len(self) > self.SAFE_LIMIT:
|
|
112
|
-
logger.error(
|
|
123
|
+
logger.error(
|
|
124
|
+
f"Tile count exceeds safe limit in to_list: {len(self)} > {self.SAFE_LIMIT}"
|
|
125
|
+
)
|
|
113
126
|
raise ValueError("Too many tiles")
|
|
114
|
-
|
|
115
127
|
assert self._cache
|
|
116
|
-
return self._cache
|
|
128
|
+
return list(self._cache)
|
|
117
129
|
|
|
118
|
-
def
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
def tile_bbox_geojson(self, x: int, y: int, z: int) -> dict:
|
|
131
|
+
min_lon, min_lat, max_lon, max_lat = self.tile_bbox(x, y, z)
|
|
132
|
+
return {
|
|
133
|
+
"type": "Polygon",
|
|
134
|
+
"coordinates": [[
|
|
135
|
+
[min_lon, min_lat],
|
|
136
|
+
[min_lon, max_lat],
|
|
137
|
+
[max_lon, max_lat],
|
|
138
|
+
[max_lon, min_lat],
|
|
139
|
+
[min_lon, min_lat]
|
|
140
|
+
]]
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
def tile_bbox(self, x: int, y: int, z: int) -> Tuple[float, float, float, float]:
|
|
144
|
+
"""
|
|
145
|
+
Return bounding box of a Slippy Map tile as (min_lon, min_lat, max_lon, max_lat).
|
|
146
|
+
x, y are tile indices at zoom z.
|
|
147
|
+
"""
|
|
148
|
+
n = 2.0 ** z
|
|
149
|
+
min_lon = x / n * 360.0 - 180.0
|
|
150
|
+
max_lon = (x + 1) / n * 360.0 - 180.0
|
|
151
|
+
|
|
152
|
+
def tile_y_to_lat(yt: float) -> float:
|
|
153
|
+
# convert fractional tile y to latitude in degrees
|
|
154
|
+
merc_y = math.pi * (1 - 2 * yt / n)
|
|
155
|
+
lat_rad = math.atan(math.sinh(merc_y))
|
|
156
|
+
return math.degrees(lat_rad)
|
|
157
|
+
|
|
158
|
+
max_lat = tile_y_to_lat(y) # top edge (smaller y => larger lat)
|
|
159
|
+
min_lat = tile_y_to_lat(y + 1) # bottom edge
|
|
160
|
+
|
|
161
|
+
return (min_lon, min_lat, max_lon, max_lat)
|
|
162
|
+
|
|
163
|
+
def _tiles_in_bounds(self, clip_to_shape=False) -> Iterator[Tile]:
|
|
164
|
+
|
|
165
|
+
bbox = self.feature.bbox
|
|
166
|
+
|
|
167
|
+
def tile(lng, lat, zoom):
|
|
168
|
+
logger.debug(f"Creating new Tile; lat={lat}; lng={lng}")
|
|
169
|
+
|
|
170
|
+
x = lng / 360.0 + 0.5
|
|
171
|
+
sinlat = math.sin(math.radians(lat))
|
|
124
172
|
|
|
125
|
-
|
|
173
|
+
try:
|
|
174
|
+
y = 0.5 - 0.25 * math.log((1.0 + sinlat) / (1.0 - sinlat)) / math.pi
|
|
175
|
+
except:
|
|
176
|
+
raise
|
|
177
|
+
|
|
178
|
+
Z2 = math.pow(2, zoom)
|
|
179
|
+
|
|
180
|
+
if x <= 0:
|
|
181
|
+
xtile = 0
|
|
182
|
+
elif x >= 1:
|
|
183
|
+
xtile = int(Z2 - 1)
|
|
184
|
+
else:
|
|
185
|
+
# To address loss of precision in round-tripping between tile
|
|
186
|
+
# and lng/lat, points within EPSILON of the right side of a tile
|
|
187
|
+
# are counted in the next tile over.
|
|
188
|
+
xtile = int(math.floor((x + EPSILON) * Z2))
|
|
189
|
+
|
|
190
|
+
if y <= 0:
|
|
191
|
+
ytile = 0
|
|
192
|
+
elif y >= 1:
|
|
193
|
+
ytile = int(Z2 - 1)
|
|
194
|
+
else:
|
|
195
|
+
ytile = int(math.floor((y + EPSILON) * Z2))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
return Box({"x": xtile, "y": ytile})
|
|
199
|
+
|
|
200
|
+
w, s, e, n = bbox.minx, bbox.miny, bbox.maxx, bbox.maxy
|
|
201
|
+
if s < -85.051129 or n > 85.051129:
|
|
202
|
+
logger.warning("Your geometry bounds exceed the Web Mercator's limits")
|
|
203
|
+
logger.info("Clipping bounds for Web Mercator's limits")
|
|
204
|
+
|
|
205
|
+
w = max(-180.0, w)
|
|
206
|
+
s = max(-85.051129, s)
|
|
207
|
+
e = min(180.0, e)
|
|
208
|
+
n = min(85.051129, n)
|
|
209
|
+
|
|
210
|
+
ul_tile = tile(w, n, self.zoom)
|
|
211
|
+
lr_tile = tile(e - LL_EPSILON, s + LL_EPSILON, self.zoom)
|
|
212
|
+
logger.debug(f"UpperLeft Tile=({ul_tile}); LowerRight Tile=({lr_tile})")
|
|
213
|
+
|
|
214
|
+
self._tile_count = 0
|
|
215
|
+
self.MAX_X, self.MIN_X = lr_tile.x, ul_tile.x
|
|
216
|
+
self.MAX_Y, self.MIN_Y = lr_tile.y, ul_tile.y
|
|
217
|
+
logger.info(
|
|
218
|
+
f"TileCollection bounds: x=({self.MIN_X}, {self.MAX_X}) y=({self.MIN_Y}, {self.MAX_Y})"
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
for i in range(ul_tile.x, lr_tile.x + 1):
|
|
222
|
+
for j in range(ul_tile.y, lr_tile.y + 1):
|
|
223
|
+
if clip_to_shape:
|
|
224
|
+
tb = box(*self.tile_bbox(i,j,self.zoom))
|
|
225
|
+
if not tb.intersects(self.feature.shape.geometry).any():
|
|
226
|
+
logger.debug(f"Tile excluded: z={self.zoom}, x={i}, y={j}")
|
|
227
|
+
continue
|
|
228
|
+
self._tile_count += 1
|
|
229
|
+
t = Tile(i, j, self.zoom)
|
|
230
|
+
t.position = self.MIN_X, self.MIN_Y
|
|
231
|
+
yield t
|
|
126
232
|
|
|
127
|
-
for i in range(len(self._cache)):
|
|
128
|
-
self._cache[i].position = self.MIN_X, self.MIN_Y
|
|
129
233
|
|
|
130
234
|
class TilesByBBox(TileCollection):
|
|
131
|
-
|
|
132
|
-
def _build_tile_cache(self) ->
|
|
235
|
+
|
|
236
|
+
def _build_tile_cache(self) -> List[Tile]:
|
|
133
237
|
logger.info(f"Building tiles by bounding box at zoom level {self.zoom}")
|
|
134
238
|
bbox = self.feature.bbox
|
|
135
|
-
logger.debug(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
bbox.miny,
|
|
142
|
-
bbox.maxx,
|
|
143
|
-
bbox.maxy,
|
|
144
|
-
self.zoom,
|
|
145
|
-
)
|
|
146
|
-
]
|
|
147
|
-
|
|
148
|
-
logger.debug(f"Generated {len(self._cache)} tiles from bounding box")
|
|
239
|
+
logger.debug(
|
|
240
|
+
f"BBox coordinates: minx={bbox.minx}, miny={bbox.miny}, maxx={bbox.maxx}, maxy={bbox.maxy}"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
self._cache = list(self._tiles_in_bounds(True))
|
|
244
|
+
logger.debug(f"Generated {len(self)} tiles from bounding box")
|
|
149
245
|
return self._cache
|
|
150
246
|
|
|
247
|
+
|
|
151
248
|
class TilesByShape(TileCollection):
|
|
152
249
|
|
|
153
|
-
def _build_tile_cache(self) ->
|
|
154
|
-
from shapely.geometry import box
|
|
250
|
+
def _build_tile_cache(self) -> List[Tile]:
|
|
155
251
|
|
|
156
252
|
logger.info(f"Building tiles by shape intersection at zoom level {self.zoom}")
|
|
157
|
-
geometry = self.feature.shape
|
|
158
|
-
if hasattr(geometry, "geometry"):
|
|
159
|
-
geometry = geometry.geometry.unary_union
|
|
160
|
-
logger.debug("Converted GeoDataFrame geometry to unary_union")
|
|
161
|
-
|
|
162
|
-
tiles = []
|
|
163
|
-
bbox = self.feature.bbox
|
|
164
|
-
logger.debug(f"Checking tiles within bbox: minx={bbox.minx}, miny={bbox.miny}, maxx={bbox.maxx}, maxy={bbox.maxy}")
|
|
165
|
-
|
|
166
|
-
for t in mercantile.tiles(
|
|
167
|
-
bbox.minx,
|
|
168
|
-
bbox.miny,
|
|
169
|
-
bbox.maxx,
|
|
170
|
-
bbox.maxy,
|
|
171
|
-
self.zoom,
|
|
172
|
-
):
|
|
173
|
-
tb = box(*self.tile_bounds(t.x, t.y, t.z))
|
|
174
|
-
if tb.intersects(geometry):
|
|
175
|
-
tiles.append(Tile(t.x, t.y, t.z))
|
|
176
253
|
|
|
177
|
-
|
|
178
|
-
logger.
|
|
179
|
-
|
|
254
|
+
bbox = self.feature.bbox
|
|
255
|
+
logger.debug(
|
|
256
|
+
f"Checking tiles within bbox: minx={bbox.minx}, miny={bbox.miny}, maxx={bbox.maxx}, maxy={bbox.maxy}"
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
self._cache = list(self._tiles_in_bounds(True))
|
|
260
|
+
logger.info(f"Generated {len(self)} tiles from shape intersection")
|
|
261
|
+
return self._cache
|
|
@@ -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
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
tilegrab/__init__.py,sha256=unu6CU3corZS1SmNsFCi-o6od2C-yIzCfVm82ADJTXM,295
|
|
2
|
+
tilegrab/__main__.py,sha256=wT62aXCuN7iwRPQoswnmeGiBS5Cou0CIBvMxbOTfvFs,33
|
|
3
|
+
tilegrab/cli.py,sha256=RH787UzO4pGpjPMq_BHnTO_A9qhuJnurtvn2LG4sn4U,6395
|
|
4
|
+
tilegrab/dataset.py,sha256=tmbvR7wd9NYWCPsDLo9SP0x0_3edyRURQHo-6EnEIAM,2623
|
|
5
|
+
tilegrab/downloader.py,sha256=ge3o4PS0v3_npaiOvFmlNSx62VqL8nwjcefy4Jr_UQo,5120
|
|
6
|
+
tilegrab/images.py,sha256=VUE9Wsswkl2wAzkQnwDtncgKVU-wl3xGWagmx3u6Dk8,9278
|
|
7
|
+
tilegrab/logs.py,sha256=serS8_0bhxoTO0F03t-BDSdkMYR_eX78vI2y1sSVpUk,2151
|
|
8
|
+
tilegrab/sources.py,sha256=GzldtFGndziw9NNB1YMG4WUrD0fTRET1FjGCfIMMDlQ,2188
|
|
9
|
+
tilegrab/tiles.py,sha256=nCJL4tOKKQoZPJbGgDfL-I1DxYjo2Ajk69McvtAoEWM,8960
|
|
10
|
+
tilegrab-1.2.0b1.dist-info/licenses/LICENSE,sha256=bcZProekTTcPtnEpRKB2i1kUJ6ujlaxBZchNWKeoXc8,1094
|
|
11
|
+
tilegrab-1.2.0b1.dist-info/METADATA,sha256=UHVyRZO5L1yo1Upe7jQoig0KG4Af_p6gzv6cJAfl4AA,10197
|
|
12
|
+
tilegrab-1.2.0b1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
13
|
+
tilegrab-1.2.0b1.dist-info/entry_points.txt,sha256=z-WoKN8NnA5_ZsWXucSeq-r4TeEscu0xjWYu560uNTk,47
|
|
14
|
+
tilegrab-1.2.0b1.dist-info/top_level.txt,sha256=lVio8bCk3r4Bu_INLgaj4PZCrhFY2UcxHjnFBdHwyPo,9
|
|
15
|
+
tilegrab-1.2.0b1.dist-info/RECORD,,
|
tilegrab/mosaic.py
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from typing import List
|
|
4
|
-
from PIL import Image
|
|
5
|
-
import re
|
|
6
|
-
import os
|
|
7
|
-
|
|
8
|
-
from tilegrab.tiles import TileCollection
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
class Mosaic:
|
|
12
|
-
|
|
13
|
-
def __init__(
|
|
14
|
-
self, directory: str = "saved_tiles", ext: str = ".png", recursive: bool = False
|
|
15
|
-
):
|
|
16
|
-
self.directory = directory
|
|
17
|
-
self.ext = ext
|
|
18
|
-
self.recursive = recursive
|
|
19
|
-
|
|
20
|
-
self.image_col = self._get_images()
|
|
21
|
-
assert len(self.image_col) > 0
|
|
22
|
-
|
|
23
|
-
pat = re.compile(
|
|
24
|
-
r"^([0-9]+)_([0-9]+)_([0-9]+)\.[A-Za-z0-9]+$"
|
|
25
|
-
)
|
|
26
|
-
self.image_data = {}
|
|
27
|
-
|
|
28
|
-
for i in self.image_col:
|
|
29
|
-
m = pat.match(os.path.basename(str(i)))
|
|
30
|
-
if m:
|
|
31
|
-
first = m.group(1)
|
|
32
|
-
second = m.group(2)
|
|
33
|
-
third = m.group(3)
|
|
34
|
-
|
|
35
|
-
self.image_data[i] = [int(second), int(third), int(first)]
|
|
36
|
-
|
|
37
|
-
assert len(self.image_data.keys()) > 0
|
|
38
|
-
|
|
39
|
-
print(f"Processing {len(self.image_data.keys())} tiles...")
|
|
40
|
-
|
|
41
|
-
def _get_images(self) -> List[Path]:
|
|
42
|
-
|
|
43
|
-
directory = Path(self.directory)
|
|
44
|
-
ext = self.ext
|
|
45
|
-
|
|
46
|
-
if not ext.startswith("."):
|
|
47
|
-
ext = "." + self.ext
|
|
48
|
-
|
|
49
|
-
if self.recursive:
|
|
50
|
-
return [p for p in directory.rglob(f"*{ext}") if p.is_file()]
|
|
51
|
-
else:
|
|
52
|
-
return [p for p in directory.glob(f"*{ext}") if p.is_file()]
|
|
53
|
-
|
|
54
|
-
def merge(self, tiles: TileCollection, tile_size: int = 256):
|
|
55
|
-
|
|
56
|
-
img_w = int((tiles.MAX_X - tiles.MIN_X + 1) * tile_size)
|
|
57
|
-
img_h = int((tiles.MAX_Y - tiles.MIN_Y + 1) * tile_size)
|
|
58
|
-
print(f"Image size: {img_w}x{img_h}")
|
|
59
|
-
|
|
60
|
-
merged_image = Image.new("RGB", (img_w, img_h))
|
|
61
|
-
|
|
62
|
-
for img_path, img_id in self.image_data.items():
|
|
63
|
-
x, y, _ = img_id
|
|
64
|
-
|
|
65
|
-
print(x - tiles.MIN_X + 1, "x" ,y - tiles.MIN_Y + 1)
|
|
66
|
-
|
|
67
|
-
img = Image.open(img_path)
|
|
68
|
-
img.load()
|
|
69
|
-
|
|
70
|
-
px = int((x - tiles.MIN_X) * tile_size)
|
|
71
|
-
py = int((y - tiles.MIN_Y) * tile_size)
|
|
72
|
-
|
|
73
|
-
merged_image.paste(img, (px, py))
|
|
74
|
-
|
|
75
|
-
merged_image.save(os.path.join(self.directory, "merged_output.png"))
|
tilegrab-1.1.0.dist-info/RECORD
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
tilegrab/__init__.py,sha256=0L3afZyDycCUGuBO0w7DQ9fOd798dUyinbGsM77aQOg,289
|
|
2
|
-
tilegrab/__main__.py,sha256=wT62aXCuN7iwRPQoswnmeGiBS5Cou0CIBvMxbOTfvFs,33
|
|
3
|
-
tilegrab/cli.py,sha256=4AxEpxBhl8REUFAc7Dqa-bCxCo5oUNJGZ1Om3QnTHV0,7700
|
|
4
|
-
tilegrab/dataset.py,sha256=dNZ52mTkX_FC4e-hKAdEsvL5JVeo-MXhhAHpxKjwyGE,2525
|
|
5
|
-
tilegrab/downloader.py,sha256=ge3o4PS0v3_npaiOvFmlNSx62VqL8nwjcefy4Jr_UQo,5120
|
|
6
|
-
tilegrab/images.py,sha256=rld-BLuGA8LPcq0QbGsVY48mbMRivvzxdyfAwVfe0dk,7466
|
|
7
|
-
tilegrab/mosaic.py,sha256=H2-Mr2j9AmwSicPGwYE5z5t0wdAfj2m42J0G7IxE0zA,2191
|
|
8
|
-
tilegrab/sources.py,sha256=GzldtFGndziw9NNB1YMG4WUrD0fTRET1FjGCfIMMDlQ,2188
|
|
9
|
-
tilegrab/tiles.py,sha256=WEWv96fCeHlZs_Gxzg0m3wYAy_qsHfAwkCQHcWEFwf4,6137
|
|
10
|
-
tilegrab-1.1.0.dist-info/licenses/LICENSE,sha256=bcZProekTTcPtnEpRKB2i1kUJ6ujlaxBZchNWKeoXc8,1094
|
|
11
|
-
tilegrab-1.1.0.dist-info/METADATA,sha256=ddmKHnd5A9LaTsjOIxSics7kovf_AXKC92bN3rrhdYY,10197
|
|
12
|
-
tilegrab-1.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
13
|
-
tilegrab-1.1.0.dist-info/entry_points.txt,sha256=z-WoKN8NnA5_ZsWXucSeq-r4TeEscu0xjWYu560uNTk,47
|
|
14
|
-
tilegrab-1.1.0.dist-info/top_level.txt,sha256=lVio8bCk3r4Bu_INLgaj4PZCrhFY2UcxHjnFBdHwyPo,9
|
|
15
|
-
tilegrab-1.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|