182 lines
7.1 KiB
Python
182 lines
7.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Sovereign 3D DEM Tile Seeder for Jordan (سيرفر سيادي لتخزين تضاريس الأردن ثلاثية الأبعاد)
|
|
Downloads Terrarium 3D DEM elevation tiles for the Jordan National Bounding Box
|
|
and packages them into a sovereign MBTiles SQLite database and/or local tile directory.
|
|
|
|
Bounding Box (Jordan):
|
|
West: 34.8°, South: 29.1°, East: 39.3°, North: 33.4°
|
|
Zoom Levels: 0 to 14 (overscaled to z20 by MapLibre GPU)
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import math
|
|
import sqlite3
|
|
import argparse
|
|
import time
|
|
import urllib.request
|
|
import urllib.error
|
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
|
|
# National Jordan Geographic Envelope
|
|
JORDAN_BBOX = {
|
|
'min_lon': 34.8,
|
|
'min_lat': 29.1,
|
|
'max_lon': 39.3,
|
|
'max_lat': 33.4
|
|
}
|
|
|
|
DEM_URL_TEMPLATE = "https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png"
|
|
|
|
def deg2num(lat_deg, lon_deg, zoom):
|
|
lat_rad = math.radians(lat_deg)
|
|
n = 2.0 ** zoom
|
|
xtile = int((lon_deg + 180.0) / 360.0 * n)
|
|
ytile = int((1.0 - math.asinh(math.tan(lat_rad)) / math.pi) / 2.0 * n)
|
|
return (xtile, ytile)
|
|
|
|
def init_mbtiles(db_path, min_zoom, max_zoom):
|
|
conn = sqlite3.connect(db_path)
|
|
cur = conn.cursor()
|
|
cur.execute("PRAGMA synchronous = NORMAL")
|
|
cur.execute("PRAGMA journal_mode = WAL")
|
|
|
|
cur.execute("""
|
|
CREATE TABLE IF NOT EXISTS metadata (
|
|
name TEXT PRIMARY KEY,
|
|
value TEXT
|
|
);
|
|
""")
|
|
cur.execute("""
|
|
CREATE TABLE IF NOT EXISTS tiles (
|
|
zoom_level INTEGER,
|
|
tile_column INTEGER,
|
|
tile_row INTEGER,
|
|
tile_data BLOB,
|
|
PRIMARY KEY (zoom_level, tile_column, tile_row)
|
|
);
|
|
""")
|
|
|
|
meta = [
|
|
('name', 'Jordan Sovereign 3D Elevation DEM (Terrarium)'),
|
|
('type', 'baselayer'),
|
|
('version', '1.0'),
|
|
('description', 'High-accuracy Sovereign 3D DEM Terrarium Tiles for the Hashemite Kingdom of Jordan'),
|
|
('format', 'png'),
|
|
('bounds', f"{JORDAN_BBOX['min_lon']},{JORDAN_BBOX['min_lat']},{JORDAN_BBOX['max_lon']},{JORDAN_BBOX['max_lat']}"),
|
|
('minzoom', str(min_zoom)),
|
|
('maxzoom', str(max_zoom)),
|
|
('attribution', '© Intaleq Sovereign Spatial Engine | Mapzen Terrarium')
|
|
]
|
|
for k, v in meta:
|
|
cur.execute("INSERT OR REPLACE INTO metadata (name, value) VALUES (?, ?)", (k, v))
|
|
conn.commit()
|
|
return conn
|
|
|
|
def download_tile(z, x, y, retries=3):
|
|
url = DEM_URL_TEMPLATE.format(z=z, x=x, y=y)
|
|
req = urllib.request.Request(
|
|
url,
|
|
headers={'User-Agent': 'Intaleq-Sovereign-Map-Seeder/1.0'}
|
|
)
|
|
for attempt in range(retries):
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=10) as response:
|
|
if response.status == 200:
|
|
return (z, x, y, response.read(), None)
|
|
except Exception as e:
|
|
if attempt == retries - 1:
|
|
return (z, x, y, None, str(e))
|
|
time.sleep(0.5 * (attempt + 1))
|
|
return (z, x, y, None, "Timeout")
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Jordan Sovereign 3D DEM Tile Seeder")
|
|
parser.add_argument("--min-zoom", type=int, default=0, help="Minimum zoom level (default: 0)")
|
|
parser.add_argument("--max-zoom", type=int, default=14, help="Maximum zoom level (default: 14)")
|
|
parser.add_argument("--output-mbtiles", type=str, default="data/jordan-dem.mbtiles", help="Output MBTiles path")
|
|
parser.add_argument("--output-dir", type=str, default="", help="Optional output directory for raw {z}/{x}/{y}.png files")
|
|
parser.add_argument("--workers", type=int, default=16, help="Concurrent worker threads (default: 16)")
|
|
args = parser.parse_args()
|
|
|
|
os.makedirs(os.path.dirname(os.path.abspath(args.output_mbtiles)), exist_ok=True)
|
|
if args.output_dir:
|
|
os.makedirs(args.output_dir, exist_ok=True)
|
|
|
|
print(f"🚀 Initializing Sovereign 3D DEM Tile Seeder for Jordan...")
|
|
print(f"📍 Geographic Bounding Box: {JORDAN_BBOX}")
|
|
print(f"🔍 Zoom range: {args.min_zoom} -> {args.max_zoom}")
|
|
print(f"💾 Target MBTiles: {args.output_mbtiles}")
|
|
|
|
conn = init_mbtiles(args.output_mbtiles, args.min_zoom, args.max_zoom)
|
|
cursor = conn.cursor()
|
|
|
|
# Pre-calculate tile list
|
|
tiles_to_download = []
|
|
for z in range(args.min_zoom, args.max_zoom + 1):
|
|
x1, y2 = deg2num(JORDAN_BBOX['min_lat'], JORDAN_BBOX['min_lon'], z)
|
|
x2, y1 = deg2num(JORDAN_BBOX['max_lat'], JORDAN_BBOX['max_lon'], z)
|
|
min_x, max_x = min(x1, x2), max(x1, x2)
|
|
min_y, max_y = min(y1, y2), max(y1, y2)
|
|
for x in range(min_x, max_x + 1):
|
|
for y in range(min_y, max_y + 1):
|
|
# Check if tile already exists in MBTiles
|
|
# Note: MBTiles uses TMS y-coordinates: tms_y = (2^z - 1) - y
|
|
tms_y = (1 << z) - 1 - y
|
|
cursor.execute("SELECT 1 FROM tiles WHERE zoom_level=? AND tile_column=? AND tile_row=?", (z, x, tms_y))
|
|
if not cursor.fetchone():
|
|
tiles_to_download.append((z, x, y))
|
|
|
|
total = len(tiles_to_download)
|
|
print(f"📦 Total new tiles to fetch: {total:,}")
|
|
if total == 0:
|
|
print("✅ All tiles are already cached and present in the sovereign database!")
|
|
conn.close()
|
|
return
|
|
|
|
downloaded = 0
|
|
errors = 0
|
|
batch = []
|
|
start_time = time.time()
|
|
|
|
with ThreadPoolExecutor(max_workers=args.workers) as executor:
|
|
futures = {executor.submit(download_tile, z, x, y): (z, x, y) for z, x, y in tiles_to_download}
|
|
|
|
for future in as_completed(futures):
|
|
z, x, y, data, err = future.result()
|
|
if data:
|
|
tms_y = (1 << z) - 1 - y
|
|
batch.append((z, x, tms_y, data))
|
|
downloaded += 1
|
|
|
|
# If raw dir specified, also save file
|
|
if args.output_dir:
|
|
tile_file_dir = os.path.join(args.output_dir, str(z), str(x))
|
|
os.makedirs(tile_file_dir, exist_ok=True)
|
|
with open(os.path.join(tile_file_dir, f"{y}.png"), "wb") as f:
|
|
f.write(data)
|
|
else:
|
|
errors += 1
|
|
|
|
if len(batch) >= 200:
|
|
cursor.executemany("INSERT OR REPLACE INTO tiles (zoom_level, tile_column, tile_row, tile_data) VALUES (?, ?, ?, ?)", batch)
|
|
conn.commit()
|
|
batch.clear()
|
|
elapsed = time.time() - start_time
|
|
speed = downloaded / elapsed if elapsed > 0 else 0
|
|
pct = (downloaded + errors) / total * 100
|
|
print(f"⏳ Progress: {downloaded:,}/{total:,} ({pct:.1f}%) | Speed: {speed:.1f} tiles/s | Errors: {errors}")
|
|
|
|
if batch:
|
|
cursor.executemany("INSERT OR REPLACE INTO tiles (zoom_level, tile_column, tile_row, tile_data) VALUES (?, ?, ?, ?)", batch)
|
|
conn.commit()
|
|
|
|
conn.close()
|
|
elapsed = time.time() - start_time
|
|
print(f"\n🎉 Finished! Downloaded: {downloaded:,} tiles in {elapsed:.1f}s. Errors: {errors}.")
|
|
print(f"🛡️ Sovereign MBTiles stored at: {args.output_mbtiles}")
|
|
|
|
if __name__ == "__main__":
|
|
main()
|