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/tiles.py
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
import math
|
|
3
|
-
from typing import
|
|
4
|
-
import mercantile
|
|
3
|
+
from typing import Iterator, List, Tuple, Union
|
|
5
4
|
from abc import ABC, abstractmethod
|
|
6
5
|
from dataclasses import dataclass
|
|
7
|
-
from typing import Union
|
|
8
6
|
from .dataset import GeoDataset
|
|
9
7
|
from box import Box
|
|
8
|
+
from functools import cache
|
|
10
9
|
|
|
11
10
|
logger = logging.getLogger(__name__)
|
|
12
11
|
|
|
12
|
+
EPSILON = 1e-14
|
|
13
|
+
LL_EPSILON = 1e-11
|
|
14
|
+
|
|
15
|
+
|
|
13
16
|
@dataclass
|
|
14
17
|
class Tile:
|
|
15
18
|
x: int = 0
|
|
@@ -18,13 +21,13 @@ class Tile:
|
|
|
18
21
|
|
|
19
22
|
def __post_init__(self):
|
|
20
23
|
self._position = None
|
|
21
|
-
logger.debug(f"Tile created:
|
|
24
|
+
logger.debug(f"Tile created: x={self.x}, y={self.y}, z={self.z}")
|
|
22
25
|
|
|
23
26
|
# @classmethod
|
|
24
27
|
# def from_tuple(cls, t: tuple[int, int, int]) -> "Tile":
|
|
25
28
|
# logger.debug(f"Creating Tile from tuple: {t}")
|
|
26
29
|
# return cls(*t)
|
|
27
|
-
|
|
30
|
+
|
|
28
31
|
@property
|
|
29
32
|
def url(self) -> Union[str, None]:
|
|
30
33
|
return self._url
|
|
@@ -38,59 +41,67 @@ class Tile:
|
|
|
38
41
|
def position(self) -> Box:
|
|
39
42
|
if self._position is None:
|
|
40
43
|
logger.error(f"Tile position not set: z={self.z}, x={self.x}, y={self.y}")
|
|
41
|
-
raise RuntimeError("Image does not have
|
|
44
|
+
raise RuntimeError("Image does not have a position")
|
|
42
45
|
return self._position
|
|
43
46
|
|
|
44
47
|
@position.setter
|
|
45
48
|
def position(self, value: Tuple[float, float]):
|
|
46
49
|
x, y = self.x - value[0], self.y - value[1]
|
|
47
|
-
self._position = Box({
|
|
50
|
+
self._position = Box({"x": x, "y": y})
|
|
48
51
|
logger.debug(f"Tile position calculated: x={x}, y={y}")
|
|
49
52
|
|
|
53
|
+
|
|
50
54
|
class TileCollection(ABC):
|
|
51
|
-
|
|
55
|
+
|
|
52
56
|
MIN_X: float = 0
|
|
53
57
|
MAX_X: float = 0
|
|
54
58
|
MIN_Y: float = 0
|
|
55
59
|
MAX_Y: float = 0
|
|
56
|
-
_cache:
|
|
60
|
+
_cache: List[Tile]
|
|
61
|
+
_tile_count: int = 0
|
|
57
62
|
|
|
58
63
|
@abstractmethod
|
|
59
|
-
def _build_tile_cache(self) ->
|
|
64
|
+
def _build_tile_cache(self) -> List[Tile]:
|
|
60
65
|
raise NotImplementedError
|
|
61
66
|
|
|
62
67
|
def __len__(self):
|
|
63
|
-
return
|
|
68
|
+
return self._tile_count
|
|
64
69
|
|
|
65
70
|
def __iter__(self):
|
|
66
|
-
for t in self._cache:
|
|
67
|
-
yield t
|
|
71
|
+
for t in self._cache:
|
|
72
|
+
yield t
|
|
68
73
|
|
|
69
|
-
def __init__(self,
|
|
74
|
+
def __init__(self,
|
|
75
|
+
geo_dataset: GeoDataset,
|
|
76
|
+
zoom: int,
|
|
77
|
+
SAFE_LIMIT: int = 250
|
|
78
|
+
):
|
|
70
79
|
self.zoom = zoom
|
|
71
80
|
self.SAFE_LIMIT = SAFE_LIMIT
|
|
72
|
-
self.
|
|
81
|
+
self.geo_dataset = geo_dataset
|
|
73
82
|
|
|
74
|
-
logger.info(
|
|
83
|
+
logger.info(
|
|
84
|
+
f"Initializing TileCollection: zoom={zoom}, safe_limit={SAFE_LIMIT}"
|
|
85
|
+
)
|
|
75
86
|
|
|
76
|
-
assert
|
|
77
|
-
assert
|
|
87
|
+
assert geo_dataset.bbox.minx < geo_dataset.bbox.maxx
|
|
88
|
+
assert geo_dataset.bbox.miny < geo_dataset.bbox.maxy
|
|
78
89
|
|
|
79
90
|
self._build_tile_cache()
|
|
80
|
-
|
|
81
|
-
|
|
91
|
+
|
|
82
92
|
if len(self) > SAFE_LIMIT:
|
|
83
93
|
logger.error(f"Tile count exceeds safe limit: {len(self)} > {SAFE_LIMIT}")
|
|
84
94
|
raise ValueError(
|
|
85
95
|
f"Your query excedes the hard limit {len(self)} > {SAFE_LIMIT}"
|
|
86
96
|
)
|
|
87
|
-
|
|
97
|
+
|
|
88
98
|
logger.info(f"TileCollection initialized with {len(self)} tiles")
|
|
89
99
|
|
|
90
100
|
def __repr__(self) -> str:
|
|
91
|
-
return f"TileCollection; len={len(self)}; x-extent=({self.
|
|
101
|
+
return f"TileCollection; len={len(self)}; x-extent=({self.geo_dataset.bbox.minx}-{self.geo_dataset.bbox.maxx}); y-extent=({self.geo_dataset.bbox.miny}-{self.geo_dataset.bbox.maxy})"
|
|
92
102
|
|
|
93
|
-
def tile_bounds(self,
|
|
103
|
+
def tile_bounds(self, tile: Union[Tile, Box]) -> Tuple[float, float, float, float]:
|
|
104
|
+
x, y, z = tile.x, tile.y, tile.z
|
|
94
105
|
n = 2**z
|
|
95
106
|
|
|
96
107
|
lon_min = x / n * 360.0 - 180.0
|
|
@@ -99,81 +110,156 @@ class TileCollection(ABC):
|
|
|
99
110
|
lat_min = math.degrees(math.atan(math.sinh(math.pi * (1 - 2 * (y + 1) / n))))
|
|
100
111
|
lat_max = math.degrees(math.atan(math.sinh(math.pi * (1 - 2 * y / n))))
|
|
101
112
|
|
|
102
|
-
logger.debug(
|
|
113
|
+
logger.debug(
|
|
114
|
+
f"Tile bounds calculated for z={z},x={x},y={y}: ({lon_min},{lat_min},{lon_max},{lat_max})"
|
|
115
|
+
)
|
|
103
116
|
return lon_min, lat_min, lon_max, lat_max
|
|
104
117
|
|
|
105
118
|
@property
|
|
106
|
-
|
|
107
|
-
|
|
119
|
+
@cache
|
|
120
|
+
def to_list(self) -> List[Tile]:
|
|
121
|
+
cache = list(self._cache)
|
|
122
|
+
if cache is None or len(cache) < 1:
|
|
108
123
|
logger.debug("Building tile cache from to_list property")
|
|
109
124
|
self._build_tile_cache()
|
|
110
|
-
self._update_min_max()
|
|
111
125
|
if len(self) > self.SAFE_LIMIT:
|
|
112
|
-
logger.error(
|
|
126
|
+
logger.error(
|
|
127
|
+
f"Tile count exceeds safe limit in to_list: {len(self)} > {self.SAFE_LIMIT}"
|
|
128
|
+
)
|
|
113
129
|
raise ValueError("Too many tiles")
|
|
114
|
-
|
|
115
130
|
assert self._cache
|
|
116
|
-
return self._cache
|
|
131
|
+
return list(self._cache)
|
|
117
132
|
|
|
118
|
-
def
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
133
|
+
def tile_bbox_geojson(self, x: int, y: int, z: int) -> dict:
|
|
134
|
+
min_lon, min_lat, max_lon, max_lat = self.tile_bbox(x, y, z)
|
|
135
|
+
return {
|
|
136
|
+
"type": "Polygon",
|
|
137
|
+
"coordinates": [[
|
|
138
|
+
[min_lon, min_lat],
|
|
139
|
+
[min_lon, max_lat],
|
|
140
|
+
[max_lon, max_lat],
|
|
141
|
+
[max_lon, min_lat],
|
|
142
|
+
[min_lon, min_lat]
|
|
143
|
+
]]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
def tile_bbox(self, x: int, y: int, z: int) -> Tuple[float, float, float, float]:
|
|
147
|
+
"""
|
|
148
|
+
Return bounding box of a Slippy Map tile as (min_lon, min_lat, max_lon, max_lat).
|
|
149
|
+
x, y are tile indices at zoom z.
|
|
150
|
+
"""
|
|
151
|
+
n = 2.0 ** z
|
|
152
|
+
min_lon = x / n * 360.0 - 180.0
|
|
153
|
+
max_lon = (x + 1) / n * 360.0 - 180.0
|
|
154
|
+
|
|
155
|
+
def tile_y_to_lat(yt: float) -> float:
|
|
156
|
+
# convert fractional tile y to latitude in degrees
|
|
157
|
+
merc_y = math.pi * (1 - 2 * yt / n)
|
|
158
|
+
lat_rad = math.atan(math.sinh(merc_y))
|
|
159
|
+
return math.degrees(lat_rad)
|
|
160
|
+
|
|
161
|
+
max_lat = tile_y_to_lat(y) # top edge (smaller y => larger lat)
|
|
162
|
+
min_lat = tile_y_to_lat(y + 1) # bottom edge
|
|
163
|
+
|
|
164
|
+
return (min_lon, min_lat, max_lon, max_lat)
|
|
165
|
+
|
|
166
|
+
def _tiles_in_bounds(self, clip_to_shape=False) -> Iterator[Tile]:
|
|
167
|
+
|
|
168
|
+
bbox = self.geo_dataset.bbox
|
|
169
|
+
|
|
170
|
+
def tile(lng, lat, zoom):
|
|
171
|
+
logger.debug(f"Creating new Tile; lat={lat}; lng={lng}")
|
|
124
172
|
|
|
125
|
-
|
|
173
|
+
x = lng / 360.0 + 0.5
|
|
174
|
+
sinlat = math.sin(math.radians(lat))
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
y = 0.5 - 0.25 * math.log((1.0 + sinlat) / (1.0 - sinlat)) / math.pi
|
|
178
|
+
except:
|
|
179
|
+
raise
|
|
180
|
+
|
|
181
|
+
Z2 = math.pow(2, zoom)
|
|
182
|
+
|
|
183
|
+
if x <= 0:
|
|
184
|
+
xtile = 0
|
|
185
|
+
elif x >= 1:
|
|
186
|
+
xtile = int(Z2 - 1)
|
|
187
|
+
else:
|
|
188
|
+
# To address loss of precision in round-tripping between tile
|
|
189
|
+
# and lng/lat, points within EPSILON of the right side of a tile
|
|
190
|
+
# are counted in the next tile over.
|
|
191
|
+
xtile = int(math.floor((x + EPSILON) * Z2))
|
|
192
|
+
|
|
193
|
+
if y <= 0:
|
|
194
|
+
ytile = 0
|
|
195
|
+
elif y >= 1:
|
|
196
|
+
ytile = int(Z2 - 1)
|
|
197
|
+
else:
|
|
198
|
+
ytile = int(math.floor((y + EPSILON) * Z2))
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
return Box({"x": xtile, "y": ytile})
|
|
202
|
+
|
|
203
|
+
w, s, e, n = bbox.minx, bbox.miny, bbox.maxx, bbox.maxy
|
|
204
|
+
if s < -85.051129 or n > 85.051129:
|
|
205
|
+
logger.warning("Your geometry bounds exceed the Web Mercator's limits")
|
|
206
|
+
logger.info("Clipping bounds for Web Mercator's limits")
|
|
207
|
+
|
|
208
|
+
w = max(-180.0, w)
|
|
209
|
+
s = max(-85.051129, s)
|
|
210
|
+
e = min(180.0, e)
|
|
211
|
+
n = min(85.051129, n)
|
|
212
|
+
|
|
213
|
+
ul_tile = tile(w, n, self.zoom)
|
|
214
|
+
lr_tile = tile(e - LL_EPSILON, s + LL_EPSILON, self.zoom)
|
|
215
|
+
logger.debug(f"UpperLeft Tile=({ul_tile}); LowerRight Tile=({lr_tile})")
|
|
216
|
+
|
|
217
|
+
self._tile_count = 0
|
|
218
|
+
self.MAX_X, self.MIN_X = lr_tile.x, ul_tile.x
|
|
219
|
+
self.MAX_Y, self.MIN_Y = lr_tile.y, ul_tile.y
|
|
220
|
+
logger.info(
|
|
221
|
+
f"TileCollection bounds: x=({self.MIN_X}, {self.MAX_X}) y=({self.MIN_Y}, {self.MAX_Y})"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
for i in range(ul_tile.x, lr_tile.x + 1):
|
|
225
|
+
for j in range(ul_tile.y, lr_tile.y + 1):
|
|
226
|
+
if clip_to_shape:
|
|
227
|
+
from shapely.geometry import box
|
|
228
|
+
tb = box(*self.tile_bbox(i,j,self.zoom))
|
|
229
|
+
if not tb.intersects(self.geo_dataset.shape.geometry).any():
|
|
230
|
+
logger.debug(f"Tile excluded: z={self.zoom}, x={i}, y={j}")
|
|
231
|
+
continue
|
|
232
|
+
self._tile_count += 1
|
|
233
|
+
t = Tile(i, j, self.zoom)
|
|
234
|
+
t.position = self.MIN_X, self.MIN_Y
|
|
235
|
+
yield t
|
|
126
236
|
|
|
127
|
-
for i in range(len(self._cache)):
|
|
128
|
-
self._cache[i].position = self.MIN_X, self.MIN_Y
|
|
129
237
|
|
|
130
238
|
class TilesByBBox(TileCollection):
|
|
131
|
-
|
|
132
|
-
def _build_tile_cache(self) ->
|
|
239
|
+
|
|
240
|
+
def _build_tile_cache(self) -> List[Tile]:
|
|
133
241
|
logger.info(f"Building tiles by bounding box at zoom level {self.zoom}")
|
|
134
|
-
bbox = self.
|
|
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")
|
|
242
|
+
bbox = self.geo_dataset.bbox
|
|
243
|
+
logger.debug(
|
|
244
|
+
f"BBox coordinates: minx={bbox.minx}, miny={bbox.miny}, maxx={bbox.maxx}, maxy={bbox.maxy}"
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
self._cache = list(self._tiles_in_bounds(True))
|
|
248
|
+
logger.debug(f"Generated {len(self)} tiles from bounding box")
|
|
149
249
|
return self._cache
|
|
150
250
|
|
|
251
|
+
|
|
151
252
|
class TilesByShape(TileCollection):
|
|
152
253
|
|
|
153
|
-
def _build_tile_cache(self) ->
|
|
154
|
-
from shapely.geometry import box
|
|
254
|
+
def _build_tile_cache(self) -> List[Tile]:
|
|
155
255
|
|
|
156
256
|
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
257
|
|
|
177
|
-
|
|
178
|
-
logger.
|
|
179
|
-
|
|
258
|
+
bbox = self.geo_dataset.bbox
|
|
259
|
+
logger.debug(
|
|
260
|
+
f"Checking tiles within bbox: minx={bbox.minx}, miny={bbox.miny}, maxx={bbox.maxx}, maxy={bbox.maxy}"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
self._cache = list(self._tiles_in_bounds(True))
|
|
264
|
+
logger.info(f"Generated {len(self)} tiles from shape intersection")
|
|
265
|
+
return self._cache
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tilegrab
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0b2
|
|
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
|
|
@@ -32,13 +32,13 @@ Dynamic: license-file
|
|
|
32
32
|
|
|
33
33
|
<div align="center">
|
|
34
34
|
<h1 align="center">TileGrab 🧩</h1>
|
|
35
|
-
|
|
36
|
-
<img alt="
|
|
37
|
-
|
|
38
|
-
<img alt="
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
<img alt="
|
|
35
|
+
<img alt="TileGrab" src="https://img.shields.io/pypi/v/tilegrab.svg">
|
|
36
|
+
<img alt="PyPI - Python Version" src="https://img.shields.io/pypi/pyversions/tilegrab.svg">
|
|
37
|
+
<img alt="PyPI - Wheel" src="https://img.shields.io/pypi/wheel/tilegrab">
|
|
38
|
+
<img alt="Test Status" src="https://img.shields.io/github/actions/workflow/status/thiwaK/tilegrab/test.yml?branch=main&event=push&style=flat&label=CI">
|
|
39
|
+
<img alt="PyPI - Implementation" src="https://img.shields.io/pypi/implementation/tilegrab">
|
|
40
|
+
<img alt="GitHub code size in bytes" src="https://img.shields.io/github/languages/code-size/thiwaK/tilegrab">
|
|
41
|
+
<img alt="GitHub License" src="https://img.shields.io/github/license/thiwaK/tilegrab">
|
|
42
42
|
<br/>
|
|
43
43
|
<br/>
|
|
44
44
|
</div>
|
|
@@ -47,7 +47,7 @@ Dynamic: license-file
|
|
|
47
47
|
**Fast, scriptable map tile downloader and mosaicker for geospatial workflows.**
|
|
48
48
|
|
|
49
49
|
|
|
50
|
-
`tilegrab` downloads raster map tiles from common providers (OSM, Google Satellite, ESRI World Imagery) using a **vector extent** (polygon or bounding box), then optionally mosaics them into a single
|
|
50
|
+
`tilegrab` downloads raster map tiles from common providers (OSM, Google Satellite, ESRI World Imagery) using a **vector extent** (polygon shape or bounding box), then optionally mosaics them into a single or multiple rasters.
|
|
51
51
|
|
|
52
52
|
---
|
|
53
53
|
|
|
@@ -62,20 +62,19 @@ Dynamic: license-file
|
|
|
62
62
|
|
|
63
63
|
## Why tilegrab?
|
|
64
64
|
|
|
65
|
-
Most tile downloaders
|
|
65
|
+
Most tile downloaders have two major drawbacks:
|
|
66
66
|
- GUI tools that don’t scale or automate
|
|
67
67
|
- Scripts that only support bounding boxes and break on real geometries
|
|
68
68
|
|
|
69
69
|
`tilegrab` is different:
|
|
70
70
|
|
|
71
|
-
- Uses **actual vector geometries**, not just extents
|
|
71
|
+
- Uses **actual vector geometries**, not only just extents
|
|
72
|
+
- Scalable API
|
|
72
73
|
- Clean CLI, easy to script and integrate
|
|
73
74
|
- Works with **Shapefiles, GeoPackages, GeoJSON**
|
|
74
|
-
- Supports **download-only**, **mosaic-only
|
|
75
|
+
- Supports **download-only**, **mosaic-only** or full pipelines
|
|
75
76
|
- Designed for **GIS, remote sensing, and map production workflows**
|
|
76
77
|
|
|
77
|
-
No magic. No black boxes.
|
|
78
|
-
|
|
79
78
|
---
|
|
80
79
|
|
|
81
80
|
## Features
|
|
@@ -86,8 +85,9 @@ No magic. No black boxes.
|
|
|
86
85
|
- Multiple tile providers
|
|
87
86
|
- OpenStreetMap
|
|
88
87
|
- Google Satellite
|
|
89
|
-
- ESRI World Imagery
|
|
90
|
-
-
|
|
88
|
+
- ESRI World Imagery
|
|
89
|
+
- or Custom providers
|
|
90
|
+
- Tile mosaicking
|
|
91
91
|
- Progress reporting (optional)
|
|
92
92
|
- API-key support where required
|
|
93
93
|
- Sensible defaults, strict CLI validation
|
|
@@ -98,12 +98,15 @@ No magic. No black boxes.
|
|
|
98
98
|
|
|
99
99
|
### From TestPyPI
|
|
100
100
|
|
|
101
|
+
#### Stable version
|
|
101
102
|
```bash
|
|
102
|
-
pip install -i
|
|
103
|
+
pip install -i tilegrab
|
|
103
104
|
````
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
106
|
+
#### Beta version
|
|
107
|
+
```bash
|
|
108
|
+
pip install tilegrab==1.2.0b2
|
|
109
|
+
````
|
|
107
110
|
|
|
108
111
|
---
|
|
109
112
|
|
|
@@ -134,76 +137,55 @@ tilegrab \
|
|
|
134
137
|
## CLI Usage
|
|
135
138
|
|
|
136
139
|
```bash
|
|
137
|
-
usage: tilegrab [-h] --source SOURCE (--shape | --bbox) (--osm | --google_sat | --esri_sat | --key KEY) --zoom ZOOM [--out
|
|
140
|
+
usage: tilegrab [-h] --source SOURCE (--shape | --bbox) (--osm | --google_sat | --esri_sat | --key KEY) (--jpg | --png | --tiff) --zoom ZOOM [--tiles-out TILES_OUT] [--download-only] [--mosaic-only]
|
|
141
|
+
[--group-tiles GROUP_TILES] [--group-overlap] [--tile-limit TILE_LIMIT] [--workers WORKERS] [--no-parallel] [--no-progress] [--quiet] [--debug]
|
|
138
142
|
|
|
139
143
|
Download and mosaic map tiles
|
|
140
144
|
|
|
141
145
|
options:
|
|
142
|
-
-h, --help
|
|
143
|
-
--zoom ZOOM
|
|
144
|
-
--out
|
|
145
|
-
|
|
146
|
-
--
|
|
147
|
-
--
|
|
148
|
-
--
|
|
146
|
+
-h, --help show this help message and exit
|
|
147
|
+
--zoom ZOOM Zoom level (integer between 1 and 22)
|
|
148
|
+
--tiles-out TILES_OUT
|
|
149
|
+
Output directory for downloaded tiles (default: ./saved_tiles)
|
|
150
|
+
--download-only Only download tiles; do not run mosaicking or postprocessing
|
|
151
|
+
--mosaic-only Only mosaic tiles; do not download
|
|
152
|
+
--group-tiles GROUP_TILES
|
|
153
|
+
Mosaic tiles but according to given WxH into ./grouped_tiles
|
|
154
|
+
--group-overlap Overlap with the next consecutive tile when grouping
|
|
155
|
+
--tile-limit TILE_LIMIT
|
|
156
|
+
Override maximum tile limit that can download (use with caution)
|
|
157
|
+
--workers WORKERS Max number of threads to use when parallel downloading
|
|
158
|
+
--no-parallel Download tiles sequentially, no parallel downloading
|
|
159
|
+
--no-progress Hide tile download progress bar
|
|
160
|
+
--quiet Hide all prints
|
|
161
|
+
--debug Enable debug logging
|
|
149
162
|
|
|
150
163
|
Source options(Extent):
|
|
151
164
|
Options for the vector polygon source
|
|
152
165
|
|
|
153
|
-
--source SOURCE
|
|
154
|
-
--shape
|
|
155
|
-
--bbox
|
|
166
|
+
--source SOURCE The vector polygon source for filter tiles
|
|
167
|
+
--shape Use actual shape to derive tiles
|
|
168
|
+
--bbox Use shape's bbox to derive tiles
|
|
156
169
|
|
|
157
170
|
Source options(Map tiles):
|
|
158
171
|
Options for the map tile source
|
|
159
172
|
|
|
160
|
-
--osm
|
|
161
|
-
--google_sat
|
|
162
|
-
--esri_sat
|
|
163
|
-
--key KEY
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
---
|
|
167
|
-
<!--
|
|
168
|
-
## Required Arguments
|
|
169
|
-
|
|
170
|
-
| Argument | Description |
|
|
171
|
-
| ---------- | ----------------------------------------- |
|
|
172
|
-
| `--source` | Vector dataset used to derive tile extent |
|
|
173
|
-
| `--shape` | Use exact geometry to select tiles |
|
|
174
|
-
| `--bbox` | Use geometry bounding box |
|
|
175
|
-
| `--zoom` | Web map zoom level |
|
|
173
|
+
--osm OpenStreetMap
|
|
174
|
+
--google_sat Google Satellite
|
|
175
|
+
--esri_sat ESRI World Imagery
|
|
176
|
+
--key KEY API key where required by source
|
|
176
177
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
## Tile Sources (CLI)
|
|
178
|
+
Mosaic export formats:
|
|
179
|
+
Formats for the output mosaic image
|
|
180
180
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
| `--google_sat` | Google Satellite |
|
|
185
|
-
| `--esri_sat` | ESRI World Imagery |
|
|
181
|
+
--jpg JPG image; no geo-reference
|
|
182
|
+
--png PNG image; no geo-reference
|
|
183
|
+
--tiff GeoTiff image; with geo-reference
|
|
186
184
|
|
|
187
|
-
Optional API key:
|
|
188
|
-
|
|
189
|
-
```bash
|
|
190
|
-
--key YOUR_API_KEY
|
|
191
185
|
```
|
|
192
186
|
|
|
193
187
|
---
|
|
194
188
|
|
|
195
|
-
## Output & Processing Options
|
|
196
|
-
|
|
197
|
-
| Option | Description |
|
|
198
|
-
| ----------------- | ------------------------------------------- |
|
|
199
|
-
| `--out <dir>` | Output directory (default: `./saved_tiles`) |
|
|
200
|
-
| `--download-only` | Download tiles only |
|
|
201
|
-
| `--mosaic-only` | Mosaic existing tiles only |
|
|
202
|
-
| `--no-progress` | Disable progress bar |
|
|
203
|
-
| `--quiet` | Suppress console output |
|
|
204
|
-
|
|
205
|
-
---
|
|
206
|
-
-->
|
|
207
189
|
|
|
208
190
|
## Supported Vector Formats
|
|
209
191
|
|
|
@@ -222,9 +204,6 @@ Any format readable by **GeoPandas**, including:
|
|
|
222
204
|
|
|
223
205
|
If a tile service follows the standard `{z}/{x}/{y}` pattern, you can add it in **one small class** by extending `TileSource`.
|
|
224
206
|
|
|
225
|
-
No registration. No plugin system. No magic.
|
|
226
|
-
|
|
227
|
-
---
|
|
228
207
|
|
|
229
208
|
### Example
|
|
230
209
|
|
|
@@ -234,12 +213,9 @@ from tilegrab.sources import TileSource
|
|
|
234
213
|
class MyCustomSource(TileSource):
|
|
235
214
|
name = "MyCustomSource name"
|
|
236
215
|
description = "MyCustomSource description"
|
|
237
|
-
|
|
216
|
+
url_template = "https://MyCustomSource/{z}/{x}/{y}.png"
|
|
238
217
|
```
|
|
239
218
|
|
|
240
|
-
That’s it.
|
|
241
|
-
|
|
242
|
-
Once instantiated, the source works exactly like built-in providers.
|
|
243
219
|
|
|
244
220
|
---
|
|
245
221
|
|
|
@@ -251,28 +227,19 @@ You can change how the url is generate by override `get_url` function, inside yo
|
|
|
251
227
|
```python
|
|
252
228
|
def get_url(self, z: int, x: int, y: int) -> str:
|
|
253
229
|
assert self.api_key
|
|
254
|
-
return self.
|
|
230
|
+
return self.url_template.format(x=x, y=y, z=z, token=self.api_key)
|
|
255
231
|
```
|
|
256
232
|
|
|
257
233
|
### URL Template Rules
|
|
258
234
|
|
|
259
235
|
Your tile source **must** define:
|
|
260
|
-
|
|
261
|
-
* `URL_TEMPLATE`
|
|
236
|
+
* `url_template`
|
|
262
237
|
Must contain `{z}`, `{x}`, `{y}` placeholders.
|
|
263
238
|
|
|
264
239
|
Optional but recommended:
|
|
265
|
-
|
|
266
240
|
* `name` – Human-readable name
|
|
267
241
|
* `description` – Short description of the imagery
|
|
268
242
|
|
|
269
|
-
Example templates:
|
|
270
|
-
|
|
271
|
-
```text
|
|
272
|
-
https://server/{z}/{x}/{y}.png
|
|
273
|
-
https://tiles.example.com/{z}/{x}/{y}.jpg
|
|
274
|
-
https://api.provider.com/tiles/{z}/{x}/{y}?key={token}
|
|
275
|
-
```
|
|
276
243
|
|
|
277
244
|
---
|
|
278
245
|
|
|
@@ -284,8 +251,6 @@ If your provider requires an API key, pass it during instantiation:
|
|
|
284
251
|
source = MyCustomSource(api_key="YOUR_KEY")
|
|
285
252
|
```
|
|
286
253
|
|
|
287
|
-
`TileSource` already handles key injection — you don’t need to reinvent it.
|
|
288
|
-
|
|
289
254
|
---
|
|
290
255
|
|
|
291
256
|
|
|
@@ -299,10 +264,9 @@ from tilegrab.tiles import TilesByShape
|
|
|
299
264
|
from tilegrab.dataset import GeoDataset
|
|
300
265
|
|
|
301
266
|
dataset = GeoDataset("area.gpkg")
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
downloader = Downloader(tiles, source, "output")
|
|
267
|
+
tile_collection = TilesByShape(dataset, zoom=16)
|
|
268
|
+
tile_source = MyCustomSource(api_key="XYZ")
|
|
269
|
+
downloader = Downloader(tile_collection, tile_source, "output")
|
|
306
270
|
downloader.run()
|
|
307
271
|
```
|
|
308
272
|
|
|
@@ -321,7 +285,7 @@ If you need full flexibility, use the Python API.
|
|
|
321
285
|
|
|
322
286
|
---
|
|
323
287
|
|
|
324
|
-
## Project Structure
|
|
288
|
+
<!-- ## Project Structure
|
|
325
289
|
|
|
326
290
|
```text
|
|
327
291
|
tilegrab/
|
|
@@ -331,11 +295,12 @@ tilegrab/
|
|
|
331
295
|
├── downloader.py # Tile download engine
|
|
332
296
|
├── mosaic.py # Tile mosaicking
|
|
333
297
|
└── sources.py # Tile providers
|
|
334
|
-
```
|
|
298
|
+
```
|
|
335
299
|
|
|
336
300
|
---
|
|
301
|
+
-->
|
|
337
302
|
|
|
338
|
-
## Who This Is For
|
|
303
|
+
<!-- ## Who This Is For
|
|
339
304
|
|
|
340
305
|
* GIS analysts automating basemap generation
|
|
341
306
|
* Remote sensing workflows needing tiled imagery
|
|
@@ -345,7 +310,7 @@ tilegrab/
|
|
|
345
310
|
If you want a GUI, this isn’t it.
|
|
346
311
|
If you want control, repeatability, and speed — it is.
|
|
347
312
|
|
|
348
|
-
---
|
|
313
|
+
--- -->
|
|
349
314
|
|
|
350
315
|
## Roadmap
|
|
351
316
|
|
|
@@ -353,9 +318,9 @@ Planned (not promises):
|
|
|
353
318
|
|
|
354
319
|
* Additional tile providers
|
|
355
320
|
* Parallel download tuning
|
|
356
|
-
* Cloud-optimized raster output
|
|
357
321
|
* Raster reprojection and resampling options
|
|
358
322
|
* Expanded Python API documentation
|
|
323
|
+
* Test implementation
|
|
359
324
|
|
|
360
325
|
---
|
|
361
326
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
tilegrab/__init__.py,sha256=Ti-EFZrLJA9WtEN3ZwvstVIBAsceEuZgjV7gw9JOoyk,295
|
|
2
|
+
tilegrab/__main__.py,sha256=wT62aXCuN7iwRPQoswnmeGiBS5Cou0CIBvMxbOTfvFs,33
|
|
3
|
+
tilegrab/cli.py,sha256=XaSC-qhANE7Y_73k18Izjpi_pGiZu7Qey1s4zP_JQAU,8099
|
|
4
|
+
tilegrab/dataset.py,sha256=kzLMBbLLjdnlKN7Ac-qB7k7e63VCDs3T23CweRdvKLQ,2709
|
|
5
|
+
tilegrab/downloader.py,sha256=ApQLgDjBo-RYrUuaavbulGbHaZ__G9xVQvkPO35RoP4,5371
|
|
6
|
+
tilegrab/images.py,sha256=RAv_gsW5HYOk7cAcu00tHILEFBpF4tKmQ-xDXPtaNmw,11991
|
|
7
|
+
tilegrab/logs.py,sha256=serS8_0bhxoTO0F03t-BDSdkMYR_eX78vI2y1sSVpUk,2151
|
|
8
|
+
tilegrab/sources.py,sha256=rQshFLjzbJu4x0AXTLa3YXhfK07719bMoZrSoOiAWG8,2344
|
|
9
|
+
tilegrab/tiles.py,sha256=XeFxO1i4kXokyK0mg_6f7FeHOkPXOMk8n2m-4C6H87Y,9044
|
|
10
|
+
tilegrab-1.2.0b2.dist-info/licenses/LICENSE,sha256=bcZProekTTcPtnEpRKB2i1kUJ6ujlaxBZchNWKeoXc8,1094
|
|
11
|
+
tilegrab-1.2.0b2.dist-info/METADATA,sha256=363_bNbE-ONRebVSiEX1TGwsmT0KQ4uRowALZhKsVs8,9684
|
|
12
|
+
tilegrab-1.2.0b2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
13
|
+
tilegrab-1.2.0b2.dist-info/entry_points.txt,sha256=z-WoKN8NnA5_ZsWXucSeq-r4TeEscu0xjWYu560uNTk,47
|
|
14
|
+
tilegrab-1.2.0b2.dist-info/top_level.txt,sha256=lVio8bCk3r4Bu_INLgaj4PZCrhFY2UcxHjnFBdHwyPo,9
|
|
15
|
+
tilegrab-1.2.0b2.dist-info/RECORD,,
|