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.
@@ -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
@@ -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
+ ];