Source code for contrailopt.waypoints
"""Download and parse FAA NASR waypoint data."""
import io
import pathlib
import re
import zipfile
from datetime import UTC, date, datetime, timedelta
import pandas as pd
import platformdirs
import requests
NASR_SUBSCRIPTION_URL = (
"https://www.faa.gov/air_traffic/flight_info/aeronav/aero_data/NASR_Subscription/"
)
NASR_EFFECTIVE_DATE_RE = re.compile(r"NASR_Subscription/(\d{4}-\d{2}-\d{2})", flags=re.IGNORECASE)
CACHE_PATH = pathlib.Path(platformdirs.user_cache_dir("contrailopt")) / "faa_waypoints.parquet"
CACHE_MAX_AGE = timedelta(days=90)
HTTP_TIMEOUT_SECONDS = 60.0
def _download_bytes(url: str) -> bytes:
response = requests.get(url, timeout=HTTP_TIMEOUT_SECONDS)
response.raise_for_status()
return response.content
def _latest_subscription_zip_url() -> str:
html = _download_bytes(NASR_SUBSCRIPTION_URL).decode("utf-8", errors="replace")
dates = sorted(date.fromisoformat(d) for d in NASR_EFFECTIVE_DATE_RE.findall(html))
if not dates:
raise ValueError("Could not find NASR subscription effective dates on the FAA page.")
today = datetime.now(UTC).date()
current_or_past = [d for d in dates if d <= today]
effective_date = max(current_or_past) if current_or_past else max(dates)
return (
"https://nfdc.faa.gov/webContent/28DaySub/"
f"28DaySubscription_Effective_{effective_date.isoformat()}.zip"
)
def _cache_is_fresh(cache_path: pathlib.Path) -> bool:
if not cache_path.exists():
return False
modified_at = datetime.fromtimestamp(cache_path.stat().st_mtime, tz=UTC)
return (datetime.now(UTC) - modified_at) <= CACHE_MAX_AGE
def _read_cached_waypoints(refresh_cache: bool) -> pd.DataFrame | None:
if refresh_cache or not CACHE_PATH.exists():
return None
if not _cache_is_fresh(CACHE_PATH):
return None
return pd.read_parquet(CACHE_PATH)
def _write_waypoints_cache(waypoints: pd.DataFrame) -> None:
CACHE_PATH.parent.mkdir(parents=True, exist_ok=True)
waypoints.to_parquet(CACHE_PATH, index=False)
def _load_waypoints_from_zip_bytes(zip_bytes: bytes) -> pd.DataFrame:
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as outer_zip:
for name in outer_zip.namelist():
if name.lower().endswith("_csv.zip"):
csv_bundle_name = name
break
else:
raise ValueError("Could not find the CSV bundle in the FAA subscription archive.")
with outer_zip.open(csv_bundle_name) as csv_bundle_file:
csv_bundle_bytes = csv_bundle_file.read()
with zipfile.ZipFile(io.BytesIO(csv_bundle_bytes)) as csv_bundle:
names = csv_bundle.namelist()
if "FIX_BASE.csv" not in names:
raise ValueError("Could not find FIX_BASE.csv in the CSV bundle.")
# Apply some filtering ... this should be revisited
usecols = (
"FIX_ID",
"LONG_DECIMAL",
"LAT_DECIMAL",
"FIX_USE_CODE",
"ARTCC_ID_HIGH",
"CHARTS",
)
rename = {"FIX_ID": "name", "LONG_DECIMAL": "longitude", "LAT_DECIMAL": "latitude"}
with csv_bundle.open("FIX_BASE.csv") as fix_csv_file:
return (
pd.read_csv(fix_csv_file, usecols=usecols)
.query(
"CHARTS.str.contains('ENROUTE HIGH') and "
"FIX_USE_CODE.str.strip() in ('WP', 'RP', 'NRS')"
)
.dropna(subset="ARTCC_ID_HIGH")[list(rename)]
.rename(columns=rename)
)
[docs]
def load_faa_waypoints(refresh_cache: bool = False) -> pd.DataFrame:
"""Load FAA FIX waypoints with ``CACHE_MAX_AGE`` cache refresh.
If a cached DataFrame exists and is less than ``CACHE_MAX_AGE`` old, use it.
Otherwise, download the latest FAA NASR subscription zip and rebuild the cache.
"""
cached_waypoints = _read_cached_waypoints(refresh_cache=refresh_cache)
if cached_waypoints is not None:
return cached_waypoints
zip_bytes = _download_bytes(_latest_subscription_zip_url())
waypoints = _load_waypoints_from_zip_bytes(zip_bytes)
_write_waypoints_cache(waypoints)
return waypoints