topo_map_processor 0.1.9__tar.gz → 0.1.10__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.
- {topo_map_processor-0.1.9 → topo_map_processor-0.1.10}/PKG-INFO +10 -9
- {topo_map_processor-0.1.9 → topo_map_processor-0.1.10}/README.md +8 -7
- {topo_map_processor-0.1.9 → topo_map_processor-0.1.10}/pyproject.toml +3 -1
- {topo_map_processor-0.1.9 → topo_map_processor-0.1.10}/topo_map_processor/_version.py +2 -2
- topo_map_processor-0.1.10/topo_map_processor/tools/download_mosaic.py +375 -0
- {topo_map_processor-0.1.9 → topo_map_processor-0.1.10}/topo_map_processor/tools/execute_bash.py +2 -0
- topo_map_processor-0.1.10/topo_map_processor/tools/partition.py +604 -0
- topo_map_processor-0.1.10/topo_map_processor/tools/partition_basic.py +514 -0
- {topo_map_processor-0.1.9 → topo_map_processor-0.1.10}/topo_map_processor/tools/retile.py +18 -11
- topo_map_processor-0.1.10/topo_map_processor/tools/retile_e2e.py +181 -0
- {topo_map_processor-0.1.9 → topo_map_processor-0.1.10}/topo_map_processor/tools/tile.py +5 -8
- {topo_map_processor-0.1.9 → topo_map_processor-0.1.10}/topo_map_processor/tools/tile_sources.py +79 -26
- topo_map_processor-0.1.9/topo_map_processor/tools/download_mosaic.py +0 -256
- topo_map_processor-0.1.9/topo_map_processor/tools/partition.py +0 -438
- {topo_map_processor-0.1.9 → topo_map_processor-0.1.10}/.gitignore +0 -0
- {topo_map_processor-0.1.9 → topo_map_processor-0.1.10}/LICENSE +0 -0
- {topo_map_processor-0.1.9 → topo_map_processor-0.1.10}/topo_map_processor/__init__.py +0 -0
- {topo_map_processor-0.1.9 → topo_map_processor-0.1.10}/topo_map_processor/bash_scripts/__init__.py +0 -0
- {topo_map_processor-0.1.9 → topo_map_processor-0.1.10}/topo_map_processor/bash_scripts/generate_lists.sh +0 -0
- {topo_map_processor-0.1.9 → topo_map_processor-0.1.10}/topo_map_processor/bash_scripts/upload_to_release.sh +0 -0
- {topo_map_processor-0.1.9 → topo_map_processor-0.1.10}/topo_map_processor/processor.py +0 -0
- {topo_map_processor-0.1.9 → topo_map_processor-0.1.10}/topo_map_processor/tools/__init__.py +0 -0
- {topo_map_processor-0.1.9 → topo_map_processor-0.1.10}/topo_map_processor/tools/collect_bounds.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: topo_map_processor
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.10
|
|
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
|
|
@@ -32,7 +32,7 @@ License: This is free and unencumbered software released into the public domain.
|
|
|
32
32
|
License-File: LICENSE
|
|
33
33
|
Requires-Python: >=3.12
|
|
34
34
|
Requires-Dist: mercantile>=1.2.1
|
|
35
|
-
Requires-Dist: pmtiles
|
|
35
|
+
Requires-Dist: pmtiles==3.4.1
|
|
36
36
|
Requires-Dist: requests>=2.32.4
|
|
37
37
|
Requires-Dist: shapely>=2.1.1
|
|
38
38
|
Provides-Extra: easyocr
|
|
@@ -235,14 +235,15 @@ collect-bounds --bounds-dir <directory> --output-file <file> [--preexisting-file
|
|
|
235
235
|
|
|
236
236
|
#### `download-mosaic`
|
|
237
237
|
|
|
238
|
-
Downloads a PMTiles mosaic and converts it to a single MBTiles file. A mosaic is a JSON file that lists multiple PMTiles files.
|
|
238
|
+
Downloads a PMTiles mosaic and converts it to a single MBTiles or PMTiles file. A mosaic is a JSON file that lists multiple PMTiles files.
|
|
239
239
|
|
|
240
240
|
```bash
|
|
241
|
-
download-mosaic --
|
|
241
|
+
download-mosaic --mosaic-url <url> [--output-file <file>] [--archive-type <type>] [--request-timeout-secs <seconds>] [--num-http-retries <retries>]
|
|
242
242
|
```
|
|
243
243
|
|
|
244
|
-
- `--
|
|
245
|
-
- `--
|
|
244
|
+
- `--mosaic-url`, `-u`: URL of the mosaic JSON file. (Required)
|
|
245
|
+
- `--output-file`, `-o`: Output MBTiles/PMTiles file name. The format is inferred from the extension (`.mbtiles` or `.pmtiles`). If not specified, it is derived from the mosaic URL. (Optional)
|
|
246
|
+
- `--archive-type`: Type of archive to create (`mbtiles` or `pmtiles`). Required if `--output-file` is not provided or its extension is not recognized.
|
|
246
247
|
- `--request-timeout-secs`, `-t`: Timeout for HTTP requests in seconds (default: 60).
|
|
247
248
|
- `--num-http-retries`, `-r`: Number of retries for HTTP requests (default: 3).
|
|
248
249
|
|
|
@@ -285,7 +286,7 @@ retile --retile-list-file <file> --bounds-file <file> [--max-zoom <zoom>] [--tif
|
|
|
285
286
|
- `--from-source`: Location of the source from which to pull existing tiles. Can be a glob pattern to match a group of pmtiles. Required if not in pull-list generation mode.
|
|
286
287
|
- `--sheets-to-pull-list-outfile`: If provided, the script will only calculate the full list of sheets that need to be processed (including adjacent ones) and write it to this file, then exit.
|
|
287
288
|
- `--num-parallel`: Number of parallel processes to use for tiling (default: number of CPU cores).
|
|
288
|
-
- `--tile-quality`: Quality of compression for webp and
|
|
289
|
+
- `--tile-quality`: Quality of compression for webp and jpeg (default: 75).
|
|
289
290
|
|
|
290
291
|
#### `tile`
|
|
291
292
|
|
|
@@ -303,8 +304,8 @@ tile --tiles-dir <dir> --tiffs-dir <dir> --max-zoom <zoom> --name <name> --descr
|
|
|
303
304
|
- `--attribution`: Attribution text for the mosaic. Required if `--attribution-file` is not used.
|
|
304
305
|
- `--attribution-file`: File containing attribution text for the mosaic. Required if `--attribution` is not used.
|
|
305
306
|
- `--min-zoom`: Minimum zoom level for tiling (default: 0).
|
|
306
|
-
- `--tile-extension`: Tile file extension (default: webp). Choices: `webp`, `
|
|
307
|
-
- `--tile-quality`: Compression quality of the tiles for `webp` and `
|
|
307
|
+
- `--tile-extension`: Tile file extension (default: webp). Choices: `webp`, `jpeg`, `png`.
|
|
308
|
+
- `--tile-quality`: Compression quality of the tiles for `webp` and `jpeg` (default: 75).
|
|
308
309
|
- `--num-parallel`: Number of parallel processes to use for tiling (default: number of CPU cores).
|
|
309
310
|
|
|
310
311
|
### Running Tools Directly with `uvx`
|
|
@@ -186,14 +186,15 @@ collect-bounds --bounds-dir <directory> --output-file <file> [--preexisting-file
|
|
|
186
186
|
|
|
187
187
|
#### `download-mosaic`
|
|
188
188
|
|
|
189
|
-
Downloads a PMTiles mosaic and converts it to a single MBTiles file. A mosaic is a JSON file that lists multiple PMTiles files.
|
|
189
|
+
Downloads a PMTiles mosaic and converts it to a single MBTiles or PMTiles file. A mosaic is a JSON file that lists multiple PMTiles files.
|
|
190
190
|
|
|
191
191
|
```bash
|
|
192
|
-
download-mosaic --
|
|
192
|
+
download-mosaic --mosaic-url <url> [--output-file <file>] [--archive-type <type>] [--request-timeout-secs <seconds>] [--num-http-retries <retries>]
|
|
193
193
|
```
|
|
194
194
|
|
|
195
|
-
- `--
|
|
196
|
-
- `--
|
|
195
|
+
- `--mosaic-url`, `-u`: URL of the mosaic JSON file. (Required)
|
|
196
|
+
- `--output-file`, `-o`: Output MBTiles/PMTiles file name. The format is inferred from the extension (`.mbtiles` or `.pmtiles`). If not specified, it is derived from the mosaic URL. (Optional)
|
|
197
|
+
- `--archive-type`: Type of archive to create (`mbtiles` or `pmtiles`). Required if `--output-file` is not provided or its extension is not recognized.
|
|
197
198
|
- `--request-timeout-secs`, `-t`: Timeout for HTTP requests in seconds (default: 60).
|
|
198
199
|
- `--num-http-retries`, `-r`: Number of retries for HTTP requests (default: 3).
|
|
199
200
|
|
|
@@ -236,7 +237,7 @@ retile --retile-list-file <file> --bounds-file <file> [--max-zoom <zoom>] [--tif
|
|
|
236
237
|
- `--from-source`: Location of the source from which to pull existing tiles. Can be a glob pattern to match a group of pmtiles. Required if not in pull-list generation mode.
|
|
237
238
|
- `--sheets-to-pull-list-outfile`: If provided, the script will only calculate the full list of sheets that need to be processed (including adjacent ones) and write it to this file, then exit.
|
|
238
239
|
- `--num-parallel`: Number of parallel processes to use for tiling (default: number of CPU cores).
|
|
239
|
-
- `--tile-quality`: Quality of compression for webp and
|
|
240
|
+
- `--tile-quality`: Quality of compression for webp and jpeg (default: 75).
|
|
240
241
|
|
|
241
242
|
#### `tile`
|
|
242
243
|
|
|
@@ -254,8 +255,8 @@ tile --tiles-dir <dir> --tiffs-dir <dir> --max-zoom <zoom> --name <name> --descr
|
|
|
254
255
|
- `--attribution`: Attribution text for the mosaic. Required if `--attribution-file` is not used.
|
|
255
256
|
- `--attribution-file`: File containing attribution text for the mosaic. Required if `--attribution` is not used.
|
|
256
257
|
- `--min-zoom`: Minimum zoom level for tiling (default: 0).
|
|
257
|
-
- `--tile-extension`: Tile file extension (default: webp). Choices: `webp`, `
|
|
258
|
-
- `--tile-quality`: Compression quality of the tiles for `webp` and `
|
|
258
|
+
- `--tile-extension`: Tile file extension (default: webp). Choices: `webp`, `jpeg`, `png`.
|
|
259
|
+
- `--tile-quality`: Compression quality of the tiles for `webp` and `jpeg` (default: 75).
|
|
259
260
|
- `--num-parallel`: Number of parallel processes to use for tiling (default: number of CPU cores).
|
|
260
261
|
|
|
261
262
|
### Running Tools Directly with `uvx`
|
|
@@ -9,7 +9,7 @@ authors = [
|
|
|
9
9
|
]
|
|
10
10
|
dependencies = [
|
|
11
11
|
"mercantile>=1.2.1",
|
|
12
|
-
"pmtiles
|
|
12
|
+
"pmtiles==3.4.1",
|
|
13
13
|
"requests>=2.32.4",
|
|
14
14
|
"shapely>=2.1.1",
|
|
15
15
|
]
|
|
@@ -40,10 +40,12 @@ repository = "https://github.com/ramSeraph/topo_map_processor.git"
|
|
|
40
40
|
collect-bounds = "topo_map_processor.tools.collect_bounds:cli"
|
|
41
41
|
download-mosaic = "topo_map_processor.tools.download_mosaic:cli"
|
|
42
42
|
partition = "topo_map_processor.tools.partition:cli"
|
|
43
|
+
partition-basic = "topo_map_processor.tools.partition_basic:cli"
|
|
43
44
|
retile = "topo_map_processor.tools.retile:cli"
|
|
44
45
|
tile = "topo_map_processor.tools.tile:cli"
|
|
45
46
|
generate-lists = "topo_map_processor.tools.execute_bash:generate_lists"
|
|
46
47
|
upload-to-release = "topo_map_processor.tools.execute_bash:upload_to_release"
|
|
48
|
+
retile-e2e = "topo_map_processor.tools.retile_e2e:cli"
|
|
47
49
|
|
|
48
50
|
|
|
49
51
|
[build-system]
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.12"
|
|
4
|
+
# dependencies = [
|
|
5
|
+
# "mercantile",
|
|
6
|
+
# "pmtiles",
|
|
7
|
+
# "requests",
|
|
8
|
+
# ]
|
|
9
|
+
# ///
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sqlite3
|
|
13
|
+
|
|
14
|
+
from pathlib import Path, PurePosixPath
|
|
15
|
+
from urllib.parse import urlparse, urljoin, unquote
|
|
16
|
+
|
|
17
|
+
import requests
|
|
18
|
+
from requests.adapters import HTTPAdapter
|
|
19
|
+
from requests.packages.urllib3.util.retry import Retry
|
|
20
|
+
|
|
21
|
+
from pmtiles.reader import Reader, MmapSource, all_tiles
|
|
22
|
+
from pmtiles.writer import Writer
|
|
23
|
+
from pmtiles.tile import zxy_to_tileid, Compression, TileType
|
|
24
|
+
|
|
25
|
+
# heavily copied from https://github.com/protomaps/PMTiles/blob/main/python/pmtiles/convert.py
|
|
26
|
+
# and https://github.com/mapbox/mbutil/blob/master/mbutil/util.py
|
|
27
|
+
|
|
28
|
+
class ArchiveWriter:
|
|
29
|
+
def __init__(self, output_file):
|
|
30
|
+
self.output_file = output_file
|
|
31
|
+
|
|
32
|
+
def init(self):
|
|
33
|
+
raise NotImplementedError("This method should be overridden by subclasses")
|
|
34
|
+
|
|
35
|
+
def commit(self):
|
|
36
|
+
raise NotImplementedError("This method should be overridden by subclasses")
|
|
37
|
+
|
|
38
|
+
def add_to_archive(self, zxy, tile_data):
|
|
39
|
+
raise NotImplementedError("This method should be overridden by subclasses")
|
|
40
|
+
|
|
41
|
+
def add(self, pmtiles_fname):
|
|
42
|
+
print(f'adding {pmtiles_fname} to archive')
|
|
43
|
+
with open(pmtiles_fname, "r+b") as f:
|
|
44
|
+
source = MmapSource(f)
|
|
45
|
+
reader = Reader(source)
|
|
46
|
+
|
|
47
|
+
for zxy, tile_data in all_tiles(reader.get_bytes):
|
|
48
|
+
self.add_to_archive(zxy, tile_data)
|
|
49
|
+
|
|
50
|
+
self.commit()
|
|
51
|
+
|
|
52
|
+
def finalize(self, metadata=None, header=None):
|
|
53
|
+
raise NotImplementedError("This method should be overridden by subclasses")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class PmtilesArchiveWriter(ArchiveWriter):
|
|
57
|
+
def __init__(self, output_file):
|
|
58
|
+
super().__init__(output_file)
|
|
59
|
+
self.pmtiles_writer = None
|
|
60
|
+
|
|
61
|
+
def init(self):
|
|
62
|
+
self.pmtiles_writer = Writer(open(self.output_file, 'wb'))
|
|
63
|
+
|
|
64
|
+
def add_to_archive(self, zxy, tile_data):
|
|
65
|
+
tile_id = zxy_to_tileid(zxy[0], zxy[1], zxy[2])
|
|
66
|
+
self.pmtiles_writer.write_tile(tile_id, tile_data)
|
|
67
|
+
|
|
68
|
+
def commit(self):
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
def enhance_header(self, h):
|
|
72
|
+
h['tile_compression'] = Compression(h['tile_compression'])
|
|
73
|
+
h['tile_type'] = TileType(h['tile_type'])
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def finalize(self, metadata, header):
|
|
77
|
+
self.enhance_header(header)
|
|
78
|
+
self.pmtiles_writer.finalize(header, metadata)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class MbtilesArchiveWriter(ArchiveWriter):
|
|
82
|
+
def __init__(self, output_file):
|
|
83
|
+
super().__init__(output_file)
|
|
84
|
+
self.conn = None
|
|
85
|
+
self.cursor = None
|
|
86
|
+
|
|
87
|
+
def init(self):
|
|
88
|
+
self.conn = sqlite3.connect(self.output_file)
|
|
89
|
+
self.cursor = self.conn.cursor()
|
|
90
|
+
|
|
91
|
+
self.cursor.execute("""PRAGMA synchronous=0""")
|
|
92
|
+
self.cursor.execute("""PRAGMA locking_mode=EXCLUSIVE""")
|
|
93
|
+
self.cursor.execute("""PRAGMA journal_mode=DELETE""")
|
|
94
|
+
|
|
95
|
+
self.cursor.execute("CREATE TABLE metadata (name text, value text);")
|
|
96
|
+
self.cursor.execute(
|
|
97
|
+
"CREATE TABLE tiles (zoom_level integer, tile_column integer, tile_row integer, tile_data blob);"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def add_to_archive(self, zxy, tile_data):
|
|
101
|
+
flipped_y = (1 << zxy[0]) - 1 - zxy[2]
|
|
102
|
+
self.cursor.execute(
|
|
103
|
+
"INSERT INTO tiles VALUES(?,?,?,?)",
|
|
104
|
+
(zxy[0], zxy[1], flipped_y, tile_data),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def commit(self):
|
|
108
|
+
self.conn.commit()
|
|
109
|
+
|
|
110
|
+
def add_metadata(self, metadata):
|
|
111
|
+
json_metadata = {}
|
|
112
|
+
for k, v in metadata.items():
|
|
113
|
+
if k == "vector_layers":
|
|
114
|
+
json_metadata["vector_layers"] = v
|
|
115
|
+
continue
|
|
116
|
+
elif k == "tilestats":
|
|
117
|
+
json_metadata["tilestats"] = v
|
|
118
|
+
continue
|
|
119
|
+
elif not isinstance(v, str):
|
|
120
|
+
v = json.dumps(v, ensure_ascii=False)
|
|
121
|
+
|
|
122
|
+
self.cursor.execute("INSERT INTO metadata VALUES(?,?)", (k, v))
|
|
123
|
+
|
|
124
|
+
if len(json_metadata) > 0:
|
|
125
|
+
self.cursor.execute(
|
|
126
|
+
"INSERT INTO metadata VALUES(?,?)",
|
|
127
|
+
("json", json.dumps(json_metadata, ensure_ascii=False)),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def finalize_mbtiles(self):
|
|
132
|
+
self.cursor.execute(
|
|
133
|
+
"CREATE UNIQUE INDEX tile_index on tiles (zoom_level, tile_column, tile_row);"
|
|
134
|
+
)
|
|
135
|
+
self.conn.commit()
|
|
136
|
+
self.cursor.execute("""ANALYZE;""")
|
|
137
|
+
|
|
138
|
+
self.conn.close()
|
|
139
|
+
|
|
140
|
+
def enhance_metadata(self, cm, ch):
|
|
141
|
+
|
|
142
|
+
cm['maxzoom'] = ch['max_zoom']
|
|
143
|
+
cm['minzoom'] = ch['min_zoom']
|
|
144
|
+
|
|
145
|
+
max_lat = ch['max_lat_e7'] / 10000000
|
|
146
|
+
min_lat = ch['min_lat_e7'] / 10000000
|
|
147
|
+
max_lon = ch['max_lon_e7'] / 10000000
|
|
148
|
+
min_lon = ch['min_lon_e7'] / 10000000
|
|
149
|
+
cm['bounds'] = f"{min_lon},{min_lat},{max_lon},{max_lat}"
|
|
150
|
+
|
|
151
|
+
center_lat = ch['center_lat_e7'] / 10000000
|
|
152
|
+
center_lon = ch['center_lon_e7'] / 10000000
|
|
153
|
+
center_zoom = ch['center_zoom']
|
|
154
|
+
cm['center'] = f"{center_lon},{center_lat},{center_zoom}"
|
|
155
|
+
|
|
156
|
+
def finalize(self, metadata, header):
|
|
157
|
+
self.enhance_metadata(metadata, header)
|
|
158
|
+
self.add_metadata(metadata)
|
|
159
|
+
self.finalize_mbtiles()
|
|
160
|
+
|
|
161
|
+
def get_filename_from_url(url):
|
|
162
|
+
url_parsed = urlparse(url)
|
|
163
|
+
return unquote(PurePosixPath(url_parsed.path).name)
|
|
164
|
+
|
|
165
|
+
class Merger:
|
|
166
|
+
def __init__(self, mosaic_url, archive_type, output_file, request_timeout_secs, num_http_retries):
|
|
167
|
+
self.session = self.get_session(num_http_retries)
|
|
168
|
+
self.request_timeout_secs = request_timeout_secs
|
|
169
|
+
|
|
170
|
+
self.output_file = output_file
|
|
171
|
+
self.output_dir = output_file.parent
|
|
172
|
+
self.tracker_file = self.output_dir / 'tracker.txt'
|
|
173
|
+
self.mosaic_file = self.output_dir / get_filename_from_url(mosaic_url)
|
|
174
|
+
|
|
175
|
+
self.archive_type = archive_type
|
|
176
|
+
self.archive_writer = self.get_archive_writer()
|
|
177
|
+
|
|
178
|
+
self.mosaic_url = mosaic_url
|
|
179
|
+
self.mosaic_data = None
|
|
180
|
+
self.mosaic_version = 0
|
|
181
|
+
self.done_stages = set()
|
|
182
|
+
|
|
183
|
+
def get_archive_writer(self):
|
|
184
|
+
if self.archive_type == 'mbtiles':
|
|
185
|
+
return MbtilesArchiveWriter(self.output_file)
|
|
186
|
+
|
|
187
|
+
if self.archive_type == 'pmtiles':
|
|
188
|
+
return PmtilesArchiveWriter(self.output_file)
|
|
189
|
+
|
|
190
|
+
raise ValueError(f'Unsupported archive type: {self.archive_type}')
|
|
191
|
+
|
|
192
|
+
def get_session(self, num_http_retries):
|
|
193
|
+
session = requests.session()
|
|
194
|
+
retries = num_http_retries
|
|
195
|
+
retry = Retry(
|
|
196
|
+
total=retries,
|
|
197
|
+
read=retries,
|
|
198
|
+
connect=retries,
|
|
199
|
+
)
|
|
200
|
+
session.mount('http://', HTTPAdapter(max_retries=retry))
|
|
201
|
+
session.mount('https://', HTTPAdapter(max_retries=retry))
|
|
202
|
+
return session
|
|
203
|
+
|
|
204
|
+
def get_mosaic(self, mosaic_url):
|
|
205
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
206
|
+
if not self.mosaic_file.exists():
|
|
207
|
+
self.download_file(mosaic_url, self.mosaic_file)
|
|
208
|
+
|
|
209
|
+
self.mosaic_data = json.loads(self.mosaic_file.read_text())
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def download_file(self, url, file):
|
|
213
|
+
print(f'downloading {url} to {file}')
|
|
214
|
+
resp = self.session.get(url, stream=True, timeout=self.request_timeout_secs)
|
|
215
|
+
with open(file, 'wb') as f:
|
|
216
|
+
for data in resp.iter_content(chunk_size=4096):
|
|
217
|
+
f.write(data)
|
|
218
|
+
|
|
219
|
+
def init_tracker(self):
|
|
220
|
+
if not self.tracker_file.exists():
|
|
221
|
+
self.tracker_file.write_text("")
|
|
222
|
+
|
|
223
|
+
def populate_done_list(self):
|
|
224
|
+
if not self.tracker_file.exists():
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
self.done_stages = set(self.tracker_file.read_text().strip().split('\n'))
|
|
228
|
+
|
|
229
|
+
def mark_as_done(self, stage):
|
|
230
|
+
with open(self.tracker_file, 'a') as f:
|
|
231
|
+
f.write(stage)
|
|
232
|
+
f.write('\n')
|
|
233
|
+
self.done_stages.add(stage)
|
|
234
|
+
|
|
235
|
+
def get_pmtiles_url(self, k):
|
|
236
|
+
if self.mosaic_version == 0 and k.startswith('../'):
|
|
237
|
+
k = k[3:]
|
|
238
|
+
return urljoin(self.mosaic_url, k)
|
|
239
|
+
|
|
240
|
+
def collect_header(self, items):
|
|
241
|
+
ch = {}
|
|
242
|
+
for item in items:
|
|
243
|
+
h = item['header']
|
|
244
|
+
if 'max_zoom' not in ch or ch['max_zoom'] < h['max_zoom']:
|
|
245
|
+
ch['max_zoom'] = h['max_zoom']
|
|
246
|
+
if 'min_zoom' not in ch or ch['min_zoom'] > h['min_zoom']:
|
|
247
|
+
ch['min_zoom'] = h['min_zoom']
|
|
248
|
+
|
|
249
|
+
if 'max_lat_e7' not in ch or ch['max_lat_e7'] < h['max_lat_e7']:
|
|
250
|
+
ch['max_lat_e7'] = h['max_lat_e7']
|
|
251
|
+
if 'min_lat_e7' not in ch or ch['min_lat_e7'] > h['min_lat_e7']:
|
|
252
|
+
ch['min_lat_e7'] = h['min_lat_e7']
|
|
253
|
+
if 'max_lon_e7' not in ch or ch['max_lon_e7'] < h['max_lon_e7']:
|
|
254
|
+
ch['max_lon_e7'] = h['max_lon_e7']
|
|
255
|
+
if 'min_lon_e7' not in ch or ch['min_lon_e7'] > h['min_lon_e7']:
|
|
256
|
+
ch['min_lon_e7'] = h['min_lon_e7']
|
|
257
|
+
|
|
258
|
+
ch['tile_compression'] = h['tile_compression']
|
|
259
|
+
ch['tile_type'] = h['tile_type']
|
|
260
|
+
|
|
261
|
+
ch['center_lat_e7'] = (ch['max_lat_e7'] + ch['min_lat_e7']) // 2
|
|
262
|
+
ch['center_lon_e7'] = (ch['max_lon_e7'] + ch['min_lon_e7']) // 2
|
|
263
|
+
ch['center_zoom'] = (ch['max_zoom'] + ch['min_zoom']) // 2
|
|
264
|
+
|
|
265
|
+
return ch
|
|
266
|
+
|
|
267
|
+
def get_metadata_and_header(self):
|
|
268
|
+
if self.mosaic_version != 0:
|
|
269
|
+
metadata = self.mosaic_data['metadata']
|
|
270
|
+
header = self.mosaic_data['header']
|
|
271
|
+
else:
|
|
272
|
+
metadata = list(self.mosaic_data.values())[0]['metadata']
|
|
273
|
+
header = self.collect_header(self.mosaic_data.values())
|
|
274
|
+
|
|
275
|
+
return metadata, header
|
|
276
|
+
|
|
277
|
+
def process(self):
|
|
278
|
+
self.get_mosaic(self.mosaic_url)
|
|
279
|
+
if 'version' in self.mosaic_data:
|
|
280
|
+
self.mosaic_version = self.mosaic_data['version']
|
|
281
|
+
|
|
282
|
+
if self.output_file.exists() and not self.tracker_file.exists():
|
|
283
|
+
raise Exception(f'Output file {self.archive_writer.output_file} already exists, and no tracker file found. Exiting to avoid overwriting.')
|
|
284
|
+
|
|
285
|
+
self.init_tracker()
|
|
286
|
+
self.populate_done_list()
|
|
287
|
+
|
|
288
|
+
self.archive_writer.init()
|
|
289
|
+
|
|
290
|
+
slice_data = self.mosaic_data if self.mosaic_version == 0 else self.mosaic_data.get('slices', {})
|
|
291
|
+
|
|
292
|
+
for k in slice_data.keys():
|
|
293
|
+
|
|
294
|
+
pmtiles_url = self.get_pmtiles_url(k)
|
|
295
|
+
if k in self.done_stages:
|
|
296
|
+
print(f'Stage {k} already done, skipping')
|
|
297
|
+
continue
|
|
298
|
+
|
|
299
|
+
pmtiles_file = self.output_dir / get_filename_from_url(pmtiles_url)
|
|
300
|
+
if pmtiles_file.exists():
|
|
301
|
+
raise Exception(f'{pmtiles_file} already exists, delete existing file to continue')
|
|
302
|
+
|
|
303
|
+
self.download_file(pmtiles_url, pmtiles_file)
|
|
304
|
+
|
|
305
|
+
self.archive_writer.add(pmtiles_file)
|
|
306
|
+
|
|
307
|
+
self.mark_as_done(k)
|
|
308
|
+
|
|
309
|
+
pmtiles_file.unlink()
|
|
310
|
+
|
|
311
|
+
metadata, header = self.get_metadata_and_header()
|
|
312
|
+
self.archive_writer.finalize(metadata, header)
|
|
313
|
+
|
|
314
|
+
def cleanup(self):
|
|
315
|
+
if self.tracker_file.exists():
|
|
316
|
+
self.tracker_file.unlink()
|
|
317
|
+
print('Tracker file deleted.')
|
|
318
|
+
|
|
319
|
+
if self.mosaic_file.exists():
|
|
320
|
+
self.mosaic_file.unlink()
|
|
321
|
+
print('Mosaic file deleted.')
|
|
322
|
+
|
|
323
|
+
def cli():
|
|
324
|
+
import argparse
|
|
325
|
+
parser = argparse.ArgumentParser(description='Download a mosaic and convert it to MBTiles or PMTiles format.')
|
|
326
|
+
parser.add_argument('--mosaic-url', '-u', required=True, type=str, help='URL of the mosaic JSON file')
|
|
327
|
+
parser.add_argument('--output-file', '-o', type=str, help='Output MBTiles/PMTiles file name. The format is inferred from the extension.')
|
|
328
|
+
parser.add_argument('--archive-type', '-a', choices=['mbtiles', 'pmtiles'], help='Type of archive to create. Required if --output-file is not provided.')
|
|
329
|
+
parser.add_argument('--request-timeout-secs', '-t', type=int, default=60, help='Timeout for HTTP requests in seconds')
|
|
330
|
+
parser.add_argument('--num-http-retries', '-r', type=int, default=3, help='Number of retries for HTTP requests')
|
|
331
|
+
args = parser.parse_args()
|
|
332
|
+
|
|
333
|
+
archive_type = args.archive_type
|
|
334
|
+
output_file = None
|
|
335
|
+
mosaic_fname = get_filename_from_url(args.mosaic_url)
|
|
336
|
+
|
|
337
|
+
if args.output_file:
|
|
338
|
+
output_file = Path(args.output_file)
|
|
339
|
+
if output_file.suffix == '.mbtiles':
|
|
340
|
+
if archive_type and archive_type != 'mbtiles':
|
|
341
|
+
raise ValueError("Output file extension is .mbtiles but --archive-type is not 'mbtiles'")
|
|
342
|
+
archive_type = 'mbtiles'
|
|
343
|
+
elif output_file.suffix == '.pmtiles':
|
|
344
|
+
if archive_type and archive_type != 'pmtiles':
|
|
345
|
+
raise ValueError("Output file extension is .pmtiles but --archive-type is not 'pmtiles'")
|
|
346
|
+
archive_type = 'pmtiles'
|
|
347
|
+
else:
|
|
348
|
+
raise ValueError("If --output-file is provided, it must have a .mbtiles or .pmtiles extension.")
|
|
349
|
+
|
|
350
|
+
if not archive_type:
|
|
351
|
+
raise ValueError("You must specify either --output-file (with .mbtiles or .pmtiles extension) or --archive-type.")
|
|
352
|
+
|
|
353
|
+
if not output_file:
|
|
354
|
+
if not mosaic_fname.endswith('.mosaic.json'):
|
|
355
|
+
raise ValueError('Input mosaic URL must point to a file ending with .mosaic.json if --output-file is not specified.')
|
|
356
|
+
|
|
357
|
+
out_fname = mosaic_fname[:-len('.mosaic.json')] + '.' + archive_type
|
|
358
|
+
output_file = Path(out_fname)
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
merger = Merger(args.mosaic_url,
|
|
362
|
+
archive_type,
|
|
363
|
+
output_file,
|
|
364
|
+
args.request_timeout_secs,
|
|
365
|
+
args.num_http_retries)
|
|
366
|
+
|
|
367
|
+
merger.process()
|
|
368
|
+
merger.cleanup()
|
|
369
|
+
|
|
370
|
+
print('Done!!!')
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
if __name__ == '__main__':
|
|
374
|
+
cli()
|
|
375
|
+
|