geodot 0.1.0__tar.gz
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.
- geodot-0.1.0/.github/workflows/publish.yml +46 -0
- geodot-0.1.0/.github/workflows/test.yml +44 -0
- geodot-0.1.0/.gitignore +26 -0
- geodot-0.1.0/PKG-INFO +255 -0
- geodot-0.1.0/README.md +239 -0
- geodot-0.1.0/eslint.config.js +20 -0
- geodot-0.1.0/js/bin/geodot.js +114 -0
- geodot-0.1.0/js/src/index.js +260 -0
- geodot-0.1.0/js/tests/e2e.test.js +105 -0
- geodot-0.1.0/js/tests/geodot.test.js +92 -0
- geodot-0.1.0/package-lock.json +1123 -0
- geodot-0.1.0/package.json +33 -0
- geodot-0.1.0/pyproject.toml +37 -0
- geodot-0.1.0/python/geodot/__init__.py +35 -0
- geodot-0.1.0/python/geodot/cli.py +58 -0
- geodot-0.1.0/python/geodot/core.py +234 -0
- geodot-0.1.0/python/tests/test_e2e.py +88 -0
- geodot-0.1.0/python/tests/test_geodot.py +65 -0
- geodot-0.1.0/rust/Cargo.lock +1778 -0
- geodot-0.1.0/rust/Cargo.toml +31 -0
- geodot-0.1.0/rust/src/lib.rs +492 -0
- geodot-0.1.0/rust/src/main.rs +136 -0
- geodot-0.1.0/rust/tests/e2e.rs +118 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
npm:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
id-token: write
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v5
|
|
16
|
+
- uses: actions/setup-node@v5
|
|
17
|
+
with:
|
|
18
|
+
node-version: '24'
|
|
19
|
+
registry-url: https://registry.npmjs.org
|
|
20
|
+
- run: npm publish --provenance --access public
|
|
21
|
+
env:
|
|
22
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
23
|
+
|
|
24
|
+
crates:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
defaults:
|
|
27
|
+
run:
|
|
28
|
+
working-directory: rust
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v5
|
|
31
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
32
|
+
- run: cargo publish --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
|
33
|
+
|
|
34
|
+
pypi:
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
permissions:
|
|
37
|
+
contents: read
|
|
38
|
+
id-token: write
|
|
39
|
+
steps:
|
|
40
|
+
- uses: actions/checkout@v5
|
|
41
|
+
- uses: actions/setup-python@v6
|
|
42
|
+
with:
|
|
43
|
+
python-version: '3.14'
|
|
44
|
+
- run: python -m pip install build
|
|
45
|
+
- run: python -m build
|
|
46
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
python:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v5
|
|
12
|
+
- uses: actions/setup-python@v6
|
|
13
|
+
with:
|
|
14
|
+
python-version: '3.14'
|
|
15
|
+
- run: python -m pip install -e '.[test,dev]'
|
|
16
|
+
- run: ruff format --check python
|
|
17
|
+
- run: ruff check python
|
|
18
|
+
- run: pytest
|
|
19
|
+
|
|
20
|
+
javascript:
|
|
21
|
+
runs-on: ubuntu-latest
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v5
|
|
24
|
+
- uses: actions/setup-node@v5
|
|
25
|
+
with:
|
|
26
|
+
node-version: '24'
|
|
27
|
+
- run: npm ci
|
|
28
|
+
- run: npm run format:check
|
|
29
|
+
- run: npm run lint
|
|
30
|
+
- run: npm test
|
|
31
|
+
|
|
32
|
+
rust:
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
defaults:
|
|
35
|
+
run:
|
|
36
|
+
working-directory: rust
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v5
|
|
39
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
40
|
+
with:
|
|
41
|
+
components: rustfmt, clippy
|
|
42
|
+
- run: cargo fmt --check
|
|
43
|
+
- run: cargo clippy --all-targets -- -D warnings
|
|
44
|
+
- run: cargo test
|
geodot-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
.DS_Store
|
|
2
|
+
|
|
3
|
+
# Build outputs
|
|
4
|
+
target/
|
|
5
|
+
dist/
|
|
6
|
+
build/
|
|
7
|
+
coverage/
|
|
8
|
+
*.egg-info/
|
|
9
|
+
|
|
10
|
+
# Python
|
|
11
|
+
__pycache__/
|
|
12
|
+
*.py[cod]
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
.venv/
|
|
15
|
+
venv/
|
|
16
|
+
|
|
17
|
+
# JavaScript
|
|
18
|
+
node_modules/
|
|
19
|
+
npm-debug.log*
|
|
20
|
+
|
|
21
|
+
# Rust
|
|
22
|
+
.ruff_cache
|
|
23
|
+
|
|
24
|
+
# geodot output
|
|
25
|
+
data/
|
|
26
|
+
tiles/
|
geodot-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: geodot
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Download satellite map tiles from the command line or as a Python library
|
|
5
|
+
Author: geodot contributors
|
|
6
|
+
License-Expression: MIT OR Apache-2.0
|
|
7
|
+
Keywords: gis,maps,satellite,tiles
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Requires-Python: >=3.9
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: ruff>=0.14; extra == 'dev'
|
|
13
|
+
Provides-Extra: test
|
|
14
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# geodot
|
|
18
|
+
|
|
19
|
+
`geodot` downloads satellite map tiles and can be used as a CLI or as a library from Python, JavaScript, and Rust.
|
|
20
|
+
|
|
21
|
+
Tiles are saved as:
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
{out}/tiles/{z}/{x}/{y}.jpg
|
|
25
|
+
{out}/manifest.json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
cargo install geodot
|
|
32
|
+
npm install geodot
|
|
33
|
+
pip install geodot
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Run without installing globally:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx -y geodot -x 37.6504907 -y 55.7303 -z 18 -c 1 -r 1
|
|
40
|
+
uvx geodot -x 37.6504907 -y 55.7303 -z 18 -c 1 -r 1
|
|
41
|
+
cargo install geodot && geodot -x 37.6504907 -y 55.7303 -z 18 -c 1 -r 1
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
During local development:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
python -m pip install -e '.[test]'
|
|
48
|
+
npm test
|
|
49
|
+
cargo test --manifest-path rust/Cargo.toml
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Lint and format checks:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
python -m pip install -e '.[test,dev]'
|
|
56
|
+
ruff format --check python
|
|
57
|
+
ruff check python
|
|
58
|
+
|
|
59
|
+
npm install
|
|
60
|
+
npm run format:check
|
|
61
|
+
npm run lint
|
|
62
|
+
|
|
63
|
+
cargo fmt --manifest-path rust/Cargo.toml -- --check
|
|
64
|
+
cargo clippy --manifest-path rust/Cargo.toml --all-targets -- -D warnings
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## CLI
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
geodot -x 37.6504907 -y 55.7303 -z 18 -c 3 -r 3 -o data -j 16
|
|
71
|
+
|
|
72
|
+
geodot -x 37.6504907 -y 55.7303 --x2 37.652 --y2 55.7297 -z 18 -o data
|
|
73
|
+
|
|
74
|
+
geodot -p "37.6504,55.7304;37.6520,55.7304;37.6520,55.7297;37.6504,55.7297" -z 18 -o data
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
| Flag | Default | Description |
|
|
78
|
+
|------|---------|-------------|
|
|
79
|
+
| `-x`, `--lon` | `37.6504907` | Top-left longitude |
|
|
80
|
+
| `-y`, `--lat` | `55.7303` | Top-left latitude |
|
|
81
|
+
| `--x2`, `--bottom-right-lon` | none | Bottom-right longitude |
|
|
82
|
+
| `--y2`, `--bottom-right-lat` | none | Bottom-right latitude |
|
|
83
|
+
| `-p`, `--polygon` | none | Closed area as `lon,lat;lon,lat;lon,lat` |
|
|
84
|
+
| `-z`, `--zoom` | `18` | Zoom level |
|
|
85
|
+
| `-c`, `--cols` | `3` | Tile columns to the right of the top-left tile |
|
|
86
|
+
| `-r`, `--rows` | `3` | Tile rows downward from the top-left tile |
|
|
87
|
+
| `-o`, `--out` | `data` | Output directory |
|
|
88
|
+
| `-j`, `--jobs` | `16` | Concurrent downloads |
|
|
89
|
+
|
|
90
|
+
## Output
|
|
91
|
+
|
|
92
|
+
For `-o data`, a 3 by 3 download at zoom 18 writes files like this:
|
|
93
|
+
|
|
94
|
+
```text
|
|
95
|
+
data/
|
|
96
|
+
├── manifest.json
|
|
97
|
+
└── tiles/
|
|
98
|
+
└── 18/
|
|
99
|
+
└── 158488/
|
|
100
|
+
└── 81979.jpg
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
JPEG bytes are written directly from the tile server without re-compression.
|
|
104
|
+
|
|
105
|
+
Tile images are always 256x256 pixels. `geodot` does not currently support other tile sizes.
|
|
106
|
+
|
|
107
|
+
`manifest.json` contains:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"center": { "x": 158488, "y": 81979, "z": 18 },
|
|
112
|
+
"tiles": [
|
|
113
|
+
{
|
|
114
|
+
"tile": { "x": 158488, "y": 81979, "z": 18 },
|
|
115
|
+
"bounds": {
|
|
116
|
+
"lat_min": 55.730012,
|
|
117
|
+
"lon_min": 37.650146,
|
|
118
|
+
"lat_max": 55.730793,
|
|
119
|
+
"lon_max": 37.651520
|
|
120
|
+
},
|
|
121
|
+
"path": "data/tiles/18/158488/81979.jpg",
|
|
122
|
+
"bytes": 12345
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
"failed": []
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Selection Modes
|
|
130
|
+
|
|
131
|
+
`geodot` supports three tile selection modes:
|
|
132
|
+
|
|
133
|
+
1. Grid mode: `-x/-y` selects the top-left tile, then `cols/rows` expands right and down.
|
|
134
|
+
2. Rectangle mode: `-x/-y` is the top-left geographic coordinate and `--x2/--y2` is the bottom-right geographic coordinate.
|
|
135
|
+
3. Polygon mode: `-p/--polygon` specifies a closed area as semicolon-separated `lon,lat` pairs. The closing edge is implicit.
|
|
136
|
+
|
|
137
|
+
Polygon downloads include tiles whose center or corners fall inside the polygon, plus tiles containing polygon vertices.
|
|
138
|
+
|
|
139
|
+
## Python API
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from geodot import Coordinate, DownloadOptions, download, latlon_to_tile, tile_bounds, tile_grid, tile_grid_between, tile_grid_for_polygon
|
|
143
|
+
|
|
144
|
+
tile = latlon_to_tile(55.7303, 37.6504907, 18)
|
|
145
|
+
bounds = tile_bounds(tile)
|
|
146
|
+
tiles = tile_grid(55.7303, 37.6504907, zoom=18, cols=3, rows=3)
|
|
147
|
+
rectangle = tile_grid_between(55.7303, 37.6504907, 55.7297, 37.652, zoom=18)
|
|
148
|
+
polygon = tile_grid_for_polygon([
|
|
149
|
+
Coordinate(lon=37.6504, lat=55.7304),
|
|
150
|
+
Coordinate(lon=37.6520, lat=55.7304),
|
|
151
|
+
Coordinate(lon=37.6520, lat=55.7297),
|
|
152
|
+
Coordinate(lon=37.6504, lat=55.7297),
|
|
153
|
+
], zoom=18)
|
|
154
|
+
|
|
155
|
+
report = download(DownloadOptions(
|
|
156
|
+
lat=55.7303,
|
|
157
|
+
lon=37.6504907,
|
|
158
|
+
bottom_right_lat=55.7297,
|
|
159
|
+
bottom_right_lon=37.652,
|
|
160
|
+
zoom=18,
|
|
161
|
+
cols=3,
|
|
162
|
+
rows=3,
|
|
163
|
+
out="data",
|
|
164
|
+
jobs=16,
|
|
165
|
+
))
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## JavaScript API
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
import { download, latlonToTile, tileBounds, tileGrid, tileGridBetween, tileGridForPolygon } from 'geodot';
|
|
172
|
+
|
|
173
|
+
const tile = latlonToTile(55.7303, 37.6504907, 18);
|
|
174
|
+
const bounds = tileBounds(tile);
|
|
175
|
+
const tiles = tileGrid(55.7303, 37.6504907, 18, 3, 3);
|
|
176
|
+
const rectangle = tileGridBetween(55.7303, 37.6504907, 55.7297, 37.652, 18);
|
|
177
|
+
const polygon = tileGridForPolygon([
|
|
178
|
+
{ lon: 37.6504, lat: 55.7304 },
|
|
179
|
+
{ lon: 37.6520, lat: 55.7304 },
|
|
180
|
+
{ lon: 37.6520, lat: 55.7297 },
|
|
181
|
+
{ lon: 37.6504, lat: 55.7297 },
|
|
182
|
+
], 18);
|
|
183
|
+
|
|
184
|
+
const report = await download({
|
|
185
|
+
lat: 55.7303,
|
|
186
|
+
lon: 37.6504907,
|
|
187
|
+
bottomRightLat: 55.7297,
|
|
188
|
+
bottomRightLon: 37.652,
|
|
189
|
+
zoom: 18,
|
|
190
|
+
cols: 3,
|
|
191
|
+
rows: 3,
|
|
192
|
+
out: 'data',
|
|
193
|
+
jobs: 16,
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Rust API
|
|
198
|
+
|
|
199
|
+
```rust
|
|
200
|
+
use geodot::{download, latlon_to_tile, tile_bounds, tile_grid, tile_grid_between, tile_grid_for_polygon, Coordinate, DownloadOptions};
|
|
201
|
+
|
|
202
|
+
#[tokio::main]
|
|
203
|
+
async fn main() -> anyhow::Result<()> {
|
|
204
|
+
let tile = latlon_to_tile(55.7303, 37.6504907, 18);
|
|
205
|
+
let bounds = tile_bounds(tile);
|
|
206
|
+
let tiles = tile_grid(55.7303, 37.6504907, 18, 3, 3);
|
|
207
|
+
let rectangle = tile_grid_between(55.7303, 37.6504907, 55.7297, 37.652, 18);
|
|
208
|
+
let polygon = tile_grid_for_polygon(&[
|
|
209
|
+
Coordinate { lon: 37.6504, lat: 55.7304 },
|
|
210
|
+
Coordinate { lon: 37.6520, lat: 55.7304 },
|
|
211
|
+
Coordinate { lon: 37.6520, lat: 55.7297 },
|
|
212
|
+
Coordinate { lon: 37.6504, lat: 55.7297 },
|
|
213
|
+
], 18);
|
|
214
|
+
|
|
215
|
+
let report = download(DownloadOptions {
|
|
216
|
+
lat: 55.7303,
|
|
217
|
+
lon: 37.6504907,
|
|
218
|
+
bottom_right_lat: Some(55.7297),
|
|
219
|
+
bottom_right_lon: Some(37.652),
|
|
220
|
+
polygon: Vec::new(),
|
|
221
|
+
zoom: 18,
|
|
222
|
+
cols: 3,
|
|
223
|
+
rows: 3,
|
|
224
|
+
out: "data".into(),
|
|
225
|
+
jobs: 16,
|
|
226
|
+
})
|
|
227
|
+
.await?;
|
|
228
|
+
|
|
229
|
+
Ok(())
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Tile Math
|
|
234
|
+
|
|
235
|
+
Given tile `{ z: 18, x: 158488, y: 81979 }`:
|
|
236
|
+
|
|
237
|
+
```text
|
|
238
|
+
n = 2^z
|
|
239
|
+
lon_min = x / n * 360 - 180
|
|
240
|
+
lon_max = (x + 1) / n * 360 - 180
|
|
241
|
+
lat_max = atan(sinh(pi * (1 - 2y/n))) * 180/pi
|
|
242
|
+
lat_min = atan(sinh(pi * (1 - 2(y+1)/n))) * 180/pi
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Tile bounds are returned as `[lat_min, lon_min, lat_max, lon_max]` fields.
|
|
246
|
+
|
|
247
|
+
Approximate resolution:
|
|
248
|
+
|
|
249
|
+
| Zoom | m/px | Tile covers |
|
|
250
|
+
|------|------|-------------|
|
|
251
|
+
| 18 | 0.34 | 86 x 86 m |
|
|
252
|
+
| 16 | 1.36 | 347 x 347 m |
|
|
253
|
+
| 14 | 5.45 | 1.4 x 1.4 km |
|
|
254
|
+
| 10 | 86 | 22 x 22 km |
|
|
255
|
+
| 8 | 345 | 88 x 88 km |
|
geodot-0.1.0/README.md
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
# geodot
|
|
2
|
+
|
|
3
|
+
`geodot` downloads satellite map tiles and can be used as a CLI or as a library from Python, JavaScript, and Rust.
|
|
4
|
+
|
|
5
|
+
Tiles are saved as:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
{out}/tiles/{z}/{x}/{y}.jpg
|
|
9
|
+
{out}/manifest.json
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
cargo install geodot
|
|
16
|
+
npm install geodot
|
|
17
|
+
pip install geodot
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Run without installing globally:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npx -y geodot -x 37.6504907 -y 55.7303 -z 18 -c 1 -r 1
|
|
24
|
+
uvx geodot -x 37.6504907 -y 55.7303 -z 18 -c 1 -r 1
|
|
25
|
+
cargo install geodot && geodot -x 37.6504907 -y 55.7303 -z 18 -c 1 -r 1
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
During local development:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
python -m pip install -e '.[test]'
|
|
32
|
+
npm test
|
|
33
|
+
cargo test --manifest-path rust/Cargo.toml
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Lint and format checks:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
python -m pip install -e '.[test,dev]'
|
|
40
|
+
ruff format --check python
|
|
41
|
+
ruff check python
|
|
42
|
+
|
|
43
|
+
npm install
|
|
44
|
+
npm run format:check
|
|
45
|
+
npm run lint
|
|
46
|
+
|
|
47
|
+
cargo fmt --manifest-path rust/Cargo.toml -- --check
|
|
48
|
+
cargo clippy --manifest-path rust/Cargo.toml --all-targets -- -D warnings
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## CLI
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
geodot -x 37.6504907 -y 55.7303 -z 18 -c 3 -r 3 -o data -j 16
|
|
55
|
+
|
|
56
|
+
geodot -x 37.6504907 -y 55.7303 --x2 37.652 --y2 55.7297 -z 18 -o data
|
|
57
|
+
|
|
58
|
+
geodot -p "37.6504,55.7304;37.6520,55.7304;37.6520,55.7297;37.6504,55.7297" -z 18 -o data
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
| Flag | Default | Description |
|
|
62
|
+
|------|---------|-------------|
|
|
63
|
+
| `-x`, `--lon` | `37.6504907` | Top-left longitude |
|
|
64
|
+
| `-y`, `--lat` | `55.7303` | Top-left latitude |
|
|
65
|
+
| `--x2`, `--bottom-right-lon` | none | Bottom-right longitude |
|
|
66
|
+
| `--y2`, `--bottom-right-lat` | none | Bottom-right latitude |
|
|
67
|
+
| `-p`, `--polygon` | none | Closed area as `lon,lat;lon,lat;lon,lat` |
|
|
68
|
+
| `-z`, `--zoom` | `18` | Zoom level |
|
|
69
|
+
| `-c`, `--cols` | `3` | Tile columns to the right of the top-left tile |
|
|
70
|
+
| `-r`, `--rows` | `3` | Tile rows downward from the top-left tile |
|
|
71
|
+
| `-o`, `--out` | `data` | Output directory |
|
|
72
|
+
| `-j`, `--jobs` | `16` | Concurrent downloads |
|
|
73
|
+
|
|
74
|
+
## Output
|
|
75
|
+
|
|
76
|
+
For `-o data`, a 3 by 3 download at zoom 18 writes files like this:
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
data/
|
|
80
|
+
├── manifest.json
|
|
81
|
+
└── tiles/
|
|
82
|
+
└── 18/
|
|
83
|
+
└── 158488/
|
|
84
|
+
└── 81979.jpg
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
JPEG bytes are written directly from the tile server without re-compression.
|
|
88
|
+
|
|
89
|
+
Tile images are always 256x256 pixels. `geodot` does not currently support other tile sizes.
|
|
90
|
+
|
|
91
|
+
`manifest.json` contains:
|
|
92
|
+
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"center": { "x": 158488, "y": 81979, "z": 18 },
|
|
96
|
+
"tiles": [
|
|
97
|
+
{
|
|
98
|
+
"tile": { "x": 158488, "y": 81979, "z": 18 },
|
|
99
|
+
"bounds": {
|
|
100
|
+
"lat_min": 55.730012,
|
|
101
|
+
"lon_min": 37.650146,
|
|
102
|
+
"lat_max": 55.730793,
|
|
103
|
+
"lon_max": 37.651520
|
|
104
|
+
},
|
|
105
|
+
"path": "data/tiles/18/158488/81979.jpg",
|
|
106
|
+
"bytes": 12345
|
|
107
|
+
}
|
|
108
|
+
],
|
|
109
|
+
"failed": []
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Selection Modes
|
|
114
|
+
|
|
115
|
+
`geodot` supports three tile selection modes:
|
|
116
|
+
|
|
117
|
+
1. Grid mode: `-x/-y` selects the top-left tile, then `cols/rows` expands right and down.
|
|
118
|
+
2. Rectangle mode: `-x/-y` is the top-left geographic coordinate and `--x2/--y2` is the bottom-right geographic coordinate.
|
|
119
|
+
3. Polygon mode: `-p/--polygon` specifies a closed area as semicolon-separated `lon,lat` pairs. The closing edge is implicit.
|
|
120
|
+
|
|
121
|
+
Polygon downloads include tiles whose center or corners fall inside the polygon, plus tiles containing polygon vertices.
|
|
122
|
+
|
|
123
|
+
## Python API
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from geodot import Coordinate, DownloadOptions, download, latlon_to_tile, tile_bounds, tile_grid, tile_grid_between, tile_grid_for_polygon
|
|
127
|
+
|
|
128
|
+
tile = latlon_to_tile(55.7303, 37.6504907, 18)
|
|
129
|
+
bounds = tile_bounds(tile)
|
|
130
|
+
tiles = tile_grid(55.7303, 37.6504907, zoom=18, cols=3, rows=3)
|
|
131
|
+
rectangle = tile_grid_between(55.7303, 37.6504907, 55.7297, 37.652, zoom=18)
|
|
132
|
+
polygon = tile_grid_for_polygon([
|
|
133
|
+
Coordinate(lon=37.6504, lat=55.7304),
|
|
134
|
+
Coordinate(lon=37.6520, lat=55.7304),
|
|
135
|
+
Coordinate(lon=37.6520, lat=55.7297),
|
|
136
|
+
Coordinate(lon=37.6504, lat=55.7297),
|
|
137
|
+
], zoom=18)
|
|
138
|
+
|
|
139
|
+
report = download(DownloadOptions(
|
|
140
|
+
lat=55.7303,
|
|
141
|
+
lon=37.6504907,
|
|
142
|
+
bottom_right_lat=55.7297,
|
|
143
|
+
bottom_right_lon=37.652,
|
|
144
|
+
zoom=18,
|
|
145
|
+
cols=3,
|
|
146
|
+
rows=3,
|
|
147
|
+
out="data",
|
|
148
|
+
jobs=16,
|
|
149
|
+
))
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## JavaScript API
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
import { download, latlonToTile, tileBounds, tileGrid, tileGridBetween, tileGridForPolygon } from 'geodot';
|
|
156
|
+
|
|
157
|
+
const tile = latlonToTile(55.7303, 37.6504907, 18);
|
|
158
|
+
const bounds = tileBounds(tile);
|
|
159
|
+
const tiles = tileGrid(55.7303, 37.6504907, 18, 3, 3);
|
|
160
|
+
const rectangle = tileGridBetween(55.7303, 37.6504907, 55.7297, 37.652, 18);
|
|
161
|
+
const polygon = tileGridForPolygon([
|
|
162
|
+
{ lon: 37.6504, lat: 55.7304 },
|
|
163
|
+
{ lon: 37.6520, lat: 55.7304 },
|
|
164
|
+
{ lon: 37.6520, lat: 55.7297 },
|
|
165
|
+
{ lon: 37.6504, lat: 55.7297 },
|
|
166
|
+
], 18);
|
|
167
|
+
|
|
168
|
+
const report = await download({
|
|
169
|
+
lat: 55.7303,
|
|
170
|
+
lon: 37.6504907,
|
|
171
|
+
bottomRightLat: 55.7297,
|
|
172
|
+
bottomRightLon: 37.652,
|
|
173
|
+
zoom: 18,
|
|
174
|
+
cols: 3,
|
|
175
|
+
rows: 3,
|
|
176
|
+
out: 'data',
|
|
177
|
+
jobs: 16,
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Rust API
|
|
182
|
+
|
|
183
|
+
```rust
|
|
184
|
+
use geodot::{download, latlon_to_tile, tile_bounds, tile_grid, tile_grid_between, tile_grid_for_polygon, Coordinate, DownloadOptions};
|
|
185
|
+
|
|
186
|
+
#[tokio::main]
|
|
187
|
+
async fn main() -> anyhow::Result<()> {
|
|
188
|
+
let tile = latlon_to_tile(55.7303, 37.6504907, 18);
|
|
189
|
+
let bounds = tile_bounds(tile);
|
|
190
|
+
let tiles = tile_grid(55.7303, 37.6504907, 18, 3, 3);
|
|
191
|
+
let rectangle = tile_grid_between(55.7303, 37.6504907, 55.7297, 37.652, 18);
|
|
192
|
+
let polygon = tile_grid_for_polygon(&[
|
|
193
|
+
Coordinate { lon: 37.6504, lat: 55.7304 },
|
|
194
|
+
Coordinate { lon: 37.6520, lat: 55.7304 },
|
|
195
|
+
Coordinate { lon: 37.6520, lat: 55.7297 },
|
|
196
|
+
Coordinate { lon: 37.6504, lat: 55.7297 },
|
|
197
|
+
], 18);
|
|
198
|
+
|
|
199
|
+
let report = download(DownloadOptions {
|
|
200
|
+
lat: 55.7303,
|
|
201
|
+
lon: 37.6504907,
|
|
202
|
+
bottom_right_lat: Some(55.7297),
|
|
203
|
+
bottom_right_lon: Some(37.652),
|
|
204
|
+
polygon: Vec::new(),
|
|
205
|
+
zoom: 18,
|
|
206
|
+
cols: 3,
|
|
207
|
+
rows: 3,
|
|
208
|
+
out: "data".into(),
|
|
209
|
+
jobs: 16,
|
|
210
|
+
})
|
|
211
|
+
.await?;
|
|
212
|
+
|
|
213
|
+
Ok(())
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Tile Math
|
|
218
|
+
|
|
219
|
+
Given tile `{ z: 18, x: 158488, y: 81979 }`:
|
|
220
|
+
|
|
221
|
+
```text
|
|
222
|
+
n = 2^z
|
|
223
|
+
lon_min = x / n * 360 - 180
|
|
224
|
+
lon_max = (x + 1) / n * 360 - 180
|
|
225
|
+
lat_max = atan(sinh(pi * (1 - 2y/n))) * 180/pi
|
|
226
|
+
lat_min = atan(sinh(pi * (1 - 2(y+1)/n))) * 180/pi
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Tile bounds are returned as `[lat_min, lon_min, lat_max, lon_max]` fields.
|
|
230
|
+
|
|
231
|
+
Approximate resolution:
|
|
232
|
+
|
|
233
|
+
| Zoom | m/px | Tile covers |
|
|
234
|
+
|------|------|-------------|
|
|
235
|
+
| 18 | 0.34 | 86 x 86 m |
|
|
236
|
+
| 16 | 1.36 | 347 x 347 m |
|
|
237
|
+
| 14 | 5.45 | 1.4 x 1.4 km |
|
|
238
|
+
| 10 | 86 | 22 x 22 km |
|
|
239
|
+
| 8 | 345 | 88 x 88 km |
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import js from "@eslint/js";
|
|
2
|
+
|
|
3
|
+
export default [
|
|
4
|
+
js.configs.recommended,
|
|
5
|
+
{
|
|
6
|
+
files: ["js/**/*.js"],
|
|
7
|
+
languageOptions: {
|
|
8
|
+
ecmaVersion: "latest",
|
|
9
|
+
globals: {
|
|
10
|
+
Buffer: "readonly",
|
|
11
|
+
console: "readonly",
|
|
12
|
+
fetch: "readonly",
|
|
13
|
+
globalThis: "readonly",
|
|
14
|
+
performance: "readonly",
|
|
15
|
+
process: "readonly",
|
|
16
|
+
Response: "readonly",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
];
|