Initial import of NavSea pbf project

This commit is contained in:
OpenAI Codex
2026-03-17 19:48:15 +08:00
commit 5f3f7409b3
84 changed files with 43889 additions and 0 deletions

211
src/gfs_downloader.py Normal file
View File

@@ -0,0 +1,211 @@
from pathlib import Path
from datetime import datetime, timedelta, timezone
import time
try:
import requests
except ModuleNotFoundError: # pragma: no cover - dependency guard for runtime environments
requests = None
BASE_URL = "https://nomads.ncep.noaa.gov/cgi-bin/filter_gfs_0p25.pl"
WAVE_BASE_URL = "https://nomads.ncep.noaa.gov/cgi-bin/filter_gfswave.pl"
PROJECT_ROOT = Path(__file__).resolve().parent.parent
OUTPUT_DIR = PROJECT_ROOT / "data" / "grib"
REQUEST_TIMEOUT = (10, 120)
RETRY_LIMIT = 3
MIN_FILE_SIZE_BYTES = 1024
FORECAST_HOURS = [
0, 3, 6, 9, 12, 15, 18, 21, 24,
27, 30, 33, 36, 39, 42, 45,
48, 51, 54, 57, 60, 63, 66,
69, 72,
]
REGION = {
"leftlon": 120,
"rightlon": 150,
"toplat": 50,
"bottomlat": 20,
}
ATMOS_VARIABLES = [
"UGRD",
"VGRD",
"APCP",
"PRMSL",
"TMP",
]
WAVE_VARIABLES = [
"HTSGW",
"DIRPW",
"PERPW",
]
ATMOS_LEVELS = [
"lev_10_m_above_ground",
"lev_surface",
"lev_mean_sea_level",
]
WAVE_LEVELS = [
"lev_surface",
]
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
def get_cycle(reference_time=None):
now = reference_time or datetime.now(timezone.utc)
# NOMADS availability usually lags the wall clock. Bias one cycle back.
candidate = now - timedelta(hours=5)
hour = (candidate.hour // 6) * 6
cycle_time = candidate.replace(hour=hour, minute=0, second=0, microsecond=0)
return cycle_time.strftime("%Y%m%d"), f"{cycle_time.hour:02d}"
def build_atmos_request(date, cycle, forecast_hour):
filename = f"gfs.t{cycle}z.pgrb2.0p25.f{forecast_hour}"
params = {
"file": filename,
"leftlon": REGION["leftlon"],
"rightlon": REGION["rightlon"],
"toplat": REGION["toplat"],
"bottomlat": REGION["bottomlat"],
"dir": f"/gfs.{date}/{cycle}/atmos",
}
for variable in ATMOS_VARIABLES:
params[f"var_{variable}"] = "on"
for level in ATMOS_LEVELS:
params[level] = "on"
return params
def build_wave_request(date, cycle, forecast_hour):
filename = f"gfswave.t{cycle}z.global.0p25.f{forecast_hour}.grib2"
params = {
"file": filename,
"leftlon": REGION["leftlon"],
"rightlon": REGION["rightlon"],
"toplat": REGION["toplat"],
"bottomlat": REGION["bottomlat"],
"dir": f"/gfs.{date}/{cycle}/wave/gridded",
}
for variable in WAVE_VARIABLES:
params[f"var_{variable}"] = "on"
for level in WAVE_LEVELS:
params[level] = "on"
return params
def validate_response(response):
content_type = response.headers.get("Content-Type", "").lower()
if "html" in content_type or "text/plain" in content_type:
preview = response.text[:200].strip().replace("\n", " ")
raise ValueError(f"unexpected response content type {content_type}: {preview}")
def is_fatal_network_error(error):
error_text = str(error)
fatal_markers = (
"NameResolutionError",
"Failed to resolve",
"Temporary failure in name resolution",
)
return any(marker in error_text for marker in fatal_markers)
def download_file(session, base_url, params, output_path):
temp_path = output_path.with_suffix(".grib2.part")
if output_path.exists() and output_path.stat().st_size >= MIN_FILE_SIZE_BYTES:
print("skip", output_path)
return
for attempt in range(1, RETRY_LIMIT + 1):
try:
print(f"downloading {output_path} (attempt {attempt}/{RETRY_LIMIT})")
with session.get(
base_url,
params=params,
stream=True,
timeout=REQUEST_TIMEOUT,
) as response:
response.raise_for_status()
validate_response(response)
with temp_path.open("wb") as file_handle:
for chunk in response.iter_content(1024 * 1024):
if chunk:
file_handle.write(chunk)
if temp_path.stat().st_size < MIN_FILE_SIZE_BYTES:
raise ValueError(f"downloaded file too small: {temp_path.stat().st_size} bytes")
temp_path.replace(output_path)
return
except (requests.RequestException, ValueError) as exc:
if temp_path.exists():
temp_path.unlink()
print(" download failed:", exc)
if is_fatal_network_error(exc):
raise RuntimeError("fatal network error while reaching NOAA") from exc
if attempt == RETRY_LIMIT:
raise
time.sleep(attempt * 2)
def download_forecast(session, date, cycle, forecast_hour):
atmos_path = OUTPUT_DIR / f"{date}_{cycle}_f{forecast_hour}.grib2"
wave_path = OUTPUT_DIR / f"{date}_{cycle}_f{forecast_hour}_wave.grib2"
download_file(
session,
BASE_URL,
build_atmos_request(date, cycle, forecast_hour),
atmos_path,
)
download_file(
session,
WAVE_BASE_URL,
build_wave_request(date, cycle, forecast_hour),
wave_path,
)
def main():
if requests is None:
raise RuntimeError("requests is required to download GFS data")
date, cycle = get_cycle()
print("cycle:", date, cycle)
session = requests.Session()
session.headers["User-Agent"] = "weather-pipeline/1.0"
failures = []
for forecast_hour in FORECAST_HOURS:
forecast_hour_str = f"{forecast_hour:03d}"
try:
download_forecast(session, date, cycle, forecast_hour_str)
except RuntimeError as exc:
failures.append((forecast_hour_str, str(exc)))
break
except Exception as exc:
failures.append((forecast_hour_str, str(exc)))
if failures:
for forecast_hour, error in failures:
print(f"failed forecast {forecast_hour}: {error}")
raise SystemExit(1)
if __name__ == "__main__":
main()

189
src/grid_builder_v2.py Normal file
View File

@@ -0,0 +1,189 @@
from pathlib import Path
import json
import warnings
import numpy as np
import xarray as xr
try:
import cfgrib
except ModuleNotFoundError: # pragma: no cover - dependency guard for runtime environments
cfgrib = None
PROJECT_ROOT = Path(__file__).resolve().parent.parent
INPUT_DIR = PROJECT_ROOT / "data" / "grib"
OUTPUT_DIR = PROJECT_ROOT / "data" / "grid"
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
REGION = {
"lon_min": 120,
"lon_max": 150,
"lat_min": 20,
"lat_max": 50,
}
VARIABLE_CANDIDATES = {
"u10": ("u10", "u"),
"v10": ("v10", "v"),
"tp": ("tp", "prate", "unknown"),
"msl": ("prmsl", "msl", "pres"),
"temp": ("t2m", "t"),
"wave_h": ("htsgw", "swh", "wvhgt"),
"wave_dir": ("dirpw", "mwd", "wvdir"),
"wave_period": ("perpw", "mwp", "wvper"),
}
def compute_wind(u_component, v_component):
speed = np.sqrt(u_component ** 2 + v_component ** 2)
# Meteorological direction: where the wind comes from, in degrees clockwise from north.
direction = (270 - np.degrees(np.arctan2(v_component, u_component))) % 360
return speed, direction
def load_datasets(path):
if cfgrib is None:
raise RuntimeError("cfgrib is required to build grids from GRIB2 files")
with xr.set_options(use_new_combine_kwarg_defaults=True):
with warnings.catch_warnings():
warnings.filterwarnings(
"ignore",
message="In a future version of xarray the default value for compat will change",
category=FutureWarning,
)
return cfgrib.xarray_store.open_datasets(str(path))
def find_variable(datasets, candidates):
for candidate in candidates:
for dataset in datasets:
if candidate in dataset:
return dataset[candidate]
return None
def get_lat_lon(datasets):
for dataset in datasets:
if "latitude" in dataset and "longitude" in dataset:
return dataset["latitude"].values, dataset["longitude"].values
raise ValueError("no latitude/longitude coordinates found in GRIB datasets")
def to_2d_values(data_array, lat_size, lon_size):
if data_array is None:
return np.zeros((lat_size, lon_size), dtype=float)
values = np.asarray(data_array.squeeze().values)
if values.ndim != 2:
raise ValueError(f"expected 2D field, got shape {values.shape} for {data_array.name}")
if values.shape != (lat_size, lon_size):
raise ValueError(
f"field {data_array.name} shape {values.shape} does not match coordinates {(lat_size, lon_size)}"
)
return values
def normalize_longitudes(lon_values, fields):
lon_values = np.asarray(lon_values, dtype=float)
normalized_lon = ((lon_values + 180) % 360) - 180
sort_idx = np.argsort(normalized_lon)
normalized_lon = normalized_lon[sort_idx]
normalized_fields = [field[:, sort_idx] for field in fields]
return normalized_lon, normalized_fields
def select_region(lat_values, lon_values, fields):
lat_mask = (lat_values >= REGION["lat_min"]) & (lat_values <= REGION["lat_max"])
lon_mask = (lon_values >= REGION["lon_min"]) & (lon_values <= REGION["lon_max"])
lat_idx = np.where(lat_mask)[0]
lon_idx = np.where(lon_mask)[0]
if lat_idx.size == 0 or lon_idx.size == 0:
raise ValueError("selected region is empty after coordinate filtering")
return (
lat_values[lat_idx],
lon_values[lon_idx],
[field[np.ix_(lat_idx, lon_idx)] for field in fields],
)
def process_file(path):
datasets = load_datasets(path)
try:
wave_path = path.with_name(f"{path.stem}_wave.grib2")
wave_datasets = load_datasets(wave_path) if wave_path.exists() else []
all_datasets = [*datasets, *wave_datasets]
lat_values, lon_values = get_lat_lon(datasets)
lat_size = len(lat_values)
lon_size = len(lon_values)
u10 = to_2d_values(find_variable(all_datasets, VARIABLE_CANDIDATES["u10"]), lat_size, lon_size)
v10 = to_2d_values(find_variable(all_datasets, VARIABLE_CANDIDATES["v10"]), lat_size, lon_size)
rain = to_2d_values(find_variable(all_datasets, VARIABLE_CANDIDATES["tp"]), lat_size, lon_size)
pressure = to_2d_values(find_variable(all_datasets, VARIABLE_CANDIDATES["msl"]), lat_size, lon_size)
temp = to_2d_values(find_variable(all_datasets, VARIABLE_CANDIDATES["temp"]), lat_size, lon_size)
wave_h = to_2d_values(find_variable(all_datasets, VARIABLE_CANDIDATES["wave_h"]), lat_size, lon_size)
wave_dir = to_2d_values(find_variable(all_datasets, VARIABLE_CANDIDATES["wave_dir"]), lat_size, lon_size)
wave_period = to_2d_values(find_variable(all_datasets, VARIABLE_CANDIDATES["wave_period"]), lat_size, lon_size)
wind_speed, wind_dir = compute_wind(u10, v10)
lon_values, normalized_fields = normalize_longitudes(
lon_values,
[wind_speed, wind_dir, rain, temp, pressure, wave_h, wave_dir, wave_period],
)
wind_speed, wind_dir, rain, temp, pressure, wave_h, wave_dir, wave_period = normalized_fields
lat_region, lon_region, region_fields = select_region(
lat_values,
lon_values,
[wind_speed, wind_dir, rain, temp, pressure, wave_h, wave_dir, wave_period],
)
wind_speed, wind_dir, rain, temp, pressure, wave_h, wave_dir, wave_period = region_fields
return {
"lat": lat_region.tolist(),
"lon": lon_region.tolist(),
"wind_speed": wind_speed.tolist(),
"wind_dir": wind_dir.tolist(),
"rain": rain.tolist(),
"temp": temp.tolist(),
"pressure": pressure.tolist(),
"wave_h": wave_h.tolist(),
"wave_dir": wave_dir.tolist(),
"wave_period": wave_period.tolist(),
}
finally:
for dataset in datasets:
dataset.close()
if 'wave_datasets' in locals():
for dataset in wave_datasets:
dataset.close()
def main():
for path in sorted(INPUT_DIR.glob("*.grib2")):
if path.stem.endswith("_wave"):
continue
print("processing", path.name)
try:
grid = process_file(path)
except Exception as exc:
print(" error processing", path.name, exc)
continue
output_path = OUTPUT_DIR / f"grid_{path.stem}.json"
payload = {
"time": path.stem,
"grid": grid,
}
with output_path.open("w", encoding="utf-8") as file_handle:
json.dump(payload, file_handle)
print("saved", output_path)
if __name__ == "__main__":
main()

104
src/pbf/audit_pbf_mysql.py Normal file
View File

@@ -0,0 +1,104 @@
import pymysql
import os
OUTPUT_DIR = "report"
os.makedirs(OUTPUT_DIR, exist_ok=True)
conn = pymysql.connect(
host="localhost",
user="root",
password="2chi9ks2",
database="pbf_analysis",
charset="utf8mb4"
)
cur = conn.cursor()
def write_file(name, rows):
path = os.path.join(OUTPUT_DIR, name)
with open(path,"w",encoding="utf8") as f:
for r in rows:
f.write("\t".join([str(x) for x in r])+"\n")
print("write:",path)
print("Layer summary...")
cur.execute("""
SELECT vt_layer, COUNT(*)
FROM features
GROUP BY vt_layer
ORDER BY COUNT(*) DESC
""")
write_file("layer_summary.txt",cur.fetchall())
print("Geometry summary...")
cur.execute("""
SELECT vt_layer, geom_type, COUNT(*)
FROM features
GROUP BY vt_layer, geom_type
ORDER BY vt_layer
""")
write_file("geometry_summary.txt",cur.fetchall())
print("Property keys...")
cur.execute("""
SELECT k, COUNT(*)
FROM properties
GROUP BY k
ORDER BY COUNT(*) DESC
""")
write_file("property_keys.txt",cur.fetchall())
print("Layer properties...")
cur.execute("""
SELECT f.vt_layer, p.k, COUNT(*)
FROM properties p
JOIN features f ON p.feature_id=f.id
GROUP BY f.vt_layer,p.k
ORDER BY f.vt_layer
""")
write_file("layer_properties.txt",cur.fetchall())
print("Classification distribution...")
cur.execute("""
SELECT v, COUNT(*)
FROM properties
WHERE k='分類番号'
GROUP BY v
ORDER BY COUNT(*) DESC
""")
write_file("classification_distribution.txt",cur.fetchall())
print("Tile density...")
cur.execute("""
SELECT z,x,y,COUNT(*)
FROM features
GROUP BY z,x,y
ORDER BY COUNT(*) DESC
LIMIT 100
""")
write_file("tile_density.txt",cur.fetchall())
print("done.")

5652
src/pbf/style.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,692 @@
{
"version": 8,
"name": "navsea-redesign",
"glyphs": "http://192.168.200.184/newpec/fonts/{fontstack}/{range}.pbf",
"sources": {
"navsea": {
"type": "vector",
"minzoom": 0,
"maxzoom": 12,
"tiles": [
"http://192.168.200.184/pbf/{z}/{x}/{y}.pbf"
],
"attribution": "NavSea semantic vector tiles"
}
},
"layers": [
{
"id": "background",
"type": "background",
"paint": {
"background-color": "#dfe8d8"
}
},
{
"id": "land-area",
"type": "fill",
"source": "navsea",
"source-layer": "P陸域",
"paint": {
"fill-color": "#d9cfab"
}
},
{
"id": "land-hole",
"type": "fill",
"source": "navsea",
"source-layer": "P穴",
"paint": {
"fill-color": "#bfc8b1"
}
},
{
"id": "water-depth-fill",
"type": "fill",
"source": "navsea",
"source-layer": "P基本線",
"filter": [
"match",
[
"get",
"canonical_family"
],
[
"surface"
],
true,
false
],
"paint": {
"fill-color": [
"match",
[
"get",
"canonical_object_type"
],
"0-2m",
"#d8f3f6",
"0-5m",
"#d2eff4",
"2-5m",
"#c8eaf0",
"5-10m",
"#bce3ec",
"10-20m",
"#b0dbe8",
"20-100m",
"#a0d2e0",
"100-200m",
"#93c7d6",
"200-500m",
"#7eb7ca",
"500-1000m",
"#68a6bc",
"1000-2000m",
"#558eab",
"#dff3f7"
],
"fill-opacity": 0.9
}
},
{
"id": "shoal-hazard-fill",
"type": "fill",
"source": "navsea",
"source-layer": "P基本線",
"filter": [
"==",
[
"get",
"canonical_object_type"
],
"浅所危険界"
],
"paint": {
"fill-color": "#f2a65a",
"fill-opacity": 0.45
}
},
{
"id": "tidal-flat-fill",
"type": "fill",
"source": "navsea",
"source-layer": "P基本線",
"filter": [
"==",
[
"get",
"canonical_object_type"
],
"干潮帯"
],
"paint": {
"fill-color": "#d7d0b4",
"fill-opacity": 0.75
}
},
{
"id": "river-lake-fill",
"type": "fill",
"source": "navsea",
"source-layer": "P基本線",
"filter": [
"match",
[
"get",
"canonical_object_type"
],
[
"河川域",
"湖沼域",
"陸上水域"
],
true,
false
],
"paint": {
"fill-color": "#b8d8e8",
"fill-opacity": 0.85
}
},
{
"id": "unsurveyed-fill",
"type": "fill",
"source": "navsea",
"source-layer": "P基本線",
"filter": [
"==",
[
"get",
"canonical_object_type"
],
"未測海域"
],
"paint": {
"fill-color": "#f7f0b8",
"fill-opacity": 0.5
}
},
{
"id": "coast-structure-fill",
"type": "fill",
"source": "navsea",
"source-layer": "P基本線",
"filter": [
"match",
[
"get",
"canonical_object_type"
],
[
"防波堤",
"浮施設・桟橋",
"撤去跡"
],
true,
false
],
"paint": {
"fill-color": "#8d8578",
"fill-opacity": 0.85
}
},
{
"id": "bridge-fill",
"type": "fill",
"source": "navsea",
"source-layer": "P橋りょう等構造物",
"paint": {
"fill-color": "#736b5f",
"fill-opacity": 0.85
}
},
{
"id": "fishery-fill",
"type": "fill",
"source": "navsea",
"source-layer": "P漁具定置箇所",
"paint": {
"fill-color": "#89b26f",
"fill-opacity": 0.45,
"fill-outline-color": "#56764a"
}
},
{
"id": "land-structure-fill",
"type": "fill",
"source": "navsea",
"source-layer": "P陸上構造物陸",
"paint": {
"fill-color": "#8e6f3e",
"fill-opacity": 0.8
}
},
{
"id": "coast-outline",
"type": "line",
"source": "navsea",
"source-layer": "P基本線ククリ",
"paint": {
"line-color": "#736b5f",
"line-width": 1.2
}
},
{
"id": "danger-outline",
"type": "line",
"source": "navsea",
"source-layer": "P危険界ククリ",
"paint": {
"line-color": "#c95c2a",
"line-width": 1.3,
"line-dasharray": [
2,
1
]
}
},
{
"id": "anchor-danger-outline",
"type": "line",
"source": "navsea",
"source-layer": "P投錨注意障害物ククリ",
"paint": {
"line-color": "#c03d3d",
"line-width": 1.1,
"line-dasharray": [
1,
1
]
}
},
{
"id": "bathymetry-lines",
"type": "line",
"source": "navsea",
"source-layer": "L海底地形",
"paint": {
"line-color": "#7fa6b7",
"line-width": 0.5,
"line-opacity": 0.45
}
},
{
"id": "depth-contours",
"type": "line",
"source": "navsea",
"source-layer": "L等深線",
"paint": {
"line-color": "#487b94",
"line-width": [
"interpolate",
[
"linear"
],
[
"zoom"
],
5,
0.3,
8,
0.6,
12,
1.1
],
"line-opacity": 0.85
}
},
{
"id": "subsea-lines",
"type": "line",
"source": "navsea",
"source-layer": "L海底線",
"paint": {
"line-color": "#8d4f9c",
"line-width": 1.2,
"line-dasharray": [
3,
1
]
}
},
{
"id": "coast-structure-lines",
"type": "line",
"source": "navsea",
"source-layer": "L基本線",
"paint": {
"line-color": "#60584d",
"line-width": 1.4
}
},
{
"id": "land-structure-lines",
"type": "line",
"source": "navsea",
"source-layer": "L陸上構造物陸",
"paint": {
"line-color": "#805f2c",
"line-width": 1
}
},
{
"id": "water-boundary-line",
"type": "line",
"source": "navsea",
"source-layer": "L739",
"paint": {
"line-color": "#4b89a5",
"line-width": 1.1
}
},
{
"id": "regulatory-boundary-line",
"type": "line",
"source": "navsea",
"source-layer": "L741",
"paint": {
"line-color": "#cf5b47",
"line-width": 1.2,
"line-dasharray": [
2,
2
]
}
},
{
"id": "height-limit-line",
"type": "line",
"source": "navsea",
"source-layer": "L高さ制限",
"paint": {
"line-color": "#b1466b",
"line-width": 1.1
}
},
{
"id": "hazard-area-fill",
"type": "fill",
"source": "navsea",
"source-layer": "P投錨注意障害物",
"paint": {
"fill-color": "#d85f4c",
"fill-opacity": 0.42,
"fill-outline-color": "#9b3324"
}
},
{
"id": "hazard-points",
"type": "circle",
"source": "navsea",
"source-layer": "p航行危険障害物",
"paint": {
"circle-radius": [
"interpolate",
[
"linear"
],
[
"zoom"
],
7,
2,
10,
4,
12,
6
],
"circle-color": "#d5452f",
"circle-stroke-color": "#fff4df",
"circle-stroke-width": 1.1
}
},
{
"id": "anchor-hazard-points",
"type": "circle",
"source": "navsea",
"source-layer": "p投錨注意障害物",
"paint": {
"circle-radius": [
"interpolate",
[
"linear"
],
[
"zoom"
],
7,
2,
10,
4,
12,
6
],
"circle-color": "#cf7d2b",
"circle-stroke-color": "#fff4df",
"circle-stroke-width": 1
}
},
{
"id": "nav-marks",
"type": "circle",
"source": "navsea",
"source-layer": "p航路標識群",
"paint": {
"circle-radius": [
"interpolate",
[
"linear"
],
[
"zoom"
],
7,
2,
10,
4,
12,
6
],
"circle-color": "#f6ce4f",
"circle-stroke-color": "#313131",
"circle-stroke-width": 1
}
},
{
"id": "facility-points",
"type": "circle",
"source": "navsea",
"source-layer": "p施設・境界線等",
"paint": {
"circle-radius": [
"interpolate",
[
"linear"
],
[
"zoom"
],
8,
2,
12,
5
],
"circle-color": "#4d6b86",
"circle-stroke-color": "#ffffff",
"circle-stroke-width": 0.8
}
},
{
"id": "landmark-points",
"type": "circle",
"source": "navsea",
"source-layer": "p陸上構造物",
"paint": {
"circle-radius": [
"interpolate",
[
"linear"
],
[
"zoom"
],
8,
2,
12,
4.5
],
"circle-color": "#6b5840",
"circle-stroke-color": "#f7f1e2",
"circle-stroke-width": 0.8
}
},
{
"id": "bottom-material-points",
"type": "circle",
"source": "navsea",
"source-layer": "p底質",
"paint": {
"circle-radius": 1.5,
"circle-color": "#3f7183",
"circle-opacity": 0.55
}
},
{
"id": "height-limit-points",
"type": "circle",
"source": "navsea",
"source-layer": "p高さ制限",
"paint": {
"circle-radius": 3,
"circle-color": "#a83566",
"circle-stroke-color": "#ffffff",
"circle-stroke-width": 0.8
}
},
{
"id": "sea-place-labels",
"type": "symbol",
"source": "navsea",
"source-layer": "p地名",
"layout": {
"text-field": [
"coalesce",
[
"get",
"日本語地名"
],
[
"get",
"英文字地名"
],
[
"get",
"name"
]
],
"text-font": [
"NotoSerifCJK-Regular"
],
"text-size": [
"interpolate",
[
"linear"
],
[
"zoom"
],
6,
9,
12,
14
],
"text-anchor": "center",
"text-max-width": 10
},
"paint": {
"text-color": "#284857",
"text-halo-color": "#f6fbff",
"text-halo-width": 1
}
},
{
"id": "land-place-labels",
"type": "symbol",
"source": "navsea",
"source-layer": "p地名陸",
"layout": {
"text-field": [
"coalesce",
[
"get",
"日本語地名"
],
[
"get",
"英文字地名"
],
[
"get",
"name"
]
],
"text-font": [
"NotoSerifCJK-Regular"
],
"text-size": [
"interpolate",
[
"linear"
],
[
"zoom"
],
6,
9,
12,
14
],
"text-anchor": "center",
"text-max-width": 10
},
"paint": {
"text-color": "#3a3327",
"text-halo-color": "#fff9ef",
"text-halo-width": 1
}
},
{
"id": "nav-object-labels",
"type": "symbol",
"source": "navsea",
"source-layer": "p航路標識群",
"minzoom": 10,
"layout": {
"text-field": [
"coalesce",
[
"get",
"名称"
],
[
"get",
"name"
],
[
"get",
"canonical_object_type"
]
],
"text-font": [
"NotoSansCJK-Regular"
],
"text-size": 11,
"text-offset": [
0,
1.1
],
"text-anchor": "top",
"text-max-width": 12
},
"paint": {
"text-color": "#2f2f2f",
"text-halo-color": "#fff8e0",
"text-halo-width": 1.1
}
},
{
"id": "hazard-labels",
"type": "symbol",
"source": "navsea",
"source-layer": "p航行危険障害物",
"minzoom": 10,
"layout": {
"text-field": [
"coalesce",
[
"get",
"名称"
],
[
"get",
"canonical_object_type"
]
],
"text-font": [
"NotoSansCJK-Regular"
],
"text-size": 11,
"text-offset": [
0,
1.1
],
"text-anchor": "top"
},
"paint": {
"text-color": "#7f221a",
"text-halo-color": "#fff3eb",
"text-halo-width": 1
}
}
]
}

View File

@@ -0,0 +1,679 @@
{
"version": 8,
"name": "navsea-semantic-only",
"glyphs": "http://192.168.200.184/newpec/fonts/{fontstack}/{range}.pbf",
"sources": {
"navsea": {
"type": "vector",
"minzoom": 0,
"maxzoom": 12,
"tiles": [
"http://192.168.200.184/pbf/{z}/{x}/{y}.pbf"
],
"attribution": "NavSea semantic vector tiles"
}
},
"layers": [
{
"id": "background",
"type": "background",
"paint": {
"background-color": "#edf3ef"
}
},
{
"id": "land-fill",
"type": "fill",
"source": "navsea",
"source-layer": "P陸域",
"paint": {
"fill-color": "#d8cda8"
}
},
{
"id": "hole-fill",
"type": "fill",
"source": "navsea",
"source-layer": "P穴",
"paint": {
"fill-color": "#c7cfbb"
}
},
{
"id": "surface-water",
"type": "fill",
"source": "navsea",
"source-layer": "P基本線",
"filter": [
"match",
[
"get",
"canonical_object_type"
],
[
"0-2m",
"0-5m",
"2-5m",
"5-10m",
"10-20m",
"20-100m",
"100-200m",
"200-500m",
"500-1000m",
"1000-2000m",
"2000-3000m",
"3000-4000m",
"4000-5000m",
"5000-6000m",
"6000-7000m",
"7000-8000m",
"8000-9000m",
"9000m以深",
"干潮帯",
"河川域",
"湖沼域",
"陸上水域"
],
true,
false
],
"paint": {
"fill-color": [
"match",
[
"get",
"canonical_object_type"
],
"干潮帯",
"#d7cfb4",
"河川域",
"#afd8e7",
"湖沼域",
"#afd8e7",
"陸上水域",
"#afd8e7",
"0-2m",
"#d8f1f4",
"0-5m",
"#d0edf1",
"2-5m",
"#c4e7ed",
"5-10m",
"#b6dfe8",
"10-20m",
"#a8d6e1",
"20-100m",
"#96cbd8",
"100-200m",
"#84bfd0",
"200-500m",
"#71afc2",
"500-1000m",
"#629fb4",
"1000-2000m",
"#548ea4",
"2000-3000m",
"#497c93",
"3000-4000m",
"#3f6c84",
"4000-5000m",
"#365e75",
"5000-6000m",
"#2f5268",
"6000-7000m",
"#28475d",
"7000-8000m",
"#213d52",
"8000-9000m",
"#1d3448",
"#182c3f"
],
"fill-opacity": 0.88
}
},
{
"id": "hazard-area",
"type": "fill",
"source": "navsea",
"source-layer": "P基本線",
"filter": [
"match",
[
"get",
"canonical_object_type"
],
[
"浅所危険界",
"未測海域",
"浮施設・桟橋",
"防波堤",
"撤去跡"
],
true,
false
],
"paint": {
"fill-color": [
"match",
[
"get",
"canonical_object_type"
],
"浅所危険界",
"#ee9f56",
"未測海域",
"#eadf9b",
"浮施設・桟橋",
"#88857d",
"防波堤",
"#7d796f",
"#9d7c67"
],
"fill-opacity": 0.45
}
},
{
"id": "fishery-area",
"type": "fill",
"source": "navsea",
"source-layer": "P漁具定置箇所",
"paint": {
"fill-color": "#7db065",
"fill-opacity": 0.38,
"fill-outline-color": "#507541"
}
},
{
"id": "hazard-polygon",
"type": "fill",
"source": "navsea",
"source-layer": "P投錨注意障害物",
"paint": {
"fill-color": "#d15d49",
"fill-opacity": 0.4,
"fill-outline-color": "#8b3226"
}
},
{
"id": "bridge-fill",
"type": "fill",
"source": "navsea",
"source-layer": "P橋りょう等構造物",
"paint": {
"fill-color": "#71685b",
"fill-opacity": 0.8
}
},
{
"id": "land-structure-fill",
"type": "fill",
"source": "navsea",
"source-layer": "P陸上構造物陸",
"paint": {
"fill-color": "#896d3d",
"fill-opacity": 0.82
}
},
{
"id": "submerged-fill",
"type": "fill",
"source": "navsea",
"source-layer": "P潜堤",
"paint": {
"fill-color": "#757d95",
"fill-opacity": 0.45
}
},
{
"id": "depth-lines",
"type": "line",
"source": "navsea",
"source-layer": "L等深線",
"paint": {
"line-color": "#447a94",
"line-width": [
"interpolate",
[
"linear"
],
[
"zoom"
],
5,
0.3,
8,
0.6,
12,
1.1
],
"line-opacity": 0.9
}
},
{
"id": "major-depth-lines",
"type": "line",
"source": "navsea",
"source-layer": "L概略等深線",
"paint": {
"line-color": "#275f77",
"line-width": [
"interpolate",
[
"linear"
],
[
"zoom"
],
5,
0.5,
8,
0.9,
12,
1.5
]
}
},
{
"id": "bathymetry-support",
"type": "line",
"source": "navsea",
"source-layer": "L海底地形",
"paint": {
"line-color": "#8aadb9",
"line-width": 0.45,
"line-opacity": 0.4
}
},
{
"id": "cable-line",
"type": "line",
"source": "navsea",
"source-layer": "L海底線",
"paint": {
"line-color": "#7d4a94",
"line-width": 1.2,
"line-dasharray": [
3,
1
]
}
},
{
"id": "coast-structure-line",
"type": "line",
"source": "navsea",
"source-layer": "L基本線",
"paint": {
"line-color": "#635d53",
"line-width": 1.4
}
},
{
"id": "land-structure-line",
"type": "line",
"source": "navsea",
"source-layer": "L陸上構造物陸",
"paint": {
"line-color": "#7b5d2e",
"line-width": 1
}
},
{
"id": "boundary-line",
"type": "line",
"source": "navsea",
"source-layer": "L739",
"paint": {
"line-color": "#4f8ba5",
"line-width": 1.1
}
},
{
"id": "regulatory-line",
"type": "line",
"source": "navsea",
"source-layer": "L741",
"paint": {
"line-color": "#c95b46",
"line-width": 1.2,
"line-dasharray": [
2,
2
]
}
},
{
"id": "height-limit-line",
"type": "line",
"source": "navsea",
"source-layer": "L高さ制限",
"paint": {
"line-color": "#a93a62",
"line-width": 1.1
}
},
{
"id": "coast-outline",
"type": "line",
"source": "navsea",
"source-layer": "P基本線ククリ",
"paint": {
"line-color": "#7d7566",
"line-width": 1
}
},
{
"id": "danger-outline",
"type": "line",
"source": "navsea",
"source-layer": "P危険界ククリ",
"paint": {
"line-color": "#c35f31",
"line-width": 1.2,
"line-dasharray": [
2,
1
]
}
},
{
"id": "anchor-outline",
"type": "line",
"source": "navsea",
"source-layer": "P投錨注意障害物ククリ",
"paint": {
"line-color": "#b64335",
"line-width": 1.1,
"line-dasharray": [
1,
1
]
}
},
{
"id": "hazard-points",
"type": "circle",
"source": "navsea",
"source-layer": "p航行危険障害物",
"paint": {
"circle-radius": [
"interpolate",
[
"linear"
],
[
"zoom"
],
7,
2,
10,
4,
12,
6
],
"circle-color": "#d1442f",
"circle-stroke-color": "#fff4ea",
"circle-stroke-width": 1
}
},
{
"id": "anchor-hazard-points",
"type": "circle",
"source": "navsea",
"source-layer": "p投錨注意障害物",
"paint": {
"circle-radius": [
"interpolate",
[
"linear"
],
[
"zoom"
],
7,
2,
10,
4,
12,
6
],
"circle-color": "#d18a38",
"circle-stroke-color": "#fff6e7",
"circle-stroke-width": 1
}
},
{
"id": "nav-points",
"type": "circle",
"source": "navsea",
"source-layer": "p航路標識群",
"paint": {
"circle-radius": [
"interpolate",
[
"linear"
],
[
"zoom"
],
7,
2,
10,
4,
12,
6
],
"circle-color": "#f1cb4d",
"circle-stroke-color": "#313131",
"circle-stroke-width": 1
}
},
{
"id": "facility-points",
"type": "circle",
"source": "navsea",
"source-layer": "p施設・境界線等",
"paint": {
"circle-radius": 3.5,
"circle-color": "#4f6b86",
"circle-stroke-color": "#ffffff",
"circle-stroke-width": 0.8
}
},
{
"id": "bottom-points",
"type": "circle",
"source": "navsea",
"source-layer": "p底質",
"paint": {
"circle-radius": 1.8,
"circle-color": "#3d7182",
"circle-opacity": 0.65
}
},
{
"id": "landmark-points",
"type": "circle",
"source": "navsea",
"source-layer": "p陸上構造物",
"paint": {
"circle-radius": 3,
"circle-color": "#6a5640",
"circle-stroke-color": "#fff7eb",
"circle-stroke-width": 0.8
}
},
{
"id": "height-points",
"type": "circle",
"source": "navsea",
"source-layer": "p高さ制限",
"paint": {
"circle-radius": 3,
"circle-color": "#aa365f",
"circle-stroke-color": "#ffffff",
"circle-stroke-width": 0.8
}
},
{
"id": "sea-labels",
"type": "symbol",
"source": "navsea",
"source-layer": "p地名",
"layout": {
"text-field": [
"coalesce",
[
"get",
"日本語地名"
],
[
"get",
"英文字地名"
],
[
"get",
"canonical_object_type"
]
],
"text-font": [
"NotoSerifCJK-Regular"
],
"text-size": [
"interpolate",
[
"linear"
],
[
"zoom"
],
6,
9,
12,
15
],
"text-anchor": "center",
"text-max-width": 10
},
"paint": {
"text-color": "#264858",
"text-halo-color": "#f8fcff",
"text-halo-width": 1
}
},
{
"id": "land-labels",
"type": "symbol",
"source": "navsea",
"source-layer": "p地名陸",
"layout": {
"text-field": [
"coalesce",
[
"get",
"日本語地名"
],
[
"get",
"英文字地名"
],
[
"get",
"canonical_object_type"
]
],
"text-font": [
"NotoSerifCJK-Regular"
],
"text-size": [
"interpolate",
[
"linear"
],
[
"zoom"
],
6,
9,
12,
15
],
"text-anchor": "center",
"text-max-width": 10
},
"paint": {
"text-color": "#3a3228",
"text-halo-color": "#fff8ef",
"text-halo-width": 1
}
},
{
"id": "nav-labels",
"type": "symbol",
"source": "navsea",
"source-layer": "p航路標識群",
"minzoom": 10,
"layout": {
"text-field": [
"coalesce",
[
"get",
"名称"
],
[
"get",
"canonical_object_type"
]
],
"text-font": [
"NotoSansCJK-Regular"
],
"text-size": 11,
"text-offset": [
0,
1.1
],
"text-anchor": "top"
},
"paint": {
"text-color": "#303030",
"text-halo-color": "#fff8df",
"text-halo-width": 1
}
},
{
"id": "hazard-labels",
"type": "symbol",
"source": "navsea",
"source-layer": "p航行危険障害物",
"minzoom": 10,
"layout": {
"text-field": [
"coalesce",
[
"get",
"名称"
],
[
"get",
"canonical_object_type"
]
],
"text-font": [
"NotoSansCJK-Regular"
],
"text-size": 11,
"text-offset": [
0,
1.1
],
"text-anchor": "top"
},
"paint": {
"text-color": "#7c2219",
"text-halo-color": "#fff4ec",
"text-halo-width": 1
}
}
]
}

1391
src/pbf/style.navsea-v2.json Normal file

File diff suppressed because it is too large Load Diff

2232
src/pbf/style.pruned.json Normal file

File diff suppressed because it is too large Load Diff

View 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()

86
src/weather_pipeline.py Normal file
View File

@@ -0,0 +1,86 @@
from pathlib import Path
import importlib.util
import subprocess
import sys
import time
SCRIPT_DIR = Path(__file__).resolve().parent
DOWNLOADER = SCRIPT_DIR / "gfs_downloader.py"
GRID_BUILDER = SCRIPT_DIR / "grid_builder_v2.py"
TILE_GENERATOR = SCRIPT_DIR / "vector_tile_generator.py"
STEP_DEPENDENCIES = {
"GFS Downloader": ("requests",),
"Grid Builder v2": ("numpy", "cfgrib"),
"Vector Tile Generator": ("mercantile", "mapbox_vector_tile"),
}
def check_dependencies():
missing = []
for step_name, modules in STEP_DEPENDENCIES.items():
missing_modules = [module for module in modules if importlib.util.find_spec(module) is None]
if missing_modules:
missing.append(f"{step_name}: {', '.join(missing_modules)}")
return missing
def run_step(name, script_path):
print("\n==========================")
print("Running:", name)
print("==========================\n")
start = time.time()
command = [sys.executable, "-u", str(script_path)]
process = subprocess.Popen(
command,
cwd=str(SCRIPT_DIR.parent),
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
assert process.stdout is not None
for line in process.stdout:
print(line, end="")
return_code = process.wait()
if return_code != 0:
raise RuntimeError(f"{name} failed with exit code {return_code}")
end = time.time()
print("\nFinished:", name)
print("Time:", round(end - start, 2), "seconds")
def main():
print("Weather Pipeline Starting...")
print("Date:", time.strftime("%Y-%m-%d %H:%M:%S"))
missing_dependencies = check_dependencies()
if missing_dependencies:
print("\n" + "!" * 50)
print("Pipeline Failed: missing runtime dependencies")
for item in missing_dependencies:
print("-", item)
print("!" * 50)
raise SystemExit(1)
try:
run_step("GFS Downloader", DOWNLOADER)
run_step("Grid Builder v2", GRID_BUILDER)
run_step("Vector Tile Generator", TILE_GENERATOR)
print("\n" + "=" * 50)
print("Weather Pipeline Completed Successfully!")
print("=" * 50)
except RuntimeError as exc:
print("\n" + "!" * 50)
print("Pipeline Failed:", str(exc))
print("!" * 50)
raise SystemExit(1)
if __name__ == "__main__":
main()