commit 780fae7df0506f2a9fbc8e8f9ffd375884e3f9cf Author: admin Date: Thu Jun 25 23:17:45 2026 +0000 initial commit diff --git a/api/forecast.py b/api/forecast.py new file mode 100755 index 0000000..28f1480 --- /dev/null +++ b/api/forecast.py @@ -0,0 +1,834 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +CGI endpoint for on-demand, cached consensus forecasts. +Usage: + GET /api/forecast.py?q=&debug=1 # e.g., "Dallas, TX", "76084" + GET /api/forecast.py?lat=..&lon=.. # explicit coordinates + +Geocoding order: + 1) Open‑Meteo Geocoding (names & postal codes, no key) + 2) Nominatim fallback (no key) – requires custom UA; ≤1 rps + +Weather providers (no key): + - Open‑Meteo (daily temp max/min, precip prob max, wind 10 m max; hourly humidity/precip/wind fallbacks) + - api.weather.gov (NWS; period PoP, wind, humidity → daily roll‑up; requires User‑Agent) + - MET Norway Locationforecast (hourly humidity/wind/temp → daily roll‑up; requires User‑Agent) + - 7Timer! civillight (daily temp max/min) + +Cache: + 24h per‑location under /var/www/WEATHER/public_html/data/locations//latest.json +""" +import os +import sys +import json +import re +import time +from datetime import datetime as dt, timezone +from urllib.parse import parse_qs +from collections import defaultdict +from zoneinfo import ZoneInfo +import requests + +# -------------------- Paths & Config -------------------- +PUBLIC_ROOT = "/var/www/WEATHER/public_html" +DATA_ROOT = os.path.join(PUBLIC_ROOT, "data") +FETCHER_DIR = os.path.join(PUBLIC_ROOT, "fetcher") # config.json lives here +CONFIG_PATH = os.path.join(FETCHER_DIR, "config.json") + +CACHE_TTL_SECONDS = 24 * 3600 # 24 hours +NOMINATIM_RATELIMIT_MARK = "/tmp/nominatim_last_call" # crude 1 rps limiter across CGI invocations + +# Bump when payload shape changes +# v5 adds: payload["timezone"], payload["all_days"]; cached responses are re-sliced to "today" +SCHEMA_VERSION = 5 + + +def load_config(): + with open(CONFIG_PATH, "r") as f: + return json.load(f) + +# -------------------- CGI helpers -------------------- +def http_json(payload, status=200, cors="*"): + """Emit CGI headers + JSON body (always).""" + reason = "OK" if status == 200 else "ERROR" + print(f"Status: {status} {reason}") + print("Content-Type: application/json; charset=utf-8") + if cors: + print(f"Access-Control-Allow-Origin: {cors}") + print() + sys.stdout.write(json.dumps(payload, indent=2)) + +def http_error(message, status=400, debug=None): + body = {"error": message} + if debug is not None: + body["debug"] = debug + http_json(body, status=status) + +# -------------------- General helpers -------------------- +def parse_query(): + qs = os.environ.get("QUERY_STRING", "") + params = parse_qs(qs, keep_blank_values=False) + q = (params.get("q", [None])[0] or "").strip() + debug = (params.get("debug", ["0"])[0] == "1") + lat = params.get("lat", [None])[0] + lon = params.get("lon", [None])[0] + latf = lonf = None + if lat is not None and lon is not None: + try: + latf = float(lat) + lonf = float(lon) + except Exception: + latf = lonf = None + return q, latf, lonf, debug + +def slugify(s: str) -> str: + safe = "".join(ch.lower() if ch.isalnum() else "-" for ch in s.strip()) + while "--" in safe: + safe = safe.replace("--", "-") + return safe.strip("-") + +def ensure_dirs(path): + os.makedirs(path, exist_ok=True) + +def file_is_fresh(path, ttl_seconds): + if not os.path.exists(path): + return False + age = time.time() - os.path.getmtime(path) + return 0 <= age < ttl_seconds + +def c_to_f(c): + return (float(c) * 9.0 / 5.0) + 32.0 + +def f_to_c(f): + return (float(f) - 32.0) * 5.0 / 9.0 + +def mph_to_kmh(m): + return float(m) * 1.609344 + +def mps_to_mph(s): + return float(s) * 2.23693629 + +def mps_to_kmh(s): + return float(s) * 3.6 + +def normalize_temp(value, target_units, source_units): + if value is None: + return None + if target_units == source_units: + return float(value) + if source_units == "metric" and target_units == "imperial": + return c_to_f(value) + if source_units == "imperial" and target_units == "metric": + return f_to_c(value) + return float(value) + +def normalize_wind(value, target_units, source_origin): + """Return wind speed in target_units: imperial=>mph, metric=>km/h.""" + if value is None: + return None + v = float(value) + if target_units == "imperial": + if source_origin == "mph": + return v + if source_origin == "kmh": + return v / 1.609344 + if source_origin == "mps": + return mps_to_mph(v) + else: # metric + if source_origin == "mph": + return mph_to_kmh(v) + if source_origin == "kmh": + return v + if source_origin == "mps": + return mps_to_kmh(v) + return v + +def iso_to_local_date(iso_ts, tz_name): + """ISO string (maybe Z) -> local YYYY-MM-DD in tz_name.""" + if iso_ts.endswith("Z"): + iso_ts = iso_ts.replace("Z", "+00:00") + d = dt.fromisoformat(iso_ts) + if d.tzinfo is None: + d = d.replace(tzinfo=timezone.utc) + return d.astimezone(ZoneInfo(tz_name)).strftime("%Y-%m-%d") + +# -------------------- Geocoding: overrides + Open‑Meteo + Nominatim -------------------- +US_STATES = { + "AL": "Alabama", "AK": "Alaska", "AZ": "Arizona", "AR": "Arkansas", "CA": "California", + "CO": "Colorado", "CT": "Connecticut", "DE": "Delaware", "FL": "Florida", "GA": "Georgia", + "HI": "Hawaii", "ID": "Idaho", "IL": "Illinois", "IN": "Indiana", "IA": "Iowa", "KS": "Kansas", + "KY": "Kentucky", "LA": "Louisiana", "ME": "Maine", "MD": "Maryland", "MA": "Massachusetts", + "MI": "Michigan", "MN": "Minnesota", "MS": "Mississippi", "MO": "Missouri", "MT": "Montana", + "NE": "Nebraska", "NV": "Nevada", "NH": "New Hampshire", "NJ": "New Jersey", "NM": "New Mexico", + "NY": "New York", "NC": "North Carolina", "ND": "North Dakota", "OH": "Ohio", "OK": "Oklahoma", + "OR": "Oregon", "PA": "Pennsylvania", "RI": "Rhode Island", "SC": "South Carolina", + "SD": "South Dakota", "TN": "Tennessee", "TX": "Texas", "UT": "Utah", "VT": "Vermont", + "VA": "Virginia", "WA": "Washington", "WV": "West Virginia", "WI": "Wisconsin", "WY": "Wyoming", + "DC": "District of Columbia" +} +_zip_re = re.compile(r"^\s*\d{5}(?:-\d{4})?\s*$") + +def norm_key(s: str) -> str: + t = s.lower().strip() + t = re.sub(r"[^\w,\s\-]", " ", t) + t = re.sub(r"\s+", " ", t) + return t + +def lookup_override(q: str, overrides: dict): + """Return dict(name,lat,lon) if q matches an override key or any alias.""" + if not overrides: + return None + k = norm_key(q) + if k in overrides: + o = overrides[k] + if "lat" in o and "lon" in o: + name = o.get("name") or q + return {"source": "override", "name": name, "lat": float(o["lat"]), "lon": float(o["lon"]), "admin1": None, "country": None} + for ok, ov in overrides.items(): + aliases = ov.get("aliases") or [] + for a in aliases: + if norm_key(a) == k and "lat" in ov and "lon" in ov: + name = ov.get("name") or q + return {"source": "override", "name": name, "lat": float(ov["lat"]), "lon": float(ov["lon"]), "admin1": None, "country": None} + return None + +def build_om_attempts(query: str): + """Return list of (variant_string, countryCode or None) to try with Open‑Meteo.""" + q = (query or "").strip() + attempts = [] + if not q: + return attempts + is_zip = bool(_zip_re.match(q)) + tokens = re.split(r"[, ]+", q) + last = tokens[-1].upper() if tokens else "" + state_full = US_STATES.get(last) + if is_zip: + attempts.append((q, "US")) + else: + attempts.append((q, None)) + if state_full: + city = " ".join(tokens[:-1]).strip(", ") + if city: + attempts.append((f"{city}, {state_full}", "US")) + attempts.append((f"{city}, {last}, US", "US")) + stripped = re.sub(r"[^a-zA-Z0-9 ]+", " ", q) + stripped = re.sub(r"\s+", " ", stripped).strip() + if stripped and stripped != q: + attempts.append((stripped, None)) + if any(k in q.lower() for k in [" texas", ", tx", " usa"]): + attempts.append((q + ", United States", "US")) + seen = set(); deduped = [] + for name, cc in attempts: + key = (name.lower(), (cc or "").lower()) + if key not in seen: + seen.add(key); deduped.append((name, cc)) + return deduped + +def geocode_open_meteo_first(query, user_agent, language="en", attempts_out=None): + base_url = "https://geocoding-api.open-meteo.com/v1/search" + headers = {"User-Agent": user_agent} + if attempts_out is None: + attempts_out = [] + for name, cc in build_om_attempts(query): + attempts_out.append({"geocoder": "open-meteo", "name": name, "countryCode": cc}) + params = {"name": name, "count": 1, "language": language, "format": "json"} + if cc: + params["countryCode"] = cc + try: + r = requests.get(base_url, params=params, headers=headers, timeout=15) + r.raise_for_status() + j = r.json() + results = j.get("results") or [] + if results: + r0 = results[0] + return { + "source": "open-meteo", + "name": r0.get("name"), + "admin1": r0.get("admin1"), + "country": r0.get("country"), + "lat": float(r0["latitude"]), + "lon": float(r0["longitude"]) + } + except Exception as e: + print(f"Open-Meteo geocode error for {name} cc={cc}: {e}", file=sys.stderr) + return None + +def _nominatim_rate_limit_once_per_second(): + try: + now = time.time() + if os.path.exists(NOMINATIM_RATELIMIT_MARK): + last = os.path.getmtime(NOMINATIM_RATELIMIT_MARK) + wait = 1.0 - (now - last) + if wait > 0: + time.sleep(wait) + with open(NOMINATIM_RATELIMIT_MARK, "w") as f: + f.write(str(now)) + except Exception: + pass + +def geocode_nominatim_fallback(query, user_agent, email=None, country_hint="us", language="en", attempts_out=None): + if attempts_out is None: + attempts_out = [] + q = (query or "").strip() + if not q: + return None + base = "https://nominatim.openstreetmap.org/search" + headers = {"User-Agent": user_agent} + + def call_nominatim(params): + _nominatim_rate_limit_once_per_second() + attempts_out.append({"geocoder": "nominatim", **{k: v for k, v in params.items() if k in ("q", "city", "state", "country", "countrycodes")}}) + r = requests.get(base, params=params, headers=headers, timeout=15) + r.raise_for_status() + arr = r.json() + if isinstance(arr, list) and arr: + top = arr[0] + lat = float(top["lat"]); lon = float(top["lon"]) + addr = top.get("address") or {} + name = addr.get("city") or addr.get("town") or addr.get("village") or addr.get("municipality") or top.get("display_name", q) + admin1 = addr.get("state"); country = addr.get("country") + display = ", ".join([x for x in [name, admin1, country] if x]) + return {"source": "nominatim", "name": display or q, "admin1": admin1, "country": country, "lat": lat, "lon": lon} + return None + + # A) Free‑form with country hint + A = {"q": q, "format": "json", "addressdetails": 1, "limit": 1, "accept-language": language} + if country_hint: + A["countrycodes"] = country_hint.lower() + if email: + A["email"] = email + try: + res = call_nominatim(A) + if res: + return res + except Exception as e: + print(f"Nominatim free-form (hint) error: {e}", file=sys.stderr) + + # B) Structured City/State[/Country] + m = re.match(r"^\s*([A-Za-z .'\-]+)[,\s]+\s*([A-Za-z]{2,})\s*$", q) + if m: + city_raw, state_token = m.group(1), m.group(2) + state_full = US_STATES.get(state_token.upper(), state_token) + B = {"city": city_raw.strip(), "state": state_full, "country": "US", + "format": "json", "addressdetails": 1, "limit": 1, "accept-language": language} + if email: + B["email"] = email + try: + res = call_nominatim(B) + if res: + return res + except Exception as e: + print(f"Nominatim structured error: {e}", file=sys.stderr) + + # C) Free‑form without hint + C = {"q": q, "format": "json", "addressdetails": 1, "limit": 1, "accept-language": language} + if email: + C["email"] = email + try: + res = call_nominatim(C) + if res: + return res + except Exception as e: + print(f"Nominatim free-form (no hint) error: {e}", file=sys.stderr) + return None + +# -------------------- Providers (Open‑Meteo / NWS / MET / 7Timer) -------------------- +def fetch_open_meteo(conf, lat, lon, days, units, tz_name, user_agent): + """Return per-day dicts: high, low, precip_chance, wind_max, humidity_avg (with hourly fallbacks).""" + url = conf["base_url"] + params = { + "latitude": lat, "longitude": lon, + "daily": "temperature_2m_max,temperature_2m_min,precipitation_probability_max,wind_speed_10m_max", + "hourly": "relative_humidity_2m,precipitation_probability,wind_speed_10m", + "temperature_unit": "fahrenheit" if units == "imperial" else "celsius", + "windspeed_unit": "mph" if units == "imperial" else "kmh", + "timezone": tz_name or "auto" + } + r = requests.get(url, params=params, headers={"User-Agent": user_agent}, timeout=20) + r.raise_for_status() + data = r.json() + out = defaultdict(lambda: {"provider": "open-meteo"}) + + # ---- daily (direct) ---- + daily = data.get("daily") or {} + times_d = daily.get("time") or [] + tmax_d = daily.get("temperature_2m_max") or [None] * len(times_d) + tmin_d = daily.get("temperature_2m_min") or [None] * len(times_d) + pmax_d = daily.get("precipitation_probability_max") or [None] * len(times_d) + wmax_d = daily.get("wind_speed_10m_max") or [None] * len(times_d) + for i, dday in enumerate(times_d): + rec = out[dday] + if tmax_d[i] is not None: + rec["high"] = round(float(tmax_d[i]), 1) + if tmin_d[i] is not None: + rec["low"] = round(float(tmin_d[i]), 1) + if pmax_d[i] is not None: + rec["precip_chance"] = int(pmax_d[i]) + if wmax_d[i] is not None: + rec["wind_max"] = round(float(wmax_d[i]), 1) + + # ---- hourly fallbacks + humidity avg ---- + hourly = data.get("hourly") or {} + times_h = hourly.get("time") or [] + rh_h = hourly.get("relative_humidity_2m") or [] + pop_h = hourly.get("precipitation_probability") or [] + wind_h = hourly.get("wind_speed_10m") or [] + + for t, h in zip(times_h, rh_h): + if h is None: + continue + out[t[:10]].setdefault("_hum_list", []).append(float(h)) + for t, p in zip(times_h, pop_h): + if p is None: + continue + dday = t[:10] + cur = out[dday].get("_pop_max") + out[dday]["_pop_max"] = max(cur, int(p)) if cur is not None else int(p) + for t, w in zip(times_h, wind_h): + if w is None: + continue + dday = t[:10] + cur = out[dday].get("_wind_max") + out[dday]["_wind_max"] = max(cur, float(w)) if cur is not None else float(w) + + result = [] + for dday in sorted(out.keys())[:days]: + rec = out[dday] + if rec.get("_hum_list"): + rec["humidity_avg"] = int(round(sum(rec["_hum_list"]) / len(rec["_hum_list"]))) + del rec["_hum_list"] + if "precip_chance" not in rec and rec.get("_pop_max") is not None: + rec["precip_chance"] = int(rec["_pop_max"]) + if "_pop_max" in rec: + del rec["_pop_max"] + if "wind_max" not in rec and rec.get("_wind_max") is not None: + rec["wind_max"] = round(float(rec["_wind_max"]), 1) + if "_wind_max" in rec: + del rec["_wind_max"] + rec["date"] = dday + rec["units"] = units + result.append(rec) + return result, data + +def _parse_nws_wind_mph(s): + """Parse 'Calm', '5 mph', '10 to 20 mph' -> number mph (max).""" + if not s or not isinstance(s, str): + return None + s_lower = s.lower() + if "calm" in s_lower: + return 0.0 + nums = [float(x) for x in re.findall(r"(\d+(?:\.\d+)?)", s)] + return max(nums) if nums else None + +def fetch_nws(conf, lat, lon, days, units, tz_name, user_agent): + base = conf["base_url"].rstrip("/") + headers = {"User-Agent": user_agent} + meta_url = f"{base}/points/{round(lat,4)},{round(lon,4)}" + r_meta = requests.get(meta_url, headers=headers, timeout=20) + r_meta.raise_for_status() + meta = r_meta.json() + forecast_url = (meta.get("properties") or {}).get("forecast") + if not forecast_url: + return [], {"points": meta} + r_fcst = requests.get(forecast_url, headers=headers, timeout=20) + r_fcst.raise_for_status() + fcst = r_fcst.json() + + day_buckets = defaultdict(lambda: {"provider": "nws"}) + for p in (fcst.get("properties") or {}).get("periods", []): + start = p.get("startTime") + if not start: + continue + day = iso_to_local_date(start, tz_name) + rec = day_buckets[day] + # Temperature (F) -> track both min and max + t = p.get("temperature") + if t is not None: + val = float(normalize_temp(t, units, "imperial")) + rec["high"] = max(rec.get("high", -1e9), val) + rec["low"] = min(rec.get("low", 1e9), val) + # Precip chance (%) + pop = ((p.get("probabilityOfPrecipitation") or {}).get("value")) + if pop is not None: + rec["precip_chance"] = max(int(pop), rec.get("precip_chance", -1)) + # Wind ("10 mph" / "10 to 20 mph") + w = _parse_nws_wind_mph(p.get("windSpeed")) + if w is not None: + w_norm = normalize_wind(w, units, "mph") + rec["wind_max"] = max(rec.get("wind_max", -1e9), w_norm) + # Humidity (%) + rh = ((p.get("relativeHumidity") or {}).get("value")) + if rh is not None: + rec.setdefault("_hum_list", []).append(float(rh)) + + result = [] + for day in sorted(day_buckets.keys())[:days]: + rec = day_buckets[day] + if rec.get("_hum_list"): + rec["humidity_avg"] = int(round(sum(rec["_hum_list"]) / len(rec["_hum_list"]))) + del rec["_hum_list"] + if "wind_max" in rec: + rec["wind_max"] = round(rec["wind_max"], 1) + if "high" in rec: + rec["high"] = round(rec["high"], 1) + if "low" in rec and rec["low"] != 1e9: + rec["low"] = round(rec["low"], 1) + rec["date"] = day + rec["units"] = units + result.append(rec) + return result, {"points": meta, "forecast": fcst} + +def fetch_metno(conf, lat, lon, days, units, tz_name, user_agent): + url = conf["base_url"] + headers = {"User-Agent": user_agent} + params = {"lat": round(lat, 4), "lon": round(lon, 4)} + r = requests.get(url, params=params, headers=headers, timeout=20) + r.raise_for_status() + data = r.json() + + day_buckets = defaultdict(lambda: {"provider": "metno"}) + for item in (data.get("properties") or {}).get("timeseries", []): + t = item.get("time") + if not t: + continue + day = iso_to_local_date(t, tz_name) + details = ((item.get("data") or {}).get("instant") or {}).get("details") or {} + rh = details.get("relative_humidity") # % + ws = details.get("wind_speed") # m/s + ta = details.get("air_temperature") # °C + if rh is not None: + day_buckets[day].setdefault("_hum_list", []).append(float(rh)) + if ws is not None: + v = normalize_wind(ws, units, "mps") + day_buckets[day]["wind_max"] = max(day_buckets[day].get("wind_max", -1e9), v) + if ta is not None: + val = float(ta) if units == "metric" else float(c_to_f(ta)) + day_buckets[day].setdefault("_t_list", []).append(val) + + result = [] + for day in sorted(day_buckets.keys())[:days]: + rec = day_buckets[day] + if rec.get("_hum_list"): + rec["humidity_avg"] = int(round(sum(rec["_hum_list"]) / len(rec["_hum_list"]))) + del rec["_hum_list"] + if rec.get("_t_list"): + rec["low"] = round(min(rec["_t_list"]), 1) + del rec["_t_list"] + if "wind_max" in rec: + rec["wind_max"] = round(rec["wind_max"], 1) + rec["date"] = day + rec["units"] = units + result.append(rec) + return result, data + +def fetch_seventimer(conf, lat, lon, days, units, tz_name, user_agent): + url = conf["base_url"] + params = {"lon": lon, "lat": lat, "product": "civillight", "output": "json", "unit": "metric"} + r = requests.get(url, params=params, headers={"User-Agent": user_agent}, timeout=20) + r.raise_for_status() + data = r.json() + result = [] + for d in (data.get("dataseries") or [])[:days]: + date_raw = d.get("date") + t2m = d.get("temp2m") or {} + tmax_c = t2m.get("max") + tmin_c = t2m.get("min") + if date_raw is None: + continue + s = str(date_raw) + day = f"{s[0:4]}-{s[4:6]}-{s[6:8]}" + rec = {"date": day, "units": units, "provider": "7timer"} + if tmax_c is not None: + rec["high"] = round(float(normalize_temp(tmax_c, target_units=units, source_units="metric")), 1) + if tmin_c is not None: + rec["low"] = round(float(normalize_temp(tmin_c, target_units=units, source_units="metric")), 1) + result.append(rec) + return result, data + +# -------------------- Aggregation -------------------- +def aggregate_days(provider_lists, units, max_days=None): + by_date = defaultdict(list) + for lst in provider_lists: + for rec in lst: + if rec: + by_date[rec["date"]].append(rec) + + def _finite(xs): + return [x for x in xs if isinstance(x, (int, float)) and x == x and abs(x) != float("inf")] + + # Unit-aware temperature guardrails + if units == "metric": + TEMP_MIN, TEMP_MAX = -73.3, 60.0 # ~ -100..140 F + else: + TEMP_MIN, TEMP_MAX = -100.0, 140.0 + + def _clamp_temp(xs): + xs2 = [] + for x in _finite(xs): + if TEMP_MIN <= x <= TEMP_MAX: + xs2.append(x) + return xs2 + + def _clamp_pct(xs): + return [min(100, max(0, int(round(x)))) for x in _finite(xs)] + + def _clamp_wind(xs): + # allow 0..200 mph (or ~0..320 km/h) + xs2 = [] + for x in _finite(xs): + if 0 <= x <= 200: + xs2.append(x) + return xs2 + + aggregated = [] + for day in sorted(by_date.keys()): + entries = by_date[day] + highs = [e.get("high") for e in entries] + lows = [e.get("low") for e in entries] + pops = [e.get("precip_chance") for e in entries] + winds = [e.get("wind_max") for e in entries] + hums = [e.get("humidity_avg") for e in entries] + + # Sanitize / clamp before computing statistics + highs = _clamp_temp(highs) + lows = _clamp_temp(lows) + pops = _clamp_pct(pops) + winds = _clamp_wind(winds) + hums = _clamp_pct(hums) + + def mean_or_none(xs): return round(sum(xs)/len(xs), 1) if xs else None + def int_mean_or_none(xs): return int(round(sum(xs)/len(xs))) if xs else None + def median_or_none(xs): + if not xs: return None + s = sorted(xs); n = len(s) + return round(s[n//2], 1) if n % 2 == 1 else round((s[n//2 - 1] + s[n//2]) / 2.0, 1) + + day_obj = { + "date": day, + "providers": entries, + "consensus": { + "mean_high": mean_or_none(highs), + "median_high": median_or_none(highs), + "mean_low": mean_or_none(lows), + "median_low": median_or_none(lows), + "precip_chance_mean": int_mean_or_none(pops), + "wind_max_mean": mean_or_none(winds), + "humidity_avg_mean": int_mean_or_none(hums), + "provider_count": len(entries), + "spread_high": (round(max(highs) - min(highs), 1) if len(highs) >= 2 else None), + "spread_low": (round(max(lows) - min(lows), 1) if len(lows) >= 2 else None), + }, + "units": units, + "wind_unit": "mph" if units == "imperial" else "km/h", + "humidity_unit": "%", + "precip_unit": "%" + } + aggregated.append(day_obj) + + if max_days is not None: + aggregated = aggregated[:max_days] + return aggregated + +# -------------------- Display label de-duplication -------------------- +def _dedupe_label(name, admin1, country): + parts_in = [p for p in [name, admin1, country] if p] + out = [] + seen = set() + combined = [] + for p in parts_in: + if p: + combined.extend([s.strip() for s in str(p).split(",") if s and s.strip()]) + for s in combined: + key = s.lower() + if key and key not in seen: + seen.add(key) + out.append(s) + return ", ".join(out) + +# -------------------- Slicing helpers for "today" in forecast timezone -------------------- +def _parse_ymd_safe(s: str): + """Robust YYYY-M-D or YYYY-MM-DD parser returning a date() (naively in UTC).""" + s2 = (s or "").strip() + m = re.match(r"^\s*(\d{4})-(\d{1,2})-(\d{1,2})\s*$", s2) + if not m: + return None + y, mo, d = int(m.group(1)), int(m.group(2)), int(m.group(3)) + try: + return dt(y, mo, d, tzinfo=timezone.utc).date() + except Exception: + return None + +def slice_days_for_timezone(all_days, tz_name, tz_fallback): + """Given unfiltered all_days, compute today's 7-day window using tz_name.""" + try: + today_local_date = dt.now(ZoneInfo(tz_name)).date() + except Exception: + today_local_date = dt.now(ZoneInfo(tz_fallback)).date() + + normalized = [] + for d in all_days or []: + d_date = _parse_ymd_safe(d.get("date")) + if d_date is None: + continue + nd = dict(d) + nd["__date_obj"] = d_date + normalized.append(nd) + + normalized = [d for d in normalized if d["__date_obj"] >= today_local_date] + normalized.sort(key=lambda r: r["__date_obj"]) + window = normalized[:7] + for d in window: + d.pop("__date_obj", None) + return window, today_local_date.isoformat() + +# -------------------- Main handler -------------------- +def run(): + conf = load_config() + units = conf.get("units", "imperial") # 'imperial' or 'metric' + days = int(conf.get("days", 7)) # fetch horizon for providers + + # Fallback only; actual tz will be detected from Open‑Meteo's response (timezone=auto) + tz_fallback = conf["location"].get("timezone", "UTC") + user_agent = conf.get("user_agent") or "WeatherConsensus/1.0 (https://weather.thedarkelite.com, admin@thedarkelite.com)" + nominatim_email = conf.get("nominatim_email") # optional (polite) + overrides = conf.get("geocoding_overrides") or {} + + q, lat_in, lon_in, debug_flag = parse_query() + debug_info = {"attempts": []} + + # Resolve coordinates + if lat_in is not None and lon_in is not None: + loc = {"name": f"{lat_in:.4f},{lon_in:.4f}", "lat": lat_in, "lon": lon_in} + debug_info["geocode_source"] = "coords" + debug_info["query"] = None + elif q: + debug_info["query"] = q + g = lookup_override(q, overrides) + if g: + debug_info["attempts"].append({"geocoder": "override", "match": norm_key(q)}) + else: + g = geocode_open_meteo_first(q, user_agent=user_agent, attempts_out=debug_info["attempts"]) + if not g: + hint = "us" if (_zip_re.match(q) or " texas" in q.lower() or re.search(r",\s*[A-Za-z]{2}$", q)) else None + g = geocode_nominatim_fallback(q, user_agent=user_agent, email=nominatim_email, country_hint=hint, attempts_out=debug_info["attempts"]) + if not g: + return http_error("Location not found", status=404, debug=debug_info if debug_flag else None) + display_name = _dedupe_label(g.get("name"), g.get("admin1"), g.get("country")) or q + loc = {"name": display_name, "lat": g["lat"], "lon": g["lon"]} + debug_info["geocode_source"] = g.get("source") or "open-meteo/nominatim/override" + else: + return http_error("Missing query. Provide q=city,state or lat & lon.", 400) + + # Cache paths + slug = slugify(f"{loc['name']}_{loc['lat']:.4f}_{loc['lon']:.4f}") + loc_dir = os.path.join(DATA_ROOT, "locations", slug) + raw_dir = os.path.join(loc_dir, "raw") + ensure_dirs(raw_dir) + cache_path = os.path.join(loc_dir, "latest.json") + + # Try cache; if schema matches, re-slice to "today" using stored timezone + all_days + if file_is_fresh(cache_path, CACHE_TTL_SECONDS): + try: + with open(cache_path, "r") as f: + cached = json.load(f) + if cached.get("schema_version") == SCHEMA_VERSION: + tz_eff = cached.get("timezone") or tz_fallback + all_days = cached.get("all_days") or cached.get("days") or [] + sliced, today_iso = slice_days_for_timezone(all_days, tz_eff, tz_fallback) + cached = dict(cached) + cached["days"] = sliced + if debug_flag: + cached["debug"] = {**debug_info, "tz_effective": tz_eff, "today_local_date": today_iso, "from_cache": True} + return http_json(cached, status=200) + except Exception as e: + print(f"Cache read error (ignored): {e}", file=sys.stderr) + + # Fetch from providers + provider_lists = [] + date_tag = dt.now(timezone.utc).strftime("%Y-%m-%d") + + # (A) Open‑Meteo FIRST, with timezone=auto to detect the location's tz + tz_effective = tz_fallback + try: + if conf["providers"]["open_meteo"]["enabled"]: + om_conf = dict(conf["providers"]["open_meteo"]) + lst, raw = fetch_open_meteo(om_conf, loc["lat"], loc["lon"], days, units, "auto", user_agent) + provider_lists.append(lst) + if isinstance(raw, dict): + tz_effective = raw.get("timezone") or tz_fallback + with open(os.path.join(raw_dir, f"open-meteo_{date_tag}.json"), "w") as f: + json.dump(raw, f, indent=2) + except Exception as e: + print(f"Open‑Meteo error: {e}", file=sys.stderr) + + # (B) Remaining providers – pass tz_effective for correct local-day bucketing + try: + if conf["providers"]["nws"]["enabled"]: + lst, raw = fetch_nws(conf["providers"]["nws"], loc["lat"], loc["lon"], days, units, tz_effective, user_agent) + provider_lists.append(lst) + with open(os.path.join(raw_dir, f"nws_{date_tag}.json"), "w") as f: + json.dump(raw, f, indent=2) + except Exception as e: + print(f"NWS error: {e}", file=sys.stderr) + + try: + if conf["providers"]["metno"]["enabled"]: + lst, raw = fetch_metno(conf["providers"]["metno"], loc["lat"], loc["lon"], days, units, tz_effective, user_agent) + provider_lists.append(lst) + with open(os.path.join(raw_dir, f"metno_{date_tag}.json"), "w") as f: + json.dump(raw, f, indent=2) + except Exception as e: + print(f"MET Norway error: {e}", file=sys.stderr) + + try: + if conf["providers"]["seventimer"]["enabled"]: + lst, raw = fetch_seventimer(conf["providers"]["seventimer"], loc["lat"], loc["lon"], days, units, tz_effective, user_agent) + provider_lists.append(lst) + with open(os.path.join(raw_dir, f"7timer_{date_tag}.json"), "w") as f: + json.dump(raw, f, indent=2) + except Exception as e: + print(f"7Timer error: {e}", file=sys.stderr) + + # Aggregate across providers (unfiltered) + aggregated_full = aggregate_days(provider_lists, units, max_days=None) + + # Compute the visible window TODAY..TODAY+6 using the location timezone + days_view, today_iso = slice_days_for_timezone(aggregated_full, tz_effective, tz_fallback) + + payload = { + "schema_version": SCHEMA_VERSION, + "location": loc["name"], + "lat": round(loc["lat"], 4), + "lon": round(loc["lon"], 4), + "timezone": tz_effective, # <-- NEW: forecast location timezone + "generated_at_utc": dt.now(timezone.utc).isoformat().replace("+00:00", "Z"), + "units": units, + "all_days": aggregated_full, # <-- NEW: keep unfiltered days in cache + "days": days_view + } + + if debug_flag: + payload["debug"] = { + **debug_info, + "tz_effective": tz_effective, + "today_local_date": today_iso, + "first_3_dates_full": sorted({(d.get("date") or "").strip() for d in aggregated_full})[:3] + } + + # Save cache & return + try: + with open(cache_path, "w") as f: + json.dump(payload, f, indent=2) + except Exception as e: + print(f"Cache write error (ignored): {e}", file=sys.stderr) + + return http_json(payload, status=200) + +# -------------------- Entrypoint -------------------- +if __name__ == "__main__": + try: + run() + except Exception as ex: + print(f"FATAL: {ex}", file=sys.stderr) + http_error(f"Server error: {ex}", 500) \ No newline at end of file diff --git a/app.v6.4.js b/app.v6.4.js new file mode 100644 index 0000000..8995638 --- /dev/null +++ b/app.v6.4.js @@ -0,0 +1,596 @@ +/* Weather Consensus — app.v6.4.js */ +(function () { + const API_BASE = "/api"; + // DOM refs + const form = document.getElementById('searchForm'); + const qInput = document.getElementById('q'); + const daysEl = document.getElementById('days'); + const locationTitle = document.getElementById('locationTitle'); + const generatedAt = document.getElementById('generatedAt'); + const yearEl = document.getElementById('year'); + const themeSelect = document.getElementById('themeMode'); + const rainLayer = document.querySelector('.rain'); + const snowLayer = document.querySelector('.snow'); + const radarPanel = document.getElementById('radarPanel'); + if (yearEl) yearEl.textContent = new Date().getFullYear(); + + // Units + utils + const unitTemp = (u) => u === 'metric' ? '°C' : '°F'; + const unitWind = (u) => u === 'metric' ? 'km/h' : 'mph'; + const isNum = (v) => Number.isFinite(v); + const el = (tag, cls, html) => { const d=document.createElement(tag); if (cls) d.className=cls; if (html!==undefined) d.innerHTML=html; return d; }; + + // Dedupe location label + function dedupeLocationLabel(label){ + if (!label || typeof label!=='string') return label; + const parts = label.split(',').map(s=>s.trim()).filter(Boolean); + const seen=new Set(), out=[]; + for (const p of parts){ const k=p.toLowerCase(); if(!seen.has(k)){ seen.add(k); out.push(p); } } + return out.join(', '); + } + + // Device‑agnostic date label (we always format the YMD as UTC so it’s stable) + function labelFromYMD(ymd){ + if (typeof ymd!=='string') return String(ymd ?? ''); + const m=ymd.trim().match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/); + if(!m) return ymd; + const y=+m[1], mo=+m[2]-1, d=+m[3]; + const dt=new Date(Date.UTC(y,mo,d)); + return dt.toLocaleDateString(undefined,{weekday:'short',month:'short',day:'numeric',timeZone:'UTC'}); + } + + // ---- Region‑local “today” helpers ---- + function ymdNowInTZ(tz) { + const fmt = new Intl.DateTimeFormat('en-CA', { timeZone: tz, year:'numeric', month:'2-digit', day:'2-digit' }); + const parts = Object.fromEntries(fmt.formatToParts(new Date()).map(p => [p.type, p.value])); + return `${parts.year}-${parts.month}-${parts.day}`; + } + function formatInTZ(dateISO, tz) { + try { return new Date(dateISO).toLocaleString(undefined, { timeZone: tz }); } + catch { return new Date(dateISO).toLocaleString(); } + } + + // Visual sanity checks + function saneTemp(v,u){ if(!isNum(v)) return null; return (u==='metric') ? ((v>=-73.3&&v<=60.0)?v:null) : ((v>=-100&&v<=140)?v:null); } + function sanePct(v){ if(!isNum(v)) return null; const n=Math.round(v); return (n>=0&&n<=100)?n:null; } + function saneWind(v,u){ if(!isNum(v)) return null; const cap=(u==='metric')?320:200; return (v>=0&&v<=cap)?v:null; } + + // Icons + function svgBase(pathD,opts={}){ const ns='http://www.w3.org/2000/svg'; const s=document.createElementNS(ns,'svg'); s.setAttribute('viewBox','0 0 24 24'); s.setAttribute('aria-hidden','true'); const p=document.createElementNS(ns,'path'); for(const [k,v] of Object.entries(opts)){ if(['fill','stroke','stroke-width','stroke-linecap','stroke-linejoin'].includes(k)) p.setAttribute(k,v);} p.setAttribute('d',pathD); s.appendChild(p); return s; } + const iconDroplet=()=>svgBase("M12 2C8.2 7.1 6.2 10.2 6.2 13.1a5.8 5.8 0 0011.6 0c0-2.9-2-6-5.8-11.1z",{fill:'currentColor'}); + function iconWind(){ const ns='http://www.w3.org/2000/svg'; const s=document.createElementNS(ns,'svg'); s.setAttribute('viewBox','0 0 24 24'); s.setAttribute('aria-hidden','true'); const p1=document.createElementNS(ns,'path'); p1.setAttribute('fill','none'); p1.setAttribute('stroke','currentColor'); p1.setAttribute('stroke-width','2'); p1.setAttribute('stroke-linecap','round'); p1.setAttribute('stroke-linejoin','round'); p1.setAttribute('d',"M3 8h10a3 3 0 100-6"); const p2=document.createElementNS(ns,'path'); p2.setAttribute('fill','none'); p2.setAttribute('stroke','currentColor'); p2.setAttribute('stroke-width','2'); p2.setAttribute('stroke-linecap','round'); p2.setAttribute('stroke-linejoin','round'); p2.setAttribute('d',"M3 14h14a3 3 0 110 6"); s.append(p1,p2); return s; } + function iconHumidity(){ const ns='http://www.w3.org/2000/svg'; const s=document.createElementNS(ns,'svg'); s.setAttribute('viewBox','0 0 24 24'); s.setAttribute('aria-hidden','true'); const p1=document.createElementNS(ns,'path'); p1.setAttribute('fill','none'); p1.setAttribute('stroke','currentColor'); p1.setAttribute('stroke-width','2'); p1.setAttribute('stroke-linecap','round'); p1.setAttribute('stroke-linejoin','round'); p1.setAttribute('d',"M12 2C7.5 8 5 11 5 14a7 7 0 0014 0c0-3-2.5-6-7-12z"); const p2=document.createElementNS(ns,'path'); p2.setAttribute('fill','none'); p2.setAttribute('stroke','currentColor'); p2.setAttribute('stroke-width','2'); p2.setAttribute('stroke-linecap','round'); p2.setAttribute('d',"M8 16c1.5 1 3 1 4.5 0s3-1 4.5 0"); s.append(p1,p2); return s; } + + // API + async function queryForecast(q){ + const url=new URL(`${API_BASE}/forecast.py`, window.location.origin); + if(q) url.searchParams.set('q',q); + const res=await fetch(url.toString(),{cache:'no-store'}); + const txt=await res.text().catch(()=> ''); + if(!res.ok) throw new Error(`API error ${res.status}: ${txt.slice(0,200)}`); + return JSON.parse(txt); + } + + // Theme helpers (unchanged) + async function getSunTimes(lat,lon,ymd){ + try{ + const url=new URL('https://api.open-meteo.com/v1/forecast'); + url.searchParams.set('latitude',lat); url.searchParams.set('longitude',lon); + url.searchParams.set('daily','sunrise,sunset'); url.searchParams.set('timezone','auto'); + url.searchParams.set('start_date',ymd); url.searchParams.set('end_date',ymd); + const r=await fetch(url.toString(),{cache:'no-store'}); const j=await r.json(); + const tz=j.timezone||'UTC'; const daily=j.daily||{}; + return { tz, sunrise:(daily.sunrise&&daily.sunrise[0])||null, sunset:(daily.sunset&&daily.sunset[0])||null }; + }catch(e){ console.warn('sun times fetch failed',e); return { tz:'UTC', sunrise:null, sunset:null }; } + } + function nowPartsInTZ(tz){ + const fmt=new Intl.DateTimeFormat('en-CA',{timeZone:tz,year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',hour12:false}); + const parts=fmt.formatToParts(new Date()); const map=Object.fromEntries(parts.map(p=>[p.type,p.value])); + return { ymd:`${map.year}-${map.month}-${map.day}`, hm:`${map.hour}:${map.minute}` }; + } + function extractHM(iso){ if(!iso) return null; const m=/T(\d{2}):(\d{2})/.exec(iso); return m ? `${m[1]}:${m[2]}` : null; } + function applyTheme(mode,ctx){ const body=document.body; if(mode==='day') body.setAttribute('data-theme','day'); else if(mode==='night') body.setAttribute('data-theme','night'); else body.setAttribute('data-theme',(ctx&&ctx.isDay)?'day':'night'); } + async function refreshTheme(data){ + const mode=(localStorage.getItem('themeMode')||'auto'); if(themeSelect) themeSelect.value=mode; + if(mode==='day'||mode==='night'){ applyTheme(mode); return; } + const today=(data.days&&data.days[0])?data.days[0].date:null; const {lat,lon}=data; + if(!today||!Number.isFinite(lat)||!Number.isFinite(lon)){ applyTheme('auto',{isDay:true}); return; } + const {tz,sunrise,sunset}=await getSunTimes(lat,lon,today); const now=nowPartsInTZ(tz); + const sr=extractHM(sunrise), ss=extractHM(sunset); let isDay=true; + if(now.ymd===today && sr && ss) isDay=(now.hm>=sr && now.hm0) return; + const COUNT=36; const w=rainLayer.clientWidth||window.innerWidth||1200; + for(let i=0;i=40){ body.classList.add('rainy'); ensureRainDrops(); } else { body.classList.remove('rainy'); } + } + + // Snow overlay + const SNOW_MIN_POP=40, SNOW_MAX_FLAKES=180; + const SNOW_BASE_PER_400PX={far:18, mid:24, near:32}; + let __snowLastWidthBucket=null; + function ensureSnowFlakes(){ + if(!snowLayer) return; + const w=snowLayer.clientWidth||window.innerWidth||1200; + const widthBucket=Math.max(400, Math.round(w/200)*200); + if(snowLayer.childElementCount>0 && __snowLastWidthBucket===widthBucket) return; + __snowLastWidthBucket=widthBucket; + snowLayer.innerHTML=''; + const scale=widthBucket/400; + let far=Math.round(SNOW_BASE_PER_400PX.far*scale), mid=Math.round(SNOW_BASE_PER_400PX.mid*scale), near=Math.round(SNOW_BASE_PER_400PX.near*scale); + const total=far+mid+near; if(total>SNOW_MAX_FLAKES){ const k=SNOW_MAX_FLAKES/total; far=Math.max(8,Math.floor(far*k)); mid=Math.max(8,Math.floor(mid*k)); near=Math.max(8,Math.floor(near*k)); } + const make=(count,opts)=>{ for(let i=0;i{ if(!document.body.classList.contains('snowy')) return; if(__snowResizeTid) clearTimeout(__snowResizeTid); __snowResizeTid=setTimeout(()=>ensureSnowFlakes(),200); }); + + // Rendering helpers + function metricRow(svg,label,value){ const row=el('div','metric'); const i=el('span','i'); i.appendChild(svg); row.append(i, el('span','name',label), el('span','value',value)); return row; } + + // >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> + // CHANGED: header markup – place date and Today badge in a flex row + function renderDay(d,u, todayYMD){ + const uT=unitTemp(u), uW=unitWind(u), c=d.consensus||{}; + const hi=isNum(c.mean_high)?`${c.mean_high.toFixed(1)}${uT}`:'—'; + const lo=isNum(c.mean_low)?`${c.mean_low.toFixed(1)}${uT}`:'—'; + const pc=isNum(c.precip_chance_mean)?`${Math.round(c.precip_chance_mean)}%`:'—'; + const wm=isNum(c.wind_max_mean)?`${c.wind_max_mean.toFixed(1)} ${uW}`:'—'; + const rh=isNum(c.humidity_avg_mean)?`${Math.round(c.humidity_avg_mean)}%`:'—'; + + const card=el('article','card'); + + // Build card head with a single-row date line + optional Today badge + const top=el('div','card-head'); + const dateRow = el('div','date-row'); + const dateText = el('span','date-text', labelFromYMD(d.date)); + dateRow.appendChild(dateText); + if (d.date === todayYMD) { + dateRow.appendChild(el('span','today-badge','Today')); + } + top.appendChild(dateRow); + + const hiLo=el('div','hi-lo', `High ${hi} Low ${lo}`); + top.appendChild(hiLo); + card.appendChild(top); + + const metrics=el('div','metrics'); metrics.append( + metricRow(iconDroplet(),'Precip chance',pc), + metricRow(iconWind(),'Max wind',wm), + metricRow(iconHumidity(),'Humidity (avg)',rh) + ); card.appendChild(metrics); + + const providers=el('details','providers'); providers.appendChild(el('summary','', 'Provider breakdown')); + const pre=el('pre','prov-pre',(d.providers||[]).map(p=>{ + const bits=[]; const h=isNum(p.high)?p.high:null; const l=isNum(p.low)?p.low:null; const pop=isNum(p.precip_chance)?Math.round(p.precip_chance):null; const w=isNum(p.wind_max)?p.wind_max:null; const hum=isNum(p.humidity_avg)?Math.round(p.humidity_avg):null; + if(h!==null) bits.push(`high=${h}${uT}`); if(l!==null) bits.push(`low=${l}${uT}`); if(pop!==null) bits.push(`precip=${pop}%`); if(w!==null) bits.push(`wind=${w} ${uW}`); if(hum!==null) bits.push(`rh=${hum}%`); + return bits.length ? `${p.provider}: ${bits.join(' · ')}` : `${p.provider}: —`; + }).join('\n')); providers.appendChild(pre); card.appendChild(providers); + + return card; + } + // <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< + + // Reverse‑geocode prettifier for "lat,lon" + function parseLatLonLabel(label){ + if (typeof label !== 'string') return null; + const m = label.trim().match(/^\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*$/); + if (!m) return null; + const lat = parseFloat(m[1]), lon = parseFloat(m[2]); + if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null; + return { lat, lon }; + } + function labelFromNominatim(j){ + if (!j) return null; + const a = j.address || {}; + const locality = a.city || a.town || a.village || a.hamlet || a.municipality || a.county; + const state = a.state || a.region; + const country = a.country || (a.country_code ? a.country_code.toUpperCase() : null); + const parts = [locality, state, (country && country !== 'US') ? country : null].filter(Boolean); + return parts.length ? parts.join(', ') : (j.display_name || null); + } + const revGeoSessionKey = (lat, lon) => `wc:revgeo:${lat.toFixed(4)},${lon.toFixed(4)}`; + async function reverseGeocodeNominatim(lat, lon){ + try{ + const key = revGeoSessionKey(lat, lon); + const cached = sessionStorage.getItem(key); + if (cached) return cached; + const url = new URL('https://nominatim.openstreetmap.org/reverse'); + url.searchParams.set('lat', String(lat)); + url.searchParams.set('lon', String(lon)); + url.searchParams.set('format', 'json'); + url.searchParams.set('addressdetails', '1'); + url.searchParams.set('accept-language', 'en'); + const r = await fetch(url.toString(), { cache: 'no-store' }); + if (!r.ok) throw new Error(`Nominatim ${r.status}`); + const j = await r.json(); + const label = labelFromNominatim(j); + if (label) sessionStorage.setItem(key, label); + return label; + }catch(e){ + console.warn('reverseGeocode failed', e); + return null; + } + } + + // Rendering root + function render(data){ + const tz = data.timezone || 'UTC'; + const todayYMD = ymdNowInTZ(tz); + + locationTitle.textContent=`Forecast · ${dedupeLocationLabel(data.location)}`; + if (generatedAt) generatedAt.textContent=`Updated (local to forecast): ${formatInTZ(data.generated_at_utc, tz)}`; + + daysEl.innerHTML=''; + (data.days||[]).slice(0,7).forEach(d=>daysEl.appendChild(renderDay(d, data.units, todayYMD))); + + refreshTheme(data); updateRainFlag(data); updateSnowFlag(data); + + if (radarPanel && radarPanel.style.display === 'none') { + radarPanel.style.display = 'block'; + initRadar(); + } + if (Number.isFinite(data.lat) && Number.isFinite(data.lon) && window.__WC_centerRadar) { + window.__WC_centerRadar(data.lat, data.lon); + } + const coords = parseLatLonLabel(data.location); + if (coords){ + reverseGeocodeNominatim(coords.lat, coords.lon).then(label=>{ + if (label && locationTitle) { + locationTitle.textContent = `Forecast · ${dedupeLocationLabel(label)}`; + if (qInput && (qInput.value.trim() === '' || qInput.value === 'Your location')) qInput.value = label; + } + }); + } + } + + // Submit handler + async function handleSubmit(e){ + e.preventDefault(); + const q=(qInput.value||'').trim(); if(!q) return; + closeSuggest(); + daysEl.innerHTML='\nLoading…\n'; + try{ + const data=await queryForecast(q); + render(data); + history.replaceState({}, "", `?q=${encodeURIComponent(q)}`); + saveLastLocation({ q, label: data.location }); + }catch(err){ + console.error(err); + daysEl.innerHTML=`\nUnable to load forecast. ${err.message || err}\n`; + } + } + if(form) form.addEventListener('submit', handleSubmit); + + // Theme control + if(themeSelect){ + const saved=(localStorage.getItem('themeMode')||'auto'); + themeSelect.value=saved; applyTheme(saved); + themeSelect.addEventListener('change',()=>{ const mode=themeSelect.value||'auto'; localStorage.setItem('themeMode',mode); applyTheme(mode); }); + }else applyTheme(localStorage.getItem('themeMode')||'auto'); + + // --- Autocomplete (unchanged from v6.3) --- + const suggBox=document.createElement('div'); + suggBox.className='sugg-box'; + suggBox.setAttribute('role','listbox'); + suggBox.id='suggestions'; + suggBox.style.position='fixed'; + suggBox.style.display='none'; + suggBox.style.zIndex='10000'; + document.body.appendChild(suggBox); + let tId=null; let suppressSuggestOnce=false; let committing=false; + function positionSuggest(anchorEl){ + if(!anchorEl || !suggBox || suggBox.style.display!=='block') return; + const rect=anchorEl.getBoundingClientRect(); + const vw=window.innerWidth || document.documentElement.clientWidth || 360; + const vh=window.innerHeight|| document.documentElement.clientHeight|| 640; + const isNarrow=vw<=640; + const width=Math.min(520, Math.round(vw * (isNarrow ? 0.96 : 0.86))); + const left = isNarrow ? Math.round((vw - width)/2) : Math.round(rect.left + (rect.width/2) - (width/2)); + const top = Math.round(rect.bottom) + 6; + const margin=6; + const clampedLeft=Math.max(margin, Math.min(left, vw - width - margin)); + const maxH=Math.max(120, vh - top - margin - 10); + suggBox.style.transform='none'; + suggBox.style.left = `${clampedLeft}px`; + suggBox.style.top = `${Math.max(margin, top)}px`; + suggBox.style.width= `${width}px`; + suggBox.style.maxWidth= `${Math.round(vw*0.98)}px`; + suggBox.style.maxHeight= `${maxH}px`; + } + function closeSuggest(){ suggBox.style.display='none'; suggBox.innerHTML=''; } + qInput?.addEventListener('input', ()=>{ + const q=qInput.value.trim(); + if(tId) clearTimeout(tId); + if(suppressSuggestOnce){ suppressSuggestOnce=false; return; } + if(q.length<2 || committing || document.activeElement!==qInput){ closeSuggest(); return; } + tId=setTimeout(()=>fetchSuggest(q), 200); + }); + document.addEventListener('click',(e)=>{ if(!suggBox.contains(e.target) && e.target!==qInput) closeSuggest(); }); + window.addEventListener('resize', ()=>{ if(suggBox.style.display==='block') positionSuggest(qInput); }); + window.addEventListener('scroll', ()=>{ if(suggBox.style.display==='block') positionSuggest(qInput); }, {passive:true}); + async function fetchSuggest(q){ + try{ + if(committing) return; + const url=new URL('https://geocoding-api.open-meteo.com/v1/search'); + url.searchParams.set('name', q); + url.searchParams.set('count','8'); + url.searchParams.set('language','en'); + url.searchParams.set('format','json'); + const r=await fetch(url.toString(),{cache:'no-store'}); + const j=await r.json(); + const list=(j.results||[]).map(r=>({ label:[r.name, r.admin1, r.country].filter(Boolean).join(', '), + query:[r.name, r.admin1, r.country].filter(Boolean).join(', ') })); + showSuggest(list); + positionSuggest(qInput); + }catch(e){ console.warn('suggest error',e); closeSuggest(); } + } + function showSuggest(items){ + suggBox.innerHTML=''; + if(!items.length){ closeSuggest(); return; } + items.forEach((it)=>{ + const opt=document.createElement('div'); + opt.className='sugg-item'; + opt.textContent=it.label; + opt.setAttribute('role','option'); + opt.setAttribute('tabindex','0'); + const choose=(e)=>{ + if(e) e.preventDefault(); + committing=true; + suppressSuggestOnce=true; + qInput.value=it.query; + qInput.blur(); + closeSuggest(); + if(form && typeof form.requestSubmit==='function'){ form.requestSubmit(); } + else{ form.dispatchEvent(new Event('submit',{bubbles:true, cancelable:true})); } + setTimeout(()=>{ committing=false; }, 300); + }; + opt.addEventListener('click', choose); + opt.addEventListener('keydown', (e)=>{ if(e.key==='Enter'||e.key===' ') choose(e); }); + suggBox.appendChild(opt); + }); + suggBox.style.display='block'; + positionSuggest(qInput); + } + + // Coord fetch helper + async function queryForecastByCoords(lat, lon) { + const url = new URL(`${API_BASE}/forecast.py`, window.location.origin); + url.searchParams.set('lat', String(lat)); + url.searchParams.set('lon', String(lon)); + const res = await fetch(url.toString(), { cache: 'no-store' }); + const txt = await res.text().catch(() => ''); + if (!res.ok) throw new Error(`API error ${res.status}: ${txt.slice(0,200)}`); + return JSON.parse(txt); + } + + // Use current location + const useMyLocationBtn = document.getElementById('useMyLocation'); + function showInlineMessage(msg) { if (!daysEl) return; daysEl.innerHTML = `\n${msg}\n`; } + async function handleUseMyLocation() { + if (!('geolocation' in navigator)) { showInlineMessage('Geolocation isn’t available in this browser.'); return; } + const prevHTML = daysEl.innerHTML; + showInlineMessage('Getting your location…'); + const options = { enableHighAccuracy: false, timeout: 10000, maximumAge: 60_000 }; + let settled = false; + const done = () => { settled = true; }; + try { + await new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition(resolve, reject, options); }) + .then(async (pos) => { + const { latitude, longitude } = pos.coords || {}; + if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) { throw new Error('Got invalid coordinates.'); } + showInlineMessage('Loading forecast for your location…'); + const data = await queryForecastByCoords(latitude, longitude); + render(data); + history.replaceState({}, '', `?lat=${latitude.toFixed(4)}&lon=${longitude.toFixed(4)}`); + const pretty = await reverseGeocodeNominatim(latitude, longitude); + if (pretty && qInput) qInput.value = pretty; else if (qInput) qInput.value = 'Your location'; + saveLastLocation({ lat: latitude, lon: longitude, label: pretty || `${latitude.toFixed(4)},${longitude.toFixed(4)}` }); + }).finally(done); + } catch (err) { + console.error(err); + if (!settled) showInlineMessage('Could not get your location. You can type a city or ZIP instead.'); + else showInlineMessage('Could not get your location. You can type a city or ZIP instead.'); + setTimeout(() => { if (daysEl && daysEl.innerHTML.includes('Could not get')) daysEl.innerHTML = prevHTML; }, 3000); + } + } + if (useMyLocationBtn) { useMyLocationBtn.addEventListener('click', handleUseMyLocation); } + + // Persist/restore last location + const lastLocKey = 'wc:lastLocation'; + function saveLastLocation(obj){ + try{ const payload = { ...obj, t: Date.now() }; localStorage.setItem(lastLocKey, JSON.stringify(payload)); }catch{} + } + function loadLastLocation(){ + try{ + const raw = localStorage.getItem(lastLocKey); + if (!raw) return null; + const j = JSON.parse(raw); + if (j && (typeof j.q === 'string' || (Number.isFinite(j.lat) && Number.isFinite(j.lon)))) return j; + return null; + }catch{ return null; } + } + + // Bootstrap + const params=new URLSearchParams(window.location.search); + const qParam=params.get('q'); + const latParam=params.get('lat'), lonParam=params.get('lon'); + if(qParam){ qInput.value=qParam; } + else if(latParam && lonParam){ /* user will submit or use current location */ } + else{ + const last = loadLastLocation(); + if (last){ + (async ()=>{ + try{ + if (typeof last.q === 'string'){ + qInput.value = last.label || last.q; + const data = await queryForecast(last.q); + render(data); + history.replaceState({}, '', `?q=${encodeURIComponent(last.q)}`); + }else if (Number.isFinite(last.lat) && Number.isFinite(last.lon)){ + if (qInput) qInput.value = last.label || 'Your last location'; + const data = await queryForecastByCoords(last.lat, last.lon); + render(data); + history.replaceState({}, '', `?lat=${last.lat.toFixed(4)}&lon=${last.lon.toFixed(4)}`); + } + }catch(e){ + console.warn('restore last location failed', e); + daysEl.innerHTML='\nEnter a city/state or ZIP (e.g., "76084") and tap Get Forecast.\n'; + } + })(); + }else{ + daysEl.innerHTML='\nEnter a city/state or ZIP (e.g., "76084") and tap Get Forecast.\n'; + } + } + + // =============================== + // Time-enabled Radar (unchanged) + // =============================== + const RADAR_IMG_URL = + 'https://mapservices.weather.noaa.gov/eventdriven/rest/services/radar/radar_base_reflectivity_time/ImageServer'; + const FRAME_RATE = 1; + const AUTO_PLAY_ON_LOAD = true; + const mapEl = document.getElementById('radarMap'); + const playBtn = document.getElementById('radarPlayPause'); + const slider = document.getElementById('radarSlider'); + const tsLabel = document.getElementById('radarTs'); + let dbg = document.getElementById('radarDbg'); + if (!dbg) { dbg = document.createElement('div'); dbg.id = 'radarDbg'; dbg.style.cssText = 'margin-top:6px;font-size:12px;color:#9ba3af'; tsLabel?.parentNode?.insertAdjacentElement('afterend', dbg); } + const setDbg = (t) => { if (dbg) dbg.textContent = t; }; + let radarMap, baseTiles, imageLayer; + let frames = [], idx = 0, playing = false, animTimer = null, radarInitialized = false; + let stepMs = 10 * 60 * 1000; + function fmtLocal(d){ return d.toLocaleString(); } + async function buildSnappedFrames() { + let startMs, endMs; + try { + const r = await fetch(`${RADAR_IMG_URL}?f=pjson`, { cache: 'no-store' }); + const j = await r.json(); + if (Array.isArray(j?.timeInfo?.timeExtent) && j.timeInfo.timeExtent.length === 2) { + startMs = j.timeInfo.timeExtent[0]; + endMs = j.timeInfo.timeExtent[1]; + } else { + endMs = Date.now(); startMs = endMs - 4 * 3600 * 1000; + } + stepMs = (typeof j?.timeInfo?.defaultTimeInterval === 'number' && j.timeInfo.defaultTimeInterval > 0) + ? j.timeInfo.defaultTimeInterval : 10 * 60 * 1000; + } catch { + endMs = Date.now(); startMs = endMs - 4 * 3600 * 1000; stepMs = 10 * 60 * 1000; + } + const end = Math.floor(endMs / stepMs) * stepMs; + const out = []; + for (let t = end - 8 * stepMs; t <= end; t += stepMs) { if (t >= startMs) out.push(new Date(t)); } + if (out.length < 2) out.push(new Date(end - stepMs), new Date(end)); + return out; + } + function ensureRadarMap() { + if (!mapEl || typeof L === 'undefined' || !L.esri) return; + if (radarMap) return; + radarMap = L.map('radarMap', { preferCanvas:true, zoomControl:true }) + .setView([37.5, -96.5], 5); + baseTiles = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + attribution: '© OpenStreetMap · Radar © NOAA/NWS', + maxZoom: 12 + }).addTo(radarMap); + imageLayer = L.esri.imageMapLayer({ + url: RADAR_IMG_URL, + f: 'image', + format: 'png32', + opacity: 0.85, + useCors: true + }).addTo(radarMap); + imageLayer.on('load', () => {}); + imageLayer.on('error', (e) => { console.warn('ImageServer exportImage error', e); }); + } + function showFrame() { + if (!frames.length || !imageLayer) return; + const d = frames[idx]; + imageLayer.setTimeRange(d, d); + tsLabel && (tsLabel.textContent = `Showing: ${fmtLocal(d)} (local)`); + slider && (slider.value = String(idx)); + setDbg(`Mode: ImageServer (REST time) · frames=${frames.length} · step=${Math.round(stepMs/60000)}min · ${d.toISOString()}`); + } + function play() { + if (playing || frames.length < 2) return; + playing = true; if (playBtn) playBtn.textContent = '❚❚ Pause'; + animTimer = setInterval(() => { idx = (idx + 1) % frames.length; showFrame(); }, 1000 / FRAME_RATE); + } + function pause() { + playing = false; if (playBtn) playBtn.textContent = '▶︎ Play'; + if (animTimer) { clearInterval(animTimer); animTimer = null; } + } + function toggle(){ playing ? pause() : play(); } + async function initRadar(){ + if (radarInitialized) return; + radarInitialized = true; + ensureRadarMap(); + if (!radarMap || !imageLayer) return; + frames = await buildSnappedFrames(); + idx = frames.length - 1; + if (slider){ + slider.min = '0'; slider.max = String(frames.length - 1); slider.value = String(idx); + slider.disabled = false; + slider.addEventListener('input', () => { idx = +slider.value; showFrame(); }); + } + if (playBtn){ + playBtn.disabled = false; + playBtn.addEventListener('click', toggle); + } + showFrame(); + if (AUTO_PLAY_ON_LOAD) play(); + setInterval(async ()=>{ + const fresh = await buildSnappedFrames(); + if (fresh.length){ + const wasAtEnd = (idx >= frames.length - 2); + frames = fresh; + if (slider) slider.max = String(frames.length - 1); + if (wasAtEnd) idx = frames.length - 1; + showFrame(); + } + }, 300000); + } + window.__WC_centerRadar = function(lat, lon){ + try{ + ensureRadarMap(); + if (!radarMap || !Number.isFinite(lat) || !Number.isFinite(lon)) return; + const z = (lon > -170 && lon < -50 && lat > 15 && lat < 60) ? 7 : 5; + radarMap.setView([lat, lon], z, { animate: true }); + }catch(e){} + }; +})(); \ No newline at end of file diff --git a/esri-leaflet.js b/esri-leaflet.js new file mode 100644 index 0000000..75925da --- /dev/null +++ b/esri-leaflet.js @@ -0,0 +1,12 @@ +/* esri-leaflet - v3.1.0 - Tue Jul 09 2024 08:42:28 GMT-0500 (Central Daylight Time) + * Copyright (c) 2024 Environmental Systems Research Institute, Inc. + * Apache-2.0 */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("leaflet")):"function"==typeof define&&define.amd?define(["exports","leaflet"],e):e(((t="undefined"!=typeof globalThis?globalThis:t||self).L=t.L||{},t.L.esri={}),t.L)}(this,(function(t,e){"use strict";var i="3.1.0",s=window.XMLHttpRequest&&"withCredentials"in new window.XMLHttpRequest,r=""===document.documentElement.style.pointerEvents,o={cors:s,pointerEvents:r},n={attributionWidthOffset:55},a=0;function l(t){var e="";for(var i in t.f=t.f||"json",t)if(Object.prototype.hasOwnProperty.call(t,i)){var s,r=t[i],o=Object.prototype.toString.call(r);e.length&&(e+="&"),s="[object Array]"===o?"[object Object]"===Object.prototype.toString.call(r[0])?JSON.stringify(r):r.join(","):"[object Object]"===o?JSON.stringify(r):"[object Date]"===o?r.valueOf():r,e+=encodeURIComponent(i)+"="+encodeURIComponent(s)}return e.replaceAll("'","%27")}function u(t,i){var s=new window.XMLHttpRequest;return s.onerror=function(r){s.onreadystatechange=e.Util.falseFn,t.call(i,{error:{code:500,message:"XMLHttpRequest error"}},null)},s.onreadystatechange=function(){var r,o;if(4===s.readyState){try{r=JSON.parse(s.responseText)}catch(t){r=null,o={code:500,message:"Could not parse response as JSON. This could also be caused by a CORS or XMLHttpRequest error."}}!o&&r.error&&(o=r.error,r=null),s.onerror=e.Util.falseFn,t.call(i,o,r)}},s.ontimeout=function(){this.onerror()},s}function c(t,e,i,s){var r=u(i,s);return r.open("POST",t),null!=s&&void 0!==s.options&&(r.timeout=s.options.timeout),r.setRequestHeader("Content-Type","application/x-www-form-urlencoded; charset=UTF-8"),r.send(l(e)),r}function h(t,e,i,s){var r=u(i,s);return r.open("GET",t+"?"+l(e),!0),null!=s&&void 0!==s.options&&(r.timeout=s.options.timeout,s.options.withCredentials&&(r.withCredentials=!0)),r.send(null),r}function p(t,e,i,s){var r=l(e),n=u(i,s),a=(t+"?"+r).length;if(a<=2e3&&o.cors?n.open("GET",t+"?"+r):a>2e3&&o.cors&&(n.open("POST",t),n.setRequestHeader("Content-Type","application/x-www-form-urlencoded; charset=UTF-8")),null!=s&&void 0!==s.options&&(n.timeout=s.options.timeout,s.options.withCredentials&&(n.withCredentials=!0)),a<=2e3&&o.cors)n.send(null);else{if(!(a>2e3&&o.cors))return a<=2e3&&!o.cors?m(t,e,i,s):void f("a request to "+t+" was longer then 2000 characters and this browser cannot make a cross-domain post request. Please use a proxy https://developers.arcgis.com/esri-leaflet/api-reference/request/");n.send(r)}return n}function m(t,i,s,r){window._EsriLeafletCallbacks=window._EsriLeafletCallbacks||{};var o="c"+a;i.callback="window._EsriLeafletCallbacks."+o,window._EsriLeafletCallbacks[o]=function(t){if(!0!==window._EsriLeafletCallbacks[o]){var e,i=Object.prototype.toString.call(t);"[object Object]"!==i&&"[object Array]"!==i&&(e={error:{code:500,message:"Expected array or object as JSONP response"}},t=null),!e&&t.error&&(e=t,t=null),s.call(r,e,t),window._EsriLeafletCallbacks[o]=!0}};var n=e.DomUtil.create("script",null,document.body);return n.type="text/javascript",n.src=t+"?"+l(i),n.id=o,n.onerror=function(t){if(t&&!0!==window._EsriLeafletCallbacks[o]){s.call(r,{error:{code:500,message:"An unknown error occurred"}}),window._EsriLeafletCallbacks[o]=!0}},e.DomUtil.addClass(n,"esri-leaflet-jsonp"),a++,{id:o,url:n.src,abort:function(){window._EsriLeafletCallbacks._callback[o]({code:0,message:"Request aborted."})}}}var d=o.cors?h:m;function f(){console&&console.warn&&console.warn.apply(console,arguments)}d.CORS=h,d.JSONP=m;var y={request:p,get:d,post:c},g=function(t,e,i,s){var r=(s[0]-i[0])*(t[1]-i[1])-(s[1]-i[1])*(t[0]-i[0]),o=(e[0]-t[0])*(t[1]-i[1])-(e[1]-t[1])*(t[0]-i[0]),n=(s[1]-i[1])*(e[0]-t[0])-(s[0]-i[0])*(e[1]-t[1]);if(0!==n){var a=r/n,l=o/n;if(a>=0&&a<=1&&l>=0&&l<=1)return!0}return!1},_=function(t,e){for(var i=0;i=0},x=function(t){var e={};for(var i in t)t.hasOwnProperty(i)&&(e[i]=t[i]);return e},S=function(t,e){var i=_(t,e),s=function(t,e){for(var i=!1,s=-1,r=t.length,o=r-1;++s=0;e--)if(i=r[e][0],S(i,s)){r[e].push(s),c=!0;break}c||u.push(s)}for(;u.length;){s=u.pop();var h=!1;for(e=r.length-1;e>=0;e--)if(i=r[e][0],_(i,s)){r[e].push(s),h=!0;break}h||r.push([s.reverse()])}return 1===r.length?{type:"Polygon",coordinates:r[0]}:{type:"MultiPolygon",coordinates:r}}(e.rings.slice(0))),"number"==typeof e.xmin&&"number"==typeof e.ymin&&"number"==typeof e.xmax&&"number"==typeof e.ymax&&(s.type="Polygon",s.coordinates=[[[e.xmax,e.ymax],[e.xmin,e.ymax],[e.xmin,e.ymin],[e.xmax,e.ymin],[e.xmax,e.ymax]]]),(e.geometry||e.attributes)&&(s.type="Feature",s.geometry=e.geometry?t(e.geometry):null,s.properties=e.attributes?x(e.attributes):null,e.attributes))try{s.id=function(t,e){for(var i=e?[e,"OBJECTID","FID"]:["OBJECTID","FID"],s=0;s=4){b(s)||s.reverse(),e.push(s);for(var r=0;r=4&&(b(o)&&o.reverse(),e.push(o))}}return e},L=function t(e,i){i=i||"OBJECTID";var s,r={wkid:4326},o={};switch(e.type){case"Point":o.x=e.coordinates[0],o.y=e.coordinates[1],e.coordinates[2]&&(o.z=e.coordinates[2]),o.spatialReference=r;break;case"MultiPoint":o.points=e.coordinates.slice(0),e.coordinates[0][2]&&(o.hasZ=!0),o.spatialReference=r;break;case"LineString":o.paths=[e.coordinates.slice(0)],e.coordinates[0][2]&&(o.hasZ=!0),o.spatialReference=r;break;case"MultiLineString":o.paths=e.coordinates.slice(0),e.coordinates[0][0][2]&&(o.hasZ=!0),o.spatialReference=r;break;case"Polygon":o.rings=T(e.coordinates.slice(0)),e.coordinates[0][0][2]&&(o.hasZ=!0),o.spatialReference=r;break;case"MultiPolygon":o.rings=function(t){for(var e=[],i=0;i=0;r--){var o=s[r].slice(0);e.push(o)}return e}(e.coordinates.slice(0)),e.coordinates[0][0][0][2]&&(o.hasZ=!0),o.spatialReference=r;break;case"Feature":e.geometry&&(o.geometry=t(e.geometry,i)),o.attributes=e.properties?x(e.properties):{},e.id&&(o.attributes[i]=e.id);break;case"FeatureCollection":for(o=[],s=0;s=0;n--){var a=C(s[n],i||k(s[n]));o.features.push(a)}return o}function G(t){return"/"!==(t=e.Util.trim(t))[t.length-1]&&(t+="/"),t}function U(t){if(-1!==t.url.indexOf("?")){t.requestParams=t.requestParams||{};var e=t.url.substring(t.url.indexOf("?")+1);t.url=t.url.split("?")[0],t.requestParams=JSON.parse('{"'+decodeURI(e).replace(/"/g,'\\"').replace(/&/g,'","').replace(/=/g,'":"')+'"}')}return t.url=G(t.url.split("?")[0]),t}function q(t){return/^(?!.*utility\.arcgis\.com).*\.arcgis\.com.*FeatureServer/i.test(t)}function z(t){var e;switch(t){case"Point":e="esriGeometryPoint";break;case"MultiPoint":e="esriGeometryMultipoint";break;case"LineString":case"MultiLineString":e="esriGeometryPolyline";break;case"Polygon":case"MultiPolygon":e="esriGeometryPolygon"}return e}function M(t){return t.getSize().x-n.attributionWidthOffset+"px"}function Z(t){if(t.attributionControl){if(t.attributionControl._esriAttributionLayerCount||(t.attributionControl._esriAttributionLayerCount=0),0===t.attributionControl._esriAttributionLayerCount){if(!t.attributionControl._esriAttributionAddedOnce){var i=document.createElement("style");i.type="text/css",i.innerHTML=".esri-truncated-attribution:hover {white-space: normal;}",document.getElementsByTagName("head")[0].appendChild(i);var s=document.createElement("style");s.type="text/css",s.innerHTML=".esri-truncated-attribution {vertical-align: -3px;white-space: nowrap;overflow: hidden;text-overflow: ellipsis;display: inline-block;transition: 0s white-space;transition-delay: 1s;max-width: "+M(t)+";}",document.getElementsByTagName("head")[0].appendChild(s),t.on("resize",(function(e){t.attributionControl&&(t.attributionControl._container.style.maxWidth=M(e.target))})),t.attributionControl._esriAttributionAddedOnce=!0}e.DomUtil.addClass(t.attributionControl._container,"esri-truncated-attribution:hover"),e.DomUtil.addClass(t.attributionControl._container,"esri-truncated-attribution")}t.attributionControl._esriAttributionLayerCount=t.attributionControl._esriAttributionLayerCount+1}}function E(t){t.attributionControl&&(t.attributionControl._esriAttributionLayerCount&&1===t.attributionControl._esriAttributionLayerCount&&(e.DomUtil.removeClass(t.attributionControl._container,"esri-truncated-attribution:hover"),e.DomUtil.removeClass(t.attributionControl._container,"esri-truncated-attribution")),t.attributionControl._esriAttributionLayerCount=t.attributionControl._esriAttributionLayerCount-1)}function D(t){var i={geometry:null,geometryType:null};return t instanceof e.LatLngBounds?(i.geometry=R(t),i.geometryType="esriGeometryEnvelope",i):(t.getLatLng&&(t=t.getLatLng()),t instanceof e.LatLng&&(t={type:"Point",coordinates:[t.lng,t.lat]}),t instanceof e.GeoJSON&&(t=t.getLayers()[0].feature.geometry,i.geometry=I(t),i.geometryType=z(t.type)),t.toGeoJSON&&(t=t.toGeoJSON()),"Feature"===t.type&&(t=t.geometry),"Point"===t.type||"LineString"===t.type||"Polygon"===t.type||"MultiPolygon"===t.type?(i.geometry=I(t),i.geometryType=z(t.type),i):void f("invalid geometry passed to spatial query. Should be L.LatLng, L.LatLngBounds, L.Marker or a GeoJSON Point, Line, Polygon or MultiPolygon object"))}function B(t,i){o.cors&&p(t,{},e.Util.bind((function(t,s){if(!t){i._esriAttributions=[];for(var r=0;r=c.minZoom&&l<=c.maxZoom&&(o+=", "+h)}o='Powered by Esri | '+o.substr(2),r.innerHTML=o,r.style.maxWidth=M(i),i.fire("attributionupdated",{attribution:o})}}}var j={warn:f,cleanUrl:G,getUrlParams:U,isArcgisOnline:q,geojsonTypeToArcGIS:z,responseToFeatureCollection:P,geojsonToArcGIS:I,arcgisToGeoJSON:C,boundsToExtent:R,extentToBounds:O,calcAttributionWidth:M,setEsriAttribution:Z,removeEsriAttribution:E,_setGeometry:D,_getAttributionData:B,_updateMapAttribution:N,_findIdAttributeFromFeature:k,_findIdAttributeFromResponse:F},W=e.Class.extend({options:{proxy:!1,useCors:s},generateSetter:function(t,i){return e.Util.bind((function(e){return this.params[t]=e,this}),i)},initialize:function(t){if(t.request&&t.options?(this._service=t,e.Util.setOptions(this,t.options)):(e.Util.setOptions(this,t),this.options.url=G(t.url)),this.params=e.Util.extend({},this.params||{}),this.setters)for(var i in this.setters){var s=this.setters[i];this[i]=this.generateSetter(s,this)}},token:function(t){return this._service?this._service.authenticate(t):this.params.token=t,this},apikey:function(t){return this.token(t)},format:function(t){return this.params.returnUnformattedValues=!t,this},request:function(t,i){return this.options.requestParams&&e.Util.extend(this.params,this.options.requestParams),this._service?this._service.request(this.path,this.params,t,i):this._request("request",this.path,this.params,t,i)},_request:function(t,e,i,s,r){var o=this.options.proxy?this.options.proxy+"?"+this.options.url+e:this.options.url+e;return"get"!==t&&"request"!==t||this.options.useCors?y[t](o,i,s,r):y.get.JSONP(o,i,s,r)}});var J=W.extend({setters:{offset:"resultOffset",limit:"resultRecordCount",fields:"outFields",precision:"geometryPrecision",featureIds:"objectIds",returnGeometry:"returnGeometry",returnM:"returnM",transform:"datumTransformation",token:"token"},path:"query",params:{returnGeometry:!0,where:"1=1",outSR:4326,outFields:"*"},within:function(t){return this._setGeometryParams(t),this.params.spatialRel="esriSpatialRelContains",this},intersects:function(t){return this._setGeometryParams(t),this.params.spatialRel="esriSpatialRelIntersects",this},contains:function(t){return this._setGeometryParams(t),this.params.spatialRel="esriSpatialRelWithin",this},crosses:function(t){return this._setGeometryParams(t),this.params.spatialRel="esriSpatialRelCrosses",this},touches:function(t){return this._setGeometryParams(t),this.params.spatialRel="esriSpatialRelTouches",this},overlaps:function(t){return this._setGeometryParams(t),this.params.spatialRel="esriSpatialRelOverlaps",this},bboxIntersects:function(t){return this._setGeometryParams(t),this.params.spatialRel="esriSpatialRelEnvelopeIntersects",this},indexIntersects:function(t){return this._setGeometryParams(t),this.params.spatialRel="esriSpatialRelIndexIntersects",this},nearby:function(t,i){return t=e.latLng(t),this.params.geometry=[t.lng,t.lat],this.params.geometryType="esriGeometryPoint",this.params.spatialRel="esriSpatialRelIntersects",this.params.units="esriSRUnit_Meter",this.params.distance=i,this.params.inSR=4326,this},where:function(t){return this.params.where=t,this},between:function(t,e){return this.params.time=[t.valueOf(),e.valueOf()],this},simplify:function(t,e){var i=Math.abs(t.getBounds().getWest()-t.getBounds().getEast());return this.params.maxAllowableOffset=i/t.getSize().y*e,this},orderBy:function(t,e){return e=e||"ASC",this.params.orderByFields=this.params.orderByFields?this.params.orderByFields+",":"",this.params.orderByFields+=[t,e].join(" "),this},run:function(t,e){return this._cleanParams(),this.options.isModern||q(this.options.url)&&void 0===this.options.isModern?(this.params.f="geojson",this.request((function(i,s){this._trapSQLerrors(i),t.call(e,i,s,s)}),this)):this.request((function(i,s){this._trapSQLerrors(i),t.call(e,i,s&&P(s),s)}),this)},count:function(t,e){return this._cleanParams(),this.params.returnCountOnly=!0,this.request((function(e,i){t.call(this,e,i&&i.count,i)}),e)},ids:function(t,e){return this._cleanParams(),this.params.returnIdsOnly=!0,this.request((function(e,i){t.call(this,e,i&&i.objectIds,i)}),e)},bounds:function(t,e){return this._cleanParams(),this.params.returnExtentOnly=!0,this.request((function(i,s){s&&s.extent&&O(s.extent)?t.call(e,i,O(s.extent),s):(i={message:"Invalid Bounds"},t.call(e,i,null,s))}),e)},distinct:function(){return this.params.returnGeometry=!1,this.params.returnDistinctValues=!0,this},pixelSize:function(t){var i=e.point(t);return this.params.pixelSize=[i.x,i.y],this},layer:function(t){return this.path=t+"/query",this},_trapSQLerrors:function(t){t&&"400"===t.code&&f("one common syntax error in query requests is encasing string values in double quotes instead of single quotes")},_cleanParams:function(){delete this.params.returnIdsOnly,delete this.params.returnExtentOnly,delete this.params.returnCountOnly},_setGeometryParams:function(t){this.params.inSR=4326;var e=D(t);this.params.geometry=e.geometry,this.params.geometryType=e.geometryType}});function V(t){return new J(t)}var Q=W.extend({setters:{contains:"contains",text:"searchText",fields:"searchFields",spatialReference:"sr",sr:"sr",layers:"layers",returnGeometry:"returnGeometry",maxAllowableOffset:"maxAllowableOffset",precision:"geometryPrecision",dynamicLayers:"dynamicLayers",returnZ:"returnZ",returnM:"returnM",gdbVersion:"gdbVersion",token:"token"},path:"find",params:{sr:4326,contains:!0,returnGeometry:!0,returnZ:!0,returnM:!1},layerDefs:function(t,e){return this.params.layerDefs=this.params.layerDefs?this.params.layerDefs+";":"",this.params.layerDefs+=[t,e].join(":"),this},simplify:function(t,e){var i=Math.abs(t.getBounds().getWest()-t.getBounds().getEast());return this.params.maxAllowableOffset=i/t.getSize().y*e,this},run:function(t,e){return this.request((function(i,s){t.call(e,i,s&&P(s),s)}),e)}});function K(t){return new Q(t)}var H=W.extend({path:"identify",between:function(t,e){return this.params.time=[t.valueOf(),e.valueOf()],this}});var X=H.extend({setters:{layers:"layers",precision:"geometryPrecision",tolerance:"tolerance",returnGeometry:"returnGeometry"},params:{sr:4326,layers:"all",tolerance:3,returnGeometry:!0},on:function(t){var e=R(t.getBounds()),i=t.getSize();return this.params.imageDisplay=[i.x,i.y,96],this.params.mapExtent=[e.xmin,e.ymin,e.xmax,e.ymax],this},at:function(t){return 2===t.length&&(t=e.latLng(t)),this._setGeometryParams(t),this},layerDef:function(t,e){return this.params.layerDefs=this.params.layerDefs?this.params.layerDefs+";":"",this.params.layerDefs+=[t,e].join(":"),this},simplify:function(t,e){var i=Math.abs(t.getBounds().getWest()-t.getBounds().getEast());return this.params.maxAllowableOffset=i/t.getSize().y*e,this},run:function(t,e){return this.request((function(i,s){if(i)t.call(e,i,void 0,s);else{var r=P(s);s.results=s.results.reverse();for(var o=0;o=0;o--)r.catalogItems.features[o].properties.catalogItemVisibility=s[o];return r}});function tt(t){return new $(t)}var et=e.Evented.extend({options:{proxy:!1,useCors:s,timeout:0},initialize:function(t){t=t||{},this._requestQueue=[],this._authenticating=!1,e.Util.setOptions(this,t),this.options.url=G(this.options.url)},get:function(t,e,i,s){return this._request("get",t,e,i,s)},post:function(t,e,i,s){return this._request("post",t,e,i,s)},request:function(t,e,i,s){return this._request("request",t,e,i,s)},metadata:function(t,e){return this._request("get","",{},t,e)},authenticate:function(t){return this._authenticating=!1,this.options.token=t,this._runQueue(),this},getTimeout:function(){return this.options.timeout},setTimeout:function(t){this.options.timeout=t},_request:function(t,i,s,r,o){this.fire("requeststart",{url:this.options.url+i,params:s,method:t},!0);var n=this._createServiceCallback(t,i,s,r,o);if(this.options.token&&(s.token=this.options.token),this.options.requestParams&&e.Util.extend(s,this.options.requestParams),!this._authenticating){var a=this.options.proxy?this.options.proxy+"?"+this.options.url+i:this.options.url+i;return"get"!==t&&"request"!==t||this.options.useCors?y[t](a,s,n,o):y.get.JSONP(a,s,n,o)}this._requestQueue.push([t,i,s,r,o])},_createServiceCallback:function(t,i,s,r,o){return e.Util.bind((function(n,a){!n||499!==n.code&&498!==n.code||(this._authenticating=!0,this._requestQueue.push([t,i,s,r,o]),this.fire("authenticationrequired",{authenticate:e.Util.bind(this.authenticate,this)},!0),n.authenticate=e.Util.bind(this.authenticate,this)),r.call(o,n,a),n?this.fire("requesterror",{url:this.options.url+i,params:s,message:n.message,code:n.code,method:t},!0):this.fire("requestsuccess",{url:this.options.url+i,params:s,response:a,method:t},!0),this.fire("requestend",{url:this.options.url+i,params:s,method:t},!0)}),this)},_runQueue:function(){for(var t=this._requestQueue.length-1;t>=0;t--){var e=this._requestQueue[t];this[e.shift()].apply(this,e)}this._requestQueue=[]}});var it=et.extend({identify:function(){return Y(this)},find:function(){return K(this)},query:function(){return V(this)}});function st(t){return new it(t)}var rt=et.extend({query:function(){return V(this)},identify:function(){return tt(this)}});function ot(t){return new rt(t)}var nt=et.extend({options:{idAttribute:"OBJECTID"},query:function(){return V(this)},addFeature:function(t,e,i){this.addFeatures(t,e,i)},addFeatures:function(t,e,i){for(var s=t.features?t.features:[t],r=s.length-1;r>=0;r--)delete s[r].id;return t=I(t),t=s.length>1?t:[t],this.post("addFeatures",{features:t},(function(t,s){var r=s&&s.addResults?s.addResults.length>1?s.addResults:s.addResults[0]:void 0;e&&e.call(i,t||s.addResults[0].error,r)}),i)},updateFeature:function(t,e,i){this.updateFeatures(t,e,i)},updateFeatures:function(t,e,i){var s=t.features?t.features:[t];return t=I(t,this.options.idAttribute),t=s.length>1?t:[t],this.post("updateFeatures",{features:t},(function(t,s){var r=s&&s.updateResults?s.updateResults.length>1?s.updateResults:s.updateResults[0]:void 0;e&&e.call(i,t||s.updateResults[0].error,r)}),i)},deleteFeature:function(t,e,i){this.deleteFeatures(t,e,i)},deleteFeatures:function(t,e,i){return this.post("deleteFeatures",{objectIds:t},(function(t,s){var r=s&&s.deleteResults?s.deleteResults.length>1?s.deleteResults:s.deleteResults[0]:void 0;e&&e.call(i,t||s.deleteResults[0].error,r)}),i)}});function at(t){return new nt(t)}var lt="https:"!==window.location.protocol?"http:":"https:",ut=e.TileLayer.extend({statics:{TILES:{Streets:{deprecated:!0,tokenRequired:!1,urlTemplate:lt+"//{s}.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:19,subdomains:["server","services"],attribution:"USGS, NOAA",attributionUrl:"https://static.arcgis.com/attribution/World_Street_Map"}},Topographic:{deprecated:!0,tokenRequired:!1,urlTemplate:lt+"//{s}.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:19,subdomains:["server","services"],attribution:"USGS, NOAA",attributionUrl:"https://static.arcgis.com/attribution/World_Topo_Map"}},Oceans:{deprecated:!0,tokenRequired:!1,urlTemplate:lt+"//{s}.arcgisonline.com/arcgis/rest/services/Ocean/World_Ocean_Base/MapServer/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:16,subdomains:["server","services"],attribution:"USGS, NOAA",attributionUrl:"https://static.arcgis.com/attribution/Ocean_Basemap"}},OceansLabels:{deprecated:!0,tokenRequired:!1,urlTemplate:lt+"//{s}.arcgisonline.com/arcgis/rest/services/Ocean/World_Ocean_Reference/MapServer/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:16,subdomains:["server","services"],pane:r?"esri-labels":"tilePane",attribution:""}},NationalGeographic:{deprecated:!0,tokenRequired:!1,urlTemplate:lt+"//{s}.arcgisonline.com/ArcGIS/rest/services/NatGeo_World_Map/MapServer/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:16,subdomains:["server","services"],attribution:"National Geographic, DeLorme, HERE, UNEP-WCMC, USGS, NASA, ESA, METI, NRCAN, GEBCO, NOAA, increment P Corp."}},DarkGray:{deprecated:!0,tokenRequired:!1,urlTemplate:lt+"//{s}.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Dark_Gray_Base/MapServer/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:16,subdomains:["server","services"],attribution:"HERE, DeLorme, MapmyIndia, © OpenStreetMap contributors"}},DarkGrayLabels:{deprecated:!0,tokenRequired:!1,urlTemplate:lt+"//{s}.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Dark_Gray_Reference/MapServer/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:16,subdomains:["server","services"],pane:r?"esri-labels":"tilePane",attribution:""}},Gray:{deprecated:!0,tokenRequired:!1,urlTemplate:lt+"//{s}.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:16,subdomains:["server","services"],attribution:"HERE, DeLorme, MapmyIndia, © OpenStreetMap contributors"}},GrayLabels:{deprecated:!0,tokenRequired:!1,urlTemplate:lt+"//{s}.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Reference/MapServer/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:16,subdomains:["server","services"],pane:r?"esri-labels":"tilePane",attribution:""}},Imagery:{deprecated:!0,tokenRequired:!1,urlTemplate:lt+"//{s}.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:19,subdomains:["server","services"],attribution:"DigitalGlobe, GeoEye, i-cubed, USDA, USGS, AEX, Getmapping, Aerogrid, IGN, IGP, swisstopo, and the GIS User Community",attributionUrl:"https://static.arcgis.com/attribution/World_Imagery"}},ImageryLabels:{deprecated:!0,tokenRequired:!1,urlTemplate:lt+"//{s}.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:19,subdomains:["server","services"],pane:r?"esri-labels":"tilePane",attribution:""}},ImageryTransportation:{deprecated:!0,tokenRequired:!1,urlTemplate:lt+"//{s}.arcgisonline.com/ArcGIS/rest/services/Reference/World_Transportation/MapServer/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:19,subdomains:["server","services"],pane:r?"esri-labels":"tilePane",attribution:""}},ShadedRelief:{deprecated:!0,tokenRequired:!1,urlTemplate:lt+"//{s}.arcgisonline.com/ArcGIS/rest/services/World_Shaded_Relief/MapServer/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:13,subdomains:["server","services"],attribution:"USGS"}},ShadedReliefLabels:{deprecated:!0,tokenRequired:!1,urlTemplate:lt+"//{s}.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places_Alternate/MapServer/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:12,subdomains:["server","services"],pane:r?"esri-labels":"tilePane",attribution:""}},Terrain:{deprecated:!0,tokenRequired:!1,urlTemplate:lt+"//{s}.arcgisonline.com/ArcGIS/rest/services/World_Terrain_Base/MapServer/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:13,subdomains:["server","services"],attribution:"USGS, NOAA"}},TerrainLabels:{deprecated:!0,tokenRequired:!1,urlTemplate:lt+"//{s}.arcgisonline.com/ArcGIS/rest/services/Reference/World_Reference_Overlay/MapServer/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:13,subdomains:["server","services"],pane:r?"esri-labels":"tilePane",attribution:""}},USATopo:{deprecated:!0,tokenRequired:!1,urlTemplate:lt+"//{s}.arcgisonline.com/ArcGIS/rest/services/USA_Topo_Maps/MapServer/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:15,subdomains:["server","services"],attribution:"USGS, National Geographic Society, i-cubed"}},ImageryClarity:{deprecated:!0,tokenRequired:!1,urlTemplate:lt+"//clarity.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:19,attribution:"Esri, DigitalGlobe, GeoEye, Earthstar Geographics, CNES/Airbus DS, USDA, USGS, AeroGRID, IGN, and the GIS User Community"}},Physical:{deprecated:!0,tokenRequired:!1,urlTemplate:lt+"//{s}.arcgisonline.com/arcgis/rest/services/World_Physical_Map/MapServer/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:8,subdomains:["server","services"],attribution:"U.S. National Park Service"}},ImageryFirefly:{deprecated:!0,tokenRequired:!1,urlTemplate:lt+"//fly.maptiles.arcgis.com/arcgis/rest/services/World_Imagery_Firefly/MapServer/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:19,attribution:"Esri, DigitalGlobe, GeoEye, Earthstar Geographics, CNES/Airbus DS, USDA, USGS, AeroGRID, IGN, and the GIS User Community",attributionUrl:"https://static.arcgis.com/attribution/World_Imagery"}},"arcgis/navigation-beta":{deprecated:!1,tokenRequired:!0,urlTemplate:lt+"//static-map-tiles-api.arcgis.com/arcgis/rest/services/static-basemap-tiles-service/beta/arcgis/navigation/static/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:19,tileSize:512,zoomOffset:-1,attribution:"Esri, TomTom, Garmin, FAO, NOAA, USGS, © OpenStreetMap contributors, and the GIS User Community"}},"arcgis/streets-beta":{deprecated:!1,tokenRequired:!0,urlTemplate:lt+"//static-map-tiles-api.arcgis.com/arcgis/rest/services/static-basemap-tiles-service/beta/arcgis/streets/static/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:19,tileSize:512,zoomOffset:-1,attribution:"Esri, TomTom, Garmin, FAO, NOAA, USGS, © OpenStreetMap contributors, and the GIS User Community"}},"arcgis/outdoor-beta":{deprecated:!1,tokenRequired:!0,urlTemplate:lt+"//static-map-tiles-api.arcgis.com/arcgis/rest/services/static-basemap-tiles-service/beta/arcgis/outdoor/static/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:19,tileSize:512,zoomOffset:-1,attribution:"Esri, TomTom, Garmin, FAO, NOAA, USGS, © OpenStreetMap contributors, and the GIS User Community"}},"arcgis/light-gray-beta":{deprecated:!1,tokenRequired:!0,urlTemplate:lt+"//static-map-tiles-api.arcgis.com/arcgis/rest/services/static-basemap-tiles-service/beta/arcgis/light-gray/static/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:19,tileSize:512,zoomOffset:-1,attribution:"Esri, TomTom, Garmin, FAO, NOAA, USGS, © OpenStreetMap contributors, and the GIS User Community"}},"arcgis/imagery/labels-beta":{deprecated:!1,tokenRequired:!0,urlTemplate:lt+"//static-map-tiles-api.arcgis.com/arcgis/rest/services/static-basemap-tiles-service/beta/arcgis/imagery/labels/static/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:19,tileSize:512,zoomOffset:-1,attribution:"Esri, TomTom, Garmin, FAO, NOAA, USGS, © OpenStreetMap contributors, and the GIS User Community"}},"arcgis/dark-gray-beta":{deprecated:!1,tokenRequired:!0,urlTemplate:lt+"//static-map-tiles-api.arcgis.com/arcgis/rest/services/static-basemap-tiles-service/beta/arcgis/dark-gray/static/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:19,tileSize:512,zoomOffset:-1,attribution:"Esri, TomTom, Garmin, FAO, NOAA, USGS, © OpenStreetMap contributors, and the GIS User Community"}},"arcgis/navigation-night-beta":{deprecated:!1,tokenRequired:!0,urlTemplate:lt+"//static-map-tiles-api.arcgis.com/arcgis/rest/services/static-basemap-tiles-service/beta/arcgis/navigation-night/static/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:19,tileSize:512,zoomOffset:-1,attribution:"Esri, TomTom, Garmin, FAO, NOAA, USGS, © OpenStreetMap contributors, and the GIS User Community"}},"arcgis/streets-night-beta":{deprecated:!1,tokenRequired:!0,urlTemplate:lt+"//static-map-tiles-api.arcgis.com/arcgis/rest/services/static-basemap-tiles-service/beta/arcgis/streets-night/static/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:19,tileSize:512,zoomOffset:-1,attribution:"Esri, TomTom, Garmin, FAO, NOAA, USGS, © OpenStreetMap contributors, and the GIS User Community"}},"arcgis/oceans/labels-beta":{deprecated:!1,tokenRequired:!0,urlTemplate:lt+"//static-map-tiles-api.arcgis.com/arcgis/rest/services/static-basemap-tiles-service/beta/arcgis/oceans/labels/static/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:19,tileSize:512,zoomOffset:-1,attribution:"Esri, TomTom, Garmin, FAO, NOAA, USGS, © OpenStreetMap contributors, and the GIS User Community"}},"arcgis/community-beta":{deprecated:!1,tokenRequired:!0,urlTemplate:lt+"//static-map-tiles-api.arcgis.com/arcgis/rest/services/static-basemap-tiles-service/beta/arcgis/community/static/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:19,tileSize:512,zoomOffset:-1,attribution:"Esri, TomTom, Garmin, FAO, NOAA, USGS, © OpenStreetMap contributors, and the GIS User Community"}},"arcgis/nova-beta":{deprecated:!1,tokenRequired:!0,urlTemplate:lt+"//static-map-tiles-api.arcgis.com/arcgis/rest/services/static-basemap-tiles-service/beta/arcgis/nova/static/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:19,tileSize:512,zoomOffset:-1,attribution:"Esri, TomTom, Garmin, FAO, NOAA, USGS, © OpenStreetMap contributors, and the GIS User Community"}},"arcgis/midcentury-beta":{deprecated:!1,tokenRequired:!0,urlTemplate:lt+"//static-map-tiles-api.arcgis.com/arcgis/rest/services/static-basemap-tiles-service/beta/arcgis/midcentury/static/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:19,tileSize:512,zoomOffset:-1,attribution:"Esri, TomTom, Garmin, FAO, NOAA, USGS, © OpenStreetMap contributors, and the GIS User Community"}},"arcgis/newspaper-beta":{deprecated:!1,tokenRequired:!0,urlTemplate:lt+"//static-map-tiles-api.arcgis.com/arcgis/rest/services/static-basemap-tiles-service/beta/arcgis/newspaper/static/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:19,tileSize:512,zoomOffset:-1,attribution:"Esri, TomTom, Garmin, FAO, NOAA, USGS, © OpenStreetMap contributors, and the GIS User Community"}},"arcgis/human-geography-beta":{deprecated:!1,tokenRequired:!0,urlTemplate:lt+"//static-map-tiles-api.arcgis.com/arcgis/rest/services/static-basemap-tiles-service/beta/arcgis/human-geography/static/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:19,tileSize:512,zoomOffset:-1,attribution:"Esri, TomTom, Garmin, FAO, NOAA, USGS, © OpenStreetMap contributors, and the GIS User Community"}},"arcgis/human-geography-dark-beta":{deprecated:!1,tokenRequired:!0,urlTemplate:lt+"//static-map-tiles-api.arcgis.com/arcgis/rest/services/static-basemap-tiles-service/beta/arcgis/human-geography-dark/static/tile/{z}/{y}/{x}",options:{minZoom:1,maxZoom:19,tileSize:512,zoomOffset:-1,attribution:"Esri, TomTom, Garmin, FAO, NOAA, USGS, © OpenStreetMap contributors, and the GIS User Community"}}}},initialize:function(t,i){var s;const r=Object.entries(ut.TILES).filter((([t,e])=>!e.deprecated)).map((([t,e])=>`"${t}"`));if("object"==typeof t&&t.urlTemplate&&t.options)s=t;else{if("string"!=typeof t||!ut.TILES[t])throw new Error(`L.esri.BasemapLayer: Invalid parameter. Use one of ${r.join(", ")}.`);s=ut.TILES[t]}if(s.tokenRequired&&!0===s.tokenRequired&&(!i||!i.token))throw new Error("L.esri.BasemapLayer: token required.");var o=e.Util.extend(s.options,i);e.Util.setOptions(this,o),!this.options.ignoreDeprecationWarning&&s.deprecated&&!0===s.deprecated&&console.warn(`WARNING: L.esri.BasemapLayer uses data services that are in mature support and are not being updated. Please use one of the new values (${r.join(", ")}) or L.esri.Vector.vectorBasemapLayer instead. More info: https://esriurl.com/esri-leaflet-basemap`);const n=[];if(this.options.token&&n.push("token="+this.options.token),s.tokenRequired&&!0===s.tokenRequired){const t=this.options.language?this.options.language:"en";n.push("language="+t)}n.length>0&&(s.urlTemplate+="?"+n.join("&")),this.options.proxy&&(s.urlTemplate=this.options.proxy+"?"+s.urlTemplate),e.TileLayer.prototype.initialize.call(this,s.urlTemplate,o)},onAdd:function(t){Z(t),"esri-labels"===this.options.pane&&this._initPane(),this.options.attributionUrl&&B((this.options.proxy?this.options.proxy+"?":"")+this.options.attributionUrl,t),t.on("moveend",N),e.TileLayer.prototype.onAdd.call(this,t)},onRemove:function(t){E(t),t.off("moveend",N),e.TileLayer.prototype.onRemove.call(this,t)},_initPane:function(){if(!this._map.getPane(this.options.pane)){var t=this._map.createPane(this.options.pane);t.style.pointerEvents="none",t.style.zIndex=500}},getAttribution:function(){if(this.options.attribution)var t=''+this.options.attribution+"";return t}});var ct=e.TileLayer.extend({options:{zoomOffsetAllowance:.1,errorTileUrl:"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEABAMAAACuXLVVAAAAA1BMVEUzNDVszlHHAAAAAXRSTlMAQObYZgAAAAlwSFlzAAAAAAAAAAAB6mUWpAAAADZJREFUeJztwQEBAAAAgiD/r25IQAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA7waBAAABw08RwAAAAABJRU5ErkJggg=="},statics:{MercatorZoomLevels:{0:156543.033928,1:78271.5169639999,2:39135.7584820001,3:19567.8792409999,4:9783.93962049996,5:4891.96981024998,6:2445.98490512499,7:1222.99245256249,8:611.49622628138,9:305.748113140558,10:152.874056570411,11:76.4370282850732,12:38.2185141425366,13:19.1092570712683,14:9.55462853563415,15:4.77731426794937,16:2.38865713397468,17:1.19432856685505,18:.597164283559817,19:.298582141647617,20:.14929107082381,21:.07464553541191,22:.0373227677059525,23:.0186613838529763}},initialize:function(t){t=U(t=e.Util.setOptions(this,t)),this.tileUrl=(t.proxy?t.proxy+"?":"")+t.url+"tile/{z}/{y}/{x}"+(t.requestParams&&Object.keys(t.requestParams).length>0?e.Util.getParamString(t.requestParams):""),-1!==t.url.indexOf("{s}")&&t.subdomains&&(t.url=t.url.replace("{s}",t.subdomains[0])),this.service=st(t),this.service.addEventParent(this),new RegExp(/tiles.arcgis(online)?\.com/g).test(t.url)&&(this.tileUrl=this.tileUrl.replace("://tiles","://tiles{s}"),t.subdomains=["1","2","3","4"]),this.options.token&&(this.tileUrl+="?token="+this.options.token),e.TileLayer.prototype.initialize.call(this,this.tileUrl,t)},getTileUrl:function(t){var i=this._getZoomForUrl();return e.Util.template(this.tileUrl,e.Util.extend({s:this._getSubdomain(t),x:t.x,y:t.y,z:this._lodMap&&void 0!==this._lodMap[i]?this._lodMap[i]:i},this.options))},createTile:function(t,i){var s=document.createElement("img");return e.DomEvent.on(s,"load",e.Util.bind(this._tileOnLoad,this,i,s)),e.DomEvent.on(s,"error",e.Util.bind(this._tileOnError,this,i,s)),this.options.crossOrigin&&(s.crossOrigin=""),s.alt="",!this._lodMap||this._lodMap&&void 0!==this._lodMap[this._getZoomForUrl()]?s.src=this.getTileUrl(t):this.once("lodmap",(function(){s.src=this.getTileUrl(t)}),this),s},onAdd:function(t){Z(t),this._lodMap||this.metadata((function(i,s){if(!i&&s.spatialReference){var r=s.spatialReference.latestWkid||s.spatialReference.wkid;if(!this.options.attribution&&t.attributionControl&&s.copyrightText&&(this.options.attribution=s.copyrightText,t.attributionControl.addAttribution(this.getAttribution())),t.options.crs!==e.CRS.EPSG3857||102100!==r&&3857!==r)t.options.crs&&t.options.crs.code&&t.options.crs.code.indexOf(r)>-1||f("L.esri.TiledMapLayer is using a non-mercator spatial reference. Support may be available through Proj4Leaflet https://developers.arcgis.com/esri-leaflet/samples/non-mercator-projection/");else{this._lodMap={};for(var o=s.tileInfo.lods,n=ct.MercatorZoomLevels,a=0;athis.options.maxZoom||t0||or&&this._retainParent(o,n,a,r))},_retainChildren:function(t,i,s,r){for(var o=2*t;o<2*t+2;o++)for(var n=2*i;n<2*i+2;n++){var a=new e.Point(o,n);a.z=s+1;var l=this._cellCoordsToKey(a),u=this._cells[l];u&&u.active?u.retain=!0:(u&&u.loaded&&(u.retain=!0),s+11)this._setView(t,s);else{for(var p=o.min.y;p<=o.max.y;p++)for(var m=o.min.x;m<=o.max.x;m++){var d=new e.Point(m,p);if(d.z=this._cellZoom,this._isValidCell(d)){var f=this._cells[this._cellCoordsToKey(d)];f?f.current=!0:a.push(d)}}if(a.sort((function(t,e){return t.distanceTo(n)-e.distanceTo(n)})),0!==a.length)for(this._loading||(this._loading=!0),m=0;ms.max.x)||!i.wrapLat&&(t.ys.max.y))return!1}if(!this.options.bounds)return!0;var r=this._cellCoordsToBounds(t);return e.toLatLngBounds(this.options.bounds).overlaps(r)},_keyToBounds:function(t){return this._cellCoordsToBounds(this._keyToCellCoords(t))},_cellCoordsToNwSe:function(t){var e=this._map,i=this.getCellSize(),s=t.scaleBy(i),r=s.add(i);return[e.unproject(s,t.z),e.unproject(r,t.z)]},_cellCoordsToBounds:function(t){var i=this._cellCoordsToNwSe(t),s=new e.LatLngBounds(i[0],i[1]);return this.options.noWrap||(s=this._map.wrapLatLngBounds(s)),s},_cellCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToCellCoords:function(t){var i=t.split(":"),s=new e.Point(+i[0],+i[1]);return s.z=+i[2],s},_removeCell:function(t){var e=this._cells[t];if(e){var i=this._keyToCellCoords(t),s=this._wrapCoords(i),r=this._cellCoordsToBounds(this._wrapCoords(i));e.current=!1,delete this._cells[t],this._activeCells[t]=e,this.cellLeave(r,s,t),this.fire("cellleave",{key:t,coords:s,bounds:r})}},_reuseCell:function(t){var e=this._cellCoordsToKey(t);this._cells[e]=this._activeCells[e],this._cells[e].current=!0;var i=this._wrapCoords(t),s=this._cellCoordsToBounds(this._wrapCoords(t));this.cellEnter(s,i,e),this.fire("cellenter",{key:e,coords:i,bounds:s})},_createCell:function(t){var i=this._cellCoordsToKey(t),s=this._wrapCoords(t),r=this._cellCoordsToBounds(this._wrapCoords(t));this.createCell(r,s,i),this.fire("cellcreate",{key:i,coords:s,bounds:r}),this._cells[i]={coords:t,current:!0},e.Util.requestAnimFrame(this._pruneCells,this)},_cellReady:function(t,e,i){var s=this._cellCoordsToKey(t);(i=this._cells[s])&&(i.loaded=+new Date,i.active=!0)},_getCellPos:function(t){return t.scaleBy(this.getCellSize())},_wrapCoords:function(t){var i=new e.Point(this._wrapX?e.Util.wrapNum(t.x,this._wrapX):t.x,this._wrapY?e.Util.wrapNum(t.y,this._wrapY):t.y);return i.z=t.z,i},_pxBoundsToCellRange:function(t){var i=this.getCellSize();return new e.Bounds(t.min.unscaleBy(i).floor(),t.max.unscaleBy(i).ceil().subtract([1,1]))}});function yt(t){this.values=[].concat(t||[])}yt.prototype.query=function(t){var e=this.getIndex(t);return this.values[e]},yt.prototype.getIndex=function(t){this.dirty&&this.sort();for(var e,i,s=0,r=this.values.length-1;s<=r;)if(e=(s+r)/2|0,+(i=this.values[Math.round(e)]).value<+t)s=e+1;else{if(!(+i.value>+t))return e;r=e-1}return Math.abs(~r)},yt.prototype.between=function(t,e){var i=this.getIndex(t),s=this.getIndex(e);if(0===i&&0===s)return[];for(;this.values[i-1]&&this.values[i-1].value===t;)i--;for(;this.values[s+1]&&this.values[s+1].value===e;)s++;return this.values[s]&&this.values[s].value===e&&this.values[s+1]&&s++,this.values.slice(i,s)},yt.prototype.insert=function(t){return this.values.splice(this.getIndex(t.value),0,t),this},yt.prototype.bulkAdd=function(t,e){return this.values=this.values.concat([].concat(t||[])),e?this.sort():this.dirty=!0,this},yt.prototype.sort=function(){return this.values.sort((function(t,e){return+e.value-+t.value})).reverse(),this.dirty=!1,this};var gt=ft.extend({options:{attribution:null,where:"1=1",fields:["*"],from:!1,to:!1,timeField:!1,timeFilterMode:"server",simplifyFactor:0,precision:6,fetchAllFeatures:!1},initialize:function(t){if(ft.prototype.initialize.call(this,t),t=U(t),t=e.Util.setOptions(this,t),this.service=at(t),this.service.addEventParent(this),"*"!==this.options.fields[0]){for(var i=!1,s=0;s=0;s--){var r=t[s].id;-1===this._currentSnapshot.indexOf(r)&&this._currentSnapshot.push(r),void 0!==i&&-1===this._cache[i].indexOf(r)&&this._cache[i].push(r)}this.options.timeField&&this._buildTimeIndexes(t),this.createLayers(t)},_buildQuery:function(t,i){var s=this.service.query().intersects(t).where(this.options.where).fields(this.options.fields).precision(this.options.precision);return this.options.fetchAllFeatures&&!isNaN(parseInt(i))&&(s=s.offset(i)),s.params.resultType="tile",this.options.requestParams&&e.Util.extend(s.params,this.options.requestParams),this.options.simplifyFactor&&s.simplify(this._map,this.options.simplifyFactor),"server"===this.options.timeFilterMode&&this.options.from&&this.options.to&&s.between(this.options.from,this.options.to),s},setWhere:function(t,i,s){this.options.where=t&&t.length?t:"1=1";for(var r=[],o=[],n=0,a=null,l=e.Util.bind((function(l,u){if(l&&(a=l),u)for(var c=u.features.length-1;c>=0;c--)o.push(u.features[c].id);--n<=0&&this._visibleZoom()&&t===this.options.where&&(this._currentSnapshot=o,e.Util.requestAnimFrame(e.Util.bind((function(){this.removeLayers(r),this.addLayers(o),i&&i.call(s,a)}),this)))}),this),u=this._currentSnapshot.length-1;u>=0;u--)r.push(this._currentSnapshot[u]);for(var c in this._cache={},this._cells){n++;var h=this._keyToCellCoords(c),p=this._cellCoordsToBounds(h);this._requestFeatures(p,h,l)}return this},getWhere:function(){return this.options.where},getTimeRange:function(){return[this.options.from,this.options.to]},setTimeRange:function(t,i,s,r){var o=this.options.from,n=this.options.to,a=0,l=null,u=e.Util.bind((function(e){e&&(l=e),this._filterExistingFeatures(o,n,t,i),a--,s&&a<=0&&s.call(r,l)}),this);if(this.options.from=t,this.options.to=i,this._filterExistingFeatures(o,n,t,i),"server"===this.options.timeFilterMode)for(var c in this._cells){a++;var h=this._keyToCellCoords(c),p=this._cellCoordsToBounds(h);this._requestFeatures(p,h,u)}return this},refresh:function(){this.setWhere(this.options.where)},_filterExistingFeatures:function(t,i,s,r){var o=t&&i?this._getFeaturesInTimeRange(t,i):this._currentSnapshot,n=this._getFeaturesInTimeRange(s,r);if(n.indexOf)for(var a=0;a=0&&o.splice(l,1)}e.Util.requestAnimFrame(e.Util.bind((function(){this.removeLayers(o),this.addLayers(n)}),this))},_getFeaturesInTimeRange:function(t,e){var i,s=[];if(this.options.timeField.start&&this.options.timeField.end){var r=this._startTimeIndex.between(t,e),o=this._endTimeIndex.between(t,e);i=r.concat(o)}else{if(!this._timeIndex)return f("You must set timeField in the layer constructor in order to manipulate the start and end time filter."),[];i=this._timeIndex.between(t,e)}for(var n=i.length-1;n>=0;n--)s.push(i[n].id);return s},_buildTimeIndexes:function(t){var e,i;if(this.options.timeField.start&&this.options.timeField.end){var s=[],r=[];for(e=t.length-1;e>=0;e--)i=t[e],s.push({id:i.id,value:new Date(i.properties[this.options.timeField.start])}),r.push({id:i.id,value:new Date(i.properties[this.options.timeField.end])});this._startTimeIndex.bulkAdd(s),this._endTimeIndex.bulkAdd(r)}else{var o=[];for(e=t.length-1;e>=0;e--)i=t[e],o.push({id:i.id,value:new Date(i.properties[this.options.timeField])});this._timeIndex.bulkAdd(o)}},_featureWithinTimeRange:function(t){if(!this.options.from||!this.options.to)return!0;var e=+this.options.from.valueOf(),i=+this.options.to.valueOf();if("string"==typeof this.options.timeField){var s=+t.properties[this.options.timeField];return s>=e&&s<=i}if(this.options.timeField.start&&this.options.timeField.end){var r=+t.properties[this.options.timeField.start],o=+t.properties[this.options.timeField.end];return r>=e&&r<=i||o>=e&&o<=i||r<=e&&o>=i}},_visibleZoom:function(){if(!this._map)return!1;var t=this._map.getZoom();return!(t>this.options.maxZoom||t=0;r--)n[r].properties[o.objectIdField]=n.length>1?e[r].objectId:e.objectId,n[r].id=n.length>1?e[r].objectId:e.objectId;this._addFeatures(n)}i&&i.call(s,t,e)}),this))}}),this))},updateFeature:function(t,e,i){this.updateFeatures(t,e,i)},updateFeatures:function(t,e,i){var s=t.features?t.features:[t];this.service.updateFeatures(t,(function(t,r){if(!t){for(var o=s.length-1;o>=0;o--)this.removeLayers([s[o].id],!0);this._addFeatures(s)}e&&e.call(i,t,r)}),this)},deleteFeature:function(t,e,i){this.deleteFeatures(t,e,i)},deleteFeatures:function(t,e,i){return this.service.deleteFeatures(t,(function(t,s){var r=s.length?s:[s];if(!t&&r.length>0)for(var o=r.length-1;o>=0;o--)this.removeLayers([r[o].objectId],!0);e&&e.call(i,t,s)}),this)}}),_t=gt.extend({options:{cacheLayers:!0},initialize:function(t){t.apikey&&(t.token=t.apikey),gt.prototype.initialize.call(this,t),this._originalStyle=this.options.style,this._layers={}},onRemove:function(t){for(var e in this._layers)t.removeLayer(this._layers[e]),this.fire("removefeature",{feature:this._layers[e].feature,permanent:!1},!0);return gt.prototype.onRemove.call(this,t)},createNewLayer:function(t){var i=e.GeoJSON.geometryToLayer(t,this.options);return i&&(i.defaultOptions=i.options),i},_updateLayer:function(t,i){var s=[],r=this.options.coordsToLatLng||e.GeoJSON.coordsToLatLng;switch(i.properties&&(t.feature.properties=i.properties),i.geometry.type){case"Point":s=e.GeoJSON.coordsToLatLng(i.geometry.coordinates),t.setLatLng(s);break;case"LineString":s=e.GeoJSON.coordsToLatLngs(i.geometry.coordinates,0,r),t.setLatLngs(s);break;case"MultiLineString":case"Polygon":s=e.GeoJSON.coordsToLatLngs(i.geometry.coordinates,1,r),t.setLatLngs(s);break;case"MultiPolygon":s=e.GeoJSON.coordsToLatLngs(i.geometry.coordinates,2,r),t.setLatLngs(s)}this.redraw(t.feature.id)},createLayers:function(t){for(var e=t.length-1;e>=0;e--){var i,s=t[e],r=this._layers[s.id];!this._visibleZoom()||!r||this._map.hasLayer(r)||this.options.timeField&&!this._featureWithinTimeRange(s)||(this._map.addLayer(r),this.fire("addfeature",{feature:r.feature},!0)),r&&(r.setLatLngs||r.setLatLng)&&this._updateLayer(r,s),r||((i=this.createNewLayer(s))?(i.feature=s,i.addEventParent(this),this.options.onEachFeature&&this.options.onEachFeature(i.feature,i),this._layers[i.feature.id]=i,this.setFeatureStyle(i.feature.id,this.options.style),this.fire("createfeature",{feature:i.feature},!0),this._visibleZoom()&&(!this.options.timeField||this.options.timeField&&this._featureWithinTimeRange(s))&&this._map.addLayer(i)):f("invalid GeoJSON encountered"))}},addLayers:function(t){for(var e=t.length-1;e>=0;e--){var i=this._layers[t[e]];!i||this.options.timeField&&!this._featureWithinTimeRange(i.feature)||(this._map.addLayer(i),this.fire("addfeature",{feature:i.feature},!0))}},removeLayers:function(t,e){for(var i=t.length-1;i>=0;i--){var s=t[i],r=this._layers[s];r&&(this.fire("removefeature",{feature:r.feature,permanent:e},!0),this._map.removeLayer(r)),r&&e&&delete this._layers[s]}},cellEnter:function(t,i){this._visibleZoom()&&!this._zooming&&this._map&&e.Util.requestAnimFrame(e.Util.bind((function(){var t=this._cacheKey(i),e=this._cellCoordsToKey(i),s=this._cache[t];this._activeCells[e]&&s&&this.addLayers(s)}),this))},cellLeave:function(t,i){this._zooming||e.Util.requestAnimFrame(e.Util.bind((function(){if(this._map){var t=this._cacheKey(i),e=this._cellCoordsToKey(i),s=this._cache[t],r=this._map.getBounds();if(!this._activeCells[e]&&s){for(var o=!0,n=0;n TTL + except Exception: + return False + +def main(): + # Remove old files + for root, dirs, files in os.walk(DATA_ROOT, topdown=False): + for name in files: + p = os.path.join(root, name) + if is_old(p): + try: + os.remove(p) + except Exception as e: + print(f"Failed to remove {p}: {e}") + # Remove empty dirs + for d in dirs: + dp = os.path.join(root, d) + try: + if not os.listdir(dp): + os.rmdir(dp) + except Exception: + pass + +if __name__ == "__main__": + main() diff --git a/fetcher/config.example.json b/fetcher/config.example.json new file mode 100644 index 0000000..513d6d8 --- /dev/null +++ b/fetcher/config.example.json @@ -0,0 +1,22 @@ +{ + "location": { + "name": "Seattle", + "lat": 47.6062, + "lon": -122.3321 + }, + "providers": { + "openweathermap": { + "enabled": true, + "api_key": "YOUR_OPENWEATHERMAP_KEY", + "base_url": "https://api.openweathermap.org/data/2.5/forecast" + }, + "weatherapi": { + "enabled": true, + "api_key": "YOUR_WEATHERAPI_KEY", + "base_url": "https://api.weatherapi.com/v1/forecast.json" + } + }, + "output_dir": "/var/www/WEATHER/public_html/data", + "days": 5, + "units": "imperial" +} diff --git a/fetcher/config.json b/fetcher/config.json new file mode 100644 index 0000000..f470ee0 --- /dev/null +++ b/fetcher/config.json @@ -0,0 +1,36 @@ +{ + "location": { + "name": "Venus, TX", + "lat": 32.4330, + "lon": -97.1010, + "timezone": "America/Chicago" + }, + "output_dir": "/var/www/WEATHER/public_html/data", + "days": 7, + "units": "imperial", + + + "user_agent": "WeatherConsensus/1.0 (https://weather.thedarkelite.com, info@thedarkelite.com)", + + + "nominatim_email": "info@thedarkelite.com", + + "providers": { + "open_meteo": { + "enabled": true, + "base_url": "https://api.open-meteo.com/v1/forecast" + }, + "nws": { + "enabled": true, + "base_url": "https://api.weather.gov" + }, + "metno": { + "enabled": true, + "base_url": "https://api.met.no/weatherapi/locationforecast/2.0/compact" + }, + "seventimer": { + "enabled": true, + "base_url": "http://www.7timer.info/bin/api.pl" + } + } +} diff --git a/fetcher/fetch_and_aggregate.py b/fetcher/fetch_and_aggregate.py new file mode 100644 index 0000000..c5fd01c --- /dev/null +++ b/fetcher/fetch_and_aggregate.py @@ -0,0 +1,371 @@ +#!/usr/bin/env python3 +""" +Fetches weather from *no-key* providers, normalizes units, aggregates to a daily consensus, +and writes JSON outputs into /var/www/WEATHER/public_html/data/. + +Providers: + - Open-Meteo (no key) -> daily temperature_2m_max (C/F) [https://open-meteo.com] + - api.weather.gov (no key; User-Agent required) -> periods [https://api.weather.gov] + - MET Norway Locationforecast (no key; User-Agent) -> hourly [https://api.met.no] + - 7Timer! civillight (no key) -> daily max/min [http://www.7timer.info] + +Outputs: + data/raw/__.json (raw) + data/aggregated__.json (aggregated daily consensus) + data/latest.json (copy of aggregated) +""" + +import os +import json +from datetime import datetime as dt +from datetime import timezone +from collections import defaultdict + +from zoneinfo import ZoneInfo # stdlib (Python 3.9+) +import requests + +# ---------------------- Utilities ---------------------- + +def here(*parts): + return os.path.join(os.path.dirname(os.path.abspath(__file__)), *parts) + +def load_config(): + conf_path = here("config.json") + if not os.path.exists(conf_path): + raise FileNotFoundError( + f"Missing config.json at {conf_path}. Copy config.example.json and fill in your values." + ) + with open(conf_path, "r") as f: + return json.load(f) + +def ensure_dirs(output_dir): + raw_dir = os.path.join(output_dir, "raw") + os.makedirs(raw_dir, exist_ok=True) + return raw_dir + +def date_str_utc(): + return dt.now(timezone.utc).strftime("%Y-%m-%d") + +def c_to_f(c): return (float(c) * 9.0/5.0) + 32.0 +def f_to_c(f): return (float(f) - 32.0) * 5.0/9.0 + +def normalize_temp(value, target_units, source_units): + """Return temp in target_units ('imperial' or 'metric') given source_units.""" + if value is None: + return None + if target_units == source_units: + return float(value) + if source_units == "metric" and target_units == "imperial": + return c_to_f(value) + if source_units == "imperial" and target_units == "metric": + return f_to_c(value) + # default passthrough + return float(value) + +def iso_to_local_date(iso_ts, tz_name): + """ + Convert ISO-8601 string to local YYYY-MM-DD using zoneinfo tz. + Accepts Z or +00:00. Returns date string. + """ + if iso_ts.endswith("Z"): + iso_ts = iso_ts.replace("Z", "+00:00") + dt_utc = dt.fromisoformat(iso_ts) + if dt_utc.tzinfo is None: + dt_utc = dt_utc.replace(tzinfo=timezone.utc) + local = dt_utc.astimezone(ZoneInfo(tz_name)) + return local.strftime("%Y-%m-%d") + +# ---------------------- Providers ---------------------- + +def fetch_open_meteo(provider_conf, lat, lon, days, units, tz_name): + """ + Open-Meteo: request daily max temperature. Supports temperature_unit and timezone parameters. + No API key required. + """ + url = provider_conf["base_url"] + params = { + "latitude": lat, + "longitude": lon, + "daily": "temperature_2m_max", + "timezone": tz_name or "auto", + "temperature_unit": "fahrenheit" if units == "imperial" else "celsius" + } + r = requests.get(url, params=params, timeout=20) + r.raise_for_status() + data = r.json() + + result = [] + daily = (data.get("daily") or {}) + times = daily.get("time") or [] + highs = daily.get("temperature_2m_max") or [] + for t, h in list(zip(times, highs))[:days]: + if h is None: + continue + result.append({ + "date": t, # already aligned to requested timezone + "high": round(float(h), 1), + "units": units, + "provider": "open-meteo" + }) + return result, data + +def fetch_nws(provider_conf, lat, lon, days, units, tz_name, user_agent): + """ + NWS api.weather.gov: + 1) /points/{lat},{lon} -> get 'forecast' URL + 2) GET forecast -> 'properties.periods' (local time). Temperatures usually in Fahrenheit. + Requires a User-Agent header. + """ + base = provider_conf["base_url"].rstrip("/") + headers = {"User-Agent": user_agent} + # Metadata to find forecast URL + meta_url = f"{base}/points/{round(lat,4)},{round(lon,4)}" + r_meta = requests.get(meta_url, headers=headers, timeout=20) + r_meta.raise_for_status() + meta = r_meta.json() + + forecast_url = (meta.get("properties") or {}).get("forecast") + if not forecast_url: + return [], {"points": meta} + + r_fcst = requests.get(forecast_url, headers=headers, timeout=20) + r_fcst.raise_for_status() + fcst = r_fcst.json() + + by_date = defaultdict(list) + for p in (fcst.get("properties") or {}).get("periods", []): + start = p.get("startTime") + temp = p.get("temperature") + unit = p.get("temperatureUnit") # e.g., 'F' + if start is None or temp is None: + continue + # Convert to configured timezone day + day_key = iso_to_local_date(start, tz_name) + # Normalize temperature to requested display units + src_units = "imperial" if str(unit).upper() == "F" else "metric" + temp_norm = normalize_temp(temp, target_units=units, source_units=src_units) + by_date[day_key].append(temp_norm) + + result = [] + for day in sorted(by_date.keys())[:days]: + # Take the max observed temp for that calendar day + vals = [v for v in by_date[day] if isinstance(v, (int, float))] + if not vals: + continue + result.append({ + "date": day, + "high": round(max(vals), 1), + "units": units, + "provider": "nws" + }) + # Return both combined raw (meta + forecast) + return result, {"points": meta, "forecast": fcst} + +def fetch_metno(provider_conf, lat, lon, days, units, tz_name, user_agent): + """ + MET Norway Locationforecast/2.0 compact (GeoJSON-like). + 'properties.timeseries' with hourly 'instant.details.air_temperature' (°C). + User-Agent is required by policy. + """ + url = provider_conf["base_url"] + headers = {"User-Agent": user_agent} + params = {"lat": round(lat, 4), "lon": round(lon, 4)} + r = requests.get(url, headers=headers, params=params, timeout=20) + r.raise_for_status() + data = r.json() + + by_date = defaultdict(list) + for item in (data.get("properties") or {}).get("timeseries", []): + t = item.get("time") + details = ((item.get("data") or {}).get("instant") or {}).get("details") or {} + temp_c = details.get("air_temperature") + if t is None or temp_c is None: + continue + # Convert timestamp to local calendar day for bucketing + day_key = iso_to_local_date(t, tz_name) + # Normalize to requested units + val = normalize_temp(temp_c, target_units=units, source_units="metric") + by_date[day_key].append(val) + + result = [] + for day in sorted(by_date.keys())[:days]: + vals = [v for v in by_date[day] if isinstance(v, (int, float))] + if not vals: + continue + result.append({ + "date": day, + "high": round(max(vals), 1), + "units": units, + "provider": "metno" + }) + return result, data + +def fetch_seventimer(provider_conf, lat, lon, days, units, tz_name): + """ + 7Timer! civillight: + JSON shape: { product:'civillight', init:'YYYYMMDDHH', dataseries:[ { date:YYYYMMDD, temp2m:{max,min}, ... } ] } + Values are in metric by default; we request metric explicitly. + """ + url = provider_conf["base_url"] + params = { + "lon": lon, + "lat": lat, + "product": "civillight", + "output": "json", + "unit": "metric" + } + r = requests.get(url, params=params, timeout=20) + r.raise_for_status() + data = r.json() + + result = [] + for d in (data.get("dataseries") or [])[:days]: + date_raw = d.get("date") # e.g. 20250210 + t2m = d.get("temp2m") or {} + tmax_c = t2m.get("max") + if date_raw is None or tmax_c is None: + continue + # 7Timer uses YYYYMMDD integer; convert to ISO date string + s = str(date_raw) + day = f"{s[0:4]}-{s[4:6]}-{s[6:8]}" + high = normalize_temp(tmax_c, target_units=units, source_units="metric") + result.append({ + "date": day, + "high": round(float(high), 1), + "units": units, + "provider": "7timer" + }) + return result, data + +# ---------------------- Aggregation ---------------------- + +def aggregate_daily_highs(provider_forecasts, units, max_days=None): + """ + Combine daily highs across providers. + Output per date: providers[], consensus{mean, median, spread, provider_count}, units + """ + by_date = defaultdict(list) + for lst in provider_forecasts: + for rec in lst: + if rec and isinstance(rec.get("high"), (int, float)): + by_date[rec["date"]].append(rec) + + aggregated = [] + for day in sorted(by_date.keys()): + entries = by_date[day] + highs = [e["high"] for e in entries if isinstance(e["high"], (int, float))] + if not highs: + continue + highs_sorted = sorted(highs) + n = len(highs_sorted) + mean_val = sum(highs_sorted) / n + if n % 2 == 1: + median_val = highs_sorted[n // 2] + else: + median_val = (highs_sorted[n // 2 - 1] + highs_sorted[n // 2]) / 2.0 + spread = highs_sorted[-1] - highs_sorted[0] + + aggregated.append({ + "date": day, + "providers": entries, + "consensus": { + "mean_high": round(mean_val, 1), + "median_high": round(median_val, 1), + "provider_count": n, + "spread": round(spread, 1) + }, + "units": units + }) + + if max_days is not None: + aggregated = aggregated[:max_days] + return aggregated + +# ---------------------- Main ---------------------- + +def main(): + conf = load_config() + + out_dir = conf["output_dir"] + os.makedirs(out_dir, exist_ok=True) + raw_dir = ensure_dirs(out_dir) + + days = int(conf.get("days", 5)) + units = conf.get("units", "imperial") # 'imperial' or 'metric' + lat = float(conf["location"]["lat"]) + lon = float(conf["location"]["lon"]) + loc_name = conf["location"]["name"] + tz_name = conf["location"].get("timezone") or "UTC" + user_agent = conf.get("user_agent", "WeatherConsensus/1.0 (contact: example@example.com)") + + date_tag = date_str_utc() + provider_results = [] + raw_bundle = {} + + # Open-Meteo + try: + if conf["providers"]["open_meteo"]["enabled"]: + om_list, om_raw = fetch_open_meteo(conf["providers"]["open_meteo"], lat, lon, days, units, tz_name) + provider_results.append(om_list) + raw_bundle["open_meteo"] = om_raw + with open(os.path.join(raw_dir, f"open-meteo_{date_tag}_{loc_name}.json"), "w") as f: + json.dump(om_raw, f, indent=2) + except Exception as e: + print("Open-Meteo error:", repr(e)) + + # NWS (api.weather.gov) + try: + if conf["providers"]["nws"]["enabled"]: + nws_list, nws_raw = fetch_nws(conf["providers"]["nws"], lat, lon, days, units, tz_name, user_agent) + provider_results.append(nws_list) + raw_bundle["nws"] = nws_raw + with open(os.path.join(raw_dir, f"nws_{date_tag}_{loc_name}.json"), "w") as f: + json.dump(nws_raw, f, indent=2) + except Exception as e: + print("NWS error:", repr(e)) + + # MET Norway + try: + if conf["providers"]["metno"]["enabled"]: + met_list, met_raw = fetch_metno(conf["providers"]["metno"], lat, lon, days, units, tz_name, user_agent) + provider_results.append(met_list) + raw_bundle["metno"] = met_raw + with open(os.path.join(raw_dir, f"metno_{date_tag}_{loc_name}.json"), "w") as f: + json.dump(met_raw, f, indent=2) + except Exception as e: + print("MET Norway error:", repr(e)) + + # 7Timer! + try: + if conf["providers"]["seventimer"]["enabled"]: + st_list, st_raw = fetch_seventimer(conf["providers"]["seventimer"], lat, lon, days, units, tz_name) + provider_results.append(st_list) + raw_bundle["7timer"] = st_raw + with open(os.path.join(raw_dir, f"7timer_{date_tag}_{loc_name}.json"), "w") as f: + json.dump(st_raw, f, indent=2) + except Exception as e: + print("7Timer error:", repr(e)) + + # Aggregate across providers + aggregated = aggregate_daily_highs(provider_forecasts=provider_results, units=units, max_days=days) + + # Write aggregated & latest + agg_path = os.path.join(out_dir, f"aggregated_{date_tag}_{loc_name}.json") + with open(agg_path, "w") as f: + json.dump({ + "location": loc_name, + "generated_at_utc": dt.now(timezone.utc).isoformat().replace("+00:00", "Z"), + "units": units, + "days": aggregated + }, f, indent=2) + + # Update latest.json + latest_path = os.path.join(out_dir, "latest.json") + try: + with open(agg_path, "r") as src, open(latest_path, "w") as dst: + dst.write(src.read()) + except Exception as e: + print("Failed to update latest.json:", repr(e)) + +if __name__ == "__main__": + main() diff --git a/index.html b/index.html new file mode 100644 index 0000000..b842629 --- /dev/null +++ b/index.html @@ -0,0 +1,148 @@ + + + + + + + + Weather Consensus + + + + + + + + + + + + + + +
+ + + + +
+ +
+

