tilegrab 1.1.0__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/mosaic.py ADDED
@@ -0,0 +1,75 @@
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/sources.py ADDED
@@ -0,0 +1,62 @@
1
+ import logging
2
+ from typing import Dict, Optional
3
+
4
+ logger = logging.getLogger(__name__)
5
+
6
+ class TileSource:
7
+ URL_TEMPLATE = ""
8
+ name = None
9
+
10
+ def __init__(
11
+ self,
12
+ api_key: Optional[str] = None,
13
+ headers: Optional[Dict[str, str]] = None
14
+ ) -> None:
15
+ self._headers = headers
16
+ self.api_key = api_key
17
+ logger.debug(f"Initializing TileSource: {self.name}, has_api_key={api_key is not None}")
18
+
19
+ def get_url(self, z: int, x: int, y: int) -> str:
20
+ url = self.URL_TEMPLATE.format(x=x, y=y, z=z)
21
+ logger.debug(f"Generated URL for {self.name}: z={z}, x={x}, y={y}")
22
+ return url
23
+
24
+ def headers(self) -> Dict[str, str]:
25
+ return self._headers or {
26
+ "referer": "",
27
+ "accept": "*/*",
28
+ "user-agent": "Mozilla/5.0 QGIS/34202/Windows 11 Version 2009",
29
+ "connection": "Keep-Alive ",
30
+ "accept-encoding": "gzip, deflate",
31
+ "accept-language": "en-US,*",
32
+ }
33
+
34
+
35
+ class GoogleSat(TileSource):
36
+ name = "GoogleSat"
37
+ description = "Google satellite imageries"
38
+ URL_TEMPLATE = "https://mt1.google.com/vt/lyrs=s&x={x}&y={y}&z={z}"
39
+
40
+
41
+ class OSM(TileSource):
42
+ name = "OSM"
43
+ description = "OpenStreetMap imageries"
44
+ URL_TEMPLATE = "https://tile.openstreetmap.org/{z}/{x}/{y}.png"
45
+
46
+ class ESRIWorldImagery(TileSource):
47
+ name = "ESRIWorldImagery"
48
+ description = "ESRI satellite imageries"
49
+ URL_TEMPLATE = "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
50
+
51
+ class Nearmap(TileSource):
52
+ name = "NearmapSat"
53
+ description = "Nearmap satellite imageries"
54
+ URL_TEMPLATE = "https://api.nearmap.com/tiles/v3/Vert/{z}/{x}/{y}.png?apikey={token}"
55
+
56
+ def get_url(self, z: int, x: int, y: int) -> str:
57
+ if not self.api_key:
58
+ logger.error("Nearmap API key is required but not provided")
59
+ raise AssertionError("API key required for Nearmap")
60
+ url = self.URL_TEMPLATE.format(x=x, y=y, z=z, token=self.api_key)
61
+ logger.debug(f"Generated Nearmap URL: z={z}, x={x}, y={y}")
62
+ return url
tilegrab/tiles.py ADDED
@@ -0,0 +1,179 @@
1
+ import logging
2
+ import math
3
+ from typing import Any, List, Tuple
4
+ import mercantile
5
+ from abc import ABC, abstractmethod
6
+ from dataclasses import dataclass
7
+ from typing import Union
8
+ from .dataset import GeoDataset
9
+ from box import Box
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ @dataclass
14
+ class Tile:
15
+ x: int = 0
16
+ y: int = 0
17
+ z: int = 0
18
+
19
+ def __post_init__(self):
20
+ self._position = None
21
+ logger.debug(f"Tile created: z={self.z}, x={self.x}, y={self.y}")
22
+
23
+ # @classmethod
24
+ # def from_tuple(cls, t: tuple[int, int, int]) -> "Tile":
25
+ # logger.debug(f"Creating Tile from tuple: {t}")
26
+ # return cls(*t)
27
+
28
+ @property
29
+ def url(self) -> Union[str, None]:
30
+ return self._url
31
+
32
+ @url.setter
33
+ def url(self, value: str):
34
+ logger.debug(f"Tile URL set for z={self.z},x={self.x},y={self.y}")
35
+ self._url = value
36
+
37
+ @property
38
+ def position(self) -> Box:
39
+ if self._position is None:
40
+ logger.error(f"Tile position not set: z={self.z}, x={self.x}, y={self.y}")
41
+ raise RuntimeError("Image does not have an position")
42
+ return self._position
43
+
44
+ @position.setter
45
+ def position(self, value: Tuple[float, float]):
46
+ x, y = self.x - value[0], self.y - value[1]
47
+ self._position = Box({'x': x, 'y': y})
48
+ logger.debug(f"Tile position calculated: x={x}, y={y}")
49
+
50
+ class TileCollection(ABC):
51
+
52
+ MIN_X: float = 0
53
+ MAX_X: float = 0
54
+ MIN_Y: float = 0
55
+ MAX_Y: float = 0
56
+ _cache: Union[List[Tile], None] = None
57
+
58
+ @abstractmethod
59
+ def _build_tile_cache(self) -> list[Tile]:
60
+ raise NotImplementedError
61
+
62
+ def __len__(self):
63
+ return sum(1 for _ in self)
64
+
65
+ def __iter__(self):
66
+ for t in self._cache: # type: ignore
67
+ yield t.z, t.x, t.y
68
+
69
+ def __init__(self, feature: GeoDataset, zoom: int, SAFE_LIMIT: int = 250):
70
+ self.zoom = zoom
71
+ self.SAFE_LIMIT = SAFE_LIMIT
72
+ self.feature = feature
73
+
74
+ logger.info(f"Initializing TileCollection: zoom={zoom}, safe_limit={SAFE_LIMIT}")
75
+
76
+ assert feature.bbox.minx < feature.bbox.maxx
77
+ assert feature.bbox.miny < feature.bbox.maxy
78
+
79
+ self._build_tile_cache()
80
+ self._update_min_max()
81
+
82
+ if len(self) > SAFE_LIMIT:
83
+ logger.error(f"Tile count exceeds safe limit: {len(self)} > {SAFE_LIMIT}")
84
+ raise ValueError(
85
+ f"Your query excedes the hard limit {len(self)} > {SAFE_LIMIT}"
86
+ )
87
+
88
+ logger.info(f"TileCollection initialized with {len(self)} tiles")
89
+
90
+ def __repr__(self) -> str:
91
+ 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
+
93
+ def tile_bounds(self, x, y, z) -> Tuple[float, float, float, float]:
94
+ n = 2**z
95
+
96
+ lon_min = x / n * 360.0 - 180.0
97
+ lon_max = (x + 1) / n * 360.0 - 180.0
98
+
99
+ lat_min = math.degrees(math.atan(math.sinh(math.pi * (1 - 2 * (y + 1) / n))))
100
+ lat_max = math.degrees(math.atan(math.sinh(math.pi * (1 - 2 * y / n))))
101
+
102
+ logger.debug(f"Tile bounds calculated for z={z},x={x},y={y}: ({lon_min},{lat_min},{lon_max},{lat_max})")
103
+ return lon_min, lat_min, lon_max, lat_max
104
+
105
+ @property
106
+ def to_list(self) -> list[Tile]:
107
+ if self._cache is None:
108
+ logger.debug("Building tile cache from to_list property")
109
+ self._build_tile_cache()
110
+ self._update_min_max()
111
+ if len(self) > self.SAFE_LIMIT:
112
+ logger.error(f"Tile count exceeds safe limit in to_list: {len(self)} > {self.SAFE_LIMIT}")
113
+ raise ValueError("Too many tiles")
114
+
115
+ assert self._cache
116
+ return self._cache
117
+
118
+ def _update_min_max(self):
119
+ assert self._cache
120
+ x = [t.x for t in self._cache]
121
+ y = [t.y for t in self._cache]
122
+ self.MAX_X, self.MIN_X = max(x), min(x)
123
+ self.MAX_Y, self.MIN_Y = max(y), min(y)
124
+
125
+ logger.info(f"TileCollection bounds: x=({self.MIN_X}, {self.MAX_X}) y=({self.MIN_Y}, {self.MAX_Y})")
126
+
127
+ for i in range(len(self._cache)):
128
+ self._cache[i].position = self.MIN_X, self.MIN_Y
129
+
130
+ class TilesByBBox(TileCollection):
131
+
132
+ def _build_tile_cache(self) -> list[Tile]:
133
+ logger.info(f"Building tiles by bounding box at zoom level {self.zoom}")
134
+ bbox = self.feature.bbox
135
+ logger.debug(f"BBox coordinates: minx={bbox.minx}, miny={bbox.miny}, maxx={bbox.maxx}, maxy={bbox.maxy}")
136
+
137
+ self._cache = [
138
+ Tile(t.x, t.y, t.z)
139
+ for t in mercantile.tiles(
140
+ bbox.minx,
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")
149
+ return self._cache
150
+
151
+ class TilesByShape(TileCollection):
152
+
153
+ def _build_tile_cache(self) -> list[Tile]:
154
+ from shapely.geometry import box
155
+
156
+ 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
+
177
+ self._cache = tiles
178
+ logger.info(f"Generated {len(tiles)} tiles from shape intersection")
179
+ return tiles
@@ -0,0 +1,375 @@
1
+ Metadata-Version: 2.4
2
+ Name: tilegrab
3
+ Version: 1.1.0
4
+ Summary: Fast geospatial map tile downloader and mosaicker
5
+ Author: Thiwanka Munasinghe
6
+ License-Expression: MIT
7
+ Project-URL: Changelog, https://github.com/thiwaK/tilegrab
8
+ Project-URL: Contact, https://github.com/thiwaK
9
+ Project-URL: Homepage, https://github.com/thiwaK/tilegrab
10
+ Project-URL: Source, https://github.com/thiwaK/tilegrab
11
+ Project-URL: Tracker, https://github.com/thiwaK/tilegrab/issues
12
+ Keywords: tilegrab,Tilegrab,TileGrab,tileGrab
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: requests
17
+ Requires-Dist: mercantile
18
+ Requires-Dist: pillow
19
+ Requires-Dist: tqdm
20
+ Requires-Dist: python-box
21
+ Requires-Dist: geopandas
22
+ Requires-Dist: pyproj
23
+ Requires-Dist: shapely
24
+ Provides-Extra: dev
25
+ Requires-Dist: pytest; extra == "dev"
26
+ Requires-Dist: pytest-mock; extra == "dev"
27
+ Requires-Dist: setuptools; extra == "dev"
28
+ Requires-Dist: mock; extra == "dev"
29
+ Requires-Dist: tox; extra == "dev"
30
+ Dynamic: license-file
31
+
32
+
33
+ <div align="center">
34
+ <h1 align="center">TileGrab 🧩</h1>
35
+ <!-- until publish on PyPi -->
36
+ <img alt="TileGrab" src="https://img.shields.io/badge/testpypi-1.0.0-blue">
37
+ <!-- <img alt="TileGrab" src="https://img.shields.io/pypi/v/tilegrab.svg"> -->
38
+ <img alt="TileGrab - Python Versions" src="https://img.shields.io/badge/python-3.9%20|%203.10%20|%203.11|%203.12|%203.13-blue">
39
+ <!-- <img alt="TileGrab - Python Versions" src="https://img.shields.io/pypi/pyversions/tilegrab.svg"> -->
40
+ <!-- -->
41
+ <img alt="Test Status" src="https://img.shields.io/github/actions/workflow/status/thiwaK/tilegrab/test.yml?branch=main&event=push&style=flat&label=test">
42
+ <br/>
43
+ <br/>
44
+ </div>
45
+
46
+
47
+ **Fast, scriptable map tile downloader and mosaicker for geospatial workflows.**
48
+
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 raster. Built for automation, reproducibility, and real GIS work — not GUI clicking.
51
+
52
+ ---
53
+
54
+ <!-- <p align="center">
55
+ <picture align="center">
56
+ <source media="(prefers-color-scheme: dark)" srcset="https://github.com/astral-sh/uv/assets/1309177/03aa9163-1c79-4a87-a31d-7a9311ed9310">
57
+ <source media="(prefers-color-scheme: light)" srcset="https://github.com/astral-sh/uv/assets/1309177/629e59c0-9c6e-4013-9ad4-adb2bcf5080d">
58
+ <img alt="Shows a bar chart with benchmark results." src="https://github.com/astral-sh/uv/assets/1309177/629e59c0-9c6e-4013-9ad4-adb2bcf5080d">
59
+ </picture>
60
+ </p> -->
61
+
62
+
63
+ ## Why tilegrab?
64
+
65
+ Most tile downloaders fall into one of two traps:
66
+ - GUI tools that don’t scale or automate
67
+ - Scripts that only support bounding boxes and break on real geometries
68
+
69
+ `tilegrab` is different:
70
+
71
+ - Uses **actual vector geometries**, not just extents
72
+ - Clean CLI, easy to script and integrate
73
+ - Works with **Shapefiles, GeoPackages, GeoJSON**
74
+ - Supports **download-only**, **mosaic-only**, or full pipelines
75
+ - Designed for **GIS, remote sensing, and map production workflows**
76
+
77
+ No magic. No black boxes.
78
+
79
+ ---
80
+
81
+ ## Features
82
+
83
+ - Vector-driven tile selection
84
+ - Exact geometry-based tile filtering
85
+ - Or fast bounding-box-based selection
86
+ - Multiple tile providers
87
+ - OpenStreetMap
88
+ - Google Satellite
89
+ - ESRI World Imagery
90
+ - Automatic tile mosaicking
91
+ - Progress reporting (optional)
92
+ - API-key support where required
93
+ - Sensible defaults, strict CLI validation
94
+
95
+ ---
96
+
97
+ ## Installation
98
+
99
+ ### From TestPyPI
100
+
101
+ ```bash
102
+ pip install -i https://test.pypi.org/simple/tilegrab
103
+ ````
104
+
105
+ > [!NOTE]
106
+ > A stable PyPI release will follow once the API is finalized.
107
+
108
+ ---
109
+
110
+ ## Quick Start
111
+
112
+ ### Download and mosaic tiles using a polygon
113
+
114
+ ```bash
115
+ tilegrab \
116
+ --source boundary.shp \
117
+ --shape \
118
+ --osm \
119
+ --zoom 16
120
+ ```
121
+
122
+ ### Use bounding box instead of exact geometry
123
+
124
+ ```bash
125
+ tilegrab \
126
+ --source boundary.geojson \
127
+ --bbox \
128
+ --esri_sat \
129
+ --zoom 17
130
+ ```
131
+
132
+ ---
133
+
134
+ ## CLI Usage
135
+
136
+ ```bash
137
+ usage: tilegrab [-h] --source SOURCE (--shape | --bbox) (--osm | --google_sat | --esri_sat | --key KEY) --zoom ZOOM [--out OUT] [--download-only] [--mosaic-only] [--no-progress] [--quiet]
138
+
139
+ Download and mosaic map tiles
140
+
141
+ options:
142
+ -h, --help show this help message and exit
143
+ --zoom ZOOM Zoom level (integer)
144
+ --out OUT Output directory (default: ./saved_tiles)
145
+ --download-only Only download tiles; do not run mosaicking or postprocessing
146
+ --mosaic-only Only mosaic tiles; do not download
147
+ --no-progress Hide download progress bar
148
+ --quiet Hide all prints
149
+
150
+ Source options(Extent):
151
+ Options for the vector polygon source
152
+
153
+ --source SOURCE The vector polygon source for filter tiles
154
+ --shape Use actual shape to derive tiles
155
+ --bbox Use shape's bounding box to derive tiles
156
+
157
+ Source options(Map tiles):
158
+ Options for the map tile source
159
+
160
+ --osm OpenStreetMap
161
+ --google_sat Google Satellite
162
+ --esri_sat ESRI World Imagery
163
+ --key KEY API key where required by source
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 |
176
+
177
+ ---
178
+
179
+ ## Tile Sources (CLI)
180
+
181
+ | Flag | Source |
182
+ | -------------- | ------------------ |
183
+ | `--osm` | OpenStreetMap |
184
+ | `--google_sat` | Google Satellite |
185
+ | `--esri_sat` | ESRI World Imagery |
186
+
187
+ Optional API key:
188
+
189
+ ```bash
190
+ --key YOUR_API_KEY
191
+ ```
192
+
193
+ ---
194
+
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
+
208
+ ## Supported Vector Formats
209
+
210
+ Any format readable by **GeoPandas**, including:
211
+
212
+ * Shapefile (`.shp`)
213
+ * GeoPackage (`.gpkg`)
214
+ * GeoJSON (`.geojson`)
215
+ * Spatial databases (via supported drivers)
216
+
217
+ ---
218
+
219
+ ## Custom Tile Sources (Bring Your Own Provider)
220
+
221
+ `tilegrab` is **not limited** to built-in providers.
222
+
223
+ If a tile service follows the standard `{z}/{x}/{y}` pattern, you can add it in **one small class** by extending `TileSource`.
224
+
225
+ No registration. No plugin system. No magic.
226
+
227
+ ---
228
+
229
+ ### Example
230
+
231
+ ```python
232
+ from tilegrab.sources import TileSource
233
+
234
+ class MyCustomSource(TileSource):
235
+ name = "MyCustomSource name"
236
+ description = "MyCustomSource description"
237
+ URL_TEMPLATE = "https://MyCustomSource/{z}/{x}/{y}.png"
238
+ ```
239
+
240
+ That’s it.
241
+
242
+ Once instantiated, the source works exactly like built-in providers.
243
+
244
+ ---
245
+
246
+
247
+ ### get_url Function
248
+
249
+ You can change how the url is generate by override `get_url` function, inside your Custom Tile Sources. If you are planning to use API key, you must override this function.
250
+
251
+ ```python
252
+ def get_url(self, z: int, x: int, y: int) -> str:
253
+ assert self.api_key
254
+ return self.URL_TEMPLATE.format(x=x, y=y, z=z, token=self.api_key)
255
+ ```
256
+
257
+ ### URL Template Rules
258
+
259
+ Your tile source **must** define:
260
+
261
+ * `URL_TEMPLATE`
262
+ Must contain `{z}`, `{x}`, `{y}` placeholders.
263
+
264
+ Optional but recommended:
265
+
266
+ * `name` – Human-readable name
267
+ * `description` – Short description of the imagery
268
+
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
+
277
+ ---
278
+
279
+ ### API Keys
280
+
281
+ If your provider requires an API key, pass it during instantiation:
282
+
283
+ ```python
284
+ source = MyCustomSource(api_key="YOUR_KEY")
285
+ ```
286
+
287
+ `TileSource` already handles key injection — you don’t need to reinvent it.
288
+
289
+ ---
290
+
291
+
292
+ ### Using a Custom Source in Code
293
+
294
+ Custom sources are intended for **programmatic use** (not CLI flags):
295
+
296
+ ```python
297
+ from tilegrab.downloader import Downloader
298
+ from tilegrab.tiles import TilesByShape
299
+ from tilegrab.dataset import GeoDataset
300
+
301
+ dataset = GeoDataset("area.gpkg")
302
+ tiles = TilesByShape(dataset, zoom=16)
303
+
304
+ source = MyCustomSource(api_key="XYZ")
305
+ downloader = Downloader(tiles, source, "output")
306
+ downloader.run()
307
+ ```
308
+
309
+ This keeps the CLI clean while giving developers full control.
310
+
311
+ ---
312
+
313
+ ### Why This Design?
314
+
315
+ * Zero configuration overhead
316
+ * No registry or plugin boilerplate
317
+ * Easy to vendor in private or internal tile servers
318
+ * Safe default for public CLI usage
319
+
320
+ If you need full flexibility, use the Python API.
321
+
322
+ ---
323
+
324
+ ## Project Structure
325
+
326
+ ```text
327
+ tilegrab/
328
+ ├── cli.py # CLI entry point
329
+ ├── dataset.py # Vector dataset handling
330
+ ├── tiles.py # Tile calculation logic
331
+ ├── downloader.py # Tile download engine
332
+ ├── mosaic.py # Tile mosaicking
333
+ └── sources.py # Tile providers
334
+ ```
335
+
336
+ ---
337
+
338
+ ## Who This Is For
339
+
340
+ * GIS analysts automating basemap generation
341
+ * Remote sensing workflows needing tiled imagery
342
+ * Developers building spatial data pipelines
343
+ * Anyone tired of manual tile grabbing
344
+
345
+ If you want a GUI, this isn’t it.
346
+ If you want control, repeatability, and speed — it is.
347
+
348
+ ---
349
+
350
+ ## Roadmap
351
+
352
+ Planned (not promises):
353
+
354
+ * Additional tile providers
355
+ * Parallel download tuning
356
+ * Cloud-optimized raster output
357
+ * Raster reprojection and resampling options
358
+ * Expanded Python API documentation
359
+
360
+ ---
361
+
362
+ ## License
363
+
364
+ MIT License.
365
+ Do whatever you want — just don’t pretend you wrote it.
366
+
367
+ ---
368
+
369
+ ## Author
370
+
371
+ **Thiwanka Munasinghe**
372
+ GitHub: [https://github.com/thiwaK](https://github.com/thiwaK)
373
+
374
+ ```
375
+ ```
@@ -0,0 +1,15 @@
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,,