topo_map_processor 0.1.3__py3-none-any.whl
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/__init__.py +1844 -0
- topo_map_processor/_version.py +21 -0
- topo_map_processor/tools/__init__.py +0 -0
- topo_map_processor/tools/collect_bounds.py +23 -0
- topo_map_processor/tools/partition.py +361 -0
- topo_map_processor/tools/retile.py +311 -0
- topo_map_processor/tools/tile.py +86 -0
- topo_map_processor/tools/tile_sources.py +170 -0
- topo_map_processor/tools/update_bounds.py +53 -0
- topo_map_processor-0.1.3.dist-info/METADATA +187 -0
- topo_map_processor-0.1.3.dist-info/RECORD +14 -0
- topo_map_processor-0.1.3.dist-info/WHEEL +4 -0
- topo_map_processor-0.1.3.dist-info/entry_points.txt +6 -0
- topo_map_processor-0.1.3.dist-info/licenses/LICENSE +24 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.12"
|
|
3
|
+
# dependencies = [
|
|
4
|
+
# "mercantile",
|
|
5
|
+
# "pmtiles",
|
|
6
|
+
# ]
|
|
7
|
+
# ///
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import json
|
|
12
|
+
import argparse
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
import subprocess
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
import mercantile
|
|
21
|
+
from shapely.geometry import mapping
|
|
22
|
+
|
|
23
|
+
from osgeo_utils.gdal2tiles import main as gdal2tiles_main
|
|
24
|
+
from osgeo_utils.gdal2tiles import create_overview_tile, TileJobInfo, GDAL2Tiles
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
from tile_sources import PartitionedPMTilesSource, MissingTileError
|
|
28
|
+
|
|
29
|
+
WEBP_QUALITY = 75
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AttrDict(dict):
|
|
34
|
+
def __init__(self, *args, **kwargs):
|
|
35
|
+
super(AttrDict, self).__init__(*args, **kwargs)
|
|
36
|
+
self.__dict__ = self
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def run_external(cmd):
|
|
40
|
+
print(f'running cmd - {cmd}')
|
|
41
|
+
start = time.time()
|
|
42
|
+
res = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
|
43
|
+
end = time.time()
|
|
44
|
+
print(f'STDOUT: {res.stdout}')
|
|
45
|
+
print(f'STDERR: {res.stderr}')
|
|
46
|
+
print(f'command took {end - start} secs to run')
|
|
47
|
+
if res.returncode != 0:
|
|
48
|
+
raise Exception(f'command {cmd} failed')
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def convert_paths_in_vrt(vrt_file):
|
|
52
|
+
# <SourceFilename relativeToVRT="1">40M_15.tif</SourceFilename>
|
|
53
|
+
vrt_dirname = str(vrt_file.resolve().parent)
|
|
54
|
+
vrt_text = vrt_file.read_text()
|
|
55
|
+
replaced = re.sub(
|
|
56
|
+
r'<SourceFilename relativeToVRT="1">(.*)</SourceFilename>',
|
|
57
|
+
rf'<SourceFilename relativeToVRT="0">{vrt_dirname}/\1</SourceFilename>',
|
|
58
|
+
vrt_text
|
|
59
|
+
)
|
|
60
|
+
vrt_file.write_text(replaced)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def create_base_tiles(inp_file, output_dir, zoom_levels):
|
|
64
|
+
print('start tiling')
|
|
65
|
+
os.environ['GDAL_CACHEMAX'] = '2048'
|
|
66
|
+
os.environ['GDAL_MAX_DATASET_POOL_SIZE'] = '5000'
|
|
67
|
+
os.environ['GDAL_DISABLE_READDIR_ON_OPEN'] = 'TRUE'
|
|
68
|
+
#os.environ['VRT_SHARED_SOURCE'] = '1'
|
|
69
|
+
#os.environ['GTIFF_VIRTUAL_MEM_IO'] = 'TRUE'
|
|
70
|
+
gdal2tiles_main(['gdal2tiles.py',
|
|
71
|
+
'-r', 'antialias',
|
|
72
|
+
'--verbose',
|
|
73
|
+
'-w', 'none',
|
|
74
|
+
'--exclude',
|
|
75
|
+
'--resume',
|
|
76
|
+
'--xyz',
|
|
77
|
+
'--processes=8',
|
|
78
|
+
'-z', zoom_levels,
|
|
79
|
+
'--tiledriver', 'WEBP',
|
|
80
|
+
'--webp-quality', f'{WEBP_QUALITY}',
|
|
81
|
+
inp_file, output_dir])
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
pmtiles_reader = None
|
|
86
|
+
def get_pmtiles_reader(from_pmtiles_prefix):
|
|
87
|
+
global pmtiles_reader
|
|
88
|
+
if pmtiles_reader is None:
|
|
89
|
+
pmtiles_reader = PartitionedPMTilesSource(from_pmtiles_prefix)
|
|
90
|
+
return pmtiles_reader
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def pull_from_pmtiles(file, from_pmtiles_prefix):
|
|
94
|
+
reader = get_pmtiles_reader(from_pmtiles_prefix)
|
|
95
|
+
fname = str(file)
|
|
96
|
+
pieces = fname.split('/')
|
|
97
|
+
tile = mercantile.Tile(x=int(pieces[-2]),
|
|
98
|
+
y=int(pieces[-1].replace('.webp', '')),
|
|
99
|
+
z=int(pieces[-3]))
|
|
100
|
+
try:
|
|
101
|
+
t_data = reader.get_tile_data(tile)
|
|
102
|
+
except MissingTileError:
|
|
103
|
+
return
|
|
104
|
+
file.parent.mkdir(exist_ok=True, parents=True)
|
|
105
|
+
file.write_bytes(t_data)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def get_tile_file(tile, tiles_dir):
|
|
109
|
+
return f'{tiles_dir}/{tile.z}/{tile.x}/{tile.y}.webp'
|
|
110
|
+
|
|
111
|
+
def check_sheets(sheets_to_pull, tiffs_dir):
|
|
112
|
+
for sheet_no in sheets_to_pull:
|
|
113
|
+
to = tiffs_dir.joinpath(f'{sheet_no}.tif')
|
|
114
|
+
if not to.exists():
|
|
115
|
+
raise Exception(f'missing file {to}')
|
|
116
|
+
|
|
117
|
+
def copy_tiles_over(tiles_to_pull, tiles_dir, from_pmtiles_prefix):
|
|
118
|
+
for tile in tiles_to_pull:
|
|
119
|
+
to = Path(get_tile_file(tile, tiles_dir))
|
|
120
|
+
if to.exists():
|
|
121
|
+
continue
|
|
122
|
+
pull_from_pmtiles(to, from_pmtiles_prefix)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def create_upper_tiles(tiles_to_create, tiles_dir):
|
|
126
|
+
options = AttrDict({
|
|
127
|
+
'resume': True,
|
|
128
|
+
'verbose': False,
|
|
129
|
+
'quiet': False,
|
|
130
|
+
'xyz': True,
|
|
131
|
+
'exclude_transparent': True,
|
|
132
|
+
'profile': 'mercator',
|
|
133
|
+
'resampling': 'antialias',
|
|
134
|
+
'tiledriver': 'WEBP',
|
|
135
|
+
'webp_quality': WEBP_QUALITY,
|
|
136
|
+
'webp_lossless': False
|
|
137
|
+
})
|
|
138
|
+
tile_job_info = TileJobInfo(
|
|
139
|
+
tile_driver='WEBP',
|
|
140
|
+
nb_data_bands=3,
|
|
141
|
+
tile_size=256,
|
|
142
|
+
tile_extension='webp',
|
|
143
|
+
kml=False
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
for tile in tiles_to_create:
|
|
147
|
+
ctiles = mercantile.children(tile)
|
|
148
|
+
tiles_dir.joinpath(str(tile.z), str(tile.x)).mkdir(parents=True, exist_ok=True)
|
|
149
|
+
base_tile_group = [ (t.x, GDAL2Tiles.getYTile(t.y, t.z, options)) for t in ctiles ]
|
|
150
|
+
#print(f'{tile=}, {base_tile_group=}')
|
|
151
|
+
create_overview_tile(z + 1, output_folder=str(tiles_dir), tile_job_info=tile_job_info, options=options, base_tiles=base_tile_group)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_sheet_data(bounds_fname):
|
|
155
|
+
|
|
156
|
+
index_file = Path(bounds_fname)
|
|
157
|
+
if not index_file.exists():
|
|
158
|
+
raise Exception(F'missing index file at {bounds_fname}')
|
|
159
|
+
|
|
160
|
+
index_data = json.loads(index_file.read_text())
|
|
161
|
+
|
|
162
|
+
sheets_to_box = {}
|
|
163
|
+
for f in index_data['features']:
|
|
164
|
+
sheet_no = f['properties']['id']
|
|
165
|
+
geom = mapping(f['geometry'])
|
|
166
|
+
xmin, ymin, xmax, ymax = geom.bounds
|
|
167
|
+
box = mercantile.LngLatBbox(xmin, ymin, xmax, ymax)
|
|
168
|
+
sheets_to_box[sheet_no] = box
|
|
169
|
+
|
|
170
|
+
return sheets_to_box
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def get_base_tile_sheet_mappings(sheets_to_box, base_zoom):
|
|
174
|
+
sheets_to_base_tiles = {}
|
|
175
|
+
base_tiles_to_sheets = {}
|
|
176
|
+
|
|
177
|
+
for sheet_no, box in sheets_to_box.items():
|
|
178
|
+
tiles = set(mercantile.tiles(box.west, box.south, box.east, box.north, [base_zoom]))
|
|
179
|
+
sheets_to_base_tiles[sheet_no] = tiles
|
|
180
|
+
for tile in tiles:
|
|
181
|
+
if tile not in base_tiles_to_sheets:
|
|
182
|
+
base_tiles_to_sheets[tile] = set()
|
|
183
|
+
base_tiles_to_sheets[tile].add(sheet_no)
|
|
184
|
+
|
|
185
|
+
return sheets_to_base_tiles, base_tiles_to_sheets
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def delete_unwanted_tiles(tiles_to_keep, z, tiles_dir):
|
|
189
|
+
delete_count = 0
|
|
190
|
+
for file in tiles_dir.glob(f'{z}/*/*.webp'):
|
|
191
|
+
y = int(file.name[:-5])
|
|
192
|
+
x = int(file.parent.name)
|
|
193
|
+
|
|
194
|
+
disk_tile = mercantile.Tile(z=z, x=x, y=y)
|
|
195
|
+
if disk_tile not in tiles_to_keep:
|
|
196
|
+
#print(f'deleting {disk_tile}')
|
|
197
|
+
file.unlink()
|
|
198
|
+
delete_count += 1
|
|
199
|
+
print(f'deleted {delete_count} files at level {z}')
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def create_vrt_file(sheets, tiffs_dir):
|
|
203
|
+
vrt_file = Path(f'{tiffs_dir}/combined.vrt')
|
|
204
|
+
if vrt_file.exists():
|
|
205
|
+
return vrt_file
|
|
206
|
+
tiff_list = [ tiffs_dir.joinpath(f'{p_sheet}.tif').resolve() for p_sheet in sheets ]
|
|
207
|
+
tiff_list = [ str(f) for f in tiff_list if f.exists() ]
|
|
208
|
+
|
|
209
|
+
tiff_list_str = ' '.join(tiff_list)
|
|
210
|
+
|
|
211
|
+
run_external(f'gdalbuildvrt {vrt_file} {tiff_list_str}')
|
|
212
|
+
convert_paths_in_vrt(vrt_file)
|
|
213
|
+
|
|
214
|
+
return vrt_file
|
|
215
|
+
|
|
216
|
+
def cli():
|
|
217
|
+
parser = argparse.ArgumentParser()
|
|
218
|
+
parser.add_argument('--retile-list-file', required=True, help='File containing list of sheets to retile')
|
|
219
|
+
parser.add_argument('--bounds-file', required=True, help='Geojson file containing list of available sheets and their georaphic bounds')
|
|
220
|
+
parser.add_argument('--min-zoom', default=0, type=int)
|
|
221
|
+
parser.add_argument('--max-zoom', required=True, type=int)
|
|
222
|
+
parser.add_argument('--sheets-to-pull-list-outfile', default=None,
|
|
223
|
+
help='Output into which we write the list of sheet that need to be pulled, if set the script ends after created the list file')
|
|
224
|
+
parser.add_argument('--from-pmtiles-prefix')
|
|
225
|
+
parser.add_argument('--tiles-dir', required=True)
|
|
226
|
+
parser.add_argument('--tiffs-dir', required=True)
|
|
227
|
+
|
|
228
|
+
args = parser.parse_args()
|
|
229
|
+
if not args.sheets_to_pull_list_outfile:
|
|
230
|
+
missing = []
|
|
231
|
+
if not args.from_pmtiles_prefix:
|
|
232
|
+
missing.append('--from-pmtiles-prefix')
|
|
233
|
+
|
|
234
|
+
if missing:
|
|
235
|
+
parser.error(f"The following arguments are required when --sheets-to-pull-list-outfile is not provided: {', '.join(missing)}")
|
|
236
|
+
|
|
237
|
+
retile_sheets = Path(args.retile_list_file).read_text().split('\n')
|
|
238
|
+
retile_sheets = set([ r.strip() for r in retile_sheets if r.strip() != '' ])
|
|
239
|
+
|
|
240
|
+
print('getting base tiles to sheet mapping')
|
|
241
|
+
sheets_to_box = get_sheet_data(args.bounds_file)
|
|
242
|
+
sheets_to_base_tiles, base_tiles_to_sheets = get_base_tile_sheet_mappings(sheets_to_box, args.max_zoom)
|
|
243
|
+
|
|
244
|
+
all_affected_tiles = set()
|
|
245
|
+
|
|
246
|
+
print('calculating sheets to pull')
|
|
247
|
+
affected_base_tiles = set()
|
|
248
|
+
for sheet_no in retile_sheets:
|
|
249
|
+
affected_base_tiles.update(sheets_to_base_tiles[sheet_no])
|
|
250
|
+
all_affected_tiles.update(affected_base_tiles)
|
|
251
|
+
|
|
252
|
+
sheets_to_pull = set()
|
|
253
|
+
for tile in affected_base_tiles:
|
|
254
|
+
to_add = base_tiles_to_sheets[tile]
|
|
255
|
+
for sheet in to_add:
|
|
256
|
+
sheets_to_pull.add(sheet)
|
|
257
|
+
|
|
258
|
+
Path(args.tiffs_dir).mkdir(exist_ok=True, parents=True)
|
|
259
|
+
Path(args.tiles_dir).mkdir(exist_ok=True, parents=True)
|
|
260
|
+
print(f'{sheets_to_pull=}')
|
|
261
|
+
if args.sheets_to_pull_list_outfile is not None:
|
|
262
|
+
Path(args.sheets_to_pull_list_outfile).write_text('\n'.join(sheets_to_pull) + '\n')
|
|
263
|
+
exit(0)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
print('check the sheets availability')
|
|
267
|
+
check_sheets(sheets_to_pull, args.tiffs_dir)
|
|
268
|
+
|
|
269
|
+
print('creating vrt file from sheets involved')
|
|
270
|
+
vrt_file = create_vrt_file(sheets_to_pull, args.tiffs_dir)
|
|
271
|
+
|
|
272
|
+
print('creating tiles for base zoom with a vrt')
|
|
273
|
+
create_base_tiles(f'{vrt_file}', str(args.tiles_dir), f'{args.max_zoom}')
|
|
274
|
+
|
|
275
|
+
print('deleting unwanted base tiles')
|
|
276
|
+
delete_unwanted_tiles(affected_base_tiles, args.max_zoom, args.tiles_dir)
|
|
277
|
+
|
|
278
|
+
prev_affected_tiles = affected_base_tiles
|
|
279
|
+
for z in range(args.max_zoom-1, args.min_zoom-1, -1):
|
|
280
|
+
print(f'handling level {z}')
|
|
281
|
+
|
|
282
|
+
curr_affected_tiles = set()
|
|
283
|
+
for tile in prev_affected_tiles:
|
|
284
|
+
curr_affected_tiles.add(mercantile.parent(tile))
|
|
285
|
+
all_affected_tiles.update(curr_affected_tiles)
|
|
286
|
+
|
|
287
|
+
child_tiles_to_pull = set()
|
|
288
|
+
for ptile in curr_affected_tiles:
|
|
289
|
+
for ctile in mercantile.children(ptile):
|
|
290
|
+
if ctile not in prev_affected_tiles:
|
|
291
|
+
child_tiles_to_pull.add(ctile)
|
|
292
|
+
|
|
293
|
+
print('copying additional child tiles required for curr level')
|
|
294
|
+
copy_tiles_over(child_tiles_to_pull, args.tiles_dir, args.from_pmtiles_prefix)
|
|
295
|
+
|
|
296
|
+
print('creating tiles for current level')
|
|
297
|
+
create_upper_tiles(curr_affected_tiles, args.tiles_dir)
|
|
298
|
+
|
|
299
|
+
print('removing unwanted child tiles')
|
|
300
|
+
delete_unwanted_tiles(prev_affected_tiles, z+1, args.tiles_dir)
|
|
301
|
+
|
|
302
|
+
prev_affected_tiles = curr_affected_tiles
|
|
303
|
+
|
|
304
|
+
print('All Done!!!')
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
if __name__ == '__main__':
|
|
308
|
+
cli()
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import os
|
|
3
|
+
import glob
|
|
4
|
+
import argparse
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from osgeo_utils.gdal2tiles import main as gdal2tiles_main
|
|
12
|
+
|
|
13
|
+
def run_external(cmd):
|
|
14
|
+
print(f'running cmd - {cmd}')
|
|
15
|
+
start = time.time()
|
|
16
|
+
res = subprocess.run(cmd, shell=True, capture_output=True, text=True)
|
|
17
|
+
end = time.time()
|
|
18
|
+
print(f'STDOUT: {res.stdout}')
|
|
19
|
+
print(f'STDERR: {res.stderr}')
|
|
20
|
+
print(f'command took {end - start} secs to run')
|
|
21
|
+
if res.returncode != 0:
|
|
22
|
+
raise Exception(f'command {cmd} failed')
|
|
23
|
+
|
|
24
|
+
def convert_paths_in_vrt(vrt_file):
|
|
25
|
+
vrt_dirname = str(vrt_file.resolve().parent)
|
|
26
|
+
vrt_text = vrt_file.read_text()
|
|
27
|
+
replaced = re.sub(
|
|
28
|
+
r'<SourceFilename relativeToVRT="1">(.*)</SourceFilename>',
|
|
29
|
+
rf'<SourceFilename relativeToVRT="0">{vrt_dirname}/\1</SourceFilename>',
|
|
30
|
+
vrt_text
|
|
31
|
+
)
|
|
32
|
+
vrt_file.write_text(replaced)
|
|
33
|
+
|
|
34
|
+
def cli():
|
|
35
|
+
parser = argparse.ArgumentParser(description='Tile the GTiffs')
|
|
36
|
+
parser.add_argument('tiles-dir', required=True, help='Directory to store the tiles')
|
|
37
|
+
parser.add_argument('tiffs-dir', required=True, help='Directory with GTiffs to tile')
|
|
38
|
+
parser.add_argument('max-zoom', type=int, required=True, help='Maximum zoom level for tiling')
|
|
39
|
+
parser.add_argument('min-zoom', type=int, default=0, help='Minimum zoom level for tiling')
|
|
40
|
+
|
|
41
|
+
args = parser.parse_args()
|
|
42
|
+
|
|
43
|
+
tiles_dir = Path(args.tiles_dir)
|
|
44
|
+
tiles_dir.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
file_names = list(glob.glob(f'{args.tiffs_dir}/*.tif'))
|
|
48
|
+
file_names = [ str(Path(f).resolve()) for f in file_names ]
|
|
49
|
+
print(f'total files: {len(file_names)}')
|
|
50
|
+
|
|
51
|
+
file_list_file = Path('files_to_tile.txt')
|
|
52
|
+
file_list_file.write_text('\n'.join(file_names))
|
|
53
|
+
|
|
54
|
+
vrt_file = Path('files_to_tile.vrt')
|
|
55
|
+
|
|
56
|
+
# create vrt file
|
|
57
|
+
run_external(f'gdalbuildvrt -input_file_list {str(file_list_file)} {str(vrt_file)}')
|
|
58
|
+
convert_paths_in_vrt(vrt_file)
|
|
59
|
+
file_list_file.unlink()
|
|
60
|
+
|
|
61
|
+
print('start tiling')
|
|
62
|
+
os.environ['GDAL_CACHEMAX'] = '2048'
|
|
63
|
+
os.environ['GDAL_MAX_DATASET_POOL_SIZE'] = '5000'
|
|
64
|
+
os.environ['GDAL_DISABLE_READDIR_ON_OPEN'] = 'TRUE'
|
|
65
|
+
#os.environ['VRT_SHARED_SOURCE'] = '1'
|
|
66
|
+
#os.environ['GTIFF_VIRTUAL_MEM_IO'] = 'TRUE'
|
|
67
|
+
gdal2tiles_main(['gdal2tiles.py',
|
|
68
|
+
'-r', 'antialias',
|
|
69
|
+
'--verbose',
|
|
70
|
+
'--exclude',
|
|
71
|
+
'--resume',
|
|
72
|
+
'--xyz',
|
|
73
|
+
'--processes=8',
|
|
74
|
+
'-z', f'{args.min_zoom}-{args.max_zoom}',
|
|
75
|
+
'--tiledriver', 'WEBP',
|
|
76
|
+
'--webp-quality', '75',
|
|
77
|
+
str(vrt_file), str(tiles_dir)])
|
|
78
|
+
|
|
79
|
+
vrt_file.unlink()
|
|
80
|
+
|
|
81
|
+
print('All Done!!')
|
|
82
|
+
|
|
83
|
+
if __name__ == '__main__':
|
|
84
|
+
cli()
|
|
85
|
+
|
|
86
|
+
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import glob
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import mercantile
|
|
8
|
+
|
|
9
|
+
from pmtiles.reader import MmapSource, Reader as PMTilesReader, all_tiles
|
|
10
|
+
|
|
11
|
+
class MissingTileError(Exception):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DiskSource:
|
|
16
|
+
def __init__(self, directory):
|
|
17
|
+
self.dir = directory
|
|
18
|
+
|
|
19
|
+
def get_tile_from_file(self, fname):
|
|
20
|
+
parts = fname.split('/')
|
|
21
|
+
tile = mercantile.Tile(z=int(parts[-3]),
|
|
22
|
+
x=int(parts[-2]),
|
|
23
|
+
y=int(parts[-1].replace('.webp', '')))
|
|
24
|
+
return tile
|
|
25
|
+
|
|
26
|
+
def file_from_tile(self, tile):
|
|
27
|
+
return f'{self.dir}/{tile.z}/{tile.x}/{tile.y}.webp'
|
|
28
|
+
|
|
29
|
+
def get_tile_data(self, tile):
|
|
30
|
+
fname = self.file_from_tile(tile)
|
|
31
|
+
try:
|
|
32
|
+
os.stat(fname)
|
|
33
|
+
except FileNotFoundError:
|
|
34
|
+
raise MissingTileError()
|
|
35
|
+
|
|
36
|
+
with open(fname, 'rb') as f:
|
|
37
|
+
return f.read()
|
|
38
|
+
|
|
39
|
+
def get_tile_size(self, tile):
|
|
40
|
+
fname = self.file_from_tile(tile)
|
|
41
|
+
try:
|
|
42
|
+
fstats = os.stat(fname)
|
|
43
|
+
except FileNotFoundError:
|
|
44
|
+
raise MissingTileError()
|
|
45
|
+
return fstats.st_size
|
|
46
|
+
|
|
47
|
+
def for_all_z(self, z):
|
|
48
|
+
fnames = glob.glob(f'{self.dir}/{z}/*/*.webp')
|
|
49
|
+
for fname in fnames:
|
|
50
|
+
tile = self.get_tile_from_file(fname)
|
|
51
|
+
fstats = os.stat(fname)
|
|
52
|
+
yield (tile, fstats.st_size)
|
|
53
|
+
|
|
54
|
+
def all(self):
|
|
55
|
+
fnames = glob.glob(f'{self.dir}/*/*/*.webp')
|
|
56
|
+
for fname in fnames:
|
|
57
|
+
tile = self.get_tile_from_file(fname)
|
|
58
|
+
t_data = Path(fname).read_bytes()
|
|
59
|
+
yield (tile, t_data)
|
|
60
|
+
|
|
61
|
+
def cleanup(self):
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
class PartitionedPMTilesSource:
|
|
65
|
+
def __init__(self, pmtiles_prefix):
|
|
66
|
+
pmtiles_files = glob.glob(f'{pmtiles_prefix}*.pmtiles')
|
|
67
|
+
self.files = []
|
|
68
|
+
self.readers = {}
|
|
69
|
+
for fname in pmtiles_files:
|
|
70
|
+
file = open(fname, 'rb')
|
|
71
|
+
self.files.append(file)
|
|
72
|
+
src = MmapSource(open(fname, 'rb'))
|
|
73
|
+
reader = PMTilesReader(src)
|
|
74
|
+
self.readers[fname] = reader
|
|
75
|
+
|
|
76
|
+
self.fnames = list(pmtiles_files)
|
|
77
|
+
fname_to_index = { fname:i for i,fname in enumerate(self.fnames) }
|
|
78
|
+
self.tiles_to_suffix = {}
|
|
79
|
+
self.tiles_to_size = {}
|
|
80
|
+
for fname in self.fnames:
|
|
81
|
+
print(f'collecting tile sizes from {fname}')
|
|
82
|
+
reader = self.readers[fname]
|
|
83
|
+
for t, t_data in all_tiles(reader.get_bytes):
|
|
84
|
+
tile = mercantile.Tile(x=t[1], y=t[2], z=t[0])
|
|
85
|
+
self.tiles_to_findex[tile] = fname_to_index[fname]
|
|
86
|
+
self.tiles_to_size[tile] = len(t_data)
|
|
87
|
+
|
|
88
|
+
def get_reader_from_tile(self, tile):
|
|
89
|
+
if tile not in self.tiles_to_suffix:
|
|
90
|
+
raise MissingTileError()
|
|
91
|
+
s = self.tiles_to_findex[tile]
|
|
92
|
+
fname = self.fnames[s]
|
|
93
|
+
reader = self.readers[fname]
|
|
94
|
+
return reader
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_tile_data(self, tile):
|
|
98
|
+
reader = self.get_reader_from_tile(tile)
|
|
99
|
+
data = reader.get(tile.z, tile.x, tile.y)
|
|
100
|
+
if data is None:
|
|
101
|
+
raise MissingTileError()
|
|
102
|
+
return data
|
|
103
|
+
|
|
104
|
+
def get_tile_size(self, tile):
|
|
105
|
+
if tile not in self.tiles_to_size:
|
|
106
|
+
raise MissingTileError()
|
|
107
|
+
return self.tiles_to_size[tile]
|
|
108
|
+
|
|
109
|
+
def for_all_z(self, z):
|
|
110
|
+
for tile, size in self.tiles_to_size.items():
|
|
111
|
+
if tile.z == z:
|
|
112
|
+
yield (tile, size)
|
|
113
|
+
|
|
114
|
+
def all(self):
|
|
115
|
+
for suffix, reader in self.readers.items():
|
|
116
|
+
print(f'yielding from {suffix}')
|
|
117
|
+
for t, t_data in all_tiles(reader.get_bytes):
|
|
118
|
+
tile = mercantile.Tile(x=t[1], y=t[2], z=t[0])
|
|
119
|
+
yield tile, t_data
|
|
120
|
+
|
|
121
|
+
def cleanup(self):
|
|
122
|
+
for f in self.files:
|
|
123
|
+
f.close()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class DiskAndPartitionedPMTilesSource:
|
|
128
|
+
def __init__(self, directory, pmtiles_prefix):
|
|
129
|
+
self.dsrc = DiskSource(directory)
|
|
130
|
+
self.psrc = PartitionedPMTilesSource(pmtiles_prefix)
|
|
131
|
+
|
|
132
|
+
def get_tile_data(self, tile):
|
|
133
|
+
try:
|
|
134
|
+
return self.dsrc.get_tile_data(tile)
|
|
135
|
+
except MissingTileError:
|
|
136
|
+
return self.psrc.get_tile_data(tile)
|
|
137
|
+
|
|
138
|
+
def get_tile_size(self, tile):
|
|
139
|
+
try:
|
|
140
|
+
return self.dsrc.get_tile_size(tile)
|
|
141
|
+
except MissingTileError:
|
|
142
|
+
return self.psrc.get_tile_size(tile)
|
|
143
|
+
|
|
144
|
+
def for_all_z(self, z):
|
|
145
|
+
print(f'iterating over all {z}')
|
|
146
|
+
seen = set()
|
|
147
|
+
for (tile, size) in self.dsrc.for_all_z(z):
|
|
148
|
+
seen.add(tile)
|
|
149
|
+
yield (tile, size)
|
|
150
|
+
|
|
151
|
+
print(f'iterated over all {z} disk')
|
|
152
|
+
for (tile, size) in self.psrc.for_all_z(z):
|
|
153
|
+
if tile in seen:
|
|
154
|
+
continue
|
|
155
|
+
yield (tile, size)
|
|
156
|
+
print(f'iterated over all {z} pmtiles')
|
|
157
|
+
|
|
158
|
+
def all(self):
|
|
159
|
+
print('yielding from disk')
|
|
160
|
+
for res in self.dsrc.all():
|
|
161
|
+
yield res
|
|
162
|
+
print('yielding from pmtiles files')
|
|
163
|
+
for res in self.psrc.all():
|
|
164
|
+
yield res
|
|
165
|
+
|
|
166
|
+
def cleanup(self):
|
|
167
|
+
self.dsrc.cleanup()
|
|
168
|
+
self.psrc.cleanup()
|
|
169
|
+
|
|
170
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
|
|
2
|
+
import json
|
|
3
|
+
import argparse
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
def cli():
|
|
7
|
+
parser = argparse.ArgumentParser(description="Update bounds in a GeoJSON file.")
|
|
8
|
+
parser.add_argument("bounds-file", help="Input GeoJSON file with bounds.")
|
|
9
|
+
parser.add_argument("bounds-dir", help="Directory containing the individual bounds files")
|
|
10
|
+
parser.add_argument("retile-list-file", help="File containing the list of sheets to update.")
|
|
11
|
+
|
|
12
|
+
args = parser.parse_args()
|
|
13
|
+
|
|
14
|
+
bounds_file = Path(args.bounds_file)
|
|
15
|
+
bounds_dir = Path(args.bounds_dir)
|
|
16
|
+
retile_list_file = Path(args.retile_list_file)
|
|
17
|
+
|
|
18
|
+
existing_map = {}
|
|
19
|
+
if not bounds_file.exists():
|
|
20
|
+
raise FileNotFoundError(f"Bounds file does not exist: {bounds_file}")
|
|
21
|
+
|
|
22
|
+
# Read the existing bounds file
|
|
23
|
+
existing_data = json.loads(bounds_file.read_text())
|
|
24
|
+
for feature in existing_data.get('features', []):
|
|
25
|
+
if 'properties' in feature and 'id' in feature['properties']:
|
|
26
|
+
sheet_name = feature['properties']['id']
|
|
27
|
+
existing_map[sheet_name] = feature
|
|
28
|
+
|
|
29
|
+
sheets = retile_list_file.read_text().strip().splitlines()
|
|
30
|
+
sheets = [s.strip() for s in sheets if s.strip()]
|
|
31
|
+
for s in sheets:
|
|
32
|
+
bfile = bounds_dir / f'{s}.geojsonl'
|
|
33
|
+
if not bfile.exists():
|
|
34
|
+
raise FileNotFoundError(f"Bounds file for sheet '{s}' does not exist: {bfile}")
|
|
35
|
+
|
|
36
|
+
feature = json.loads(bfile.read_text())
|
|
37
|
+
existing_data[s] = feature
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
all_feats = list(existing_data.values())
|
|
41
|
+
|
|
42
|
+
with open(bounds_file, 'w') as f:
|
|
43
|
+
f.write('{ "type": "FeatureCollection", "features": [\n')
|
|
44
|
+
for i,feat in enumerate(all_feats):
|
|
45
|
+
line = json.dumps(feat)
|
|
46
|
+
if i != len(all_feats) - 1:
|
|
47
|
+
line += ','
|
|
48
|
+
line += '\n'
|
|
49
|
+
f.write(line)
|
|
50
|
+
f.write(']}\n')
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
cli()
|