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/tiles.py CHANGED
@@ -1,15 +1,18 @@
1
1
  import logging
2
2
  import math
3
- from typing import Any, List, Tuple
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: z={self.z}, x={self.x}, y={self.y}")
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 an position")
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({'x': x, 'y': y})
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: Union[List[Tile], None] = None
60
+ _cache: List[Tile]
61
+ _tile_count: int = 0
57
62
 
58
63
  @abstractmethod
59
- def _build_tile_cache(self) -> list[Tile]:
64
+ def _build_tile_cache(self) -> List[Tile]:
60
65
  raise NotImplementedError
61
66
 
62
67
  def __len__(self):
63
- return sum(1 for _ in self)
68
+ return self._tile_count
64
69
 
65
70
  def __iter__(self):
66
- for t in self._cache: # type: ignore
67
- yield t.z, t.x, t.y
71
+ for t in self._cache:
72
+ yield t
68
73
 
69
- def __init__(self, feature: GeoDataset, zoom: int, SAFE_LIMIT: int = 250):
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.feature = feature
81
+ self.geo_dataset = geo_dataset
73
82
 
74
- logger.info(f"Initializing TileCollection: zoom={zoom}, safe_limit={SAFE_LIMIT}")
83
+ logger.info(
84
+ f"Initializing TileCollection: zoom={zoom}, safe_limit={SAFE_LIMIT}"
85
+ )
75
86
 
76
- assert feature.bbox.minx < feature.bbox.maxx
77
- assert feature.bbox.miny < feature.bbox.maxy
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
- self._update_min_max()
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.feature.bbox.minx}-{self.feature.bbox.maxx}); y-extent=({self.feature.bbox.miny}-{self.feature.bbox.maxy})"
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, x, y, z) -> Tuple[float, float, float, float]:
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(f"Tile bounds calculated for z={z},x={x},y={y}: ({lon_min},{lat_min},{lon_max},{lat_max})")
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
- def to_list(self) -> list[Tile]:
107
- if self._cache is None:
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(f"Tile count exceeds safe limit in to_list: {len(self)} > {self.SAFE_LIMIT}")
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 _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)
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
- logger.info(f"TileCollection bounds: x=({self.MIN_X}, {self.MAX_X}) y=({self.MIN_Y}, {self.MAX_Y})")
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) -> list[Tile]:
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.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")
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) -> list[Tile]:
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
- self._cache = tiles
178
- logger.info(f"Generated {len(tiles)} tiles from shape intersection")
179
- return tiles
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.1.0
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: mercantile
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
- <!-- 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">
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 raster. Built for automation, reproducibility, and real GIS work — not GUI clicking.
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 fall into one of two traps:
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**, or full pipelines
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
- - Automatic tile mosaicking
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 https://test.pypi.org/simple/tilegrab
103
+ pip install -i tilegrab
103
104
  ````
104
105
 
105
- > [!NOTE]
106
- > A stable PyPI release will follow once the API is finalized.
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 OUT] [--download-only] [--mosaic-only] [--no-progress] [--quiet]
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 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
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 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
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 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 |
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
- | Flag | Source |
182
- | -------------- | ------------------ |
183
- | `--osm` | OpenStreetMap |
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
- URL_TEMPLATE = "https://MyCustomSource/{z}/{x}/{y}.png"
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.URL_TEMPLATE.format(x=x, y=y, z=z, token=self.api_key)
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
- tiles = TilesByShape(dataset, zoom=16)
303
-
304
- source = MyCustomSource(api_key="XYZ")
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,,