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.
@@ -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
+