Initial import
This commit is contained in:
136
src/vector_tile_generator.py
Normal file
136
src/vector_tile_generator.py
Normal file
@@ -0,0 +1,136 @@
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
import json
|
||||
|
||||
try:
|
||||
import mapbox_vector_tile
|
||||
except ModuleNotFoundError: # pragma: no cover - dependency guard for runtime environments
|
||||
mapbox_vector_tile = None
|
||||
|
||||
try:
|
||||
import mercantile
|
||||
except ModuleNotFoundError: # pragma: no cover - dependency guard for runtime environments
|
||||
mercantile = None
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
GRID_DIR = PROJECT_ROOT / "data" / "grid"
|
||||
OUTPUT_DIR = Path("/home/wwwroot/weather")
|
||||
|
||||
DEFAULT_ZOOMS = [2, 4, 6, 8, 10, 12]
|
||||
|
||||
|
||||
def get_zoom_levels():
|
||||
raw_value = os.environ.get("WEATHER_TILE_ZOOMS", "")
|
||||
if not raw_value.strip():
|
||||
return DEFAULT_ZOOMS
|
||||
|
||||
zooms = []
|
||||
for chunk in raw_value.split(","):
|
||||
chunk = chunk.strip()
|
||||
if not chunk:
|
||||
continue
|
||||
zoom = int(chunk)
|
||||
if zoom < 0:
|
||||
raise ValueError(f"Invalid zoom level: {zoom}")
|
||||
zooms.append(zoom)
|
||||
|
||||
if not zooms:
|
||||
raise ValueError("WEATHER_TILE_ZOOMS did not contain any usable zoom levels")
|
||||
|
||||
return sorted(set(zooms))
|
||||
|
||||
|
||||
def load_grid(path):
|
||||
with path.open(encoding="utf-8") as file_handle:
|
||||
data = json.load(file_handle)
|
||||
return data["time"], data["grid"]
|
||||
|
||||
|
||||
def get_grid_field(grid, field_name, latitudes, longitudes):
|
||||
values = grid.get(field_name)
|
||||
if values is not None:
|
||||
return values
|
||||
|
||||
return [[0.0 for _ in longitudes] for _ in latitudes]
|
||||
|
||||
|
||||
def grid_to_features(grid):
|
||||
latitudes = grid["lat"]
|
||||
longitudes = grid["lon"]
|
||||
wind_speed = grid["wind_speed"]
|
||||
wind_dir = grid["wind_dir"]
|
||||
rain = grid["rain"]
|
||||
temp = grid["temp"]
|
||||
pressure = grid["pressure"]
|
||||
wave_h = get_grid_field(grid, "wave_h", latitudes, longitudes)
|
||||
wave_dir = get_grid_field(grid, "wave_dir", latitudes, longitudes)
|
||||
wave_period = get_grid_field(grid, "wave_period", latitudes, longitudes)
|
||||
|
||||
features = []
|
||||
for lat_index, latitude in enumerate(latitudes):
|
||||
for lon_index, longitude in enumerate(longitudes):
|
||||
features.append(
|
||||
{
|
||||
"geometry": {"type": "Point", "coordinates": [longitude, latitude]},
|
||||
"properties": {
|
||||
"ws": wind_speed[lat_index][lon_index],
|
||||
"wd": wind_dir[lat_index][lon_index],
|
||||
"r": rain[lat_index][lon_index],
|
||||
"t": temp[lat_index][lon_index],
|
||||
"p": pressure[lat_index][lon_index],
|
||||
"wh": wave_h[lat_index][lon_index],
|
||||
"wdir": wave_dir[lat_index][lon_index],
|
||||
"wp": wave_period[lat_index][lon_index],
|
||||
},
|
||||
}
|
||||
)
|
||||
return features
|
||||
|
||||
|
||||
def bucket_features_by_tile(features, zoom):
|
||||
buckets = defaultdict(list)
|
||||
for feature in features:
|
||||
longitude, latitude = feature["geometry"]["coordinates"]
|
||||
tile = mercantile.tile(longitude, latitude, zoom)
|
||||
buckets[(tile.x, tile.y)].append(feature)
|
||||
return buckets
|
||||
|
||||
|
||||
def write_tile(tile_time, zoom, tile_x, tile_y, features):
|
||||
bounds = mercantile.bounds(mercantile.Tile(x=tile_x, y=tile_y, z=zoom))
|
||||
tile_dir = OUTPUT_DIR / tile_time / str(zoom) / str(tile_x)
|
||||
tile_dir.mkdir(parents=True, exist_ok=True)
|
||||
tile_path = tile_dir / f"{tile_y}.pbf"
|
||||
|
||||
layer = {"name": "weather", "features": features}
|
||||
tile_data = mapbox_vector_tile.encode(
|
||||
layer,
|
||||
default_options={
|
||||
"quantize_bounds": (bounds.west, bounds.south, bounds.east, bounds.north),
|
||||
},
|
||||
)
|
||||
tile_path.write_bytes(tile_data)
|
||||
print("tile", tile_time, zoom, tile_x, tile_y)
|
||||
|
||||
|
||||
def generate_tiles(tile_time, features):
|
||||
for zoom in get_zoom_levels():
|
||||
buckets = bucket_features_by_tile(features, zoom)
|
||||
for (tile_x, tile_y), tile_features in buckets.items():
|
||||
write_tile(tile_time, zoom, tile_x, tile_y, tile_features)
|
||||
|
||||
|
||||
def main():
|
||||
if mapbox_vector_tile is None or mercantile is None:
|
||||
raise RuntimeError("mapbox-vector-tile and mercantile are required to generate vector tiles")
|
||||
|
||||
for path in sorted(GRID_DIR.glob("*.json")):
|
||||
print("processing", path.name)
|
||||
tile_time, grid = load_grid(path)
|
||||
features = grid_to_features(grid)
|
||||
generate_tiles(tile_time, features)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user