topo_map_processor 0.1.14__tar.gz → 0.1.17__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.
- topo_map_processor-0.1.14/README.md → topo_map_processor-0.1.17/PKG-INFO +135 -8
- topo_map_processor-0.1.14/PKG-INFO → topo_map_processor-0.1.17/README.md +60 -57
- {topo_map_processor-0.1.14 → topo_map_processor-0.1.17}/pyproject.toml +28 -0
- {topo_map_processor-0.1.14 → topo_map_processor-0.1.17}/topo_map_processor/_version.py +2 -2
- {topo_map_processor-0.1.14 → topo_map_processor-0.1.17}/topo_map_processor/processor.py +77 -7
- {topo_map_processor-0.1.14 → topo_map_processor-0.1.17}/topo_map_processor/tools/collect_bounds.py +17 -8
- topo_map_processor-0.1.17/topo_map_processor/tools/create_force_redo_bounds.py +100 -0
- {topo_map_processor-0.1.14 → topo_map_processor-0.1.17}/topo_map_processor/tools/retile.py +31 -24
- {topo_map_processor-0.1.14 → topo_map_processor-0.1.17}/topo_map_processor/tools/retile_e2e.py +46 -50
- {topo_map_processor-0.1.14 → topo_map_processor-0.1.17}/.gitignore +0 -0
- {topo_map_processor-0.1.14 → topo_map_processor-0.1.17}/LICENSE +0 -0
- {topo_map_processor-0.1.14 → topo_map_processor-0.1.17}/topo_map_processor/__init__.py +0 -0
- {topo_map_processor-0.1.14 → topo_map_processor-0.1.17}/topo_map_processor/tools/__init__.py +0 -0
- {topo_map_processor-0.1.14 → topo_map_processor-0.1.17}/topo_map_processor/tools/tile.py +0 -0
- {topo_map_processor-0.1.14 → topo_map_processor-0.1.17}/topo_map_processor/tools/utils.py +0 -0
|
@@ -1,3 +1,78 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: topo_map_processor
|
|
3
|
+
Version: 0.1.17
|
|
4
|
+
Summary: Library for processing topographic maps
|
|
5
|
+
Project-URL: homepage, https://github.com/ramSeraph/topo_map_processor
|
|
6
|
+
Project-URL: repository, https://github.com/ramSeraph/topo_map_processor.git
|
|
7
|
+
Author-email: Sreeram Kandimalla <kandimalla.sreeram@gmail.com>
|
|
8
|
+
License: This is free and unencumbered software released into the public domain.
|
|
9
|
+
|
|
10
|
+
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
11
|
+
distribute this software, either in source code form or as a compiled
|
|
12
|
+
binary, for any purpose, commercial or non-commercial, and by any
|
|
13
|
+
means.
|
|
14
|
+
|
|
15
|
+
In jurisdictions that recognize copyright laws, the author or authors
|
|
16
|
+
of this software dedicate any and all copyright interest in the
|
|
17
|
+
software to the public domain. We make this dedication for the benefit
|
|
18
|
+
of the public at large and to the detriment of our heirs and
|
|
19
|
+
successors. We intend this dedication to be an overt act of
|
|
20
|
+
relinquishment in perpetuity of all present and future rights to this
|
|
21
|
+
software under copyright law.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
24
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
25
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
26
|
+
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
27
|
+
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
28
|
+
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
29
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
|
30
|
+
|
|
31
|
+
For more information, please refer to <https://unlicense.org>
|
|
32
|
+
License-File: LICENSE
|
|
33
|
+
Requires-Python: >=3.12
|
|
34
|
+
Requires-Dist: mercantile>=1.2.1
|
|
35
|
+
Requires-Dist: pmtiles-mosaic>=0.1.0
|
|
36
|
+
Requires-Dist: requests>=2.32.4
|
|
37
|
+
Requires-Dist: shapely>=2.1.1
|
|
38
|
+
Provides-Extra: easyocr
|
|
39
|
+
Requires-Dist: easyocr>=1.7.2; extra == 'easyocr'
|
|
40
|
+
Provides-Extra: parse
|
|
41
|
+
Requires-Dist: imgcat>=0.6.0; extra == 'parse'
|
|
42
|
+
Requires-Dist: numpy>=2.3.1; extra == 'parse'
|
|
43
|
+
Requires-Dist: opencv-python-headless>=4.11.0.86; extra == 'parse'
|
|
44
|
+
Requires-Dist: pillow>=11.3.0; extra == 'parse'
|
|
45
|
+
Requires-Dist: pyproj>=3.7.1; extra == 'parse'
|
|
46
|
+
Requires-Dist: pytesseract>=0.3.13; extra == 'parse'
|
|
47
|
+
Requires-Dist: rasterio>=1.4.3; extra == 'parse'
|
|
48
|
+
Provides-Extra: pinned
|
|
49
|
+
Requires-Dist: aiofiles==24.1.0; extra == 'pinned'
|
|
50
|
+
Requires-Dist: aiohappyeyeballs==2.6.1; extra == 'pinned'
|
|
51
|
+
Requires-Dist: aiohttp==3.12.15; extra == 'pinned'
|
|
52
|
+
Requires-Dist: aiosignal==1.4.0; extra == 'pinned'
|
|
53
|
+
Requires-Dist: attrs==25.3.0; extra == 'pinned'
|
|
54
|
+
Requires-Dist: certifi==2025.8.3; extra == 'pinned'
|
|
55
|
+
Requires-Dist: charset-normalizer==3.4.3; extra == 'pinned'
|
|
56
|
+
Requires-Dist: click==8.3.0; extra == 'pinned'
|
|
57
|
+
Requires-Dist: colorama==0.4.6; (sys_platform == 'win32') and extra == 'pinned'
|
|
58
|
+
Requires-Dist: colorlog==6.9.0; extra == 'pinned'
|
|
59
|
+
Requires-Dist: frozenlist==1.7.0; extra == 'pinned'
|
|
60
|
+
Requires-Dist: idna==3.10; extra == 'pinned'
|
|
61
|
+
Requires-Dist: mercantile==1.2.1; extra == 'pinned'
|
|
62
|
+
Requires-Dist: multidict==6.6.4; extra == 'pinned'
|
|
63
|
+
Requires-Dist: numpy==2.3.3; extra == 'pinned'
|
|
64
|
+
Requires-Dist: pillow==11.3.0; extra == 'pinned'
|
|
65
|
+
Requires-Dist: pmtiles-mosaic==0.1.5; extra == 'pinned'
|
|
66
|
+
Requires-Dist: pmtiles==3.4.1; extra == 'pinned'
|
|
67
|
+
Requires-Dist: propcache==0.3.2; extra == 'pinned'
|
|
68
|
+
Requires-Dist: pypdl==1.5.6; extra == 'pinned'
|
|
69
|
+
Requires-Dist: requests==2.32.5; extra == 'pinned'
|
|
70
|
+
Requires-Dist: shapely==2.1.1; extra == 'pinned'
|
|
71
|
+
Requires-Dist: typing-extensions==4.15.0; (python_full_version < '3.13') and extra == 'pinned'
|
|
72
|
+
Requires-Dist: urllib3==2.5.0; extra == 'pinned'
|
|
73
|
+
Requires-Dist: yarl==1.20.1; extra == 'pinned'
|
|
74
|
+
Description-Content-Type: text/markdown
|
|
75
|
+
|
|
1
76
|
# Topo Map Processor [](https://pypi.org/project/topo_map_processor/) [](https://github.com/ramSeraph/topo_map_processor/releases/latest)
|
|
2
77
|
|
|
3
78
|
|
|
@@ -174,28 +249,29 @@ This project provides several command-line tools to work with topographic maps.
|
|
|
174
249
|
Collects individual GeoJSONL bound files into a single GeoJSON FeatureCollection.
|
|
175
250
|
|
|
176
251
|
```bash
|
|
177
|
-
collect-bounds
|
|
252
|
+
collect-bounds -b <directory> -o <file> [-u] [-d <id1> <id2> ...]
|
|
178
253
|
```
|
|
179
254
|
|
|
180
|
-
- `--bounds-dir`: Directory containing GeoJSONL files. (Required)
|
|
181
|
-
- `--output-file`: Output GeoJSON file path. (Required)
|
|
182
|
-
- `--
|
|
255
|
+
- `-b`, `--bounds-dir`: Directory containing GeoJSONL files. (Required)
|
|
256
|
+
- `-o`, `--output-file`: Output GeoJSON file path. (Required)
|
|
257
|
+
- `-u`, `--update`: Update the output file in place. (Optional)
|
|
258
|
+
- `-d`, `--delete`: List of ids to delete from the output file (only available in update mode). (Optional)
|
|
183
259
|
|
|
184
260
|
#### `retile`
|
|
185
261
|
|
|
186
|
-
Retiles a specific list of sheets (GeoTIFFs) and updates the corresponding tiles in an existing tile set. It can determine which other sheets are affected and need to be pulled in to correctly retile the area.
|
|
262
|
+
Retiles a specific list of sheets (GeoTIFFs) or a specific area and updates the corresponding tiles in an existing tile set. It can determine which other sheets are affected and need to be pulled in to correctly retile the area.
|
|
187
263
|
|
|
188
264
|
The tool works in two stages. First, it calculates the full set of sheets that need to be processed (the "pull list"), which includes the sheets from the input list plus any adjacent sheets that share tiles. You can run the tool with `--sheets-to-pull-list-outfile` to generate this list and then exit. This is useful for fetching the required GeoTIFFs before proceeding.
|
|
189
265
|
|
|
190
266
|
In the second stage, it retiles the affected area. It uses the pull list to create a temporary virtual raster (`.vrt`), generates new base tiles for the affected region, and then reconstructs the upper-level tiles by pulling existing, unaffected tiles from the PMTiles source.
|
|
191
267
|
|
|
192
268
|
```bash
|
|
193
|
-
retile --retile-list-file <file> --bounds-file <file> [--
|
|
269
|
+
retile [--retile-list-file <file>] --bounds-file <file> [--force-redo-bounds-file <file>] [--max-zoom <zoom>] [--tiffs-dir <dir>] [--tiles-dir <dir>] [--from-source <source>] [--sheets-to-pull-list-outfile <file>] [--num-parallel <processes>] [--tile-quality <quality>]
|
|
194
270
|
```
|
|
195
271
|
|
|
196
|
-
- `--retile-list-file`: File containing the list of sheets to retile. (
|
|
272
|
+
- `--retile-list-file`: File containing the list of sheets to retile. (Optional)
|
|
197
273
|
- `--bounds-file`: GeoJSON file containing the list of available sheets and their geographic bounds. (Required)
|
|
198
|
-
- `--
|
|
274
|
+
- `--force-redo-bounds-file`: GeoJSON file containing geographic bounds of areas that need to be updated. These areas might not have any new sheets. (Optional)
|
|
199
275
|
- `--max-zoom`: Maximum zoom level to create tiles for. Defaults to the max zoom from the source if `--from-source` is provided, otherwise it is required.
|
|
200
276
|
- `--tiffs-dir`: Directory where the GeoTIFFs are located. (Required if not in pull-list generation mode)
|
|
201
277
|
- `--tiles-dir`: Directory where the tiles will be created. (Required if not in pull-list generation mode)
|
|
@@ -204,6 +280,18 @@ retile --retile-list-file <file> --bounds-file <file> [--dummy-bounds-file <file
|
|
|
204
280
|
- `--num-parallel`: Number of parallel processes to use for tiling (default: number of CPU cores).
|
|
205
281
|
- `--tile-quality`: Quality of compression for webp and jpeg (default: 75).
|
|
206
282
|
|
|
283
|
+
#### `create-force-redo-bounds`
|
|
284
|
+
|
|
285
|
+
Creates a GeoJSON file that defines areas to be forcibly re-tiled, based on the differences between an old and a new bounds file. This is useful for identifying regions where map sheets may have been updated, moved, or removed.
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
create-force-redo-bounds --old-bounds-file <file> --new-bounds-file <file> --output-file <file>
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
- `--old-bounds-file`: The previous version of the bounds GeoJSON file. (Required)
|
|
292
|
+
- `--new-bounds-file`: The new version of the bounds GeoJSON file. (Required)
|
|
293
|
+
- `--output-file`: The path to write the output GeoJSON file, which will contain the bounds of the areas to be force-retiled. (Required)
|
|
294
|
+
|
|
207
295
|
#### `tile`
|
|
208
296
|
|
|
209
297
|
Creates web map tiles from a directory of GeoTIFF files. It combines them into a virtual raster (`.vrt`) for efficient processing.
|
|
@@ -241,6 +329,45 @@ GDAL_VERSION=$(gdalinfo --version | cut -d"," -f1 | cut -d" " -f2)
|
|
|
241
329
|
uvx --with numpy --with pillow --with gdal==$GDAL_VERSION --from topo_map_processor tile --help
|
|
242
330
|
```
|
|
243
331
|
|
|
332
|
+
### Map Update Workflow
|
|
333
|
+
|
|
334
|
+
This project provides a robust workflow for updating an existing tile set when map sheets are added, removed, or modified. The primary tools for this are `retile` and `create-force-redo-bounds`.
|
|
335
|
+
|
|
336
|
+
The general workflow is as follows:
|
|
337
|
+
|
|
338
|
+
1. **Generate New Bounds**: After processing new or updated map scans with the library, you will have a new set of COGs and `.geojsonl` bound files. Use the `collect-bounds` tool to create an updated `bounds.geojson` file that reflects the current state of your map collection.
|
|
339
|
+
|
|
340
|
+
2. **Identify Changes**: Use the `create-force-redo-bounds` tool to compare your old `bounds.geojson` with the new one.
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
create-force-redo-bounds --old-bounds-file old-bounds.geojson --new-bounds-file new-bounds.geojson --output-file force-redo.geojson
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
This command analyzes the differences:
|
|
347
|
+
* **Added Sheets**: If a map sheet exists in `new-bounds.geojson` but not in `old-bounds.geojson`.
|
|
348
|
+
* **Deleted Sheets**: If a sheet is in `old-bounds.geojson` but has been removed from `new-bounds.geojson`.
|
|
349
|
+
* **Modified Sheets**: If a sheet's geometry (boundary) or its content (indicated by a change in the `digest` property) has been updated.
|
|
350
|
+
|
|
351
|
+
The output, `force-redo.geojson`, will contain the geographic areas for all these changes, which tells the `retile` tool exactly which regions of the map need to be regenerated.
|
|
352
|
+
|
|
353
|
+
3. **Retile Affected Areas**: Use the `retile` tool to perform the update. The `force-redo.geojson` file you created contains all the information about what has changed, so a separate list of sheets is not required.
|
|
354
|
+
|
|
355
|
+
```bash
|
|
356
|
+
retile --bounds-file new-bounds.geojson \
|
|
357
|
+
--force-redo-bounds-file force-redo.geojson \
|
|
358
|
+
--from-source "existing-tiles/*.pmtiles" \
|
|
359
|
+
--tiffs-dir /path/to/cogs \
|
|
360
|
+
--tiles-dir /path/to/output/tiles
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
The `retile` tool will:
|
|
364
|
+
* Identify all GeoTIFFs needed to regenerate the affected tiles (including adjacent, unchanged sheets).
|
|
365
|
+
* Generate new base tiles for the changed areas.
|
|
366
|
+
* Copy unchanged tiles from your existing tile set (`--from-source`).
|
|
367
|
+
* Re-create parent tiles up the zoom levels.
|
|
368
|
+
|
|
369
|
+
This workflow ensures that your tile set is updated efficiently, without having to re-process the entire map collection.
|
|
370
|
+
|
|
244
371
|
## Contributing
|
|
245
372
|
|
|
246
373
|
Contributions are welcome! Please feel free to submit a pull request.
|
|
@@ -1,52 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: topo_map_processor
|
|
3
|
-
Version: 0.1.14
|
|
4
|
-
Summary: Library for processing topographic maps
|
|
5
|
-
Project-URL: homepage, https://github.com/ramSeraph/topo_map_processor
|
|
6
|
-
Project-URL: repository, https://github.com/ramSeraph/topo_map_processor.git
|
|
7
|
-
Author-email: Sreeram Kandimalla <kandimalla.sreeram@gmail.com>
|
|
8
|
-
License: This is free and unencumbered software released into the public domain.
|
|
9
|
-
|
|
10
|
-
Anyone is free to copy, modify, publish, use, compile, sell, or
|
|
11
|
-
distribute this software, either in source code form or as a compiled
|
|
12
|
-
binary, for any purpose, commercial or non-commercial, and by any
|
|
13
|
-
means.
|
|
14
|
-
|
|
15
|
-
In jurisdictions that recognize copyright laws, the author or authors
|
|
16
|
-
of this software dedicate any and all copyright interest in the
|
|
17
|
-
software to the public domain. We make this dedication for the benefit
|
|
18
|
-
of the public at large and to the detriment of our heirs and
|
|
19
|
-
successors. We intend this dedication to be an overt act of
|
|
20
|
-
relinquishment in perpetuity of all present and future rights to this
|
|
21
|
-
software under copyright law.
|
|
22
|
-
|
|
23
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
24
|
-
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
25
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
26
|
-
IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
|
|
27
|
-
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
|
28
|
-
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
29
|
-
OTHER DEALINGS IN THE SOFTWARE.
|
|
30
|
-
|
|
31
|
-
For more information, please refer to <https://unlicense.org>
|
|
32
|
-
License-File: LICENSE
|
|
33
|
-
Requires-Python: >=3.12
|
|
34
|
-
Requires-Dist: mercantile>=1.2.1
|
|
35
|
-
Requires-Dist: pmtiles-mosaic>=0.1.0
|
|
36
|
-
Requires-Dist: requests>=2.32.4
|
|
37
|
-
Requires-Dist: shapely>=2.1.1
|
|
38
|
-
Provides-Extra: easyocr
|
|
39
|
-
Requires-Dist: easyocr>=1.7.2; extra == 'easyocr'
|
|
40
|
-
Provides-Extra: parse
|
|
41
|
-
Requires-Dist: imgcat>=0.6.0; extra == 'parse'
|
|
42
|
-
Requires-Dist: numpy>=2.3.1; extra == 'parse'
|
|
43
|
-
Requires-Dist: opencv-python-headless>=4.11.0.86; extra == 'parse'
|
|
44
|
-
Requires-Dist: pillow>=11.3.0; extra == 'parse'
|
|
45
|
-
Requires-Dist: pyproj>=3.7.1; extra == 'parse'
|
|
46
|
-
Requires-Dist: pytesseract>=0.3.13; extra == 'parse'
|
|
47
|
-
Requires-Dist: rasterio>=1.4.3; extra == 'parse'
|
|
48
|
-
Description-Content-Type: text/markdown
|
|
49
|
-
|
|
50
1
|
# Topo Map Processor [](https://pypi.org/project/topo_map_processor/) [](https://github.com/ramSeraph/topo_map_processor/releases/latest)
|
|
51
2
|
|
|
52
3
|
|
|
@@ -223,28 +174,29 @@ This project provides several command-line tools to work with topographic maps.
|
|
|
223
174
|
Collects individual GeoJSONL bound files into a single GeoJSON FeatureCollection.
|
|
224
175
|
|
|
225
176
|
```bash
|
|
226
|
-
collect-bounds
|
|
177
|
+
collect-bounds -b <directory> -o <file> [-u] [-d <id1> <id2> ...]
|
|
227
178
|
```
|
|
228
179
|
|
|
229
|
-
- `--bounds-dir`: Directory containing GeoJSONL files. (Required)
|
|
230
|
-
- `--output-file`: Output GeoJSON file path. (Required)
|
|
231
|
-
- `--
|
|
180
|
+
- `-b`, `--bounds-dir`: Directory containing GeoJSONL files. (Required)
|
|
181
|
+
- `-o`, `--output-file`: Output GeoJSON file path. (Required)
|
|
182
|
+
- `-u`, `--update`: Update the output file in place. (Optional)
|
|
183
|
+
- `-d`, `--delete`: List of ids to delete from the output file (only available in update mode). (Optional)
|
|
232
184
|
|
|
233
185
|
#### `retile`
|
|
234
186
|
|
|
235
|
-
Retiles a specific list of sheets (GeoTIFFs) and updates the corresponding tiles in an existing tile set. It can determine which other sheets are affected and need to be pulled in to correctly retile the area.
|
|
187
|
+
Retiles a specific list of sheets (GeoTIFFs) or a specific area and updates the corresponding tiles in an existing tile set. It can determine which other sheets are affected and need to be pulled in to correctly retile the area.
|
|
236
188
|
|
|
237
189
|
The tool works in two stages. First, it calculates the full set of sheets that need to be processed (the "pull list"), which includes the sheets from the input list plus any adjacent sheets that share tiles. You can run the tool with `--sheets-to-pull-list-outfile` to generate this list and then exit. This is useful for fetching the required GeoTIFFs before proceeding.
|
|
238
190
|
|
|
239
191
|
In the second stage, it retiles the affected area. It uses the pull list to create a temporary virtual raster (`.vrt`), generates new base tiles for the affected region, and then reconstructs the upper-level tiles by pulling existing, unaffected tiles from the PMTiles source.
|
|
240
192
|
|
|
241
193
|
```bash
|
|
242
|
-
retile --retile-list-file <file> --bounds-file <file> [--
|
|
194
|
+
retile [--retile-list-file <file>] --bounds-file <file> [--force-redo-bounds-file <file>] [--max-zoom <zoom>] [--tiffs-dir <dir>] [--tiles-dir <dir>] [--from-source <source>] [--sheets-to-pull-list-outfile <file>] [--num-parallel <processes>] [--tile-quality <quality>]
|
|
243
195
|
```
|
|
244
196
|
|
|
245
|
-
- `--retile-list-file`: File containing the list of sheets to retile. (
|
|
197
|
+
- `--retile-list-file`: File containing the list of sheets to retile. (Optional)
|
|
246
198
|
- `--bounds-file`: GeoJSON file containing the list of available sheets and their geographic bounds. (Required)
|
|
247
|
-
- `--
|
|
199
|
+
- `--force-redo-bounds-file`: GeoJSON file containing geographic bounds of areas that need to be updated. These areas might not have any new sheets. (Optional)
|
|
248
200
|
- `--max-zoom`: Maximum zoom level to create tiles for. Defaults to the max zoom from the source if `--from-source` is provided, otherwise it is required.
|
|
249
201
|
- `--tiffs-dir`: Directory where the GeoTIFFs are located. (Required if not in pull-list generation mode)
|
|
250
202
|
- `--tiles-dir`: Directory where the tiles will be created. (Required if not in pull-list generation mode)
|
|
@@ -253,6 +205,18 @@ retile --retile-list-file <file> --bounds-file <file> [--dummy-bounds-file <file
|
|
|
253
205
|
- `--num-parallel`: Number of parallel processes to use for tiling (default: number of CPU cores).
|
|
254
206
|
- `--tile-quality`: Quality of compression for webp and jpeg (default: 75).
|
|
255
207
|
|
|
208
|
+
#### `create-force-redo-bounds`
|
|
209
|
+
|
|
210
|
+
Creates a GeoJSON file that defines areas to be forcibly re-tiled, based on the differences between an old and a new bounds file. This is useful for identifying regions where map sheets may have been updated, moved, or removed.
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
create-force-redo-bounds --old-bounds-file <file> --new-bounds-file <file> --output-file <file>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
- `--old-bounds-file`: The previous version of the bounds GeoJSON file. (Required)
|
|
217
|
+
- `--new-bounds-file`: The new version of the bounds GeoJSON file. (Required)
|
|
218
|
+
- `--output-file`: The path to write the output GeoJSON file, which will contain the bounds of the areas to be force-retiled. (Required)
|
|
219
|
+
|
|
256
220
|
#### `tile`
|
|
257
221
|
|
|
258
222
|
Creates web map tiles from a directory of GeoTIFF files. It combines them into a virtual raster (`.vrt`) for efficient processing.
|
|
@@ -290,6 +254,45 @@ GDAL_VERSION=$(gdalinfo --version | cut -d"," -f1 | cut -d" " -f2)
|
|
|
290
254
|
uvx --with numpy --with pillow --with gdal==$GDAL_VERSION --from topo_map_processor tile --help
|
|
291
255
|
```
|
|
292
256
|
|
|
257
|
+
### Map Update Workflow
|
|
258
|
+
|
|
259
|
+
This project provides a robust workflow for updating an existing tile set when map sheets are added, removed, or modified. The primary tools for this are `retile` and `create-force-redo-bounds`.
|
|
260
|
+
|
|
261
|
+
The general workflow is as follows:
|
|
262
|
+
|
|
263
|
+
1. **Generate New Bounds**: After processing new or updated map scans with the library, you will have a new set of COGs and `.geojsonl` bound files. Use the `collect-bounds` tool to create an updated `bounds.geojson` file that reflects the current state of your map collection.
|
|
264
|
+
|
|
265
|
+
2. **Identify Changes**: Use the `create-force-redo-bounds` tool to compare your old `bounds.geojson` with the new one.
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
create-force-redo-bounds --old-bounds-file old-bounds.geojson --new-bounds-file new-bounds.geojson --output-file force-redo.geojson
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
This command analyzes the differences:
|
|
272
|
+
* **Added Sheets**: If a map sheet exists in `new-bounds.geojson` but not in `old-bounds.geojson`.
|
|
273
|
+
* **Deleted Sheets**: If a sheet is in `old-bounds.geojson` but has been removed from `new-bounds.geojson`.
|
|
274
|
+
* **Modified Sheets**: If a sheet's geometry (boundary) or its content (indicated by a change in the `digest` property) has been updated.
|
|
275
|
+
|
|
276
|
+
The output, `force-redo.geojson`, will contain the geographic areas for all these changes, which tells the `retile` tool exactly which regions of the map need to be regenerated.
|
|
277
|
+
|
|
278
|
+
3. **Retile Affected Areas**: Use the `retile` tool to perform the update. The `force-redo.geojson` file you created contains all the information about what has changed, so a separate list of sheets is not required.
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
retile --bounds-file new-bounds.geojson \
|
|
282
|
+
--force-redo-bounds-file force-redo.geojson \
|
|
283
|
+
--from-source "existing-tiles/*.pmtiles" \
|
|
284
|
+
--tiffs-dir /path/to/cogs \
|
|
285
|
+
--tiles-dir /path/to/output/tiles
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The `retile` tool will:
|
|
289
|
+
* Identify all GeoTIFFs needed to regenerate the affected tiles (including adjacent, unchanged sheets).
|
|
290
|
+
* Generate new base tiles for the changed areas.
|
|
291
|
+
* Copy unchanged tiles from your existing tile set (`--from-source`).
|
|
292
|
+
* Re-create parent tiles up the zoom levels.
|
|
293
|
+
|
|
294
|
+
This workflow ensures that your tile set is updated efficiently, without having to re-process the entire map collection.
|
|
295
|
+
|
|
293
296
|
## Contributing
|
|
294
297
|
|
|
295
298
|
Contributions are welcome! Please feel free to submit a pull request.
|
|
@@ -30,6 +30,33 @@ parse = [
|
|
|
30
30
|
"pytesseract>=0.3.13",
|
|
31
31
|
"rasterio>=1.4.3",
|
|
32
32
|
]
|
|
33
|
+
pinned = [
|
|
34
|
+
"aiofiles==24.1.0",
|
|
35
|
+
"aiohappyeyeballs==2.6.1",
|
|
36
|
+
"aiohttp==3.12.15",
|
|
37
|
+
"aiosignal==1.4.0",
|
|
38
|
+
"attrs==25.3.0",
|
|
39
|
+
"certifi==2025.8.3",
|
|
40
|
+
"charset-normalizer==3.4.3",
|
|
41
|
+
"click==8.3.0",
|
|
42
|
+
"colorama==0.4.6 ; sys_platform == 'win32'",
|
|
43
|
+
"colorlog==6.9.0",
|
|
44
|
+
"frozenlist==1.7.0",
|
|
45
|
+
"idna==3.10",
|
|
46
|
+
"mercantile==1.2.1",
|
|
47
|
+
"multidict==6.6.4",
|
|
48
|
+
"numpy==2.3.3",
|
|
49
|
+
"pillow==11.3.0",
|
|
50
|
+
"pmtiles==3.4.1",
|
|
51
|
+
"pmtiles-mosaic==0.1.5",
|
|
52
|
+
"propcache==0.3.2",
|
|
53
|
+
"pypdl==1.5.6",
|
|
54
|
+
"requests==2.32.5",
|
|
55
|
+
"shapely==2.1.1",
|
|
56
|
+
"typing-extensions==4.15.0 ; python_full_version < '3.13'",
|
|
57
|
+
"urllib3==2.5.0",
|
|
58
|
+
"yarl==1.20.1",
|
|
59
|
+
]
|
|
33
60
|
|
|
34
61
|
[project.urls]
|
|
35
62
|
homepage = "https://github.com/ramSeraph/topo_map_processor"
|
|
@@ -41,6 +68,7 @@ collect-bounds = "topo_map_processor.tools.collect_bounds:cli"
|
|
|
41
68
|
retile = "topo_map_processor.tools.retile:cli"
|
|
42
69
|
tile = "topo_map_processor.tools.tile:cli"
|
|
43
70
|
retile-e2e = "topo_map_processor.tools.retile_e2e:cli"
|
|
71
|
+
create-force-redo-bounds = "topo_map_processor.tools.create_force_redo_bounds:cli"
|
|
44
72
|
|
|
45
73
|
[build-system]
|
|
46
74
|
requires = [
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.1.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 1,
|
|
31
|
+
__version__ = version = '0.1.17'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 17)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -4,6 +4,7 @@ import time
|
|
|
4
4
|
import json
|
|
5
5
|
import pickle
|
|
6
6
|
import shutil
|
|
7
|
+
import hashlib
|
|
7
8
|
import subprocess
|
|
8
9
|
|
|
9
10
|
from pathlib import Path
|
|
@@ -81,6 +82,7 @@ class TopoMapProcessor:
|
|
|
81
82
|
self.small_img = None
|
|
82
83
|
self.mapbox_corners = None
|
|
83
84
|
self.rotation_reversal_params = None
|
|
85
|
+
self.digest = None
|
|
84
86
|
|
|
85
87
|
self.extents = extra.get('extents', None)
|
|
86
88
|
self.pixel_cutlines = extra.get('pixel_cutlines', [])
|
|
@@ -106,6 +108,7 @@ class TopoMapProcessor:
|
|
|
106
108
|
self.use_bbox_area = extra.get('use_bbox_area', True)
|
|
107
109
|
self.shrunk_map_area_corners = extra.get('shrunk_map_area_corners', None)
|
|
108
110
|
self.poly_approx_factor = extra.get('poly_approx_factor', 0.001)
|
|
111
|
+
self.map_area_ratio_thresh = extra.get('map_area_ratio_thresh', 0.5)
|
|
109
112
|
|
|
110
113
|
# corners related
|
|
111
114
|
self.corner_ratio = extra.get('corner_ratio', 400.0 / 9000.0)
|
|
@@ -132,6 +135,7 @@ class TopoMapProcessor:
|
|
|
132
135
|
self.warp_jpeg_export_quality = extra.get('warp_jpeg_export_quality', 75)
|
|
133
136
|
self.warp_output_height = extra.get('warp_output_height', 6500)
|
|
134
137
|
|
|
138
|
+
|
|
135
139
|
self.color_map = {
|
|
136
140
|
'black': ((0, 0, 0), (179, 255, 130)),
|
|
137
141
|
'greyish': ((0, 0, 50), (179, 150, 192)),
|
|
@@ -484,6 +488,10 @@ class TopoMapProcessor:
|
|
|
484
488
|
ls = LineString(line)
|
|
485
489
|
line_poly = ls.buffer(line_buf, resolution=1, cap_style=CAP_STYLE.flat).intersection(limits)
|
|
486
490
|
blur_poly = ls.buffer(blur_buf, resolution=1, cap_style=CAP_STYLE.flat).intersection(limits)
|
|
491
|
+
print(f'removing line {line} with line_buf {line_buf}, blur_buf {blur_buf}, blur_kern {blur_kern}, blur_repeat {blur_repeat}')
|
|
492
|
+
if blur_poly.is_empty:
|
|
493
|
+
print('blur poly empty, nothing to do')
|
|
494
|
+
return
|
|
487
495
|
bb = blur_poly.bounds
|
|
488
496
|
bb = [ round(x) for x in bb ]
|
|
489
497
|
# restrict to a small img strip to make things less costly
|
|
@@ -810,7 +818,7 @@ class TopoMapProcessor:
|
|
|
810
818
|
|
|
811
819
|
h, w = img.shape[:2]
|
|
812
820
|
total_area = w * h
|
|
813
|
-
if
|
|
821
|
+
if map_area / total_area < self.map_area_ratio_thresh:
|
|
814
822
|
raise Exception(f'map area less than expected, {map_area=}, {total_area=}')
|
|
815
823
|
|
|
816
824
|
self.show_contours(img_mask, [map_contour])
|
|
@@ -1088,7 +1096,8 @@ class TopoMapProcessor:
|
|
|
1088
1096
|
if remove_text:
|
|
1089
1097
|
self.remove_text(img_mask)
|
|
1090
1098
|
|
|
1091
|
-
|
|
1099
|
+
if remove_corner_edges_ratio is not None:
|
|
1100
|
+
self.remove_corner_edges(img_mask, direction, remove_corner_edges_ratio)
|
|
1092
1101
|
|
|
1093
1102
|
h, w = img_mask.shape[:2]
|
|
1094
1103
|
self.show_image(img_mask)
|
|
@@ -1166,7 +1175,8 @@ class TopoMapProcessor:
|
|
|
1166
1175
|
|
|
1167
1176
|
anchor_corner = (ax, ay)
|
|
1168
1177
|
|
|
1169
|
-
|
|
1178
|
+
if remove_corner_edges_ratio is not None:
|
|
1179
|
+
self.remove_corner_edges(img_mask, direction, remove_corner_edges_ratio)
|
|
1170
1180
|
|
|
1171
1181
|
frame_contour = self.get_biggest_contour(img_mask, corner_erode, True, max_corner_contour_area_ratio)
|
|
1172
1182
|
|
|
@@ -1230,7 +1240,8 @@ class TopoMapProcessor:
|
|
|
1230
1240
|
|
|
1231
1241
|
img_c_mask = self.get_color_mask(img, corner_contour_color)
|
|
1232
1242
|
self.remove_text(img_c_mask)
|
|
1233
|
-
|
|
1243
|
+
if remove_corner_edges_ratio is not None:
|
|
1244
|
+
self.remove_corner_edges(img_c_mask, direction, remove_corner_edges_ratio)
|
|
1234
1245
|
frame_contour = self.get_biggest_contour(img_c_mask, self.corner_erode, True, max_corner_contour_area_ratio)
|
|
1235
1246
|
|
|
1236
1247
|
frame_contour_area = self.get_bbox_area_simple(frame_contour)
|
|
@@ -1250,6 +1261,7 @@ class TopoMapProcessor:
|
|
|
1250
1261
|
fx + fw * ( 1 if direction[0] < 0 else 0),
|
|
1251
1262
|
fy + fh * ( 1 if direction[1] < 0 else 0),
|
|
1252
1263
|
]
|
|
1264
|
+
|
|
1253
1265
|
frame_diag_len = self.get_distance((0,0), (fh,fw))
|
|
1254
1266
|
self.show_points([frame_anchor_point], img_c_mask, [0,0,255])
|
|
1255
1267
|
|
|
@@ -1294,10 +1306,24 @@ class TopoMapProcessor:
|
|
|
1294
1306
|
map_poly_points = self.reorder_poly_points(map_poly_points)
|
|
1295
1307
|
print(f'{map_poly_points=}')
|
|
1296
1308
|
|
|
1309
|
+
all_corner_overrides_given = all([ c is not None for c in corner_overrides ])
|
|
1310
|
+
if all_corner_overrides_given:
|
|
1311
|
+
return corner_overrides
|
|
1312
|
+
|
|
1313
|
+
some_corner_overrides_given = any([ c is not None for c in corner_overrides ])
|
|
1314
|
+
if some_corner_overrides_given and len(corner_overrides) != len(map_poly_points):
|
|
1315
|
+
raise ValueError("If some corner overrides are given, either all must be given or the number of overrides must match the number of polygon points.")
|
|
1316
|
+
|
|
1297
1317
|
#num_corners = len(map_poly_points)
|
|
1298
1318
|
box = LinearRing(map_poly_points + [map_poly_points[0]])
|
|
1299
1319
|
buffered_poly = box.buffer(-cw/2, single_sided=True, join_style=JOIN_STYLE.mitre)
|
|
1300
|
-
|
|
1320
|
+
|
|
1321
|
+
# TODO: this is a hack.. understand why this happens and fix it properly
|
|
1322
|
+
if len(buffered_poly.interiors) == 0:
|
|
1323
|
+
inner_ring = buffered_poly.exterior
|
|
1324
|
+
else:
|
|
1325
|
+
inner_ring = buffered_poly.interiors[0]
|
|
1326
|
+
|
|
1301
1327
|
corner_centers = list(inner_ring.coords)
|
|
1302
1328
|
corner_centers.reverse()
|
|
1303
1329
|
print(f'{corner_centers=}')
|
|
@@ -1390,7 +1416,7 @@ class TopoMapProcessor:
|
|
|
1390
1416
|
corner_overrides = [ (c[0] - map_bbox_scaled[0], c[1] - map_bbox_scaled[1]) if c is not None else None for c in corner_overrides_full ]
|
|
1391
1417
|
map_poly_points_scaled = [ (p[0] - map_bbox_scaled[0], p[1] - map_bbox_scaled[1]) for p in map_poly_points_scaled ]
|
|
1392
1418
|
|
|
1393
|
-
if
|
|
1419
|
+
if len(corner_overrides) == 4:
|
|
1394
1420
|
corners = self.locate_corners(map_img, corner_overrides)
|
|
1395
1421
|
else:
|
|
1396
1422
|
corners = self.locate_corners_generic(map_img, map_poly_points_scaled, corner_overrides)
|
|
@@ -1610,6 +1636,21 @@ class TopoMapProcessor:
|
|
|
1610
1636
|
translate_cmd = f'gdal_translate {creation_options} {perf_options} {gcp_str} -a_srs "{crs_proj}" -of GTiff {str(from_file)} {str(georef_file)}'
|
|
1611
1637
|
self.run_external(translate_cmd)
|
|
1612
1638
|
|
|
1639
|
+
def get_digest(self):
|
|
1640
|
+
if self.digest is not None:
|
|
1641
|
+
return self.digest
|
|
1642
|
+
|
|
1643
|
+
hasher = hashlib.sha256()
|
|
1644
|
+
export_file = self.get_export_file()
|
|
1645
|
+
with open(export_file, 'rb') as f:
|
|
1646
|
+
while True:
|
|
1647
|
+
data = f.read(65536)
|
|
1648
|
+
if not data:
|
|
1649
|
+
break
|
|
1650
|
+
hasher.update(data)
|
|
1651
|
+
self.digest = hasher.hexdigest()
|
|
1652
|
+
return self.digest
|
|
1653
|
+
|
|
1613
1654
|
def get_cutline_props(self):
|
|
1614
1655
|
crs_proj = self.get_crs_proj()
|
|
1615
1656
|
|
|
@@ -1687,7 +1728,7 @@ class TopoMapProcessor:
|
|
|
1687
1728
|
workdir = self.get_workdir()
|
|
1688
1729
|
rotated_info_file = workdir.joinpath('rotated_info.txt')
|
|
1689
1730
|
if not rotated_info_file.exists():
|
|
1690
|
-
|
|
1731
|
+
return RotationReversalParams(False, 0.0, None, None)
|
|
1691
1732
|
|
|
1692
1733
|
parts = rotated_info_file.read_text().strip().split(',')
|
|
1693
1734
|
angle = float(parts[0])
|
|
@@ -1813,6 +1854,32 @@ class TopoMapProcessor:
|
|
|
1813
1854
|
|
|
1814
1855
|
self.run_external(f'ogr2ogr -t_srs EPSG:4326 -s_srs "{crs_proj}" -f GeoJSONSeq {str(bounds_file)} {cutline_file}')
|
|
1815
1856
|
|
|
1857
|
+
def add_digest_to_bounds_file(self):
|
|
1858
|
+
bounds_dir = self.get_bounds_dir()
|
|
1859
|
+
|
|
1860
|
+
bounds_file = bounds_dir.joinpath(f'{self.get_id()}.geojsonl')
|
|
1861
|
+
if not bounds_file.exists():
|
|
1862
|
+
print(f'{bounds_file} does not exist.. cannot add digest')
|
|
1863
|
+
return
|
|
1864
|
+
|
|
1865
|
+
digest = self.get_digest()
|
|
1866
|
+
|
|
1867
|
+
with open(bounds_file, 'r') as f:
|
|
1868
|
+
lines = f.readlines()
|
|
1869
|
+
|
|
1870
|
+
if len(lines) != 1:
|
|
1871
|
+
print(f'expected 1 line in {bounds_file}, got {len(lines)}')
|
|
1872
|
+
return
|
|
1873
|
+
|
|
1874
|
+
feature = json.loads(lines[0])
|
|
1875
|
+
if 'properties' not in feature:
|
|
1876
|
+
feature['properties'] = {}
|
|
1877
|
+
|
|
1878
|
+
feature['properties']['digest'] = digest
|
|
1879
|
+
|
|
1880
|
+
with open(bounds_file, 'w') as f:
|
|
1881
|
+
f.write(json.dumps(feature) + '\n')
|
|
1882
|
+
|
|
1816
1883
|
def warp(self):
|
|
1817
1884
|
workdir = self.get_workdir()
|
|
1818
1885
|
|
|
@@ -1851,6 +1918,9 @@ class TopoMapProcessor:
|
|
|
1851
1918
|
|
|
1852
1919
|
self.export_gtiff(str(final_file), str(export_file), self.jpeg_export_quality)
|
|
1853
1920
|
|
|
1921
|
+
self.add_digest_to_bounds_file()
|
|
1922
|
+
|
|
1923
|
+
|
|
1854
1924
|
def remove_insets(self):
|
|
1855
1925
|
if self.inset_pixel_cutlines is None or len(self.inset_pixel_cutlines) == 0:
|
|
1856
1926
|
return
|
{topo_map_processor-0.1.14 → topo_map_processor-0.1.17}/topo_map_processor/tools/collect_bounds.py
RENAMED
|
@@ -5,23 +5,32 @@ from pathlib import Path
|
|
|
5
5
|
|
|
6
6
|
def cli():
|
|
7
7
|
parser = argparse.ArgumentParser(description='Collect GeoJSONL files into a single GeoJSON file.')
|
|
8
|
-
parser.add_argument('--bounds-dir', required=True, help='Directory containing GeoJSONL files')
|
|
9
|
-
parser.add_argument('--output-file', required=True, help='Output GeoJSON file path')
|
|
10
|
-
parser.add_argument('
|
|
8
|
+
parser.add_argument('-b', '--bounds-dir', required=True, help='Directory containing GeoJSONL files')
|
|
9
|
+
parser.add_argument('-o', '--output-file', required=True, help='Output GeoJSON file path')
|
|
10
|
+
parser.add_argument('-u', '--update', action='store_true', help='Update the output file in place')
|
|
11
|
+
parser.add_argument('-d', '--delete', nargs='+', help='List of ids to delete from the output file (only in update mode)')
|
|
11
12
|
|
|
12
13
|
args = parser.parse_args()
|
|
13
14
|
|
|
15
|
+
if args.delete and not args.update:
|
|
16
|
+
raise parser.error("--delete can only be used with --update")
|
|
17
|
+
|
|
14
18
|
feat_map = {}
|
|
15
|
-
if args.
|
|
16
|
-
|
|
17
|
-
if not
|
|
18
|
-
raise FileNotFoundError(f"
|
|
19
|
+
if args.update:
|
|
20
|
+
output_file = Path(args.output_file)
|
|
21
|
+
if not output_file.exists():
|
|
22
|
+
raise FileNotFoundError(f"Output file does not exist for updating: {output_file}")
|
|
19
23
|
|
|
20
|
-
existing_data = json.loads(
|
|
24
|
+
existing_data = json.loads(output_file.read_text())
|
|
21
25
|
for feature in existing_data.get('features', []):
|
|
22
26
|
if 'properties' in feature and 'id' in feature['properties']:
|
|
23
27
|
sheet_name = feature['properties']['id']
|
|
24
28
|
feat_map[sheet_name] = feature
|
|
29
|
+
|
|
30
|
+
if args.delete:
|
|
31
|
+
for sheet_id in args.delete:
|
|
32
|
+
if sheet_id in feat_map:
|
|
33
|
+
del feat_map[sheet_id]
|
|
25
34
|
|
|
26
35
|
bounds_dir = Path(args.bounds_dir)
|
|
27
36
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
def get_sheet_map(bounds_file):
|
|
7
|
+
"""Reads a bounds file and returns a map of sheet ID to feature."""
|
|
8
|
+
with open(bounds_file, 'r') as f:
|
|
9
|
+
data = json.load(f)
|
|
10
|
+
|
|
11
|
+
sheet_map = {}
|
|
12
|
+
for feature in data.get('features', []):
|
|
13
|
+
if 'properties' in feature and 'id' in feature['properties']:
|
|
14
|
+
sheet_id = feature['properties']['id']
|
|
15
|
+
sheet_map[sheet_id] = feature
|
|
16
|
+
return sheet_map
|
|
17
|
+
|
|
18
|
+
def main(argv):
|
|
19
|
+
parser = argparse.ArgumentParser(description='Create a force redo bounds file by comparing old and new bounds.')
|
|
20
|
+
parser.add_argument('--old-bounds-file', required=True, help='Path to the old GeoJSON bounds file.')
|
|
21
|
+
parser.add_argument('--new-bounds-file', required=True, help='Path to the new GeoJSON bounds file.')
|
|
22
|
+
parser.add_argument('--output-file', required=True, help='Path to write the output GeoJSON file with force redo bounds.')
|
|
23
|
+
|
|
24
|
+
args = parser.parse_args(argv)
|
|
25
|
+
|
|
26
|
+
old_sheets = get_sheet_map(args.old_bounds_file)
|
|
27
|
+
new_sheets = get_sheet_map(args.new_bounds_file)
|
|
28
|
+
|
|
29
|
+
old_ids = set(old_sheets.keys())
|
|
30
|
+
new_ids = set(new_sheets.keys())
|
|
31
|
+
|
|
32
|
+
added_ids = new_ids - old_ids
|
|
33
|
+
deleted_ids = old_ids - new_ids
|
|
34
|
+
common_ids = old_ids.intersection(new_ids)
|
|
35
|
+
|
|
36
|
+
output_features = []
|
|
37
|
+
|
|
38
|
+
# Added sheets
|
|
39
|
+
for sheet_id in added_ids:
|
|
40
|
+
feature = new_sheets[sheet_id]
|
|
41
|
+
output_features.append({
|
|
42
|
+
"type": "Feature",
|
|
43
|
+
"geometry": feature['geometry'],
|
|
44
|
+
"properties": { "reason": f"Sheet {sheet_id} added" }
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
# Deleted sheets
|
|
48
|
+
for sheet_id in deleted_ids:
|
|
49
|
+
feature = old_sheets[sheet_id]
|
|
50
|
+
output_features.append({
|
|
51
|
+
"type": "Feature",
|
|
52
|
+
"geometry": feature['geometry'],
|
|
53
|
+
"properties": { "reason": f"Sheet {sheet_id} deleted" }
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
# Common sheets (check for modifications)
|
|
57
|
+
for sheet_id in common_ids:
|
|
58
|
+
old_feature = old_sheets[sheet_id]
|
|
59
|
+
new_feature = new_sheets[sheet_id]
|
|
60
|
+
old_geom = old_feature.get('geometry')
|
|
61
|
+
new_geom = new_feature.get('geometry')
|
|
62
|
+
geom_changed = json.dumps(old_geom, sort_keys=True) != json.dumps(new_geom, sort_keys=True)
|
|
63
|
+
old_digest = old_feature.get('properties', {}).get('digest')
|
|
64
|
+
new_digest = new_feature.get('properties', {}).get('digest')
|
|
65
|
+
digest_changed = old_digest != new_digest
|
|
66
|
+
if geom_changed:
|
|
67
|
+
output_features.append({
|
|
68
|
+
"type": "Feature",
|
|
69
|
+
"geometry": old_geom,
|
|
70
|
+
"properties": { "reason": f"Sheet {sheet_id} geometry changed (old)" }
|
|
71
|
+
})
|
|
72
|
+
output_features.append({
|
|
73
|
+
"type": "Feature",
|
|
74
|
+
"geometry": new_geom,
|
|
75
|
+
"properties": { "reason": f"Sheet {sheet_id} geometry changed (new)" }
|
|
76
|
+
})
|
|
77
|
+
elif digest_changed:
|
|
78
|
+
output_features.append({
|
|
79
|
+
"type": "Feature",
|
|
80
|
+
"geometry": new_geom,
|
|
81
|
+
"properties": { "reason": f"Sheet {sheet_id} digest changed" }
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
with open(args.output_file, 'w') as f:
|
|
85
|
+
f.write('{ "type": "FeatureCollection", "features": [\n')
|
|
86
|
+
for i, feature in enumerate(output_features):
|
|
87
|
+
line = json.dumps(feature)
|
|
88
|
+
if i < len(output_features) - 1:
|
|
89
|
+
line += ','
|
|
90
|
+
line += '\n'
|
|
91
|
+
f.write(line)
|
|
92
|
+
f.write(']}\n')
|
|
93
|
+
|
|
94
|
+
print(f"Created force redo bounds file at {args.output_file} with {len(output_features)} features.")
|
|
95
|
+
|
|
96
|
+
def cli():
|
|
97
|
+
main(sys.argv[1:])
|
|
98
|
+
|
|
99
|
+
if __name__ == "__main__":
|
|
100
|
+
cli()
|
|
@@ -186,7 +186,8 @@ class Tiler:
|
|
|
186
186
|
'-r', 'antialias',
|
|
187
187
|
'--verbose',
|
|
188
188
|
'-w', 'none',
|
|
189
|
-
|
|
189
|
+
# N.B. if transperent tiles are excluded, the old data might show through
|
|
190
|
+
#'--exclude',
|
|
190
191
|
'--resume',
|
|
191
192
|
'--xyz',
|
|
192
193
|
'--processes', f'{self.pool._processes}',
|
|
@@ -206,7 +207,8 @@ class Tiler:
|
|
|
206
207
|
'verbose': False,
|
|
207
208
|
'quiet': False,
|
|
208
209
|
'xyz': True,
|
|
209
|
-
|
|
210
|
+
# N.B. if transperent tiles are excluded, the old data might show through
|
|
211
|
+
#'exclude_transparent': True,
|
|
210
212
|
'profile': 'mercator',
|
|
211
213
|
'resampling': 'antialias',
|
|
212
214
|
'tiledriver': tile_driver,
|
|
@@ -360,17 +362,19 @@ def get_base_tile_sheet_mappings(sheets_to_box, base_zoom):
|
|
|
360
362
|
|
|
361
363
|
|
|
362
364
|
|
|
363
|
-
def assess_sheet_requirements(retile_list_file, bounds_file,
|
|
365
|
+
def assess_sheet_requirements(retile_list_file, bounds_file, force_redo_bounds_file, max_zoom):
|
|
364
366
|
|
|
365
|
-
retile_sheets =
|
|
366
|
-
|
|
367
|
+
retile_sheets = set()
|
|
368
|
+
if retile_list_file:
|
|
369
|
+
retile_sheets = retile_list_file.read_text().split('\n')
|
|
370
|
+
retile_sheets = set([ r.strip().replace('.tif', '') for r in retile_sheets if r.strip() != '' ])
|
|
367
371
|
|
|
368
372
|
print('getting base tiles to sheet mapping')
|
|
369
373
|
sheets_to_box = get_sheet_data(bounds_file)
|
|
370
|
-
|
|
371
|
-
if
|
|
372
|
-
|
|
373
|
-
sheets_to_box.update(
|
|
374
|
+
force_redo_sheets_to_box = {}
|
|
375
|
+
if force_redo_bounds_file is not None:
|
|
376
|
+
force_redo_sheets_to_box = get_sheet_data(force_redo_bounds_file, dummy=True)
|
|
377
|
+
sheets_to_box.update(force_redo_sheets_to_box)
|
|
374
378
|
|
|
375
379
|
sheets_to_base_tiles, base_tiles_to_sheets = get_base_tile_sheet_mappings(sheets_to_box, max_zoom)
|
|
376
380
|
|
|
@@ -378,20 +382,20 @@ def assess_sheet_requirements(retile_list_file, bounds_file, dummy_bounds_file,
|
|
|
378
382
|
affected_base_tiles = set()
|
|
379
383
|
for sheet_no in retile_sheets:
|
|
380
384
|
affected_base_tiles.update(sheets_to_base_tiles[sheet_no])
|
|
381
|
-
for sheet_no in
|
|
385
|
+
for sheet_no in force_redo_sheets_to_box.keys():
|
|
382
386
|
affected_base_tiles.update(sheets_to_base_tiles[sheet_no])
|
|
383
387
|
|
|
384
388
|
sheets_to_pull = set()
|
|
385
389
|
for tile in affected_base_tiles:
|
|
386
390
|
to_add = base_tiles_to_sheets[tile]
|
|
387
391
|
for sheet in to_add:
|
|
388
|
-
if sheet not in
|
|
392
|
+
if sheet not in force_redo_sheets_to_box:
|
|
389
393
|
sheets_to_pull.add(sheet + '.tif')
|
|
390
394
|
|
|
391
395
|
return sheets_to_pull, affected_base_tiles
|
|
392
396
|
|
|
393
397
|
def create_dummy_sheets(dummy_bounds_file, tiffs_dir):
|
|
394
|
-
print('creating dummy sheets')
|
|
398
|
+
print('creating dummy sheets for force redo areas')
|
|
395
399
|
dummy_sheets_to_box = get_sheet_data(dummy_bounds_file, dummy=True)
|
|
396
400
|
|
|
397
401
|
for sheet_no, box in dummy_sheets_to_box.items():
|
|
@@ -412,9 +416,9 @@ def create_dummy_sheets(dummy_bounds_file, tiffs_dir):
|
|
|
412
416
|
def retile_main(args):
|
|
413
417
|
|
|
414
418
|
parser = argparse.ArgumentParser()
|
|
415
|
-
parser.add_argument('--retile-list-file', required=
|
|
419
|
+
parser.add_argument('--retile-list-file', required=False, default=None, help='File containing list of sheets to retile')
|
|
416
420
|
parser.add_argument('--bounds-file', required=True, help='Geojson file containing list of available sheets and their georaphic bounds')
|
|
417
|
-
parser.add_argument('--
|
|
421
|
+
parser.add_argument('--force-redo-bounds-file', required=False, help='Geojson file containing georaphic bounds of areas that need to be updated, these areas might not have any new sheets')
|
|
418
422
|
parser.add_argument('--max-zoom', type=int, help='Maximum zoom level to create tiles for, needed only when --from-source is not provided')
|
|
419
423
|
parser.add_argument('--sheets-to-pull-list-outfile', default=None,
|
|
420
424
|
help='Output into which we write the list of sheet that need to be pulled, if set, the script ends after it created the list file')
|
|
@@ -426,19 +430,22 @@ def retile_main(args):
|
|
|
426
430
|
|
|
427
431
|
args = parser.parse_args(args)
|
|
428
432
|
|
|
429
|
-
retile_list_file
|
|
430
|
-
|
|
433
|
+
if not args.retile_list_file and not args.force_redo_bounds_file:
|
|
434
|
+
parser.error('At least one of --retile-list-file or --force-redo-bounds-file must be provided.')
|
|
435
|
+
|
|
436
|
+
retile_list_file = Path(args.retile_list_file) if args.retile_list_file else None
|
|
437
|
+
if retile_list_file and not retile_list_file.exists():
|
|
431
438
|
parser.error(f'Retile list file {args.retile_list_file} does not exist')
|
|
432
439
|
|
|
433
440
|
bounds_file = Path(args.bounds_file)
|
|
434
441
|
if not bounds_file.exists():
|
|
435
442
|
parser.error(f'Bounds file {args.bounds_file} does not exist')
|
|
436
443
|
|
|
437
|
-
|
|
438
|
-
if args.
|
|
439
|
-
|
|
440
|
-
if not
|
|
441
|
-
parser.error(f'
|
|
444
|
+
force_redo_bounds_file = None
|
|
445
|
+
if args.force_redo_bounds_file:
|
|
446
|
+
force_redo_bounds_file = Path(args.force_redo_bounds_file)
|
|
447
|
+
if not force_redo_bounds_file.exists():
|
|
448
|
+
parser.error(f'Force redo bounds file {args.force_redo_bounds_file} does not exist')
|
|
442
449
|
|
|
443
450
|
|
|
444
451
|
if not args.sheets_to_pull_list_outfile:
|
|
@@ -470,7 +477,7 @@ def retile_main(args):
|
|
|
470
477
|
print(f'--max-zoom argument is ignored when --from-source is set, using max zoom from original source: {max_zoom}')
|
|
471
478
|
|
|
472
479
|
# calculate the sheets to pull and affected base tiles
|
|
473
|
-
sheets_to_pull, affected_base_tiles = assess_sheet_requirements(retile_list_file, bounds_file,
|
|
480
|
+
sheets_to_pull, affected_base_tiles = assess_sheet_requirements(retile_list_file, bounds_file, force_redo_bounds_file, max_zoom)
|
|
474
481
|
|
|
475
482
|
if args.sheets_to_pull_list_outfile is not None:
|
|
476
483
|
Path(args.sheets_to_pull_list_outfile).write_text('\n'.join(sheets_to_pull) + '\n')
|
|
@@ -483,8 +490,8 @@ def retile_main(args):
|
|
|
483
490
|
print('check the sheets availability')
|
|
484
491
|
check_sheets(sheets_to_pull, tiffs_dir)
|
|
485
492
|
|
|
486
|
-
if
|
|
487
|
-
create_dummy_sheets(
|
|
493
|
+
if force_redo_bounds_file is not None:
|
|
494
|
+
create_dummy_sheets(force_redo_bounds_file, tiffs_dir)
|
|
488
495
|
|
|
489
496
|
metadata = orig_source.get_metadata()
|
|
490
497
|
tile_extension = metadata['format']
|
{topo_map_processor-0.1.14 → topo_map_processor-0.1.17}/topo_map_processor/tools/retile_e2e.py
RENAMED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
|
|
2
1
|
import argparse
|
|
3
|
-
import csv
|
|
4
2
|
import glob
|
|
3
|
+
import json
|
|
5
4
|
import shutil
|
|
6
5
|
import subprocess
|
|
7
6
|
import sys
|
|
@@ -11,6 +10,7 @@ from multiprocessing import set_start_method
|
|
|
11
10
|
from pmtiles_mosaic.partition import partition_main
|
|
12
11
|
|
|
13
12
|
from .retile import retile_main
|
|
13
|
+
from .create_force_redo_bounds import main as create_force_redo_bounds_main
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
def run_command(cmd, check=True, cwd=None):
|
|
@@ -50,7 +50,7 @@ def cli():
|
|
|
50
50
|
parser.add_argument("-p", "--pmtiles-release", required=True, help="PMTiles release tag")
|
|
51
51
|
parser.add_argument("-g", "--gtiffs-release", required=True, help="GeoTIFFs release tag")
|
|
52
52
|
parser.add_argument("-x", "--pmtiles-prefix", required=True, help="Prefix for PMTiles files")
|
|
53
|
-
parser.add_argument("
|
|
53
|
+
parser.add_argument("--bounds-file-tiled", required=True, help="Name of the bounds file in the pmtiles release (the old bounds file)")
|
|
54
54
|
args = parser.parse_args()
|
|
55
55
|
|
|
56
56
|
# Define paths and filenames
|
|
@@ -59,7 +59,10 @@ def cli():
|
|
|
59
59
|
tiffs_dir = Path("staging/gtiffs/")
|
|
60
60
|
from_pmtiles_dir = Path("staging/pmtiles")
|
|
61
61
|
to_pmtiles_dir = Path("export/pmtiles")
|
|
62
|
-
|
|
62
|
+
|
|
63
|
+
old_bounds_file = Path("old_bounds.geojson")
|
|
64
|
+
new_bounds_file = Path("new_bounds.geojson")
|
|
65
|
+
force_redo_bounds_file = Path("force_redo_bounds.geojson")
|
|
63
66
|
|
|
64
67
|
from_pmtiles_prefix = from_pmtiles_dir / args.pmtiles_prefix
|
|
65
68
|
to_pmtiles_prefix = to_pmtiles_dir / args.pmtiles_prefix
|
|
@@ -68,44 +71,38 @@ def cli():
|
|
|
68
71
|
for d in [tiles_dir, tiffs_dir, from_pmtiles_dir, to_pmtiles_dir]:
|
|
69
72
|
d.mkdir(parents=True, exist_ok=True)
|
|
70
73
|
|
|
71
|
-
# Download
|
|
72
|
-
print("Downloading
|
|
73
|
-
run_command(["gh", "release", "download", args.gtiffs_release, "-p", "
|
|
74
|
-
run_command(["gh", "release", "download", args.pmtiles_release, "-p", args.listing_files_tiled, "--clobber", "-O", "listing_files_tiled.csv"])
|
|
75
|
-
|
|
76
|
-
# Determine sheets to retile
|
|
77
|
-
print("Determining sheets to retile...")
|
|
78
|
-
with open("listing_files.csv") as f:
|
|
79
|
-
gtiffs_sheets = {row['name'] for row in csv.DictReader(f)}
|
|
80
|
-
with open("listing_files_tiled.csv") as f:
|
|
81
|
-
pmtiles_sheets = {row['name'] for row in csv.DictReader(f)}
|
|
82
|
-
|
|
83
|
-
sheets_to_retile = sorted(list(gtiffs_sheets - pmtiles_sheets))
|
|
74
|
+
# Download bounds files
|
|
75
|
+
print("Downloading new bounds file from gtiffs release...")
|
|
76
|
+
run_command(["gh", "release", "download", args.gtiffs_release, "-p", "bounds.geojson", "--clobber", "-O", str(new_bounds_file)])
|
|
84
77
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
print(
|
|
90
|
-
|
|
78
|
+
print("Downloading old bounds file from pmtiles release...")
|
|
79
|
+
run_command(["gh", "release", "download", args.pmtiles_release, "-p", args.bounds_file_tiled, "--clobber", "-O", str(old_bounds_file)])
|
|
80
|
+
|
|
81
|
+
# Create force redo bounds file
|
|
82
|
+
print("Creating force redo bounds file...")
|
|
83
|
+
create_force_redo_bounds_main([
|
|
84
|
+
"--old-bounds-file", str(old_bounds_file),
|
|
85
|
+
"--new-bounds-file", str(new_bounds_file),
|
|
86
|
+
"--output-file", str(force_redo_bounds_file)
|
|
87
|
+
])
|
|
91
88
|
|
|
92
|
-
if
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
# Check if there are any changes
|
|
90
|
+
with open(force_redo_bounds_file) as f:
|
|
91
|
+
force_redo_data = json.load(f)
|
|
92
|
+
|
|
93
|
+
if not force_redo_data.get("features"):
|
|
94
|
+
print("No changes detected between bounds files. Exiting.")
|
|
96
95
|
sys.exit(0)
|
|
97
96
|
|
|
98
|
-
# Download
|
|
99
|
-
print("Downloading bounds file...")
|
|
100
|
-
run_command(["gh", "release", "download", args.gtiffs_release, "-p", "bounds.geojson"])
|
|
97
|
+
# Download original PMTiles
|
|
101
98
|
print("Getting original PMTiles files...")
|
|
102
99
|
run_command(["gh", "release", "download", args.pmtiles_release, "-D", str(from_pmtiles_dir), "-p", f"{args.pmtiles_prefix}*"])
|
|
103
100
|
|
|
104
101
|
# Get list of sheets to pull
|
|
105
102
|
print("Getting list of sheets to pull...")
|
|
106
103
|
retile_main([
|
|
107
|
-
"--
|
|
108
|
-
"--bounds-file",
|
|
104
|
+
"--bounds-file", str(new_bounds_file),
|
|
105
|
+
"--force-redo-bounds-file", str(force_redo_bounds_file),
|
|
109
106
|
"--sheets-to-pull-list-outfile", str(sheets_to_pull_list_outfile),
|
|
110
107
|
"--from-source", f"{from_pmtiles_prefix}*.pmtiles",
|
|
111
108
|
])
|
|
@@ -115,31 +112,30 @@ def cli():
|
|
|
115
112
|
|
|
116
113
|
# Download TIFFs
|
|
117
114
|
print("Downloading TIFFs...")
|
|
118
|
-
with open("listing_files.csv") as f:
|
|
119
|
-
url_map = {row[0]: row[2] for row in csv.reader(f)}
|
|
120
|
-
|
|
121
115
|
with open(sheets_to_pull_list_outfile) as f:
|
|
122
116
|
for line in f:
|
|
123
117
|
fname = line.strip()
|
|
124
|
-
if fname
|
|
125
|
-
url = url_map[fname]
|
|
118
|
+
if fname:
|
|
126
119
|
print(f"Pulling {fname}")
|
|
127
|
-
|
|
120
|
+
try:
|
|
121
|
+
run_command(["gh", "release", "download", args.gtiffs_release, "-p", fname, "-D", str(tiffs_dir), "--clobber"])
|
|
122
|
+
except subprocess.CalledProcessError as e:
|
|
123
|
+
print(f"Failed to download {fname}. It might be a dummy sheet from force redo. Continuing...")
|
|
128
124
|
|
|
129
125
|
sheets_to_pull_list_outfile.unlink()
|
|
130
126
|
|
|
131
127
|
# Retile sheets
|
|
132
128
|
print("Retiling the sheets...")
|
|
133
129
|
retile_main([
|
|
134
|
-
"--
|
|
135
|
-
"--bounds-file",
|
|
130
|
+
"--bounds-file", str(new_bounds_file),
|
|
131
|
+
"--force-redo-bounds-file", str(force_redo_bounds_file),
|
|
136
132
|
"--from-source", f"{from_pmtiles_prefix}*.pmtiles",
|
|
137
133
|
"--tiles-dir", str(tiles_dir),
|
|
138
134
|
"--tiffs-dir", str(tiffs_dir),
|
|
139
135
|
])
|
|
140
136
|
|
|
141
|
-
|
|
142
|
-
|
|
137
|
+
old_bounds_file.unlink()
|
|
138
|
+
force_redo_bounds_file.unlink()
|
|
143
139
|
|
|
144
140
|
# Create new PMTiles files
|
|
145
141
|
print("Creating new pmtiles files...")
|
|
@@ -159,14 +155,14 @@ def cli():
|
|
|
159
155
|
new_pmtiles = glob.glob(f"{to_pmtiles_prefix}*")
|
|
160
156
|
run_command(["gh", "release", "upload", args.pmtiles_release, "--clobber"] + new_pmtiles)
|
|
161
157
|
|
|
162
|
-
# Handle
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
158
|
+
# Handle bounds file
|
|
159
|
+
print("Uploading new bounds file to pmtiles release...")
|
|
160
|
+
upload_bounds_file = Path(args.bounds_file_tiled)
|
|
161
|
+
new_bounds_file.rename(upload_bounds_file)
|
|
162
|
+
|
|
163
|
+
run_command(["gh", "release", "upload", args.pmtiles_release, str(upload_bounds_file), "--clobber"])
|
|
166
164
|
|
|
167
|
-
|
|
168
|
-
run_command(["gh", "release", "upload", args.pmtiles_release, args.listing_files_tiled, "--clobber"])
|
|
169
|
-
Path(args.listing_files_tiled).unlink()
|
|
165
|
+
upload_bounds_file.unlink()
|
|
170
166
|
|
|
171
167
|
# Cleanup
|
|
172
168
|
print("Cleaning up staging directories...")
|
|
@@ -179,4 +175,4 @@ def cli():
|
|
|
179
175
|
|
|
180
176
|
|
|
181
177
|
if __name__ == "__main__":
|
|
182
|
-
cli()
|
|
178
|
+
cli()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{topo_map_processor-0.1.14 → topo_map_processor-0.1.17}/topo_map_processor/tools/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|