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.
@@ -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 [![PyPI - Latest Version](https://img.shields.io/pypi/v/topo_map_processor)](https://pypi.org/project/topo_map_processor/) [![GitHub Tag](https://img.shields.io/github/v/tag/ramSeraph/topo_map_processor?filter=v*)](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 --bounds-dir <directory> --output-file <file> [--preexisting-file <file>]
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
- - `--preexisting-file`: Pre existing GeoJSON file path. (Optional)
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> [--dummy-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>]
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. (Required)
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
- - `--dummy-bounds-file`: GeoJSON file containing geographic bounds of areas that need to be updated. These areas might not have any new sheets. (Optional)
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 [![PyPI - Latest Version](https://img.shields.io/pypi/v/topo_map_processor)](https://pypi.org/project/topo_map_processor/) [![GitHub Tag](https://img.shields.io/github/v/tag/ramSeraph/topo_map_processor?filter=v*)](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 --bounds-dir <directory> --output-file <file> [--preexisting-file <file>]
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
- - `--preexisting-file`: Pre existing GeoJSON file path. (Optional)
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> [--dummy-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
+ 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. (Required)
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
- - `--dummy-bounds-file`: GeoJSON file containing geographic bounds of areas that need to be updated. These areas might not have any new sheets. (Optional)
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.14'
32
- __version_tuple__ = version_tuple = (0, 1, 14)
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 total_area / map_area > 2:
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
- self.remove_corner_edges(img_mask, direction, remove_corner_edges_ratio)
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
- self.remove_corner_edges(img_mask, direction, remove_corner_edges_ratio)
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
- self.remove_corner_edges(img_c_mask, direction, self.remove_corner_edges_ratio)
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
- inner_ring = buffered_poly.interiors[0]
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 self.extents is None:
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
- raise Exception(f'rotated_info.txt does not exist in {workdir}')
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
@@ -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('--preexisting-file', help='preexisting GeoJSON file to base things on', default=None)
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.preexisting_file:
16
- preexisting_file = Path(args.preexisting_file)
17
- if not preexisting_file.exists():
18
- raise FileNotFoundError(f"Preexisting file does not exist: {preexisting_file}")
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(preexisting_file.read_text())
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
- '--exclude',
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
- 'exclude_transparent': True,
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, dummy_bounds_file, max_zoom):
365
+ def assess_sheet_requirements(retile_list_file, bounds_file, force_redo_bounds_file, max_zoom):
364
366
 
365
- retile_sheets = retile_list_file.read_text().split('\n')
366
- retile_sheets = set([ r.strip().replace('.tif', '') for r in retile_sheets if r.strip() != '' ])
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
- dummy_sheets_to_box = {}
371
- if dummy_bounds_file:
372
- dummy_sheets_to_box = get_sheet_data(dummy_bounds_file, dummy=True)
373
- sheets_to_box.update(dummy_sheets_to_box)
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 dummy_sheets_to_box.keys():
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 dummy_sheets_to_box:
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=True, help='File containing list of sheets to retile')
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('--dummy-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')
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 = Path(args.retile_list_file)
430
- if not retile_list_file.exists():
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
- dummy_bounds_file = None
438
- if args.dummy_bounds_file:
439
- dummy_bounds_file = Path(args.dummy_bounds_file)
440
- if not dummy_bounds_file.exists():
441
- parser.error(f'Dummy bounds file {args.dummy_bounds_file} does not exist')
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, dummy_bounds_file, max_zoom)
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 dummy_bounds_file is not None:
487
- create_dummy_sheets(dummy_bounds_file, tiffs_dir)
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']
@@ -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("-l", "--listing-files-tiled", required=True, help="Name of the tiled listing file")
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
- retile_list_file = Path("to_retile.txt")
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 listing files
72
- print("Downloading listing files...")
73
- run_command(["gh", "release", "download", args.gtiffs_release, "-p", "listing_files.csv", "--clobber"])
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
- with open(retile_list_file, "w") as f:
86
- for sheet in sheets_to_retile:
87
- f.write(f"{sheet}\n")
88
-
89
- print(f"Found {len(sheets_to_retile)} sheets to retile.")
90
- Path("listing_files_tiled.csv").unlink()
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 not sheets_to_retile:
93
- print("No sheets to retile found. Cleaning up and exiting.")
94
- Path("listing_files.csv").unlink()
95
- retile_list_file.unlink()
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 bounds and original PMTiles
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
- "--retile-list-file", str(retile_list_file),
108
- "--bounds-file", "bounds.geojson",
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 in url_map:
125
- url = url_map[fname]
118
+ if fname:
126
119
  print(f"Pulling {fname}")
127
- run_command(["wget", "-q", "-P", str(tiffs_dir), url])
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
- "--retile-list-file", str(retile_list_file),
135
- "--bounds-file", "bounds.geojson",
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
- retile_list_file.unlink()
142
- Path("bounds.geojson").unlink()
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 listing files
163
- if args.listing_files_tiled != "listing_files.csv":
164
- print(f"Renaming listing_files.csv to {args.listing_files_tiled}")
165
- Path("listing_files.csv").rename(args.listing_files_tiled)
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
- print("Uploading new listing file...")
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()