Enter a location to see the forecast

+

+
+ + +
+ + + + + +
+
+ Data sources +
    +
  • Geocoding: Open‑Meteo (primary), Nominatim / OpenStreetMap (fallback)
  • +
  • Weather providers: Open‑Meteo, National Weather Service (api.weather.gov), MET Norway, 7Timer! +
  • +
  • Radar: NOAA/NWS MRMS via public OGC services (WMS/WMTS)
  • +
+

+ We cache results for 24 hours to minimize network calls and improve performance. +

+
+
+
+ +
+
+ © WEATHER +
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/leaflet.css b/leaflet.css new file mode 100644 index 0000000..2961b76 --- /dev/null +++ b/leaflet.css @@ -0,0 +1,661 @@ +/* required styles */ + +.leaflet-pane, +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-tile-container, +.leaflet-pane > svg, +.leaflet-pane > canvas, +.leaflet-zoom-box, +.leaflet-image-layer, +.leaflet-layer { + position: absolute; + left: 0; + top: 0; + } +.leaflet-container { + overflow: hidden; + } +.leaflet-tile, +.leaflet-marker-icon, +.leaflet-marker-shadow { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + -webkit-user-drag: none; + } +/* Prevents IE11 from highlighting tiles in blue */ +.leaflet-tile::selection { + background: transparent; +} +/* Safari renders non-retina tile on retina better with this, but Chrome is worse */ +.leaflet-safari .leaflet-tile { + image-rendering: -webkit-optimize-contrast; + } +/* hack that prevents hw layers "stretching" when loading new tiles */ +.leaflet-safari .leaflet-tile-container { + width: 1600px; + height: 1600px; + -webkit-transform-origin: 0 0; + } +.leaflet-marker-icon, +.leaflet-marker-shadow { + display: block; + } +/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */ +/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */ +.leaflet-container .leaflet-overlay-pane svg { + max-width: none !important; + max-height: none !important; + } +.leaflet-container .leaflet-marker-pane img, +.leaflet-container .leaflet-shadow-pane img, +.leaflet-container .leaflet-tile-pane img, +.leaflet-container img.leaflet-image-layer, +.leaflet-container .leaflet-tile { + max-width: none !important; + max-height: none !important; + width: auto; + padding: 0; + } + +.leaflet-container img.leaflet-tile { + /* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */ + mix-blend-mode: plus-lighter; +} + +.leaflet-container.leaflet-touch-zoom { + -ms-touch-action: pan-x pan-y; + touch-action: pan-x pan-y; + } +.leaflet-container.leaflet-touch-drag { + -ms-touch-action: pinch-zoom; + /* Fallback for FF which doesn't support pinch-zoom */ + touch-action: none; + touch-action: pinch-zoom; +} +.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom { + -ms-touch-action: none; + touch-action: none; +} +.leaflet-container { + -webkit-tap-highlight-color: transparent; +} +.leaflet-container a { + -webkit-tap-highlight-color: rgba(51, 181, 229, 0.4); +} +.leaflet-tile { + filter: inherit; + visibility: hidden; + } +.leaflet-tile-loaded { + visibility: inherit; + } +.leaflet-zoom-box { + width: 0; + height: 0; + -moz-box-sizing: border-box; + box-sizing: border-box; + z-index: 800; + } +/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */ +.leaflet-overlay-pane svg { + -moz-user-select: none; + } + +.leaflet-pane { z-index: 400; } + +.leaflet-tile-pane { z-index: 200; } +.leaflet-overlay-pane { z-index: 400; } +.leaflet-shadow-pane { z-index: 500; } +.leaflet-marker-pane { z-index: 600; } +.leaflet-tooltip-pane { z-index: 650; } +.leaflet-popup-pane { z-index: 700; } + +.leaflet-map-pane canvas { z-index: 100; } +.leaflet-map-pane svg { z-index: 200; } + +.leaflet-vml-shape { + width: 1px; + height: 1px; + } +.lvml { + behavior: url(#default#VML); + display: inline-block; + position: absolute; + } + + +/* control positioning */ + +.leaflet-control { + position: relative; + z-index: 800; + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; + } +.leaflet-top, +.leaflet-bottom { + position: absolute; + z-index: 1000; + pointer-events: none; + } +.leaflet-top { + top: 0; + } +.leaflet-right { + right: 0; + } +.leaflet-bottom { + bottom: 0; + } +.leaflet-left { + left: 0; + } +.leaflet-control { + float: left; + clear: both; + } +.leaflet-right .leaflet-control { + float: right; + } +.leaflet-top .leaflet-control { + margin-top: 10px; + } +.leaflet-bottom .leaflet-control { + margin-bottom: 10px; + } +.leaflet-left .leaflet-control { + margin-left: 10px; + } +.leaflet-right .leaflet-control { + margin-right: 10px; + } + + +/* zoom and fade animations */ + +.leaflet-fade-anim .leaflet-popup { + opacity: 0; + -webkit-transition: opacity 0.2s linear; + -moz-transition: opacity 0.2s linear; + transition: opacity 0.2s linear; + } +.leaflet-fade-anim .leaflet-map-pane .leaflet-popup { + opacity: 1; + } +.leaflet-zoom-animated { + -webkit-transform-origin: 0 0; + -ms-transform-origin: 0 0; + transform-origin: 0 0; + } +svg.leaflet-zoom-animated { + will-change: transform; +} + +.leaflet-zoom-anim .leaflet-zoom-animated { + -webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1); + -moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1); + transition: transform 0.25s cubic-bezier(0,0,0.25,1); + } +.leaflet-zoom-anim .leaflet-tile, +.leaflet-pan-anim .leaflet-tile { + -webkit-transition: none; + -moz-transition: none; + transition: none; + } + +.leaflet-zoom-anim .leaflet-zoom-hide { + visibility: hidden; + } + + +/* cursors */ + +.leaflet-interactive { + cursor: pointer; + } +.leaflet-grab { + cursor: -webkit-grab; + cursor: -moz-grab; + cursor: grab; + } +.leaflet-crosshair, +.leaflet-crosshair .leaflet-interactive { + cursor: crosshair; + } +.leaflet-popup-pane, +.leaflet-control { + cursor: auto; + } +.leaflet-dragging .leaflet-grab, +.leaflet-dragging .leaflet-grab .leaflet-interactive, +.leaflet-dragging .leaflet-marker-draggable { + cursor: move; + cursor: -webkit-grabbing; + cursor: -moz-grabbing; + cursor: grabbing; + } + +/* marker & overlays interactivity */ +.leaflet-marker-icon, +.leaflet-marker-shadow, +.leaflet-image-layer, +.leaflet-pane > svg path, +.leaflet-tile-container { + pointer-events: none; + } + +.leaflet-marker-icon.leaflet-interactive, +.leaflet-image-layer.leaflet-interactive, +.leaflet-pane > svg path.leaflet-interactive, +svg.leaflet-image-layer.leaflet-interactive path { + pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */ + pointer-events: auto; + } + +/* visual tweaks */ + +.leaflet-container { + background: #ddd; + outline-offset: 1px; + } +.leaflet-container a { + color: #0078A8; + } +.leaflet-zoom-box { + border: 2px dotted #38f; + background: rgba(255,255,255,0.5); + } + + +/* general typography */ +.leaflet-container { + font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; + font-size: 12px; + font-size: 0.75rem; + line-height: 1.5; + } + + +/* general toolbar styles */ + +.leaflet-bar { + box-shadow: 0 1px 5px rgba(0,0,0,0.65); + border-radius: 4px; + } +.leaflet-bar a { + background-color: #fff; + border-bottom: 1px solid #ccc; + width: 26px; + height: 26px; + line-height: 26px; + display: block; + text-align: center; + text-decoration: none; + color: black; + } +.leaflet-bar a, +.leaflet-control-layers-toggle { + background-position: 50% 50%; + background-repeat: no-repeat; + display: block; + } +.leaflet-bar a:hover, +.leaflet-bar a:focus { + background-color: #f4f4f4; + } +.leaflet-bar a:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + } +.leaflet-bar a:last-child { + border-bottom-left-radius: 4px; + border-bottom-right-radius: 4px; + border-bottom: none; + } +.leaflet-bar a.leaflet-disabled { + cursor: default; + background-color: #f4f4f4; + color: #bbb; + } + +.leaflet-touch .leaflet-bar a { + width: 30px; + height: 30px; + line-height: 30px; + } +.leaflet-touch .leaflet-bar a:first-child { + border-top-left-radius: 2px; + border-top-right-radius: 2px; + } +.leaflet-touch .leaflet-bar a:last-child { + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; + } + +/* zoom control */ + +.leaflet-control-zoom-in, +.leaflet-control-zoom-out { + font: bold 18px 'Lucida Console', Monaco, monospace; + text-indent: 1px; + } + +.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out { + font-size: 22px; + } + + +/* layers control */ + +.leaflet-control-layers { + box-shadow: 0 1px 5px rgba(0,0,0,0.4); + background: #fff; + border-radius: 5px; + } +.leaflet-control-layers-toggle { + background-image: url(images/layers.png); + width: 36px; + height: 36px; + } +.leaflet-retina .leaflet-control-layers-toggle { + background-image: url(images/layers-2x.png); + background-size: 26px 26px; + } +.leaflet-touch .leaflet-control-layers-toggle { + width: 44px; + height: 44px; + } +.leaflet-control-layers .leaflet-control-layers-list, +.leaflet-control-layers-expanded .leaflet-control-layers-toggle { + display: none; + } +.leaflet-control-layers-expanded .leaflet-control-layers-list { + display: block; + position: relative; + } +.leaflet-control-layers-expanded { + padding: 6px 10px 6px 6px; + color: #333; + background: #fff; + } +.leaflet-control-layers-scrollbar { + overflow-y: scroll; + overflow-x: hidden; + padding-right: 5px; + } +.leaflet-control-layers-selector { + margin-top: 2px; + position: relative; + top: 1px; + } +.leaflet-control-layers label { + display: block; + font-size: 13px; + font-size: 1.08333em; + } +.leaflet-control-layers-separator { + height: 0; + border-top: 1px solid #ddd; + margin: 5px -10px 5px -6px; + } + +/* Default icon URLs */ +.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */ + background-image: url(images/marker-icon.png); + } + + +/* attribution and scale controls */ + +.leaflet-container .leaflet-control-attribution { + background: #fff; + background: rgba(255, 255, 255, 0.8); + margin: 0; + } +.leaflet-control-attribution, +.leaflet-control-scale-line { + padding: 0 5px; + color: #333; + line-height: 1.4; + } +.leaflet-control-attribution a { + text-decoration: none; + } +.leaflet-control-attribution a:hover, +.leaflet-control-attribution a:focus { + text-decoration: underline; + } +.leaflet-attribution-flag { + display: inline !important; + vertical-align: baseline !important; + width: 1em; + height: 0.6669em; + } +.leaflet-left .leaflet-control-scale { + margin-left: 5px; + } +.leaflet-bottom .leaflet-control-scale { + margin-bottom: 5px; + } +.leaflet-control-scale-line { + border: 2px solid #777; + border-top: none; + line-height: 1.1; + padding: 2px 5px 1px; + white-space: nowrap; + -moz-box-sizing: border-box; + box-sizing: border-box; + background: rgba(255, 255, 255, 0.8); + text-shadow: 1px 1px #fff; + } +.leaflet-control-scale-line:not(:first-child) { + border-top: 2px solid #777; + border-bottom: none; + margin-top: -2px; + } +.leaflet-control-scale-line:not(:first-child):not(:last-child) { + border-bottom: 2px solid #777; + } + +.leaflet-touch .leaflet-control-attribution, +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + box-shadow: none; + } +.leaflet-touch .leaflet-control-layers, +.leaflet-touch .leaflet-bar { + border: 2px solid rgba(0,0,0,0.2); + background-clip: padding-box; + } + + +/* popup */ + +.leaflet-popup { + position: absolute; + text-align: center; + margin-bottom: 20px; + } +.leaflet-popup-content-wrapper { + padding: 1px; + text-align: left; + border-radius: 12px; + } +.leaflet-popup-content { + margin: 13px 24px 13px 20px; + line-height: 1.3; + font-size: 13px; + font-size: 1.08333em; + min-height: 1px; + } +.leaflet-popup-content p { + margin: 17px 0; + margin: 1.3em 0; + } +.leaflet-popup-tip-container { + width: 40px; + height: 20px; + position: absolute; + left: 50%; + margin-top: -1px; + margin-left: -20px; + overflow: hidden; + pointer-events: none; + } +.leaflet-popup-tip { + width: 17px; + height: 17px; + padding: 1px; + + margin: -10px auto 0; + pointer-events: auto; + + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); + } +.leaflet-popup-content-wrapper, +.leaflet-popup-tip { + background: white; + color: #333; + box-shadow: 0 3px 14px rgba(0,0,0,0.4); + } +.leaflet-container a.leaflet-popup-close-button { + position: absolute; + top: 0; + right: 0; + border: none; + text-align: center; + width: 24px; + height: 24px; + font: 16px/24px Tahoma, Verdana, sans-serif; + color: #757575; + text-decoration: none; + background: transparent; + } +.leaflet-container a.leaflet-popup-close-button:hover, +.leaflet-container a.leaflet-popup-close-button:focus { + color: #585858; + } +.leaflet-popup-scrolled { + overflow: auto; + } + +.leaflet-oldie .leaflet-popup-content-wrapper { + -ms-zoom: 1; + } +.leaflet-oldie .leaflet-popup-tip { + width: 24px; + margin: 0 auto; + + -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)"; + filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678); + } + +.leaflet-oldie .leaflet-control-zoom, +.leaflet-oldie .leaflet-control-layers, +.leaflet-oldie .leaflet-popup-content-wrapper, +.leaflet-oldie .leaflet-popup-tip { + border: 1px solid #999; + } + + +/* div icon */ + +.leaflet-div-icon { + background: #fff; + border: 1px solid #666; + } + + +/* Tooltip */ +/* Base styles for the element that has a tooltip */ +.leaflet-tooltip { + position: absolute; + padding: 6px; + background-color: #fff; + border: 1px solid #fff; + border-radius: 3px; + color: #222; + white-space: nowrap; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; + box-shadow: 0 1px 3px rgba(0,0,0,0.4); + } +.leaflet-tooltip.leaflet-interactive { + cursor: pointer; + pointer-events: auto; + } +.leaflet-tooltip-top:before, +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + position: absolute; + pointer-events: none; + border: 6px solid transparent; + background: transparent; + content: ""; + } + +/* Directions */ + +.leaflet-tooltip-bottom { + margin-top: 6px; +} +.leaflet-tooltip-top { + margin-top: -6px; +} +.leaflet-tooltip-bottom:before, +.leaflet-tooltip-top:before { + left: 50%; + margin-left: -6px; + } +.leaflet-tooltip-top:before { + bottom: 0; + margin-bottom: -12px; + border-top-color: #fff; + } +.leaflet-tooltip-bottom:before { + top: 0; + margin-top: -12px; + margin-left: -6px; + border-bottom-color: #fff; + } +.leaflet-tooltip-left { + margin-left: -6px; +} +.leaflet-tooltip-right { + margin-left: 6px; +} +.leaflet-tooltip-left:before, +.leaflet-tooltip-right:before { + top: 50%; + margin-top: -6px; + } +.leaflet-tooltip-left:before { + right: 0; + margin-right: -12px; + border-left-color: #fff; + } +.leaflet-tooltip-right:before { + left: 0; + margin-left: -12px; + border-right-color: #fff; + } + +/* Printing */ + +@media print { + /* Prevent printers from removing background-images of controls. */ + .leaflet-control { + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } + } diff --git a/leaflet.js b/leaflet.js new file mode 100644 index 0000000..976177e --- /dev/null +++ b/leaflet.js @@ -0,0 +1,6 @@ +/* @preserve + * Leaflet 1.9.4+v1.d15112c, a JS library for interactive maps. https://leafletjs.com + * (c) 2010-2023 Vladimir Agafonkin, (c) 2010-2011 CloudMade + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).leaflet={})}(this,function(t){"use strict";function l(t){for(var e,i,n=1,o=arguments.length;n=this.min.x&&i.x<=this.max.x&&e.y>=this.min.y&&i.y<=this.max.y},intersects:function(t){t=_(t);var e=this.min,i=this.max,n=t.min,t=t.max,o=t.x>=e.x&&n.x<=i.x,t=t.y>=e.y&&n.y<=i.y;return o&&t},overlaps:function(t){t=_(t);var e=this.min,i=this.max,n=t.min,t=t.max,o=t.x>e.x&&n.xe.y&&n.y=n.lat&&i.lat<=o.lat&&e.lng>=n.lng&&i.lng<=o.lng},intersects:function(t){t=g(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),t=t.getNorthEast(),o=t.lat>=e.lat&&n.lat<=i.lat,t=t.lng>=e.lng&&n.lng<=i.lng;return o&&t},overlaps:function(t){t=g(t);var e=this._southWest,i=this._northEast,n=t.getSouthWest(),t=t.getNorthEast(),o=t.lat>e.lat&&n.late.lng&&n.lng","http://www.w3.org/2000/svg"===(Wt.firstChild&&Wt.firstChild.namespaceURI));function y(t){return 0<=navigator.userAgent.toLowerCase().indexOf(t)}var b={ie:pt,ielt9:mt,edge:n,webkit:ft,android:gt,android23:vt,androidStock:yt,opera:xt,chrome:wt,gecko:bt,safari:Pt,phantom:Lt,opera12:o,win:Tt,ie3d:Mt,webkit3d:zt,gecko3d:_t,any3d:Ct,mobile:Zt,mobileWebkit:St,mobileWebkit3d:Et,msPointer:kt,pointer:Ot,touch:Bt,touchNative:At,mobileOpera:It,mobileGecko:Rt,retina:Nt,passiveEvents:Dt,canvas:jt,svg:Ht,vml:!Ht&&function(){try{var t=document.createElement("div"),e=(t.innerHTML='',t.firstChild);return e.style.behavior="url(#default#VML)",e&&"object"==typeof e.adj}catch(t){return!1}}(),inlineSvg:Wt,mac:0===navigator.platform.indexOf("Mac"),linux:0===navigator.platform.indexOf("Linux")},Ft=b.msPointer?"MSPointerDown":"pointerdown",Ut=b.msPointer?"MSPointerMove":"pointermove",Vt=b.msPointer?"MSPointerUp":"pointerup",qt=b.msPointer?"MSPointerCancel":"pointercancel",Gt={touchstart:Ft,touchmove:Ut,touchend:Vt,touchcancel:qt},Kt={touchstart:function(t,e){e.MSPOINTER_TYPE_TOUCH&&e.pointerType===e.MSPOINTER_TYPE_TOUCH&&O(e);ee(t,e)},touchmove:ee,touchend:ee,touchcancel:ee},Yt={},Xt=!1;function Jt(t,e,i){return"touchstart"!==e||Xt||(document.addEventListener(Ft,$t,!0),document.addEventListener(Ut,Qt,!0),document.addEventListener(Vt,te,!0),document.addEventListener(qt,te,!0),Xt=!0),Kt[e]?(i=Kt[e].bind(this,i),t.addEventListener(Gt[e],i,!1),i):(console.warn("wrong event specified:",e),u)}function $t(t){Yt[t.pointerId]=t}function Qt(t){Yt[t.pointerId]&&(Yt[t.pointerId]=t)}function te(t){delete Yt[t.pointerId]}function ee(t,e){if(e.pointerType!==(e.MSPOINTER_TYPE_MOUSE||"mouse")){for(var i in e.touches=[],Yt)e.touches.push(Yt[i]);e.changedTouches=[e],t(e)}}var ie=200;function ne(t,i){t.addEventListener("dblclick",i);var n,o=0;function e(t){var e;1!==t.detail?n=t.detail:"mouse"===t.pointerType||t.sourceCapabilities&&!t.sourceCapabilities.firesTouchEvents||((e=Ne(t)).some(function(t){return t instanceof HTMLLabelElement&&t.attributes.for})&&!e.some(function(t){return t instanceof HTMLInputElement||t instanceof HTMLSelectElement})||((e=Date.now())-o<=ie?2===++n&&i(function(t){var e,i,n={};for(i in t)e=t[i],n[i]=e&&e.bind?e.bind(t):e;return(t=n).type="dblclick",n.detail=2,n.isTrusted=!1,n._simulated=!0,n}(t)):n=1,o=e))}return t.addEventListener("click",e),{dblclick:i,simDblclick:e}}var oe,se,re,ae,he,le,ue=we(["transform","webkitTransform","OTransform","MozTransform","msTransform"]),ce=we(["webkitTransition","transition","OTransition","MozTransition","msTransition"]),de="webkitTransition"===ce||"OTransition"===ce?ce+"End":"transitionend";function _e(t){return"string"==typeof t?document.getElementById(t):t}function pe(t,e){var i=t.style[e]||t.currentStyle&&t.currentStyle[e];return"auto"===(i=i&&"auto"!==i||!document.defaultView?i:(t=document.defaultView.getComputedStyle(t,null))?t[e]:null)?null:i}function P(t,e,i){t=document.createElement(t);return t.className=e||"",i&&i.appendChild(t),t}function T(t){var e=t.parentNode;e&&e.removeChild(t)}function me(t){for(;t.firstChild;)t.removeChild(t.firstChild)}function fe(t){var e=t.parentNode;e&&e.lastChild!==t&&e.appendChild(t)}function ge(t){var e=t.parentNode;e&&e.firstChild!==t&&e.insertBefore(t,e.firstChild)}function ve(t,e){return void 0!==t.classList?t.classList.contains(e):0<(t=xe(t)).length&&new RegExp("(^|\\s)"+e+"(\\s|$)").test(t)}function M(t,e){var i;if(void 0!==t.classList)for(var n=F(e),o=0,s=n.length;othis.options.maxZoom)?this.setZoom(t):this},panInsideBounds:function(t,e){this._enforcingBounds=!0;var i=this.getCenter(),t=this._limitCenter(i,this._zoom,g(t));return i.equals(t)||this.panTo(t,e),this._enforcingBounds=!1,this},panInside:function(t,e){var i=m((e=e||{}).paddingTopLeft||e.padding||[0,0]),n=m(e.paddingBottomRight||e.padding||[0,0]),o=this.project(this.getCenter()),t=this.project(t),s=this.getPixelBounds(),i=_([s.min.add(i),s.max.subtract(n)]),s=i.getSize();return i.contains(t)||(this._enforcingBounds=!0,n=t.subtract(i.getCenter()),i=i.extend(t).getSize().subtract(s),o.x+=n.x<0?-i.x:i.x,o.y+=n.y<0?-i.y:i.y,this.panTo(this.unproject(o),e),this._enforcingBounds=!1),this},invalidateSize:function(t){if(!this._loaded)return this;t=l({animate:!1,pan:!0},!0===t?{animate:!0}:t);var e=this.getSize(),i=(this._sizeChanged=!0,this._lastCenter=null,this.getSize()),n=e.divideBy(2).round(),o=i.divideBy(2).round(),n=n.subtract(o);return n.x||n.y?(t.animate&&t.pan?this.panBy(n):(t.pan&&this._rawPanBy(n),this.fire("move"),t.debounceMoveend?(clearTimeout(this._sizeTimer),this._sizeTimer=setTimeout(a(this.fire,this,"moveend"),200)):this.fire("moveend")),this.fire("resize",{oldSize:e,newSize:i})):this},stop:function(){return this.setZoom(this._limitZoom(this._zoom)),this.options.zoomSnap||this.fire("viewreset"),this._stop()},locate:function(t){var e,i;return t=this._locateOptions=l({timeout:1e4,watch:!1},t),"geolocation"in navigator?(e=a(this._handleGeolocationResponse,this),i=a(this._handleGeolocationError,this),t.watch?this._locationWatchId=navigator.geolocation.watchPosition(e,i,t):navigator.geolocation.getCurrentPosition(e,i,t)):this._handleGeolocationError({code:0,message:"Geolocation not supported."}),this},stopLocate:function(){return navigator.geolocation&&navigator.geolocation.clearWatch&&navigator.geolocation.clearWatch(this._locationWatchId),this._locateOptions&&(this._locateOptions.setView=!1),this},_handleGeolocationError:function(t){var e;this._container._leaflet_id&&(e=t.code,t=t.message||(1===e?"permission denied":2===e?"position unavailable":"timeout"),this._locateOptions.setView&&!this._loaded&&this.fitWorld(),this.fire("locationerror",{code:e,message:"Geolocation error: "+t+"."}))},_handleGeolocationResponse:function(t){if(this._container._leaflet_id){var e,i,n=new v(t.coords.latitude,t.coords.longitude),o=n.toBounds(2*t.coords.accuracy),s=this._locateOptions,r=(s.setView&&(e=this.getBoundsZoom(o),this.setView(n,s.maxZoom?Math.min(e,s.maxZoom):e)),{latlng:n,bounds:o,timestamp:t.timestamp});for(i in t.coords)"number"==typeof t.coords[i]&&(r[i]=t.coords[i]);this.fire("locationfound",r)}},addHandler:function(t,e){return e&&(e=this[t]=new e(this),this._handlers.push(e),this.options[t]&&e.enable()),this},remove:function(){if(this._initEvents(!0),this.options.maxBounds&&this.off("moveend",this._panInsideMaxBounds),this._containerId!==this._container._leaflet_id)throw new Error("Map container is being reused by another instance");try{delete this._container._leaflet_id,delete this._containerId}catch(t){this._container._leaflet_id=void 0,this._containerId=void 0}for(var t in void 0!==this._locationWatchId&&this.stopLocate(),this._stop(),T(this._mapPane),this._clearControlPos&&this._clearControlPos(),this._resizeRequest&&(r(this._resizeRequest),this._resizeRequest=null),this._clearHandlers(),this._loaded&&this.fire("unload"),this._layers)this._layers[t].remove();for(t in this._panes)T(this._panes[t]);return this._layers=[],this._panes=[],delete this._mapPane,delete this._renderer,this},createPane:function(t,e){e=P("div","leaflet-pane"+(t?" leaflet-"+t.replace("Pane","")+"-pane":""),e||this._mapPane);return t&&(this._panes[t]=e),e},getCenter:function(){return this._checkIfLoaded(),this._lastCenter&&!this._moved()?this._lastCenter.clone():this.layerPointToLatLng(this._getCenterLayerPoint())},getZoom:function(){return this._zoom},getBounds:function(){var t=this.getPixelBounds();return new s(this.unproject(t.getBottomLeft()),this.unproject(t.getTopRight()))},getMinZoom:function(){return void 0===this.options.minZoom?this._layersMinZoom||0:this.options.minZoom},getMaxZoom:function(){return void 0===this.options.maxZoom?void 0===this._layersMaxZoom?1/0:this._layersMaxZoom:this.options.maxZoom},getBoundsZoom:function(t,e,i){t=g(t),i=m(i||[0,0]);var n=this.getZoom()||0,o=this.getMinZoom(),s=this.getMaxZoom(),r=t.getNorthWest(),t=t.getSouthEast(),i=this.getSize().subtract(i),t=_(this.project(t,n),this.project(r,n)).getSize(),r=b.any3d?this.options.zoomSnap:1,a=i.x/t.x,i=i.y/t.y,t=e?Math.max(a,i):Math.min(a,i),n=this.getScaleZoom(t,n);return r&&(n=Math.round(n/(r/100))*(r/100),n=e?Math.ceil(n/r)*r:Math.floor(n/r)*r),Math.max(o,Math.min(s,n))},getSize:function(){return this._size&&!this._sizeChanged||(this._size=new p(this._container.clientWidth||0,this._container.clientHeight||0),this._sizeChanged=!1),this._size.clone()},getPixelBounds:function(t,e){t=this._getTopLeftPoint(t,e);return new f(t,t.add(this.getSize()))},getPixelOrigin:function(){return this._checkIfLoaded(),this._pixelOrigin},getPixelWorldBounds:function(t){return this.options.crs.getProjectedBounds(void 0===t?this.getZoom():t)},getPane:function(t){return"string"==typeof t?this._panes[t]:t},getPanes:function(){return this._panes},getContainer:function(){return this._container},getZoomScale:function(t,e){var i=this.options.crs;return e=void 0===e?this._zoom:e,i.scale(t)/i.scale(e)},getScaleZoom:function(t,e){var i=this.options.crs,t=(e=void 0===e?this._zoom:e,i.zoom(t*i.scale(e)));return isNaN(t)?1/0:t},project:function(t,e){return e=void 0===e?this._zoom:e,this.options.crs.latLngToPoint(w(t),e)},unproject:function(t,e){return e=void 0===e?this._zoom:e,this.options.crs.pointToLatLng(m(t),e)},layerPointToLatLng:function(t){t=m(t).add(this.getPixelOrigin());return this.unproject(t)},latLngToLayerPoint:function(t){return this.project(w(t))._round()._subtract(this.getPixelOrigin())},wrapLatLng:function(t){return this.options.crs.wrapLatLng(w(t))},wrapLatLngBounds:function(t){return this.options.crs.wrapLatLngBounds(g(t))},distance:function(t,e){return this.options.crs.distance(w(t),w(e))},containerPointToLayerPoint:function(t){return m(t).subtract(this._getMapPanePos())},layerPointToContainerPoint:function(t){return m(t).add(this._getMapPanePos())},containerPointToLatLng:function(t){t=this.containerPointToLayerPoint(m(t));return this.layerPointToLatLng(t)},latLngToContainerPoint:function(t){return this.layerPointToContainerPoint(this.latLngToLayerPoint(w(t)))},mouseEventToContainerPoint:function(t){return De(t,this._container)},mouseEventToLayerPoint:function(t){return this.containerPointToLayerPoint(this.mouseEventToContainerPoint(t))},mouseEventToLatLng:function(t){return this.layerPointToLatLng(this.mouseEventToLayerPoint(t))},_initContainer:function(t){t=this._container=_e(t);if(!t)throw new Error("Map container not found.");if(t._leaflet_id)throw new Error("Map container is already initialized.");S(t,"scroll",this._onScroll,this),this._containerId=h(t)},_initLayout:function(){var t=this._container,e=(this._fadeAnimated=this.options.fadeAnimation&&b.any3d,M(t,"leaflet-container"+(b.touch?" leaflet-touch":"")+(b.retina?" leaflet-retina":"")+(b.ielt9?" leaflet-oldie":"")+(b.safari?" leaflet-safari":"")+(this._fadeAnimated?" leaflet-fade-anim":"")),pe(t,"position"));"absolute"!==e&&"relative"!==e&&"fixed"!==e&&"sticky"!==e&&(t.style.position="relative"),this._initPanes(),this._initControlPos&&this._initControlPos()},_initPanes:function(){var t=this._panes={};this._paneRenderers={},this._mapPane=this.createPane("mapPane",this._container),Z(this._mapPane,new p(0,0)),this.createPane("tilePane"),this.createPane("overlayPane"),this.createPane("shadowPane"),this.createPane("markerPane"),this.createPane("tooltipPane"),this.createPane("popupPane"),this.options.markerZoomAnimation||(M(t.markerPane,"leaflet-zoom-hide"),M(t.shadowPane,"leaflet-zoom-hide"))},_resetView:function(t,e,i){Z(this._mapPane,new p(0,0));var n=!this._loaded,o=(this._loaded=!0,e=this._limitZoom(e),this.fire("viewprereset"),this._zoom!==e);this._moveStart(o,i)._move(t,e)._moveEnd(o),this.fire("viewreset"),n&&this.fire("load")},_moveStart:function(t,e){return t&&this.fire("zoomstart"),e||this.fire("movestart"),this},_move:function(t,e,i,n){void 0===e&&(e=this._zoom);var o=this._zoom!==e;return this._zoom=e,this._lastCenter=t,this._pixelOrigin=this._getNewPixelOrigin(t),n?i&&i.pinch&&this.fire("zoom",i):((o||i&&i.pinch)&&this.fire("zoom",i),this.fire("move",i)),this},_moveEnd:function(t){return t&&this.fire("zoomend"),this.fire("moveend")},_stop:function(){return r(this._flyToFrame),this._panAnim&&this._panAnim.stop(),this},_rawPanBy:function(t){Z(this._mapPane,this._getMapPanePos().subtract(t))},_getZoomSpan:function(){return this.getMaxZoom()-this.getMinZoom()},_panInsideMaxBounds:function(){this._enforcingBounds||this.panInsideBounds(this.options.maxBounds)},_checkIfLoaded:function(){if(!this._loaded)throw new Error("Set map center and zoom first.")},_initEvents:function(t){this._targets={};var e=t?k:S;e((this._targets[h(this._container)]=this)._container,"click dblclick mousedown mouseup mouseover mouseout mousemove contextmenu keypress keydown keyup",this._handleDOMEvent,this),this.options.trackResize&&e(window,"resize",this._onResize,this),b.any3d&&this.options.transform3DLimit&&(t?this.off:this.on).call(this,"moveend",this._onMoveEnd)},_onResize:function(){r(this._resizeRequest),this._resizeRequest=x(function(){this.invalidateSize({debounceMoveend:!0})},this)},_onScroll:function(){this._container.scrollTop=0,this._container.scrollLeft=0},_onMoveEnd:function(){var t=this._getMapPanePos();Math.max(Math.abs(t.x),Math.abs(t.y))>=this.options.transform3DLimit&&this._resetView(this.getCenter(),this.getZoom())},_findEventTargets:function(t,e){for(var i,n=[],o="mouseout"===e||"mouseover"===e,s=t.target||t.srcElement,r=!1;s;){if((i=this._targets[h(s)])&&("click"===e||"preclick"===e)&&this._draggableMoved(i)){r=!0;break}if(i&&i.listens(e,!0)){if(o&&!We(s,t))break;if(n.push(i),o)break}if(s===this._container)break;s=s.parentNode}return n=n.length||r||o||!this.listens(e,!0)?n:[this]},_isClickDisabled:function(t){for(;t&&t!==this._container;){if(t._leaflet_disable_click)return!0;t=t.parentNode}},_handleDOMEvent:function(t){var e,i=t.target||t.srcElement;!this._loaded||i._leaflet_disable_events||"click"===t.type&&this._isClickDisabled(i)||("mousedown"===(e=t.type)&&Me(i),this._fireDOMEvent(t,e))},_mouseEvents:["click","dblclick","mouseover","mouseout","contextmenu"],_fireDOMEvent:function(t,e,i){"click"===t.type&&((a=l({},t)).type="preclick",this._fireDOMEvent(a,a.type,i));var n=this._findEventTargets(t,e);if(i){for(var o=[],s=0;sthis.options.zoomAnimationThreshold)return!1;var n=this.getZoomScale(e),n=this._getCenterOffset(t)._divideBy(1-1/n);if(!0!==i.animate&&!this.getSize().contains(n))return!1;x(function(){this._moveStart(!0,i.noMoveStart||!1)._animateZoom(t,e,!0)},this)}return!0},_animateZoom:function(t,e,i,n){this._mapPane&&(i&&(this._animatingZoom=!0,this._animateToCenter=t,this._animateToZoom=e,M(this._mapPane,"leaflet-zoom-anim")),this.fire("zoomanim",{center:t,zoom:e,noUpdate:n}),this._tempFireZoomEvent||(this._tempFireZoomEvent=this._zoom!==this._animateToZoom),this._move(this._animateToCenter,this._animateToZoom,void 0,!0),setTimeout(a(this._onZoomTransitionEnd,this),250))},_onZoomTransitionEnd:function(){this._animatingZoom&&(this._mapPane&&z(this._mapPane,"leaflet-zoom-anim"),this._animatingZoom=!1,this._move(this._animateToCenter,this._animateToZoom,void 0,!0),this._tempFireZoomEvent&&this.fire("zoom"),delete this._tempFireZoomEvent,this.fire("move"),this._moveEnd(!0))}});function Ue(t){return new B(t)}var B=et.extend({options:{position:"topright"},initialize:function(t){c(this,t)},getPosition:function(){return this.options.position},setPosition:function(t){var e=this._map;return e&&e.removeControl(this),this.options.position=t,e&&e.addControl(this),this},getContainer:function(){return this._container},addTo:function(t){this.remove(),this._map=t;var e=this._container=this.onAdd(t),i=this.getPosition(),t=t._controlCorners[i];return M(e,"leaflet-control"),-1!==i.indexOf("bottom")?t.insertBefore(e,t.firstChild):t.appendChild(e),this._map.on("unload",this.remove,this),this},remove:function(){return this._map&&(T(this._container),this.onRemove&&this.onRemove(this._map),this._map.off("unload",this.remove,this),this._map=null),this},_refocusOnMap:function(t){this._map&&t&&0",e=document.createElement("div");return e.innerHTML=t,e.firstChild},_addItem:function(t){var e,i=document.createElement("label"),n=this._map.hasLayer(t.layer),n=(t.overlay?((e=document.createElement("input")).type="checkbox",e.className="leaflet-control-layers-selector",e.defaultChecked=n):e=this._createRadioElement("leaflet-base-layers_"+h(this),n),this._layerControlInputs.push(e),e.layerId=h(t.layer),S(e,"click",this._onInputClick,this),document.createElement("span")),o=(n.innerHTML=" "+t.name,document.createElement("span"));return i.appendChild(o),o.appendChild(e),o.appendChild(n),(t.overlay?this._overlaysList:this._baseLayersList).appendChild(i),this._checkDisabledLayers(),i},_onInputClick:function(){if(!this._preventClick){var t,e,i=this._layerControlInputs,n=[],o=[];this._handlingClick=!0;for(var s=i.length-1;0<=s;s--)t=i[s],e=this._getLayer(t.layerId).layer,t.checked?n.push(e):t.checked||o.push(e);for(s=0;se.options.maxZoom},_expandIfNotCollapsed:function(){return this._map&&!this.options.collapsed&&this.expand(),this},_expandSafely:function(){var t=this._section,e=(this._preventClick=!0,S(t,"click",O),this.expand(),this);setTimeout(function(){k(t,"click",O),e._preventClick=!1})}})),qe=B.extend({options:{position:"topleft",zoomInText:'',zoomInTitle:"Zoom in",zoomOutText:'',zoomOutTitle:"Zoom out"},onAdd:function(t){var e="leaflet-control-zoom",i=P("div",e+" leaflet-bar"),n=this.options;return this._zoomInButton=this._createButton(n.zoomInText,n.zoomInTitle,e+"-in",i,this._zoomIn),this._zoomOutButton=this._createButton(n.zoomOutText,n.zoomOutTitle,e+"-out",i,this._zoomOut),this._updateDisabled(),t.on("zoomend zoomlevelschange",this._updateDisabled,this),i},onRemove:function(t){t.off("zoomend zoomlevelschange",this._updateDisabled,this)},disable:function(){return this._disabled=!0,this._updateDisabled(),this},enable:function(){return this._disabled=!1,this._updateDisabled(),this},_zoomIn:function(t){!this._disabled&&this._map._zoomthis._map.getMinZoom()&&this._map.zoomOut(this._map.options.zoomDelta*(t.shiftKey?3:1))},_createButton:function(t,e,i,n,o){i=P("a",i,n);return i.innerHTML=t,i.href="#",i.title=e,i.setAttribute("role","button"),i.setAttribute("aria-label",e),Ie(i),S(i,"click",Re),S(i,"click",o,this),S(i,"click",this._refocusOnMap,this),i},_updateDisabled:function(){var t=this._map,e="leaflet-disabled";z(this._zoomInButton,e),z(this._zoomOutButton,e),this._zoomInButton.setAttribute("aria-disabled","false"),this._zoomOutButton.setAttribute("aria-disabled","false"),!this._disabled&&t._zoom!==t.getMinZoom()||(M(this._zoomOutButton,e),this._zoomOutButton.setAttribute("aria-disabled","true")),!this._disabled&&t._zoom!==t.getMaxZoom()||(M(this._zoomInButton,e),this._zoomInButton.setAttribute("aria-disabled","true"))}}),Ge=(A.mergeOptions({zoomControl:!0}),A.addInitHook(function(){this.options.zoomControl&&(this.zoomControl=new qe,this.addControl(this.zoomControl))}),B.extend({options:{position:"bottomleft",maxWidth:100,metric:!0,imperial:!0},onAdd:function(t){var e="leaflet-control-scale",i=P("div",e),n=this.options;return this._addScales(n,e+"-line",i),t.on(n.updateWhenIdle?"moveend":"move",this._update,this),t.whenReady(this._update,this),i},onRemove:function(t){t.off(this.options.updateWhenIdle?"moveend":"move",this._update,this)},_addScales:function(t,e,i){t.metric&&(this._mScale=P("div",e,i)),t.imperial&&(this._iScale=P("div",e,i))},_update:function(){var t=this._map,e=t.getSize().y/2,t=t.distance(t.containerPointToLatLng([0,e]),t.containerPointToLatLng([this.options.maxWidth,e]));this._updateScales(t)},_updateScales:function(t){this.options.metric&&t&&this._updateMetric(t),this.options.imperial&&t&&this._updateImperial(t)},_updateMetric:function(t){var e=this._getRoundNum(t);this._updateScale(this._mScale,e<1e3?e+" m":e/1e3+" km",e/t)},_updateImperial:function(t){var e,i,t=3.2808399*t;5280'+(b.inlineSvg?' ':"")+"Leaflet"},initialize:function(t){c(this,t),this._attributions={}},onAdd:function(t){for(var e in(t.attributionControl=this)._container=P("div","leaflet-control-attribution"),Ie(this._container),t._layers)t._layers[e].getAttribution&&this.addAttribution(t._layers[e].getAttribution());return this._update(),t.on("layeradd",this._addAttribution,this),this._container},onRemove:function(t){t.off("layeradd",this._addAttribution,this)},_addAttribution:function(t){t.layer.getAttribution&&(this.addAttribution(t.layer.getAttribution()),t.layer.once("remove",function(){this.removeAttribution(t.layer.getAttribution())},this))},setPrefix:function(t){return this.options.prefix=t,this._update(),this},addAttribution:function(t){return t&&(this._attributions[t]||(this._attributions[t]=0),this._attributions[t]++,this._update()),this},removeAttribution:function(t){return t&&this._attributions[t]&&(this._attributions[t]--,this._update()),this},_update:function(){if(this._map){var t,e=[];for(t in this._attributions)this._attributions[t]&&e.push(t);var i=[];this.options.prefix&&i.push(this.options.prefix),e.length&&i.push(e.join(", ")),this._container.innerHTML=i.join(' ')}}}),n=(A.mergeOptions({attributionControl:!0}),A.addInitHook(function(){this.options.attributionControl&&(new Ke).addTo(this)}),B.Layers=Ve,B.Zoom=qe,B.Scale=Ge,B.Attribution=Ke,Ue.layers=function(t,e,i){return new Ve(t,e,i)},Ue.zoom=function(t){return new qe(t)},Ue.scale=function(t){return new Ge(t)},Ue.attribution=function(t){return new Ke(t)},et.extend({initialize:function(t){this._map=t},enable:function(){return this._enabled||(this._enabled=!0,this.addHooks()),this},disable:function(){return this._enabled&&(this._enabled=!1,this.removeHooks()),this},enabled:function(){return!!this._enabled}})),ft=(n.addTo=function(t,e){return t.addHandler(e,this),this},{Events:e}),Ye=b.touch?"touchstart mousedown":"mousedown",Xe=it.extend({options:{clickTolerance:3},initialize:function(t,e,i,n){c(this,n),this._element=t,this._dragStartTarget=e||t,this._preventOutline=i},enable:function(){this._enabled||(S(this._dragStartTarget,Ye,this._onDown,this),this._enabled=!0)},disable:function(){this._enabled&&(Xe._dragging===this&&this.finishDrag(!0),k(this._dragStartTarget,Ye,this._onDown,this),this._enabled=!1,this._moved=!1)},_onDown:function(t){var e,i;this._enabled&&(this._moved=!1,ve(this._element,"leaflet-zoom-anim")||(t.touches&&1!==t.touches.length?Xe._dragging===this&&this.finishDrag():Xe._dragging||t.shiftKey||1!==t.which&&1!==t.button&&!t.touches||((Xe._dragging=this)._preventOutline&&Me(this._element),Le(),re(),this._moving||(this.fire("down"),i=t.touches?t.touches[0]:t,e=Ce(this._element),this._startPoint=new p(i.clientX,i.clientY),this._startPos=Pe(this._element),this._parentScale=Ze(e),i="mousedown"===t.type,S(document,i?"mousemove":"touchmove",this._onMove,this),S(document,i?"mouseup":"touchend touchcancel",this._onUp,this)))))},_onMove:function(t){var e;this._enabled&&(t.touches&&1e&&(i.push(t[n]),o=n);oe.max.x&&(i|=2),t.ye.max.y&&(i|=8),i}function ri(t,e,i,n){var o=e.x,e=e.y,s=i.x-o,r=i.y-e,a=s*s+r*r;return 0this._layersMaxZoom&&this.setZoom(this._layersMaxZoom),void 0===this.options.minZoom&&this._layersMinZoom&&this.getZoom()t.y!=n.y>t.y&&t.x<(n.x-i.x)*(t.y-i.y)/(n.y-i.y)+i.x&&(l=!l);return l||yi.prototype._containsPoint.call(this,t,!0)}});var wi=ci.extend({initialize:function(t,e){c(this,e),this._layers={},t&&this.addData(t)},addData:function(t){var e,i,n,o=d(t)?t:t.features;if(o){for(e=0,i=o.length;es.x&&(r=i.x+a-s.x+o.x),i.x-r-n.x<(a=0)&&(r=i.x-n.x),i.y+e+o.y>s.y&&(a=i.y+e-s.y+o.y),i.y-a-n.y<0&&(a=i.y-n.y),(r||a)&&(this.options.keepInView&&(this._autopanning=!0),t.fire("autopanstart").panBy([r,a]))))},_getAnchor:function(){return m(this._source&&this._source._getPopupAnchor?this._source._getPopupAnchor():[0,0])}})),Ii=(A.mergeOptions({closePopupOnClick:!0}),A.include({openPopup:function(t,e,i){return this._initOverlay(Bi,t,e,i).openOn(this),this},closePopup:function(t){return(t=arguments.length?t:this._popup)&&t.close(),this}}),o.include({bindPopup:function(t,e){return this._popup=this._initOverlay(Bi,this._popup,t,e),this._popupHandlersAdded||(this.on({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!0),this},unbindPopup:function(){return this._popup&&(this.off({click:this._openPopup,keypress:this._onKeyPress,remove:this.closePopup,move:this._movePopup}),this._popupHandlersAdded=!1,this._popup=null),this},openPopup:function(t){return this._popup&&(this instanceof ci||(this._popup._source=this),this._popup._prepareOpen(t||this._latlng)&&this._popup.openOn(this._map)),this},closePopup:function(){return this._popup&&this._popup.close(),this},togglePopup:function(){return this._popup&&this._popup.toggle(this),this},isPopupOpen:function(){return!!this._popup&&this._popup.isOpen()},setPopupContent:function(t){return this._popup&&this._popup.setContent(t),this},getPopup:function(){return this._popup},_openPopup:function(t){var e;this._popup&&this._map&&(Re(t),e=t.layer||t.target,this._popup._source!==e||e instanceof fi?(this._popup._source=e,this.openPopup(t.latlng)):this._map.hasLayer(this._popup)?this.closePopup():this.openPopup(t.latlng))},_movePopup:function(t){this._popup.setLatLng(t.latlng)},_onKeyPress:function(t){13===t.originalEvent.keyCode&&this._openPopup(t)}}),Ai.extend({options:{pane:"tooltipPane",offset:[0,0],direction:"auto",permanent:!1,sticky:!1,opacity:.9},onAdd:function(t){Ai.prototype.onAdd.call(this,t),this.setOpacity(this.options.opacity),t.fire("tooltipopen",{tooltip:this}),this._source&&(this.addEventParent(this._source),this._source.fire("tooltipopen",{tooltip:this},!0))},onRemove:function(t){Ai.prototype.onRemove.call(this,t),t.fire("tooltipclose",{tooltip:this}),this._source&&(this.removeEventParent(this._source),this._source.fire("tooltipclose",{tooltip:this},!0))},getEvents:function(){var t=Ai.prototype.getEvents.call(this);return this.options.permanent||(t.preclick=this.close),t},_initLayout:function(){var t="leaflet-tooltip "+(this.options.className||"")+" leaflet-zoom-"+(this._zoomAnimated?"animated":"hide");this._contentNode=this._container=P("div",t),this._container.setAttribute("role","tooltip"),this._container.setAttribute("id","leaflet-tooltip-"+h(this))},_updateLayout:function(){},_adjustPan:function(){},_setPosition:function(t){var e,i=this._map,n=this._container,o=i.latLngToContainerPoint(i.getCenter()),i=i.layerPointToContainerPoint(t),s=this.options.direction,r=n.offsetWidth,a=n.offsetHeight,h=m(this.options.offset),l=this._getAnchor(),i="top"===s?(e=r/2,a):"bottom"===s?(e=r/2,0):(e="center"===s?r/2:"right"===s?0:"left"===s?r:i.xthis.options.maxZoom||nthis.options.maxZoom||void 0!==this.options.minZoom&&oi.max.x)||!e.wrapLat&&(t.yi.max.y))return!1}return!this.options.bounds||(e=this._tileCoordsToBounds(t),g(this.options.bounds).overlaps(e))},_keyToBounds:function(t){return this._tileCoordsToBounds(this._keyToTileCoords(t))},_tileCoordsToNwSe:function(t){var e=this._map,i=this.getTileSize(),n=t.scaleBy(i),i=n.add(i);return[e.unproject(n,t.z),e.unproject(i,t.z)]},_tileCoordsToBounds:function(t){t=this._tileCoordsToNwSe(t),t=new s(t[0],t[1]);return t=this.options.noWrap?t:this._map.wrapLatLngBounds(t)},_tileCoordsToKey:function(t){return t.x+":"+t.y+":"+t.z},_keyToTileCoords:function(t){var t=t.split(":"),e=new p(+t[0],+t[1]);return e.z=+t[2],e},_removeTile:function(t){var e=this._tiles[t];e&&(T(e.el),delete this._tiles[t],this.fire("tileunload",{tile:e.el,coords:this._keyToTileCoords(t)}))},_initTile:function(t){M(t,"leaflet-tile");var e=this.getTileSize();t.style.width=e.x+"px",t.style.height=e.y+"px",t.onselectstart=u,t.onmousemove=u,b.ielt9&&this.options.opacity<1&&C(t,this.options.opacity)},_addTile:function(t,e){var i=this._getTilePos(t),n=this._tileCoordsToKey(t),o=this.createTile(this._wrapCoords(t),a(this._tileReady,this,t));this._initTile(o),this.createTile.length<2&&x(a(this._tileReady,this,t,null,o)),Z(o,i),this._tiles[n]={el:o,coords:t,current:!0},e.appendChild(o),this.fire("tileloadstart",{tile:o,coords:t})},_tileReady:function(t,e,i){e&&this.fire("tileerror",{error:e,tile:i,coords:t});var n=this._tileCoordsToKey(t);(i=this._tiles[n])&&(i.loaded=+new Date,this._map._fadeAnimated?(C(i.el,0),r(this._fadeFrame),this._fadeFrame=x(this._updateOpacity,this)):(i.active=!0,this._pruneTiles()),e||(M(i.el,"leaflet-tile-loaded"),this.fire("tileload",{tile:i.el,coords:t})),this._noTilesToLoad()&&(this._loading=!1,this.fire("load"),b.ielt9||!this._map._fadeAnimated?x(this._pruneTiles,this):setTimeout(a(this._pruneTiles,this),250)))},_getTilePos:function(t){return t.scaleBy(this.getTileSize()).subtract(this._level.origin)},_wrapCoords:function(t){var e=new p(this._wrapX?H(t.x,this._wrapX):t.x,this._wrapY?H(t.y,this._wrapY):t.y);return e.z=t.z,e},_pxBoundsToTileRange:function(t){var e=this.getTileSize();return new f(t.min.unscaleBy(e).floor(),t.max.unscaleBy(e).ceil().subtract([1,1]))},_noTilesToLoad:function(){for(var t in this._tiles)if(!this._tiles[t].loaded)return!1;return!0}});var Di=Ni.extend({options:{minZoom:0,maxZoom:18,subdomains:"abc",errorTileUrl:"",zoomOffset:0,tms:!1,zoomReverse:!1,detectRetina:!1,crossOrigin:!1,referrerPolicy:!1},initialize:function(t,e){this._url=t,(e=c(this,e)).detectRetina&&b.retina&&0')}}catch(t){}return function(t){return document.createElement("<"+t+' xmlns="urn:schemas-microsoft.com:vml" class="lvml">')}}(),zt={_initContainer:function(){this._container=P("div","leaflet-vml-container")},_update:function(){this._map._animatingZoom||(Wi.prototype._update.call(this),this.fire("update"))},_initPath:function(t){var e=t._container=Vi("shape");M(e,"leaflet-vml-shape "+(this.options.className||"")),e.coordsize="1 1",t._path=Vi("path"),e.appendChild(t._path),this._updateStyle(t),this._layers[h(t)]=t},_addPath:function(t){var e=t._container;this._container.appendChild(e),t.options.interactive&&t.addInteractiveTarget(e)},_removePath:function(t){var e=t._container;T(e),t.removeInteractiveTarget(e),delete this._layers[h(t)]},_updateStyle:function(t){var e=t._stroke,i=t._fill,n=t.options,o=t._container;o.stroked=!!n.stroke,o.filled=!!n.fill,n.stroke?(e=e||(t._stroke=Vi("stroke")),o.appendChild(e),e.weight=n.weight+"px",e.color=n.color,e.opacity=n.opacity,n.dashArray?e.dashStyle=d(n.dashArray)?n.dashArray.join(" "):n.dashArray.replace(/( *, *)/g," "):e.dashStyle="",e.endcap=n.lineCap.replace("butt","flat"),e.joinstyle=n.lineJoin):e&&(o.removeChild(e),t._stroke=null),n.fill?(i=i||(t._fill=Vi("fill")),o.appendChild(i),i.color=n.fillColor||n.color,i.opacity=n.fillOpacity):i&&(o.removeChild(i),t._fill=null)},_updateCircle:function(t){var e=t._point.round(),i=Math.round(t._radius),n=Math.round(t._radiusY||i);this._setPath(t,t._empty()?"M0 0":"AL "+e.x+","+e.y+" "+i+","+n+" 0,23592600")},_setPath:function(t,e){t._path.v=e},_bringToFront:function(t){fe(t._container)},_bringToBack:function(t){ge(t._container)}},qi=b.vml?Vi:ct,Gi=Wi.extend({_initContainer:function(){this._container=qi("svg"),this._container.setAttribute("pointer-events","none"),this._rootGroup=qi("g"),this._container.appendChild(this._rootGroup)},_destroyContainer:function(){T(this._container),k(this._container),delete this._container,delete this._rootGroup,delete this._svgSize},_update:function(){var t,e,i;this._map._animatingZoom&&this._bounds||(Wi.prototype._update.call(this),e=(t=this._bounds).getSize(),i=this._container,this._svgSize&&this._svgSize.equals(e)||(this._svgSize=e,i.setAttribute("width",e.x),i.setAttribute("height",e.y)),Z(i,t.min),i.setAttribute("viewBox",[t.min.x,t.min.y,e.x,e.y].join(" ")),this.fire("update"))},_initPath:function(t){var e=t._path=qi("path");t.options.className&&M(e,t.options.className),t.options.interactive&&M(e,"leaflet-interactive"),this._updateStyle(t),this._layers[h(t)]=t},_addPath:function(t){this._rootGroup||this._initContainer(),this._rootGroup.appendChild(t._path),t.addInteractiveTarget(t._path)},_removePath:function(t){T(t._path),t.removeInteractiveTarget(t._path),delete this._layers[h(t)]},_updatePath:function(t){t._project(),t._update()},_updateStyle:function(t){var e=t._path,t=t.options;e&&(t.stroke?(e.setAttribute("stroke",t.color),e.setAttribute("stroke-opacity",t.opacity),e.setAttribute("stroke-width",t.weight),e.setAttribute("stroke-linecap",t.lineCap),e.setAttribute("stroke-linejoin",t.lineJoin),t.dashArray?e.setAttribute("stroke-dasharray",t.dashArray):e.removeAttribute("stroke-dasharray"),t.dashOffset?e.setAttribute("stroke-dashoffset",t.dashOffset):e.removeAttribute("stroke-dashoffset")):e.setAttribute("stroke","none"),t.fill?(e.setAttribute("fill",t.fillColor||t.color),e.setAttribute("fill-opacity",t.fillOpacity),e.setAttribute("fill-rule",t.fillRule||"evenodd")):e.setAttribute("fill","none"))},_updatePoly:function(t,e){this._setPath(t,dt(t._parts,e))},_updateCircle:function(t){var e=t._point,i=Math.max(Math.round(t._radius),1),n="a"+i+","+(Math.max(Math.round(t._radiusY),1)||i)+" 0 1,0 ",e=t._empty()?"M0 0":"M"+(e.x-i)+","+e.y+n+2*i+",0 "+n+2*-i+",0 ";this._setPath(t,e)},_setPath:function(t,e){t._path.setAttribute("d",e)},_bringToFront:function(t){fe(t._path)},_bringToBack:function(t){ge(t._path)}});function Ki(t){return b.svg||b.vml?new Gi(t):null}b.vml&&Gi.include(zt),A.include({getRenderer:function(t){t=(t=t.options.renderer||this._getPaneRenderer(t.options.pane)||this.options.renderer||this._renderer)||(this._renderer=this._createRenderer());return this.hasLayer(t)||this.addLayer(t),t},_getPaneRenderer:function(t){var e;return"overlayPane"!==t&&void 0!==t&&(void 0===(e=this._paneRenderers[t])&&(e=this._createRenderer({pane:t}),this._paneRenderers[t]=e),e)},_createRenderer:function(t){return this.options.preferCanvas&&Ui(t)||Ki(t)}});var Yi=xi.extend({initialize:function(t,e){xi.prototype.initialize.call(this,this._boundsToLatLngs(t),e)},setBounds:function(t){return this.setLatLngs(this._boundsToLatLngs(t))},_boundsToLatLngs:function(t){return[(t=g(t)).getSouthWest(),t.getNorthWest(),t.getNorthEast(),t.getSouthEast()]}});Gi.create=qi,Gi.pointsToPath=dt,wi.geometryToLayer=bi,wi.coordsToLatLng=Li,wi.coordsToLatLngs=Ti,wi.latLngToCoords=Mi,wi.latLngsToCoords=zi,wi.getFeature=Ci,wi.asFeature=Zi,A.mergeOptions({boxZoom:!0});var _t=n.extend({initialize:function(t){this._map=t,this._container=t._container,this._pane=t._panes.overlayPane,this._resetStateTimeout=0,t.on("unload",this._destroy,this)},addHooks:function(){S(this._container,"mousedown",this._onMouseDown,this)},removeHooks:function(){k(this._container,"mousedown",this._onMouseDown,this)},moved:function(){return this._moved},_destroy:function(){T(this._pane),delete this._pane},_resetState:function(){this._resetStateTimeout=0,this._moved=!1},_clearDeferredResetState:function(){0!==this._resetStateTimeout&&(clearTimeout(this._resetStateTimeout),this._resetStateTimeout=0)},_onMouseDown:function(t){if(!t.shiftKey||1!==t.which&&1!==t.button)return!1;this._clearDeferredResetState(),this._resetState(),re(),Le(),this._startPoint=this._map.mouseEventToContainerPoint(t),S(document,{contextmenu:Re,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseMove:function(t){this._moved||(this._moved=!0,this._box=P("div","leaflet-zoom-box",this._container),M(this._container,"leaflet-crosshair"),this._map.fire("boxzoomstart")),this._point=this._map.mouseEventToContainerPoint(t);var t=new f(this._point,this._startPoint),e=t.getSize();Z(this._box,t.min),this._box.style.width=e.x+"px",this._box.style.height=e.y+"px"},_finish:function(){this._moved&&(T(this._box),z(this._container,"leaflet-crosshair")),ae(),Te(),k(document,{contextmenu:Re,mousemove:this._onMouseMove,mouseup:this._onMouseUp,keydown:this._onKeyDown},this)},_onMouseUp:function(t){1!==t.which&&1!==t.button||(this._finish(),this._moved&&(this._clearDeferredResetState(),this._resetStateTimeout=setTimeout(a(this._resetState,this),0),t=new s(this._map.containerPointToLatLng(this._startPoint),this._map.containerPointToLatLng(this._point)),this._map.fitBounds(t).fire("boxzoomend",{boxZoomBounds:t})))},_onKeyDown:function(t){27===t.keyCode&&(this._finish(),this._clearDeferredResetState(),this._resetState())}}),Ct=(A.addInitHook("addHandler","boxZoom",_t),A.mergeOptions({doubleClickZoom:!0}),n.extend({addHooks:function(){this._map.on("dblclick",this._onDoubleClick,this)},removeHooks:function(){this._map.off("dblclick",this._onDoubleClick,this)},_onDoubleClick:function(t){var e=this._map,i=e.getZoom(),n=e.options.zoomDelta,i=t.originalEvent.shiftKey?i-n:i+n;"center"===e.options.doubleClickZoom?e.setZoom(i):e.setZoomAround(t.containerPoint,i)}})),Zt=(A.addInitHook("addHandler","doubleClickZoom",Ct),A.mergeOptions({dragging:!0,inertia:!0,inertiaDeceleration:3400,inertiaMaxSpeed:1/0,easeLinearity:.2,worldCopyJump:!1,maxBoundsViscosity:0}),n.extend({addHooks:function(){var t;this._draggable||(t=this._map,this._draggable=new Xe(t._mapPane,t._container),this._draggable.on({dragstart:this._onDragStart,drag:this._onDrag,dragend:this._onDragEnd},this),this._draggable.on("predrag",this._onPreDragLimit,this),t.options.worldCopyJump&&(this._draggable.on("predrag",this._onPreDragWrap,this),t.on("zoomend",this._onZoomEnd,this),t.whenReady(this._onZoomEnd,this))),M(this._map._container,"leaflet-grab leaflet-touch-drag"),this._draggable.enable(),this._positions=[],this._times=[]},removeHooks:function(){z(this._map._container,"leaflet-grab"),z(this._map._container,"leaflet-touch-drag"),this._draggable.disable()},moved:function(){return this._draggable&&this._draggable._moved},moving:function(){return this._draggable&&this._draggable._moving},_onDragStart:function(){var t,e=this._map;e._stop(),this._map.options.maxBounds&&this._map.options.maxBoundsViscosity?(t=g(this._map.options.maxBounds),this._offsetLimit=_(this._map.latLngToContainerPoint(t.getNorthWest()).multiplyBy(-1),this._map.latLngToContainerPoint(t.getSouthEast()).multiplyBy(-1).add(this._map.getSize())),this._viscosity=Math.min(1,Math.max(0,this._map.options.maxBoundsViscosity))):this._offsetLimit=null,e.fire("movestart").fire("dragstart"),e.options.inertia&&(this._positions=[],this._times=[])},_onDrag:function(t){var e,i;this._map.options.inertia&&(e=this._lastTime=+new Date,i=this._lastPos=this._draggable._absPos||this._draggable._newPos,this._positions.push(i),this._times.push(e),this._prunePositions(e)),this._map.fire("move",t).fire("drag",t)},_prunePositions:function(t){for(;1e.max.x&&(t.x=this._viscousLimit(t.x,e.max.x)),t.y>e.max.y&&(t.y=this._viscousLimit(t.y,e.max.y)),this._draggable._newPos=this._draggable._startPos.add(t))},_onPreDragWrap:function(){var t=this._worldWidth,e=Math.round(t/2),i=this._initialWorldOffset,n=this._draggable._newPos.x,o=(n-e+i)%t+e-i,n=(n+e+i)%t-e-i,t=Math.abs(o+i)e.getMaxZoom()&&1