topo_map_processor 0.1.13__tar.gz → 0.1.16__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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: topo_map_processor
3
- Version: 0.1.13
3
+ Version: 0.1.16
4
4
  Summary: Library for processing topographic maps
5
5
  Project-URL: homepage, https://github.com/ramSeraph/topo_map_processor
6
6
  Project-URL: repository, https://github.com/ramSeraph/topo_map_processor.git
@@ -45,6 +45,32 @@ Requires-Dist: pillow>=11.3.0; extra == 'parse'
45
45
  Requires-Dist: pyproj>=3.7.1; extra == 'parse'
46
46
  Requires-Dist: pytesseract>=0.3.13; extra == 'parse'
47
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'
48
74
  Description-Content-Type: text/markdown
49
75
 
50
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)
@@ -239,11 +265,12 @@ The tool works in two stages. First, it calculates the full set of sheets that n
239
265
  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
266
 
241
267
  ```bash
242
- retile --retile-list-file <file> --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>]
268
+ 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>]
243
269
  ```
244
270
 
245
271
  - `--retile-list-file`: File containing the list of sheets to retile. (Required)
246
272
  - `--bounds-file`: GeoJSON file containing the list of available sheets and their geographic bounds. (Required)
273
+ - `--dummy-bounds-file`: GeoJSON file containing geographic bounds of areas that need to be updated. These areas might not have any new sheets. (Optional)
247
274
  - `--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.
248
275
  - `--tiffs-dir`: Directory where the GeoTIFFs are located. (Required if not in pull-list generation mode)
249
276
  - `--tiles-dir`: Directory where the tiles will be created. (Required if not in pull-list generation mode)
@@ -190,11 +190,12 @@ The tool works in two stages. First, it calculates the full set of sheets that n
190
190
  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
191
 
192
192
  ```bash
193
- retile --retile-list-file <file> --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>]
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>]
194
194
  ```
195
195
 
196
196
  - `--retile-list-file`: File containing the list of sheets to retile. (Required)
197
197
  - `--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)
