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,21 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
|
5
|
+
|
|
6
|
+
TYPE_CHECKING = False
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
12
|
+
else:
|
|
13
|
+
VERSION_TUPLE = object
|
|
14
|
+
|
|
15
|
+
version: str
|
|
16
|
+
__version__: str
|
|
17
|
+
__version_tuple__: VERSION_TUPLE
|
|
18
|
+
version_tuple: VERSION_TUPLE
|
|
19
|
+
|
|
20
|
+
__version__ = version = '0.1.3'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 1, 3)
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
def cli():
|
|
6
|
+
bounds_dir = Path(sys.argv[1])
|
|
7
|
+
output_file = Path(sys.argv[2])
|
|
8
|
+
|
|
9
|
+
paths = list(bounds_dir.glob('*.geojsonl'))
|
|
10
|
+
with open(output_file, 'w') as f:
|
|
11
|
+
f.write('{ "type": "FeatureCollection", "features": [\n')
|
|
12
|
+
for i,p in enumerate(paths):
|
|
13
|
+
line = p.read_text().strip('\n')
|
|
14
|
+
|
|
15
|
+
if i != len(paths) - 1:
|
|
16
|
+
line += ','
|
|
17
|
+
line += '\n'
|
|
18
|
+
f.write(line)
|
|
19
|
+
f.write(']}\n')
|
|
20
|
+
|
|
21
|
+
if __name__ == "__main__":
|
|
22
|
+
cli()
|
|
23
|
+
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# /// script
|
|
2
|
+
# requires-python = ">=3.12"
|
|
3
|
+
# dependencies = [
|
|
4
|
+
# "mercantile",
|
|
5
|
+
# "pmtiles",
|
|
6
|
+
# ]
|
|
7
|
+
# ///
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import copy
|
|
11
|
+
import argparse
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import mercantile
|
|
15
|
+
from pprint import pprint
|
|
16
|
+
from pmtiles.tile import zxy_to_tileid, TileType, Compression
|
|
17
|
+
from pmtiles.writer import Writer as PMTilesWriter
|
|
18
|
+
|
|
19
|
+
from tile_sources import (
|
|
20
|
+
DiskSource,
|
|
21
|
+
DiskAndPartitionedPMTilesSource,
|
|
22
|
+
MissingTileError
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# account for some directory overhead
|
|
26
|
+
# TODO: this should be dependant on the file size
|
|
27
|
+
DELTA = 5 * 1024 * 1024
|
|
28
|
+
# github release limit
|
|
29
|
+
SIZE_LIMIT_BYTES = (2 * 1024 * 1024 * 1024) - DELTA
|
|
30
|
+
# github git file size limit
|
|
31
|
+
#SIZE_LIMIT_BYTES = (100 * 1024 * 1024) - DELTA
|
|
32
|
+
# cloudflare cache size limit
|
|
33
|
+
#SIZE_LIMIT_BYTES = (512 * 1024 * 1024) - DELTA
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def get_layer_info(level, reader):
|
|
37
|
+
tiles = {}
|
|
38
|
+
total_size = 0
|
|
39
|
+
for tile, size in reader.for_all_z(level):
|
|
40
|
+
tiles[tile] = size
|
|
41
|
+
total_size += size
|
|
42
|
+
return total_size, tiles
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_buckets(sizes, tiles):
|
|
46
|
+
buckets = []
|
|
47
|
+
bucket_tiles = []
|
|
48
|
+
|
|
49
|
+
max_x = max(sizes.keys())
|
|
50
|
+
min_x = min(sizes.keys())
|
|
51
|
+
|
|
52
|
+
cb = (min_x, min_x)
|
|
53
|
+
cs = 0
|
|
54
|
+
bts = {}
|
|
55
|
+
for i in range(min_x, max_x + 1):
|
|
56
|
+
if i not in sizes:
|
|
57
|
+
continue
|
|
58
|
+
if cs > SIZE_LIMIT_BYTES:
|
|
59
|
+
buckets.append(cb)
|
|
60
|
+
bucket_tiles.append(bts)
|
|
61
|
+
cb = (i,i)
|
|
62
|
+
cs = 0
|
|
63
|
+
bts = {}
|
|
64
|
+
cs += sizes[i]
|
|
65
|
+
bts.update(tiles[i])
|
|
66
|
+
cb = (cb[0], i)
|
|
67
|
+
buckets.append(cb)
|
|
68
|
+
bucket_tiles.append(bts)
|
|
69
|
+
return buckets, bucket_tiles
|
|
70
|
+
|
|
71
|
+
missing_tiles = set()
|
|
72
|
+
|
|
73
|
+
# TODO: can do better than just vertical slices
|
|
74
|
+
def get_stripes(level, reader, max_zoom_level):
|
|
75
|
+
tiles = {}
|
|
76
|
+
sizes = {}
|
|
77
|
+
#max_x = min_x = max_y = min_y = None
|
|
78
|
+
#missed_count = 0
|
|
79
|
+
print(f'striping from level {level}')
|
|
80
|
+
count = 0
|
|
81
|
+
for tile, size in reader.for_all_z(level):
|
|
82
|
+
count += 1
|
|
83
|
+
#if count % 1000 == 0:
|
|
84
|
+
# print(f'handled {count} tiles')
|
|
85
|
+
to_add = [(tile, size)]
|
|
86
|
+
for clevel in range(level + 1, max_zoom_level + 1):
|
|
87
|
+
children = mercantile.children(tile, zoom=clevel)
|
|
88
|
+
for ctile in children:
|
|
89
|
+
try:
|
|
90
|
+
csize = reader.get_tile_size(ctile)
|
|
91
|
+
to_add.append((ctile, csize))
|
|
92
|
+
except MissingTileError:
|
|
93
|
+
missing_tiles.add(ctile)
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
for t, s in to_add:
|
|
97
|
+
if tile.x not in tiles:
|
|
98
|
+
tiles[tile.x] = {}
|
|
99
|
+
tiles[tile.x][t] = s
|
|
100
|
+
if tile.x not in sizes:
|
|
101
|
+
sizes[tile.x] = 0
|
|
102
|
+
sizes[tile.x] += s
|
|
103
|
+
print(f'{len(missing_tiles)=}')
|
|
104
|
+
return sizes, tiles
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_top_slice(reader, min_zoom_level, max_zoom_level):
|
|
108
|
+
print('getting top slice')
|
|
109
|
+
size_till_now = 0
|
|
110
|
+
tiles = {}
|
|
111
|
+
for level in range(min_zoom_level, max_zoom_level + 1):
|
|
112
|
+
lsize, ltiles = get_layer_info(level, reader)
|
|
113
|
+
size_till_now += lsize
|
|
114
|
+
print(f'{level=}, {lsize=}, {size_till_now=}, {SIZE_LIMIT_BYTES=}')
|
|
115
|
+
tiles.update(ltiles)
|
|
116
|
+
if size_till_now > SIZE_LIMIT_BYTES:
|
|
117
|
+
return level - 1, tiles
|
|
118
|
+
return max_zoom_level, tiles
|
|
119
|
+
|
|
120
|
+
def save_partition_info(inp_p_info, partition_file):
|
|
121
|
+
p_info = copy.deepcopy(inp_p_info)
|
|
122
|
+
for p_name, p_data in p_info.items():
|
|
123
|
+
tiles_new = { f'{t.z},{t.x},{t.y}':size for t, size in p_data['tiles'].items() }
|
|
124
|
+
p_data['tiles'] = tiles_new
|
|
125
|
+
|
|
126
|
+
partition_file.parent.mkdir(exist_ok=True, parents=True)
|
|
127
|
+
partition_file.write_text(json.dumps(p_info))
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def get_bounds(tiles):
|
|
131
|
+
|
|
132
|
+
bounds = [ mercantile.bounds(t) for t in tiles ]
|
|
133
|
+
|
|
134
|
+
max_x = bounds[0].east
|
|
135
|
+
min_x = bounds[0].west
|
|
136
|
+
max_y = bounds[0].north
|
|
137
|
+
min_y = bounds[0].south
|
|
138
|
+
for b in bounds:
|
|
139
|
+
if b.east > max_x:
|
|
140
|
+
max_x = b.east
|
|
141
|
+
|
|
142
|
+
if b.west < min_x:
|
|
143
|
+
min_x = b.west
|
|
144
|
+
|
|
145
|
+
if b.north > max_y:
|
|
146
|
+
max_y = b.north
|
|
147
|
+
|
|
148
|
+
if b.south < min_y:
|
|
149
|
+
min_y = b.south
|
|
150
|
+
|
|
151
|
+
return (min_y, min_x, max_y, max_x)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_partition_info(reader, min_zoom_level, max_zoom_level, to_partition_file):
|
|
155
|
+
if to_partition_file.exists():
|
|
156
|
+
partition_info = json.loads(to_partition_file.read_text())
|
|
157
|
+
for suffix, data in partition_info.items():
|
|
158
|
+
tdata = {}
|
|
159
|
+
for k, size in data['tiles'].items():
|
|
160
|
+
kps = k.split(',')
|
|
161
|
+
tile = mercantile.Tile(
|
|
162
|
+
x=int(kps[1]),
|
|
163
|
+
y=int(kps[2]),
|
|
164
|
+
z=int(kps[0]),
|
|
165
|
+
)
|
|
166
|
+
tdata[tile] = size
|
|
167
|
+
data['tiles'] = tdata
|
|
168
|
+
return partition_info
|
|
169
|
+
|
|
170
|
+
partition_info = {}
|
|
171
|
+
top_slice_max_level, top_slice_tiles = get_top_slice(reader, min_zoom_level, max_zoom_level)
|
|
172
|
+
|
|
173
|
+
top_slice_bounds = get_bounds(top_slice_tiles.keys())
|
|
174
|
+
|
|
175
|
+
if top_slice_max_level == min_zoom_level:
|
|
176
|
+
partition_name = f'z{min_zoom_level}'
|
|
177
|
+
else:
|
|
178
|
+
partition_name = f'z{min_zoom_level}-{top_slice_max_level}'
|
|
179
|
+
|
|
180
|
+
if top_slice_max_level == max_zoom_level:
|
|
181
|
+
partition_name = ''
|
|
182
|
+
|
|
183
|
+
partition_info[partition_name] = {
|
|
184
|
+
"tiles": top_slice_tiles,
|
|
185
|
+
"min_zoom": min_zoom_level,
|
|
186
|
+
"max_zoom": top_slice_max_level,
|
|
187
|
+
"bounds": top_slice_bounds
|
|
188
|
+
}
|
|
189
|
+
if top_slice_max_level == max_zoom_level:
|
|
190
|
+
print('no more slicing required')
|
|
191
|
+
save_partition_info(partition_info, to_partition_file)
|
|
192
|
+
return partition_info
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
from_level = top_slice_max_level + 1
|
|
196
|
+
stripe_sizes, stripe_tiles = get_stripes(from_level, reader, max_zoom_level)
|
|
197
|
+
buckets, bucket_tiles = get_buckets(stripe_sizes, stripe_tiles)
|
|
198
|
+
|
|
199
|
+
num_buckets = len(buckets)
|
|
200
|
+
for i,bucket in enumerate(buckets):
|
|
201
|
+
|
|
202
|
+
if from_level == max_zoom_level:
|
|
203
|
+
partition_name = f'z{max_zoom_level}'
|
|
204
|
+
else:
|
|
205
|
+
partition_name = f'z{from_level}-{max_zoom_level}'
|
|
206
|
+
|
|
207
|
+
if num_buckets > 1:
|
|
208
|
+
partition_name += f'-part{i}'
|
|
209
|
+
|
|
210
|
+
partition_info[partition_name] = {
|
|
211
|
+
'tiles': bucket_tiles[i],
|
|
212
|
+
"min_zoom": from_level,
|
|
213
|
+
"max_zoom": max_zoom_level,
|
|
214
|
+
"bounds": get_bounds(bucket_tiles[i].keys()),
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
save_partition_info(partition_info, to_partition_file)
|
|
218
|
+
return partition_info
|
|
219
|
+
|
|
220
|
+
def get_pmtiles_file_name(to_pmtiles_prefix, suffix):
|
|
221
|
+
if suffix == '':
|
|
222
|
+
out_pmtiles_file = f'{to_pmtiles_prefix}.pmtiles'
|
|
223
|
+
else:
|
|
224
|
+
out_pmtiles_file = f'{to_pmtiles_prefix}-{suffix}.pmtiles'
|
|
225
|
+
|
|
226
|
+
return out_pmtiles_file
|
|
227
|
+
|
|
228
|
+
def create_pmtiles(partition_info, reader, to_pmtiles_prefix, name, description, attribution):
|
|
229
|
+
mosaic_data = {}
|
|
230
|
+
writers = {}
|
|
231
|
+
suffix_arr = []
|
|
232
|
+
tiles_to_suffix = {}
|
|
233
|
+
i = 0
|
|
234
|
+
for suffix, data in partition_info.items():
|
|
235
|
+
out_pmtiles_file = get_pmtiles_file_name(to_pmtiles_prefix, suffix)
|
|
236
|
+
Path(out_pmtiles_file).parent.mkdir(exist_ok=True, parents=True)
|
|
237
|
+
writer = PMTilesWriter(open(out_pmtiles_file, 'wb'))
|
|
238
|
+
writers[suffix] = writer
|
|
239
|
+
suffix_arr.append(suffix)
|
|
240
|
+
for t in data['tiles'].keys():
|
|
241
|
+
tiles_to_suffix[t] = i
|
|
242
|
+
i += 1
|
|
243
|
+
|
|
244
|
+
curr_zs = {}
|
|
245
|
+
max_lats = {}
|
|
246
|
+
min_lats = {}
|
|
247
|
+
max_lons = {}
|
|
248
|
+
min_lons = {}
|
|
249
|
+
min_zooms = {}
|
|
250
|
+
max_zooms = {}
|
|
251
|
+
for suffix in suffix_arr:
|
|
252
|
+
curr_zs[suffix] = None
|
|
253
|
+
max_lats[suffix] = min_lats[suffix] = max_lons[suffix] = min_lons[suffix] = None
|
|
254
|
+
min_zooms[suffix] = max_zooms[suffix] = None
|
|
255
|
+
done = set()
|
|
256
|
+
|
|
257
|
+
for t, t_data in reader.all():
|
|
258
|
+
if t in done:
|
|
259
|
+
continue
|
|
260
|
+
suffix = suffix_arr[tiles_to_suffix[t]]
|
|
261
|
+
writer = writers[suffix]
|
|
262
|
+
if curr_zs[suffix] is None or curr_zs[suffix] < t.z:
|
|
263
|
+
max_lats[suffix] = min_lats[suffix] = max_lons[suffix] = min_lons[suffix] = None
|
|
264
|
+
curr_zs[suffix] = t.z
|
|
265
|
+
t_bounds = mercantile.bounds(t)
|
|
266
|
+
if max_lats[suffix] is None or t_bounds.north > max_lats[suffix]:
|
|
267
|
+
max_lats[suffix] = t_bounds.north
|
|
268
|
+
if min_lats[suffix] is None or t_bounds.south < min_lats[suffix]:
|
|
269
|
+
min_lats[suffix] = t_bounds.south
|
|
270
|
+
if max_lons[suffix] is None or t_bounds.east > max_lons[suffix]:
|
|
271
|
+
max_lons[suffix] = t_bounds.east
|
|
272
|
+
if min_lons[suffix] is None or t_bounds.west < min_lons[suffix]:
|
|
273
|
+
min_lons[suffix] = t_bounds.west
|
|
274
|
+
if min_zooms[suffix] is None or min_zooms[suffix] > t.z:
|
|
275
|
+
min_zooms[suffix] = t.z
|
|
276
|
+
if max_zooms[suffix] is None or max_zooms[suffix] < t.z:
|
|
277
|
+
max_zooms[suffix] = t.z
|
|
278
|
+
t_id = zxy_to_tileid(t.z, t.x, t.y)
|
|
279
|
+
writer.write_tile(t_id, t_data)
|
|
280
|
+
done.add(t)
|
|
281
|
+
|
|
282
|
+
for suffix in suffix_arr:
|
|
283
|
+
out_pmtiles_file = get_pmtiles_file_name(to_pmtiles_prefix, suffix)
|
|
284
|
+
metadata = {
|
|
285
|
+
"type": "baselayer",
|
|
286
|
+
"format": "webp",
|
|
287
|
+
"version": "1",
|
|
288
|
+
"name": name,
|
|
289
|
+
"description": description,
|
|
290
|
+
'attribution': attribution
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
header = {
|
|
294
|
+
"tile_type": TileType.WEBP,
|
|
295
|
+
"tile_compression": Compression.NONE,
|
|
296
|
+
"min_lon_e7": int(min_lons[suffix] * 10000000),
|
|
297
|
+
"min_lat_e7": int(min_lats[suffix] * 10000000),
|
|
298
|
+
"max_lon_e7": int(max_lons[suffix] * 10000000),
|
|
299
|
+
"max_lat_e7": int(max_lats[suffix] * 10000000),
|
|
300
|
+
"min_zoom": min_zooms[suffix],
|
|
301
|
+
"max_zoom": max_zooms[suffix],
|
|
302
|
+
"center_zoom": 0,
|
|
303
|
+
"center_lon_e7": int(10000000 * (min_lons[suffix] + max_lons[suffix])/2),
|
|
304
|
+
"center_lat_e7": int(10000000 * (min_lats[suffix] + max_lats[suffix])/2),
|
|
305
|
+
}
|
|
306
|
+
m_header = copy.copy(header)
|
|
307
|
+
m_key = f'../{Path(out_pmtiles_file).name}'
|
|
308
|
+
m_header['tile_type'] = header['tile_type'].value
|
|
309
|
+
m_header['tile_compression'] = header['tile_compression'].value
|
|
310
|
+
writer = writers[suffix]
|
|
311
|
+
print(f'finalizing writing {suffix}')
|
|
312
|
+
writer.finalize(header, metadata)
|
|
313
|
+
mosaic_data[m_key] = { 'header': m_header, 'metadata': metadata }
|
|
314
|
+
return mosaic_data
|
|
315
|
+
|
|
316
|
+
def cli():
|
|
317
|
+
parser = argparse.ArgumentParser()
|
|
318
|
+
parser.add_argument('--only-disk', action='store_true')
|
|
319
|
+
parser.add_argument('--to-pmtiles-prefix', required=True)
|
|
320
|
+
parser.add_argument('--from-pmtiles-prefix')
|
|
321
|
+
parser.add_argument('--from-tiles-dir', required=True)
|
|
322
|
+
parser.add_argument('--name', required=True)
|
|
323
|
+
parser.add_argument('--description', required=True)
|
|
324
|
+
parser.add_argument('--min-zoom', default=0, type=int)
|
|
325
|
+
parser.add_argument('--max-zoom', required=True, type=int)
|
|
326
|
+
group = parser.add_mutually_exclusive_group(required=True)
|
|
327
|
+
group.add_argument('--attribution')
|
|
328
|
+
group.add_argument('--attribution-file')
|
|
329
|
+
args = parser.parse_args()
|
|
330
|
+
|
|
331
|
+
if args.attribution_file:
|
|
332
|
+
with open(args.attribution_file, 'r') as f:
|
|
333
|
+
attribution_text = f.read().strip()
|
|
334
|
+
else:
|
|
335
|
+
attribution_text = args.attribution
|
|
336
|
+
|
|
337
|
+
if not args.only_disk and not args.from_pmtiles_prefix:
|
|
338
|
+
parser.error("--from-pmtiles-prefix is required unless --only-disk is set.")
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
if args.only_disk:
|
|
342
|
+
reader = DiskSource(args.from_tiles_dir)
|
|
343
|
+
else:
|
|
344
|
+
reader = DiskAndPartitionedPMTilesSource(args.from_tiles_dir, args.from_pmtiles_prefix)
|
|
345
|
+
|
|
346
|
+
print('getting partition info')
|
|
347
|
+
to_partition_file = Path(f'{args.to_pmtiles_prefix}.partition_info.json')
|
|
348
|
+
partition_info = get_partition_info(reader, args.min_zoom, args.max_zoom, to_partition_file)
|
|
349
|
+
|
|
350
|
+
print('creating pmtiles')
|
|
351
|
+
mosaic_data = create_pmtiles(partition_info, reader, args.to_pmtiles_prefix,
|
|
352
|
+
args.name, args.description, attribution_text)
|
|
353
|
+
pprint(mosaic_data)
|
|
354
|
+
if len(mosaic_data) > 1:
|
|
355
|
+
Path(f'{args.to_pmtiles_prefix}.mosaic.json').write_text(json.dumps(mosaic_data))
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
if __name__ == '__main__':
|
|
359
|
+
cli()
|
|
360
|
+
|
|
361
|
+
|