198
199
  - `--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.
199
200
  - `--tiffs-dir`: Directory where the GeoTIFFs are located. (Required if not in pull-list generation mode)
200
201
  - `--tiles-dir`: Directory where the tiles will be created. (Required if not in pull-list generation mode)
@@ -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"
@@ -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.13'
32
- __version_tuple__ = version_tuple = (0, 1, 13)
31
+ __version__ = version = '0.1.16'
32
+ __version_tuple__ = version_tuple = (0, 1, 16)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -4,6 +4,7 @@ import re
4
4
  import sys
5
5
  import json
6
6
  import argparse
7
+ import tempfile
7
8
 
8
9
  import time
9
10
  import subprocess
@@ -185,7 +186,8 @@ class Tiler:
185
186
  '-r', 'antialias',
186
187
  '--verbose',
187
188
  '-w', 'none',
188
- '--exclude',
189
+ # N.B. if transperent tiles are excluded, the old data might show through
190
+ #'--exclude',
189
191
  '--resume',
190
192
  '--xyz',
191
193
  '--processes', f'{self.pool._processes}',
@@ -205,7 +207,8 @@ class Tiler:
205
207
  'verbose': False,
206
208
  'quiet': False,
207
209
  'xyz': True,
208
- 'exclude_transparent': True,
210
+ # N.B. if transperent tiles are excluded, the old data might show through
211
+ #'exclude_transparent': True,
209
212
  'profile': 'mercator',
210
213
  'resampling': 'antialias',
211
214
  'tiledriver': tile_driver,
@@ -319,7 +322,7 @@ def check_sheets(sheets_to_pull, tiffs_dir):
319
322
  raise Exception(f'missing files {missing}')
320
323
 
321
324
 
322
- def get_sheet_data(bounds_fname):
325
+ def get_sheet_data(bounds_fname, dummy=False):
323
326
 
324
327
  index_file = Path(bounds_fname)
325
328
  if not index_file.exists():
@@ -327,9 +330,14 @@ def get_sheet_data(bounds_fname):
327
330
 
328
331
  index_data = json.loads(index_file.read_text())
329
332
 
333
+ dummy_count = 0
330
334
  sheets_to_box = {}
331
335
  for f in index_data['features']:
332
- sheet_no = f['properties']['id']
336
+ if not dummy:
337
+ sheet_no = f['properties']['id']
338
+ else:
339
+ sheet_no = f'dummy_{dummy_count}'
340
+ dummy_count += 1
333
341
  geom = shape(f['geometry'])
334
342
  xmin, ymin, xmax, ymax = geom.bounds
335
343
  box = mercantile.LngLatBbox(xmin, ymin, xmax, ymax)
@@ -354,33 +362,61 @@ def get_base_tile_sheet_mappings(sheets_to_box, base_zoom):
354
362
 
355
363
 
356
364
 
357
- def assess_sheet_requirements(retile_list_file, bounds_file, max_zoom):
365
+ def assess_sheet_requirements(retile_list_file, bounds_file, force_redo_bounds_file, max_zoom):
358
366
 
359
367
  retile_sheets = retile_list_file.read_text().split('\n')
360
368
  retile_sheets = set([ r.strip().replace('.tif', '') for r in retile_sheets if r.strip() != '' ])
361
369
 
362
370
  print('getting base tiles to sheet mapping')
363
371
  sheets_to_box = get_sheet_data(bounds_file)
372
+ force_redo_sheets_to_box = {}
373
+ if force_redo_bounds_file is not None:
374
+ force_redo_sheets_to_box = get_sheet_data(force_redo_bounds_file, dummy=True)
375
+ sheets_to_box.update(force_redo_sheets_to_box)
376
+
364
377
  sheets_to_base_tiles, base_tiles_to_sheets = get_base_tile_sheet_mappings(sheets_to_box, max_zoom)
365
378
 
366
379
  print('calculating sheets to pull')
367
380
  affected_base_tiles = set()
368
381
  for sheet_no in retile_sheets:
369
382
  affected_base_tiles.update(sheets_to_base_tiles[sheet_no])
383
+ for sheet_no in force_redo_sheets_to_box.keys():
384
+ affected_base_tiles.update(sheets_to_base_tiles[sheet_no])
370
385
 
371
386
  sheets_to_pull = set()
372
387
  for tile in affected_base_tiles:
373
388
  to_add = base_tiles_to_sheets[tile]
374
389
  for sheet in to_add:
375
- sheets_to_pull.add(sheet + '.tif')
390
+ if sheet not in force_redo_sheets_to_box:
391
+ sheets_to_pull.add(sheet + '.tif')
376
392
 
377
393
  return sheets_to_pull, affected_base_tiles
378
394
 
395
+ def create_dummy_sheets(dummy_bounds_file, tiffs_dir):
396
+ print('creating dummy sheets for force redo areas')
397
+ dummy_sheets_to_box = get_sheet_data(dummy_bounds_file, dummy=True)
398
+
399
+ for sheet_no, box in dummy_sheets_to_box.items():
400
+ file = tiffs_dir.joinpath(f'{sheet_no}.tif')
401
+ if file.exists():
402
+ continue
403
+
404
+ west, south, east, north = box.west, box.south, box.east, box.north
405
+ temp_file = Path(tempfile.mktemp(suffix='.tif'))
406
+ cmd = f'gdal_create -of GTiff -a_srs EPSG:4326 -a_ullr {west} {north} {east} {south} -a_nodata 0 -ot Byte -co COMPRESS=LZW -bands 3 -burn 0 0 0 -outsize 256 256 {temp_file}'
407
+ run_external(cmd)
408
+ temp_file_1 = Path(tempfile.mktemp(suffix='.tif'))
409
+ cmd = f'gdalwarp -dstalpha -t_srs EPSG:3857 -co COMPRESS=LZW {temp_file} {temp_file_1}'
410
+ run_external(cmd)
411
+ cmd = f'gdal_translate -co TILING_SCHEME=GoogleMapsCompatible -co COMPRESS=JPEG -co QUALITY=100 --config GDAL_TIFF_INTERNAL_MASK YES -b 1 -b 2 -b 3 -mask 4 --config GDAL_CACHEMAX 512 -of COG {temp_file_1} {file}'
412
+ run_external(cmd)
413
+
379
414
  def retile_main(args):
380
415
 
381
416
  parser = argparse.ArgumentParser()
382
417
  parser.add_argument('--retile-list-file', required=True, help='File containing list of sheets to retile')
383
418
  parser.add_argument('--bounds-file', required=True, help='Geojson file containing list of available sheets and their georaphic bounds')
419
+ 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')
384
420
  parser.add_argument('--max-zoom', type=int, help='Maximum zoom level to create tiles for, needed only when --from-source is not provided')
385
421
  parser.add_argument('--sheets-to-pull-list-outfile', default=None,
386
422
  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')
@@ -400,6 +436,12 @@ def retile_main(args):
400
436
  if not bounds_file.exists():
401
437
  parser.error(f'Bounds file {args.bounds_file} does not exist')
402
438
 
439
+ force_redo_bounds_file = None
440
+ if args.force_redo_bounds_file:
441
+ force_redo_bounds_file = Path(args.force_redo_bounds_file)
442
+ if not force_redo_bounds_file.exists():
443
+ parser.error(f'Force redo bounds file {args.force_redo_bounds_file} does not exist')
444
+
403
445
 
404
446
  if not args.sheets_to_pull_list_outfile:
405
447
  missing = []
@@ -430,7 +472,7 @@ def retile_main(args):
430
472
  print(f'--max-zoom argument is ignored when --from-source is set, using max zoom from original source: {max_zoom}')
431
473
 
432
474
  # calculate the sheets to pull and affected base tiles
433
- sheets_to_pull, affected_base_tiles = assess_sheet_requirements(retile_list_file, bounds_file, max_zoom)
475
+ sheets_to_pull, affected_base_tiles = assess_sheet_requirements(retile_list_file, bounds_file, force_redo_bounds_file, max_zoom)
434
476
 
435
477
  if args.sheets_to_pull_list_outfile is not None:
436
478
  Path(args.sheets_to_pull_list_outfile).write_text('\n'.join(sheets_to_pull) + '\n')
@@ -443,6 +485,9 @@ def retile_main(args):
443
485
  print('check the sheets availability')
444
486
  check_sheets(sheets_to_pull, tiffs_dir)
445
487
 
488
+ if force_redo_bounds_file is not None:
489
+ create_dummy_sheets(force_redo_bounds_file, tiffs_dir)
490
+
446
491
  metadata = orig_source.get_metadata()
447
492
  tile_extension = metadata['format']
448
493
  if tile_extension == 'jpg':
@@ -73,6 +73,15 @@ def cli():
73
73
  run_command(["gh", "release", "download", args.gtiffs_release, "-p", "listing_files.csv", "--clobber"])
74
74
  run_command(["gh", "release", "download", args.pmtiles_release, "-p", args.listing_files_tiled, "--clobber", "-O", "listing_files_tiled.csv"])
75
75
 
76
+ print("Download force redo bounds")
77
+ force_redo_bounds_file = Path("force_redo_bounds.geojson")
78
+ try:
79
+ run_command(["gh", "release", "download", args.pmtiles_release, "-p", f"{force_redo_bounds_file}", "--clobber"])
80
+ print("Force redo bounds file found and downloaded.")
81
+ except subprocess.CalledProcessError:
82
+ print("No force redo bounds file found; proceeding without it.")
83
+ force_redo_bounds_file = None
84
+
76
85
  # Determine sheets to retile
77
86
  print("Determining sheets to retile...")
78
87
  with open("listing_files.csv") as f:
@@ -101,6 +110,9 @@ def cli():
101
110
  print("Getting original PMTiles files...")
102
111
  run_command(["gh", "release", "download", args.pmtiles_release, "-D", str(from_pmtiles_dir), "-p", f"{args.pmtiles_prefix}*"])
103
112
 
113
+ extra_bounds_args = []
114
+ if force_redo_bounds_file:
115
+ extra_bounds_args = ["--force-redo-bounds-file", str(force_redo_bounds_file)]
104
116
  # Get list of sheets to pull
105
117
  print("Getting list of sheets to pull...")
106
118
  retile_main([
@@ -108,7 +120,7 @@ def cli():
108
120
  "--bounds-file", "bounds.geojson",
109
121
  "--sheets-to-pull-list-outfile", str(sheets_to_pull_list_outfile),
110
122
  "--from-source", f"{from_pmtiles_prefix}*.pmtiles",
111
- ])
123
+ ] + extra_bounds_args)
112
124
 
113
125
  print("Sheets to pull:")
114
126
  print(sheets_to_pull_list_outfile.read_text())
@@ -136,7 +148,7 @@ def cli():
136
148
  "--from-source", f"{from_pmtiles_prefix}*.pmtiles",
137
149
  "--tiles-dir", str(tiles_dir),
138
150
  "--tiffs-dir", str(tiffs_dir),
139
- ])
151
+ ] + extra_bounds_args)
140
152
 
141
153
  retile_list_file.unlink()
142
154
  Path("bounds.geojson").unlink()
@@ -168,6 +180,12 @@ def cli():
168
180
  run_command(["gh", "release", "upload", args.pmtiles_release, args.listing_files_tiled, "--clobber"])
169
181
  Path(args.listing_files_tiled).unlink()
170
182
 
183
+ # Handle force redo bounds file
184
+ if force_redo_bounds_file:
185
+ print("Deleting force redo bounds file...")
186
+ run_command(["gh", "release", "delete-asset", args.gtiffs_release, str(force_redo_bounds_file), '-y'])
187
+ force_redo_bounds_file.unlink()
188
+
171
189
  # Cleanup
172
190
  print("Cleaning up staging directories...")
173
191
  shutil.rmtree(tiles_dir, ignore_errors=True)