From 4617efa085ac5eb4b2df5e19b4cfcf4bcc835584 Mon Sep 17 00:00:00 2001 From: Claude Opus 5 Date: Fri, 31 Jul 2026 10:34:03 +0200 Subject: [PATCH 01/26] feat(weather): add STRANG as an irradiance source and declare source coverage STRANG becomes a real irradiance source rather than an unreferenced client, every external data source now says where in the world it works, and the location picker moves off Leaflet. STRANG's parameter set was mapped against the live API because SMHI's apidocs pages 404: exactly 116-122 exist. Identification was confirmed by physics rather than by guessing - at solar noon 121 + 122 = 723.0 + 87.5 = 810.5, exactly parameter 117, and 119 caps at 60, i.e. minutes within the hour. STRANG publishes no cloud cover; it is a radiation model. Cloudiness is instead derived from sunshine duration as 1 - minutes/60, which is observed rather than inferred but coarser: blind to thin cirrus, undefined at night. CloudCover() therefore returns an explicit unknown instead of defaulting to clear, because those two lead to opposite decisions. The new coverage registry makes an existing silence explicit. STRANG is Nordic-only and every price provider is European, so sites elsewhere were getting empty results with no explanation (#726). GET /api/data-sources now reports area, countries, licence and whether each source reaches this site, and the Weather tab renders it under the map. Bounds are advisory: STRANG's grid is rotated, so a lat/lon box can only ever be a superset - all four in-box corners were probed and returned no data. False is definitive, true means worth trying. Scoring now declines to start outside the domain instead of retrying nightly forever. Stacked on #718, which carries the plane-of-array wiring this builds on. Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com> Signed-off-by: Hugo Karlsson <48095810+HuggeK@users.noreply.github.com> --- .changeset/strang-source-coverage.md | 46 ++++ README.md | 1 + docs/data-coverage.md | 154 ++++++++++++ go/cmd/ftw/main.go | 22 ++ go/internal/api/api.go | 143 +++++++++++ go/internal/api/api_datasources_test.go | 157 ++++++++++++ go/internal/api/api_pvperf_test.go | 91 +++++++ go/internal/coverage/coverage.go | 188 +++++++++++++++ go/internal/coverage/coverage_test.go | 174 ++++++++++++++ go/internal/forecast/forecast.go | 39 ++- go/internal/forecast/forecast_test.go | 156 ++++++++++-- go/internal/mpc/external_optimizer.go | 5 +- go/internal/mpc/pvband.go | 37 +++ go/internal/mpc/pvband_test.go | 86 +++++++ go/internal/pvperf/calibration.go | 117 +++++++++ go/internal/pvperf/calibration_test.go | 153 ++++++++++++ go/internal/pvperf/pvperf.go | 89 +++++++ go/internal/pvperf/pvperf_test.go | 83 +++++++ go/internal/pvperf/service.go | 227 ++++++++++++++++++ go/internal/pvperf/service_test.go | 156 ++++++++++++ go/internal/state/pvperf.go | 138 +++++++++++ go/internal/state/pvperf_test.go | 119 +++++++++ go/internal/state/store.go | 28 +++ go/internal/strang/strang.go | 232 ++++++++++++++++++ go/internal/strang/strang_test.go | 306 ++++++++++++++++++++++++ go/internal/units/consistency_test.go | 12 + web/components/ftw-bar-chart.js | 68 +++++- web/components/ftw-history-card.js | 113 ++++++++- web/components/index.js | 4 +- web/index.html | 10 +- web/settings/tabs/weather.js | 175 +++++++++++--- 31 files changed, 3260 insertions(+), 69 deletions(-) create mode 100644 .changeset/strang-source-coverage.md create mode 100644 docs/data-coverage.md create mode 100644 go/internal/api/api_datasources_test.go create mode 100644 go/internal/api/api_pvperf_test.go create mode 100644 go/internal/coverage/coverage.go create mode 100644 go/internal/coverage/coverage_test.go create mode 100644 go/internal/mpc/pvband.go create mode 100644 go/internal/mpc/pvband_test.go create mode 100644 go/internal/pvperf/calibration.go create mode 100644 go/internal/pvperf/calibration_test.go create mode 100644 go/internal/pvperf/pvperf.go create mode 100644 go/internal/pvperf/pvperf_test.go create mode 100644 go/internal/pvperf/service.go create mode 100644 go/internal/pvperf/service_test.go create mode 100644 go/internal/state/pvperf.go create mode 100644 go/internal/state/pvperf_test.go create mode 100644 go/internal/strang/strang.go create mode 100644 go/internal/strang/strang_test.go diff --git a/.changeset/strang-source-coverage.md b/.changeset/strang-source-coverage.md new file mode 100644 index 000000000..6fc1a2b08 --- /dev/null +++ b/.changeset/strang-source-coverage.md @@ -0,0 +1,46 @@ +--- +"ftw": minor +--- + +SMHI STRÅNG becomes a first-class irradiance source, every external data source +now declares where in the world it works, and the location picker moves to +MapLibre GL JS. + +- **STRÅNG as an irradiance source.** The client now covers the model's full + parameter set and knows its own domain. A nightly backfill scores measured + production against the DC energy the configured arrays should have produced + under that irradiance, exposed at `GET /api/pv/performance` and drawn as a + dashed "expected (STRÅNG)" overlay on the Produced tile. The resulting + performance ratio feeds back as a calibration factor on the forward forecast, + refused outright when it lands outside a plausible band — a site reading at + 10% or 160% of nameplate is a configuration fault, and silently rescaling the + forecast would hide it. + +- **Cloud cover, derived.** STRÅNG publishes no cloud-cover parameter; it is a + radiation model. It does publish sunshine duration (minutes per hour above the + WMO beam threshold), so cloudiness is recovered as `1 − minutes/60`. That is an + observed quantity rather than an inferred cloud field, but coarser: blind to + thin cirrus, and undefined at night. The API returns an explicit *unknown* + rather than defaulting to *clear*, because those lead to opposite decisions. + +- **Coverage metadata.** New `GET /api/data-sources` reports every forecast, + irradiance and price source with its coverage area, country list, licence and + whether it reaches this specific site; the Weather tab renders it under the + map and flags sources that do not. This makes an existing silence explicit: + STRÅNG is Nordic-only and every price provider is European, so sites elsewhere + were getting empty results with no explanation. Bounds are advisory — a + rotated model grid means a lat/lon box can only be a superset — so `false` is + definitive and `true` means "worth trying". PV performance scoring now declines + to start outside the STRÅNG domain instead of retrying nightly forever. + +- **MapLibre GL JS location picker.** The Weather tab's map is now MapLibre GL + JS 6 (BSD-3) instead of Leaflet, still lazy-loaded only when the tab opens. v6 + is ESM-only and code-split, so it loads via a pinned dynamic import; the + stylesheet keeps its integrity hash, while the JS relies on version pinning + (an integrity hash on the entry point would not cover the shared chunk it + imports anyway). The style is built inline from the same OpenStreetMap raster + tiles as before, so neither the tile source nor the attribution changed. The + numeric latitude/longitude fields remain authoritative, so a CDN or WebGL + failure costs the picker and nothing else. + +Nothing here touches the control tick, dispatch or the optimizer contract. diff --git a/README.md b/README.md index 8a045ac62..176ddc882 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,7 @@ metadata are the detailed reference. - [Product roadmap](docs/roadmap.md) - [Power sign convention](docs/site-convention.md) - [Safety invariants](docs/safety.md) +- [Geographic coverage of external data](docs/data-coverage.md) - [Operations and recovery](docs/operations.md) - [Full backup and safe restore](docs/backup-and-restore.md) - [Writing a driver](docs/writing-a-driver.md) diff --git a/docs/data-coverage.md b/docs/data-coverage.md new file mode 100644 index 000000000..a38a678ad --- /dev/null +++ b/docs/data-coverage.md @@ -0,0 +1,154 @@ +# Geographic coverage of external data sources + +FTW controls hardware anywhere, but it depends on external data for three +things: **spot prices**, **weather/PV forecasts** and **PV performance +scoring**. Those three have very different geographic reach, and the difference +decides how much of FTW is useful at a given site. + +Short version: + +- **Weather and PV forecasting works worldwide.** +- **Price-driven planning works in Europe only.** +- **PV performance scoring works in the Nordic region only.** +- **Roof geometry (planned) is Sweden only.** + +A site outside Europe can still run FTW for monitoring, safety and control — but +the economic optimisation that motivates most of the planner has no price source +to work from. + +`GET /api/data-sources` answers this per site: it returns every source with its +coverage area and, when the site location is known, whether that source reaches +it. The Weather settings tab renders the same data under the map. This file is +the prose; `go/internal/coverage` is the machine-readable source of truth, and +the two are meant to stay in step. + +> **Coverage bounds are advisory.** Each bounded source declares a lat/lon box, +> but STRÅNG's model grid is rotated relative to lat/lon, so its box is a +> *superset* of the real domain — points near a corner pass the box test and +> still return nothing. Treat `covers: false` as definitive and `covers: true` +> as "worth trying". The upstream API is always the final word. + +## Spot prices — Europe only + +Configured under `price.provider`. + +| Provider | Coverage | API key | Notes | +|---|---|---|---| +| `sourceful` | European day-ahead markets | No | Default. Sourceful's cached ENTSO-E API. | +| `elprisetjustnu` | **Sweden only** — zones SE1–SE4 | No | 15-minute PTU since late 2025. | +| `entsoe` | ENTSO-E member markets (most of Europe) | Yes | Direct from the Transparency Platform. | +| `none` | — | — | Disables price fetching entirely. | + +There is **no provider for any market outside Europe**. North America (CAISO, +ERCOT, PJM, ISO-NE, NYISO, MISO, SPP, AESO, IESO), Australia (AEMO/NEM), Japan +(JEPX) and everywhere else are unsupported, and there is no manual or +fixed-tariff provider to stand in for them. + +Two further Europe-centric assumptions live in the price layer: prices are +stored internally in **öre** (1 SEK = 100 öre), and ENTSO-E's EUR/MWh figures +are converted using **ECB** daily FX rates. + +> The Tibber driver (`drivers/tibber.lua`) is telemetry only — it reports meter +> readings, not prices, so it is not a fourth price source. + +## Weather and PV forecasts — worldwide + +Configured under `weather.provider`. All four work at any latitude/longitude. + +| Provider | Coverage | API key | Signal quality | +|---|---|---|---| +| `met_no` | Global | No | Cloud cover only — weakest PV signal. | +| `openweather` | Global | Yes | Cloud cover only. | +| `open_meteo` | Global | No | Shortwave radiation (GHI) — good. | +| `forecast_solar` | Global | No (free tier) | Site-calibrated watts from panel geometry — best. | + +Accuracy varies by region because the underlying numerical weather models do, +but none of these are geographically gated. Outside the Nordics, prefer +`open_meteo` or `forecast_solar`: they carry an irradiance signal, which is what +the orientation-aware plane-of-array model needs. + +## PV performance scoring — Nordic region only + +The scorer (`GET /api/pv/performance`) compares measured production against a +physics baseline built from **SMHI STRÅNG** irradiance. STRÅNG is a mesoscale +analysis product covering the **Nordic region** hourly at ~2.5 km from 1999 to +roughly one day ago. It is free, keyless and CC BY 4.0. + +Outside that domain STRÅNG returns no data, so scoring simply never produces +rows and the dashboard overlay stays hidden. Nothing fails loudly; the feature +is just unavailable. + +Because the same scoring feeds the **forecast calibration factor**, sites +outside the STRÅNG domain also do not get measured calibration of their PV +forecast — they fall back to the uncalibrated physics estimate. + +STRÅNG has **no forward horizon**. It is never used as a forecast provider; see +[architecture.md](architecture.md) for where it sits. + +### What STRÅNG actually publishes + +Probing the live API on 2026-07-31 (SMHI's own apidocs pages currently 404) +returned data for exactly seven parameters and 404 for everything else. Names +were confirmed from their magnitudes on a clear day rather than from docs: + +| Code | Quantity | Unit | Noon value, Stockholm 2026-06-21 | +|---|---|---|---| +| 116 | CIE-weighted UV irradiance | mW/m² | 146.6 | +| 117 | **Global horizontal (GHI)** | W/m² | 810.5 | +| 118 | Direct normal (DNI) | W/m² | 917.2 | +| 119 | **Sunshine duration** | min/h | 60.0 | +| 120 | Photosynthetically active radiation | W/m² | 357.9 | +| 121 | Direct horizontal | W/m² | 723.0 | +| 122 | **Diffuse horizontal (DHI)** | W/m² | 87.5 | + +Two checks confirm the identification: 121 + 122 = 723.0 + 87.5 = 810.5, exactly +parameter 117 (direct + diffuse = global), and 119 caps at exactly 60, i.e. +minutes within the hour. + +**STRÅNG publishes no cloud cover.** It is a radiation model; cloudiness is not +among its outputs. It is however *derivable*: parameter 119 counts the minutes +in each hour during which direct beam irradiance exceeded the WMO sunshine +threshold, so `1 − minutes/60` is the fraction of the hour the sun spent +obscured. That is an observed quantity rather than an inferred cloud field, but +it is coarser than a forecast provider's cloud percentage — it cannot see thin +cirrus that dims without blocking. FTW exposes it via +`strang.IrradianceHour.CloudCover(lat, lon)`, which returns an explicit +"unknown" rather than defaulting to "clear". + +The location argument is not decoration. Sunshine duration is zero at night for +the trivial reason that there is no sun, and zero again near sunrise and sunset +because the beam crosses ten or more air masses and cannot reach the 120 W/m² +threshold even under a spotless sky. Both would read as "100% overcast" if taken +at face value. `CloudCover` therefore declines to answer unless the sun clears +**5° of elevation** at some point in the hour, sampling the hour's start, +midpoint and end so the hour in which the sun crosses that line is still counted. + +Live data from 2026-06-21 at Stockholm shows the distinction: + +| Hour (UTC) | GHI W/m² | Sunshine | Cloud cover | +|---|---|---|---| +| 00:00 | 0.0 | 0 min | *unknown* — sun below horizon | +| 04:00 | 163.2 | 60 min | 0% | +| 12:00 | 810.5 | 60 min | 0% | +| 20:00 | 2.5 | 0 min | *unknown* — sun minutes from setting | + +## Roof geometry — Sweden only (planned) + +The roof-derivation module proposed in +[RFC #717](https://github.com/srcfl/ftw/discussions/717) reads **Lantmäteriet** +building footprints and LiDAR, which exist for **Sweden only** and require a +Geotorget account. Everywhere else, panel tilt/azimuth/kWp stays a manual entry +in the Weather settings tab — which is the fallback by design, not a +degraded mode. + +## What a non-European site loses + +| Capability | Works outside Europe? | +|---|---| +| Device control, safety, dispatch | Yes | +| Telemetry, history, dashboard | Yes | +| Weather + PV forecasting | Yes | +| Self-learning PV twin | Yes | +| Price-driven planning / optimisation | **No** — no price source | +| PV performance scoring + calibration | **No** — outside STRÅNG's domain | +| Automatic roof geometry | **No** — Sweden only | diff --git a/go/cmd/ftw/main.go b/go/cmd/ftw/main.go index c8d35f144..219e7b19c 100644 --- a/go/cmd/ftw/main.go +++ b/go/cmd/ftw/main.go @@ -64,6 +64,7 @@ import ( "github.com/srcfl/ftw/go/internal/prices" "github.com/srcfl/ftw/go/internal/proxy" "github.com/srcfl/ftw/go/internal/pvmodel" + "github.com/srcfl/ftw/go/internal/pvperf" "github.com/srcfl/ftw/go/internal/selftune" "github.com/srcfl/ftw/go/internal/selfupdate" "github.com/srcfl/ftw/go/internal/state" @@ -1204,6 +1205,26 @@ func main() { "lat", forecastSvc.Lat, "lon", forecastSvc.Lon, "rated_pv_w", ratedPVW) } + // ---- Start PV performance scoring (optional) ---- + // Nightly backfill of SMHI STRÅNG historical irradiance + expected-vs-actual + // PV scoring. Nil when the site has no PV geometry to score against. This is + // read-only with respect to control: it only fetches weather data and writes + // the irradiance_history + pv_performance_daily tables. + pvPerfSvc := pvperf.FromConfig(cfg.Weather, ratedPVW, st, + "ftw/"+Version+" github.com/srcfl/ftw") + if pvPerfSvc != nil { + pvPerfSvc.Start(ctx) + defer pvPerfSvc.Stop() + // Close the loop: measured performance calibrates the forward + // forecast. The hook is read at fetch time, so it starts correcting + // as soon as enough days are scored — no restart needed. + if forecastSvc != nil { + forecastSvc.Calibration = pvPerfSvc.CalibrationFactor + } + slog.Info("pv performance scoring started", + "lat", pvPerfSvc.Lat, "lon", pvPerfSvc.Lon, "arrays", len(pvPerfSvc.Arrays)) + } + // ---- Start PV digital twin (optional, requires weather config) ---- // pvSvc is pre-declared above so the reload Applier can update it. if cfg.Weather != nil && cfg.Weather.Provider != "" && cfg.Weather.Provider != "none" { @@ -2568,6 +2589,7 @@ func main() { SnapshotDir: filepath.Join(filepath.Dir(statePath), "snapshots"), Prices: priceSvc, Forecast: forecastSvc, + PVPerf: pvPerfSvc, MPC: mpcSvc, PlannerPrefs: plannerPrefs, PVModel: pvSvc, diff --git a/go/internal/api/api.go b/go/internal/api/api.go index 40bfec1b3..ebc21e36a 100644 --- a/go/internal/api/api.go +++ b/go/internal/api/api.go @@ -36,6 +36,7 @@ import ( "github.com/srcfl/ftw/go/internal/config" "github.com/srcfl/ftw/go/internal/configreload" "github.com/srcfl/ftw/go/internal/control" + "github.com/srcfl/ftw/go/internal/coverage" "github.com/srcfl/ftw/go/internal/driverrepo" "github.com/srcfl/ftw/go/internal/drivers" "github.com/srcfl/ftw/go/internal/evcloud" @@ -50,6 +51,7 @@ import ( "github.com/srcfl/ftw/go/internal/ocpp" "github.com/srcfl/ftw/go/internal/prices" "github.com/srcfl/ftw/go/internal/pvmodel" + "github.com/srcfl/ftw/go/internal/pvperf" "github.com/srcfl/ftw/go/internal/scanner" "github.com/srcfl/ftw/go/internal/selftune" "github.com/srcfl/ftw/go/internal/selfupdate" @@ -139,6 +141,10 @@ type Deps struct { Prices *prices.Service Forecast *forecast.Service + // Optional: STRÅNG-based PV performance scoring. Nil when the site has no + // PV geometry to score against (surfaced as {enabled:false}). + PVPerf *pvperf.Service + // Optional: MPC planner. Nil if disabled or a buildMPC gate skipped it. MPC *mpc.Service @@ -495,6 +501,8 @@ func (s *Server) routes() { s.handle("GET /api/prices", Read, s.handlePrices) s.handle("GET /api/prices/zones", Read, s.handlePriceZones) s.handle("GET /api/forecast", Read, s.handleForecast) + s.handle("GET /api/pv/performance", Read, s.handlePVPerformance) + s.handle("GET /api/data-sources", Read, s.handleDataSources) s.handle("GET /api/mpc/plan", Read, s.handleMPCPlan) s.handle("POST /api/mpc/replan", Configure, s.handleMPCReplan) s.handle("GET /api/mpc/diagnose", Read, s.handleMPCDiagnose) @@ -2420,6 +2428,141 @@ func (s *Server) handleForecast(w http.ResponseWriter, r *http.Request) { writeJSON(w, 200, map[string]any{"items": rows, "enabled": true}) } +// ---- /api/data-sources ---- +// +// Where each external data source works, and whether it covers this site. +// Response: {latitude, longitude, sources:[{id, kind, label, area, countries, +// worldwide, requires_key, license, note, covers}]}. `covers` is advisory: for +// a bounded source it is a lat/lon box test, and STRÅNG's grid is rotated, so a +// true near a corner still means "worth trying", not "guaranteed". False is +// reliable — that location is definitely not served. +// +// This exists because several sources are regional (STRÅNG is Nordic-only, +// every price provider is European) and nothing previously said so: a site +// outside those areas got an empty result and no explanation. See #726. +func (s *Server) handleDataSources(w http.ResponseWriter, r *http.Request) { + var lat, lon float64 + var haveSite bool + // Weather is an optional config section, so it is nil on a site that has + // never configured one — which is exactly the site most likely to be + // looking at this endpoint. + if s.deps.CfgMu != nil { + s.deps.CfgMu.RLock() + if s.deps.Cfg != nil && s.deps.Cfg.Weather != nil { + lat, lon = s.deps.Cfg.Weather.Latitude, s.deps.Cfg.Weather.Longitude + haveSite = lat != 0 || lon != 0 + } + s.deps.CfgMu.RUnlock() + } + + // An explicit ?lat=&lon= overrides the configured site so the Weather tab + // can preview coverage for a pin the operator is still dragging around, + // before they save it. + if v := r.URL.Query().Get("lat"); v != "" { + if f, err := strconv.ParseFloat(v, 64); err == nil { + lat, haveSite = f, true + } + } + if v := r.URL.Query().Get("lon"); v != "" { + if f, err := strconv.ParseFloat(v, 64); err == nil { + lon, haveSite = f, true + } + } + + items := make([]map[string]any, 0, len(coverage.All())) + for _, src := range coverage.All() { + item := map[string]any{ + "id": src.ID, + "kind": string(src.Kind), + "label": src.Label, + "area": src.Area, + "worldwide": src.Worldwide(), + "requires_key": src.RequiresKey, + } + if len(src.Countries) > 0 { + item["countries"] = src.Countries + } + if src.License != "" { + item["license"] = src.License + } + if src.Note != "" { + item["note"] = src.Note + } + // Without a site location there is nothing to test against, so omit + // `covers` entirely rather than defaulting it to a misleading true. + if haveSite { + item["covers"] = src.Covers(lat, lon) + } + items = append(items, item) + } + resp := map[string]any{"sources": items} + if haveSite { + resp["latitude"], resp["longitude"] = lat, lon + } + writeJSON(w, 200, resp) +} + +// ---- /api/pv/performance ---- +// +// STRÅNG-based expected-vs-actual PV performance scoring. Query param days=N +// (default 30, max 365) selects the lookback window. Response: +// {enabled, items:[{day, expected_wh, actual_wh, pr, ...}], performance_ratio, +// attribution}. Returns {enabled:false} when scoring is unavailable (no PV +// geometry configured). +func (s *Server) handlePVPerformance(w http.ResponseWriter, r *http.Request) { + if s.deps.PVPerf == nil { + writeJSON(w, 200, map[string]any{"items": []any{}, "enabled": false}) + return + } + days := 30 + if v := r.URL.Query().Get("days"); v != "" { + if n, err := strconv.Atoi(v); err == nil && n > 0 { + days = n + } + } + if days > 365 { + days = 365 + } + now := time.Now() + loc := now.Location() + today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc) + sinceDay := today.AddDate(0, 0, -days).Format("2006-01-02") + untilDay := today.Format("2006-01-02") + items, err := s.deps.PVPerf.Load(sinceDay, untilDay) + if err != nil { + writeJSON(w, 500, map[string]string{"error": err.Error()}) + return + } + // Energy-weighted overall performance ratio across the window — only days + // with a meaningful expected baseline (pr != null) contribute. + var sumExpected, sumActual float64 + for _, it := range items { + if it.PR != nil { + sumExpected += it.ExpectedWh + sumActual += it.ActualWh + } + } + // The calibration is reported over its own fixed window rather than the + // caller's, so a short ?days= request cannot make the site look + // uncalibrated. "applied" is what actually reaches the forward forecast. + cal := s.deps.PVPerf.Calibration() + resp := map[string]any{ + "items": items, + "enabled": true, + "attribution": "Irradiance: SMHI STRÅNG (CC BY 4.0)", + "calibration": map[string]any{ + "factor": cal.Factor, + "sigma_rel": cal.SigmaRel, + "days": cal.Days, + "applied": cal.Valid, + }, + } + if sumExpected > 0 { + resp["performance_ratio"] = sumActual / sumExpected + } + writeJSON(w, 200, resp) +} + // ---- MPC planner ---- func (s *Server) mpcDisabledPayload() map[string]any { diff --git a/go/internal/api/api_datasources_test.go b/go/internal/api/api_datasources_test.go new file mode 100644 index 000000000..74fd3ddea --- /dev/null +++ b/go/internal/api/api_datasources_test.go @@ -0,0 +1,157 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "sync" + "testing" + + "github.com/srcfl/ftw/go/internal/config" +) + +type dataSource struct { + ID string `json:"id"` + Kind string `json:"kind"` + Label string `json:"label"` + Area string `json:"area"` + Countries []string `json:"countries"` + Worldwide bool `json:"worldwide"` + RequiresKey bool `json:"requires_key"` + Note string `json:"note"` + Covers *bool `json:"covers"` +} + +type dataSourcesResp struct { + Latitude *float64 `json:"latitude"` + Longitude *float64 `json:"longitude"` + Sources []dataSource `json:"sources"` +} + +func getDataSources(t *testing.T, deps *Deps, query string) dataSourcesResp { + t.Helper() + srv := New(deps) + req := httptest.NewRequest(http.MethodGet, "/api/data-sources"+query, nil) + rr := httptest.NewRecorder() + srv.Handler().ServeHTTP(rr, req) + if rr.Code != 200 { + t.Fatalf("status = %d, want 200", rr.Code) + } + var resp dataSourcesResp + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatal(err) + } + return resp +} + +func depsAt(lat, lon float64) *Deps { + cfg := &config.Config{Weather: &config.Weather{Latitude: lat, Longitude: lon}} + return &Deps{Cfg: cfg, CfgMu: &sync.RWMutex{}} +} + +func find(t *testing.T, resp dataSourcesResp, id string) dataSource { + t.Helper() + for _, s := range resp.Sources { + if s.ID == id { + return s + } + } + t.Fatalf("source %q missing from response", id) + return dataSource{} +} + +func TestDataSourcesListsEverySource(t *testing.T) { + resp := getDataSources(t, depsAt(59.33, 18.07), "") + for _, id := range []string{ + "met_no", "openweather", "open_meteo", "forecast_solar", + "strang", "sourceful", "elprisetjustnu", "entsoe", + } { + find(t, resp, id) // fails the test if absent + } +} + +// A Nordic site: STRÅNG and the Swedish price feed both apply. +func TestDataSourcesCoversNordicSite(t *testing.T) { + resp := getDataSources(t, depsAt(59.33, 18.07), "") + for _, id := range []string{"strang", "elprisetjustnu", "sourceful", "open_meteo"} { + s := find(t, resp, id) + if s.Covers == nil || !*s.Covers { + t.Errorf("%s: want covers=true for Stockholm", id) + } + } +} + +// The case that motivated this endpoint: outside Europe the forecast still +// works, but irradiance scoring and every price provider do not. +func TestDataSourcesExplainsWhySydneyIsLimited(t *testing.T) { + resp := getDataSources(t, depsAt(-33.87, 151.21), "") + + for _, id := range []string{"met_no", "openweather", "open_meteo", "forecast_solar"} { + s := find(t, resp, id) + if s.Covers == nil || !*s.Covers { + t.Errorf("%s: forecast providers are worldwide, want covers=true", id) + } + } + for _, id := range []string{"strang", "sourceful", "elprisetjustnu", "entsoe"} { + s := find(t, resp, id) + if s.Covers == nil || *s.Covers { + t.Errorf("%s: want covers=false in Sydney", id) + } + if s.Note == "" && s.Area == "" { + t.Errorf("%s: an uncovered source must still explain its area", id) + } + } +} + +// The Weather tab previews a pin before it is saved, so an explicit lat/lon +// must override the configured site. +func TestDataSourcesQueryOverridesConfiguredSite(t *testing.T) { + deps := depsAt(59.33, 18.07) // configured: Stockholm + resp := getDataSources(t, deps, "?lat=-33.87&lon=151.21") + if s := find(t, resp, "strang"); s.Covers == nil || *s.Covers { + t.Error("query lat/lon should override config and report not covered") + } + if resp.Latitude == nil || *resp.Latitude != -33.87 { + t.Errorf("latitude = %v, want the overridden -33.87", resp.Latitude) + } +} + +// With no location configured there is nothing to test against, so `covers` +// must be absent rather than defaulting to a misleading true. +func TestDataSourcesOmitsCoversWithoutASite(t *testing.T) { + resp := getDataSources(t, &Deps{}, "") + if len(resp.Sources) == 0 { + t.Fatal("sources should still be listed without a site") + } + for _, s := range resp.Sources { + if s.Covers != nil { + t.Errorf("%s: covers should be omitted when no site is known", s.ID) + } + } + if resp.Latitude != nil || resp.Longitude != nil { + t.Error("latitude/longitude should be omitted when no site is known") + } +} + +// Metadata is the whole point of the endpoint; assert it actually arrives. +func TestDataSourcesCarriesRegionMetadata(t *testing.T) { + resp := getDataSources(t, depsAt(59.33, 18.07), "") + + strang := find(t, resp, "strang") + if strang.Worldwide { + t.Error("strang must not be reported worldwide") + } + if strang.Area == "" || len(strang.Countries) == 0 { + t.Error("strang should carry an area and country list") + } + if strang.RequiresKey { + t.Error("strang needs no API key") + } + + if ow := find(t, resp, "openweather"); !ow.RequiresKey { + t.Error("openweather requires an API key") + } + if mn := find(t, resp, "met_no"); !mn.Worldwide { + t.Error("met_no is worldwide") + } +} diff --git a/go/internal/api/api_pvperf_test.go b/go/internal/api/api_pvperf_test.go new file mode 100644 index 000000000..17107bb1e --- /dev/null +++ b/go/internal/api/api_pvperf_test.go @@ -0,0 +1,91 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "path/filepath" + "testing" + "time" + + "github.com/srcfl/ftw/go/internal/pvperf" + "github.com/srcfl/ftw/go/internal/state" +) + +func TestPVPerformanceDisabled(t *testing.T) { + srv := New(&Deps{}) // no PVPerf service + req := httptest.NewRequest(http.MethodGet, "/api/pv/performance", nil) + rr := httptest.NewRecorder() + srv.Handler().ServeHTTP(rr, req) + + if rr.Code != 200 { + t.Fatalf("status = %d, want 200", rr.Code) + } + var resp struct { + Enabled bool `json:"enabled"` + Items []any `json:"items"` + } + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatal(err) + } + if resp.Enabled { + t.Error("enabled should be false when no PVPerf service is wired") + } + if len(resp.Items) != 0 { + t.Errorf("items should be empty, got %d", len(resp.Items)) + } +} + +func TestPVPerformanceEnabledReturnsScores(t *testing.T) { + st, err := state.Open(filepath.Join(t.TempDir(), "state.db")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { st.Close() }) + + // Seed two recent scored days (relative to now so the default window covers them). + now := time.Now() + pr := 0.9 + for i := 1; i <= 2; i++ { + day := now.AddDate(0, 0, -i).Format("2006-01-02") + if err := st.SavePVPerformance(state.PVPerformanceDay{ + Day: day, ExpectedWh: 10000, ActualWh: 9000, PR: &pr, + }); err != nil { + t.Fatal(err) + } + } + + srv := New(&Deps{PVPerf: &pvperf.Service{Store: st}}) + req := httptest.NewRequest(http.MethodGet, "/api/pv/performance?days=30", nil) + rr := httptest.NewRecorder() + srv.Handler().ServeHTTP(rr, req) + + if rr.Code != 200 { + t.Fatalf("status = %d, want 200", rr.Code) + } + var resp struct { + Enabled bool `json:"enabled"` + PerformanceRatio *float64 `json:"performance_ratio"` + Attribution string `json:"attribution"` + Items []struct { + Day string `json:"day"` + ExpectedWh float64 `json:"expected_wh"` + ActualWh float64 `json:"actual_wh"` + } `json:"items"` + } + if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil { + t.Fatal(err) + } + if !resp.Enabled { + t.Error("enabled should be true") + } + if len(resp.Items) != 2 { + t.Fatalf("want 2 scored days, got %d", len(resp.Items)) + } + if resp.PerformanceRatio == nil || *resp.PerformanceRatio < 0.89 || *resp.PerformanceRatio > 0.91 { + t.Errorf("energy-weighted PR should be ~0.9, got %v", resp.PerformanceRatio) + } + if resp.Attribution == "" { + t.Error("attribution (SMHI STRÅNG CC BY 4.0) should be present") + } +} diff --git a/go/internal/coverage/coverage.go b/go/internal/coverage/coverage.go new file mode 100644 index 000000000..12ba85d90 --- /dev/null +++ b/go/internal/coverage/coverage.go @@ -0,0 +1,188 @@ +// Package coverage records where each external data source FTW talks to +// actually returns usable data. +// +// FTW runs outside the Nordics, but several of its sources are regional: +// STRÅNG models only the Nordic domain, and every price provider is European. +// Nothing in the code said so, so a site in Australia would get an empty price +// curve and an unscored PV history with no explanation. This package is that +// missing explanation, in one place, so the API and the UI can tell an operator +// *before* they select a source that it cannot serve their location. +// +// Bounds here are ADVISORY, and deliberately generous. Coverage is declared as +// a lat/lon box, but STRÅNG's grid is rotated relative to lat/lon, so the box is +// a superset of the real domain: a point near a corner can pass Covers and still +// return no data. Read Covers()==false as "definitely not supported, do not +// bother asking" and Covers()==true as "worth trying" — the upstream API stays +// authoritative. Nothing here is a safety input; it only decides what we show +// and whether we skip a pointless fetch. +package coverage + +// Kind groups sources by what they supply, so the UI can present forecast, +// irradiance and price coverage separately. +type Kind string + +const ( + KindForecast Kind = "forecast" + KindIrradiance Kind = "irradiance" + KindPrice Kind = "price" +) + +// BBox is an inclusive latitude/longitude bounding box in WGS84 degrees. +type BBox struct { + MinLat float64 `json:"min_lat"` + MinLon float64 `json:"min_lon"` + MaxLat float64 `json:"max_lat"` + MaxLon float64 `json:"max_lon"` +} + +// Contains reports whether (lat, lon) falls inside the box. Longitude is not +// wrapped: no source described here spans the antimeridian, and silently +// wrapping would turn a nonsense coordinate into a plausible-looking hit. +func (b BBox) Contains(lat, lon float64) bool { + return lat >= b.MinLat && lat <= b.MaxLat && lon >= b.MinLon && lon <= b.MaxLon +} + +// Source describes one external data source and where it works. +type Source struct { + ID string `json:"id"` + Kind Kind `json:"kind"` + Label string `json:"label"` + // Area is the human-readable coverage, shown in the UI. + Area string `json:"area"` + // Countries lists ISO 3166-1 alpha-2 codes when the source is bounded to a + // known set. Empty means either worldwide or "bounded by BBox, not by + // borders" — check Worldwide() rather than inferring from length. + Countries []string `json:"countries,omitempty"` + // BBox bounds the source geographically. nil means worldwide. + BBox *BBox `json:"bbox,omitempty"` + // RequiresKey is true when the operator must supply their own credential. + RequiresKey bool `json:"requires_key"` + License string `json:"license,omitempty"` + Note string `json:"note,omitempty"` +} + +// Worldwide reports whether the source is unbounded geographically. +func (s Source) Worldwide() bool { return s.BBox == nil } + +// Covers reports whether the source plausibly serves (lat, lon). Worldwide +// sources always do. See the package doc: a true result is advisory. +func (s Source) Covers(lat, lon float64) bool { + if s.BBox == nil { + return true + } + return s.BBox.Contains(lat, lon) +} + +// strangDomain is the STRÅNG model domain, measured against the live API rather +// than taken from documentation (SMHI's apidocs pages 404 as of 2026-07). +// Probing parameter 117 found data at lat 53.5 and 72.5 but not 53.0 or 73.5, +// and at lon 0 and 30 but not -2 or 35. The corner (53.5, -4) returns nothing +// even though it is inside this box, which is the rotated grid showing through — +// hence "advisory superset" in the package doc. +var strangDomain = &BBox{MinLat: 53.0, MinLon: -1.0, MaxLat: 73.0, MaxLon: 33.0} + +// sources is the registry. Keep it ordered by kind then id so the API response +// is stable and diffs stay readable. +var sources = []Source{ + { + ID: "met_no", Kind: KindForecast, Label: "MET Norway", + Area: "Worldwide", + License: "NLOD / CC BY 4.0", + Note: "Cloud cover only — no irradiance, so PV is derived from a cloud-derated clear-sky prior.", + }, + { + ID: "openweather", Kind: KindForecast, Label: "OpenWeather", + Area: "Worldwide", + RequiresKey: true, + Note: "Cloud cover only — same cloud-derated prior as MET Norway.", + }, + { + ID: "open_meteo", Kind: KindForecast, Label: "Open-Meteo", + Area: "Worldwide", + License: "CC BY 4.0", + Note: "Publishes shortwave radiation, so PV is irradiance-derived rather than cloud-derated.", + }, + { + ID: "forecast_solar", Kind: KindForecast, Label: "Forecast.Solar", + Area: "Worldwide", + Note: "Returns site-calibrated watts from the configured array geometry; free tier is rate-limited.", + }, + { + ID: "strang", Kind: KindIrradiance, Label: "SMHI STRÅNG", + Area: "Nordic region", + Countries: []string{"SE", "NO", "FI", "DK", "EE", "LV", "LT"}, + BBox: strangDomain, + License: "CC BY 4.0", + Note: "Historical only (1999 to ~1 day ago). Used for PV performance scoring and forecast calibration, never as a forward forecast.", + }, + { + ID: "sourceful", Kind: KindPrice, Label: "Sourceful (cached ENTSO-E)", + Area: "Europe", + Countries: europeanPriceCountries, + BBox: &BBox{MinLat: 34.0, MinLon: -25.0, MaxLat: 72.0, MaxLon: 45.0}, + Note: "European day-ahead bidding zones. No key required.", + }, + { + ID: "elprisetjustnu", Kind: KindPrice, Label: "Elpriset just nu", + Area: "Sweden", + Countries: []string{"SE"}, + BBox: &BBox{MinLat: 55.0, MinLon: 10.0, MaxLat: 69.5, MaxLon: 24.5}, + Note: "Swedish bidding zones SE1-SE4 only. No key required.", + }, + { + ID: "entsoe", Kind: KindPrice, Label: "ENTSO-E Transparency", + Area: "Europe", + Countries: europeanPriceCountries, + BBox: &BBox{MinLat: 34.0, MinLon: -25.0, MaxLat: 72.0, MaxLon: 45.0}, + RequiresKey: true, + Note: "All ENTSO-E member bidding zones.", + }, +} + +// europeanPriceCountries are the ENTSO-E member states whose day-ahead prices +// the European providers can serve. Shared by sourceful and entsoe because both +// resolve to the same underlying bidding zones. +var europeanPriceCountries = []string{ + "AT", "BE", "BG", "CH", "CZ", "DE", "DK", "EE", "ES", "FI", + "FR", "GR", "HR", "HU", "IE", "IT", "LT", "LU", "LV", "NL", + "NO", "PL", "PT", "RO", "RS", "SE", "SI", "SK", +} + +// All returns every known source. +func All() []Source { + out := make([]Source, len(sources)) + copy(out, sources) + return out +} + +// ByID returns the source with the given id. +func ByID(id string) (Source, bool) { + for _, s := range sources { + if s.ID == id { + return s, true + } + } + return Source{}, false +} + +// ForKind returns every source of one kind, in registry order. +func ForKind(k Kind) []Source { + var out []Source + for _, s := range sources { + if s.Kind == k { + out = append(out, s) + } + } + return out +} + +// Covers reports whether the named source plausibly serves (lat, lon). An +// unknown id returns false: callers ask about a source they intend to use, and +// answering "sure" for a source we know nothing about is the wrong default. +func Covers(id string, lat, lon float64) bool { + s, ok := ByID(id) + if !ok { + return false + } + return s.Covers(lat, lon) +} diff --git a/go/internal/coverage/coverage_test.go b/go/internal/coverage/coverage_test.go new file mode 100644 index 000000000..36de8d82a --- /dev/null +++ b/go/internal/coverage/coverage_test.go @@ -0,0 +1,174 @@ +package coverage + +import "testing" + +// The STRÅNG cases below are the live-API probe results recorded on 2026-07-31 +// (parameter 117, 2026-06-21). They are the reason the box has the bounds it +// does, so if someone widens it these fail and say why. +func TestStrangCoversProbedNordicPoints(t *testing.T) { + in := []struct { + name string + lat, lon float64 + }{ + {"Stockholm", 59.33, 18.07}, + {"Tromsø", 69.65, 18.96}, + {"Helsinki", 60.17, 24.94}, + {"Copenhagen", 55.68, 12.57}, + } + for _, c := range in { + if !Covers("strang", c.lat, c.lon) { + t.Errorf("%s (%.2f,%.2f): want covered, got not covered", c.name, c.lat, c.lon) + } + } +} + +func TestStrangRejectsProbedOutsidePoints(t *testing.T) { + // Every one of these returned no data from the live API. + out := []struct { + name string + lat, lon float64 + }{ + {"Berlin", 52.52, 13.40}, + {"London", 51.51, -0.13}, + {"Paris", 48.86, 2.35}, + {"Reykjavík", 64.15, -21.94}, + {"Sydney", -33.87, 151.21}, + {"New York", 40.71, -74.01}, + } + for _, c := range out { + if Covers("strang", c.lat, c.lon) { + t.Errorf("%s (%.2f,%.2f): want not covered, got covered", c.name, c.lat, c.lon) + } + } +} + +// The declared box is a superset of the rotated grid: these four corners are +// inside the box yet every one returned no data when probed live on 2026-07-31. +// That gap is intentional and documented — Covers()==true means "worth asking", +// not "guaranteed". Pinned so nobody tightens the box into a false promise, or +// starts treating a true result as a guarantee. +func TestStrangBoxIsAdvisorySupersetAtCorners(t *testing.T) { + corners := [][2]float64{ + {53.5, -0.5}, {53.5, 32.0}, {72.5, -0.5}, {72.5, 32.0}, + } + for _, c := range corners { + if !Covers("strang", c[0], c[1]) { + t.Errorf("(%.1f,%.1f): corner should pass the advisory box test", c[0], c[1]) + } + } +} + +func TestForecastProvidersAreWorldwide(t *testing.T) { + for _, id := range []string{"met_no", "openweather", "open_meteo", "forecast_solar"} { + s, ok := ByID(id) + if !ok { + t.Fatalf("%s: not registered", id) + } + if !s.Worldwide() { + t.Errorf("%s: want worldwide", id) + } + // A worldwide source must cover anywhere, including the far south. + if !s.Covers(-33.87, 151.21) { + t.Errorf("%s: worldwide source must cover Sydney", id) + } + } +} + +// The whole point of #726: price data is Europe-only. If someone adds a global +// price provider this test should be updated deliberately, not incidentally. +func TestPriceProvidersAreEuropeOnly(t *testing.T) { + prices := ForKind(KindPrice) + if len(prices) == 0 { + t.Fatal("no price sources registered") + } + for _, s := range prices { + if s.Worldwide() { + t.Errorf("%s: price sources are not worldwide", s.ID) + } + if s.Covers(-33.87, 151.21) { + t.Errorf("%s: must not claim to cover Sydney", s.ID) + } + if s.Covers(40.71, -74.01) { + t.Errorf("%s: must not claim to cover New York", s.ID) + } + } +} + +func TestSwedishPriceProviderIsNarrowerThanEuropean(t *testing.T) { + // Berlin: served by the European providers, not by the Swedish one. + if Covers("elprisetjustnu", 52.52, 13.40) { + t.Error("elprisetjustnu must not claim Berlin") + } + if !Covers("sourceful", 52.52, 13.40) { + t.Error("sourceful should cover Berlin") + } + if !Covers("elprisetjustnu", 59.33, 18.07) { + t.Error("elprisetjustnu should cover Stockholm") + } +} + +// An unknown id must not be treated as universally available. +func TestUnknownSourceIsNotCovered(t *testing.T) { + if Covers("does_not_exist", 59.33, 18.07) { + t.Error("unknown source must report not covered") + } + if _, ok := ByID("does_not_exist"); ok { + t.Error("unknown source must not resolve") + } +} + +func TestBBoxContainsIsInclusive(t *testing.T) { + b := BBox{MinLat: 10, MinLon: 20, MaxLat: 30, MaxLon: 40} + for _, c := range []struct { + lat, lon float64 + want bool + }{ + {10, 20, true}, // min corner + {30, 40, true}, // max corner + {20, 30, true}, // interior + {9.99, 30, false}, // just south + {20, 40.01, false}, // just east + } { + if got := b.Contains(c.lat, c.lon); got != c.want { + t.Errorf("Contains(%v,%v) = %v, want %v", c.lat, c.lon, got, c.want) + } + } +} + +// Longitude is intentionally not wrapped; a nonsense coordinate must stay a +// miss rather than being folded into range. +func TestBBoxDoesNotWrapLongitude(t *testing.T) { + b := BBox{MinLat: -90, MinLon: -180, MaxLat: 90, MaxLon: 180} + if b.Contains(0, 200) { + t.Error("lon 200 must not wrap to -160") + } +} + +func TestRegistryIsInternallyConsistent(t *testing.T) { + seen := map[string]bool{} + for _, s := range All() { + if s.ID == "" || s.Label == "" || s.Area == "" { + t.Errorf("%+v: id, label and area are all required", s) + } + if seen[s.ID] { + t.Errorf("%s: duplicate id", s.ID) + } + seen[s.ID] = true + if s.BBox != nil { + if s.BBox.MinLat > s.BBox.MaxLat || s.BBox.MinLon > s.BBox.MaxLon { + t.Errorf("%s: inverted bbox %+v", s.ID, *s.BBox) + } + } + } +} + +// All() must hand out a copy: a caller mutating the result must not corrupt the +// registry for everyone else in the process. +func TestAllReturnsACopy(t *testing.T) { + got := All() + original := got[0].ID + got[0].ID = "mutated" + if All()[0].ID != original { + t.Fatal("All() exposed the backing array") + } +} diff --git a/go/internal/forecast/forecast.go b/go/internal/forecast/forecast.go index acefbc773..733a8632d 100644 --- a/go/internal/forecast/forecast.go +++ b/go/internal/forecast/forecast.go @@ -265,12 +265,21 @@ type Service struct { Lat, Lon float64 RatedPVW float64 // total rated PV across all arrays (used for estimate) - // Arrays holds per-plane geometry (tilt/azimuth/kWp) mirrored from the + // Arrays holds per-plane geometry (tilt/azimuth/rated_w) mirrored from the // weather config. When set, a radiation-bearing provider's horizontal // GHI is projected onto each plane via sunpos and summed, instead of the // orientation-blind flat rated×(W/m²/1000) estimate. Empty → flat estimate. Arrays []Array + // Calibration optionally supplies a measured site correction factor, + // wired to the STRÅNG performance scorer. The irradiance-derived + // estimates below are deliberately loss-free (no inverter, wiring or + // temperature derate), so a site settles at a stable ratio slightly under + // 1; feeding that measured ratio back closes the gap. The second return + // is false whenever the factor must not be applied, and nil means the + // scorer isn't wired at all — both leave the estimate untouched. + Calibration func() (float64, bool) + stop chan struct{} done chan struct{} } @@ -367,6 +376,15 @@ func (s *Service) fetchAndStore(ctx context.Context) { return } nowMs := time.Now().UnixMilli() + + // Resolved once per fetch so every point in a batch shares one factor. + calibration, calibrated := 1.0, false + if s.Calibration != nil { + if f, ok := s.Calibration(); ok && f > 0 { + calibration, calibrated = f, true + } + } + points := make([]state.ForecastPoint, 0, len(rows)) for _, r := range rows { // A negative irradiance is not physical; retain the row with a @@ -386,19 +404,35 @@ func (s *Service) fetchAndStore(ctx context.Context) { // radiation we turn into watts via rated × W/m²/1000; met.no only has // cloud fraction, so we fall through to the naive cloud-derated prior. var pvW float64 + // Only the two irradiance-derived branches are calibrated. They share + // the loss-free "irradiance × nameplate" baseline the performance ratio + // was measured against, so the correction is the same quantity. A + // provider-native figure is already site-calibrated upstream and the + // cloud-derated prior carries its own empirical derate; scaling either + // would double-count. + applyCalibration := false switch { case r.PVWEstimated != nil: pvW = *r.PVWEstimated case solarWm2 != nil: + // Irradiance-derived: orientation-aware when per-plane geometry + // exists, flat rated×(W/m²/1000) otherwise. Either way it is the + // loss-free baseline the performance ratio was measured against. var ok bool pvW, ok = pvWFromGHI(s.Lat, s.Lon, r.HourStart, *solarWm2, s.RatedPVW, s.Arrays) if !ok { slog.Warn("forecast row skipped", "reason", "non-finite irradiance", "provider", s.Provider.Name(), "slot", r.HourStart) continue } + applyCalibration = true default: pvW = EstimatePVW(s.Lat, s.Lon, r.HourStart, r.CloudCoverPct, s.RatedPVW) } + // Calibrate before the finiteness guard so the stored value is the one + // that was checked, not the one before scaling. + if applyCalibration && calibrated { + pvW *= calibration + } if math.IsNaN(pvW) || math.IsInf(pvW, 0) { slog.Warn("forecast row skipped", "reason", "non-finite PV estimate", "provider", s.Provider.Name(), "slot", r.HourStart) continue @@ -430,7 +464,8 @@ func (s *Service) fetchAndStore(ctx context.Context) { slog.Warn("forecast save failed", "err", err) return } - slog.Info("forecast fetched", "count", len(points), "provider", s.Provider.Name()) + slog.Info("forecast fetched", "count", len(points), "provider", s.Provider.Name(), + "pv_calibration", calibration, "calibrated", calibrated) } func arrayFromConfig(a config.PVArray) (Array, bool) { diff --git a/go/internal/forecast/forecast_test.go b/go/internal/forecast/forecast_test.go index 9c6567c7f..7f0f4ffb6 100644 --- a/go/internal/forecast/forecast_test.go +++ b/go/internal/forecast/forecast_test.go @@ -111,7 +111,9 @@ func TestEstimatePVWCloudReduction(t *testing.T) { func TestEstimatePVWNilCloudIsMid(t *testing.T) { tt := time.Date(2026, 6, 21, 11, 0, 0, 0, time.UTC) pv := EstimatePVW(59.3293, 18.0686, tt, nil, 10000) - if pv == 0 { t.Error("nil cloud should default to mid-range, not zero") } + if pv == 0 { + t.Error("nil cloud should default to mid-range, not zero") + } } // ---- met.no HTTP ---- @@ -130,7 +132,7 @@ func TestMetNoFetchParses(t *testing.T) { "instant": map[string]any{ "details": map[string]any{ "cloud_area_fraction": 75.0, - "air_temperature": 8.5, + "air_temperature": 8.5, }, }, }, @@ -141,7 +143,7 @@ func TestMetNoFetchParses(t *testing.T) { "instant": map[string]any{ "details": map[string]any{ "cloud_area_fraction": 20.0, - "air_temperature": 7.2, + "air_temperature": 7.2, }, }, }, @@ -156,8 +158,12 @@ func TestMetNoFetchParses(t *testing.T) { p := NewMetNo("test-ua") p.BaseURL = srv.URL rows, err := p.Fetch(context.Background(), 59.3, 18.1) - if err != nil { t.Fatal(err) } - if len(rows) != 2 { t.Fatalf("got %d rows, want 2", len(rows)) } + if err != nil { + t.Fatal(err) + } + if len(rows) != 2 { + t.Fatalf("got %d rows, want 2", len(rows)) + } if rows[0].CloudCoverPct == nil || *rows[0].CloudCoverPct != 75 { t.Errorf("cloud cover: %+v", rows[0].CloudCoverPct) } @@ -174,7 +180,9 @@ func TestMetNoErrorsOn500(t *testing.T) { p := NewMetNo("test") p.BaseURL = srv.URL _, err := p.Fetch(context.Background(), 59, 18) - if err == nil { t.Error("expected error on 500") } + if err == nil { + t.Error("expected error on 500") + } } // ---- OpenWeather HTTP ---- @@ -193,16 +201,26 @@ func TestOpenWeatherFetchParses(t *testing.T) { p := NewOpenWeather("test-key") p.BaseURL = srv.URL rows, err := p.Fetch(context.Background(), 59, 18) - if err != nil { t.Fatal(err) } - if len(rows) != 2 { t.Fatalf("got %d", len(rows)) } - if *rows[0].CloudCoverPct != 40 { t.Errorf("cloud: %f", *rows[0].CloudCoverPct) } - if *rows[1].TempC != 10.5 { t.Errorf("temp: %f", *rows[1].TempC) } + if err != nil { + t.Fatal(err) + } + if len(rows) != 2 { + t.Fatalf("got %d", len(rows)) + } + if *rows[0].CloudCoverPct != 40 { + t.Errorf("cloud: %f", *rows[0].CloudCoverPct) + } + if *rows[1].TempC != 10.5 { + t.Errorf("temp: %f", *rows[1].TempC) + } } func TestOpenWeatherRequiresKey(t *testing.T) { p := NewOpenWeather("") _, err := p.Fetch(context.Background(), 59, 18) - if err == nil { t.Error("expected API key error") } + if err == nil { + t.Error("expected API key error") + } } // ---- Service integration ---- @@ -241,8 +259,12 @@ func TestServiceFetchesAndStoresWithPVEstimate(t *testing.T) { // Load back tt := time.Date(2026, 6, 21, 11, 0, 0, 0, time.UTC) rows, err := st.LoadForecasts(tt.UnixMilli(), tt.Add(time.Hour).UnixMilli()) - if err != nil { t.Fatal(err) } - if len(rows) != 1 { t.Fatalf("got %d forecasts", len(rows)) } + if err != nil { + t.Fatal(err) + } + if len(rows) != 1 { + t.Fatalf("got %d forecasts", len(rows)) + } // Stockholm summer clear-ish sky at noon with 10kW array should give ~4-8 kW estimate if rows[0].PVWEstimated == nil || *rows[0].PVWEstimated < 1000 { t.Errorf("PV estimate should be substantial for clear summer, got %+v", rows[0].PVWEstimated) @@ -253,18 +275,30 @@ func TestServiceFetchesAndStoresWithPVEstimate(t *testing.T) { // ---- FromConfig ---- func TestFromConfigNilWhenDisabled(t *testing.T) { - if FromConfig(nil, 10000, nil, "") != nil { t.Error("nil cfg → nil svc") } - if FromConfig(&config.Weather{Provider: "none"}, 10000, nil, "") != nil { t.Error("none → nil svc") } - if FromConfig(&config.Weather{Provider: ""}, 10000, nil, "") != nil { t.Error("empty → nil svc") } + if FromConfig(nil, 10000, nil, "") != nil { + t.Error("nil cfg → nil svc") + } + if FromConfig(&config.Weather{Provider: "none"}, 10000, nil, "") != nil { + t.Error("none → nil svc") + } + if FromConfig(&config.Weather{Provider: ""}, 10000, nil, "") != nil { + t.Error("empty → nil svc") + } } func TestFromConfigBuildsMetNo(t *testing.T) { st, _ := state.Open(filepath.Join(t.TempDir(), "t.db")) defer st.Close() s := FromConfig(&config.Weather{Provider: "met_no", Latitude: 59, Longitude: 18}, 10000, st, "ua") - if s == nil { t.Fatal("expected service") } - if s.Lat != 59 { t.Errorf("lat: %f", s.Lat) } - if s.RatedPVW != 10000 { t.Errorf("rated: %f", s.RatedPVW) } + if s == nil { + t.Fatal("expected service") + } + if s.Lat != 59 { + t.Errorf("lat: %f", s.Lat) + } + if s.RatedPVW != 10000 { + t.Errorf("rated: %f", s.RatedPVW) + } } func TestFromConfigPopulatesArrays(t *testing.T) { @@ -279,7 +313,9 @@ func TestFromConfigPopulatesArrays(t *testing.T) { }, } s := FromConfig(cfg, 10000, st, "ua") - if s == nil { t.Fatal("expected service") } + if s == nil { + t.Fatal("expected service") + } if len(s.Arrays) != 2 { t.Fatalf("expected 2 arrays (kWp>0 only), got %d", len(s.Arrays)) } @@ -621,3 +657,83 @@ func TestLoadClampsStoredMegawattForecast(t *testing.T) { t.Fatalf("clamp should sit on the nameplate ceiling, got %.1f W", got) } } + +// ---- STRÅNG calibration hook ---- + +// radiationForecastPVW runs one fetch against a stub shortwave-radiation +// provider and returns the stored PV estimate, so calibration variants can be +// compared against an otherwise identical run. +func radiationForecastPVW(t *testing.T, calibration func() (float64, bool)) float64 { + t.Helper() + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + resp := map[string]any{ + "hourly": map[string]any{ + "time": []string{"2026-06-21T11:00"}, + "shortwave_radiation": []float64{700}, + "cloud_cover": []float64{5}, + "temperature_2m": []float64{20}, + }, + } + _ = json.NewEncoder(w).Encode(resp) + }) + srv := httptest.NewServer(handler) + defer srv.Close() + + st, _ := state.Open(filepath.Join(t.TempDir(), "state.db")) + defer st.Close() + + p := NewOpenMeteo() + p.BaseURL = srv.URL + s := &Service{ + Provider: p, Store: st, Lat: 59.3293, Lon: 18.0686, RatedPVW: 10000, + Arrays: []Array{{TiltDeg: 35, AzimuthDeg: 180, RatedW: 10000}}, + Calibration: calibration, + } + s.fetchAndStore(context.Background()) + + tt := time.Date(2026, 6, 21, 11, 0, 0, 0, time.UTC) + rows, err := st.LoadForecasts(tt.UnixMilli(), tt.Add(time.Hour).UnixMilli()) + if err != nil { + t.Fatal(err) + } + if len(rows) != 1 || rows[0].PVWEstimated == nil { + t.Fatalf("expected 1 forecast with PV estimate, got %+v", rows) + } + return *rows[0].PVWEstimated +} + +// The point of scoring: a site measured at 80% of its physics baseline should +// have its forward forecast scaled to match. +func TestServiceAppliesCalibrationToIrradianceEstimate(t *testing.T) { + uncalibrated := radiationForecastPVW(t, nil) + calibrated := radiationForecastPVW(t, func() (float64, bool) { return 0.8, true }) + + want := uncalibrated * 0.8 + if math.Abs(calibrated-want) > 1.0 { + t.Errorf("calibrated estimate = %.1f W, want %.1f W (0.8 × %.1f)", calibrated, want, uncalibrated) + } + t.Logf("uncalibrated %.0fW → calibrated %.0fW", uncalibrated, calibrated) +} + +// An untrusted factor must change nothing: too few days, or a ratio outside the +// plausible band, leaves the physics estimate exactly as it was. +func TestServiceIgnoresUntrustedCalibration(t *testing.T) { + uncalibrated := radiationForecastPVW(t, nil) + rejected := radiationForecastPVW(t, func() (float64, bool) { return 0.05, false }) + + if math.Abs(rejected-uncalibrated) > 1e-9 { + t.Errorf("estimate = %.1f W, want the uncalibrated %.1f W", rejected, uncalibrated) + } +} + +// A zero or negative factor would silently zero out the site's whole forecast, +// so it is refused even when the source claims it is usable. +func TestServiceIgnoresNonPositiveCalibration(t *testing.T) { + uncalibrated := radiationForecastPVW(t, nil) + for _, factor := range []float64{0, -0.5} { + got := radiationForecastPVW(t, func() (float64, bool) { return factor, true }) + if math.Abs(got-uncalibrated) > 1e-9 { + t.Errorf("factor %v: estimate = %.1f W, want the uncalibrated %.1f W", factor, got, uncalibrated) + } + } +} diff --git a/go/internal/mpc/external_optimizer.go b/go/internal/mpc/external_optimizer.go index 189416b28..7c9fd651f 100644 --- a/go/internal/mpc/external_optimizer.go +++ b/go/internal/mpc/external_optimizer.go @@ -496,8 +496,9 @@ func (o *ExternalOptimizer) buildRequest(slots []Slot, p Params) externalRequest if p.PVRelativeUncertainty > 0 { spread = relSpread * generation } - downsidePV[i] = -math.Max(0, generation-spread) - upsidePV[i] = -(generation + spread) + // PVBand owns the site-sign mapping: "low" is more negative + // (more generation, optimistic), "high" is nearer zero. + upsidePV[i], downsidePV[i] = PVBand(slot.PVW, spread) } } if hasDaylight { diff --git a/go/internal/mpc/pvband.go b/go/internal/mpc/pvband.go new file mode 100644 index 000000000..f25c42d83 --- /dev/null +++ b/go/internal/mpc/pvband.go @@ -0,0 +1,37 @@ +package mpc + +import "math" + +// PVBand returns an uncertainty band around a slot's expected PV power. +// +// Everything here is in the site convention: power into the site is positive, +// so generation is NEGATIVE. That inverts the intuitive reading of the return +// values, which is exactly why this lives in one tested function instead of +// being open-coded at each call site: +// +// - low is the numerically smaller (more negative) bound — MORE generation, +// i.e. the OPTIMISTIC case. +// - high is the numerically larger bound (closer to zero) — LESS generation, +// i.e. the PESSIMISTIC case. +// +// The optimizer validates `low <= base <= high <= 0` and rejects the request +// outright if any of that fails, so two clamps are load-bearing: high can never +// cross zero (a spread wider than expected generation degrades to "produces +// nothing", never to positive PV, which would be read as load), and a +// non-positive spread collapses the band onto the base forecast exactly. +// +// Slots with no expected generation return a zero-width band at zero, matching +// how night slots are left untouched. +func PVBand(basePVW, spreadW float64) (low, high float64) { + // `!(x < 0)` rather than `x >= 0` so NaN falls into this branch too. + if !(basePVW < 0) || math.IsInf(basePVW, 0) { + return 0, 0 + } + if !(spreadW > 0) || math.IsInf(spreadW, 0) { + return basePVW, basePVW + } + generation := -basePVW // > 0 + low = -(generation + spreadW) + high = -math.Max(0, generation-spreadW) + return low, high +} diff --git a/go/internal/mpc/pvband_test.go b/go/internal/mpc/pvband_test.go new file mode 100644 index 000000000..7dab3fc4e --- /dev/null +++ b/go/internal/mpc/pvband_test.go @@ -0,0 +1,86 @@ +package mpc + +import ( + "math" + "testing" +) + +// The optimizer rejects any request where the band fails to bracket the base +// forecast or where either bound crosses zero. This is the invariant the whole +// helper exists to guarantee, so it is asserted over a spread of inputs +// including the degenerate ones. +func TestPVBandHoldsSiteSignInvariant(t *testing.T) { + bases := []float64{0, -1, -250, -2000, -9500, math.NaN(), math.Inf(-1)} + spreads := []float64{0, 1, 250, 3000, 100000, -50, math.NaN()} + + for _, base := range bases { + for _, spread := range spreads { + low, high := PVBand(base, spread) + + if math.IsNaN(low) || math.IsNaN(high) { + t.Fatalf("PVBand(%v, %v) produced NaN: low=%v high=%v", base, spread, low, high) + } + if low > 0 || high > 0 { + t.Errorf("PVBand(%v, %v) = (%v, %v); bounds must stay <= 0 (site convention)", + base, spread, low, high) + } + if low > high { + t.Errorf("PVBand(%v, %v) = (%v, %v); low must not exceed high", base, spread, low, high) + } + // The base is only bracketed when it is itself a valid generation + // figure; invalid bases are normalised to a zero-width band. + if base < 0 && !math.IsInf(base, 0) { + if low > base || base > high { + t.Errorf("PVBand(%v, %v) = (%v, %v); band must bracket the base forecast", + base, spread, low, high) + } + } + } + } +} + +// A site with no measured uncertainty must plan against the plain forecast -- +// the band collapses to a point rather than quietly widening. +func TestPVBandZeroSpreadCollapsesOntoBase(t *testing.T) { + const base = -3200.0 + low, high := PVBand(base, 0) + if low != base || high != base { + t.Errorf("PVBand(%v, 0) = (%v, %v), want both == %v", base, low, high, base) + } +} + +// The clamp that keeps the pessimistic bound from crossing into positive +// territory: a spread wider than the expected generation means "might produce +// nothing", never "might consume". +func TestPVBandClampsPessimisticBoundAtZero(t *testing.T) { + low, high := PVBand(-1000, 4000) + if high != 0 { + t.Errorf("high = %v, want 0 (clamped); a positive bound would read as load", high) + } + if low != -5000 { + t.Errorf("low = %v, want -5000 (optimistic bound is unclamped)", low) + } +} + +// Direction check stated in production terms, so a future sign flip fails here +// with an obvious message rather than as an opaque optimizer ProtocolError. +func TestPVBandLowIsOptimisticHighIsPessimistic(t *testing.T) { + const base = -5000.0 + low, high := PVBand(base, 1500) + if generation := -low; generation != 6500 { + t.Errorf("optimistic generation = %v W, want 6500 W", generation) + } + if generation := -high; generation != 3500 { + t.Errorf("pessimistic generation = %v W, want 3500 W", generation) + } +} + +// Night slots carry no expected generation, so they get no band at all. +func TestPVBandNoGenerationYieldsNoBand(t *testing.T) { + for _, base := range []float64{0, 250} { + low, high := PVBand(base, 900) + if low != 0 || high != 0 { + t.Errorf("PVBand(%v, 900) = (%v, %v), want (0, 0)", base, low, high) + } + } +} diff --git a/go/internal/pvperf/calibration.go b/go/internal/pvperf/calibration.go new file mode 100644 index 000000000..c78689ebb --- /dev/null +++ b/go/internal/pvperf/calibration.go @@ -0,0 +1,117 @@ +package pvperf + +import ( + "math" + "sort" + "time" + + "github.com/srcfl/ftw/go/internal/state" +) + +// Calibration is the site-specific correction learned by comparing measured +// production against the STRÅNG physics baseline. It is the payoff of scoring: +// once a site has enough closed days, its typical performance ratio *is* the +// derate the forward forecast should apply, and the spread of that ratio is a +// measured uncertainty that beats any hand-tuned constant. +type Calibration struct { + // Factor is the representative performance ratio — multiply a loss-free + // physics PV estimate by this to get an expected real-world figure. + Factor float64 + // SigmaRel is the robust spread of the daily ratio. PR is dimensionless, + // so this is already a relative uncertainty. + SigmaRel float64 + // Days is how many scored days carried a usable ratio. + Days int + // Valid reports whether Factor is trustworthy enough to apply. A factor + // outside the plausible band means the geometry or the meter is wrong, + // not that the panels are dirty — better to leave the forecast alone. + Valid bool +} + +const ( + // minCalibrationDays is the smallest sample that can outvote a single + // snowy or curtailed day. + minCalibrationDays = 7 + + // Plausible bounds for a real site's ratio. Below the floor points at + // broken telemetry or an array that never ran; above the ceiling means + // the declared rated watts understate the installation. Both are configuration + // faults that a silent forecast rescale would only hide. + minCalibrationFactor = 0.30 + maxCalibrationFactor = 1.30 + + // maxCalibrationSigma caps the reported spread so a pathological sample + // cannot widen the planner's uncertainty without bound. + maxCalibrationSigma = 1.0 + + // madToSigma converts a median absolute deviation into a standard + // deviation for normally distributed data. + madToSigma = 1.4826 + + // calibrationWindowDays is how far back Service.Calibration looks. + calibrationWindowDays = 30 +) + +// Calibrate summarises scored days into a site calibration. It uses the median +// and a median-absolute-deviation sigma rather than mean/stddev: a single +// snow-covered or curtailed day is a large outlier, and the mean would chase it. +func Calibrate(days []state.PVPerformanceDay) Calibration { + ratios := make([]float64, 0, len(days)) + for _, d := range days { + if d.PR != nil && !math.IsNaN(*d.PR) && !math.IsInf(*d.PR, 0) { + ratios = append(ratios, *d.PR) + } + } + c := Calibration{Days: len(ratios)} + if len(ratios) < minCalibrationDays { + return c + } + c.Factor = median(ratios) + + deviations := make([]float64, len(ratios)) + for i, r := range ratios { + deviations[i] = math.Abs(r - c.Factor) + } + c.SigmaRel = math.Min(maxCalibrationSigma, madToSigma*median(deviations)) + c.Valid = c.Factor >= minCalibrationFactor && c.Factor <= maxCalibrationFactor + return c +} + +// median returns the middle value of xs without mutating the caller's slice. +func median(xs []float64) float64 { + if len(xs) == 0 { + return 0 + } + sorted := append([]float64(nil), xs...) + sort.Float64s(sorted) + n := len(sorted) + if n%2 == 1 { + return sorted[n/2] + } + return (sorted[n/2-1] + sorted[n/2]) / 2 +} + +// Calibration returns the site calibration over the recent scored window. +// A zero value (Valid false) is returned whenever the service is unwired or +// the history is too thin to draw a conclusion. +func (s *Service) Calibration() Calibration { + if s == nil || s.Store == nil { + return Calibration{} + } + now := time.Now() + days, err := s.Store.LoadPVPerformance( + now.AddDate(0, 0, -calibrationWindowDays).Format("2006-01-02"), + now.Format("2006-01-02"), + ) + if err != nil { + return Calibration{} + } + return Calibrate(days) +} + +// CalibrationFactor adapts Calibration to the hook shape the forecast service +// consumes. The second return is false when the factor must not be applied. +func (s *Service) CalibrationFactor() (float64, bool) { + c := s.Calibration() + return c.Factor, c.Valid +} diff --git a/go/internal/pvperf/calibration_test.go b/go/internal/pvperf/calibration_test.go new file mode 100644 index 000000000..76c7155ee --- /dev/null +++ b/go/internal/pvperf/calibration_test.go @@ -0,0 +1,153 @@ +package pvperf + +import ( + "fmt" + "math" + "testing" + "time" + + "github.com/srcfl/ftw/go/internal/state" +) + +// scoredDays builds a run of days carrying the given performance ratios. +func scoredDays(ratios ...float64) []state.PVPerformanceDay { + days := make([]state.PVPerformanceDay, 0, len(ratios)) + for i, r := range ratios { + ratio := r + days = append(days, state.PVPerformanceDay{ + Day: fmt.Sprintf("2026-07-%02d", i+1), + ExpectedWh: 50000, + ActualWh: 50000 * ratio, + PR: &ratio, + }) + } + return days +} + +// A handful of days is not evidence. Applying a factor from a thin sample would +// let one cloudy week permanently depress the forecast. +func TestCalibrateNeedsEnoughDays(t *testing.T) { + c := Calibrate(scoredDays(0.9, 0.9, 0.9)) + if c.Valid { + t.Error("calibration should not be valid on 3 days") + } + if c.Days != 3 { + t.Errorf("Days = %d, want 3", c.Days) + } +} + +// Days without a usable ratio (polar night, no history) must not be counted as +// evidence, and must not drag the factor toward zero. +func TestCalibrateIgnoresDaysWithoutRatio(t *testing.T) { + days := scoredDays(0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9) + days = append(days, state.PVPerformanceDay{Day: "2026-07-30", ExpectedWh: 10, ActualWh: 0}) + c := Calibrate(days) + if c.Days != 7 { + t.Errorf("Days = %d, want 7 (the ratio-less day is not evidence)", c.Days) + } + if math.Abs(c.Factor-0.9) > 1e-9 { + t.Errorf("Factor = %v, want 0.9", c.Factor) + } +} + +// The reason for median-over-mean: a snow day is a huge outlier, and a site +// that normally runs at 0.90 must still calibrate to ~0.90 despite one. +func TestCalibrateResistsOutlierDays(t *testing.T) { + c := Calibrate(scoredDays(0.90, 0.91, 0.89, 0.90, 0.92, 0.88, 0.90, 0.05)) + if !c.Valid { + t.Fatal("expected a valid calibration") + } + if math.Abs(c.Factor-0.90) > 0.02 { + t.Errorf("Factor = %v, want ~0.90; a single snow day should not move it", c.Factor) + } +} + +// A factor this low means the geometry or the meter is misconfigured. Silently +// rescaling the forecast would bake the fault in instead of surfacing it. +func TestCalibrateRejectsImplausiblyLowFactor(t *testing.T) { + c := Calibrate(scoredDays(0.10, 0.11, 0.09, 0.10, 0.12, 0.08, 0.10, 0.11)) + if c.Valid { + t.Errorf("factor %v should be rejected as implausible", c.Factor) + } + if c.Days != 8 { + t.Errorf("Days = %d, want 8 — the sample is still reported", c.Days) + } +} + +// Likewise for a site consistently beating its own nameplate: that is an +// understated kWp, not a bonus to hand to the planner. +func TestCalibrateRejectsImplausiblyHighFactor(t *testing.T) { + c := Calibrate(scoredDays(1.6, 1.7, 1.55, 1.62, 1.58, 1.7, 1.65, 1.6)) + if c.Valid { + t.Errorf("factor %v should be rejected as implausible", c.Factor) + } +} + +// A perfectly steady site reports no uncertainty, so the planner's band +// collapses onto the forecast rather than inventing spread. +func TestCalibrateSigmaIsZeroForSteadySite(t *testing.T) { + c := Calibrate(scoredDays(0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9, 0.9)) + if !c.Valid { + t.Fatal("expected a valid calibration") + } + if c.SigmaRel != 0 { + t.Errorf("SigmaRel = %v, want 0 for an unvarying site", c.SigmaRel) + } +} + +// A variable site reports a real spread, which is what makes the measured band +// better than a hand-tuned constant. +func TestCalibrateSigmaGrowsWithSpread(t *testing.T) { + steady := Calibrate(scoredDays(0.90, 0.91, 0.89, 0.90, 0.91, 0.89, 0.90, 0.90)) + variable := Calibrate(scoredDays(0.60, 0.95, 0.70, 0.99, 0.65, 0.92, 0.75, 0.88)) + if !(variable.SigmaRel > steady.SigmaRel) { + t.Errorf("variable SigmaRel %v should exceed steady %v", variable.SigmaRel, steady.SigmaRel) + } +} + +// The forecast hook is wired unconditionally at startup, so it must be safe on +// a service that was never built. +func TestCalibrationFactorNilServiceIsSafe(t *testing.T) { + var s *Service + factor, ok := s.CalibrationFactor() + if ok { + t.Errorf("nil service reported a usable factor (%v)", factor) + } +} + +// End-to-end through the store, since that is how the API and the forecast +// hook actually reach it. +func TestServiceCalibrationReadsPersistedDays(t *testing.T) { + st := openStore(t) + svc := &Service{Store: st} + + if c := svc.Calibration(); c.Valid || c.Days != 0 { + t.Fatalf("empty store should yield no calibration, got %+v", c) + } + + now := time.Now() + for i := 1; i <= 10; i++ { + ratio := 0.88 + day := now.AddDate(0, 0, -i).Format("2006-01-02") + if err := st.SavePVPerformance(state.PVPerformanceDay{ + Day: day, ExpectedWh: 40000, ActualWh: 40000 * ratio, PR: &ratio, + }); err != nil { + t.Fatalf("save %s: %v", day, err) + } + } + c := svc.Calibration() + if !c.Valid { + t.Fatalf("expected a valid calibration, got %+v", c) + } + if math.Abs(c.Factor-0.88) > 1e-9 { + t.Errorf("Factor = %v, want 0.88", c.Factor) + } + if c.Days != 10 { + t.Errorf("Days = %d, want 10", c.Days) + } + + factor, ok := svc.CalibrationFactor() + if !ok || math.Abs(factor-0.88) > 1e-9 { + t.Errorf("CalibrationFactor() = (%v, %v), want (0.88, true)", factor, ok) + } +} diff --git a/go/internal/pvperf/pvperf.go b/go/internal/pvperf/pvperf.go new file mode 100644 index 000000000..d0e9d0354 --- /dev/null +++ b/go/internal/pvperf/pvperf.go @@ -0,0 +1,89 @@ +// Package pvperf scores measured PV production against a weather-expected +// baseline derived from SMHI STRÅNG irradiance and the site's per-plane +// geometry. +// +// It is read-only analytics with no control-path coupling: given historical +// irradiance and the panel arrays, it computes the DC energy the arrays should +// have produced under that irradiance, then compares to the measured energy to +// yield a performance ratio (PR). A sustained PR below ~1 hints at soiling, +// snow, shading or degradation; the raw PR series is the signal, the diagnosis +// is left to the caller/UI. +// +// The expected baseline deliberately omits system losses (inverter, wiring, +// temperature). The performance ratio absorbs the site's fixed derate, so a +// healthy site simply sits at a stable PR < 1 and anomalies show as departures +// from that baseline. +package pvperf + +import ( + "time" + + "github.com/srcfl/ftw/go/internal/sunpos" + "github.com/srcfl/ftw/go/internal/units" +) + +// Array is one panel plane: nameplate DC watts at a tilt/azimuth (same +// conventions as sunpos — tilt 0=flat..90=wall, azimuth 0=N,90=E,180=S,270=W). +type Array struct { + RatedW float64 + TiltDeg float64 + AzimuthDeg float64 +} + +// Irradiance is one hour of horizontal irradiance (W/m²). DHIWm2 is nil when +// the diffuse component is unavailable, in which case an Erbs split is used. +type Irradiance struct { + HourStart time.Time + GHIWm2 float64 + DHIWm2 *float64 +} + +// ExpectedWh returns the DC energy (Wh, positive) the given arrays would +// produce under the supplied hourly irradiance at (lat, lon). Each hour's +// plane-of-array irradiance is projected via sunpos and integrated over one +// hour. When measured diffuse is present it is used directly (more accurate); +// otherwise the diffuse fraction is estimated from the clearness index. +func ExpectedWh(lat, lon float64, arrays []Array, hours []Irradiance) float64 { + var wh float64 + for _, h := range hours { + if h.GHIWm2 <= 0 { + continue + } + sun := sunpos.At(h.HourStart, lat, lon) + for _, a := range arrays { + if a.RatedW <= 0 { + continue + } + var poa float64 + if h.DHIWm2 != nil { + poa = sunpos.POAFromComponents(sun, h.GHIWm2, *h.DHIWm2, a.TiltDeg, a.AzimuthDeg) + } else { + poa = sunpos.POAFromGHI(h.HourStart, lat, lon, h.GHIWm2, a.TiltDeg, a.AzimuthDeg) + } + // Watts at STC × (POA / 1000 W/m²) × 1 h = Wh this hour. + wh += units.PVFromIrradiance(a.RatedW, poa) + } + } + return wh +} + +// minExpectedWh is the floor below which a performance ratio is meaningless +// (polar-night / near-dark days) — reported as "not available" instead. +const minExpectedWh = 100.0 + +// PerformanceRatio returns actualWh / expectedWh, clamped to [0, 2]. The second +// return is false when expected production is too small to form a meaningful +// ratio, so callers can render "n/a" rather than a divide-by-tiny artifact. +func PerformanceRatio(expectedWh, actualWh float64) (float64, bool) { + if expectedWh < minExpectedWh { + return 0, false + } + pr := actualWh / expectedWh + if pr < 0 { + pr = 0 + } + if pr > 2 { + pr = 2 + } + return pr, true +} diff --git a/go/internal/pvperf/pvperf_test.go b/go/internal/pvperf/pvperf_test.go new file mode 100644 index 000000000..2b767acc7 --- /dev/null +++ b/go/internal/pvperf/pvperf_test.go @@ -0,0 +1,83 @@ +package pvperf + +import ( + "math" + "testing" + "time" +) + +// A few clear-ish midday summer hours at Stockholm, GHI + DHI populated. +func summerHours() []Irradiance { + base := time.Date(2024, 6, 21, 8, 0, 0, 0, time.UTC) + ghi := []float64{300, 450, 600, 700, 680, 550, 400, 250} + out := make([]Irradiance, len(ghi)) + for i, g := range ghi { + d := g * 0.25 + out[i] = Irradiance{HourStart: base.Add(time.Duration(i) * time.Hour), GHIWm2: g, DHIWm2: &d} + } + return out +} + +func TestExpectedWhScalesWithRatedW(t *testing.T) { + hours := summerHours() + e5 := ExpectedWh(59.33, 18.07, []Array{{RatedW: 5000, TiltDeg: 35, AzimuthDeg: 180}}, hours) + e10 := ExpectedWh(59.33, 18.07, []Array{{RatedW: 10000, TiltDeg: 35, AzimuthDeg: 180}}, hours) + if e5 <= 0 { + t.Fatalf("expected positive energy, got %.1f", e5) + } + if math.Abs(e10/e5-2) > 1e-9 { + t.Errorf("10 kW should be 2× 5 kW, ratio %.4f", e10/e5) + } +} + +func TestExpectedWhSumsArrays(t *testing.T) { + hours := summerHours() + south := ExpectedWh(59.33, 18.07, []Array{{RatedW: 5000, TiltDeg: 35, AzimuthDeg: 180}}, hours) + east := ExpectedWh(59.33, 18.07, []Array{{RatedW: 5000, TiltDeg: 35, AzimuthDeg: 90}}, hours) + both := ExpectedWh(59.33, 18.07, []Array{ + {RatedW: 5000, TiltDeg: 35, AzimuthDeg: 180}, + {RatedW: 5000, TiltDeg: 35, AzimuthDeg: 90}, + }, hours) + if math.Abs(both-(south+east)) > 1e-6 { + t.Errorf("multi-array should sum: both=%.2f south+east=%.2f", both, south+east) + } +} + +// Measured diffuse should be honoured: an all-diffuse hour yields less on a +// south-tilted panel than the GHI-only Erbs path (which infers more beam). +func TestExpectedWhUsesMeasuredDiffuse(t *testing.T) { + base := time.Date(2024, 6, 21, 11, 0, 0, 0, time.UTC) + ghiOnly := []Irradiance{{HourStart: base, GHIWm2: 600}} + full := 600.0 + allDiffuse := []Irradiance{{HourStart: base, GHIWm2: 600, DHIWm2: &full}} + arr := []Array{{RatedW: 10000, TiltDeg: 35, AzimuthDeg: 180}} + + eErbs := ExpectedWh(59.33, 18.07, arr, ghiOnly) + eDiffuse := ExpectedWh(59.33, 18.07, arr, allDiffuse) + if !(eDiffuse < eErbs) { + t.Errorf("all-diffuse (%.1f) should be < Erbs-inferred-beam (%.1f)", eDiffuse, eErbs) + } +} + +func TestExpectedWhZeroWhenDark(t *testing.T) { + night := []Irradiance{{HourStart: time.Date(2024, 12, 21, 23, 0, 0, 0, time.UTC), GHIWm2: 0}} + if e := ExpectedWh(59.33, 18.07, []Array{{RatedW: 10000, TiltDeg: 35, AzimuthDeg: 180}}, night); e != 0 { + t.Errorf("dark hours should yield 0 Wh, got %.2f", e) + } +} + +func TestPerformanceRatio(t *testing.T) { + if _, ok := PerformanceRatio(50, 40); ok { + t.Error("tiny expected should be n/a") + } + pr, ok := PerformanceRatio(1000, 850) + if !ok || math.Abs(pr-0.85) > 1e-9 { + t.Errorf("pr=%.3f ok=%v, want 0.85 true", pr, ok) + } + if pr, _ := PerformanceRatio(1000, 5000); pr != 2 { + t.Errorf("PR should clamp high to 2, got %.2f", pr) + } + if pr, _ := PerformanceRatio(1000, -10); pr != 0 { + t.Errorf("PR should clamp low to 0, got %.2f", pr) + } +} diff --git a/go/internal/pvperf/service.go b/go/internal/pvperf/service.go new file mode 100644 index 000000000..23353d59f --- /dev/null +++ b/go/internal/pvperf/service.go @@ -0,0 +1,227 @@ +package pvperf + +import ( + "context" + "log/slog" + "time" + + "github.com/srcfl/ftw/go/internal/config" + "github.com/srcfl/ftw/go/internal/coverage" + "github.com/srcfl/ftw/go/internal/state" + "github.com/srcfl/ftw/go/internal/strang" +) + +// irradianceSource labels rows persisted by this service. +const irradianceSource = "strang" + +// defaultLookbackDays is how far back the backfill scores on each run. Older +// closed days already have an immutable cached score and are skipped. +const defaultLookbackDays = 30 + +// rescoreTailDays is how many of the most recent days are recomputed on every +// run even if already scored, so the STRÅNG ~1-day analysis lag is corrected as +// data lands (a day scored against partial irradiance is refreshed next run). +const rescoreTailDays = 3 + +// Service backfills historical STRÅNG irradiance and scores realised PV +// production against it, once at startup and nightly thereafter. It is +// read-only with respect to control: it only fetches weather data and writes +// the irradiance_history + pv_performance_daily tables. Nil when the site has +// no usable PV geometry (scoring is impossible), which the API surfaces as +// {enabled:false}. +type Service struct { + Store *state.Store + Strang *strang.Client + Lat, Lon float64 + Arrays []Array + LookbackDays int + + stop chan struct{} + done chan struct{} +} + +// FromConfig builds a scoring Service from the weather config, mirroring how +// forecast.FromConfig derives per-plane geometry (explicit pv_arrays, else a +// single synthesized array from the legacy flat fields). Returns nil when no +// geometry is available — without arrays there is nothing to score against. +func FromConfig(cfg *config.Weather, ratedPVW float64, st *state.Store, userAgent string) *Service { + if cfg == nil || st == nil { + return nil + } + var arrays []Array + for _, a := range cfg.PVArrays { + // CompleteGeometry is the same gate forecast.arrayFromConfig uses: a + // plane missing its tilt or azimuth is skipped rather than scored as + // 0° flat, so expected-vs-actual is never measured against a plane + // the operator never described. + tiltDeg, azimuthDeg, ratedW, ok := a.CompleteGeometry() + if !ok { + continue + } + arrays = append(arrays, Array{RatedW: ratedW, TiltDeg: tiltDeg, AzimuthDeg: azimuthDeg}) + } + if len(arrays) == 0 && ratedPVW > 0 { + arrays = append(arrays, Array{RatedW: ratedPVW, TiltDeg: cfg.PVTiltDeg, AzimuthDeg: cfg.PVAzimuthDeg}) + } + if len(arrays) == 0 { + return nil + } + // STRÅNG only models the Nordic domain. Outside it every nightly backfill + // would spend three HTTP requests to be told nothing, forever, so decline + // to start at all. GET /api/data-sources is where an operator finds out + // why — this is a silent no-op by design, not a hidden failure. + if !coverage.Covers("strang", cfg.Latitude, cfg.Longitude) { + slog.Info("pvperf: site is outside the STRÅNG domain, PV performance scoring disabled", + "lat", cfg.Latitude, "lon", cfg.Longitude) + return nil + } + return &Service{ + Store: st, + Strang: strang.NewClient(userAgent), + Lat: cfg.Latitude, + Lon: cfg.Longitude, + Arrays: arrays, + LookbackDays: defaultLookbackDays, + stop: make(chan struct{}), + done: make(chan struct{}), + } +} + +// Start runs an initial backfill shortly after boot, then nightly. +func (s *Service) Start(ctx context.Context) { + go s.loop(ctx) +} + +// Stop terminates the backfill loop and waits for it to drain. +func (s *Service) Stop() { + close(s.stop) + <-s.done +} + +func (s *Service) loop(ctx context.Context) { + defer close(s.done) + // Delay the first run so boot isn't competing with a network fetch, and + // so telemetry has a moment to settle before we read history. + first := time.NewTimer(3 * time.Minute) + defer first.Stop() + tick := time.NewTicker(24 * time.Hour) + defer tick.Stop() + for { + select { + case <-s.stop: + return + case <-ctx.Done(): + return + case <-first.C: + s.runBackfill(ctx) + case <-tick.C: + s.runBackfill(ctx) + } + } +} + +// runBackfill fetches the lookback window from STRÅNG, persists the irradiance, +// and scores each closed day that is missing or within the rescore tail. +func (s *Service) runBackfill(ctx context.Context) { + fetchCtx, cancel := context.WithTimeout(ctx, 2*time.Minute) + defer cancel() + + now := time.Now() + loc := now.Location() + todayMidnight := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc) + windowStart := todayMidnight.AddDate(0, 0, -s.LookbackDays) + + // STRÅNG takes calendar dates (UTC). Pad by a day each side so local-day + // boundaries are fully covered regardless of the UTC offset. + hours, err := s.Strang.FetchWindow(fetchCtx, s.Lat, s.Lon, + windowStart.UTC().AddDate(0, 0, -1), todayMidnight.UTC()) + if err != nil { + slog.Warn("pvperf: STRÅNG fetch failed", "err", err) + return + } + if len(hours) == 0 { + slog.Info("pvperf: STRÅNG returned no data for window", "lat", s.Lat, "lon", s.Lon) + return + } + + fetchedAtMs := now.UnixMilli() + rows := make([]state.IrradianceRow, 0, len(hours)) + for _, h := range hours { + rows = append(rows, state.IrradianceRow{ + SlotTsMs: h.HourStart.UnixMilli(), + GHIWm2: h.GHIWm2, + DHIWm2: h.DHIWm2, + Source: irradianceSource, + FetchedAtMs: fetchedAtMs, + }) + } + if err := s.Store.SaveIrradiance(rows); err != nil { + slog.Warn("pvperf: save irradiance failed", "err", err) + return + } + + scored := 0 + // Score closed days only (strictly before today's midnight). + for i := s.LookbackDays; i >= 1; i-- { + dayStart := todayMidnight.AddDate(0, 0, -i) + dayEnd := dayStart.AddDate(0, 0, 1) + day := dayStart.Format("2006-01-02") + + // Skip days already scored, except the recent tail we always refresh. + if i > rescoreTailDays { + if _, ok, _ := s.Store.LoadPVPerformanceDay(day); ok { + continue + } + } + if s.scoreDay(day, dayStart, dayEnd, hours, fetchedAtMs) { + scored++ + } + } + slog.Info("pvperf: backfill complete", "irradiance_rows", len(rows), "days_scored", scored) +} + +// scoreDay computes and persists one day's performance score. Returns false +// (and persists nothing) when there is no measured PV history for the day. +func (s *Service) scoreDay(day string, dayStart, dayEnd time.Time, hours []strang.IrradianceHour, fetchedAtMs int64) bool { + startMs, endMs := dayStart.UnixMilli(), dayEnd.UnixMilli() + + dayHours := make([]Irradiance, 0, 24) + for _, h := range hours { + ms := h.HourStart.UnixMilli() + if ms < startMs || ms >= endMs { + continue + } + dayHours = append(dayHours, Irradiance{HourStart: h.HourStart, GHIWm2: h.GHIWm2, DHIWm2: h.DHIWm2}) + } + + de, err := s.Store.DailyEnergy(startMs, endMs-1) + if err != nil { + slog.Warn("pvperf: read actual energy failed", "day", day, "err", err) + return false + } + if de.Intervals == 0 { + // No measured history for this day — nothing to score against. + return false + } + + expectedWh := ExpectedWh(s.Lat, s.Lon, s.Arrays, dayHours) + rec := state.PVPerformanceDay{ + Day: day, + ExpectedWh: expectedWh, + ActualWh: de.PVWh, + StrangDataDateMs: &fetchedAtMs, + } + if pr, ok := PerformanceRatio(expectedWh, de.PVWh); ok { + rec.PR = &pr + } + if err := s.Store.SavePVPerformance(rec); err != nil { + slog.Warn("pvperf: save score failed", "day", day, "err", err) + return false + } + return true +} + +// Load returns scored days in [sinceDay, untilDay] (inclusive YYYY-MM-DD). +func (s *Service) Load(sinceDay, untilDay string) ([]state.PVPerformanceDay, error) { + return s.Store.LoadPVPerformance(sinceDay, untilDay) +} diff --git a/go/internal/pvperf/service_test.go b/go/internal/pvperf/service_test.go new file mode 100644 index 000000000..ff7cb2636 --- /dev/null +++ b/go/internal/pvperf/service_test.go @@ -0,0 +1,156 @@ +package pvperf + +import ( + "math" + "path/filepath" + "testing" + "time" + + "github.com/srcfl/ftw/go/internal/config" + "github.com/srcfl/ftw/go/internal/state" + "github.com/srcfl/ftw/go/internal/strang" +) + +// f64 addresses a literal: config.PVArray keeps tilt and azimuth as pointers so +// an omitted field cannot pass for a valid 0°. +func f64(v float64) *float64 { return &v } + +func openStore(t *testing.T) *state.Store { + t.Helper() + st, err := state.Open(filepath.Join(t.TempDir(), "state.db")) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { st.Close() }) + return st +} + +func TestFromConfigGating(t *testing.T) { + st := openStore(t) + + if FromConfig(nil, 5000, st, "ua") != nil { + t.Error("nil weather config should yield nil service") + } + if FromConfig(&config.Weather{Latitude: 59, Longitude: 18}, 0, st, "ua") != nil { + t.Error("no arrays and no rated PV should yield nil service") + } + + // Explicit arrays win. + svc := FromConfig(&config.Weather{ + Latitude: 59, Longitude: 18, + PVArrays: []config.PVArray{ + {RatedW: 5000, TiltDeg: f64(35), AzimuthDeg: f64(180)}, + {RatedW: 3000, TiltDeg: f64(20), AzimuthDeg: f64(90)}, + }, + }, 0, st, "ua") + if svc == nil || len(svc.Arrays) != 2 { + t.Fatalf("expected 2 arrays, got %+v", svc) + } + if svc.Arrays[0].RatedW != 5000 || svc.Arrays[1].AzimuthDeg != 90 { + t.Errorf("array geometry mismatch: %+v", svc.Arrays) + } + + // An array whose tilt was never filled in is not a 0° flat roof; scoring + // against it would invent a plane the operator never described, so the + // incomplete entry is skipped and only the usable one survives. + partial := FromConfig(&config.Weather{ + Latitude: 59, Longitude: 18, + PVArrays: []config.PVArray{ + {RatedW: 5000, AzimuthDeg: f64(180)}, + {RatedW: 3000, TiltDeg: f64(20), AzimuthDeg: f64(90)}, + }, + }, 0, st, "ua") + if partial == nil || len(partial.Arrays) != 1 { + t.Fatalf("incomplete geometry should be skipped, got %+v", partial) + } + if partial.Arrays[0].RatedW != 3000 { + t.Errorf("wrong array survived: %+v", partial.Arrays[0]) + } + + // Flat fallback synthesizes one array from rated + legacy tilt/azimuth. + flat := FromConfig(&config.Weather{ + Latitude: 59, Longitude: 18, PVTiltDeg: 30, PVAzimuthDeg: 180, + }, 8000, st, "ua") + if flat == nil || len(flat.Arrays) != 1 { + t.Fatalf("flat fallback should synthesize one array, got %+v", flat) + } + if flat.Arrays[0].RatedW != 8000 || flat.Arrays[0].TiltDeg != 30 { + t.Errorf("synthesized array mismatch: %+v", flat.Arrays[0]) + } +} + +// A bell-ish clear day of hourly GHI centered on solar noon. +func syntheticDay(dayStart time.Time) []strang.IrradianceHour { + out := []strang.IrradianceHour{} + for hr := 4; hr <= 20; hr++ { + // crude parabola peaking ~700 W/m² at hour 12 + g := 700.0 - 12.0*float64((hr-12)*(hr-12)) + if g < 0 { + g = 0 + } + out = append(out, strang.IrradianceHour{ + HourStart: dayStart.Add(time.Duration(hr) * time.Hour), + GHIWm2: g, + }) + } + return out +} + +func TestScoreDayPersistsExpectedVsActual(t *testing.T) { + st := openStore(t) + svc := &Service{ + Store: st, + Lat: 59.33, + Lon: 18.07, + Arrays: []Array{{RatedW: 10000, TiltDeg: 35, AzimuthDeg: 180}}, + } + + dayStart := time.Date(2024, 6, 21, 0, 0, 0, 0, time.UTC) + dayEnd := dayStart.AddDate(0, 0, 1) + day := dayStart.Format("2006-01-02") + + // Seed measured PV history: -2000 W constant across 8h → 16000 Wh produced + // (PV is stored site-signed negative; DailyEnergy integrates SUM(-pv_w·Δt)). + if err := st.RecordHistory(state.HistoryPoint{TsMs: dayStart.Add(8 * time.Hour).UnixMilli(), PVW: -2000}); err != nil { + t.Fatal(err) + } + if err := st.RecordHistory(state.HistoryPoint{TsMs: dayStart.Add(16 * time.Hour).UnixMilli(), PVW: -2000}); err != nil { + t.Fatal(err) + } + + hours := syntheticDay(dayStart) + if !svc.scoreDay(day, dayStart, dayEnd, hours, 12345) { + t.Fatal("scoreDay should succeed with history present") + } + + got, ok, err := st.LoadPVPerformanceDay(day) + if err != nil || !ok { + t.Fatalf("score not persisted: ok=%v err=%v", ok, err) + } + if math.Abs(got.ActualWh-16000) > 1 { + t.Errorf("actual Wh: want ~16000, got %.1f", got.ActualWh) + } + if got.ExpectedWh <= 0 { + t.Errorf("expected Wh should be positive, got %.1f", got.ExpectedWh) + } + if got.PR == nil { + t.Error("PR should be set when expected is above the floor") + } + if got.StrangDataDateMs == nil || *got.StrangDataDateMs != 12345 { + t.Errorf("provenance not stamped: %+v", got.StrangDataDateMs) + } +} + +func TestScoreDaySkipsWhenNoHistory(t *testing.T) { + st := openStore(t) + svc := &Service{Store: st, Lat: 59.33, Lon: 18.07, Arrays: []Array{{RatedW: 10000, TiltDeg: 35, AzimuthDeg: 180}}} + + dayStart := time.Date(2024, 6, 21, 0, 0, 0, 0, time.UTC) + day := dayStart.Format("2006-01-02") + if svc.scoreDay(day, dayStart, dayStart.AddDate(0, 0, 1), syntheticDay(dayStart), 1) { + t.Error("scoreDay should return false with no measured history") + } + if _, ok, _ := st.LoadPVPerformanceDay(day); ok { + t.Error("nothing should be persisted when there's no history") + } +} diff --git a/go/internal/state/pvperf.go b/go/internal/state/pvperf.go new file mode 100644 index 000000000..59476f1e5 --- /dev/null +++ b/go/internal/state/pvperf.go @@ -0,0 +1,138 @@ +package state + +import ( + "database/sql" + "time" +) + +// ---- Irradiance history (cache.db) ---- + +// IrradianceRow is one hour of historical horizontal irradiance (W/m²). +// DHIWm2 is nil when the source did not provide a diffuse component. +type IrradianceRow struct { + SlotTsMs int64 `json:"slot_ts_ms"` + GHIWm2 float64 `json:"ghi_wm2"` + DHIWm2 *float64 `json:"dhi_wm2,omitempty"` + Source string `json:"source"` + FetchedAtMs int64 `json:"fetched_at_ms"` +} + +// SaveIrradiance upserts a batch of historical-irradiance rows (keyed by +// slot_ts_ms). Re-fetching a window overwrites the existing rows. +func (s *Store) SaveIrradiance(rows []IrradianceRow) error { + if len(rows) == 0 { + return nil + } + tx, err := s.cache.Begin() + if err != nil { + return err + } + defer tx.Rollback() + stmt, err := tx.Prepare(`INSERT INTO irradiance_history + (slot_ts_ms, ghi_wm2, dhi_wm2, source, fetched_at_ms) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT (slot_ts_ms) DO UPDATE SET + ghi_wm2 = excluded.ghi_wm2, + dhi_wm2 = excluded.dhi_wm2, + source = excluded.source, + fetched_at_ms = excluded.fetched_at_ms`) + if err != nil { + return err + } + defer stmt.Close() + for _, r := range rows { + if _, err := stmt.Exec(r.SlotTsMs, r.GHIWm2, r.DHIWm2, r.Source, r.FetchedAtMs); err != nil { + return err + } + } + return tx.Commit() +} + +// LoadIrradiance returns irradiance rows in [sinceMs, untilMs], ascending. +func (s *Store) LoadIrradiance(sinceMs, untilMs int64) ([]IrradianceRow, error) { + rows, err := s.cache.Query(`SELECT slot_ts_ms, ghi_wm2, dhi_wm2, source, fetched_at_ms + FROM irradiance_history + WHERE slot_ts_ms BETWEEN ? AND ? + ORDER BY slot_ts_ms ASC`, sinceMs, untilMs) + if err != nil { + return nil, err + } + defer rows.Close() + out := []IrradianceRow{} + for rows.Next() { + var r IrradianceRow + if err := rows.Scan(&r.SlotTsMs, &r.GHIWm2, &r.DHIWm2, &r.Source, &r.FetchedAtMs); err != nil { + return out, err + } + out = append(out, r) + } + return out, rows.Err() +} + +// ---- PV performance daily (state.db) ---- + +// PVPerformanceDay is one day's PV performance score: expected DC energy under +// measured irradiance versus the site's actual generation, plus their ratio. +// PR is nil when expected production was below a meaningful floor (n/a). +type PVPerformanceDay struct { + Day string `json:"day"` // YYYY-MM-DD, local date + ExpectedWh float64 `json:"expected_wh"` + ActualWh float64 `json:"actual_wh"` + PR *float64 `json:"pr,omitempty"` + StrangDataDateMs *int64 `json:"strang_data_date_ms,omitempty"` + ComputedAtMs int64 `json:"computed_at_ms"` +} + +// SavePVPerformance upserts one day's PV performance score (keyed by day). +func (s *Store) SavePVPerformance(p PVPerformanceDay) error { + const q = ` + INSERT INTO pv_performance_daily( + day, expected_wh, actual_wh, pr, strang_data_date_ms, computed_at_ms + ) VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(day) DO UPDATE SET + expected_wh = excluded.expected_wh, + actual_wh = excluded.actual_wh, + pr = excluded.pr, + strang_data_date_ms = excluded.strang_data_date_ms, + computed_at_ms = excluded.computed_at_ms + ` + _, err := s.db.Exec(q, p.Day, p.ExpectedWh, p.ActualWh, p.PR, p.StrangDataDateMs, time.Now().UnixMilli()) + return err +} + +// LoadPVPerformanceDay returns one day's score, or ok=false on a cache miss. +func (s *Store) LoadPVPerformanceDay(day string) (PVPerformanceDay, bool, error) { + const q = `SELECT day, expected_wh, actual_wh, pr, strang_data_date_ms, computed_at_ms + FROM pv_performance_daily WHERE day = ?` + var p PVPerformanceDay + err := s.db.QueryRow(q, day).Scan(&p.Day, &p.ExpectedWh, &p.ActualWh, &p.PR, &p.StrangDataDateMs, &p.ComputedAtMs) + if err == sql.ErrNoRows { + return PVPerformanceDay{}, false, nil + } + if err != nil { + return PVPerformanceDay{}, false, err + } + return p, true, nil +} + +// LoadPVPerformance returns scored days in [sinceDay, untilDay] (inclusive, +// YYYY-MM-DD string compare), ascending by day. +func (s *Store) LoadPVPerformance(sinceDay, untilDay string) ([]PVPerformanceDay, error) { + rows, err := s.db.Query(`SELECT day, expected_wh, actual_wh, pr, strang_data_date_ms, computed_at_ms + FROM pv_performance_daily + WHERE day BETWEEN ? AND ? + ORDER BY day ASC`, sinceDay, untilDay) + if err != nil { + return nil, err + } + defer rows.Close() + out := []PVPerformanceDay{} + for rows.Next() { + var p PVPerformanceDay + if err := rows.Scan(&p.Day, &p.ExpectedWh, &p.ActualWh, &p.PR, &p.StrangDataDateMs, &p.ComputedAtMs); err != nil { + return out, err + } + out = append(out, p) + } + return out, rows.Err() +} diff --git a/go/internal/state/pvperf_test.go b/go/internal/state/pvperf_test.go new file mode 100644 index 000000000..ef1becad6 --- /dev/null +++ b/go/internal/state/pvperf_test.go @@ -0,0 +1,119 @@ +package state + +import ( + "testing" +) + +func f64(v float64) *float64 { return &v } +func i64(v int64) *int64 { return &v } + +func TestSaveIrradianceRoundtrip(t *testing.T) { + s := freshStore(t) + rows := []IrradianceRow{ + {SlotTsMs: 1000, GHIWm2: 300, DHIWm2: f64(80), Source: "strang", FetchedAtMs: 5000}, + {SlotTsMs: 2000, GHIWm2: 450, DHIWm2: nil, Source: "strang", FetchedAtMs: 5000}, + } + if err := s.SaveIrradiance(rows); err != nil { + t.Fatal(err) + } + got, err := s.LoadIrradiance(0, 10000) + if err != nil { + t.Fatal(err) + } + if len(got) != 2 { + t.Fatalf("want 2 rows, got %d", len(got)) + } + if got[0].SlotTsMs != 1000 || got[0].GHIWm2 != 300 || got[0].DHIWm2 == nil || *got[0].DHIWm2 != 80 { + t.Errorf("row0 mismatch: %+v", got[0]) + } + if got[1].DHIWm2 != nil { + t.Errorf("row1 diffuse should be nil, got %v", *got[1].DHIWm2) + } + + // Upsert overwrites, no duplicate. + if err := s.SaveIrradiance([]IrradianceRow{{SlotTsMs: 1000, GHIWm2: 999, Source: "strang", FetchedAtMs: 6000}}); err != nil { + t.Fatal(err) + } + got, _ = s.LoadIrradiance(0, 10000) + if len(got) != 2 { + t.Fatalf("upsert should not add a row, got %d", len(got)) + } + if got[0].GHIWm2 != 999 { + t.Errorf("upsert should overwrite ghi, got %.0f", got[0].GHIWm2) + } +} + +func TestLoadIrradianceRangeFilters(t *testing.T) { + s := freshStore(t) + _ = s.SaveIrradiance([]IrradianceRow{ + {SlotTsMs: 100, GHIWm2: 1, Source: "strang", FetchedAtMs: 1}, + {SlotTsMs: 200, GHIWm2: 2, Source: "strang", FetchedAtMs: 1}, + {SlotTsMs: 300, GHIWm2: 3, Source: "strang", FetchedAtMs: 1}, + }) + got, err := s.LoadIrradiance(150, 250) + if err != nil { + t.Fatal(err) + } + if len(got) != 1 || got[0].SlotTsMs != 200 { + t.Fatalf("range filter failed: %+v", got) + } +} + +func TestPVPerformanceRoundtrip(t *testing.T) { + s := freshStore(t) + day := "2026-06-21" + p := PVPerformanceDay{ + Day: day, + ExpectedWh: 12000, + ActualWh: 10800, + PR: f64(0.9), + StrangDataDateMs: i64(1719000000000), + } + if err := s.SavePVPerformance(p); err != nil { + t.Fatal(err) + } + got, ok, err := s.LoadPVPerformanceDay(day) + if err != nil || !ok { + t.Fatalf("load ok=%v err=%v", ok, err) + } + if got.ExpectedWh != 12000 || got.ActualWh != 10800 || got.PR == nil || *got.PR != 0.9 { + t.Errorf("mismatch: %+v", got) + } + if got.StrangDataDateMs == nil || *got.StrangDataDateMs != 1719000000000 { + t.Errorf("provenance mismatch: %+v", got.StrangDataDateMs) + } + if got.ComputedAtMs == 0 { + t.Error("computed_at_ms should be stamped") + } + + // Upsert with n/a PR (nil) overwrites. + p.PR = nil + p.ExpectedWh = 50 + if err := s.SavePVPerformance(p); err != nil { + t.Fatal(err) + } + got, _, _ = s.LoadPVPerformanceDay(day) + if got.PR != nil { + t.Errorf("PR should be nil after upsert, got %v", *got.PR) + } + if got.ExpectedWh != 50 { + t.Errorf("expected_wh should overwrite, got %.0f", got.ExpectedWh) + } +} + +func TestLoadPVPerformanceMissAndRange(t *testing.T) { + s := freshStore(t) + if _, ok, err := s.LoadPVPerformanceDay("2000-01-01"); ok || err != nil { + t.Fatalf("miss should be ok=false err=nil, got ok=%v err=%v", ok, err) + } + for _, d := range []string{"2026-06-19", "2026-06-20", "2026-06-21"} { + _ = s.SavePVPerformance(PVPerformanceDay{Day: d, ExpectedWh: 1000, ActualWh: 900, PR: f64(0.9)}) + } + got, err := s.LoadPVPerformance("2026-06-20", "2026-06-21") + if err != nil { + t.Fatal(err) + } + if len(got) != 2 || got[0].Day != "2026-06-20" || got[1].Day != "2026-06-21" { + t.Fatalf("range/order wrong: %+v", got) + } +} diff --git a/go/internal/state/store.go b/go/internal/state/store.go index ef67150c5..c46b59947 100644 --- a/go/internal/state/store.go +++ b/go/internal/state/store.go @@ -991,6 +991,22 @@ func (s *Store) migrate() error { ts_ms INTEGER NOT NULL, PRIMARY KEY(asset_id, flow, cursor_kind) ) WITHOUT ROWID, STRICT`, + // Persistent per-day PV performance score: the DC energy the + // configured arrays should have produced under measured STRÅNG + // irradiance (expected_wh) versus what the site actually generated + // (actual_wh, from history), and their ratio. Precious like + // energy_daily — closed days are immutable, so a computed score is + // cached here forever and never recomputed. pr is null when expected + // production was below a meaningful floor (polar-night / near-dark). + // strang_data_date_ms records the STRÅNG fetch time for provenance. + `CREATE TABLE IF NOT EXISTS pv_performance_daily ( + day TEXT PRIMARY KEY, + expected_wh REAL NOT NULL, + actual_wh REAL NOT NULL, + pr REAL, + strang_data_date_ms INTEGER, + computed_at_ms INTEGER NOT NULL + ) STRICT`, } for _, stmt := range stmts { if _, err := s.db.Exec(stmt); err != nil { @@ -1039,6 +1055,18 @@ func (s *Store) migrate() error { source TEXT NOT NULL, fetched_at_ms INTEGER NOT NULL )`, + // Historical solar irradiance — one row per hour. Backfilled from + // SMHI STRÅNG (an analysis product, ~1-day lag) to score realised PV + // performance against a weather-expected baseline. Disposable and + // re-fetchable, so it lives in cache.db alongside forecasts. dhi_wm2 + // (diffuse) is null when the source doesn't provide it. + `CREATE TABLE IF NOT EXISTS irradiance_history ( + slot_ts_ms INTEGER PRIMARY KEY, + ghi_wm2 REAL NOT NULL, + dhi_wm2 REAL, + source TEXT NOT NULL, + fetched_at_ms INTEGER NOT NULL + )`, } for _, stmt := range cacheStmts { if _, err := s.cache.Exec(stmt); err != nil { diff --git a/go/internal/strang/strang.go b/go/internal/strang/strang.go new file mode 100644 index 000000000..579d5136c --- /dev/null +++ b/go/internal/strang/strang.go @@ -0,0 +1,232 @@ +// Package strang fetches historical solar irradiance from SMHI's STRÅNG +// mesoscale model (https://strang.smhi.se/). +// +// STRÅNG is an analysis/reanalysis product: it covers the Nordic region hourly +// at ~2.5 km resolution from 1999 to ~1 day ago. It has NO forward horizon, so +// it is used here for historical PV-performance scoring and model calibration, +// never as a forward forecast provider (those stay in the forecast package). +// +// Data is free and licensed CC BY 4.0 — attribution to SMHI required. No API +// key. The public point time-series endpoint is: +// +// {base}/geotype/point/lon/{lon}/lat/{lat}/parameter/{p}/data.json?from=&to=&interval=hourly +package strang + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "sort" + "time" + + "github.com/srcfl/ftw/go/internal/coverage" + "github.com/srcfl/ftw/go/internal/sunpos" +) + +// ErrOutsideDomain is returned when a request falls outside STRÅNG's Nordic +// model domain. Callers should treat it as "this site can never be scored by +// STRÅNG" and stop asking, rather than as a transient failure to retry. +var ErrOutsideDomain = errors.New("outside STRÅNG domain") + +// STRÅNG parameter codes (category "strang1g", version 1). +// +// These are the complete set — probing 100..130 against the live API on +// 2026-07-31 returned 200 for exactly 116..122 and 404 for everything else. +// Names were confirmed by their magnitudes on a clear day at Stockholm rather +// than from documentation, since SMHI's apidocs pages currently 404: parameter +// 119 caps at exactly 60 (minutes in an hour), 118 exceeds 117 the way direct +// *normal* irradiance exceeds global, and at solar noon 121 + 122 = 723.0 + +// 87.5 = 810.5, which is exactly 117 — the direct-plus-diffuse identity that +// tells us 121 is direct *horizontal* and not something else. +// +// Note what is absent: STRÅNG models radiation only and publishes no cloud +// cover. See CloudCover for how cloudiness is recovered from parameter 119. +const ( + ParamCIEUV = 116 // CIE-weighted UV irradiance, mW/m² + ParamGlobalIrradiance = 117 // Global (horizontal) irradiance, W/m² — GHI + ParamDirectNormal = 118 // Direct normal irradiance, W/m² — DNI + ParamSunshineDuration = 119 // Sunshine duration within the hour, minutes 0..60 + ParamPAR = 120 // Photosynthetically active radiation, W/m² + ParamDirectHorizontal = 121 // Direct horizontal irradiance, W/m² + ParamDiffuseIrradiance = 122 // Diffuse (horizontal) irradiance, W/m² — DHI +) + +// minutesPerHour is the full-sun value of ParamSunshineDuration. +const minutesPerHour = 60.0 + +// DefaultBaseURL is SMHI's open-data meteorological-analysis host for STRÅNG. +const DefaultBaseURL = "https://opendata-download-metanalys.smhi.se/api/category/strang1g/version/1" + +// IrradianceHour is one hour of horizontal irradiance at a point. DHIWm2 is nil +// when the diffuse component is unavailable (e.g. windows before 2017-04-18, or +// a transient diffuse-parameter error) — callers then estimate the split. +// SunshineMin is nil on the same terms and carries parameter 119. +type IrradianceHour struct { + HourStart time.Time + GHIWm2 float64 + DHIWm2 *float64 + SunshineMin *float64 +} + +// CloudCover derives a cloud-cover fraction (0 = clear, 1 = overcast) for the +// hour at (lat, lon), and reports whether it could be derived at all. +// +// STRÅNG publishes no cloud-cover parameter, but parameter 119 is sunshine +// duration: the number of minutes in the hour during which direct beam +// irradiance exceeded the WMO sunshine threshold (120 W/m²). One minus that +// fraction is the share of the hour the sun spent obscured, which is what +// "cloud cover" means for a solar model's purposes. +// +// This is an *observed* quantity, not an inference from a cloud field, so it is +// better grounded than a forecast provider's cloud percentage. It is coarser in +// one direction: it cannot see thin cirrus that dims without blocking. +// +// The location is required because sunshine duration is zero at night for the +// trivial reason that there is no sun — reading that as "100% overcast" would be +// confidently wrong every single night. When the sun is below the horizon for +// the whole hour this returns not-ok, and callers must treat that as unknown +// rather than as clear or as overcast. +func (h IrradianceHour) CloudCover(lat, lon float64) (float64, bool) { + if h.SunshineMin == nil { + return 0, false + } + if !h.daylight(lat, lon) { + return 0, false + } + m := *h.SunshineMin + if m < 0 { + return 0, false + } + if m > minutesPerHour { + m = minutesPerHour + } + return 1 - m/minutesPerHour, true +} + +// minSunElevationDeg is how high the sun must get during the hour before a +// sunshine-duration reading says anything about cloud. +// +// The WMO sunshine threshold is 120 W/m² of direct beam. Near the horizon the +// beam crosses roughly ten or more air masses and cannot reach that threshold +// even under a spotless sky, so a zero reading there is a statement about +// geometry, not about cloud. Five degrees is where the beam can plausibly clear +// the threshold; below it we decline to answer rather than report a twilight +// hour as fully overcast. +const minSunElevationDeg = 5.0 + +// daylight reports whether the sun climbs above minSunElevationDeg at any point +// in the hour. Sampling start, middle and end catches the sunrise and sunset +// hours, where the midpoint alone would misclassify half the hour. +func (h IrradianceHour) daylight(lat, lon float64) bool { + for _, off := range []time.Duration{0, 30 * time.Minute, 59 * time.Minute} { + if sunpos.At(h.HourStart.Add(off), lat, lon).ZenithDeg < 90-minSunElevationDeg { + return true + } + } + return false +} + +// Client is a thin STRÅNG point-series HTTP client. +type Client struct { + HTTP *http.Client + BaseURL string + UserAgent string +} + +// NewClient returns a Client with sane defaults. A descriptive User-Agent is +// required by SMHI's fair-use policy, mirroring the forecast providers. +func NewClient(userAgent string) *Client { + if userAgent == "" { + userAgent = "FTW github.com/srcfl/ftw" + } + return &Client{ + HTTP: &http.Client{Timeout: 30 * time.Second}, + BaseURL: DefaultBaseURL, + UserAgent: userAgent, + } +} + +// FetchWindow returns hourly irradiance for [start, end] (dates, UTC) at +// (lat, lon). Global irradiance is required; diffuse is best-effort — a diffuse +// error (common for pre-2017 windows) leaves DHIWm2 nil rather than failing the +// whole window. Rows are returned ascending by hour. +func (c *Client) FetchWindow(ctx context.Context, lat, lon float64, start, end time.Time) ([]IrradianceHour, error) { + if !coverage.Covers("strang", lat, lon) { + return nil, fmt.Errorf("strang: %w: (%.4f, %.4f) is outside the Nordic model domain", ErrOutsideDomain, lat, lon) + } + ghi, err := c.fetchParam(ctx, lat, lon, ParamGlobalIrradiance, start, end) + if err != nil { + return nil, fmt.Errorf("strang: global irradiance: %w", err) + } + // Best-effort extras: never fail the window because a secondary parameter + // errored. Both leave their field nil and callers fall back — the diffuse + // split is estimated, and cloud cover simply reports unknown. + dhi, _ := c.fetchParam(ctx, lat, lon, ParamDiffuseIrradiance, start, end) + sun, _ := c.fetchParam(ctx, lat, lon, ParamSunshineDuration, start, end) + + hours := make([]int64, 0, len(ghi)) + for ms := range ghi { + hours = append(hours, ms) + } + sort.Slice(hours, func(i, j int) bool { return hours[i] < hours[j] }) + + out := make([]IrradianceHour, 0, len(hours)) + for _, ms := range hours { + h := IrradianceHour{HourStart: time.UnixMilli(ms).UTC(), GHIWm2: ghi[ms]} + if d, ok := dhi[ms]; ok { + dv := d + h.DHIWm2 = &dv + } + if s, ok := sun[ms]; ok { + sv := s + h.SunshineMin = &sv + } + out = append(out, h) + } + return out, nil +} + +// fetchParam returns hour-start-ms → value for one STRÅNG parameter. +func (c *Client) fetchParam(ctx context.Context, lat, lon float64, param int, start, end time.Time) (map[int64]float64, error) { + url := fmt.Sprintf("%s/geotype/point/lon/%.4f/lat/%.4f/parameter/%d/data.json?from=%s&to=%s&interval=hourly", + c.BaseURL, lon, lat, param, + start.UTC().Format("2006-01-02"), end.UTC().Format("2006-01-02")) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, err + } + req.Header.Set("User-Agent", c.UserAgent) + req.Header.Set("Accept", "application/json") + resp, err := c.HTTP.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + body, _ := io.ReadAll(io.LimitReader(resp.Body, 512)) + return nil, fmt.Errorf("status %d: %s", resp.StatusCode, string(body)) + } + // STRÅNG data.json is an array of {date_time, value}. + var doc []struct { + DateTime string `json:"date_time"` + Value *float64 `json:"value"` + } + if err := json.NewDecoder(resp.Body).Decode(&doc); err != nil { + return nil, fmt.Errorf("decode: %w", err) + } + out := make(map[int64]float64, len(doc)) + for _, d := range doc { + if d.Value == nil { + continue + } + t, err := time.Parse(time.RFC3339, d.DateTime) + if err != nil { + continue + } + out[t.UTC().Truncate(time.Hour).UnixMilli()] = *d.Value + } + return out, nil +} diff --git a/go/internal/strang/strang_test.go b/go/internal/strang/strang_test.go new file mode 100644 index 000000000..1596fbff6 --- /dev/null +++ b/go/internal/strang/strang_test.go @@ -0,0 +1,306 @@ +package strang + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/srcfl/ftw/go/internal/sunpos" +) + +func TestFetchWindowMergesGHIAndDHI(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var arr []map[string]any + switch { + case strings.Contains(r.URL.Path, "/parameter/117/"): + arr = []map[string]any{ + {"date_time": "2024-06-01T10:00:00Z", "value": 500.0}, + {"date_time": "2024-06-01T11:00:00Z", "value": 650.0}, + } + case strings.Contains(r.URL.Path, "/parameter/122/"): + arr = []map[string]any{ + {"date_time": "2024-06-01T10:00:00Z", "value": 120.0}, + {"date_time": "2024-06-01T11:00:00Z", "value": 150.0}, + } + } + _ = json.NewEncoder(w).Encode(arr) + }) + srv := httptest.NewServer(handler) + defer srv.Close() + + c := NewClient("test") + c.BaseURL = srv.URL + hours, err := c.FetchWindow(context.Background(), 59.33, 18.07, + time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC), + time.Date(2024, 6, 2, 0, 0, 0, 0, time.UTC)) + if err != nil { + t.Fatal(err) + } + if len(hours) != 2 { + t.Fatalf("got %d hours, want 2", len(hours)) + } + if hours[0].GHIWm2 != 500 || hours[1].GHIWm2 != 650 { + t.Errorf("GHI mismatch: %+v", hours) + } + if hours[0].DHIWm2 == nil || *hours[0].DHIWm2 != 120 { + t.Errorf("DHI[0] mismatch: %+v", hours[0]) + } + if !hours[0].HourStart.Before(hours[1].HourStart) { + t.Error("hours should be ascending") + } +} + +// Diffuse (122) unavailable — common for pre-2017 windows — must not fail the +// window; GHI still returns with nil DHI. +func TestFetchWindowDiffuseErrorTolerated(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.URL.Path, "/parameter/122/") { + w.WriteHeader(500) + return + } + _ = json.NewEncoder(w).Encode([]map[string]any{ + {"date_time": "2016-06-01T10:00:00Z", "value": 480.0}, + }) + }) + srv := httptest.NewServer(handler) + defer srv.Close() + + c := NewClient("test") + c.BaseURL = srv.URL + hours, err := c.FetchWindow(context.Background(), 59, 18, + time.Date(2016, 6, 1, 0, 0, 0, 0, time.UTC), + time.Date(2016, 6, 2, 0, 0, 0, 0, time.UTC)) + if err != nil { + t.Fatalf("diffuse error should not fail window: %v", err) + } + if len(hours) != 1 || hours[0].DHIWm2 != nil { + t.Errorf("expected 1 hour with nil DHI, got %+v", hours) + } +} + +// Global (117) error must fail the window — GHI is required. +func TestFetchWindowGlobalErrorFails(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(500) + })) + defer srv.Close() + + c := NewClient("t") + c.BaseURL = srv.URL + _, err := c.FetchWindow(context.Background(), 59, 18, + time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC)) + if err == nil { + t.Error("global irradiance error should fail the window") + } +} + +// Null values in the series are skipped, not decoded as 0. +func TestFetchWindowSkipsNullValues(t *testing.T) { + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.Contains(r.URL.Path, "/parameter/122/") { + _ = json.NewEncoder(w).Encode([]map[string]any{}) + return + } + _ = json.NewEncoder(w).Encode([]map[string]any{ + {"date_time": "2024-06-01T10:00:00Z", "value": 500.0}, + {"date_time": "2024-06-01T11:00:00Z", "value": nil}, + }) + }) + srv := httptest.NewServer(handler) + defer srv.Close() + + c := NewClient("t") + c.BaseURL = srv.URL + hours, err := c.FetchWindow(context.Background(), 59, 18, + time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC), + time.Date(2024, 6, 2, 0, 0, 0, 0, time.UTC)) + if err != nil { + t.Fatal(err) + } + if len(hours) != 1 || hours[0].GHIWm2 != 500 { + t.Errorf("null value should be skipped, got %+v", hours) + } +} + +// --- cloud cover derived from sunshine duration (parameter 119) --- + +func minutesPtr(v float64) *float64 { return &v } + +const ( + sthlmLat = 59.33 + sthlmLon = 18.07 +) + +// Midsummer noon and midnight at Stockholm: unambiguously day and night. +var ( + noonUTC = time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC) + midnightUTC = time.Date(2026, 12, 21, 23, 0, 0, 0, time.UTC) +) + +func TestCloudCoverFromSunshineDuration(t *testing.T) { + cases := []struct { + name string + minutes *float64 + want float64 + wantOK bool + }{ + {"full hour of sun is clear sky", minutesPtr(60), 0, true}, + {"no sun at all is overcast", minutesPtr(0), 1, true}, + {"half an hour is half cover", minutesPtr(30), 0.5, true}, + {"quarter hour is three quarters cover", minutesPtr(15), 0.75, true}, + {"missing parameter is unknown, not clear", nil, 0, false}, + {"negative is rejected as unknown", minutesPtr(-1), 0, false}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + h := IrradianceHour{HourStart: noonUTC, SunshineMin: c.minutes} + got, ok := h.CloudCover(sthlmLat, sthlmLon) + if ok != c.wantOK { + t.Fatalf("ok = %v, want %v", ok, c.wantOK) + } + if ok && got != c.want { + t.Errorf("cover = %v, want %v", got, c.want) + } + }) + } +} + +// A value above 60 would push cover negative and read as "brighter than clear", +// which is meaningless. Clamp instead. +func TestCloudCoverClampsAboveFullHour(t *testing.T) { + h := IrradianceHour{HourStart: noonUTC, SunshineMin: minutesPtr(75)} + got, ok := h.CloudCover(sthlmLat, sthlmLon) + if !ok { + t.Fatal("want derivable") + } + if got != 0 { + t.Errorf("cover = %v, want 0 (clamped)", got) + } +} + +// The distinction that matters at a call site: unknown must never be mistaken +// for clear, because they lead to opposite decisions. +func TestCloudCoverUnknownIsDistinguishableFromClear(t *testing.T) { + unknown, okU := IrradianceHour{HourStart: noonUTC}.CloudCover(sthlmLat, sthlmLon) + clear, okC := IrradianceHour{HourStart: noonUTC, SunshineMin: minutesPtr(60)}.CloudCover(sthlmLat, sthlmLon) + if okU { + t.Error("absent sunshine must report not-ok") + } + if !okC { + t.Error("full sun must report ok") + } + if unknown != clear { + t.Log("values differ, but callers must branch on the boolean, not the value") + } +} + +// Outside the Nordic domain STRÅNG can never return data, so the client must +// refuse locally rather than spend three HTTP requests learning that. +func TestFetchWindowRefusesOutsideDomain(t *testing.T) { + c := NewClient("test") + c.BaseURL = "http://127.0.0.1:1" // must never be dialled + _, err := c.FetchWindow(context.Background(), -33.87, 151.21, + time.Date(2026, 6, 21, 0, 0, 0, 0, time.UTC), + time.Date(2026, 6, 22, 0, 0, 0, 0, time.UTC)) + if err == nil { + t.Fatal("want an error for Sydney") + } + if !errors.Is(err, ErrOutsideDomain) { + t.Errorf("err = %v, want ErrOutsideDomain", err) + } +} + +func TestFetchWindowAcceptsInsideDomain(t *testing.T) { + // Stockholm is in-domain, so this must get past the guard and fail on the + // unreachable transport instead. + c := NewClient("test") + c.BaseURL = "http://127.0.0.1:1" + _, err := c.FetchWindow(context.Background(), 59.33, 18.07, + time.Date(2026, 6, 21, 0, 0, 0, 0, time.UTC), + time.Date(2026, 6, 22, 0, 0, 0, 0, time.UTC)) + if errors.Is(err, ErrOutsideDomain) { + t.Fatal("Stockholm must not be rejected as outside the domain") + } +} + +// Sunshine duration is zero at night because there is no sun, not because it is +// overcast. Reporting 100% cover would be confidently wrong every single night, +// which is exactly what the live API returned before this guard existed. +func TestCloudCoverIsUnknownAtNight(t *testing.T) { + h := IrradianceHour{HourStart: midnightUTC, SunshineMin: minutesPtr(0)} + if _, ok := h.CloudCover(sthlmLat, sthlmLon); ok { + t.Error("midwinter midnight must report unknown, not 100% cloud") + } +} + +// The hour the sun climbs through the threshold must be answerable, and it is +// only answerable because all three sample points are checked. Midsummer at +// Stockholm, 02:00Z: elevation runs 1.59 deg at :00 and 4.34 deg at :30 — both +// below the 5 deg cutoff — reaching 7.25 deg by :59. Sampling the start or the +// midpoint alone would discard a genuinely observed half-hour of sunshine. +func TestCloudCoverCountsHourWhereSunCrossesThreshold(t *testing.T) { + start := time.Date(2026, 6, 21, 2, 0, 0, 0, time.UTC) + if sunpos.At(start, sthlmLat, sthlmLon).ZenithDeg < 90-minSunElevationDeg { + t.Fatal("premise broken: the sun should start this hour below the cutoff") + } + h := IrradianceHour{HourStart: start, SunshineMin: minutesPtr(30)} + got, ok := h.CloudCover(sthlmLat, sthlmLon) + if !ok { + t.Fatal("the hour the sun crosses the cutoff should be derivable") + } + if got != 0.5 { + t.Errorf("cover = %v, want 0.5", got) + } +} + +// Polar night: the sun never rises, so no hour of the day is derivable. +func TestCloudCoverUnknownThroughPolarNight(t *testing.T) { + const tromsoLat, tromsoLon = 69.65, 18.96 + for hour := 0; hour < 24; hour++ { + h := IrradianceHour{ + HourStart: time.Date(2026, 12, 21, hour, 0, 0, 0, time.UTC), + SunshineMin: minutesPtr(0), + } + if _, ok := h.CloudCover(tromsoLat, tromsoLon); ok { + t.Errorf("hour %02d: polar night must report unknown", hour) + } + } +} + +// Near sunrise/sunset the beam crosses too much atmosphere to clear the WMO +// threshold even under a clear sky, so a zero reading there describes geometry +// rather than cloud. Verified against the live API: 2026-06-21 20:00Z at +// Stockholm has GHI 2.5 W/m2 and 0 minutes of sunshine — the sun is minutes +// from setting, and calling that "100% overcast" would be wrong. +func TestCloudCoverDeclinesNearTheHorizon(t *testing.T) { + h := IrradianceHour{ + HourStart: time.Date(2026, 6, 21, 20, 0, 0, 0, time.UTC), + SunshineMin: minutesPtr(0), + } + if _, ok := h.CloudCover(sthlmLat, sthlmLon); ok { + t.Error("a sun about to set must report unknown, not fully overcast") + } +} + +// ...but a genuinely low-yet-usable sun must still be answerable, otherwise the +// guard would silently discard most of a Nordic winter. +func TestCloudCoverStillAnswersWhenSunIsUsablyUp(t *testing.T) { + // 2026-06-21 04:00Z at Stockholm: live GHI 163.2 W/m2, 60 min sunshine. + h := IrradianceHour{ + HourStart: time.Date(2026, 6, 21, 4, 0, 0, 0, time.UTC), + SunshineMin: minutesPtr(60), + } + got, ok := h.CloudCover(sthlmLat, sthlmLon) + if !ok { + t.Fatal("a usable morning sun must be derivable") + } + if got != 0 { + t.Errorf("cover = %v, want 0 (full sunshine)", got) + } +} diff --git a/go/internal/units/consistency_test.go b/go/internal/units/consistency_test.go index 54ce42874..053c1e6b5 100644 --- a/go/internal/units/consistency_test.go +++ b/go/internal/units/consistency_test.go @@ -10,6 +10,7 @@ import ( "github.com/srcfl/ftw/go/internal/forecast" "github.com/srcfl/ftw/go/internal/loadpoint" "github.com/srcfl/ftw/go/internal/mpc" + "github.com/srcfl/ftw/go/internal/pvperf" "github.com/srcfl/ftw/go/internal/telemetry" "github.com/srcfl/ftw/go/internal/units" "github.com/srcfl/ftw/go/internal/v2x" @@ -43,6 +44,16 @@ func TestForecastArrayHasNoKWp(t *testing.T) { } } +func TestPVPerfArrayHasNoKWp(t *testing.T) { + typ := reflect.TypeOf(pvperf.Array{}) + if _, ok := typ.FieldByName("KWp"); ok { + t.Fatal("pvperf.Array must not have KWp; store RatedW") + } + if _, ok := typ.FieldByName("RatedW"); !ok { + t.Fatal("pvperf.Array must store RatedW (watts)") + } +} + func TestMPCParamsSoCIsFraction(t *testing.T) { typ := reflect.TypeOf(mpc.Params{}) for _, banned := range []string{"SoCMinPct", "SoCMaxPct", "InitialSoCPct"} { @@ -243,6 +254,7 @@ func TestCoreBannedSoCPercentFieldNames(t *testing.T) { reflect.TypeOf(mpc.Plan{}), reflect.TypeOf(mpc.SlotDirective{}), reflect.TypeOf(forecast.Array{}), + reflect.TypeOf(pvperf.Array{}), } banned := []string{"CurrentSoCPct", "TargetSoCPct", "PluginSoCPct", "VehicleSoCPct", "SoCPct", "SoCMinPct", "SoCMaxPct", "SoCTargetPct", "LivePVSurplusSoCCapPct", "LoadpointSoCTargetPct", "KWp"} for _, typ := range types { diff --git a/web/components/ftw-bar-chart.js b/web/components/ftw-bar-chart.js index d7465a6ba..6f0144d8e 100644 --- a/web/components/ftw-bar-chart.js +++ b/web/components/ftw-bar-chart.js @@ -188,6 +188,26 @@ class FtwBarChart extends FtwElement { opacity: 0.85; pointer-events: none; } + /* Optional dashed overlay line (e.g. STRÅNG-expected PV vs the + produced bars). An SVG stretched to fill .bar-area, plotted in a + 0..100 viewBox so it needs no pixel math against the CSS grid; + non-scaling-stroke keeps the dash crisp despite the stretch. */ + .overlay-line { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + pointer-events: none; + overflow: visible; + } + .overlay-line polyline { + fill: none; + stroke: var(--ftw-overlay-color, var(--amber, #f59e0b)); + stroke-width: 1.5; + stroke-dasharray: 4 3; + vector-effect: non-scaling-stroke; + opacity: 0.9; + } `; static get observedAttributes() { @@ -197,6 +217,7 @@ class FtwBarChart extends FtwElement { constructor() { super(); this._data = []; + this._overlay = null; } attributeChangedCallback() { this.update(); } @@ -209,6 +230,16 @@ class FtwBarChart extends FtwElement { } get data() { return this._data; } + // Optional dashed overlay line, index-aligned with .data. Shape: + // { values: (number|null)[], color?: string } + // values[i] is plotted above column i on the SAME axis as the bars + // (nulls / non-finite entries break the line). Set null to remove. + set overlay(o) { + this._overlay = o && Array.isArray(o.values) ? o : null; + this.update(); + } + get overlay() { return this._overlay; } + render() { const accent = this.getAttribute("accent"); const height = this.getAttribute("chart-height"); @@ -262,6 +293,17 @@ class FtwBarChart extends FtwElement { } const avg = count > 0 ? sum / count : 0; + // Fold overlay values into the axis max so the expected line and the + // produced bars share one scale (an expected level above the tallest + // bar must still fit in-frame). + const overlayVals = this._overlay ? this._overlay.values : null; + if (overlayVals) { + for (const ov of overlayVals) { + const n = Number(ov); + if (isFinite(n) && n > max) max = n; + } + } + const colsSvg = this._data.map((d) => { const v = Number(d.value) || 0; // 2% floor keeps tiny-but-nonzero values visible; gate on v>0 so @@ -300,10 +342,34 @@ class FtwBarChart extends FtwElement { `title="average ${display}">`; } + // Dashed overlay line (expected series). Plotted in a 0..100 viewBox + // stretched to fill .bar-area: x centers each column, y is the value + // as a percentage of the shared max (inverted — SVG y grows downward). + let overlayLine = ""; + if (overlayVals && max > 0) { + const n = this._data.length; + const pts = []; + for (let i = 0; i < n; i++) { + const val = Number(overlayVals[i]); + if (!isFinite(val)) continue; + const x = n > 1 ? (i + 0.5) / n * 100 : 50; + const y = Math.max(0, Math.min(100, 100 - (val / max) * 100)); + pts.push(`${x.toFixed(2)},${y.toFixed(2)}`); + } + if (pts.length >= 2) { + const color = this._overlay.color; + if (color) this.style.setProperty("--ftw-overlay-color", color); + overlayLine = + ``; + } + } + return `
-
${colsSvg}${avgOverlay}
+
${colsSvg}${avgOverlay}${overlayLine}
${lblsSvg}
diff --git a/web/components/ftw-history-card.js b/web/components/ftw-history-card.js index ed659092e..1e1393c0c 100644 --- a/web/components/ftw-history-card.js +++ b/web/components/ftw-history-card.js @@ -28,7 +28,7 @@ import { FtwElement, ftwDebugDelay } from "./ftw-element.js"; import { apiFetch } from "./api-fetch.js"; -import "./ftw-bar-chart.js"; +import "./ftw-bar-chart.js?v=strang1"; const FIELD_BY_METRIC = { import: "import_wh", @@ -82,6 +82,36 @@ function fetchDailyEnergy(days) { return promise; } +// STRÅNG-based PV performance (expected-vs-actual). Only the "Produced" +// (metric="pv") tile fetches this, to overlay the weather-expected line. +// Tolerant: a disabled/absent service or any error resolves to +// { enabled:false } so the bars still render without an overlay. +const pvPerfFetchCache = new Map(); // days -> { at, data?, promise? } + +function fetchPVPerformance(days) { + const now = Date.now(); + const cached = pvPerfFetchCache.get(days); + if (cached && cached.data && now - cached.at < DAILY_CACHE_TTL_MS) { + return Promise.resolve(cached.data); + } + if (cached && cached.promise && now - cached.at < DAILY_CACHE_TTL_MS) { + return cached.promise; + } + const promise = apiFetch("/api/pv/performance?days=" + days) + .then((r) => (r.ok ? r.json() : { enabled: false })) + .then((resp) => { + const data = resp || { enabled: false }; + pvPerfFetchCache.set(days, { at: Date.now(), data }); + return data; + }) + .catch(() => { + pvPerfFetchCache.delete(days); + return { enabled: false }; + }); + pvPerfFetchCache.set(days, { at: now, promise }); + return promise; +} + class FtwHistoryCard extends FtwElement { static styles = ` :host { display: block; } @@ -205,6 +235,27 @@ class FtwHistoryCard extends FtwElement { margin-left: 6px; letter-spacing: 0; } + /* STRÅNG expected-vs-actual caption under the Produced chart. Hidden + until the pv tile has a performance overlay to describe. The dashed + swatch mirrors the overlay line so the legend reads at a glance. */ + .strang-note { + margin-top: 6px; + font-size: 0.72rem; + color: var(--fg-muted); + font-family: var(--mono); + display: flex; + align-items: center; + gap: 6px; + } + .strang-note[hidden] { display: none; } + .strang-note .swatch { + display: inline-block; + width: 16px; + height: 0; + border-top: 2px dashed #fcd34d; + flex: 0 0 auto; + } + .strang-note .pr { color: var(--fg-label); font-weight: 600; } @media (max-width: 900px) { .card-inner { padding: var(--card-pad-tight, 12px 14px); } } @@ -302,6 +353,7 @@ class FtwHistoryCard extends FtwElement {
— kWh
+ `; } @@ -309,6 +361,7 @@ class FtwHistoryCard extends FtwElement { afterRender() { this._chart = this.shadowRoot.querySelector('[data-role="chart"]'); this._totalEl = this.shadowRoot.querySelector('[data-role="total"]'); + this._strangEl = this.shadowRoot.querySelector('[data-role="strang"]'); this._toggleEl = this.shadowRoot.querySelector('.toggle'); if (this._chart) this._chart.setAttribute("accent", this._accent()); if (this._toggleEl) { @@ -387,6 +440,12 @@ class FtwHistoryCard extends FtwElement { this._totalEl.textContent = "— kWh"; } this._chart.data = data; + // Produced tile: overlay the STRÅNG expected line. Fetched + // separately so a disabled/slow scoring service never holds up + // the bars. Aligned to the same day buckets by ISO date. + if (metric === "pv") { + this._loadOverlay(days, buckets.map((b) => b.day), seq); + } }; // `?delay=N` — hold in the skeleton state for N ms after the // fetch resolves, for inspecting the loading→loaded transition. @@ -402,6 +461,58 @@ class FtwHistoryCard extends FtwElement { this._totalEl.textContent = "failed to load"; }); } + + // Fetch STRÅNG performance scores and overlay the expected-production + // line onto the Produced bars, aligned to `dayKeys` (ISO dates, same + // order as the bars). Hides the overlay + caption when scoring is + // unavailable. Guarded by `seq` so a stale response can't paint over a + // newer Week/Month selection. + _loadOverlay(days, dayKeys, seq) { + fetchPVPerformance(days) + .then((perf) => { + if (seq !== this._reqSeq || !this._chart) return; + if (!perf || perf.enabled === false || !Array.isArray(perf.items) || !perf.items.length) { + this._chart.overlay = null; + if (this._strangEl) this._strangEl.setAttribute("hidden", ""); + return; + } + const expByDay = new Map(); + for (const it of perf.items) { + if (it && it.day != null) expByDay.set(it.day, Number(it.expected_wh) || 0); + } + // kWh, index-aligned to the bars; null where no score exists so + // the dashed line breaks rather than dropping to zero. + const values = dayKeys.map((d) => { + const wh = expByDay.get(d); + return wh == null ? null : wh / 1000; + }); + const hasAny = values.some((v) => v != null); + this._chart.overlay = hasAny ? { values, color: "#fcd34d" } : null; + + if (this._strangEl) { + const pr = typeof perf.performance_ratio === "number" ? perf.performance_ratio : null; + const prTxt = pr != null ? `${Math.round(pr * 100)}% of expected` : ""; + // Only announce the calibration once it is actually being applied to + // the forward forecast — reporting a factor we ignore would mislead. + const cal = perf.calibration; + const calTxt = + cal && cal.applied && typeof cal.factor === "number" + ? `calibrating forecast ×${cal.factor.toFixed(2)}` + : ""; + this._strangEl.innerHTML = + ` expected (STRÅNG)` + + (prTxt ? " · " + prTxt : "") + + (calTxt ? " · " + calTxt : ""); + if (hasAny) this._strangEl.removeAttribute("hidden"); + else this._strangEl.setAttribute("hidden", ""); + } + }) + .catch(() => { + if (seq !== this._reqSeq || !this._chart) return; + this._chart.overlay = null; + if (this._strangEl) this._strangEl.setAttribute("hidden", ""); + }); + } } function fmtKwh(wh) { diff --git a/web/components/index.js b/web/components/index.js index 38c87bd81..b104323e4 100644 --- a/web/components/index.js +++ b/web/components/index.js @@ -23,8 +23,8 @@ import "./ftw-battery-control.js?v=apifetch1"; import "./ftw-pv-control.js?v=apifetch1"; import "./ftw-price-chart.js?v=zones1"; import "./ftw-energy-cake.js"; -import "./ftw-bar-chart.js"; -import "./ftw-history-card.js?v=apiread2"; +import "./ftw-bar-chart.js?v=strang1"; +import "./ftw-history-card.js?v=strang2"; import "./ftw-savings-card.js?v=zones1"; import "./ftw-update-check.js?v=apifetch1"; import "./ftw-notif-status.js?v=apifetch1"; diff --git a/web/index.html b/web/index.html index 1e46ac6d0..9b629af8f 100644 --- a/web/index.html +++ b/web/index.html @@ -5,10 +5,10 @@ FTW - + 0;)i[a++]=e[o++]}}return i}var HS=class extends LS{constructor(e,t,n,r,i,a,o,s){super(e,n,r,o??t.length),this.indexBuffer=t,this.symbolOffsetBuffer=i,this.symbolTableBuffer=a,this.sharedDictionaryCache=s}getValueFromBuffer(e){this.decodedDictionary??(this.decodedDictionary=this.sharedDictionaryCache?.decodedDictionary,this.decodedDictionary??(this.decodedDictionary=this.decodeDictionary(),this.sharedDictionaryCache&&(this.sharedDictionaryCache.decodedDictionary=this.decodedDictionary)));let t=this.indexBuffer[e],n=this.offsetBuffer[t],r=this.offsetBuffer[t+1];return Nx(this.decodedDictionary,n,r)}decodeDictionary(){return this.symbolLengthBuffer??=this.offsetToLengthBuffer(this.symbolOffsetBuffer),VS(this.symbolTableBuffer,this.symbolLengthBuffer,this.dataBuffer)}offsetToLengthBuffer(e){let t=new Uint32Array(e.length-1),n=e[0];for(let r=1;r1&&!c.nullable||l===1&&c.nullable)throw Error(`The number of streams for the child field ${c.name} does not match its nullability. nullibilty: ${c.nullable}, numStreams: ${l}`);let m;if(c.nullable){let n=Q(e,t);m=new Sx(Ex(e,n.numValues,n.byteLength,t),n.numValues)}let h=Ix(e,t,Q(e,t),void 0,m);if(s){if(!o)throw Error(`Incomplete shared FSST dictionary for column "${p}"`);u[f++]=new HS(p,h,i,a,o,s,m,d)}else u[f++]=new zS(p,h,i,a,m)}return u}var JS=class extends hy{constructor(e,t,n){super(e,new Uint8Array,n??t.length),this.values=t}getValueFromBuffer(e){return this.values[e]}},YS;(function(e){e[e.STRING=1]=`STRING`,e[e.INT32=2]=`INT32`,e[e.UINT32=4]=`UINT32`,e[e.INT64=8]=`INT64`,e[e.UINT64=16]=`UINT64`,e[e.FLOAT=32]=`FLOAT`,e[e.DOUBLE=64]=`DOUBLE`,e[e.PRESENCE=128]=`PRESENCE`})(YS||={});var XS;(function(e){e[e.FALSE=0]=`FALSE`,e[e.TRUE=1]=`TRUE`,e[e.START_MAP=2]=`START_MAP`,e[e.START_LIST=3]=`START_LIST`,e[e.COUNT=4]=`COUNT`})(XS||={});function ZS(e,t,n,r){let i=QS(n);if(r===0)return i.map(e=>new JS(e,[]));let a=$S(e,t,r),o=(a.presentStream?a.presentCount:a.lengthStream.length)/i.length,s=[],c=0,l=0;for(let e=0;ee.name+(t.name??``))}function $S(e,t,n){let r=e[t.get()];t.add(1);let i=Ix(e,t,Q(e,t)),a=n-1,o=[];r&YS.STRING&&(a-=eC(e,t,o)),a-=tC(e,t,r,o),a-=nC(e,t,r,o);let s,c=0;if(r&YS.PRESENCE){let n=rC(e,t);s=n.value,c=n.count,a--}let l=new Uint32Array;if(a>0&&(l=Ix(e,t,Q(e,t)),a--),a!==0)throw Error(`Unexpected number of remaining streams while decoding map column: ${a}`);return{lengthStream:i,dictionary:o,presentStream:s,presentCount:c,flattenedValues:l}}function eC(e,t,n){let r=e[t.get()];t.add(1);let i=US(``,e,t,r);if(i)for(let e=0;ea.length)throw Error(`Merged map counts underflow while decoding child streams`);let p=Array(n),m=r,h=i;for(let e=0;eo.length)throw Error(`Map value stream underflow while decoding feature payload`);let n=aC(o,h,t,c);p[e]=n.value,h=n.nextIndex}let g=0;for(let e=r;e=n)throw Error(`Unexpected end of map value stream`);let i=e[t];if(i===XS.FALSE)return{value:!1,nextIndex:t+1};if(i===XS.TRUE)return{value:!0,nextIndex:t+1};if(i===XS.START_MAP){let i=cC(e,t,n);return{value:oC(e,t+2,i,r).value,nextIndex:i}}if(i===XS.START_LIST){let i=cC(e,t,n),a=[],o=t+2;for(;o=n)throw Error(`Missing length for nested map/list payload`);let r=e[t+1];if(r<2)throw Error(`Invalid nested payload length: ${r}`);let i=t+r;if(i>n)throw Error(`Nested payload exceeds containing payload bounds`);return i}function lC(e,t){let n=e-XS.COUNT;if(n<0||n>=t.length)throw Error(`Scalar dictionary index out of range: ${e}`);return t[n]}function uC(e,t){for(let n of t)e.push(n)}function dC(e,t,n,r,i,a){return n.type===`scalarType`?a&&!a.has(n.name)?(Tx(r,e,t),null):fC(r,e,t,i,n.scalarType,n):n.complexType?.physicalType===wy.MAP?ZS(e,t,n,r):r===0?null:qS(e,t,n,a)}function fC(e,t,n,r,i,a){let o;if(e===0)return null;if(a.nullable){let e=Q(t,n),r=e.numValues,i=n.get(),a=Ex(t,r,e.byteLength,n);n.set(i+e.byteLength),o=new Sx(a,e.numValues)}let s=o??r;switch(i.physicalType){case Y.UINT_32:case Y.INT_32:return _C(t,n,a,i,s);case Y.STRING:{let r=a.nullable?e-1:e;return US(a.name,t,n,r,o)??null}case Y.BOOLEAN:return pC(t,n,a,r,s);case Y.UINT_64:case Y.INT_64:return gC(t,n,a,s,i);case Y.FLOAT:return mC(t,n,a,s);case Y.DOUBLE:return hC(t,n,a,s);default:throw Error(`The specified data type for the field is currently not supported: ${i}`)}}function pC(e,t,n,r,i){let a=Q(e,t),o=a.numValues,s=t.get(),c=vC(i)?i:void 0,l=Ex(e,o,a.byteLength,t,c);t.set(s+a.byteLength);let u=new Sx(l,o);return new PS(n.name,u,i)}function mC(e,t,n,r){let i=Q(e,t),a=vC(r)?r:void 0,o=Ox(e,t,i.numValues,a);return new FS(n.name,o,r)}function hC(e,t,n,r){let i=Q(e,t),a=vC(r)?r:void 0,o=kx(e,t,i.numValues,a);return new vy(n.name,o,r)}function gC(e,t,n,r,i){let a=Q(e,t),o=nS(a,r,e,t,`int64`),s=i.physicalType===Y.INT_64;if(o===$.FLAT){let i=vC(r)?r:void 0,o=s?Wx(e,t,a,i):Gx(e,t,a,i);return new aS(n.name,o,r)}if(o===$.SEQUENCE){let r=Ux(e,t,a);return new oS(n.name,r[0],r[1],a.numRleValues,s)}let c=s?Jx(e,t,a):Yx(e,t,a);return new IS(n.name,c,r,s)}function _C(e,t,n,r,i){let a=Q(e,t),o=nS(a,i,e,t),s=r.physicalType===Y.INT_32;if(o===$.FLAT){let r=vC(i)?i:void 0,o=s?Fx(e,t,a,void 0,r):Ix(e,t,a,void 0,r);return new _y(n.name,o,i)}if(o===$.SEQUENCE){let r=Hx(e,t,a);return new by(n.name,r[0],r[1],a.numRleValues,s)}let c=s?Bx(e,t,a):Vx(e,t,a);return new xy(n.name,c,i,s)}function vC(e){return e instanceof Sx}const yC={ID:0,ID_NULLABLE:1,ID_LONG:2,GEOMETRY:4,SCALAR_BASE:10,STRUCT:30,MAP:31};function bC(e){switch(e){case yC.ID:case yC.ID|yC.ID_NULLABLE:case yC.ID|yC.ID_LONG:case yC.ID|yC.ID_LONG|yC.ID_NULLABLE:return{nullable:(e&yC.ID_NULLABLE)!==0,columnScope:Cy.FEATURE,type:`scalarType`,scalarType:{longID:(e&yC.ID_LONG)!==0,type:`logicalType`,logicalType:Ty.ID}};case yC.GEOMETRY:return{nullable:!1,columnScope:Cy.FEATURE,type:`complexType`,complexType:{type:`physicalType`,physicalType:wy.GEOMETRY,children:[]}};case yC.STRUCT:return{nullable:!1,columnScope:Cy.FEATURE,type:`complexType`,complexType:{type:`physicalType`,physicalType:wy.STRUCT,children:[]}};case yC.MAP:return{nullable:!0,columnScope:Cy.FEATURE,type:`complexType`,complexType:{type:`physicalType`,physicalType:wy.MAP,children:[]}};default:return EC(e)}}function xC(e){return e>=yC.SCALAR_BASE}function SC(e){return e===yC.STRUCT||e===yC.MAP}function CC(e){if(e.type===`scalarType`){let t=e.scalarType;if(t.type===`physicalType`)switch(t.physicalType){case Y.BOOLEAN:case Y.INT_8:case Y.UINT_8:case Y.INT_32:case Y.UINT_32:case Y.INT_64:case Y.UINT_64:case Y.FLOAT:case Y.DOUBLE:return!1;case Y.STRING:return!0;default:return!1}if(t.type===`logicalType`)return!1}else if(e.type===`complexType`){let t=e.complexType;if(t.type===`physicalType`)switch(t.physicalType){case wy.GEOMETRY:case wy.STRUCT:case wy.MAP:return!0;default:return!1}}return console.warn(`Unexpected column type in hasStreamCount`,e),!1}function wC(e){return e.type===`scalarType`&&e.scalarType?.type===`logicalType`&&e.scalarType.logicalType===Ty.ID}function TC(e){return e.type===`complexType`&&e.complexType?.type===`physicalType`&&e.complexType.physicalType===wy.GEOMETRY}function EC(e){let t;switch(e){case 10:case 11:t=Y.BOOLEAN;break;case 12:case 13:t=Y.INT_8;break;case 14:case 15:t=Y.UINT_8;break;case 16:case 17:t=Y.INT_32;break;case 18:case 19:t=Y.UINT_32;break;case 20:case 21:t=Y.INT_64;break;case 22:case 23:t=Y.UINT_64;break;case 24:case 25:t=Y.FLOAT;break;case 26:case 27:t=Y.DOUBLE;break;case 28:case 29:t=Y.STRING;break;default:return null}return{nullable:!!(e&1),columnScope:Cy.FEATURE,type:`scalarType`,scalarType:{longID:!1,type:`physicalType`,physicalType:t}}}const DC=new TextDecoder,OC=`0-3(ID), 4(GEOMETRY), 10-29(scalars), 30(STRUCT), 31(MAP)`;function kC(e,t){let n=xb(e,t,1)[0];if(n===0)return``;let r=t.get(),i=r+n,a=e.subarray(r,i);return t.add(n),DC.decode(a)}function AC(e){let t=e.name,n=e.nullable;return e.type===`scalarType`?{type:`scalarField`,scalarField:e.scalarType,name:t,nullable:n}:{type:`complexField`,complexField:e.complexType,name:t,nullable:n}}function jC(e,t){let n=xb(e,t,1)[0]>>>0,r=n>=yC.SCALAR_BASE?bC(n):null;if(!r)throw Error(`Unsupported field type code ${n}. Supported: 10-29(scalars), 30(STRUCT), 31(MAP)`);let i={...r,name:kC(e,t)};if(i.type===`complexType`&&SC(n)){let n=i.complexType,r=xb(e,t,1)[0]>>>0;n.children=Array(r);for(let i=0;i>>0,r=bC(n);if(!r)throw Error(`Unsupported column type code ${n}. Supported: ${OC}`);let i;if(xC(n))i=kC(e,t);else if(n>>0,r=a.complexType;r.children=Array(n);for(let i=0;i>>0,a=xb(e,t,1)[0]>>>0;r.columns=Array(a);for(let n=0;n>>0,o=r.get()+a;if(o>e.length)throw Error(`Block overruns tile: ${o} > ${e.length}`);let s=xb(e,r,1)[0]>>>0;if(s!==1&&s!==2){r.set(o);continue}let[c,l]=NC(e,r),u=c.featureTables[0],d=null,f=null,p=[],m=0;for(let i of u.columns){let a=i.name;if(wC(i)){let t=null;if(i.nullable){let n=Q(e,r),i=r.get(),a=Ex(e,n.numValues,n.byteLength,r);r.set(i+n.byteLength),t=new Sx(a,n.numValues)}let o=Q(e,r);m=t?t.size():o.decompressedCount,d=FC(e,i,r,a,o,t??m,n)}else if(TC(i)){let n=xb(e,r,1)[0];if(m===0){let t=r.get();m=Q(e,r).decompressedCount,r.set(t)}t&&(t.scale=t.extent/l),f=kS(e,n,r,m,t)}else{let t=CC(i)?xb(e,r,1)[0]:1;if(t===0)continue;let n=dC(e,r,i,t,m,void 0);if(n){if(Array.isArray(n))for(let e of n)p.push(e);else p.push(n)}}}let h=new Sy(u.name,f,d,p,l);i.push(h),r.set(o)}return i}function FC(e,t,n,r,i,a,o=!1){let s=t.scalarType?.longID?Y.UINT_64:Y.UINT_32,c=typeof a==`number`?void 0:a,l=nS(i,a,e,n,s===Y.UINT_64?`int64`:`int32`);if(s===Y.UINT_32)switch(l){case $.FLAT:return new _y(r,Ix(e,n,i,void 0,c),a);case $.SEQUENCE:{let t=Hx(e,n,i);return new by(r,t[0],t[1],i.numRleValues,!1)}case $.CONST:return new xy(r,Vx(e,n,i),a,!1)}switch(l){case $.FLAT:return o?new vy(r,Kx(e,n,i,c),a):new aS(r,Gx(e,n,i,c),a);case $.SEQUENCE:{let t=Ux(e,n,i);return new oS(r,t[0],t[1],i.numRleValues,!1)}case $.CONST:return new IS(r,Yx(e,n,i),a,!1)}throw Error(`Vector type not supported for id column.`)}var IC=class{constructor(e,t){switch(this._featureData=e,this.properties=this._featureData.properties||{},this._featureData.geometry?.type){case lS.POINT:case lS.MULTIPOINT:this.type=1;break;case lS.LINESTRING:case lS.MULTILINESTRING:this.type=2;break;case lS.POLYGON:case lS.MULTIPOLYGON:this.type=3;break;default:this.type=0}this.extent=t,this.id=Number(this._featureData.id)}loadGeometry(){let e=[];for(let t of this._featureData.geometry.coordinates){let n=[];for(let e of t)n.push(new l(e.x,e.y));e.push(n)}return e}},LC=class{constructor(e){this.features=[],this.featureTable=e,this.name=e.name,this.extent=e.extent,this.version=2,this.features=e.getFeatures(),this.length=this.features.length}feature(e){return new IC(this.features[e],this.extent)}},RC=class{constructor(e){this.layers={};let t=PC(new Uint8Array(e));this.layers=t.reduce((e,t)=>({...e,[t.name]:new LC(t)}),{})}},zC=class{constructor(e,t){this.tileID=e,this.x=e.canonical.x,this.y=e.canonical.y,this.z=e.canonical.z,this.grid=new gc(j,16,0),this.grid3D=new gc(j,16,0),this.featureIndexArray=new nu,this.promoteId=t}insert(e,t,n,r,i,a){let o=this.featureIndexArray.length;this.featureIndexArray.emplaceBack(n,r,i);let s=a?this.grid3D:this.grid;for(let e of t){let t=[1/0,1/0,-1/0,-1/0];for(let n of e)t[0]=Math.min(t[0],n.x),t[1]=Math.min(t[1],n.y),t[2]=Math.max(t[2],n.x),t[3]=Math.max(t[3],n.y);t[0]<8192&&t[1]<8192&&t[2]>=0&&t[3]>=0&&s.insert(o,t[0],t[1],t[2],t[3])}}loadVTLayers(){if(!this.vtLayers){switch(this.encoding){case`mlt`:this.vtLayers=new RC(this.rawTileData).layers;break;default:this.vtLayers=new $p(new i_(this.rawTileData)).layers}this.sourceLayerCoder=new py(this.vtLayers?Object.keys(this.vtLayers).sort():[ny])}return this.vtLayers}query(e,t,n,r){this.loadVTLayers();let i=e.params,a=j/e.tileSize/e.scale,o=ts(i.filter,`queryRenderedFeatures filter`,i.globalState),s=e.queryGeometry,c=e.queryPadding*a,l=Wp.fromPoints(s),u=this.grid.query(l.minX-c,l.minY-c,l.maxX+c,l.maxY+c),d=Wp.fromPoints(e.cameraQueryGeometry).expandBy(c),f=this.grid3D.query(d.minX,d.minY,d.maxX,d.maxY,(t,n,r,i)=>wd(e.cameraQueryGeometry,t-c,n-c,r+c,i+c));for(let e of f)u.push(e);u.sort(HC);let p={},m;for(let c of u){if(c===m)continue;m=c;let l=this.featureIndexArray.get(c),u=null;this.loadMatchingFeature(p,l.bucketIndex,l.sourceLayerIndex,l.featureIndex,o,i.layers,i.availableImages,t,n,r,(t,n,r)=>(u||=cd(t),n.queryIntersectsFeature({queryGeometry:s,feature:t,featureState:r,geometry:u,zoom:this.z,transform:e.transform,pixelsToTileUnits:a,pixelPosMatrix:e.pixelPosMatrix,unwrappedTileID:this.tileID.toUnwrapped(),getElevation:e.getElevation})))}return p}loadMatchingFeature(e,t,n,r,i,a,o,s,c,l,u){let d=this.bucketLayerIDs[t];if(a&&!d.some(e=>a.has(e)))return;let f=this.sourceLayerCoder.decode(n),p=this.vtLayers[f].feature(r);if(i.needGeometry){let e=ld(p,!0);if(!i.filter(new U(this.tileID.overscaledZ),e,this.tileID.canonical))return}else if(!i.filter(new U(this.tileID.overscaledZ),p))return;let m=this.getId(p,f);for(let t of d){if(a&&!a.has(t))continue;let n=s[t];if(!n)continue;let i={};m&&l&&(i=l.getState(n.sourceLayer||`_geojsonTileLayer`,m));let d=St({},c[t]);d.paint=VC(d.paint,n.paint,p,i,o),d.layout=VC(d.layout,n.layout,p,i,o);let f=!u||u(p,n,i);if(!f)continue;let h=new my(p,this.z,this.x,this.y,m);h.layer=d;let g=e[t];g===void 0&&(g=e[t]=[]),g.push({featureIndex:r,feature:h,intersectionZ:f})}}lookupSymbolFeatures(e,t,n,r,i,a,o,s){let c={};this.loadVTLayers();let l=ts(i.filterSpec,`queryRenderedFeatures symbol filter`,i.globalState);for(let i of e)this.loadMatchingFeature(c,n,r,i,l,a,o,s,t);return c}hasLayer(e){for(let t of this.bucketLayerIDs)for(let n of t)if(e===n)return!0;return!1}getId(e,t){let n=e.id;if(this.promoteId){let r=typeof this.promoteId==`string`?this.promoteId:this.promoteId[t];n=e.properties[r],typeof n==`boolean`&&(n=Number(n)),n===void 0&&e.properties?.cluster&&this.promoteId&&(n=Number(e.properties.cluster_id))}return n}};H(`FeatureIndex`,zC,{omit:[`rawTileData`,`sourceLayerCoder`]});function BC(e){return typeof e==`object`&&!!e&&`evaluate`in e}function VC(e,t,n,r,i){return jt(e,(e,a)=>{let o=t instanceof nl?t.get(a):null;return BC(o)?o.evaluate(n,r,void 0,i):o})}function HC(e,t){return t-e}var UC=class{constructor(e,t){this.max=e,this.onRemove=t,this.reset()}reset(){for(let e in this.data)for(let t of this.data[e])t.timeout&&clearTimeout(t.timeout),this.onRemove(t.value);return this.data={},this.order=[],this}add(e,t,n){let r=e.wrapped().key;this.data[r]===void 0&&(this.data[r]=[]);let i={value:t,timeout:void 0};if(n!==void 0&&(i.timeout=setTimeout(()=>{this.remove(e,i)},n)),this.data[r].push(i),this.order.push(r),this.order.length>this.max){let e=this._getAndRemoveByKey(this.order[0]);e&&this.onRemove(e)}return this}has(e){return e.wrapped().key in this.data}getAndRemove(e){return this.has(e)?this._getAndRemoveByKey(e.wrapped().key):null}_getAndRemoveByKey(e){let t=this.data[e].shift();return t.timeout&&clearTimeout(t.timeout),this.data[e].length===0&&delete this.data[e],this.order.splice(this.order.indexOf(e),1),t.value}getByKey(e){let t=this.data[e];return t?t[0].value:null}get(e){return this.has(e)?this.data[e.wrapped().key][0].value:null}remove(e,t){if(!this.has(e))return this;let n=e.wrapped().key,r=t===void 0?0:this.data[n].indexOf(t),i=this.data[n][r];return this.data[n].splice(r,1),i.timeout&&clearTimeout(i.timeout),this.data[n].length===0&&delete this.data[n],this.onRemove(i.value),this.order.splice(this.order.indexOf(n),1),this}setMaxSize(e){for(this.max=e;this.order.length>this.max;){let e=this._getAndRemoveByKey(this.order[0]);e&&this.onRemove(e)}return this}filter(e){let t=[];for(let n in this.data)for(let r of this.data[n])e(r.value)||t.push(r);for(let e of t)this.remove(e.value.tileID,e)}},WC=class{constructor(e){this.maxEntries=e,this.map=new Map}get(e){let t=this.map.get(e);return t!==void 0&&(this.map.delete(e),this.map.set(e,t)),t}set(e,t){if(this.map.has(e))this.map.delete(e);else if(this.map.size>=this.maxEntries){let e=this.map.keys().next().value;this.map.delete(e)}this.map.set(e,t)}clear(){this.map.clear()}};function GC(e,t,n,r,i){let a=[];for(let o of e){let e;for(let s=0;s=r&&u.x>=r)&&(c.x>=r?c=new l(r,c.y+(u.y-c.y)*((r-c.x)/(u.x-c.x)))._round():u.x>=r&&(u=new l(r,c.y+(u.y-c.y)*((r-c.x)/(u.x-c.x)))._round()),!(c.y>=i&&u.y>=i)&&(c.y>=i?c=new l(c.x+(u.x-c.x)*((i-c.y)/(u.y-c.y)),i)._round():u.y>=i&&(u=new l(c.x+(u.x-c.x)*((i-c.y)/(u.y-c.y)),i)._round()),(!e||!c.equals(e[e.length-1]))&&(e=[c],a.push(e)),e.push(u)))))}}return a}function KC(e,t,n,r,i,a){let o=qC(e,t,n,i,0);return o=qC(o,t,r,a,1),o}function qC(e,t,n,r,i){switch(t){case 1:return JC(e,n,r,i);case 2:return XC(e,n,r,i,!1);case 3:return XC(e,n,r,i,!0)}return[]}function JC(e,t,n,r){let i=[];for(let a of e)for(let e of a){let a=r===0?e.x:e.y;a>=t&&a<=n&&i.push([e])}return i}function YC(e,t,n,r,i){let a=r===0?ZC:QC,o=[],s=[];for(let c=0;ct&&o.push(a(l,u,t)):d>n?f=t&&(o.push(a(l,u,t)),p=!0),f>n&&d<=n&&(o.push(a(l,u,n)),p=!0),!i&&p&&(s.push(o),o=[])}let c=e.length-1,u=r===0?e[c].x:e[c].y;return u>=t&&u<=n&&o.push(e[c]),i&&o.length>0&&!o[0].equals(o[o.length-1])&&o.push(new l(o[0].x,o[0].y)),o.length>0&&s.push(o),s}function XC(e,t,n,r,i){let a=[];for(let o of e){let e=YC(o,t,n,r,i);e.length>0&&a.push(...e)}return a}function ZC(e,t,n){let r=(n-e.x)/(t.x-e.x);return new l(n,e.y+(t.y-e.y)*r)}function QC(e,t,n){let r=(n-e.y)/(t.y-e.y);return new l(e.x+(t.x-e.x)*r,n)}var $C=class e extends l{constructor(e,t,n,r){super(e,t),this.angle=n,r!==void 0&&(this.segment=r)}clone(){return new e(this.x,this.y,this.angle,this.segment)}};H(`Anchor`,$C);function ew(e,t,n,r,i){if(t.segment===void 0||n===0)return!0;let a=t,o=t.segment+1,s=0;for(;s>-n/2;){if(o--,o<0)return!1;s-=e[o].dist(a),a=e[o]}s+=e[o].dist(e[o+1]),o++;let c=[],l=0;for(;sr;)l-=c.shift().angleDelta;if(l>i)return!1;o++,s+=n.dist(a)}return!0}function tw(e){let t=0;for(let n=0;nl){let u=(l-c)/a,d=new $C(ti.number(r.x,i.x,u),ti.number(r.y,i.y,u),i.angleTo(r),n);return d._round(),!o||ew(e,d,s,o,t)?d:void 0}c+=a}}function aw(e,t,n,r,i,a,o,s,c){let l=nw(r,a,o),u=rw(r,i),d=u*o,f=e[0].x===0||e[0].x===c||e[0].y===0||e[0].y===c;t-d=0&&_=0&&v=0&&f+l<=u){let n=new $C(_,v,h,t);n._round(),(!r||ew(e,n,a,r,i))&&p.push(n)}}d+=m}return!s&&!p.length&&!o&&(p=ow(e,d/2,n,r,i,a,o,!0,c)),p}function sw(e,t,n,r){let i=[],a=e.image,o=a.pixelRatio,s=a.paddedRect.w-2,c=a.paddedRect.h-2,u={x1:e.left,y1:e.top,x2:e.right,y2:e.bottom},d=a.stretchX||[[0,s]],f=a.stretchY||[[0,c]],p=(e,t)=>e+t[1]-t[0],m=d.reduce(p,0),h=f.reduce(p,0),g=s-m,_=c-h,v=0,y=m,b=0,x=h,S=0,C=g,w=0,T=_;if(a.content&&r){let t=a.content,n=t[2]-t[0],r=t[3]-t[1];(a.textFitWidth||a.textFitHeight)&&(u=iv(e)),v=cw(d,0,t[0]),b=cw(f,0,t[1]),y=cw(d,t[0],t[2]),x=cw(f,t[1],t[3]),S=t[0]-v,w=t[1]-b,C=n-y,T=r-x}let E=u.x1,D=u.y1,O=u.x2-E,k=u.y2-D,A=(e,r,i,s)=>{let c=uw(e.stretch-v,y,O,E),u=dw(e.fixed-S,C,e.stretch,m),d=uw(r.stretch-b,x,k,D),f=dw(r.fixed-w,T,r.stretch,h),p=uw(i.stretch-v,y,O,E),g=dw(i.fixed-S,C,i.stretch,m),_=uw(s.stretch-b,x,k,D),A=dw(s.fixed-w,T,s.stretch,h),ee=new l(c,d),te=new l(p,d),ne=new l(p,_),re=new l(c,_),ie=new l(u/o,f/o),ae=new l(g/o,A/o),oe=t*Math.PI/180;if(oe){let e=Math.sin(oe),t=Math.cos(oe),n=[t,-e,e,t];ee._matMult(n),te._matMult(n),re._matMult(n),ne._matMult(n)}let se=e.stretch+e.fixed,ce=i.stretch+i.fixed,le=r.stretch+r.fixed,ue=s.stretch+s.fixed;return{tl:ee,tr:te,bl:re,br:ne,tex:{x:a.paddedRect.x+1+se,y:a.paddedRect.y+1+le,w:ce-se,h:ue-le},writingMode:void 0,glyphOffset:[0,0],sectionIndex:0,pixelOffsetTL:ie,pixelOffsetBR:ae,minFontScaleX:C/o/O,minFontScaleY:T/o/k,isSDF:n}};if(!r||!a.stretchX&&!a.stretchY)i.push(A({fixed:0,stretch:-1},{fixed:0,stretch:-1},{fixed:0,stretch:s+1},{fixed:0,stretch:c+1}));else{let e=lw(d,g,m),t=lw(f,_,h);for(let n=0;n0&&(r=Math.max(10,r),this.circleDiameter=r)}else{let c=a.image?.content&&(a.image.textFitWidth||a.image.textFitHeight)?iv(a):{x1:a.left,y1:a.top,x2:a.right,y2:a.bottom};c.y1=c.y1*o-s[0],c.y2=c.y2*o+s[2],c.x1=c.x1*o-s[3],c.x2=c.x2*o+s[1];let d=a.collisionPadding;if(d&&(c.x1-=d[0]*o,c.y1-=d[1]*o,c.x2+=d[2]*o,c.y2+=d[3]*o),u){let e=new l(c.x1,c.y1),t=new l(c.x2,c.y1),n=new l(c.x1,c.y2),r=new l(c.x2,c.y2),i=u*Math.PI/180;e._rotate(i),t._rotate(i),n._rotate(i),r._rotate(i),c.x1=Math.min(e.x,t.x,n.x,r.x),c.x2=Math.max(e.x,t.x,n.x,r.x),c.y1=Math.min(e.y,t.y,n.y,r.y),c.y2=Math.max(e.y,t.y,n.y,r.y)}e.emplaceBack(t.x,t.y,c.x1,c.y1,c.x2,c.y2,n,r,i)}this.boxEndIndex=e.length}},mw=class{constructor(e=[],t=(e,t)=>et)){if(this.data=e,this.length=this.data.length,this.compare=t,this.length>0)for(let e=(this.length>>1)-1;e>=0;e--)this._down(e)}push(e){this.data.push(e),this._up(this.length++)}pop(){if(this.length===0)return;let e=this.data[0],t=this.data.pop();return--this.length>0&&(this.data[0]=t,this._down(0)),e}peek(){return this.data[0]}_up(e){let{data:t,compare:n}=this,r=t[e];for(;e>0;){let i=e-1>>1,a=t[i];if(n(r,a)>=0)break;t[e]=a,e=i}t[e]=r}_down(e){let{data:t,compare:n}=this,r=this.length>>1,i=t[e];for(;e=0)break;t[e]=t[r],e=r}t[e]=i}};function hw(e,t=1){let n=Wp.fromPoints(e[0]),r=Math.min(n.width(),n.height()),i=r/2,a=new mw([],gw),{minX:o,minY:s,maxX:c,maxY:u}=n;if(r===0)return new l(o,s);for(let t=o;tf.d||!f.d)&&(f=n),!(n.max-f.d<=t)&&(i=n.h/2,a.push(new _w(n.p.x-i,n.p.y-i,i,e)),a.push(new _w(n.p.x+i,n.p.y-i,i,e)),a.push(new _w(n.p.x-i,n.p.y+i,i,e)),a.push(new _w(n.p.x+i,n.p.y+i,i,e)))}return d.d>0&&f.d-d.d<=t?d.p:f.p}function gw(e,t){return t.max-e.max}var _w=class{constructor(e,t,n,r){this.p=new l(e,t),this.h=n,this.d=vw(this.p,r),this.max=this.d+this.h*Math.SQRT2}};function vw(e,t){let n=!1,r=1/0;for(let i of t)for(let t=0,a=i.length,o=a-1;te.y!=s.y>e.y&&e.x<(s.x-a.x)*(e.y-a.y)/(s.y-a.y)+a.x&&(n=!n),r=Math.min(r,xd(e,a,s))}return(n?1:-1)*Math.sqrt(r)}function yw(e){let t=0,n=0,r=0,i=e[0];for(let e=0,a=i.length,o=a-1;ee*24);r.startsWith(`top`)?i[1]-=7:r.startsWith(`bottom`)&&(i[1]+=7),t[n+1]=i}return new zr(t)}let a=r.get(`text-variable-anchor`);if(a){let i;i=e._unevaluatedLayout.getValue(`text-radial-offset`)===void 0?r.get(`text-offset`).evaluate(t,{},n).map(e=>e*24):[r.get(`text-radial-offset`).evaluate(t,{},n)*24,xw];let o=[];for(let e of a)o.push(e,Sw(e,i));return new zr(o)}return null}function ww(e){e.bucket.createArrays();let t=512*e.bucket.overscaling;e.bucket.tilePixelRatio=j/t,e.bucket.compareText={},e.bucket.iconsNeedLinear=!1;let n=e.bucket.layers[0],r=n.layout,i=n._unevaluatedLayout._values,a={layoutIconSize:i[`icon-size`].possiblyEvaluate(new U(e.bucket.zoom+1),e.canonical),layoutTextSize:i[`text-size`].possiblyEvaluate(new U(e.bucket.zoom+1),e.canonical),textMaxSize:i[`text-size`].possiblyEvaluate(new U(18))};if(e.bucket.textSizeData.kind===`composite`){let{minZoom:t,maxZoom:n}=e.bucket.textSizeData;a.compositeTextSizes=[i[`text-size`].possiblyEvaluate(new U(t),e.canonical),i[`text-size`].possiblyEvaluate(new U(n),e.canonical)]}if(e.bucket.iconSizeData.kind===`composite`){let{minZoom:t,maxZoom:n}=e.bucket.iconSizeData;a.compositeIconSizes=[i[`icon-size`].possiblyEvaluate(new U(t),e.canonical),i[`icon-size`].possiblyEvaluate(new U(n),e.canonical)]}let o=r.get(`text-line-height`)*24,s=r.get(`text-rotation-alignment`)!==`viewport`&&r.get(`symbol-placement`)!==`point`,c=r.get(`text-keep-upright`),l=r.get(`text-size`);for(let t of e.bucket.features){let i=r.get(`text-font`).evaluate(t,{},e.canonical).join(`,`),u=l.evaluate(t,{},e.canonical),d=a.layoutTextSize.evaluate(t,{},e.canonical),f=a.layoutIconSize.evaluate(t,{},e.canonical),p={horizontal:{},vertical:void 0},m=t.text,h=[0,0];if(m){let a=m.toString(),l=r.get(`text-letter-spacing`).evaluate(t,{},e.canonical)*24,f=Fc(a)?l:0,g=r.get(`text-anchor`).evaluate(t,{},e.canonical),_=Cw(n,t,e.canonical);if(!_){let n=r.get(`text-radial-offset`).evaluate(t,{},e.canonical);h=n?Sw(g,[n*24,xw]):r.get(`text-offset`).evaluate(t,{},e.canonical).map(e=>e*24)}let v=s?`center`:r.get(`text-justify`).evaluate(t,{},e.canonical),y=r.get(`symbol-placement`)===`point`?r.get(`text-max-width`).evaluate(t,{},e.canonical)*24:1/0,b=()=>{e.bucket.allowVerticalPlacement&&Pc(a)&&(p.vertical=z_(m,e.glyphMap,e.glyphPositions,e.imagePositions,i,y,o,g,`left`,f,h,2,!0,d,u))};if(!s&&_){let t=new Set;if(v===`auto`)for(let e=0;e<_.values.length;e+=2)t.add(Tw(_.values[e]));else t.add(v);let n=!1;for(let r of t)if(!p.horizontal[r]){if(n)p.horizontal[r]=p.horizontal[0];else{let t=z_(m,e.glyphMap,e.glyphPositions,e.imagePositions,i,y,o,`center`,r,f,h,1,!1,d,u);t&&(p.horizontal[r]=t,n=t.positionedLines.length===1)}}b()}else{v===`auto`&&(v=Tw(g));let t=z_(m,e.glyphMap,e.glyphPositions,e.imagePositions,i,y,o,g,v,f,h,1,!1,d,u);t&&(p.horizontal[v]=t),b(),Pc(a)&&s&&c&&(p.vertical=z_(m,e.glyphMap,e.glyphPositions,e.imagePositions,i,y,o,g,v,f,h,2,!1,d,u))}}let g,_=!1;if(t.icon?.name){let n=e.imageMap[t.icon.name];n&&(g=rv(e.imagePositions[t.icon.name],r.get(`icon-offset`).evaluate(t,{},e.canonical),r.get(`icon-anchor`).evaluate(t,{},e.canonical)),_=!!n.sdf,e.bucket.sdfIcons===void 0?e.bucket.sdfIcons=_:e.bucket.sdfIcons!==_&&It(`Style sheet warning: Cannot mix SDF and non-SDF icons in one buffer`),n.pixelRatio===e.bucket.pixelRatio?r.get(`icon-rotate`).constantOr(1)!==0&&(e.bucket.iconsNeedLinear=!0):e.bucket.iconsNeedLinear=!0)}let v=kw(p.horizontal)||p.vertical;e.bucket.iconsInText||=v?v.iconsInText:!1,(v||g)&&Ew(e.bucket,t,p,g,e.imageMap,a,d,f,h,_,e.canonical,e.subdivisionGranularity)}e.showCollisionBoxes&&e.bucket.generateCollisionDebugBuffers()}function Tw(e){switch(e){case`right`:case`top-right`:case`bottom-right`:return`right`;case`left`:case`top-left`:case`bottom-left`:return`left`}return`center`}function Ew(e,t,n,r,i,a,o,s,c,l,u,d){let f=a.textMaxSize.evaluate(t,{});f===void 0&&(f=o);let p=e.layers[0].layout,m=p.get(`icon-offset`).evaluate(t,{},u),h=kw(n.horizontal),g=o/24,_=e.tilePixelRatio*g,v=e.tilePixelRatio*f/24,y=e.tilePixelRatio*s,b=e.tilePixelRatio*p.get(`symbol-spacing`),x=p.get(`text-padding`)*e.tilePixelRatio,S=jv(p,t,u,e.tilePixelRatio),C=p.get(`text-max-angle`)/180*Math.PI,w=p.get(`text-rotation-alignment`)!==`viewport`&&p.get(`symbol-placement`)!==`point`,T=p.get(`icon-rotation-alignment`).constantOr(`viewport`)===`map`&&p.get(`symbol-placement`)!==`point`,E=p.get(`symbol-placement`),D=b/2,O=p.get(`icon-text-fit`),k;r&&O!==`none`&&(e.allowVerticalPlacement&&n.vertical&&(k=av(r,n.vertical,O,p.get(`icon-text-fit-padding`),m,g)),h&&(r=av(r,h,O,p.get(`icon-text-fit-padding`),m,g)));let A=u?d.line.getGranularityForZoomLevel(u.z):1,ee=(s,d)=>{d.x<0||d.x>=8192||d.y<0||d.y>=8192||Aw(e,d,s,n,r,i,k,e.layers[0],e.collisionBoxArray,t.index,t.sourceLayerIndex,e.index,_,[x,x,x,x],w,c,y,S,T,m,t,a,l,u,o)};if(E===`line`)for(let i of GC(t.geometry,0,0,j,j)){let t=Tp(i,A),a=aw(t,b,C,n.vertical||h,r,24,v,e.overscaling,j);for(let n of a){let r=h;(!r||!jw(e,r.text,D,n))&&ee(t,n)}}else if(E===`line-center`){for(let e of t.geometry)if(e.length>1){let t=Tp(e,A),i=iw(t,C,n.vertical||h,r,24,v);i&&ee(t,i)}}else if(t.type===`Polygon`)for(let e of oi(t.geometry,0)){let t=hw(e,16);ee(Tp(e[0],A,!0),new $C(t.x,t.y,0))}else if(t.type===`LineString`)for(let e of t.geometry){let t=Tp(e,A);ee(t,new $C(t[0].x,t[0].y,0))}else if(t.type===`Point`)for(let e of t.geometry)for(let t of e)ee([t],new $C(t.x,t.y,0))}function Dw(e,t){let n=e.length,r=t?.values;if(r?.length>0)for(let t=0;t32640&&It(`${e.layerIds[0]}: Value for "text-size" is >= 255. Reduce your "text-size".`)):_.kind===`composite`&&(v=[128*m.compositeTextSizes[0].evaluate(o,{},h),128*m.compositeTextSizes[1].evaluate(o,{},h)],(v[0]>32640||v[1]>32640)&&It(`${e.layerIds[0]}: Value for "text-size" is >= 255. Reduce your "text-size".`)),e.addSymbols(e.text,g,v,s,a,o,u,t,l.lineStartIndex,l.lineLength,p,h,c);for(let t of d)f[t]=e.text.placedSymbolArray.length-1;return g.length*4}function kw(e){for(let t in e)return e[t];return null}function Aw(e,t,n,r,i,a,o,s,c,l,u,d,f,p,m,h,g,_,v,y,b,x,S,C,w){let T=e.addToLineVertexArray(t,n),E=s.layout.get(`symbol-height-offset`).evaluate(b,{},C),D,O,k,A,ee=0,te=0,ne=0,re=0,ie=-1,ae=-1,oe={},se=(0,ju.default)(``);if(e.allowVerticalPlacement&&r.vertical){let e=s.layout.get(`text-rotate`).evaluate(b,{},C)+90,n=r.vertical;k=new pw(c,t,l,u,d,n,f,p,m,e),o&&(A=new pw(c,t,l,u,d,o,g,_,m,e))}if(i){let n=s.layout.get(`icon-rotate`).evaluate(b,{}),r=s.layout.get(`icon-text-fit`)!==`none`,a=sw(i,n,S,r),f=o?sw(o,n,S,r):void 0;O=new pw(c,t,l,u,d,i,g,_,!1,n),ee=a.length*4;let p=e.iconSizeData,m=null;p.kind===`source`?(m=[128*s.layout.get(`icon-size`).evaluate(b,{})],m[0]>32640&&It(`${e.layerIds[0]}: Value for "icon-size" is >= 255. Reduce your "icon-size".`)):p.kind===`composite`&&(m=[128*x.compositeIconSizes[0].evaluate(b,{},C),128*x.compositeIconSizes[1].evaluate(b,{},C)],(m[0]>32640||m[1]>32640)&&It(`${e.layerIds[0]}: Value for "icon-size" is >= 255. Reduce your "icon-size".`)),e.addSymbols(e.icon,a,m,y,v,b,0,t,T.lineStartIndex,T.lineLength,-1,C,E),ie=e.icon.placedSymbolArray.length-1,f&&(te=f.length*4,e.addSymbols(e.icon,f,m,y,v,b,2,t,T.lineStartIndex,T.lineLength,-1,C,E),ae=e.icon.placedSymbolArray.length-1)}let ce=Object.keys(r.horizontal);for(let n of ce){let i=r.horizontal[n];D||=(se=(0,ju.default)(i.text),new pw(c,t,l,u,d,i,f,p,m,s.layout.get(`text-rotate`).evaluate(b,{},C)));let o=i.positionedLines.length===1;if(ne+=Ow(e,t,i,a,s,m,b,h,E,T,r.vertical?1:3,o?ce:[n],oe,ie,x,C),o)break}r.vertical&&(re+=Ow(e,t,r.vertical,a,s,m,b,h,E,T,2,[`vertical`],oe,ae,x,C));let le=D?D.boxStartIndex:e.collisionBoxArray.length,ue=D?D.boxEndIndex:e.collisionBoxArray.length,de=k?k.boxStartIndex:e.collisionBoxArray.length,fe=k?k.boxEndIndex:e.collisionBoxArray.length,pe=O?O.boxStartIndex:e.collisionBoxArray.length,me=O?O.boxEndIndex:e.collisionBoxArray.length,he=A?A.boxStartIndex:e.collisionBoxArray.length,ge=A?A.boxEndIndex:e.collisionBoxArray.length,_e=-1,ve=(e,t)=>e?.circleDiameter?Math.max(e.circleDiameter,t):t;_e=ve(D,_e),_e=ve(k,_e),_e=ve(O,_e),_e=ve(A,_e);let ye=+(_e>-1);ye&&(_e*=w/24),e.glyphOffsetArray.length>=xv.MAX_GLYPHS&&It(`Too many glyphs being rendered in a tile. See https://github.com/mapbox/mapbox-gl-js/issues/2907`),b.sortKey!==void 0&&e.addToSortKeyRanges(e.symbolInstances.length,b.sortKey);let be=Cw(s,b,C),[xe,Se]=Dw(e.textAnchorOffsets,be);e.symbolInstances.emplaceBack(t.x,t.y,oe.right>=0?oe.right:-1,oe.center>=0?oe.center:-1,oe.left>=0?oe.left:-1,oe.vertical||-1,ie,ae,se,le,ue,de,fe,pe,me,he,ge,l,ne,re,ee,te,ye,0,f,_e,xe,Se,E)}function jw(e,t,n,r){let i=e.compareText;if(!(t in i))i[t]=[];else{let e=i[t];for(let t=e.length-1;t>=0;t--)if(r.dist(e[t])this._layers[e.id]),n=t[0];if(n.isHidden())continue;let r=n.source||``,i=this.familiesBySource[r];i||=this.familiesBySource[r]={};let a=n.sourceLayer||`_geojsonTileLayer`,o=i[a];o||=i[a]=[],o.push(t)}}},W=class{constructor(e){let t={},n=[];for(let r in e){let i=e[r],a=t[r]={};for(let e in i){let t=i[+e];if(!t||t.bitmap.width===0||t.bitmap.height===0)continue;let r={x:0,y:0,w:t.bitmap.width+2,h:t.bitmap.height+2};n.push(r),a[e]={rect:r,metrics:t.metrics}}}let{w:r,h:i}=a(n),o=new T({width:r||1,height:i||1});for(let n in e){let r=e[n];for(let e in r){let i=r[+e];if(!i||i.bitmap.width===0||i.bitmap.height===0)continue;let a=t[n][e].rect;T.copy(i.bitmap,o,{x:0,y:0},{x:a.x+1,y:a.y+1},i.bitmap)}}this.image=o,this.positions=t}};g(`GlyphAtlas`,W);var G=class{constructor(e){this.tileID=new v(e.tileID.overscaledZ,e.tileID.wrap,e.tileID.canonical.z,e.tileID.canonical.x,e.tileID.canonical.y),this.uid=e.uid,this.zoom=e.zoom,this.pixelRatio=e.pixelRatio,this.tileSize=e.tileSize,this.source=e.source,this.overscaling=this.tileID.overscaleFactor(),this.showCollisionBoxes=e.showCollisionBoxes,this.collectResourceTiming=!!e.collectResourceTiming,this.returnDependencies=!!e.returnDependencies,this.promoteId=e.promoteId,this.inFlightDependencies=[]}async parse(e,t,r,i,a){this.status=`parsing`,this.data=e,this.collisionBoxArray=new j;let o=new w(Object.keys(e.layers).sort()),c=new S(this.tileID,this.promoteId);c.bucketLayerIDs=[];let l={},u={featureIndex:c,iconDependencies:{},patternDependencies:{},glyphDependencies:{},dashDependencies:{},availableImages:r,subdivisionGranularity:a},f=t.familiesBySource[this.source];for(let t in f){let i=e.layers[t];if(!i)continue;i.version===1&&n(`Vector tile source "${this.source}" layer "${t}" does not use vector tile spec v2 and therefore may have some rendering errors.`);let a=o.encode(t),s=[];for(let e=0;ee.id)))}}let m=ee(u.glyphDependencies,e=>Object.keys(e).map(Number));for(let e of this.inFlightDependencies)e?.abort();this.inFlightDependencies=[];let h=Promise.resolve({});if(Object.keys(m).length){let e=new AbortController;this.inFlightDependencies.push(e),h=i.sendAsync({type:`GG`,data:{stacks:m,source:this.source,tileID:this.tileID,type:`glyphs`}},e)}let g=Object.keys(u.iconDependencies),_=Promise.resolve({});if(g.length){let e=new AbortController;this.inFlightDependencies.push(e),_=i.sendAsync({type:`GI`,data:{icons:g,source:this.source,tileID:this.tileID,type:`icons`}},e)}let v=Object.keys(u.patternDependencies),y=Promise.resolve({});if(v.length){let e=new AbortController;this.inFlightDependencies.push(e),y=i.sendAsync({type:`GI`,data:{icons:v,source:this.source,tileID:this.tileID,type:`patterns`}},e)}let b=u.dashDependencies,x=Promise.resolve({});if(Object.keys(b).length){let e=new AbortController;this.inFlightDependencies.push(e),x=i.sendAsync({type:`GDA`,data:{dashes:b}},e)}let[C,T,E,D]=await Promise.all([h,_,y,x]),k=new W(C),A=new p(T,E);for(let e in l){let t=l[e];t instanceof s?(K(t.layers,this.zoom,r),ne({bucket:t,glyphMap:C,glyphPositions:k.positions,imageMap:T,imagePositions:A.iconPositions,showCollisionBoxes:this.showCollisionBoxes,canonical:this.tileID.canonical,subdivisionGranularity:u.subdivisionGranularity})):t.hasDependencies&&(t instanceof O||t instanceof R||t instanceof d)&&(K(t.layers,this.zoom,r),t.addFeatures(u,this.tileID.canonical,A.patternPositions,D))}return this.status=`done`,{buckets:Object.values(l).filter(e=>!e.isEmpty()),featureIndex:c,collisionBoxArray:this.collisionBoxArray,glyphAtlasImage:k.image,imageAtlas:A,dashPositions:D,glyphMap:this.returnDependencies?C:null,iconMap:this.returnDependencies?T:null,glyphPositions:this.returnDependencies?k.positions:null}}};function K(e,t,n){let r=new i(t);for(let t of e)t.recalculate(r,n)}var q=class{constructor(){this.loading={},this.loaded={},this.parsing={}}startLoading(e,t){this.loading[e]=t}finishLoading(e){delete this.loading[e]}abort(e){let t=this.loading[e];t?.abort&&(t.abort.abort(),delete this.loading[e])}getParsing(e){return this.parsing[e]}setParsing(e,t){this.parsing[e]=t}removeParsing(e){delete this.parsing[e]}markLoaded(e,t){this.loaded[e]=t}getLoaded(e){let t=this.loaded[e];if(t)return t}removeLoaded(e){delete this.loaded[e]}clearLoaded(){this.loaded={}}},J=class{constructor(e){this.start=`${e}#start`,this.end=`${e}#end`,this.measure=e,performance.mark(this.start)}finish(){performance.mark(this.end);let e=performance.getEntriesByName(this.measure);return e.length===0&&(performance.measure(this.measure,this.start,this.end),e=performance.getEntriesByName(this.measure),performance.clearMarks(this.start),performance.clearMarks(this.end),performance.clearMeasures(this.measure)),e}},Y=class{constructor(e,t,n,r,i){this.type=e,this.properties=n||{},this.extent=i,this.pointsArray=t,this.id=r}loadGeometry(){return this.pointsArray.map(e=>e.map(e=>new A(e.x,e.y)))}},X=class{constructor(e,t,n){this.version=2,this._myFeatures=e,this.name=t,this.length=e.length,this.extent=n}feature(e){return this._myFeatures[e]}},re=class{constructor(){this.layers={}}addLayer(e){this.layers[e.name]=e}};function ie(e,t,n){let{extent:r}=e,i=2**(n.z-t.z),a=(n.x-t.x*i)*r,o=(n.y-t.y*i)*r,s=[];for(let t=0;t0&&c.addLayer(i)}let u={vectorTile:c,rawData:N(c).buffer};return this.overzoomedTileResultCache.set(o,u),u}async reloadTile(e){let t=e.uid,n=this.tileState.getLoaded(t);if(!n)throw Error(`Should not be trying to reload a tile that was never loaded or has been removed`);if(n.showCollisionBoxes=e.showCollisionBoxes,n.status===`parsing`){let r=this.tileState.getParsing(t);try{return await this._parseWorkerTile(n,e,r)}finally{this.tileState.removeParsing(t)}}if(n.status===`done`&&n.vectorTile)return await this._parseWorkerTile(n,e)}async abortTile(e){this.tileState.abort(e.uid)}async removeTile(e){this.tileState.removeLoaded(e.uid)}},oe=class{constructor(){this.loaded={}}async loadTile(e){let{uid:t,encoding:n,rawImageData:r,redFactor:i,greenFactor:a,blueFactor:o,baseShift:s}=e,c=r.width+2,l=r.height+2,u=new B(t,x(r)?new b({width:c,height:l},await L(r,-1,-1,c,l)):r,n,i,a,o,s);return this.loaded||={},this.loaded[t]=u,u}removeTile(e){let t=this.loaded,n=e.uid;t?.[n]&&delete t[n]}},se=class{constructor(e,t,n,r=ce){this.actor=e,this.layerIndex=t,this.availableImages=n,this.tileState=new q,this._createGeoJSONIndex=r}loadVectorTile(e){if(!this._geoJSONIndex)throw Error(`Unable to parse the data into a cluster or geojson`);let{z:n,x:r,y:i}=e.tileID.canonical,a=this._geoJSONIndex.getTile(n,r,i);if(!a)return null;let o=new I(a.features,{version:2,extent:u});return{vectorTile:o,rawData:N(o,t).buffer}}async loadTile(e){let{uid:t}=e,n=new G(e);n.abort=new AbortController;try{let r=this.loadVectorTile(e);if(!r)return null;let{vectorTile:i,rawData:a}=r;n.vectorTile=i,this.tileState.markLoaded(t,n);let o={rawData:a};this.tileState.setParsing(t,o);try{return await this._parseWorkerTile(n,e,o)}finally{this.tileState.removeParsing(t)}}catch(e){throw n.status=`done`,this.tileState.markLoaded(t,n),e}}async _reloadLoadedTile(e){let t=e.uid,n=this.tileState.getLoaded(t);if(!n)throw Error(`Should not be trying to reload a tile that was never loaded or has been removed`);if(n.showCollisionBoxes=e.showCollisionBoxes,n.status===`parsing`){let r=this.tileState.getParsing(t);try{return await this._parseWorkerTile(n,e,r)}finally{this.tileState.removeParsing(t)}}if(n.status===`done`&&n.vectorTile)return await this._parseWorkerTile(n,e)}async _parseWorkerTile(e,t,n){let r=await e.parse(e.vectorTile,this.layerIndex,this.availableImages,this.actor,t.subdivisionGranularity);if(n){let{rawData:e}=n;r=_({rawTileData:e.slice(0),encoding:`mvt`},r)}return r}async abortTile(e){this.tileState.abort(e.uid)}async removeTile(e){this.tileState.removeLoaded(e.uid)}async loadData(e){this._pendingRequest?.abort();let t=this._startRequestTiming(e);this._pendingRequest=new AbortController;try{await this.loadAndProcessGeoJSON(e,this._pendingRequest),delete this._pendingRequest,this.tileState.clearLoaded();let n={};return e.request&&(n.data=e.data),this._finishRequestTiming(t,e,n),n}catch(e){if(delete this._pendingRequest,!l(e))throw e;return{abandoned:!0}}}_startRequestTiming(e){if(e.request?.collectResourceTiming)return new J(e.request.url)}_finishRequestTiming(e,t,n){let r=e?.finish();r&&(n.resourceTiming={[t.source]:JSON.parse(JSON.stringify(r))})}reloadTile(e){return this.tileState.getLoaded(e.uid)?this._reloadLoadedTile(e):this.loadTile(e)}async loadAndProcessGeoJSON(e,t){if(e.request&&(e.data=(await V(e.request,t)).data),e.data){e.data=this._filterGeoJSON(e.data,e.filter,e.source),this._geoJSONIndex=this._createGeoJSONIndex(e.data,e);return}if(e.dataDiff){this._geoJSONIndex??=this._createGeoJSONIndex({type:`FeatureCollection`,features:[]},e),this._geoJSONIndex.updateData(e.dataDiff,this._getFilterPredicate(e.filter,e.source));return}if(e.updateCluster&&this._geoJSONIndex.updateClusterOptions(e.geojsonVtOptions.cluster,Z(e)),this._geoJSONIndex==null)throw Error(`Input data given to '${e.source}' is not a valid GeoJSON object.`)}_filterGeoJSON(e,t,n){if(e.type!==`FeatureCollection`)return e;let r=this._getFilterPredicate(t,n);return r?{type:`FeatureCollection`,features:e.features.filter(e=>r(e))}:e}_getFilterPredicate(e,t){if(typeof e!=`boolean`&&!e?.length)return;let n=D(e,`sources.${t}.filter`,{type:`boolean`,"property-type":`data-driven`,overridable:!1,transition:!1});if(n.result===`error`)throw Error(n.value.map(e=>`${e.key}: ${e.message}`).join(`, `));return e=>n.value.evaluate({zoom:0},e)}async removeSource(e){this._pendingRequest?.abort()}getClusterExpansionZoom(e){return this._geoJSONIndex.getClusterExpansionZoom(e.clusterId)}getClusterChildren(e){return this._geoJSONIndex.getClusterChildren(e.clusterId)}getClusterLeaves(e){return this._geoJSONIndex.getClusterLeaves(e.clusterId,e.limit,e.offset)}};function ce(t,n){return new e(t,_(n.geojsonVtOptions||{},{updateable:!0,clusterOptions:Z(n)}))}function Z({geojsonVtOptions:e,clusterProperties:t,source:n}){if(!t||!e.clusterOptions)return e.clusterOptions;let r={},i={},a={accumulated:null,zoom:0},o={properties:null},s=Object.keys(t);for(let e of s){let[a,o]=t[e],s=D(o,`sources.${n}.clusterProperties.${e}[1]`),c=D(typeof a==`string`?[a,[`accumulated`],[`get`,e]]:a,`sources.${n}.clusterProperties.${e}[0]`);r[e]=s.value,i[e]=c.value}return e.clusterOptions.map=e=>{o.properties=e;let t={};for(let e of s)t[e]=r[e].evaluate(a,o);return t},e.clusterOptions.reduce=(e,t)=>{o.properties=t;for(let t of s)a.accumulated=e[t],e[t]=i[t].evaluate(a,o)},e.clusterOptions}async function Q(e){if(e.endsWith(`.mjs`)){await import(e);return}let t=await fetch(e,{credentials:`same-origin`});if(!t.ok)throw Error(`Failed to load ${e}: ${t.status}`);let n=await t.text();if(/^[ \t]*(import|export)\s/m.test(n)){let e=URL.createObjectURL(new Blob([n],{type:`text/javascript`}));try{await import(e)}finally{URL.revokeObjectURL(e)}return}globalThis.eval(n)}var $=class{constructor(e){this.self=e,this.actor=new k(e),this.layerIndexes={},this.availableImages={},this.workerSources={},this.demWorkerSources={},this.externalWorkerSourceTypes={},this.globalStates=new Map,this.self.registerWorkerSource=(e,t)=>{if(this.externalWorkerSourceTypes[e])throw Error(`Worker source with name "${e}" already registered.`);this.externalWorkerSourceTypes[e]=t},this.self.addProtocol=r,this.self.removeProtocol=f,this.self.registerRTLTextPlugin=e=>{o.setMethods(e)},this.self.makeRequest=H,this.actor.registerMessageHandler(`LDT`,(e,t)=>this._getDEMWorkerSource(e,t.source).loadTile(t)),this.actor.registerMessageHandler(`RDT`,async(e,t)=>{this._getDEMWorkerSource(e,t.source).removeTile(t)}),this.actor.registerMessageHandler(`GCEZ`,async(e,t)=>this._getWorkerSource(e,t.type,t.source).getClusterExpansionZoom(t)),this.actor.registerMessageHandler(`GCC`,async(e,t)=>this._getWorkerSource(e,t.type,t.source).getClusterChildren(t)),this.actor.registerMessageHandler(`GCL`,async(e,t)=>this._getWorkerSource(e,t.type,t.source).getClusterLeaves(t)),this.actor.registerMessageHandler(`LD`,(e,t)=>this._getWorkerSource(e,t.type,t.source).loadData(t)),this.actor.registerMessageHandler(`LT`,(e,t)=>this._getWorkerSource(e,t.type,t.source).loadTile(t)),this.actor.registerMessageHandler(`RT`,(e,t)=>this._getWorkerSource(e,t.type,t.source).reloadTile(t)),this.actor.registerMessageHandler(`AT`,(e,t)=>this._getWorkerSource(e,t.type,t.source).abortTile(t)),this.actor.registerMessageHandler(`RMT`,(e,t)=>this._getWorkerSource(e,t.type,t.source).removeTile(t)),this.actor.registerMessageHandler(`RS`,async(e,t)=>{if(!this.workerSources[e]?.[t.type]?.[t.source])return;let n=this.workerSources[e][t.type][t.source];delete this.workerSources[e][t.type][t.source],n.removeSource!==void 0&&n.removeSource(t)}),this.actor.registerMessageHandler(`RM`,async e=>{delete this.layerIndexes[e],delete this.availableImages[e],delete this.workerSources[e],delete this.demWorkerSources[e],this.globalStates.delete(e)}),this.actor.registerMessageHandler(`SR`,async(e,t)=>{this.referrer=t}),this.actor.registerMessageHandler(`SRPS`,(e,t)=>this._syncRTLPluginState(e,t)),this.actor.registerMessageHandler(`IS`,async(e,t)=>{await Q(t)}),this.actor.registerMessageHandler(`SI`,(e,t)=>this._setImages(e,t)),this.actor.registerMessageHandler(`UL`,async(e,t)=>{this._getLayerIndex(e).update(t.layers,t.removedIds,this._getGlobalState(e))}),this.actor.registerMessageHandler(`UGS`,async(e,t)=>{let n=this._getGlobalState(e);for(let e in t)n[e]=t[e]}),this.actor.registerMessageHandler(`SL`,async(e,t)=>{this._getLayerIndex(e).replace(t,this._getGlobalState(e))})}_getGlobalState(e){let t=this.globalStates.get(e);return t||(t={},this.globalStates.set(e,t)),t}async _setImages(e,t){this.availableImages[e]=t;for(let n in this.workerSources[e]){let r=this.workerSources[e][n];for(let e in r)r[e].availableImages=t}}async _syncRTLPluginState(e,t){return await o.syncState(t,Q)}_getAvailableImages(e){let t=this.availableImages[e];return t||=[],t}_getLayerIndex(e){let t=this.layerIndexes[e];return t||=this.layerIndexes[e]=new U,t}_getWorkerSource(e,t,n){if(this.workerSources[e]||={},this.workerSources[e][t]||={},!this.workerSources[e][t][n]){let r={sendAsync:(t,n)=>(t.targetMapId=e,this.actor.sendAsync(t,n))};switch(t){case`vector`:this.workerSources[e][t][n]=new ae(r,this._getLayerIndex(e),this._getAvailableImages(e));break;case`geojson`:this.workerSources[e][t][n]=new se(r,this._getLayerIndex(e),this._getAvailableImages(e));break;default:this.workerSources[e][t][n]=new this.externalWorkerSourceTypes[t](r,this._getLayerIndex(e),this._getAvailableImages(e));break}}return this.workerSources[e][t][n]}_getDEMWorkerSource(e,t){return this.demWorkerSources[e]||={},this.demWorkerSources[e][t]||=new oe,this.demWorkerSources[e][t]}};z(self)&&(self.worker=new $(self));export{$ as default}; +import{An as e,B as t,Bi as n,D as r,Dn as i,En as a,G as o,Hr as s,I as c,Ln as l,Mn as u,Ot as d,P as f,Pn as p,Rr as m,S as h,St as g,Ut as _,W as v,ar as y,br as b,c as x,d as S,dr as C,ft as w,g as T,gt as ee,i as E,in as D,kt as O,l as k,m as A,mn as j,n as te,o as ne,p as re,pn as ie,pr as M,q as N,rn as P,rr as F,sn as I,vr as L,x as R,yn as z,zn as B}from"./maplibre-gl-shared.mjs";function V(e){let t=typeof e;if(t===`number`||t===`boolean`||t===`string`||e==null)return JSON.stringify(e);if(Array.isArray(e)){let t=`[`;for(let n of e)t+=`${V(n)},`;return`${t}]`}let n=Object.keys(e).sort(),r=`{`;for(let t=0;tthis._layers[e.id]),n=t[0];if(n.isHidden())continue;let r=n.source||``,i=this.familiesBySource[r];i||=this.familiesBySource[r]={};let a=n.sourceLayer||`_geojsonTileLayer`,o=i[a];o||=i[a]=[],o.push(t)}}},G=class{constructor(e){let t={},n=[];for(let r in e){let i=e[r],a=t[r]={};for(let e in i){let t=i[e];if(!t||t.bitmap.width===0||t.bitmap.height===0)continue;let r={x:0,y:0,w:t.bitmap.width+2,h:t.bitmap.height+2};n.push(r),a[e]={rect:r,metrics:t.metrics}}}let{w:r,h:i}=c(n),a=new d({width:r||1,height:i||1});for(let n in e){let r=e[n];for(let e in r){let i=r[e];if(!i||i.bitmap.width===0||i.bitmap.height===0)continue;let o=t[n][e].rect;d.copy(i.bitmap,a,{x:0,y:0},{x:o.x+1,y:o.y+1},i.bitmap)}}this.image=a,this.positions=t}};I(`GlyphAtlas`,G);var K=class{constructor(e){this.tileID=new T(e.tileID.overscaledZ,e.tileID.wrap,e.tileID.canonical.z,e.tileID.canonical.x,e.tileID.canonical.y),this.uid=e.uid,this.zoom=e.zoom,this.pixelRatio=e.pixelRatio,this.tileSize=e.tileSize,this.source=e.source,this.overscaling=this.tileID.overscaleFactor(),this.showCollisionBoxes=e.showCollisionBoxes,this.collectResourceTiming=!!e.collectResourceTiming,this.returnDependencies=!!e.returnDependencies,this.promoteId=e.promoteId,this.inFlightDependencies=[]}async parse(e,t,n,i,a){this.data=e,this.collisionBoxArray=new _;let o=new S(Object.keys(e.layers).sort()),s=new x(this.tileID,this.promoteId);s.bucketLayerIDs=[];let c={},l={featureIndex:s,iconDependencies:{},patternDependencies:{},glyphDependencies:{},dashDependencies:{},availableImages:n,subdivisionGranularity:a},u=t.familiesBySource[this.source];for(let t in u){let r=e.layers[t];if(!r)continue;r.version===1&&m(`Vector tile source "${this.source}" layer "${t}" does not use vector tile spec v2 and therefore may have some rendering errors.`);let i=o.encode(t),a=[];for(let e=0;ee.id)))}}let d=b(l.glyphDependencies,e=>Object.keys(e));for(let e of this.inFlightDependencies)e?.abort();this.inFlightDependencies=[];let p=Promise.resolve({});if(Object.keys(d).length){let e=new AbortController;this.inFlightDependencies.push(e),p=i.sendAsync({type:`GG`,data:{stacks:d,source:this.source,tileID:this.tileID,type:`glyphs`}},e)}let h=Object.keys(l.iconDependencies),g=Promise.resolve({});if(h.length){let e=new AbortController;this.inFlightDependencies.push(e),g=i.sendAsync({type:`GI`,data:{icons:h,source:this.source,tileID:this.tileID,type:`icons`}},e)}let y=Object.keys(l.patternDependencies),C=Promise.resolve({});if(y.length){let e=new AbortController;this.inFlightDependencies.push(e),C=i.sendAsync({type:`GI`,data:{icons:y,source:this.source,tileID:this.tileID,type:`patterns`}},e)}let w=l.dashDependencies,T=Promise.resolve({});if(Object.keys(w).length){let e=new AbortController;this.inFlightDependencies.push(e),T=i.sendAsync({type:`GDA`,data:{dashes:w}},e)}let[E,D,O,k]=await Promise.all([p,g,C,T]),A=new G(E),j=new f(D,O);for(let e in c){let t=c[e];t instanceof r?(q(t.layers,this.zoom,n),te({bucket:t,glyphMap:E,glyphPositions:A.positions,imageMap:D,imagePositions:j.iconPositions,showCollisionBoxes:this.showCollisionBoxes,canonical:this.tileID.canonical,subdivisionGranularity:l.subdivisionGranularity})):t.hasDependencies&&(t instanceof ee||t instanceof N||t instanceof v)&&(q(t.layers,this.zoom,n),t.addFeatures(l,this.tileID.canonical,j.patternPositions,k))}return{buckets:Object.values(c).filter(e=>!e.isEmpty()),featureIndex:s,collisionBoxArray:this.collisionBoxArray,glyphAtlasImage:A.image,imageAtlas:j,dashPositions:k,glyphMap:this.returnDependencies?E:null,iconMap:this.returnDependencies?D:null,glyphPositions:this.returnDependencies?A.positions:null}}};function q(e,t,n){let r=new P(t);for(let t of e)t.recalculate(r,n)}var J=class{constructor(){this.loading={},this.loaded={},this.parsing={}}startLoading(e,t){this.loading[e]=t}finishLoading(e){delete this.loading[e]}abort(e){let t=this.loading[e];t?.abort&&(t.abort.abort(),delete this.loading[e])}getParsing(e){return this.parsing[e]}setParsing(e,t){this.parsing[e]=t}removeParsing(e){delete this.parsing[e]}markLoaded(e,t){this.loaded[e]=t}getLoaded(e){let t=this.loaded[e];if(t)return t}removeLoaded(e){delete this.loaded[e]}clearLoaded(){this.loaded={}}},Y=class{constructor(e){this.start=`${e}#start`,this.end=`${e}#end`,this.measure=e,performance.mark(this.start)}finish(){performance.mark(this.end);let e=performance.getEntriesByName(this.measure);return e.length===0&&(performance.measure(this.measure,this.start,this.end),e=performance.getEntriesByName(this.measure),performance.clearMarks(this.start),performance.clearMarks(this.end),performance.clearMeasures(this.measure)),e}},ae=class{constructor(e,t,n,r,i){this.type=e,this.properties=n||{},this.extent=i,this.pointsArray=t,this.id=r}loadGeometry(){return this.pointsArray.map(e=>e.map(e=>new n(e.x,e.y)))}},oe=class{constructor(e,t,n){this.version=2,this._myFeatures=e,this.name=t,this.length=e.length,this.extent=n}feature(e){return this._myFeatures[e]}},se=class{constructor(){this.layers={}}addLayer(e){this.layers[e.name]=e}};function ce(e,t,n){let{extent:r}=e,i=2**(n.z-t.z),a=(n.x-t.x*i)*r,o=(n.y-t.y*i)*r,s=[];for(let t=0;t0&&c.addLayer(i)}let u={vectorTile:c,rawData:A(c).buffer};return this.overzoomedTileResultCache.set(o,u),u}async reloadTile(e){let t=e.uid,n=this.tileState.getLoaded(t);if(!n)throw Error(`Should not be trying to reload a tile that was never loaded or has been removed`);if(n.vectorTile)return n.showCollisionBoxes=e.showCollisionBoxes,await this._parseWorkerTile(n,e)}async abortTile(e){this.tileState.abort(e.uid)}async removeTile(e){this.tileState.removeLoaded(e.uid)}},X=class{constructor(){this.loaded={}}async loadTile(e){let{uid:t,encoding:n,rawImageData:r,redFactor:i,greenFactor:a,blueFactor:o,baseShift:s}=e,c=r.width+2,l=r.height+2,u=M(r)?new O({width:c,height:l},await C(r,-1,-1,c,l)):r,d=new g(t,u,n,i,a,o,s);return this.loaded||={},this.loaded[t]=d,d}removeTile(e){let t=this.loaded,n=e.uid;t?.[n]&&delete t[n]}},ue=class{constructor(e,t,n,r=de){this.actor=e,this.layerIndex=t,this.availableImages=n,this.tileState=new J,this._createGeoJSONIndex=r}loadVectorTile(e){if(!this._geoJSONIndex)throw Error(`Unable to parse the data into a cluster or geojson`);let{z:t,x:n,y:r}=e.tileID.canonical,i=this._geoJSONIndex.getTile(t,n,r);if(!i)return null;let a=new re(i.features,{version:2,extent:s});return{vectorTile:a,rawData:A(a,B).buffer}}async loadTile(e){let{uid:t}=e,n=new K(e);n.abort=new AbortController;try{let r=this.loadVectorTile(e);if(!r)return null;let{vectorTile:i,rawData:a}=r;n.vectorTile=i,this.tileState.markLoaded(t,n);let o={rawData:a};return this.tileState.setParsing(t,o),await this._parseWorkerTile(n,e)}catch(e){throw this.tileState.markLoaded(t,n),e}}async _parseWorkerTile(e,t){let n=this.tileState.getParsing(e.uid),r=await e.parse(e.vectorTile,this.layerIndex,this.availableImages,this.actor,t.subdivisionGranularity);if(n){let{rawData:t}=n;r=y({rawTileData:t.slice(0),encoding:`mvt`},r),this.tileState.removeParsing(e.uid)}return r}async abortTile(e){this.tileState.abort(e.uid)}async removeTile(e){this.tileState.removeLoaded(e.uid)}async loadData(e){this._pendingRequest?.abort();let t=this._startRequestTiming(e);this._pendingRequest=new AbortController;try{await this.loadAndProcessGeoJSON(e,this._pendingRequest),delete this._pendingRequest,this.tileState.clearLoaded();let n={};return e.request&&(n.data=e.data),this._finishRequestTiming(t,e,n),n}catch(e){if(delete this._pendingRequest,!l(e))throw e;return{abandoned:!0}}}_startRequestTiming(e){if(e.request?.collectResourceTiming)return new Y(e.request.url)}_finishRequestTiming(e,t,n){let r=e?.finish();r&&(n.resourceTiming={[t.source]:JSON.parse(JSON.stringify(r))})}async reloadTile(e){let t=e.uid,n=this.tileState.getLoaded(t);if(!n)return await this.loadTile(e);if(n.vectorTile)return n.showCollisionBoxes=e.showCollisionBoxes,await this._parseWorkerTile(n,e)}async loadAndProcessGeoJSON(e,t){if(e.request&&(e.data=(await i(e.request,t)).data),e.data){e.data=this._filterGeoJSON(e.data,e.filter,e.source),this._geoJSONIndex=this._createGeoJSONIndex(e.data,e);return}if(e.dataDiff){this._geoJSONIndex??=this._createGeoJSONIndex({type:`FeatureCollection`,features:[]},e),this._geoJSONIndex.updateData(e.dataDiff,this._getFilterPredicate(e.filter,e.source));return}if(e.updateCluster&&this._geoJSONIndex.updateClusterOptions(e.geojsonVtOptions.cluster,Z(e)),this._geoJSONIndex==null)throw Error(`Input data given to '${e.source}' is not a valid GeoJSON object.`)}_filterGeoJSON(e,t,n){if(e.type!==`FeatureCollection`)return e;let r=this._getFilterPredicate(t,n);return r?{type:`FeatureCollection`,features:e.features.filter(e=>r(e))}:e}_getFilterPredicate(e,t){if(typeof e!=`boolean`&&!e?.length)return;let n=j(e,`sources.${t}.filter`,{type:`boolean`,"property-type":`data-driven`,overridable:!1,transition:!1});if(n.result===`error`)throw Error(n.value.map(e=>`${e.key}: ${e.message}`).join(`, `));return e=>n.value.evaluate({zoom:0},e)}async removeSource(e){this._pendingRequest?.abort()}getClusterExpansionZoom(e){return this._geoJSONIndex.getClusterExpansionZoom(e.clusterId)}getClusterChildren(e){return this._geoJSONIndex.getClusterChildren(e.clusterId)}getClusterLeaves(e){return this._geoJSONIndex.getClusterLeaves(e.clusterId,e.limit,e.offset)}};function de(e,t){let n=y(t.geojsonVtOptions||{},{updateable:!0,clusterOptions:Z(t)});return new o(e,n)}function Z({geojsonVtOptions:e,clusterProperties:t,source:n}){if(!t||!e.clusterOptions)return e.clusterOptions;let r={},i={},a={accumulated:null,zoom:0},o={properties:null},s=Object.keys(t);for(let e of s){let[a,o]=t[e],s=j(o,`sources.${n}.clusterProperties.${e}[1]`),c=j(typeof a==`string`?[a,[`accumulated`],[`get`,e]]:a,`sources.${n}.clusterProperties.${e}[0]`);r[e]=s.value,i[e]=c.value}return e.clusterOptions.map=e=>{o.properties=e;let t={};for(let e of s)t[e]=r[e].evaluate(a,o);return t},e.clusterOptions.reduce=(e,t)=>{o.properties=t;for(let t of s)a.accumulated=e[t],e[t]=i[t].evaluate(a,o)},e.clusterOptions}async function Q(e){if(e.endsWith(`.mjs`)){await import(e);return}let t=await fetch(e,{credentials:`same-origin`});if(!t.ok)throw Error(`Failed to load ${e}: ${t.status}`);let n=await t.text();if(/^[ \t]*(import|export)\s/m.test(n)){let e=URL.createObjectURL(new Blob([n],{type:`text/javascript`}));try{await import(e)}finally{URL.revokeObjectURL(e)}return}globalThis.eval(n)}var $=class{constructor(t){this.self=t,this.actor=new R(t),this.layerIndexes={},this.availableImages={},this.workerSources={},this.demWorkerSources={},this.externalWorkerSourceTypes={},this.globalStates=new Map,this.self.registerWorkerSource=(e,t)=>{if(this.externalWorkerSourceTypes[e])throw Error(`Worker source with name "${e}" already registered.`);this.externalWorkerSourceTypes[e]=t},this.self.addProtocol=u,this.self.removeProtocol=p,this.self.registerRTLTextPlugin=e=>{D.setMethods(e)},this.self.makeRequest=e,this.actor.registerMessageHandler(`LDT`,(e,t)=>this._getDEMWorkerSource(e,t.source).loadTile(t)),this.actor.registerMessageHandler(`RDT`,async(e,t)=>{this._getDEMWorkerSource(e,t.source).removeTile(t)}),this.actor.registerMessageHandler(`GCEZ`,async(e,t)=>this._getWorkerSource(e,t.type,t.source).getClusterExpansionZoom(t)),this.actor.registerMessageHandler(`GCC`,async(e,t)=>this._getWorkerSource(e,t.type,t.source).getClusterChildren(t)),this.actor.registerMessageHandler(`GCL`,async(e,t)=>this._getWorkerSource(e,t.type,t.source).getClusterLeaves(t)),this.actor.registerMessageHandler(`LD`,(e,t)=>this._getWorkerSource(e,t.type,t.source).loadData(t)),this.actor.registerMessageHandler(`LT`,(e,t)=>this._getWorkerSource(e,t.type,t.source).loadTile(t)),this.actor.registerMessageHandler(`RT`,(e,t)=>this._getWorkerSource(e,t.type,t.source).reloadTile(t)),this.actor.registerMessageHandler(`AT`,(e,t)=>this._getWorkerSource(e,t.type,t.source).abortTile(t)),this.actor.registerMessageHandler(`RMT`,(e,t)=>this._getWorkerSource(e,t.type,t.source).removeTile(t)),this.actor.registerMessageHandler(`RS`,async(e,t)=>{if(!this.workerSources[e]?.[t.type]?.[t.source])return;let n=this.workerSources[e][t.type][t.source];delete this.workerSources[e][t.type][t.source],n.removeSource!==void 0&&n.removeSource(t)}),this.actor.registerMessageHandler(`RM`,async e=>{delete this.layerIndexes[e],delete this.availableImages[e],delete this.workerSources[e],delete this.demWorkerSources[e],this.globalStates.delete(e)}),this.actor.registerMessageHandler(`SR`,async(e,t)=>{this.referrer=t}),this.actor.registerMessageHandler(`SRPS`,(e,t)=>this._syncRTLPluginState(e,t)),this.actor.registerMessageHandler(`IS`,async(e,t)=>{await Q(t)}),this.actor.registerMessageHandler(`SI`,(e,t)=>this._setImages(e,t)),this.actor.registerMessageHandler(`UL`,async(e,t)=>{this._getLayerIndex(e).update(t.layers,t.removedIds,this._getGlobalState(e))}),this.actor.registerMessageHandler(`UGS`,async(e,t)=>{let n=this._getGlobalState(e);for(let e in t)n[e]=t[e]}),this.actor.registerMessageHandler(`SL`,async(e,t)=>{this._getLayerIndex(e).replace(t,this._getGlobalState(e))})}_getGlobalState(e){let t=this.globalStates.get(e);return t||(t={},this.globalStates.set(e,t)),t}async _setImages(e,t){this.availableImages[e]=t;for(let n in this.workerSources[e]){let r=this.workerSources[e][n];for(let e in r)r[e].availableImages=t}}async _syncRTLPluginState(e,t){return await D.syncState(t,Q)}_getAvailableImages(e){let t=this.availableImages[e];return t||=[],t}_getLayerIndex(e){let t=this.layerIndexes[e];return t||=this.layerIndexes[e]=new W,t}_getWorkerSource(e,t,n){if(this.workerSources[e]||={},this.workerSources[e][t]||={},!this.workerSources[e][t][n]){let r={sendAsync:(t,n)=>(t.targetMapId=e,this.actor.sendAsync(t,n))};switch(t){case`vector`:this.workerSources[e][t][n]=new le(r,this._getLayerIndex(e),this._getAvailableImages(e));break;case`geojson`:this.workerSources[e][t][n]=new ue(r,this._getLayerIndex(e),this._getAvailableImages(e));break;default:this.workerSources[e][t][n]=new this.externalWorkerSourceTypes[t](r,this._getLayerIndex(e),this._getAvailableImages(e))}}return this.workerSources[e][t][n]}_getDEMWorkerSource(e,t){return this.demWorkerSources[e]||={},this.demWorkerSources[e][t]||=new X,this.demWorkerSources[e][t]}};L(self)&&(self.worker=new $(self));export{$ as default}; //# sourceMappingURL=maplibre-gl-worker.mjs.map \ No newline at end of file diff --git a/web/vendor/maplibre/maplibre-gl.css b/web/vendor/maplibre/maplibre-gl.css index e8ac5ddd3..2c85b53a0 100644 --- a/web/vendor/maplibre/maplibre-gl.css +++ b/web/vendor/maplibre/maplibre-gl.css @@ -1 +1 @@ -.maplibregl-map{font:12px/20px Helvetica Neue,Arial,Helvetica,sans-serif;overflow:hidden;position:relative;-webkit-tap-highlight-color:rgb(0 0 0/0)}.maplibregl-canvas{position:absolute;left:0;top:0}.maplibregl-map:fullscreen{width:100%;height:100%}.maplibregl-ctrl-group button.maplibregl-ctrl-compass{touch-action:none}.maplibregl-canvas-container.maplibregl-interactive,.maplibregl-ctrl-group button.maplibregl-ctrl-compass{cursor:grab;-webkit-user-select:none;-moz-user-select:none;user-select:none}.maplibregl-canvas-container.maplibregl-interactive.maplibregl-track-pointer{cursor:pointer}.maplibregl-canvas-container.maplibregl-interactive:active,.maplibregl-ctrl-group button.maplibregl-ctrl-compass:active{cursor:grabbing}.maplibregl-canvas-container.maplibregl-touch-zoom-rotate,.maplibregl-canvas-container.maplibregl-touch-zoom-rotate .maplibregl-canvas{touch-action:pan-x pan-y}.maplibregl-canvas-container.maplibregl-touch-drag-pan,.maplibregl-canvas-container.maplibregl-touch-drag-pan .maplibregl-canvas{touch-action:pinch-zoom}.maplibregl-canvas-container.maplibregl-touch-zoom-rotate.maplibregl-touch-drag-pan,.maplibregl-canvas-container.maplibregl-touch-zoom-rotate.maplibregl-touch-drag-pan .maplibregl-canvas{touch-action:none}.maplibregl-canvas-container.maplibregl-touch-drag-pan.maplibregl-cooperative-gestures,.maplibregl-canvas-container.maplibregl-touch-drag-pan.maplibregl-cooperative-gestures .maplibregl-canvas{touch-action:pan-x pan-y}.maplibregl-ctrl-bottom-left,.maplibregl-ctrl-bottom-right,.maplibregl-ctrl-top-left,.maplibregl-ctrl-top-right{position:absolute;pointer-events:none;z-index:2}.maplibregl-ctrl-top-left{top:0;left:0}.maplibregl-ctrl-top-right{top:0;right:0}.maplibregl-ctrl-bottom-left{bottom:0;left:0}.maplibregl-ctrl-bottom-right{right:0;bottom:0}.maplibregl-ctrl{clear:both;pointer-events:auto;transform:translate(0)}.maplibregl-ctrl-top-left .maplibregl-ctrl{margin:10px 0 0 10px;float:left}.maplibregl-ctrl-top-right .maplibregl-ctrl{margin:10px 10px 0 0;float:right}.maplibregl-ctrl-bottom-left .maplibregl-ctrl{margin:0 0 10px 10px;float:left}.maplibregl-ctrl-bottom-right .maplibregl-ctrl{margin:0 10px 10px 0;float:right}.maplibregl-ctrl-group{border-radius:4px;background:#fff}.maplibregl-ctrl-group:not(:empty){box-shadow:0 0 0 2px rgba(0,0,0,.1)}@media (forced-colors:active){.maplibregl-ctrl-group:not(:empty){box-shadow:0 0 0 2px ButtonText}}.maplibregl-ctrl-group button{width:29px;height:29px;display:block;padding:0;outline:none;border:0;box-sizing:border-box;background-color:transparent;cursor:pointer}.maplibregl-ctrl-group button+button{border-top:1px solid #ddd}.maplibregl-ctrl button .maplibregl-ctrl-icon{display:block;width:100%;height:100%;background-repeat:no-repeat;background-position:50%}@media (forced-colors:active){.maplibregl-ctrl-icon{background-color:transparent}.maplibregl-ctrl-group button+button{border-top:1px solid ButtonText}}.maplibregl-ctrl button::-moz-focus-inner{border:0;padding:0}.maplibregl-ctrl-attrib-button:focus,.maplibregl-ctrl-group button:focus{box-shadow:0 0 2px 2px #0096ff}.maplibregl-ctrl button:disabled{cursor:not-allowed}.maplibregl-ctrl button:disabled .maplibregl-ctrl-icon{opacity:.25}@media (hover:hover){.maplibregl-ctrl button:not(:disabled):hover{background-color:rgba(0,0,0,.05)}}.maplibregl-ctrl button:not(:disabled):active{background-color:rgba(0,0,0,.05)}.maplibregl-ctrl-group button:focus:focus-visible{box-shadow:0 0 2px 2px #0096ff}.maplibregl-ctrl-group button:focus:not(:focus-visible){box-shadow:none}.maplibregl-ctrl-group button:focus:first-child{border-radius:4px 4px 0 0}.maplibregl-ctrl-group button:focus:last-child{border-radius:0 0 4px 4px}.maplibregl-ctrl-group button:focus:only-child{border-radius:inherit}.maplibregl-ctrl button.maplibregl-ctrl-zoom-out .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23333' viewBox='0 0 29 29'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-zoom-in .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23333' viewBox='0 0 29 29'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5'/%3E%3C/svg%3E")}@media (forced-colors:active){.maplibregl-ctrl button.maplibregl-ctrl-zoom-out .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-zoom-in .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5'/%3E%3C/svg%3E")}}@media (forced-colors:active) and (prefers-color-scheme:light){.maplibregl-ctrl button.maplibregl-ctrl-zoom-out .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' viewBox='0 0 29 29'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-zoom-in .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' viewBox='0 0 29 29'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5'/%3E%3C/svg%3E")}}.maplibregl-ctrl button.maplibregl-ctrl-fullscreen .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23333' viewBox='0 0 29 29'%3E%3Cpath d='M24 16v5.5c0 1.75-.75 2.5-2.5 2.5H16v-1l3-1.5-4-5.5 1-1 5.5 4 1.5-3zM6 16l1.5 3 5.5-4 1 1-4 5.5 3 1.5v1H7.5C5.75 24 5 23.25 5 21.5V16zm7-11v1l-3 1.5 4 5.5-1 1-5.5-4L6 13H5V7.5C5 5.75 5.75 5 7.5 5zm11 2.5c0-1.75-.75-2.5-2.5-2.5H16v1l3 1.5-4 5.5 1 1 5.5-4 1.5 3h1z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-shrink .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' viewBox='0 0 29 29'%3E%3Cpath d='M18.5 16c-1.75 0-2.5.75-2.5 2.5V24h1l1.5-3 5.5 4 1-1-4-5.5 3-1.5v-1zM13 18.5c0-1.75-.75-2.5-2.5-2.5H5v1l3 1.5L4 24l1 1 5.5-4 1.5 3h1zm3-8c0 1.75.75 2.5 2.5 2.5H24v-1l-3-1.5L25 5l-1-1-5.5 4L17 5h-1zM10.5 13c1.75 0 2.5-.75 2.5-2.5V5h-1l-1.5 3L5 4 4 5l4 5.5L5 12v1z'/%3E%3C/svg%3E")}@media (forced-colors:active){.maplibregl-ctrl button.maplibregl-ctrl-fullscreen .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M24 16v5.5c0 1.75-.75 2.5-2.5 2.5H16v-1l3-1.5-4-5.5 1-1 5.5 4 1.5-3zM6 16l1.5 3 5.5-4 1 1-4 5.5 3 1.5v1H7.5C5.75 24 5 23.25 5 21.5V16zm7-11v1l-3 1.5 4 5.5-1 1-5.5-4L6 13H5V7.5C5 5.75 5.75 5 7.5 5zm11 2.5c0-1.75-.75-2.5-2.5-2.5H16v1l3 1.5-4 5.5 1 1 5.5-4 1.5 3h1z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-shrink .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M18.5 16c-1.75 0-2.5.75-2.5 2.5V24h1l1.5-3 5.5 4 1-1-4-5.5 3-1.5v-1zM13 18.5c0-1.75-.75-2.5-2.5-2.5H5v1l3 1.5L4 24l1 1 5.5-4 1.5 3h1zm3-8c0 1.75.75 2.5 2.5 2.5H24v-1l-3-1.5L25 5l-1-1-5.5 4L17 5h-1zM10.5 13c1.75 0 2.5-.75 2.5-2.5V5h-1l-1.5 3L5 4 4 5l4 5.5L5 12v1z'/%3E%3C/svg%3E")}}@media (forced-colors:active) and (prefers-color-scheme:light){.maplibregl-ctrl button.maplibregl-ctrl-fullscreen .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' viewBox='0 0 29 29'%3E%3Cpath d='M24 16v5.5c0 1.75-.75 2.5-2.5 2.5H16v-1l3-1.5-4-5.5 1-1 5.5 4 1.5-3zM6 16l1.5 3 5.5-4 1 1-4 5.5 3 1.5v1H7.5C5.75 24 5 23.25 5 21.5V16zm7-11v1l-3 1.5 4 5.5-1 1-5.5-4L6 13H5V7.5C5 5.75 5.75 5 7.5 5zm11 2.5c0-1.75-.75-2.5-2.5-2.5H16v1l3 1.5-4 5.5 1 1 5.5-4 1.5 3h1z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-shrink .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' viewBox='0 0 29 29'%3E%3Cpath d='M18.5 16c-1.75 0-2.5.75-2.5 2.5V24h1l1.5-3 5.5 4 1-1-4-5.5 3-1.5v-1zM13 18.5c0-1.75-.75-2.5-2.5-2.5H5v1l3 1.5L4 24l1 1 5.5-4 1.5 3h1zm3-8c0 1.75.75 2.5 2.5 2.5H24v-1l-3-1.5L25 5l-1-1-5.5 4L17 5h-1zM10.5 13c1.75 0 2.5-.75 2.5-2.5V5h-1l-1.5 3L5 4 4 5l4 5.5L5 12v1z'/%3E%3C/svg%3E")}}.maplibregl-ctrl button.maplibregl-ctrl-compass .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23333' viewBox='0 0 29 29'%3E%3Cpath d='m10.5 14 4-8 4 8z'/%3E%3Cpath fill='%23ccc' d='m10.5 16 4 8 4-8z'/%3E%3C/svg%3E")}@media (forced-colors:active){.maplibregl-ctrl button.maplibregl-ctrl-compass .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='m10.5 14 4-8 4 8z'/%3E%3Cpath fill='%23ccc' d='m10.5 16 4 8 4-8z'/%3E%3C/svg%3E")}}@media (forced-colors:active) and (prefers-color-scheme:light){.maplibregl-ctrl button.maplibregl-ctrl-compass .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' viewBox='0 0 29 29'%3E%3Cpath d='m10.5 14 4-8 4 8z'/%3E%3Cpath fill='%23ccc' d='m10.5 16 4 8 4-8z'/%3E%3C/svg%3E")}}.maplibregl-ctrl button.maplibregl-ctrl-globe .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22' fill='none' stroke='%23333' viewBox='0 0 22 22'%3E%3Ccircle cx='11' cy='11' r='8.5'/%3E%3Cpath d='M17.5 11c0 4.819-3.02 8.5-6.5 8.5S4.5 15.819 4.5 11 7.52 2.5 11 2.5s6.5 3.681 6.5 8.5Z'/%3E%3Cpath d='M13.5 11c0 2.447-.331 4.64-.853 6.206-.262.785-.562 1.384-.872 1.777-.314.399-.58.517-.775.517s-.461-.118-.775-.517c-.31-.393-.61-.992-.872-1.777C8.831 15.64 8.5 13.446 8.5 11s.331-4.64.853-6.206c.262-.785.562-1.384.872-1.777.314-.399.58-.517.775-.517s.461.118.775.517c.31.393.61.992.872 1.777.522 1.565.853 3.76.853 6.206Z'/%3E%3Cpath d='M11 7.5c-1.909 0-3.622-.166-4.845-.428-.616-.132-1.08-.283-1.379-.434a1.3 1.3 0 0 1-.224-.138q.07-.058.224-.138c.299-.151.763-.302 1.379-.434C7.378 5.666 9.091 5.5 11 5.5s3.622.166 4.845.428c.616.132 1.08.283 1.379.434.105.053.177.1.224.138q-.07.058-.224.138c-.299.151-.763.302-1.379.434-1.223.262-2.936.428-4.845.428ZM4.486 6.436ZM11 16.5c-1.909 0-3.622-.166-4.845-.428-.616-.132-1.08-.283-1.379-.434a1.3 1.3 0 0 1-.224-.138 1.3 1.3 0 0 1 .224-.138c.299-.151.763-.302 1.379-.434C7.378 14.666 9.091 14.5 11 14.5s3.622.166 4.845.428c.616.132 1.08.283 1.379.434.105.053.177.1.224.138a1.3 1.3 0 0 1-.224.138c-.299.151-.763.302-1.379.434-1.223.262-2.936.428-4.845.428Zm-6.514-1.064ZM11 12.5c-2.46 0-4.672-.222-6.255-.574-.796-.177-1.406-.38-1.805-.59a1.5 1.5 0 0 1-.39-.272.3.3 0 0 1-.047-.064.3.3 0 0 1 .048-.064c.066-.073.189-.167.389-.272.399-.21 1.009-.413 1.805-.59C6.328 9.722 8.54 9.5 11 9.5s4.672.222 6.256.574c.795.177 1.405.38 1.804.59.2.105.323.2.39.272a.3.3 0 0 1 .047.064.3.3 0 0 1-.048.064 1.4 1.4 0 0 1-.389.272c-.399.21-1.009.413-1.804.59-1.584.352-3.796.574-6.256.574Zm-8.501-1.51v.002zm0 .018v.002zm17.002.002v-.002zm0-.018v-.002z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-globe-enabled .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22' fill='none' stroke='%2333b5e5' viewBox='0 0 22 22'%3E%3Ccircle cx='11' cy='11' r='8.5'/%3E%3Cpath d='M17.5 11c0 4.819-3.02 8.5-6.5 8.5S4.5 15.819 4.5 11 7.52 2.5 11 2.5s6.5 3.681 6.5 8.5Z'/%3E%3Cpath d='M13.5 11c0 2.447-.331 4.64-.853 6.206-.262.785-.562 1.384-.872 1.777-.314.399-.58.517-.775.517s-.461-.118-.775-.517c-.31-.393-.61-.992-.872-1.777C8.831 15.64 8.5 13.446 8.5 11s.331-4.64.853-6.206c.262-.785.562-1.384.872-1.777.314-.399.58-.517.775-.517s.461.118.775.517c.31.393.61.992.872 1.777.522 1.565.853 3.76.853 6.206Z'/%3E%3Cpath d='M11 7.5c-1.909 0-3.622-.166-4.845-.428-.616-.132-1.08-.283-1.379-.434a1.3 1.3 0 0 1-.224-.138q.07-.058.224-.138c.299-.151.763-.302 1.379-.434C7.378 5.666 9.091 5.5 11 5.5s3.622.166 4.845.428c.616.132 1.08.283 1.379.434.105.053.177.1.224.138q-.07.058-.224.138c-.299.151-.763.302-1.379.434-1.223.262-2.936.428-4.845.428ZM4.486 6.436ZM11 16.5c-1.909 0-3.622-.166-4.845-.428-.616-.132-1.08-.283-1.379-.434a1.3 1.3 0 0 1-.224-.138 1.3 1.3 0 0 1 .224-.138c.299-.151.763-.302 1.379-.434C7.378 14.666 9.091 14.5 11 14.5s3.622.166 4.845.428c.616.132 1.08.283 1.379.434.105.053.177.1.224.138a1.3 1.3 0 0 1-.224.138c-.299.151-.763.302-1.379.434-1.223.262-2.936.428-4.845.428Zm-6.514-1.064ZM11 12.5c-2.46 0-4.672-.222-6.255-.574-.796-.177-1.406-.38-1.805-.59a1.5 1.5 0 0 1-.39-.272.3.3 0 0 1-.047-.064.3.3 0 0 1 .048-.064c.066-.073.189-.167.389-.272.399-.21 1.009-.413 1.805-.59C6.328 9.722 8.54 9.5 11 9.5s4.672.222 6.256.574c.795.177 1.405.38 1.804.59.2.105.323.2.39.272a.3.3 0 0 1 .047.064.3.3 0 0 1-.048.064 1.4 1.4 0 0 1-.389.272c-.399.21-1.009.413-1.804.59-1.584.352-3.796.574-6.256.574Zm-8.501-1.51v.002zm0 .018v.002zm17.002.002v-.002zm0-.018v-.002z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-terrain .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22' fill='%23333' viewBox='0 0 22 22'%3E%3Cpath d='m1.754 13.406 4.453-4.851 3.09 3.09 3.281 3.277.969-.969-3.309-3.312 3.844-4.121 6.148 6.886h1.082v-.855l-7.207-8.07-4.84 5.187L6.169 6.57l-5.48 5.965v.871ZM.688 16.844h20.625v1.375H.688Zm0 0'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-terrain-enabled .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22' fill='%2333b5e5' viewBox='0 0 22 22'%3E%3Cpath d='m1.754 13.406 4.453-4.851 3.09 3.09 3.281 3.277.969-.969-3.309-3.312 3.844-4.121 6.148 6.886h1.082v-.855l-7.207-8.07-4.84 5.187L6.169 6.57l-5.48 5.965v.871ZM.688 16.844h20.625v1.375H.688Zm0 0'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23333' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate:disabled .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23aaa' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3Cpath fill='red' d='m14 5 1 1-9 9-1-1z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%2333b5e5' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active-error .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23e58978' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%2333b5e5' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background-error .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23e54e33' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-waiting .maplibregl-ctrl-icon{animation:maplibregl-spin 2s linear infinite}@media (forced-colors:active){.maplibregl-ctrl button.maplibregl-ctrl-geolocate .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23fff' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate:disabled .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23999' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3Cpath fill='red' d='m14 5 1 1-9 9-1-1z'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%2333b5e5' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active-error .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23e58978' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%2333b5e5' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background-error .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23e54e33' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3C/svg%3E")}}@media (forced-colors:active) and (prefers-color-scheme:light){.maplibregl-ctrl button.maplibregl-ctrl-geolocate .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3C/svg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate:disabled .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23666' viewBox='0 0 20 20'%3E%3Cpath d='M10 4C9 4 9 5 9 5v.1A5 5 0 0 0 5.1 9H5s-1 0-1 1 1 1 1 1h.1A5 5 0 0 0 9 14.9v.1s0 1 1 1 1-1 1-1v-.1a5 5 0 0 0 3.9-3.9h.1s1 0 1-1-1-1-1-1h-.1A5 5 0 0 0 11 5.1V5s0-1-1-1m0 2.5a3.5 3.5 0 1 1 0 7 3.5 3.5 0 1 1 0-7'/%3E%3Ccircle cx='10' cy='10' r='2'/%3E%3Cpath fill='red' d='m14 5 1 1-9 9-1-1z'/%3E%3C/svg%3E")}}@keyframes maplibregl-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}a.maplibregl-ctrl-logo{width:88px;height:23px;margin:0 0 -4px -4px;display:block;background-repeat:no-repeat;cursor:pointer;overflow:hidden;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='88' height='23' fill='none'%3E%3Cpath fill='%23000' fill-opacity='.4' fill-rule='evenodd' d='M17.408 16.796h-1.827l2.501-12.095h.198l3.324 6.533.988 2.19.988-2.19 3.258-6.533h.181l2.6 12.095h-1.81l-1.218-5.644-.362-1.71-.658 1.71-2.929 5.644h-.098l-2.914-5.644-.757-1.71-.345 1.71zm1.958-3.42-.726 3.663a1.255 1.255 0 0 1-1.232 1.011h-1.827a1.255 1.255 0 0 1-1.229-1.509l2.501-12.095a1.255 1.255 0 0 1 1.23-1.001h.197a1.25 1.25 0 0 1 1.12.685l3.19 6.273 3.125-6.263a1.25 1.25 0 0 1 1.123-.695h.181a1.255 1.255 0 0 1 1.227.991l1.443 6.71a5 5 0 0 1 .314-.787l.009-.016a4.6 4.6 0 0 1 1.777-1.887c.782-.46 1.668-.667 2.611-.667a4.6 4.6 0 0 1 1.7.32l.306.134c.21-.16.474-.256.759-.256h1.694a1.255 1.255 0 0 1 1.212.925 1.255 1.255 0 0 1 1.212-.925h1.711c.284 0 .545.094.755.252.613-.3 1.312-.45 2.075-.45 1.356 0 2.557.445 3.482 1.4q.47.48.763 1.064V4.701a1.255 1.255 0 0 1 1.255-1.255h1.86A1.255 1.255 0 0 1 54.44 4.7v9.194h2.217c.19 0 .37.043.532.118v-4.77c0-.356.147-.678.385-.906a2.42 2.42 0 0 1-.682-1.71c0-.665.267-1.253.735-1.7a2.45 2.45 0 0 1 1.722-.674 2.43 2.43 0 0 1 1.705.675q.318.302.504.683V4.7a1.255 1.255 0 0 1 1.255-1.255h1.744A1.255 1.255 0 0 1 65.812 4.7v3.335a4.8 4.8 0 0 1 1.526-.246c.938 0 1.817.214 2.59.69a4.47 4.47 0 0 1 1.67 1.743v-.98a1.255 1.255 0 0 1 1.256-1.256h1.777c.233 0 .451.064.639.174a3.4 3.4 0 0 1 1.567-.372c.346 0 .861.02 1.285.232a1.25 1.25 0 0 1 .689 1.004 4.7 4.7 0 0 1 .853-.588c.795-.44 1.675-.647 2.61-.647 1.385 0 2.65.39 3.525 1.396.836.938 1.168 2.173 1.168 3.528q-.001.515-.056 1.051a1.255 1.255 0 0 1-.947 1.09l.408.952a1.255 1.255 0 0 1-.477 1.552c-.418.268-.92.463-1.458.612-.613.171-1.304.244-2.049.244-1.06 0-2.043-.207-2.886-.698l-.015-.008c-.798-.48-1.419-1.135-1.818-1.963l-.004-.008a5.8 5.8 0 0 1-.548-2.512q0-.429.053-.843a1.3 1.3 0 0 1-.333-.086l-.166-.004c-.223 0-.426.062-.643.228-.03.024-.142.139-.142.59v3.883a1.255 1.255 0 0 1-1.256 1.256h-1.777a1.255 1.255 0 0 1-1.256-1.256V15.69l-.032.057a4.8 4.8 0 0 1-1.86 1.833 5.04 5.04 0 0 1-2.484.634 4.5 4.5 0 0 1-1.935-.424 1.25 1.25 0 0 1-.764.258h-1.71a1.255 1.255 0 0 1-1.256-1.255V7.687a2.4 2.4 0 0 1-.428.625c.253.23.412.561.412.93v7.553a1.255 1.255 0 0 1-1.256 1.255h-1.843a1.25 1.25 0 0 1-.894-.373c-.228.23-.544.373-.894.373H51.32a1.255 1.255 0 0 1-1.256-1.255v-1.251l-.061.117a4.7 4.7 0 0 1-1.782 1.884 4.77 4.77 0 0 1-2.485.67 5.6 5.6 0 0 1-1.485-.188l.009 2.764a1.255 1.255 0 0 1-1.255 1.259h-1.729a1.255 1.255 0 0 1-1.255-1.255v-3.537a1.255 1.255 0 0 1-1.167.793h-1.679a1.25 1.25 0 0 1-.77-.263 4.5 4.5 0 0 1-1.945.429c-.885 0-1.724-.21-2.495-.632l-.017-.01a5 5 0 0 1-1.081-.836 1.255 1.255 0 0 1-1.254 1.312h-1.81a1.255 1.255 0 0 1-1.228-.99l-.782-3.625-2.044 3.939a1.25 1.25 0 0 1-1.115.676h-.098a1.25 1.25 0 0 1-1.116-.68l-2.061-3.994zM35.92 16.63l.207-.114.223-.15q.493-.356.735-.785l.061-.118.033 1.332h1.678V9.242h-1.694l-.033 1.267q-.133-.329-.526-.658l-.032-.028a3.2 3.2 0 0 0-.668-.428l-.27-.12a3.3 3.3 0 0 0-1.235-.23q-1.136-.001-1.974.493a3.36 3.36 0 0 0-1.3 1.382q-.445.89-.444 2.074 0 1.2.51 2.107a3.8 3.8 0 0 0 1.382 1.381 3.9 3.9 0 0 0 1.893.477q.795 0 1.455-.33zm-2.789-5.38q-.576.675-.575 1.762 0 1.102.559 1.794.576.675 1.645.675a2.25 2.25 0 0 0 .934-.19 2.2 2.2 0 0 0 .468-.29l.178-.161a2.2 2.2 0 0 0 .397-.561q.244-.5.244-1.15v-.115q0-.708-.296-1.267l-.043-.077a2.2 2.2 0 0 0-.633-.709l-.13-.086-.047-.028a2.1 2.1 0 0 0-1.073-.285q-1.052 0-1.629.692zm2.316 2.706c.163-.17.28-.407.28-.83v-.114c0-.292-.06-.508-.15-.68a.96.96 0 0 0-.353-.389.85.85 0 0 0-.464-.127c-.4 0-.56.114-.664.239l-.01.012c-.148.174-.275.45-.275.945 0 .506.122.801.27.99.097.11.266.224.68.224.303 0 .504-.09.687-.269zm7.545 1.705a2.6 2.6 0 0 0 .331.423q.319.33.755.548l.173.074q.65.255 1.49.255 1.02 0 1.844-.493a3.45 3.45 0 0 0 1.316-1.4q.493-.904.493-2.089 0-1.909-.988-2.913-.988-1.02-2.584-1.02-.898 0-1.575.347a3 3 0 0 0-.415.262l-.199.166a3.4 3.4 0 0 0-.64.82V9.242h-1.712v11.553h1.729l-.017-5.134zm.53-1.138q.206.29.48.5l.155.11.053.034q.51.296 1.119.297 1.07 0 1.645-.675.577-.69.576-1.762 0-1.119-.576-1.777-.558-.675-1.645-.675-.435 0-.835.16a2 2 0 0 0-.284.136 2 2 0 0 0-.363.254 2.2 2.2 0 0 0-.46.569l-.082.162a2.6 2.6 0 0 0-.213 1.072v.115q0 .707.296 1.267l.135.211zm.964-.818a1.1 1.1 0 0 0 .367.385.94.94 0 0 0 .476.118c.423 0 .59-.117.687-.23.159-.194.28-.478.28-.95 0-.53-.133-.8-.266-.952l-.021-.025c-.078-.094-.231-.221-.68-.221a1 1 0 0 0-.503.135l-.012.007a.86.86 0 0 0-.335.343c-.073.133-.132.324-.132.614v.115a1.4 1.4 0 0 0 .14.66zm15.7-6.222q.347-.346.346-.856a1.05 1.05 0 0 0-.345-.79 1.18 1.18 0 0 0-.84-.329q-.51 0-.855.33a1.05 1.05 0 0 0-.346.79q0 .51.346.855.345.346.856.346.51 0 .839-.346zm4.337 9.314.033-1.332q.191.403.59.747l.098.081a4 4 0 0 0 .316.224l.223.122a3.2 3.2 0 0 0 1.44.322 3.8 3.8 0 0 0 1.875-.477 3.5 3.5 0 0 0 1.382-1.366q.527-.89.526-2.09 0-1.184-.444-2.073a3.24 3.24 0 0 0-1.283-1.399q-.823-.51-1.942-.51a3.5 3.5 0 0 0-1.527.344l-.086.043-.165.09a3 3 0 0 0-.33.214q-.432.315-.656.707a2 2 0 0 0-.099.198l.082-1.283V4.701h-1.744v12.095zm.473-2.509a2.5 2.5 0 0 0 .566.7q.117.098.245.18l.144.08a2.1 2.1 0 0 0 .975.232q1.07 0 1.645-.675.576-.69.576-1.778 0-1.102-.576-1.777-.56-.691-1.645-.692a2.2 2.2 0 0 0-1.015.235q-.22.113-.415.282l-.15.142a2.1 2.1 0 0 0-.42.594q-.223.479-.223 1.1v.115q0 .705.293 1.26zm2.616-.293c.157-.191.28-.479.28-.967 0-.51-.13-.79-.276-.961l-.021-.026c-.082-.1-.232-.225-.67-.225a.87.87 0 0 0-.681.279l-.012.011c-.154.155-.274.38-.274.807v.115c0 .285.057.499.144.669a1.1 1.1 0 0 0 .367.405c.137.082.28.123.455.123.423 0 .59-.118.686-.23zm8.266-3.013q.345-.13.724-.14l.069-.002q.493 0 .642.099l.247-1.794q-.196-.099-.717-.099a2.3 2.3 0 0 0-.545.063 2 2 0 0 0-.411.148 2.2 2.2 0 0 0-.4.249 2.5 2.5 0 0 0-.485.499 2.7 2.7 0 0 0-.32.581l-.05.137v-1.48h-1.778v7.553h1.777v-3.884q0-.546.159-.943a1.5 1.5 0 0 1 .466-.636 2.5 2.5 0 0 1 .399-.253 2 2 0 0 1 .224-.099zm9.784 2.656.05-.922q0-1.743-.856-2.698-.838-.97-2.584-.97-1.119-.001-2.007.493a3.46 3.46 0 0 0-1.4 1.382q-.493.906-.493 2.106 0 1.07.428 1.975.428.89 1.332 1.432.906.526 2.255.526.973 0 1.668-.185l.044-.012.135-.04q.613-.184.984-.421l-.542-1.267q-.3.162-.642.274l-.297.087q-.51.131-1.3.131-.954 0-1.497-.444a1.6 1.6 0 0 1-.192-.193q-.366-.44-.512-1.234l-.004-.021zm-5.427-1.256-.003.022h3.752v-.138q-.011-.727-.288-1.118a1 1 0 0 0-.156-.176q-.46-.428-1.316-.428-.986 0-1.494.604-.379.45-.494 1.234zm-27.053 2.77V4.7h-1.86v12.095h5.333V15.15zm7.103-5.908v7.553h-1.843V9.242h1.843z'/%3E%3Cpath fill='%23fff' d='m19.63 11.151-.757-1.71-.345 1.71-1.12 5.644h-1.827L18.083 4.7h.197l3.325 6.533.988 2.19.988-2.19L26.839 4.7h.181l2.6 12.095h-1.81l-1.218-5.644-.362-1.71-.658 1.71-2.93 5.644h-.098l-2.913-5.644zm14.836 5.81q-1.02 0-1.893-.478a3.8 3.8 0 0 1-1.381-1.382q-.51-.906-.51-2.106 0-1.185.444-2.074a3.36 3.36 0 0 1 1.3-1.382q.839-.494 1.974-.494a3.3 3.3 0 0 1 1.234.231 3.3 3.3 0 0 1 .97.575q.396.33.527.659l.033-1.267h1.694v7.553H37.18l-.033-1.332q-.279.593-1.02 1.053a3.17 3.17 0 0 1-1.662.444zm.296-1.482q.938 0 1.58-.642.642-.66.642-1.711v-.115q0-.708-.296-1.267a2.2 2.2 0 0 0-.807-.872 2.1 2.1 0 0 0-1.119-.313q-1.053 0-1.629.692-.575.675-.575 1.76 0 1.103.559 1.795.577.675 1.645.675zm6.521-6.237h1.711v1.4q.906-1.597 2.83-1.597 1.596 0 2.584 1.02.988 1.005.988 2.914 0 1.185-.493 2.09a3.46 3.46 0 0 1-1.316 1.399 3.5 3.5 0 0 1-1.844.493q-.954 0-1.662-.329a2.67 2.67 0 0 1-1.086-.97l.017 5.134h-1.728zm4.048 6.22q1.07 0 1.645-.674.577-.69.576-1.762 0-1.119-.576-1.777-.558-.675-1.645-.675-.592 0-1.12.296-.51.28-.822.823-.296.527-.296 1.234v.115q0 .708.296 1.267.313.543.823.855.51.296 1.119.297z'/%3E%3Cpath fill='%23e1e3e9' d='M51.325 4.7h1.86v10.45h3.473v1.646h-5.333zm7.12 4.542h1.843v7.553h-1.843zm.905-1.415a1.16 1.16 0 0 1-.856-.346 1.17 1.17 0 0 1-.346-.856 1.05 1.05 0 0 1 .346-.79q.346-.329.856-.329.494 0 .839.33a1.05 1.05 0 0 1 .345.79 1.16 1.16 0 0 1-.345.855q-.33.346-.84.346zm7.875 9.133a3.17 3.17 0 0 1-1.662-.444q-.723-.46-1.004-1.053l-.033 1.332h-1.71V4.701h1.743v4.657l-.082 1.283q.279-.658 1.086-1.119a3.5 3.5 0 0 1 1.778-.477q1.119 0 1.942.51a3.24 3.24 0 0 1 1.283 1.4q.445.888.444 2.072 0 1.201-.526 2.09a3.5 3.5 0 0 1-1.382 1.366 3.8 3.8 0 0 1-1.876.477zm-.296-1.481q1.069 0 1.645-.675.577-.69.577-1.778 0-1.102-.577-1.776-.56-.691-1.645-.692a2.12 2.12 0 0 0-1.58.659q-.642.641-.642 1.694v.115q0 .71.296 1.267a2.4 2.4 0 0 0 .807.872 2.1 2.1 0 0 0 1.119.313zm5.927-6.237h1.777v1.481q.263-.757.856-1.217a2.14 2.14 0 0 1 1.349-.46q.527 0 .724.098l-.247 1.794q-.149-.099-.642-.099-.774 0-1.416.494-.626.493-.626 1.58v3.883h-1.777V9.242zm9.534 7.718q-1.35 0-2.255-.526-.904-.543-1.332-1.432a4.6 4.6 0 0 1-.428-1.975q0-1.2.493-2.106a3.46 3.46 0 0 1 1.4-1.382q.889-.495 2.007-.494 1.744 0 2.584.97.855.956.856 2.7 0 .444-.05.92h-5.43q.18 1.005.708 1.45.542.443 1.497.443.79 0 1.3-.131a4 4 0 0 0 .938-.362l.542 1.267q-.411.263-1.119.46-.708.198-1.711.197zm1.596-4.558q.016-1.02-.444-1.432-.46-.428-1.316-.428-1.728 0-1.991 1.86z'/%3E%3Cpath d='M5.074 15.948a.484.657 0 0 0-.486.659v1.84a.484.657 0 0 0 .486.659h4.101a.484.657 0 0 0 .486-.659v-1.84a.484.657 0 0 0-.486-.659zm3.56 1.16H5.617v.838h3.017z' style='fill:%23fff;fill-rule:evenodd;stroke-width:1.03600001'/%3E%3Cg style='stroke-width:1.12603545'%3E%3Cpath d='M-9.408-1.416c-3.833-.025-7.056 2.912-7.08 6.615-.02 3.08 1.653 4.832 3.107 6.268.903.892 1.721 1.74 2.32 2.902l-.525-.004c-.543-.003-.992.304-1.24.639a1.87 1.87 0 0 0-.362 1.121l-.011 1.877c-.003.402.104.787.347 1.125.244.338.688.653 1.23.656l4.142.028c.542.003.99-.306 1.238-.641a1.87 1.87 0 0 0 .363-1.121l.012-1.875a1.87 1.87 0 0 0-.348-1.127c-.243-.338-.688-.653-1.23-.656l-.518-.004c.597-1.145 1.425-1.983 2.348-2.87 1.473-1.414 3.18-3.149 3.2-6.226-.016-3.59-2.923-6.684-6.993-6.707m-.006 1.1v.002c3.274.02 5.92 2.532 5.9 5.6-.017 2.706-1.39 4.026-2.863 5.44-1.034.994-2.118 2.033-2.814 3.633-.018.041-.052.055-.075.065q-.013.004-.02.01a.34.34 0 0 1-.226.084.34.34 0 0 1-.224-.086l-.092-.077c-.699-1.615-1.768-2.669-2.781-3.67-1.454-1.435-2.797-2.762-2.78-5.478.02-3.067 2.7-5.545 5.975-5.523m-.02 2.826c-1.62-.01-2.944 1.315-2.955 2.96-.01 1.646 1.295 2.988 2.916 2.999h.002c1.621.01 2.943-1.316 2.953-2.961.011-1.646-1.294-2.988-2.916-2.998m-.005 1.1c1.017.006 1.829.83 1.822 1.89s-.83 1.874-1.848 1.867c-1.018-.006-1.829-.83-1.822-1.89s.83-1.874 1.848-1.868m-2.155 11.857 4.14.025c.271.002.49.305.487.676l-.013 1.875c-.003.37-.224.67-.495.668l-4.14-.025c-.27-.002-.487-.306-.485-.676l.012-1.875c.003-.37.224-.67.494-.668' style='color:%23000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:%23000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:evenodd;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:%23000;solid-opacity:1;vector-effect:none;fill:%23000;fill-opacity:.4;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto' transform='translate(15.553 2.85)scale(.88807)'/%3E%3Cpath d='M-9.415-.316C-12.69-.338-15.37 2.14-15.39 5.207c-.017 2.716 1.326 4.041 2.78 5.477 1.013 1 2.081 2.055 2.78 3.67l.092.076a.34.34 0 0 0 .225.086.34.34 0 0 0 .227-.083l.019-.01c.022-.009.057-.024.074-.064.697-1.6 1.78-2.64 2.814-3.634 1.473-1.414 2.847-2.733 2.864-5.44.02-3.067-2.627-5.58-5.901-5.601m-.057 8.784c1.621.011 2.944-1.315 2.955-2.96.01-1.646-1.295-2.988-2.916-2.999-1.622-.01-2.945 1.315-2.955 2.96s1.295 2.989 2.916 3' style='clip-rule:evenodd;fill:%23e1e3e9;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:.4' transform='translate(15.553 2.85)scale(.88807)'/%3E%3Cpath d='M-11.594 15.465c-.27-.002-.492.297-.494.668l-.012 1.876c-.003.371.214.673.485.675l4.14.027c.271.002.492-.298.495-.668l.012-1.877c.003-.37-.215-.672-.485-.674z' style='clip-rule:evenodd;fill:%23fff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:.4' transform='translate(15.553 2.85)scale(.88807)'/%3E%3C/g%3E%3C/svg%3E")}a.maplibregl-ctrl-logo.maplibregl-compact{width:14px}@media (forced-colors:active){a.maplibregl-ctrl-logo{background-color:transparent;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='88' height='23' fill='none'%3E%3Cpath fill='%23000' fill-opacity='.4' fill-rule='evenodd' d='M17.408 16.796h-1.827l2.501-12.095h.198l3.324 6.533.988 2.19.988-2.19 3.258-6.533h.181l2.6 12.095h-1.81l-1.218-5.644-.362-1.71-.658 1.71-2.929 5.644h-.098l-2.914-5.644-.757-1.71-.345 1.71zm1.958-3.42-.726 3.663a1.255 1.255 0 0 1-1.232 1.011h-1.827a1.255 1.255 0 0 1-1.229-1.509l2.501-12.095a1.255 1.255 0 0 1 1.23-1.001h.197a1.25 1.25 0 0 1 1.12.685l3.19 6.273 3.125-6.263a1.25 1.25 0 0 1 1.123-.695h.181a1.255 1.255 0 0 1 1.227.991l1.443 6.71a5 5 0 0 1 .314-.787l.009-.016a4.6 4.6 0 0 1 1.777-1.887c.782-.46 1.668-.667 2.611-.667a4.6 4.6 0 0 1 1.7.32l.306.134c.21-.16.474-.256.759-.256h1.694a1.255 1.255 0 0 1 1.212.925 1.255 1.255 0 0 1 1.212-.925h1.711c.284 0 .545.094.755.252.613-.3 1.312-.45 2.075-.45 1.356 0 2.557.445 3.482 1.4q.47.48.763 1.064V4.701a1.255 1.255 0 0 1 1.255-1.255h1.86A1.255 1.255 0 0 1 54.44 4.7v9.194h2.217c.19 0 .37.043.532.118v-4.77c0-.356.147-.678.385-.906a2.42 2.42 0 0 1-.682-1.71c0-.665.267-1.253.735-1.7a2.45 2.45 0 0 1 1.722-.674 2.43 2.43 0 0 1 1.705.675q.318.302.504.683V4.7a1.255 1.255 0 0 1 1.255-1.255h1.744A1.255 1.255 0 0 1 65.812 4.7v3.335a4.8 4.8 0 0 1 1.526-.246c.938 0 1.817.214 2.59.69a4.47 4.47 0 0 1 1.67 1.743v-.98a1.255 1.255 0 0 1 1.256-1.256h1.777c.233 0 .451.064.639.174a3.4 3.4 0 0 1 1.567-.372c.346 0 .861.02 1.285.232a1.25 1.25 0 0 1 .689 1.004 4.7 4.7 0 0 1 .853-.588c.795-.44 1.675-.647 2.61-.647 1.385 0 2.65.39 3.525 1.396.836.938 1.168 2.173 1.168 3.528q-.001.515-.056 1.051a1.255 1.255 0 0 1-.947 1.09l.408.952a1.255 1.255 0 0 1-.477 1.552c-.418.268-.92.463-1.458.612-.613.171-1.304.244-2.049.244-1.06 0-2.043-.207-2.886-.698l-.015-.008c-.798-.48-1.419-1.135-1.818-1.963l-.004-.008a5.8 5.8 0 0 1-.548-2.512q0-.429.053-.843a1.3 1.3 0 0 1-.333-.086l-.166-.004c-.223 0-.426.062-.643.228-.03.024-.142.139-.142.59v3.883a1.255 1.255 0 0 1-1.256 1.256h-1.777a1.255 1.255 0 0 1-1.256-1.256V15.69l-.032.057a4.8 4.8 0 0 1-1.86 1.833 5.04 5.04 0 0 1-2.484.634 4.5 4.5 0 0 1-1.935-.424 1.25 1.25 0 0 1-.764.258h-1.71a1.255 1.255 0 0 1-1.256-1.255V7.687a2.4 2.4 0 0 1-.428.625c.253.23.412.561.412.93v7.553a1.255 1.255 0 0 1-1.256 1.255h-1.843a1.25 1.25 0 0 1-.894-.373c-.228.23-.544.373-.894.373H51.32a1.255 1.255 0 0 1-1.256-1.255v-1.251l-.061.117a4.7 4.7 0 0 1-1.782 1.884 4.77 4.77 0 0 1-2.485.67 5.6 5.6 0 0 1-1.485-.188l.009 2.764a1.255 1.255 0 0 1-1.255 1.259h-1.729a1.255 1.255 0 0 1-1.255-1.255v-3.537a1.255 1.255 0 0 1-1.167.793h-1.679a1.25 1.25 0 0 1-.77-.263 4.5 4.5 0 0 1-1.945.429c-.885 0-1.724-.21-2.495-.632l-.017-.01a5 5 0 0 1-1.081-.836 1.255 1.255 0 0 1-1.254 1.312h-1.81a1.255 1.255 0 0 1-1.228-.99l-.782-3.625-2.044 3.939a1.25 1.25 0 0 1-1.115.676h-.098a1.25 1.25 0 0 1-1.116-.68l-2.061-3.994zM35.92 16.63l.207-.114.223-.15q.493-.356.735-.785l.061-.118.033 1.332h1.678V9.242h-1.694l-.033 1.267q-.133-.329-.526-.658l-.032-.028a3.2 3.2 0 0 0-.668-.428l-.27-.12a3.3 3.3 0 0 0-1.235-.23q-1.136-.001-1.974.493a3.36 3.36 0 0 0-1.3 1.382q-.445.89-.444 2.074 0 1.2.51 2.107a3.8 3.8 0 0 0 1.382 1.381 3.9 3.9 0 0 0 1.893.477q.795 0 1.455-.33zm-2.789-5.38q-.576.675-.575 1.762 0 1.102.559 1.794.576.675 1.645.675a2.25 2.25 0 0 0 .934-.19 2.2 2.2 0 0 0 .468-.29l.178-.161a2.2 2.2 0 0 0 .397-.561q.244-.5.244-1.15v-.115q0-.708-.296-1.267l-.043-.077a2.2 2.2 0 0 0-.633-.709l-.13-.086-.047-.028a2.1 2.1 0 0 0-1.073-.285q-1.052 0-1.629.692zm2.316 2.706c.163-.17.28-.407.28-.83v-.114c0-.292-.06-.508-.15-.68a.96.96 0 0 0-.353-.389.85.85 0 0 0-.464-.127c-.4 0-.56.114-.664.239l-.01.012c-.148.174-.275.45-.275.945 0 .506.122.801.27.99.097.11.266.224.68.224.303 0 .504-.09.687-.269zm7.545 1.705a2.6 2.6 0 0 0 .331.423q.319.33.755.548l.173.074q.65.255 1.49.255 1.02 0 1.844-.493a3.45 3.45 0 0 0 1.316-1.4q.493-.904.493-2.089 0-1.909-.988-2.913-.988-1.02-2.584-1.02-.898 0-1.575.347a3 3 0 0 0-.415.262l-.199.166a3.4 3.4 0 0 0-.64.82V9.242h-1.712v11.553h1.729l-.017-5.134zm.53-1.138q.206.29.48.5l.155.11.053.034q.51.296 1.119.297 1.07 0 1.645-.675.577-.69.576-1.762 0-1.119-.576-1.777-.558-.675-1.645-.675-.435 0-.835.16a2 2 0 0 0-.284.136 2 2 0 0 0-.363.254 2.2 2.2 0 0 0-.46.569l-.082.162a2.6 2.6 0 0 0-.213 1.072v.115q0 .707.296 1.267l.135.211zm.964-.818a1.1 1.1 0 0 0 .367.385.94.94 0 0 0 .476.118c.423 0 .59-.117.687-.23.159-.194.28-.478.28-.95 0-.53-.133-.8-.266-.952l-.021-.025c-.078-.094-.231-.221-.68-.221a1 1 0 0 0-.503.135l-.012.007a.86.86 0 0 0-.335.343c-.073.133-.132.324-.132.614v.115a1.4 1.4 0 0 0 .14.66zm15.7-6.222q.347-.346.346-.856a1.05 1.05 0 0 0-.345-.79 1.18 1.18 0 0 0-.84-.329q-.51 0-.855.33a1.05 1.05 0 0 0-.346.79q0 .51.346.855.345.346.856.346.51 0 .839-.346zm4.337 9.314.033-1.332q.191.403.59.747l.098.081a4 4 0 0 0 .316.224l.223.122a3.2 3.2 0 0 0 1.44.322 3.8 3.8 0 0 0 1.875-.477 3.5 3.5 0 0 0 1.382-1.366q.527-.89.526-2.09 0-1.184-.444-2.073a3.24 3.24 0 0 0-1.283-1.399q-.823-.51-1.942-.51a3.5 3.5 0 0 0-1.527.344l-.086.043-.165.09a3 3 0 0 0-.33.214q-.432.315-.656.707a2 2 0 0 0-.099.198l.082-1.283V4.701h-1.744v12.095zm.473-2.509a2.5 2.5 0 0 0 .566.7q.117.098.245.18l.144.08a2.1 2.1 0 0 0 .975.232q1.07 0 1.645-.675.576-.69.576-1.778 0-1.102-.576-1.777-.56-.691-1.645-.692a2.2 2.2 0 0 0-1.015.235q-.22.113-.415.282l-.15.142a2.1 2.1 0 0 0-.42.594q-.223.479-.223 1.1v.115q0 .705.293 1.26zm2.616-.293c.157-.191.28-.479.28-.967 0-.51-.13-.79-.276-.961l-.021-.026c-.082-.1-.232-.225-.67-.225a.87.87 0 0 0-.681.279l-.012.011c-.154.155-.274.38-.274.807v.115c0 .285.057.499.144.669a1.1 1.1 0 0 0 .367.405c.137.082.28.123.455.123.423 0 .59-.118.686-.23zm8.266-3.013q.345-.13.724-.14l.069-.002q.493 0 .642.099l.247-1.794q-.196-.099-.717-.099a2.3 2.3 0 0 0-.545.063 2 2 0 0 0-.411.148 2.2 2.2 0 0 0-.4.249 2.5 2.5 0 0 0-.485.499 2.7 2.7 0 0 0-.32.581l-.05.137v-1.48h-1.778v7.553h1.777v-3.884q0-.546.159-.943a1.5 1.5 0 0 1 .466-.636 2.5 2.5 0 0 1 .399-.253 2 2 0 0 1 .224-.099zm9.784 2.656.05-.922q0-1.743-.856-2.698-.838-.97-2.584-.97-1.119-.001-2.007.493a3.46 3.46 0 0 0-1.4 1.382q-.493.906-.493 2.106 0 1.07.428 1.975.428.89 1.332 1.432.906.526 2.255.526.973 0 1.668-.185l.044-.012.135-.04q.613-.184.984-.421l-.542-1.267q-.3.162-.642.274l-.297.087q-.51.131-1.3.131-.954 0-1.497-.444a1.6 1.6 0 0 1-.192-.193q-.366-.44-.512-1.234l-.004-.021zm-5.427-1.256-.003.022h3.752v-.138q-.011-.727-.288-1.118a1 1 0 0 0-.156-.176q-.46-.428-1.316-.428-.986 0-1.494.604-.379.45-.494 1.234zm-27.053 2.77V4.7h-1.86v12.095h5.333V15.15zm7.103-5.908v7.553h-1.843V9.242h1.843z'/%3E%3Cpath fill='%23fff' d='m19.63 11.151-.757-1.71-.345 1.71-1.12 5.644h-1.827L18.083 4.7h.197l3.325 6.533.988 2.19.988-2.19L26.839 4.7h.181l2.6 12.095h-1.81l-1.218-5.644-.362-1.71-.658 1.71-2.93 5.644h-.098l-2.913-5.644zm14.836 5.81q-1.02 0-1.893-.478a3.8 3.8 0 0 1-1.381-1.382q-.51-.906-.51-2.106 0-1.185.444-2.074a3.36 3.36 0 0 1 1.3-1.382q.839-.494 1.974-.494a3.3 3.3 0 0 1 1.234.231 3.3 3.3 0 0 1 .97.575q.396.33.527.659l.033-1.267h1.694v7.553H37.18l-.033-1.332q-.279.593-1.02 1.053a3.17 3.17 0 0 1-1.662.444zm.296-1.482q.938 0 1.58-.642.642-.66.642-1.711v-.115q0-.708-.296-1.267a2.2 2.2 0 0 0-.807-.872 2.1 2.1 0 0 0-1.119-.313q-1.053 0-1.629.692-.575.675-.575 1.76 0 1.103.559 1.795.577.675 1.645.675zm6.521-6.237h1.711v1.4q.906-1.597 2.83-1.597 1.596 0 2.584 1.02.988 1.005.988 2.914 0 1.185-.493 2.09a3.46 3.46 0 0 1-1.316 1.399 3.5 3.5 0 0 1-1.844.493q-.954 0-1.662-.329a2.67 2.67 0 0 1-1.086-.97l.017 5.134h-1.728zm4.048 6.22q1.07 0 1.645-.674.577-.69.576-1.762 0-1.119-.576-1.777-.558-.675-1.645-.675-.592 0-1.12.296-.51.28-.822.823-.296.527-.296 1.234v.115q0 .708.296 1.267.313.543.823.855.51.296 1.119.297z'/%3E%3Cpath fill='%23e1e3e9' d='M51.325 4.7h1.86v10.45h3.473v1.646h-5.333zm7.12 4.542h1.843v7.553h-1.843zm.905-1.415a1.16 1.16 0 0 1-.856-.346 1.17 1.17 0 0 1-.346-.856 1.05 1.05 0 0 1 .346-.79q.346-.329.856-.329.494 0 .839.33a1.05 1.05 0 0 1 .345.79 1.16 1.16 0 0 1-.345.855q-.33.346-.84.346zm7.875 9.133a3.17 3.17 0 0 1-1.662-.444q-.723-.46-1.004-1.053l-.033 1.332h-1.71V4.701h1.743v4.657l-.082 1.283q.279-.658 1.086-1.119a3.5 3.5 0 0 1 1.778-.477q1.119 0 1.942.51a3.24 3.24 0 0 1 1.283 1.4q.445.888.444 2.072 0 1.201-.526 2.09a3.5 3.5 0 0 1-1.382 1.366 3.8 3.8 0 0 1-1.876.477zm-.296-1.481q1.069 0 1.645-.675.577-.69.577-1.778 0-1.102-.577-1.776-.56-.691-1.645-.692a2.12 2.12 0 0 0-1.58.659q-.642.641-.642 1.694v.115q0 .71.296 1.267a2.4 2.4 0 0 0 .807.872 2.1 2.1 0 0 0 1.119.313zm5.927-6.237h1.777v1.481q.263-.757.856-1.217a2.14 2.14 0 0 1 1.349-.46q.527 0 .724.098l-.247 1.794q-.149-.099-.642-.099-.774 0-1.416.494-.626.493-.626 1.58v3.883h-1.777V9.242zm9.534 7.718q-1.35 0-2.255-.526-.904-.543-1.332-1.432a4.6 4.6 0 0 1-.428-1.975q0-1.2.493-2.106a3.46 3.46 0 0 1 1.4-1.382q.889-.495 2.007-.494 1.744 0 2.584.97.855.956.856 2.7 0 .444-.05.92h-5.43q.18 1.005.708 1.45.542.443 1.497.443.79 0 1.3-.131a4 4 0 0 0 .938-.362l.542 1.267q-.411.263-1.119.46-.708.198-1.711.197zm1.596-4.558q.016-1.02-.444-1.432-.46-.428-1.316-.428-1.728 0-1.991 1.86z'/%3E%3Cpath d='M5.074 15.948a.484.657 0 0 0-.486.659v1.84a.484.657 0 0 0 .486.659h4.101a.484.657 0 0 0 .486-.659v-1.84a.484.657 0 0 0-.486-.659zm3.56 1.16H5.617v.838h3.017z' style='fill:%23fff;fill-rule:evenodd;stroke-width:1.03600001'/%3E%3Cg style='stroke-width:1.12603545'%3E%3Cpath d='M-9.408-1.416c-3.833-.025-7.056 2.912-7.08 6.615-.02 3.08 1.653 4.832 3.107 6.268.903.892 1.721 1.74 2.32 2.902l-.525-.004c-.543-.003-.992.304-1.24.639a1.87 1.87 0 0 0-.362 1.121l-.011 1.877c-.003.402.104.787.347 1.125.244.338.688.653 1.23.656l4.142.028c.542.003.99-.306 1.238-.641a1.87 1.87 0 0 0 .363-1.121l.012-1.875a1.87 1.87 0 0 0-.348-1.127c-.243-.338-.688-.653-1.23-.656l-.518-.004c.597-1.145 1.425-1.983 2.348-2.87 1.473-1.414 3.18-3.149 3.2-6.226-.016-3.59-2.923-6.684-6.993-6.707m-.006 1.1v.002c3.274.02 5.92 2.532 5.9 5.6-.017 2.706-1.39 4.026-2.863 5.44-1.034.994-2.118 2.033-2.814 3.633-.018.041-.052.055-.075.065q-.013.004-.02.01a.34.34 0 0 1-.226.084.34.34 0 0 1-.224-.086l-.092-.077c-.699-1.615-1.768-2.669-2.781-3.67-1.454-1.435-2.797-2.762-2.78-5.478.02-3.067 2.7-5.545 5.975-5.523m-.02 2.826c-1.62-.01-2.944 1.315-2.955 2.96-.01 1.646 1.295 2.988 2.916 2.999h.002c1.621.01 2.943-1.316 2.953-2.961.011-1.646-1.294-2.988-2.916-2.998m-.005 1.1c1.017.006 1.829.83 1.822 1.89s-.83 1.874-1.848 1.867c-1.018-.006-1.829-.83-1.822-1.89s.83-1.874 1.848-1.868m-2.155 11.857 4.14.025c.271.002.49.305.487.676l-.013 1.875c-.003.37-.224.67-.495.668l-4.14-.025c-.27-.002-.487-.306-.485-.676l.012-1.875c.003-.37.224-.67.494-.668' style='color:%23000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:%23000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:evenodd;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:%23000;solid-opacity:1;vector-effect:none;fill:%23000;fill-opacity:.4;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto' transform='translate(15.553 2.85)scale(.88807)'/%3E%3Cpath d='M-9.415-.316C-12.69-.338-15.37 2.14-15.39 5.207c-.017 2.716 1.326 4.041 2.78 5.477 1.013 1 2.081 2.055 2.78 3.67l.092.076a.34.34 0 0 0 .225.086.34.34 0 0 0 .227-.083l.019-.01c.022-.009.057-.024.074-.064.697-1.6 1.78-2.64 2.814-3.634 1.473-1.414 2.847-2.733 2.864-5.44.02-3.067-2.627-5.58-5.901-5.601m-.057 8.784c1.621.011 2.944-1.315 2.955-2.96.01-1.646-1.295-2.988-2.916-2.999-1.622-.01-2.945 1.315-2.955 2.96s1.295 2.989 2.916 3' style='clip-rule:evenodd;fill:%23e1e3e9;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:.4' transform='translate(15.553 2.85)scale(.88807)'/%3E%3Cpath d='M-11.594 15.465c-.27-.002-.492.297-.494.668l-.012 1.876c-.003.371.214.673.485.675l4.14.027c.271.002.492-.298.495-.668l.012-1.877c.003-.37-.215-.672-.485-.674z' style='clip-rule:evenodd;fill:%23fff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:.4' transform='translate(15.553 2.85)scale(.88807)'/%3E%3C/g%3E%3C/svg%3E")}}@media (forced-colors:active) and (prefers-color-scheme:light){a.maplibregl-ctrl-logo{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='88' height='23' fill='none'%3E%3Cpath fill='%23000' fill-opacity='.4' fill-rule='evenodd' d='M17.408 16.796h-1.827l2.501-12.095h.198l3.324 6.533.988 2.19.988-2.19 3.258-6.533h.181l2.6 12.095h-1.81l-1.218-5.644-.362-1.71-.658 1.71-2.929 5.644h-.098l-2.914-5.644-.757-1.71-.345 1.71zm1.958-3.42-.726 3.663a1.255 1.255 0 0 1-1.232 1.011h-1.827a1.255 1.255 0 0 1-1.229-1.509l2.501-12.095a1.255 1.255 0 0 1 1.23-1.001h.197a1.25 1.25 0 0 1 1.12.685l3.19 6.273 3.125-6.263a1.25 1.25 0 0 1 1.123-.695h.181a1.255 1.255 0 0 1 1.227.991l1.443 6.71a5 5 0 0 1 .314-.787l.009-.016a4.6 4.6 0 0 1 1.777-1.887c.782-.46 1.668-.667 2.611-.667a4.6 4.6 0 0 1 1.7.32l.306.134c.21-.16.474-.256.759-.256h1.694a1.255 1.255 0 0 1 1.212.925 1.255 1.255 0 0 1 1.212-.925h1.711c.284 0 .545.094.755.252.613-.3 1.312-.45 2.075-.45 1.356 0 2.557.445 3.482 1.4q.47.48.763 1.064V4.701a1.255 1.255 0 0 1 1.255-1.255h1.86A1.255 1.255 0 0 1 54.44 4.7v9.194h2.217c.19 0 .37.043.532.118v-4.77c0-.356.147-.678.385-.906a2.42 2.42 0 0 1-.682-1.71c0-.665.267-1.253.735-1.7a2.45 2.45 0 0 1 1.722-.674 2.43 2.43 0 0 1 1.705.675q.318.302.504.683V4.7a1.255 1.255 0 0 1 1.255-1.255h1.744A1.255 1.255 0 0 1 65.812 4.7v3.335a4.8 4.8 0 0 1 1.526-.246c.938 0 1.817.214 2.59.69a4.47 4.47 0 0 1 1.67 1.743v-.98a1.255 1.255 0 0 1 1.256-1.256h1.777c.233 0 .451.064.639.174a3.4 3.4 0 0 1 1.567-.372c.346 0 .861.02 1.285.232a1.25 1.25 0 0 1 .689 1.004 4.7 4.7 0 0 1 .853-.588c.795-.44 1.675-.647 2.61-.647 1.385 0 2.65.39 3.525 1.396.836.938 1.168 2.173 1.168 3.528q-.001.515-.056 1.051a1.255 1.255 0 0 1-.947 1.09l.408.952a1.255 1.255 0 0 1-.477 1.552c-.418.268-.92.463-1.458.612-.613.171-1.304.244-2.049.244-1.06 0-2.043-.207-2.886-.698l-.015-.008c-.798-.48-1.419-1.135-1.818-1.963l-.004-.008a5.8 5.8 0 0 1-.548-2.512q0-.429.053-.843a1.3 1.3 0 0 1-.333-.086l-.166-.004c-.223 0-.426.062-.643.228-.03.024-.142.139-.142.59v3.883a1.255 1.255 0 0 1-1.256 1.256h-1.777a1.255 1.255 0 0 1-1.256-1.256V15.69l-.032.057a4.8 4.8 0 0 1-1.86 1.833 5.04 5.04 0 0 1-2.484.634 4.5 4.5 0 0 1-1.935-.424 1.25 1.25 0 0 1-.764.258h-1.71a1.255 1.255 0 0 1-1.256-1.255V7.687a2.4 2.4 0 0 1-.428.625c.253.23.412.561.412.93v7.553a1.255 1.255 0 0 1-1.256 1.255h-1.843a1.25 1.25 0 0 1-.894-.373c-.228.23-.544.373-.894.373H51.32a1.255 1.255 0 0 1-1.256-1.255v-1.251l-.061.117a4.7 4.7 0 0 1-1.782 1.884 4.77 4.77 0 0 1-2.485.67 5.6 5.6 0 0 1-1.485-.188l.009 2.764a1.255 1.255 0 0 1-1.255 1.259h-1.729a1.255 1.255 0 0 1-1.255-1.255v-3.537a1.255 1.255 0 0 1-1.167.793h-1.679a1.25 1.25 0 0 1-.77-.263 4.5 4.5 0 0 1-1.945.429c-.885 0-1.724-.21-2.495-.632l-.017-.01a5 5 0 0 1-1.081-.836 1.255 1.255 0 0 1-1.254 1.312h-1.81a1.255 1.255 0 0 1-1.228-.99l-.782-3.625-2.044 3.939a1.25 1.25 0 0 1-1.115.676h-.098a1.25 1.25 0 0 1-1.116-.68l-2.061-3.994zM35.92 16.63l.207-.114.223-.15q.493-.356.735-.785l.061-.118.033 1.332h1.678V9.242h-1.694l-.033 1.267q-.133-.329-.526-.658l-.032-.028a3.2 3.2 0 0 0-.668-.428l-.27-.12a3.3 3.3 0 0 0-1.235-.23q-1.136-.001-1.974.493a3.36 3.36 0 0 0-1.3 1.382q-.445.89-.444 2.074 0 1.2.51 2.107a3.8 3.8 0 0 0 1.382 1.381 3.9 3.9 0 0 0 1.893.477q.795 0 1.455-.33zm-2.789-5.38q-.576.675-.575 1.762 0 1.102.559 1.794.576.675 1.645.675a2.25 2.25 0 0 0 .934-.19 2.2 2.2 0 0 0 .468-.29l.178-.161a2.2 2.2 0 0 0 .397-.561q.244-.5.244-1.15v-.115q0-.708-.296-1.267l-.043-.077a2.2 2.2 0 0 0-.633-.709l-.13-.086-.047-.028a2.1 2.1 0 0 0-1.073-.285q-1.052 0-1.629.692zm2.316 2.706c.163-.17.28-.407.28-.83v-.114c0-.292-.06-.508-.15-.68a.96.96 0 0 0-.353-.389.85.85 0 0 0-.464-.127c-.4 0-.56.114-.664.239l-.01.012c-.148.174-.275.45-.275.945 0 .506.122.801.27.99.097.11.266.224.68.224.303 0 .504-.09.687-.269zm7.545 1.705a2.6 2.6 0 0 0 .331.423q.319.33.755.548l.173.074q.65.255 1.49.255 1.02 0 1.844-.493a3.45 3.45 0 0 0 1.316-1.4q.493-.904.493-2.089 0-1.909-.988-2.913-.988-1.02-2.584-1.02-.898 0-1.575.347a3 3 0 0 0-.415.262l-.199.166a3.4 3.4 0 0 0-.64.82V9.242h-1.712v11.553h1.729l-.017-5.134zm.53-1.138q.206.29.48.5l.155.11.053.034q.51.296 1.119.297 1.07 0 1.645-.675.577-.69.576-1.762 0-1.119-.576-1.777-.558-.675-1.645-.675-.435 0-.835.16a2 2 0 0 0-.284.136 2 2 0 0 0-.363.254 2.2 2.2 0 0 0-.46.569l-.082.162a2.6 2.6 0 0 0-.213 1.072v.115q0 .707.296 1.267l.135.211zm.964-.818a1.1 1.1 0 0 0 .367.385.94.94 0 0 0 .476.118c.423 0 .59-.117.687-.23.159-.194.28-.478.28-.95 0-.53-.133-.8-.266-.952l-.021-.025c-.078-.094-.231-.221-.68-.221a1 1 0 0 0-.503.135l-.012.007a.86.86 0 0 0-.335.343c-.073.133-.132.324-.132.614v.115a1.4 1.4 0 0 0 .14.66zm15.7-6.222q.347-.346.346-.856a1.05 1.05 0 0 0-.345-.79 1.18 1.18 0 0 0-.84-.329q-.51 0-.855.33a1.05 1.05 0 0 0-.346.79q0 .51.346.855.345.346.856.346.51 0 .839-.346zm4.337 9.314.033-1.332q.191.403.59.747l.098.081a4 4 0 0 0 .316.224l.223.122a3.2 3.2 0 0 0 1.44.322 3.8 3.8 0 0 0 1.875-.477 3.5 3.5 0 0 0 1.382-1.366q.527-.89.526-2.09 0-1.184-.444-2.073a3.24 3.24 0 0 0-1.283-1.399q-.823-.51-1.942-.51a3.5 3.5 0 0 0-1.527.344l-.086.043-.165.09a3 3 0 0 0-.33.214q-.432.315-.656.707a2 2 0 0 0-.099.198l.082-1.283V4.701h-1.744v12.095zm.473-2.509a2.5 2.5 0 0 0 .566.7q.117.098.245.18l.144.08a2.1 2.1 0 0 0 .975.232q1.07 0 1.645-.675.576-.69.576-1.778 0-1.102-.576-1.777-.56-.691-1.645-.692a2.2 2.2 0 0 0-1.015.235q-.22.113-.415.282l-.15.142a2.1 2.1 0 0 0-.42.594q-.223.479-.223 1.1v.115q0 .705.293 1.26zm2.616-.293c.157-.191.28-.479.28-.967 0-.51-.13-.79-.276-.961l-.021-.026c-.082-.1-.232-.225-.67-.225a.87.87 0 0 0-.681.279l-.012.011c-.154.155-.274.38-.274.807v.115c0 .285.057.499.144.669a1.1 1.1 0 0 0 .367.405c.137.082.28.123.455.123.423 0 .59-.118.686-.23zm8.266-3.013q.345-.13.724-.14l.069-.002q.493 0 .642.099l.247-1.794q-.196-.099-.717-.099a2.3 2.3 0 0 0-.545.063 2 2 0 0 0-.411.148 2.2 2.2 0 0 0-.4.249 2.5 2.5 0 0 0-.485.499 2.7 2.7 0 0 0-.32.581l-.05.137v-1.48h-1.778v7.553h1.777v-3.884q0-.546.159-.943a1.5 1.5 0 0 1 .466-.636 2.5 2.5 0 0 1 .399-.253 2 2 0 0 1 .224-.099zm9.784 2.656.05-.922q0-1.743-.856-2.698-.838-.97-2.584-.97-1.119-.001-2.007.493a3.46 3.46 0 0 0-1.4 1.382q-.493.906-.493 2.106 0 1.07.428 1.975.428.89 1.332 1.432.906.526 2.255.526.973 0 1.668-.185l.044-.012.135-.04q.613-.184.984-.421l-.542-1.267q-.3.162-.642.274l-.297.087q-.51.131-1.3.131-.954 0-1.497-.444a1.6 1.6 0 0 1-.192-.193q-.366-.44-.512-1.234l-.004-.021zm-5.427-1.256-.003.022h3.752v-.138q-.011-.727-.288-1.118a1 1 0 0 0-.156-.176q-.46-.428-1.316-.428-.986 0-1.494.604-.379.45-.494 1.234zm-27.053 2.77V4.7h-1.86v12.095h5.333V15.15zm7.103-5.908v7.553h-1.843V9.242h1.843z'/%3E%3Cpath fill='%23fff' d='m19.63 11.151-.757-1.71-.345 1.71-1.12 5.644h-1.827L18.083 4.7h.197l3.325 6.533.988 2.19.988-2.19L26.839 4.7h.181l2.6 12.095h-1.81l-1.218-5.644-.362-1.71-.658 1.71-2.93 5.644h-.098l-2.913-5.644zm14.836 5.81q-1.02 0-1.893-.478a3.8 3.8 0 0 1-1.381-1.382q-.51-.906-.51-2.106 0-1.185.444-2.074a3.36 3.36 0 0 1 1.3-1.382q.839-.494 1.974-.494a3.3 3.3 0 0 1 1.234.231 3.3 3.3 0 0 1 .97.575q.396.33.527.659l.033-1.267h1.694v7.553H37.18l-.033-1.332q-.279.593-1.02 1.053a3.17 3.17 0 0 1-1.662.444zm.296-1.482q.938 0 1.58-.642.642-.66.642-1.711v-.115q0-.708-.296-1.267a2.2 2.2 0 0 0-.807-.872 2.1 2.1 0 0 0-1.119-.313q-1.053 0-1.629.692-.575.675-.575 1.76 0 1.103.559 1.795.577.675 1.645.675zm6.521-6.237h1.711v1.4q.906-1.597 2.83-1.597 1.596 0 2.584 1.02.988 1.005.988 2.914 0 1.185-.493 2.09a3.46 3.46 0 0 1-1.316 1.399 3.5 3.5 0 0 1-1.844.493q-.954 0-1.662-.329a2.67 2.67 0 0 1-1.086-.97l.017 5.134h-1.728zm4.048 6.22q1.07 0 1.645-.674.577-.69.576-1.762 0-1.119-.576-1.777-.558-.675-1.645-.675-.592 0-1.12.296-.51.28-.822.823-.296.527-.296 1.234v.115q0 .708.296 1.267.313.543.823.855.51.296 1.119.297z'/%3E%3Cpath fill='%23e1e3e9' d='M51.325 4.7h1.86v10.45h3.473v1.646h-5.333zm7.12 4.542h1.843v7.553h-1.843zm.905-1.415a1.16 1.16 0 0 1-.856-.346 1.17 1.17 0 0 1-.346-.856 1.05 1.05 0 0 1 .346-.79q.346-.329.856-.329.494 0 .839.33a1.05 1.05 0 0 1 .345.79 1.16 1.16 0 0 1-.345.855q-.33.346-.84.346zm7.875 9.133a3.17 3.17 0 0 1-1.662-.444q-.723-.46-1.004-1.053l-.033 1.332h-1.71V4.701h1.743v4.657l-.082 1.283q.279-.658 1.086-1.119a3.5 3.5 0 0 1 1.778-.477q1.119 0 1.942.51a3.24 3.24 0 0 1 1.283 1.4q.445.888.444 2.072 0 1.201-.526 2.09a3.5 3.5 0 0 1-1.382 1.366 3.8 3.8 0 0 1-1.876.477zm-.296-1.481q1.069 0 1.645-.675.577-.69.577-1.778 0-1.102-.577-1.776-.56-.691-1.645-.692a2.12 2.12 0 0 0-1.58.659q-.642.641-.642 1.694v.115q0 .71.296 1.267a2.4 2.4 0 0 0 .807.872 2.1 2.1 0 0 0 1.119.313zm5.927-6.237h1.777v1.481q.263-.757.856-1.217a2.14 2.14 0 0 1 1.349-.46q.527 0 .724.098l-.247 1.794q-.149-.099-.642-.099-.774 0-1.416.494-.626.493-.626 1.58v3.883h-1.777V9.242zm9.534 7.718q-1.35 0-2.255-.526-.904-.543-1.332-1.432a4.6 4.6 0 0 1-.428-1.975q0-1.2.493-2.106a3.46 3.46 0 0 1 1.4-1.382q.889-.495 2.007-.494 1.744 0 2.584.97.855.956.856 2.7 0 .444-.05.92h-5.43q.18 1.005.708 1.45.542.443 1.497.443.79 0 1.3-.131a4 4 0 0 0 .938-.362l.542 1.267q-.411.263-1.119.46-.708.198-1.711.197zm1.596-4.558q.016-1.02-.444-1.432-.46-.428-1.316-.428-1.728 0-1.991 1.86z'/%3E%3Cpath d='M5.074 15.948a.484.657 0 0 0-.486.659v1.84a.484.657 0 0 0 .486.659h4.101a.484.657 0 0 0 .486-.659v-1.84a.484.657 0 0 0-.486-.659zm3.56 1.16H5.617v.838h3.017z' style='fill:%23fff;fill-rule:evenodd;stroke-width:1.03600001'/%3E%3Cg style='stroke-width:1.12603545'%3E%3Cpath d='M-9.408-1.416c-3.833-.025-7.056 2.912-7.08 6.615-.02 3.08 1.653 4.832 3.107 6.268.903.892 1.721 1.74 2.32 2.902l-.525-.004c-.543-.003-.992.304-1.24.639a1.87 1.87 0 0 0-.362 1.121l-.011 1.877c-.003.402.104.787.347 1.125.244.338.688.653 1.23.656l4.142.028c.542.003.99-.306 1.238-.641a1.87 1.87 0 0 0 .363-1.121l.012-1.875a1.87 1.87 0 0 0-.348-1.127c-.243-.338-.688-.653-1.23-.656l-.518-.004c.597-1.145 1.425-1.983 2.348-2.87 1.473-1.414 3.18-3.149 3.2-6.226-.016-3.59-2.923-6.684-6.993-6.707m-.006 1.1v.002c3.274.02 5.92 2.532 5.9 5.6-.017 2.706-1.39 4.026-2.863 5.44-1.034.994-2.118 2.033-2.814 3.633-.018.041-.052.055-.075.065q-.013.004-.02.01a.34.34 0 0 1-.226.084.34.34 0 0 1-.224-.086l-.092-.077c-.699-1.615-1.768-2.669-2.781-3.67-1.454-1.435-2.797-2.762-2.78-5.478.02-3.067 2.7-5.545 5.975-5.523m-.02 2.826c-1.62-.01-2.944 1.315-2.955 2.96-.01 1.646 1.295 2.988 2.916 2.999h.002c1.621.01 2.943-1.316 2.953-2.961.011-1.646-1.294-2.988-2.916-2.998m-.005 1.1c1.017.006 1.829.83 1.822 1.89s-.83 1.874-1.848 1.867c-1.018-.006-1.829-.83-1.822-1.89s.83-1.874 1.848-1.868m-2.155 11.857 4.14.025c.271.002.49.305.487.676l-.013 1.875c-.003.37-.224.67-.495.668l-4.14-.025c-.27-.002-.487-.306-.485-.676l.012-1.875c.003-.37.224-.67.494-.668' style='color:%23000;font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:%23000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:evenodd;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:%23000;solid-opacity:1;vector-effect:none;fill:%23000;fill-opacity:.4;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto' transform='translate(15.553 2.85)scale(.88807)'/%3E%3Cpath d='M-9.415-.316C-12.69-.338-15.37 2.14-15.39 5.207c-.017 2.716 1.326 4.041 2.78 5.477 1.013 1 2.081 2.055 2.78 3.67l.092.076a.34.34 0 0 0 .225.086.34.34 0 0 0 .227-.083l.019-.01c.022-.009.057-.024.074-.064.697-1.6 1.78-2.64 2.814-3.634 1.473-1.414 2.847-2.733 2.864-5.44.02-3.067-2.627-5.58-5.901-5.601m-.057 8.784c1.621.011 2.944-1.315 2.955-2.96.01-1.646-1.295-2.988-2.916-2.999-1.622-.01-2.945 1.315-2.955 2.96s1.295 2.989 2.916 3' style='clip-rule:evenodd;fill:%23e1e3e9;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:.4' transform='translate(15.553 2.85)scale(.88807)'/%3E%3Cpath d='M-11.594 15.465c-.27-.002-.492.297-.494.668l-.012 1.876c-.003.371.214.673.485.675l4.14.027c.271.002.492-.298.495-.668l.012-1.877c.003-.37-.215-.672-.485-.674z' style='clip-rule:evenodd;fill:%23fff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:2.47727823;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:.4' transform='translate(15.553 2.85)scale(.88807)'/%3E%3C/g%3E%3C/svg%3E")}}.maplibregl-ctrl.maplibregl-ctrl-attrib{padding:0 5px;background-color:hsla(0,0%,100%,.5);margin:0}@media screen{.maplibregl-ctrl-attrib.maplibregl-compact{min-height:20px;padding:2px 24px 2px 0;margin:10px;position:relative;background-color:#fff;color:#000;border-radius:12px;box-sizing:content-box}.maplibregl-ctrl-attrib.maplibregl-compact-show{padding:2px 28px 2px 8px;visibility:visible}.maplibregl-ctrl-bottom-left>.maplibregl-ctrl-attrib.maplibregl-compact-show,.maplibregl-ctrl-top-left>.maplibregl-ctrl-attrib.maplibregl-compact-show{padding:2px 8px 2px 28px;border-radius:12px}.maplibregl-ctrl-attrib.maplibregl-compact .maplibregl-ctrl-attrib-inner{display:none}.maplibregl-ctrl-attrib-button{display:none;cursor:pointer;position:absolute;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill-rule='evenodd' viewBox='0 0 20 20'%3E%3Cpath d='M4 10a6 6 0 1 0 12 0 6 6 0 1 0-12 0m5-3a1 1 0 1 0 2 0 1 1 0 1 0-2 0m0 3a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0'/%3E%3C/svg%3E");background-color:hsla(0,0%,100%,.5);width:24px;height:24px;box-sizing:border-box;border-radius:12px;outline:none;top:0;right:0;border:0}.maplibregl-ctrl-attrib summary.maplibregl-ctrl-attrib-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;list-style:none}.maplibregl-ctrl-attrib summary.maplibregl-ctrl-attrib-button::-webkit-details-marker{display:none}.maplibregl-ctrl-bottom-left .maplibregl-ctrl-attrib-button,.maplibregl-ctrl-top-left .maplibregl-ctrl-attrib-button{left:0}.maplibregl-ctrl-attrib.maplibregl-compact .maplibregl-ctrl-attrib-button,.maplibregl-ctrl-attrib.maplibregl-compact-show .maplibregl-ctrl-attrib-inner{display:block}.maplibregl-ctrl-attrib.maplibregl-compact-show .maplibregl-ctrl-attrib-button{background-color:rgba(0,0,0,.05)}.maplibregl-ctrl-bottom-right>.maplibregl-ctrl-attrib.maplibregl-compact:after{bottom:0;right:0}.maplibregl-ctrl-top-right>.maplibregl-ctrl-attrib.maplibregl-compact:after{top:0;right:0}.maplibregl-ctrl-top-left>.maplibregl-ctrl-attrib.maplibregl-compact:after{top:0;left:0}.maplibregl-ctrl-bottom-left>.maplibregl-ctrl-attrib.maplibregl-compact:after{bottom:0;left:0}}@media screen and (forced-colors:active){.maplibregl-ctrl-attrib.maplibregl-compact:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill='%23fff' fill-rule='evenodd' viewBox='0 0 20 20'%3E%3Cpath d='M4 10a6 6 0 1 0 12 0 6 6 0 1 0-12 0m5-3a1 1 0 1 0 2 0 1 1 0 1 0-2 0m0 3a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0'/%3E%3C/svg%3E")}}@media screen and (forced-colors:active) and (prefers-color-scheme:light){.maplibregl-ctrl-attrib.maplibregl-compact:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' fill-rule='evenodd' viewBox='0 0 20 20'%3E%3Cpath d='M4 10a6 6 0 1 0 12 0 6 6 0 1 0-12 0m5-3a1 1 0 1 0 2 0 1 1 0 1 0-2 0m0 3a1 1 0 1 1 2 0v3a1 1 0 1 1-2 0'/%3E%3C/svg%3E")}}.maplibregl-ctrl-attrib a{color:rgba(0,0,0,.75);text-decoration:none}.maplibregl-ctrl-attrib a:hover{color:inherit;text-decoration:underline}.maplibregl-attrib-empty{display:none}.maplibregl-ctrl-scale{background-color:hsla(0,0%,100%,.75);font-size:10px;white-space:nowrap;border:2px solid #333;border-top:#333;padding:0 5px;color:#333;box-sizing:border-box}.maplibregl-popup{position:absolute;top:0;left:0;display:flex;will-change:transform;pointer-events:none}.maplibregl-popup-anchor-top,.maplibregl-popup-anchor-top-left,.maplibregl-popup-anchor-top-right{flex-direction:column}.maplibregl-popup-anchor-bottom,.maplibregl-popup-anchor-bottom-left,.maplibregl-popup-anchor-bottom-right{flex-direction:column-reverse}.maplibregl-popup-anchor-left{flex-direction:row}.maplibregl-popup-anchor-right{flex-direction:row-reverse}.maplibregl-popup-tip{width:0;height:0;border:10px solid transparent;z-index:1}.maplibregl-popup-anchor-top .maplibregl-popup-tip{align-self:center;border-top:none;border-bottom-color:#fff}.maplibregl-popup-anchor-top-left .maplibregl-popup-tip{align-self:flex-start;border-top:none;border-left:none;border-bottom-color:#fff}.maplibregl-popup-anchor-top-right .maplibregl-popup-tip{align-self:flex-end;border-top:none;border-right:none;border-bottom-color:#fff}.maplibregl-popup-anchor-bottom .maplibregl-popup-tip{align-self:center;border-bottom:none;border-top-color:#fff}.maplibregl-popup-anchor-bottom-left .maplibregl-popup-tip{align-self:flex-start;border-bottom:none;border-left:none;border-top-color:#fff}.maplibregl-popup-anchor-bottom-right .maplibregl-popup-tip{align-self:flex-end;border-bottom:none;border-right:none;border-top-color:#fff}.maplibregl-popup-anchor-left .maplibregl-popup-tip{align-self:center;border-left:none;border-right-color:#fff}.maplibregl-popup-anchor-right .maplibregl-popup-tip{align-self:center;border-right:none;border-left-color:#fff}[dir=rtl] .maplibregl-popup-anchor-left{flex-direction:row-reverse}[dir=rtl] .maplibregl-popup-anchor-right{flex-direction:row}[dir=rtl] .maplibregl-popup-anchor-top-left .maplibregl-popup-tip{align-self:flex-end}[dir=rtl] .maplibregl-popup-anchor-top-right .maplibregl-popup-tip{align-self:flex-start}[dir=rtl] .maplibregl-popup-anchor-bottom-left .maplibregl-popup-tip{align-self:flex-end}[dir=rtl] .maplibregl-popup-anchor-bottom-right .maplibregl-popup-tip{align-self:flex-start}.maplibregl-popup-close-button{position:absolute;right:0;top:0;border:0;border-radius:0 3px 0 0;cursor:pointer;background-color:transparent}.maplibregl-popup-close-button:hover{background-color:rgba(0,0,0,.05)}.maplibregl-popup-content{position:relative;background:#fff;border-radius:3px;box-shadow:0 1px 2px rgba(0,0,0,.1);padding:15px 10px;pointer-events:auto}.maplibregl-popup-anchor-top-left .maplibregl-popup-content{border-top-left-radius:0}.maplibregl-popup-anchor-top-right .maplibregl-popup-content{border-top-right-radius:0}.maplibregl-popup-anchor-bottom-left .maplibregl-popup-content{border-bottom-left-radius:0}.maplibregl-popup-anchor-bottom-right .maplibregl-popup-content{border-bottom-right-radius:0}.maplibregl-popup-track-pointer{display:none}.maplibregl-popup-track-pointer *{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.maplibregl-map:hover .maplibregl-popup-track-pointer{display:flex}.maplibregl-map:active .maplibregl-popup-track-pointer{display:none}.maplibregl-marker{position:absolute;top:0;left:0;will-change:transform;transition:opacity .2s}.maplibregl-user-location-dot,.maplibregl-user-location-dot:before{background-color:#1da1f2;width:15px;height:15px;border-radius:50%}.maplibregl-user-location-dot:before{content:"";position:absolute;animation:maplibregl-user-location-dot-pulse 2s infinite}.maplibregl-user-location-dot:after{border-radius:50%;border:2px solid #fff;content:"";height:19px;left:-2px;position:absolute;top:-2px;width:19px;box-sizing:border-box;box-shadow:0 0 3px rgba(0,0,0,.35)}@media (prefers-reduced-motion:reduce){.maplibregl-user-location-dot:before{animation:none}}@keyframes maplibregl-user-location-dot-pulse{0%{transform:scale(1);opacity:1}70%{transform:scale(3);opacity:0}to{transform:scale(1);opacity:0}}.maplibregl-user-location-dot-stale{background-color:#aaa}.maplibregl-user-location-dot-stale:after{display:none}.maplibregl-user-location-accuracy-circle{background-color:#1da1f233;width:1px;height:1px;border-radius:100%}.maplibregl-crosshair,.maplibregl-crosshair .maplibregl-interactive,.maplibregl-crosshair .maplibregl-interactive:active{cursor:crosshair}.maplibregl-boxzoom{position:absolute;top:0;left:0;width:0;height:0;background:#fff;border:2px dotted #202020;opacity:.5}.maplibregl-cooperative-gesture-screen{background:rgba(0,0,0,.4);position:absolute;inset:0;display:flex;justify-content:center;align-items:center;color:#fff;padding:1rem;font-size:1.4em;line-height:1.2;opacity:0;pointer-events:none;transition:opacity 1s ease 1s;z-index:99999}.maplibregl-cooperative-gesture-screen.maplibregl-show{opacity:1;transition:opacity .05s}.maplibregl-cooperative-gesture-screen .maplibregl-mobile-message{display:none}@media (hover:none),(pointer:coarse){.maplibregl-cooperative-gesture-screen .maplibregl-desktop-message{display:none}.maplibregl-cooperative-gesture-screen .maplibregl-mobile-message{display:block}}.maplibregl-pseudo-fullscreen{position:fixed!important;width:100%!important;height:100%!important;top:0!important;left:0!important;z-index:99999} \ No newline at end of file +.maplibregl-map{font:12px/20px Helvetica Neue,Arial,Helvetica,sans-serif;overflow:hidden;position:relative;-webkit-tap-highlight-color:rgb(0 0 0/0)}.maplibregl-canvas{position:absolute;left:0;top:0}.maplibregl-map:fullscreen{width:100%;height:100%}.maplibregl-ctrl-group button.maplibregl-ctrl-compass{touch-action:none}.maplibregl-canvas-container.maplibregl-interactive,.maplibregl-ctrl-group button.maplibregl-ctrl-compass{cursor:grab;-webkit-user-select:none;-moz-user-select:none;user-select:none}.maplibregl-canvas-container.maplibregl-interactive.maplibregl-track-pointer{cursor:pointer}.maplibregl-canvas-container.maplibregl-interactive:active,.maplibregl-ctrl-group button.maplibregl-ctrl-compass:active{cursor:grabbing}.maplibregl-canvas-container.maplibregl-touch-zoom-rotate,.maplibregl-canvas-container.maplibregl-touch-zoom-rotate .maplibregl-canvas{touch-action:pan-x pan-y}.maplibregl-canvas-container.maplibregl-touch-drag-pan,.maplibregl-canvas-container.maplibregl-touch-drag-pan .maplibregl-canvas{touch-action:pinch-zoom}.maplibregl-canvas-container.maplibregl-touch-zoom-rotate.maplibregl-touch-drag-pan,.maplibregl-canvas-container.maplibregl-touch-zoom-rotate.maplibregl-touch-drag-pan .maplibregl-canvas{touch-action:none}.maplibregl-canvas-container.maplibregl-touch-drag-pan.maplibregl-cooperative-gestures,.maplibregl-canvas-container.maplibregl-touch-drag-pan.maplibregl-cooperative-gestures .maplibregl-canvas{touch-action:pan-x pan-y}.maplibregl-ctrl-bottom-left,.maplibregl-ctrl-bottom-right,.maplibregl-ctrl-top-left,.maplibregl-ctrl-top-right{position:absolute;pointer-events:none;z-index:2}.maplibregl-ctrl-top-left{top:0;left:0}.maplibregl-ctrl-top-right{top:0;right:0}.maplibregl-ctrl-bottom-left{bottom:0;left:0}.maplibregl-ctrl-bottom-right{right:0;bottom:0}.maplibregl-ctrl{clear:both;pointer-events:auto;transform:translate(0)}.maplibregl-ctrl-top-left .maplibregl-ctrl{margin:10px 0 0 10px;float:left}.maplibregl-ctrl-top-right .maplibregl-ctrl{margin:10px 10px 0 0;float:right}.maplibregl-ctrl-bottom-left .maplibregl-ctrl{margin:0 0 10px 10px;float:left}.maplibregl-ctrl-bottom-right .maplibregl-ctrl{margin:0 10px 10px 0;float:right}.maplibregl-ctrl-group{border-radius:4px;background:#fff}.maplibregl-ctrl-group:not(:empty){box-shadow:0 0 0 2px rgba(0,0,0,.1)}@media (forced-colors:active){.maplibregl-ctrl-group:not(:empty){box-shadow:0 0 0 2px ButtonText}}.maplibregl-ctrl-group button{width:29px;height:29px;display:block;padding:0;outline:none;border:0;box-sizing:border-box;background-color:transparent;cursor:pointer}.maplibregl-ctrl-group button+button{border-top:1px solid #ddd}.maplibregl-ctrl button .maplibregl-ctrl-icon{display:block;width:100%;height:100%;background-repeat:no-repeat;background-position:50%}@media (forced-colors:active){.maplibregl-ctrl-icon{background-color:transparent}.maplibregl-ctrl-group button+button{border-top:1px solid ButtonText}}.maplibregl-ctrl button::-moz-focus-inner{border:0;padding:0}.maplibregl-ctrl-attrib-button:focus,.maplibregl-ctrl-group button:focus{box-shadow:0 0 2px 2px #0096ff}.maplibregl-ctrl button:disabled{cursor:not-allowed}.maplibregl-ctrl button:disabled .maplibregl-ctrl-icon{opacity:.25}@media (hover:hover){.maplibregl-ctrl button:not(:disabled):hover{background-color:rgba(0,0,0,.05)}}.maplibregl-ctrl button:not(:disabled):active{background-color:rgba(0,0,0,.05)}.maplibregl-ctrl-group button:focus:focus-visible{box-shadow:0 0 2px 2px #0096ff}.maplibregl-ctrl-group button:focus:not(:focus-visible){box-shadow:none}.maplibregl-ctrl-group button:focus:first-child{border-radius:4px 4px 0 0}.maplibregl-ctrl-group button:focus:last-child{border-radius:0 0 4px 4px}.maplibregl-ctrl-group button:focus:only-child{border-radius:inherit}.maplibregl-ctrl button.maplibregl-ctrl-zoom-out .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20fill%3D%22%23333%22%20viewBox%3D%220%200%2029%2029%22%3E%3Cpath%20d%3D%22M10%2013c-.75%200-1.5.75-1.5%201.5S9.25%2016%2010%2016h9c.75%200%201.5-.75%201.5-1.5S19.75%2013%2019%2013z%22%2F%3E%3C%2Fsvg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-zoom-in .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20fill%3D%22%23333%22%20viewBox%3D%220%200%2029%2029%22%3E%3Cpath%20d%3D%22M14.5%208.5c-.75%200-1.5.75-1.5%201.5v3h-3c-.75%200-1.5.75-1.5%201.5S9.25%2016%2010%2016h3v3c0%20.75.75%201.5%201.5%201.5S16%2019.75%2016%2019v-3h3c.75%200%201.5-.75%201.5-1.5S19.75%2013%2019%2013h-3v-3c0-.75-.75-1.5-1.5-1.5%22%2F%3E%3C%2Fsvg%3E")}@media (forced-colors:active){.maplibregl-ctrl button.maplibregl-ctrl-zoom-out .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20fill%3D%22%23fff%22%20viewBox%3D%220%200%2029%2029%22%3E%3Cpath%20d%3D%22M10%2013c-.75%200-1.5.75-1.5%201.5S9.25%2016%2010%2016h9c.75%200%201.5-.75%201.5-1.5S19.75%2013%2019%2013z%22%2F%3E%3C%2Fsvg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-zoom-in .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20fill%3D%22%23fff%22%20viewBox%3D%220%200%2029%2029%22%3E%3Cpath%20d%3D%22M14.5%208.5c-.75%200-1.5.75-1.5%201.5v3h-3c-.75%200-1.5.75-1.5%201.5S9.25%2016%2010%2016h3v3c0%20.75.75%201.5%201.5%201.5S16%2019.75%2016%2019v-3h3c.75%200%201.5-.75%201.5-1.5S19.75%2013%2019%2013h-3v-3c0-.75-.75-1.5-1.5-1.5%22%2F%3E%3C%2Fsvg%3E")}}@media (forced-colors:active) and (prefers-color-scheme:light){.maplibregl-ctrl button.maplibregl-ctrl-zoom-out .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20viewBox%3D%220%200%2029%2029%22%3E%3Cpath%20d%3D%22M10%2013c-.75%200-1.5.75-1.5%201.5S9.25%2016%2010%2016h9c.75%200%201.5-.75%201.5-1.5S19.75%2013%2019%2013z%22%2F%3E%3C%2Fsvg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-zoom-in .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20viewBox%3D%220%200%2029%2029%22%3E%3Cpath%20d%3D%22M14.5%208.5c-.75%200-1.5.75-1.5%201.5v3h-3c-.75%200-1.5.75-1.5%201.5S9.25%2016%2010%2016h3v3c0%20.75.75%201.5%201.5%201.5S16%2019.75%2016%2019v-3h3c.75%200%201.5-.75%201.5-1.5S19.75%2013%2019%2013h-3v-3c0-.75-.75-1.5-1.5-1.5%22%2F%3E%3C%2Fsvg%3E")}}.maplibregl-ctrl button.maplibregl-ctrl-fullscreen .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20fill%3D%22%23333%22%20viewBox%3D%220%200%2029%2029%22%3E%3Cpath%20d%3D%22M24%2016v5.5c0%201.75-.75%202.5-2.5%202.5H16v-1l3-1.5-4-5.5%201-1%205.5%204%201.5-3zM6%2016l1.5%203%205.5-4%201%201-4%205.5%203%201.5v1H7.5C5.75%2024%205%2023.25%205%2021.5V16zm7-11v1l-3%201.5%204%205.5-1%201-5.5-4L6%2013H5V7.5C5%205.75%205.75%205%207.5%205zm11%202.5c0-1.75-.75-2.5-2.5-2.5H16v1l3%201.5-4%205.5%201%201%205.5-4%201.5%203h1z%22%2F%3E%3C%2Fsvg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-shrink .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20viewBox%3D%220%200%2029%2029%22%3E%3Cpath%20d%3D%22M18.5%2016c-1.75%200-2.5.75-2.5%202.5V24h1l1.5-3%205.5%204%201-1-4-5.5%203-1.5v-1zM13%2018.5c0-1.75-.75-2.5-2.5-2.5H5v1l3%201.5L4%2024l1%201%205.5-4%201.5%203h1zm3-8c0%201.75.75%202.5%202.5%202.5H24v-1l-3-1.5L25%205l-1-1-5.5%204L17%205h-1zM10.5%2013c1.75%200%202.5-.75%202.5-2.5V5h-1l-1.5%203L5%204%204%205l4%205.5L5%2012v1z%22%2F%3E%3C%2Fsvg%3E")}@media (forced-colors:active){.maplibregl-ctrl button.maplibregl-ctrl-fullscreen .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20fill%3D%22%23fff%22%20viewBox%3D%220%200%2029%2029%22%3E%3Cpath%20d%3D%22M24%2016v5.5c0%201.75-.75%202.5-2.5%202.5H16v-1l3-1.5-4-5.5%201-1%205.5%204%201.5-3zM6%2016l1.5%203%205.5-4%201%201-4%205.5%203%201.5v1H7.5C5.75%2024%205%2023.25%205%2021.5V16zm7-11v1l-3%201.5%204%205.5-1%201-5.5-4L6%2013H5V7.5C5%205.75%205.75%205%207.5%205zm11%202.5c0-1.75-.75-2.5-2.5-2.5H16v1l3%201.5-4%205.5%201%201%205.5-4%201.5%203h1z%22%2F%3E%3C%2Fsvg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-shrink .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20fill%3D%22%23fff%22%20viewBox%3D%220%200%2029%2029%22%3E%3Cpath%20d%3D%22M18.5%2016c-1.75%200-2.5.75-2.5%202.5V24h1l1.5-3%205.5%204%201-1-4-5.5%203-1.5v-1zM13%2018.5c0-1.75-.75-2.5-2.5-2.5H5v1l3%201.5L4%2024l1%201%205.5-4%201.5%203h1zm3-8c0%201.75.75%202.5%202.5%202.5H24v-1l-3-1.5L25%205l-1-1-5.5%204L17%205h-1zM10.5%2013c1.75%200%202.5-.75%202.5-2.5V5h-1l-1.5%203L5%204%204%205l4%205.5L5%2012v1z%22%2F%3E%3C%2Fsvg%3E")}}@media (forced-colors:active) and (prefers-color-scheme:light){.maplibregl-ctrl button.maplibregl-ctrl-fullscreen .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20viewBox%3D%220%200%2029%2029%22%3E%3Cpath%20d%3D%22M24%2016v5.5c0%201.75-.75%202.5-2.5%202.5H16v-1l3-1.5-4-5.5%201-1%205.5%204%201.5-3zM6%2016l1.5%203%205.5-4%201%201-4%205.5%203%201.5v1H7.5C5.75%2024%205%2023.25%205%2021.5V16zm7-11v1l-3%201.5%204%205.5-1%201-5.5-4L6%2013H5V7.5C5%205.75%205.75%205%207.5%205zm11%202.5c0-1.75-.75-2.5-2.5-2.5H16v1l3%201.5-4%205.5%201%201%205.5-4%201.5%203h1z%22%2F%3E%3C%2Fsvg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-shrink .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20viewBox%3D%220%200%2029%2029%22%3E%3Cpath%20d%3D%22M18.5%2016c-1.75%200-2.5.75-2.5%202.5V24h1l1.5-3%205.5%204%201-1-4-5.5%203-1.5v-1zM13%2018.5c0-1.75-.75-2.5-2.5-2.5H5v1l3%201.5L4%2024l1%201%205.5-4%201.5%203h1zm3-8c0%201.75.75%202.5%202.5%202.5H24v-1l-3-1.5L25%205l-1-1-5.5%204L17%205h-1zM10.5%2013c1.75%200%202.5-.75%202.5-2.5V5h-1l-1.5%203L5%204%204%205l4%205.5L5%2012v1z%22%2F%3E%3C%2Fsvg%3E")}}.maplibregl-ctrl button.maplibregl-ctrl-compass .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20fill%3D%22%23333%22%20viewBox%3D%220%200%2029%2029%22%3E%3Cpath%20d%3D%22m10.5%2014%204-8%204%208z%22%2F%3E%3Cpath%20fill%3D%22%23ccc%22%20d%3D%22m10.5%2016%204%208%204-8z%22%2F%3E%3C%2Fsvg%3E")}@media (forced-colors:active){.maplibregl-ctrl button.maplibregl-ctrl-compass .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20fill%3D%22%23fff%22%20viewBox%3D%220%200%2029%2029%22%3E%3Cpath%20d%3D%22m10.5%2014%204-8%204%208z%22%2F%3E%3Cpath%20fill%3D%22%23ccc%22%20d%3D%22m10.5%2016%204%208%204-8z%22%2F%3E%3C%2Fsvg%3E")}}@media (forced-colors:active) and (prefers-color-scheme:light){.maplibregl-ctrl button.maplibregl-ctrl-compass .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20viewBox%3D%220%200%2029%2029%22%3E%3Cpath%20d%3D%22m10.5%2014%204-8%204%208z%22%2F%3E%3Cpath%20fill%3D%22%23ccc%22%20d%3D%22m10.5%2016%204%208%204-8z%22%2F%3E%3C%2Fsvg%3E")}}.maplibregl-ctrl button.maplibregl-ctrl-globe .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2222%22%20height%3D%2222%22%20fill%3D%22none%22%20stroke%3D%22%23333%22%20viewBox%3D%220%200%2022%2022%22%3E%3Ccircle%20cx%3D%2211%22%20cy%3D%2211%22%20r%3D%228.5%22%2F%3E%3Cpath%20d%3D%22M17.5%2011c0%204.819-3.02%208.5-6.5%208.5S4.5%2015.819%204.5%2011%207.52%202.5%2011%202.5s6.5%203.681%206.5%208.5Z%22%2F%3E%3Cpath%20d%3D%22M13.5%2011c0%202.447-.331%204.64-.853%206.206-.262.785-.562%201.384-.872%201.777-.314.399-.58.517-.775.517s-.461-.118-.775-.517c-.31-.393-.61-.992-.872-1.777C8.831%2015.64%208.5%2013.446%208.5%2011s.331-4.64.853-6.206c.262-.785.562-1.384.872-1.777.314-.399.58-.517.775-.517s.461.118.775.517c.31.393.61.992.872%201.777.522%201.565.853%203.76.853%206.206Z%22%2F%3E%3Cpath%20d%3D%22M11%207.5c-1.909%200-3.622-.166-4.845-.428-.616-.132-1.08-.283-1.379-.434a1.3%201.3%200%200%201-.224-.138q.07-.058.224-.138c.299-.151.763-.302%201.379-.434C7.378%205.666%209.091%205.5%2011%205.5s3.622.166%204.845.428c.616.132%201.08.283%201.379.434.105.053.177.1.224.138q-.07.058-.224.138c-.299.151-.763.302-1.379.434-1.223.262-2.936.428-4.845.428Zm0%209c-1.909%200-3.622-.166-4.845-.428-.616-.132-1.08-.283-1.379-.434a1.3%201.3%200%200%201-.224-.138%201.3%201.3%200%200%201%20.224-.138c.299-.151.763-.302%201.379-.434C7.378%2014.666%209.091%2014.5%2011%2014.5s3.622.166%204.845.428c.616.132%201.08.283%201.379.434.105.053.177.1.224.138a1.3%201.3%200%200%201-.224.138c-.299.151-.763.302-1.379.434-1.223.262-2.936.428-4.845.428Zm0-4c-2.46%200-4.672-.222-6.255-.574-.796-.177-1.406-.38-1.805-.59a1.5%201.5%200%200%201-.39-.272.3.3%200%200%201-.047-.064.3.3%200%200%201%20.048-.064c.066-.073.189-.167.389-.272.399-.21%201.009-.413%201.805-.59C6.328%209.722%208.54%209.5%2011%209.5s4.672.222%206.256.574c.795.177%201.405.38%201.804.59.2.105.323.2.39.272a.3.3%200%200%201%20.047.064.3.3%200%200%201-.048.064%201.4%201.4%200%200%201-.389.272c-.399.21-1.009.413-1.804.59-1.584.352-3.796.574-6.256.574Zm-8.501-1.51v.002zm0%20.018v.002zm17.002.002v-.002zm0-.018v-.002z%22%2F%3E%3C%2Fsvg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-globe-enabled .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2222%22%20height%3D%2222%22%20fill%3D%22none%22%20stroke%3D%22%2333b5e5%22%20viewBox%3D%220%200%2022%2022%22%3E%3Ccircle%20cx%3D%2211%22%20cy%3D%2211%22%20r%3D%228.5%22%2F%3E%3Cpath%20d%3D%22M17.5%2011c0%204.819-3.02%208.5-6.5%208.5S4.5%2015.819%204.5%2011%207.52%202.5%2011%202.5s6.5%203.681%206.5%208.5Z%22%2F%3E%3Cpath%20d%3D%22M13.5%2011c0%202.447-.331%204.64-.853%206.206-.262.785-.562%201.384-.872%201.777-.314.399-.58.517-.775.517s-.461-.118-.775-.517c-.31-.393-.61-.992-.872-1.777C8.831%2015.64%208.5%2013.446%208.5%2011s.331-4.64.853-6.206c.262-.785.562-1.384.872-1.777.314-.399.58-.517.775-.517s.461.118.775.517c.31.393.61.992.872%201.777.522%201.565.853%203.76.853%206.206Z%22%2F%3E%3Cpath%20d%3D%22M11%207.5c-1.909%200-3.622-.166-4.845-.428-.616-.132-1.08-.283-1.379-.434a1.3%201.3%200%200%201-.224-.138q.07-.058.224-.138c.299-.151.763-.302%201.379-.434C7.378%205.666%209.091%205.5%2011%205.5s3.622.166%204.845.428c.616.132%201.08.283%201.379.434.105.053.177.1.224.138q-.07.058-.224.138c-.299.151-.763.302-1.379.434-1.223.262-2.936.428-4.845.428Zm0%209c-1.909%200-3.622-.166-4.845-.428-.616-.132-1.08-.283-1.379-.434a1.3%201.3%200%200%201-.224-.138%201.3%201.3%200%200%201%20.224-.138c.299-.151.763-.302%201.379-.434C7.378%2014.666%209.091%2014.5%2011%2014.5s3.622.166%204.845.428c.616.132%201.08.283%201.379.434.105.053.177.1.224.138a1.3%201.3%200%200%201-.224.138c-.299.151-.763.302-1.379.434-1.223.262-2.936.428-4.845.428Zm0-4c-2.46%200-4.672-.222-6.255-.574-.796-.177-1.406-.38-1.805-.59a1.5%201.5%200%200%201-.39-.272.3.3%200%200%201-.047-.064.3.3%200%200%201%20.048-.064c.066-.073.189-.167.389-.272.399-.21%201.009-.413%201.805-.59C6.328%209.722%208.54%209.5%2011%209.5s4.672.222%206.256.574c.795.177%201.405.38%201.804.59.2.105.323.2.39.272a.3.3%200%200%201%20.047.064.3.3%200%200%201-.048.064%201.4%201.4%200%200%201-.389.272c-.399.21-1.009.413-1.804.59-1.584.352-3.796.574-6.256.574Zm-8.501-1.51v.002zm0%20.018v.002zm17.002.002v-.002zm0-.018v-.002z%22%2F%3E%3C%2Fsvg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-terrain .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2222%22%20height%3D%2222%22%20fill%3D%22%23333%22%20viewBox%3D%220%200%2022%2022%22%3E%3Cpath%20d%3D%22m1.754%2013.406%204.453-4.851%203.09%203.09%203.281%203.277.969-.969-3.309-3.312%203.844-4.121%206.148%206.886h1.082v-.855l-7.207-8.07-4.84%205.187L6.169%206.57l-5.48%205.965v.871ZM.688%2016.844h20.625v1.375H.688Zm0%200%22%2F%3E%3C%2Fsvg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-terrain-enabled .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2222%22%20height%3D%2222%22%20fill%3D%22%2333b5e5%22%20viewBox%3D%220%200%2022%2022%22%3E%3Cpath%20d%3D%22m1.754%2013.406%204.453-4.851%203.09%203.09%203.281%203.277.969-.969-3.309-3.312%203.844-4.121%206.148%206.886h1.082v-.855l-7.207-8.07-4.84%205.187L6.169%206.57l-5.48%205.965v.871ZM.688%2016.844h20.625v1.375H.688Zm0%200%22%2F%3E%3C%2Fsvg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20fill%3D%22%23333%22%20viewBox%3D%220%200%2020%2020%22%3E%3Cpath%20d%3D%22M10%204C9%204%209%205%209%205v.1A5%205%200%200%200%205.1%209H5s-1%200-1%201%201%201%201%201h.1A5%205%200%200%200%209%2014.9v.1s0%201%201%201%201-1%201-1v-.1a5%205%200%200%200%203.9-3.9h.1s1%200%201-1-1-1-1-1h-.1A5%205%200%200%200%2011%205.1V5s0-1-1-1m0%202.5a3.5%203.5%200%201%201%200%207%203.5%203.5%200%201%201%200-7%22%2F%3E%3Ccircle%20cx%3D%2210%22%20cy%3D%2210%22%20r%3D%222%22%2F%3E%3C%2Fsvg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate:disabled .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20fill%3D%22%23aaa%22%20viewBox%3D%220%200%2020%2020%22%3E%3Cpath%20d%3D%22M10%204C9%204%209%205%209%205v.1A5%205%200%200%200%205.1%209H5s-1%200-1%201%201%201%201%201h.1A5%205%200%200%200%209%2014.9v.1s0%201%201%201%201-1%201-1v-.1a5%205%200%200%200%203.9-3.9h.1s1%200%201-1-1-1-1-1h-.1A5%205%200%200%200%2011%205.1V5s0-1-1-1m0%202.5a3.5%203.5%200%201%201%200%207%203.5%203.5%200%201%201%200-7%22%2F%3E%3Ccircle%20cx%3D%2210%22%20cy%3D%2210%22%20r%3D%222%22%2F%3E%3Cpath%20fill%3D%22red%22%20d%3D%22m14%205%201%201-9%209-1-1z%22%2F%3E%3C%2Fsvg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20fill%3D%22%2333b5e5%22%20viewBox%3D%220%200%2020%2020%22%3E%3Cpath%20d%3D%22M10%204C9%204%209%205%209%205v.1A5%205%200%200%200%205.1%209H5s-1%200-1%201%201%201%201%201h.1A5%205%200%200%200%209%2014.9v.1s0%201%201%201%201-1%201-1v-.1a5%205%200%200%200%203.9-3.9h.1s1%200%201-1-1-1-1-1h-.1A5%205%200%200%200%2011%205.1V5s0-1-1-1m0%202.5a3.5%203.5%200%201%201%200%207%203.5%203.5%200%201%201%200-7%22%2F%3E%3Ccircle%20cx%3D%2210%22%20cy%3D%2210%22%20r%3D%222%22%2F%3E%3C%2Fsvg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active-error .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20fill%3D%22%23e58978%22%20viewBox%3D%220%200%2020%2020%22%3E%3Cpath%20d%3D%22M10%204C9%204%209%205%209%205v.1A5%205%200%200%200%205.1%209H5s-1%200-1%201%201%201%201%201h.1A5%205%200%200%200%209%2014.9v.1s0%201%201%201%201-1%201-1v-.1a5%205%200%200%200%203.9-3.9h.1s1%200%201-1-1-1-1-1h-.1A5%205%200%200%200%2011%205.1V5s0-1-1-1m0%202.5a3.5%203.5%200%201%201%200%207%203.5%203.5%200%201%201%200-7%22%2F%3E%3Ccircle%20cx%3D%2210%22%20cy%3D%2210%22%20r%3D%222%22%2F%3E%3C%2Fsvg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20fill%3D%22%2333b5e5%22%20viewBox%3D%220%200%2020%2020%22%3E%3Cpath%20d%3D%22M10%204C9%204%209%205%209%205v.1A5%205%200%200%200%205.1%209H5s-1%200-1%201%201%201%201%201h.1A5%205%200%200%200%209%2014.9v.1s0%201%201%201%201-1%201-1v-.1a5%205%200%200%200%203.9-3.9h.1s1%200%201-1-1-1-1-1h-.1A5%205%200%200%200%2011%205.1V5s0-1-1-1m0%202.5a3.5%203.5%200%201%201%200%207%203.5%203.5%200%201%201%200-7%22%2F%3E%3C%2Fsvg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background-error .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20fill%3D%22%23e54e33%22%20viewBox%3D%220%200%2020%2020%22%3E%3Cpath%20d%3D%22M10%204C9%204%209%205%209%205v.1A5%205%200%200%200%205.1%209H5s-1%200-1%201%201%201%201%201h.1A5%205%200%200%200%209%2014.9v.1s0%201%201%201%201-1%201-1v-.1a5%205%200%200%200%203.9-3.9h.1s1%200%201-1-1-1-1-1h-.1A5%205%200%200%200%2011%205.1V5s0-1-1-1m0%202.5a3.5%203.5%200%201%201%200%207%203.5%203.5%200%201%201%200-7%22%2F%3E%3C%2Fsvg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-waiting .maplibregl-ctrl-icon{animation:maplibregl-spin 2s linear infinite}@media (forced-colors:active){.maplibregl-ctrl button.maplibregl-ctrl-geolocate .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20fill%3D%22%23fff%22%20viewBox%3D%220%200%2020%2020%22%3E%3Cpath%20d%3D%22M10%204C9%204%209%205%209%205v.1A5%205%200%200%200%205.1%209H5s-1%200-1%201%201%201%201%201h.1A5%205%200%200%200%209%2014.9v.1s0%201%201%201%201-1%201-1v-.1a5%205%200%200%200%203.9-3.9h.1s1%200%201-1-1-1-1-1h-.1A5%205%200%200%200%2011%205.1V5s0-1-1-1m0%202.5a3.5%203.5%200%201%201%200%207%203.5%203.5%200%201%201%200-7%22%2F%3E%3Ccircle%20cx%3D%2210%22%20cy%3D%2210%22%20r%3D%222%22%2F%3E%3C%2Fsvg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate:disabled .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20fill%3D%22%23999%22%20viewBox%3D%220%200%2020%2020%22%3E%3Cpath%20d%3D%22M10%204C9%204%209%205%209%205v.1A5%205%200%200%200%205.1%209H5s-1%200-1%201%201%201%201%201h.1A5%205%200%200%200%209%2014.9v.1s0%201%201%201%201-1%201-1v-.1a5%205%200%200%200%203.9-3.9h.1s1%200%201-1-1-1-1-1h-.1A5%205%200%200%200%2011%205.1V5s0-1-1-1m0%202.5a3.5%203.5%200%201%201%200%207%203.5%203.5%200%201%201%200-7%22%2F%3E%3Ccircle%20cx%3D%2210%22%20cy%3D%2210%22%20r%3D%222%22%2F%3E%3Cpath%20fill%3D%22red%22%20d%3D%22m14%205%201%201-9%209-1-1z%22%2F%3E%3C%2Fsvg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20fill%3D%22%2333b5e5%22%20viewBox%3D%220%200%2020%2020%22%3E%3Cpath%20d%3D%22M10%204C9%204%209%205%209%205v.1A5%205%200%200%200%205.1%209H5s-1%200-1%201%201%201%201%201h.1A5%205%200%200%200%209%2014.9v.1s0%201%201%201%201-1%201-1v-.1a5%205%200%200%200%203.9-3.9h.1s1%200%201-1-1-1-1-1h-.1A5%205%200%200%200%2011%205.1V5s0-1-1-1m0%202.5a3.5%203.5%200%201%201%200%207%203.5%203.5%200%201%201%200-7%22%2F%3E%3Ccircle%20cx%3D%2210%22%20cy%3D%2210%22%20r%3D%222%22%2F%3E%3C%2Fsvg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-active-error .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20fill%3D%22%23e58978%22%20viewBox%3D%220%200%2020%2020%22%3E%3Cpath%20d%3D%22M10%204C9%204%209%205%209%205v.1A5%205%200%200%200%205.1%209H5s-1%200-1%201%201%201%201%201h.1A5%205%200%200%200%209%2014.9v.1s0%201%201%201%201-1%201-1v-.1a5%205%200%200%200%203.9-3.9h.1s1%200%201-1-1-1-1-1h-.1A5%205%200%200%200%2011%205.1V5s0-1-1-1m0%202.5a3.5%203.5%200%201%201%200%207%203.5%203.5%200%201%201%200-7%22%2F%3E%3Ccircle%20cx%3D%2210%22%20cy%3D%2210%22%20r%3D%222%22%2F%3E%3C%2Fsvg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20fill%3D%22%2333b5e5%22%20viewBox%3D%220%200%2020%2020%22%3E%3Cpath%20d%3D%22M10%204C9%204%209%205%209%205v.1A5%205%200%200%200%205.1%209H5s-1%200-1%201%201%201%201%201h.1A5%205%200%200%200%209%2014.9v.1s0%201%201%201%201-1%201-1v-.1a5%205%200%200%200%203.9-3.9h.1s1%200%201-1-1-1-1-1h-.1A5%205%200%200%200%2011%205.1V5s0-1-1-1m0%202.5a3.5%203.5%200%201%201%200%207%203.5%203.5%200%201%201%200-7%22%2F%3E%3C%2Fsvg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate.maplibregl-ctrl-geolocate-background-error .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20fill%3D%22%23e54e33%22%20viewBox%3D%220%200%2020%2020%22%3E%3Cpath%20d%3D%22M10%204C9%204%209%205%209%205v.1A5%205%200%200%200%205.1%209H5s-1%200-1%201%201%201%201%201h.1A5%205%200%200%200%209%2014.9v.1s0%201%201%201%201-1%201-1v-.1a5%205%200%200%200%203.9-3.9h.1s1%200%201-1-1-1-1-1h-.1A5%205%200%200%200%2011%205.1V5s0-1-1-1m0%202.5a3.5%203.5%200%201%201%200%207%203.5%203.5%200%201%201%200-7%22%2F%3E%3C%2Fsvg%3E")}}@media (forced-colors:active) and (prefers-color-scheme:light){.maplibregl-ctrl button.maplibregl-ctrl-geolocate .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20viewBox%3D%220%200%2020%2020%22%3E%3Cpath%20d%3D%22M10%204C9%204%209%205%209%205v.1A5%205%200%200%200%205.1%209H5s-1%200-1%201%201%201%201%201h.1A5%205%200%200%200%209%2014.9v.1s0%201%201%201%201-1%201-1v-.1a5%205%200%200%200%203.9-3.9h.1s1%200%201-1-1-1-1-1h-.1A5%205%200%200%200%2011%205.1V5s0-1-1-1m0%202.5a3.5%203.5%200%201%201%200%207%203.5%203.5%200%201%201%200-7%22%2F%3E%3Ccircle%20cx%3D%2210%22%20cy%3D%2210%22%20r%3D%222%22%2F%3E%3C%2Fsvg%3E")}.maplibregl-ctrl button.maplibregl-ctrl-geolocate:disabled .maplibregl-ctrl-icon{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2229%22%20height%3D%2229%22%20fill%3D%22%23666%22%20viewBox%3D%220%200%2020%2020%22%3E%3Cpath%20d%3D%22M10%204C9%204%209%205%209%205v.1A5%205%200%200%200%205.1%209H5s-1%200-1%201%201%201%201%201h.1A5%205%200%200%200%209%2014.9v.1s0%201%201%201%201-1%201-1v-.1a5%205%200%200%200%203.9-3.9h.1s1%200%201-1-1-1-1-1h-.1A5%205%200%200%200%2011%205.1V5s0-1-1-1m0%202.5a3.5%203.5%200%201%201%200%207%203.5%203.5%200%201%201%200-7%22%2F%3E%3Ccircle%20cx%3D%2210%22%20cy%3D%2210%22%20r%3D%222%22%2F%3E%3Cpath%20fill%3D%22red%22%20d%3D%22m14%205%201%201-9%209-1-1z%22%2F%3E%3C%2Fsvg%3E")}}@keyframes maplibregl-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}a.maplibregl-ctrl-logo{width:88px;height:23px;margin:0 0 -4px -4px;display:block;background-repeat:no-repeat;cursor:pointer;overflow:hidden;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2288%22%20height%3D%2223%22%20fill%3D%22none%22%3E%3Cpath%20fill%3D%22%23000%22%20fill-opacity%3D%22.4%22%20fill-rule%3D%22evenodd%22%20d%3D%22M17.408%2016.796h-1.827l2.501-12.095h.198l3.324%206.533.988%202.19.988-2.19%203.258-6.533h.181l2.6%2012.095h-1.81l-1.218-5.644-.362-1.71-.658%201.71-2.929%205.644h-.098l-2.914-5.644-.757-1.71-.345%201.71zm1.958-3.42-.726%203.663a1.255%201.255%200%200%201-1.232%201.011h-1.827a1.255%201.255%200%200%201-1.229-1.509l2.501-12.095a1.255%201.255%200%200%201%201.23-1.001h.197a1.25%201.25%200%200%201%201.12.685l3.19%206.273%203.125-6.263a1.25%201.25%200%200%201%201.123-.695h.181a1.255%201.255%200%200%201%201.227.991l1.443%206.71a5%205%200%200%201%20.314-.787l.009-.016a4.6%204.6%200%200%201%201.777-1.887c.782-.46%201.668-.667%202.611-.667a4.6%204.6%200%200%201%201.7.32l.306.134c.21-.16.474-.256.759-.256h1.694a1.255%201.255%200%200%201%201.212.925%201.255%201.255%200%200%201%201.212-.925h1.711c.284%200%20.545.094.755.252.613-.3%201.312-.45%202.075-.45%201.356%200%202.557.445%203.482%201.4q.47.48.763%201.064V4.701a1.255%201.255%200%200%201%201.255-1.255h1.86A1.255%201.255%200%200%201%2054.44%204.7v9.194h2.217c.19%200%20.37.043.532.118v-4.77c0-.356.147-.678.385-.906a2.42%202.42%200%200%201-.682-1.71c0-.665.267-1.253.735-1.7a2.45%202.45%200%200%201%201.722-.674%202.43%202.43%200%200%201%201.705.675q.318.302.504.683V4.7a1.255%201.255%200%200%201%201.255-1.255h1.744A1.255%201.255%200%200%201%2065.812%204.7v3.335a4.8%204.8%200%200%201%201.526-.246c.938%200%201.817.214%202.59.69a4.47%204.47%200%200%201%201.67%201.743v-.98a1.255%201.255%200%200%201%201.256-1.256h1.777c.233%200%20.451.064.639.174a3.4%203.4%200%200%201%201.567-.372c.346%200%20.861.02%201.285.232a1.25%201.25%200%200%201%20.689%201.004%204.7%204.7%200%200%201%20.853-.588c.795-.44%201.675-.647%202.61-.647%201.385%200%202.65.39%203.525%201.396.836.938%201.168%202.173%201.168%203.528q-.001.515-.056%201.051a1.255%201.255%200%200%201-.947%201.09l.408.952a1.255%201.255%200%200%201-.477%201.552c-.418.268-.92.463-1.458.612-.613.171-1.304.244-2.049.244-1.06%200-2.043-.207-2.886-.698l-.015-.008c-.798-.48-1.419-1.135-1.818-1.963l-.004-.008a5.8%205.8%200%200%201-.548-2.512q0-.429.053-.843a1.3%201.3%200%200%201-.333-.086l-.166-.004c-.223%200-.426.062-.643.228-.03.024-.142.139-.142.59v3.883a1.255%201.255%200%200%201-1.256%201.256h-1.777a1.255%201.255%200%200%201-1.256-1.256V15.69l-.032.057a4.8%204.8%200%200%201-1.86%201.833%205.04%205.04%200%200%201-2.484.634%204.5%204.5%200%200%201-1.935-.424%201.25%201.25%200%200%201-.764.258h-1.71a1.255%201.255%200%200%201-1.256-1.255V7.687a2.4%202.4%200%200%201-.428.625c.253.23.412.561.412.93v7.553a1.255%201.255%200%200%201-1.256%201.255h-1.843a1.25%201.25%200%200%201-.894-.373c-.228.23-.544.373-.894.373H51.32a1.255%201.255%200%200%201-1.256-1.255v-1.251l-.061.117a4.7%204.7%200%200%201-1.782%201.884%204.77%204.77%200%200%201-2.485.67%205.6%205.6%200%200%201-1.485-.188l.009%202.764a1.255%201.255%200%200%201-1.255%201.259h-1.729a1.255%201.255%200%200%201-1.255-1.255v-3.537a1.255%201.255%200%200%201-1.167.793h-1.679a1.25%201.25%200%200%201-.77-.263%204.5%204.5%200%200%201-1.945.429c-.885%200-1.724-.21-2.495-.632l-.017-.01a5%205%200%200%201-1.081-.836%201.255%201.255%200%200%201-1.254%201.312h-1.81a1.255%201.255%200%200%201-1.228-.99l-.782-3.625-2.044%203.939a1.25%201.25%200%200%201-1.115.676h-.098a1.25%201.25%200%200%201-1.116-.68l-2.061-3.994zM35.92%2016.63l.207-.114.223-.15q.493-.356.735-.785l.061-.118.033%201.332h1.678V9.242h-1.694l-.033%201.267q-.133-.329-.526-.658l-.032-.028a3.2%203.2%200%200%200-.668-.428l-.27-.12a3.3%203.3%200%200%200-1.235-.23q-1.136-.001-1.974.493a3.36%203.36%200%200%200-1.3%201.382q-.445.89-.444%202.074%200%201.2.51%202.107a3.8%203.8%200%200%200%201.382%201.381%203.9%203.9%200%200%200%201.893.477q.795%200%201.455-.33zm-2.789-5.38q-.576.675-.575%201.762%200%201.102.559%201.794.576.675%201.645.675a2.25%202.25%200%200%200%20.934-.19%202.2%202.2%200%200%200%20.468-.29l.178-.161a2.2%202.2%200%200%200%20.397-.561q.244-.5.244-1.15v-.115q0-.708-.296-1.267l-.043-.077a2.2%202.2%200%200%200-.633-.709l-.13-.086-.047-.028a2.1%202.1%200%200%200-1.073-.285q-1.052%200-1.629.692zm2.316%202.706c.163-.17.28-.407.28-.83v-.114c0-.292-.06-.508-.15-.68a.96.96%200%200%200-.353-.389.85.85%200%200%200-.464-.127c-.4%200-.56.114-.664.239l-.01.012c-.148.174-.275.45-.275.945%200%20.506.122.801.27.99.097.11.266.224.68.224.303%200%20.504-.09.687-.269zm7.545%201.705a2.6%202.6%200%200%200%20.331.423q.319.33.755.548l.173.074q.65.255%201.49.255%201.02%200%201.844-.493a3.45%203.45%200%200%200%201.316-1.4q.493-.904.493-2.089%200-1.909-.988-2.913-.988-1.02-2.584-1.02-.898%200-1.575.347a3%203%200%200%200-.415.262l-.199.166a3.4%203.4%200%200%200-.64.82V9.242h-1.712v11.553h1.729l-.017-5.134zm.53-1.138q.206.29.48.5l.155.11.053.034q.51.296%201.119.297%201.07%200%201.645-.675.577-.69.576-1.762%200-1.119-.576-1.777-.558-.675-1.645-.675-.435%200-.835.16a2%202%200%200%200-.284.136%202%202%200%200%200-.363.254%202.2%202.2%200%200%200-.46.569l-.082.162a2.6%202.6%200%200%200-.213%201.072v.115q0%20.707.296%201.267l.135.211zm.964-.818a1.1%201.1%200%200%200%20.367.385.94.94%200%200%200%20.476.118c.423%200%20.59-.117.687-.23.159-.194.28-.478.28-.95%200-.53-.133-.8-.266-.952l-.021-.025c-.078-.094-.231-.221-.68-.221a1%201%200%200%200-.503.135l-.012.007a.86.86%200%200%200-.335.343c-.073.133-.132.324-.132.614v.115a1.4%201.4%200%200%200%20.14.66zm15.7-6.222q.347-.346.346-.856a1.05%201.05%200%200%200-.345-.79%201.18%201.18%200%200%200-.84-.329q-.51%200-.855.33a1.05%201.05%200%200%200-.346.79q0%20.51.346.855.345.346.856.346.51%200%20.839-.346zm4.337%209.314.033-1.332q.191.403.59.747l.098.081a4%204%200%200%200%20.316.224l.223.122a3.2%203.2%200%200%200%201.44.322%203.8%203.8%200%200%200%201.875-.477%203.5%203.5%200%200%200%201.382-1.366q.527-.89.526-2.09%200-1.184-.444-2.073a3.24%203.24%200%200%200-1.283-1.399q-.823-.51-1.942-.51a3.5%203.5%200%200%200-1.527.344l-.086.043-.165.09a3%203%200%200%200-.33.214q-.432.315-.656.707a2%202%200%200%200-.099.198l.082-1.283V4.701h-1.744v12.095zm.473-2.509a2.5%202.5%200%200%200%20.566.7q.117.098.245.18l.144.08a2.1%202.1%200%200%200%20.975.232q1.07%200%201.645-.675.576-.69.576-1.778%200-1.102-.576-1.777-.56-.691-1.645-.692a2.2%202.2%200%200%200-1.015.235q-.22.113-.415.282l-.15.142a2.1%202.1%200%200%200-.42.594q-.223.479-.223%201.1v.115q0%20.705.293%201.26zm2.616-.293c.157-.191.28-.479.28-.967%200-.51-.13-.79-.276-.961l-.021-.026c-.082-.1-.232-.225-.67-.225a.87.87%200%200%200-.681.279l-.012.011c-.154.155-.274.38-.274.807v.115c0%20.285.057.499.144.669a1.1%201.1%200%200%200%20.367.405c.137.082.28.123.455.123.423%200%20.59-.118.686-.23zm8.266-3.013q.345-.13.724-.14l.069-.002q.493%200%20.642.099l.247-1.794q-.196-.099-.717-.099a2.3%202.3%200%200%200-.545.063%202%202%200%200%200-.411.148%202.2%202.2%200%200%200-.4.249%202.5%202.5%200%200%200-.485.499%202.7%202.7%200%200%200-.32.581l-.05.137v-1.48h-1.778v7.553h1.777v-3.884q0-.546.159-.943a1.5%201.5%200%200%201%20.466-.636%202.5%202.5%200%200%201%20.399-.253%202%202%200%200%201%20.224-.099zm9.784%202.656.05-.922q0-1.743-.856-2.698-.838-.97-2.584-.97-1.119-.001-2.007.493a3.46%203.46%200%200%200-1.4%201.382q-.493.906-.493%202.106%200%201.07.428%201.975.428.89%201.332%201.432.906.526%202.255.526.973%200%201.668-.185l.044-.012.135-.04q.613-.184.984-.421l-.542-1.267q-.3.162-.642.274l-.297.087q-.51.131-1.3.131-.954%200-1.497-.444a1.6%201.6%200%200%201-.192-.193q-.366-.44-.512-1.234l-.004-.021zm-5.427-1.256-.003.022h3.752v-.138q-.011-.727-.288-1.118a1%201%200%200%200-.156-.176q-.46-.428-1.316-.428-.986%200-1.494.604-.379.45-.494%201.234zm-27.053%202.77V4.7h-1.86v12.095h5.333V15.15zm7.103-5.908v7.553h-1.843V9.242h1.843z%22%2F%3E%3Cpath%20fill%3D%22%23fff%22%20d%3D%22m19.63%2011.151-.757-1.71-.345%201.71-1.12%205.644h-1.827L18.083%204.7h.197l3.325%206.533.988%202.19.988-2.19L26.839%204.7h.181l2.6%2012.095h-1.81l-1.218-5.644-.362-1.71-.658%201.71-2.93%205.644h-.098l-2.913-5.644zm14.836%205.81q-1.02%200-1.893-.478a3.8%203.8%200%200%201-1.381-1.382q-.51-.906-.51-2.106%200-1.185.444-2.074a3.36%203.36%200%200%201%201.3-1.382q.839-.494%201.974-.494a3.3%203.3%200%200%201%201.234.231%203.3%203.3%200%200%201%20.97.575q.396.33.527.659l.033-1.267h1.694v7.553H37.18l-.033-1.332q-.279.593-1.02%201.053a3.17%203.17%200%200%201-1.662.444zm.296-1.482q.938%200%201.58-.642.642-.66.642-1.711v-.115q0-.708-.296-1.267a2.2%202.2%200%200%200-.807-.872%202.1%202.1%200%200%200-1.119-.313q-1.053%200-1.629.692-.575.675-.575%201.76%200%201.103.559%201.795.577.675%201.645.675zm6.521-6.237h1.711v1.4q.906-1.597%202.83-1.597%201.596%200%202.584%201.02.988%201.005.988%202.914%200%201.185-.493%202.09a3.46%203.46%200%200%201-1.316%201.399%203.5%203.5%200%200%201-1.844.493q-.954%200-1.662-.329a2.67%202.67%200%200%201-1.086-.97l.017%205.134h-1.728zm4.048%206.22q1.07%200%201.645-.674.577-.69.576-1.762%200-1.119-.576-1.777-.558-.675-1.645-.675-.592%200-1.12.296-.51.28-.822.823-.296.527-.296%201.234v.115q0%20.708.296%201.267.313.543.823.855.51.296%201.119.297z%22%2F%3E%3Cpath%20fill%3D%22%23e1e3e9%22%20d%3D%22M51.325%204.7h1.86v10.45h3.473v1.646h-5.333zm7.12%204.542h1.843v7.553h-1.843zm.905-1.415a1.16%201.16%200%200%201-.856-.346%201.17%201.17%200%200%201-.346-.856%201.05%201.05%200%200%201%20.346-.79q.346-.329.856-.329.494%200%20.839.33a1.05%201.05%200%200%201%20.345.79%201.16%201.16%200%200%201-.345.855q-.33.346-.84.346zm7.875%209.133a3.17%203.17%200%200%201-1.662-.444q-.723-.46-1.004-1.053l-.033%201.332h-1.71V4.701h1.743v4.657l-.082%201.283q.279-.658%201.086-1.119a3.5%203.5%200%200%201%201.778-.477q1.119%200%201.942.51a3.24%203.24%200%200%201%201.283%201.4q.445.888.444%202.072%200%201.201-.526%202.09a3.5%203.5%200%200%201-1.382%201.366%203.8%203.8%200%200%201-1.876.477zm-.296-1.481q1.069%200%201.645-.675.577-.69.577-1.778%200-1.102-.577-1.776-.56-.691-1.645-.692a2.12%202.12%200%200%200-1.58.659q-.642.641-.642%201.694v.115q0%20.71.296%201.267a2.4%202.4%200%200%200%20.807.872%202.1%202.1%200%200%200%201.119.313zm5.927-6.237h1.777v1.481q.263-.757.856-1.217a2.14%202.14%200%200%201%201.349-.46q.527%200%20.724.098l-.247%201.794q-.149-.099-.642-.099-.774%200-1.416.494-.626.493-.626%201.58v3.883h-1.777V9.242zm9.534%207.718q-1.35%200-2.255-.526-.904-.543-1.332-1.432a4.6%204.6%200%200%201-.428-1.975q0-1.2.493-2.106a3.46%203.46%200%200%201%201.4-1.382q.889-.495%202.007-.494%201.744%200%202.584.97.855.956.856%202.7%200%20.444-.05.92h-5.43q.18%201.005.708%201.45.542.443%201.497.443.79%200%201.3-.131a4%204%200%200%200%20.938-.362l.542%201.267q-.411.263-1.119.46-.708.198-1.711.197zm1.596-4.558q.016-1.02-.444-1.432-.46-.428-1.316-.428-1.728%200-1.991%201.86z%22%2F%3E%3Cpath%20d%3D%22M5.074%2015.948a.484.657%200%200%200-.486.659v1.84a.484.657%200%200%200%20.486.659h4.101a.484.657%200%200%200%20.486-.659v-1.84a.484.657%200%200%200-.486-.659zm3.56%201.16H5.617v.838h3.017z%22%20style%3D%22fill%3A%23fff%3Bfill-rule%3Aevenodd%3Bstroke-width%3A1.03600001%22%2F%3E%3Cg%20style%3D%22stroke-width%3A1.12603545%22%3E%3Cpath%20d%3D%22M-9.408-1.416c-3.833-.025-7.056%202.912-7.08%206.615-.02%203.08%201.653%204.832%203.107%206.268.903.892%201.721%201.74%202.32%202.902l-.525-.004c-.543-.003-.992.304-1.24.639a1.87%201.87%200%200%200-.362%201.121l-.011%201.877c-.003.402.104.787.347%201.125.244.338.688.653%201.23.656l4.142.028c.542.003.99-.306%201.238-.641a1.87%201.87%200%200%200%20.363-1.121l.012-1.875a1.87%201.87%200%200%200-.348-1.127c-.243-.338-.688-.653-1.23-.656l-.518-.004c.597-1.145%201.425-1.983%202.348-2.87%201.473-1.414%203.18-3.149%203.2-6.226-.016-3.59-2.923-6.684-6.993-6.707m-.006%201.1v.002c3.274.02%205.92%202.532%205.9%205.6-.017%202.706-1.39%204.026-2.863%205.44-1.034.994-2.118%202.033-2.814%203.633-.018.041-.052.055-.075.065q-.013.004-.02.01a.34.34%200%200%201-.226.084.34.34%200%200%201-.224-.086l-.092-.077c-.699-1.615-1.768-2.669-2.781-3.67-1.454-1.435-2.797-2.762-2.78-5.478.02-3.067%202.7-5.545%205.975-5.523m-.02%202.826c-1.62-.01-2.944%201.315-2.955%202.96-.01%201.646%201.295%202.988%202.916%202.999h.002c1.621.01%202.943-1.316%202.953-2.961.011-1.646-1.294-2.988-2.916-2.998m-.005%201.1c1.017.006%201.829.83%201.822%201.89s-.83%201.874-1.848%201.867c-1.018-.006-1.829-.83-1.822-1.89s.83-1.874%201.848-1.868m-2.155%2011.857%204.14.025c.271.002.49.305.487.676l-.013%201.875c-.003.37-.224.67-.495.668l-4.14-.025c-.27-.002-.487-.306-.485-.676l.012-1.875c.003-.37.224-.67.494-.668%22%20style%3D%22color%3A%23000%3Bfont-style%3Anormal%3Bfont-variant%3Anormal%3Bfont-weight%3A400%3Bfont-stretch%3Anormal%3Bfont-size%3Amedium%3Bline-height%3Anormal%3Bfont-family%3Asans-serif%3Bfont-variant-ligatures%3Anormal%3Bfont-variant-position%3Anormal%3Bfont-variant-caps%3Anormal%3Bfont-variant-numeric%3Anormal%3Bfont-variant-alternates%3Anormal%3Bfont-feature-settings%3Anormal%3Btext-indent%3A0%3Btext-align%3Astart%3Btext-decoration%3Anone%3Btext-decoration-line%3Anone%3Btext-decoration-style%3Asolid%3Btext-decoration-color%3A%23000%3Bletter-spacing%3Anormal%3Bword-spacing%3Anormal%3Btext-transform%3Anone%3Bwriting-mode%3Alr-tb%3Bdirection%3Altr%3Btext-orientation%3Amixed%3Bdominant-baseline%3Aauto%3Bbaseline-shift%3Abaseline%3Btext-anchor%3Astart%3Bwhite-space%3Anormal%3Bshape-padding%3A0%3Bclip-rule%3Aevenodd%3Bdisplay%3Ainline%3Boverflow%3Avisible%3Bvisibility%3Avisible%3Bopacity%3A1%3Bisolation%3Aauto%3Bmix-blend-mode%3Anormal%3Bcolor-interpolation%3AsRGB%3Bcolor-interpolation-filters%3AlinearRGB%3Bsolid-color%3A%23000%3Bsolid-opacity%3A1%3Bvector-effect%3Anone%3Bfill%3A%23000%3Bfill-opacity%3A.4%3Bfill-rule%3Aevenodd%3Bstroke%3Anone%3Bstroke-width%3A2.47727823%3Bstroke-linecap%3Abutt%3Bstroke-linejoin%3Amiter%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-dashoffset%3A0%3Bstroke-opacity%3A1%3Bcolor-rendering%3Aauto%3Bimage-rendering%3Aauto%3Bshape-rendering%3Aauto%3Btext-rendering%3Aauto%22%20transform%3D%22translate(15.553%202.85)scale(.88807)%22%2F%3E%3Cpath%20d%3D%22M-9.415-.316C-12.69-.338-15.37%202.14-15.39%205.207c-.017%202.716%201.326%204.041%202.78%205.477%201.013%201%202.081%202.055%202.78%203.67l.092.076a.34.34%200%200%200%20.225.086.34.34%200%200%200%20.227-.083l.019-.01c.022-.009.057-.024.074-.064.697-1.6%201.78-2.64%202.814-3.634%201.473-1.414%202.847-2.733%202.864-5.44.02-3.067-2.627-5.58-5.901-5.601m-.057%208.784c1.621.011%202.944-1.315%202.955-2.96.01-1.646-1.295-2.988-2.916-2.999-1.622-.01-2.945%201.315-2.955%202.96s1.295%202.989%202.916%203%22%20style%3D%22clip-rule%3Aevenodd%3Bfill%3A%23e1e3e9%3Bfill-opacity%3A1%3Bfill-rule%3Aevenodd%3Bstroke%3Anone%3Bstroke-width%3A2.47727823%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A.4%22%20transform%3D%22translate(15.553%202.85)scale(.88807)%22%2F%3E%3Cpath%20d%3D%22M-11.594%2015.465c-.27-.002-.492.297-.494.668l-.012%201.876c-.003.371.214.673.485.675l4.14.027c.271.002.492-.298.495-.668l.012-1.877c.003-.37-.215-.672-.485-.674z%22%20style%3D%22clip-rule%3Aevenodd%3Bfill%3A%23fff%3Bfill-opacity%3A1%3Bfill-rule%3Aevenodd%3Bstroke%3Anone%3Bstroke-width%3A2.47727823%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A.4%22%20transform%3D%22translate(15.553%202.85)scale(.88807)%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")}a.maplibregl-ctrl-logo.maplibregl-compact{width:14px}@media (forced-colors:active){a.maplibregl-ctrl-logo{background-color:transparent;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2288%22%20height%3D%2223%22%20fill%3D%22none%22%3E%3Cpath%20fill%3D%22%23000%22%20fill-opacity%3D%22.4%22%20fill-rule%3D%22evenodd%22%20d%3D%22M17.408%2016.796h-1.827l2.501-12.095h.198l3.324%206.533.988%202.19.988-2.19%203.258-6.533h.181l2.6%2012.095h-1.81l-1.218-5.644-.362-1.71-.658%201.71-2.929%205.644h-.098l-2.914-5.644-.757-1.71-.345%201.71zm1.958-3.42-.726%203.663a1.255%201.255%200%200%201-1.232%201.011h-1.827a1.255%201.255%200%200%201-1.229-1.509l2.501-12.095a1.255%201.255%200%200%201%201.23-1.001h.197a1.25%201.25%200%200%201%201.12.685l3.19%206.273%203.125-6.263a1.25%201.25%200%200%201%201.123-.695h.181a1.255%201.255%200%200%201%201.227.991l1.443%206.71a5%205%200%200%201%20.314-.787l.009-.016a4.6%204.6%200%200%201%201.777-1.887c.782-.46%201.668-.667%202.611-.667a4.6%204.6%200%200%201%201.7.32l.306.134c.21-.16.474-.256.759-.256h1.694a1.255%201.255%200%200%201%201.212.925%201.255%201.255%200%200%201%201.212-.925h1.711c.284%200%20.545.094.755.252.613-.3%201.312-.45%202.075-.45%201.356%200%202.557.445%203.482%201.4q.47.48.763%201.064V4.701a1.255%201.255%200%200%201%201.255-1.255h1.86A1.255%201.255%200%200%201%2054.44%204.7v9.194h2.217c.19%200%20.37.043.532.118v-4.77c0-.356.147-.678.385-.906a2.42%202.42%200%200%201-.682-1.71c0-.665.267-1.253.735-1.7a2.45%202.45%200%200%201%201.722-.674%202.43%202.43%200%200%201%201.705.675q.318.302.504.683V4.7a1.255%201.255%200%200%201%201.255-1.255h1.744A1.255%201.255%200%200%201%2065.812%204.7v3.335a4.8%204.8%200%200%201%201.526-.246c.938%200%201.817.214%202.59.69a4.47%204.47%200%200%201%201.67%201.743v-.98a1.255%201.255%200%200%201%201.256-1.256h1.777c.233%200%20.451.064.639.174a3.4%203.4%200%200%201%201.567-.372c.346%200%20.861.02%201.285.232a1.25%201.25%200%200%201%20.689%201.004%204.7%204.7%200%200%201%20.853-.588c.795-.44%201.675-.647%202.61-.647%201.385%200%202.65.39%203.525%201.396.836.938%201.168%202.173%201.168%203.528q-.001.515-.056%201.051a1.255%201.255%200%200%201-.947%201.09l.408.952a1.255%201.255%200%200%201-.477%201.552c-.418.268-.92.463-1.458.612-.613.171-1.304.244-2.049.244-1.06%200-2.043-.207-2.886-.698l-.015-.008c-.798-.48-1.419-1.135-1.818-1.963l-.004-.008a5.8%205.8%200%200%201-.548-2.512q0-.429.053-.843a1.3%201.3%200%200%201-.333-.086l-.166-.004c-.223%200-.426.062-.643.228-.03.024-.142.139-.142.59v3.883a1.255%201.255%200%200%201-1.256%201.256h-1.777a1.255%201.255%200%200%201-1.256-1.256V15.69l-.032.057a4.8%204.8%200%200%201-1.86%201.833%205.04%205.04%200%200%201-2.484.634%204.5%204.5%200%200%201-1.935-.424%201.25%201.25%200%200%201-.764.258h-1.71a1.255%201.255%200%200%201-1.256-1.255V7.687a2.4%202.4%200%200%201-.428.625c.253.23.412.561.412.93v7.553a1.255%201.255%200%200%201-1.256%201.255h-1.843a1.25%201.25%200%200%201-.894-.373c-.228.23-.544.373-.894.373H51.32a1.255%201.255%200%200%201-1.256-1.255v-1.251l-.061.117a4.7%204.7%200%200%201-1.782%201.884%204.77%204.77%200%200%201-2.485.67%205.6%205.6%200%200%201-1.485-.188l.009%202.764a1.255%201.255%200%200%201-1.255%201.259h-1.729a1.255%201.255%200%200%201-1.255-1.255v-3.537a1.255%201.255%200%200%201-1.167.793h-1.679a1.25%201.25%200%200%201-.77-.263%204.5%204.5%200%200%201-1.945.429c-.885%200-1.724-.21-2.495-.632l-.017-.01a5%205%200%200%201-1.081-.836%201.255%201.255%200%200%201-1.254%201.312h-1.81a1.255%201.255%200%200%201-1.228-.99l-.782-3.625-2.044%203.939a1.25%201.25%200%200%201-1.115.676h-.098a1.25%201.25%200%200%201-1.116-.68l-2.061-3.994zM35.92%2016.63l.207-.114.223-.15q.493-.356.735-.785l.061-.118.033%201.332h1.678V9.242h-1.694l-.033%201.267q-.133-.329-.526-.658l-.032-.028a3.2%203.2%200%200%200-.668-.428l-.27-.12a3.3%203.3%200%200%200-1.235-.23q-1.136-.001-1.974.493a3.36%203.36%200%200%200-1.3%201.382q-.445.89-.444%202.074%200%201.2.51%202.107a3.8%203.8%200%200%200%201.382%201.381%203.9%203.9%200%200%200%201.893.477q.795%200%201.455-.33zm-2.789-5.38q-.576.675-.575%201.762%200%201.102.559%201.794.576.675%201.645.675a2.25%202.25%200%200%200%20.934-.19%202.2%202.2%200%200%200%20.468-.29l.178-.161a2.2%202.2%200%200%200%20.397-.561q.244-.5.244-1.15v-.115q0-.708-.296-1.267l-.043-.077a2.2%202.2%200%200%200-.633-.709l-.13-.086-.047-.028a2.1%202.1%200%200%200-1.073-.285q-1.052%200-1.629.692zm2.316%202.706c.163-.17.28-.407.28-.83v-.114c0-.292-.06-.508-.15-.68a.96.96%200%200%200-.353-.389.85.85%200%200%200-.464-.127c-.4%200-.56.114-.664.239l-.01.012c-.148.174-.275.45-.275.945%200%20.506.122.801.27.99.097.11.266.224.68.224.303%200%20.504-.09.687-.269zm7.545%201.705a2.6%202.6%200%200%200%20.331.423q.319.33.755.548l.173.074q.65.255%201.49.255%201.02%200%201.844-.493a3.45%203.45%200%200%200%201.316-1.4q.493-.904.493-2.089%200-1.909-.988-2.913-.988-1.02-2.584-1.02-.898%200-1.575.347a3%203%200%200%200-.415.262l-.199.166a3.4%203.4%200%200%200-.64.82V9.242h-1.712v11.553h1.729l-.017-5.134zm.53-1.138q.206.29.48.5l.155.11.053.034q.51.296%201.119.297%201.07%200%201.645-.675.577-.69.576-1.762%200-1.119-.576-1.777-.558-.675-1.645-.675-.435%200-.835.16a2%202%200%200%200-.284.136%202%202%200%200%200-.363.254%202.2%202.2%200%200%200-.46.569l-.082.162a2.6%202.6%200%200%200-.213%201.072v.115q0%20.707.296%201.267l.135.211zm.964-.818a1.1%201.1%200%200%200%20.367.385.94.94%200%200%200%20.476.118c.423%200%20.59-.117.687-.23.159-.194.28-.478.28-.95%200-.53-.133-.8-.266-.952l-.021-.025c-.078-.094-.231-.221-.68-.221a1%201%200%200%200-.503.135l-.012.007a.86.86%200%200%200-.335.343c-.073.133-.132.324-.132.614v.115a1.4%201.4%200%200%200%20.14.66zm15.7-6.222q.347-.346.346-.856a1.05%201.05%200%200%200-.345-.79%201.18%201.18%200%200%200-.84-.329q-.51%200-.855.33a1.05%201.05%200%200%200-.346.79q0%20.51.346.855.345.346.856.346.51%200%20.839-.346zm4.337%209.314.033-1.332q.191.403.59.747l.098.081a4%204%200%200%200%20.316.224l.223.122a3.2%203.2%200%200%200%201.44.322%203.8%203.8%200%200%200%201.875-.477%203.5%203.5%200%200%200%201.382-1.366q.527-.89.526-2.09%200-1.184-.444-2.073a3.24%203.24%200%200%200-1.283-1.399q-.823-.51-1.942-.51a3.5%203.5%200%200%200-1.527.344l-.086.043-.165.09a3%203%200%200%200-.33.214q-.432.315-.656.707a2%202%200%200%200-.099.198l.082-1.283V4.701h-1.744v12.095zm.473-2.509a2.5%202.5%200%200%200%20.566.7q.117.098.245.18l.144.08a2.1%202.1%200%200%200%20.975.232q1.07%200%201.645-.675.576-.69.576-1.778%200-1.102-.576-1.777-.56-.691-1.645-.692a2.2%202.2%200%200%200-1.015.235q-.22.113-.415.282l-.15.142a2.1%202.1%200%200%200-.42.594q-.223.479-.223%201.1v.115q0%20.705.293%201.26zm2.616-.293c.157-.191.28-.479.28-.967%200-.51-.13-.79-.276-.961l-.021-.026c-.082-.1-.232-.225-.67-.225a.87.87%200%200%200-.681.279l-.012.011c-.154.155-.274.38-.274.807v.115c0%20.285.057.499.144.669a1.1%201.1%200%200%200%20.367.405c.137.082.28.123.455.123.423%200%20.59-.118.686-.23zm8.266-3.013q.345-.13.724-.14l.069-.002q.493%200%20.642.099l.247-1.794q-.196-.099-.717-.099a2.3%202.3%200%200%200-.545.063%202%202%200%200%200-.411.148%202.2%202.2%200%200%200-.4.249%202.5%202.5%200%200%200-.485.499%202.7%202.7%200%200%200-.32.581l-.05.137v-1.48h-1.778v7.553h1.777v-3.884q0-.546.159-.943a1.5%201.5%200%200%201%20.466-.636%202.5%202.5%200%200%201%20.399-.253%202%202%200%200%201%20.224-.099zm9.784%202.656.05-.922q0-1.743-.856-2.698-.838-.97-2.584-.97-1.119-.001-2.007.493a3.46%203.46%200%200%200-1.4%201.382q-.493.906-.493%202.106%200%201.07.428%201.975.428.89%201.332%201.432.906.526%202.255.526.973%200%201.668-.185l.044-.012.135-.04q.613-.184.984-.421l-.542-1.267q-.3.162-.642.274l-.297.087q-.51.131-1.3.131-.954%200-1.497-.444a1.6%201.6%200%200%201-.192-.193q-.366-.44-.512-1.234l-.004-.021zm-5.427-1.256-.003.022h3.752v-.138q-.011-.727-.288-1.118a1%201%200%200%200-.156-.176q-.46-.428-1.316-.428-.986%200-1.494.604-.379.45-.494%201.234zm-27.053%202.77V4.7h-1.86v12.095h5.333V15.15zm7.103-5.908v7.553h-1.843V9.242h1.843z%22%2F%3E%3Cpath%20fill%3D%22%23fff%22%20d%3D%22m19.63%2011.151-.757-1.71-.345%201.71-1.12%205.644h-1.827L18.083%204.7h.197l3.325%206.533.988%202.19.988-2.19L26.839%204.7h.181l2.6%2012.095h-1.81l-1.218-5.644-.362-1.71-.658%201.71-2.93%205.644h-.098l-2.913-5.644zm14.836%205.81q-1.02%200-1.893-.478a3.8%203.8%200%200%201-1.381-1.382q-.51-.906-.51-2.106%200-1.185.444-2.074a3.36%203.36%200%200%201%201.3-1.382q.839-.494%201.974-.494a3.3%203.3%200%200%201%201.234.231%203.3%203.3%200%200%201%20.97.575q.396.33.527.659l.033-1.267h1.694v7.553H37.18l-.033-1.332q-.279.593-1.02%201.053a3.17%203.17%200%200%201-1.662.444zm.296-1.482q.938%200%201.58-.642.642-.66.642-1.711v-.115q0-.708-.296-1.267a2.2%202.2%200%200%200-.807-.872%202.1%202.1%200%200%200-1.119-.313q-1.053%200-1.629.692-.575.675-.575%201.76%200%201.103.559%201.795.577.675%201.645.675zm6.521-6.237h1.711v1.4q.906-1.597%202.83-1.597%201.596%200%202.584%201.02.988%201.005.988%202.914%200%201.185-.493%202.09a3.46%203.46%200%200%201-1.316%201.399%203.5%203.5%200%200%201-1.844.493q-.954%200-1.662-.329a2.67%202.67%200%200%201-1.086-.97l.017%205.134h-1.728zm4.048%206.22q1.07%200%201.645-.674.577-.69.576-1.762%200-1.119-.576-1.777-.558-.675-1.645-.675-.592%200-1.12.296-.51.28-.822.823-.296.527-.296%201.234v.115q0%20.708.296%201.267.313.543.823.855.51.296%201.119.297z%22%2F%3E%3Cpath%20fill%3D%22%23e1e3e9%22%20d%3D%22M51.325%204.7h1.86v10.45h3.473v1.646h-5.333zm7.12%204.542h1.843v7.553h-1.843zm.905-1.415a1.16%201.16%200%200%201-.856-.346%201.17%201.17%200%200%201-.346-.856%201.05%201.05%200%200%201%20.346-.79q.346-.329.856-.329.494%200%20.839.33a1.05%201.05%200%200%201%20.345.79%201.16%201.16%200%200%201-.345.855q-.33.346-.84.346zm7.875%209.133a3.17%203.17%200%200%201-1.662-.444q-.723-.46-1.004-1.053l-.033%201.332h-1.71V4.701h1.743v4.657l-.082%201.283q.279-.658%201.086-1.119a3.5%203.5%200%200%201%201.778-.477q1.119%200%201.942.51a3.24%203.24%200%200%201%201.283%201.4q.445.888.444%202.072%200%201.201-.526%202.09a3.5%203.5%200%200%201-1.382%201.366%203.8%203.8%200%200%201-1.876.477zm-.296-1.481q1.069%200%201.645-.675.577-.69.577-1.778%200-1.102-.577-1.776-.56-.691-1.645-.692a2.12%202.12%200%200%200-1.58.659q-.642.641-.642%201.694v.115q0%20.71.296%201.267a2.4%202.4%200%200%200%20.807.872%202.1%202.1%200%200%200%201.119.313zm5.927-6.237h1.777v1.481q.263-.757.856-1.217a2.14%202.14%200%200%201%201.349-.46q.527%200%20.724.098l-.247%201.794q-.149-.099-.642-.099-.774%200-1.416.494-.626.493-.626%201.58v3.883h-1.777V9.242zm9.534%207.718q-1.35%200-2.255-.526-.904-.543-1.332-1.432a4.6%204.6%200%200%201-.428-1.975q0-1.2.493-2.106a3.46%203.46%200%200%201%201.4-1.382q.889-.495%202.007-.494%201.744%200%202.584.97.855.956.856%202.7%200%20.444-.05.92h-5.43q.18%201.005.708%201.45.542.443%201.497.443.79%200%201.3-.131a4%204%200%200%200%20.938-.362l.542%201.267q-.411.263-1.119.46-.708.198-1.711.197zm1.596-4.558q.016-1.02-.444-1.432-.46-.428-1.316-.428-1.728%200-1.991%201.86z%22%2F%3E%3Cpath%20d%3D%22M5.074%2015.948a.484.657%200%200%200-.486.659v1.84a.484.657%200%200%200%20.486.659h4.101a.484.657%200%200%200%20.486-.659v-1.84a.484.657%200%200%200-.486-.659zm3.56%201.16H5.617v.838h3.017z%22%20style%3D%22fill%3A%23fff%3Bfill-rule%3Aevenodd%3Bstroke-width%3A1.03600001%22%2F%3E%3Cg%20style%3D%22stroke-width%3A1.12603545%22%3E%3Cpath%20d%3D%22M-9.408-1.416c-3.833-.025-7.056%202.912-7.08%206.615-.02%203.08%201.653%204.832%203.107%206.268.903.892%201.721%201.74%202.32%202.902l-.525-.004c-.543-.003-.992.304-1.24.639a1.87%201.87%200%200%200-.362%201.121l-.011%201.877c-.003.402.104.787.347%201.125.244.338.688.653%201.23.656l4.142.028c.542.003.99-.306%201.238-.641a1.87%201.87%200%200%200%20.363-1.121l.012-1.875a1.87%201.87%200%200%200-.348-1.127c-.243-.338-.688-.653-1.23-.656l-.518-.004c.597-1.145%201.425-1.983%202.348-2.87%201.473-1.414%203.18-3.149%203.2-6.226-.016-3.59-2.923-6.684-6.993-6.707m-.006%201.1v.002c3.274.02%205.92%202.532%205.9%205.6-.017%202.706-1.39%204.026-2.863%205.44-1.034.994-2.118%202.033-2.814%203.633-.018.041-.052.055-.075.065q-.013.004-.02.01a.34.34%200%200%201-.226.084.34.34%200%200%201-.224-.086l-.092-.077c-.699-1.615-1.768-2.669-2.781-3.67-1.454-1.435-2.797-2.762-2.78-5.478.02-3.067%202.7-5.545%205.975-5.523m-.02%202.826c-1.62-.01-2.944%201.315-2.955%202.96-.01%201.646%201.295%202.988%202.916%202.999h.002c1.621.01%202.943-1.316%202.953-2.961.011-1.646-1.294-2.988-2.916-2.998m-.005%201.1c1.017.006%201.829.83%201.822%201.89s-.83%201.874-1.848%201.867c-1.018-.006-1.829-.83-1.822-1.89s.83-1.874%201.848-1.868m-2.155%2011.857%204.14.025c.271.002.49.305.487.676l-.013%201.875c-.003.37-.224.67-.495.668l-4.14-.025c-.27-.002-.487-.306-.485-.676l.012-1.875c.003-.37.224-.67.494-.668%22%20style%3D%22color%3A%23000%3Bfont-style%3Anormal%3Bfont-variant%3Anormal%3Bfont-weight%3A400%3Bfont-stretch%3Anormal%3Bfont-size%3Amedium%3Bline-height%3Anormal%3Bfont-family%3Asans-serif%3Bfont-variant-ligatures%3Anormal%3Bfont-variant-position%3Anormal%3Bfont-variant-caps%3Anormal%3Bfont-variant-numeric%3Anormal%3Bfont-variant-alternates%3Anormal%3Bfont-feature-settings%3Anormal%3Btext-indent%3A0%3Btext-align%3Astart%3Btext-decoration%3Anone%3Btext-decoration-line%3Anone%3Btext-decoration-style%3Asolid%3Btext-decoration-color%3A%23000%3Bletter-spacing%3Anormal%3Bword-spacing%3Anormal%3Btext-transform%3Anone%3Bwriting-mode%3Alr-tb%3Bdirection%3Altr%3Btext-orientation%3Amixed%3Bdominant-baseline%3Aauto%3Bbaseline-shift%3Abaseline%3Btext-anchor%3Astart%3Bwhite-space%3Anormal%3Bshape-padding%3A0%3Bclip-rule%3Aevenodd%3Bdisplay%3Ainline%3Boverflow%3Avisible%3Bvisibility%3Avisible%3Bopacity%3A1%3Bisolation%3Aauto%3Bmix-blend-mode%3Anormal%3Bcolor-interpolation%3AsRGB%3Bcolor-interpolation-filters%3AlinearRGB%3Bsolid-color%3A%23000%3Bsolid-opacity%3A1%3Bvector-effect%3Anone%3Bfill%3A%23000%3Bfill-opacity%3A.4%3Bfill-rule%3Aevenodd%3Bstroke%3Anone%3Bstroke-width%3A2.47727823%3Bstroke-linecap%3Abutt%3Bstroke-linejoin%3Amiter%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-dashoffset%3A0%3Bstroke-opacity%3A1%3Bcolor-rendering%3Aauto%3Bimage-rendering%3Aauto%3Bshape-rendering%3Aauto%3Btext-rendering%3Aauto%22%20transform%3D%22translate(15.553%202.85)scale(.88807)%22%2F%3E%3Cpath%20d%3D%22M-9.415-.316C-12.69-.338-15.37%202.14-15.39%205.207c-.017%202.716%201.326%204.041%202.78%205.477%201.013%201%202.081%202.055%202.78%203.67l.092.076a.34.34%200%200%200%20.225.086.34.34%200%200%200%20.227-.083l.019-.01c.022-.009.057-.024.074-.064.697-1.6%201.78-2.64%202.814-3.634%201.473-1.414%202.847-2.733%202.864-5.44.02-3.067-2.627-5.58-5.901-5.601m-.057%208.784c1.621.011%202.944-1.315%202.955-2.96.01-1.646-1.295-2.988-2.916-2.999-1.622-.01-2.945%201.315-2.955%202.96s1.295%202.989%202.916%203%22%20style%3D%22clip-rule%3Aevenodd%3Bfill%3A%23e1e3e9%3Bfill-opacity%3A1%3Bfill-rule%3Aevenodd%3Bstroke%3Anone%3Bstroke-width%3A2.47727823%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A.4%22%20transform%3D%22translate(15.553%202.85)scale(.88807)%22%2F%3E%3Cpath%20d%3D%22M-11.594%2015.465c-.27-.002-.492.297-.494.668l-.012%201.876c-.003.371.214.673.485.675l4.14.027c.271.002.492-.298.495-.668l.012-1.877c.003-.37-.215-.672-.485-.674z%22%20style%3D%22clip-rule%3Aevenodd%3Bfill%3A%23fff%3Bfill-opacity%3A1%3Bfill-rule%3Aevenodd%3Bstroke%3Anone%3Bstroke-width%3A2.47727823%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A.4%22%20transform%3D%22translate(15.553%202.85)scale(.88807)%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")}}@media (forced-colors:active) and (prefers-color-scheme:light){a.maplibregl-ctrl-logo{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2288%22%20height%3D%2223%22%20fill%3D%22none%22%3E%3Cpath%20fill%3D%22%23000%22%20fill-opacity%3D%22.4%22%20fill-rule%3D%22evenodd%22%20d%3D%22M17.408%2016.796h-1.827l2.501-12.095h.198l3.324%206.533.988%202.19.988-2.19%203.258-6.533h.181l2.6%2012.095h-1.81l-1.218-5.644-.362-1.71-.658%201.71-2.929%205.644h-.098l-2.914-5.644-.757-1.71-.345%201.71zm1.958-3.42-.726%203.663a1.255%201.255%200%200%201-1.232%201.011h-1.827a1.255%201.255%200%200%201-1.229-1.509l2.501-12.095a1.255%201.255%200%200%201%201.23-1.001h.197a1.25%201.25%200%200%201%201.12.685l3.19%206.273%203.125-6.263a1.25%201.25%200%200%201%201.123-.695h.181a1.255%201.255%200%200%201%201.227.991l1.443%206.71a5%205%200%200%201%20.314-.787l.009-.016a4.6%204.6%200%200%201%201.777-1.887c.782-.46%201.668-.667%202.611-.667a4.6%204.6%200%200%201%201.7.32l.306.134c.21-.16.474-.256.759-.256h1.694a1.255%201.255%200%200%201%201.212.925%201.255%201.255%200%200%201%201.212-.925h1.711c.284%200%20.545.094.755.252.613-.3%201.312-.45%202.075-.45%201.356%200%202.557.445%203.482%201.4q.47.48.763%201.064V4.701a1.255%201.255%200%200%201%201.255-1.255h1.86A1.255%201.255%200%200%201%2054.44%204.7v9.194h2.217c.19%200%20.37.043.532.118v-4.77c0-.356.147-.678.385-.906a2.42%202.42%200%200%201-.682-1.71c0-.665.267-1.253.735-1.7a2.45%202.45%200%200%201%201.722-.674%202.43%202.43%200%200%201%201.705.675q.318.302.504.683V4.7a1.255%201.255%200%200%201%201.255-1.255h1.744A1.255%201.255%200%200%201%2065.812%204.7v3.335a4.8%204.8%200%200%201%201.526-.246c.938%200%201.817.214%202.59.69a4.47%204.47%200%200%201%201.67%201.743v-.98a1.255%201.255%200%200%201%201.256-1.256h1.777c.233%200%20.451.064.639.174a3.4%203.4%200%200%201%201.567-.372c.346%200%20.861.02%201.285.232a1.25%201.25%200%200%201%20.689%201.004%204.7%204.7%200%200%201%20.853-.588c.795-.44%201.675-.647%202.61-.647%201.385%200%202.65.39%203.525%201.396.836.938%201.168%202.173%201.168%203.528q-.001.515-.056%201.051a1.255%201.255%200%200%201-.947%201.09l.408.952a1.255%201.255%200%200%201-.477%201.552c-.418.268-.92.463-1.458.612-.613.171-1.304.244-2.049.244-1.06%200-2.043-.207-2.886-.698l-.015-.008c-.798-.48-1.419-1.135-1.818-1.963l-.004-.008a5.8%205.8%200%200%201-.548-2.512q0-.429.053-.843a1.3%201.3%200%200%201-.333-.086l-.166-.004c-.223%200-.426.062-.643.228-.03.024-.142.139-.142.59v3.883a1.255%201.255%200%200%201-1.256%201.256h-1.777a1.255%201.255%200%200%201-1.256-1.256V15.69l-.032.057a4.8%204.8%200%200%201-1.86%201.833%205.04%205.04%200%200%201-2.484.634%204.5%204.5%200%200%201-1.935-.424%201.25%201.25%200%200%201-.764.258h-1.71a1.255%201.255%200%200%201-1.256-1.255V7.687a2.4%202.4%200%200%201-.428.625c.253.23.412.561.412.93v7.553a1.255%201.255%200%200%201-1.256%201.255h-1.843a1.25%201.25%200%200%201-.894-.373c-.228.23-.544.373-.894.373H51.32a1.255%201.255%200%200%201-1.256-1.255v-1.251l-.061.117a4.7%204.7%200%200%201-1.782%201.884%204.77%204.77%200%200%201-2.485.67%205.6%205.6%200%200%201-1.485-.188l.009%202.764a1.255%201.255%200%200%201-1.255%201.259h-1.729a1.255%201.255%200%200%201-1.255-1.255v-3.537a1.255%201.255%200%200%201-1.167.793h-1.679a1.25%201.25%200%200%201-.77-.263%204.5%204.5%200%200%201-1.945.429c-.885%200-1.724-.21-2.495-.632l-.017-.01a5%205%200%200%201-1.081-.836%201.255%201.255%200%200%201-1.254%201.312h-1.81a1.255%201.255%200%200%201-1.228-.99l-.782-3.625-2.044%203.939a1.25%201.25%200%200%201-1.115.676h-.098a1.25%201.25%200%200%201-1.116-.68l-2.061-3.994zM35.92%2016.63l.207-.114.223-.15q.493-.356.735-.785l.061-.118.033%201.332h1.678V9.242h-1.694l-.033%201.267q-.133-.329-.526-.658l-.032-.028a3.2%203.2%200%200%200-.668-.428l-.27-.12a3.3%203.3%200%200%200-1.235-.23q-1.136-.001-1.974.493a3.36%203.36%200%200%200-1.3%201.382q-.445.89-.444%202.074%200%201.2.51%202.107a3.8%203.8%200%200%200%201.382%201.381%203.9%203.9%200%200%200%201.893.477q.795%200%201.455-.33zm-2.789-5.38q-.576.675-.575%201.762%200%201.102.559%201.794.576.675%201.645.675a2.25%202.25%200%200%200%20.934-.19%202.2%202.2%200%200%200%20.468-.29l.178-.161a2.2%202.2%200%200%200%20.397-.561q.244-.5.244-1.15v-.115q0-.708-.296-1.267l-.043-.077a2.2%202.2%200%200%200-.633-.709l-.13-.086-.047-.028a2.1%202.1%200%200%200-1.073-.285q-1.052%200-1.629.692zm2.316%202.706c.163-.17.28-.407.28-.83v-.114c0-.292-.06-.508-.15-.68a.96.96%200%200%200-.353-.389.85.85%200%200%200-.464-.127c-.4%200-.56.114-.664.239l-.01.012c-.148.174-.275.45-.275.945%200%20.506.122.801.27.99.097.11.266.224.68.224.303%200%20.504-.09.687-.269zm7.545%201.705a2.6%202.6%200%200%200%20.331.423q.319.33.755.548l.173.074q.65.255%201.49.255%201.02%200%201.844-.493a3.45%203.45%200%200%200%201.316-1.4q.493-.904.493-2.089%200-1.909-.988-2.913-.988-1.02-2.584-1.02-.898%200-1.575.347a3%203%200%200%200-.415.262l-.199.166a3.4%203.4%200%200%200-.64.82V9.242h-1.712v11.553h1.729l-.017-5.134zm.53-1.138q.206.29.48.5l.155.11.053.034q.51.296%201.119.297%201.07%200%201.645-.675.577-.69.576-1.762%200-1.119-.576-1.777-.558-.675-1.645-.675-.435%200-.835.16a2%202%200%200%200-.284.136%202%202%200%200%200-.363.254%202.2%202.2%200%200%200-.46.569l-.082.162a2.6%202.6%200%200%200-.213%201.072v.115q0%20.707.296%201.267l.135.211zm.964-.818a1.1%201.1%200%200%200%20.367.385.94.94%200%200%200%20.476.118c.423%200%20.59-.117.687-.23.159-.194.28-.478.28-.95%200-.53-.133-.8-.266-.952l-.021-.025c-.078-.094-.231-.221-.68-.221a1%201%200%200%200-.503.135l-.012.007a.86.86%200%200%200-.335.343c-.073.133-.132.324-.132.614v.115a1.4%201.4%200%200%200%20.14.66zm15.7-6.222q.347-.346.346-.856a1.05%201.05%200%200%200-.345-.79%201.18%201.18%200%200%200-.84-.329q-.51%200-.855.33a1.05%201.05%200%200%200-.346.79q0%20.51.346.855.345.346.856.346.51%200%20.839-.346zm4.337%209.314.033-1.332q.191.403.59.747l.098.081a4%204%200%200%200%20.316.224l.223.122a3.2%203.2%200%200%200%201.44.322%203.8%203.8%200%200%200%201.875-.477%203.5%203.5%200%200%200%201.382-1.366q.527-.89.526-2.09%200-1.184-.444-2.073a3.24%203.24%200%200%200-1.283-1.399q-.823-.51-1.942-.51a3.5%203.5%200%200%200-1.527.344l-.086.043-.165.09a3%203%200%200%200-.33.214q-.432.315-.656.707a2%202%200%200%200-.099.198l.082-1.283V4.701h-1.744v12.095zm.473-2.509a2.5%202.5%200%200%200%20.566.7q.117.098.245.18l.144.08a2.1%202.1%200%200%200%20.975.232q1.07%200%201.645-.675.576-.69.576-1.778%200-1.102-.576-1.777-.56-.691-1.645-.692a2.2%202.2%200%200%200-1.015.235q-.22.113-.415.282l-.15.142a2.1%202.1%200%200%200-.42.594q-.223.479-.223%201.1v.115q0%20.705.293%201.26zm2.616-.293c.157-.191.28-.479.28-.967%200-.51-.13-.79-.276-.961l-.021-.026c-.082-.1-.232-.225-.67-.225a.87.87%200%200%200-.681.279l-.012.011c-.154.155-.274.38-.274.807v.115c0%20.285.057.499.144.669a1.1%201.1%200%200%200%20.367.405c.137.082.28.123.455.123.423%200%20.59-.118.686-.23zm8.266-3.013q.345-.13.724-.14l.069-.002q.493%200%20.642.099l.247-1.794q-.196-.099-.717-.099a2.3%202.3%200%200%200-.545.063%202%202%200%200%200-.411.148%202.2%202.2%200%200%200-.4.249%202.5%202.5%200%200%200-.485.499%202.7%202.7%200%200%200-.32.581l-.05.137v-1.48h-1.778v7.553h1.777v-3.884q0-.546.159-.943a1.5%201.5%200%200%201%20.466-.636%202.5%202.5%200%200%201%20.399-.253%202%202%200%200%201%20.224-.099zm9.784%202.656.05-.922q0-1.743-.856-2.698-.838-.97-2.584-.97-1.119-.001-2.007.493a3.46%203.46%200%200%200-1.4%201.382q-.493.906-.493%202.106%200%201.07.428%201.975.428.89%201.332%201.432.906.526%202.255.526.973%200%201.668-.185l.044-.012.135-.04q.613-.184.984-.421l-.542-1.267q-.3.162-.642.274l-.297.087q-.51.131-1.3.131-.954%200-1.497-.444a1.6%201.6%200%200%201-.192-.193q-.366-.44-.512-1.234l-.004-.021zm-5.427-1.256-.003.022h3.752v-.138q-.011-.727-.288-1.118a1%201%200%200%200-.156-.176q-.46-.428-1.316-.428-.986%200-1.494.604-.379.45-.494%201.234zm-27.053%202.77V4.7h-1.86v12.095h5.333V15.15zm7.103-5.908v7.553h-1.843V9.242h1.843z%22%2F%3E%3Cpath%20fill%3D%22%23fff%22%20d%3D%22m19.63%2011.151-.757-1.71-.345%201.71-1.12%205.644h-1.827L18.083%204.7h.197l3.325%206.533.988%202.19.988-2.19L26.839%204.7h.181l2.6%2012.095h-1.81l-1.218-5.644-.362-1.71-.658%201.71-2.93%205.644h-.098l-2.913-5.644zm14.836%205.81q-1.02%200-1.893-.478a3.8%203.8%200%200%201-1.381-1.382q-.51-.906-.51-2.106%200-1.185.444-2.074a3.36%203.36%200%200%201%201.3-1.382q.839-.494%201.974-.494a3.3%203.3%200%200%201%201.234.231%203.3%203.3%200%200%201%20.97.575q.396.33.527.659l.033-1.267h1.694v7.553H37.18l-.033-1.332q-.279.593-1.02%201.053a3.17%203.17%200%200%201-1.662.444zm.296-1.482q.938%200%201.58-.642.642-.66.642-1.711v-.115q0-.708-.296-1.267a2.2%202.2%200%200%200-.807-.872%202.1%202.1%200%200%200-1.119-.313q-1.053%200-1.629.692-.575.675-.575%201.76%200%201.103.559%201.795.577.675%201.645.675zm6.521-6.237h1.711v1.4q.906-1.597%202.83-1.597%201.596%200%202.584%201.02.988%201.005.988%202.914%200%201.185-.493%202.09a3.46%203.46%200%200%201-1.316%201.399%203.5%203.5%200%200%201-1.844.493q-.954%200-1.662-.329a2.67%202.67%200%200%201-1.086-.97l.017%205.134h-1.728zm4.048%206.22q1.07%200%201.645-.674.577-.69.576-1.762%200-1.119-.576-1.777-.558-.675-1.645-.675-.592%200-1.12.296-.51.28-.822.823-.296.527-.296%201.234v.115q0%20.708.296%201.267.313.543.823.855.51.296%201.119.297z%22%2F%3E%3Cpath%20fill%3D%22%23e1e3e9%22%20d%3D%22M51.325%204.7h1.86v10.45h3.473v1.646h-5.333zm7.12%204.542h1.843v7.553h-1.843zm.905-1.415a1.16%201.16%200%200%201-.856-.346%201.17%201.17%200%200%201-.346-.856%201.05%201.05%200%200%201%20.346-.79q.346-.329.856-.329.494%200%20.839.33a1.05%201.05%200%200%201%20.345.79%201.16%201.16%200%200%201-.345.855q-.33.346-.84.346zm7.875%209.133a3.17%203.17%200%200%201-1.662-.444q-.723-.46-1.004-1.053l-.033%201.332h-1.71V4.701h1.743v4.657l-.082%201.283q.279-.658%201.086-1.119a3.5%203.5%200%200%201%201.778-.477q1.119%200%201.942.51a3.24%203.24%200%200%201%201.283%201.4q.445.888.444%202.072%200%201.201-.526%202.09a3.5%203.5%200%200%201-1.382%201.366%203.8%203.8%200%200%201-1.876.477zm-.296-1.481q1.069%200%201.645-.675.577-.69.577-1.778%200-1.102-.577-1.776-.56-.691-1.645-.692a2.12%202.12%200%200%200-1.58.659q-.642.641-.642%201.694v.115q0%20.71.296%201.267a2.4%202.4%200%200%200%20.807.872%202.1%202.1%200%200%200%201.119.313zm5.927-6.237h1.777v1.481q.263-.757.856-1.217a2.14%202.14%200%200%201%201.349-.46q.527%200%20.724.098l-.247%201.794q-.149-.099-.642-.099-.774%200-1.416.494-.626.493-.626%201.58v3.883h-1.777V9.242zm9.534%207.718q-1.35%200-2.255-.526-.904-.543-1.332-1.432a4.6%204.6%200%200%201-.428-1.975q0-1.2.493-2.106a3.46%203.46%200%200%201%201.4-1.382q.889-.495%202.007-.494%201.744%200%202.584.97.855.956.856%202.7%200%20.444-.05.92h-5.43q.18%201.005.708%201.45.542.443%201.497.443.79%200%201.3-.131a4%204%200%200%200%20.938-.362l.542%201.267q-.411.263-1.119.46-.708.198-1.711.197zm1.596-4.558q.016-1.02-.444-1.432-.46-.428-1.316-.428-1.728%200-1.991%201.86z%22%2F%3E%3Cpath%20d%3D%22M5.074%2015.948a.484.657%200%200%200-.486.659v1.84a.484.657%200%200%200%20.486.659h4.101a.484.657%200%200%200%20.486-.659v-1.84a.484.657%200%200%200-.486-.659zm3.56%201.16H5.617v.838h3.017z%22%20style%3D%22fill%3A%23fff%3Bfill-rule%3Aevenodd%3Bstroke-width%3A1.03600001%22%2F%3E%3Cg%20style%3D%22stroke-width%3A1.12603545%22%3E%3Cpath%20d%3D%22M-9.408-1.416c-3.833-.025-7.056%202.912-7.08%206.615-.02%203.08%201.653%204.832%203.107%206.268.903.892%201.721%201.74%202.32%202.902l-.525-.004c-.543-.003-.992.304-1.24.639a1.87%201.87%200%200%200-.362%201.121l-.011%201.877c-.003.402.104.787.347%201.125.244.338.688.653%201.23.656l4.142.028c.542.003.99-.306%201.238-.641a1.87%201.87%200%200%200%20.363-1.121l.012-1.875a1.87%201.87%200%200%200-.348-1.127c-.243-.338-.688-.653-1.23-.656l-.518-.004c.597-1.145%201.425-1.983%202.348-2.87%201.473-1.414%203.18-3.149%203.2-6.226-.016-3.59-2.923-6.684-6.993-6.707m-.006%201.1v.002c3.274.02%205.92%202.532%205.9%205.6-.017%202.706-1.39%204.026-2.863%205.44-1.034.994-2.118%202.033-2.814%203.633-.018.041-.052.055-.075.065q-.013.004-.02.01a.34.34%200%200%201-.226.084.34.34%200%200%201-.224-.086l-.092-.077c-.699-1.615-1.768-2.669-2.781-3.67-1.454-1.435-2.797-2.762-2.78-5.478.02-3.067%202.7-5.545%205.975-5.523m-.02%202.826c-1.62-.01-2.944%201.315-2.955%202.96-.01%201.646%201.295%202.988%202.916%202.999h.002c1.621.01%202.943-1.316%202.953-2.961.011-1.646-1.294-2.988-2.916-2.998m-.005%201.1c1.017.006%201.829.83%201.822%201.89s-.83%201.874-1.848%201.867c-1.018-.006-1.829-.83-1.822-1.89s.83-1.874%201.848-1.868m-2.155%2011.857%204.14.025c.271.002.49.305.487.676l-.013%201.875c-.003.37-.224.67-.495.668l-4.14-.025c-.27-.002-.487-.306-.485-.676l.012-1.875c.003-.37.224-.67.494-.668%22%20style%3D%22color%3A%23000%3Bfont-style%3Anormal%3Bfont-variant%3Anormal%3Bfont-weight%3A400%3Bfont-stretch%3Anormal%3Bfont-size%3Amedium%3Bline-height%3Anormal%3Bfont-family%3Asans-serif%3Bfont-variant-ligatures%3Anormal%3Bfont-variant-position%3Anormal%3Bfont-variant-caps%3Anormal%3Bfont-variant-numeric%3Anormal%3Bfont-variant-alternates%3Anormal%3Bfont-feature-settings%3Anormal%3Btext-indent%3A0%3Btext-align%3Astart%3Btext-decoration%3Anone%3Btext-decoration-line%3Anone%3Btext-decoration-style%3Asolid%3Btext-decoration-color%3A%23000%3Bletter-spacing%3Anormal%3Bword-spacing%3Anormal%3Btext-transform%3Anone%3Bwriting-mode%3Alr-tb%3Bdirection%3Altr%3Btext-orientation%3Amixed%3Bdominant-baseline%3Aauto%3Bbaseline-shift%3Abaseline%3Btext-anchor%3Astart%3Bwhite-space%3Anormal%3Bshape-padding%3A0%3Bclip-rule%3Aevenodd%3Bdisplay%3Ainline%3Boverflow%3Avisible%3Bvisibility%3Avisible%3Bopacity%3A1%3Bisolation%3Aauto%3Bmix-blend-mode%3Anormal%3Bcolor-interpolation%3AsRGB%3Bcolor-interpolation-filters%3AlinearRGB%3Bsolid-color%3A%23000%3Bsolid-opacity%3A1%3Bvector-effect%3Anone%3Bfill%3A%23000%3Bfill-opacity%3A.4%3Bfill-rule%3Aevenodd%3Bstroke%3Anone%3Bstroke-width%3A2.47727823%3Bstroke-linecap%3Abutt%3Bstroke-linejoin%3Amiter%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-dashoffset%3A0%3Bstroke-opacity%3A1%3Bcolor-rendering%3Aauto%3Bimage-rendering%3Aauto%3Bshape-rendering%3Aauto%3Btext-rendering%3Aauto%22%20transform%3D%22translate(15.553%202.85)scale(.88807)%22%2F%3E%3Cpath%20d%3D%22M-9.415-.316C-12.69-.338-15.37%202.14-15.39%205.207c-.017%202.716%201.326%204.041%202.78%205.477%201.013%201%202.081%202.055%202.78%203.67l.092.076a.34.34%200%200%200%20.225.086.34.34%200%200%200%20.227-.083l.019-.01c.022-.009.057-.024.074-.064.697-1.6%201.78-2.64%202.814-3.634%201.473-1.414%202.847-2.733%202.864-5.44.02-3.067-2.627-5.58-5.901-5.601m-.057%208.784c1.621.011%202.944-1.315%202.955-2.96.01-1.646-1.295-2.988-2.916-2.999-1.622-.01-2.945%201.315-2.955%202.96s1.295%202.989%202.916%203%22%20style%3D%22clip-rule%3Aevenodd%3Bfill%3A%23e1e3e9%3Bfill-opacity%3A1%3Bfill-rule%3Aevenodd%3Bstroke%3Anone%3Bstroke-width%3A2.47727823%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A.4%22%20transform%3D%22translate(15.553%202.85)scale(.88807)%22%2F%3E%3Cpath%20d%3D%22M-11.594%2015.465c-.27-.002-.492.297-.494.668l-.012%201.876c-.003.371.214.673.485.675l4.14.027c.271.002.492-.298.495-.668l.012-1.877c.003-.37-.215-.672-.485-.674z%22%20style%3D%22clip-rule%3Aevenodd%3Bfill%3A%23fff%3Bfill-opacity%3A1%3Bfill-rule%3Aevenodd%3Bstroke%3Anone%3Bstroke-width%3A2.47727823%3Bstroke-miterlimit%3A4%3Bstroke-dasharray%3Anone%3Bstroke-opacity%3A.4%22%20transform%3D%22translate(15.553%202.85)scale(.88807)%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E")}}.maplibregl-ctrl.maplibregl-ctrl-attrib{padding:0 5px;background-color:hsla(0,0%,100%,.5);margin:0}@media screen{.maplibregl-ctrl-attrib.maplibregl-compact{min-height:20px;padding:2px 24px 2px 0;margin:10px;position:relative;background-color:#fff;color:#000;border-radius:12px;box-sizing:content-box}.maplibregl-ctrl-attrib.maplibregl-compact-show{padding:2px 28px 2px 8px;visibility:visible}.maplibregl-ctrl-bottom-left>.maplibregl-ctrl-attrib.maplibregl-compact-show,.maplibregl-ctrl-top-left>.maplibregl-ctrl-attrib.maplibregl-compact-show{padding:2px 8px 2px 28px;border-radius:12px}.maplibregl-ctrl-attrib.maplibregl-compact .maplibregl-ctrl-attrib-inner{display:none}.maplibregl-ctrl-attrib-button{display:none;cursor:pointer;position:absolute;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20fill-rule%3D%22evenodd%22%20viewBox%3D%220%200%2020%2020%22%3E%3Cpath%20d%3D%22M4%2010a6%206%200%201%200%2012%200%206%206%200%201%200-12%200m5-3a1%201%200%201%200%202%200%201%201%200%201%200-2%200m0%203a1%201%200%201%201%202%200v3a1%201%200%201%201-2%200%22%2F%3E%3C%2Fsvg%3E");background-color:hsla(0,0%,100%,.5);width:24px;height:24px;box-sizing:border-box;border-radius:12px;outline:none;top:0;right:0;border:0}.maplibregl-ctrl-attrib summary.maplibregl-ctrl-attrib-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;list-style:none}.maplibregl-ctrl-attrib summary.maplibregl-ctrl-attrib-button::-webkit-details-marker{display:none}.maplibregl-ctrl-bottom-left .maplibregl-ctrl-attrib-button,.maplibregl-ctrl-top-left .maplibregl-ctrl-attrib-button{left:0}.maplibregl-ctrl-attrib.maplibregl-compact .maplibregl-ctrl-attrib-button,.maplibregl-ctrl-attrib.maplibregl-compact-show .maplibregl-ctrl-attrib-inner{display:block}.maplibregl-ctrl-attrib.maplibregl-compact-show .maplibregl-ctrl-attrib-button{background-color:rgba(0,0,0,.05)}.maplibregl-ctrl-bottom-right>.maplibregl-ctrl-attrib.maplibregl-compact:after{bottom:0;right:0}.maplibregl-ctrl-top-right>.maplibregl-ctrl-attrib.maplibregl-compact:after{top:0;right:0}.maplibregl-ctrl-top-left>.maplibregl-ctrl-attrib.maplibregl-compact:after{top:0;left:0}.maplibregl-ctrl-bottom-left>.maplibregl-ctrl-attrib.maplibregl-compact:after{bottom:0;left:0}}@media screen and (forced-colors:active){.maplibregl-ctrl-attrib.maplibregl-compact:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20fill%3D%22%23fff%22%20fill-rule%3D%22evenodd%22%20viewBox%3D%220%200%2020%2020%22%3E%3Cpath%20d%3D%22M4%2010a6%206%200%201%200%2012%200%206%206%200%201%200-12%200m5-3a1%201%200%201%200%202%200%201%201%200%201%200-2%200m0%203a1%201%200%201%201%202%200v3a1%201%200%201%201-2%200%22%2F%3E%3C%2Fsvg%3E")}}@media screen and (forced-colors:active) and (prefers-color-scheme:light){.maplibregl-ctrl-attrib.maplibregl-compact:after{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20fill-rule%3D%22evenodd%22%20viewBox%3D%220%200%2020%2020%22%3E%3Cpath%20d%3D%22M4%2010a6%206%200%201%200%2012%200%206%206%200%201%200-12%200m5-3a1%201%200%201%200%202%200%201%201%200%201%200-2%200m0%203a1%201%200%201%201%202%200v3a1%201%200%201%201-2%200%22%2F%3E%3C%2Fsvg%3E")}}.maplibregl-ctrl-attrib a{color:rgba(0,0,0,.75);text-decoration:none}.maplibregl-ctrl-attrib a:hover{color:inherit;text-decoration:underline}.maplibregl-attrib-empty{display:none}.maplibregl-ctrl-scale{background-color:hsla(0,0%,100%,.75);font-size:10px;white-space:nowrap;border-color:#333;border-style:none solid solid;border-width:medium 2px 2px;padding:0 5px;color:#333;box-sizing:border-box}.maplibregl-popup{position:absolute;top:0;left:0;display:flex;will-change:transform;pointer-events:none}.maplibregl-popup-anchor-top,.maplibregl-popup-anchor-top-left,.maplibregl-popup-anchor-top-right{flex-direction:column}.maplibregl-popup-anchor-bottom,.maplibregl-popup-anchor-bottom-left,.maplibregl-popup-anchor-bottom-right{flex-direction:column-reverse}.maplibregl-popup-anchor-left{flex-direction:row}.maplibregl-popup-anchor-right{flex-direction:row-reverse}.maplibregl-popup-tip{width:0;height:0;border:10px solid transparent;z-index:1}.maplibregl-popup-anchor-top .maplibregl-popup-tip{align-self:center;border-top:none;border-bottom-color:#fff}.maplibregl-popup-anchor-top-left .maplibregl-popup-tip{align-self:flex-start;border-top:none;border-left:none;border-bottom-color:#fff}.maplibregl-popup-anchor-top-right .maplibregl-popup-tip{align-self:flex-end;border-top:none;border-right:none;border-bottom-color:#fff}.maplibregl-popup-anchor-bottom .maplibregl-popup-tip{align-self:center;border-bottom:none;border-top-color:#fff}.maplibregl-popup-anchor-bottom-left .maplibregl-popup-tip{align-self:flex-start;border-bottom:none;border-left:none;border-top-color:#fff}.maplibregl-popup-anchor-bottom-right .maplibregl-popup-tip{align-self:flex-end;border-bottom:none;border-right:none;border-top-color:#fff}.maplibregl-popup-anchor-left .maplibregl-popup-tip{align-self:center;border-left:none;border-right-color:#fff}.maplibregl-popup-anchor-right .maplibregl-popup-tip{align-self:center;border-right:none;border-left-color:#fff}[dir=rtl] .maplibregl-popup-anchor-left{flex-direction:row-reverse}[dir=rtl] .maplibregl-popup-anchor-right{flex-direction:row}[dir=rtl] .maplibregl-popup-anchor-top-left .maplibregl-popup-tip{align-self:flex-end}[dir=rtl] .maplibregl-popup-anchor-top-right .maplibregl-popup-tip{align-self:flex-start}[dir=rtl] .maplibregl-popup-anchor-bottom-left .maplibregl-popup-tip{align-self:flex-end}[dir=rtl] .maplibregl-popup-anchor-bottom-right .maplibregl-popup-tip{align-self:flex-start}.maplibregl-popup-close-button{position:absolute;right:0;top:0;border:0;border-radius:0 3px 0 0;cursor:pointer;background-color:transparent}.maplibregl-popup-close-button:hover{background-color:rgba(0,0,0,.05)}.maplibregl-popup-content{position:relative;background:#fff;border-radius:3px;box-shadow:0 1px 2px rgba(0,0,0,.1);padding:15px 10px;pointer-events:auto}.maplibregl-popup-anchor-top-left .maplibregl-popup-content{border-top-left-radius:0}.maplibregl-popup-anchor-top-right .maplibregl-popup-content{border-top-right-radius:0}.maplibregl-popup-anchor-bottom-left .maplibregl-popup-content{border-bottom-left-radius:0}.maplibregl-popup-anchor-bottom-right .maplibregl-popup-content{border-bottom-right-radius:0}.maplibregl-popup-track-pointer{display:none}.maplibregl-popup-track-pointer *{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.maplibregl-map:hover .maplibregl-popup-track-pointer{display:flex}.maplibregl-map:active .maplibregl-popup-track-pointer{display:none}.maplibregl-marker{position:absolute;top:0;left:0;will-change:transform;transition:opacity .2s}.maplibregl-marker-draggable{cursor:grab}.maplibregl-user-location-dot,.maplibregl-user-location-dot:before{background-color:#1da1f2;width:15px;height:15px;border-radius:50%}.maplibregl-user-location-dot:before{content:"";position:absolute;animation:maplibregl-user-location-dot-pulse 2s infinite}.maplibregl-user-location-dot:after{border-radius:50%;border:2px solid #fff;content:"";height:19px;left:-2px;position:absolute;top:-2px;width:19px;box-sizing:border-box;box-shadow:0 0 3px rgba(0,0,0,.35)}@media (prefers-reduced-motion:reduce){.maplibregl-user-location-dot:before{animation:none}}@keyframes maplibregl-user-location-dot-pulse{0%{transform:scale(1);opacity:1}70%{transform:scale(3);opacity:0}to{transform:scale(1);opacity:0}}.maplibregl-user-location-dot-stale{background-color:#aaa}.maplibregl-user-location-dot-stale:after{display:none}.maplibregl-user-location-accuracy-circle{background-color:#1da1f233;width:1px;height:1px;border-radius:100%}.maplibregl-crosshair,.maplibregl-crosshair .maplibregl-interactive,.maplibregl-crosshair .maplibregl-interactive:active{cursor:crosshair}.maplibregl-boxzoom{position:absolute;top:0;left:0;width:0;height:0;background:#fff;border:2px dotted #202020;opacity:.5}.maplibregl-cooperative-gesture-screen{background:rgba(0,0,0,.4);position:absolute;inset:0;display:flex;justify-content:center;align-items:center;color:#fff;padding:1rem;font-size:1.4em;line-height:1.2;opacity:0;pointer-events:none;transition:opacity 1s ease 1s;z-index:99999}.maplibregl-cooperative-gesture-screen.maplibregl-show{opacity:1;transition:opacity .05s}.maplibregl-cooperative-gesture-screen .maplibregl-mobile-message{display:none}@media (hover:none),(pointer:coarse){.maplibregl-cooperative-gesture-screen .maplibregl-desktop-message{display:none}.maplibregl-cooperative-gesture-screen .maplibregl-mobile-message{display:block}}.maplibregl-pseudo-fullscreen{position:fixed!important;width:100%!important;height:100%!important;top:0!important;left:0!important;z-index:99999} \ No newline at end of file diff --git a/web/vendor/maplibre/maplibre-gl.mjs b/web/vendor/maplibre/maplibre-gl.mjs index cef84f20d..16c40880e 100644 --- a/web/vendor/maplibre/maplibre-gl.mjs +++ b/web/vendor/maplibre/maplibre-gl.mjs @@ -1,8 +1,8 @@ /** * MapLibre GL JS -* @license 3-Clause BSD. Full text of license: https://github.com/maplibre/maplibre-gl-js/blob/v6.0.0/LICENSE.txt +* @license 3-Clause BSD. Full text of license: https://github.com/maplibre/maplibre-gl-js/blob/v6.7.0/LICENSE.txt */ -import{$n as e,$r as t,$t as n,A as r,Ai as i,Ar as a,At as o,B as s,Bn as c,Br as l,Bt as u,C as d,Ci as f,Cn as p,Cr as m,Ct as h,D as g,Di as _,Dn as v,Dr as y,Dt as b,E as x,Ei as S,En as C,Er as w,Et as T,F as ee,Fn as E,Fr as D,Ft as O,G as te,Gn as k,Gr as A,Gt as ne,Hn as re,Hr as ie,Ht as ae,I as oe,In as j,Ir as se,It as ce,J as le,Jn as ue,Jr as de,Jt as fe,K as pe,Kn as me,Kr as he,L as ge,Ln as _e,Lr as ve,Lt as ye,M as be,Mn as xe,Mr as Se,Mt as Ce,N as we,Nn as Te,Nr as Ee,Nt as De,O as Oe,Oi as ke,On as Ae,Or as je,Ot as Me,P as Ne,Pn as Pe,Pr as M,Pt as Fe,Qn as Ie,Qr as Le,Qt as Re,R as ze,Rn as Be,Rr as Ve,Rt as He,S as N,Si as Ue,Sn as We,Sr as Ge,St as P,T as Ke,Ti as F,Tn as qe,Tr as Je,Tt as Ye,U as Xe,Un as Ze,Ur as Qe,Ut as $e,V as et,Vn as I,Vr as tt,Vt as nt,Wn as rt,Wr as it,Wt as at,X as ot,Xn as st,Xr as ct,Xt as lt,Yn as ut,Yr as dt,Z as ft,Zn as L,Zr as pt,Zt as mt,_ as ht,_i as gt,_n as _t,_r as vt,_t as yt,a as bt,ai as xt,an as St,ar as Ct,at as wt,b as Tt,bi as Et,bn as Dt,br as Ot,bt as kt,ci as At,cn as jt,cr as Mt,ct as Nt,di as Pt,dn as Ft,dr as It,dt as Lt,ei as Rt,en as zt,er as Bt,et as Vt,f as Ht,fi as Ut,fn as R,fr as Wt,ft as Gt,g as Kt,gi as qt,gn as Jt,gr as Yt,gt as Xt,h as Zt,hi as Qt,hn as $t,hr as en,ht as tn,ii as nn,ir as rn,j as an,ji as z,jn as on,jr as sn,jt as cn,k as B,ki as ln,kn as un,kr as dn,kt as fn,li as pn,lr as mn,lt as hn,mi as gn,mn as _n,mr as vn,mt as yn,ni as bn,nn as xn,nr as Sn,oi as Cn,on as wn,or as Tn,ot as En,pi as Dn,pn as On,pr as kn,pt as An,q as jn,qn as Mn,qr as Nn,qt as Pn,r as Fn,ri as In,rn as Ln,rt as Rn,s as zn,si as Bn,sn as Vn,sr as Hn,st as Un,t as Wn,ti as Gn,tn as V,tr as Kn,u as qn,ui as Jn,un as Yn,ur as Xn,v as Zn,vi as Qn,vn as $n,vr as er,vt as tr,w as nr,wi as rr,wn as ir,wr as ar,wt as or,x as sr,xi as cr,xn as lr,xr as ur,xt as H,y as dr,yi as fr,yn as pr,yr as mr,yt as hr,z as gr,zn as _r,zr as vr,zt as yr}from"./maplibre-gl-shared.mjs";var br=`6.0.0`;function xr(){var e=new ke(4);return ke!=Float32Array&&(e[1]=0,e[2]=0),e[0]=1,e[3]=1,e}function Sr(e,t){var n=t[0],r=t[1],i=t[2],a=t[3],o=n*a-i*r;return o?(o=1/o,e[0]=a*o,e[1]=-r*o,e[2]=-i*o,e[3]=n*o,e):null}function Cr(e,t,n){var r=t[0],i=t[1],a=t[2],o=t[3],s=Math.sin(n),c=Math.cos(n);return e[0]=r*c+a*s,e[1]=i*c+o*s,e[2]=r*-s+a*c,e[3]=i*-s+o*c,e}let wr,Tr,Er;const Dr={frame(e,t,n,r){let i=r||window,a=i.requestAnimationFrame(e=>{o(),t(e)}),{unsubscribe:o}=w(e.signal,`abort`,()=>{o(),i.cancelAnimationFrame(a),n(new v(e.signal.reason))},!1)},frameAsync(e,t){return new Promise((n,r)=>{this.frame(e,n,r,t)})},getImageData(e,t=0){return this.getImageCanvasContext(e).getImageData(-t,-t,e.width+2*t,e.height+2*t)},getImageCanvasContext(e){let t=window.document.createElement(`canvas`),n=t.getContext(`2d`,{willReadFrequently:!0});if(!n)throw Error(`failed to create canvas 2d context`);return t.width=e.width,t.height=e.height,n.drawImage(e,0,0,e.width,e.height),n},resolveURL(e){return wr||=document.createElement(`a`),wr.href=e,wr.href},get hardwareConcurrency(){return typeof navigator<`u`&&navigator.hardwareConcurrency||4},get prefersReducedMotion(){return Er===void 0?matchMedia?(Tr??=matchMedia(`(prefers-reduced-motion: reduce)`),Tr.matches):!1:Er},set prefersReducedMotion(e){Er=e}},Or=new class{constructor(){this._frozenAt=null}getCurrentTime(){return this._frozenAt===null?performance.now():this._frozenAt}setNow(e){this._frozenAt=e}restoreNow(){this._frozenAt=null}isFrozen(){return this._frozenAt!==null}};function U(){return Or.getCurrentTime()}function kr(e){Or.setNow(e)}function Ar(){Or.restoreNow()}function jr(){return Or.isFrozen()}var W=class e{static{this.docStyle=typeof window<`u`&&window.document?.documentElement.style}static{this.selectProp=!e.docStyle||`userSelect`in e.docStyle?`userSelect`:`webkitUserSelect`}static create(e,t,n){let r=window.document.createElement(e);return t!==void 0&&(r.className=t),n&&n.appendChild(r),r}static createNS(e,t){return window.document.createElementNS(e,t)}static disableDrag(){e.docStyle&&e.selectProp&&(e.userSelect=e.docStyle[e.selectProp],e.docStyle[e.selectProp]=`none`)}static enableDrag(){e.docStyle&&e.selectProp&&(e.docStyle[e.selectProp]=e.userSelect)}static suppressClickInternal(t){t.preventDefault(),t.stopPropagation(),window.removeEventListener(`click`,e.suppressClickInternal,!0)}static suppressClick(){window.addEventListener(`click`,e.suppressClickInternal,!0),window.setTimeout(()=>{window.removeEventListener(`click`,e.suppressClickInternal,!0)},0)}static getScale(e){let t=e.getBoundingClientRect();return{x:t.width/e.offsetWidth||1,y:t.height/e.offsetHeight||1,boundingClientRect:t}}static getPoint(e,t,n){let r=t.boundingClientRect;return new z((n.clientX-r.left)/t.x-e.clientLeft,(n.clientY-r.top)/t.y-e.clientTop)}static mousePos(t,n){let r=e.getScale(t);return e.getPoint(t,r,n)}static touchPos(t,n){let r=[],i=e.getScale(t);for(let a of n)r.push(e.getPoint(t,i,a));return r}static sanitize(t){let n=new DOMParser().parseFromString(t,`text/html`).body||document.createElement(`body`),r=n.querySelectorAll(`script`);for(let e of r)e.remove();return e.clean(n),n.innerHTML}static isPossiblyDangerous(e,t){let n=t.replace(/\s+/g,``).toLowerCase();if([`src`,`href`,`xlink:href`].includes(e)&&(n.includes(`javascript:`)||n.includes(`data:`))||e.startsWith(`on`))return!0}static clean(t){let n=t.children;for(let t of n)e.removeAttributes(t),e.clean(t)}static removeAttributes(t){for(let{name:n,value:r}of t.attributes)e.isPossiblyDangerous(n,r)&&t.removeAttribute(n)}};let Mr;(function(e){let t,n,r,i;e.resetRequestQueue=()=>{t=[],n=0,r=0,i={}},e.addThrottleControl=e=>{let t=r++;return i[t]=e,t},e.removeThrottleControl=e=>{delete i[e],c()};let a=()=>{for(let e of Object.keys(i))if(i[e]())return!0;return!1};e.getImage=(e,n,r=!0,i)=>new Promise((a,o)=>{e.headers||={},e.headers.accept=`image/webp,*/*`,L(e,{type:`image`});let s={abortController:n,requestParameters:e,supportImageRefresh:r,imageBitmapOptions:i,state:`queued`,onError:e=>{o(e)},onSuccess:e=>{a(e)}};t.push(s),c()});let o=(e,t)=>typeof createImageBitmap==`function`?Pe(e,t):Te(e),s=async e=>{e.state=`running`;let{requestParameters:t,supportImageRefresh:r,imageBitmapOptions:i,onError:a,onSuccess:s,abortController:u}=e,d=r===!1&&!i&&!Xn(self)&&!ir(t.url)&&(!t.headers||Object.keys(t.headers).reduce((e,t)=>e&&t===`accept`,!0));n++;let f=d?l(t,u):lr(t,u);try{let t=await f;delete e.abortController,e.state=`completed`,t.data instanceof HTMLImageElement||Ct(t.data)?s(t):t.data&&s({data:await o(t.data,i),cacheControl:t.cacheControl,expires:t.expires})}catch(t){delete e.abortController,a(ut(t))}finally{n--,c()}},c=()=>{let e=a()?C.MAX_PARALLEL_IMAGE_REQUESTS_PER_FRAME:C.MAX_PARALLEL_IMAGE_REQUESTS;for(let r=n;r0;r++){let e=t.shift();if(e.abortController.signal.aborted){r--;continue}s(e)}},l=(e,t)=>new Promise((n,r)=>{let i=new Image,a=e.url,o=e.credentials;o&&o===`include`?i.crossOrigin=`use-credentials`:(o&&o===`same-origin`||!We(a))&&(i.crossOrigin=`anonymous`),t.signal.addEventListener(`abort`,()=>{i.src=``,r(new v(t.signal.reason))}),i.fetchPriority=`high`,i.onload=()=>{i.onerror=i.onload=null,n({data:i})},i.onerror=()=>{i.onerror=i.onload=null,!t.signal.aborted&&r(Error(`Could not load image. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported.`))},i.src=a})})(Mr||={}),Mr.resetRequestQueue();var Nr=class{constructor(e){this._transformRequestFn=e??null}transformRequest(e,t){return this._transformRequestFn&&this._transformRequestFn(e,t)||{url:e}}setTransformRequest(e){this._transformRequestFn=e}},Pr=class extends On{},G=class extends Pr{},Fr=class extends Pr{constructor(e={}){super(`style.load`,e)}},Ir=class extends Pr{constructor(e,t={}){super(e,t),this.dataType=`style`}},K=class extends Pr{constructor(e,t={}){super(e,t),this.dataType=`source`}},Lr=class extends Pr{preventDefault(){this._defaultPrevented=!0}get defaultPrevented(){return this._defaultPrevented}constructor(e,t,n,r={}){n=n instanceof MouseEvent?n:new MouseEvent(e,n);let i=W.mousePos(t.getCanvas(),n),a=t.unproject(i);super(e,L({point:i,lngLat:a,originalEvent:n},r)),this._defaultPrevented=!1,this.target=t}},Rr=class extends Pr{preventDefault(){this._defaultPrevented=!0}get defaultPrevented(){return this._defaultPrevented}constructor(e,t,n){let r=e===`touchend`?n.changedTouches:n.touches,i=W.touchPos(t.getCanvasContainer(),r),a=i.map(e=>t.unproject(e)),o=i.reduce((e,t,n,r)=>e.add(t.div(r.length)),new z(0,0)),s=t.unproject(o);super(e,{points:i,point:o,lngLats:a,lngLat:s,originalEvent:n}),this._defaultPrevented=!1}},zr=class extends Pr{preventDefault(){this._defaultPrevented=!0}get defaultPrevented(){return this._defaultPrevented}constructor(e,t){super(`wheel`,{originalEvent:t}),this._defaultPrevented=!1}},Br=class extends Pr{},Vr=class extends Pr{constructor(e={}){super(`terrain`,e)}},Hr=class extends Pr{constructor(e={}){super(`projectiontransition`,e)}},Ur=class extends Pr{},Wr=class extends Pr{constructor(e={}){super(`styleimagemissing`,e)}};function Gr(e){let t=[];if(typeof e==`string`)t.push({id:`default`,url:e});else if(e&&e.length>0){let n=[];for(let{id:r,url:i}of e){let e=`${r}${i}`;n.includes(e)||(n.push(e),t.push({id:r,url:i}))}}return t}function Kr(e,t,n){try{let r=new URL(e);return r.pathname+=`${t}${n}`,r.toString()}catch{throw Error(`Invalid sprite URL "${e}", must be absolute. Modify style specification directly or use TransformStyleFunction to correct the issue dynamically`)}}async function qr(e,t,n,r){let i=Gr(e),a=n>1?`@2x`:``,o={},s={};for(let{id:e,url:n}of i){o[e]=$n(await t.transformRequest(Kr(n,a,`.json`),`SpriteJSON`),r);let i=await t.transformRequest(Kr(n,a,`.png`),`SpriteImage`);s[e]=Mr.getImage(i,r)}return await Promise.all([...Object.values(o),...Object.values(s)]),Jr(o,s)}async function Jr(e,t){let n={};for(let r in e){n[r]={};let i=Dr.getImageCanvasContext((await t[r]).data),a=(await e[r]).data;for(let e in a){let{width:t,height:o,x:s,y:c,sdf:l,pixelRatio:u,stretchX:d,stretchY:f,content:p,textFitWidth:m,textFitHeight:h}=a[e],g={width:t,height:o,x:s,y:c,context:i};n[r][e]={data:null,pixelRatio:u,sdf:l,stretchX:d,stretchY:f,content:p,textFitWidth:m,textFitHeight:h,spriteData:g}}}return n}var Yr=class extends _n{constructor(){super(),this.images={},this.updatedImages={},this.callbackDispatchedThisFrame={},this.loaded=!1,this.requestors=[],this.missingImageResolver=null,this.patterns={},this.atlasImage=new yt({width:1,height:1}),this.dirty=!0}destroy(){this.atlasTexture&&=(this.atlasTexture.destroy(),null);for(let e of Object.keys(this.images))this.removeImage(e);this.patterns={},this.atlasImage=new yt({width:1,height:1}),this.dirty=!0}isLoaded(){return this.loaded}setLoaded(e){if(this.loaded!==e&&(this.loaded=e,e)){for(let{ids:e,promiseResolve:t}of this.requestors)t(this._getImagesForIds(e));this.requestors=[]}}getImage(e){let t=this.images[e];if(t&&!t.data&&t.spriteData){let e=t.spriteData;t.data=new yt({width:e.width,height:e.height},e.context.getImageData(e.x,e.y,e.width,e.height).data),t.spriteData=null}return t}addImage(e,t){if(this.images[e])throw Error(`Image id ${e} already exist, use updateImage instead`);this._validate(e,t)&&(this.images[e]=t)}_validate(e,t){let n=!0,r=t.data||t.spriteData;return this._validateStretch(t.stretchX,r?.width)||(this.fire(new R(Error(`Image "${e}" has invalid "stretchX" value`))),n=!1),this._validateStretch(t.stretchY,r?.height)||(this.fire(new R(Error(`Image "${e}" has invalid "stretchY" value`))),n=!1),this._validateContent(t.content,t)||(this.fire(new R(Error(`Image "${e}" has invalid "content" value`))),n=!1),n}_validateStretch(e,t){if(!e)return!0;let n=0;for(let r of e){if(r[0]=e[1]}updateImage(e,t,n=!0){let r=this.getImage(e);if(n&&(r.data.width!==t.data.width||r.data.height!==t.data.height))throw Error(`size mismatch between old image (${r.data.width}x${r.data.height}) and new image (${t.data.width}x${t.data.height}).`);t.version=r.version+1,this.images[e]=t,this.updatedImages[e]=!0}removeImage(e){let t=this.images[e];delete this.images[e],delete this.patterns[e],t.userImage?.onRemove&&t.userImage.onRemove()}listImages(){return Object.keys(this.images)}setMissingImageResolver(e){this.missingImageResolver=e}getImages(e){return new Promise((t,n)=>{let r=!0;if(!this.isLoaded())for(let t of e)this.images[t]||(r=!1);this.isLoaded()||r?t(this._getImagesForIds(e)):this.requestors.push({ids:e,promiseResolve:t})})}async _getImagesForIds(e){let t=new Set(e.filter(e=>!this.getImage(e))),n=this.missingImageResolver;n&&await Promise.all(Array.from(t,e=>n(e)));let r={};for(let n of e){let e=this.getImage(n);e&&(t.delete(n),r[n]={data:e.data.clone(),pixelRatio:e.pixelRatio,sdf:e.sdf,version:e.version,stretchX:e.stretchX,stretchY:e.stretchY,content:e.content,textFitWidth:e.textFitWidth,textFitHeight:e.textFitHeight,hasRenderCallback:!!e.userImage?.render})}for(let e of t)this.fire(new Wr({id:e})),a(`Image "${e}" could not be loaded. Please make sure you have added the image before it is needed with map.addImage(), resolved it with map.setMissingStyleImageResolver(), or included it in a "sprite" property in your style.`);return r}getPixelSize(){let{width:e,height:t}=this.atlasImage;return{width:e,height:t}}getPattern(e){let t=this.patterns[e],n=this.getImage(e);if(!n)return null;if(t&&t.position.version===n.version)return t.position;if(t)t.position.version=n.version;else{let t={w:n.data.width+2,h:n.data.height+2,x:0,y:0},r=new te(t,n);this.patterns[e]={bin:t,position:r}}return this._updatePatternAtlas(),this.patterns[e].position}bind(e){let t=e.gl;this.atlasTexture?this.dirty&&=(this.atlasTexture.update(this.atlasImage),!1):this.atlasTexture=new Lt(e,this.atlasImage,t.RGBA),this.atlasTexture.bind(t.LINEAR,t.CLAMP_TO_EDGE)}_updatePatternAtlas(){let e=[];for(let t in this.patterns)e.push(this.patterns[t].bin);let{w:t,h:n}=pe(e),r=this.atlasImage;r.resize({width:t||1,height:n||1});for(let e in this.patterns){let{bin:t}=this.patterns[e],n=t.x+1,i=t.y+1,a=this.getImage(e).data,o=a.width,s=a.height;yt.copy(a,r,{x:0,y:0},{x:n,y:i},{width:o,height:s}),yt.copy(a,r,{x:0,y:s-1},{x:n,y:i-1},{width:o,height:1}),yt.copy(a,r,{x:0,y:0},{x:n,y:i+s},{width:o,height:1}),yt.copy(a,r,{x:o-1,y:0},{x:n-1,y:i},{width:1,height:s}),yt.copy(a,r,{x:0,y:0},{x:n+o,y:i},{width:1,height:s})}this.dirty=!0}beginFrame(){this.callbackDispatchedThisFrame={}}dispatchRenderCallbacks(e){for(let t of e){if(this.callbackDispatchedThisFrame[t])continue;this.callbackDispatchedThisFrame[t]=!0;let e=this.getImage(t);e||a(`Image with ID: "${t}" was not found`),jn(e)&&this.updateImage(t,e)}}cloneImages(){let e={};for(let t in this.images){let n=this.images[t];e[t]={...n,data:n.data?n.data.clone():null}}return e}};async function Xr(e,t,n,r){let i=t*256,a=i+255,o=await _t(await r.transformRequest(n.replace(`{fontstack}`,e).replace(`{range}`,`${i}-${a}`),`Glyphs`),new AbortController);if(!o?.data)throw Error(`Could not load glyph range. range: ${t}, ${i}-${a}`);let s={};for(let e of le(o.data))s[e.id]=e;return s}const Zr=0x56bc75e2d63100000,Qr=new Float64Array(256);for(let e=0;e<256;e++){let t=.5-(e/255)**(1/2.2);Qr[e]=t*Math.abs(t)}Qr[255]=-0x56bc75e2d63100000;var $r=class{constructor({fontSize:e=24,buffer:t=3,radius:n=8,cutoff:r=.25,fontFamily:i=`sans-serif`,fontWeight:a=`normal`,fontStyle:o=`normal`,lang:s=null}={}){this.buffer=t,this.radius=n,this.cutoff=r,this.lang=s;let c=this.size=e+t*4,l=this._createCanvas(c),u=this.ctx=l.getContext(`2d`,{willReadFrequently:!0});u.font=`${o} ${a} ${e}px ${i}`,u.textBaseline=`alphabetic`,u.textAlign=`left`,u.fillStyle=`black`,this.gridOuter=new Float64Array(c*c),this.gridInner=new Float64Array(c*c),this.f=new Float64Array(c),this.z=new Float64Array(c+1),this.v=new Uint16Array(c)}_createCanvas(e){if(typeof OffscreenCanvas<`u`)return new OffscreenCanvas(e,e);let t=document.createElement(`canvas`);return t.width=t.height=e,t}draw(e){let{width:t,actualBoundingBoxAscent:n,actualBoundingBoxDescent:r,actualBoundingBoxLeft:i,actualBoundingBoxRight:a}=this.ctx.measureText(e),o=Math.ceil(n),s=Math.floor(-i),c=Math.max(0,Math.min(this.size-this.buffer,Math.ceil(a)-s)),l=Math.max(0,Math.min(this.size-this.buffer,o+Math.ceil(r))),u=c+2*this.buffer,d=l+2*this.buffer,f=Math.max(u*d,0),p=new Uint8ClampedArray(f),m={data:p,width:u,height:d,glyphWidth:c,glyphHeight:l,glyphTop:o,glyphLeft:s,glyphAdvance:t};if(c===0||l===0)return m;let{ctx:h,buffer:g,gridInner:_,gridOuter:v}=this;this.lang&&(h.lang=this.lang),h.clearRect(g,g,c,l),h.fillText(e,g-s,g+o);let y=h.getImageData(g,g,c,l);v.fill(Zr,0,f),_.fill(0,0,f);let b=3;for(let e=0;e-1);c++,a[c]=s,o[c]=l,o[c+1]=Zr}for(let s=0,c=0;s/[-\w]+/.test(e)?e:`'${CSS.escape(e)}'`).join(`,`),i=this._fontWeight(n[0]),o=this._fontStyle(n[0]);if(typeof document<`u`&&document.fonts?.load)try{await document.fonts.load(`${o} ${i||`normal`} 48px ${r}`)}catch(e){a(`Failed to load font "${r}": ${ut(e).message}`)}return new e.TinySDF({fontSize:48,buffer:6,radius:16,cutoff:.25,fontFamily:r,fontWeight:i,fontStyle:o,lang:this.lang})}_fontStyle(e){return/italic/i.test(e)?`italic`:/oblique/i.test(e)?`oblique`:`normal`}_fontWeight(e){let t={thin:100,hairline:100,"extra light":200,"ultra light":200,light:300,normal:400,regular:400,medium:500,semibold:600,demibold:600,bold:700,"extra bold":800,"ultra bold":800,black:900,heavy:900,"extra black":950,"ultra black":950},n;for(let[r,i]of Object.entries(t))RegExp(`\\b${r}\\b`,`i`).test(e)&&(n=`${i}`);return n}destroy(){for(let e in this.entries){let t=this.entries[e];t.tinySDF=null,t.ideographTinySDF=null,t.glyphs={},t.requests={},t.ranges={}}this.entries={}}};let ii;const ai=()=>ii||=new ae({anchor:new nt(Ft.light.anchor,`anchor`),position:new nt(Ft.light.position,`position`),color:new nt(Ft.light.color,`color`),intensity:new nt(Ft.light.intensity,`intensity`)});var oi=class extends _n{constructor(e){super(),this._transitionable=new at(ai(),`light`,void 0),this.setLight(e),this._transitioning=this._transitionable.untransitioned()}getLight(){return this._transitionable.serialize()}getCartesianPosition(){return Je(this.properties.get(`position`))}setLight(e,t={}){if(!this._validate(n.light,e,t))for(let t in e){let n=e[t];t.endsWith(`-transition`)?this._transitionable.setTransition(t.slice(0,-$e.length),n):this._transitionable.setValue(t,n)}}updateTransitions(e){this._transitioning=this._transitionable.transitioned(e,this._transitioning)}hasTransition(){return this._transitioning.hasTransition()}recalculate(e){this.properties=this._transitioning.possiblyEvaluate(e)}_validate(e,t,n){return Re(this,e,{value:t},n)}};let si;const ci=()=>si||=new ae({"sky-color":new nt(Ft.sky[`sky-color`],`sky-color`),"horizon-color":new nt(Ft.sky[`horizon-color`],`horizon-color`),"fog-color":new nt(Ft.sky[`fog-color`],`fog-color`),"fog-ground-blend":new nt(Ft.sky[`fog-ground-blend`],`fog-ground-blend`),"horizon-fog-blend":new nt(Ft.sky[`horizon-fog-blend`],`horizon-fog-blend`),"sky-horizon-blend":new nt(Ft.sky[`sky-horizon-blend`],`sky-horizon-blend`),"atmosphere-blend":new nt(Ft.sky[`atmosphere-blend`],`atmosphere-blend`)});var li=class extends _n{constructor(e){super(),this._transitionable=new at(ci(),`sky`,void 0),this.setSky(e),this._transitioning=this._transitionable.untransitioned(),this.recalculate(new ne(0))}setSky(e,t={}){if(!this._validate(n.sky,e,t)){e||={"sky-color":`transparent`,"horizon-color":`transparent`,"fog-color":`transparent`,"fog-ground-blend":1,"atmosphere-blend":0};for(let t in e){let n=e[t];t.endsWith(`-transition`)?this._transitionable.setTransition(t.slice(0,-$e.length),n):this._transitionable.setValue(t,n)}}}getSky(){return this._transitionable.serialize()}updateTransitions(e){this._transitioning=this._transitionable.transitioned(e,this._transitioning)}hasTransition(){return this._transitioning.hasTransition()}recalculate(e){this.properties=this._transitioning.possiblyEvaluate(e)}_validate(e,t,n={}){return Re(this,e,{value:t},n)}calculateFogBlendOpacity(e){return e<60?0:e<70?(e-60)/10:1}},ui=class{constructor(e,t){this.width=e,this.height=t,this.nextRow=0,this.data=new Uint8Array(this.width*this.height),this.dashEntry={}}getDash(e,t){let n=e.join(`,`)+String(t);return this.dashEntry[n]||=this.addDash(e,t),this.dashEntry[n]}getDashRanges(e,t,n){let r=e.length%2==1,i=[],a=r?-e[e.length-1]*n:0,o=e[0]*n,s=!0;i.push({left:a,right:o,isDash:s,zeroLength:e[0]===0});let c=e[0];for(let t=1;t1&&(s=e[++o]);let c=Math.abs(i-s.left),l=Math.abs(i-s.right),u=Math.min(c,l),d,f=t/n*(r+1);if(s.isDash){let e=r-Math.abs(f);d=Math.sqrt(u*u+e*e)}else d=r-Math.sqrt(u*u+f*f);this.data[a+i]=Math.max(0,Math.min(255,d+128))}}}addRegularDash(e){for(let t=e.length-1;t>=0;--t){let n=e[t],r=e[t+1];n.zeroLength?e.splice(t,1):r?.isDash===n.isDash&&(r.left=n.left,e.splice(t,1))}let t=e[0],n=e[e.length-1];t.isDash===n.isDash&&(t.left=n.left-this.width,n.right=t.right+this.width);let r=this.width*this.nextRow,i=0,a=e[i];for(let t=0;t1&&(a=e[++i]);let n=Math.abs(t-a.left),o=Math.abs(t-a.right),s=Math.min(n,o),c=a.isDash?s:-s;this.data[r+t]=Math.max(0,Math.min(255,c+128))}}addDash(e,t){let n=t?7:0,r=2*n+1;if(this.nextRow+r>this.height)return a(`LineAtlas out of space`),null;let i=0;for(let t of e)i+=t;if(i!==0){let r=this.width/i,a=this.getDashRanges(e,this.width,r);t?this.addRoundDash(a,r,n):this.addRegularDash(a)}let o={y:this.nextRow+n,height:2*n,width:i};return this.nextRow+=r,this.dirty=!0,o}bind(e){let t=e.gl;this.texture?(t.bindTexture(t.TEXTURE_2D,this.texture),this.dirty&&(this.dirty=!1,t.texSubImage2D(t.TEXTURE_2D,0,0,0,this.width,this.height,t.ALPHA,t.UNSIGNED_BYTE,this.data))):(this.texture=t.createTexture(),t.bindTexture(t.TEXTURE_2D,this.texture),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_S,t.REPEAT),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_T,t.REPEAT),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MIN_FILTER,t.LINEAR),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MAG_FILTER,t.LINEAR),t.texImage2D(t.TEXTURE_2D,0,t.ALPHA,this.width,this.height,0,t.ALPHA,t.UNSIGNED_BYTE,this.data))}};function di(e){if(!e)return!1;let t=globalThis.location;if(!t)return!1;try{return new URL(e,t.href).origin!==t.origin}catch{return!1}}function fi(){let e=import.meta.url;if(!/^https?:/.test(e))return``;let t=e.endsWith(`-dev.mjs`)?`maplibre-gl-worker-dev.mjs`:`maplibre-gl-worker.mjs`;return new URL(`./${t}`,e).href}function pi(e,t){if(t)try{return new Worker(e,{type:`module`})}catch(e){console.warn(`Module worker not supported, falling back to classic worker`,e)}return new Worker(e)}async function mi(e){let t=await fetch(e);if(!t.ok)throw Error(`Failed to fetch worker script (${t.status}): ${e}`);let n=await t.text(),r=new Blob([n],{type:`text/javascript`});return URL.createObjectURL(r)}function hi(e){let t=new Blob([`import ${JSON.stringify(new URL(e,import.meta.url).href)}`],{type:`text/javascript`});return URL.createObjectURL(t)}async function gi(){let e=C.WORKER_URL||fi(),t=!e?.endsWith(`.cjs`);if(!di(e))return pi(e,t);if(t){let n=hi(e);try{return pi(n,t)}finally{URL.revokeObjectURL(n)}}let n=await mi(e);try{return pi(n,t)}finally{URL.revokeObjectURL(n)}}const _i=`maplibre_preloaded_worker_pool`;var vi=class e{constructor(){this.active={},this.workersPromise=null}async acquire(t){if(this.active[t]=!0,!this.workersPromise){let t=[];for(;t.length{for(let t of e)t.terminate()})}}isPreloaded(){return!!this.active[_i]}numActive(){return Object.keys(this.active).length}};const yi=Math.floor(Dr.hardwareConcurrency/2);vi.workerCount=Hn(globalThis)?Math.max(Math.min(yi,3),1):1;let bi;function xi(){return bi||=new vi,bi}function Si(){xi().acquire(_i)}function Ci(){let e=bi;e&&(e.isPreloaded()&&e.numActive()===1?(e.release(_i),bi=null):console.warn(`Could not clear WebWorkers since there are active Map instances that still reference it. The pre-warmed WebWorker pool can only be cleared when all map instances have been removed with map.remove()`))}var wi=class{constructor(e,t){this.workerPool=e,this.actors=[],this.currentActor=0,this.id=t,this.removed=!1,this.actorsPromise=this.initActors(t)}async initActors(e){let t=await this.workerPool.acquire(e);if(this.removed)return[];if(this.actors=t.map((t,n)=>{let r=new an(t,e);return r.name=`Worker ${n}`,r}),!this.actors.length)throw Error(`No actors found`);return this.actors}async broadcast(e,t){let n=await this.actorsPromise;return Promise.all(n.map(n=>n.sendAsync({type:e,data:t})))}async getActor(){let e=await this.actorsPromise;return this.currentActor=(this.currentActor+1)%e.length,e[this.currentActor]}async waitForInitComplete(){this.actors.length===0&&await this.actorsPromise}getReadyActor(){return this.currentActor=(this.currentActor+1)%this.actors.length,this.actors[this.currentActor]}remove(e=!0){this.removed=!0;for(let e of this.actors)e.remove();this.actors=[],e&&this.workerPool.release(this.id)}async registerMessageHandler(e,t){let n=await this.actorsPromise;for(let r of n)r.registerMessageHandler(e,t)}async unregisterMessageHandler(e){let t=await this.actorsPromise;for(let n of t)n.unregisterMessageHandler(e)}};let Ti;function Ei(){return Ti||(Ti=new wi(xi(),Jt),Ti.registerMessageHandler(`GR`,(e,t,n)=>lr(t,n))),Ti}function Di(e,t){let n=Ut();return F(n,n,[1,1,0]),rr(n,n,[e.width*.5,e.height*.5,1]),e.calculatePosMatrix?Qn(n,n,e.calculatePosMatrix(t.toUnwrapped())):n}function Oi(e,t,n){if(e)for(let r of e){let e=t[r];if(e?.source===n&&e.type===`fill-extrusion`)return!0}else for(let e in t){let r=t[e];if(r.source===n&&r.type===`fill-extrusion`)return!0}return!1}function ki(e,t,n,r,i,a,o){let s=Oi(i?.layers??null,t,e.id),c=a.maxPitchScaleFactor(),l=e.tilesIn(r,c,s);l.sort(Mi);let u=[];for(let r of l)u.push({wrappedTileID:r.tileID.wrapped().key,queryResults:r.tile.queryRenderedFeatures(t,n,e.getState(),r.queryGeometry,r.cameraQueryGeometry,r.scale,i,a,c,Di(a,r.tileID),o?(e,t)=>o(r.tileID,e,t):void 0)});return Pi(Ni(u),e)}function Ai(e,t,n,r,i,a,o){let s={},c=a.queryRenderedSymbols(r),l=[];for(let e of Object.keys(c).map(Number))l.push(o[e]);l.sort(Mi);for(let n of l){let r=n.featureIndex.lookupSymbolFeatures(c[n.bucketInstanceId],t,n.bucketIndex,n.sourceLayerIndex,{filterSpec:i.filter,globalState:i.globalState},i.layers,i.availableImages,e);for(let e in r){s[e]||=[];let t=r[e];t.sort((e,t)=>{let r=n.featureSortOrder;if(r){let n=r.indexOf(e.featureIndex);return r.indexOf(t.featureIndex)-n}else return t.featureIndex-e.featureIndex});for(let n of t)s[e].push(n)}}return Fi(s,e,n)}function ji(e,t){let n=e.getRenderableIds().map(t=>e.getTileByID(t)),r=[],i={};for(let e of n){let n=e.tileID.canonical.key;i[n]||(i[n]=!0,e.querySourceFeatures(r,t))}return r}function Mi(e,t){let n=e.tileID,r=t.tileID;return n.overscaledZ-r.overscaledZ||n.canonical.y-r.canonical.y||n.wrap-r.wrap||n.canonical.x-r.canonical.x}function Ni(e){let t={},n={};for(let{queryResults:r,wrappedTileID:i}of e){n[i]||={};let e=n[i];for(let n in r){let i=r[n];e[n]||={};let a=e[n];t[n]||=[];for(let e of i)a[e.featureIndex]||(a[e.featureIndex]=!0,t[n].push(e))}}return t}function Pi(e,t){for(let n in e)for(let r of e[n])Ii(r,t);return e}function Fi(e,t,n){for(let r in e)for(let i of e[r]){let e=n[t[r].source];Ii(i,e)}return e}function Ii(e,t){let n=e.feature,r=t.getFeatureState(n.layer[`source-layer`],n.id);n.source=n.layer.source,n.layer[`source-layer`]&&(n.sourceLayer=n.layer[`source-layer`]),n.state=r}async function Li(e,t,n,r){let i=e;if(e.url?i=(await $n(await t.transformRequest(e.url,`Source`),n)).data:await Dr.frameAsync(n,r),!i)return null;let a=Yt(L(i,e),[`tiles`,`minzoom`,`maxzoom`,`attribution`,`bounds`,`scheme`,`tileSize`,`encoding`]);return`vector_layers`in i&&i.vector_layers&&(a.vectorLayerIds=i.vector_layers.map(e=>e.id)),a}var Ri=class e{constructor(e,t){e&&(t?this.setSouthWest(e).setNorthEast(t):Array.isArray(e)&&(e.length===4?this.setSouthWest([e[0],e[1]]).setNorthEast([e[2],e[3]]):this.setSouthWest(e[0]).setNorthEast(e[1])))}setNorthEast(e){return this._ne=e instanceof B?new B(e.lng,e.lat):B.convert(e),this}setSouthWest(e){return this._sw=e instanceof B?new B(e.lng,e.lat):B.convert(e),this}extend(t){let n=this._sw,r=this._ne,i,a;if(t instanceof B)i=t,a=t;else if(t instanceof e){if(i=t._sw,a=t._ne,!i||!a)return this}else{if(Array.isArray(t))if(t.length===4||t.every(Array.isArray)){let n=t;return this.extend(e.convert(n))}else{let e=t;return this.extend(B.convert(e))}else if(t&&(`lng`in t||`lon`in t)&&`lat`in t)return this.extend(B.convert(t));return this}return!n&&!r?(this._sw=new B(i.lng,i.lat),this._ne=new B(a.lng,a.lat)):(n.lng=Math.min(i.lng,n.lng),n.lat=Math.min(i.lat,n.lat),r.lng=Math.max(a.lng,r.lng),r.lat=Math.max(a.lat,r.lat)),this}getCenter(){return new B((this._sw.lng+this._ne.lng)/2,(this._sw.lat+this._ne.lat)/2)}getSouthWest(){return this._sw}getNorthEast(){return this._ne}getNorthWest(){return new B(this.getWest(),this.getNorth())}getSouthEast(){return new B(this.getEast(),this.getSouth())}getWest(){return this._sw.lng}getSouth(){return this._sw.lat}getEast(){return this._ne.lng}getNorth(){return this._ne.lat}toArray(){return[this._sw.toArray(),this._ne.toArray()]}toString(){return`LngLatBounds(${this._sw.toString()}, ${this._ne.toString()})`}isEmpty(){return!(this._sw&&this._ne)}contains(e){let{lng:t,lat:n}=B.convert(e),r=this._sw.lat<=n&&n<=this._ne.lat,i=this._sw.lng<=t&&t<=this._ne.lng;return this._sw.lng>this._ne.lng&&(i=this._sw.lng>=t&&t>=this._ne.lng),r&&i}intersects(t){if(t=e.convert(t),!(t.getNorth()>=this.getSouth()&&t.getSouth()<=this.getNorth()))return!1;let n=Math.abs(this.getEast()-this.getWest()),r=Math.abs(t.getEast()-t.getWest());if(n>=360||r>=360)return!0;let i=sn(this.getWest(),-180,180),a=sn(this.getEast(),-180,180),o=sn(t.getWest(),-180,180),s=sn(t.getEast(),-180,180),c=i>a,l=o>s;return c&&l?!0:c?s>=i||o<=a:l?a>=o||i<=s:o<=a&&s>=i}static convert(t){return t instanceof e||!t?t:new e(t)}static fromLngLat(t,n=0){let r=360*n/40075017,i=r/Math.cos(Math.PI/180*t.lat);return new e(new B(t.lng-i,t.lat-r),new B(t.lng+i,t.lat+r))}adjustAntiMeridian(){let t=new B(this._sw.lng,this._sw.lat),n=new B(this._ne.lng,this._ne.lat);return t.lng>n.lng?new e(t,new B(n.lng+360,n.lat)):new e(t,n)}},zi=class{constructor(e,t,n){this.bounds=Ri.convert(this.validateBounds(e)),this.minzoom=t||0,this.maxzoom=n||24}validateBounds(e){return!Array.isArray(e)||e.length!==4?[-180,-90,180,90]:[Math.max(-180,e[0]),Math.max(-90,e[1]),Math.min(180,e[2]),Math.min(90,e[3])]}contains(e){let t=2**e.z,n={minX:Math.floor(x(this.bounds.getWest())*t),minY:Math.floor(g(this.bounds.getNorth())*t),maxX:Math.ceil(x(this.bounds.getEast())*t),maxY:Math.ceil(g(this.bounds.getSouth())*t)};return e.x>=n.minX&&e.x=n.minY&&e.y{this._options.tiles=e}),this}setUrl(e){return this.setSourceProperty(()=>{this.url=e,this._options.url=e}),this}onRemove(){this._tileJSONRequest&&=(this._tileJSONRequest.abort(),null)}serialize(){return L({},this._options)}async loadTile(e){let t=e.tileID.canonical.url(this.tiles,this.map.getPixelRatio(),this.scheme),n={request:await this.map._requestManager.transformRequest(t,`Tile`),uid:e.uid,tileID:e.tileID,zoom:e.tileID.overscaledZ,tileSize:this.tileSize*e.tileID.overscaleFactor(),type:this.type,source:this.id,pixelRatio:this.map.getPixelRatio(),showCollisionBoxes:this.map.showCollisionBoxes,promoteId:this.promoteId,subdivisionGranularity:this.map.style.projection.subdivisionGranularity,encoding:this.encoding,overzoomParameters:await this._getOverzoomParameters(e),etag:e.etag};n.request.collectResourceTiming=this._collectResourceTiming,await this.dispatcher.waitForInitComplete();let r=`RT`;if(!e.actor||e.state===`expired`)e.actor=this.dispatcher.getReadyActor(),r=`LT`;else if(e.state===`loading`)return new Promise((t,n)=>{e.reloadPromise={resolve:t,reject:n}});e.abortController=new AbortController;try{let t=await e.actor.sendAsync({type:r,data:n},e.abortController);if(delete e.abortController,e.aborted)return;this._afterTileLoadWorkerResponse(e,t);let i={};return t?.etagUnmodified&&(i.unmodified=!0),i}catch(t){if(delete e.abortController,e.aborted||Ae(t))return;if(t&&t.status!==404)throw t;this._afterTileLoadWorkerResponse(e,null)}}async _getOverzoomParameters(e){if(e.tileID.canonical.z<=this.maxzoom||this.map._zoomLevelsToOverscale===void 0)return;let t=e.tileID.scaledTo(this.maxzoom).canonical,n=t.url(this.tiles,this.map.getPixelRatio(),this.scheme);return{maxZoomTileID:t,overzoomRequest:await this.map._requestManager.transformRequest(n,`Tile`)}}_afterTileLoadWorkerResponse(e,t){if(t?.resourceTiming&&(e.resourceTiming=t.resourceTiming),t&&this.map._refreshExpiredTiles&&e.setExpiryData(t),e.etag=t?.etag,e.loadVectorData(t,this.map.painter),e.reloadPromise){let t=e.reloadPromise;e.reloadPromise=null,this.loadTile(e).then(t.resolve).catch(t.reject)}}async abortTile(e){e.abortController&&(e.abortController.abort(),delete e.abortController),e.actor&&await e.actor.sendAsync({type:`AT`,data:{uid:e.uid,type:this.type,source:this.id}})}async unloadTile(e){e.unloadVectorData(),e.actor&&await e.actor.sendAsync({type:`RMT`,data:{uid:e.uid,type:this.type,source:this.id}})}hasTransition(){return!1}},Vi=class extends _n{constructor(e,t,n,r){super(),this.id=e,this.dispatcher=n,this.setEventedParent(r),this.type=`raster`,this.minzoom=0,this.maxzoom=22,this.roundZoom=!0,this.scheme=`xyz`,this.tileSize=512,this._loaded=!1,this._premultiplyAlpha=!0,this._options=L({type:`raster`},t),L(this,Yt(t,[`url`,`scheme`,`tileSize`]))}async load(e=!1){this._loaded=!1,this.fire(new K(`dataloading`)),this._tileJSONRequest=new AbortController;try{let t=await Li(this._options,this.map._requestManager,this._tileJSONRequest,this.map._ownerWindow);this._tileJSONRequest=null,this._loaded=!0,t&&(L(this,t),t.bounds&&(this.tileBounds=new zi(t.bounds,this.minzoom,this.maxzoom)),this.fire(new K(`data`,{sourceDataType:`metadata`})),this.fire(new K(`data`,{sourceDataType:`content`,sourceDataChanged:e})))}catch(e){this._tileJSONRequest=null,this._loaded=!0,Ae(e)||this.fire(new R(ut(e)))}}loaded(){return this._loaded}onAdd(e){this.map=e,this.load()}onRemove(){this._tileJSONRequest&&=(this._tileJSONRequest.abort(),null)}setSourceProperty(e){this._tileJSONRequest&&=(this._tileJSONRequest.abort(),null),e(),this.load(!0)}setTiles(e){return this.setSourceProperty(()=>{this._options.tiles=e}),this}setUrl(e){return this.setSourceProperty(()=>{this.url=e,this._options.url=e}),this}serialize(){return L({},this._options)}setPremultiplyAlpha(e){return this._premultiplyAlpha===e||this.setSourceProperty(()=>{this._premultiplyAlpha=e}),this}hasTile(e){return!this.tileBounds||this.tileBounds.contains(e.canonical)}async loadTile(e){let t=e.tileID.canonical.url(this.tiles,this.map.getPixelRatio(),this.scheme),n=this._premultiplyAlpha,r=n?void 0:{premultiplyAlpha:`none`};e.abortController=new AbortController;try{let i=await Mr.getImage(await this.map._requestManager.transformRequest(t,`Tile`),e.abortController,this.map._refreshExpiredTiles,r);if(delete e.abortController,e.aborted){e.state=`unloaded`;return}if(i?.data){this.map._refreshExpiredTiles&&(i.cacheControl||i.expires)&&e.setExpiryData({cacheControl:i.cacheControl,expires:i.expires});let t=this.map.painter.context,r=t.gl,a=i.data;e.texture=this.map.painter.getTileTexture(a.width),e.texture?e.texture.update(a,{useMipmap:!0,premultiply:n}):(e.texture=new Lt(t,a,r.RGBA,{useMipmap:!0,premultiply:n}),e.texture.bind(r.LINEAR,r.CLAMP_TO_EDGE,r.LINEAR_MIPMAP_NEAREST)),e.state=`loaded`}}catch(t){if(delete e.abortController,e.aborted)e.state=`unloaded`;else if(t)throw e.state=`errored`,t}}async abortTile(e){e.abortController&&(e.abortController.abort(),delete e.abortController)}async unloadTile(e){e.texture&&this.map.painter.saveTileTexture(e.texture)}hasTransition(){return!1}},Hi=class extends Vi{constructor(e,t,n,r){super(e,t,n,r),this.type=`raster-dem`,this.maxzoom=22,this._options=L({type:`raster-dem`},t),this.encoding=t.encoding||`mapbox`,this.redFactor=t.redFactor,this.greenFactor=t.greenFactor,this.blueFactor=t.blueFactor,this.baseShift=t.baseShift}async loadTile(e){let t=e.tileID.canonical.url(this.tiles,this.map.getPixelRatio(),this.scheme),n=await this.map._requestManager.transformRequest(t,`Tile`);e.neighboringTiles=this._getNeighboringTiles(e.tileID),e.abortController=new AbortController;try{let t=await Mr.getImage(n,e.abortController,this.map._refreshExpiredTiles);if(delete e.abortController,e.aborted){e.state=`unloaded`;return}if(t?.data){let n=t.data;this.map._refreshExpiredTiles&&(t.cacheControl||t.expires)&&e.setExpiryData({cacheControl:t.cacheControl,expires:t.expires});let r=Ct(n)&&i()?n:await this.readImageNow(n),a={type:this.type,uid:e.uid,source:this.id,rawImageData:r,encoding:this.encoding,redFactor:this.redFactor,greenFactor:this.greenFactor,blueFactor:this.blueFactor,baseShift:this.baseShift};if(e.actor&&e.state!==`expired`&&e.state!==`reloading`)return;await this.dispatcher.waitForInitComplete(),(!e.actor||e.state===`expired`)&&(e.actor=this.dispatcher.getReadyActor()),e.dem=await e.actor.sendAsync({type:`LDT`,data:a}),e.needsHillshadePrepare=!0,e.needsTerrainPrepare=!0,e.state=`loaded`}}catch(t){if(delete e.abortController,e.aborted)e.state=`unloaded`;else if(t)throw e.state=`errored`,t}}async readImageNow(e){if(typeof VideoFrame<`u`&&ln()){let t=e.width+2,n=e.height+2;try{return new yt({width:t,height:n},await Ot(e,-1,-1,t,n))}catch{}}return Dr.getImageData(e,1)}_getNeighboringTiles(e){let t=e.canonical,n=2**t.z,r=(t.x-1+n)%n,i=t.x===0?e.wrap-1:e.wrap,a=(t.x+1+n)%n,o=t.x+1===n?e.wrap+1:e.wrap,s={};return s[new ht(e.overscaledZ,i,t.z,r,t.y).key]={backfilled:!1},s[new ht(e.overscaledZ,o,t.z,a,t.y).key]={backfilled:!1},t.y>0&&(s[new ht(e.overscaledZ,i,t.z,r,t.y-1).key]={backfilled:!1},s[new ht(e.overscaledZ,e.wrap,t.z,t.x,t.y-1).key]={backfilled:!1},s[new ht(e.overscaledZ,o,t.z,a,t.y-1).key]={backfilled:!1}),t.y+10||n.addOrUpdateProperties?.length>0;if(!i&&!a)continue;r.push(t.geometry);let o={...t};if(e.set(n.id,o),i&&(r.push(n.newGeometry),o.geometry=n.newGeometry),a){if(n.removeAllProperties?o.properties={}:o.properties={...o.properties||{}},n.removeProperties)for(let e of n.removeProperties)delete o.properties[e];if(n.addOrUpdateProperties)for(let{key:e,value:t}of n.addOrUpdateProperties)o.properties[e]=t}}return r}function Ki(e,t,n){if(!e)return t||{};if(!t)return e||{};n&&(Yi(e.add,n),Yi(t.add,n));let r=Zi(e),i=Zi(t);qi(r,i);let a={};if((r.removeAll||i.removeAll)&&(a.removeAll=!0),a.remove=new Set([...r.remove,...i.remove]),a.add=new Map([...r.add,...i.add]),a.update=new Map([...r.update,...i.update]),a.remove.size&&a.add.size)for(let e of a.add.keys())a.remove.delete(e);let o=Qi(a);return n&&Xi(o.add,n),o}function qi(e,t){t.removeAll&&(e.add.clear(),e.update.clear(),e.remove.clear(),t.remove.clear());for(let n of t.remove)e.add.delete(n),e.update.delete(n);for(let[n,r]of t.update){let i=e.update.get(n);i&&(t.update.set(n,Ji(i,r)),e.update.delete(n))}}function Ji(e,t){let n={id:e.id};if(t.removeAllProperties&&(delete e.removeProperties,delete e.addOrUpdateProperties,delete t.removeProperties),t.removeProperties)for(let n of t.removeProperties){let t=e.addOrUpdateProperties.findIndex(e=>e.key===n);t>-1&&e.addOrUpdateProperties.splice(t,1)}return(e.removeAllProperties||t.removeAllProperties)&&(n.removeAllProperties=!0),(e.removeProperties||t.removeProperties)&&(n.removeProperties=[...e.removeProperties||[],...t.removeProperties||[]]),(e.addOrUpdateProperties||t.addOrUpdateProperties)&&(n.addOrUpdateProperties=[...e.addOrUpdateProperties||[],...t.addOrUpdateProperties||[]]),(e.newGeometry||t.newGeometry)&&(n.newGeometry=t.newGeometry||e.newGeometry),n}function Yi(e,t){if(e)for(let n of e){let e=Ui(n,t);e!=null&&(n.id=e)}}function Xi(e,t){if(e)for(let n of e)Ui(n,t)!=null&&delete n.id}function Zi(e){if(!e)return{};let t={};return t.removeAll=e.removeAll,t.remove=new Set(e.remove||[]),t.add=new Map(e.add?.map(e=>[e.id,e])),t.update=new Map(e.update?.map(e=>[e.id,e])),t}function Qi(e){let t={};return e.removeAll&&(t.removeAll=e.removeAll),e.remove&&(t.remove=Array.from(e.remove)),e.add&&(t.add=Array.from(e.add.values())),e.update&&(t.update=Array.from(e.update.values())),t}function $i(e){return!e||e.length===0?[]:typeof e[0]==`number`?[e]:e.flatMap(e=>$i(e))}function ea(e){return e.type===`GeometryCollection`?e.geometries.flatMap(e=>ea(e)):$i(e.coordinates)}function ta(e){let t=new Ri,n;switch(e.type){case`FeatureCollection`:n=e.features.flatMap(e=>ea(e.geometry));break;case`Feature`:n=ea(e.geometry);break;default:n=ea(e);break}if(n.length===0)return t;for(let e of n){let[n,r]=e;t.extend([n,r])}return t}function na({x:e,y:t,z:n},r=0){let i=Ke((e-r)/2**n),a=nr((t+1+r)/2**n),o=Ke((e+1+r)/2**n),s=nr((t-r)/2**n);return new Ri([i,a],[o,s])}var ra=class extends _n{constructor(e,t,n,r){super(),this.id=e,this.type=`geojson`,this.minzoom=0,this.maxzoom=18,this.tileSize=512,this.isTileClipped=!0,this.reparseOverscaled=!0,this._removed=!1,this._isUpdatingWorker=!1,this._pendingWorkerUpdate={data:t.data},this.actorPromise=n.getActor(),this.setEventedParent(r),this._data=typeof t.data==`string`?{url:t.data}:{geojson:t.data},this._options=L({},t),this._collectResourceTiming=t.collectResourceTiming,t.maxzoom!==void 0&&(this.maxzoom=t.maxzoom),t.type&&(this.type=t.type),t.attribution&&(this.attribution=t.attribution),this.promoteId=t.promoteId,t.clusterMaxZoom!==void 0&&this.maxzoom<=t.clusterMaxZoom&&a(`The maxzoom value "${this.maxzoom}" is expected to be greater than the clusterMaxZoom value "${t.clusterMaxZoom}".`),this.workerOptions=L({source:this.id,geojsonVtOptions:{buffer:this._pixelsToTileUnits(t.buffer===void 0?128:t.buffer),tolerance:this._pixelsToTileUnits(t.tolerance===void 0?.375:t.tolerance),extent:M,maxZoom:this.maxzoom,lineMetrics:t.lineMetrics||!1,generateId:t.generateId||!1,promoteId:typeof t.promoteId==`string`?t.promoteId:void 0,cluster:t.cluster||!1,clusterOptions:{maxZoom:this._getClusterMaxZoom(t.clusterMaxZoom),minPoints:Math.max(2,t.clusterMinPoints||2),extent:M,radius:this._pixelsToTileUnits(t.clusterRadius||50),log:!1,generateId:t.generateId||!1}},clusterProperties:t.clusterProperties,filter:t.filter},t.workerOptions)}_hasPendingWorkerUpdate(){return this._pendingWorkerUpdate.data!==void 0||this._pendingWorkerUpdate.diff!==void 0||this._pendingWorkerUpdate.updateCluster}_pixelsToTileUnits(e){return e*(M/this.tileSize)}_getClusterMaxZoom(e){let t=e?Math.round(e):this.maxzoom-1;return Number.isInteger(e)||e===void 0||a(`Integer expected for option 'clusterMaxZoom': provided value "${e}" rounded to "${t}"`),t}async load(){await this._updateWorkerData()}onAdd(e){this.map=e,this.load()}setData(e){return this._data=typeof e==`string`?{url:e}:{geojson:e},this._pendingWorkerUpdate={data:e},this._updateWorkerData()}updateData(e){return this._pendingWorkerUpdate.diff=Ki(this._pendingWorkerUpdate.diff,e),this._updateWorkerData()}async getData(){return this._data.url&&await this.once(`data`),this._data.geojson?this._data.geojson:{type:`FeatureCollection`,features:Array.from(this._data.updateable.values())}}async getBounds(){return ta(await this.getData())}setClusterOptions(e){return this.workerOptions.geojsonVtOptions.cluster=e.cluster,e.clusterRadius!==void 0&&(this.workerOptions.geojsonVtOptions.clusterOptions.radius=this._pixelsToTileUnits(e.clusterRadius)),e.clusterMaxZoom!==void 0&&(this.workerOptions.geojsonVtOptions.clusterOptions.maxZoom=this._getClusterMaxZoom(e.clusterMaxZoom)),this._pendingWorkerUpdate.updateCluster=!0,this._updateWorkerData()}async getClusterExpansionZoom(e){return(await this.actorPromise).sendAsync({type:`GCEZ`,data:{type:this.type,clusterId:e,source:this.id}})}async getClusterChildren(e){return(await this.actorPromise).sendAsync({type:`GCC`,data:{type:this.type,clusterId:e,source:this.id}})}async getClusterLeaves(e,t,n){return(await this.actorPromise).sendAsync({type:`GCL`,data:{type:this.type,source:this.id,clusterId:e,limit:t,offset:n}})}async _updateWorkerData(){if(this._isUpdatingWorker)return this._updatePromise;if(!this._hasPendingWorkerUpdate()){a(`No pending worker updates for GeoJSONSource ${this.id}.`);return}let{data:e,diff:t,updateCluster:n}=this._pendingWorkerUpdate,r=this._getLoadGeoJSONParameters(e,t,n);e===void 0?t?this._pendingWorkerUpdate.diff=void 0:n&&(this._pendingWorkerUpdate.updateCluster=void 0):this._pendingWorkerUpdate.data=void 0,this._updatePromise=this._dispatchWorkerUpdate(r),await this._updatePromise}async _getLoadGeoJSONParameters(e,t,n){let r=L({type:this.type,source:this.id},this.workerOptions);if(typeof e==`string`)return r.request=await this.map._requestManager.transformRequest(Dr.resolveURL(e),`Source`),r.request.collectResourceTiming=this._collectResourceTiming,r;if(e!==void 0)return r.data=e,r;if(t)return r.dataDiff=t,r;if(n)return r.updateCluster=!0,r}async _dispatchWorkerUpdate(e){this._isUpdatingWorker=!0,this.fire(new K(`dataloading`));try{let t=await e,n=await(await this.actorPromise).sendAsync({type:`LD`,data:t});if(this._isUpdatingWorker=!1,this._removed||n.abandoned){this.fire(new K(`dataabort`));return}n.data&&(this._data={geojson:n.data});let r=this._applyDiffToSource(t.dataDiff),i=this._getShouldReloadTileOptions(r),a={};this._applyResourceTiming(a,n),this.fire(new K(`data`,{...a,sourceDataType:`metadata`})),this.fire(new K(`data`,{...a,sourceDataType:`content`,shouldReloadTileOptions:i}))}catch(e){if(this._isUpdatingWorker=!1,this._removed){this.fire(new K(`dataabort`));return}this.fire(new R(ut(e)))}finally{this._hasPendingWorkerUpdate()&&await this._updateWorkerData()}}_applyResourceTiming(e,t){if(!this._collectResourceTiming)return;let n=t.resourceTiming?.[this.id];if(!n)return;let r=n.slice(0);r?.length&&L(e,{resourceTiming:r})}_applyDiffToSource(e){if(!e)return;let t=typeof this.promoteId==`string`?this.promoteId:void 0;if(!this._data.url&&!this._data.updateable){let e=Wi(this._data.geojson,t);if(!e)throw Error(`GeoJSONSource "${this.id}": GeoJSON data is not compatible with updateData`);this._data={updateable:e}}if(!this._data.updateable)return;let n=Gi(this._data.updateable,e,t);if(!(e.removeAll||this._options.cluster))return n}_getShouldReloadTileOptions(e){if(e)return{affectedBounds:e.filter(Boolean).map(e=>ta(e))}}shouldReloadTile(e,{affectedBounds:t}){if(e.state===`loading`)return!0;if(e.state===`unloaded`)return!1;let{buffer:n,extent:r}=this.workerOptions.geojsonVtOptions,i=na(e.tileID.canonical,n/r);for(let e of t)if(i.intersects(e))return!0;return!1}loaded(){return!this._isUpdatingWorker&&!this._hasPendingWorkerUpdate()}async loadTile(e){let t=e.actor?`RT`:`LT`;e.actor=await this.actorPromise;let n={type:this.type,uid:e.uid,tileID:e.tileID,zoom:e.tileID.overscaledZ,maxZoom:this.maxzoom,tileSize:this.tileSize,source:this.id,pixelRatio:this.map.getPixelRatio(),showCollisionBoxes:this.map.showCollisionBoxes,promoteId:this.promoteId,subdivisionGranularity:this.map.style.projection.subdivisionGranularity};e.abortController=new AbortController;try{let r=await(await this.actorPromise).sendAsync({type:t,data:n},e.abortController);delete e.abortController,e.unloadVectorData(),e.aborted||e.loadVectorData(r,this.map.painter,t===`RT`)}catch(t){if(delete e.abortController,e.aborted||Ae(t))return;throw t}}async abortTile(e){e.abortController&&(e.abortController.abort(),delete e.abortController),e.aborted=!0}async unloadTile(e){e.unloadVectorData(),await(await this.actorPromise).sendAsync({type:`RMT`,data:{uid:e.uid,type:this.type,source:this.id}})}onRemove(){this._removed=!0,this.actorPromise.then(e=>e.sendAsync({type:`RS`,data:{type:this.type,source:this.id}}))}serialize(){return L({},this._options,{type:this.type,data:this._data.updateable?{type:`FeatureCollection`,features:Array.from(this._data.updateable.values())}:this._data.url||this._data.geojson})}hasTransition(){return!1}},ia=class extends _n{constructor(e,t,n,r){super(),this.flippedWindingOrder=!1,this.id=e,this.dispatcher=n,this.coordinates=t.coordinates,this.type=`image`,this.minzoom=0,this.maxzoom=22,this.tileSize=512,this.tiles={},this._loaded=!1,this.setEventedParent(r),this.options=t}async load(e){this._loaded=!1,this.fire(new K(`dataloading`)),this.url=this.options.url,this._request=new AbortController;try{let t=await Mr.getImage(await this.map._requestManager.transformRequest(this.url,`Image`),this._request);this._request=null,this._loaded=!0,t?.data&&(this.image=t.data,e&&(this.coordinates=e),this._finishLoading())}catch(e){this._request=null,this._loaded=!0,Ae(e)||this.fire(new R(ut(e)))}}loaded(){return this._loaded}updateImage(e){return e.url?(this._request&&=(this._request.abort(),null),this.options.url=e.url,this.load(e.coordinates).finally(()=>this.texture=null),this):this}_finishLoading(){this.map&&(this.setCoordinates(this.coordinates),this.fire(new K(`data`,{sourceDataType:`metadata`})))}onAdd(e){this.map=e,this.load()}onRemove(){this._request&&=(this._request.abort(),null)}setCoordinates(e){this.coordinates=e;let t=e.map(N.fromLngLat);return this.tileID=aa(t),this.terrainTileRanges=this._getOverlappingTileRanges(t),this.minzoom=this.maxzoom=this.tileID.z,this.tileCoords=t.map(e=>this.tileID.getTilePoint(e)._round()),this.flippedWindingOrder=oa(this.tileCoords),this.fire(new K(`data`,{sourceDataType:`content`})),this}prepare(){if(Object.keys(this.tiles).length===0||!this.image)return;let e=this.map.painter.context,t=e.gl;this.texture||(this.texture=new Lt(e,this.image,t.RGBA),this.texture.bind(t.LINEAR,t.CLAMP_TO_EDGE));let n=!1;for(let e in this.tiles){let t=this.tiles[e];t.state!==`loaded`&&(t.state=`loaded`,t.texture=this.texture,n=!0)}n&&this.fire(new K(`data`,{sourceDataType:`idle`,sourceId:this.id}))}async loadTile(e){this.tileID?.equals(e.tileID.canonical)?(this.tiles[String(e.tileID.wrap)]=e,e.buckets={}):e.state=`errored`}serialize(){return{type:`image`,url:this.options.url,coordinates:this.coordinates}}hasTransition(){return!1}_getOverlappingTileRanges(e){let{minX:t,minY:n,maxX:r,maxY:i}=Zt.fromPoints(e),a={};for(let e=0;e<=25;e++){let o=2**e,s=Math.floor(t*o),c=Math.floor(n*o),l=Math.floor(r*o),u=Math.floor(i*o),d=(s%o+o)%o,f=l%o;a[e]={minWrap:Math.floor(s/o),maxWrap:Math.floor(l/o),minTileXWrapped:d,maxTileXWrapped:f,minTileY:c,maxTileY:u}}return a}};function aa(e){let t=Zt.fromPoints(e),n=t.width(),r=t.height(),i=Math.max(0,Math.floor(-Math.log(Math.max(n,r))/Math.LN2)),a=2**i;return new Kt(i,Math.floor((t.minX+t.maxX)/2*a),Math.floor((t.minY+t.maxY)/2*a))}function oa(e){let t=e[1].x-e[0].x,n=e[1].y-e[0].y,r=e[2].x-e[0].x;return t*(e[2].y-e[0].y)-n*r<0}var sa=class extends ia{constructor(e,t,n,r){super(e,t,n,r),this._onPlayingHandler=()=>{this.map?.triggerRepaint()},this.roundZoom=!0,this.type=`video`,this.options=t}async load(){this._loaded=!1;let e=this.options;this.urls=[];for(let t of e.urls)this.urls.push((await this.map._requestManager.transformRequest(t,`Source`)).url);try{let e=await Dt(this.urls);if(this._loaded=!0,!e)return;this.video=e,this.video.loop=!0,this.video.addEventListener(`playing`,this._onPlayingHandler),this.map&&this.video.play(),this._finishLoading()}catch(e){this.fire(new R(ut(e)))}}pause(){this.video&&this.video.pause()}play(){this.video&&this.video.play()}seek(e){if(this.video){let t=this.video.seekable;et.end(0)?this.fire(new R(new Ln(`sources.${this.id}`,null,`Playback for this video can be set only between the ${t.start(0)} and ${t.end(0)}-second mark.`))):this.video.currentTime=e}}getVideo(){return this.video}onAdd(e){this.map||(this.map=e,this.load(),this.video&&(this.video.play(),this.setCoordinates(this.coordinates)))}onRemove(){super.onRemove(),this.video&&(this.video.removeEventListener(`playing`,this._onPlayingHandler),this.video.pause())}prepare(){if(Object.keys(this.tiles).length===0||this.video.readyState<2)return;let e=this.map.painter.context,t=e.gl;this.texture?this.video.paused||(this.texture.bind(t.LINEAR,t.CLAMP_TO_EDGE),t.texSubImage2D(t.TEXTURE_2D,0,0,0,t.RGBA,t.UNSIGNED_BYTE,this.video)):(this.texture=new Lt(e,this.video,t.RGBA),this.texture.bind(t.LINEAR,t.CLAMP_TO_EDGE));let n=!1;for(let e in this.tiles){let t=this.tiles[e];t.state!==`loaded`&&(t.state=`loaded`,t.texture=this.texture,n=!0)}n&&this.fire(new K(`data`,{sourceDataType:`idle`,sourceId:this.id}))}serialize(){return{type:`video`,urls:this.urls,coordinates:this.coordinates}}hasTransition(){return this.video&&!this.video.paused}},ca=class extends ia{constructor(e,t,n,r){super(e,t,n,r),t.coordinates?(!Array.isArray(t.coordinates)||t.coordinates.length!==4||t.coordinates.some(e=>!Array.isArray(e)||e.length!==2||e.some(e=>typeof e!=`number`)))&&this.fire(new R(new Ln(`sources.${e}`,null,`"coordinates" property must be an array of 4 longitude/latitude array pairs`))):this.fire(new R(new Ln(`sources.${e}`,null,`missing required property "coordinates"`))),t.animate&&typeof t.animate!=`boolean`&&this.fire(new R(new Ln(`sources.${e}`,null,`optional "animate" property must be a boolean value`))),t.canvas?typeof t.canvas!=`string`&&!(t.canvas instanceof HTMLCanvasElement)&&this.fire(new R(new Ln(`sources.${e}`,null,`"canvas" must be either a string representing the ID of the canvas element from which to read, or an HTMLCanvasElement instance`))):this.fire(new R(new Ln(`sources.${e}`,null,`missing required property "canvas"`))),this.options=t,this.animate=t.animate===void 0||t.animate}async load(){if(this._loaded=!0,this.canvas||=this.options.canvas instanceof HTMLCanvasElement?this.options.canvas:document.getElementById(this.options.canvas),this.width=this.canvas.width,this.height=this.canvas.height,this._hasInvalidDimensions()){this.fire(new R(Error(`Canvas dimensions cannot be less than or equal to zero.`)));return}this.play=function(){this._playing=!0,this.map.triggerRepaint()},this.pause=function(){this._playing&&=(this.prepare(),!1)},this._finishLoading()}getCanvas(){return this.canvas}onAdd(e){this.map=e,this.load(),this.canvas&&this.animate&&this.play()}onRemove(){this.pause()}prepare(){let e=!1;if(this.canvas.width!==this.width&&(this.width=this.canvas.width,e=!0),this.canvas.height!==this.height&&(this.height=this.canvas.height,e=!0),this._hasInvalidDimensions()||Object.keys(this.tiles).length===0)return;let t=this.map.painter.context,n=t.gl;this.texture?(e||this._playing)&&this.texture.update(this.canvas,{premultiply:!0}):(this.texture=new Lt(t,this.canvas,n.RGBA,{premultiply:!0}),this.texture.bind(n.LINEAR,n.CLAMP_TO_EDGE));let r=!1;for(let e in this.tiles){let t=this.tiles[e];t.state!==`loaded`&&(t.state=`loaded`,t.texture=this.texture,r=!0)}r&&this.fire(new K(`data`,{sourceDataType:`idle`,sourceId:this.id}))}serialize(){return{type:`canvas`,animate:this.animate,canvas:this.options.canvas,coordinates:this.coordinates}}hasTransition(){return this._playing}_hasInvalidDimensions(){for(let e of[this.canvas.width,this.canvas.height])if(isNaN(e)||e<=0)return!0;return!1}};const la={},ua=(e,t,n,r)=>{let i=new(da(t.type))(e,t,n,r);if(i.id!==e)throw Error(`Expected Source id to be ${e} instead of ${i.id}`);return i},da=e=>{switch(e){case`geojson`:return ra;case`image`:return ia;case`raster`:return Vi;case`raster-dem`:return Hi;case`vector`:return Bi;case`video`:return sa;case`canvas`:return ca}return la[e]},fa=(e,t)=>{la[e]=t},pa=async(e,t)=>{if(da(e))throw Error(`A source type called "${e}" already exists.`);fa(e,t)};function ma(e,t){let n={};if(!t)return n;for(let r of e){let e=r.layerIds.map(e=>t.getLayer(e)).filter(Boolean);if(e.length!==0){r.layers=e,r.stateDependentLayerIds&&(r.stateDependentLayers=r.stateDependentLayerIds.map(t=>e.filter(e=>e.id===t)[0]));for(let t of e)n[t.id]=r}}return n}const ha=`RTLPluginLoaded`;var ga=class extends _n{constructor(...e){super(...e),this.status=`unavailable`,this.url=null,this.dispatcher=Ei()}_syncState(e){return this.status=e,this.dispatcher.broadcast(`SRPS`,{pluginStatus:e,pluginURL:this.url}).catch(e=>{throw this.status=`error`,e})}getRTLTextPluginStatus(){return this.status}clearRTLTextPlugin(){this.status=`unavailable`,this.url=null}async setRTLTextPlugin(e,t=!1){if(this.url)throw Error(`setRTLTextPlugin cannot be called multiple times.`);if(this.url=Dr.resolveURL(e),!this.url)throw Error(`requested url ${e} is invalid`);if(this.status===`unavailable`)if(t)this.status=`deferred`,this._syncState(this.status);else return this._requestImport();else if(this.status===`requested`)return this._requestImport()}async _requestImport(){await this._syncState(`loading`),this.status=`loaded`,this.fire(new On(ha))}lazyLoad(){this.status===`unavailable`?this.status=`requested`:this.status===`deferred`&&this._requestImport()}};let _a=null;function va(){return _a||=new ga,_a}var ya=class{constructor(e,t){this.timeAdded=0,this.fadeEndTime=0,this.fadeOpacity=1,this.tileID=e,this.uid=dn(),this.uses=0,this.tileSize=t,this.buckets={},this.expirationTime=null,this.queryPadding=0,this.hasSymbolBuckets=!1,this.hasRTLText=!1,this.dependencies={},this.rttObjects=[],this.rttFingerprint={},this.expiredRequestCount=0,this.state=`loading`,this.featureStateRevision=-1}isRenderable(e){return this.hasData()&&(!this.fadeEndTime||this.fadeOpacity>0)&&(e||!this.holdingForSymbolFade())}setCrossFadeLogic({fadingRole:e,fadingDirection:t,fadingParentID:n,fadeEndTime:r}){this.resetFadeLogic(),this.fadingRole=e,this.fadingDirection=t,this.fadingParentID=n,this.fadeEndTime=r}setSelfFadeLogic(e){this.resetFadeLogic(),this.selfFading=!0,this.fadeEndTime=e}resetFadeLogic(){this.fadingRole=null,this.fadingDirection=null,this.fadingParentID=null,this.selfFading=!1,this.timeAdded=U(),this.fadeEndTime=0,this.fadeOpacity=1}wasRequested(){return this.state===`errored`||this.state===`loaded`||this.state===`reloading`}clearTextures(e){this.demTexture&&e.saveTileTexture(this.demTexture),this.demTexture=null}getRTT(e){return this.rttObjects[e]}acquireRTT(e,t,n){return this.rttObjects[t]=e.acquireRTT(n)}releaseRTT(e){if(this.rttObjects.length!==0){for(let t of this.rttObjects)t&&e.releaseRTT(t);this.rttObjects.length=0}}loadVectorData(e,t,n){if(e?.etagUnmodified===!0){this.state=`loaded`;return}if(this.hasData()&&this.unloadVectorData(),this.state=`loaded`,!e){this.collisionBoxArray=new cn;return}e.featureIndex&&(this.latestFeatureIndex=e.featureIndex,e.rawTileData?(this.latestRawTileData=e.rawTileData,this.latestEncoding=e.encoding,this.latestFeatureIndex.rawTileData=e.rawTileData,this.latestFeatureIndex.encoding=e.encoding):this.latestRawTileData&&(this.latestFeatureIndex.rawTileData=this.latestRawTileData,this.latestFeatureIndex.encoding=this.latestEncoding)),this.collisionBoxArray=e.collisionBoxArray,this.buckets=ma(e.buckets,t?.style),this.hasSymbolBuckets=!1;for(let e in this.buckets){let t=this.buckets[e];if(t instanceof ge)if(this.hasSymbolBuckets=!0,n)t.justReloaded=!0;else break}if(this.hasRTLText=!1,this.hasSymbolBuckets)for(let e in this.buckets){let t=this.buckets[e];if(t instanceof ge&&t.hasRTLText){this.hasRTLText=!0,va().lazyLoad();break}}this.queryPadding=0;for(let e in this.buckets){let n=this.buckets[e];this.queryPadding=Math.max(this.queryPadding,t.style.getLayer(e).queryRadius(n))}e.imageAtlas&&(this.imageAtlas=e.imageAtlas),e.glyphAtlasImage&&(this.glyphAtlasImage=e.glyphAtlasImage),this.dashPositions=e.dashPositions}unloadVectorData(){for(let e in this.buckets)this.buckets[e].destroy();this.buckets={},this.imageAtlasTexture&&this.imageAtlasTexture.destroy(),this.glyphAtlasTexture&&this.glyphAtlasTexture.destroy(),this.imageAtlas=null,this.dashPositions=null,this.latestFeatureIndex=null,this.state=`unloaded`}getBucket(e){return this.buckets[e.id]}upload(e){for(let t in this.buckets){let n=this.buckets[t];n.uploadPending()&&n.upload(e)}let t=e.gl;this.imageAtlas&&!this.imageAtlas.uploaded&&(this.imageAtlasTexture=new Lt(e,this.imageAtlas.image,t.RGBA),this.imageAtlas.uploaded=!0),this.glyphAtlasImage&&=(this.glyphAtlasTexture=new Lt(e,this.glyphAtlasImage,t.ALPHA),null)}prepare(e){this.imageAtlas&&this.imageAtlas.patchUpdatedImages(e,this.imageAtlasTexture)}queryRenderedFeatures(e,t,n,r,i,a,o,s,c,l,u){return this.latestFeatureIndex?.rawTileData?this.latestFeatureIndex.query({queryGeometry:r,cameraQueryGeometry:i,scale:a,tileSize:this.tileSize,pixelPosMatrix:l,transform:s,params:o,queryPadding:this.queryPadding*c,getElevation:u},e,t,n):{}}querySourceFeatures(e,t){let n=this.latestFeatureIndex;if(!n?.rawTileData)return;let r=n.loadVTLayers(),i=t?.sourceLayer?t.sourceLayer:``,a=r._geojsonTileLayer||r[i];if(!a)return;let o=jt(t?.filter,`querySourceFeatures[${i}].filter`,t?.globalState),{z:s,x:c,y:l}=this.tileID.canonical,u={z:s,x:c,y:l};for(let t=0;te)n=!1;else if(!t)n=!0;else if(this.expirationTime({zoom:0,x:0,y:0,wrap:e,fullyVisible:!1}),y=[],b=[];if(e.renderWorldCopies&&o.allowWorldCopies())for(let e=1;e<=3;e++)y.push(v(-e)),y.push(v(e));for(y.push(v(0));y.length>0;){let f=y.pop(),h=f.x,v=f.y,x=f.fullyVisible,S={x:h,y:v,z:f.zoom},C=o.getTileBoundingVolume(S,f.wrap,e.elevation,t);if(!x){let e=Aa(n,C,r);if(e===0)continue;x=e===2}let w=o.distanceToTile2d(i.x,i.y,S,C),T=c;s&&(T=(t.calculateTileZoom||Na)(e.zoom+ar(e.tileSize/t.tileSize),w,g,_,e.fov)),T=(t.roundZoom?Math.round:Math.floor)(T),T=Math.max(0,T);let ee=Math.min(T,u);if(f.wrap=o.getWrap(a,S,f.wrap),f.zoom>=ee){if(f.zoom>1),r=f.zoom+1;y.push({zoom:r,x:t,y:n,wrap:f.wrap,fullyVisible:x})}}return b.sort((e,t)=>e.distanceSq-t.distanceSq).map(e=>e.tileID)}const Ia=Zt.fromPoints([new z(0,0),new z(M,M)]);function La(e){return e===`raster`||e===`image`||e===`video`}function Ra(e,t,n,r,i,a,o){let s=U(),c=Sn(t);for(let l of t){let t=e.getTileById(l.key);(t.fadingDirection===0||t.fadeOpacity===0)&&t.resetFadeLogic(),!za(e,t,n,s,r,i,o)&&(Ba(e,t,n,s,a,o)||Ha(t,c,s,o)||t.resetFadeLogic())}}function za(e,t,n,r,i,a,o){if(!t.hasData())return!1;let{tileID:s,fadingRole:c,fadingDirection:l,fadingParentID:u}=t;if(c===0&&l===1&&u)return n[u.key]=u,!0;let d=Math.max(s.overscaledZ-i,a);for(let i=s.overscaledZ-1;i>=d;i--){let a=s.scaledTo(i),c=e.getLoadedTile(a);if(c)return t.setCrossFadeLogic({fadingRole:0,fadingDirection:1,fadingParentID:c.tileID,fadeEndTime:r+o}),c.setCrossFadeLogic({fadingRole:1,fadingDirection:0,fadeEndTime:r+o}),n[a.key]=a,!0}return!1}function Ba(e,t,n,r,i,a){if(!t.hasData())return!1;let o=t.tileID.children(i),s=Va(e,t,o,n,r,i,a);if(s)return!0;for(let c of o)Va(e,t,c.children(i),n,r,i,a)&&(s=!0);return s}function Va(e,t,n,r,i,a,o){if(n[0].overscaledZ>=a)return!1;let s=!1;for(let a of n){let n=e.getLoadedTile(a);if(!n)continue;let{fadingRole:c,fadingDirection:l,fadingParentID:u}=n;(c!==0||l!==0||!u)&&(n.setCrossFadeLogic({fadingRole:0,fadingDirection:0,fadingParentID:t.tileID,fadeEndTime:i+o}),t.setCrossFadeLogic({fadingRole:1,fadingDirection:1,fadeEndTime:i+o})),r[a.key]=a,s=!0}return s}function Ha(e,t,n,r){let i=e.tileID;if(e.selfFading)return!0;if(e.hasData())return!1;if(t.has(i)){let t=n+r;return e.setSelfFadeLogic(t),!0}return!1}function Ua(e,t){if(t<=0)return!1;let n=U();for(let t of e.getAllTiles())if(t.fadeEndTime>=n)return!0;return!1}function Wa(e,t){let n=t.getRenderableIds();for(let r of n){if(!e.neighboringTiles?.[r])continue;let n=t.getTileById(r);e.neighboringTiles[r].backfilled||Ga(e,n),!n.neighboringTiles?.[e.tileID.key]?.backfilled&&Ga(n,e)}}function Ga(e,t){e.needsHillshadePrepare=!0,e.needsTerrainPrepare=!0;let n=t.tileID.canonical.x-e.tileID.canonical.x,r=t.tileID.canonical.y-e.tileID.canonical.y,i=2**e.tileID.canonical.z,a=t.tileID.key;(n!==0||r!==0)&&(Math.abs(r)>1||(Math.abs(n)>1&&(Math.abs(n+i)===1?n+=i:Math.abs(n-i)===1&&(n-=i)),!(!t.dem||!e.dem)&&(e.dem.backfillBorder(t.dem,n,r),e.neighboringTiles?.[a]&&(e.neighboringTiles[a].backfilled=!0))))}var Ka=class{constructor(){this._tiles={}}handleWrapJump(e){let t={};for(let n in this._tiles){let r=this._tiles[n];r.tileID=r.tileID.unwrapTo(r.tileID.wrap+e),t[r.tileID.key]=r}this._tiles=t}setFeatureState(e,t,n){for(let r in this._tiles)this._tiles[r].setFeatureState(e,t,n)}getAllTiles(){return Object.values(this._tiles)}getAllIds(e=!1){return e?Object.values(this._tiles).map(e=>e.tileID).sort(Tt).map(e=>e.key):Object.keys(this._tiles)}getTileById(e){return this._tiles[e]}setTile(e,t){this._tiles[e]=t}deleteTileById(e){delete this._tiles[e]}getLoadedTile(e){let t=this.getTileById(e.key);return t?.hasData()?t:null}isIdRenderable(e,t=!1){return this.getTileById(e)?.isRenderable(t)}getRenderableIds(e=0,t){let n=[];for(let e of this.getAllIds())this.isIdRenderable(e,t)&&n.push(this.getTileById(e));return t?n.sort((t,n)=>{let r=t.tileID,i=n.tileID,a=new z(r.canonical.x,r.canonical.y)._rotate(-e),o=new z(i.canonical.x,i.canonical.y)._rotate(-e);return r.overscaledZ-i.overscaledZ||o.y-a.y||o.x-a.x}).map(e=>e.tileID.key):n.map(e=>e.tileID).sort(Tt).map(e=>e.key)}},qa=class e extends _n{static{this.maxUnderzooming=10}static{this.maxOverzooming=3}constructor(e,t,n){super(),this.id=e,this.dispatcher=n,this.on(`data`,e=>{this._dataHandler(e)}),this.on(`dataloading`,()=>{this._sourceErrored=!1}),this.on(`error`,()=>{this._sourceErrored=this._source.loaded()}),this._source=ua(e,t,n,this),this._inViewTiles=new Ka,this._outOfViewCache=new zn(0,e=>this._unloadTile(e)),this._timers={},this._maxTileCacheSize=null,this._maxTileCacheZoomLevels=null,this._rasterFadeDuration=0,this._maxFadingAncestorLevels=5,this._state=new xa,this._didEmitContent=!1,this._updated=!1}onAdd(e){this.map=e,this._maxTileCacheSize=e?e._maxTileCacheSize:null,this._maxTileCacheZoomLevels=e?e._maxTileCacheZoomLevels:null,this._source?.onAdd&&this._source.onAdd(e)}onRemove(e){for(let e of this._inViewTiles.getAllTiles())e.unloadVectorData();this.clearTiles(),this._source?.onRemove&&this._source.onRemove(e),this._inViewTiles=new Ka}loaded(){if(this._sourceErrored)return!0;if(!this._sourceLoaded||!this._source.loaded())return!1;if((this.used!==void 0||this.usedForTerrain!==void 0)&&!this.used&&!this.usedForTerrain)return!0;if(!this._updated)return!1;for(let e of this._inViewTiles.getAllTiles())if(e.state!==`loaded`&&e.state!==`errored`)return!1;return!0}getSource(){return this._source}getState(){return this._state}pause(){this._paused=!0}resume(){if(!this._paused)return;let e=this._shouldReloadOnResume;this._paused=!1,this._shouldReloadOnResume=!1,e&&this.reload(),this.transform&&this.update(this.transform,this.terrain)}async _loadTile(e,t,n){try{let r=await this._source.loadTile(e);this._tileLoaded(e,t,n,r)}catch(t){e.state=`errored`,t.status===404?this.update(this.transform,this.terrain):this._source.fire(new R(ut(t),{tile:e}))}}_unloadTile(e){this._source.unloadTile&&this._source.unloadTile(e)}_abortTile(e){this._source.abortTile&&this._source.abortTile(e),this._source.fire(new K(`dataabort`,{tile:e,coord:e.tileID}))}serialize(){return this._source.serialize()}prepare(e){this._source.prepare&&this._source.prepare(),this._state.coalesceChanges(this._inViewTiles,this.map?this.map.painter:null);for(let t of this._inViewTiles.getAllTiles())t.upload(e),t.prepare(this.map.style.imageManager)}getIds(){return this._inViewTiles.getAllIds(!0)}getRenderableIds(e){return this._inViewTiles.getRenderableIds(this.transform?.bearingInRadians,e)}hasRenderableParent(e){let t=e.overscaledZ-1;if(t>=this._source.minzoom){let n=this.getLoadedTile(e.scaledTo(t));if(n)return this._inViewTiles.isIdRenderable(n.tileID.key)}return!1}reload(e,t=void 0){if(this._paused){this._shouldReloadOnResume=!0;return}this._outOfViewCache.reset();for(let n of this._inViewTiles.getAllIds()){let r=this._inViewTiles.getTileById(n);t&&!this._source.shouldReloadTile(r,t)||(e?this._reloadTile(n,`expired`):r.state!==`errored`&&this._reloadTile(n,`reloading`))}}async _reloadTile(e,t){let n=this._inViewTiles.getTileById(e);n&&(n.state!==`loading`&&(n.state=t),await this._loadTile(n,e,t))}_tileLoaded(e,t,n,r){e.timeAdded=U(),e.selfFading&&(e.fadeEndTime=e.timeAdded+this._rasterFadeDuration),n===`expired`&&(e.refreshedUponExpiration=!0),this._setTileReloadTimer(t,e),!r?.unmodified&&(this.getSource().type===`raster-dem`&&e.dem&&Wa(e,this._inViewTiles),e.featureStateRevision=-1,this._state.initializeTileState(e,this.map?this.map.painter:null),e.aborted||this._source.fire(new K(`data`,{tile:e,coord:e.tileID})))}getTile(e){return this.getTileByID(e.key)}getTileByID(e){return this._inViewTiles.getTileById(e)}_retainLoadedChildren(t,n){let r=this._getLoadedDescendents(n),i=new Set;for(let a of n){let n=r[a.key];if(!n?.length){i.add(a);continue}let o=a.overscaledZ+e.maxOverzooming,s=n.filter(e=>e.tileID.overscaledZ<=o);if(!s.length){i.add(a);continue}let c=Math.min(...s.map(e=>e.tileID.overscaledZ)),l=s.filter(e=>e.tileID.overscaledZ===c).map(e=>e.tileID);for(let e of l)t[e.key]=e;this._areDescendentsComplete(l,c,a.overscaledZ)||i.add(a)}return i}_getLoadedDescendents(e){let t={};for(let n of this._inViewTiles.getAllTiles().filter(e=>e.hasData()))for(let r of e)n.tileID.isChildOf(r)&&(t[r.key]||=[],t[r.key].push(n));return t}_areDescendentsComplete(e,t,n){return e.length===1&&e[0].isOverscaled()?e[0].overscaledZ===t:4**(t-n)===e.length}getLoadedTile(e){return this._inViewTiles.getLoadedTile(e)}updateCacheSize(e){let t=(Math.ceil(e.width/this._source.tileSize)+1)*(Math.ceil(e.height/this._source.tileSize)+1),n=this._maxTileCacheZoomLevels===null?C.MAX_TILE_CACHE_ZOOM_LEVELS:this._maxTileCacheZoomLevels,r=Math.floor(t*n),i=typeof this._maxTileCacheSize==`number`?Math.min(this._maxTileCacheSize,r):r;this._outOfViewCache.setMaxSize(i)}handleWrapJump(e){let t=(e-(this._prevLng===void 0?e:this._prevLng))/360,n=Math.round(t);this._prevLng=e,n&&(this._inViewTiles.handleWrapJump(n),this._resetTileReloadTimers())}update(e,t){if(!this._sourceLoaded||this._paused)return;this.transform=e,this.terrain=t,this.updateCacheSize(e),this.handleWrapJump(this.transform.center.lng);let n;!this.used&&!this.usedForTerrain?n=[]:this._source.tileID?n=e.getVisibleUnwrappedCoordinates(this._source.tileID).map(e=>new ht(e.canonical.z,e.wrap,e.canonical.z,e.canonical.x,e.canonical.y)):(n=Fa(e,{tileSize:this.usedForTerrain?this.tileSize:this._source.tileSize,minzoom:this._source.minzoom,maxzoom:this._source.type===`vector`&&this.map._zoomLevelsToOverscale!==void 0?Math.max(this._source.maxzoom,e.maxZoom-this.map._zoomLevelsToOverscale):this._source.maxzoom,roundZoom:!this.usedForTerrain&&this._source.roundZoom,reparseOverscaled:this._source.reparseOverscaled,terrain:t,calculateTileZoom:this._source.calculateTileZoom}),this._source.hasTile&&(n=n.filter(e=>this._source.hasTile(e)))),this.usedForTerrain&&(n=this._addTerrainIdealTiles(n));let r=n.length===0&&!this._updated&&this._didEmitContent;this._updated=!0,r&&this.fire(new K(`data`,{sourceDataType:`idle`,sourceId:this.id}));let i=Pa(e,this._source),a=this._updateRetainedTiles(n,i),o=La(this._source.type);o&&this._rasterFadeDuration>0&&!t&&Ra(this._inViewTiles,n,a,this._maxFadingAncestorLevels,this._source.minzoom,this._source.maxzoom,this._rasterFadeDuration),o?this._cleanUpRasterTiles(a):this._cleanUpVectorTiles(a)}_cleanUpRasterTiles(e){for(let t of this._inViewTiles.getAllIds())e[t]||this._removeTile(t)}_cleanUpVectorTiles(e){for(let t of this._inViewTiles.getAllIds()){let n=this._inViewTiles.getTileById(t);if(e[t]){n.clearSymbolFadeHold();continue}if(!n.hasSymbolBuckets){this._removeTile(t);continue}n.holdingForSymbolFade()?n.symbolFadeFinished()&&this._removeTile(t):n.setSymbolHoldDuration(this.map._fadeDuration)}}_addTerrainIdealTiles(e){let t=[];for(let n of e)if(n.canonical.z>this._source.minzoom){let e=n.scaledTo(n.canonical.z-1);t.push(e);let r=n.scaledTo(Math.max(this._source.minzoom,Math.min(n.canonical.z,5)));t.push(r)}return e.concat(t)}releaseSymbolFadeTiles(){for(let e of this._inViewTiles.getAllIds())this._inViewTiles.getTileById(e).holdingForSymbolFade()&&this._removeTile(e)}_updateRetainedTiles(t,n){let r=new Set;for(let e of t)this._addTile(e).hasData()||r.add(e);let i=t.reduce((e,t)=>(e[t.key]=t,e),{}),a=this._retainLoadedChildren(i,r),o={},s=Math.max(n-e.maxUnderzooming,this._source.minzoom);for(let e of a){let t=this._inViewTiles.getTileById(e.key),n=t?.wasRequested();for(let r=e.overscaledZ-1;r>=s;--r){let a=e.scaledTo(r);if(o[a.key])break;if(o[a.key]=!0,t=this.getTile(a),!t&&n&&(t=this._addTile(a)),t){let e=t.hasData();if((e||!this.map?.cancelPendingTileRequestsWhileZooming||n)&&(i[a.key]=a),n=t.wasRequested(),e)break}}}return i}_addTile(e){let t=this._inViewTiles.getTileById(e.key);if(t)return t;t=this._outOfViewCache.getAndRemove(e),t&&(t.resetFadeLogic(),this._setTileReloadTimer(e.key,t),t.tileID=e,this._state.initializeTileState(t,this.map?this.map.painter:null));let n=t;return t||(t=new ya(e,this._source.tileSize*e.overscaleFactor()),this._loadTile(t,e.key,t.state)),t.uses++,this._inViewTiles.setTile(e.key,t),n||this._source.fire(new K(`dataloading`,{tile:t,coord:t.tileID})),t}_setTileReloadTimer(e,t){this._clearTileReloadTimer(e);let n=t.getExpiryTimeout();if(n){let t=()=>{this._reloadTile(e,`expired`),delete this._timers[e]};this._timers[e]=setTimeout(t,n)}}_clearTileReloadTimer(e){let t=this._timers[e];t&&(clearTimeout(t),delete this._timers[e])}_resetTileReloadTimers(){for(let e in this._timers)clearTimeout(this._timers[e]),delete this._timers[e];for(let e of this._inViewTiles.getAllIds()){let t=this._inViewTiles.getTileById(e);this._setTileReloadTimer(e,t)}}refreshTiles(e){for(let t of this._inViewTiles.getAllIds()){let n=this._inViewTiles.getTileById(t);!this._inViewTiles.isIdRenderable(t)&&n.state!=`errored`||e.some(e=>e.equals(n.tileID.canonical))&&this._reloadTile(t,`expired`)}}_removeTile(e){let t=this._inViewTiles.getTileById(e);t&&(t.uses--,this._inViewTiles.deleteTileById(e),this._clearTileReloadTimer(e),!(t.uses>0)&&(t.hasData()&&t.state!==`reloading`?this._outOfViewCache.add(t.tileID,t,t.getExpiryTimeout()):(t.aborted=!0,this._abortTile(t),this._unloadTile(t))))}_dataHandler(e){if(e.dataType===`source`){if(e.sourceDataType===`metadata`){this._sourceLoaded=!0;return}e.sourceDataType!==`content`||!this._sourceLoaded||this._paused||(this.reload(e.sourceDataChanged,e.shouldReloadTileOptions),this.transform&&this.update(this.transform,this.terrain),this._didEmitContent=!0)}}clearTiles(){this._shouldReloadOnResume=!1,this._paused=!1;for(let e of this._inViewTiles.getAllIds())this._removeTile(e);this._outOfViewCache.reset()}tilesIn(e,t,n){let r=[],i=this.transform;if(!i)return r;let a=i.getCoveringTilesDetailsProvider().allowWorldCopies(),o=n?i.getCameraQueryGeometry(e):e,s=e=>i.screenPointToMercatorCoordinate(e,this.terrain),c=this.transformBbox(e,s,!a),l=this.transformBbox(o,s,!a),u=this.getIds(),d=Zt.fromPoints(l);for(let e of u){let n=this._inViewTiles.getTileById(e);if(n.holdingForSymbolFade())continue;let o=a?[n.tileID]:[n.tileID.unwrapTo(-1),n.tileID.unwrapTo(0)],s=2**(i.zoom-n.tileID.overscaledZ),u=t*n.queryPadding*M/n.tileSize/s;for(let e of o){let t=d.map(t=>e.getTilePoint(new N(t.x,t.y)));if(t.expandBy(u),t.intersects(Ia)){let t=c.map(t=>e.getTilePoint(t)),i=l.map(t=>e.getTilePoint(t));r.push({tile:n,tileID:a?e:e.unwrapTo(0),queryGeometry:t,cameraQueryGeometry:i,scale:s})}}}return r}transformBbox(e,t,n){let r=e.map(t);if(n){let n=Zt.fromPoints(e);n.shrinkBy(Math.min(n.width(),n.height())*.001);let i=n.map(t);Zt.fromPoints(r).covers(i)||(r=r.map(e=>e.x>.5?new N(e.x-1,e.y,e.z):e))}return r}getVisibleCoordinates(e){let t=this.getRenderableIds(e).map(e=>this._inViewTiles.getTileById(e).tileID);return this.transform&&this.transform.populateCache(t),t}hasTransition(){return this._source.hasTransition()?!0:La(this._source.type)&&Ua(this._inViewTiles,this._rasterFadeDuration)}setRasterFadeDuration(e){this._rasterFadeDuration=e}setFeatureState(e,t,n){e||=Ht,this._state.updateState(e,t,n)}removeFeatureState(e,t,n){e||=Ht,this._state.removeFeatureState(e,t,n)}getFeatureState(e,t){return e||=Ht,this._state.getState(e,t)}setDependencies(e,t,n){let r=this._inViewTiles.getTileById(e);r&&r.setDependencies(t,n)}reloadTilesForDependencies(e,t){for(let n of this._inViewTiles.getAllIds())this._inViewTiles.getTileById(n).hasDependency(e,t)&&this._reloadTile(n,`reloading`);this._outOfViewCache.filter(n=>!n.hasDependency(e,t))}areTilesLoaded(){for(let e of this._inViewTiles.getAllTiles())if(e.state!==`loaded`&&e.state!==`errored`)return!1;return!0}},Ja=class{constructor(e,t){this.reset(e,t)}reset(e,t){this.points=e||[],this._distances=[0];for(let e=1;e0?(r-a)/o:0;return this.points[i].mult(1-s).add(this.points[t].mult(s))}};function Ya(e,t){let n=!0;return e===`always`||(e===`never`||t===`never`)&&(n=!1),n}var Xa=class{constructor(e,t,n){let r=this.boxCells=[],i=this.circleCells=[];this.xCellCount=Math.ceil(e/n),this.yCellCount=Math.ceil(t/n);for(let e=0;ethis.width||r<0||t>this.height)return[];let s=[];if(e<=0&&t<=0&&this.width<=n&&this.height<=r){if(i)return[{key:null,x1:e,y1:t,x2:n,y2:r}];for(let e=0;e0}hitTestCircle(e,t,n,r,i){let a=e-n,o=e+n,s=t-n,c=t+n;if(o<0||a>this.width||c<0||s>this.height)return!1;let l=[],u={hitTest:!0,overlapMode:r,circle:{x:e,y:t,radius:n},seenUids:{box:{},circle:{}}};return this._forEachCell(a,s,o,c,this._queryCellCircle,l,u,i),l.length>0}_queryCell(e,t,n,r,i,a,o,s){let{seenUids:c,hitTest:l,overlapMode:u}=o,d=this.boxCells[i],f=1e-6;if(d!==null){let i=this.bboxes;for(let o of d)if(!c.box[o]){c.box[o]=!0;let d=o*4,p=this.boxKeys[o];if(e<=i[d+2]+f&&t<=i[d+3]+f&&n>=i[d+0]-f&&r>=i[d+1]-f&&(!s||s(p))&&(!l||!Ya(u,p.overlapMode))&&(a.push({key:p,x1:i[d],y1:i[d+1],x2:i[d+2],y2:i[d+3]}),l))return!0}}let p=this.circleCells[i];if(p!==null){let i=this.circles;for(let o of p)if(!c.circle[o]){c.circle[o]=!0;let d=o*3,f=this.circleKeys[o];if(this._circleAndRectCollide(i[d],i[d+1],i[d+2],e,t,n,r)&&(!s||s(f))&&(!l||!Ya(u,f.overlapMode))){let e=i[d],t=i[d+1],n=i[d+2];if(a.push({key:f,x1:e-n,y1:t-n,x2:e+n,y2:t+n}),l)return!0}}}return!1}_queryCellCircle(e,t,n,r,i,a,o,s){let{circle:c,seenUids:l,overlapMode:u}=o,d=this.boxCells[i];if(d!==null){let e=this.bboxes;for(let t of d)if(!l.box[t]){l.box[t]=!0;let n=t*4,r=this.boxKeys[t];if(this._circleAndRectCollide(c.x,c.y,c.radius,e[n+0],e[n+1],e[n+2],e[n+3])&&(!s||s(r))&&!Ya(u,r.overlapMode))return a.push(!0),!0}}let f=this.circleCells[i];if(f!==null){let e=this.circles;for(let t of f)if(!l.circle[t]){l.circle[t]=!0;let n=t*3,r=this.circleKeys[t];if(this._circlesCollide(e[n],e[n+1],e[n+2],c.x,c.y,c.radius)&&(!s||s(r))&&!Ya(u,r.overlapMode))return a.push(!0),!0}}}_forEachCell(e,t,n,r,i,a,o,s){let c=this._convertToXCellCoord(e),l=this._convertToYCellCoord(t),u=this._convertToXCellCoord(n),d=this._convertToYCellCoord(r);for(let f=c;f<=u;f++)for(let c=l;c<=d;c++){let l=this.xCellCount*c+f;if(i.call(this,e,t,n,r,l,a,o,s))return}}_convertToXCellCoord(e){return Math.max(0,Math.min(this.xCellCount-1,Math.floor(e*this.xScale)))}_convertToYCellCoord(e){return Math.max(0,Math.min(this.yCellCount-1,Math.floor(e*this.yScale)))}_circlesCollide(e,t,n,r,i,a){let o=r-e,s=i-t,c=n+a;return c*c>o*o+s*s}_circleAndRectCollide(e,t,n,r,i,a,o){let s=(a-r)/2,c=Math.abs(e-(r+s));if(c>s+n)return!1;let l=(o-i)/2,u=Math.abs(t-(i+l));if(u>l+n)return!1;if(c<=s||u<=l)return!0;let d=c-s,f=u-l;return d*d+f*f<=n*n}};function Za(e,t){let n=1/(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]),r=1/(t[8]*t[8]+t[9]*t[9]+t[10]*t[10]),i=t[0]*n,a=t[4]*n,o=t[8]*r,s=t[1]*n,c=t[5]*n,l=t[9]*r,u=t[2]*n,d=t[6]*n,f=t[10]*r;e[0]=i,e[1]=a,e[2]=o,e[4]=s,e[5]=c,e[6]=l,e[8]=u,e[9]=d,e[10]=f;let p=t[12],m=t[13],h=t[14];return e[12]=-i*p-s*m-u*h,e[13]=-a*p-c*m-d*h,e[14]=-o*p-l*m-f*h,e[3]=0,e[7]=0,e[11]=0,e[15]=1,e}function Qa(e,t){return e[0]=1/t[0],e[1]=0,e[2]=0,e[3]=0,e[4]=0,e[5]=1/t[5],e[6]=0,e[7]=0,e[8]=0,e[9]=0,e[10]=0,e[11]=1/t[14],e[12]=0,e[13]=0,e[14]=-1,e[15]=t[10]/t[14],e}function $a(e,t){let n=1/(t[0]*t[5]-t[1]*t[4]);return e[0]=t[5]*n,e[1]=-t[1]*n,e[2]=0,e[3]=0,e[4]=-t[4]*n,e[5]=t[0]*n,e[6]=0,e[7]=0,e[8]=0,e[9]=0,e[10]=1/t[10],e[11]=0,e[12]=0,e[13]=0,e[14]=0,e[15]=1/t[15],e}const eo=Ut();function to(e,t,n){let r=Ut();if(!e){let{vecSouth:e,vecEast:n}=ro(t),i=xr();i[0]=n[0],i[1]=n[1],i[2]=e[0],i[3]=e[1],Sr(i,i),r[0]=i[0],r[1]=i[1],r[4]=i[2],r[5]=i[3]}return rr(r,r,[1/n,1/n,1]),r}function no(e,t,n,r){if(e){let e=Ut();if(!t){let{vecSouth:t,vecEast:r}=ro(n);e[0]=r[0],e[1]=r[1],e[4]=t[0],e[5]=t[1]}return rr(e,e,[r,r,1]),e}else return n.pixelsToClipSpaceMatrix}function ro(e){let t=Math.cos(e.rollInRadians),n=Math.sin(e.rollInRadians),r=Math.cos(e.pitchInRadians),i=Math.cos(e.bearingInRadians),a=Math.sin(e.bearingInRadians),o=D();o[0]=-i*r*n-a*t,o[1]=-a*r*n+i*t;let s=se(o);s<1e-9?l(o):ve(o,o,1/s);let c=D();c[0]=i*r*t-a*n,c[1]=a*r*t+i*n;let u=se(c);return u<1e-9?l(c):ve(c,c,1/u),{vecEast:c,vecSouth:o}}function io(e,t,n,r){let i;r?(i=[e,t,r(e,t),1],A(i,i,n)):(i=[e,t,0,1],So(i,i,n));let a=i[3];return{point:new z(i[0]/a,i[1]/a),signedDistanceFromCamera:a,isOccluded:!1}}function ao(e,t){return .5+e/t*.5}function oo(e,t){return e.x>=-t[0]&&e.x<=t[0]&&e.y>=-t[1]&&e.y<=t[1]}function so(e,t,n,r,i,a,o,c,l,u,d,f,p){let m=n?e.textSizeData:e.iconSizeData,h=et(m,t.transform.zoom),g=[256/t.width*2+1,256/t.height*2+1],_=n?e.text.dynamicLayoutVertexArray:e.icon.dynamicLayoutVertexArray;_.clear();let v=e.lineVertexArray,y=n?e.text.placedSymbolArray:e.icon.placedSymbolArray,b=t.transform.width/t.transform.height,x=!1;for(let n=0;nMath.abs(n.x-t.x)*r?{useVertical:!0}:(e===2?t.yn.x)?{needsFlipping:!0}:null}function uo(e){let{projectionContext:t,pitchedLabelPlaneMatrixInverse:n,symbol:r,fontSize:i,flip:a,keepUpright:o,glyphOffsetArray:s,dynamicLayoutVertexArray:c,aspectRatio:l,rotateToLine:u}=e,d=i/24,f=r.lineOffsetX*d,p=r.lineOffsetY*d,m;if(r.numGlyphs>1){let e=r.glyphStartIndex+r.numGlyphs,i=r.lineStartIndex,c=r.lineStartIndex+r.lineLength,h=co(d,s,f,p,a,r,u,t);if(!h)return{notEnoughRoom:!0};let g=ho(h.first.point.x,h.first.point.y,t,n),_=ho(h.last.point.x,h.last.point.y,t,n);if(o&&!a){let e=lo(r.writingMode,g,_,l);if(e)return e}m=[h.first];for(let n=r.glyphStartIndex+1;n0?o.point:fo(t.tileAnchorPoint,a,e,1,t),c=ho(e.x,e.y,t,n),u=ho(s.x,s.y,t,n),d=lo(r.writingMode,c,u,l);if(d)return d}let e=yo(d*s.getoffsetX(r.glyphStartIndex),f,p,a,r.segment,r.lineStartIndex,r.lineStartIndex+r.lineLength,t,u);if(!e||t.projectionCache.anyProjectionOccluded)return{notEnoughRoom:!0};m=[e]}for(let e of m)ze(c,e.point,e.angle);return{}}function fo(e,t,n,r,i){let a=e.add(e.sub(t)._unit()),o=mo(a.x,a.y,i).point,s=n.sub(o);return n.add(s._mult(r/s.mag()))}function po(e,t,n){let r=t.projectionCache;if(r.projections[e])return r.projections[e];let i=new z(t.lineVertexArray.getx(e),t.lineVertexArray.gety(e)),a=mo(i.x,i.y,t);if(a.signedDistanceFromCamera>0)return r.projections[e]=a.point,r.anyProjectionOccluded||=a.isOccluded,a.point;let o=e-n.direction,s=n.distanceFromAnchor===0?t.tileAnchorPoint:new z(t.lineVertexArray.getx(o),t.lineVertexArray.gety(o)),c=n.absOffsetX-n.distanceFromAnchor+1;return fo(s,i,n.previousVertex,c,t)}function mo(e,t,n){let r=e+n.translation[0],i=t+n.translation[1],a;return n.pitchWithMap?(a=io(r,i,n.pitchedLabelPlaneMatrix,n.getElevation),a.isOccluded=!1):(a=n.transform.projectTileCoordinates(r,i,n.unwrappedTileID,n.getElevation),a.point.x=(a.point.x*.5+.5)*n.width,a.point.y=(-a.point.y*.5+.5)*n.height),a}function ho(e,t,n,r){if(n.pitchWithMap){let i=[e,t,0,1];return A(i,i,r),n.transform.projectTileCoordinates(i[0]/i[3],i[1]/i[3],n.unwrappedTileID,n.getElevation).point}else return{x:e/n.width*2-1,y:1-t/n.height*2}}function go(e,t,n){return n.transform.projectTileCoordinates(e,t,n.unwrappedTileID,n.getElevation)}function _o(e,t,n){return e._unit()._perp()._mult(t*n)}function vo(t,n,r,i,a,o,s,c,l){if(c.projectionCache.offsets[t])return c.projectionCache.offsets[t];let u=r.add(n);if(t+l.direction=a)return c.projectionCache.offsets[t]=u,u;let d=po(t+l.direction,c,l),f=_o(d.sub(r),s,l.direction),p=r.add(f),m=d.add(f);return c.projectionCache.offsets[t]=e(o,u,p,m)||u,c.projectionCache.offsets[t]}function yo(e,t,n,r,i,a,o,s,c){let l=r?e-t:e+t,u=l>0?1:-1,d=0;r&&(u*=-1,d=Math.PI),u<0&&(d+=Math.PI);let f=u>0?a+i:a+i+1,p;s.projectionCache.cachedAnchorPoint?p=s.projectionCache.cachedAnchorPoint:(p=mo(s.tileAnchorPoint.x,s.tileAnchorPoint.y,s).point,s.projectionCache.cachedAnchorPoint=p);let m=p,h=p,g,_,v=0,y=0,b=Math.abs(l),x=[],S;for(;v+y<=b;){if(f+=u,f=o)return null;v+=y,h=m,_=g;let e={absOffsetX:b,direction:u,distanceFromAnchor:v,previousVertex:h};if(m=po(f,s,e),n===0)x.push(h),S=m.sub(h);else{let t,r=m.sub(h);t=r.mag()===0?_o(po(f+u,s,e).sub(m),n,u):_o(r,n,u),_||=h.add(t),g=vo(f,t,m,a,o,_,n,s,e),x.push(_),S=g.sub(_)}y=S.mag()}let C=(b-v)/y,w=S._mult(C)._add(_||h),T=d+Math.atan2(m.y-h.y,m.x-h.x);return x.push(w),{point:w,angle:c?T:0,path:x}}const bo=new Float32Array([-1/0,-1/0,0,-1/0,-1/0,0,-1/0,-1/0,0,-1/0,-1/0,0]);function xo(e,t){for(let n=0;n{let r=io(e.x,e.y,n,t.getElevation),i=t.transform.projectTileCoordinates(r.point.x,r.point.y,t.unwrappedTileID,t.getElevation);return i.point.x=(i.point.x*.5+.5)*t.width,i.point.y=(-i.point.y*.5+.5)*t.height,i})}function wo(e){let t=0,n=0,r=0,i=0;for(let a=0;an&&(n=i,t=r));return e.slice(t,t+n)}var To=class{constructor(e,t=new Xa(e.width+200,e.height+200,25),n=new Xa(e.width+200,e.height+200,25)){this.transform=e,this.grid=t,this.ignoredGrid=n,this.pitchFactor=Math.cos(e.pitch*Math.PI/180)*e.cameraToCenterDistance,this.screenRightBoundary=e.width+100,this.screenBottomBoundary=e.height+100,this.gridRightBoundary=e.width+200,this.gridBottomBoundary=e.height+200,this.perspectiveRatioCutoff=.6}placeCollisionBox(e,t,n,r,i,a,o,s,c,l,u,d){let f=e.anchorPointX+s[0],p=e.anchorPointY+s[1],m=this.projectAndGetPerspectiveRatio(f,p,i,l,d),h=n*m.perspectiveRatio,g;if(!a&&!o){let t=m.x+(u?u.x*h:0),n=m.y+(u?u.y*h:0);g={allPointsOccluded:!1,box:[t+e.x1*h,n+e.y1*h,t+e.x2*h,n+e.y2*h]}}else g=this._projectCollisionBox(e,h,r,i,a,o,s,m,l,u,d);let[_,v,y,b]=g.box,x=a?g.allPointsOccluded:m.isOccluded,S=x;return S||=m.perspectiveRatio=1;e--)f.push(a.path[e]);for(let e=1;ee.signedDistanceFromCamera<=0)?[]:e.map(e=>e.point)}let h=[];if(f.length>0){let e=f[0].clone(),t=f[0].clone();for(let n=1;n=n.x&&t.x<=r.x&&e.y>=n.y&&t.y<=r.y?[f]:t.xr.x||t.yr.y?[]:bt([f],n.x,n.y,r.x,r.y)}for(let n of h){i.reset(n,t*.25);let r=0;r=i.length<=.5*t?1:Math.ceil(i.paddedLength/p)+1;for(let n=0;n=this.screenRightBoundary||r<100||t>this.screenBottomBoundary}isInsideGrid(e,t,n,r){return n>=0&&e=0&&tthis.projectAndGetPerspectiveRatio(e.x,e.y,r,c,u));D=e.some(e=>!e.isOccluded),E=e.map(e=>new z(e.x,e.y))}else D=!0;return{box:Bt(E),allPointsOccluded:!D}}},Eo=class{constructor(e,t,n,r){e?this.opacity=Math.max(0,Math.min(1,e.opacity+(e.placed?t:-t))):this.opacity=r&&n?1:0,this.placed=n}isHidden(){return this.opacity===0&&!this.placed}},Do=class{constructor(e,t,n,r,i){this.text=new Eo(e?e.text:null,t,n,i),this.icon=new Eo(e?e.icon:null,t,r,i)}isHidden(){return this.text.isHidden()&&this.icon.isHidden()}},Oo=class{constructor(e,t,n){this.text=e,this.icon=t,this.skipFade=n}},ko=class{constructor(e,t,n,r,i){this.bucketInstanceId=e,this.featureIndex=t,this.sourceLayerIndex=n,this.bucketIndex=r,this.tileID=i}},Ao=class{constructor(e){this.crossSourceCollisions=e,this.maxGroupID=0,this.collisionGroups={}}get(e){if(this.crossSourceCollisions)return{ID:0,predicate:null};if(!this.collisionGroups[e]){let t=++this.maxGroupID;this.collisionGroups[e]={ID:t,predicate:e=>e.collisionGroupID===t}}return this.collisionGroups[e]}};function jo(e,t,n,r,i){let{horizontalAlign:a,verticalAlign:o}=Xe(e),s=-(a-.5)*t,c=-(o-.5)*n;return new z(s+r[0]*i,c+r[1]*i)}var Mo=class{constructor(e,t,n,r,i){this.transform=e.clone(),this.terrain=t,this.collisionIndex=new To(this.transform),this.placements={},this.opacities={},this.variableOffsets={},this.stale=!1,this.commitTime=0,this.fadeDuration=n,this.retainedQueryData={},this.collisionGroups=new Ao(r),this.collisionCircleArrays={},this.collisionBoxArrays=new Map,this.prevPlacement=i,i&&(i.prevPlacement=void 0),this.placedOrientations={}}_getTerrainElevationFunc(e){let t=this.terrain;return t?(n,r)=>t.getElevation(e,n,r):null}getBucketParts(e,t,n,r){let i=n.getBucket(t),a=n.latestFeatureIndex;if(!i||!a||t.id!==i.layerIds[0])return;let o=n.collisionBoxArray,s=i.layers[0].layout,c=i.layers[0].paint,l=2**(this.transform.zoom-n.tileID.overscaledZ),u=n.tileSize/M,d=n.tileID.toUnwrapped(),f=s.get(`text-rotation-alignment`)===`map`,p=Ee(n,1,this.transform.zoom),m=je(this.collisionIndex.transform,n,c.get(`text-translate`),c.get(`text-translate-anchor`)),h=je(this.collisionIndex.transform,n,c.get(`icon-translate`),c.get(`icon-translate-anchor`)),g=to(f,this.transform,p);this.retainedQueryData[i.bucketInstanceId]=new ko(i.bucketInstanceId,a,i.sourceLayerIndex,i.index,n.tileID);let _={bucket:i,layout:s,translationText:m,translationIcon:h,unwrappedTileID:d,pitchedLabelPlaneMatrix:g,scale:l,textPixelRatio:u,holdingForFade:n.holdingForSymbolFade(),collisionBoxArray:o,partiallyEvaluatedTextSize:et(i.textSizeData,this.transform.zoom),collisionGroup:this.collisionGroups.get(i.sourceID)};if(r)for(let t of i.sortKeyRanges){let{sortKey:n,symbolInstanceStart:r,symbolInstanceEnd:i}=t;e.push({sortKey:n,symbolInstanceStart:r,symbolInstanceEnd:i,parameters:_})}else e.push({symbolInstanceStart:0,symbolInstanceEnd:i.symbolInstances.length,parameters:_})}attemptAnchorPlacement(e,t,n,r,i,a,o,s,c,l,u,d,f,p,m,h,g,_,v,y){let b=Fn[e.textAnchor],x=[e.textOffset0,e.textOffset1],S=jo(b,n,r,x,i),C=this.collisionIndex.placeCollisionBox(t,d,s,c,l,o,a,h,u.predicate,v,S,y);if(!(_&&!this.collisionIndex.placeCollisionBox(_,d,s,c,l,o,a,g,u.predicate,v,S,y).placeable)&&C.placeable){let e;if(this.prevPlacement?.variableOffsets[f.crossTileID]&&this.prevPlacement?.placements[f.crossTileID]?.text&&(e=this.prevPlacement.variableOffsets[f.crossTileID].anchor),f.crossTileID===0)throw Error(`symbolInstance.crossTileID can't be 0`);return this.variableOffsets[f.crossTileID]={textOffset:x,width:n,height:r,anchor:b,textBoxScale:i,prevAnchor:e},this.markUsedJustification(p,b,f,m),p.allowVerticalPlacement&&(this.markUsedOrientation(p,m,f),this.placedOrientations[f.crossTileID]=m),{shift:S,placedGlyphBoxes:C}}}placeLayerBucketPart(e,t,n){let{bucket:r,layout:i,translationText:o,translationIcon:c,unwrappedTileID:l,pitchedLabelPlaneMatrix:u,textPixelRatio:d,holdingForFade:f,collisionBoxArray:p,partiallyEvaluatedTextSize:m,collisionGroup:h}=e.parameters,g=i.get(`text-optional`),_=i.get(`icon-optional`),v=gr(i,`text-overlap`,`text-allow-overlap`),y=v===`always`,b=gr(i,`icon-overlap`,`icon-allow-overlap`),x=b===`always`,S=i.get(`text-rotation-alignment`)===`map`,C=i.get(`text-pitch-alignment`)===`map`,w=i.get(`icon-text-fit`)!==`none`,T=i.get(`symbol-z-order`)===`viewport-y`,ee=y&&(x||!r.hasIconData()||_),E=x&&(y||!r.hasTextData()||g);!r.collisionArrays&&p&&r.deserializeCollisionBoxes(p);let D=this.retainedQueryData[r.bucketInstanceId].tileID,O=this._getTerrainElevationFunc(D),te=this.transform.getFastPathSimpleProjectionMatrix(D),k=(e,p,x)=>{if(t[e.crossTileID])return;if(f){this.placements[e.crossTileID]=new Oo(!1,!1,!1);return}let T=!1,k=!1,A=!0,ne=null,re={box:null,placeable:!1,offscreen:null,occluded:!1},ie={box:null,placeable:!1,offscreen:null},ae=null,oe=null,j=null,se=0,ce=0,le=0;p.textFeatureIndex?se=p.textFeatureIndex:e.useRuntimeCollisionCircles&&(se=e.featureIndex),p.verticalTextFeatureIndex&&(ce=p.verticalTextFeatureIndex);let ue=p.textBox;if(ue){let t=t=>{let n=1;if(r.allowVerticalPlacement&&!t&&this.prevPlacement){let t=this.prevPlacement.placedOrientations[e.crossTileID];t&&(this.placedOrientations[e.crossTileID]=t,n=t,this.markUsedOrientation(r,n,e))}return n},i=(t,n)=>{if(r.allowVerticalPlacement&&e.numVerticalGlyphVertices>0&&p.verticalTextBox){for(let e of r.writingModes)if(e===2?(re=n(),ie=re):re=t(),re?.placeable)break}else re=t()},a=e.textAnchorOffsetStartIndex,s=e.textAnchorOffsetEndIndex;if(s===a){let n=(t,n)=>{let i=this.collisionIndex.placeCollisionBox(t,v,d,D,l,C,S,o,h.predicate,O,void 0,te);return i?.placeable&&(this.markUsedOrientation(r,n,e),this.placedOrientations[e.crossTileID]=n),i};i(()=>n(ue,1),()=>{let t=p.verticalTextBox;return r.allowVerticalPlacement&&e.numVerticalGlyphVertices>0&&t?n(t,2):{box:null,offscreen:null}}),t(re?.placeable)}else{let u=Fn[this.prevPlacement?.variableOffsets[e.crossTileID]?.anchor],f=(t,i,f)=>{let p=t.x2-t.x1,m=t.y2-t.y1,g=e.textBoxScale,_=w&&b===`never`?i:null,y=null,x=v===`never`?1:2,ee=`never`;u&&x++;for(let n=0;nf(ue,p.iconBox,1),()=>{let t=p.verticalTextBox,n=re?.placeable;return r.allowVerticalPlacement&&!n&&e.numVerticalGlyphVertices>0&&t?f(t,p.verticalIconBox,2):{box:null,occluded:!0,offscreen:null}}),re&&(T=re.placeable,A=re.offscreen);let m=t(re?.placeable);if(!T&&this.prevPlacement){let t=this.prevPlacement.variableOffsets[e.crossTileID];t&&(this.variableOffsets[e.crossTileID]=t,this.markUsedJustification(r,t.anchor,e,m))}}}if(ae=re,T=ae?.placeable,A=ae?.offscreen,e.useRuntimeCollisionCircles&&e.centerJustifiedTextSymbolIndex>=0){let t=r.text.placedSymbolArray.get(e.centerJustifiedTextSymbolIndex),c=s(r.textSizeData,m,t),d=i.get(`text-padding`),f=e.collisionCircleDiameter;oe=this.collisionIndex.placeCollisionCircles(v,t,r.lineVertexArray,r.glyphOffsetArray,c,l,u,n,C,h.predicate,f,d,o,O),oe.circles.length&&oe.collisionDetected&&!n&&a(`Collisions detected, but collision boxes are not shown`),T=y||oe.circles.length>0&&!oe.collisionDetected,A&&=oe.offscreen}if(p.iconFeatureIndex&&(le=p.iconFeatureIndex),p.iconBox){let e=e=>this.collisionIndex.placeCollisionBox(e,b,d,D,l,C,S,c,h.predicate,O,w&&ne?ne:void 0,te);ie&&ie.placeable&&p.verticalIconBox?(j=e(p.verticalIconBox),k=j.placeable):(j=e(p.iconBox),k=j.placeable),A&&=j.offscreen}let de=g||e.numHorizontalGlyphVertices===0&&e.numVerticalGlyphVertices===0,fe=_||e.numIconVertices===0;!de&&!fe?k=T=k&&T:fe?de||(k&&=T):T=k&&T;let pe=T&&ae.placeable,me=k&&j.placeable;if(pe&&(ie&&ie.placeable&&ce?this.collisionIndex.insertCollisionBox(ae.box,v,i.get(`text-ignore-placement`),r.bucketInstanceId,ce,h.ID):this.collisionIndex.insertCollisionBox(ae.box,v,i.get(`text-ignore-placement`),r.bucketInstanceId,se,h.ID)),me&&this.collisionIndex.insertCollisionBox(j.box,b,i.get(`icon-ignore-placement`),r.bucketInstanceId,le,h.ID),oe&&T&&this.collisionIndex.insertCollisionCircles(oe.circles,v,i.get(`text-ignore-placement`),r.bucketInstanceId,se,h.ID),n&&this.storeCollisionData(r.bucketInstanceId,x,p,ae,j,oe),e.crossTileID===0)throw Error(`symbolInstance.crossTileID can't be 0`);if(r.bucketInstanceId===0)throw Error(`bucket.bucketInstanceId can't be 0`);let he=(T||ee)&&!ae?.occluded,ge=(k||E)&&!j?.occluded;this.placements[e.crossTileID]=new Oo(he,ge,A||r.justReloaded),t[e.crossTileID]=!0};if(T){if(e.symbolInstanceStart!==0)throw Error(`bucket.bucketInstanceId should be 0`);let t=r.getSortedSymbolIndexes(-this.transform.bearingInRadians);for(let e=t.length-1;e>=0;--e){let n=t[e];k(r.symbolInstances.get(n),r.collisionArrays[n],n)}}else for(let t=e.symbolInstanceStart;t=0&&(a>=0&&t!==a?e.text.placedSymbolArray.get(t).crossTileID=0:e.text.placedSymbolArray.get(t).crossTileID=n.crossTileID)}markUsedOrientation(e,t,n){let r=t===1||t===3?t:0,i=t===2?t:0,a=[n.leftJustifiedTextSymbolIndex,n.centerJustifiedTextSymbolIndex,n.rightJustifiedTextSymbolIndex];for(let t of a)e.text.placedSymbolArray.get(t).placedOrientation=r;n.verticalPlacedTextSymbolIndex&&(e.text.placedSymbolArray.get(n.verticalPlacedTextSymbolIndex).placedOrientation=i)}commit(e){this.commitTime=e,this.zoomAtLastRecencyCheck=this.transform.zoom;let t=this.prevPlacement,n=!1;this.prevZoomAdjustment=t?t.zoomAdjustment(this.transform.zoom):0;let r=t?t.symbolFadeChange(e):1,i=t?t.opacities:{},a=t?t.variableOffsets:{},o=t?t.placedOrientations:{};for(let e in this.placements){let t=this.placements[e],a=i[e];a?(this.opacities[e]=new Do(a,r,t.text,t.icon),n||=t.text!==a.text.placed,n||=t.icon!==a.icon.placed):(this.opacities[e]=new Do(null,r,t.text,t.icon,t.skipFade),n||=t.text||t.icon)}for(let e in i){let t=i[e];if(!this.opacities[e]){let i=new Do(t,r,!1,!1);i.isHidden()||(this.opacities[e]=i,n||=t.text.placed,n||=t.icon.placed)}}for(let e in a)!this.variableOffsets[e]&&this.opacities[e]&&!this.opacities[e].isHidden()&&(this.variableOffsets[e]=a[e]);for(let e in o)!this.placedOrientations[e]&&this.opacities[e]&&!this.opacities[e].isHidden()&&(this.placedOrientations[e]=o[e]);if(t&&t.lastPlacementChangeTime===void 0)throw Error(`Last placement time for previous placement is not defined`);n?this.lastPlacementChangeTime=e:typeof this.lastPlacementChangeTime!=`number`&&(this.lastPlacementChangeTime=t?t.lastPlacementChangeTime:e)}updateLayerOpacities(e,t){let n={};for(let r of t){let t=r.getBucket(e);t&&r.latestFeatureIndex&&e.id===t.layerIds[0]&&this.updateBucketOpacities(t,r.tileID,n,r.collisionBoxArray)}}updateBucketOpacities(e,t,n,r){e.hasTextData()&&(e.text.opacityVertexArray.clear(),e.text.hasVisibleVertices=!1),e.hasIconData()&&(e.icon.opacityVertexArray.clear(),e.icon.hasVisibleVertices=!1),e.hasIconCollisionBoxData()&&e.iconCollisionBox.collisionVertexArray.clear(),e.hasTextCollisionBoxData()&&e.textCollisionBox.collisionVertexArray.clear();let i=e.layers[0],a=i.layout,o=new Do(null,0,!1,!1,!0),s=a.get(`text-allow-overlap`),c=a.get(`icon-allow-overlap`),l=i._unevaluatedLayout.hasValue(`text-variable-anchor`)||i._unevaluatedLayout.hasValue(`text-variable-anchor-offset`),u=a.get(`text-rotation-alignment`)===`map`,d=a.get(`text-pitch-alignment`)===`map`,f=a.get(`icon-text-fit`)!==`none`,p=new Do(null,0,s&&(c||!e.hasIconData()||a.get(`icon-optional`)),c&&(s||!e.hasTextData()||a.get(`text-optional`)),!0);!e.collisionArrays&&r&&(e.hasIconCollisionBoxData()||e.hasTextCollisionBoxData())&&e.deserializeCollisionBoxes(r);let m=(e,t,n)=>{for(let r=0;r0||a>0,v=r.numIconVertices>0,y=this.placedOrientations[r.crossTileID],b=y===2,x=y===1||y===3;if(_){let t=Ro(g.text),n=b?zo:t;m(e.text,i,n);let o=x?zo:t;m(e.text,a,o);let s=g.text.isHidden(),c=[r.rightJustifiedTextSymbolIndex,r.centerJustifiedTextSymbolIndex,r.leftJustifiedTextSymbolIndex];for(let t of c)t>=0&&(e.text.placedSymbolArray.get(t).hidden=s||b?1:0);r.verticalPlacedTextSymbolIndex>=0&&(e.text.placedSymbolArray.get(r.verticalPlacedTextSymbolIndex).hidden=s||x?1:0);let l=this.variableOffsets[r.crossTileID];l&&this.markUsedJustification(e,l.anchor,r,y);let u=this.placedOrientations[r.crossTileID];u&&(this.markUsedJustification(e,`left`,r,u),this.markUsedOrientation(e,u,r))}if(v){let t=Ro(g.icon),n=!(f&&r.verticalPlacedIconSymbolIndex&&b);if(r.placedIconSymbolIndex>=0){let i=n?t:zo;m(e.icon,r.numIconVertices,i),e.icon.placedSymbolArray.get(r.placedIconSymbolIndex).hidden=g.icon.isHidden()}if(r.verticalPlacedIconSymbolIndex>=0){let i=n?zo:t;m(e.icon,r.numVerticalIconVertices,i),e.icon.placedSymbolArray.get(r.verticalPlacedIconSymbolIndex).hidden=g.icon.isHidden()}}let S=h?.has(t)?h.get(t):{text:null,icon:null};if(e.hasIconCollisionBoxData()||e.hasTextCollisionBoxData()){let n=e.collisionArrays[t];if(n){let t=new z(0,0);if(n.textBox||n.verticalTextBox){let r=!0;if(l){let e=this.variableOffsets[s];e?(t=jo(e.anchor,e.width,e.height,e.textOffset,e.textBoxScale),u&&t._rotate(d?-this.transform.bearingInRadians:this.transform.bearingInRadians)):r=!1}if(n.textBox||n.verticalTextBox){let i;n.textBox&&(i=b),n.verticalTextBox&&(i=x),No(e.textCollisionBox.collisionVertexArray,g.text.placed,!r||i,S.text,t.x,t.y)}}if(n.iconBox||n.verticalIconBox){let r=!!(!x&&n.verticalIconBox),i;n.iconBox&&(i=r),n.verticalIconBox&&(i=!r),No(e.iconCollisionBox.collisionVertexArray,g.icon.placed,i,S.icon,f?t.x:0,f?t.y:0)}}}}if(e.sortFeatures(-this.transform.bearingInRadians),this.retainedQueryData[e.bucketInstanceId]&&(this.retainedQueryData[e.bucketInstanceId].featureSortOrder=e.featureSortOrder),e.hasTextData()&&e.text.opacityVertexBuffer&&e.text.opacityVertexBuffer.updateData(e.text.opacityVertexArray),e.hasIconData()&&e.icon.opacityVertexBuffer&&e.icon.opacityVertexBuffer.updateData(e.icon.opacityVertexArray),e.hasIconCollisionBoxData()&&e.iconCollisionBox.collisionVertexBuffer&&e.iconCollisionBox.collisionVertexBuffer.updateData(e.iconCollisionBox.collisionVertexArray),e.hasTextCollisionBoxData()&&e.textCollisionBox.collisionVertexBuffer&&e.textCollisionBox.collisionVertexBuffer.updateData(e.textCollisionBox.collisionVertexArray),e.text.opacityVertexArray.length!==e.text.layoutVertexArray.length/4)throw Error(`bucket.text.opacityVertexArray.length (= ${e.text.opacityVertexArray.length}) !== bucket.text.layoutVertexArray.length (= ${e.text.layoutVertexArray.length}) / 4`);if(e.icon.opacityVertexArray.length!==e.icon.layoutVertexArray.length/4)throw Error(`bucket.icon.opacityVertexArray.length (= ${e.icon.opacityVertexArray.length}) !== bucket.icon.layoutVertexArray.length (= ${e.icon.layoutVertexArray.length}) / 4`);e.bucketInstanceId in this.collisionCircleArrays&&(e.collisionCircleArray=this.collisionCircleArrays[e.bucketInstanceId],delete this.collisionCircleArrays[e.bucketInstanceId])}symbolFadeChange(e){return this.fadeDuration===0?1:(e-this.commitTime)/this.fadeDuration+this.prevZoomAdjustment}zoomAdjustment(e){return Math.max(0,(this.transform.zoom-e)/1.5)}hasTransitions(e){return this.stale||e-this.lastPlacementChangeTimee}setStale(){this.stale=!0}};function No(e,t,n,r,i,a){(!r||r.length===0)&&(r=[0,0,0,0]);let o=r[0]-100,s=r[1]-100,c=r[2]-100,l=r[3]-100;e.emplaceBack(+!!t,+!!n,i||0,a||0,o,s),e.emplaceBack(+!!t,+!!n,i||0,a||0,c,s),e.emplaceBack(+!!t,+!!n,i||0,a||0,c,l),e.emplaceBack(+!!t,+!!n,i||0,a||0,o,l)}const Po=2**25,Fo=2**24,Io=2**17,Lo=2**16;function Ro(e){if(e.opacity===0&&!e.placed)return 0;if(e.opacity===1&&e.placed)return 4294967295;let t=+!!e.placed,n=Math.floor(e.opacity*127);return n*Po+t*Fo+n*Io+t*Lo+n*512+t*256+n*2+t}const zo=0;var Bo=class{constructor(e){this._sortAcrossTiles=e.layout.get(`symbol-z-order`)!==`viewport-y`&&!e.layout.get(`symbol-sort-key`).isConstant(),this._currentTileIndex=0,this._currentPartIndex=0,this._seenCrossTileIDs={},this._bucketParts=[]}continuePlacement(e,t,n,r,i){let a=this._bucketParts;for(;this._currentTileIndexe.sortKey-t.sortKey));this._currentPartIndex!this._forceFullPlacement&&U()-r>2;for(;this._currentPlacementIndex>=0;){let r=t[e[this._currentPlacementIndex]],a=this.placement.collisionIndex.transform.zoom;if(oe(r)&&r.layout&&(!r.minzoom||r.minzoom<=a)&&(!r.maxzoom||r.maxzoom>a)){if(this._inProgressLayer||=new Bo(r),this._inProgressLayer.continuePlacement(n[r.source],this.placement,this._showCollisionBoxes,r,i))return;delete this._inProgressLayer}this._currentPlacementIndex--}this._done=!0}commit(e){return this.placement.commit(e),this.placement}};const Ho=[Int8Array,Uint8Array,Uint8ClampedArray,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array],q=new Uint32Array(96);var Uo=class e{static from(t){if(!t||t.byteLength===void 0||t.buffer)throw Error(`Data must be an instance of ArrayBuffer or SharedArrayBuffer.`);let[n,r]=new Uint8Array(t,0,2);if(n!==219)throw Error(`Data does not appear to be in a KDBush format.`);let i=r>>4;if(i!==1)throw Error(`Got v${i} data when expected v1.`);let a=Ho[r&15];if(!a)throw Error(`Unrecognized array type.`);let[o]=new Uint16Array(t,2,1),[s]=new Uint32Array(t,4,1);return new e(s,o,a,void 0,t)}constructor(e,t=64,n=Float64Array,r=ArrayBuffer,i){if(isNaN(e)||e<0)throw Error(`Unexpected numItems value: ${e}.`);this.numItems=+e,this.nodeSize=Math.min(Math.max(+t,2),65535),this.ArrayType=n,this.IndexArrayType=e<65536?Uint16Array:Uint32Array;let a=Ho.indexOf(this.ArrayType),o=e*2*this.ArrayType.BYTES_PER_ELEMENT,s=e*this.IndexArrayType.BYTES_PER_ELEMENT,c=(8-s%8)%8;if(a<0)throw Error(`Unexpected typed array class: ${n}.`);if(i)this.data=i,this.ids=new this.IndexArrayType(i,8,e),this.coords=new n(i,8+s+c,e*2),this._pos=e*2,this._finished=!0;else{let i=this.data=new r(8+o+s+c);this.ids=new this.IndexArrayType(i,8,e),this.coords=new n(i,8+s+c,e*2),this._pos=0,this._finished=!1,new Uint8Array(i,0,2).set([219,16+a]),new Uint16Array(i,2,1)[0]=t,new Uint32Array(i,4,1)[0]=e}}add(e,t){let n=this._pos>>1;return this.ids[n]=n,this.coords[this._pos++]=e,this.coords[this._pos++]=t,n}finish(){let e=this._pos>>1;if(e!==this.numItems)throw Error(`Added ${e} items when expected ${this.numItems}.`);return Wo(this.ids,this.coords,this.nodeSize,0,this.numItems-1,0),this._finished=!0,this}range(e,t,n,r){if(!this._finished)throw Error(`Data not yet indexed - call index.finish().`);let{ids:i,coords:a,nodeSize:o}=this;q[0]=0,q[1]=i.length-1,q[2]=0;let s=3,c=[];for(;s>0;){let l=q[--s],u=q[--s],d=q[--s];if(u-d<=o){for(let o=d;o<=u;o++){let s=a[2*o],l=a[2*o+1];s>=e&&s<=n&&l>=t&&l<=r&&c.push(i[o])}continue}let f=d+u>>1,p=a[2*f],m=a[2*f+1];p>=e&&p<=n&&m>=t&&m<=r&&c.push(i[f]),(l===0?e<=p:t<=m)&&(q[s++]=d,q[s++]=f-1,q[s++]=1-l),(l===0?n>=p:r>=m)&&(q[s++]=f+1,q[s++]=u,q[s++]=1-l)}return c}within(e,t,n){let r=[];return this.withinInto(e,t,n,r),r}withinInto(e,t,n,r){if(!this._finished)throw Error(`Data not yet indexed - call index.finish().`);let{ids:i,coords:a,nodeSize:o}=this;q[0]=0,q[1]=i.length-1,q[2]=0;let s=3,c=0,l=n*n;for(;s>0;){let u=q[--s],d=q[--s],f=q[--s];if(d-f<=o){for(let n=f;n<=d;n++)Jo(a[2*n],a[2*n+1],e,t)<=l&&(r[c++]=i[n]);continue}let p=f+d>>1,m=a[2*p],h=a[2*p+1];Jo(m,h,e,t)<=l&&(r[c++]=i[p]),(u===0?e-n<=m:t-n<=h)&&(q[s++]=f,q[s++]=p-1,q[s++]=1-u),(u===0?e+n>=m:t+n>=h)&&(q[s++]=p+1,q[s++]=d,q[s++]=1-u)}return c}};function Wo(e,t,n,r,i,a){if(i-r<=n)return;let o=r+i>>1;Go(e,t,o,r,i,a),Wo(e,t,n,r,o-1,1-a),Wo(e,t,n,o+1,i,1-a)}function Go(e,t,n,r,i,a){for(;i>r;){if(i-r>600){let o=i-r+1,s=n-r+1,c=Math.log(o),l=.5*Math.exp(2*c/3),u=.5*Math.sqrt(c*l*(o-l)/o)*(s-o/2<0?-1:1);Go(e,t,n,Math.max(r,Math.floor(n-s*l/o+u)),Math.min(i,Math.floor(n+(o-s)*l/o+u)),a)}let o=t[2*n+a],s=r,c=i;for(Ko(e,t,r,n),t[2*i+a]>o&&Ko(e,t,r,i);so;)c--}t[2*r+a]===o?Ko(e,t,r,c):(c++,Ko(e,t,c,i)),c<=n&&(r=c+1),n<=c&&(i=c-1)}}function Ko(e,t,n,r){qo(e,n,r),qo(t,2*n,2*r),qo(t,2*n+1,2*r+1)}function qo(e,t,n){let r=e[t];e[t]=e[n],e[n]=r}function Jo(e,t,n,r){let i=e-n,a=t-r;return i*i+a*a}const Yo=512/M/2;var Xo=class{constructor(e,t,n){this.tileID=e,this.bucketInstanceId=n,this._symbolsByKey={};let r=new Map;for(let e=0;e({x:Math.floor(e.anchorX*Yo),y:Math.floor(e.anchorY*Yo)})),crossTileIDs:t.map(e=>e.crossTileID)};if(n.positions.length>128){let e=new Uo(n.positions.length,16,Uint16Array);for(let{x:t,y:r}of n.positions)e.add(t,r);e.finish(),delete n.positions,n.index=e}this._symbolsByKey[e]=n}}getScaledCoordinates(e,t){let{x:n,y:r,z:i}=this.tileID.canonical,{x:a,y:o,z:s}=t.canonical,c=s-i,l=Yo/2**c,u=(a*M+e.anchorX)*l,d=(o*M+e.anchorY)*l,f=n*M*Yo,p=r*M*Yo;return{x:Math.floor(u-f),y:Math.floor(d-p)}}findMatches(e,t,n){let r=this.tileID.canonical.ze)}},Zo=class{constructor(){this.maxCrossTileID=0}generate(){return++this.maxCrossTileID}},Qo=class{constructor(){this.indexes={},this.usedCrossTileIDs={},this.lng=0}handleWrapJump(e){let t=Math.round((e-this.lng)/360);if(t!==0)for(let e in this.indexes){let n=this.indexes[e],r={};for(let e in n){let i=n[e];i.tileID=i.tileID.unwrapTo(i.tileID.wrap+t),r[i.tileID.key]=i}this.indexes[e]=r}this.lng=e}addBucket(e,t,n){if(this.indexes[e.overscaledZ]?.[e.key]){if(this.indexes[e.overscaledZ][e.key].bucketInstanceId===t.bucketInstanceId)return!1;this.removeBucketCrossTileIDs(e.overscaledZ,this.indexes[e.overscaledZ][e.key])}for(let e=0;ee.overscaledZ)for(let n in i){let a=i[n];a.tileID.isChildOf(e)&&a.findMatches(t.symbolInstances,e,r)}else{let a=i[e.scaledTo(Number(n)).key];a&&a.findMatches(t.symbolInstances,e,r)}}for(let e=0;e{o(),t(e)}),{unsubscribe:o}=ze(e.signal,`abort`,()=>{o(),i.cancelAnimationFrame(a),n(new ce(e.signal.reason))},!1)},frameAsync(e,t){return new Promise((n,r)=>{this.frame(e,n,r,t)})},getImageData(e,t=0){return this.getImageCanvasContext(e).getImageData(-t,-t,e.width+2*t,e.height+2*t)},getImageCanvasContext(e){let t=window.document.createElement(`canvas`),n=t.getContext(`2d`,{willReadFrequently:!0});if(!n)throw Error(`failed to create canvas 2d context`);return t.width=e.width,t.height=e.height,n.drawImage(e,0,0,e.width,e.height),n},resolveURL(e){return Fr||=document.createElement(`a`),Fr.href=e,Fr.href},get hardwareConcurrency(){return typeof navigator<`u`&&navigator.hardwareConcurrency||4},get prefersReducedMotion(){return Lr===void 0?matchMedia?(Ir??=matchMedia(`(prefers-reduced-motion: reduce)`),Ir.matches):!1:Lr},set prefersReducedMotion(e){Lr=e}},zr=new class{constructor(){this._frozenAt=null}getCurrentTime(){return this._frozenAt===null?performance.now():this._frozenAt}setNow(e){this._frozenAt=e}restoreNow(){this._frozenAt=null}isFrozen(){return this._frozenAt!==null}};function U(){return zr.getCurrentTime()}function Br(e){zr.setNow(e)}function Vr(){zr.restoreNow()}function Hr(){return zr.isFrozen()}var W=class e{static{this.docStyle=typeof window<`u`&&window.document?.documentElement.style}static{this.selectProp=!e.docStyle||`userSelect`in e.docStyle?`userSelect`:`webkitUserSelect`}static create(e,t,n){let r=window.document.createElement(e);return t!==void 0&&(r.className=t),n&&n.appendChild(r),r}static createNS(e,t){return window.document.createElementNS(e,t)}static disableDrag(){e.docStyle&&e.selectProp&&(e.userSelect=e.docStyle[e.selectProp],e.docStyle[e.selectProp]=`none`)}static enableDrag(){e.docStyle&&e.selectProp&&(e.docStyle[e.selectProp]=e.userSelect)}static suppressClickInternal(t){t.preventDefault(),t.stopPropagation(),window.removeEventListener(`click`,e.suppressClickInternal,!0)}static suppressClick(){window.addEventListener(`click`,e.suppressClickInternal,!0),window.setTimeout(()=>{window.removeEventListener(`click`,e.suppressClickInternal,!0)},0)}static getScale(e){let t=e.getBoundingClientRect();return{x:t.width/e.offsetWidth||1,y:t.height/e.offsetHeight||1,boundingClientRect:t}}static getPoint(e,t,n){let r=t.boundingClientRect;return new l((n.clientX-r.left)/t.x-e.clientLeft,(n.clientY-r.top)/t.y-e.clientTop)}static mousePos(t,n){let r=e.getScale(t);return e.getPoint(t,r,n)}static touchPos(t,n){let r=[],i=e.getScale(t);for(let a of n)r.push(e.getPoint(t,i,a));return r}static sanitize(t){let n=new DOMParser().parseFromString(t,`text/html`).body||document.createElement(`body`),r=n.querySelectorAll(`script`);for(let e of r)e.remove();return e.clean(n),n.innerHTML}static isPossiblyDangerous(e,t){let n=t.replace(/\s+/g,``).toLowerCase();if([`src`,`href`,`xlink:href`].includes(e)&&(n.includes(`javascript:`)||n.includes(`data:`))||e.startsWith(`on`))return!0}static clean(t){let n=t.children;for(let t of n)e.removeAttributes(t),e.clean(t)}static removeAttributes(t){for(let{name:n,value:r}of Array.from(t.attributes))e.isPossiblyDangerous(n,r)&&t.removeAttribute(n)}};let Ur;(function(e){let t,n,r,i;e.resetRequestQueue=()=>{t=[],n=0,r=0,i={}},e.addThrottleControl=e=>{let t=r++;return i[t]=e,t},e.removeThrottleControl=e=>{delete i[e],u()};let a=()=>{for(let e of Object.keys(i))if(i[e]())return!0;return!1};async function s(e,t,n,r,i=!0,a){let o=await e.transformRequest(t,n);return Ke(r.signal),Ur.getImage(o,r,i,a)}e.transformAndGetImage=s,e.getImage=(e,n,r=!0,i)=>new Promise((a,o)=>{e.headers||={},e.headers.accept=`image/webp,*/*`,z(e,{type:`image`});let s={abortController:n,requestParameters:e,supportImageRefresh:r,imageBitmapOptions:i,state:`queued`,onError:e=>{o(e)},onSuccess:e=>{a(e)}};t.push(s),u()});let c=(e,t)=>typeof createImageBitmap==`function`?it(e,t):ie(e),l=async e=>{e.state=`running`;let{requestParameters:t,supportImageRefresh:r,imageBitmapOptions:i,onError:a,onSuccess:s,abortController:l}=e,f=r===!1&&!i&&!ur(self)&&!Ae(t.url)&&(!t.headers||Object.keys(t.headers).reduce((e,t)=>e&&t===`accept`,!0));n++;let p=f?d(t,l):o(t,l);try{let t=await p;delete e.abortController,e.state=`completed`,t.data instanceof HTMLImageElement||zn(t.data)?s(t):t.data&&s({data:await c(t.data,i),cacheControl:t.cacheControl,expires:t.expires})}catch(t){delete e.abortController,a(qn(t))}finally{n--,u()}},u=()=>{let e=a()?k.MAX_PARALLEL_IMAGE_REQUESTS_PER_FRAME:k.MAX_PARALLEL_IMAGE_REQUESTS;for(let r=n;r0;r++){let e=t.shift();if(e.abortController.signal.aborted){r--;continue}l(e)}},d=(e,t)=>new Promise((n,r)=>{let i=new Image,a=e.url,o=e.credentials;o&&o===`include`?i.crossOrigin=`use-credentials`:(o&&o===`same-origin`||!mn(a))&&(i.crossOrigin=`anonymous`),t.signal.addEventListener(`abort`,()=>{i.src=``,r(new ce(t.signal.reason))}),i.fetchPriority=`high`,i.onload=()=>{i.onerror=i.onload=null,n({data:i})},i.onerror=()=>{i.onerror=i.onload=null,!t.signal.aborted&&r(Error(`Could not load image. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported.`))},i.src=a})})(Ur||={}),Ur.resetRequestQueue();var Wr=class{constructor(e){this._transformRequestFn=e??null}transformRequest(e,t){return this._transformRequestFn&&this._transformRequestFn(e,t)||{url:e}}setTransformRequest(e){this._transformRequestFn=e}},Gr=class extends Xe{},G=class extends Gr{},Kr=class extends Gr{constructor(e={}){super(`style.load`,e)}},qr=class extends Gr{constructor(e,t={}){super(e,t),this.dataType=`style`}},K=class extends Gr{constructor(e,t={}){super(e,t),this.dataType=`source`}},Jr=class extends Gr{preventDefault(){this._defaultPrevented=!0}get defaultPrevented(){return this._defaultPrevented}constructor(e,t,n,r={}){n=n instanceof MouseEvent?n:new MouseEvent(e,n);let i=W.mousePos(t.getCanvas(),n),a=t.unproject(i);super(e,z({point:i,lngLat:a,originalEvent:n},r)),this._defaultPrevented=!1,this.target=t}},Yr=class extends Gr{preventDefault(){this._defaultPrevented=!0}get defaultPrevented(){return this._defaultPrevented}constructor(e,t,n){let r=e===`touchend`?n.changedTouches:n.touches,i=W.touchPos(t.getCanvasContainer(),r),a=i.map(e=>t.unproject(e)),o=i.reduce((e,t,n,r)=>e.add(t.div(r.length)),new l(0,0)),s=t.unproject(o);super(e,{points:i,point:o,lngLats:a,lngLat:s,originalEvent:n}),this._defaultPrevented=!1}},Xr=class extends Gr{preventDefault(){this._defaultPrevented=!0}get defaultPrevented(){return this._defaultPrevented}constructor(e,t){super(`wheel`,{originalEvent:t}),this._defaultPrevented=!1}},Zr=class extends Gr{},Qr=class extends Gr{constructor(e={}){super(`terrain`,e)}},$r=class extends Gr{constructor(e={}){super(`projectiontransition`,e)}},ei=class extends Gr{},ti=class extends Gr{constructor(e={}){super(`styleimagemissing`,e)}};function ni(e,t){let n={};for(let t in e)t!==`ref`&&(n[t]=e[t]);return Cr.forEach(e=>{e in t&&(n[e]=t[e])}),n}function ri(e){e=e.slice();let t=Object.create(null);for(let n=0;n{`source`in e&&r[e.source]?n.push({command:`removeLayer`,args:[e.id]}):a.push(e)}),n=n.concat(i),pi(a,t.layers,n)}catch(e){console.warn(`Unable to compute style diff:`,e),n=[{command:`setStyle`,args:[t]}]}return n}function hi(){let e={},t=Ft.$version;for(let n in Ft.$root){let r=Ft.$root[n];if(r.required){let i=null;i=n===`version`?t:r.type===`array`?[]:{},i!=null&&(e[n]=i)}}return e}function gi(e){let t=[];if(typeof e==`string`)t.push({id:`default`,url:e});else if(e&&e.length>0){let n=[];for(let{id:r,url:i}of e){let e=`${r}${i}`;n.includes(e)||(n.push(e),t.push({id:r,url:i}))}}return t}function _i(e,t,n){try{let r=new URL(e);return r.pathname+=`${t}${n}`,r.toString()}catch{throw Error(`Invalid sprite URL "${e}", must be absolute. Modify style specification directly or use TransformStyleFunction to correct the issue dynamically`)}}async function vi(e,t,n,r){let i=gi(e),a=n>1?`@2x`:``,o={},s={};for(let{id:e,url:n}of i){let i=await t.transformRequest(_i(n,a,`.json`),`SpriteJSON`);o[e]=b(i,r);let c=await t.transformRequest(_i(n,a,`.png`),`SpriteImage`);s[e]=Ur.getImage(c,r)}return await Promise.all([...Object.values(o),...Object.values(s)]),yi(o,s)}async function yi(e,t){let n={};for(let r in e){n[r]={};let i=Rr.getImageCanvasContext((await t[r]).data),a=(await e[r]).data;for(let e in a){let{width:t,height:o,x:s,y:c,sdf:l,pixelRatio:u,stretchX:d,stretchY:f,content:p,textFitWidth:m,textFitHeight:h}=a[e],g={width:t,height:o,x:s,y:c,context:i};n[r][e]={data:null,pixelRatio:u,sdf:l,stretchX:d,stretchY:f,content:p,textFitWidth:m,textFitHeight:h,spriteData:g}}}return n}var bi=class extends h{constructor(){super(),this.images={},this.updateVersion=0,this.loaded=!1,this.requestors=[],this.missingImageResolver=null,this._spriteImagesIds={},this._imagesIds=null,this._renderCallbacksDispatchedThisFrame={}}destroy(){for(let e of Object.keys(this.images))this.removeImage(e);this._spriteImagesIds={}}isLoaded(){return this.loaded}setLoaded(e){if(this.loaded!==e&&(this.loaded=e,e)){for(let{ids:e,promiseResolve:t}of this.requestors)t(this._getImagesForIds(e));this.requestors=[]}}getImage(e){let t=this.images[e];if(t&&!t.data&&t.spriteData){let e=t.spriteData;t.data=new xn({width:e.width,height:e.height},e.context.getImageData(e.x,e.y,e.width,e.height).data),t.spriteData=null}return t}addImage(e,t){if(this.images[e])throw Error(`Image id ${e} already exist, use updateImage instead`);this._validate(e,t)&&(this.images[e]=t,this._imagesIds=null,t.isWebGLImage&&this.updateImage(e,t,!1))}_validate(e,t){let n=!0,r=t.data||t.spriteData;return this._validateStretch(t.stretchX,r?.width)||(this.fire(new H(Error(`Image "${e}" has invalid "stretchX" value`))),n=!1),this._validateStretch(t.stretchY,r?.height)||(this.fire(new H(Error(`Image "${e}" has invalid "stretchY" value`))),n=!1),this._validateContent(t.content,t)||(this.fire(new H(Error(`Image "${e}" has invalid "content" value`))),n=!1),n}_validateStretch(e,t){if(!e)return!0;let n=0;for(let r of e){if(r[0]=e[1]}updateImage(e,t,n=!0){let r=this.images[e];if(n){let e=r.data||r.spriteData;if(e.width!==t.data.width||e.height!==t.data.height)throw Error(`size mismatch between old image (${e.width}x${e.height}) and new image (${t.data.width}x${t.data.height}).`)}t.version=(r.version??0)+1,this.images[e]=t,this.updateVersion++}removeImage(e){let t=this.images[e];t&&(delete this.images[e],this._imagesIds=null,t.userImage?.onRemove&&t.userImage.onRemove())}listImages(){return this._imagesIds??=Object.keys(this.images),this._imagesIds}_getSpriteImageId(e,t){return e==="default"?t:`${e}:${t}`}setSpriteImages(e,t){let n=this._spriteImagesIds[e]??[],r=[];for(let n in t){let i=this._getSpriteImageId(e,n);r.push(i),i in this.images?this.updateImage(i,t[n],!1):this.addImage(i,t[n])}let i=new Set(r),a=n.filter(e=>!i.has(e));for(let e of a)this.removeImage(e);return this._spriteImagesIds[e]=r,{loaded:r,removed:a}}removeSpriteImages(e){let t=this._spriteImagesIds[e]??[];for(let e of t)this.removeImage(e);return delete this._spriteImagesIds[e],t}removeAllSpriteImages(){let e=Object.values(this._spriteImagesIds).flat();for(let t of e)this.removeImage(t);return this._spriteImagesIds={},e}setMissingImageResolver(e){this.missingImageResolver=e}getImages(e){return new Promise((t,n)=>{let r=!0;if(!this.isLoaded())for(let t of e)this.images[t]||(r=!1);this.isLoaded()||r?t(this._getImagesForIds(e)):this.requestors.push({ids:e,promiseResolve:t})})}async _getImagesForIds(e){let t=new Set(e.filter(e=>!this.getImage(e))),n=this.missingImageResolver;n&&await Promise.allSettled(Array.from(t,e=>n(e)));let r={};for(let n of e){let e=this.getImage(n);e&&(t.delete(n),r[n]={data:e.data.clone(),pixelRatio:e.pixelRatio,sdf:e.sdf,version:e.version,stretchX:e.stretchX,stretchY:e.stretchY,content:e.content,textFitWidth:e.textFitWidth,textFitHeight:e.textFitHeight,hasRenderCallback:!!e.userImage?.render,isWebGLImage:e.isWebGLImage})}for(let e of t)this.fire(new ti({id:e})),I(`Image "${e}" could not be loaded. Please make sure you have added the image before it is needed with map.addImage(), resolved it with map.setMissingStyleImageResolver(), or included it in a "sprite" property in your style.`);return r}beginFrame(){this._renderCallbacksDispatchedThisFrame={}}dispatchRenderCallbacks(e){for(let t of e){if(this._renderCallbacksDispatchedThisFrame[t])continue;this._renderCallbacksDispatchedThisFrame[t]=!0;let e=this.getImage(t);e||I(`Image with ID: "${t}" was not found`),We(e)&&this.updateImage(t,e)}}cloneImages(){let e={};for(let t in this.images){let n=this.images[t];e[t]={...n,data:n.data?n.data.clone():null}}return e}},xi=class{constructor(e){this._imageManager=e,this._entries={},this._image=new xn({width:1,height:1}),this._dirty=!0}destroy(){this._texture&&=(this._texture.destroy(),null),this._entries={},this._image=new xn({width:1,height:1}),this._dirty=!0}getPixelSize(){let{width:e,height:t}=this._image;return{width:e,height:t}}getPattern(e){let t=this._imageManager.getImage(e);if(!t)return null;let n=this._entries[e];if(n?.image!==t){let n={w:t.data.width+2,h:t.data.height+2,x:0,y:0};this._entries[e]={bin:n,position:new ee(n,t),image:t}}else if(n.position.version!==t.version)n.position.version=t.version;else return n.position;return this._update(),this._entries[e].position}bind(e){let t=e.gl;this._texture?this._dirty&&=(this._texture.update(this._image),!1):(this._texture=new _(e,this._image,t.RGBA),this._dirty=!1),this._texture.bind(t.LINEAR,t.CLAMP_TO_EDGE)}_update(){for(let e in this._entries)this._imageManager.getImage(e)||delete this._entries[e];let e=[];for(let t in this._entries)e.push(this._entries[t].bin);let{w:t,h:n}=oe(e),r=this._image;r.resize({width:t||1,height:n||1});for(let e in this._entries){let{bin:t}=this._entries[e],n=t.x+1,i=t.y+1,a=this._entries[e].image.data,o=a.width,s=a.height;xn.copy(a,r,{x:0,y:0},{x:n,y:i},{width:o,height:s}),xn.copy(a,r,{x:0,y:s-1},{x:n,y:i-1},{width:o,height:1}),xn.copy(a,r,{x:0,y:0},{x:n,y:i+s},{width:o,height:1}),xn.copy(a,r,{x:o-1,y:0},{x:n-1,y:i},{width:1,height:s}),xn.copy(a,r,{x:0,y:0},{x:n+o,y:i},{width:1,height:s})}this._dirty=!0}};const Si=1114111,Ci={start:0,end:Si};let wi=0;function Ti(e){let t=/^u\+([0-9a-f]*)(\?+)$/i.exec(e);if(t){let[,e,n]=t;return e.length+n.length>6?null:Ei(parseInt(`${e}${`0`.repeat(n.length)}`,16),parseInt(`${e}${`f`.repeat(n.length)}`,16))}let n=/^u\+([0-9a-f]{1,6})(?:-([0-9a-f]{1,6}))?$/i.exec(e);if(!n)return null;let r=parseInt(n[1],16);return Ei(r,n[2]===void 0?r:parseInt(n[2],16))}function Ei(e,t){return e>t||e>Si?null:{start:e,end:Math.min(t,Si)}}function Di(e,t){return e.ranges.some(({start:e,end:n})=>t>=e&&t<=n)}var Oi=class{constructor(e){this.requestManager=e,this._faces={},this._registered=new Set}setFontFaces(e){this._unregisterAll(),this._faces={};for(let[t,n]of Object.entries(e??{})){let e=Array.isArray(n)?n:[n];this._faces[t]=e.map(e=>this._declareFontFace(t,e)).filter(e=>e!==null)}}hasFontFaces(){return Object.keys(this._faces).length>0}async getFontFamily(e,t){for(let n of e.split(`,`))for(let e of this._faces[n.trim()]??[])if(Di(e,t)&&(e.loaded??=this._loadFontFace(e),await e.loaded))return e.family;return null}_declareFontFace(e,t){let n=typeof t==`string`?{url:t}:t;if(typeof n?.url!=`string`)return I(`Ignoring the font face declared for "${e}": it has no URL.`),null;let r=`maplibre-gl-font-face-${wi++}`,i=n[`unicode-range`];if(!i?.length)return{url:n.url,ranges:[Ci],family:r};let a=[];for(let e of i){let t=Ti(e);if(!t){I(`Ignoring the unicode range "${e}" of the font face at ${n.url}: it is not a valid range.`);continue}a.push(t)}return a.length?{url:n.url,ranges:a,family:r}:null}async _loadFontFace(e){if(typeof FontFace>`u`||typeof document>`u`||!document.fonts)return I(`Ignoring the font face at ${e.url}: this environment has no CSS Font Loading API.`),!1;let t;try{return t=new FontFace(e.family,await this._downloadFontFile(e.url)),Object.values(this._faces).some(t=>t.includes(e))?(document.fonts.add(t),this._registered.add(t),await t.load(),!0):!1}catch(n){return t&&this._unregister(t),I(`Ignoring the font face at ${e.url}: ${qn(n).message}`),!1}}async _downloadFontFile(e){let t=await this.requestManager.transformRequest(e,`Glyphs`),n=await T(t,new AbortController);if(!n?.data)throw Error(`the response was empty for the font file at ${e}`);return n.data}_unregister(e){document.fonts?.delete(e),this._registered.delete(e)}_unregisterAll(){for(let e of this._registered)document.fonts?.delete(e);this._registered.clear()}destroy(){this._unregisterAll(),this._faces={}}};const ki=0x56bc75e2d63100000,Ai=new Float64Array(256);for(let e=0;e<256;e++){let t=.5-(e/255)**(1/2.2);Ai[e]=t*Math.abs(t)}Ai[255]=-0x56bc75e2d63100000;var ji=class{constructor({fontSize:e=24,buffer:t=3,radius:n=8,cutoff:r=.25,fontFamily:i=`sans-serif`,fontWeight:a=`normal`,fontStyle:o=`normal`,lang:s=null}={}){this.buffer=t,this.radius=n,this.cutoff=r,this.lang=s;let c=this.size=e+t*4,l=this._createCanvas(c),u=this.ctx=l.getContext(`2d`,{willReadFrequently:!0});u.font=`${o} ${a} ${e}px ${i}`,u.textBaseline=`alphabetic`,u.textAlign=`left`,u.fillStyle=`black`,this.gridOuter=new Float64Array(c*c),this.gridInner=new Float64Array(c*c),this.f=new Float64Array(c),this.z=new Float64Array(c+1),this.v=new Uint16Array(c)}_createCanvas(e){if(typeof OffscreenCanvas<`u`)return new OffscreenCanvas(e,e);let t=document.createElement(`canvas`);return t.width=t.height=e,t}draw(e){let{width:t,actualBoundingBoxAscent:n,actualBoundingBoxDescent:r,actualBoundingBoxLeft:i,actualBoundingBoxRight:a}=this.ctx.measureText(e),o=Math.ceil(n),s=Math.floor(-i),c=Math.max(0,Math.min(this.size-this.buffer,Math.ceil(a)-s)),l=Math.max(0,Math.min(this.size-this.buffer,o+Math.ceil(r))),u=c+2*this.buffer,d=l+2*this.buffer,f=Math.max(u*d,0),p=new Uint8ClampedArray(f),m={data:p,width:u,height:d,glyphWidth:c,glyphHeight:l,glyphTop:o,glyphLeft:s,glyphAdvance:t};if(c===0||l===0)return m;let{ctx:h,buffer:g,gridInner:_,gridOuter:v}=this;this.lang&&(h.lang=this.lang),h.clearRect(g,g,c,l),h.fillText(e,g-s,g+o);let y=h.getImageData(g,g,c,l);v.fill(ki,0,f),_.fill(0,0,f);let b=3;for(let e=0;e-1);c++,a[c]=s,o[c]=l,o[c+1]=ki}for(let s=0,c=0;s{let n=new ji(e);return n.buffer=t,n};var Ii=class{constructor(e,t,n,r=Fi){this.requestManager=e,this.localIdeographFontFamily=t,this.entries={},this.lang=n,this.fontFaceManager=new Oi(e),this.createRasterizer=r}setURL(e){this.url=e}setFontFaces(e){this.fontFaceManager.setFontFaces(e),this.entries={}}async getGlyphs(e){let t=[];for(let n in e)for(let r of e[n])t.push(this._getAndCacheGlyphsPromise(n,r));let n=await Promise.all(t),r={};for(let{stack:e,id:t,glyph:i}of n)r[e]||={},r[e][t]=i&&{id:i.id,bitmap:i.bitmap.clone(),metrics:i.metrics};return r}async _getAndCacheGlyphsPromise(e,t){this.entries[e]??={glyphs:{},requests:{},ranges:{}};let n=this.entries[e],r=n.glyphs[t];if(r!==void 0)return{stack:e,id:t,glyph:r};let i=t.codePointAt(0),a=this.fontFaceManager.hasFontFaces()?await this.fontFaceManager.getFontFamily(e,i):null;return a?(r=n.glyphs[t]=await this._drawGlyph(n,e,t,a),{stack:e,id:t,glyph:r}):st(t)?(r=n.glyphs[t]=null,{stack:e,id:t,glyph:r}):!this.url||this._charUsesLocalIdeographFontFamily(i)?(r=n.glyphs[t]=await this._drawGlyph(n,e,t),{stack:e,id:t,glyph:r}):await this._downloadAndCacheRangePromise(e,t)}async _downloadAndCacheRangePromise(e,t){let n=t.codePointAt(0),r=this.entries[e],i=Math.floor(n/256);if(r.ranges[i])return{stack:e,id:t,glyph:null};r.requests[i]||=this._loadGlyphRange(e,i);try{let a=await r.requests[i];for(let e in a)r.glyphs[String.fromCodePoint(+e)]=a[+e];return r.ranges[i]=!0,{stack:e,id:t,glyph:a[n]||null}}catch(a){let o=r.glyphs[t]=await this._drawGlyph(r,e,t);return this._warnOnMissingGlyphRange(o,i,n,qn(a)),{stack:e,id:t,glyph:o}}}async _loadGlyphRange(e,t){let n=t*256,r=n+255,i=await this.requestManager.transformRequest(this.url.replace(`{fontstack}`,e).replace(`{range}`,`${n}-${r}`),`Glyphs`),a=await T(i,new AbortController);if(!a?.data)throw Error(`Could not load glyph range. range: ${t}, ${n}-${r}`);let o={};for(let e of Er(a.data))o[e.id]=e;return o}_warnOnMissingGlyphRange(e,t,n,r){let i=t*256,a=i+255,o=n.toString(16).padStart(4,`0`).toUpperCase();I(`Unable to load glyph range ${t}, ${i}-${a}. Rendering codepoint U+${o} locally instead. ${r}`)}_charUsesLocalIdeographFontFamily(e){return!!this.localIdeographFontFamily&&jt(e)}async _drawGlyph(e,t,n,r){let i=(await this._getTinySDF(e,t,n,r)).draw(n),a=/^\p{gc=Cf}+$/u.test(n);return{id:n.codePointAt(0),bitmap:new Ie({width:i.width||60,height:i.height||60},i.data),metrics:{width:a?0:i.glyphWidth/2||24,height:i.glyphHeight/2||24,left:i.glyphLeft/2+.5||0,top:i.glyphTop/2-27.5||-8,advance:a?0:i.glyphAdvance/2||24,isDoubleResolution:!0}}}_getTinySDF(e,t,n,r){if(r){let t=st(n),i=t?`clusterTinySDFs`:`fontFaceTinySDFs`;return e[i]??={},e[i][r]||=this._createTinySDF(r,!1,t?3:1),e[i][r]}let i=t===Pi&&this.localIdeographFontFamily!==``&&this._charUsesLocalIdeographFontFamily(n.codePointAt(0)),a=i?`ideographTinySDF`:`tinySDF`;return e[a]||=this._createTinySDF(i?this.localIdeographFontFamily:t),e[a]}async _createTinySDF(e,t=!0,n=1){let r=e?e.split(`,`):[];r.push(`sans-serif`);let i=r.map(e=>/[-\w]+/.test(e)?e:`'${CSS.escape(e)}'`).join(`,`),a=t?this._fontWeight(r[0]):void 0,o=t?this._fontStyle(r[0]):`normal`;if(typeof document<`u`&&document.fonts?.load)try{await document.fonts.load(`${o} ${a||`normal`} 48px ${i}`)}catch(e){I(`Failed to load font "${i}": ${qn(e).message}`)}return this.createRasterizer({fontSize:48,buffer:Math.max(6,Math.ceil(48*(n-1)/4)),radius:16,cutoff:.25,fontFamily:i,fontWeight:a,fontStyle:o,lang:this.lang},6)}_fontStyle(e){return/italic/i.test(e)?`italic`:/oblique/i.test(e)?`oblique`:`normal`}_fontWeight(e){let t={thin:100,hairline:100,"extra light":200,"ultra light":200,light:300,normal:400,regular:400,medium:500,semibold:600,demibold:600,bold:700,"extra bold":800,"ultra bold":800,black:900,heavy:900,"extra black":950,"ultra black":950},n;for(let[r,i]of Object.entries(t))RegExp(`\\b${r}\\b`,`i`).test(e)&&(n=`${i}`);return n}destroy(){for(let e in this.entries){let t=this.entries[e];t.tinySDF=null,t.ideographTinySDF=null,t.fontFaceTinySDFs={},t.glyphs={},t.requests={},t.ranges={}}this.entries={},this.fontFaceManager.destroy()}};let Li;const Ri=()=>Li||=new Kt({anchor:new r(Ft.light.anchor,`anchor`),position:new r(Ft.light.position,`position`),color:new r(Ft.light.color,`color`),intensity:new r(Ft.light.intensity,`intensity`)});var zi=class extends h{constructor(e,t){super(),this._transitionable=new An(Ri(),`light`,t),this.setLight(e),this._transitioning=this._transitionable.untransitioned()}getLight(){return this._transitionable.serialize()}getCartesianPosition(){return je(this.properties.get(`position`))}setLight(e,t={}){if(!this._validate(Ut.light,e,t))for(let t in e){let n=e[t];t.endsWith(`-transition`)?this._transitionable.setTransition(t.slice(0,-er.length),n):this._transitionable.setValue(t,n)}}updateTransitions(e){this._transitioning=this._transitionable.transitioned(e,this._transitioning)}hasTransition(){return this._transitioning.hasTransition()}recalculate(e){this.properties=this._transitioning.possiblyEvaluate(e)}_validate(e,t,n){return ar(this,e,{value:t},n)}};let Bi;const Vi=()=>Bi||=new Kt({"sky-color":new r(Ft.sky[`sky-color`],`sky-color`),"horizon-color":new r(Ft.sky[`horizon-color`],`horizon-color`),"fog-color":new r(Ft.sky[`fog-color`],`fog-color`),"fog-ground-blend":new r(Ft.sky[`fog-ground-blend`],`fog-ground-blend`),"horizon-fog-blend":new r(Ft.sky[`horizon-fog-blend`],`horizon-fog-blend`),"sky-horizon-blend":new r(Ft.sky[`sky-horizon-blend`],`sky-horizon-blend`),"atmosphere-blend":new r(Ft.sky[`atmosphere-blend`],`atmosphere-blend`)});var Hi=class extends h{constructor(e,t){super(),this._transitionable=new An(Vi(),`sky`,t),this.setSky(e),this._transitioning=this._transitionable.untransitioned(),this.recalculate(new Kn(0))}setSky(e,t={}){if(!this._validate(Ut.sky,e,t)){e||={"sky-color":`transparent`,"horizon-color":`transparent`,"fog-color":`transparent`,"fog-ground-blend":1,"atmosphere-blend":0};for(let t in e){let n=e[t];t.endsWith(`-transition`)?this._transitionable.setTransition(t.slice(0,-er.length),n):this._transitionable.setValue(t,n)}}}getSky(){return this._transitionable.serialize()}updateTransitions(e){this._transitioning=this._transitionable.transitioned(e,this._transitioning)}hasTransition(){return this._transitioning.hasTransition()}recalculate(e){this.properties=this._transitioning.possiblyEvaluate(e)}_validate(e,t,n={}){return ar(this,e,{value:t},n)}calculateFogBlendOpacity(e){return e<60?0:e<70?(e-60)/10:1}},Ui=class{constructor(e,t){this.width=e,this.height=t,this.nextRow=0,this.data=new Uint8Array(this.width*this.height),this.dashEntry={}}getDash(e,t){let n=e.join(`,`)+String(t);return this.dashEntry[n]||=this.addDash(e,t),this.dashEntry[n]}getDashRanges(e,t,n){let r=e.length%2==1,i=[],a=r?-e[e.length-1]*n:0,o=e[0]*n,s=!0;i.push({left:a,right:o,isDash:s,zeroLength:e[0]===0});let c=e[0];for(let t=1;t1&&(s=e[++o]);let c=Math.abs(i-s.left),l=Math.abs(i-s.right),u=Math.min(c,l),d,f=t/n*(r+1);if(s.isDash){let e=r-Math.abs(f);d=Math.sqrt(u*u+e*e)}else d=r-Math.sqrt(u*u+f*f);this.data[a+i]=Math.max(0,Math.min(255,d+128))}}}addRegularDash(e){for(let t=e.length-1;t>=0;--t){let n=e[t],r=e[t+1];n.zeroLength?e.splice(t,1):r?.isDash===n.isDash&&(r.left=n.left,e.splice(t,1))}let t=e[0],n=e[e.length-1];t.isDash===n.isDash&&(t.left=n.left-this.width,n.right=t.right+this.width);let r=this.width*this.nextRow,i=0,a=e[i];for(let t=0;t1&&(a=e[++i]);let n=Math.abs(t-a.left),o=Math.abs(t-a.right),s=Math.min(n,o),c=a.isDash?s:-s;this.data[r+t]=Math.max(0,Math.min(255,c+128))}}addDash(e,t){let n=t?7:0,r=2*n+1;if(this.nextRow+r>this.height)return I(`LineAtlas out of space`),null;let i=0;for(let t of e)i+=t;if(i!==0){let r=this.width/i,a=this.getDashRanges(e,this.width,r);t?this.addRoundDash(a,r,n):this.addRegularDash(a)}let a={y:this.nextRow+n,height:2*n,width:i};return this.nextRow+=r,this.dirty=!0,a}bind(e){let t=e.gl;this.texture?(t.bindTexture(t.TEXTURE_2D,this.texture),this.dirty&&(this.dirty=!1,t.texSubImage2D(t.TEXTURE_2D,0,0,0,this.width,this.height,t.ALPHA,t.UNSIGNED_BYTE,this.data))):(this.texture=t.createTexture(),t.bindTexture(t.TEXTURE_2D,this.texture),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_S,t.REPEAT),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_T,t.REPEAT),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MIN_FILTER,t.LINEAR),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MAG_FILTER,t.LINEAR),t.texImage2D(t.TEXTURE_2D,0,t.ALPHA,this.width,this.height,0,t.ALPHA,t.UNSIGNED_BYTE,this.data))}};function Wi(e){if(!e)return!1;let t=globalThis.location;if(!t)return!1;try{return new URL(e,t.href).origin!==t.origin}catch{return!1}}function Gi(){let e=import.meta.url;if(!/^https?:/.test(e))return``;let t=e.endsWith(`-dev.mjs`)?`maplibre-gl-worker-dev.mjs`:`maplibre-gl-worker.mjs`;return new URL(`./${t}`,e).href}function Ki(e,t){if(t)try{return new Worker(e,{type:`module`})}catch(e){console.warn(`Module worker not supported, falling back to classic worker`,e)}return new Worker(e)}async function qi(e){let t=await fetch(e);if(!t.ok)throw Error(`Failed to fetch worker script (${t.status}): ${e}`);let n=await t.text(),r=new Blob([n],{type:`text/javascript`});return URL.createObjectURL(r)}function Ji(e){let t=new Blob([`import ${JSON.stringify(new URL(e,import.meta.url).href)}`],{type:`text/javascript`});return URL.createObjectURL(t)}async function Yi(){let e=k.WORKER_URL||Gi(),t=!e?.endsWith(`.cjs`);if(!Wi(e))return Ki(e,t);if(t){let n=Ji(e);try{return Ki(n,t)}finally{URL.revokeObjectURL(n)}}let n=await qi(e);try{return Ki(n,t)}finally{URL.revokeObjectURL(n)}}const Xi=`maplibre_preloaded_worker_pool`;var Zi=class e{constructor(){this.active={},this.workersPromise=null}async acquire(t){if(this.active[t]=!0,!this.workersPromise){let t=[];for(;t.length{for(let t of e)t.terminate()})}}isPreloaded(){return!!this.active[Xi]}numActive(){return Object.keys(this.active).length}};const Qi=Math.floor(Rr.hardwareConcurrency/2);Zi.workerCount=sn(globalThis)?Math.max(Math.min(Qi,3),1):1;let $i;function ea(){return $i||=new Zi,$i}function ta(){ea().acquire(Xi)}function na(){let e=$i;e&&(e.isPreloaded()&&e.numActive()===1?(e.release(Xi),$i=null):console.warn(`Could not clear WebWorkers since there are active Map instances that still reference it. The pre-warmed WebWorker pool can only be cleared when all map instances have been removed with map.remove()`))}var ra=class{constructor(e,t){this.workerPool=e,this.actors=[],this.currentActor=0,this.id=t,this.removed=!1,this.actorsPromise=this.initActors(t)}async initActors(e){let t=await this.workerPool.acquire(e);if(this.removed)return[];if(this.actors=t.map((t,n)=>{let r=new _r(t,e);return r.name=`Worker ${n}`,r}),!this.actors.length)throw Error(`No actors found`);return this.actors}async broadcast(e,t){let n=await this.actorsPromise;return Promise.all(n.map(n=>n.sendAsync({type:e,data:t})))}async getActor(){let e=await this.actorsPromise;return this.currentActor=(this.currentActor+1)%e.length,e[this.currentActor]}async waitForInitComplete(){this.actors.length===0&&await this.actorsPromise}getReadyActor(){return this.currentActor=(this.currentActor+1)%this.actors.length,this.actors[this.currentActor]}remove(e=!0){this.removed=!0;for(let e of this.actors)e.remove();this.actors=[],e&&this.workerPool.release(this.id)}async registerMessageHandler(e,t){let n=await this.actorsPromise;for(let r of n)r.registerMessageHandler(e,t)}async unregisterMessageHandler(e){let t=await this.actorsPromise;for(let n of t)n.unregisterMessageHandler(e)}};let ia;function aa(){return ia||(ia=new ra(ea(),et),ia.registerMessageHandler(`GR`,(e,t,n)=>o(t,n))),ia}function oa(e,t){let n=vr();return Le(n,n,[1,1,0]),ke(n,n,[e.width*.5,e.height*.5,1]),e.calculatePosMatrix?y(n,n,e.calculatePosMatrix(t.toUnwrapped())):n}function sa(e,t,n){if(e)for(let r of e){let e=t[r];if(e?.source===n&&e.type===`fill-extrusion`)return!0}else for(let e in t){let r=t[e];if(r.source===n&&r.type===`fill-extrusion`)return!0}return!1}function ca(e,t,n,r,i,a,o){let s=sa(i?.layers??null,t,e.id),c=a.maxPitchScaleFactor(),l=e.tilesIn(r,c,s);l.sort(da);let u=[];for(let r of l)u.push({wrappedTileID:r.tileID.wrapped().key,queryResults:r.tile.queryRenderedFeatures(t,n,e.getState(),r.queryGeometry,r.cameraQueryGeometry,r.scale,i,a,c,oa(a,r.tileID),o?(e,t)=>o(r.tileID,e,t):void 0)});return pa(fa(u),e)}function la(e,t,n,r,i,a,o){let s={},c=a.queryRenderedSymbols(r),l=[];for(let e of Object.keys(c).map(Number))l.push(o[e]);l.sort(da);for(let n of l){let r=n.featureIndex.lookupSymbolFeatures(c[n.bucketInstanceId],t,n.bucketIndex,n.sourceLayerIndex,{filterSpec:i.filter,globalState:i.globalState},i.layers,i.availableImages,e);for(let e in r){s[e]||=[];let t=r[e];t.sort((e,t)=>{let r=n.featureSortOrder;if(r){let n=r.indexOf(e.featureIndex);return r.indexOf(t.featureIndex)-n}return t.featureIndex-e.featureIndex});for(let n of t)s[e].push(n)}}return ma(s,e,n)}function ua(e,t){let n=e.getRenderableIds().map(t=>e.getTileByID(t)),r=[],i={};for(let e of n){let n=e.tileID.canonical.key;i[n]||(i[n]=!0,e.querySourceFeatures(r,t))}return r}function da(e,t){let n=e.tileID,r=t.tileID;return n.overscaledZ-r.overscaledZ||n.canonical.y-r.canonical.y||n.wrap-r.wrap||n.canonical.x-r.canonical.x}function fa(e){let t={},n={};for(let{queryResults:r,wrappedTileID:i}of e){n[i]||={};let e=n[i];for(let n in r){let i=r[n];e[n]||={};let a=e[n];t[n]||=[];for(let e of i)a[e.featureIndex]||(a[e.featureIndex]=!0,t[n].push(e))}}return t}function pa(e,t){for(let n in e)for(let r of e[n])ha(r,t);return e}function ma(e,t,n){for(let r in e)for(let i of e[r]){let e=n[t[r].source];ha(i,e)}return e}function ha(e,t){let n=e.feature,r=t.getFeatureState(n.layer[`source-layer`],n.id);n.source=n.layer.source,n.layer[`source-layer`]&&(n.sourceLayer=n.layer[`source-layer`]),n.state=r}async function ga(e,t,n,r){let i=e;if(e.url?i=(await b(await t.transformRequest(e.url,`Source`),n)).data:await Rr.frameAsync(n,r),!i)return null;let a=hr(z(i,e),[`tiles`,`minzoom`,`maxzoom`,`attribution`,`bounds`,`scheme`,`tileSize`,`encoding`]);return`vector_layers`in i&&i.vector_layers&&(a.vectorLayerIds=i.vector_layers.map(e=>e.id)),a}var _a=class e{constructor(e,t){e&&(t?this.setSouthWest(e).setNorthEast(t):Array.isArray(e)&&(e.length===4?this.setSouthWest([e[0],e[1]]).setNorthEast([e[2],e[3]]):this.setSouthWest(e[0]).setNorthEast(e[1])))}setNorthEast(e){return this._ne=e instanceof V?new V(e.lng,e.lat):V.convert(e),this}setSouthWest(e){return this._sw=e instanceof V?new V(e.lng,e.lat):V.convert(e),this}extend(t){let n=this._sw,r=this._ne,i,a;if(t instanceof V)i=t,a=t;else if(t instanceof e){if(i=t._sw,a=t._ne,!i||!a)return this}else{if(Array.isArray(t)){if(t.length===4||t.every(Array.isArray)){let n=t;return this.extend(e.convert(n))}{let e=t;return this.extend(V.convert(e))}}return t&&(`lng`in t||`lon`in t)&&`lat`in t?this.extend(V.convert(t)):this}return!n&&!r?(this._sw=new V(i.lng,i.lat),this._ne=new V(a.lng,a.lat)):(n.lng=Math.min(i.lng,n.lng),n.lat=Math.min(i.lat,n.lat),r.lng=Math.max(a.lng,r.lng),r.lat=Math.max(a.lat,r.lat)),this}getCenter(){return new V((this._sw.lng+this._ne.lng)/2,(this._sw.lat+this._ne.lat)/2)}getSouthWest(){return this._sw}getNorthEast(){return this._ne}getNorthWest(){return new V(this.getWest(),this.getNorth())}getSouthEast(){return new V(this.getEast(),this.getSouth())}getWest(){return this._sw.lng}getSouth(){return this._sw.lat}getEast(){return this._ne.lng}getNorth(){return this._ne.lat}toArray(){return[this._sw.toArray(),this._ne.toArray()]}toString(){return`LngLatBounds(${this._sw.toString()}, ${this._ne.toString()})`}isEmpty(){return!(this._sw&&this._ne)}contains(e){let{lng:t,lat:n}=V.convert(e),r=this._sw.lat<=n&&n<=this._ne.lat,i=this._sw.lng<=t&&t<=this._ne.lng;return this._sw.lng>this._ne.lng&&(i=this._sw.lng>=t&&t>=this._ne.lng),r&&i}intersects(t){if(t=e.convert(t),!(t.getNorth()>=this.getSouth()&&t.getSouth()<=this.getNorth()))return!1;let n=Math.abs(this.getEast()-this.getWest()),r=Math.abs(t.getEast()-t.getWest());if(n>=360||r>=360)return!0;let i=Or(this.getWest(),-180,180),a=Or(this.getEast(),-180,180),o=Or(t.getWest(),-180,180),s=Or(t.getEast(),-180,180),c=i>a,l=o>s;return c&&l?!0:c?s>=i||o<=a:l?a>=o||i<=s:o<=a&&s>=i}static convert(t){return t instanceof e||!t?t:new e(t)}static fromLngLat(t,n=0){let r=360*n/40075017,i=r/Math.cos(Math.PI/180*t.lat);return new e(new V(t.lng-i,t.lat-r),new V(t.lng+i,t.lat+r))}adjustAntiMeridian(){let t=new V(this._sw.lng,this._sw.lat),n=new V(this._ne.lng,this._ne.lat);return t.lng>n.lng?new e(t,new V(n.lng+360,n.lat)):new e(t,n)}},va=class{constructor(e,t,n){this.bounds=_a.convert(this.validateBounds(e)),this.minzoom=t||0,this.maxzoom=n||24}validateBounds(e){return!Array.isArray(e)||e.length!==4?[-180,-90,180,90]:[Math.max(-180,e[0]),Math.max(-90,e[1]),Math.min(180,e[2]),Math.min(90,e[3])]}contains(e){let t=2**e.z,n={minX:Math.floor(Zn(this.bounds.getWest())*t),minY:Math.floor(Vt(this.bounds.getNorth())*t),maxX:Math.ceil(Zn(this.bounds.getEast())*t),maxY:Math.ceil(Vt(this.bounds.getSouth())*t)};return e.x>=n.minX&&e.x=n.minY&&e.y{this._options.tiles=e}),this}setUrl(e){return this.setSourceProperty(()=>{this.url=e,this._options.url=e}),this}onRemove(){this._tileJSONRequest&&=(this._tileJSONRequest.abort(),null)}serialize(){return z({},this._options)}async loadTile(e){let t=e.tileID.canonical.url(this.tiles,this.map.getPixelRatio(),this.scheme),n={request:await this.map._requestManager.transformRequest(t,`Tile`),uid:e.uid,tileID:e.tileID,zoom:e.tileID.overscaledZ,tileSize:this.tileSize*e.tileID.overscaleFactor(),type:this.type,source:this.id,pixelRatio:this.map.getPixelRatio(),showCollisionBoxes:this.map.showCollisionBoxes,promoteId:this.promoteId,subdivisionGranularity:this.map.style.projection.subdivisionGranularity,encoding:this.encoding,overzoomParameters:await this._getOverzoomParameters(e),etag:e.etag};n.request.collectResourceTiming=this._collectResourceTiming,await this.dispatcher.waitForInitComplete();let r=`RT`;if(!e.actor||e.state===`expired`)e.actor=this.dispatcher.getReadyActor(),r=`LT`;else if(e.state===`loading`)return new Promise((t,n)=>{e.reloadPromise={resolve:t,reject:n}});e.abortController=new AbortController;try{let t=await e.actor.sendAsync({type:r,data:n},e.abortController);if(delete e.abortController,e.aborted)return;this._afterTileLoadWorkerResponse(e,t);let i={};return t?.etagUnmodified&&(i.unmodified=!0),i}catch(t){if(delete e.abortController,e.aborted||xe(t))return;if(t&&t.status!==404)throw t;this._afterTileLoadWorkerResponse(e,null)}}async _getOverzoomParameters(e){if(e.tileID.canonical.z<=this.maxzoom||this.map._zoomLevelsToOverscale===void 0)return;let t=e.tileID.scaledTo(this.maxzoom).canonical,n=t.url(this.tiles,this.map.getPixelRatio(),this.scheme);return{maxZoomTileID:t,overzoomRequest:await this.map._requestManager.transformRequest(n,`Tile`)}}_afterTileLoadWorkerResponse(e,t){if(t?.resourceTiming&&(e.resourceTiming=t.resourceTiming),t&&this.map._refreshExpiredTiles&&e.setExpiryData(t),e.etag=t?.etag,e.loadVectorData(t,this.map.painter),e.reloadPromise){let t=e.reloadPromise;e.reloadPromise=null,this.loadTile(e).then(t.resolve).catch(t.reject)}}async abortTile(e){e.abortController&&(e.abortController.abort(),delete e.abortController),e.actor&&await e.actor.sendAsync({type:`AT`,data:{uid:e.uid,type:this.type,source:this.id}})}async unloadTile(e){e.unloadVectorData(),e.actor&&await e.actor.sendAsync({type:`RMT`,data:{uid:e.uid,type:this.type,source:this.id}})}hasTransition(){return!1}},ba=class extends h{constructor(e,t,n,r){super(),this.id=e,this.dispatcher=n,this.setEventedParent(r),this.type=`raster`,this.minzoom=0,this.maxzoom=22,this.roundZoom=!0,this.scheme=`xyz`,this.tileSize=512,this._loaded=!1,this._premultiplyAlpha=!0,this._options=z({type:`raster`},t),z(this,hr(t,[`url`,`scheme`,`tileSize`]))}async load(e=!1){this._loaded=!1,this.fire(new K(`dataloading`)),this._tileJSONRequest=new AbortController;try{let t=await ga(this._options,this.map._requestManager,this._tileJSONRequest,this.map._ownerWindow);this._tileJSONRequest=null,this._loaded=!0,t&&(z(this,t),t.bounds&&(this.tileBounds=new va(t.bounds,this.minzoom,this.maxzoom)),this.fire(new K(`data`,{sourceDataType:`metadata`})),this.fire(new K(`data`,{sourceDataType:`content`,sourceDataChanged:e})))}catch(e){this._tileJSONRequest=null,this._loaded=!0,xe(e)||this.fire(new H(qn(e)))}}loaded(){return this._loaded}onAdd(e){this.map=e,this.load()}onRemove(){this._tileJSONRequest&&=(this._tileJSONRequest.abort(),null)}setSourceProperty(e){this._tileJSONRequest&&=(this._tileJSONRequest.abort(),null),e(),this.load(!0)}setTiles(e){return this.setSourceProperty(()=>{this._options.tiles=e}),this}setUrl(e){return this.setSourceProperty(()=>{this.url=e,this._options.url=e}),this}serialize(){return z({},this._options)}setPremultiplyAlpha(e){return this._premultiplyAlpha===e||this.setSourceProperty(()=>{this._premultiplyAlpha=e}),this}hasTile(e){return!this.tileBounds||this.tileBounds.contains(e.canonical)}async loadTile(e){let t=e.tileID.canonical.url(this.tiles,this.map.getPixelRatio(),this.scheme),n=this._premultiplyAlpha,r=n?void 0:{premultiplyAlpha:`none`};e.abortController=new AbortController;try{let i=await Ur.transformAndGetImage(this.map._requestManager,t,`Tile`,e.abortController,this.map._refreshExpiredTiles,r);if(delete e.abortController,e.aborted){e.state=`unloaded`;return}if(i?.data){this.map._refreshExpiredTiles&&(i.cacheControl||i.expires)&&e.setExpiryData({cacheControl:i.cacheControl,expires:i.expires});let t=this.map.painter.context,r=t.gl,a=i.data;e.texture=this.map.painter.getTileTexture(a.width),e.texture?e.texture.update(a,{useMipmap:!0,premultiply:n}):(e.texture=new _(t,a,r.RGBA,{useMipmap:!0,premultiply:n}),e.texture.bind(r.LINEAR,r.CLAMP_TO_EDGE,r.LINEAR_MIPMAP_NEAREST)),e.state=`loaded`}}catch(t){if(delete e.abortController,e.aborted)e.state=`unloaded`;else if(t)throw e.state=`errored`,t}}async abortTile(e){e.abortController&&(e.abortController.abort(),delete e.abortController)}async unloadTile(e){e.texture&&this.map.painter.saveTileTexture(e.texture)}hasTransition(){return!1}},xa=class extends ba{constructor(e,t,n,r){super(e,t,n,r),this.type=`raster-dem`,this.maxzoom=22,this._options=z({type:`raster-dem`},t),this.encoding=t.encoding||`mapbox`,this.redFactor=t.redFactor,this.greenFactor=t.greenFactor,this.blueFactor=t.blueFactor,this.baseShift=t.baseShift}async loadTile(e){let t=e.tileID.canonical.url(this.tiles,this.map.getPixelRatio(),this.scheme);e.neighboringTiles=this._getNeighboringTiles(e.tileID),e.abortController=new AbortController;try{let n=await Ur.transformAndGetImage(this.map._requestManager,t,`Tile`,e.abortController,this.map._refreshExpiredTiles,{colorSpaceConversion:`none`});if(delete e.abortController,e.aborted){e.state=`unloaded`;return}if(n?.data){let t=n.data;this.map._refreshExpiredTiles&&(n.cacheControl||n.expires)&&e.setExpiryData({cacheControl:n.cacheControl,expires:n.expires});let r=zn(t)&&Dr()?t:await this.readImageNow(t),i={type:this.type,uid:e.uid,source:this.id,rawImageData:r,encoding:this.encoding,redFactor:this.redFactor,greenFactor:this.greenFactor,blueFactor:this.blueFactor,baseShift:this.baseShift};if(e.actor&&e.state!==`expired`&&e.state!==`reloading`)return;await this.dispatcher.waitForInitComplete(),(!e.actor||e.state===`expired`)&&(e.actor=this.dispatcher.getReadyActor()),e.dem=await e.actor.sendAsync({type:`LDT`,data:i}),e.needsHillshadePrepare=!0,e.needsTerrainPrepare=!0,e.needsColorReliefPrepare=!0,e.state=`loaded`}}catch(t){if(delete e.abortController,e.aborted)e.state=`unloaded`;else if(t)throw e.state=`errored`,t}}async readImageNow(e){if(typeof VideoFrame<`u`&&Ge()){let t=e.width+2,n=e.height+2;try{return new xn({width:t,height:n},await Fe(e,-1,-1,t,n))}catch{}}return Rr.getImageData(e,1)}_getNeighboringTiles(e){let t=e.canonical,n=2**t.z,r=(t.x-1+n)%n,i=t.x===0?e.wrap-1:e.wrap,a=(t.x+1+n)%n,o=t.x+1===n?e.wrap+1:e.wrap,s={};return s[new $t(e.overscaledZ,i,t.z,r,t.y).key]={backfilled:!1},s[new $t(e.overscaledZ,o,t.z,a,t.y).key]={backfilled:!1},t.y>0&&(s[new $t(e.overscaledZ,i,t.z,r,t.y-1).key]={backfilled:!1},s[new $t(e.overscaledZ,e.wrap,t.z,t.x,t.y-1).key]={backfilled:!1},s[new $t(e.overscaledZ,o,t.z,a,t.y-1).key]={backfilled:!1}),t.y+10||n.addOrUpdateProperties?.length>0;if(!i&&!a)continue;r.push(t.geometry);let o={...t};if(e.set(n.id,o),i&&(r.push(n.newGeometry),o.geometry=n.newGeometry),a){if(o.properties=n.removeAllProperties?{}:{...o.properties||{}},n.removeProperties)for(let e of n.removeProperties)delete o.properties[e];if(n.addOrUpdateProperties)for(let{key:e,value:t}of n.addOrUpdateProperties)o.properties[e]=t}}return r}function Ta(e,t,n){if(!e)return t||{};if(!t)return e||{};n&&(Oa(e.add,n),Oa(t.add,n));let r=Aa(e),i=Aa(t);Ea(r,i);let a={};if((r.removeAll||i.removeAll)&&(a.removeAll=!0),a.remove=new Set([...r.remove,...i.remove]),a.add=new Map([...r.add,...i.add]),a.update=new Map([...r.update,...i.update]),a.remove.size&&a.add.size)for(let e of a.add.keys())a.remove.delete(e);let o=ja(a);return n&&ka(o.add,n),o}function Ea(e,t){t.removeAll&&(e.add.clear(),e.update.clear(),e.remove.clear(),t.remove.clear());for(let n of t.remove)e.add.delete(n),e.update.delete(n);for(let[n,r]of t.update){let i=e.update.get(n);i&&(t.update.set(n,Da(i,r)),e.update.delete(n))}}function Da(e,t){let n={id:e.id};if(t.removeAllProperties&&(delete e.removeProperties,delete e.addOrUpdateProperties,delete t.removeProperties),t.removeProperties)for(let n of t.removeProperties){let t=e.addOrUpdateProperties.findIndex(e=>e.key===n);t>-1&&e.addOrUpdateProperties.splice(t,1)}return(e.removeAllProperties||t.removeAllProperties)&&(n.removeAllProperties=!0),(e.removeProperties||t.removeProperties)&&(n.removeProperties=[...e.removeProperties||[],...t.removeProperties||[]]),(e.addOrUpdateProperties||t.addOrUpdateProperties)&&(n.addOrUpdateProperties=[...e.addOrUpdateProperties||[],...t.addOrUpdateProperties||[]]),(e.newGeometry||t.newGeometry)&&(n.newGeometry=t.newGeometry||e.newGeometry),n}function Oa(e,t){if(e)for(let n of e){let e=Sa(n,t);e!=null&&(n.id=e)}}function ka(e,t){if(e)for(let n of e)Sa(n,t)!=null&&delete n.id}function Aa(e){if(!e)return{};let t={};return t.removeAll=e.removeAll,t.remove=new Set(e.remove||[]),t.add=new Map(e.add?.map(e=>[e.id,e])),t.update=new Map(e.update?.map(e=>[e.id,e])),t}function ja(e){let t={};return e.removeAll&&(t.removeAll=e.removeAll),e.remove&&(t.remove=Array.from(e.remove)),e.add&&(t.add=Array.from(e.add.values())),e.update&&(t.update=Array.from(e.update.values())),t}function Ma(e){return!e||e.length===0?[]:typeof e[0]==`number`?[e]:e.flatMap(e=>Ma(e))}function Na(e){return e.type===`GeometryCollection`?e.geometries.flatMap(e=>Na(e)):Ma(e.coordinates)}function Pa(e){let t=new _a,n;switch(e.type){case`FeatureCollection`:n=e.features.flatMap(e=>Na(e.geometry));break;case`Feature`:n=Na(e.geometry);break;default:n=Na(e)}if(n.length===0)return t;for(let e of n){let[n,r]=e;t.extend([n,r])}return t}function Fa({x:e,y:t,z:n},r=0){let i=In((e-r)/2**n),a=Mt((t+1+r)/2**n),o=In((e+1+r)/2**n),s=Mt((t-r)/2**n);return new _a([i,a],[o,s])}var Ia=class extends h{constructor(e,t,n,r){super(),this.id=e,this.type=`geojson`,this.minzoom=0,this.maxzoom=18,this.tileSize=512,this.isTileClipped=!0,this.reparseOverscaled=!0,this._removed=!1,this._isUpdatingWorker=!1,this._pendingWorkerUpdate={data:t.data},this.actorPromise=n.getActor(),this.setEventedParent(r),this._data=typeof t.data==`string`?{url:t.data}:{geojson:t.data},this._options=z({},t),this._collectResourceTiming=t.collectResourceTiming,t.maxzoom!==void 0&&(this.maxzoom=t.maxzoom),t.type&&(this.type=t.type),t.attribution&&(this.attribution=t.attribution),this.promoteId=t.promoteId,t.clusterMaxZoom!==void 0&&this.maxzoom<=t.clusterMaxZoom&&I(`The maxzoom value "${this.maxzoom}" is expected to be greater than the clusterMaxZoom value "${t.clusterMaxZoom}".`),this.workerOptions=z({source:this.id,geojsonVtOptions:{buffer:this._pixelsToTileUnits(t.buffer===void 0?128:t.buffer),tolerance:this._pixelsToTileUnits(t.tolerance===void 0?.375:t.tolerance),extent:N,maxZoom:this.maxzoom,lineMetrics:t.lineMetrics||!1,generateId:t.generateId||!1,promoteId:typeof t.promoteId==`string`?t.promoteId:void 0,cluster:t.cluster||!1,clusterOptions:{maxZoom:this._getClusterMaxZoom(t.clusterMaxZoom),minPoints:Math.max(2,t.clusterMinPoints||2),extent:N,radius:this._pixelsToTileUnits(t.clusterRadius||50),log:!1,generateId:t.generateId||!1}},clusterProperties:t.clusterProperties,filter:t.filter},t.workerOptions)}_hasPendingWorkerUpdate(){return this._pendingWorkerUpdate.data!==void 0||this._pendingWorkerUpdate.diff!==void 0||this._pendingWorkerUpdate.updateCluster}_pixelsToTileUnits(e){return e*(N/this.tileSize)}_tileUnitsToPixels(e){return e/(N/this.tileSize)}_getClusterMaxZoom(e){let t=e?Math.round(e):this.maxzoom-1;return Number.isInteger(e)||e===void 0||I(`Integer expected for option 'clusterMaxZoom': provided value "${e}" rounded to "${t}"`),t}async load(){await this._updateWorkerData()}onAdd(e){this.map=e,this.load()}setData(e){return this._data=typeof e==`string`?{url:e}:{geojson:e},this._pendingWorkerUpdate={data:e},this._updateWorkerData()}updateData(e){return this._pendingWorkerUpdate.diff=Ta(this._pendingWorkerUpdate.diff,e),this._updateWorkerData()}async getData(){return this._data.url&&await this.once(`data`),this._data.geojson?this._data.geojson:{type:`FeatureCollection`,features:Array.from(this._data.updateable.values())}}async getBounds(){return Pa(await this.getData())}setClusterOptions(e){return this.workerOptions.geojsonVtOptions.cluster=e.cluster,e.clusterRadius!==void 0&&(this.workerOptions.geojsonVtOptions.clusterOptions.radius=this._pixelsToTileUnits(e.clusterRadius)),e.clusterMaxZoom!==void 0&&(this.workerOptions.geojsonVtOptions.clusterOptions.maxZoom=this._getClusterMaxZoom(e.clusterMaxZoom)),this._pendingWorkerUpdate.updateCluster=!0,this._updateWorkerData()}getClusterOptions(){let{cluster:e,clusterOptions:t}=this.workerOptions.geojsonVtOptions;return{cluster:e,clusterMaxZoom:t.maxZoom,clusterRadius:this._tileUnitsToPixels(t.radius)}}async getClusterExpansionZoom(e){return(await this.actorPromise).sendAsync({type:`GCEZ`,data:{type:this.type,clusterId:e,source:this.id}})}async getClusterChildren(e){return(await this.actorPromise).sendAsync({type:`GCC`,data:{type:this.type,clusterId:e,source:this.id}})}async getClusterLeaves(e,t,n){return(await this.actorPromise).sendAsync({type:`GCL`,data:{type:this.type,source:this.id,clusterId:e,limit:t,offset:n}})}async _updateWorkerData(){if(this._isUpdatingWorker)return this._updatePromise;if(!this._hasPendingWorkerUpdate()){I(`No pending worker updates for GeoJSONSource ${this.id}.`);return}let{data:e,diff:t,updateCluster:n}=this._pendingWorkerUpdate,r=this._getLoadGeoJSONParameters(e,t,n);e===void 0?t?this._pendingWorkerUpdate.diff=void 0:n&&(this._pendingWorkerUpdate.updateCluster=void 0):this._pendingWorkerUpdate.data=void 0,this._updatePromise=this._dispatchWorkerUpdate(r),await this._updatePromise}async _getLoadGeoJSONParameters(e,t,n){let r=z({type:this.type,source:this.id},this.workerOptions);if(typeof e==`string`)return r.request=await this.map._requestManager.transformRequest(Rr.resolveURL(e),`Source`),r.request.collectResourceTiming=this._collectResourceTiming,r;if(e!==void 0)return r.data=e,r;if(t)return r.dataDiff=t,r;if(n)return r.updateCluster=!0,r}async _dispatchWorkerUpdate(e){this._isUpdatingWorker=!0,this.fire(new K(`dataloading`));try{let t=await e,n=await(await this.actorPromise).sendAsync({type:`LD`,data:t});if(this._isUpdatingWorker=!1,this._removed||n.abandoned){this.fire(new K(`dataabort`));return}n.data&&(this._data={geojson:n.data});let r=this._applyDiffToSource(t.dataDiff),i=this._getShouldReloadTileOptions(r),a={};this._applyResourceTiming(a,n),this.fire(new K(`data`,{...a,sourceDataType:`metadata`})),this.fire(new K(`data`,{...a,sourceDataType:`content`,shouldReloadTileOptions:i}))}catch(e){if(this._isUpdatingWorker=!1,this._removed){this.fire(new K(`dataabort`));return}this.fire(new H(qn(e)))}finally{this._hasPendingWorkerUpdate()&&await this._updateWorkerData()}}_applyResourceTiming(e,t){if(!this._collectResourceTiming)return;let n=t.resourceTiming?.[this.id];if(!n)return;let r=n.slice(0);r?.length&&z(e,{resourceTiming:r})}_applyDiffToSource(e){if(!e)return;let t=typeof this.promoteId==`string`?this.promoteId:void 0;if(!this._data.url&&!this._data.updateable){let e=Ca(this._data.geojson,t);if(!e)throw Error(`GeoJSONSource "${this.id}": GeoJSON data is not compatible with updateData`);this._data={updateable:e}}if(!this._data.updateable)return;let n=wa(this._data.updateable,e,t);if(!(e.removeAll||this._options.cluster))return n}_getShouldReloadTileOptions(e){if(e)return{affectedBounds:e.filter(Boolean).map(e=>Pa(e))}}shouldReloadTile(e,{affectedBounds:t}){if(e.state===`loading`)return!0;if(e.state===`unloaded`)return!1;let{buffer:n,extent:r}=this.workerOptions.geojsonVtOptions,i=Fa(e.tileID.canonical,n/r);for(let e of t)if(i.intersects(e))return!0;return!1}loaded(){return!this._isUpdatingWorker&&!this._hasPendingWorkerUpdate()}async loadTile(e){let t=e.actor?`RT`:`LT`;e.actor=await this.actorPromise;let n={type:this.type,uid:e.uid,tileID:e.tileID,zoom:e.tileID.overscaledZ,maxZoom:this.maxzoom,tileSize:this.tileSize,source:this.id,pixelRatio:this.map.getPixelRatio(),showCollisionBoxes:this.map.showCollisionBoxes,promoteId:this.promoteId,subdivisionGranularity:this.map.style.projection.subdivisionGranularity};e.abortController=new AbortController;try{let r=await(await this.actorPromise).sendAsync({type:t,data:n},e.abortController);delete e.abortController,e.unloadVectorData(),e.aborted||e.loadVectorData(r,this.map.painter,t===`RT`)}catch(t){if(delete e.abortController,e.aborted||xe(t))return;throw t}}async abortTile(e){e.abortController&&(e.abortController.abort(),delete e.abortController),e.aborted=!0}async unloadTile(e){e.unloadVectorData(),await(await this.actorPromise).sendAsync({type:`RMT`,data:{uid:e.uid,type:this.type,source:this.id}})}onRemove(){this._removed=!0,this.actorPromise.then(e=>e.sendAsync({type:`RS`,data:{type:this.type,source:this.id}}))}serialize(){return z({},this._options,{type:this.type,data:this._data.updateable?{type:`FeatureCollection`,features:Array.from(this._data.updateable.values())}:this._data.url||this._data.geojson})}hasTransition(){return!1}};const La=[0,0,1],Ra=(e,t)=>({u_tl_parent:new j(e,t.u_tl_parent),u_scale_parent:new P(e,t.u_scale_parent),u_buffer_scale:new P(e,t.u_buffer_scale),u_image_warp:new ue(e,t.u_image_warp),u_fade_t:new P(e,t.u_fade_t),u_opacity:new P(e,t.u_opacity),u_image0:new F(e,t.u_image0),u_image1:new F(e,t.u_image1),u_brightness_low:new P(e,t.u_brightness_low),u_brightness_high:new P(e,t.u_brightness_high),u_saturation_factor:new P(e,t.u_saturation_factor),u_contrast_factor:new P(e,t.u_contrast_factor),u_spin_weights:new ue(e,t.u_spin_weights),u_coords_top:new Ce(e,t.u_coords_top),u_coords_bottom:new Ce(e,t.u_coords_bottom)}),za=(e,t,n,r,i,a)=>({u_tl_parent:e,u_scale_parent:t,u_buffer_scale:1,u_image_warp:a,u_fade_t:n.mix,u_opacity:n.opacity*r.paint.get(`raster-opacity`),u_image0:0,u_image1:1,u_brightness_low:r.paint.get(`raster-brightness-min`),u_brightness_high:r.paint.get(`raster-brightness-max`),u_saturation_factor:Ha(r.paint.get(`raster-saturation`)),u_contrast_factor:Va(r.paint.get(`raster-contrast`)),u_spin_weights:Ba(r.paint.get(`raster-hue-rotate`)),u_coords_top:[i[0].x,i[0].y,i[1].x,i[1].y],u_coords_bottom:[i[3].x,i[3].y,i[2].x,i[2].y]});function Ba(e){e*=Math.PI/180;let t=Math.sin(e),n=Math.cos(e);return[(2*n+1)/3,(-Math.sqrt(3)*t-n+1)/3,(Math.sqrt(3)*t-n+1)/3]}function Va(e){return e>0?1/(1-e):1+e}function Ha(e){return e>0?1-1/(1.001-e):-e}var Ua=class{constructor(e,t,n){this.vertexBuffer=e,this.indexBuffer=t,this.segments=n}destroy(){this.vertexBuffer.destroy(),this.indexBuffer.destroy(),this.segments.destroy(),this.vertexBuffer=null,this.indexBuffer=null,this.segments=null}};const Wa=wt([{name:`a_pos`,type:`Int16`,components:2}]),Ga=N/128;function Ka(e,t){let n=qa(t,`16bit`),r=Un.deserialize({arrayBuffer:n.vertices,length:n.vertices.byteLength/2/2}),i=gt.deserialize({arrayBuffer:n.indices,length:n.indices.byteLength/2/3});return new Ua(e.createVertexBuffer(r,Wa.members),e.createIndexBuffer(i),ae.simpleSegment(0,0,r.length,i.length))}function qa(e,t){let n=e.granularity===void 0?1:Math.max(e.granularity,1),r=n+(e.generateBorders?2:0),i=n+(e.extendToNorthPole||e.generateBorders?1:0)+(e.extendToSouthPole||e.generateBorders?1:0),a=r+1,o=i+1,s=e.generateBorders?-1:0,c=e.generateBorders||e.extendToNorthPole?-1:0,l=n+ +!!e.generateBorders,u=n+(e.generateBorders||e.extendToSouthPole?1:0),d=a*o,f=r*i*6,p=a*o>65536;if(p&&t===`16bit`)throw Error(`Granularity is too large and meshes would not fit inside 16 bit vertex indices.`);let m=p||t===`32bit`,h=new Int16Array(d*2),g=0;for(let t=c;t<=u;t++)for(let r=s;r<=l;r++){let i=r/n*N;r===-1&&(i=-Ga),r===n+1&&(i=N+Ga);let a=t/n*N;t===-1&&(a=e.extendToNorthPole?Ot:-Ga),t===n+1&&(a=e.extendToSouthPole?dr:N+Ga),h[g++]=i,h[g++]=a}let _=m?new Uint32Array(f):new Uint16Array(f),v=0;for(let e=0;ethis.tileID.getTilePoint(e)._round()),this.imageWarp=Za(this.tileCoords,this._warp),this._subdividedQuad=this.imageWarp[2]>0&&!$a(this.tileCoords),this.flippedWindingOrder=Xa(this.tileCoords),this.fire(new K(`data`,{sourceDataType:`content`})),this}prepare(){if(Object.keys(this.tiles).length===0||!this.image)return;let e=this.map.painter.context,t=e.gl;this.texture?this._imageDirty&&(this.texture.update(this.image),this.texture.bind(t.LINEAR,t.CLAMP_TO_EDGE)):(this.texture=new _(e,this.image,t.RGBA),this.texture.bind(t.LINEAR,t.CLAMP_TO_EDGE)),this._imageDirty=!1;let n=!1;for(let e in this.tiles){let t=this.tiles[e];t.state!==`loaded`&&(t.state=`loaded`,t.texture=this.texture,n=!0)}n&&this.fire(new K(`data`,{sourceDataType:`idle`,sourceId:this.id}))}async loadTile(e){this.tileID?.equals(e.tileID.canonical)?(this.tiles[String(e.tileID.wrap)]=e,e.buckets={}):e.state=`errored`}serialize(){let e={type:`image`,coordinates:this.coordinates};return this.options.url!==void 0&&(e.url=this.options.url),e}hasTransition(){return!1}_getOverlappingTileRanges(e){let{minX:t,minY:n,maxX:r,maxY:i}=On.fromPoints(e),a={};for(let e=0;e<=25;e++){let o=2**e,s=Math.floor(t*o),c=Math.floor(n*o),l=Math.floor(r*o),u=Math.floor(i*o),d=(s%o+o)%o,f=l%o,p=Math.floor(s/o),m=Math.floor(l/o);a[e]={minWrap:p,maxWrap:m,minTileXWrapped:d,maxTileXWrapped:f,minTileY:c,maxTileY:u}}return a}};function Ya(e){let t=On.fromPoints(e),n=t.width(),r=t.height(),i=Math.max(0,Math.floor(-Math.log(Math.max(n,r))/Math.LN2)),a=2**i;return new rn(i,Math.floor((t.minX+t.maxX)/2*a),Math.floor((t.minY+t.maxY)/2*a))}function Xa(e){let t=e[1].x-e[0].x,n=e[1].y-e[0].y,r=e[2].x-e[0].x;return t*(e[2].y-e[0].y)-n*r<0}function Za(e,t){if(t===`flat`||$a(e))return La;let[n,r,i,a]=e,o=n.x-r.x+i.x-a.x,s=n.y-r.y+i.y-a.y,c=[r.x-i.x,r.y-i.y,a.x-i.x,a.y-i.y],[l,u,d,f]=c,p=Nr(c),m=(o*f-d*s)/p,h=(l*s-o*u)/p,g=[1,1+m,1+m+h,1+h],_=Math.max(...g)/Math.min(...g),v=t===`perspective`?0:Qa(_);return!(_>=1&&_<=512)||v>=1?La:[m,h,v]}function Qa(e){let t=(1-4/e)/(1-4/512);return Math.max(0,t)}function $a(e){let[t,n,r,i]=e;return t.x+r.x===n.x+i.x&&t.y+r.y===n.y+i.y}var eo=class extends Ja{constructor(e,t,n,r){super(e,t,n,r),this._onPlayingHandler=()=>{this.map?.triggerRepaint()},this.roundZoom=!0,this.type=`video`,this.options=t}async load(){this._loaded=!1;let e=this.options;this.urls=[];for(let t of e.urls)this.urls.push((await this.map._requestManager.transformRequest(t,`Source`)).url);try{let e=await yn(this.urls);if(this._loaded=!0,!e)return;this.video=e,this.video.loop=!0,this.video.addEventListener(`playing`,this._onPlayingHandler),this.map&&this.video.play(),this._finishLoading()}catch(e){this.fire(new H(qn(e)))}}pause(){this.video&&this.video.pause()}play(){this.video&&this.video.play()}seek(e){if(this.video){let t=this.video.seekable;et.end(0)?this.fire(new H(new lr(`sources.${this.id}`,null,`Playback for this video can be set only between the ${t.start(0)} and ${t.end(0)}-second mark.`))):this.video.currentTime=e}}getVideo(){return this.video}onAdd(e){this.map||(this.map=e,this.load(),this.video&&(this.video.play(),this.setCoordinates(this.coordinates)))}onRemove(){super.onRemove(),this.video&&(this.video.removeEventListener(`playing`,this._onPlayingHandler),this.video.pause())}prepare(){if(Object.keys(this.tiles).length===0||this.video.readyState<2)return;let e=this.map.painter.context,t=e.gl;this.texture?this.video.paused||(this.texture.bind(t.LINEAR,t.CLAMP_TO_EDGE),t.texSubImage2D(t.TEXTURE_2D,0,0,0,t.RGBA,t.UNSIGNED_BYTE,this.video)):(this.texture=new _(e,this.video,t.RGBA),this.texture.bind(t.LINEAR,t.CLAMP_TO_EDGE));let n=!1;for(let e in this.tiles){let t=this.tiles[e];t.state!==`loaded`&&(t.state=`loaded`,t.texture=this.texture,n=!0)}n&&this.fire(new K(`data`,{sourceDataType:`idle`,sourceId:this.id}))}serialize(){return{type:`video`,urls:this.urls,coordinates:this.coordinates}}hasTransition(){return this.video&&!this.video.paused}},to=class extends Ja{constructor(e,t,n,r){super(e,t,n,r),t.coordinates?(!Array.isArray(t.coordinates)||t.coordinates.length!==4||t.coordinates.some(e=>!Array.isArray(e)||e.length!==2||e.some(e=>typeof e!=`number`)))&&this.fire(new H(new lr(`sources.${e}`,null,`"coordinates" property must be an array of 4 longitude/latitude array pairs`))):this.fire(new H(new lr(`sources.${e}`,null,`missing required property "coordinates"`))),t.animate&&typeof t.animate!=`boolean`&&this.fire(new H(new lr(`sources.${e}`,null,`optional "animate" property must be a boolean value`))),t.canvas?typeof t.canvas!=`string`&&!(t.canvas instanceof HTMLCanvasElement)&&this.fire(new H(new lr(`sources.${e}`,null,`"canvas" must be either a string representing the ID of the canvas element from which to read, or an HTMLCanvasElement instance`))):this.fire(new H(new lr(`sources.${e}`,null,`missing required property "canvas"`))),this.options=t,this.animate=t.animate===void 0||t.animate}async load(){if(this._loaded=!0,this.canvas||=this.options.canvas instanceof HTMLCanvasElement?this.options.canvas:document.getElementById(this.options.canvas),this.width=this.canvas.width,this.height=this.canvas.height,this._hasInvalidDimensions()){this.fire(new H(Error(`Canvas dimensions cannot be less than or equal to zero.`)));return}this.play=function(){this._playing=!0,this.map.triggerRepaint()},this.pause=function(){this._playing&&=(this.prepare(),!1)},this._finishLoading()}getCanvas(){return this.canvas}onAdd(e){this.map=e,this.load(),this.canvas&&this.animate&&this.play()}onRemove(){this._playing=!1,super.onRemove()}prepare(){let e=!1;if(this.canvas.width!==this.width&&(this.width=this.canvas.width,e=!0),this.canvas.height!==this.height&&(this.height=this.canvas.height,e=!0),this._hasInvalidDimensions()||Object.keys(this.tiles).length===0)return;let t=this.map.painter.context,n=t.gl;this.texture?(e||this._playing)&&this.texture.update(this.canvas,{premultiply:!0}):(this.texture=new _(t,this.canvas,n.RGBA,{premultiply:!0}),this.texture.bind(n.LINEAR,n.CLAMP_TO_EDGE));let r=!1;for(let e in this.tiles){let t=this.tiles[e];t.state!==`loaded`&&(t.state=`loaded`,t.texture=this.texture,r=!0)}r&&this.fire(new K(`data`,{sourceDataType:`idle`,sourceId:this.id}))}serialize(){return{type:`canvas`,animate:this.animate,canvas:this.options.canvas,coordinates:this.coordinates}}hasTransition(){return this._playing}_hasInvalidDimensions(){for(let e of[this.canvas.width,this.canvas.height])if(isNaN(e)||e<=0)return!0;return!1}};const no={},ro=(e,t,n,r)=>{let i=new(io(t.type))(e,t,n,r);if(i.id!==e)throw Error(`Expected Source id to be ${e} instead of ${i.id}`);return i},io=e=>{switch(e){case`geojson`:return Ia;case`image`:return Ja;case`raster`:return ba;case`raster-dem`:return xa;case`vector`:return ya;case`video`:return eo;case`canvas`:return to}return no[e]},ao=(e,t)=>{no[e]=t},oo=async(e,t)=>{if(io(e))throw Error(`A source type called "${e}" already exists.`);ao(e,t)};function so(e,t){let n={};if(!t)return n;for(let r of e){let e=r.layerIds.map(e=>t.getLayer(e)).filter(Boolean);if(e.length!==0){r.layers=e,r.stateDependentLayerIds&&(r.stateDependentLayers=r.stateDependentLayerIds.map(t=>e.filter(e=>e.id===t)[0]));for(let t of e)n[t.id]=r}}return n}const co=`RTLPluginLoaded`;var lo=class extends h{constructor(...e){super(...e),this.status=`unavailable`,this.url=null,this.dispatcher=aa()}_syncState(e){return this.status=e,this.dispatcher.broadcast(`SRPS`,{pluginStatus:e,pluginURL:this.url}).catch(e=>{throw this.status=`error`,e})}getRTLTextPluginStatus(){return this.status}clearRTLTextPlugin(){this.status=`unavailable`,this.url=null}async setRTLTextPlugin(e,t=!1){if(this.url)throw Error(`setRTLTextPlugin cannot be called multiple times.`);if(this.url=Rr.resolveURL(e),!this.url)throw Error(`requested url ${e} is invalid`);if(this.status===`unavailable`){if(t)this.status=`deferred`,this._syncState(this.status);else return this._requestImport()}else if(this.status===`requested`)return this._requestImport()}async _requestImport(){await this._syncState(`loading`),this.status=`loaded`,this.fire(new Xe(co))}lazyLoad(){this.status===`unavailable`?this.status=`requested`:this.status===`deferred`&&this._requestImport()}};let uo=null;function fo(){return uo||=new lo,uo}var po=class{constructor(e,t){this.timeAdded=0,this.fadeEndTime=0,this.fadeOpacity=1,this.tileID=e,this.uid=Se(),this.uses=0,this.tileSize=t,this.buckets={},this.expirationTime=null,this.queryPadding=0,this.hasSymbolBuckets=!1,this.hasRTLText=!1,this.dependencies={},this.rttObjects=[],this.rttFingerprint={},this.expiredRequestCount=0,this.state=`loading`,this.featureStateRevision=-1}isRenderable(e){return this.hasData()&&(!this.fadeEndTime||this.fadeOpacity>0)&&(e||!this.holdingForSymbolFade())}setCrossFadeLogic({fadingRole:e,fadingDirection:t,fadingParentID:n,fadeEndTime:r}){this.resetFadeLogic(),this.fadingRole=e,this.fadingDirection=t,this.fadingParentID=n,this.fadeEndTime=r}setSelfFadeLogic(e){this.resetFadeLogic(),this.selfFading=!0,this.fadeEndTime=e}resetFadeLogic(){this.fadingRole=null,this.fadingDirection=null,this.fadingParentID=null,this.selfFading=!1,this.timeAdded=U(),this.fadeEndTime=0,this.fadeOpacity=1}wasRequested(){return this.state===`errored`||this.state===`loaded`||this.state===`reloading`}clearTextures(e){this.demTexture&&e.saveTileTexture(this.demTexture),this.demTexture=null}getRTT(e){return this.rttObjects[e]}acquireRTT(e,t,n){return this.rttObjects[t]=e.acquireRTT(n)}releaseRTT(e){if(this.rttObjects.length!==0){for(let t of this.rttObjects)t&&e.releaseRTT(t);this.rttObjects.length=0}}loadVectorData(e,t,n){if(e?.etagUnmodified===!0){this.state=`loaded`;return}if(this.hasData()&&this.unloadVectorData(),this.state=`loaded`,!e){this.collisionBoxArray=new ot;return}e.featureIndex&&(this.latestFeatureIndex=e.featureIndex,e.rawTileData?(this.latestRawTileData=e.rawTileData,this.latestEncoding=e.encoding,this.latestFeatureIndex.rawTileData=e.rawTileData,this.latestFeatureIndex.encoding=e.encoding):this.latestRawTileData&&(this.latestFeatureIndex.rawTileData=this.latestRawTileData,this.latestFeatureIndex.encoding=this.latestEncoding)),this.collisionBoxArray=e.collisionBoxArray,this.buckets=so(e.buckets,t?.style),this.hasSymbolBuckets=!1;for(let e in this.buckets){let t=this.buckets[e];if(t instanceof v){if(this.hasSymbolBuckets=!0,n)t.justReloaded=!0;else break}}if(this.hasRTLText=!1,this.hasSymbolBuckets)for(let e in this.buckets){let t=this.buckets[e];if(t instanceof v&&t.hasRTLText){this.hasRTLText=!0,fo().lazyLoad();break}}this.queryPadding=0;for(let e in this.buckets){let n=this.buckets[e];this.queryPadding=Math.max(this.queryPadding,t.style.getLayer(e).queryRadius(n))}e.imageAtlas&&(this.imageAtlas=e.imageAtlas),e.glyphAtlasImage&&(this.glyphAtlasImage=e.glyphAtlasImage),this.dashPositions=e.dashPositions}unloadVectorData(){for(let e in this.buckets)this.buckets[e].destroy();this.buckets={},this.imageAtlasTexture&&this.imageAtlasTexture.destroy(),this.glyphAtlasTexture&&this.glyphAtlasTexture.destroy(),this.imageAtlas=null,this.dashPositions=null,this.latestFeatureIndex=null,this.state=`unloaded`}getBucket(e){return this.buckets[e.id]}upload(e){for(let t in this.buckets){let n=this.buckets[t];n.uploadPending()&&n.upload(e)}let t=e.gl;this.imageAtlas&&!this.imageAtlas.uploaded&&(this.imageAtlasTexture=new _(e,this.imageAtlas.image,t.RGBA),this.imageAtlas.uploaded=!0),this.glyphAtlasImage&&=(this.glyphAtlasTexture=new _(e,this.glyphAtlasImage,t.ALPHA),null)}prepare(e){this.imageAtlas&&this.imageAtlas.patchUpdatedImages(e,this.imageAtlasTexture)}queryRenderedFeatures(e,t,n,r,i,a,o,s,c,l,u){return this.latestFeatureIndex?.rawTileData?this.latestFeatureIndex.query({queryGeometry:r,cameraQueryGeometry:i,scale:a,tileSize:this.tileSize,pixelPosMatrix:l,transform:s,params:o,queryPadding:this.queryPadding*c,getElevation:u},e,t,n):{}}querySourceFeatures(e,t){let n=this.latestFeatureIndex;if(!n?.rawTileData)return;let r=n.loadVTLayers(),i=t?.sourceLayer?t.sourceLayer:``,a=r._geojsonTileLayer||r[i];if(!a)return;let o=Rn(t?.filter,`querySourceFeatures[${i}].filter`,t?.globalState),{z:s,x:c,y:l}=this.tileID.canonical,u={z:s,x:c,y:l};for(let t=0;te)n=!1;else if(!t)n=!0;else if(this.expirationTime({zoom:0,x:0,y:0,wrap:e,fullyVisible:!1}),b=[],x=[];if(e.renderWorldCopies&&s.allowWorldCopies())for(let e=1;e<=3;e++)b.push(y(-e)),b.push(y(e));for(b.push(y(0));b.length>0;){let p=b.pop(),g=p.x,y=p.y,S=p.fullyVisible,C={x:g,y,z:p.zoom},w=s.getTileBoundingVolume(C,p.wrap,o,t);if(!S){let e=go(n,w,r);if(e===0)continue;S=e===2}let T=s.distanceToTile2d(i.x,i.y,C,w),E=l;c&&(E=(t.calculateTileZoom||yo)(e.zoom+Ee(e.tileSize/t.tileSize),T,_,v,e.fov)),E=(t.roundZoom?Math.round:Math.floor)(E),E=Math.max(0,E);let D=Math.min(E,d);if(p.wrap=s.getWrap(a,C,p.wrap),p.zoom>=D){if(p.zoom>1),r=p.zoom+1;b.push({zoom:r,x:t,y:n,wrap:p.wrap,fullyVisible:S})}}return x.sort((e,t)=>e.distanceSq-t.distanceSq).map(e=>e.tileID)}function Co(e){return e===`raster`||e===`image`||e===`video`}function wo(e,t,n,r,i,a,o){let s=U(),c=or(t);for(let l of t){let t=e.getTileById(l.key);(t.fadingDirection===0||t.fadeOpacity===0)&&t.resetFadeLogic(),!To(e,t,n,s,r,i,o)&&(Eo(e,t,n,s,a,o)||Oo(t,c,s,o)||t.resetFadeLogic())}}function To(e,t,n,r,i,a,o){if(!t.hasData())return!1;let{tileID:s,fadingRole:c,fadingDirection:l,fadingParentID:u}=t;if(c===0&&l===1&&u)return n[u.key]=u,!0;let d=Math.max(s.overscaledZ-i,a);for(let i=s.overscaledZ-1;i>=d;i--){let a=s.scaledTo(i),c=e.getLoadedTile(a);if(c)return t.setCrossFadeLogic({fadingRole:0,fadingDirection:1,fadingParentID:c.tileID,fadeEndTime:r+o}),c.setCrossFadeLogic({fadingRole:1,fadingDirection:0,fadeEndTime:r+o}),n[a.key]=a,!0}return!1}function Eo(e,t,n,r,i,a){if(!t.hasData())return!1;let o=t.tileID.children(i),s=Do(e,t,o,n,r,i,a);if(s)return!0;for(let c of o)Do(e,t,c.children(i),n,r,i,a)&&(s=!0);return s}function Do(e,t,n,r,i,a,o){if(n[0].overscaledZ>=a)return!1;let s=!1;for(let a of n){let n=e.getLoadedTile(a);if(!n)continue;let{fadingRole:c,fadingDirection:l,fadingParentID:u}=n;(c!==0||l!==0||!u)&&(n.setCrossFadeLogic({fadingRole:0,fadingDirection:0,fadingParentID:t.tileID,fadeEndTime:i+o}),t.setCrossFadeLogic({fadingRole:1,fadingDirection:1,fadeEndTime:i+o})),r[a.key]=a,s=!0}return s}function Oo(e,t,n,r){let i=e.tileID;if(e.selfFading)return!0;if(e.hasData())return!1;if(t.has(i)){let t=n+r;return e.setSelfFadeLogic(t),!0}return!1}function ko(e,t){if(t<=0)return!1;let n=U();for(let t of e.getAllTiles())if(t.fadeEndTime>=n)return!0;return!1}function Ao(e,t){let n=t.getRenderableIds();for(let r of n){if(!e.neighboringTiles?.[r])continue;let n=t.getTileById(r);e.neighboringTiles[r].backfilled||jo(e,n),!n.neighboringTiles?.[e.tileID.key]?.backfilled&&jo(n,e)}}function jo(e,t){e.needsHillshadePrepare=!0,e.needsTerrainPrepare=!0,e.needsColorReliefPrepare=!0;let n=t.tileID.canonical.x-e.tileID.canonical.x,r=t.tileID.canonical.y-e.tileID.canonical.y,i=2**e.tileID.canonical.z,a=t.tileID.key;(n!==0||r!==0)&&(Math.abs(r)>1||(Math.abs(n)>1&&(Math.abs(n+i)===1?n+=i:Math.abs(n-i)===1&&(n-=i)),!(!t.dem||!e.dem)&&(e.dem.backfillBorder(t.dem,n,r),e.neighboringTiles?.[a]&&(e.neighboringTiles[a].backfilled=!0))))}var Mo=class{constructor(){this._tiles={}}handleWrapJump(e){let t={};for(let n in this._tiles){let r=this._tiles[n];r.tileID=r.tileID.unwrapTo(r.tileID.wrap+e),t[r.tileID.key]=r}this._tiles=t}setFeatureState(e,t,n){for(let r in this._tiles)this._tiles[r].setFeatureState(e,t,n)}getAllTiles(){return Object.values(this._tiles)}getAllIds(e=!1){return e?Object.values(this._tiles).map(e=>e.tileID).sort(xr).map(e=>e.key):Object.keys(this._tiles)}getTileById(e){return this._tiles[e]}setTile(e,t){this._tiles[e]=t}deleteTileById(e){delete this._tiles[e]}getLoadedTile(e){let t=this.getTileById(e.key);return t?.hasData()?t:null}isIdRenderable(e,t=!1){return this.getTileById(e)?.isRenderable(t)}getRenderableIds(e=0,t){let n=[];for(let e of this.getAllIds())this.isIdRenderable(e,t)&&n.push(this.getTileById(e));return t?n.sort((t,n)=>{let r=t.tileID,i=n.tileID,a=new l(r.canonical.x,r.canonical.y)._rotate(-e),o=new l(i.canonical.x,i.canonical.y)._rotate(-e);return r.overscaledZ-i.overscaledZ||o.y-a.y||o.x-a.x}).map(e=>e.tileID.key):n.map(e=>e.tileID).sort(xr).map(e=>e.key)}},No=class e extends h{static{this.maxUnderzooming=10}static{this.maxOverzooming=3}constructor(e,t,n){super(),this.id=e,this.dispatcher=n,this.on(`data`,e=>{this._dataHandler(e)}),this.on(`dataloading`,()=>{this._sourceErrored=!1}),this.on(`error`,()=>{this._sourceErrored=this._source.loaded()}),this._source=ro(e,t,n,this),this._inViewTiles=new Mo,this._outOfViewCache=new Jn(0,e=>this._unloadTile(e)),this._timers={},this._maxTileCacheSize=null,this._maxTileCacheZoomLevels=null,this._rasterFadeDuration=0,this._maxFadingAncestorLevels=5,this._state=new ho,this._didEmitContent=!1,this._updated=!1}onAdd(e){this.map=e,this._maxTileCacheSize=e?e._maxTileCacheSize:null,this._maxTileCacheZoomLevels=e?e._maxTileCacheZoomLevels:null,this._source?.onAdd&&this._source.onAdd(e)}onRemove(e){for(let e of this._inViewTiles.getAllTiles())e.unloadVectorData();this.clearTiles(),this._source?.onRemove&&this._source.onRemove(e),this._inViewTiles=new Mo}loaded(){if(this._sourceErrored)return!0;if(!this._sourceLoaded||!this._source.loaded())return!1;if((this.used!==void 0||this.usedForTerrain!==void 0)&&!this.used&&!this.usedForTerrain)return!0;if(!this._updated)return!1;for(let e of this._inViewTiles.getAllTiles())if(e.state!==`loaded`&&e.state!==`errored`)return!1;return!0}getSource(){return this._source}getState(){return this._state}pause(){this._paused=!0}resume(){if(!this._paused)return;let e=this._shouldReloadOnResume;this._paused=!1,this._shouldReloadOnResume=!1,e&&this.reload(),this.transform&&this.update(this.transform,this.terrain)}async _loadTile(e,t,n,r){try{let i=await this._source.loadTile(e);this._tileLoaded(e,t,n,r,i)}catch(t){e.state=`errored`,t.status===404?this.update(this.transform,this.terrain):this._source.fire(new H(qn(t),{tile:e}))}}_unloadTile(e){this._source.unloadTile&&this._source.unloadTile(e)}_abortTile(e){this._source.abortTile&&this._source.abortTile(e),this._source.fire(new K(`dataabort`,{tile:e,coord:e.tileID}))}serialize(){return this._source.serialize()}prepare(e){this._source.prepare&&this._source.prepare(),this._state.coalesceChanges(this._inViewTiles,this.map?this.map.painter:null);for(let t of this._inViewTiles.getAllTiles())t.upload(e),t.prepare(this.map.style.imageManager)}getIds(){return this._inViewTiles.getAllIds(!0)}getRenderableIds(e){return this._inViewTiles.getRenderableIds(this.transform?.bearingInRadians,e)}hasRenderableParent(e){let t=e.overscaledZ-1;if(t>=this._source.minzoom){let n=this.getLoadedTile(e.scaledTo(t));if(n)return this._inViewTiles.isIdRenderable(n.tileID.key)}return!1}reload(e,t=void 0){if(this._paused){this._shouldReloadOnResume=!0;return}this._outOfViewCache.reset();for(let n of this._inViewTiles.getAllIds()){let r=this._inViewTiles.getTileById(n);t&&!this._source.shouldReloadTile(r,t)||(e?this._reloadTile(n,r.state===`errored`?`loading`:`expired`):r.state!==`errored`&&this._reloadTile(n,`reloading`))}}async _reloadTile(e,t){let n=this._inViewTiles.getTileById(e);if(!n)return;let r=n.hasData();n.state!==`loading`&&(n.state=t),await this._loadTile(n,e,t,r)}_tileLoaded(e,t,n,r,i){r||(e.timeAdded=U(),e.selfFading&&(e.fadeEndTime=e.timeAdded+this._rasterFadeDuration)),n===`expired`&&(e.refreshedUponExpiration=!0),this._setTileReloadTimer(t,e),!i?.unmodified&&(this.getSource().type===`raster-dem`&&e.dem&&Ao(e,this._inViewTiles),e.featureStateRevision=-1,this._state.initializeTileState(e,this.map?this.map.painter:null),e.aborted||this._source.fire(new K(`data`,{tile:e,coord:e.tileID})))}getTile(e){return this.getTileByID(e.key)}getTileByID(e){return this._inViewTiles.getTileById(e)}_retainLoadedChildren(t,n){let r=this._getLoadedDescendents(n),i=new Set;for(let a of n){let n=r[a.key];if(!n?.length){i.add(a);continue}let o=a.overscaledZ+e.maxOverzooming,s=n.filter(e=>e.tileID.overscaledZ<=o);if(!s.length){i.add(a);continue}let c=Math.min(...s.map(e=>e.tileID.overscaledZ)),l=s.filter(e=>e.tileID.overscaledZ===c).map(e=>e.tileID);for(let e of l)t[e.key]=e;this._areDescendentsComplete(l,c,a.overscaledZ)||i.add(a)}return i}_getLoadedDescendents(e){let t={};for(let n of this._inViewTiles.getAllTiles().filter(e=>e.hasData()))for(let r of e)n.tileID.isChildOf(r)&&(t[r.key]||=[],t[r.key].push(n));return t}_areDescendentsComplete(e,t,n){return e.length===1&&e[0].isOverscaled()?e[0].overscaledZ===t:4**(t-n)===e.length}getLoadedTile(e){return this._inViewTiles.getLoadedTile(e)}updateCacheSize(e){let t=(Math.ceil(e.width/this._source.tileSize)+1)*(Math.ceil(e.height/this._source.tileSize)+1),n=this._maxTileCacheZoomLevels===null?k.MAX_TILE_CACHE_ZOOM_LEVELS:this._maxTileCacheZoomLevels,r=Math.floor(t*n),i=typeof this._maxTileCacheSize==`number`?Math.min(this._maxTileCacheSize,r):r;this._outOfViewCache.setMaxSize(i)}handleWrapJump(e){let t=(e-(this._prevLng===void 0?e:this._prevLng))/360,n=Math.round(t);this._prevLng=e,n&&(this._inViewTiles.handleWrapJump(n),this._resetTileReloadTimers())}update(e,t){if(!this._sourceLoaded||this._paused)return;this.transform=e,this.terrain=t,this.updateCacheSize(e),this.handleWrapJump(this.transform.center.lng);let n;!this.used&&!this.usedForTerrain?n=[]:this._source.tileID?n=e.getVisibleUnwrappedCoordinates(this._source.tileID).map(e=>new $t(e.canonical.z,e.wrap,e.canonical.z,e.canonical.x,e.canonical.y)):(n=So(e,{tileSize:this.usedForTerrain?this.tileSize:this._source.tileSize,minzoom:this._source.minzoom,maxzoom:this._source.type===`vector`&&this.map._zoomLevelsToOverscale!==void 0?Math.max(this._source.maxzoom,e.maxZoom-this.map._zoomLevelsToOverscale):this._source.maxzoom,roundZoom:!this.usedForTerrain&&this._source.roundZoom,reparseOverscaled:this._source.reparseOverscaled,terrain:t,calculateTileZoom:this._source.calculateTileZoom}),this._source.hasTile&&(n=n.filter(e=>this._source.hasTile(e)))),this.usedForTerrain&&(n=this._addTerrainIdealTiles(n));let r=n.length===0&&!this._updated&&this._didEmitContent;this._updated=!0,r&&this.fire(new K(`data`,{sourceDataType:`idle`,sourceId:this.id}));let i=bo(e,this._source),a=this._updateRetainedTiles(n,i),o=Co(this._source.type);o&&this._rasterFadeDuration>0&&!t&&wo(this._inViewTiles,n,a,this._maxFadingAncestorLevels,this._source.minzoom,this._source.maxzoom,this._rasterFadeDuration),o?this._cleanUpRasterTiles(a):this._cleanUpVectorTiles(a)}_cleanUpRasterTiles(e){for(let t of this._inViewTiles.getAllIds())e[t]||this._removeTile(t)}_cleanUpVectorTiles(e){for(let t of this._inViewTiles.getAllIds()){let n=this._inViewTiles.getTileById(t);if(e[t]){n.clearSymbolFadeHold();continue}if(!n.hasSymbolBuckets){this._removeTile(t);continue}n.holdingForSymbolFade()?n.symbolFadeFinished()&&this._removeTile(t):n.setSymbolHoldDuration(this.map._fadeDuration)}}_addTerrainIdealTiles(e){let t=[];for(let n of e)if(n.canonical.z>this._source.minzoom){let e=n.scaledTo(n.canonical.z-1);t.push(e);let r=n.scaledTo(Math.max(this._source.minzoom,Math.min(n.canonical.z,5)));t.push(r)}return e.concat(t)}releaseSymbolFadeTiles(){for(let e of this._inViewTiles.getAllIds())this._inViewTiles.getTileById(e).holdingForSymbolFade()&&this._removeTile(e)}_updateRetainedTiles(t,n){let r=new Set;for(let e of t)this._addTile(e).hasData()||r.add(e);let i=t.reduce((e,t)=>(e[t.key]=t,e),{}),a=this._retainLoadedChildren(i,r),o={},s=Math.max(n-e.maxUnderzooming,this._source.minzoom);for(let e of a){let t=this._inViewTiles.getTileById(e.key),n=t?.wasRequested();for(let r=e.overscaledZ-1;r>=s;--r){let a=e.scaledTo(r);if(o[a.key])break;if(o[a.key]=!0,t=this.getTile(a),!t&&n&&(t=this._addTile(a)),t){let e=t.hasData();if((e||!this.map?.cancelPendingTileRequestsWhileZooming||n)&&(i[a.key]=a),n=t.wasRequested(),e)break}}}return i}_addTile(e){let t=this._inViewTiles.getTileById(e.key);if(t)return t;t=this._outOfViewCache.getAndRemove(e),t&&(t.resetFadeLogic(),this._setTileReloadTimer(e.key,t),t.tileID=e,this._state.initializeTileState(t,this.map?this.map.painter:null));let n=t;return t||(t=new po(e,this._source.tileSize*e.overscaleFactor()),this._loadTile(t,e.key,t.state,!1)),t.uses++,this._inViewTiles.setTile(e.key,t),n||this._source.fire(new K(`dataloading`,{tile:t,coord:t.tileID})),t}_setTileReloadTimer(e,t){this._clearTileReloadTimer(e);let n=t.getExpiryTimeout();if(n){let t=()=>{this._reloadTile(e,`expired`),delete this._timers[e]};this._timers[e]=setTimeout(t,n)}}_clearTileReloadTimer(e){let t=this._timers[e];t&&(clearTimeout(t),delete this._timers[e])}_resetTileReloadTimers(){for(let e in this._timers)clearTimeout(this._timers[e]),delete this._timers[e];for(let e of this._inViewTiles.getAllIds()){let t=this._inViewTiles.getTileById(e);this._setTileReloadTimer(e,t)}}refreshTiles(e){for(let t of this._inViewTiles.getAllIds()){let n=this._inViewTiles.getTileById(t);!this._inViewTiles.isIdRenderable(t)&&n.state!=`errored`||e.some(e=>e.equals(n.tileID.canonical))&&this._reloadTile(t,`expired`)}}_removeTile(e){let t=this._inViewTiles.getTileById(e);t&&(t.uses--,this._inViewTiles.deleteTileById(e),this._clearTileReloadTimer(e),!(t.uses>0)&&(t.hasData()&&t.state!==`reloading`?this._outOfViewCache.add(t.tileID,t,t.getExpiryTimeout()):(t.aborted=!0,this._abortTile(t),this._unloadTile(t))))}_dataHandler(e){if(e.dataType===`source`){if(e.sourceDataType===`metadata`){this._sourceLoaded=!0;return}e.sourceDataType!==`content`||!this._sourceLoaded||this._paused||(this.reload(e.sourceDataChanged,e.shouldReloadTileOptions),this.transform&&this.update(this.transform,this.terrain),this._didEmitContent=!0)}}clearTiles(){this._shouldReloadOnResume=!1,this._paused=!1;for(let e of this._inViewTiles.getAllIds())this._removeTile(e);this._outOfViewCache.reset()}tilesIn(e,t,n){let r=[],i=this.transform;if(!i)return r;let a=i.getCoveringTilesDetailsProvider().allowWorldCopies(),o=n?i.getCameraQueryGeometry(e):e,s=e=>i.screenPointToMercatorCoordinate(e,this.terrain),c=this.transformBbox(e,s,!a),l=this.transformBbox(o,s,!a),u=this.getIds(),d=On.fromPoints(l);for(let e of u){let n=this._inViewTiles.getTileById(e);if(n.holdingForSymbolFade())continue;let o=a?[n.tileID]:[n.tileID.unwrapTo(-1),n.tileID.unwrapTo(0)],s=2**(i.zoom-n.tileID.overscaledZ),u=t*n.queryPadding*N/n.tileSize/s;for(let e of o){let t=d.map(t=>e.getTilePoint(new B(t.x,t.y)));if(t.expandBy(u),t.intersects(Bn)){let t=c.map(t=>e.getTilePoint(t)),i=l.map(t=>e.getTilePoint(t));r.push({tile:n,tileID:a?e:e.unwrapTo(0),queryGeometry:t,cameraQueryGeometry:i,scale:s})}}}return r}transformBbox(e,t,n){let r=e.map(t);if(n){let n=On.fromPoints(e);n.shrinkBy(Math.min(n.width(),n.height())*.001);let i=n.map(t);On.fromPoints(r).covers(i)||(r=r.map(e=>e.x>.5?new B(e.x-1,e.y,e.z):e))}return r}getVisibleCoordinates(e){let t=this.getRenderableIds(e).map(e=>this._inViewTiles.getTileById(e).tileID);return this.transform&&this.transform.populateCache(t),t}hasTransition(){return this._source.hasTransition()?!0:Co(this._source.type)&&ko(this._inViewTiles,this._rasterFadeDuration)}setRasterFadeDuration(e){this._rasterFadeDuration=e}setFeatureState(e,t,n){e||=Yt,this._state.updateState(e,t,n)}removeFeatureState(e,t,n){e||=Yt,this._state.removeFeatureState(e,t,n)}getFeatureState(e,t){return e||=Yt,this._state.getState(e,t)}setDependencies(e,t,n){let r=this._inViewTiles.getTileById(e);r&&r.setDependencies(t,n)}reloadTilesForDependencies(e,t){for(let n of this._inViewTiles.getAllIds())this._inViewTiles.getTileById(n).hasDependency(e,t)&&this._reloadTile(n,`reloading`);this._outOfViewCache.filter(n=>!n.hasDependency(e,t))}areTilesLoaded(){for(let e of this._inViewTiles.getAllTiles())if(e.state!==`loaded`&&e.state!==`errored`)return!1;return!0}},Po=class{constructor(e,t){this.reset(e,t)}reset(e,t){this.points=e||[],this._distances=[0];for(let e=1;e0?(r-a)/o:0;return this.points[i].mult(1-s).add(this.points[t].mult(s))}};function Fo(e,t,n,r,i){return i?e?e(t,n)+r:r===0?void 0:r:r}function Io(e,t){let n=!0;return e===`always`||(e===`never`||t===`never`)&&(n=!1),n}var Lo=class{constructor(e,t,n){let r=this.boxCells=[],i=this.circleCells=[];this.xCellCount=Math.ceil(e/n),this.yCellCount=Math.ceil(t/n);for(let e=0;ethis.width||r<0||t>this.height)return[];let s=[];if(e<=0&&t<=0&&this.width<=n&&this.height<=r){if(i)return[{key:null,x1:e,y1:t,x2:n,y2:r}];for(let e=0;e0}hitTestCircle(e,t,n,r,i){let a=e-n,o=e+n,s=t-n,c=t+n;if(o<0||a>this.width||c<0||s>this.height)return!1;let l=[],u={hitTest:!0,overlapMode:r,circle:{x:e,y:t,radius:n},seenUids:{box:{},circle:{}}};return this._forEachCell(a,s,o,c,this._queryCellCircle,l,u,i),l.length>0}_queryCell(e,t,n,r,i,a,o,s){let{seenUids:c,hitTest:l,overlapMode:u}=o,d=this.boxCells[i],f=1e-6;if(d!==null){let i=this.bboxes;for(let o of d)if(!c.box[o]){c.box[o]=!0;let d=o*4,p=this.boxKeys[o];if(e<=i[d+2]+f&&t<=i[d+3]+f&&n>=i[d+0]-f&&r>=i[d+1]-f&&(!s||s(p))&&(!l||!Io(u,p.overlapMode))&&(a.push({key:p,x1:i[d],y1:i[d+1],x2:i[d+2],y2:i[d+3]}),l))return!0}}let p=this.circleCells[i];if(p!==null){let i=this.circles;for(let o of p)if(!c.circle[o]){c.circle[o]=!0;let d=o*3,f=this.circleKeys[o];if(this._circleAndRectCollide(i[d],i[d+1],i[d+2],e,t,n,r)&&(!s||s(f))&&(!l||!Io(u,f.overlapMode))){let e=i[d],t=i[d+1],n=i[d+2];if(a.push({key:f,x1:e-n,y1:t-n,x2:e+n,y2:t+n}),l)return!0}}}return!1}_queryCellCircle(e,t,n,r,i,a,o,s){let{circle:c,seenUids:l,overlapMode:u}=o,d=this.boxCells[i];if(d!==null){let e=this.bboxes;for(let t of d)if(!l.box[t]){l.box[t]=!0;let n=t*4,r=this.boxKeys[t];if(this._circleAndRectCollide(c.x,c.y,c.radius,e[n+0],e[n+1],e[n+2],e[n+3])&&(!s||s(r))&&!Io(u,r.overlapMode))return a.push(!0),!0}}let f=this.circleCells[i];if(f!==null){let e=this.circles;for(let t of f)if(!l.circle[t]){l.circle[t]=!0;let n=t*3,r=this.circleKeys[t];if(this._circlesCollide(e[n],e[n+1],e[n+2],c.x,c.y,c.radius)&&(!s||s(r))&&!Io(u,r.overlapMode))return a.push(!0),!0}}}_forEachCell(e,t,n,r,i,a,o,s){let c=this._convertToXCellCoord(e),l=this._convertToYCellCoord(t),u=this._convertToXCellCoord(n),d=this._convertToYCellCoord(r);for(let f=c;f<=u;f++)for(let c=l;c<=d;c++){let l=this.xCellCount*c+f;if(i.call(this,e,t,n,r,l,a,o,s))return}}_convertToXCellCoord(e){return Math.max(0,Math.min(this.xCellCount-1,Math.floor(e*this.xScale)))}_convertToYCellCoord(e){return Math.max(0,Math.min(this.yCellCount-1,Math.floor(e*this.yScale)))}_circlesCollide(e,t,n,r,i,a){let o=r-e,s=i-t,c=n+a;return c*c>o*o+s*s}_circleAndRectCollide(e,t,n,r,i,a,o){let s=(a-r)/2,c=Math.abs(e-(r+s));if(c>s+n)return!1;let l=(o-i)/2,u=Math.abs(t-(i+l));if(u>l+n)return!1;if(c<=s||u<=l)return!0;let d=c-s,f=u-l;return d*d+f*f<=n*n}};function Ro(e,t){let n=1/(t[0]*t[0]+t[1]*t[1]+t[2]*t[2]),r=1/(t[8]*t[8]+t[9]*t[9]+t[10]*t[10]),i=t[0]*n,a=t[4]*n,o=t[8]*r,s=t[1]*n,c=t[5]*n,l=t[9]*r,u=t[2]*n,d=t[6]*n,f=t[10]*r;e[0]=i,e[1]=a,e[2]=o,e[4]=s,e[5]=c,e[6]=l,e[8]=u,e[9]=d,e[10]=f;let p=t[12],m=t[13],h=t[14];return e[12]=-i*p-s*m-u*h,e[13]=-a*p-c*m-d*h,e[14]=-o*p-l*m-f*h,e[3]=0,e[7]=0,e[11]=0,e[15]=1,e}function zo(e,t){return e[0]=1/t[0],e[1]=0,e[2]=0,e[3]=0,e[4]=0,e[5]=1/t[5],e[6]=0,e[7]=0,e[8]=0,e[9]=0,e[10]=0,e[11]=1/t[14],e[12]=0,e[13]=0,e[14]=-1,e[15]=t[10]/t[14],e}function Bo(e,t){let n=1/(t[0]*t[5]-t[1]*t[4]);return e[0]=t[5]*n,e[1]=-t[1]*n,e[2]=0,e[3]=0,e[4]=-t[4]*n,e[5]=t[0]*n,e[6]=0,e[7]=0,e[8]=0,e[9]=0,e[10]=1/t[10],e[11]=0,e[12]=0,e[13]=0,e[14]=0,e[15]=1/t[15],e}const Vo=vr();function Ho(e,t,n){let r=vr();if(!e){let{vecSouth:e,vecEast:n}=Wo(t),i=jr();i[0]=n[0],i[1]=n[1],i[2]=e[0],i[3]=e[1],Mr(i,i),r[0]=i[0],r[1]=i[1],r[4]=i[2],r[5]=i[3]}return ke(r,r,[1/n,1/n,1]),r}function Uo(e,t,n,r){if(e){let e=vr();if(!t){let{vecSouth:t,vecEast:r}=Wo(n);e[0]=r[0],e[1]=r[1],e[4]=t[0],e[5]=t[1]}return ke(e,e,[r,r,1]),e}return n.pixelsToClipSpaceMatrix}function Wo(e){let t=Math.cos(e.rollInRadians),n=Math.sin(e.rollInRadians),r=Math.cos(e.pitchInRadians),i=Math.cos(e.bearingInRadians),a=Math.sin(e.bearingInRadians),o=at();o[0]=-i*r*n-a*t,o[1]=-a*r*n+i*t;let s=ft(o);s<1e-9?Hn(o):te(o,o,1/s);let c=at();c[0]=i*r*t-a*n,c[1]=a*r*t+i*n;let l=ft(c);return l<1e-9?Hn(c):te(c,c,1/l),{vecEast:c,vecSouth:o}}function Go(e,t,n){return Fo(e.getElevation,t,n,e.heightOffset??0,e.heightAnchorGround??!0)}function Ko(e,t,n,r){let i;r==null?(i=[e,t,0,1],ls(i,i,n)):(i=[e,t,r,1],Gt(i,i,n));let a=i[3];return{point:new l(i[0]/a,i[1]/a),signedDistanceFromCamera:a,isOccluded:!1}}function qo(e,t){return .5+e/t*.5}function Jo(e,t){return e.x>=-t[0]&&e.x<=t[0]&&e.y>=-t[1]&&e.y<=t[1]}function Yo(e,t,n,r,a,o,s,c,u,d,f,p,m){let h=n?e.textSizeData:e.iconSizeData,g=fn(h,t.transform.zoom),_=[256/t.width*2+1,256/t.height*2+1],v=n?e.text.dynamicLayoutVertexArray:e.icon.dynamicLayoutVertexArray;v.clear();let y=e.lineVertexArray,b=n?e.text.placedSymbolArray:e.icon.placedSymbolArray,x=t.transform.width/t.transform.height,S=!1;for(let n=0;nMath.abs(n.x-t.x)*r?{useVertical:!0}:(e===2?t.yn.x)?{needsFlipping:!0}:null}function Qo(e){let{projectionContext:t,pitchedLabelPlaneMatrixInverse:n,symbol:r,fontSize:i,flip:a,keepUpright:o,glyphOffsetArray:s,dynamicLayoutVertexArray:c,aspectRatio:u,rotateToLine:d}=e,f=i/24,p=r.lineOffsetX*f,m=r.lineOffsetY*f,h;if(r.numGlyphs>1){let e=r.glyphStartIndex+r.numGlyphs,i=r.lineStartIndex,c=r.lineStartIndex+r.lineLength,l=Xo(f,s,p,m,a,r,d,t);if(!l)return{notEnoughRoom:!0};let g=ns(l.first.point.x,l.first.point.y,t,n),_=ns(l.last.point.x,l.last.point.y,t,n);if(o&&!a){let e=Zo(r.writingMode,g,_,u);if(e)return e}h=[l.first];for(let n=r.glyphStartIndex+1;n0?o.point:$o(t.tileAnchorPoint,a,e,1,t),c=ns(e.x,e.y,t,n),d=ns(s.x,s.y,t,n),f=Zo(r.writingMode,c,d,u);if(f)return f}let e=os(f*s.getoffsetX(r.glyphStartIndex),p,m,a,r.segment,r.lineStartIndex,r.lineStartIndex+r.lineLength,t,d);if(!e||t.projectionCache.anyProjectionOccluded)return{notEnoughRoom:!0};h=[e]}for(let e of h)Me(c,e.point,e.angle);return{}}function $o(e,t,n,r,i){let a=e.add(e.sub(t)._unit()),o=ts(a.x,a.y,i).point,s=n.sub(o);return n.add(s._mult(r/s.mag()))}function es(e,t,n){let r=t.projectionCache;if(r.projections[e])return r.projections[e];let i=new l(t.lineVertexArray.getx(e),t.lineVertexArray.gety(e)),a=ts(i.x,i.y,t);if(a.signedDistanceFromCamera>0)return r.projections[e]=a.point,r.anyProjectionOccluded||=a.isOccluded,a.point;let o=e-n.direction,s=n.distanceFromAnchor===0?t.tileAnchorPoint:new l(t.lineVertexArray.getx(o),t.lineVertexArray.gety(o)),c=n.absOffsetX-n.distanceFromAnchor+1;return $o(s,i,n.previousVertex,c,t)}function ts(e,t,n){let r=e+n.translation[0],i=t+n.translation[1],a;return n.pitchWithMap?(a=Ko(r,i,n.pitchedLabelPlaneMatrix,Go(n,r,i)),a.isOccluded=!1):(a=n.transform.projectTileCoordinates(r,i,n.unwrappedTileID,Go(n,r,i)),a.point.x=(a.point.x*.5+.5)*n.width,a.point.y=(-a.point.y*.5+.5)*n.height),a}function ns(e,t,n,r){if(n.pitchWithMap){let i=[e,t,0,1];Gt(i,i,r);let a=i[0]/i[3],o=i[1]/i[3];return n.transform.projectTileCoordinates(a,o,n.unwrappedTileID,Go(n,a,o)).point}return{x:e/n.width*2-1,y:1-t/n.height*2}}function rs(e,t,n){return n.transform.projectTileCoordinates(e,t,n.unwrappedTileID,Go(n,e,t))}function is(e,t,n){return e._unit()._perp()._mult(t*n)}function as(e,t,n,r,i,a,o,s,c){if(s.projectionCache.offsets[e])return s.projectionCache.offsets[e];let l=n.add(t);if(e+c.direction=i)return s.projectionCache.offsets[e]=l,l;let u=es(e+c.direction,s,c),d=is(u.sub(n),o,c.direction),f=n.add(d),p=u.add(d);return s.projectionCache.offsets[e]=Xn(a,l,f,p)||l,s.projectionCache.offsets[e]}function os(e,t,n,r,i,a,o,s,c){let l=r?e-t:e+t,u=l>0?1:-1,d=0;r&&(u*=-1,d=Math.PI),u<0&&(d+=Math.PI);let f=u>0?a+i:a+i+1,p;s.projectionCache.cachedAnchorPoint?p=s.projectionCache.cachedAnchorPoint:(p=ts(s.tileAnchorPoint.x,s.tileAnchorPoint.y,s).point,s.projectionCache.cachedAnchorPoint=p);let m=p,h=p,g,_,v=0,y=0,b=Math.abs(l),x=[],S;for(;v+y<=b;){if(f+=u,f=o)return null;v+=y,h=m,_=g;let e={absOffsetX:b,direction:u,distanceFromAnchor:v,previousVertex:h};if(m=es(f,s,e),n===0)x.push(h),S=m.sub(h);else{let t,r=m.sub(h);t=r.mag()===0?is(es(f+u,s,e).sub(m),n,u):is(r,n,u),_||=h.add(t),g=as(f,t,m,a,o,_,n,s,e),x.push(_),S=g.sub(_)}y=S.mag()}let C=(b-v)/y,w=S._mult(C)._add(_||h),T=d+Math.atan2(m.y-h.y,m.x-h.x);return x.push(w),{point:w,angle:c?T:0,path:x}}const ss=new Float32Array([-1/0,-1/0,0,-1/0,-1/0,0,-1/0,-1/0,0,-1/0,-1/0,0]);function cs(e,t){for(let n=0;n{let r=Ko(e.x,e.y,n,Go(t,e.x,e.y)),i=t.transform.projectTileCoordinates(r.point.x,r.point.y,t.unwrappedTileID,Go(t,r.point.x,r.point.y));return i.point.x=(i.point.x*.5+.5)*t.width,i.point.y=(-i.point.y*.5+.5)*t.height,i})}function ds(e){let t=0,n=0,r=0,i=0;for(let a=0;an&&(n=i,t=r));return e.slice(t,t+n)}var fs=class{constructor(e,t=new Lo(e.width+200,e.height+200,25),n=new Lo(e.width+200,e.height+200,25)){this.transform=e,this.grid=t,this.ignoredGrid=n,this.pitchFactor=Math.cos(e.pitch*Math.PI/180)*e.cameraToCenterDistance,this.screenRightBoundary=e.width+100,this.screenBottomBoundary=e.height+100,this.gridRightBoundary=e.width+200,this.gridBottomBoundary=e.height+200,this.perspectiveRatioCutoff=.6}placeCollisionBox(e,t,n,r,i,a,o,s,c,l,u,d,f=0,p=!0){let m=e.anchorPointX+s[0],h=e.anchorPointY+s[1],g=this.projectAndGetPerspectiveRatio(m,h,i,Fo(l,m,h,f,p),d),_=n*g.perspectiveRatio,v;if(!a&&!o){let t=g.x+(u?u.x*_:0),n=g.y+(u?u.y*_:0);v={allPointsOccluded:!1,box:[t+e.x1*_,n+e.y1*_,t+e.x2*_,n+e.y2*_]}}else v=this._projectCollisionBox(e,_,r,i,a,o,s,g,l,u,d,f,p);let[y,b,x,S]=v.box,C=a?v.allPointsOccluded:g.isOccluded,w=C;return w||=g.perspectiveRatio=1;e--)p.push(a.path[e]);for(let e=1;ee.signedDistanceFromCamera<=0)?[]:e.map(e=>e.point)}let g=[];if(p.length>0){let e=p[0].clone(),t=p[0].clone();for(let n=1;n=n.x&&t.x<=r.x&&e.y>=n.y&&t.y<=r.y?[p]:t.xr.x||t.yr.y?[]:kt([p],n.x,n.y,r.x,r.y)}for(let n of g){i.reset(n,t*.25);let r=0;r=i.length<=.5*t?1:Math.ceil(i.paddedLength/m)+1;for(let n=0;n=this.screenRightBoundary||r<100||t>this.screenBottomBoundary}isInsideGrid(e,t,n,r){return n>=0&&e=0&&tthis.projectAndGetPerspectiveRatio(e.x,e.y,r,Fo(c,e.x,e.y,f,p),d));A=e.some(e=>!e.isOccluded),k=e.map(e=>new l(e.x,e.y))}else A=!0;return{box:Bt(k),allPointsOccluded:!A}}},ps=class{constructor(e,t,n,r){this.opacity=e?Math.max(0,Math.min(1,e.opacity+(e.placed?t:-t))):r&&n?1:0,this.placed=n}isHidden(){return this.opacity===0&&!this.placed}},ms=class{constructor(e,t,n,r,i){this.text=new ps(e?e.text:null,t,n,i),this.icon=new ps(e?e.icon:null,t,r,i)}isHidden(){return this.text.isHidden()&&this.icon.isHidden()}},hs=class{constructor(e,t,n){this.text=e,this.icon=t,this.skipFade=n}},gs=class{constructor(e,t,n,r,i){this.bucketInstanceId=e,this.featureIndex=t,this.sourceLayerIndex=n,this.bucketIndex=r,this.tileID=i}},_s=class{constructor(e){this.crossSourceCollisions=e,this.maxGroupID=0,this.collisionGroups={}}get(e){if(this.crossSourceCollisions)return{ID:0,predicate:null};if(!this.collisionGroups[e]){let t=++this.maxGroupID;this.collisionGroups[e]={ID:t,predicate:e=>e.collisionGroupID===t}}return this.collisionGroups[e]}};function vs(e,t,n,r,i){let{horizontalAlign:a,verticalAlign:o}=Oe(e),s=-(a-.5)*t,c=-(o-.5)*n;return new l(s+r[0]*i,c+r[1]*i)}var ys=class{constructor(e,t,n,r,i){this.transform=e.clone(),this.terrain=t,this.collisionIndex=new fs(this.transform),this.placements={},this.opacities={},this.variableOffsets={},this.stale=!1,this.commitTime=0,this.fadeDuration=n,this.retainedQueryData={},this.collisionGroups=new _s(r),this.collisionCircleArrays={},this.collisionBoxArrays=new Map,this.prevPlacement=i,i&&(i.prevPlacement=void 0),this.placedOrientations={}}_getTerrainElevationFunc(e){let t=this.terrain;if(t)return(n,r)=>t.getElevation(e,n,r)}getBucketParts(e,t,n,r){let i=n.getBucket(t),a=n.latestFeatureIndex;if(!i||!a||t.id!==i.layerIds[0])return;let o=n.collisionBoxArray,s=i.layers[0].layout,c=i.layers[0].paint,l=2**(this.transform.zoom-n.tileID.overscaledZ),u=n.tileSize/N,d=n.tileID.toUnwrapped(),f=s.get(`text-rotation-alignment`)===`map`,p=lt(n,1,this.transform.zoom),m=le(this.collisionIndex.transform,n,c.get(`text-translate`),c.get(`text-translate-anchor`)),h=le(this.collisionIndex.transform,n,c.get(`icon-translate`),c.get(`icon-translate-anchor`)),g=Ho(f,this.transform,p);this.retainedQueryData[i.bucketInstanceId]=new gs(i.bucketInstanceId,a,i.sourceLayerIndex,i.index,n.tileID);let _={bucket:i,layout:s,translationText:m,translationIcon:h,unwrappedTileID:d,pitchedLabelPlaneMatrix:g,scale:l,textPixelRatio:u,holdingForFade:n.holdingForSymbolFade(),collisionBoxArray:o,partiallyEvaluatedTextSize:fn(i.textSizeData,this.transform.zoom),collisionGroup:this.collisionGroups.get(i.sourceID)};if(r)for(let t of i.sortKeyRanges){let{sortKey:n,symbolInstanceStart:r,symbolInstanceEnd:i}=t;e.push({sortKey:n,symbolInstanceStart:r,symbolInstanceEnd:i,parameters:_})}else e.push({symbolInstanceStart:0,symbolInstanceEnd:i.symbolInstances.length,parameters:_})}attemptAnchorPlacement(e,t,n,r,i,a,o,s,c,l,u,d,f,p,m,h,g,_,v,y,b,x){let S=Wn[e.textAnchor],C=[e.textOffset0,e.textOffset1],w=vs(S,n,r,C,i),T=this.collisionIndex.placeCollisionBox(t,d,s,c,l,o,a,h,u.predicate,v,w,y,b,x);if(!(_&&!this.collisionIndex.placeCollisionBox(_,d,s,c,l,o,a,g,u.predicate,v,w,y,b,x).placeable)&&T.placeable){let e;if(this.prevPlacement?.variableOffsets[f.crossTileID]&&this.prevPlacement?.placements[f.crossTileID]?.text&&(e=this.prevPlacement.variableOffsets[f.crossTileID].anchor),f.crossTileID===0)throw Error(`symbolInstance.crossTileID can't be 0`);return this.variableOffsets[f.crossTileID]={textOffset:C,width:n,height:r,anchor:S,textBoxScale:i,prevAnchor:e},this.markUsedJustification(p,S,f,m),p.allowVerticalPlacement&&(this.markUsedOrientation(p,m,f),this.placedOrientations[f.crossTileID]=m),{shift:w,placedGlyphBoxes:T}}}placeLayerBucketPart(e,t,n){let{bucket:r,layout:a,translationText:o,translationIcon:s,unwrappedTileID:c,pitchedLabelPlaneMatrix:l,textPixelRatio:u,holdingForFade:d,collisionBoxArray:f,partiallyEvaluatedTextSize:p,collisionGroup:m}=e.parameters,h=a.get(`text-optional`),g=a.get(`icon-optional`),_=_n(a,`text-overlap`,`text-allow-overlap`),v=_===`always`,y=_n(a,`icon-overlap`,`icon-allow-overlap`),b=y===`always`,x=a.get(`text-rotation-alignment`)===`map`,S=a.get(`text-pitch-alignment`)===`map`,C=a.get(`icon-text-fit`)!==`none`,w=a.get(`symbol-z-order`)===`viewport-y`,T=a.get(`symbol-height-anchor`)===`ground`,E=v&&(b||!r.hasIconData()||g),D=b&&(v||!r.hasTextData()||h);!r.collisionArrays&&f&&r.deserializeCollisionBoxes(f);let ee=this.retainedQueryData[r.bucketInstanceId].tileID,O=this._getTerrainElevationFunc(ee),k=this.transform.getFastPathSimpleProjectionMatrix(ee),A=(e,f,b)=>{if(t[e.crossTileID])return;if(d){this.placements[e.crossTileID]=new hs(!1,!1,!1);return}let w=e.heightOffset,A=!1,j=!1,M=!0,te=null,ne={box:null,placeable:!1,offscreen:null,occluded:!1},re={box:null,placeable:!1,offscreen:null},ie=null,N=null,ae=null,oe=0,se=0,ce=0;f.textFeatureIndex?oe=f.textFeatureIndex:e.useRuntimeCollisionCircles&&(oe=e.featureIndex),f.verticalTextFeatureIndex&&(se=f.verticalTextFeatureIndex);let le=f.textBox;if(le){let t=t=>{let n=1;if(r.allowVerticalPlacement&&!t&&this.prevPlacement){let t=this.prevPlacement.placedOrientations[e.crossTileID];t&&(this.placedOrientations[e.crossTileID]=t,n=t,this.markUsedOrientation(r,n,e))}return n},i=(t,n)=>{if(r.allowVerticalPlacement&&e.numVerticalGlyphVertices>0&&f.verticalTextBox){for(let e of r.writingModes)if(e===2?(ne=n(),re=ne):ne=t(),ne?.placeable)break}else ne=t()},a=e.textAnchorOffsetStartIndex,l=e.textAnchorOffsetEndIndex;if(l===a){let n=(t,n)=>{let i=this.collisionIndex.placeCollisionBox(t,_,u,ee,c,S,x,o,m.predicate,O,void 0,k,w,T);return i?.placeable&&(this.markUsedOrientation(r,n,e),this.placedOrientations[e.crossTileID]=n),i};i(()=>n(le,1),()=>{let t=f.verticalTextBox;return r.allowVerticalPlacement&&e.numVerticalGlyphVertices>0&&t?n(t,2):{box:null,offscreen:null}}),t(ne?.placeable)}else{let d=Wn[this.prevPlacement?.variableOffsets[e.crossTileID]?.anchor],p=(t,i,f)=>{let p=t.x2-t.x1,h=t.y2-t.y1,g=e.textBoxScale,v=C&&y===`never`?i:null,b=null,E=_===`never`?1:2,D=`never`;d&&E++;for(let n=0;np(le,f.iconBox,1),()=>{let t=f.verticalTextBox,n=ne?.placeable;return r.allowVerticalPlacement&&!n&&e.numVerticalGlyphVertices>0&&t?p(t,f.verticalIconBox,2):{box:null,occluded:!0,offscreen:null}}),ne&&(A=ne.placeable,M=ne.offscreen);let h=t(ne?.placeable);if(!A&&this.prevPlacement){let t=this.prevPlacement.variableOffsets[e.crossTileID];t&&(this.variableOffsets[e.crossTileID]=t,this.markUsedJustification(r,t.anchor,e,h))}}}if(ie=ne,A=ie?.placeable,M=ie?.offscreen,e.useRuntimeCollisionCircles&&e.centerJustifiedTextSymbolIndex>=0){let t=r.text.placedSymbolArray.get(e.centerJustifiedTextSymbolIndex),s=i(r.textSizeData,p,t),u=a.get(`text-padding`),d=e.collisionCircleDiameter;N=this.collisionIndex.placeCollisionCircles(_,t,r.lineVertexArray,r.glyphOffsetArray,s,c,l,n,S,m.predicate,d,u,o,O),N.circles.length&&N.collisionDetected&&!n&&I(`Collisions detected, but collision boxes are not shown`),A=v||N.circles.length>0&&!N.collisionDetected,M&&=N.offscreen}if(f.iconFeatureIndex&&(ce=f.iconFeatureIndex),f.iconBox){let e=e=>this.collisionIndex.placeCollisionBox(e,y,u,ee,c,S,x,s,m.predicate,O,C&&te?te:void 0,k,w,T);re&&re.placeable&&f.verticalIconBox?(ae=e(f.verticalIconBox),j=ae.placeable):(ae=e(f.iconBox),j=ae.placeable),M&&=ae.offscreen}let ue=h||e.numHorizontalGlyphVertices===0&&e.numVerticalGlyphVertices===0,de=g||e.numIconVertices===0;!ue&&!de?j=A=j&&A:de?ue||(j&&=A):A=j&&A;let fe=A&&ie.placeable,pe=j&&ae.placeable;if(fe&&(re&&re.placeable&&se?this.collisionIndex.insertCollisionBox(ie.box,_,a.get(`text-ignore-placement`),r.bucketInstanceId,se,m.ID):this.collisionIndex.insertCollisionBox(ie.box,_,a.get(`text-ignore-placement`),r.bucketInstanceId,oe,m.ID)),pe&&this.collisionIndex.insertCollisionBox(ae.box,y,a.get(`icon-ignore-placement`),r.bucketInstanceId,ce,m.ID),N&&A&&this.collisionIndex.insertCollisionCircles(N.circles,_,a.get(`text-ignore-placement`),r.bucketInstanceId,oe,m.ID),n&&this.storeCollisionData(r.bucketInstanceId,b,f,ie,ae,N),e.crossTileID===0)throw Error(`symbolInstance.crossTileID can't be 0`);if(r.bucketInstanceId===0)throw Error(`bucket.bucketInstanceId can't be 0`);let me=(A||E)&&!ie?.occluded,he=(j||D)&&!ae?.occluded;this.placements[e.crossTileID]=new hs(me,he,M||r.justReloaded),t[e.crossTileID]=!0};if(w){if(e.symbolInstanceStart!==0)throw Error(`bucket.bucketInstanceId should be 0`);let t=r.getSortedSymbolIndexes(-this.transform.bearingInRadians);for(let e=t.length-1;e>=0;--e){let n=t[e];A(r.symbolInstances.get(n),r.collisionArrays[n],n)}}else for(let t=e.symbolInstanceStart;t=0&&(a>=0&&t!==a?e.text.placedSymbolArray.get(t).crossTileID=0:e.text.placedSymbolArray.get(t).crossTileID=n.crossTileID)}markUsedOrientation(e,t,n){let r=t===1||t===3?t:0,i=t===2?t:0,a=[n.leftJustifiedTextSymbolIndex,n.centerJustifiedTextSymbolIndex,n.rightJustifiedTextSymbolIndex];for(let t of a)e.text.placedSymbolArray.get(t).placedOrientation=r;n.verticalPlacedTextSymbolIndex&&(e.text.placedSymbolArray.get(n.verticalPlacedTextSymbolIndex).placedOrientation=i)}commit(e){this.commitTime=e,this.zoomAtLastRecencyCheck=this.transform.zoom;let t=this.prevPlacement,n=!1;this.prevZoomAdjustment=t?t.zoomAdjustment(this.transform.zoom):0;let r=t?t.symbolFadeChange(e):1,i=t?t.opacities:{},a=t?t.variableOffsets:{},o=t?t.placedOrientations:{};for(let e in this.placements){let t=this.placements[e],a=i[e];a?(this.opacities[e]=new ms(a,r,t.text,t.icon),n||=t.text!==a.text.placed,n||=t.icon!==a.icon.placed):(this.opacities[e]=new ms(null,r,t.text,t.icon,t.skipFade),n||=t.text||t.icon)}for(let e in i){let t=i[e];if(!this.opacities[e]){let i=new ms(t,r,!1,!1);i.isHidden()||(this.opacities[e]=i,n||=t.text.placed,n||=t.icon.placed)}}for(let e in a)!this.variableOffsets[e]&&this.opacities[e]&&!this.opacities[e].isHidden()&&(this.variableOffsets[e]=a[e]);for(let e in o)!this.placedOrientations[e]&&this.opacities[e]&&!this.opacities[e].isHidden()&&(this.placedOrientations[e]=o[e]);if(t&&t.lastPlacementChangeTime===void 0)throw Error(`Last placement time for previous placement is not defined`);n?this.lastPlacementChangeTime=e:typeof this.lastPlacementChangeTime!=`number`&&(this.lastPlacementChangeTime=t?t.lastPlacementChangeTime:e)}updateLayerOpacities(e,t){let n={};for(let r of t){let t=r.getBucket(e);t&&r.latestFeatureIndex&&e.id===t.layerIds[0]&&this.updateBucketOpacities(t,r.tileID,n,r.collisionBoxArray)}}updateBucketOpacities(e,t,n,r){e.hasTextData()&&(e.text.opacityVertexArray.clear(),e.text.hasVisibleVertices=!1),e.hasIconData()&&(e.icon.opacityVertexArray.clear(),e.icon.hasVisibleVertices=!1),e.hasIconCollisionBoxData()&&e.iconCollisionBox.collisionVertexArray.clear(),e.hasTextCollisionBoxData()&&e.textCollisionBox.collisionVertexArray.clear();let i=e.layers[0],a=i.layout,o=new ms(null,0,!1,!1,!0),s=a.get(`text-allow-overlap`),c=a.get(`icon-allow-overlap`),u=i._unevaluatedLayout.hasValue(`text-variable-anchor`)||i._unevaluatedLayout.hasValue(`text-variable-anchor-offset`),d=a.get(`text-rotation-alignment`)===`map`,f=a.get(`text-pitch-alignment`)===`map`,p=a.get(`icon-text-fit`)!==`none`,m=new ms(null,0,s&&(c||!e.hasIconData()||a.get(`icon-optional`)),c&&(s||!e.hasTextData()||a.get(`text-optional`)),!0);!e.collisionArrays&&r&&(e.hasIconCollisionBoxData()||e.hasTextCollisionBoxData())&&e.deserializeCollisionBoxes(r);let h=(e,t,n)=>{for(let r=0;r0||a>0,y=r.numIconVertices>0,b=this.placedOrientations[r.crossTileID],x=b===2,S=b===1||b===3;if(v){let t=Ts(_.text),n=x?Es:t;h(e.text,i,n);let o=S?Es:t;h(e.text,a,o);let s=_.text.isHidden(),c=[r.rightJustifiedTextSymbolIndex,r.centerJustifiedTextSymbolIndex,r.leftJustifiedTextSymbolIndex];for(let t of c)t>=0&&(e.text.placedSymbolArray.get(t).hidden=s||x?1:0);r.verticalPlacedTextSymbolIndex>=0&&(e.text.placedSymbolArray.get(r.verticalPlacedTextSymbolIndex).hidden=s||S?1:0);let l=this.variableOffsets[r.crossTileID];l&&this.markUsedJustification(e,l.anchor,r,b);let u=this.placedOrientations[r.crossTileID];u&&(this.markUsedJustification(e,`left`,r,u),this.markUsedOrientation(e,u,r))}if(y){let t=Ts(_.icon),n=!(p&&r.verticalPlacedIconSymbolIndex&&x);if(r.placedIconSymbolIndex>=0){let i=n?t:Es;h(e.icon,r.numIconVertices,i),e.icon.placedSymbolArray.get(r.placedIconSymbolIndex).hidden=_.icon.isHidden()}if(r.verticalPlacedIconSymbolIndex>=0){let i=n?Es:t;h(e.icon,r.numVerticalIconVertices,i),e.icon.placedSymbolArray.get(r.verticalPlacedIconSymbolIndex).hidden=_.icon.isHidden()}}let C=g?.has(t)?g.get(t):{text:null,icon:null};if(e.hasIconCollisionBoxData()||e.hasTextCollisionBoxData()){let n=e.collisionArrays[t];if(n){let t=new l(0,0);if(n.textBox||n.verticalTextBox){let r=!0;if(u){let e=this.variableOffsets[s];e?(t=vs(e.anchor,e.width,e.height,e.textOffset,e.textBoxScale),d&&t._rotate(f?-this.transform.bearingInRadians:this.transform.bearingInRadians)):r=!1}if(n.textBox||n.verticalTextBox){let i;n.textBox&&(i=x),n.verticalTextBox&&(i=S),bs(e.textCollisionBox.collisionVertexArray,_.text.placed,!r||i,C.text,t.x,t.y)}}if(n.iconBox||n.verticalIconBox){let r=!!(!S&&n.verticalIconBox),i;n.iconBox&&(i=r),n.verticalIconBox&&(i=!r),bs(e.iconCollisionBox.collisionVertexArray,_.icon.placed,i,C.icon,p?t.x:0,p?t.y:0)}}}}if(e.sortFeatures(-this.transform.bearingInRadians),this.retainedQueryData[e.bucketInstanceId]&&(this.retainedQueryData[e.bucketInstanceId].featureSortOrder=e.featureSortOrder),e.hasTextData()&&e.text.opacityVertexBuffer&&e.text.opacityVertexBuffer.updateData(e.text.opacityVertexArray),e.hasIconData()&&e.icon.opacityVertexBuffer&&e.icon.opacityVertexBuffer.updateData(e.icon.opacityVertexArray),e.hasIconCollisionBoxData()&&e.iconCollisionBox.collisionVertexBuffer&&e.iconCollisionBox.collisionVertexBuffer.updateData(e.iconCollisionBox.collisionVertexArray),e.hasTextCollisionBoxData()&&e.textCollisionBox.collisionVertexBuffer&&e.textCollisionBox.collisionVertexBuffer.updateData(e.textCollisionBox.collisionVertexArray),e.text.opacityVertexArray.length!==e.text.layoutVertexArray.length/4)throw Error(`bucket.text.opacityVertexArray.length (= ${e.text.opacityVertexArray.length}) !== bucket.text.layoutVertexArray.length (= ${e.text.layoutVertexArray.length}) / 4`);if(e.icon.opacityVertexArray.length!==e.icon.layoutVertexArray.length/4)throw Error(`bucket.icon.opacityVertexArray.length (= ${e.icon.opacityVertexArray.length}) !== bucket.icon.layoutVertexArray.length (= ${e.icon.layoutVertexArray.length}) / 4`);e.bucketInstanceId in this.collisionCircleArrays&&(e.collisionCircleArray=this.collisionCircleArrays[e.bucketInstanceId],delete this.collisionCircleArrays[e.bucketInstanceId])}symbolFadeChange(e){return this.fadeDuration===0?1:(e-this.commitTime)/this.fadeDuration+this.prevZoomAdjustment}zoomAdjustment(e){return Math.max(0,(this.transform.zoom-e)/1.5)}hasTransitions(e){return this.stale||e-this.lastPlacementChangeTimee}setStale(){this.stale=!0}};function bs(e,t,n,r,i,a){(!r||r.length===0)&&(r=[0,0,0,0]);let o=r[0]-100,s=r[1]-100,c=r[2]-100,l=r[3]-100;e.emplaceBack(+!!t,+!!n,i||0,a||0,o,s),e.emplaceBack(+!!t,+!!n,i||0,a||0,c,s),e.emplaceBack(+!!t,+!!n,i||0,a||0,c,l),e.emplaceBack(+!!t,+!!n,i||0,a||0,o,l)}const xs=2**25,Ss=2**24,Cs=2**17,ws=2**16;function Ts(e){if(e.opacity===0&&!e.placed)return 0;if(e.opacity===1&&e.placed)return 4294967295;let t=+!!e.placed,n=Math.floor(e.opacity*127);return n*xs+t*Ss+n*Cs+t*ws+n*512+t*256+n*2+t}const Es=0;var Ds=class{constructor(e){this._sortAcrossTiles=e.layout.get(`symbol-z-order`)!==`viewport-y`&&!e.layout.get(`symbol-sort-key`).isConstant(),this._currentTileIndex=0,this._currentPartIndex=0,this._seenCrossTileIDs={},this._bucketParts=[]}continuePlacement(e,t,n,r,i){let a=this._bucketParts;for(;this._currentTileIndexe.sortKey-t.sortKey));this._currentPartIndex!this._forceFullPlacement&&U()-r>2;for(;this._currentPlacementIndex>=0;){let r=t[e[this._currentPlacementIndex]],a=this.placement.collisionIndex.transform.zoom;if(C(r)&&r.layout&&(!r.minzoom||r.minzoom<=a)&&(!r.maxzoom||r.maxzoom>a)){if(this._inProgressLayer||=new Ds(r),this._inProgressLayer.continuePlacement(n[r.source],this.placement,this._showCollisionBoxes,r,i))return;delete this._inProgressLayer}this._currentPlacementIndex--}this._done=!0}commit(e){return this.placement.commit(e),this.placement}};const ks=[Int8Array,Uint8Array,Uint8ClampedArray,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array],J=new Uint32Array(96);var As=class e{static from(t){if(!t||t.byteLength===void 0||t.buffer)throw Error(`Data must be an instance of ArrayBuffer or SharedArrayBuffer.`);let[n,r]=new Uint8Array(t,0,2);if(n!==219)throw Error(`Data does not appear to be in a KDBush format.`);let i=r>>4;if(i!==1)throw Error(`Got v${i} data when expected v1.`);let a=ks[r&15];if(!a)throw Error(`Unrecognized array type.`);let[o]=new Uint16Array(t,2,1),[s]=new Uint32Array(t,4,1);return new e(s,o,a,void 0,t)}constructor(e,t=64,n=Float64Array,r=ArrayBuffer,i){if(isNaN(e)||e<0)throw Error(`Unexpected numItems value: ${e}.`);this.numItems=+e,this.nodeSize=Math.min(Math.max(+t,2),65535),this.ArrayType=n,this.IndexArrayType=e<65536?Uint16Array:Uint32Array;let a=ks.indexOf(this.ArrayType),o=e*2*this.ArrayType.BYTES_PER_ELEMENT,s=e*this.IndexArrayType.BYTES_PER_ELEMENT,c=(8-s%8)%8;if(a<0)throw Error(`Unexpected typed array class: ${n}.`);if(i)this.data=i,this.ids=new this.IndexArrayType(i,8,e),this.coords=new n(i,8+s+c,e*2),this._pos=e*2,this._finished=!0;else{let i=this.data=new r(8+o+s+c);this.ids=new this.IndexArrayType(i,8,e),this.coords=new n(i,8+s+c,e*2),this._pos=0,this._finished=!1,new Uint8Array(i,0,2).set([219,16+a]),new Uint16Array(i,2,1)[0]=t,new Uint32Array(i,4,1)[0]=e}}add(e,t){let n=this._pos>>1;return this.ids[n]=n,this.coords[this._pos++]=e,this.coords[this._pos++]=t,n}finish(){let e=this._pos>>1;if(e!==this.numItems)throw Error(`Added ${e} items when expected ${this.numItems}.`);return js(this.ids,this.coords,this.nodeSize,0,this.numItems-1,0),this._finished=!0,this}range(e,t,n,r){if(!this._finished)throw Error(`Data not yet indexed - call index.finish().`);let{ids:i,coords:a,nodeSize:o}=this;J[0]=0,J[1]=i.length-1,J[2]=0;let s=3,c=[];for(;s>0;){let l=J[--s],u=J[--s],d=J[--s];if(u-d<=o){for(let o=d;o<=u;o++){let s=a[2*o],l=a[2*o+1];s>=e&&s<=n&&l>=t&&l<=r&&c.push(i[o])}continue}let f=d+u>>1,p=a[2*f],m=a[2*f+1];p>=e&&p<=n&&m>=t&&m<=r&&c.push(i[f]),(l===0?e<=p:t<=m)&&(J[s++]=d,J[s++]=f-1,J[s++]=1-l),(l===0?n>=p:r>=m)&&(J[s++]=f+1,J[s++]=u,J[s++]=1-l)}return c}within(e,t,n){let r=[];return this.withinInto(e,t,n,r),r}withinInto(e,t,n,r){if(!this._finished)throw Error(`Data not yet indexed - call index.finish().`);let{ids:i,coords:a,nodeSize:o}=this;J[0]=0,J[1]=i.length-1,J[2]=0;let s=3,c=0,l=n*n;for(;s>0;){let u=J[--s],d=J[--s],f=J[--s];if(d-f<=o){for(let n=f;n<=d;n++)Fs(a[2*n],a[2*n+1],e,t)<=l&&(r[c++]=i[n]);continue}let p=f+d>>1,m=a[2*p],h=a[2*p+1];Fs(m,h,e,t)<=l&&(r[c++]=i[p]),(u===0?e-n<=m:t-n<=h)&&(J[s++]=f,J[s++]=p-1,J[s++]=1-u),(u===0?e+n>=m:t+n>=h)&&(J[s++]=p+1,J[s++]=d,J[s++]=1-u)}return c}};function js(e,t,n,r,i,a){if(i-r<=n)return;let o=r+i>>1;Ms(e,t,o,r,i,a),js(e,t,n,r,o-1,1-a),js(e,t,n,o+1,i,1-a)}function Ms(e,t,n,r,i,a){for(;i>r;){if(i-r>600){let o=i-r+1,s=n-r+1,c=Math.log(o),l=.5*Math.exp(2*c/3),u=.5*Math.sqrt(c*l*(o-l)/o)*(s-o/2<0?-1:1);Ms(e,t,n,Math.max(r,Math.floor(n-s*l/o+u)),Math.min(i,Math.floor(n+(o-s)*l/o+u)),a)}let o=t[2*n+a],s=r,c=i;for(Ns(e,t,r,n),t[2*i+a]>o&&Ns(e,t,r,i);so;)c--}t[2*r+a]===o?Ns(e,t,r,c):(c++,Ns(e,t,c,i)),c<=n&&(r=c+1),n<=c&&(i=c-1)}}function Ns(e,t,n,r){Ps(e,n,r),Ps(t,2*n,2*r),Ps(t,2*n+1,2*r+1)}function Ps(e,t,n){let r=e[t];e[t]=e[n],e[n]=r}function Fs(e,t,n,r){let i=e-n,a=t-r;return i*i+a*a}const Is=512/N/2;var Ls=class{constructor(e,t,n){this.tileID=e,this.bucketInstanceId=n,this._symbolsByKey={};let r=new Map;for(let e=0;e({x:Math.floor(e.anchorX*Is),y:Math.floor(e.anchorY*Is)})),crossTileIDs:t.map(e=>e.crossTileID)};if(n.positions.length>128){let e=new As(n.positions.length,16,Uint16Array);for(let{x:t,y:r}of n.positions)e.add(t,r);e.finish(),delete n.positions,n.index=e}this._symbolsByKey[e]=n}}getScaledCoordinates(e,t){let{x:n,y:r,z:i}=this.tileID.canonical,{x:a,y:o,z:s}=t.canonical,c=s-i,l=Is/2**c,u=(a*N+e.anchorX)*l,d=(o*N+e.anchorY)*l,f=n*N*Is,p=r*N*Is;return{x:Math.floor(u-f),y:Math.floor(d-p)}}findMatches(e,t,n){let r=this.tileID.canonical.ze)}},Rs=class{constructor(){this.maxCrossTileID=0}generate(){return++this.maxCrossTileID}},zs=class{constructor(){this.indexes={},this.usedCrossTileIDs={},this.lng=0}handleWrapJump(e){let t=Math.round((e-this.lng)/360);if(t!==0)for(let e in this.indexes){let n=this.indexes[e],r={};for(let e in n){let i=n[e];i.tileID=i.tileID.unwrapTo(i.tileID.wrap+t),r[i.tileID.key]=i}this.indexes[e]=r}this.lng=e}addBucket(e,t,n){if(this.indexes[e.overscaledZ]?.[e.key]){if(this.indexes[e.overscaledZ][e.key].bucketInstanceId===t.bucketInstanceId)return!1;this.removeBucketCrossTileIDs(e.overscaledZ,this.indexes[e.overscaledZ][e.key])}for(let e=0;ee.overscaledZ)for(let n in i){let a=i[n];a.tileID.isChildOf(e)&&a.findMatches(t.symbolInstances,e,r)}else{let a=i[e.scaledTo(Number(n)).key];a&&a.findMatches(t.symbolInstances,e,r)}}for(let e=0;e> 1u)/127.0,float(packedOpacity & 1u));}vec4 decode_color(const vec2 encodedColor) {return vec4(unpack_float(encodedColor[0])/255.0,unpack_float(encodedColor[1])/255.0 );}float unpack_mix_vec2(const vec2 packedValue,const float t) {return mix(packedValue[0],packedValue[1],t);}vec4 unpack_mix_color(const vec4 packedColors,const float t) {vec4 minColor=decode_color(vec2(packedColors[0],packedColors[1]));vec4 maxColor=decode_color(vec2(packedColors[2],packedColors[3]));return mix(minColor,maxColor,t);}vec2 get_pattern_pos(const vec2 pixel_coord_upper,const vec2 pixel_coord_lower,const vec2 pattern_size,const float tile_units_to_pixels,const vec2 pos) {vec2 offset=mod(mod(mod(pixel_coord_upper,pattern_size)*256.0,pattern_size)*256.0+pixel_coord_lower,pattern_size);return (tile_units_to_pixels*pos+offset)/pattern_size;}mat3 rotationMatrixFromAxisAngle(vec3 u,float angle) {float c=cos(angle);float s=sin(angle);float c2=1.0-c;return mat3(u.x*u.x*c2+ c,u.x*u.y*c2-u.z*s,u.x*u.z*c2+u.y*s,u.y*u.x*c2+u.z*s,u.y*u.y*c2+ c,u.y*u.z*c2-u.x*s,u.z*u.x*c2-u.y*s,u.z*u.y*c2+u.x*s,u.z*u.z*c2+ c );} #ifdef TERRAIN3D @@ -46,9 +46,9 @@ vec3 frag=pos.xyz/pos.w;highp float d=depthOpacity(frag);if (d > 0.95) return 1. #else return 1.0; #endif -}float ele(vec2 pos) { +}float ele(ivec2 pos) { #ifdef TERRAIN3D -vec4 rgb=(texture(u_terrain,pos)*255.0)*u_terrain_unpack;return rgb.r+rgb.g+rgb.b-u_terrain_unpack.a; +vec4 rgb=(texelFetch(u_terrain,pos,0)*255.0)*u_terrain_unpack;return rgb.r+rgb.g+rgb.b-u_terrain_unpack.a; #else return 0.0; #endif @@ -57,19 +57,19 @@ return 0.0; #ifdef GLOBE if ((pos.y <-32767.5) || (pos.y > 32766.5)) {return 0.0;} #endif -vec2 coord=(u_terrain_matrix*vec4(pos,0.0,1.0)).xy*u_terrain_dim+1.0;vec2 f=fract(coord);vec2 c=(floor(coord)+0.5)/(u_terrain_dim+2.0);float d=1.0/(u_terrain_dim+2.0);float tl=ele(c);float tr=ele(c+vec2(d,0.0));float bl=ele(c+vec2(0.0,d));float br=ele(c+vec2(d,d));float elevation=mix(mix(tl,tr,f.x),mix(bl,br,f.x),f.y);return elevation*u_terrain_exaggeration; +vec2 coord=(u_terrain_matrix*vec4(pos,0.0,1.0)).xy*u_terrain_dim+1.0;vec2 f=fract(coord);ivec2 c=ivec2(floor(coord));ivec2 hi=textureSize(u_terrain,0)-1;float tl=ele(clamp(c,ivec2(0),hi));float tr=ele(clamp(c+ivec2(1,0),ivec2(0),hi));float bl=ele(clamp(c+ivec2(0,1),ivec2(0),hi));float br=ele(clamp(c+ivec2(1,1),ivec2(0),hi));float elevation=mix(mix(tl,tr,f.x),mix(bl,br,f.x),f.y);return elevation*u_terrain_exaggeration; #else return 0.0; #endif -}const float PI=3.141592653589793;uniform mat4 u_projection_matrix;`,ns=`uniform vec4 u_color;uniform float u_opacity;void main() {fragColor=u_color*u_opacity; +}const float PI=3.141592653589793;uniform mat4 u_projection_matrix;`,Us=`uniform vec4 u_color;uniform float u_opacity;void main() {fragColor=u_color*u_opacity; #ifdef OVERDRAW_INSPECTOR fragColor=vec4(1.0); #endif -}`,rs=`layout(location=0) in vec2 a_pos;void main() {gl_Position=projectTile(a_pos);}`,is=`uniform vec2 u_pattern_tl_a;uniform vec2 u_pattern_br_a;uniform vec2 u_pattern_tl_b;uniform vec2 u_pattern_br_b;uniform vec2 u_texsize;uniform float u_mix;uniform float u_opacity;uniform sampler2D u_image;in vec2 v_pos_a;in vec2 v_pos_b;void main() {vec2 imagecoord=mod(v_pos_a,1.0);vec2 pos=mix(u_pattern_tl_a/u_texsize,u_pattern_br_a/u_texsize,imagecoord);vec4 color1=texture(u_image,pos);vec2 imagecoord_b=mod(v_pos_b,1.0);vec2 pos2=mix(u_pattern_tl_b/u_texsize,u_pattern_br_b/u_texsize,imagecoord_b);vec4 color2=texture(u_image,pos2);fragColor=mix(color1,color2,u_mix)*u_opacity; +}`,Ws=`layout(location=0) in vec2 a_pos;void main() {gl_Position=projectTile(a_pos);}`,Gs=`uniform vec2 u_pattern_tl_a;uniform vec2 u_pattern_br_a;uniform vec2 u_pattern_tl_b;uniform vec2 u_pattern_br_b;uniform vec2 u_texsize;uniform float u_mix;uniform float u_opacity;uniform sampler2D u_image;in vec2 v_pos_a;in vec2 v_pos_b;void main() {vec2 imagecoord=mod(v_pos_a,1.0);vec2 pos=mix(u_pattern_tl_a/u_texsize,u_pattern_br_a/u_texsize,imagecoord);vec4 color1=texture(u_image,pos);vec2 imagecoord_b=mod(v_pos_b,1.0);vec2 pos2=mix(u_pattern_tl_b/u_texsize,u_pattern_br_b/u_texsize,imagecoord_b);vec4 color2=texture(u_image,pos2);fragColor=mix(color1,color2,u_mix)*u_opacity; #ifdef OVERDRAW_INSPECTOR fragColor=vec4(1.0); #endif -}`,as=`uniform vec2 u_pattern_size_a;uniform vec2 u_pattern_size_b;uniform vec2 u_pixel_coord_upper;uniform vec2 u_pixel_coord_lower;uniform float u_scale_a;uniform float u_scale_b;uniform float u_tile_units_to_pixels;layout(location=0) in vec2 a_pos;out vec2 v_pos_a;out vec2 v_pos_b;void main() {gl_Position=projectTile(a_pos);v_pos_a=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,u_scale_a*u_pattern_size_a,u_tile_units_to_pixels,a_pos);v_pos_b=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,u_scale_b*u_pattern_size_b,u_tile_units_to_pixels,a_pos);}`,os=`in vec3 v_data;flat in float v_visibility; +}`,Ks=`uniform vec2 u_pattern_size_a;uniform vec2 u_pattern_size_b;uniform vec2 u_pixel_coord_upper;uniform vec2 u_pixel_coord_lower;uniform float u_scale_a;uniform float u_scale_b;uniform float u_tile_units_to_pixels;layout(location=0) in vec2 a_pos;out vec2 v_pos_a;out vec2 v_pos_b;void main() {gl_Position=projectTile(a_pos);v_pos_a=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,u_scale_a*u_pattern_size_a,u_tile_units_to_pixels,a_pos);v_pos_b=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,u_scale_b*u_pattern_size_b,u_tile_units_to_pixels,a_pos);}`,qs=`in vec3 v_data;flat in float v_visibility; #pragma maplibre: define highp vec4 color #pragma maplibre: define mediump float radius #pragma maplibre: define lowp float blur @@ -89,7 +89,7 @@ vec2 extrude=v_data.xy;float extrude_length=length(extrude);float antialiased_bl #ifdef OVERDRAW_INSPECTOR fragColor=vec4(1.0); #endif -}`,ss=`uniform bool u_scale_with_map;uniform bool u_pitch_with_map;uniform vec2 u_extrude_scale;uniform highp float u_globe_extrude_scale;uniform lowp float u_device_pixel_ratio;uniform highp float u_camera_to_center_distance;uniform vec2 u_translate;layout(location=0) in vec2 a_pos;out vec3 v_data;flat out float v_visibility; +}`,Js=`uniform bool u_scale_with_map;uniform bool u_pitch_with_map;uniform vec2 u_extrude_scale;uniform highp float u_globe_extrude_scale;uniform lowp float u_device_pixel_ratio;uniform highp float u_camera_to_center_distance;uniform vec2 u_translate;layout(location=0) in ivec2 a_pos;out vec3 v_data;flat out float v_visibility; #pragma maplibre: define highp vec4 color #pragma maplibre: define mediump float radius #pragma maplibre: define lowp float blur @@ -105,7 +105,7 @@ void main(void) { #pragma maplibre: initialize highp vec4 stroke_color #pragma maplibre: initialize mediump float stroke_width #pragma maplibre: initialize lowp float stroke_opacity -vec2 pos_raw=a_pos+32768.0;vec2 extrude=vec2(mod(pos_raw,8.0)/7.0*2.0-1.0);vec2 circle_center=floor(pos_raw/8.0)+u_translate;float ele=get_elevation(circle_center);v_visibility=calculate_visibility(projectTileWithElevation(circle_center,ele));if (u_pitch_with_map) { +ivec2 pos_raw=a_pos+32768;vec2 extrude=vec2(pos_raw & 7)/7.0*2.0-1.0;vec2 circle_center=vec2(pos_raw >> 3)+u_translate;float ele=get_elevation(circle_center);v_visibility=calculate_visibility(projectTileWithElevation(circle_center,ele));if (u_pitch_with_map) { #ifdef GLOBE vec3 center_vector=projectToSphere(circle_center); #endif @@ -121,11 +121,11 @@ vec2 angles=extrude*angle_scale;vec3 corner_vector=globeRotateVector(center_vect #else gl_Position=projectTileWithElevation(corner_position,ele); #endif -} else {gl_Position=projectTileWithElevation(circle_center,ele);if (gl_Position.z/gl_Position.w > 1.0) {gl_Position.xy=vec2(10000.0);}if (u_scale_with_map) {gl_Position.xy+=extrude*(radius+stroke_width)*u_extrude_scale*u_camera_to_center_distance;} else {gl_Position.xy+=extrude*(radius+stroke_width)*u_extrude_scale*gl_Position.w;}}float antialiasblur=-max(1.0/u_device_pixel_ratio/(radius+stroke_width),blur);v_data=vec3(extrude.x,extrude.y,antialiasblur);}`,cs=`void main() {fragColor=vec4(1.0);}`;const ls={prelude:J(es,ts),projectionMercator:J(``,`float projectLineThickness(float tileY) {return 1.0;}float projectCircleRadius(float tileY) {return 1.0;}vec4 projectTile(vec2 p) {vec4 result=u_projection_matrix*vec4(p,0.0,1.0);return result;}vec4 projectTile(vec2 p,vec2 rawPos) {vec4 result=u_projection_matrix*vec4(p,0.0,1.0);if (rawPos.y <-32767.5 || rawPos.y > 32766.5) {result.z=-10000000.0;}return result;}vec4 projectTileWithElevation(vec2 posInTile,float elevation) {return u_projection_matrix*vec4(posInTile,elevation,1.0);}vec4 projectTileFor3D(vec2 posInTile,float elevation) {return projectTileWithElevation(posInTile,elevation);}`),projectionGlobe:J(``,`#define GLOBE_RADIUS 6371008.8 -uniform highp vec4 u_projection_tile_mercator_coords;uniform highp vec4 u_projection_clipping_plane;uniform highp float u_projection_transition;uniform mat4 u_projection_fallback_matrix;vec3 globeRotateVector(vec3 vec,vec2 angles) {vec3 axisRight=vec3(vec.z,0.0,-vec.x);vec3 axisUp=cross(axisRight,vec);axisRight=normalize(axisRight);axisUp=normalize(axisUp);vec2 t=tan(angles);return normalize(vec+axisRight*t.x+axisUp*t.y);}mat3 globeGetRotationMatrix(vec3 spherePos) {vec3 axisRight=vec3(spherePos.z,0.0,-spherePos.x);vec3 axisDown=cross(axisRight,spherePos);axisRight=normalize(axisRight);axisDown=normalize(axisDown);return mat3(axisRight,axisDown,spherePos -);}float circumferenceRatioAtTileY(float tileY) {float mercator_pos_y=u_projection_tile_mercator_coords.y+u_projection_tile_mercator_coords.w*tileY;float spherical_y=2.0*atan(exp(PI-(mercator_pos_y*PI*2.0)))-PI*0.5;return cos(spherical_y);}float projectLineThickness(float tileY) {float thickness=1.0/circumferenceRatioAtTileY(tileY); -if (u_projection_transition < 0.999) {return mix(1.0,thickness,u_projection_transition);} else {return thickness;}}vec3 projectToSphere(vec2 translatedPos,vec2 rawPos) {vec2 mercator_pos=u_projection_tile_mercator_coords.xy+u_projection_tile_mercator_coords.zw*translatedPos;vec2 spherical;spherical.x=mercator_pos.x*PI*2.0+PI;spherical.y=2.0*atan(exp(PI-(mercator_pos.y*PI*2.0)))-PI*0.5;float len=cos(spherical.y);vec3 pos=vec3(sin(spherical.x)*len,sin(spherical.y),cos(spherical.x)*len -);if (rawPos.y <-32767.5) {pos=vec3(0.0,1.0,0.0);}if (rawPos.y > 32766.5) {pos=vec3(0.0,-1.0,0.0);}return pos;}vec3 projectToSphere(vec2 posInTile) {return projectToSphere(posInTile,vec2(0.0,0.0));}float globeComputeClippingZ(vec3 spherePos) {return (1.0-(dot(spherePos,u_projection_clipping_plane.xyz)+u_projection_clipping_plane.w));}vec4 interpolateProjection(vec2 posInTile,vec3 spherePos,float elevation) {vec3 elevatedPos=spherePos*(1.0+elevation/GLOBE_RADIUS);vec4 globePosition=u_projection_matrix*vec4(elevatedPos,1.0);globePosition.z=globeComputeClippingZ(elevatedPos)*globePosition.w;if (u_projection_transition > 0.999) {return globePosition;}vec4 flatPosition=u_projection_fallback_matrix*vec4(posInTile,elevation,1.0);const float z_globeness_threshold=0.2;vec4 result=globePosition;result.z=mix(0.0,globePosition.z,clamp((u_projection_transition-z_globeness_threshold)/(1.0-z_globeness_threshold),0.0,1.0));result.xyw=mix(flatPosition.xyw,globePosition.xyw,u_projection_transition);if ((posInTile.y <-32767.5) || (posInTile.y > 32766.5)) {result=globePosition;const float poles_hidden_anim_percentage=0.02;result.z=mix(globePosition.z,100.0,pow(max((1.0-u_projection_transition)/poles_hidden_anim_percentage,0.0),8.0));}return result;}vec4 interpolateProjectionFor3D(vec2 posInTile,vec3 spherePos,float elevation) {vec3 elevatedPos=spherePos*(1.0+elevation/GLOBE_RADIUS);vec4 globePosition=u_projection_matrix*vec4(elevatedPos,1.0);if (u_projection_transition > 0.999) {return globePosition;}vec4 fallbackPosition=u_projection_fallback_matrix*vec4(posInTile,elevation,1.0);return mix(fallbackPosition,globePosition,u_projection_transition);}vec4 projectTile(vec2 posInTile) {return interpolateProjection(posInTile,projectToSphere(posInTile),0.0);}vec4 projectTile(vec2 posInTile,vec2 rawPos) {return interpolateProjection(posInTile,projectToSphere(posInTile,rawPos),0.0);}vec4 projectTileWithElevation(vec2 posInTile,float elevation) {return interpolateProjection(posInTile,projectToSphere(posInTile),elevation);}vec4 projectTileFor3D(vec2 posInTile,float elevation) {vec3 spherePos=projectToSphere(posInTile,posInTile);return interpolateProjectionFor3D(posInTile,spherePos,elevation);}`),background:J(ns,rs),backgroundPattern:J(is,as),circle:J(os,ss),clippingMask:J(cs,`layout(location=0) in vec2 a_pos;void main() {gl_Position=projectTile(a_pos);}`),heatmap:J(`uniform highp float u_intensity;in vec2 v_extrude; +} else {gl_Position=projectTileWithElevation(circle_center,ele);if (gl_Position.z/gl_Position.w > 1.0) {gl_Position.xy=vec2(10000.0);}if (u_scale_with_map) {gl_Position.xy+=extrude*(radius+stroke_width)*u_extrude_scale*u_camera_to_center_distance;} else {gl_Position.xy+=extrude*(radius+stroke_width)*u_extrude_scale*gl_Position.w;}}float antialiasblur=-max(1.0/u_device_pixel_ratio/(radius+stroke_width),blur);v_data=vec3(extrude.x,extrude.y,antialiasblur);}`,Ys=`void main() {fragColor=vec4(1.0);}`;const Xs={prelude:Y(Vs,Hs),projectionMercator:Y(` +void clipAntimeridian() {}`,`float projectLineThickness(float tileY) {return 1.0;}float projectCircleRadius(float tileY) {return 1.0;}vec4 projectTile(vec2 p) {vec4 result=u_projection_matrix*vec4(p,0.0,1.0);return result;}vec4 projectTile(vec2 p,vec2 rawPos) {vec4 result=u_projection_matrix*vec4(p,0.0,1.0);if (rawPos.y <-32767.5 || rawPos.y > 32766.5) {result.z=-10000000.0;}return result;}vec4 projectTileWithElevation(vec2 posInTile,float elevation) {return u_projection_matrix*vec4(posInTile,elevation,1.0);}vec4 projectTileFor3D(vec2 posInTile,float elevation) {return projectTileWithElevation(posInTile,elevation);}`),projectionGlobe:Y(`uniform bool u_projection_clip_antimeridian;in highp float v_projection_tile_x;void clipAntimeridian() {if (u_projection_clip_antimeridian && (v_projection_tile_x < 0.0 || v_projection_tile_x >=8192.0)) {discard;}}`,`#define GLOBE_RADIUS 6371008.8 +uniform highp vec4 u_projection_tile_mercator_coords;uniform highp vec4 u_projection_clipping_plane;uniform highp float u_projection_transition;uniform mat4 u_projection_fallback_matrix;out highp float v_projection_tile_x;vec3 globeRotateVector(vec3 vec,vec2 angles) {vec3 axisRight=vec3(vec.z,0.0,-vec.x);vec3 axisUp=cross(axisRight,vec);axisRight=normalize(axisRight);axisUp=normalize(axisUp);vec2 t=tan(angles);return normalize(vec+axisRight*t.x+axisUp*t.y);}mat3 globeGetRotationMatrix(vec3 spherePos) {vec3 axisRight=vec3(spherePos.z,0.0,-spherePos.x);vec3 axisDown=cross(axisRight,spherePos);axisRight=normalize(axisRight);axisDown=normalize(axisDown);return mat3(axisRight,axisDown,spherePos +);}float circumferenceRatioAtTileY(float tileY) {float mercator_pos_y=u_projection_tile_mercator_coords.y+u_projection_tile_mercator_coords.w*tileY;float t=exp(PI-(mercator_pos_y*PI*2.0));return (2.0*t)/(t*t+1.0);}float projectLineThickness(float tileY) {float thickness=1.0/circumferenceRatioAtTileY(tileY);if (u_projection_transition < 0.999) {return mix(1.0,thickness,u_projection_transition);} else {return thickness;}}vec3 projectToSphere(vec2 translatedPos,vec2 rawPos) {vec2 mercator_pos=u_projection_tile_mercator_coords.xy+u_projection_tile_mercator_coords.zw*translatedPos;float spherical_x=mercator_pos.x*PI*2.0+PI;float t=exp(PI-(mercator_pos.y*PI*2.0));float t2=t*t;float denom=t2+1.0;float sin_sy=(t2-1.0)/denom;float cos_sy=(2.0*t)/denom;vec3 pos=vec3(sin(spherical_x)*cos_sy,sin_sy,cos(spherical_x)*cos_sy +);if (rawPos.y <-32767.5) {pos=vec3(0.0,1.0,0.0);}if (rawPos.y > 32766.5) {pos=vec3(0.0,-1.0,0.0);}return pos;}vec3 projectToSphere(vec2 posInTile) {return projectToSphere(posInTile,vec2(0.0,0.0));}float globeComputeClippingZ(vec3 spherePos) {return (1.0-(dot(spherePos,u_projection_clipping_plane.xyz)+u_projection_clipping_plane.w));}vec4 interpolateProjection(vec2 posInTile,vec3 spherePos,float elevation) {v_projection_tile_x=posInTile.x;vec3 elevatedPos=spherePos*(1.0+elevation/GLOBE_RADIUS);vec4 globePosition=u_projection_matrix*vec4(elevatedPos,1.0);globePosition.z=globeComputeClippingZ(elevatedPos)*globePosition.w;if (u_projection_transition > 0.999) {return globePosition;}vec4 flatPosition=u_projection_fallback_matrix*vec4(posInTile,elevation,1.0);const float z_globeness_threshold=0.2;vec4 result=globePosition;result.z=mix(0.0,globePosition.z,clamp((u_projection_transition-z_globeness_threshold)/(1.0-z_globeness_threshold),0.0,1.0));result.xyw=mix(flatPosition.xyw,globePosition.xyw,u_projection_transition);if ((posInTile.y <-32767.5) || (posInTile.y > 32766.5)) {result=globePosition;const float poles_hidden_anim_percentage=0.02;result.z=mix(globePosition.z,100.0,pow(max((1.0-u_projection_transition)/poles_hidden_anim_percentage,0.0),8.0));}return result;}vec4 interpolateProjectionFor3D(vec2 posInTile,vec3 spherePos,float elevation) {v_projection_tile_x=posInTile.x;vec3 elevatedPos=spherePos*(1.0+elevation/GLOBE_RADIUS);vec4 globePosition=u_projection_matrix*vec4(elevatedPos,1.0);if (u_projection_transition > 0.999) {return globePosition;}vec4 fallbackPosition=u_projection_fallback_matrix*vec4(posInTile,elevation,1.0);return mix(fallbackPosition,globePosition,u_projection_transition);}vec4 projectTile(vec2 posInTile) {return interpolateProjection(posInTile,projectToSphere(posInTile),0.0);}vec4 projectTile(vec2 posInTile,vec2 rawPos) {return interpolateProjection(posInTile,projectToSphere(posInTile,rawPos),0.0);}vec4 projectTileWithElevation(vec2 posInTile,float elevation) {return interpolateProjection(posInTile,projectToSphere(posInTile),elevation);}vec4 projectTileFor3D(vec2 posInTile,float elevation) {vec3 spherePos=projectToSphere(posInTile,posInTile);return interpolateProjectionFor3D(posInTile,spherePos,elevation);}`),background:Y(Us,Ws),backgroundPattern:Y(Gs,Ks),circle:Y(qs,Js),clippingMask:Y(Ys,`layout(location=0) in vec2 a_pos;void main() {gl_Position=projectTile(a_pos);}`),heatmap:Y(`uniform highp float u_intensity;in vec2 v_extrude; #pragma maplibre: define highp float weight #define GAUSS_COEF 0.3989422804014327 void main() { @@ -134,7 +134,7 @@ float d=-0.5*3.0*3.0*dot(v_extrude,v_extrude);float val=weight*u_intensity*GAUSS #ifdef OVERDRAW_INSPECTOR fragColor=vec4(1.0); #endif -}`,`uniform float u_extrude_scale;uniform float u_opacity;uniform float u_intensity;uniform highp float u_globe_extrude_scale;layout(location=0) in vec2 a_pos;out vec2 v_extrude; +}`,`uniform float u_extrude_scale;uniform float u_opacity;uniform float u_intensity;uniform highp float u_globe_extrude_scale;layout(location=0) in ivec2 a_pos;out vec2 v_extrude; #pragma maplibre: define highp float weight #pragma maplibre: define mediump float radius const highp float ZERO=1.0/255.0/16.0; @@ -142,31 +142,31 @@ const highp float ZERO=1.0/255.0/16.0; void main(void) { #pragma maplibre: initialize highp float weight #pragma maplibre: initialize mediump float radius -vec2 pos_raw=a_pos+32768.0;vec2 unscaled_extrude=vec2(mod(pos_raw,8.0)/7.0*2.0-1.0);float S=sqrt(-2.0*log(ZERO/weight/u_intensity/GAUSS_COEF))/3.0;v_extrude=S*unscaled_extrude;vec2 extrude=v_extrude*radius*u_extrude_scale;vec2 circle_center=floor(pos_raw/8.0); +ivec2 pos_raw=a_pos+32768;vec2 unscaled_extrude=vec2(pos_raw & 7)/7.0*2.0-1.0;float S=sqrt(-2.0*log(ZERO/weight/u_intensity/GAUSS_COEF))/3.0;v_extrude=S*unscaled_extrude;vec2 extrude=v_extrude*radius*u_extrude_scale;vec2 circle_center=vec2(pos_raw >> 3); #ifdef GLOBE vec2 angles=v_extrude*radius*u_globe_extrude_scale;vec3 center_vector=projectToSphere(circle_center);vec3 corner_vector=globeRotateVector(center_vector,angles);gl_Position=interpolateProjection(circle_center+extrude,corner_vector,0.0); #else gl_Position=projectTileFor3D(circle_center+extrude,get_elevation(circle_center)); #endif -}`),heatmapTexture:J(`uniform sampler2D u_image;uniform sampler2D u_color_ramp;uniform float u_opacity;in vec2 v_pos;void main() {float t=texture(u_image,v_pos).r;vec4 color=texture(u_color_ramp,vec2(t,0.5));fragColor=color*u_opacity; +}`),heatmapTexture:Y(`uniform sampler2D u_image;uniform sampler2D u_color_ramp;uniform float u_opacity;in vec2 v_pos;void main() {float t=texture(u_image,v_pos).r;vec4 color=texture(u_color_ramp,vec2(t,0.5));fragColor=color*u_opacity; #ifdef OVERDRAW_INSPECTOR fragColor=vec4(0.0); #endif -}`,`uniform mat4 u_matrix;uniform vec2 u_world;layout(location=0) in vec2 a_pos;out vec2 v_pos;void main() {gl_Position=u_matrix*vec4(a_pos*u_world,0,1);v_pos.x=a_pos.x;v_pos.y=1.0-a_pos.y;}`),collisionBox:J(`flat in float v_placed;flat in float v_notUsed;void main() {float alpha=0.5;fragColor=vec4(1.0,0.0,0.0,1.0)*alpha;if (v_placed > 0.5) {fragColor=vec4(0.0,0.0,1.0,0.5)*alpha;}if (v_notUsed > 0.5) {fragColor*=.1;}}`,`layout(location=0) in vec2 a_anchor_pos;layout(location=1) in vec2 a_placed;layout(location=2) in vec2 a_box_real;uniform vec2 u_pixel_extrude_scale;flat out float v_placed;flat out float v_notUsed;void main() {gl_Position=projectTileWithElevation(a_anchor_pos,get_elevation(a_anchor_pos));gl_Position.xy=((a_box_real+0.5)*u_pixel_extrude_scale*2.0-1.0)*vec2(1.0,-1.0)*gl_Position.w;if (gl_Position.z/gl_Position.w < 1.1) {gl_Position.z=0.5;}v_placed=a_placed.x;v_notUsed=a_placed.y;}`),collisionCircle:J(`flat in float v_radius;in vec2 v_extrude;flat in float v_collision;void main() {float alpha=0.5;float stroke_radius=0.9;float distance_to_center=length(v_extrude);float distance_to_edge=abs(distance_to_center-v_radius);float opacity_t=smoothstep(-stroke_radius,0.0,-distance_to_edge);vec4 color=mix(vec4(0.0,0.0,1.0,0.5),vec4(1.0,0.0,0.0,1.0),v_collision);fragColor=color*alpha*opacity_t;}`,`layout(location=0) in vec2 a_pos;layout(location=1) in float a_radius;layout(location=2) in vec2 a_flags;uniform vec2 u_viewport_size;flat out float v_radius;out vec2 v_extrude;flat out float v_collision;void main() {float radius=a_radius;float collision=a_flags.x;float vertexIdx=a_flags.y;vec2 quadVertexOffset=vec2(mix(-1.0,1.0,float(vertexIdx >=2.0)),mix(-1.0,1.0,float(vertexIdx >=1.0 && vertexIdx <=2.0)));vec2 quadVertexExtent=quadVertexOffset*radius;float padding_factor=1.2;v_radius=radius;v_extrude=quadVertexExtent*padding_factor;v_collision=collision;gl_Position=vec4((a_pos/u_viewport_size*2.0-1.0)*vec2(1.0,-1.0),0.0,1.0)+vec4(quadVertexExtent*padding_factor/u_viewport_size*2.0,0.0,0.0);}`),colorRelief:J(`#ifdef GL_ES +}`,`uniform mat4 u_matrix;uniform vec2 u_world;layout(location=0) in vec2 a_pos;out vec2 v_pos;void main() {gl_Position=u_matrix*vec4(a_pos*u_world,0,1);v_pos.x=a_pos.x;v_pos.y=1.0-a_pos.y;}`),collisionBox:Y(`flat in float v_placed;flat in float v_notUsed;void main() {float alpha=0.5;fragColor=vec4(1.0,0.0,0.0,1.0)*alpha;if (v_placed > 0.5) {fragColor=vec4(0.0,0.0,1.0,0.5)*alpha;}if (v_notUsed > 0.5) {fragColor*=.1;}}`,`layout(location=0) in vec2 a_anchor_pos;layout(location=1) in vec2 a_placed;layout(location=2) in vec2 a_box_real;uniform vec2 u_pixel_extrude_scale;flat out float v_placed;flat out float v_notUsed;void main() {gl_Position=projectTileWithElevation(a_anchor_pos,get_elevation(a_anchor_pos));gl_Position.xy=((a_box_real+0.5)*u_pixel_extrude_scale*2.0-1.0)*vec2(1.0,-1.0)*gl_Position.w;if (gl_Position.z/gl_Position.w < 1.1) {gl_Position.z=0.5;}v_placed=a_placed.x;v_notUsed=a_placed.y;}`),collisionCircle:Y(`flat in float v_radius;in vec2 v_extrude;flat in float v_collision;void main() {float alpha=0.5;float stroke_radius=0.9;float distance_to_center=length(v_extrude);float distance_to_edge=abs(distance_to_center-v_radius);float opacity_t=smoothstep(-stroke_radius,0.0,-distance_to_edge);vec4 color=mix(vec4(0.0,0.0,1.0,0.5),vec4(1.0,0.0,0.0,1.0),v_collision);fragColor=color*alpha*opacity_t;}`,`layout(location=0) in vec2 a_pos;layout(location=1) in float a_radius;layout(location=2) in vec2 a_flags;uniform vec2 u_viewport_size;flat out float v_radius;out vec2 v_extrude;flat out float v_collision;void main() {float radius=a_radius;float collision=a_flags.x;float vertexIdx=a_flags.y;vec2 quadVertexOffset=vec2(mix(-1.0,1.0,float(vertexIdx >=2.0)),mix(-1.0,1.0,float(vertexIdx >=1.0 && vertexIdx <=2.0)));vec2 quadVertexExtent=quadVertexOffset*radius;float padding_factor=1.2;v_radius=radius;v_extrude=quadVertexExtent*padding_factor;v_collision=collision;gl_Position=vec4((a_pos/u_viewport_size*2.0-1.0)*vec2(1.0,-1.0),0.0,1.0)+vec4(quadVertexExtent*padding_factor/u_viewport_size*2.0,0.0,0.0);}`),colorRelief:Y(`#ifdef GL_ES precision highp float; #endif -uniform sampler2D u_image;uniform vec4 u_unpack;uniform sampler2D u_elevation_stops;uniform sampler2D u_color_stops;uniform int u_color_ramp_size;uniform float u_opacity;in vec2 v_pos;float getElevation(vec2 coord) {vec4 data=texture(u_image,coord)*255.0;data.a=-1.0;return dot(data,u_unpack);}float getElevationStop(int stop) {float x=(float(stop)+0.5)/float(u_color_ramp_size);vec4 data=texture(u_elevation_stops,vec2(x,0))*255.0;data.a=-1.0;return dot(data,u_unpack);}void main() {float el=getElevation(v_pos);int r=(u_color_ramp_size-1);int l=0;float el_l=getElevationStop(l);float el_r=getElevationStop(r);while(r-l > 1){int m=(r+l)/2;float el_m=getElevationStop(m);if(el < el_m){r=m;el_r=el_m;}else +uniform sampler2D u_image;uniform vec4 u_unpack;uniform sampler2D u_elevation_stops;uniform sampler2D u_color_stops;uniform int u_color_ramp_size;uniform float u_opacity;in vec2 v_pos;float getElevation(vec2 coord) {vec4 data=texture(u_image,coord)*255.0;data.a=-1.0;return dot(data,u_unpack);}float getElevationStop(int stop) {vec4 data=texelFetch(u_elevation_stops,ivec2(stop,0),0)*255.0;data.a=-1.0;return dot(data,u_unpack);}void main() {float el=getElevation(v_pos);int r=(u_color_ramp_size-1);int l=0;float el_l=getElevationStop(l);float el_r=getElevationStop(r);while(r-l > 1){int m=(r+l)/2;float el_m=getElevationStop(m);if(el < el_m){r=m;el_r=el_m;}else {l=m;el_l=el_m;}}float x=(float(l)+(el-el_l)/(el_r-el_l)+0.5)/float(u_color_ramp_size);fragColor=u_opacity*texture(u_color_stops,vec2(x,0)); #ifdef OVERDRAW_INSPECTOR fragColor=vec4(1.0); #endif -}`,`uniform vec2 u_dimension;layout(location=0) in vec2 a_pos;out vec2 v_pos;void main() {gl_Position=projectTile(a_pos,a_pos);highp vec2 epsilon=1.0/u_dimension;float scale=(u_dimension.x-2.0)/u_dimension.x;v_pos=(a_pos/8192.0)*scale+epsilon;if (a_pos.y <-32767.5) {v_pos.y=0.0;}if (a_pos.y > 32766.5) {v_pos.y=1.0;}}`),debug:J(`uniform highp vec4 u_color;uniform sampler2D u_overlay;in vec2 v_uv;void main() {vec4 overlay_color=texture(u_overlay,v_uv);fragColor=mix(u_color,overlay_color,overlay_color.a);}`,`layout(location=0) in vec2 a_pos;out vec2 v_uv;uniform float u_overlay_scale;void main() {v_uv=a_pos/8192.0;gl_Position=projectTileWithElevation(a_pos*u_overlay_scale,get_elevation(a_pos));}`),depth:J(cs,`layout(location=0) in vec2 a_pos;void main() { +}`,`uniform vec2 u_dimension;layout(location=0) in vec2 a_pos;out vec2 v_pos;void main() {gl_Position=projectTile(a_pos,a_pos);highp vec2 epsilon=1.0/u_dimension;float scale=(u_dimension.x-2.0)/u_dimension.x;v_pos=(a_pos/8192.0)*scale+epsilon;if (a_pos.y <-32767.5) {v_pos.y=0.0;}if (a_pos.y > 32766.5) {v_pos.y=1.0;}}`),debug:Y(`uniform highp vec4 u_color;uniform sampler2D u_overlay;in vec2 v_uv;void main() {vec4 overlay_color=texture(u_overlay,v_uv);fragColor=mix(u_color,overlay_color,overlay_color.a);}`,`layout(location=0) in vec2 a_pos;out vec2 v_uv;uniform float u_overlay_scale;void main() {v_uv=a_pos/8192.0;gl_Position=projectTileWithElevation(a_pos*u_overlay_scale,get_elevation(a_pos));}`),depth:Y(Ys,`layout(location=0) in vec2 a_pos;void main() { #ifdef GLOBE gl_Position=projectTileFor3D(a_pos,0.0); #else gl_Position=u_projection_matrix*vec4(a_pos,0.0,1.0); #endif -}`),fill:J(`#pragma maplibre: define highp vec4 color +}`),fill:Y(`#pragma maplibre: define highp vec4 color #pragma maplibre: define lowp float opacity void main() { #pragma maplibre: initialize highp vec4 color @@ -181,7 +181,7 @@ fragColor=vec4(1.0); void main() { #pragma maplibre: initialize highp vec4 color #pragma maplibre: initialize lowp float opacity -if (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}gl_Position=projectTile(a_pos+u_fill_translate,a_pos);}`),fillOutline:J(`in vec2 v_pos; +if (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}gl_Position=projectTile(a_pos+u_fill_translate,a_pos);}`),fillOutline:Y(`in vec2 v_pos; #ifdef GLOBE in float v_depth; #endif @@ -210,7 +210,7 @@ if (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}gl_Position=pr #ifdef GLOBE v_depth=gl_Position.z/gl_Position.w; #endif -}`),fillOutlinePattern:J(`uniform vec2 u_texsize;uniform sampler2D u_image;uniform float u_fade;in vec2 v_pos_a;in vec2 v_pos_b;in vec2 v_pos; +}`),fillOutlinePattern:Y(`uniform vec2 u_texsize;uniform sampler2D u_image;uniform float u_fade;in vec2 v_pos_a;in vec2 v_pos_b;in vec2 v_pos; #ifdef GLOBE in float v_depth; #endif @@ -247,7 +247,7 @@ if (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}vec2 pattern_t #ifdef GLOBE v_depth=gl_Position.z/gl_Position.w; #endif -}`),fillPattern:J(`#ifdef GL_ES +}`),fillPattern:Y(`#ifdef GL_ES precision highp float; #endif uniform vec2 u_texsize;uniform float u_fade;uniform sampler2D u_image;in vec2 v_pos_a;in vec2 v_pos_b; @@ -274,11 +274,11 @@ void main() { #pragma maplibre: initialize mediump vec4 pattern_to #pragma maplibre: initialize lowp float pixel_ratio_from #pragma maplibre: initialize lowp float pixel_ratio_to -if (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}vec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;float tileZoomRatio=u_scale.x;float fromScale=u_scale.y;float toScale=u_scale.z;vec2 display_size_a=(pattern_br_a-pattern_tl_a)/pixel_ratio_from;vec2 display_size_b=(pattern_br_b-pattern_tl_b)/pixel_ratio_to;gl_Position=projectTile(a_pos+u_fill_translate,a_pos);v_pos_a=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,fromScale*display_size_a,tileZoomRatio,a_pos);v_pos_b=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,toScale*display_size_b,tileZoomRatio,a_pos);}`),fillExtrusion:J(`in vec4 v_color;void main() {fragColor=v_color; +if (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}vec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;float tileZoomRatio=u_scale.x;float fromScale=u_scale.y;float toScale=u_scale.z;vec2 display_size_a=(pattern_br_a-pattern_tl_a)/pixel_ratio_from;vec2 display_size_b=(pattern_br_b-pattern_tl_b)/pixel_ratio_to;gl_Position=projectTile(a_pos+u_fill_translate,a_pos);v_pos_a=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,fromScale*display_size_a,tileZoomRatio,a_pos);v_pos_b=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,toScale*display_size_b,tileZoomRatio,a_pos);}`),fillExtrusion:Y(`in vec4 v_color;void main() {fragColor=v_color; #ifdef OVERDRAW_INSPECTOR fragColor=vec4(1.0); #endif -}`,`uniform vec3 u_lightcolor;uniform lowp vec3 u_lightpos;uniform lowp vec3 u_lightpos_globe;uniform lowp float u_lightintensity;uniform float u_vertical_gradient;uniform lowp float u_opacity;uniform vec2 u_fill_translate;layout(location=0) in vec2 a_pos;layout(location=1) in vec4 a_normal_ed; +}`,`uniform vec3 u_lightcolor;uniform lowp vec3 u_lightpos;uniform lowp vec3 u_lightpos_globe;uniform lowp float u_lightintensity;uniform float u_vertical_gradient;uniform lowp float u_opacity;uniform vec2 u_fill_translate;layout(location=0) in vec2 a_pos;layout(location=1) in ivec4 a_normal_ed; #ifdef TERRAIN3D layout(location=2) in vec2 a_centroid; #endif @@ -290,13 +290,13 @@ void main() { #pragma maplibre: initialize highp float base #pragma maplibre: initialize highp float height #pragma maplibre: initialize highp vec4 color -vec3 normal=a_normal_ed.xyz; +vec3 normal=vec3(a_normal_ed.xyz); #ifdef TERRAIN3D float height_terrain3d_offset=get_elevation(a_centroid);float base_terrain3d_offset=height_terrain3d_offset-(base > 0.0 ? 0.0 : 10.0); #else float height_terrain3d_offset=0.0;float base_terrain3d_offset=0.0; #endif -base=max(0.0,base)+base_terrain3d_offset;height=max(0.0,height)+height_terrain3d_offset;float t=mod(normal.x,2.0);float elevation=t > 0.0 ? height : base;vec2 posInTile=a_pos+u_fill_translate; +base=max(0.0,base)+base_terrain3d_offset;height=max(0.0,height)+height_terrain3d_offset;float t=float(a_normal_ed.x & 1);float elevation=t > 0.0 ? height : base;vec2 posInTile=a_pos+u_fill_translate; #ifdef GLOBE vec3 spherePos=projectToSphere(posInTile,a_pos);gl_Position=interpolateProjectionFor3D(posInTile,spherePos,elevation); #else @@ -306,7 +306,7 @@ float colorvalue=color.r*0.2126+color.g*0.7152+color.b*0.0722;v_color=vec4(0.0,0 #ifdef GLOBE mat3 rotMatrix=globeGetRotationMatrix(spherePos);normalForLighting=rotMatrix*normalForLighting;directional=mix(directional,clamp(dot(normalForLighting,u_lightpos_globe),0.0,1.0),u_projection_transition); #endif -directional=mix((1.0-u_lightintensity),max((1.0-colorvalue+u_lightintensity),1.0),directional);if (normal.y !=0.0) {directional*=((1.0-u_vertical_gradient)+(u_vertical_gradient*clamp((t+base)*pow(height/150.0,0.5),mix(0.7,0.98,1.0-u_lightintensity),1.0)));}v_color.r+=clamp(color.r*directional*u_lightcolor.r,mix(0.0,0.3,1.0-u_lightcolor.r),1.0);v_color.g+=clamp(color.g*directional*u_lightcolor.g,mix(0.0,0.3,1.0-u_lightcolor.g),1.0);v_color.b+=clamp(color.b*directional*u_lightcolor.b,mix(0.0,0.3,1.0-u_lightcolor.b),1.0);v_color*=u_opacity;}`),fillExtrusionPattern:J(`uniform vec2 u_texsize;uniform float u_fade;uniform sampler2D u_image;in vec2 v_pos_a;in vec2 v_pos_b;in vec4 v_lighting; +directional=mix((1.0-u_lightintensity),max((1.0-colorvalue+u_lightintensity),1.0),directional);if (normal.y !=0.0) {directional*=((1.0-u_vertical_gradient)+(u_vertical_gradient*clamp((t+base)*pow(height/150.0,0.5),mix(0.7,0.98,1.0-u_lightintensity),1.0)));}v_color.r+=clamp(color.r*directional*u_lightcolor.r,mix(0.0,0.3,1.0-u_lightcolor.r),1.0);v_color.g+=clamp(color.g*directional*u_lightcolor.g,mix(0.0,0.3,1.0-u_lightcolor.g),1.0);v_color.b+=clamp(color.b*directional*u_lightcolor.b,mix(0.0,0.3,1.0-u_lightcolor.b),1.0);v_color*=u_opacity;}`),fillExtrusionPattern:Y(`uniform vec2 u_texsize;uniform float u_fade;uniform sampler2D u_image;in vec2 v_pos_a;in vec2 v_pos_b;in vec4 v_lighting; #pragma maplibre: define lowp float base #pragma maplibre: define lowp float height #pragma maplibre: define lowp vec4 pattern_from @@ -324,7 +324,7 @@ vec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern #ifdef OVERDRAW_INSPECTOR fragColor=vec4(1.0); #endif -}`,`uniform vec2 u_pixel_coord_upper;uniform vec2 u_pixel_coord_lower;uniform float u_height_factor;uniform vec3 u_scale;uniform float u_vertical_gradient;uniform lowp float u_opacity;uniform vec2 u_fill_translate;uniform vec3 u_lightcolor;uniform lowp vec3 u_lightpos;uniform lowp vec3 u_lightpos_globe;uniform lowp float u_lightintensity;layout(location=0) in vec2 a_pos;layout(location=1) in vec4 a_normal_ed; +}`,`uniform vec2 u_pixel_coord_upper;uniform vec2 u_pixel_coord_lower;uniform float u_height_factor;uniform vec3 u_scale;uniform float u_vertical_gradient;uniform lowp float u_opacity;uniform vec2 u_fill_translate;uniform vec3 u_lightcolor;uniform lowp vec3 u_lightpos;uniform lowp vec3 u_lightpos_globe;uniform lowp float u_lightintensity;layout(location=0) in vec2 a_pos;layout(location=1) in ivec4 a_normal_ed; #ifdef TERRAIN3D layout(location=2) in vec2 a_centroid; #endif @@ -345,28 +345,28 @@ void main() { #pragma maplibre: initialize mediump vec4 pattern_to #pragma maplibre: initialize lowp float pixel_ratio_from #pragma maplibre: initialize lowp float pixel_ratio_to -vec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;float tileRatio=u_scale.x;float fromScale=u_scale.y;float toScale=u_scale.z;vec3 normal=a_normal_ed.xyz;float edgedistance=a_normal_ed.w;vec2 display_size_a=(pattern_br_a-pattern_tl_a)/pixel_ratio_from;vec2 display_size_b=(pattern_br_b-pattern_tl_b)/pixel_ratio_to; +vec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;float tileRatio=u_scale.x;float fromScale=u_scale.y;float toScale=u_scale.z;vec3 normal=vec3(a_normal_ed.xyz);float edgedistance=float(a_normal_ed.w);vec2 display_size_a=(pattern_br_a-pattern_tl_a)/pixel_ratio_from;vec2 display_size_b=(pattern_br_b-pattern_tl_b)/pixel_ratio_to; #ifdef TERRAIN3D float height_terrain3d_offset=get_elevation(a_centroid);float base_terrain3d_offset=height_terrain3d_offset-(base > 0.0 ? 0.0 : 10.0); #else float height_terrain3d_offset=0.0;float base_terrain3d_offset=0.0; #endif -base=max(0.0,base)+base_terrain3d_offset;height=max(0.0,height)+height_terrain3d_offset;float t=mod(normal.x,2.0);float elevation=t > 0.0 ? height : base;vec2 posInTile=a_pos+u_fill_translate; +base=max(0.0,base)+base_terrain3d_offset;height=max(0.0,height)+height_terrain3d_offset;float t=float(a_normal_ed.x & 1);float elevation=t > 0.0 ? height : base;vec2 posInTile=a_pos+u_fill_translate; #ifdef GLOBE vec3 spherePos=projectToSphere(posInTile,a_pos);vec3 elevatedPos=spherePos*(1.0+elevation/GLOBE_RADIUS);v_sphere_pos=elevatedPos;gl_Position=interpolateProjectionFor3D(posInTile,spherePos,elevation); #else gl_Position=u_projection_matrix*vec4(posInTile,elevation,1.0); #endif -vec2 pos=normal.x==1.0 && normal.y==0.0 && normal.z==16384.0 +vec2 pos=a_normal_ed.x==1 && a_normal_ed.y==0 && a_normal_ed.z==16384 ? a_pos -: vec2(edgedistance,elevation*u_height_factor);v_pos_a=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,fromScale*display_size_a,tileRatio,pos);v_pos_b=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,toScale*display_size_b,tileRatio,pos);v_lighting=vec4(0.0,0.0,0.0,1.0);float directional=clamp(dot(normal/16383.0,u_lightpos),0.0,1.0);directional=mix((1.0-u_lightintensity),max((0.5+u_lightintensity),1.0),directional);if (normal.y !=0.0) {directional*=((1.0-u_vertical_gradient)+(u_vertical_gradient*clamp((t+base)*pow(height/150.0,0.5),mix(0.7,0.98,1.0-u_lightintensity),1.0)));}v_lighting.rgb+=clamp(directional*u_lightcolor,mix(vec3(0.0),vec3(0.3),1.0-u_lightcolor),vec3(1.0));v_lighting*=u_opacity;}`),hillshadePrepare:J(`#ifdef GL_ES +: vec2(edgedistance,elevation*u_height_factor);v_pos_a=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,fromScale*display_size_a,tileRatio,pos);v_pos_b=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,toScale*display_size_b,tileRatio,pos);v_lighting=vec4(0.0,0.0,0.0,1.0);float directional=clamp(dot(normal/16383.0,u_lightpos),0.0,1.0);directional=mix((1.0-u_lightintensity),max((0.5+u_lightintensity),1.0),directional);if (normal.y !=0.0) {directional*=((1.0-u_vertical_gradient)+(u_vertical_gradient*clamp((t+base)*pow(height/150.0,0.5),mix(0.7,0.98,1.0-u_lightintensity),1.0)));}v_lighting.rgb+=clamp(directional*u_lightcolor,mix(vec3(0.0),vec3(0.3),1.0-u_lightcolor),vec3(1.0));v_lighting*=u_opacity;}`),hillshadePrepare:Y(`#ifdef GL_ES precision highp float; #endif -uniform sampler2D u_image;in vec2 v_pos;uniform vec2 u_dimension;uniform float u_zoom;uniform vec4 u_unpack;float getElevation(vec2 coord,float bias) {vec4 data=texture(u_image,coord)*255.0;data.a=-1.0;return dot(data,u_unpack);}void main() {vec2 epsilon=1.0/u_dimension;float tileSize=u_dimension.x-2.0;float a=getElevation(v_pos+vec2(-epsilon.x,-epsilon.y),0.0);float b=getElevation(v_pos+vec2(0,-epsilon.y),0.0);float c=getElevation(v_pos+vec2(epsilon.x,-epsilon.y),0.0);float d=getElevation(v_pos+vec2(-epsilon.x,0),0.0);float e=getElevation(v_pos,0.0);float f=getElevation(v_pos+vec2(epsilon.x,0),0.0);float g=getElevation(v_pos+vec2(-epsilon.x,epsilon.y),0.0);float h=getElevation(v_pos+vec2(0,epsilon.y),0.0);float i=getElevation(v_pos+vec2(epsilon.x,epsilon.y),0.0);float exaggerationFactor=u_zoom < 2.0 ? 0.4 : u_zoom < 4.5 ? 0.35 : 0.3;float exaggeration=u_zoom < 15.0 ? (u_zoom-15.0)*exaggerationFactor : 0.0;vec2 deriv=vec2((c+f+f+i)-(a+d+d+g),(g+h+h+i)-(a+b+b+c))*tileSize/pow(2.0,exaggeration+(28.2562-u_zoom));fragColor=clamp(vec4(deriv.x/8.0+0.5,deriv.y/8.0+0.5,1.0,1.0),0.0,1.0); +uniform sampler2D u_image;in vec2 v_pos;uniform vec2 u_dimension;uniform float u_zoom;uniform vec4 u_unpack;float getElevation(ivec2 texel) {vec4 data=texelFetch(u_image,texel,0)*255.0;data.a=-1.0;return dot(data,u_unpack);}void main() {ivec2 pos=ivec2(gl_FragCoord.xy)+ivec2(1);float tileSize=u_dimension.x-2.0;float a=getElevation(pos+ivec2(-1,-1));float b=getElevation(pos+ivec2(0,-1));float c=getElevation(pos+ivec2(1,-1));float d=getElevation(pos+ivec2(-1,0));float e=getElevation(pos);float f=getElevation(pos+ivec2(1,0));float g=getElevation(pos+ivec2(-1,1));float h=getElevation(pos+ivec2(0,1));float i=getElevation(pos+ivec2(1,1));float exaggerationFactor=u_zoom < 2.0 ? 0.4 : u_zoom < 4.5 ? 0.35 : 0.3;float exaggeration=u_zoom < 15.0 ? (u_zoom-15.0)*exaggerationFactor : 0.0;vec2 deriv=vec2((c+f+f+i)-(a+d+d+g),(g+h+h+i)-(a+b+b+c))*tileSize/pow(2.0,exaggeration+(28.2562-u_zoom));fragColor=clamp(vec4(deriv.x/8.0+0.5,deriv.y/8.0+0.5,1.0,1.0),0.0,1.0); #ifdef OVERDRAW_INSPECTOR fragColor=vec4(1.0); #endif -}`,`uniform mat4 u_matrix;uniform vec2 u_dimension;layout(location=0) in vec2 a_pos;layout(location=1) in vec2 a_texture_pos;out vec2 v_pos;void main() {gl_Position=u_matrix*vec4(a_pos,0,1);highp vec2 epsilon=1.0/u_dimension;float scale=(u_dimension.x-2.0)/u_dimension.x;v_pos=(a_texture_pos/8192.0)*scale+epsilon;}`),hillshade:J(`uniform sampler2D u_image;in vec2 v_pos;uniform vec2 u_latrange;uniform float u_exaggeration;uniform vec4 u_accent;uniform int u_method;uniform float u_altitudes[NUM_ILLUMINATION_SOURCES];uniform float u_azimuths[NUM_ILLUMINATION_SOURCES];uniform vec4 u_shadows[NUM_ILLUMINATION_SOURCES];uniform vec4 u_highlights[NUM_ILLUMINATION_SOURCES]; +}`,`uniform mat4 u_matrix;uniform vec2 u_dimension;layout(location=0) in vec2 a_pos;layout(location=1) in vec2 a_texture_pos;out vec2 v_pos;void main() {gl_Position=u_matrix*vec4(a_pos,0,1);highp vec2 epsilon=1.0/u_dimension;float scale=(u_dimension.x-2.0)/u_dimension.x;v_pos=(a_texture_pos/8192.0)*scale+epsilon;}`),hillshade:Y(`uniform sampler2D u_image;in vec2 v_pos;uniform vec2 u_latrange;uniform float u_exaggeration;uniform vec4 u_accent;uniform int u_method;uniform float u_altitudes[NUM_ILLUMINATION_SOURCES];uniform float u_azimuths[NUM_ILLUMINATION_SOURCES];uniform vec4 u_shadows[NUM_ILLUMINATION_SOURCES];uniform vec4 u_highlights[NUM_ILLUMINATION_SOURCES]; #define PI 3.141592653589793 #define STANDARD 0 #define COMBINED 1 @@ -379,7 +379,7 @@ float get_aspect(vec2 deriv){return deriv.x !=0.0 ? atan(deriv.y,-deriv.x) : PI/ #ifdef OVERDRAW_INSPECTOR fragColor=vec4(1.0); #endif -}`,`uniform mat4 u_matrix;layout(location=0) in vec2 a_pos;out vec2 v_pos;void main() {gl_Position=projectTile(a_pos,a_pos);v_pos=a_pos/8192.0;if (a_pos.y <-32767.5) {v_pos.y=0.0;}if (a_pos.y > 32766.5) {v_pos.y=1.0;}}`),line:J(`uniform lowp float u_device_pixel_ratio;flat in vec2 v_width2;in vec2 v_normal;in float v_gamma_scale; +}`,`uniform mat4 u_matrix;layout(location=0) in vec2 a_pos;out vec2 v_pos;void main() {gl_Position=projectTile(a_pos,a_pos);v_pos=a_pos/8192.0;if (a_pos.y <-32767.5) {v_pos.y=0.0;}if (a_pos.y > 32766.5) {v_pos.y=1.0;}}`),line:Y(`uniform lowp float u_device_pixel_ratio;flat in vec2 v_width2;in vec2 v_normal;in float v_gamma_scale; #ifdef GLOBE in float v_depth; #endif @@ -390,7 +390,7 @@ void main() { #pragma maplibre: initialize highp vec4 color #pragma maplibre: initialize lowp float blur #pragma maplibre: initialize lowp float opacity -float dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);fragColor=color*(alpha*opacity); +clipAntimeridian();float dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);fragColor=color*(alpha*opacity); #ifdef GLOBE if (v_depth > 1.0) {discard;} #endif @@ -399,7 +399,7 @@ fragColor=vec4(1.0); #endif }`,` #define scale 0.015873016 -layout(location=0) in vec2 a_pos_normal;layout(location=1) in vec4 a_data;uniform vec2 u_translation;uniform mediump float u_ratio;uniform vec2 u_units_to_pixels;uniform lowp float u_device_pixel_ratio;out vec2 v_normal;flat out vec2 v_width2;out float v_gamma_scale;out highp float v_linesofar; +layout(location=0) in ivec2 a_pos_normal;layout(location=1) in uvec4 a_data;uniform vec2 u_translation;uniform mediump float u_ratio;uniform vec2 u_units_to_pixels;uniform lowp float u_device_pixel_ratio;out vec2 v_normal;flat out vec2 v_width2;out float v_gamma_scale;out highp float v_linesofar; #ifdef GLOBE out float v_depth; #endif @@ -416,7 +416,7 @@ void main() { #pragma maplibre: initialize mediump float gapwidth #pragma maplibre: initialize lowp float offset #pragma maplibre: initialize mediump float width -if (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}float ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=a_data.xy-128.0;float a_direction=mod(a_data.z,4.0)-1.0;v_linesofar=(floor(a_data.z/4.0)+a_data.w*64.0)*2.0;vec2 pos=floor(a_pos_normal*0.5);mediump vec2 normal=a_pos_normal-2.0*pos;normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);float adjustedThickness=projectLineThickness(pos.y);vec4 projected_no_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation);vec4 projected_with_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation+dist/u_ratio*adjustedThickness);gl_Position=projected_with_extrude; +if (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}float ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=vec2(ivec2(a_data.xy)-128);float a_direction=float(int(a_data.z & 3u)-1);v_linesofar=float((a_data.z >> 2u)+a_data.w*64u)*2.0;vec2 pos=vec2(a_pos_normal >> 1);mediump vec2 normal=vec2(a_pos_normal & 1);normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);float adjustedThickness=projectLineThickness(pos.y);vec4 projected_no_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation);vec4 projected_with_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation+dist/u_ratio*adjustedThickness);gl_Position=projected_with_extrude; #ifdef GLOBE v_depth=gl_Position.z/gl_Position.w; #endif @@ -425,7 +425,7 @@ v_gamma_scale=1.0; #else float extrude_length_without_perspective=length(dist);float extrude_length_with_perspective=length((projected_with_extrude.xy-projected_no_extrude.xy)/projected_with_extrude.w*u_units_to_pixels);v_gamma_scale=extrude_length_without_perspective/extrude_length_with_perspective; #endif -v_width2=vec2(outset,inset);}`),lineGradient:J(`uniform lowp float u_device_pixel_ratio;uniform sampler2D u_image;flat in vec2 v_width2;in vec2 v_normal;in float v_gamma_scale;in highp vec2 v_uv; +v_width2=vec2(outset,inset);}`),lineGradient:Y(`uniform lowp float u_device_pixel_ratio;uniform sampler2D u_image;flat in vec2 v_width2;in vec2 v_normal;in float v_gamma_scale;in highp vec2 v_uv; #ifdef GLOBE in float v_depth; #endif @@ -434,7 +434,7 @@ in float v_depth; void main() { #pragma maplibre: initialize lowp float blur #pragma maplibre: initialize lowp float opacity -float dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);vec4 color=texture(u_image,v_uv);fragColor=color*(alpha*opacity); +clipAntimeridian();float dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);vec4 color=texture(u_image,v_uv);fragColor=color*(alpha*opacity); #ifdef GLOBE if (v_depth > 1.0) {discard;} #endif @@ -443,7 +443,7 @@ fragColor=vec4(1.0); #endif }`,` #define scale 0.015873016 -layout(location=0) in vec2 a_pos_normal;layout(location=1) in vec4 a_data;layout(location=2) in float a_uv_x;layout(location=3) in float a_split_index;uniform vec2 u_translation;uniform mediump float u_ratio;uniform lowp float u_device_pixel_ratio;uniform vec2 u_units_to_pixels;uniform float u_image_height;out vec2 v_normal;flat out vec2 v_width2;out float v_gamma_scale;out highp vec2 v_uv; +layout(location=0) in ivec2 a_pos_normal;layout(location=1) in uvec4 a_data;layout(location=2) in float a_uv_x;layout(location=3) in float a_split_index;uniform vec2 u_translation;uniform mediump float u_ratio;uniform lowp float u_device_pixel_ratio;uniform vec2 u_units_to_pixels;uniform float u_image_height;out vec2 v_normal;flat out vec2 v_width2;out float v_gamma_scale;out highp vec2 v_uv; #ifdef GLOBE out float v_depth; #endif @@ -458,7 +458,7 @@ void main() { #pragma maplibre: initialize mediump float gapwidth #pragma maplibre: initialize lowp float offset #pragma maplibre: initialize mediump float width -if (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}float ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=a_data.xy-128.0;float a_direction=mod(a_data.z,4.0)-1.0;highp float texel_height=1.0/u_image_height;highp float half_texel_height=0.5*texel_height;v_uv=vec2(a_uv_x,a_split_index*texel_height-half_texel_height);vec2 pos=floor(a_pos_normal*0.5);mediump vec2 normal=a_pos_normal-2.0*pos;normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);float adjustedThickness=projectLineThickness(pos.y);vec4 projected_no_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation);vec4 projected_with_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation+dist/u_ratio*adjustedThickness);gl_Position=projected_with_extrude; +if (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}float ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=vec2(ivec2(a_data.xy)-128);float a_direction=float(int(a_data.z & 3u)-1);highp float texel_height=1.0/u_image_height;highp float half_texel_height=0.5*texel_height;v_uv=vec2(a_uv_x,a_split_index*texel_height-half_texel_height);vec2 pos=vec2(a_pos_normal >> 1);mediump vec2 normal=vec2(a_pos_normal & 1);normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);float adjustedThickness=projectLineThickness(pos.y);vec4 projected_no_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation);vec4 projected_with_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation+dist/u_ratio*adjustedThickness);gl_Position=projected_with_extrude; #ifdef GLOBE v_depth=gl_Position.z/gl_Position.w; #endif @@ -467,7 +467,7 @@ v_gamma_scale=1.0; #else float extrude_length_without_perspective=length(dist);float extrude_length_with_perspective=length((projected_with_extrude.xy-projected_no_extrude.xy)/projected_with_extrude.w*u_units_to_pixels);v_gamma_scale=extrude_length_without_perspective/extrude_length_with_perspective; #endif -v_width2=vec2(outset,inset);}`),linePattern:J(`#ifdef GL_ES +v_width2=vec2(outset,inset);}`),linePattern:Y(`#ifdef GL_ES precision highp float; #endif uniform lowp float u_device_pixel_ratio;uniform vec2 u_texsize;uniform float u_fade;uniform mediump vec3 u_scale;uniform sampler2D u_image;in vec2 v_normal;flat in vec2 v_width2;in float v_linesofar;in float v_gamma_scale;flat in float v_width; @@ -487,7 +487,7 @@ void main() { #pragma maplibre: initialize lowp float pixel_ratio_to #pragma maplibre: initialize lowp float blur #pragma maplibre: initialize lowp float opacity -vec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;float tileZoomRatio=u_scale.x;float fromScale=u_scale.y;float toScale=u_scale.z;vec2 display_size_a=(pattern_br_a-pattern_tl_a)/pixel_ratio_from;vec2 display_size_b=(pattern_br_b-pattern_tl_b)/pixel_ratio_to;vec2 pattern_size_a=vec2(display_size_a.x*fromScale/tileZoomRatio,display_size_a.y);vec2 pattern_size_b=vec2(display_size_b.x*toScale/tileZoomRatio,display_size_b.y);float aspect_a=display_size_a.y/v_width;float aspect_b=display_size_b.y/v_width;float dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);float x_a=mod(v_linesofar/pattern_size_a.x*aspect_a,1.0);float x_b=mod(v_linesofar/pattern_size_b.x*aspect_b,1.0);float y=0.5*v_normal.y+0.5;vec2 texel_size=1.0/u_texsize;vec2 pos_a=mix(pattern_tl_a*texel_size-texel_size,pattern_br_a*texel_size+texel_size,vec2(x_a,y));vec2 pos_b=mix(pattern_tl_b*texel_size-texel_size,pattern_br_b*texel_size+texel_size,vec2(x_b,y));vec4 color=mix(texture(u_image,pos_a),texture(u_image,pos_b),u_fade);fragColor=color*alpha*opacity; +clipAntimeridian();vec2 pattern_tl_a=pattern_from.xy;vec2 pattern_br_a=pattern_from.zw;vec2 pattern_tl_b=pattern_to.xy;vec2 pattern_br_b=pattern_to.zw;float tileZoomRatio=u_scale.x;float fromScale=u_scale.y;float toScale=u_scale.z;vec2 display_size_a=(pattern_br_a-pattern_tl_a)/pixel_ratio_from;vec2 display_size_b=(pattern_br_b-pattern_tl_b)/pixel_ratio_to;vec2 pattern_size_a=vec2(display_size_a.x*fromScale/tileZoomRatio,display_size_a.y);vec2 pattern_size_b=vec2(display_size_b.x*toScale/tileZoomRatio,display_size_b.y);float aspect_a=display_size_a.y/v_width;float aspect_b=display_size_b.y/v_width;float dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);float x_a=mod(v_linesofar/pattern_size_a.x*aspect_a,1.0);float x_b=mod(v_linesofar/pattern_size_b.x*aspect_b,1.0);float y=0.5*v_normal.y+0.5;vec2 texel_size=1.0/u_texsize;vec2 pos_a=mix(pattern_tl_a*texel_size-texel_size,pattern_br_a*texel_size+texel_size,vec2(x_a,y));vec2 pos_b=mix(pattern_tl_b*texel_size-texel_size,pattern_br_b*texel_size+texel_size,vec2(x_b,y));vec4 color=mix(texture(u_image,pos_a),texture(u_image,pos_b),u_fade);fragColor=color*alpha*opacity; #ifdef GLOBE if (v_depth > 1.0) {discard;} #endif @@ -497,7 +497,7 @@ fragColor=vec4(1.0); }`,` #define scale 0.015873016 #define LINE_DISTANCE_SCALE 2.0 -layout(location=0) in vec2 a_pos_normal;layout(location=1) in vec4 a_data;uniform vec2 u_translation;uniform vec2 u_units_to_pixels;uniform mediump float u_ratio;uniform lowp float u_device_pixel_ratio;out vec2 v_normal;flat out vec2 v_width2;out float v_linesofar;out float v_gamma_scale;flat out float v_width; +layout(location=0) in ivec2 a_pos_normal;layout(location=1) in uvec4 a_data;uniform vec2 u_translation;uniform vec2 u_units_to_pixels;uniform mediump float u_ratio;uniform lowp float u_device_pixel_ratio;out vec2 v_normal;flat out vec2 v_width2;out float v_linesofar;out float v_gamma_scale;flat out float v_width; #ifdef GLOBE out float v_depth; #endif @@ -522,7 +522,7 @@ void main() { #pragma maplibre: initialize mediump vec4 pattern_to #pragma maplibre: initialize lowp float pixel_ratio_from #pragma maplibre: initialize lowp float pixel_ratio_to -if (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}float ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=a_data.xy-128.0;float a_direction=mod(a_data.z,4.0)-1.0;float a_linesofar=(floor(a_data.z/4.0)+a_data.w*64.0)*LINE_DISTANCE_SCALE;vec2 pos=floor(a_pos_normal*0.5);mediump vec2 normal=a_pos_normal-2.0*pos;normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);float adjustedThickness=projectLineThickness(pos.y);vec4 projected_no_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation);vec4 projected_with_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation+dist/u_ratio*adjustedThickness);gl_Position=projected_with_extrude; +if (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}float ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=vec2(ivec2(a_data.xy)-128);float a_direction=float(int(a_data.z & 3u)-1);float a_linesofar=float((a_data.z >> 2u)+a_data.w*64u)*LINE_DISTANCE_SCALE;vec2 pos=vec2(a_pos_normal >> 1);mediump vec2 normal=vec2(a_pos_normal & 1);normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);float adjustedThickness=projectLineThickness(pos.y);vec4 projected_no_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation);vec4 projected_with_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation+dist/u_ratio*adjustedThickness);gl_Position=projected_with_extrude; #ifdef GLOBE v_depth=gl_Position.z/gl_Position.w; #endif @@ -531,7 +531,7 @@ v_gamma_scale=1.0; #else float extrude_length_without_perspective=length(dist);float extrude_length_with_perspective=length((projected_with_extrude.xy-projected_no_extrude.xy)/projected_with_extrude.w*u_units_to_pixels);v_gamma_scale=extrude_length_without_perspective/extrude_length_with_perspective; #endif -v_linesofar=a_linesofar;v_width2=vec2(outset,inset);v_width=floorwidth;}`),lineSDF:J(`uniform lowp float u_device_pixel_ratio;uniform lowp float u_lineatlas_width;uniform sampler2D u_image;uniform float u_mix;in vec2 v_normal;flat in vec2 v_width2;in vec2 v_tex_a;in vec2 v_tex_b;in float v_gamma_scale; +v_linesofar=a_linesofar;v_width2=vec2(outset,inset);v_width=floorwidth;}`),lineSDF:Y(`uniform lowp float u_device_pixel_ratio;uniform lowp float u_lineatlas_width;uniform sampler2D u_image;uniform float u_mix;in vec2 v_normal;flat in vec2 v_width2;in vec2 v_tex_a;in vec2 v_tex_b;in float v_gamma_scale; #ifdef GLOBE in float v_depth; #endif @@ -550,7 +550,7 @@ void main() { #pragma maplibre: initialize lowp float floorwidth #pragma maplibre: initialize mediump vec4 dasharray_from #pragma maplibre: initialize mediump vec4 dasharray_to -float dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);float sdfdist_a=texture(u_image,v_tex_a).a;float sdfdist_b=texture(u_image,v_tex_b).a;float sdfdist=mix(sdfdist_a,sdfdist_b,u_mix);float sdfgamma=(u_lineatlas_width/256.0/u_device_pixel_ratio)/min(dasharray_from.w,dasharray_to.w);alpha*=smoothstep(0.5-sdfgamma/floorwidth,0.5+sdfgamma/floorwidth,sdfdist);fragColor=color*(alpha*opacity); +clipAntimeridian();float dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);float sdfdist_a=texture(u_image,v_tex_a).a;float sdfdist_b=texture(u_image,v_tex_b).a;float sdfdist=mix(sdfdist_a,sdfdist_b,u_mix);float sdfgamma=(u_lineatlas_width/256.0/u_device_pixel_ratio)/min(dasharray_from.w,dasharray_to.w);alpha*=smoothstep(0.5-sdfgamma/floorwidth,0.5+sdfgamma/floorwidth,sdfdist);fragColor=color*(alpha*opacity); #ifdef GLOBE if (v_depth > 1.0) {discard;} #endif @@ -560,7 +560,7 @@ fragColor=vec4(1.0); }`,` #define scale 0.015873016 #define LINE_DISTANCE_SCALE 2.0 -layout(location=0) in vec2 a_pos_normal;layout(location=1) in vec4 a_data;uniform vec2 u_translation;uniform mediump float u_ratio;uniform lowp float u_device_pixel_ratio;uniform vec2 u_units_to_pixels;uniform float u_tileratio;uniform float u_crossfade_from;uniform float u_crossfade_to;uniform float u_lineatlas_height;out vec2 v_normal;flat out vec2 v_width2;out vec2 v_tex_a;out vec2 v_tex_b;out float v_gamma_scale; +layout(location=0) in ivec2 a_pos_normal;layout(location=1) in uvec4 a_data;uniform vec2 u_translation;uniform mediump float u_ratio;uniform lowp float u_device_pixel_ratio;uniform vec2 u_units_to_pixels;uniform float u_tileratio;uniform float u_crossfade_from;uniform float u_crossfade_to;uniform float u_lineatlas_height;out vec2 v_normal;flat out vec2 v_width2;out vec2 v_tex_a;out vec2 v_tex_b;out float v_gamma_scale; #ifdef GLOBE out float v_depth; #endif @@ -583,7 +583,7 @@ void main() { #pragma maplibre: initialize lowp float floorwidth #pragma maplibre: initialize mediump vec4 dasharray_from #pragma maplibre: initialize mediump vec4 dasharray_to -if (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}float ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=a_data.xy-128.0;float a_direction=mod(a_data.z,4.0)-1.0;float a_linesofar=(floor(a_data.z/4.0)+a_data.w*64.0)*LINE_DISTANCE_SCALE;vec2 pos=floor(a_pos_normal*0.5);mediump vec2 normal=a_pos_normal-2.0*pos;normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);float adjustedThickness=projectLineThickness(pos.y);vec4 projected_no_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation);vec4 projected_with_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation+dist/u_ratio*adjustedThickness);gl_Position=projected_with_extrude; +if (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}float ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=vec2(ivec2(a_data.xy)-128);float a_direction=float(int(a_data.z & 3u)-1);float a_linesofar=float((a_data.z >> 2u)+a_data.w*64u)*LINE_DISTANCE_SCALE;vec2 pos=vec2(a_pos_normal >> 1);mediump vec2 normal=vec2(a_pos_normal & 1);normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);float adjustedThickness=projectLineThickness(pos.y);vec4 projected_no_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation);vec4 projected_with_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation+dist/u_ratio*adjustedThickness);gl_Position=projected_with_extrude; #ifdef GLOBE v_depth=gl_Position.z/gl_Position.w; #endif @@ -592,7 +592,7 @@ v_gamma_scale=1.0; #else float extrude_length_without_perspective=length(dist);float extrude_length_with_perspective=length((projected_with_extrude.xy-projected_no_extrude.xy)/projected_with_extrude.w*u_units_to_pixels);v_gamma_scale=extrude_length_without_perspective/extrude_length_with_perspective; #endif -float u_patternscale_a_x=u_tileratio/dasharray_from.w/u_crossfade_from;float u_patternscale_a_y=-dasharray_from.z/2.0/u_lineatlas_height;float u_patternscale_b_x=u_tileratio/dasharray_to.w/u_crossfade_to;float u_patternscale_b_y=-dasharray_to.z/2.0/u_lineatlas_height;v_tex_a=vec2(a_linesofar*u_patternscale_a_x/floorwidth,normal.y*u_patternscale_a_y+(float(dasharray_from.y)+0.5)/u_lineatlas_height);v_tex_b=vec2(a_linesofar*u_patternscale_b_x/floorwidth,normal.y*u_patternscale_b_y+(float(dasharray_to.y)+0.5)/u_lineatlas_height);v_width2=vec2(outset,inset);}`),lineGradientSDF:J(`uniform lowp float u_device_pixel_ratio;uniform sampler2D u_image;uniform sampler2D u_image_dash;uniform float u_mix;uniform lowp float u_lineatlas_width;in vec2 v_normal;flat in vec2 v_width2;in vec2 v_tex_a;in vec2 v_tex_b;in float v_gamma_scale;in highp vec2 v_uv; +float u_patternscale_a_x=u_tileratio/dasharray_from.w/u_crossfade_from;float u_patternscale_a_y=-dasharray_from.z/2.0/u_lineatlas_height;float u_patternscale_b_x=u_tileratio/dasharray_to.w/u_crossfade_to;float u_patternscale_b_y=-dasharray_to.z/2.0/u_lineatlas_height;v_tex_a=vec2(a_linesofar*u_patternscale_a_x/floorwidth,normal.y*u_patternscale_a_y+(float(dasharray_from.y)+0.5)/u_lineatlas_height);v_tex_b=vec2(a_linesofar*u_patternscale_b_x/floorwidth,normal.y*u_patternscale_b_y+(float(dasharray_to.y)+0.5)/u_lineatlas_height);v_width2=vec2(outset,inset);}`),lineGradientSDF:Y(`uniform lowp float u_device_pixel_ratio;uniform sampler2D u_image;uniform sampler2D u_image_dash;uniform float u_mix;uniform lowp float u_lineatlas_width;in vec2 v_normal;flat in vec2 v_width2;in vec2 v_tex_a;in vec2 v_tex_b;in float v_gamma_scale;in highp vec2 v_uv; #ifdef GLOBE in float v_depth; #endif @@ -609,7 +609,7 @@ void main() { #pragma maplibre: initialize lowp float floorwidth #pragma maplibre: initialize mediump vec4 dasharray_from #pragma maplibre: initialize mediump vec4 dasharray_to -float dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);vec4 color=texture(u_image,v_uv);float sdfdist_a=texture(u_image_dash,v_tex_a).a;float sdfdist_b=texture(u_image_dash,v_tex_b).a;float sdfdist=mix(sdfdist_a,sdfdist_b,u_mix);float sdfgamma=(u_lineatlas_width/256.0)/min(dasharray_from.w,dasharray_to.w);float dash_alpha=smoothstep(0.5-sdfgamma/floorwidth,0.5+sdfgamma/floorwidth,sdfdist);fragColor=color*(alpha*dash_alpha*opacity); +clipAntimeridian();float dist=length(v_normal)*v_width2.s;float blur2=(blur+1.0/u_device_pixel_ratio)*v_gamma_scale;float alpha=clamp(min(dist-(v_width2.t-blur2),v_width2.s-dist)/blur2,0.0,1.0);vec4 color=texture(u_image,v_uv);float sdfdist_a=texture(u_image_dash,v_tex_a).a;float sdfdist_b=texture(u_image_dash,v_tex_b).a;float sdfdist=mix(sdfdist_a,sdfdist_b,u_mix);float sdfgamma=(u_lineatlas_width/256.0)/min(dasharray_from.w,dasharray_to.w);float dash_alpha=smoothstep(0.5-sdfgamma/floorwidth,0.5+sdfgamma/floorwidth,sdfdist);fragColor=color*(alpha*dash_alpha*opacity); #ifdef GLOBE if (v_depth > 1.0) {discard;} #endif @@ -619,7 +619,7 @@ fragColor=vec4(1.0); }`,` #define scale 0.015873016 #define LINE_DISTANCE_SCALE 2.0 -layout(location=0) in vec2 a_pos_normal;layout(location=1) in vec4 a_data;layout(location=2) in float a_uv_x;layout(location=3) in float a_split_index;uniform vec2 u_translation;uniform mediump float u_ratio;uniform lowp float u_device_pixel_ratio;uniform vec2 u_units_to_pixels;uniform float u_image_height;uniform float u_tileratio;uniform float u_crossfade_from;uniform float u_crossfade_to;uniform float u_lineatlas_height;out vec2 v_normal;flat out vec2 v_width2;out float v_gamma_scale;out highp vec2 v_uv;out vec2 v_tex_a;out vec2 v_tex_b; +layout(location=0) in ivec2 a_pos_normal;layout(location=1) in uvec4 a_data;layout(location=2) in float a_uv_x;layout(location=3) in float a_split_index;uniform vec2 u_translation;uniform mediump float u_ratio;uniform lowp float u_device_pixel_ratio;uniform vec2 u_units_to_pixels;uniform float u_image_height;uniform float u_tileratio;uniform float u_crossfade_from;uniform float u_crossfade_to;uniform float u_lineatlas_height;out vec2 v_normal;flat out vec2 v_width2;out float v_gamma_scale;out highp vec2 v_uv;out vec2 v_tex_a;out vec2 v_tex_b; #ifdef GLOBE out float v_depth; #endif @@ -640,7 +640,7 @@ void main() { #pragma maplibre: initialize lowp float floorwidth #pragma maplibre: initialize mediump vec4 dasharray_from #pragma maplibre: initialize mediump vec4 dasharray_to -if (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}float ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=a_data.xy-128.0;float a_direction=mod(a_data.z,4.0)-1.0;float a_linesofar=(floor(a_data.z/4.0)+a_data.w*64.0)*LINE_DISTANCE_SCALE;float texel_height=1.0/u_image_height;float half_texel_height=0.5*texel_height;v_uv=vec2(a_uv_x,a_split_index*texel_height-half_texel_height);vec2 pos=floor(a_pos_normal*0.5);mediump vec2 normal=a_pos_normal-2.0*pos;normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);float adjustedThickness=projectLineThickness(pos.y);vec4 projected_no_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation);vec4 projected_with_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation+dist/u_ratio*adjustedThickness);gl_Position=projected_with_extrude; +if (opacity < 0.01) {gl_Position=vec4(-2.0,-2.0,-2.0,1.0);return;}float ANTIALIASING=1.0/u_device_pixel_ratio/2.0;vec2 a_extrude=vec2(ivec2(a_data.xy)-128);float a_direction=float(int(a_data.z & 3u)-1);float a_linesofar=float((a_data.z >> 2u)+a_data.w*64u)*LINE_DISTANCE_SCALE;float texel_height=1.0/u_image_height;float half_texel_height=0.5*texel_height;v_uv=vec2(a_uv_x,a_split_index*texel_height-half_texel_height);vec2 pos=vec2(a_pos_normal >> 1);mediump vec2 normal=vec2(a_pos_normal & 1);normal.y=normal.y*2.0-1.0;v_normal=normal;gapwidth=gapwidth/2.0;float halfwidth=width/2.0;offset=-1.0*offset;float inset=gapwidth+(gapwidth > 0.0 ? ANTIALIASING : 0.0);float outset=gapwidth+halfwidth*(gapwidth > 0.0 ? 2.0 : 1.0)+(halfwidth==0.0 ? 0.0 : ANTIALIASING);mediump vec2 dist=outset*a_extrude*scale;mediump float u=0.5*a_direction;mediump float t=1.0-abs(u);mediump vec2 offset2=offset*a_extrude*scale*normal.y*mat2(t,-u,u,t);float adjustedThickness=projectLineThickness(pos.y);vec4 projected_no_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation);vec4 projected_with_extrude=projectTile(pos+offset2/u_ratio*adjustedThickness+u_translation+dist/u_ratio*adjustedThickness);gl_Position=projected_with_extrude; #ifdef GLOBE v_depth=gl_Position.z/gl_Position.w; #endif @@ -649,33 +649,33 @@ v_gamma_scale=1.0; #else float extrude_length_without_perspective=length(dist);float extrude_length_with_perspective=length((projected_with_extrude.xy-projected_no_extrude.xy)/projected_with_extrude.w*u_units_to_pixels);v_gamma_scale=extrude_length_without_perspective/extrude_length_with_perspective; #endif -float u_patternscale_a_x=u_tileratio/dasharray_from.w/u_crossfade_from;float u_patternscale_a_y=-dasharray_from.z/2.0/u_lineatlas_height;float u_patternscale_b_x=u_tileratio/dasharray_to.w/u_crossfade_to;float u_patternscale_b_y=-dasharray_to.z/2.0/u_lineatlas_height;v_tex_a=vec2(a_linesofar*u_patternscale_a_x/floorwidth,normal.y*u_patternscale_a_y+(float(dasharray_from.y)+0.5)/u_lineatlas_height);v_tex_b=vec2(a_linesofar*u_patternscale_b_x/floorwidth,normal.y*u_patternscale_b_y+(float(dasharray_to.y)+0.5)/u_lineatlas_height);v_width2=vec2(outset,inset);}`),layerOpacity:J(`uniform sampler2D u_image;uniform float u_opacity;in vec2 v_pos;void main() {fragColor=texture(u_image,v_pos)*u_opacity; +float u_patternscale_a_x=u_tileratio/dasharray_from.w/u_crossfade_from;float u_patternscale_a_y=-dasharray_from.z/2.0/u_lineatlas_height;float u_patternscale_b_x=u_tileratio/dasharray_to.w/u_crossfade_to;float u_patternscale_b_y=-dasharray_to.z/2.0/u_lineatlas_height;v_tex_a=vec2(a_linesofar*u_patternscale_a_x/floorwidth,normal.y*u_patternscale_a_y+(float(dasharray_from.y)+0.5)/u_lineatlas_height);v_tex_b=vec2(a_linesofar*u_patternscale_b_x/floorwidth,normal.y*u_patternscale_b_y+(float(dasharray_to.y)+0.5)/u_lineatlas_height);v_width2=vec2(outset,inset);}`),layerOpacity:Y(`uniform sampler2D u_image;uniform float u_opacity;in vec2 v_pos;void main() {fragColor=texture(u_image,v_pos)*u_opacity; #ifdef OVERDRAW_INSPECTOR fragColor=vec4(0.0); #endif -}`,`layout(location=0) in vec2 a_pos;out vec2 v_pos;void main() {gl_Position=vec4(a_pos.x*2.0-1.0,1.0-a_pos.y*2.0,0.0,1.0);v_pos.x=a_pos.x;v_pos.y=1.0-a_pos.y;}`),raster:J(`uniform float u_fade_t;uniform float u_opacity;uniform sampler2D u_image0;uniform sampler2D u_image1;in vec2 v_pos0;in vec2 v_pos1;uniform float u_brightness_low;uniform float u_brightness_high;uniform float u_saturation_factor;uniform float u_contrast_factor;uniform vec3 u_spin_weights;void main() {vec4 color0=texture(u_image0,v_pos0);vec4 color1=texture(u_image1,v_pos1);if (color0.a > 0.0) {color0.rgb=color0.rgb/color0.a;}if (color1.a > 0.0) {color1.rgb=color1.rgb/color1.a;}vec4 color=mix(color0,color1,u_fade_t);color.a*=u_opacity;vec3 rgb=color.rgb;rgb=vec3(dot(rgb,u_spin_weights.xyz),dot(rgb,u_spin_weights.zxy),dot(rgb,u_spin_weights.yzx));float average=(color.r+color.g+color.b)/3.0;rgb+=(average-rgb)*u_saturation_factor;rgb=(rgb-0.5)*u_contrast_factor+0.5;vec3 u_high_vec=vec3(u_brightness_low,u_brightness_low,u_brightness_low);vec3 u_low_vec=vec3(u_brightness_high,u_brightness_high,u_brightness_high);fragColor=vec4(mix(u_high_vec,u_low_vec,rgb)*color.a,color.a); +}`,`layout(location=0) in vec2 a_pos;out vec2 v_pos;void main() {gl_Position=vec4(a_pos.x*2.0-1.0,1.0-a_pos.y*2.0,0.0,1.0);v_pos.x=a_pos.x;v_pos.y=1.0-a_pos.y;}`),raster:Y(`uniform float u_fade_t;uniform float u_opacity;uniform sampler2D u_image0;uniform sampler2D u_image1;in vec3 v_pos0;in vec3 v_pos1;uniform float u_brightness_low;uniform float u_brightness_high;uniform float u_saturation_factor;uniform float u_contrast_factor;uniform vec3 u_spin_weights;void main() {vec4 color0=texture(u_image0,v_pos0.xy/v_pos0.z);vec4 color1=texture(u_image1,v_pos1.xy/v_pos1.z);if (color0.a > 0.0) {color0.rgb=color0.rgb/color0.a;}if (color1.a > 0.0) {color1.rgb=color1.rgb/color1.a;}vec4 color=mix(color0,color1,u_fade_t);color.a*=u_opacity;vec3 rgb=color.rgb;rgb=vec3(dot(rgb,u_spin_weights.xyz),dot(rgb,u_spin_weights.zxy),dot(rgb,u_spin_weights.yzx));float average=(color.r+color.g+color.b)/3.0;rgb+=(average-rgb)*u_saturation_factor;rgb=(rgb-0.5)*u_contrast_factor+0.5;vec3 u_high_vec=vec3(u_brightness_low,u_brightness_low,u_brightness_low);vec3 u_low_vec=vec3(u_brightness_high,u_brightness_high,u_brightness_high);fragColor=vec4(mix(u_high_vec,u_low_vec,rgb)*color.a,color.a); #ifdef OVERDRAW_INSPECTOR fragColor=vec4(1.0); #endif -}`,`uniform vec2 u_tl_parent;uniform float u_scale_parent;uniform float u_buffer_scale;uniform vec4 u_coords_top;uniform vec4 u_coords_bottom;layout(location=0) in vec2 a_pos;out vec2 v_pos0;out vec2 v_pos1;void main() {vec2 fractionalPos=a_pos/8192.0;vec2 position=mix(mix(u_coords_top.xy,u_coords_top.zw,fractionalPos.x),mix(u_coords_bottom.xy,u_coords_bottom.zw,fractionalPos.x),fractionalPos.y);gl_Position=projectTile(position,position);v_pos0=((fractionalPos-0.5)/u_buffer_scale)+0.5; +}`,`uniform vec2 u_tl_parent;uniform float u_scale_parent;uniform float u_buffer_scale;uniform vec3 u_image_warp;uniform vec4 u_coords_top;uniform vec4 u_coords_bottom;layout(location=0) in vec2 a_pos;out vec3 v_pos0;out vec3 v_pos1;void main() {vec2 fractionalPos=a_pos/8192.0;vec2 topLeft=u_coords_top.xy;vec2 topRight=u_coords_top.zw;vec2 bottomLeft=u_coords_bottom.xy;vec2 bottomRight=u_coords_bottom.zw;vec2 bilinearPos=mix(mix(topLeft,topRight,fractionalPos.x),mix(bottomLeft,bottomRight,fractionalPos.x),fractionalPos.y);float denominator=dot(u_image_warp.xy,fractionalPos)+1.0;vec2 acrossTop=topRight-topLeft+u_image_warp.x*topRight;vec2 downLeft=bottomLeft-topLeft+u_image_warp.y*bottomLeft;vec2 projectivePos=(acrossTop*fractionalPos.x+downLeft*fractionalPos.y+topLeft)/denominator;vec2 position=mix(projectivePos,bilinearPos,u_image_warp.z);gl_Position=projectTile(position,position);vec2 texturePos=((fractionalPos-0.5)/u_buffer_scale)+0.5; #ifdef GLOBE -if (a_pos.y <-32767.5) {v_pos0.y=0.0;}if (a_pos.y > 32766.5) {v_pos0.y=1.0;} +if (a_pos.y <-32767.5) {texturePos.y=0.0;}if (a_pos.y > 32766.5) {texturePos.y=1.0;} #endif -v_pos1=(v_pos0*u_scale_parent)+u_tl_parent;}`),symbolIcon:J(`uniform sampler2D u_texture;in vec2 v_tex;flat in float v_total_opacity;void main() {fragColor=texture(u_texture,v_tex)*v_total_opacity; +float perspectiveRatio=mix(1.0/denominator,1.0,u_image_warp.z);v_pos0=vec3(texturePos*perspectiveRatio,perspectiveRatio);vec2 parentPos=(texturePos*u_scale_parent)+u_tl_parent;v_pos1=vec3(parentPos*perspectiveRatio,perspectiveRatio);}`),symbolIcon:Y(`uniform sampler2D u_texture;in vec2 v_tex;flat in float v_total_opacity;void main() {fragColor=texture(u_texture,v_tex)*v_total_opacity; #ifdef OVERDRAW_INSPECTOR fragColor=vec4(1.0); #endif -}`,`layout(location=0) in vec4 a_pos_offset;layout(location=1) in vec4 a_data;layout(location=2) in vec4 a_pixeloffset;layout(location=3) in vec3 a_projected_pos;layout(location=4) in float a_fade_opacity;uniform bool u_is_size_zoom_constant;uniform bool u_is_size_feature_constant;uniform highp float u_size_t;uniform highp float u_size;uniform highp float u_camera_to_center_distance;uniform highp float u_pitch;uniform bool u_rotate_symbol;uniform highp float u_aspect_ratio;uniform float u_fade_change;uniform mat4 u_label_plane_matrix;uniform mat4 u_coord_matrix;uniform bool u_is_text;uniform bool u_pitch_with_map;uniform vec2 u_texsize;uniform bool u_is_along_line;uniform bool u_is_variable_anchor;uniform vec2 u_translation;uniform float u_pitched_scale;uniform bool u_is_offset;out vec2 v_tex;flat out float v_total_opacity; +}`,`layout(location=0) in vec4 a_pos_offset;layout(location=1) in uvec4 a_data;layout(location=2) in vec4 a_pixeloffset;layout(location=3) in vec3 a_projected_pos;layout(location=4) in uint a_fade_opacity;layout(location=5) in float a_height_offset;uniform bool u_is_size_zoom_constant;uniform bool u_is_size_feature_constant;uniform highp float u_size_t;uniform highp float u_size;uniform highp float u_camera_to_center_distance;uniform highp float u_pitch;uniform bool u_rotate_symbol;uniform highp float u_aspect_ratio;uniform float u_fade_change;uniform mat4 u_label_plane_matrix;uniform mat4 u_coord_matrix;uniform bool u_is_text;uniform bool u_pitch_with_map;uniform vec2 u_texsize;uniform bool u_is_along_line;uniform bool u_is_variable_anchor;uniform vec2 u_translation;uniform float u_pitched_scale;uniform bool u_is_offset;uniform bool u_height_anchor_ground;out vec2 v_tex;flat out float v_total_opacity; #pragma maplibre: define lowp float opacity void main() { #pragma maplibre: initialize lowp float opacity -vec2 a_pos=a_pos_offset.xy;vec2 a_offset=a_pos_offset.zw;vec2 a_tex=a_data.xy;vec2 a_size=a_data.zw;float a_size_min=floor(a_size[0]*0.5);vec2 a_pxoffset=a_pixeloffset.xy;vec2 a_minFontScale=a_pixeloffset.zw/256.0;float ele=get_elevation(a_pos);highp float segment_angle=-a_projected_pos[2];float size;if (!u_is_size_zoom_constant && !u_is_size_feature_constant) {size=mix(a_size_min,a_size[1],u_size_t)/128.0;} else if (u_is_size_zoom_constant && !u_is_size_feature_constant) {size=a_size_min/128.0;} else {size=u_size;}vec2 translated_a_pos=a_pos+u_translation;vec4 projectedPoint=projectTileWithElevation(translated_a_pos,ele);vec2 fade_opacity=unpack_opacity(a_fade_opacity);float fade_change=fade_opacity[1] > 0.5 ? u_fade_change :-u_fade_change;float visibility=calculate_visibility(projectedPoint);v_total_opacity=opacity*max(0.0,min(visibility,fade_opacity[0]+fade_change));if (v_total_opacity < 0.1){gl_Position=vec4(-2.,-2.,-2.,1.);return;}highp float camera_to_anchor_distance=projectedPoint.w;highp float distance_ratio=u_pitch_with_map ? +vec2 a_pos=a_pos_offset.xy;vec2 a_offset=a_pos_offset.zw;vec2 a_tex=vec2(a_data.xy);vec2 a_size=vec2(a_data.zw);float a_size_min=float(a_data.z >> 1u);vec2 a_pxoffset=a_pixeloffset.xy;vec2 a_minFontScale=a_pixeloffset.zw/256.0;float ele=a_height_offset+(u_height_anchor_ground ? get_elevation(a_pos) : 0.0);highp float segment_angle=-a_projected_pos[2];float size;if (!u_is_size_zoom_constant && !u_is_size_feature_constant) {size=mix(a_size_min,a_size[1],u_size_t)/128.0;} else if (u_is_size_zoom_constant && !u_is_size_feature_constant) {size=a_size_min/128.0;} else {size=u_size;}vec2 translated_a_pos=a_pos+u_translation;vec4 projectedPoint=projectTileWithElevation(translated_a_pos,ele);vec2 fade_opacity=unpack_opacity(a_fade_opacity);float fade_change=fade_opacity[1] > 0.5 ? u_fade_change :-u_fade_change;float visibility=calculate_visibility(projectedPoint);v_total_opacity=opacity*max(0.0,min(visibility,fade_opacity[0]+fade_change));if (v_total_opacity < 0.1){gl_Position=vec4(-2.,-2.,-2.,1.);return;}highp float camera_to_anchor_distance=projectedPoint.w;highp float distance_ratio=u_pitch_with_map ? camera_to_anchor_distance/u_camera_to_center_distance : u_camera_to_center_distance/camera_to_anchor_distance;highp float perspective_ratio=clamp(0.5+0.5*distance_ratio,0.0,4.0);if (!u_is_offset) {size*=perspective_ratio;}float fontScale=u_is_text ? size/24.0 : size;highp float symbol_rotation=0.0;if (u_rotate_symbol) {vec4 offsetProjectedPoint=projectTileWithElevation(translated_a_pos+vec2(1,0),ele);vec2 a=projectedPoint.xy/projectedPoint.w;vec2 b=offsetProjectedPoint.xy/offsetProjectedPoint.w;symbol_rotation=atan((b.y-a.y)/u_aspect_ratio,b.x-a.x);}highp float angle_sin=sin(segment_angle+symbol_rotation);highp float angle_cos=cos(segment_angle+symbol_rotation);mat2 rotation_matrix=mat2(angle_cos,-1.0*angle_sin,angle_sin,angle_cos);vec4 projected_pos;if (u_is_along_line || u_is_variable_anchor) {projected_pos=vec4(a_projected_pos.xy,ele,1.0);} else if (u_pitch_with_map) {projected_pos=u_label_plane_matrix*vec4(a_projected_pos.xy+u_translation,ele,1.0);} else {projected_pos=u_label_plane_matrix*projectTileWithElevation(a_projected_pos.xy+u_translation,ele);}float z=float(u_pitch_with_map)*projected_pos.z/projected_pos.w;float projectionScaling=1.0; #ifdef GLOBE if(u_pitch_with_map) {float anchor_pos_tile_y=(u_coord_matrix*vec4(projected_pos.xy/projected_pos.w,z,1.0)).y;projectionScaling=mix(projectionScaling,1.0/circumferenceRatioAtTileY(anchor_pos_tile_y)*u_pitched_scale,u_projection_transition);} #endif -vec4 finalPos=u_coord_matrix*vec4(projected_pos.xy/projected_pos.w+rotation_matrix*(a_offset/32.0*max(a_minFontScale,fontScale)+a_pxoffset/16.0)*projectionScaling,z,1.0);if(u_pitch_with_map) {finalPos=projectTileWithElevation(finalPos.xy,finalPos.z);}gl_Position=finalPos;v_tex=a_tex/u_texsize;}`),symbolSDF:J(`#define SDF_PX 8.0 +vec4 finalPos=u_coord_matrix*vec4(projected_pos.xy/projected_pos.w+rotation_matrix*(a_offset/32.0*max(a_minFontScale,fontScale)+a_pxoffset/16.0)*projectionScaling,z,1.0);if(u_pitch_with_map) {finalPos=projectTileWithElevation(finalPos.xy,finalPos.z);}gl_Position=finalPos;v_tex=a_tex/u_texsize;}`),symbolSDF:Y(`#define SDF_PX 8.0 uniform bool u_is_halo;uniform bool u_is_plain;uniform sampler2D u_texture;uniform highp float u_gamma_scale;uniform lowp float u_device_pixel_ratio;uniform bool u_is_text;in vec2 v_data0;in vec3 v_data1; #pragma maplibre: define highp vec4 fill_color #pragma maplibre: define highp vec4 halo_color @@ -690,7 +690,7 @@ float EDGE_GAMMA=0.105/u_device_pixel_ratio;vec2 tex=v_data0.xy;float gamma_scal #ifdef OVERDRAW_INSPECTOR fragColor=vec4(1.0); #endif -}`,`layout(location=0) in vec4 a_pos_offset;layout(location=1) in vec4 a_data;layout(location=2) in vec4 a_pixeloffset;layout(location=3) in vec3 a_projected_pos;layout(location=4) in float a_fade_opacity;uniform bool u_is_size_zoom_constant;uniform bool u_is_size_feature_constant;uniform highp float u_size_t;uniform highp float u_size;uniform mat4 u_label_plane_matrix;uniform mat4 u_coord_matrix;uniform bool u_is_text;uniform bool u_pitch_with_map;uniform bool u_is_along_line;uniform bool u_is_variable_anchor;uniform highp float u_pitch;uniform bool u_rotate_symbol;uniform highp float u_aspect_ratio;uniform highp float u_camera_to_center_distance;uniform float u_fade_change;uniform vec2 u_texsize;uniform vec2 u_translation;uniform float u_pitched_scale;uniform bool u_is_offset;out vec2 v_data0;out vec3 v_data1; +}`,`layout(location=0) in vec4 a_pos_offset;layout(location=1) in uvec4 a_data;layout(location=2) in vec4 a_pixeloffset;layout(location=3) in vec3 a_projected_pos;layout(location=4) in uint a_fade_opacity;layout(location=5) in float a_height_offset;uniform bool u_is_size_zoom_constant;uniform bool u_is_size_feature_constant;uniform highp float u_size_t;uniform highp float u_size;uniform mat4 u_label_plane_matrix;uniform mat4 u_coord_matrix;uniform bool u_is_text;uniform bool u_pitch_with_map;uniform bool u_is_along_line;uniform bool u_is_variable_anchor;uniform highp float u_pitch;uniform bool u_rotate_symbol;uniform highp float u_aspect_ratio;uniform highp float u_camera_to_center_distance;uniform float u_fade_change;uniform vec2 u_texsize;uniform vec2 u_translation;uniform float u_pitched_scale;uniform bool u_is_offset;uniform bool u_height_anchor_ground;out vec2 v_data0;out vec3 v_data1; #pragma maplibre: define highp vec4 fill_color #pragma maplibre: define highp vec4 halo_color #pragma maplibre: define lowp float opacity @@ -702,13 +702,13 @@ void main() { #pragma maplibre: initialize lowp float opacity #pragma maplibre: initialize lowp float halo_width #pragma maplibre: initialize lowp float halo_blur -vec2 a_pos=a_pos_offset.xy;vec2 a_offset=a_pos_offset.zw;vec2 a_tex=a_data.xy;vec2 a_size=a_data.zw;float a_size_min=floor(a_size[0]*0.5);vec2 a_pxoffset=a_pixeloffset.xy/16.0;vec2 a_minFontScale=a_pixeloffset.zw/256.0;float ele=get_elevation(a_pos);highp float segment_angle=-a_projected_pos[2];float size;if (!u_is_size_zoom_constant && !u_is_size_feature_constant) {size=mix(a_size_min,a_size[1],u_size_t)/128.0;} else if (u_is_size_zoom_constant && !u_is_size_feature_constant) {size=a_size_min/128.0;} else {size=u_size;}vec2 translated_a_pos=a_pos+u_translation;vec4 projectedPoint=projectTileWithElevation(translated_a_pos,ele);vec2 fade_opacity=unpack_opacity(a_fade_opacity);float visibility=calculate_visibility(projectedPoint);float fade_change=fade_opacity[1] > 0.5 ? u_fade_change :-u_fade_change;float interpolated_fade_opacity=max(0.0,min(visibility,fade_opacity[0]+fade_change));float total_opacity=opacity*interpolated_fade_opacity;if (total_opacity < 0.1){gl_Position=vec4(-2.,-2.,-2.,1.);return;}highp float camera_to_anchor_distance=projectedPoint.w;highp float distance_ratio=u_pitch_with_map ? +vec2 a_pos=a_pos_offset.xy;vec2 a_offset=a_pos_offset.zw;vec2 a_tex=vec2(a_data.xy);vec2 a_size=vec2(a_data.zw);float a_size_min=float(a_data.z >> 1u);vec2 a_pxoffset=a_pixeloffset.xy/16.0;vec2 a_minFontScale=a_pixeloffset.zw/256.0;float ele=a_height_offset+(u_height_anchor_ground ? get_elevation(a_pos) : 0.0);highp float segment_angle=-a_projected_pos[2];float size;if (!u_is_size_zoom_constant && !u_is_size_feature_constant) {size=mix(a_size_min,a_size[1],u_size_t)/128.0;} else if (u_is_size_zoom_constant && !u_is_size_feature_constant) {size=a_size_min/128.0;} else {size=u_size;}vec2 translated_a_pos=a_pos+u_translation;vec4 projectedPoint=projectTileWithElevation(translated_a_pos,ele);vec2 fade_opacity=unpack_opacity(a_fade_opacity);float visibility=calculate_visibility(projectedPoint);float fade_change=fade_opacity[1] > 0.5 ? u_fade_change :-u_fade_change;float interpolated_fade_opacity=max(0.0,min(visibility,fade_opacity[0]+fade_change));float total_opacity=opacity*interpolated_fade_opacity;if (total_opacity < 0.1){gl_Position=vec4(-2.,-2.,-2.,1.);return;}highp float camera_to_anchor_distance=projectedPoint.w;highp float distance_ratio=u_pitch_with_map ? camera_to_anchor_distance/u_camera_to_center_distance : u_camera_to_center_distance/camera_to_anchor_distance;highp float perspective_ratio=clamp(0.5+0.5*distance_ratio,0.0,4.0);if (!u_is_offset) {size*=perspective_ratio;}float fontScale=u_is_text ? size/24.0 : size;highp float symbol_rotation=0.0;if (u_rotate_symbol) {vec4 offsetProjectedPoint=projectTileWithElevation(translated_a_pos+vec2(1,0),ele);vec2 a=projectedPoint.xy/projectedPoint.w;vec2 b=offsetProjectedPoint.xy/offsetProjectedPoint.w;symbol_rotation=atan((b.y-a.y)/u_aspect_ratio,b.x-a.x);}highp float angle_sin=sin(segment_angle+symbol_rotation);highp float angle_cos=cos(segment_angle+symbol_rotation);mat2 rotation_matrix=mat2(angle_cos,-1.0*angle_sin,angle_sin,angle_cos);vec4 projected_pos;if (u_is_along_line || u_is_variable_anchor) {projected_pos=vec4(a_projected_pos.xy,ele,1.0);} else if (u_pitch_with_map) {projected_pos=u_label_plane_matrix*vec4(a_projected_pos.xy+u_translation,ele,1.0);} else {projected_pos=u_label_plane_matrix*projectTileWithElevation(a_projected_pos.xy+u_translation,ele);}float z=float(u_pitch_with_map)*projected_pos.z/projected_pos.w;float projectionScaling=1.0; #ifdef GLOBE if(u_pitch_with_map) {float anchor_pos_tile_y=(u_coord_matrix*vec4(projected_pos.xy/projected_pos.w,z,1.0)).y;projectionScaling=mix(projectionScaling,1.0/circumferenceRatioAtTileY(anchor_pos_tile_y)*u_pitched_scale,u_projection_transition);} #endif -vec4 finalPos=u_coord_matrix*vec4(projected_pos.xy/projected_pos.w+rotation_matrix*(a_offset/32.0*max(a_minFontScale,fontScale)+a_pxoffset)*projectionScaling,z,1.0);if(u_pitch_with_map) {finalPos=projectTileWithElevation(finalPos.xy,finalPos.z);}float gamma_scale=finalPos.w;gl_Position=finalPos;v_data0=a_tex/u_texsize;v_data1=vec3(gamma_scale,size,total_opacity);}`),symbolTextAndIcon:J(`#define SDF_PX 8.0 +vec4 finalPos=u_coord_matrix*vec4(projected_pos.xy/projected_pos.w+rotation_matrix*(a_offset/32.0*max(a_minFontScale,fontScale)+a_pxoffset)*projectionScaling,z,1.0);if(u_pitch_with_map) {finalPos=projectTileWithElevation(finalPos.xy,finalPos.z);}float gamma_scale=finalPos.w;gl_Position=finalPos;v_data0=a_tex/u_texsize;v_data1=vec3(gamma_scale,size,total_opacity);}`),symbolTextAndIcon:Y(`#define SDF_PX 8.0 #define SDF 1.0 #define ICON 0.0 uniform bool u_is_halo;uniform bool u_is_text;uniform sampler2D u_texture;uniform sampler2D u_texture_icon;uniform highp float u_gamma_scale;uniform lowp float u_device_pixel_ratio;in vec4 v_data0;in vec3 v_data1;flat in float v_is_sdf; @@ -729,7 +729,7 @@ return;}vec2 tex=v_data0.xy;float EDGE_GAMMA=0.105/u_device_pixel_ratio;float ga #ifdef OVERDRAW_INSPECTOR fragColor=vec4(1.0); #endif -}`,`layout(location=0) in vec4 a_pos_offset;layout(location=1) in vec4 a_data;layout(location=2) in vec3 a_projected_pos;layout(location=3) in float a_fade_opacity;uniform bool u_is_size_zoom_constant;uniform bool u_is_size_feature_constant;uniform highp float u_size_t;uniform highp float u_size;uniform mat4 u_label_plane_matrix;uniform mat4 u_coord_matrix;uniform bool u_is_text;uniform bool u_pitch_with_map;uniform highp float u_pitch;uniform bool u_rotate_symbol;uniform highp float u_aspect_ratio;uniform highp float u_camera_to_center_distance;uniform float u_fade_change;uniform vec2 u_texsize;uniform vec2 u_texsize_icon;uniform bool u_is_along_line;uniform bool u_is_variable_anchor;uniform vec2 u_translation;uniform float u_pitched_scale;uniform bool u_is_offset;out vec4 v_data0;out vec3 v_data1;flat out float v_is_sdf; +}`,`layout(location=0) in vec4 a_pos_offset;layout(location=1) in uvec4 a_data;layout(location=2) in vec3 a_projected_pos;layout(location=3) in uint a_fade_opacity;layout(location=4) in float a_height_offset;uniform bool u_is_size_zoom_constant;uniform bool u_is_size_feature_constant;uniform highp float u_size_t;uniform highp float u_size;uniform mat4 u_label_plane_matrix;uniform mat4 u_coord_matrix;uniform bool u_is_text;uniform bool u_pitch_with_map;uniform highp float u_pitch;uniform bool u_rotate_symbol;uniform highp float u_aspect_ratio;uniform highp float u_camera_to_center_distance;uniform float u_fade_change;uniform vec2 u_texsize;uniform vec2 u_texsize_icon;uniform bool u_is_along_line;uniform bool u_is_variable_anchor;uniform vec2 u_translation;uniform float u_pitched_scale;uniform bool u_is_offset;uniform bool u_height_anchor_ground;out vec4 v_data0;out vec3 v_data1;flat out float v_is_sdf; #pragma maplibre: define highp vec4 fill_color #pragma maplibre: define highp vec4 halo_color #pragma maplibre: define lowp float opacity @@ -741,17 +741,17 @@ void main() { #pragma maplibre: initialize lowp float opacity #pragma maplibre: initialize lowp float halo_width #pragma maplibre: initialize lowp float halo_blur -vec2 a_pos=a_pos_offset.xy;vec2 a_offset=a_pos_offset.zw;vec2 a_tex=a_data.xy;vec2 a_size=a_data.zw;float a_size_min=floor(a_size[0]*0.5);float is_sdf=a_size[0]-2.0*a_size_min;float ele=get_elevation(a_pos);highp float segment_angle=-a_projected_pos[2];float size;if (!u_is_size_zoom_constant && !u_is_size_feature_constant) {size=mix(a_size_min,a_size[1],u_size_t)/128.0;} else if (u_is_size_zoom_constant && !u_is_size_feature_constant) {size=a_size_min/128.0;} else {size=u_size;}vec2 translated_a_pos=a_pos+u_translation;vec4 projectedPoint=projectTileWithElevation(translated_a_pos,ele);vec2 fade_opacity=unpack_opacity(a_fade_opacity);float visibility=calculate_visibility(projectedPoint);float fade_change=fade_opacity[1] > 0.5 ? u_fade_change :-u_fade_change;float interpolated_fade_opacity=max(0.0,min(visibility,fade_opacity[0]+fade_change));float total_opacity=opacity*interpolated_fade_opacity;if (total_opacity < 0.1){gl_Position=vec4(-2.,-2.,-2.,1.);return;}highp float camera_to_anchor_distance=projectedPoint.w;highp float distance_ratio=u_pitch_with_map ? +vec2 a_pos=a_pos_offset.xy;vec2 a_offset=a_pos_offset.zw;vec2 a_tex=vec2(a_data.xy);vec2 a_size=vec2(a_data.zw);float a_size_min=float(a_data.z >> 1u);float is_sdf=float(a_data.z & 1u);float ele=a_height_offset+(u_height_anchor_ground ? get_elevation(a_pos) : 0.0);highp float segment_angle=-a_projected_pos[2];float size;if (!u_is_size_zoom_constant && !u_is_size_feature_constant) {size=mix(a_size_min,a_size[1],u_size_t)/128.0;} else if (u_is_size_zoom_constant && !u_is_size_feature_constant) {size=a_size_min/128.0;} else {size=u_size;}vec2 translated_a_pos=a_pos+u_translation;vec4 projectedPoint=projectTileWithElevation(translated_a_pos,ele);vec2 fade_opacity=unpack_opacity(a_fade_opacity);float visibility=calculate_visibility(projectedPoint);float fade_change=fade_opacity[1] > 0.5 ? u_fade_change :-u_fade_change;float interpolated_fade_opacity=max(0.0,min(visibility,fade_opacity[0]+fade_change));float total_opacity=opacity*interpolated_fade_opacity;if (total_opacity < 0.1){gl_Position=vec4(-2.,-2.,-2.,1.);return;}highp float camera_to_anchor_distance=projectedPoint.w;highp float distance_ratio=u_pitch_with_map ? camera_to_anchor_distance/u_camera_to_center_distance : u_camera_to_center_distance/camera_to_anchor_distance;highp float perspective_ratio=clamp(0.5+0.5*distance_ratio,0.0,4.0);if (!u_is_offset) {size*=perspective_ratio;}float fontScale=size/24.0;highp float symbol_rotation=0.0;if (u_rotate_symbol) {vec4 offsetProjectedPoint=projectTileWithElevation(translated_a_pos+vec2(1,0),ele);vec2 a=projectedPoint.xy/projectedPoint.w;vec2 b=offsetProjectedPoint.xy/offsetProjectedPoint.w;symbol_rotation=atan((b.y-a.y)/u_aspect_ratio,b.x-a.x);}highp float angle_sin=sin(segment_angle+symbol_rotation);highp float angle_cos=cos(segment_angle+symbol_rotation);mat2 rotation_matrix=mat2(angle_cos,-1.0*angle_sin,angle_sin,angle_cos);vec4 projected_pos;if (u_is_along_line || u_is_variable_anchor) {projected_pos=vec4(a_projected_pos.xy,ele,1.0);} else if (u_pitch_with_map) {projected_pos=u_label_plane_matrix*vec4(a_projected_pos.xy+u_translation,ele,1.0);} else {projected_pos=u_label_plane_matrix*projectTileWithElevation(a_projected_pos.xy+u_translation,ele);}float z=float(u_pitch_with_map)*projected_pos.z/projected_pos.w;float projectionScaling=1.0; #ifdef GLOBE if(u_pitch_with_map && !u_is_along_line) {float anchor_pos_tile_y=(u_coord_matrix*vec4(projected_pos.xy/projected_pos.w,z,1.0)).y;projectionScaling=mix(projectionScaling,1.0/circumferenceRatioAtTileY(anchor_pos_tile_y)*u_pitched_scale,u_projection_transition);} #endif -vec4 finalPos=u_coord_matrix*vec4(projected_pos.xy/projected_pos.w+rotation_matrix*(a_offset/32.0*fontScale)*projectionScaling,z,1.0);if(u_pitch_with_map) {finalPos=projectTileWithElevation(finalPos.xy,finalPos.z);}float gamma_scale=finalPos.w;gl_Position=finalPos;v_data0.xy=a_tex/u_texsize;v_data0.zw=a_tex/u_texsize_icon;v_data1=vec3(gamma_scale,size,total_opacity);v_is_sdf=is_sdf;}`),terrain:J(`uniform sampler2D u_texture;uniform vec4 u_fog_color;uniform vec4 u_horizon_color;uniform float u_fog_ground_blend;uniform float u_fog_ground_blend_opacity;uniform float u_horizon_fog_blend;uniform bool u_is_globe_mode;in vec2 v_texture_pos;in float v_fog_depth;const float gamma=2.2;vec4 gammaToLinear(vec4 color) {return pow(color,vec4(gamma));}vec4 linearToGamma(vec4 color) {return pow(color,vec4(1.0/gamma));}void main() {vec4 surface_color=texture(u_texture,vec2(v_texture_pos.x,1.0-v_texture_pos.y));if (!u_is_globe_mode && u_fog_ground_blend_opacity > 0.0 && v_fog_depth > u_fog_ground_blend) {vec4 surface_color_linear=gammaToLinear(surface_color);float blend_color=smoothstep(0.0,1.0,max((v_fog_depth-u_horizon_fog_blend)/(1.0-u_horizon_fog_blend),0.0));vec4 fog_horizon_color_linear=mix(gammaToLinear(u_fog_color),gammaToLinear(u_horizon_color),blend_color);float factor_fog=max(v_fog_depth-u_fog_ground_blend,0.0)/(1.0-u_fog_ground_blend);fragColor=linearToGamma(mix(surface_color_linear,fog_horizon_color_linear,pow(factor_fog,2.0)*u_fog_ground_blend_opacity));} else {fragColor=surface_color;}}`,`layout(location=0) in vec3 a_pos3d;uniform mat4 u_fog_matrix;uniform float u_ele_delta;out vec2 v_texture_pos;out float v_fog_depth;void main() {float ele=get_elevation(a_pos3d.xy);float ele_delta=a_pos3d.z==1.0 ? u_ele_delta : 0.0;v_texture_pos=a_pos3d.xy/8192.0;gl_Position=projectTileFor3D(a_pos3d.xy,ele-ele_delta);vec4 pos=u_fog_matrix*vec4(a_pos3d.xy,ele,1.0);v_fog_depth=pos.z/pos.w*0.5+0.5;}`),terrainDepth:J(`in float v_depth;const highp vec4 bitSh=vec4(256.*256.*256.,256.*256.,256.,1.);const highp vec4 bitMsk=vec4(0.,vec3(1./256.0));highp vec4 pack(highp float value) {highp vec4 comp=fract(value*bitSh);comp-=comp.xxyz*bitMsk;return comp;}void main() {fragColor=pack(v_depth);}`,`layout(location=0) in vec3 a_pos3d;uniform float u_ele_delta;out float v_depth;void main() {float ele=get_elevation(a_pos3d.xy);float ele_delta=a_pos3d.z==1.0 ? u_ele_delta : 0.0;gl_Position=projectTileFor3D(a_pos3d.xy,ele-ele_delta);v_depth=gl_Position.z/gl_Position.w;}`),terrainCoords:J(`precision mediump float;uniform sampler2D u_texture;uniform float u_terrain_coords_id;in vec2 v_texture_pos;void main() {vec4 rgba=texture(u_texture,v_texture_pos);fragColor=vec4(rgba.r,rgba.g,rgba.b,u_terrain_coords_id);}`,`layout(location=0) in vec3 a_pos3d;uniform float u_ele_delta;out vec2 v_texture_pos;void main() {float ele=get_elevation(a_pos3d.xy);float ele_delta=a_pos3d.z==1.0 ? u_ele_delta : 0.0;v_texture_pos=a_pos3d.xy/8192.0;gl_Position=projectTileFor3D(a_pos3d.xy,ele-ele_delta);}`),projectionErrorMeasurement:J(`flat in vec4 v_output_error_encoded;void main() {fragColor=v_output_error_encoded;}`,`layout(location=0) in vec2 a_pos;uniform highp float u_input;uniform highp float u_output_expected;flat out vec4 v_output_error_encoded;void main() {float real_output=2.0*atan(exp(PI-(u_input*PI*2.0)))-PI*0.5;float error=real_output-u_output_expected;float abs_error=abs(error)*128.0;v_output_error_encoded.x=min(floor(abs_error*256.0),255.0)/255.0;abs_error-=v_output_error_encoded.x;v_output_error_encoded.y=min(floor(abs_error*65536.0),255.0)/255.0;abs_error-=v_output_error_encoded.x/255.0;v_output_error_encoded.z=min(floor(abs_error*16777216.0),255.0)/255.0;v_output_error_encoded.w=error >=0.0 ? 1.0 : 0.0;gl_Position=vec4(a_pos,0.0,1.0);}`),atmosphere:J(`#ifdef GL_ES +vec4 finalPos=u_coord_matrix*vec4(projected_pos.xy/projected_pos.w+rotation_matrix*(a_offset/32.0*fontScale)*projectionScaling,z,1.0);if(u_pitch_with_map) {finalPos=projectTileWithElevation(finalPos.xy,finalPos.z);}float gamma_scale=finalPos.w;gl_Position=finalPos;v_data0.xy=a_tex/u_texsize;v_data0.zw=a_tex/u_texsize_icon;v_data1=vec3(gamma_scale,size,total_opacity);v_is_sdf=is_sdf;}`),terrain:Y(`uniform sampler2D u_texture;uniform vec4 u_fog_color;uniform vec4 u_horizon_color;uniform float u_fog_ground_blend;uniform float u_fog_ground_blend_opacity;uniform float u_horizon_fog_blend;uniform bool u_is_globe_mode;in vec2 v_texture_pos;in float v_fog_depth;const float gamma=2.2;vec4 gammaToLinear(vec4 color) {return pow(color,vec4(gamma));}vec4 linearToGamma(vec4 color) {return pow(color,vec4(1.0/gamma));}void main() {vec4 surface_color=texture(u_texture,vec2(v_texture_pos.x,1.0-v_texture_pos.y));if (!u_is_globe_mode && u_fog_ground_blend_opacity > 0.0 && v_fog_depth > u_fog_ground_blend) {vec4 surface_color_linear=gammaToLinear(surface_color);float blend_color=smoothstep(0.0,1.0,max((v_fog_depth-u_horizon_fog_blend)/(1.0-u_horizon_fog_blend),0.0));vec4 fog_horizon_color_linear=mix(gammaToLinear(u_fog_color),gammaToLinear(u_horizon_color),blend_color);float factor_fog=max(v_fog_depth-u_fog_ground_blend,0.0)/(1.0-u_fog_ground_blend);fragColor=linearToGamma(mix(surface_color_linear,fog_horizon_color_linear,pow(factor_fog,2.0)*u_fog_ground_blend_opacity));} else {fragColor=surface_color;}}`,`layout(location=0) in vec3 a_pos3d;uniform mat4 u_fog_matrix;uniform float u_ele_delta;out vec2 v_texture_pos;out float v_fog_depth;void main() {float ele=get_elevation(a_pos3d.xy);float ele_delta=a_pos3d.z==1.0 ? u_ele_delta : 0.0;v_texture_pos=a_pos3d.xy/8192.0;gl_Position=projectTileFor3D(a_pos3d.xy,ele-ele_delta);vec4 pos=u_fog_matrix*vec4(a_pos3d.xy,ele,1.0);v_fog_depth=pos.z/pos.w*0.5+0.5;}`),terrainDepth:Y(`in float v_depth;const highp vec4 bitSh=vec4(256.*256.*256.,256.*256.,256.,1.);const highp vec4 bitMsk=vec4(0.,vec3(1./256.0));highp vec4 pack(highp float value) {highp vec4 comp=fract(value*bitSh);comp-=comp.xxyz*bitMsk;return comp;}void main() {fragColor=pack(v_depth);}`,`layout(location=0) in vec3 a_pos3d;uniform float u_ele_delta;out float v_depth;void main() {float ele=get_elevation(a_pos3d.xy);float ele_delta=a_pos3d.z==1.0 ? u_ele_delta : 0.0;gl_Position=projectTileFor3D(a_pos3d.xy,ele-ele_delta);v_depth=gl_Position.z/gl_Position.w;}`),atmosphere:Y(`#ifdef GL_ES precision highp float; #endif in vec3 view_direction;uniform vec3 u_sun_pos;uniform vec3 u_globe_position;uniform float u_globe_radius;uniform float u_atmosphere_blend;/**Shader use from https:*Made some change to adapt to MapLibre Globe geometry*/const float PI=3.141592653589793;const int iSteps=5;const int jSteps=3;/*radius of the planet*/const float EARTH_RADIUS=6371e3;/*radius of the atmosphere*/const float ATMOS_RADIUS=6471e3;vec2 rsi(vec3 r0,vec3 rd,float sr) {float a=dot(rd,rd);float b=2.0*dot(rd,r0);float c=dot(r0,r0)-(sr*sr);float d=(b*b)-4.0*a*c;if (d < 0.0) return vec2(1e5,-1e5);return vec2((-b-sqrt(d))/(2.0*a),(-b+sqrt(d))/(2.0*a));}vec4 atmosphere(vec3 r,vec3 r0,vec3 pSun,float iSun,float rPlanet,float rAtmos,vec3 kRlh,float kMie,float shRlh,float shMie,float g) {pSun=normalize(pSun);r=normalize(r);vec2 p=rsi(r0,r,rAtmos);if (p.x > p.y) {return vec4(0.0,0.0,0.0,1.0);}if (p.x < 0.0) {p.x=0.0;}vec3 pos=r0+r*p.x;vec2 p2=rsi(r0,r,rPlanet);if (p2.x <=p2.y && p2.x > 0.0) {p.y=min(p.y,p2.x);}float iStepSize=(p.y-p.x)/float(iSteps);float iTime=p.x+iStepSize*0.5;vec3 totalRlh=vec3(0,0,0);vec3 totalMie=vec3(0,0,0);float iOdRlh=0.0;float iOdMie=0.0;float mu=dot(r,pSun);float mumu=mu*mu;float gg=g*g;float pRlh=3.0/(16.0*PI)*(1.0+mumu);float pMie=3.0/(8.0*PI)*((1.0-gg)*(mumu+1.0))/(pow(1.0+gg-2.0*mu*g,1.5)*(2.0+gg));for (int i=0; i < iSteps; i++) {vec3 iPos=r0+r*iTime;float iHeight=length(iPos)-rPlanet;float odStepRlh=exp(-iHeight/shRlh)*iStepSize;float odStepMie=exp(-iHeight/shMie)*iStepSize;iOdRlh+=odStepRlh;iOdMie+=odStepMie;float jStepSize=rsi(iPos,pSun,rAtmos).y/float(jSteps);float jTime=jStepSize*0.5;float jOdRlh=0.0;float jOdMie=0.0;for (int j=0; j < jSteps; j++) {vec3 jPos=iPos+pSun*jTime;float jHeight=length(jPos)-rPlanet;jOdRlh+=exp(-jHeight/shRlh)*jStepSize;jOdMie+=exp(-jHeight/shMie)*jStepSize;jTime+=jStepSize;}vec3 attn=exp(-(kMie*(iOdMie+jOdMie)+kRlh*(iOdRlh+jOdRlh)));totalRlh+=odStepRlh*attn;totalMie+=odStepMie*attn;iTime+=iStepSize;}float opacity=exp(-(length(kRlh)*length(totalRlh)+kMie*length(totalMie)));vec3 color=iSun*(pRlh*kRlh*totalRlh+pMie*kMie*totalMie);return vec4(color,opacity);}void main() {vec3 scale_camera_pos=-u_globe_position*EARTH_RADIUS/u_globe_radius;vec4 color=atmosphere(normalize(view_direction),scale_camera_pos,u_sun_pos,22.0,EARTH_RADIUS,ATMOS_RADIUS,vec3(5.5e-6,13.0e-6,22.4e-6),21e-6,8e3,1.2e3,0.758 -);color.rgb=1.0-exp(-1.0*color.rgb);color=pow(color,vec4(1.0/2.2));fragColor=vec4(color.rgb,1.0-color.a)*u_atmosphere_blend;}`,`layout(location=0) in vec2 a_pos;uniform mat4 u_inv_proj_matrix;out vec3 view_direction;void main() {view_direction=(u_inv_proj_matrix*vec4(a_pos,0.0,1.0)).xyz;gl_Position=vec4(a_pos,0.0,1.0);}`),sky:J(`uniform vec4 u_sky_color;uniform vec4 u_horizon_color;uniform vec2 u_horizon;uniform vec2 u_horizon_normal;uniform float u_sky_horizon_blend;uniform float u_sky_blend;void main() {float x=gl_FragCoord.x;float y=gl_FragCoord.y;float blend=(y-u_horizon.y)*u_horizon_normal.y+(x-u_horizon.x)*u_horizon_normal.x;if (blend > 0.0) {if (blend < u_sky_horizon_blend) {fragColor=mix(u_sky_color,u_horizon_color,pow(1.0-blend/u_sky_horizon_blend,2.0));} else {fragColor=u_sky_color;}}fragColor=mix(fragColor,vec4(vec3(0.0),0.0),u_sky_blend);}`,`layout(location=0) in vec2 a_pos;void main() {gl_Position=vec4(a_pos,1.0,1.0);}`)};function J(e,t){let n=/#pragma maplibre: ([\w]+) ([\w]+) ([\w]+) ([\w]+)/g,r=t.match(/in ([\w]+) ([\w]+)/g),i=e.match(/uniform ([\w]+) ([\w]+)([\s]*)([\w]*)/g),a=t.match(/uniform ([\w]+) ([\w]+)([\s]*)([\w]*)/g),o=a?a.concat(i):i,s=r?r.length:0,c={};return e=e.replace(n,(e,t,n,r,i)=>(c[i]=!0,t===`define`?` +);color.rgb=1.0-exp(-1.0*color.rgb);color=pow(color,vec4(1.0/2.2));fragColor=vec4(color.rgb,1.0-color.a)*u_atmosphere_blend;}`,`layout(location=0) in vec2 a_pos;uniform mat4 u_inv_proj_matrix;out vec3 view_direction;void main() {view_direction=(u_inv_proj_matrix*vec4(a_pos,0.0,1.0)).xyz;gl_Position=vec4(a_pos,0.0,1.0);}`),sky:Y(`uniform vec4 u_sky_color;uniform vec4 u_horizon_color;uniform vec2 u_horizon;uniform vec2 u_horizon_normal;uniform float u_sky_horizon_blend;uniform float u_sky_blend;void main() {float x=gl_FragCoord.x;float y=gl_FragCoord.y;float blend=(y-u_horizon.y)*u_horizon_normal.y+(x-u_horizon.x)*u_horizon_normal.x;if (blend > 0.0) {if (blend < u_sky_horizon_blend) {fragColor=mix(u_sky_color,u_horizon_color,pow(1.0-blend/u_sky_horizon_blend,2.0));} else {fragColor=u_sky_color;}}fragColor=mix(fragColor,vec4(vec3(0.0),0.0),u_sky_blend);}`,`layout(location=0) in vec2 a_pos;void main() {gl_Position=vec4(a_pos,1.0,1.0);}`)};function Y(e,t){let n=/#pragma maplibre: ([\w]+) ([\w]+) ([\w]+) ([\w]+)/g,r=t.match(/in ([\w]+) ([\w]+)/g),i=e.match(/uniform ([\w]+) ([\w]+)([\s]*)([\w]*)/g),a=t.match(/uniform ([\w]+) ([\w]+)([\s]*)([\w]*)/g),o=a?a.concat(i):i,s=r?r.length:0,c={};return e=e.replace(n,(e,t,n,r,i)=>(c[i]=!0,t===`define`?` #ifndef HAS_UNIFORM_u_${i} in ${n} ${r} ${i}; #else @@ -800,7 +800,7 @@ uniform ${n} ${r} u_${i}; #else ${n} ${r} ${i} = u_${i}; #endif -`}),{fragmentSource:e,vertexSource:t,staticAttributes:r,staticUniforms:o}}var us=class{constructor(e,t,n){this.vertexBuffer=e,this.indexBuffer=t,this.segments=n}destroy(){this.vertexBuffer.destroy(),this.indexBuffer.destroy(),this.segments.destroy(),this.vertexBuffer=null,this.indexBuffer=null,this.segments=null}};const ds=yr([{name:`a_pos`,type:`Int16`,components:2}]),fs=`#define PROJECTION_MERCATOR`,ps=`mercator`;var ms=class{constructor(){this._cachedMesh=null}get name(){return`mercator`}get useSubdivision(){return!1}get shaderVariantName(){return ps}get shaderDefine(){return fs}get shaderPreludeCode(){return ls.projectionMercator}get vertexShaderPreludeCode(){return ls.projectionMercator.vertexSource}get subdivisionGranularity(){return Nt.noSubdivision}get useGlobeControls(){return!1}get transitionState(){return 0}get latitudeErrorCorrectionRadians(){return 0}destroy(){}updateGPUdependent(e){}getMeshFromTileID(e,t,n,r,i){if(this._cachedMesh)return this._cachedMesh;let a=new O;a.emplaceBack(0,0),a.emplaceBack(M,0),a.emplaceBack(0,M),a.emplaceBack(M,M);let s=e.createVertexBuffer(a,ds.members),c=o.simpleSegment(0,0,4,2),l=new He;l.emplaceBack(1,0,2),l.emplaceBack(1,2,3);let u=e.createIndexBuffer(l);return this._cachedMesh=new us(s,u,c),this._cachedMesh}recalculate(){}hasTransition(){return!1}setErrorQueryLatitudeDegrees(e){}},hs=class e{constructor(e=0,t=0,n=0,r=0){if(isNaN(e)||e<0||isNaN(t)||t<0||isNaN(n)||n<0||isNaN(r)||r<0)throw Error(`Invalid value for edge-insets, top, bottom, left and right must all be numbers`);this.top=e,this.bottom=t,this.left=n,this.right=r}interpolate(e,t,n){return t.top!=null&&e.top!=null&&(this.top=Yn.number(e.top,t.top,n)),t.bottom!=null&&e.bottom!=null&&(this.bottom=Yn.number(e.bottom,t.bottom,n)),t.left!=null&&e.left!=null&&(this.left=Yn.number(e.left,t.left,n)),t.right!=null&&e.right!=null&&(this.right=Yn.number(e.right,t.right,n)),this}getCenter(e,t){return new z(j((this.left+e-this.right)/2,0,e),j((this.top+t-this.bottom)/2,0,t))}equals(e){return this.top===e.top&&this.bottom===e.bottom&&this.left===e.left&&this.right===e.right}clone(){return new e(this.top,this.bottom,this.left,this.right)}toJSON(){return{top:this.top,bottom:this.bottom,left:this.left,right:this.right}}};function gs(e,t){if(!e.renderWorldCopies||e.lngRange)return;let n=t.lng-e.center.lng;t.lng+=n>180?-360:n<-180?360:0}function _s(e){return Math.max(0,Math.floor(e))}var vs=class{constructor(e,t){this.applyConstrain=(e,t)=>this._constrainOverride===null?this._callbacks.defaultConstrain(e,t):this._constrainOverride(e,t),this._callbacks=e,this._tileSize=512,this._renderWorldCopies=t?.renderWorldCopies===void 0||!!t?.renderWorldCopies,this._minZoom=t?.minZoom||0,this._maxZoom=t?.maxZoom||22,this._minPitch=t?.minPitch===void 0||t?.minPitch===null?0:t?.minPitch,this._maxPitch=t?.maxPitch===void 0||t?.maxPitch===null?60:t?.maxPitch,this._constrainOverride=t?.constrainOverride??null,this.setMaxBounds(),this._width=0,this._height=0,this._center=new B(0,0),this._elevation=0,this._zoom=0,this._tileZoom=_s(this._zoom),this._scale=Se(this._zoom),this._bearingInRadians=0,this._fovInRadians=.6435011087932844,this._pitchInRadians=0,this._rollInRadians=0,this._unmodified=!0,this._edgeInsets=new hs,this._minElevationForCurrentTile=0,this._autoCalculateNearFarZ=!0}apply(e,t,n){this._constrainOverride=e.constrainOverride,this._latRange=e.latRange,this._lngRange=e.lngRange,this._width=e.width,this._height=e.height,this._center=e.center,this._elevation=e.elevation,this._minElevationForCurrentTile=e.minElevationForCurrentTile,this._zoom=e.zoom,this._tileZoom=_s(this._zoom),this._scale=Se(this._zoom),this._bearingInRadians=e.bearingInRadians,this._fovInRadians=e.fovInRadians,this._pitchInRadians=e.pitchInRadians,this._rollInRadians=e.rollInRadians,this._unmodified=e.unmodified,this._edgeInsets=new hs(e.padding.top,e.padding.bottom,e.padding.left,e.padding.right),this._minZoom=e.minZoom,this._maxZoom=e.maxZoom,this._minPitch=e.minPitch,this._maxPitch=e.maxPitch,this._renderWorldCopies=e.renderWorldCopies,this._cameraToCenterDistance=e.cameraToCenterDistance,this._nearZ=e.nearZ,this._farZ=e.farZ,this._autoCalculateNearFarZ=!n&&e.autoCalculateNearFarZ,t&&this.constrainInternal(),this._calcMatrices()}get pixelsToClipSpaceMatrix(){return this._pixelsToClipSpaceMatrix}get clipSpaceToPixelsMatrix(){return this._clipSpaceToPixelsMatrix}get minElevationForCurrentTile(){return this._minElevationForCurrentTile}setMinElevationForCurrentTile(e){this._minElevationForCurrentTile=e}get tileSize(){return this._tileSize}get tileZoom(){return this._tileZoom}get scale(){return this._scale}get width(){return this._width}get height(){return this._height}get bearingInRadians(){return this._bearingInRadians}get lngRange(){return this._lngRange}get latRange(){return this._latRange}get pixelsToGLUnits(){return this._pixelsToGLUnits}get minZoom(){return this._minZoom}setMinZoom(e){this._minZoom!==e&&(this._minZoom=e,this.setZoom(this.applyConstrain(this._center,this.zoom).zoom))}get maxZoom(){return this._maxZoom}setMaxZoom(e){this._maxZoom!==e&&(this._maxZoom=e,this.setZoom(this.applyConstrain(this._center,this.zoom).zoom))}get minPitch(){return this._minPitch}setMinPitch(e){this._minPitch!==e&&(this._minPitch=e,this.setPitch(Math.max(this.pitch,e)))}get maxPitch(){return this._maxPitch}setMaxPitch(e){this._maxPitch!==e&&(this._maxPitch=e,this.setPitch(Math.min(this.pitch,e)))}get renderWorldCopies(){return this._renderWorldCopies}setRenderWorldCopies(e){e===void 0?e=!0:e===null&&(e=!1),this._renderWorldCopies=e}get constrainOverride(){return this._constrainOverride}setConstrainOverride(e){e===void 0&&(e=null),this._constrainOverride!==e&&(this._constrainOverride=e,this.constrainInternal(),this._calcMatrices())}get worldSize(){return this._tileSize*this._scale}get centerOffset(){return this.centerPoint._sub(this.size._div(2))}get size(){return new z(this._width,this._height)}get bearing(){return this._bearingInRadians/Math.PI*180}setBearing(e){let t=sn(e,-180,180)*Math.PI/180;this._bearingInRadians!==t&&(this._unmodified=!1,this._bearingInRadians=t,this._calcMatrices(),this._rotationMatrix=xr(),Cr(this._rotationMatrix,this._rotationMatrix,-this._bearingInRadians))}get rotationMatrix(){return this._rotationMatrix}get pitchInRadians(){return this._pitchInRadians}get pitch(){return this._pitchInRadians/Math.PI*180}setPitch(e){let t=j(e,this.minPitch,this.maxPitch)/180*Math.PI;this._pitchInRadians!==t&&(this._unmodified=!1,this._pitchInRadians=t,this._calcMatrices())}get rollInRadians(){return this._rollInRadians}get roll(){return this._rollInRadians/Math.PI*180}setRoll(e){let t=e/180*Math.PI;this._rollInRadians!==t&&(this._unmodified=!1,this._rollInRadians=t,this._calcMatrices())}get fovInRadians(){return this._fovInRadians}get fov(){return er(this._fovInRadians)}setFov(e){e=j(e,.1,150),this.fov!==e&&(this._unmodified=!1,this._fovInRadians=k(e),this._calcMatrices())}get zoom(){return this._zoom}setZoom(e){let t=this.applyConstrain(this._center,e).zoom;this._zoom!==t&&(this._unmodified=!1,this._zoom=t,this._tileZoom=Math.max(0,Math.floor(t)),this._scale=Se(t),this.constrainInternal(),this._calcMatrices())}get center(){return this._center}setCenter(e){(e.lat!==this._center.lat||e.lng!==this._center.lng)&&(this._unmodified=!1,this._center=e,this.constrainInternal(),this._calcMatrices())}get elevation(){return this._elevation}setElevation(e){e!==this._elevation&&(this._elevation=e,this.constrainInternal(),this._calcMatrices())}get padding(){return this._edgeInsets.toJSON()}setPadding(e){this._edgeInsets.equals(e)||(this._unmodified=!1,this._edgeInsets.interpolate(this._edgeInsets,e,1),this._calcMatrices())}get centerPoint(){return this._edgeInsets.getCenter(this._width,this._height)}get pixelsPerMeter(){return this._pixelPerMeter}get unmodified(){return this._unmodified}get cameraToCenterDistance(){return this._cameraToCenterDistance}get nearZ(){return this._nearZ}get farZ(){return this._farZ}get autoCalculateNearFarZ(){return this._autoCalculateNearFarZ}overrideNearFarZ(e,t){this._autoCalculateNearFarZ=!1,this._nearZ=e,this._farZ=t,this._calcMatrices()}clearNearFarZOverride(){this._autoCalculateNearFarZ=!0,this._calcMatrices()}isPaddingEqual(e){return this._edgeInsets.equals(e)}interpolatePadding(e,t,n){this._unmodified=!1,this._edgeInsets.interpolate(e,t,n),this.constrainInternal(),this._calcMatrices()}resize(e,t,n=!0){this._width=e,this._height=t,n&&this.constrainInternal(),this._calcMatrices()}getMaxBounds(){return this._latRange?.length!==2||this._lngRange?.length!==2?null:new Ri([this._lngRange[0],this._latRange[0]],[this._lngRange[1],this._latRange[1]])}setMaxBounds(e){e?(this._lngRange=[e.getWest(),e.getEast()],this._latRange=[e.getSouth(),e.getNorth()],this.constrainInternal()):(this._lngRange=null,this._latRange=[-on,on])}getCameraQueryGeometry(e,t){if(t.length===1)return[t[0],e];{let{minX:n,minY:r,maxX:i,maxY:a}=Zt.fromPoints(t).extend(e);return[new z(n,r),new z(i,r),new z(i,a),new z(n,a),new z(n,r)]}}constrainInternal(){if(!this.center||!this._width||!this._height||this._constraining)return;this._constraining=!0;let e=this._unmodified,{center:t,zoom:n}=this.applyConstrain(this.center,this.zoom);this.setCenter(t),this.setZoom(n),this._unmodified=e,this._constraining=!1}_calcMatrices(){if(this._width&&this._height){this._pixelsToGLUnits=[2/this._width,-2/this._height];let e=qt(new Float64Array(16));rr(e,e,[this._width/2,-this._height/2,1]),F(e,e,[1,-1,0]),this._clipSpaceToPixelsMatrix=e,e=qt(new Float64Array(16)),rr(e,e,[1,-1,1]),F(e,e,[-1,-1,0]),rr(e,e,[2/this._width,2/this._height,1]),this._pixelsToClipSpaceMatrix=e;let t=this.fovInRadians/2;this._cameraToCenterDistance=.5/Math.tan(t)*this._height}this._callbacks.calcMatrices()}calculateCenterFromCameraLngLatAlt(e,t,n,r){let i=n===void 0?this.bearing:n,a=r=r===void 0?this.pitch:r,{distanceToCenter:o,clampedElevation:s}=this._distanceToCenterFromAltElevationPitch(t,this.elevation,a),{x:c,y:l}=ka(a,i),u=N.fromLngLat(e,t),f=d(1,u.y),p,m,h=0;do{if(h+=1,h>10)break;m=o/f;let e=c*m,t=l*m;p=new N(u.x+e,u.y+t),f=1/p.meterInMercatorCoordinateUnits()}while(Math.abs(o-m*f)>1e-12);return{center:p.toLngLat(),elevation:s,zoom:ar(this.height/2/Math.tan(this.fovInRadians/2)/m/this.tileSize)}}recalculateZoomAndCenter(e){if(this.elevation-e===0)return;let t=1/this.worldSize,n=Oe(1,this.center.lat)*this.worldSize,r=N.fromLngLat(this.center,this.elevation),i=r.x/t,a=r.y/t,o=r.z/t,s=this.pitch,c=this.bearing,{x:l,y:u,z:d}=ka(s,c),f=this.cameraToCenterDistance,p=i+f*-l,m=a+f*-u,h=o+f*d,{distanceToCenter:g,clampedElevation:_}=this._distanceToCenterFromAltElevationPitch(h/n,e,s),v=g*n,y=p+l*v,b=m+u*v,x=new N(y*t,b*t,0).toLngLat(),S=Oe(1,x.lat),C=ar(this.height/2/Math.tan(this.fovInRadians/2)/g/S/this.tileSize);this._elevation=_,this._center=x,this.setZoom(C)}_distanceToCenterFromAltElevationPitch(e,t,n){let r=-Math.cos(k(n)),i=e-t,a,o=t;return r*i>=0||Math.abs(r)<.1?(a=1e4,o=e+a*r):a=-i/r,{distanceToCenter:a,clampedElevation:o}}getCameraPoint(){let e=this.pitchInRadians,t=Math.tan(e)*(this.cameraToCenterDistance||1);return this.centerPoint.add(new z(t*Math.sin(this.rollInRadians),t*Math.cos(this.rollInRadians)))}getCameraAltitude(){return Math.cos(this.pitchInRadians)*this._cameraToCenterDistance/this._pixelPerMeter+this.elevation}getCameraLngLat(){let e=Oe(1,this.center.lat)*this.worldSize,t=this.cameraToCenterDistance/e;return Oa(this.center,this.elevation,this.pitch,this.bearing,t).toLngLat()}getMercatorTileCoordinates(e){if(!e)return[0,0,1,1];let t=e.canonical.z>=0?1<this.max[0]||e.aabb.min[1]>this.max[1]||e.aabb.min[2]>this.max[2]||e.aabb.max[0]0?(t+=e[r]*this.min[r],n+=e[r]*this.max[r]):(n+=e[r]*this.min[r],t+=e[r]*this.max[r]);return t>=0?2:n<0?0:1}},bs=class{distanceToTile2d(e,t,n,r){let i=r,a=i.distanceX([e,t]),o=i.distanceY([e,t]);return Math.hypot(a,o)}getWrap(e,t,n){return n}getTileBoundingVolume(e,t,n,r){let i=0,a=0;if(r?.terrain){let o=new ht(e.z,t,e.z,e.x,e.y),s=r.terrain.getMinMaxElevation(o);i=s.minElevation??Math.min(0,n),a=s.maxElevation??Math.max(0,n)}let o=1<n}allowWorldCopies(){return!0}prepareNextFrame(){}},xs=class e{constructor(e,t,n){this.points=e,this.planes=t,this.aabb=n}static fromInvProjectionMatrix(n,r=1,i=0,a,o){let s=[[-1,1,-1,1],[1,1,-1,1],[1,-1,-1,1],[-1,-1,-1,1],[-1,1,1,1],[1,1,1,1],[1,-1,1,1],[-1,-1,1,1]],c=o?[[6,5,4],[0,1,2],[0,3,7],[2,1,5],[3,2,6],[0,4,5]]:[[0,1,2],[6,5,4],[0,3,7],[2,1,5],[3,2,6],[0,4,5]],l=2**i,u=s.map(e=>Ss(e,n,r,l));a&&Cs(u,c[0],a,o);let d=c.map(e=>{let n=t([],de([],xt([],u[e[0]],u[e[1]]),xt([],u[e[2]],u[e[1]]))),r=-dt(n,u[e[1]]);return n.concat(r)}),f=[1/0,1/0,1/0],p=[-1/0,-1/0,-1/0];for(let e of u)for(let t=0;t<3;t++)f[t]=Math.min(f[t],e[t]),p[t]=Math.max(p[t],e[t]);return new e(u,d,new ys(f,p))}};function Ss(e,t,n,r){let i=A([],e,t),a=1/i[3]/n*r;return Qe(i,i,[a,a,1/i[3],a])}function Cs(e,t,n,r){let i=r?4:0,a=r?0:4,o=0,s=[],c=[];for(let t=0;t<4;t++){let n=xt([],e[t+a],e[t+i]),r=pt(n);In(n,n,1/r),s.push(r),c.push(n)}for(let t=0;t<4;t++){let r=mr(e[t+i],c[t],n);o=r!==null&&r>=0?Math.max(o,r):Math.max(o,s[t])}let l=ws(e,t),u=Ts(n,l);if(u!==null){let e=u/dt(c[0],l);o=Math.min(o,e)}for(let t=0;t<4;t++){let n=Math.min(o,s[t]);e[t+a]=[e[t+i][0]+c[t][0]*n,e[t+i][1]+c[t][1]*n,e[t+i][2]+c[t][2]*n,1]}}function ws(e,n){let r=xt([],e[n[0]],e[n[1]]),i=xt([],e[n[2]],e[n[1]]),a=[0,0,0,0];return t(a,de([],r,i)),a[3]=-dt(a,e[n[0]]),a}function Ts(e,t){let n=it([],e,1/ct(e)),r=xt([],t,In([],n,dt(t,n))),i=ct(r);if(i>0){let e=Math.sqrt(1-n[3]*n[3]);return vt(t,he([],In([],n,-n[3]),In([],r,e/i)))}else return null}var Es=class e{get pixelsToClipSpaceMatrix(){return this._helper.pixelsToClipSpaceMatrix}get clipSpaceToPixelsMatrix(){return this._helper.clipSpaceToPixelsMatrix}get pixelsToGLUnits(){return this._helper.pixelsToGLUnits}get centerOffset(){return this._helper.centerOffset}get size(){return this._helper.size}get rotationMatrix(){return this._helper.rotationMatrix}get centerPoint(){return this._helper.centerPoint}get pixelsPerMeter(){return this._helper.pixelsPerMeter}setMinZoom(e){this._helper.setMinZoom(e)}setMaxZoom(e){this._helper.setMaxZoom(e)}setMinPitch(e){this._helper.setMinPitch(e)}setMaxPitch(e){this._helper.setMaxPitch(e)}setRenderWorldCopies(e){this._helper.setRenderWorldCopies(e)}setBearing(e){this._helper.setBearing(e)}setPitch(e){this._helper.setPitch(e)}setRoll(e){this._helper.setRoll(e)}setFov(e){this._helper.setFov(e)}setZoom(e){this._helper.setZoom(e)}setCenter(e){this._helper.setCenter(e)}setElevation(e){this._helper.setElevation(e)}setMinElevationForCurrentTile(e){this._helper.setMinElevationForCurrentTile(e)}setPadding(e){this._helper.setPadding(e)}interpolatePadding(e,t,n){this._helper.interpolatePadding(e,t,n)}isPaddingEqual(e){return this._helper.isPaddingEqual(e)}resize(e,t,n=!0){this._helper.resize(e,t,n)}getMaxBounds(){return this._helper.getMaxBounds()}setMaxBounds(e){this._helper.setMaxBounds(e)}setConstrainOverride(e){this._helper.setConstrainOverride(e)}overrideNearFarZ(e,t){this._helper.overrideNearFarZ(e,t)}clearNearFarZOverride(){this._helper.clearNearFarZOverride()}getCameraQueryGeometry(e){return this._helper.getCameraQueryGeometry(this.getCameraPoint(),e)}get tileSize(){return this._helper.tileSize}get tileZoom(){return this._helper.tileZoom}get scale(){return this._helper.scale}get worldSize(){return this._helper.worldSize}get width(){return this._helper.width}get height(){return this._helper.height}get lngRange(){return this._helper.lngRange}get latRange(){return this._helper.latRange}get minZoom(){return this._helper.minZoom}get maxZoom(){return this._helper.maxZoom}get zoom(){return this._helper.zoom}get center(){return this._helper.center}get minPitch(){return this._helper.minPitch}get maxPitch(){return this._helper.maxPitch}get pitch(){return this._helper.pitch}get pitchInRadians(){return this._helper.pitchInRadians}get roll(){return this._helper.roll}get rollInRadians(){return this._helper.rollInRadians}get bearing(){return this._helper.bearing}get bearingInRadians(){return this._helper.bearingInRadians}get fov(){return this._helper.fov}get fovInRadians(){return this._helper.fovInRadians}get elevation(){return this._helper.elevation}get minElevationForCurrentTile(){return this._helper.minElevationForCurrentTile}get padding(){return this._helper.padding}get unmodified(){return this._helper.unmodified}get renderWorldCopies(){return this._helper.renderWorldCopies}get cameraToCenterDistance(){return this._helper.cameraToCenterDistance}get constrainOverride(){return this._helper.constrainOverride}get nearZ(){return this._helper.nearZ}get farZ(){return this._helper.farZ}get autoCalculateNearFarZ(){return this._helper.autoCalculateNearFarZ}setTransitionState(e,t){}constructor(e){this._posMatrixCache=new Map,this._alignedPosMatrixCache=new Map,this._fogMatrixCacheF32=new Map,this.defaultConstrain=(e,t)=>{t=j(+t,this.minZoom,this.maxZoom);let n={center:new B(e.lng,e.lat),zoom:t},r=this._helper._lngRange;!this._helper._renderWorldCopies&&r===null&&(r=[-179.9999999999,179.9999999999]);let i=this.tileSize*Se(n.zoom),a=0,o=i,s=0,c=i,l=0,u=0,{x:d,y:f}=this.size;if(this._helper._latRange){let e=this._helper._latRange;a=g(e[1])*i,o=g(e[0])*i,o-ao&&(_=o-e)}if(r){let e=(s+c)/2,t=p;this._helper._renderWorldCopies&&(t=sn(p,e-i/2,e+i/2));let n=d/2;t-nc&&(h=c-n)}return(h!==void 0||_!==void 0)&&(n.center=Ta(i,new z(h??p,_??m)).wrap()),n},this.applyConstrain=(e,t)=>this._helper.applyConstrain(e,t),this._helper=new vs({calcMatrices:()=>this._calcMatrices(),defaultConstrain:(e,t)=>this.defaultConstrain(e,t)},e),this._coveringTilesDetailsProvider=new bs}clone(){let t=new e;return t.apply(this,!1),t}apply(e,t,n){this._helper.apply(e,t,n)}get cameraPosition(){return this._cameraPosition}get projectionMatrix(){return this._projectionMatrix}get modelViewProjectionMatrix(){return this._viewProjMatrix}get inverseProjectionMatrix(){return this._invProjMatrix}get mercatorMatrix(){return this._mercatorMatrix}getVisibleUnwrappedCoordinates(e){let t=[new Zn(0,e)];if(this._helper._renderWorldCopies){let n=this.screenPointToMercatorCoordinate(new z(0,0)),r=this.screenPointToMercatorCoordinate(new z(this._helper._width,0)),i=this.screenPointToMercatorCoordinate(new z(this._helper._width,this._helper._height)),a=this.screenPointToMercatorCoordinate(new z(0,this._helper._height)),o=Math.floor(Math.min(n.x,r.x,i.x,a.x)),s=Math.floor(Math.max(n.x,r.x,i.x,a.x));for(let n=o-1;n<=s+1;n++)n!==0&&t.push(new Zn(n,e))}return t}getCameraFrustum(){return xs.fromInvProjectionMatrix(this._invViewProjMatrix,this.worldSize)}getClippingPlane(){return null}getCoveringTilesDetailsProvider(){return this._coveringTilesDetailsProvider}recalculateZoomAndCenter(e){let t=this.screenPointToLocation(this.centerPoint,e),n=e?e.getElevationForLngLatZoom(t,this._helper._tileZoom):0;this._helper.recalculateZoomAndCenter(n)}setLocationAtPoint(e,t){let n=Oe(this.elevation,this.center.lat),r=this.screenPointToMercatorCoordinateAtZ(t,n),i=this.screenPointToMercatorCoordinateAtZ(this.centerPoint,n),a=N.fromLngLat(e),o=new N(a.x-(r.x-i.x),a.y-(r.y-i.y));this.setCenter(o?.toLngLat()),this._helper._renderWorldCopies&&this.setCenter(this.center.wrap())}locationToScreenPoint(e,t){return t?this.coordinatePoint(N.fromLngLat(e),t.getElevationForLngLat(e,this),this._pixelMatrix3D):this.coordinatePoint(N.fromLngLat(e))}screenPointToLocation(e,t){return this.screenPointToMercatorCoordinate(e,t)?.toLngLat()}screenPointToMercatorCoordinate(e,t){if(t){let n=t.pointCoordinate(e);if(n!=null)return n}return this.screenPointToMercatorCoordinateAtZ(e)}screenPointToMercatorCoordinateAtZ(e,t){let n=t||0,r=[e.x,e.y,0,1],i=[e.x,e.y,1,1];A(r,r,this._pixelMatrixInverse),A(i,i,this._pixelMatrixInverse);let a=r[3],o=i[3],s=r[0]/a,c=i[0]/o,l=r[1]/a,u=i[1]/o,d=r[2]/a,f=i[2]/o,p=d===f?0:(n-d)/(f-d);return new N(Yn.number(s,c,p)/this.worldSize,Yn.number(l,u,p)/this.worldSize,n)}coordinatePoint(e,t=0,n=this._pixelMatrix){let r=[e.x*this.worldSize,e.y*this.worldSize,t,1];return A(r,r,n),new z(r[0]/r[3],r[1]/r[3])}getBounds(){let e=Math.max(0,this._helper._height/2-Ea(this));return new Ri().extend(this.screenPointToLocation(new z(0,e))).extend(this.screenPointToLocation(new z(this._helper._width,e))).extend(this.screenPointToLocation(new z(this._helper._width,this._helper._height))).extend(this.screenPointToLocation(new z(0,this._helper._height)))}isPointOnMapSurface(e,t){return t?t.pointCoordinate(e)!=null:e.y>this.height/2-Ea(this)}calculatePosMatrix(e,t=!1,n=!1){let r=e.key??dr(e.wrap,e.canonical.z,e.canonical.z,e.canonical.x,e.canonical.y),i=t?this._alignedPosMatrixCache:this._posMatrixCache;if(i.has(r)){let e=i.get(r);return n?e.f32:e.f64}let a=Da(e,this.worldSize);Qn(a,t?this._alignedProjMatrix:this._viewProjMatrix,a);let o={f64:a,f32:new Float32Array(a)};return i.set(r,o),n?o.f32:o.f64}calculateFogMatrix(e){let t=e.key,n=this._fogMatrixCacheF32;if(n.has(t))return n.get(t);let r=Da(e,this.worldSize);return Qn(r,this._fogMatrix,r),n.set(t,new Float32Array(r)),n.get(t)}calculateCenterFromCameraLngLatAlt(e,t,n,r){return this._helper.calculateCenterFromCameraLngLatAlt(e,t,n,r)}_calculateNearFarZIfNeeded(e,t,n){if(!this._helper.autoCalculateNearFarZ)return;let r=Math.min(this.elevation,this.minElevationForCurrentTile,this.getCameraAltitude()-100),i=e-r*this._helper._pixelPerMeter/Math.cos(t),a=r<0?i:e,o=Math.PI/2+this.pitchInRadians,s=k(this.fov)*(Math.abs(Math.cos(k(this.roll)))*this.height+Math.abs(Math.sin(k(this.roll)))*this.width)/this.height*(.5+n.y/this.height),c=Math.sin(s)*a/Math.sin(j(Math.PI-o-s,.01,Math.PI-.01)),l=Ea(this),u=Math.atan(l/this._helper.cameraToCenterDistance),d=k(90-Sa),f=u>d?2*u*(.5+n.y/(l*2)):d,p=Math.sin(f)*a/Math.sin(j(Math.PI-o-f,.01,Math.PI-.01)),m=Math.min(c,p);this._helper._farZ=(Math.cos(Math.PI/2-t)*m+a)*1.01,this._helper._nearZ=this._helper._height/50}_calcMatrices(){if(!this._helper._height)return;let e=this.centerOffset,t=wa(this.worldSize,this.center),n=t.x,r=t.y;this._helper._pixelPerMeter=Oe(1,this.center.lat)*this.worldSize;let i=k(Math.min(this.pitch,Sa)),a=Math.max(this._helper.cameraToCenterDistance/2,this._helper.cameraToCenterDistance+this._helper._elevation*this._helper._pixelPerMeter/Math.cos(i));this._calculateNearFarZIfNeeded(a,i,e);let o;o=new Float64Array(16),Et(o,this.fovInRadians,this._helper._width/this._helper._height,this._helper._nearZ,this._helper._farZ),this._invProjMatrix=new Float64Array(16),Qa(this._invProjMatrix,o),o[8]=-e.x*2/this._helper._width,o[9]=e.y*2/this._helper._height,this._projectionMatrix=Jn(o),rr(o,o,[1,-1,1]),F(o,o,[0,0,-this._helper.cameraToCenterDistance]),f(o,o,-this.rollInRadians),cr(o,o,this.pitchInRadians),f(o,o,-this.bearingInRadians),F(o,o,[-n,-r,0]),this._mercatorMatrix=rr([],o,[this.worldSize,this.worldSize,this.worldSize]),rr(o,o,[1,1,this._helper._pixelPerMeter]),this._pixelMatrix=Qn(new Float64Array(16),this.clipSpaceToPixelsMatrix,o),F(o,o,[0,0,-this.elevation]),this._viewProjMatrix=o,this._invViewProjMatrix=gt([],o);let s=[0,0,-1,1];A(s,s,this._invViewProjMatrix),this._cameraPosition=[s[0]/s[3],s[1]/s[3],s[2]/s[3]],this._fogMatrix=new Float64Array(16),Et(this._fogMatrix,this.fovInRadians,this.width/this.height,a,this._helper._farZ),this._fogMatrix[8]=-e.x*2/this.width,this._fogMatrix[9]=e.y*2/this.height,rr(this._fogMatrix,this._fogMatrix,[1,-1,1]),F(this._fogMatrix,this._fogMatrix,[0,0,-this.cameraToCenterDistance]),f(this._fogMatrix,this._fogMatrix,-this.rollInRadians),cr(this._fogMatrix,this._fogMatrix,this.pitchInRadians),f(this._fogMatrix,this._fogMatrix,-this.bearingInRadians),F(this._fogMatrix,this._fogMatrix,[-n,-r,0]),rr(this._fogMatrix,this._fogMatrix,[1,1,this._helper._pixelPerMeter]),F(this._fogMatrix,this._fogMatrix,[0,0,-this.elevation]),this._pixelMatrix3D=Qn(new Float64Array(16),this.clipSpaceToPixelsMatrix,o);let c=this._helper._width%2/2,l=this._helper._height%2/2,u=Math.cos(this.bearingInRadians),d=Math.sin(-this.bearingInRadians),p=n-Math.round(n)+u*c+d*l,m=r-Math.round(r)+u*l+d*c,h=new Float64Array(o);if(F(h,h,[p>.5?p-1:p,m>.5?m-1:m,0]),this._alignedProjMatrix=h,o=gt(new Float64Array(16),this._pixelMatrix),!o)throw Error(`failed to invert matrix`);this._pixelMatrixInverse=o,this._clearMatrixCaches()}_clearMatrixCaches(){this._posMatrixCache.clear(),this._alignedPosMatrixCache.clear(),this._fogMatrixCacheF32.clear()}maxPitchScaleFactor(){if(!this._pixelMatrixInverse)return 1;let e=this.screenPointToMercatorCoordinate(new z(0,0)),t=[e.x*this.worldSize,e.y*this.worldSize,0,1];return A(t,t,this._pixelMatrix)[3]/this._helper.cameraToCenterDistance}getCameraPoint(){return this._helper.getCameraPoint()}getCameraAltitude(){return this._helper.getCameraAltitude()}getCameraLngLat(){let e=Oe(1,this.center.lat)*this.worldSize,t=this._helper.cameraToCenterDistance/e;return Oa(this.center,this.elevation,this.pitch,this.bearing,t).toLngLat()}lngLatToCameraDepth(e,t){let n=N.fromLngLat(e),r=[n.x*this.worldSize,n.y*this.worldSize,t,1];return A(r,r,this._viewProjMatrix),r[2]/r[3]}getProjectionData(e){let{overscaledTileID:t,aligned:n,applyTerrainMatrix:r}=e,i=this._helper.getMercatorTileCoordinates(t),a=t?this.calculatePosMatrix(t,n,!0):null,o;return o=t?.terrainRttPosMatrix32f&&r?t.terrainRttPosMatrix32f:a||Be(),{mainMatrix:o,tileMercatorCoords:i,clippingPlane:[0,0,0,0],projectionTransition:0,fallbackMatrix:o}}isLocationOccluded(e){return!1}getPixelScale(){return 1}getCircleRadiusCorrection(){return 1}getPitchedTextCorrection(e,t,n){return 1}transformLightDirection(e){return Nn(e)}getRayDirectionFromPixel(e){throw Error(`Not implemented.`)}projectTileCoordinates(e,t,n,r){let i=this.calculatePosMatrix(n),a;r?(a=[e,t,r(e,t),1],A(a,a,i)):(a=[e,t,0,1],So(a,a,i));let o=a[3];return{point:new z(a[0]/o,a[1]/o),signedDistanceFromCamera:o,isOccluded:!1}}populateCache(e){for(let t of e)this.calculatePosMatrix(t)}getProjectionDataForCustomLayer(e=!0){let t=new ht(0,0,0,0,0),n=this.getProjectionData({overscaledTileID:t,applyGlobeMatrix:e}),r=Da(t,this.worldSize);Qn(r,this._viewProjMatrix,r);let i=[M,M,this.worldSize/this._helper.pixelsPerMeter],a=c();return rr(a,r,i),{...n,tileMercatorCoords:[0,0,1,1],fallbackMatrix:a,mainMatrix:a}}getFastPathSimpleProjectionMatrix(e){return this.calculatePosMatrix(e)}};function Ds(){a(`Map cannot fit within canvas with the given bounds, padding, and/or offset.`)}function Os(e){if(e.useSlerp)if(e.k<1){let t=m(e.startEulerAngles.roll,e.startEulerAngles.pitch,e.startEulerAngles.bearing),n=m(e.endEulerAngles.roll,e.endEulerAngles.pitch,e.endEulerAngles.bearing),r=new Float64Array(4);ie(r,t,n,e.k);let i=rn(r);e.tr.setRoll(i.roll),e.tr.setPitch(i.pitch),e.tr.setBearing(i.bearing)}else e.tr.setRoll(e.endEulerAngles.roll),e.tr.setPitch(e.endEulerAngles.pitch),e.tr.setBearing(e.endEulerAngles.bearing);else e.tr.setRoll(Yn.number(e.startEulerAngles.roll,e.endEulerAngles.roll,e.k)),e.tr.setPitch(Yn.number(e.startEulerAngles.pitch,e.endEulerAngles.pitch,e.k)),e.tr.setBearing(Yn.number(e.startEulerAngles.bearing,e.endEulerAngles.bearing,e.k))}function ks(e,t,n,r,i){let a=i.padding,o=wa(i.worldSize,n.getNorthWest()),s=wa(i.worldSize,n.getNorthEast()),c=wa(i.worldSize,n.getSouthEast()),l=wa(i.worldSize,n.getSouthWest()),u=k(-r),d=o.rotate(u),f=s.rotate(u),p=c.rotate(u),m=l.rotate(u),h=new z(Math.max(d.x,f.x,m.x,p.x),Math.max(d.y,f.y,m.y,p.y)),g=new z(Math.min(d.x,f.x,m.x,p.x),Math.min(d.y,f.y,m.y,p.y)),_=h.sub(g),v=i.width-(a.left+a.right+t.left+t.right),y=i.height-(a.top+a.bottom+t.top+t.bottom),b=v/_.x,x=y/_.y;if(x<0||b<0){Ds();return}let S=Math.min(ar(i.scale*Math.min(b,x)),e.maxZoom),C=z.convert(e.offset),w=new z((t.left-t.right)/2,(t.top-t.bottom)/2).rotate(k(r)),T=C.add(w).mult(i.scale/Se(S));return{center:Ta(i.worldSize,o.add(c).div(2).sub(T)),zoom:S,bearing:r}}var As=class{get useGlobeControls(){return!1}handlePanInertia(e,t){let n=e.mag(),r=Math.abs(Ea(t));return{easingOffset:e.mult(Math.min(r*.75/n,1)),easingCenter:t.center}}handleMapControlsRollPitchBearingZoom(e,t){e.bearingDelta&&t.setBearing(t.bearing+e.bearingDelta),e.pitchDelta&&t.setPitch(t.pitch+e.pitchDelta),e.rollDelta&&t.setRoll(t.roll+e.rollDelta),e.zoomDelta&&t.setZoom(t.zoom+e.zoomDelta)}handleMapControlsPan(e,t,n){e.around.distSqr(t.centerPoint)<.01||t.setLocationAtPoint(n,e.around)}cameraForBoxAndBearing(e,t,n,r,i){return ks(e,t,n,r,i)}handleJumpToCenterZoom(e,t){let n=t.zoom===void 0?e.zoom:+t.zoom;e.zoom!==n&&e.setZoom(+t.zoom),t.center!==void 0&&e.setCenter(B.convert(t.center))}handleEaseTo(e,t){let n=e.zoom,r=e.padding,i={roll:e.roll,pitch:e.pitch,bearing:e.bearing},a={roll:t.roll===void 0?e.roll:t.roll,pitch:t.pitch===void 0?e.pitch:t.pitch,bearing:t.bearing===void 0?e.bearing:t.bearing},o=t.zoom!==void 0,s=!e.isPaddingEqual(t.padding),c=!1,l=o?+t.zoom:e.zoom,u=e.centerPoint.add(t.offsetAsPoint),d=e.screenPointToLocation(u),{center:f,zoom:p}=e.applyConstrain(B.convert(t.center||d),l??n);gs(e,f);let m=wa(e.worldSize,d),h=wa(e.worldSize,f).sub(m),g=Se(p-n);return c=p!==n,{easeFunc:o=>{if(c&&e.setZoom(Yn.number(n,p,o)),Ge(i,a)||Os({startEulerAngles:i,endEulerAngles:a,tr:e,k:o,useSlerp:i.roll!=a.roll}),s&&(e.interpolatePadding(r,t.padding,o),u=e.centerPoint.add(t.offsetAsPoint)),t.around)e.setLocationAtPoint(t.around,t.aroundPoint);else{let t=Se(e.zoom-n),r=(p>n?Math.min(2,g):Math.max(.5,g))**(1-o),i=Ta(e.worldSize,m.add(h.mult(o*r)).mult(t));e.setLocationAtPoint(e.renderWorldCopies?i.wrap():i,u)}},isZooming:c,elevationCenter:f}}handleFlyTo(e,t){let n=t.zoom!==void 0,r=e.zoom,i=e.applyConstrain(B.convert(t.center||t.locationAtOffset),n?+t.zoom:r),a=i.center,o=i.zoom;gs(e,a);let s=e.worldSize,c=wa(s,t.locationAtOffset),l=wa(s,a).sub(c),u=l.mag(),d=Se(o-r),f=t.minZoom===void 0?e.minZoom:+t.minZoom,p=Math.max(f,e.minZoom),m=Math.min(p,r,o),h=e.applyConstrain(a,m).zoom;return{easeFunc:(t,n,i,u)=>{e.setZoom(t===1?o:r+ar(n));let d=t===1?a:Ta(s,c.add(l.mult(i)));e.setLocationAtPoint(e.renderWorldCopies?d.wrap():d,u)},scaleOfZoom:d,targetCenter:a,scaleOfMinZoom:Se(h-r),pixelPathLength:u}}};let js;const Ms=()=>js||=new ae({type:new nt(Ft.projection.type,`type`)});var Y=class{constructor(e,t,n){this.blendFunction=e,this.blendColor=t,this.mask=n}};Y.Replace=[1,0],Y.disabled=new Y(Y.Replace,V.transparent,[!1,!1,!1,!1]),Y.unblended=new Y(Y.Replace,V.transparent,[!0,!0,!0,!0]),Y.alphaBlended=new Y([1,771],V.transparent,[!0,!0,!0,!0]);const Ns=1029,Ps=2305;var X=class{constructor(e,t,n){this.enable=e,this.mode=t,this.frontFace=n}};X.disabled=new X(!1,Ns,Ps),X.backCCW=new X(!0,Ns,Ps),X.frontCCW=new X(!0,1028,Ps);var Z=class{constructor(e,t,n){this.func=e,this.mask=t,this.range=n}};Z.ReadOnly=!1,Z.ReadWrite=!0,Z.disabled=new Z(519,Z.ReadOnly,[0,1]);const Fs=7680;var Q=class{constructor(e,t,n,r,i,a){this.test=e,this.ref=t,this.mask=n,this.fail=r,this.depthFail=i,this.pass=a}};Q.disabled=new Q({func:519,mask:0},0,0,Fs,Fs,Fs);const Is=(e,t)=>({u_input:new H(e,t.u_input),u_output_expected:new H(e,t.u_output_expected)}),Ls=(e,t)=>({u_input:e,u_output_expected:t});var Rs=class e{get awaitingQuery(){return!!this._readbackQueue}constructor(e){this._readbackWaitFrames=4,this._measureWaitFrames=6,this._texWidth=1,this._texHeight=1,this._measuredError=0,this._updateCount=0,this._lastReadbackFrame=-1e3,this._readbackQueue=null,this._cachedRenderContext=e;let t=e.context,n=t.gl;this._texFormat=n.RGBA,this._texType=n.UNSIGNED_BYTE;let r=new O;r.emplaceBack(-1,-1),r.emplaceBack(2,-1),r.emplaceBack(-1,2);let i=new He;i.emplaceBack(0,1,2),this._fullscreenTriangle=new us(t.createVertexBuffer(r,ds.members),t.createIndexBuffer(i),o.simpleSegment(0,0,r.length,i.length)),this._resultBuffer=new Uint8Array(4),t.activeTexture.set(n.TEXTURE1);let a=n.createTexture();n.bindTexture(n.TEXTURE_2D,a),n.texParameteri(n.TEXTURE_2D,n.TEXTURE_WRAP_S,n.CLAMP_TO_EDGE),n.texParameteri(n.TEXTURE_2D,n.TEXTURE_WRAP_T,n.CLAMP_TO_EDGE),n.texParameteri(n.TEXTURE_2D,n.TEXTURE_MIN_FILTER,n.NEAREST),n.texParameteri(n.TEXTURE_2D,n.TEXTURE_MAG_FILTER,n.NEAREST),n.texStorage2D(n.TEXTURE_2D,1,n.RGBA8,this._texWidth,this._texHeight),this._fbo=t.createFramebuffer(this._texWidth,this._texHeight,!1,!1),this._fbo.colorAttachment.set(a),this._pbo=n.createBuffer(),n.bindBuffer(n.PIXEL_PACK_BUFFER,this._pbo),n.bufferData(n.PIXEL_PACK_BUFFER,4,n.STREAM_READ),n.bindBuffer(n.PIXEL_PACK_BUFFER,null)}destroy(){let e=this._cachedRenderContext.context.gl;this._fullscreenTriangle.destroy(),this._fbo.destroy(),e.deleteBuffer(this._pbo),this._fullscreenTriangle=null,this._fbo=null,this._pbo=null,this._resultBuffer=null}updateErrorLoop(e,t){let n=this._updateCount;return this._readbackQueue?n>=this._readbackQueue.frameNumberIssued+this._readbackWaitFrames&&this._tryReadback():n>=this._lastReadbackFrame+this._measureWaitFrames&&this._renderErrorTexture(e,t),this._updateCount++,this._measuredError}_bindFramebuffer(){let e=this._cachedRenderContext.context,t=e.gl;e.activeTexture.set(t.TEXTURE1),t.bindTexture(t.TEXTURE_2D,this._fbo.colorAttachment.get()),e.bindFramebuffer.set(this._fbo.framebuffer)}_renderErrorTexture(e,t){let n=this._cachedRenderContext.context,r=n.gl;this._bindFramebuffer(),n.viewport.set([0,0,this._texWidth,this._texHeight]),n.clear({color:V.transparent}),this._cachedRenderContext.useProgram(`projectionErrorMeasurement`).draw(n,r.TRIANGLES,Z.disabled,Q.disabled,Y.unblended,X.disabled,Ls(e,t),null,null,`$clipping`,this._fullscreenTriangle.vertexBuffer,this._fullscreenTriangle.indexBuffer,this._fullscreenTriangle.segments),r.bindBuffer(r.PIXEL_PACK_BUFFER,this._pbo),r.readBuffer(r.COLOR_ATTACHMENT0),r.readPixels(0,0,this._texWidth,this._texHeight,this._texFormat,this._texType,0),r.bindBuffer(r.PIXEL_PACK_BUFFER,null);let i=r.fenceSync(r.SYNC_GPU_COMMANDS_COMPLETE,0);r.flush(),this._readbackQueue={frameNumberIssued:this._updateCount,sync:i}}_tryReadback(){let t=this._cachedRenderContext.context.gl;if(!this._readbackQueue)return;let n=t.clientWaitSync(this._readbackQueue.sync,0,0);if(n===t.WAIT_FAILED){a(`WebGL2 clientWaitSync failed.`),this._readbackQueue=null,this._lastReadbackFrame=this._updateCount;return}n!==t.TIMEOUT_EXPIRED&&(t.bindBuffer(t.PIXEL_PACK_BUFFER,this._pbo),t.getBufferSubData(t.PIXEL_PACK_BUFFER,0,this._resultBuffer,0,4),t.bindBuffer(t.PIXEL_PACK_BUFFER,null),this._readbackQueue=null,this._measuredError=e._parseRGBA8float(this._resultBuffer),this._lastReadbackFrame=this._updateCount)}static _parseRGBA8float(e){let t=0;return t+=e[0]/256,t+=e[1]/65536,t+=e[2]/16777216,e[3]<127&&(t=-t),t/128}};const zs=M/128;function Bs(e,t){let n=Vs(t,`16bit`),r=O.deserialize({arrayBuffer:n.vertices,length:n.vertices.byteLength/2/2}),i=He.deserialize({arrayBuffer:n.indices,length:n.indices.byteLength/2/3});return new us(e.createVertexBuffer(r,ds.members),e.createIndexBuffer(i),o.simpleSegment(0,0,r.length,i.length))}function Vs(e,t){let n=e.granularity===void 0?1:Math.max(e.granularity,1),r=n+(e.generateBorders?2:0),i=n+(e.extendToNorthPole||e.generateBorders?1:0)+(e.extendToSouthPole||e.generateBorders?1:0),a=r+1,o=i+1,s=e.generateBorders?-1:0,c=e.generateBorders||e.extendToNorthPole?-1:0,l=n+ +!!e.generateBorders,u=n+(e.generateBorders||e.extendToSouthPole?1:0),d=a*o,f=r*i*6,p=a*o>65536;if(p&&t===`16bit`)throw Error(`Granularity is too large and meshes would not fit inside 16 bit vertex indices.`);let m=p||t===`32bit`,h=new Int16Array(d*2),g=0;for(let t=c;t<=u;t++)for(let r=s;r<=l;r++){let i=r/n*M;r===-1&&(i=-zs),r===n+1&&(i=M+zs);let a=t/n*M;t===-1&&(a=e.extendToNorthPole?wt:-zs),t===n+1&&(a=e.extendToSouthPole?En:M+zs),h[g++]=i,h[g++]=a}let _=m?new Uint32Array(f):new Uint16Array(f),v=0;for(let e=0;e0}get latitudeErrorCorrectionRadians(){return this._verticalPerspectiveProjection.latitudeErrorCorrectionRadians}get currentProjection(){return this.useGlobeRendering?this._verticalPerspectiveProjection:this._mercatorProjection}get name(){return`globe`}get useSubdivision(){return this.currentProjection.useSubdivision}get shaderVariantName(){return this.currentProjection.shaderVariantName}get shaderDefine(){return this.currentProjection.shaderDefine}get shaderPreludeCode(){return this.currentProjection.shaderPreludeCode}get vertexShaderPreludeCode(){return this.currentProjection.vertexShaderPreludeCode}get subdivisionGranularity(){return this.currentProjection.subdivisionGranularity}get useGlobeControls(){return this.transitionState>0}destroy(){this._mercatorProjection.destroy(),this._verticalPerspectiveProjection.destroy()}updateGPUdependent(e){this._mercatorProjection.updateGPUdependent(e),this._verticalPerspectiveProjection.updateGPUdependent(e)}getMeshFromTileID(e,t,n,r,i){return this.currentProjection.getMeshFromTileID(e,t,n,r,i)}setProjection(e){this._transitionable.setValue(`type`,e?.type||`mercator`)}updateTransitions(e){this._transitioning=this._transitionable.transitioned(e,this._transitioning)}hasTransition(){return this._transitioning.hasTransition()||this.currentProjection.hasTransition()}recalculate(e){this.properties=this._transitioning.possiblyEvaluate(e)}setErrorQueryLatitudeDegrees(e){this._verticalPerspectiveProjection.setErrorQueryLatitudeDegrees(e),this._mercatorProjection.setErrorQueryLatitudeDegrees(e)}};function Ks(e){let t=Qs(e.worldSize,e.center.lat);return 2*Math.PI*t}function qs(e,t,n){let r=dt(Zs(t),Zs(n)),i=Math.acos(r),a=Ks(e);return i/(2*Math.PI)*a}function Js(e,t){return[kn(e*Math.PI*2+Math.PI,Math.PI*2),2*Math.atan(Math.exp(Math.PI-t*Math.PI*2))-Math.PI*.5]}function Ys(e,t){let n=Math.cos(t),r=new Float64Array(3);return r[0]=Math.sin(e)*n,r[1]=Math.sin(t),r[2]=Math.cos(e)*n,r}function Xs(e,t,n,r,i){let a=1/(1<1e-6){let r=e[0]/n,i=e[2]/n,a=Math.acos(i);return new B(sn((r>0?a:-a)/Math.PI*180,-180,180),t)}else return new B(0,t)}function ec(e){let t=I();return t[0]=e[0]*-e[3],t[1]=e[1]*-e[3],t[2]=e[2]*-e[3],{center:t,radius:Math.sqrt(1-e[3]*e[3])}}function tc(e,t,n){let r=I();xt(r,n,e);let i=I();return nn(i,e,r,t/ct(r)),i}function nc(e){return Math.cos(e*Math.PI/180)}function rc(e,t){let n=nc(e);return ar(nc(t)/n)}function ic(e,t){return 360/Ks({worldSize:e,center:{lat:t}})}function ac(e,t){let n=e.rotate(t.bearingInRadians),r=t.zoom+rc(t.center.lat,0),i=It(1/nc(t.center.lat),1/nc(Math.min(Math.abs(t.center.lat),60)),ur(r,7,3,0,1)),a=ic(t.worldSize,t.center.lat);return new B(t.center.lng-n.x*a*i,j(t.center.lat+n.y*a,-on,on))}function oc(e){let t=.5*e,n=Math.sin(t),r=Math.cos(t);return Math.log(n+r)-Math.log(r-n)}function sc(e,t,n,r){let i=e.lat+n*r;if(Math.abs(n)>1){let a=e.lat+n,o=(Math.sign(a)===Math.sign(e.lat)?Math.abs(e.lat):-Math.abs(e.lat))*Math.PI/180,s=Math.abs(e.lat+n)*Math.PI/180,c=oc(o+r*(s-o)),l=oc(o),u=oc(s),d=(c-l)/(u-l);return new B(e.lng+t*d,i)}else return new B(e.lng+t*r,i)}var cc=class{constructor(e){this._cachePrevious=new Map,this._cache=new Map,this._hadAnyChanges=!1,this._boundingVolumeFactory=e}swapBuffers(){if(!this._hadAnyChanges)return;let e=this._cachePrevious;this._cachePrevious=this._cache,this._cache=e,this._cache.clear(),this._hadAnyChanges=!1}getTileBoundingVolume(e,t,n,r){let i=`${e.z}_${e.x}_${e.y}_${r?.terrain?`t`:``}`,a=this._cache.get(i);if(a)return a;let o=this._cachePrevious.get(i);if(o)return this._cache.set(i,o),o;let s=this._boundingVolumeFactory(e,t,n,r);return this._cache.set(i,s),this._hadAnyChanges=!0,s}},lc=class e{constructor(e,t,n,r){this.min=n,this.max=r,this.points=e,this.planes=t}static fromAabb(t,n){let r=[];for(let e=0;e<8;e++)r.push([(e>>0&1)==1?n[0]:t[0],(e>>1&1)==1?n[1]:t[1],(e>>2&1)==1?n[2]:t[2]]);return new e(r,[[-1,0,0,n[0]],[1,0,0,-t[0]],[0,-1,0,n[1]],[0,1,0,-t[1]],[0,0,-1,n[2]],[0,0,1,-t[2]]],t,n)}static fromCenterSizeAngles(t,n,r){let i=tt([],r[0],r[1],r[2]),a=At([],[n[0],0,0],i),o=At([],[0,n[1],0],i),s=At([],[0,0,n[2]],i),c=[...t],l=[...t];for(let e=0;e<8;e++)for(let n=0;n<3;n++){let r=t[n]+a[n]*((e>>0&1)==1?1:-1)+o[n]*((e>>1&1)==1?1:-1)+s[n]*((e>>2&1)==1?1:-1);c[n]=Math.min(c[n],r),l[n]=Math.max(l[n],r)}let u=[];for(let e=0;e<8;e++){let n=[...t];he(n,n,In([],a,(e>>0&1)==1?1:-1)),he(n,n,In([],o,(e>>1&1)==1?1:-1)),he(n,n,In([],s,(e>>2&1)==1?1:-1)),u.push(n)}return new e(u,[[...a,-dt(a,u[0])],[...o,-dt(o,u[0])],[...s,-dt(s,u[0])],[-a[0],-a[1],-a[2],-dt(a,u[7])],[-o[0],-o[1],-o[2],-dt(o,u[7])],[-s[0],-s[1],-s[2],-dt(s,u[7])]],c,l)}intersectsFrustum(e){let t=!0,n=this.points.length,r=this.planes.length,i=e.planes.length,a=e.points.length;for(let r=0;r=0&&a++}if(a===0)return 0;a=0&&r++}if(r===0)return 0}return 1}intersectsPlane(e){let t=this.points.length,n=0;for(let r=0;r=0&&n++}return n===t?2:n===0?0:1}};function uc(e,t,n){let r=e-t;return r<0?-r:Math.max(0,r-n)}function dc(e,t,n,r,i){let a=e-n,o;return o=a<0?Math.min(-a,1+a-i):a>i?Math.min(Math.max(a-i,0),1-a):0,Math.max(o,uc(t,r,i))}var fc=class{constructor(){this._boundingVolumeCache=new cc(this._computeTileBoundingVolume)}prepareNextFrame(){this._boundingVolumeCache.swapBuffers()}distanceToTile2d(e,t,n,r){let i=1<4}allowWorldCopies(){return!1}getTileBoundingVolume(e,t,n,r){return this._boundingVolumeCache.getTileBoundingVolume(e,t,n,r)}_computeTileBoundingVolume(e,n,i,a){let o=0,s=0;if(a?.terrain){let t=new ht(e.z,n,e.z,e.x,e.y),r=a.terrain.getMinMaxElevation(t);o=r.minElevation??Math.min(0,i),s=r.maxElevation??Math.max(0,i)}if(o/=r,s/=r,o+=1,s+=1,e.z<=0)return lc.fromAabb([-s,-s,-s],[s,s,s]);if(e.z===1)return lc.fromAabb([e.x===0?-s:0,e.y===0?0:-s,-s],[e.x===0?0:s,e.y===0?s:0,s]);{let n=[Xs(0,0,e.x,e.y,e.z),Xs(M,0,e.x,e.y,e.z),Xs(M,M,e.x,e.y,e.z),Xs(0,M,e.x,e.y,e.z)],r=[];for(let e of n)r.push(In([],e,s));if(s!==o)for(let e of n)r.push(In([],e,o));e.y===0&&r.push([0,1,0]),e.y===(1<=(1<{let n=j(e.lat,-on,on),r=j(+t,this.minZoom+rc(0,n),this.maxZoom);return{center:new B(e.lng,n),zoom:r}},this.applyConstrain=(e,t)=>this._helper.applyConstrain(e,t),this._helper=new vs({calcMatrices:()=>this._calcMatrices(),defaultConstrain:(e,t)=>this.defaultConstrain(e,t)},e),this._coveringTilesDetailsProvider=new fc}clone(){let t=new e;return t.apply(this,!1),t}apply(e,t,n){this._globeLatitudeErrorCorrectionRadians=n||0,this._helper.apply(e,t)}get projectionMatrix(){return this._projectionMatrix}get modelViewProjectionMatrix(){return this._globeViewProjMatrixNoCorrection}get inverseProjectionMatrix(){return this._globeProjMatrixInverted}get cameraPosition(){let e=I();return e[0]=this._cameraPosition[0],e[1]=this._cameraPosition[1],e[2]=this._cameraPosition[2],e}get cameraToCenterDistance(){return this._helper.cameraToCenterDistance}getProjectionData(e){let{overscaledTileID:t,applyGlobeMatrix:n}=e,r=this._helper.getMercatorTileCoordinates(t);return{mainMatrix:this._globeViewProjMatrix32f,tileMercatorCoords:r,clippingPlane:this._cachedClippingPlane,projectionTransition:+!!n,fallbackMatrix:this._globeViewProjMatrix32f}}_computeClippingPlane(e){let t=this.pitchInRadians,n=this.cameraToCenterDistance/e,r=Math.sin(t)*n,i=Math.cos(t)*n+1,a=1/Math.sqrt(r*r+i*i)*1,o=-r,s=i,c=Math.sqrt(o*o+s*s);o/=c,s/=c;let l=[0,o,s];bn(l,l,[0,0,0],-this.bearingInRadians),Rt(l,l,[0,0,0],-1*this.center.lat*Math.PI/180),Gn(l,l,[0,0,0],this.center.lng*Math.PI/180);let u=1/pt(l);return In(l,l,u),[...l,-a*u]}isLocationOccluded(e){return!this.isSurfacePointVisible(Zs(e))}transformLightDirection(e){let n=this._helper._center.lng*Math.PI/180,r=this._helper._center.lat*Math.PI/180,i=Math.cos(r),a=[Math.sin(n)*i,Math.sin(r),Math.cos(n)*i],o=[a[2],0,-a[0]],s=[0,0,0];de(s,o,a),t(o,o),t(s,s);let c=[o[0]*e[0]+s[0]*e[1]+a[0]*e[2],o[1]*e[0]+s[1]*e[1]+a[1]*e[2],o[2]*e[0]+s[2]*e[1]+a[2]*e[2]],l=[0,0,0];return t(l,c),l}getPixelScale(){return 1/Math.cos(this._helper._center.lat*Math.PI/180)}getCircleRadiusCorrection(){return Math.cos(this._helper._center.lat*Math.PI/180)}getPitchedTextCorrection(e,t,n){let r=Ca(e,t,n.canonical),i=Js(r.x,r.y);return this.getCircleRadiusCorrection()/Math.cos(i[1])}projectTileCoordinates(e,t,n,i){let a=n.canonical,o=Xs(e,t,a.x,a.y,a.z),s=1+(i?i(e,t):0)/r,c=[o[0]*s,o[1]*s,o[2]*s,1];A(c,c,this._globeViewProjMatrixNoCorrection);let l=this._cachedClippingPlane,u=l[0]*o[0]+l[1]*o[1]+l[2]*o[2]+l[3]<0;return{point:new z(c[0]/c[3],c[1]/c[3]),signedDistanceFromCamera:c[3],isOccluded:u}}_calcMatrices(){if(!this._helper._width||!this._helper._height)return;let e=Qs(this.worldSize,this.center.lat),t=c(),n=c();this._helper.autoCalculateNearFarZ&&(this._helper._nearZ=.5,this._helper._farZ=this.cameraToCenterDistance+e*2),Et(t,this.fovInRadians,this.width/this.height,this._helper._nearZ,this._helper._farZ);let r=this.centerOffset;t[8]=-r.x*2/this._helper._width,t[9]=r.y*2/this._helper._height,this._projectionMatrix=Jn(t),this._globeProjMatrixInverted=c(),gt(this._globeProjMatrixInverted,t),F(t,t,[0,0,-this.cameraToCenterDistance]),f(t,t,this.rollInRadians),cr(t,t,-this.pitchInRadians),f(t,t,this.bearingInRadians),F(t,t,[0,0,-e]);let i=I();i[0]=e,i[1]=e,i[2]=e,cr(n,t,this.center.lat*Math.PI/180),Ue(n,n,-this.center.lng*Math.PI/180),rr(n,n,i),this._globeViewProjMatrixNoCorrection=n,cr(t,t,this.center.lat*Math.PI/180-this._globeLatitudeErrorCorrectionRadians),Ue(t,t,-this.center.lng*Math.PI/180),rr(t,t,i),this._globeViewProjMatrix32f=new Float32Array(t),this._globeViewProjMatrixNoCorrectionInverted=c(),gt(this._globeViewProjMatrixNoCorrectionInverted,n);let a=I();this._cameraPosition=I(),this._cameraPosition[2]=this.cameraToCenterDistance/e,bn(this._cameraPosition,this._cameraPosition,a,-this.rollInRadians),Rt(this._cameraPosition,this._cameraPosition,a,this.pitchInRadians),bn(this._cameraPosition,this._cameraPosition,a,-this.bearingInRadians),he(this._cameraPosition,this._cameraPosition,[0,0,1]),Rt(this._cameraPosition,this._cameraPosition,a,-this.center.lat*Math.PI/180),Gn(this._cameraPosition,this._cameraPosition,a,this.center.lng*Math.PI/180),this._cachedClippingPlane=this._computeClippingPlane(e);let o=Jn(this._globeViewProjMatrixNoCorrectionInverted);rr(o,o,[1,1,-1]),this._cachedFrustum=xs.fromInvProjectionMatrix(o,1,0,this._cachedClippingPlane,!0)}calculateFogMatrix(e){a(`calculateFogMatrix is not supported on globe projection.`);let t=c();return qt(t),t}getVisibleUnwrappedCoordinates(e){return[new Zn(0,e)]}getCameraFrustum(){return this._cachedFrustum}getClippingPlane(){return this._cachedClippingPlane}getCoveringTilesDetailsProvider(){return this._coveringTilesDetailsProvider}recalculateZoomAndCenter(e){if(e){a(`terrain is not fully supported on vertical perspective projection.`);return}this._helper.recalculateZoomAndCenter(0)}maxPitchScaleFactor(){return 1}getCameraPoint(){return this._helper.getCameraPoint()}getCameraAltitude(){return this._helper.getCameraAltitude()}getCameraLngLat(){return this._helper.getCameraLngLat()}lngLatToCameraDepth(e,t){if(!this._globeViewProjMatrixNoCorrection)return 1;let n=Zs(e);In(n,n,1+t/r);let i=re();return A(i,[n[0],n[1],n[2],1],this._globeViewProjMatrixNoCorrection),i[2]/i[3]}populateCache(e){}getBounds(){let e=this.width*.5,t=this.height*.5,n=[new z(0,0),new z(e,0),new z(this.width,0),new z(this.width,t),new z(this.width,this.height),new z(e,this.height),new z(0,this.height),new z(0,t)],r=[];for(let e of n)r.push(this.unprojectScreenPoint(e));let i=0,a=0,o=0,s=0,c=this.center;for(let e of r){let t=me(c.lng,e.lng),n=me(c.lat,e.lat);ti&&(i=t),no&&(o=n)}let l=[c.lng+a,c.lat+s,c.lng+i,c.lat+o];return this.isSurfacePointOnScreen([0,1,0])&&(l[3]=90,l[0]=-180,l[2]=180),this.isSurfacePointOnScreen([0,-1,0])&&(l[1]=-90,l[0]=-180,l[2]=180),new Ri(l)}calculateCenterFromCameraLngLatAlt(e,t,n,r){return this._helper.calculateCenterFromCameraLngLatAlt(e,t,n,r)}setLocationAtPoint(e,t){let n=Zs(this.unprojectScreenPoint(t)),r=Zs(e),i=I();pn(i);let a=I();Gn(a,n,i,-this.center.lng*Math.PI/180),Rt(a,a,i,this.center.lat*Math.PI/180);let o=r[0]*r[0]+r[2]*r[2],s=a[0]*a[0];if(o=-g&&p<=g,v=h>=-g&&h<=g,y,b;if(_&&v){let e=this.center.lng*Math.PI/180,t=this.center.lat*Math.PI/180,n=Mn(u,e),r=Mn(p,t),i=Mn(d,e),a=Mn(h,t);n+r=0}isSurfacePointOnScreen(e){if(!this.isSurfacePointVisible(e))return!1;let t=re();return A(t,[...e,1],this._globeViewProjMatrixNoCorrection),t[0]/=t[3],t[1]/=t[3],t[2]/=t[3],t[0]>-1&&t[0]<1&&t[1]>-1&&t[1]<1&&t[2]>-1&&t[2]<1}rayPlanetIntersection(e,t){let n=dt(e,t),r=I(),i=I();In(i,t,n),xt(r,e,i);let a=1-dt(r,r);if(a<0)return null;let o=dt(e,e)-1,s=-n+(n<0?1:-1)*Math.sqrt(a),c=o/s,l=s;return{tMin:Math.min(c,l),tMax:Math.max(c,l)}}unprojectScreenPoint(e){let n=this._cameraPosition,r=this.getRayDirectionFromPixel(e),i=this.rayPlanetIntersection(n,r);if(i){let e=I();he(e,n,[r[0]*i.tMin,r[1]*i.tMin,r[2]*i.tMin]);let a=I();return t(a,e),$s(a)}let a=this._cachedClippingPlane,o=a[0]*r[0]+a[1]*r[1]+a[2]*r[2],s=-vt(a,n)/o,c=I();if(s>0)he(c,n,[r[0]*s,r[1]*s,r[2]*s]);else{let e=I();he(e,n,[r[0]*2,r[1]*2,r[2]*2]);let t=vt(this._cachedClippingPlane,e);xt(c,e,[this._cachedClippingPlane[0]*t,this._cachedClippingPlane[1]*t,this._cachedClippingPlane[2]*t])}let l=ec(a);return $s(tc(l.center,l.radius,c))}getProjectionDataForCustomLayer(e=!0){let t=this.getProjectionData({overscaledTileID:new ht(0,0,0,0,0),applyGlobeMatrix:e});return t.tileMercatorCoords=[0,0,1,1],t}getFastPathSimpleProjectionMatrix(e){}},hc=class e{get pixelsToClipSpaceMatrix(){return this._helper.pixelsToClipSpaceMatrix}get clipSpaceToPixelsMatrix(){return this._helper.clipSpaceToPixelsMatrix}get pixelsToGLUnits(){return this._helper.pixelsToGLUnits}get centerOffset(){return this._helper.centerOffset}get size(){return this._helper.size}get rotationMatrix(){return this._helper.rotationMatrix}get centerPoint(){return this._helper.centerPoint}get pixelsPerMeter(){return this._helper.pixelsPerMeter}setMinZoom(e){this._helper.setMinZoom(e)}setMaxZoom(e){this._helper.setMaxZoom(e)}setMinPitch(e){this._helper.setMinPitch(e)}setMaxPitch(e){this._helper.setMaxPitch(e)}setRenderWorldCopies(e){this._helper.setRenderWorldCopies(e)}setBearing(e){this._helper.setBearing(e)}setPitch(e){this._helper.setPitch(e)}setRoll(e){this._helper.setRoll(e)}setFov(e){this._helper.setFov(e)}setZoom(e){this._helper.setZoom(e)}setCenter(e){this._helper.setCenter(e)}setElevation(e){this._helper.setElevation(e)}setMinElevationForCurrentTile(e){this._helper.setMinElevationForCurrentTile(e)}setPadding(e){this._helper.setPadding(e)}interpolatePadding(e,t,n){this._helper.interpolatePadding(e,t,n)}isPaddingEqual(e){return this._helper.isPaddingEqual(e)}resize(e,t,n=!0){this._helper.resize(e,t,n)}getMaxBounds(){return this._helper.getMaxBounds()}setMaxBounds(e){this._helper.setMaxBounds(e)}setConstrainOverride(e){this._helper.setConstrainOverride(e)}overrideNearFarZ(e,t){this._helper.overrideNearFarZ(e,t)}clearNearFarZOverride(){this._helper.clearNearFarZOverride()}getCameraQueryGeometry(e){return this._helper.getCameraQueryGeometry(this.getCameraPoint(),e)}get tileSize(){return this._helper.tileSize}get tileZoom(){return this._helper.tileZoom}get scale(){return this._helper.scale}get worldSize(){return this._helper.worldSize}get width(){return this._helper.width}get height(){return this._helper.height}get lngRange(){return this._helper.lngRange}get latRange(){return this._helper.latRange}get minZoom(){return this._helper.minZoom}get maxZoom(){return this._helper.maxZoom}get zoom(){return this._helper.zoom}get center(){return this._helper.center}get minPitch(){return this._helper.minPitch}get maxPitch(){return this._helper.maxPitch}get pitch(){return this._helper.pitch}get pitchInRadians(){return this._helper.pitchInRadians}get roll(){return this._helper.roll}get rollInRadians(){return this._helper.rollInRadians}get bearing(){return this._helper.bearing}get bearingInRadians(){return this._helper.bearingInRadians}get fov(){return this._helper.fov}get fovInRadians(){return this._helper.fovInRadians}get elevation(){return this._helper.elevation}get minElevationForCurrentTile(){return this._helper.minElevationForCurrentTile}get padding(){return this._helper.padding}get unmodified(){return this._helper.unmodified}get renderWorldCopies(){return this._helper.renderWorldCopies}get cameraToCenterDistance(){return this._helper.cameraToCenterDistance}get constrainOverride(){return this._helper.constrainOverride}get nearZ(){return this._helper.nearZ}get farZ(){return this._helper.farZ}get autoCalculateNearFarZ(){return this._helper.autoCalculateNearFarZ}get isGlobeRendering(){return this._globeness>0}setTransitionState(e,t){this._globeness=e,this._globeLatitudeErrorCorrectionRadians=t,this._calcMatrices(),this._verticalPerspectiveTransform.getCoveringTilesDetailsProvider().prepareNextFrame(),this._mercatorTransform.getCoveringTilesDetailsProvider().prepareNextFrame()}get currentTransform(){return this.isGlobeRendering?this._verticalPerspectiveTransform:this._mercatorTransform}constructor(e){this._globeLatitudeErrorCorrectionRadians=0,this._globeness=1,this.defaultConstrain=(e,t)=>this.currentTransform.defaultConstrain(e,t),this.applyConstrain=(e,t)=>this._helper.applyConstrain(e,t),this._helper=new vs({calcMatrices:()=>this._calcMatrices(),defaultConstrain:(e,t)=>this.defaultConstrain(e,t)},e),this._globeness=1,this._mercatorTransform=new Es,this._verticalPerspectiveTransform=new mc}clone(){let t=new e;return t._globeness=this._globeness,t._globeLatitudeErrorCorrectionRadians=this._globeLatitudeErrorCorrectionRadians,t.apply(this,!1),t}apply(e,t){this._helper.apply(e,t),this._mercatorTransform.apply(this,!1),this._verticalPerspectiveTransform.apply(this,!1,this._globeLatitudeErrorCorrectionRadians)}get projectionMatrix(){return this.currentTransform.projectionMatrix}get modelViewProjectionMatrix(){return this.currentTransform.modelViewProjectionMatrix}get inverseProjectionMatrix(){return this.currentTransform.inverseProjectionMatrix}get cameraPosition(){return this.currentTransform.cameraPosition}getProjectionData(e){let t=this._mercatorTransform.getProjectionData(e),n=this._verticalPerspectiveTransform.getProjectionData(e);return{mainMatrix:this.isGlobeRendering?n.mainMatrix:t.mainMatrix,clippingPlane:n.clippingPlane,tileMercatorCoords:n.tileMercatorCoords,projectionTransition:e.applyGlobeMatrix?this._globeness:0,fallbackMatrix:t.fallbackMatrix}}isLocationOccluded(e){return this.currentTransform.isLocationOccluded(e)}transformLightDirection(e){return this.currentTransform.transformLightDirection(e)}getPixelScale(){return It(this._mercatorTransform.getPixelScale(),this._verticalPerspectiveTransform.getPixelScale(),this._globeness)}getCircleRadiusCorrection(){return It(this._mercatorTransform.getCircleRadiusCorrection(),this._verticalPerspectiveTransform.getCircleRadiusCorrection(),this._globeness)}getPitchedTextCorrection(e,t,n){return It(this._mercatorTransform.getPitchedTextCorrection(e,t,n),this._verticalPerspectiveTransform.getPitchedTextCorrection(e,t,n),this._globeness)}projectTileCoordinates(e,t,n,r){return this.currentTransform.projectTileCoordinates(e,t,n,r)}_calcMatrices(){!this._helper._width||!this._helper._height||(this._verticalPerspectiveTransform.apply(this,!1,this._globeLatitudeErrorCorrectionRadians),this._helper._nearZ=this._verticalPerspectiveTransform.nearZ,this._helper._farZ=this._verticalPerspectiveTransform.farZ,this._mercatorTransform.apply(this,!0,this.isGlobeRendering),this._helper._nearZ=this._mercatorTransform.nearZ,this._helper._farZ=this._mercatorTransform.farZ)}calculateFogMatrix(e){return this.currentTransform.calculateFogMatrix(e)}getVisibleUnwrappedCoordinates(e){return this.currentTransform.getVisibleUnwrappedCoordinates(e)}getCameraFrustum(){return this.currentTransform.getCameraFrustum()}getClippingPlane(){return this.currentTransform.getClippingPlane()}getCoveringTilesDetailsProvider(){return this.currentTransform.getCoveringTilesDetailsProvider()}recalculateZoomAndCenter(e){this.currentTransform.recalculateZoomAndCenter(e)}maxPitchScaleFactor(){return this._mercatorTransform.maxPitchScaleFactor()}getCameraPoint(){return this._helper.getCameraPoint()}getCameraAltitude(){return this._helper.getCameraAltitude()}getCameraLngLat(){return this._helper.getCameraLngLat()}lngLatToCameraDepth(e,t){return this.currentTransform.lngLatToCameraDepth(e,t)}populateCache(e){this._mercatorTransform.populateCache(e),this._verticalPerspectiveTransform.populateCache(e)}getBounds(){return this.currentTransform.getBounds()}calculateCenterFromCameraLngLatAlt(e,t,n,r){return this._helper.calculateCenterFromCameraLngLatAlt(e,t,n,r)}setLocationAtPoint(e,t){if(!this.isGlobeRendering){this._mercatorTransform.setLocationAtPoint(e,t),this.apply(this._mercatorTransform,!1);return}this._verticalPerspectiveTransform.setLocationAtPoint(e,t),this.apply(this._verticalPerspectiveTransform,!1)}locationToScreenPoint(e,t){return this.currentTransform.locationToScreenPoint(e,t)}screenPointToMercatorCoordinate(e,t){return this.currentTransform.screenPointToMercatorCoordinate(e,t)}screenPointToLocation(e,t){return this.currentTransform.screenPointToLocation(e,t)}isPointOnMapSurface(e,t){return this.currentTransform.isPointOnMapSurface(e,t)}getRayDirectionFromPixel(e){return this._verticalPerspectiveTransform.getRayDirectionFromPixel(e)}getProjectionDataForCustomLayer(e=!0){let t=this._mercatorTransform.getProjectionDataForCustomLayer(e);if(!this.isGlobeRendering)return t;let n=this._verticalPerspectiveTransform.getProjectionDataForCustomLayer(e);return n.fallbackMatrix=t.mainMatrix,n}getFastPathSimpleProjectionMatrix(e){return this.currentTransform.getFastPathSimpleProjectionMatrix(e)}},gc=class e{get useGlobeControls(){return!0}handlePanInertia(e,t){let n=ac(e,t);return Math.abs(n.lng-t.center.lng)>180&&(n.lng=t.center.lng+179.5*Math.sign(n.lng-t.center.lng)),{easingCenter:n,easingOffset:new z(0,0)}}handleMapControlsRollPitchBearingZoom(e,t){let n=e.around,r=t.screenPointToLocation(n);e.bearingDelta&&t.setBearing(t.bearing+e.bearingDelta),e.pitchDelta&&t.setPitch(t.pitch+e.pitchDelta),e.rollDelta&&t.setRoll(t.roll+e.rollDelta);let i=t.zoom;e.zoomDelta&&t.setZoom(t.zoom+e.zoomDelta);let a=t.zoom-i;if(a===0)return;let o=me(t.center.lng,r.lng),s=o/(Math.abs(o/180)+1),c=me(t.center.lat,r.lat),l=t.getRayDirectionFromPixel(n),u=t.cameraPosition,d=dt(u,l)*-1,f=I();he(f,u,[l[0]*d,l[1]*d,l[2]*d]);let p=pt(f)-1,m=Math.exp(-Math.max(p-.3,0)*.5),h=Qs(t.worldSize,t.center.lat)/Math.min(t.width,t.height),g=ur(h,.9,.5,1,.25),_=(1-Se(-a))*Math.min(m,g),v=t.center.lat,y=t.zoom,b=new B(t.center.lng+s*_,j(t.center.lat+c*_,-on,on));t.setLocationAtPoint(r,n);let x=t.center,S=ur(Math.abs(o),45,85,0,1),C=ur(h,.75,.35,0,1),w=Math.max(S,C)**.25,T=me(x.lng,b.lng),ee=me(x.lat,b.lat);t.setCenter(new B(x.lng+T*w,x.lat+ee*w).wrap()),t.setZoom(y+rc(v,t.center.lat))}handleMapControlsPan(e,t,n){if(!e.panDelta)return;let r=t.center.lat,i=t.zoom;t.setCenter(ac(e.panDelta,t).wrap()),t.setZoom(i+rc(r,t.center.lat))}cameraForBoxAndBearing(t,n,r,i,a){let o=ks(t,n,r,i,a),s=n.left/a.width*2-1,c=(a.width-n.right)/a.width*2-1,l=n.top/a.height*-2+1,u=(a.height-n.bottom)/a.height*-2+1,d=me(r.getWest(),r.getEast())<0,f=d?r.getEast():r.getWest(),p=d?r.getWest():r.getEast(),m=Math.max(r.getNorth(),r.getSouth()),h=Math.min(r.getNorth(),r.getSouth()),g=f+me(f,p)*.5,_=m+me(m,h)*.5,v=a.clone();v.setCenter(o.center),v.setBearing(o.bearing),v.setPitch(0),v.setRoll(0),v.setZoom(o.zoom);let y=v.modelViewProjectionMatrix,b=[Zs(r.getNorthWest()),Zs(r.getNorthEast()),Zs(r.getSouthWest()),Zs(r.getSouthEast()),Zs(new B(p,_)),Zs(new B(f,_)),Zs(new B(g,m)),Zs(new B(g,h))],x=Zs(o.center),S=1/0;for(let t of b)s<0&&(S=e.getLesserNonNegativeNonNull(S,e.solveVectorScale(t,x,y,`x`,s))),c>0&&(S=e.getLesserNonNegativeNonNull(S,e.solveVectorScale(t,x,y,`x`,c))),l>0&&(S=e.getLesserNonNegativeNonNull(S,e.solveVectorScale(t,x,y,`y`,l))),u<0&&(S=e.getLesserNonNegativeNonNull(S,e.solveVectorScale(t,x,y,`y`,u)));if(!Number.isFinite(S)||S===0){Ds();return}return o.zoom=Math.min(v.zoom+ar(S),t.maxZoom),o}handleJumpToCenterZoom(e,t){let n=e.center.lat,r=e.applyConstrain(t.center?B.convert(t.center):e.center,e.zoom).center;e.setCenter(r.wrap());let i=t.zoom===void 0?e.zoom+rc(n,r.lat):+t.zoom;e.zoom!==i&&e.setZoom(i)}handleEaseTo(e,t){let n=e.zoom,r=e.center,i=e.padding,o={roll:e.roll,pitch:e.pitch,bearing:e.bearing},s={roll:t.roll===void 0?e.roll:t.roll,pitch:t.pitch===void 0?e.pitch:t.pitch,bearing:t.bearing===void 0?e.bearing:t.bearing},c=t.zoom!==void 0,l=!e.isPaddingEqual(t.padding),u=!1,d=t.center?B.convert(t.center):r,f=e.applyConstrain(d,n).center;gs(e,f);let p=e.clone();p.setCenter(f),p.setZoom(c?+t.zoom:n+rc(r.lat,d.lat)),p.setBearing(t.bearing);let m=new z(j(e.centerPoint.x+t.offsetAsPoint.x,0,e.width),j(e.centerPoint.y+t.offsetAsPoint.y,0,e.height));p.setLocationAtPoint(f,m);let h=(t.offset&&t.offsetAsPoint.mag())>0?p.center:f,g=c?+t.zoom:n+rc(r.lat,h.lat),_=n+rc(r.lat,0),v=g+rc(h.lat,0),y=me(r.lng,h.lng),b=me(r.lat,h.lat),x=Se(v-_);return u=g!==n,{easeFunc:n=>{if(Ge(o,s)||Os({startEulerAngles:o,endEulerAngles:s,tr:e,k:n,useSlerp:o.roll!=s.roll}),l&&e.interpolatePadding(i,t.padding,n),t.around)a(`Easing around a point is not supported under globe projection.`),e.setLocationAtPoint(t.around,t.aroundPoint);else{let t=n*(v>_?Math.min(2,x):Math.max(.5,x))**(1-n),i=sc(r,y,b,t);e.setCenter(i.wrap())}if(u){let t=Yn.number(_,v,n)+rc(0,e.center.lat);e.setZoom(t)}},isZooming:u,elevationCenter:h}}handleFlyTo(e,t){let n=t.zoom!==void 0,r=e.center,i=e.zoom,a=e.padding,o=!e.isPaddingEqual(t.padding),s=e.applyConstrain(B.convert(t.center||t.locationAtOffset),i).center,c=n?+t.zoom:e.zoom+rc(e.center.lat,s.lat),l=e.clone();l.setCenter(s),l.setZoom(c),l.setBearing(t.bearing);let u=new z(j(e.centerPoint.x+t.offsetAsPoint.x,0,e.width),j(e.centerPoint.y+t.offsetAsPoint.y,0,e.height));l.setLocationAtPoint(s,u);let d=l.center;gs(e,d);let f=qs(e,r,d),p=i+rc(r.lat,0),m=c+rc(d.lat,0),h=Se(m-p),g=typeof t.minZoom==`number`?+t.minZoom:e.minZoom,_=Math.max(g,e.minZoom)+rc(d.lat,0),v=Math.min(_,p,m)+rc(0,d.lat),y=Se(e.applyConstrain(d,v).zoom+rc(d.lat,0)-p),b=me(r.lng,d.lng),x=me(r.lat,d.lat);return{easeFunc:(n,i,s,l)=>{let u=sc(r,b,x,s);o&&e.interpolatePadding(a,t.padding,n);let f=n===1?d:u;e.setCenter(f.wrap());let m=p+ar(i);e.setZoom(n===1?c:m+rc(0,f.lat))},scaleOfZoom:h,targetCenter:d,scaleOfMinZoom:y,pixelPathLength:f}}static solveVectorScale(e,t,n,r,i){let a=i,o=r===`x`?[n[0],n[4],n[8],n[12]]:[n[1],n[5],n[9],n[13]],s=[n[3],n[7],n[11],n[15]],c=e[0]*o[0]+e[1]*o[1]+e[2]*o[2],l=e[0]*s[0]+e[1]*s[1]+e[2]*s[2],u=t[0]*o[0]+t[1]*o[1]+t[2]*o[2],d=t[0]*s[0]+t[1]*s[1]+t[2]*s[2],f=(u+o[3]-a*d-a*s[3])/(u-c-a*d+a*l);return u+a*l===c+a*d||s[3]*(c-u)+o[3]*(d-l)+c*d===u*l?null:f}static getLesserNonNegativeNonNull(e,t){return t!==null&&t>=0&&t{for(let e in this.tileManagers){let t=this.tileManagers[e].getSource().type;(t===`vector`||t===`geojson`)&&this.tileManagers[e].reload()}},this.map=e,this.dispatcher=new wi(xi(),e._getMapId()),this.dispatcher.registerMessageHandler(`GG`,(e,t)=>this.getGlyphs(e,t)),this.dispatcher.registerMessageHandler(`GI`,(e,t)=>this.getImages(e,t)),this.dispatcher.registerMessageHandler(`GDA`,(e,t)=>this.getDashes(e,t)),this.imageManager=new Yr,this.imageManager.setEventedParent(this),this.imageManager.setMissingImageResolver(e._missingStyleImageResolver);let n=e._container?.lang||typeof document<`u`&&document.documentElement?.lang||void 0;this.glyphManager=new ri(e._requestManager,t.localIdeographFontFamily,n),this.lineAtlas=new ui(256,512),this.crossTileSymbolIndex=new $o,this._setInitialValues(),this._resetUpdates(),this.dispatcher.broadcast(`SR`,pr()),va().on(ha,this._rtlPluginLoaded),this.on(`data`,e=>{if(e.dataType!==`source`||e.sourceDataType!==`metadata`)return;let t=this.tileManagers[e.sourceId];if(!t)return;let n=t.getSource();if(n?.vectorLayerIds)for(let e in this._layers){let t=this._layers[e];t.source===n.id&&this._validateLayer(t)}})}_setInitialValues(){this._spritesImagesIds={},this._layers={},this._order=[],this.tileManagers={},this.zoomHistory=new fe,this._availableImages=[],this._imagesListDirty=!1,this._globalState={},this._serializedLayers={},this.stylesheet=null,this.light=null,this.sky=null,this.projection&&(this.projection.destroy(),delete this.projection),this._loaded=!1,this._changed=!1,this._updatedLayers={},this._updatedSources={},this._changedImages={},this._glyphsDidChange=!1,this._updatedPaintProps={},this._layerOrderChanged=!1,this.crossTileSymbolIndex=new((this.crossTileSymbolIndex?.constructor)||Object),this.pauseablePlacement=void 0,this.placement=void 0,this.z=0}setGlobalStateProperty(e,t){this._checkLoaded();let n=t===null?this.stylesheet.state?.[e]?.default??null:t;if(Ze(n,this._globalState[e]))return this;this._globalState[e]=n,this._applyGlobalStateChanges([e])}getGlobalState(){return this._globalState}setGlobalState(e){this._checkLoaded();let t=[];for(let n in e)Ze(this._globalState[n],e[n].default)||(t.push(n),this._globalState[n]=e[n].default);this._applyGlobalStateChanges(t)}_applyGlobalStateChanges(e){if(e.length===0)return;let t=new Set,n={};for(let r of e){n[r]=this._globalState[r];for(let e in this._layers){let n=this._layers[e],i=n.getLayoutAffectingGlobalStateRefs(),a=n.getPaintAffectingGlobalStateRefs(),o=n.getVisibilityAffectingGlobalStateRefs();if(i.has(r)&&t.add(n.source),a.has(r))for(let{name:e,value:t}of a.get(r))this._updatePaintProperty(n,e,t);o?.has(r)&&(n.recalculateVisibility(),this._updateLayer(n))}}this.dispatcher.broadcast(`UGS`,n);for(let e in this.tileManagers)t.has(e)&&(this._reloadSource(e),this._changed=!0)}async loadURL(e,t={},n){this.fire(new Ir(`dataloading`)),t.validate=typeof t.validate!=`boolean`||t.validate,this._loadStyleRequest=new AbortController;let r=this._loadStyleRequest;try{let i=await this.map._requestManager.transformRequest(e,`Style`);un(r.signal);let a=await $n(i,r);this._loadStyleRequest===r&&(this._loadStyleRequest=null),this._load(a.data,t,n)}catch(e){this._loadStyleRequest===r&&(this._loadStyleRequest=null),e&&!r.signal.aborted&&this.fire(new R(ut(e)))}}loadJSON(e,t={},n){this.fire(new Ir(`dataloading`)),this._frameRequest=new AbortController,Dr.frameAsync(this._frameRequest,this.map._ownerWindow).then(()=>{this._frameRequest=null,t.validate=t.validate!==!1,this._load(e,t,n)}).catch(()=>{})}loadEmpty(){this.fire(new Ir(`dataloading`)),this._load(yc,{validate:!1})}_load(e,t,n){let r=t.transformStyle?t.transformStyle(n,e):e;if(!(t.validate&&zt(this,r))){r={...r},this._loaded=!0,this.stylesheet=r;for(let e in r.sources)this.addSource(e,r.sources[e],{validate:!1});r.sprite?this._loadSprite(r.sprite):this.imageManager.setLoaded(!0),this.glyphManager.setURL(r.glyphs),this._createLayers(),this.light=new oi(this.stylesheet.light),this._setProjectionInternal(this.stylesheet.projection?.type||`mercator`),this.sky=new li(this.stylesheet.sky),this.map.setTerrain(this.stylesheet.terrain??null,{validate:!1}),this.fire(new Ir(`data`)),this.fire(new Fr)}}_createLayers(){let e=St(this.stylesheet.layers);this.setGlobalState(this.stylesheet.state??null),this.dispatcher.broadcast(`SL`,e),this._order=e.map(e=>e.id),this._layers={},this._serializedLayers=null;for(let t of e){let e=be(t,this._globalState);if(e.setEventedParent(this,{layer:{id:t.id}}),this._layers[t.id]=e,u(e)&&this.tileManagers[e.source]){let n=t.paint?.[`raster-fade-duration`]??e.paint.get(`raster-fade-duration`);this.tileManagers[e.source].setRasterFadeDuration(n)}}}_loadSprite(e,t=!1,n=void 0){this.imageManager.setLoaded(!1);let r=new AbortController;this._spriteRequest=r;let i;qr(e,this.map._requestManager,this.map.getPixelRatio(),this._spriteRequest).then(e=>{if(this._spriteRequest=null,e)for(let n in e){this._spritesImagesIds[n]=[];let r=this._spritesImagesIds[n]?this._spritesImagesIds[n].filter(t=>!(t in e)):[];for(let e of r)this.imageManager.removeImage(e),this._changedImages[e]=!0;for(let r in e[n]){let i=n==="default"?r:`${n}:${r}`;this._spritesImagesIds[n].push(i),i in this.imageManager.images?this.imageManager.updateImage(i,e[n][r],!1):this.imageManager.addImage(i,e[n][r]),t&&(this._changedImages[i]=!0)}}}).catch(e=>{this._spriteRequest=null,i=e,r.signal.aborted||this.fire(new R(i))}).finally(()=>{this.imageManager.setLoaded(!0),this._availableImages=this.imageManager.listImages(),t&&(this._changed=!0),this.dispatcher.broadcast(`SI`,this._availableImages),this.fire(new Ir(`data`)),n&&n(i)})}_unloadSprite(){for(let e of Object.values(this._spritesImagesIds).flat())this.imageManager.removeImage(e),this._changedImages[e]=!0;this._spritesImagesIds={},this._availableImages=this.imageManager.listImages(),this._imagesListDirty=!0,this._changed=!0,this.fire(new Ir(`data`))}_validateLayer(e){let t=this.tileManagers[e.source];if(!t)return;let n=e.sourceLayer;if(!n)return;let r=t.getSource();(r.type===`geojson`||r.vectorLayerIds&&!r.vectorLayerIds.includes(n))&&this.fire(new R(Error(`Source layer "${n}" does not exist on source "${r.id}" as specified by style layer "${e.id}".`)))}loaded(){if(!this._loaded||Object.keys(this._updatedSources).length)return!1;for(let e in this.tileManagers)if(!this.tileManagers[e].loaded())return!1;return this.imageManager.isLoaded()}_serializeByIds(e,t=!1){let n=this._serializedAllLayers();if(!e||e.length===0)return Object.values(t?_e(n):n);let r=[];for(let i of e)if(n[i]){let e=t?_e(n[i]):n[i];r.push(e)}return r}_serializedAllLayers(){let e=this._serializedLayers;if(e)return e;e=this._serializedLayers={};let t=Object.keys(this._layers);for(let n of t){let t=this._layers[n];t.type!==`custom`&&(e[n]=t.serialize())}return e}hasTransitions(){if(this.light?.hasTransition()||this.sky?.hasTransition()||this.projection?.hasTransition())return!0;for(let e in this.tileManagers)if(this.tileManagers[e].hasTransition())return!0;for(let e in this._layers)if(this._layers[e].hasTransition())return!0;return!1}_checkLoaded(){if(!this._loaded)throw Error(`Style is not done loading.`)}update(e){if(!this._loaded)return;let t=this._changed;if(t){this._imagesListDirty&&=(this.dispatcher.broadcast(`SI`,this._availableImages),!1);let t=Object.keys(this._updatedLayers),n=Object.keys(this._removedLayers);(t.length||n.length)&&this._updateWorkerLayers(t,n);for(let e in this._updatedSources){let t=this._updatedSources[e];if(t===`reload`)this._reloadSource(e);else if(t===`clear`)this._clearSource(e);else throw Error(`Invalid action ${t}`)}this._updateTilesForChangedImages(),this._updateTilesForChangedGlyphs();for(let t in this._updatedPaintProps)this._layers[t].updateTransitions(e);this.light.updateTransitions(e),this.sky.updateTransitions(e),this._resetUpdates()}let n={};for(let e in this.tileManagers){let t=this.tileManagers[e];n[e]=t.used,t.used=!1}for(let t of this._order){let n=this._layers[t];n.recalculate(e,this._availableImages),!n.isHidden(e.zoom)&&n.source&&(this.tileManagers[n.source].used=!0)}for(let e in n){let t=this.tileManagers[e];!!n[e]!=!!t.used&&t.fire(new K(`data`,{sourceDataType:`visibility`,sourceId:e}))}this.light.recalculate(e),this.sky.recalculate(e),this.projection.recalculate(e),this.z=e.zoom,t&&this.fire(new Ir(`data`))}_updateTilesForChangedImages(){let e=Object.keys(this._changedImages);if(e.length){for(let t in this.tileManagers)this.tileManagers[t].reloadTilesForDependencies([`icons`,`patterns`],e);this._changedImages={}}}_updateTilesForChangedGlyphs(){if(this._glyphsDidChange){for(let e in this.tileManagers)this.tileManagers[e].reloadTilesForDependencies([`glyphs`],[``]);this._glyphsDidChange=!1}}_updateWorkerLayers(e,t){this.dispatcher.broadcast(`UL`,{layers:this._serializeByIds(e,!1),removedIds:t})}_resetUpdates(){this._changed=!1,this._updatedLayers={},this._removedLayers={},this._updatedSources={},this._updatedPaintProps={},this._changedImages={},this._glyphsDidChange=!1}setState(e,t={}){this._checkLoaded();let n=this.serialize();if(e=t.transformStyle?t.transformStyle(n,e):e,(t.validate??!0)&&zt(this,e))return!1;e=_e(e),e.layers=St(e.layers);let r=wn(n,e),i=this._getOperationsToPerform(r);if(i.unimplemented.length>0)throw Error(`Unimplemented: ${i.unimplemented.join(`, `)}.`);if(i.operations.length===0)return!1;for(let e of i.operations)e();return this.stylesheet=e,this._serializedLayers=null,this.fire(new Fr({style:this})),!0}_getOperationsToPerform(e){let t=[],n=[];for(let r of e)switch(r.command){case`setCenter`:case`setZoom`:case`setBearing`:case`setPitch`:case`setRoll`:continue;case`addLayer`:t.push(()=>this.addLayer.apply(this,r.args));break;case`removeLayer`:t.push(()=>this.removeLayer.apply(this,r.args));break;case`setPaintProperty`:t.push(()=>this.setPaintProperty.apply(this,r.args));break;case`setLayoutProperty`:t.push(()=>this.setLayoutProperty.apply(this,r.args));break;case`setFilter`:t.push(()=>this.setFilter.apply(this,r.args));break;case`addSource`:t.push(()=>this.addSource.apply(this,r.args));break;case`removeSource`:t.push(()=>this.removeSource.apply(this,r.args));break;case`setLayerZoomRange`:t.push(()=>this.setLayerZoomRange.apply(this,r.args));break;case`setLight`:t.push(()=>this.setLight.apply(this,r.args));break;case`setGeoJSONSourceData`:t.push(()=>this.setGeoJSONSourceData.apply(this,r.args));break;case`setGlyphs`:t.push(()=>this.setGlyphs.apply(this,r.args));break;case`setSprite`:t.push(()=>this.setSprite.apply(this,r.args));break;case`setTerrain`:t.push(()=>this.map.setTerrain.apply(this,r.args));break;case`setSky`:t.push(()=>this.setSky.apply(this,r.args));break;case`setProjection`:this.setProjection.apply(this,r.args);break;case`setGlobalState`:t.push(()=>this.setGlobalState.apply(this,r.args));break;case`setTransition`:t.push(()=>{});break;default:n.push(r.command);break}return{operations:t,unimplemented:n}}addImage(e,t){if(this.getImage(e)){this.fire(new R(Error(`An image named "${e}" already exists.`)));return}this.imageManager.addImage(e,t),this._afterImageUpdated(e)}updateImage(e,t){this.imageManager.updateImage(e,t)}getImage(e){return this.imageManager.getImage(e)}setMissingImageResolver(e){this.imageManager.setMissingImageResolver(e)}removeImage(e){if(!this.getImage(e)){this.fire(new R(Error(`An image named "${e}" does not exist.`)));return}this.imageManager.removeImage(e),this._afterImageUpdated(e)}_afterImageUpdated(e){this._availableImages=this.imageManager.listImages(),this._changedImages[e]=!0,this._imagesListDirty=!0,this._changed=!0,this.fire(new Ir(`data`))}listImages(){return this._checkLoaded(),this.imageManager.listImages()}addSource(e,t,r={}){if(this._checkLoaded(),this.tileManagers[e]!==void 0)throw Error(`Source "${e}" already exists.`);if(!t.type)throw Error(`The type property must be defined, but only the following properties were given: ${Object.keys(t).join(`, `)}.`);if(lt.has(t.type)&&this._validate(n.source,`sources.${e}`,t,null,r))return;this.map?._collectResourceTiming&&(t.collectResourceTiming=!0);let i=this.tileManagers[e]=new qa(e,t,this.dispatcher);i.style=this,i.setEventedParent(this,()=>({isSourceLoaded:i.loaded(),source:i.serialize(),sourceId:e})),i.onAdd(this.map),this._changed=!0}removeSource(e){if(this._checkLoaded(),this.tileManagers[e]===void 0)throw Error(`There is no source with this ID=${e}`);for(let t in this._layers)if(this._layers[t].source===e)return this.fire(new R(Error(`Source "${e}" cannot be removed while layer "${t}" is using it.`)));let t=this.tileManagers[e];delete this.tileManagers[e],delete this._updatedSources[e],t.fire(new K(`data`,{sourceDataType:`metadata`,sourceId:e})),t.setEventedParent(null),t.onRemove(this.map),this._changed=!0}setGeoJSONSourceData(e,t){if(this._checkLoaded(),this.tileManagers[e]===void 0)throw Error(`There is no source with this ID=${e}`);let n=this.tileManagers[e].getSource();if(n.type!==`geojson`)throw Error(`geojsonSource.type is ${n.type}, which is !== 'geojson`);n.setData(t),this._changed=!0}getSource(e){return this.tileManagers[e]?.getSource()}addLayer(e,t,r={}){this._checkLoaded();let i=e.id;if(this.getLayer(i)){this.fire(new R(Error(`Layer "${i}" already exists on this map.`)));return}let a;if(e.type===`custom`){if(mt(this,Ne(e)))return;a=be(e,this._globalState)}else{if(`source`in e&&typeof e.source==`object`&&(this.addSource(i,e.source),e=_e(e),e=L(e,{source:i})),this._validate(n.layer,`layers.${i}`,e,{arrayIndex:-1},r))return;a=be(e,this._globalState),this._validateLayer(a),a.setEventedParent(this,{layer:{id:i}})}let o=t?this._order.indexOf(t):this._order.length;if(t&&o===-1){this.fire(new R(Error(`Cannot add layer "${i}" before non-existing layer "${t}".`)));return}if(this._order.splice(o,0,i),this._layerOrderChanged=!0,this._layers[i]=a,this._removedLayers[i]&&a.source&&a.type!==`custom`){let e=this._removedLayers[i];delete this._removedLayers[i],e.type===a.type?(this._updatedSources[a.source]=`reload`,this.tileManagers[a.source].pause()):this._updatedSources[a.source]=`clear`}this._updateLayer(a),a.onAdd&&a.onAdd(this.map)}moveLayer(e,t){if(this._checkLoaded(),this._changed=!0,!this._layers[e]){this.fire(new R(Error(`The layer '${e}' does not exist in the map's style and cannot be moved.`)));return}if(e===t)return;let n=this._order.indexOf(e);this._order.splice(n,1);let r=t?this._order.indexOf(t):this._order.length;if(t&&r===-1){this.fire(new R(Error(`Cannot move layer "${e}" before non-existing layer "${t}".`)));return}this._order.splice(r,0,e),this._layerOrderChanged=!0}removeLayer(e){this._checkLoaded();let t=this._layers[e];if(!t){this.fire(new R(Error(`Cannot remove non-existing layer "${e}".`)));return}t.setEventedParent(null);let n=this._order.indexOf(e);this._order.splice(n,1),this._layerOrderChanged=!0,this._changed=!0,this._removedLayers[e]=t,delete this._layers[e],this._serializedLayers&&delete this._serializedLayers[e],delete this._updatedLayers[e],delete this._updatedPaintProps[e],t.onRemove&&t.onRemove(this.map)}getLayer(e){return this._layers[e]}getLayersOrder(){return[...this._order]}hasLayer(e){return e in this._layers}setLayerZoomRange(e,t,n){this._checkLoaded();let r=this.getLayer(e);if(!r){this.fire(new R(Error(`Cannot set the zoom range of non-existing layer "${e}".`)));return}(r.minzoom!==t||r.maxzoom!==n)&&(t!=null&&(r.minzoom=t),n!=null&&(r.maxzoom=n),this._updateLayer(r))}setFilter(e,t,r={}){this._checkLoaded();let i=this.getLayer(e);if(!i){this.fire(new R(Error(`Cannot filter non-existing layer "${e}".`)));return}if(!Ze(i.filter,t)){if(t==null){i.setFilter(void 0),this._updateLayer(i);return}this._validate(n.filter,`layers.${i.id}.filter`,t,null,r)||(i.setFilter(_e(t)),this._updateLayer(i))}}getFilter(e){return _e(this.getLayer(e).filter)}setLayoutProperty(e,t,n,r={}){this._checkLoaded();let i=this.getLayer(e);if(!i){this.fire(new R(Error(`Cannot style non-existing layer "${e}".`)));return}Ze(i.getLayoutProperty(t),n)||(i.setLayoutProperty(t,n,r),this._updateLayer(i))}getLayoutProperty(e,t){let n=this.getLayer(e);if(!n){this.fire(new R(Error(`Cannot get style of non-existing layer "${e}".`)));return}return n.getLayoutProperty(t)}setPaintProperty(e,t,n,r={}){this._checkLoaded();let i=this.getLayer(e);if(!i){this.fire(new R(Error(`Cannot style non-existing layer "${e}".`)));return}Ze(i.getPaintProperty(t),n)||this._updatePaintProperty(i,t,n,r)}_updatePaintProperty(e,t,n,r={}){e.setPaintProperty(t,n,r)&&this._updateLayer(e),u(e)&&t===`raster-fade-duration`&&this.tileManagers[e.source].setRasterFadeDuration(n),this._changed=!0,this._updatedPaintProps[e.id]=!0,this._serializedLayers=null}getPaintProperty(e,t){return this.getLayer(e).getPaintProperty(t)}setFeatureState(e,t){this._checkLoaded();let n=e.source,r=e.sourceLayer,i=this.tileManagers[n];if(i===void 0){this.fire(new R(Error(`The source '${n}' does not exist in the map's style.`)));return}let a=i.getSource().type;if(a===`geojson`&&r){this.fire(new R(Error(`GeoJSON sources cannot have a sourceLayer parameter.`)));return}if(a===`vector`&&!r){this.fire(new R(Error(`The sourceLayer parameter must be provided for vector source types.`)));return}if(e.id===void 0){this.fire(new R(Error(`The feature id parameter must be provided.`)));return}let o=[`__proto__`,`constructor`,`prototype`];if(t&&Object.keys(t).some(e=>o.includes(e))){this.fire(new R(Error(`The feature state should not include one of the following keys: ${o}`)));return}i.setFeatureState(r,e.id,t)}removeFeatureState(e,t){this._checkLoaded();let n=e.source,r=this.tileManagers[n];if(r===void 0){this.fire(new R(Error(`The source '${n}' does not exist in the map's style.`)));return}let i=r.getSource().type,a=i===`vector`?e.sourceLayer:void 0;if(i===`vector`&&!a){this.fire(new R(Error(`The sourceLayer parameter must be provided for vector source types.`)));return}if(t&&typeof e.id!=`string`&&typeof e.id!=`number`){this.fire(new R(Error(`A feature id is required to remove its specific state property.`)));return}r.removeFeatureState(a,e.id,t)}getFeatureState(e){this._checkLoaded();let t=e.source,n=e.sourceLayer,r=this.tileManagers[t];if(r===void 0){this.fire(new R(Error(`The source '${t}' does not exist in the map's style.`)));return}if(r.getSource().type===`vector`&&!n){this.fire(new R(Error(`The sourceLayer parameter must be provided for vector source types.`)));return}return e.id===void 0&&this.fire(new R(Error(`The feature id parameter must be provided.`))),r.getFeatureState(n,e.id)}getTransition(){return L({duration:300,delay:0},this.stylesheet?.transition)}serialize(){if(!this._loaded)return;let e=Wt(this.tileManagers,e=>e.serialize()),t=this._serializeByIds(this._order,!0),n=this.map.getTerrain()||void 0,r=this.stylesheet;return Ie({version:r.version,name:r.name,metadata:r.metadata,light:r.light,sky:r.sky,center:r.center,zoom:r.zoom,bearing:r.bearing,pitch:r.pitch,sprite:r.sprite,glyphs:r.glyphs,transition:r.transition,projection:r.projection,sources:e,layers:t,terrain:n},e=>e!==void 0)}_updateLayer(e){this._updatedLayers[e.id]=!0,e.source&&!this._updatedSources[e.source]&&this.tileManagers[e.source].getSource().type!==`raster`&&(this._updatedSources[e.source]=`reload`,this.tileManagers[e.source].pause()),this._serializedLayers=null,this._changed=!0}_flattenAndSortRenderedFeatures(e){let t=e=>this._layers[e].type===`fill-extrusion`,n={},r=[];for(let i=this._order.length-1;i>=0;i--){let a=this._order[i];if(t(a)){n[a]=i;for(let t of e){let e=t[a];if(e)for(let t of e)r.push(t)}}}r.sort((e,t)=>t.intersectionZ-e.intersectionZ);let i=[];for(let a=this._order.length-1;a>=0;a--){let o=this._order[a];if(t(o))for(let e=r.length-1;e>=0;e--){let t=r[e].feature;if(n[t.layer.id]this.map.terrain.getElevation(e,t,n):void 0));return this.placement&&a.push(Ai(this._layers,o,this.tileManagers,e,c,this.placement.collisionIndex,this.placement.retainedQueryData)),this._flattenAndSortRenderedFeatures(a)}querySourceFeatures(e,t){t?.filter&&this._validate(n.filter,`querySourceFeatures.filter`,t.filter,null,t);let r=this.tileManagers[e];return r?ji(r,t?{...t,globalState:this._globalState}:{globalState:this._globalState}):[]}getLight(){return this.light.getLight()}setLight(e,t={}){this._checkLoaded();let n=this.light.getLight(),r=!1;for(let t in e)if(!Ze(e[t],n[t])){r=!0;break}if(!r)return;let i={now:U(),transition:L({duration:300,delay:0},this.stylesheet.transition)};this.light.setLight(e,t),this.light.updateTransitions(i)}getProjection(){return this.stylesheet?.projection}setProjection(e){this._checkLoaded();let t=e??{type:`mercator`};if(this.stylesheet.projection=e,this.projection){if(this.projection.name===t.type)return;this.projection.destroy(),delete this.projection}this._setProjectionInternal(t.type)}getSky(){return this.stylesheet?.sky}setSky(e,t={}){this._checkLoaded();let n=this.getSky(),r=!1;if(!e&&!n)return;if(e&&!n)r=!0;else if(!e&&n)r=!0;else for(let t in e)if(!Ze(e[t],n[t])){r=!0;break}if(!r)return;let i={now:U(),transition:L({duration:300,delay:0},this.stylesheet.transition)};this.stylesheet.sky=e,this.sky.setSky(e,t),this.sky.updateTransitions(i)}_setProjectionInternal(e){let t=vc(e,this.map._camera?.transform.constrainOverride);this.projection=t.projection,this.map.migrateProjection(t.transform,t.cameraHelper);for(let e in this.tileManagers)this.tileManagers[e].reload()}_validate(e,t,n,r,i={}){return Re(this,e,{key:t,style:this.serialize(),value:n,...r},i)}_remove(e=!0){this._frameRequest&&=(this._frameRequest.abort(),null),this._loadStyleRequest&&=(this._loadStyleRequest.abort(),null),this._spriteRequest&&=(this._spriteRequest.abort(),null),va().off(ha,this._rtlPluginLoaded);for(let e in this._layers)this._layers[e].setEventedParent(null);for(let e in this.tileManagers){let t=this.tileManagers[e];t.setEventedParent(null),t.onRemove(this.map)}this.imageManager.setEventedParent(null),this.setEventedParent(null),e&&this.dispatcher.broadcast(`RM`,void 0),this.dispatcher.remove(e)}_clearSource(e){this.tileManagers[e].clearTiles()}_reloadSource(e){this.tileManagers[e].resume(),this.tileManagers[e].reload()}_updateSources(e){for(let t in this.tileManagers)this.tileManagers[t].update(e,this.map.terrain)}_generateCollisionBoxes(){for(let e in this.tileManagers)this._reloadSource(e)}_updatePlacement(e,t,n,r,i=!1){let a=!1,o=!1,s={};for(let t of this._order){let n=this._layers[t];if(n.type!==`symbol`)continue;if(!s[n.source]){let e=this.tileManagers[n.source];s[n.source]=e.getRenderableIds(!0).map(t=>e.getTileByID(t)).sort((e,t)=>t.tileID.overscaledZ-e.tileID.overscaledZ||(e.tileID.isLessThan(t.tileID)?-1:1))}let r=this.crossTileSymbolIndex.addLayer(n,s[n.source],e.center.lng);a||=r}if(this.crossTileSymbolIndex.pruneUnusedLayers(this._order),i||=this._layerOrderChanged||n===0,(i||!this.pauseablePlacement||this.pauseablePlacement.isDone()&&!this.placement.stillRecent(U(),e.zoom))&&(this.pauseablePlacement=new Vo(e,this.map.terrain,this._order,i,t,n,r,this.placement),this._layerOrderChanged=!1),this.pauseablePlacement.isDone()?this.placement.setStale():(this.pauseablePlacement.continuePlacement(this._order,this._layers,s),this.pauseablePlacement.isDone()&&(this.placement=this.pauseablePlacement.commit(U()),o=!0),a&&this.pauseablePlacement.placement.setStale()),o||a)for(let e of this._order){let t=this._layers[e];t.type===`symbol`&&this.placement.updateLayerOpacities(t,s[t.source])}return!this.pauseablePlacement.isDone()||this.placement.hasTransitions(U())}_releaseSymbolFadeTiles(){for(let e in this.tileManagers)this.tileManagers[e].releaseSymbolFadeTiles()}async getImages(e,t){let n=await this.imageManager.getImages(t.icons);this._updateTilesForChangedImages();let r=this.tileManagers[t.source];return r&&r.setDependencies(t.tileID.key,t.type,t.icons),n}async getGlyphs(e,t){let n=await this.glyphManager.getGlyphs(t.stacks),r=this.tileManagers[t.source];return r&&r.setDependencies(t.tileID.key,t.type,[``]),n}getGlyphsUrl(){return this.stylesheet.glyphs||null}setGlyphs(e,t={}){this._checkLoaded(),!(e&&this._validate(n.glyphs,`glyphs`,e,null,t))&&(this._glyphsDidChange=!0,this.stylesheet.glyphs=e,this.glyphManager.entries={},this.glyphManager.setURL(e))}async getDashes(e,t){let n={};for(let[e,r]of Object.entries(t.dashes))n[e]=this.lineAtlas.getDash(r.dasharray,r.round);return n}addSprite(e,t,r={},i){this._checkLoaded();let a=[{id:e,url:t}],o=[...Gr(this.stylesheet.sprite),...a];this._validate(n.sprite,`sprite`,o,null,r)||(this.stylesheet.sprite=o,this._loadSprite(a,!0,i))}removeSprite(e){this._checkLoaded();let t=Gr(this.stylesheet.sprite);if(!t.find(t=>t.id===e)){this.fire(new R(Error(`Sprite "${e}" doesn't exists on this map.`)));return}if(this._spritesImagesIds[e])for(let t of this._spritesImagesIds[e])this.imageManager.removeImage(t),this._changedImages[t]=!0;t.splice(t.findIndex(t=>t.id===e),1),this.stylesheet.sprite=t.length>0?t:void 0,delete this._spritesImagesIds[e],this._availableImages=this.imageManager.listImages(),this._imagesListDirty=!0,this._changed=!0,this.fire(new Ir(`data`))}getSprite(){return Gr(this.stylesheet.sprite)}setSprite(e,t={},r){this._checkLoaded(),!(e&&this._validate(n.sprite,`sprite`,e,null,t))&&(this.stylesheet.sprite=e,e?this._loadSprite(e,!0,r):(this._unloadSprite(),r&&r(null)))}destroy(){this._frameRequest&&=(this._frameRequest.abort(),null),this._loadStyleRequest&&=(this._loadStyleRequest.abort(),null),this._spriteRequest&&=(this._spriteRequest.abort(),null);for(let e in this.tileManagers){let t=this.tileManagers[e];t.setEventedParent(null),t.onRemove(this.map)}this.tileManagers={},this.imageManager&&(this.imageManager.setEventedParent(null),this.imageManager.destroy(),this._availableImages=[],this._spritesImagesIds={}),this.glyphManager&&this.glyphManager.destroy();for(let e in this._layers){let t=this._layers[e];t.setEventedParent(null),t.onRemove&&t.onRemove(this.map)}this._setInitialValues(),this.setEventedParent(null),this.dispatcher.unregisterMessageHandler(`GG`),this.dispatcher.unregisterMessageHandler(`GI`),this.dispatcher.unregisterMessageHandler(`GDA`),this.dispatcher.remove(!0),this._listeners={},this._oneTimeListeners={}}};const xc=yr([{name:`a_pos`,type:`Int16`,components:2},{name:`a_texture_pos`,type:`Int16`,components:2}]);var Sc=class{constructor(){this.boundProgram=null,this.boundLayoutVertexBuffer=null,this.boundPaintVertexBuffers=[],this.boundIndexBuffer=null,this.boundVertexOffset=null,this.boundDynamicVertexBuffer=null,this.vao=null}bind(e,t,n,r,i,a,o,s,c){this.context=e;let l=this.boundPaintVertexBuffers.length!==r.length;for(let e=0;!l&&e({u_depth:new P(e,t.u_depth),u_terrain:new P(e,t.u_terrain),u_terrain_dim:new H(e,t.u_terrain_dim),u_terrain_matrix:new fn(e,t.u_terrain_matrix),u_terrain_unpack:new Ye(e,t.u_terrain_unpack),u_terrain_exaggeration:new H(e,t.u_terrain_exaggeration)}),wc=(e,t)=>({u_texture:new P(e,t.u_texture),u_ele_delta:new H(e,t.u_ele_delta),u_fog_matrix:new fn(e,t.u_fog_matrix),u_fog_color:new T(e,t.u_fog_color),u_fog_ground_blend:new H(e,t.u_fog_ground_blend),u_fog_ground_blend_opacity:new H(e,t.u_fog_ground_blend_opacity),u_horizon_color:new T(e,t.u_horizon_color),u_horizon_fog_blend:new H(e,t.u_horizon_fog_blend),u_is_globe_mode:new H(e,t.u_is_globe_mode)}),Tc=(e,t)=>({u_ele_delta:new H(e,t.u_ele_delta)}),Ec=(e,t)=>({u_texture:new P(e,t.u_texture),u_terrain_coords_id:new H(e,t.u_terrain_coords_id),u_ele_delta:new H(e,t.u_ele_delta)}),Dc=(e,t,n,r,i)=>({u_texture:0,u_ele_delta:e,u_fog_matrix:t,u_fog_color:n?n.properties.get(`fog-color`):V.white,u_fog_ground_blend:n?n.properties.get(`fog-ground-blend`):1,u_fog_ground_blend_opacity:i?0:n?n.calculateFogBlendOpacity(r):0,u_horizon_color:n?n.properties.get(`horizon-color`):V.white,u_horizon_fog_blend:n?n.properties.get(`horizon-fog-blend`):1,u_is_globe_mode:+!!i}),Oc=e=>({u_ele_delta:e}),kc=(e,t)=>({u_terrain_coords_id:e/255,u_texture:0,u_ele_delta:t}),Ac=(e,t)=>({u_projection_matrix:new fn(e,t.u_projection_matrix),u_projection_tile_mercator_coords:new Ye(e,t.u_projection_tile_mercator_coords),u_projection_clipping_plane:new Ye(e,t.u_projection_clipping_plane),u_projection_transition:new H(e,t.u_projection_transition),u_projection_fallback_matrix:new fn(e,t.u_projection_fallback_matrix)}),jc={mainMatrix:`u_projection_matrix`,tileMercatorCoords:`u_projection_tile_mercator_coords`,clippingPlane:`u_projection_clipping_plane`,projectionTransition:`u_projection_transition`,fallbackMatrix:`u_projection_fallback_matrix`};function Mc(e){let t=[];for(let n of e){if(n===null)continue;let e=n.split(` `);t.push(e.pop())}return t}var Nc=class{constructor(e,t,n,r,i,a,o,s,c=[]){let l=e.gl;this.program=l.createProgram();let u=Mc(t.staticAttributes),d=n?n.getBinderAttributes():[],f=u.concat(d),p=ls.prelude.staticUniforms?Mc(ls.prelude.staticUniforms):[],m=o.staticUniforms?Mc(o.staticUniforms):[],h=t.staticUniforms?Mc(t.staticUniforms):[],g=n?n.getBinderUniforms():[],_=p.concat(m).concat(h).concat(g),v=[];for(let e of _)v.includes(e)||v.push(e);let y=n?n.defines():[];y.unshift(`#version 300 es`),i&&y.push(`#define OVERDRAW_INSPECTOR;`),a&&y.push(`#define TERRAIN3D;`),s&&y.push(s),c&&y.push(...c);let b=y.concat(ls.prelude.fragmentSource,o.fragmentSource,t.fragmentSource).join(` -`),x=y.concat(ls.prelude.vertexSource,o.vertexSource,t.vertexSource).join(` -`),S=l.createShader(l.FRAGMENT_SHADER);if(l.isContextLost()){this.failedToCreate=!0;return}if(l.shaderSource(S,b),l.compileShader(S),!l.getShaderParameter(S,l.COMPILE_STATUS))throw Error(`Could not compile fragment shader: ${l.getShaderInfoLog(S)}`);l.attachShader(this.program,S);let C=l.createShader(l.VERTEX_SHADER);if(l.isContextLost()){this.failedToCreate=!0;return}if(l.shaderSource(C,x),l.compileShader(C),!l.getShaderParameter(C,l.COMPILE_STATUS))throw Error(`Could not compile vertex shader: ${l.getShaderInfoLog(C)}`);l.attachShader(this.program,C),this.attributes={};let w={};this.numAttributes=f.length;for(let e=0;e=0&&(this.attributes[e]=t)}if(!l.getProgramParameter(this.program,l.LINK_STATUS))throw Error(`Program failed to link: ${l.getProgramInfoLog(this.program)}`);l.deleteShader(C),l.deleteShader(S);for(let e of v)if(e&&!w[e]){let t=l.getUniformLocation(this.program,e);t&&(w[e]=t)}this.fixedUniforms=r(e,w),this.terrainUniforms=Cc(e,w),this.projectionUniforms=Ac(e,w),this.binderUniforms=n?n.getUniforms(e,w):[]}draw(e,t,n,r,i,a,o,s,c,l,u,d,f,p,m,h,g,_,v){let y=e.gl;if(this.failedToCreate)return;if(e.program.set(this.program),e.setDepthMode(n),e.setStencilMode(r),e.setColorMode(i),e.setCullFace(a),s){e.activeTexture.set(y.TEXTURE2),y.bindTexture(y.TEXTURE_2D,s.depthTexture),e.activeTexture.set(y.TEXTURE3),y.bindTexture(y.TEXTURE_2D,s.texture);for(let e in this.terrainUniforms)this.terrainUniforms[e].set(s[e])}if(c)for(let e in c){let t=jc[e];this.projectionUniforms[t].set(c[e])}if(o)for(let e in this.fixedUniforms)this.fixedUniforms[e].set(o[e]);h&&h.setUniforms(e,this.binderUniforms,p,{zoom:m});let b=0;switch(t){case y.LINES:b=2;break;case y.TRIANGLES:b=3;break;case y.LINE_STRIP:b=1;break}for(let n of f.get())n.vaos||={},n.vaos[l]||=new Sc,n.vaos[l].bind(e,this,u,h?h.getPaintVertexBuffers():[],d,n.vertexOffset,g,_,v),y.drawElements(t,n.primitiveLength*b,y.UNSIGNED_SHORT,n.primitiveOffset*b*2)}};function Pc(e,t,n){let r=1/Ee(n,1,t.transform.tileZoom),i=2**n.tileID.overscaledZ,a=n.tileSize*2**t.transform.tileZoom/i,o=a*(n.tileID.canonical.x+n.tileID.wrap*i),s=a*n.tileID.canonical.y;return{u_image:0,u_texsize:n.imageAtlasTexture.size,u_scale:[r,e.fromScale,e.toScale],u_fade:e.t,u_pixel_coord_upper:[o>>16,s>>16],u_pixel_coord_lower:[o&65535,s&65535]}}function Fc(e,t,n,r){let i=n.imageManager.getPattern(e.from.toString()),a=n.imageManager.getPattern(e.to.toString()),{width:o,height:s}=n.imageManager.getPixelSize(),c=2**r.tileID.overscaledZ,l=r.tileSize*2**n.transform.tileZoom/c,u=l*(r.tileID.canonical.x+r.tileID.wrap*c),d=l*r.tileID.canonical.y;return{u_image:0,u_pattern_tl_a:i.tl,u_pattern_br_a:i.br,u_pattern_tl_b:a.tl,u_pattern_br_b:a.br,u_texsize:[o,s],u_mix:t.t,u_pattern_size_a:i.displaySize,u_pattern_size_b:a.displaySize,u_scale_a:t.fromScale,u_scale_b:t.toScale,u_tile_units_to_pixels:1/Ee(r,1,n.transform.tileZoom),u_pixel_coord_upper:[u>>16,d>>16],u_pixel_coord_lower:[u&65535,d&65535]}}const Ic=(e,t)=>({u_lightpos:new or(e,t.u_lightpos),u_lightpos_globe:new or(e,t.u_lightpos_globe),u_lightintensity:new H(e,t.u_lightintensity),u_lightcolor:new or(e,t.u_lightcolor),u_vertical_gradient:new H(e,t.u_vertical_gradient),u_opacity:new H(e,t.u_opacity),u_fill_translate:new h(e,t.u_fill_translate)}),Lc=(e,t)=>({u_lightpos:new or(e,t.u_lightpos),u_lightpos_globe:new or(e,t.u_lightpos_globe),u_lightintensity:new H(e,t.u_lightintensity),u_lightcolor:new or(e,t.u_lightcolor),u_vertical_gradient:new H(e,t.u_vertical_gradient),u_height_factor:new H(e,t.u_height_factor),u_opacity:new H(e,t.u_opacity),u_fill_translate:new h(e,t.u_fill_translate),u_image:new P(e,t.u_image),u_texsize:new h(e,t.u_texsize),u_pixel_coord_upper:new h(e,t.u_pixel_coord_upper),u_pixel_coord_lower:new h(e,t.u_pixel_coord_lower),u_scale:new or(e,t.u_scale),u_fade:new H(e,t.u_fade)}),Rc=(e,t,n,r)=>{let i=e.style.light,a=i.getCartesianPosition(),o=S();i.properties.get(`anchor`)===`viewport`&&_(o,e.transform.bearingInRadians),Cn(a,a,o);let s=e.transform.transformLightDirection(a),c=i.properties.get(`color`);return{u_lightpos:a,u_lightpos_globe:s,u_lightintensity:i.properties.get(`intensity`),u_lightcolor:[c.r,c.g,c.b],u_vertical_gradient:+t,u_opacity:n,u_fill_translate:r}},zc=(e,t,n,r,i,a,o)=>L(Rc(e,t,n,r),Pc(a,e,o),{u_height_factor:-(2**i.overscaledZ)/o.tileSize/8}),Bc=(e,t)=>({u_fill_translate:new h(e,t.u_fill_translate)}),Vc=(e,t)=>({u_image:new P(e,t.u_image),u_texsize:new h(e,t.u_texsize),u_pixel_coord_upper:new h(e,t.u_pixel_coord_upper),u_pixel_coord_lower:new h(e,t.u_pixel_coord_lower),u_scale:new or(e,t.u_scale),u_fade:new H(e,t.u_fade),u_fill_translate:new h(e,t.u_fill_translate)}),Hc=(e,t)=>({u_world:new h(e,t.u_world),u_fill_translate:new h(e,t.u_fill_translate)}),Uc=(e,t)=>({u_world:new h(e,t.u_world),u_image:new P(e,t.u_image),u_texsize:new h(e,t.u_texsize),u_pixel_coord_upper:new h(e,t.u_pixel_coord_upper),u_pixel_coord_lower:new h(e,t.u_pixel_coord_lower),u_scale:new or(e,t.u_scale),u_fade:new H(e,t.u_fade),u_fill_translate:new h(e,t.u_fill_translate)}),Wc=(e,t,n,r)=>L(Pc(t,e,n),{u_fill_translate:r}),Gc=e=>({u_fill_translate:e}),Kc=(e,t)=>({u_world:e,u_fill_translate:t}),qc=(e,t,n,r,i)=>L(Wc(e,t,n,i),{u_world:r}),Jc=(e,t)=>({u_camera_to_center_distance:new H(e,t.u_camera_to_center_distance),u_scale_with_map:new P(e,t.u_scale_with_map),u_pitch_with_map:new P(e,t.u_pitch_with_map),u_extrude_scale:new h(e,t.u_extrude_scale),u_device_pixel_ratio:new H(e,t.u_device_pixel_ratio),u_globe_extrude_scale:new H(e,t.u_globe_extrude_scale),u_translate:new h(e,t.u_translate)}),Yc=(e,t,n,r,i)=>{let a=e.transform,o,s,c=0;if(n.paint.get(`circle-pitch-alignment`)===`map`){let e=Ee(t,1,a.zoom);o=!0,s=[e,e],c=e/(M*2**t.tileID.overscaledZ)*2*Math.PI*i}else o=!1,s=a.pixelsToGLUnits;return{u_camera_to_center_distance:a.cameraToCenterDistance,u_scale_with_map:+(n.paint.get(`circle-pitch-scale`)===`map`),u_pitch_with_map:+o,u_device_pixel_ratio:e.pixelRatio,u_extrude_scale:s,u_globe_extrude_scale:c,u_translate:r}},Xc=(e,t)=>({u_pixel_extrude_scale:new h(e,t.u_pixel_extrude_scale)}),Zc=(e,t)=>({u_viewport_size:new h(e,t.u_viewport_size)}),Qc=e=>({u_pixel_extrude_scale:[1/e.width,1/e.height]}),$c=e=>({u_viewport_size:[e.width,e.height]}),el=(e,t)=>({u_color:new T(e,t.u_color),u_overlay:new P(e,t.u_overlay),u_overlay_scale:new H(e,t.u_overlay_scale)}),tl=(e,t=1)=>({u_color:e,u_overlay:0,u_overlay_scale:t}),nl=(e,t)=>({u_extrude_scale:new H(e,t.u_extrude_scale),u_intensity:new H(e,t.u_intensity),u_globe_extrude_scale:new H(e,t.u_globe_extrude_scale)}),rl=(e,t)=>({u_matrix:new fn(e,t.u_matrix),u_world:new h(e,t.u_world),u_image:new P(e,t.u_image),u_color_ramp:new P(e,t.u_color_ramp),u_opacity:new H(e,t.u_opacity)}),il=(e,t,n,r)=>{let i=Ee(e,1,t)/(M*2**e.tileID.overscaledZ)*2*Math.PI*r;return{u_extrude_scale:Ee(e,1,t),u_intensity:n,u_globe_extrude_scale:i}},al=(e,t,n,r)=>{let i=Ut();fr(i,0,e.width,e.height,0,0,1);let a=e.context.gl;return{u_matrix:i,u_world:[a.drawingBufferWidth,a.drawingBufferHeight],u_image:n,u_color_ramp:r,u_opacity:t.paint.get(`heatmap-opacity`)}},ol=(e,t)=>({u_image:new P(e,t.u_image),u_latrange:new h(e,t.u_latrange),u_exaggeration:new H(e,t.u_exaggeration),u_altitudes:new Me(e,t.u_altitudes),u_azimuths:new Me(e,t.u_azimuths),u_accent:new T(e,t.u_accent),u_method:new P(e,t.u_method),u_shadows:new b(e,t.u_shadows),u_highlights:new b(e,t.u_highlights)}),sl=(e,t)=>({u_matrix:new fn(e,t.u_matrix),u_image:new P(e,t.u_image),u_dimension:new h(e,t.u_dimension),u_zoom:new H(e,t.u_zoom),u_unpack:new Ye(e,t.u_unpack)}),cl=(e,t,n)=>{let r=n.paint.get(`hillshade-accent-color`),i;switch(n.paint.get(`hillshade-method`)){case`basic`:i=4;break;case`combined`:i=1;break;case`igor`:i=2;break;case`multidirectional`:i=3;break;default:i=0;break}let a=n.getIlluminationProperties();for(let t=0;t{let n=t.stride,r=Ut();return fr(r,0,M,-M,0,0,1),F(r,r,[0,-M,0]),{u_matrix:r,u_image:1,u_dimension:[n,n],u_zoom:e.overscaledZ,u_unpack:t.getUnpackVector()}};function ul(e,t){let n=2**t.canonical.z,r=t.canonical.y;return[new N(0,r/n).toLngLat().lat,new N(0,(r+1)/n).toLngLat().lat]}const dl=(e,t)=>({u_image:new P(e,t.u_image),u_unpack:new Ye(e,t.u_unpack),u_dimension:new h(e,t.u_dimension),u_elevation_stops:new P(e,t.u_elevation_stops),u_color_stops:new P(e,t.u_color_stops),u_color_ramp_size:new P(e,t.u_color_ramp_size),u_opacity:new H(e,t.u_opacity)}),fl=(e,t,n=0)=>({u_image:0,u_unpack:t.getUnpackVector(),u_dimension:[t.stride,t.stride],u_elevation_stops:1,u_color_stops:4,u_color_ramp_size:n,u_opacity:e.paint.get(`color-relief-opacity`)}),pl=(e,t)=>({u_translation:new h(e,t.u_translation),u_ratio:new H(e,t.u_ratio),u_device_pixel_ratio:new H(e,t.u_device_pixel_ratio),u_units_to_pixels:new h(e,t.u_units_to_pixels)}),ml=(e,t)=>({u_translation:new h(e,t.u_translation),u_ratio:new H(e,t.u_ratio),u_device_pixel_ratio:new H(e,t.u_device_pixel_ratio),u_units_to_pixels:new h(e,t.u_units_to_pixels),u_image:new P(e,t.u_image),u_image_height:new H(e,t.u_image_height)}),hl=(e,t)=>({u_translation:new h(e,t.u_translation),u_texsize:new h(e,t.u_texsize),u_ratio:new H(e,t.u_ratio),u_device_pixel_ratio:new H(e,t.u_device_pixel_ratio),u_image:new P(e,t.u_image),u_units_to_pixels:new h(e,t.u_units_to_pixels),u_scale:new or(e,t.u_scale),u_fade:new H(e,t.u_fade)}),gl=(e,t)=>({u_translation:new h(e,t.u_translation),u_ratio:new H(e,t.u_ratio),u_device_pixel_ratio:new H(e,t.u_device_pixel_ratio),u_units_to_pixels:new h(e,t.u_units_to_pixels),u_image:new P(e,t.u_image),u_mix:new H(e,t.u_mix),u_tileratio:new H(e,t.u_tileratio),u_crossfade_from:new H(e,t.u_crossfade_from),u_crossfade_to:new H(e,t.u_crossfade_to),u_lineatlas_width:new H(e,t.u_lineatlas_width),u_lineatlas_height:new H(e,t.u_lineatlas_height)}),_l=(e,t)=>({u_translation:new h(e,t.u_translation),u_ratio:new H(e,t.u_ratio),u_device_pixel_ratio:new H(e,t.u_device_pixel_ratio),u_units_to_pixels:new h(e,t.u_units_to_pixels),u_image:new P(e,t.u_image),u_image_height:new H(e,t.u_image_height),u_tileratio:new H(e,t.u_tileratio),u_crossfade_from:new H(e,t.u_crossfade_from),u_crossfade_to:new H(e,t.u_crossfade_to),u_image_dash:new P(e,t.u_image_dash),u_mix:new H(e,t.u_mix),u_lineatlas_width:new H(e,t.u_lineatlas_width),u_lineatlas_height:new H(e,t.u_lineatlas_height)}),vl=(e,t,n,r)=>{let i=e.transform;return{u_translation:wl(e,t,n),u_ratio:r/Ee(t,1,i.zoom),u_device_pixel_ratio:e.pixelRatio,u_units_to_pixels:[1/i.pixelsToGLUnits[0],1/i.pixelsToGLUnits[1]]}},yl=(e,t,n,r,i)=>L(vl(e,t,n,r),{u_image:0,u_image_height:i}),bl=(e,t,n,r,i)=>{let a=e.transform,o=Cl(t,a);return{u_translation:wl(e,t,n),u_texsize:t.imageAtlasTexture.size,u_ratio:r/Ee(t,1,a.zoom),u_device_pixel_ratio:e.pixelRatio,u_image:0,u_scale:[o,i.fromScale,i.toScale],u_fade:i.t,u_units_to_pixels:[1/a.pixelsToGLUnits[0],1/a.pixelsToGLUnits[1]]}},xl=(e,t,n,r,i)=>{let a=e.transform,o=Cl(t,a);return L(vl(e,t,n,r),{u_tileratio:o,u_crossfade_from:i.fromScale,u_crossfade_to:i.toScale,u_image:0,u_mix:i.t,u_lineatlas_width:e.lineAtlas.width,u_lineatlas_height:e.lineAtlas.height})},Sl=(e,t,n,r,i,a)=>{let o=e.transform,s=Cl(t,o);return L(vl(e,t,n,r),{u_image:0,u_image_height:a,u_tileratio:s,u_crossfade_from:i.fromScale,u_crossfade_to:i.toScale,u_image_dash:1,u_mix:i.t,u_lineatlas_width:e.lineAtlas.width,u_lineatlas_height:e.lineAtlas.height})};function Cl(e,t){return 1/Ee(e,1,t.tileZoom)}function wl(e,t,n){return je(e.transform,t,n.paint.get(`line-translate`),n.paint.get(`line-translate-anchor`))}const Tl=(e,t)=>({u_image:new P(e,t.u_image),u_opacity:new H(e,t.u_opacity)}),El=(e,t)=>({u_image:t,u_opacity:e}),Dl=(e,t)=>({u_tl_parent:new h(e,t.u_tl_parent),u_scale_parent:new H(e,t.u_scale_parent),u_buffer_scale:new H(e,t.u_buffer_scale),u_fade_t:new H(e,t.u_fade_t),u_opacity:new H(e,t.u_opacity),u_image0:new P(e,t.u_image0),u_image1:new P(e,t.u_image1),u_brightness_low:new H(e,t.u_brightness_low),u_brightness_high:new H(e,t.u_brightness_high),u_saturation_factor:new H(e,t.u_saturation_factor),u_contrast_factor:new H(e,t.u_contrast_factor),u_spin_weights:new or(e,t.u_spin_weights),u_coords_top:new Ye(e,t.u_coords_top),u_coords_bottom:new Ye(e,t.u_coords_bottom)}),Ol=(e,t,n,r,i)=>({u_tl_parent:e,u_scale_parent:t,u_buffer_scale:1,u_fade_t:n.mix,u_opacity:n.opacity*r.paint.get(`raster-opacity`),u_image0:0,u_image1:1,u_brightness_low:r.paint.get(`raster-brightness-min`),u_brightness_high:r.paint.get(`raster-brightness-max`),u_saturation_factor:jl(r.paint.get(`raster-saturation`)),u_contrast_factor:Al(r.paint.get(`raster-contrast`)),u_spin_weights:kl(r.paint.get(`raster-hue-rotate`)),u_coords_top:[i[0].x,i[0].y,i[1].x,i[1].y],u_coords_bottom:[i[3].x,i[3].y,i[2].x,i[2].y]});function kl(e){e*=Math.PI/180;let t=Math.sin(e),n=Math.cos(e);return[(2*n+1)/3,(-Math.sqrt(3)*t-n+1)/3,(Math.sqrt(3)*t-n+1)/3]}function Al(e){return e>0?1/(1-e):1+e}function jl(e){return e>0?1-1/(1.001-e):-e}const Ml=(e,t)=>({u_is_size_zoom_constant:new P(e,t.u_is_size_zoom_constant),u_is_size_feature_constant:new P(e,t.u_is_size_feature_constant),u_size_t:new H(e,t.u_size_t),u_size:new H(e,t.u_size),u_camera_to_center_distance:new H(e,t.u_camera_to_center_distance),u_pitch:new H(e,t.u_pitch),u_rotate_symbol:new P(e,t.u_rotate_symbol),u_aspect_ratio:new H(e,t.u_aspect_ratio),u_fade_change:new H(e,t.u_fade_change),u_label_plane_matrix:new fn(e,t.u_label_plane_matrix),u_coord_matrix:new fn(e,t.u_coord_matrix),u_is_text:new P(e,t.u_is_text),u_pitch_with_map:new P(e,t.u_pitch_with_map),u_is_along_line:new P(e,t.u_is_along_line),u_is_variable_anchor:new P(e,t.u_is_variable_anchor),u_texsize:new h(e,t.u_texsize),u_texture:new P(e,t.u_texture),u_translation:new h(e,t.u_translation),u_pitched_scale:new H(e,t.u_pitched_scale),u_is_offset:new P(e,t.u_is_offset)}),Nl=(e,t)=>({u_is_size_zoom_constant:new P(e,t.u_is_size_zoom_constant),u_is_size_feature_constant:new P(e,t.u_is_size_feature_constant),u_size_t:new H(e,t.u_size_t),u_size:new H(e,t.u_size),u_camera_to_center_distance:new H(e,t.u_camera_to_center_distance),u_pitch:new H(e,t.u_pitch),u_rotate_symbol:new P(e,t.u_rotate_symbol),u_aspect_ratio:new H(e,t.u_aspect_ratio),u_fade_change:new H(e,t.u_fade_change),u_label_plane_matrix:new fn(e,t.u_label_plane_matrix),u_coord_matrix:new fn(e,t.u_coord_matrix),u_is_text:new P(e,t.u_is_text),u_pitch_with_map:new P(e,t.u_pitch_with_map),u_is_along_line:new P(e,t.u_is_along_line),u_is_variable_anchor:new P(e,t.u_is_variable_anchor),u_texsize:new h(e,t.u_texsize),u_texture:new P(e,t.u_texture),u_gamma_scale:new H(e,t.u_gamma_scale),u_device_pixel_ratio:new H(e,t.u_device_pixel_ratio),u_is_halo:new P(e,t.u_is_halo),u_is_plain:new P(e,t.u_is_plain),u_translation:new h(e,t.u_translation),u_pitched_scale:new H(e,t.u_pitched_scale),u_is_offset:new P(e,t.u_is_offset)}),Pl=(e,t)=>({u_is_size_zoom_constant:new P(e,t.u_is_size_zoom_constant),u_is_size_feature_constant:new P(e,t.u_is_size_feature_constant),u_size_t:new H(e,t.u_size_t),u_size:new H(e,t.u_size),u_camera_to_center_distance:new H(e,t.u_camera_to_center_distance),u_pitch:new H(e,t.u_pitch),u_rotate_symbol:new P(e,t.u_rotate_symbol),u_aspect_ratio:new H(e,t.u_aspect_ratio),u_fade_change:new H(e,t.u_fade_change),u_label_plane_matrix:new fn(e,t.u_label_plane_matrix),u_coord_matrix:new fn(e,t.u_coord_matrix),u_is_text:new P(e,t.u_is_text),u_pitch_with_map:new P(e,t.u_pitch_with_map),u_is_along_line:new P(e,t.u_is_along_line),u_is_variable_anchor:new P(e,t.u_is_variable_anchor),u_texsize:new h(e,t.u_texsize),u_texsize_icon:new h(e,t.u_texsize_icon),u_texture:new P(e,t.u_texture),u_texture_icon:new P(e,t.u_texture_icon),u_gamma_scale:new H(e,t.u_gamma_scale),u_device_pixel_ratio:new H(e,t.u_device_pixel_ratio),u_is_halo:new P(e,t.u_is_halo),u_translation:new h(e,t.u_translation),u_pitched_scale:new H(e,t.u_pitched_scale),u_is_offset:new P(e,t.u_is_offset)}),Fl=(e,t,n,r,i,a,o,s,c,l,u,d,f,p)=>{let m=o.transform;return{u_is_size_zoom_constant:+(e===`constant`||e===`source`),u_is_size_feature_constant:+(e===`constant`||e===`camera`),u_size_t:t?t.uSizeT:0,u_size:t?t.uSize:0,u_camera_to_center_distance:m.cameraToCenterDistance,u_pitch:m.pitch/360*2*Math.PI,u_rotate_symbol:+n,u_aspect_ratio:m.width/m.height,u_fade_change:o.options.fadeDuration?o.symbolFadeChange:1,u_label_plane_matrix:s,u_coord_matrix:c,u_is_text:+u,u_pitch_with_map:+r,u_is_along_line:i,u_is_variable_anchor:a,u_texsize:d,u_texture:0,u_translation:l,u_pitched_scale:f,u_is_offset:p}},Il=(e,t,n,r,i,a,o,s,c,l,u,d,f,p,m)=>{let h=o.transform;return L(Fl(e,t,n,r,i,a,o,s,c,l,u,d,p,m),{u_gamma_scale:r?Math.cos(h.pitch*Math.PI/180)*h.cameraToCenterDistance:1,u_device_pixel_ratio:o.pixelRatio,u_is_halo:+!!f,u_is_plain:1})},Ll=(e,t,n,r,i,a,o,s,c,l,u,d,f,p)=>L(Il(e,t,n,r,i,a,o,s,c,l,!0,u,!0,f,p),{u_texsize_icon:d,u_texture_icon:1}),Rl=(e,t)=>({u_opacity:new H(e,t.u_opacity),u_color:new T(e,t.u_color)}),zl=(e,t)=>({u_opacity:new H(e,t.u_opacity),u_image:new P(e,t.u_image),u_pattern_tl_a:new h(e,t.u_pattern_tl_a),u_pattern_br_a:new h(e,t.u_pattern_br_a),u_pattern_tl_b:new h(e,t.u_pattern_tl_b),u_pattern_br_b:new h(e,t.u_pattern_br_b),u_texsize:new h(e,t.u_texsize),u_mix:new H(e,t.u_mix),u_pattern_size_a:new h(e,t.u_pattern_size_a),u_pattern_size_b:new h(e,t.u_pattern_size_b),u_scale_a:new H(e,t.u_scale_a),u_scale_b:new H(e,t.u_scale_b),u_pixel_coord_upper:new h(e,t.u_pixel_coord_upper),u_pixel_coord_lower:new h(e,t.u_pixel_coord_lower),u_tile_units_to_pixels:new H(e,t.u_tile_units_to_pixels)}),Bl=(e,t)=>({u_opacity:e,u_color:t}),Vl=(e,t,n,r,i)=>L(Fc(n,i,t,r),{u_opacity:e}),Hl=(e,t)=>({u_sun_pos:new or(e,t.u_sun_pos),u_atmosphere_blend:new H(e,t.u_atmosphere_blend),u_globe_position:new or(e,t.u_globe_position),u_globe_radius:new H(e,t.u_globe_radius),u_inv_proj_matrix:new fn(e,t.u_inv_proj_matrix)}),Ul=(e,t,n,r,i)=>({u_sun_pos:e,u_atmosphere_blend:t,u_globe_position:n,u_globe_radius:r,u_inv_proj_matrix:i}),Wl=(e,t)=>({u_sky_color:new T(e,t.u_sky_color),u_horizon_color:new T(e,t.u_horizon_color),u_horizon:new h(e,t.u_horizon),u_horizon_normal:new h(e,t.u_horizon_normal),u_sky_horizon_blend:new H(e,t.u_sky_horizon_blend),u_sky_blend:new H(e,t.u_sky_blend)}),Gl=(e,t,n)=>{let r=Math.cos(t.rollInRadians),i=Math.sin(t.rollInRadians),a=Ea(t),o=t.getProjectionData({overscaledTileID:null,applyGlobeMatrix:!0,applyTerrainMatrix:!0}).projectionTransition;return{u_sky_color:e.properties.get(`sky-color`),u_horizon_color:e.properties.get(`horizon-color`),u_horizon:[(t.width/2-a*i)*n,(t.height/2+a*r)*n],u_horizon_normal:[-i,r],u_sky_horizon_blend:e.properties.get(`sky-horizon-blend`)*t.height/2*n,u_sky_blend:o}},Kl=(e,t)=>{},ql={fillExtrusion:Ic,fillExtrusionPattern:Lc,fill:Bc,fillPattern:Vc,fillOutline:Hc,fillOutlinePattern:Uc,circle:Jc,collisionBox:Xc,collisionCircle:Zc,debug:el,depth:Kl,clippingMask:Kl,heatmap:nl,heatmapTexture:rl,hillshade:ol,hillshadePrepare:sl,colorRelief:dl,line:pl,lineGradient:ml,linePattern:hl,lineSDF:gl,lineGradientSDF:_l,layerOpacity:Tl,raster:Dl,symbolIcon:Ml,symbolSDF:Nl,symbolTextAndIcon:Pl,background:Rl,backgroundPattern:zl,terrain:wc,terrainDepth:Tc,terrainCoords:Ec,projectionErrorMeasurement:Is,atmosphere:Hl,sky:Wl};var Jl=class{constructor(e,t,n){this.context=e;let r=e.gl;this.buffer=r.createBuffer(),this.dynamicDraw=!!n,this.context.unbindVAO(),e.bindElementBuffer.set(this.buffer),r.bufferData(r.ELEMENT_ARRAY_BUFFER,t.arrayBuffer,this.dynamicDraw?r.DYNAMIC_DRAW:r.STATIC_DRAW),this.dynamicDraw||t.freeBufferAfterUpload()}bind(){this.context.bindElementBuffer.set(this.buffer)}updateData(e){let t=this.context.gl;if(!this.dynamicDraw)throw Error(`Attempted to update data while not in dynamic mode.`);this.context.unbindVAO(),this.bind(),t.bufferSubData(t.ELEMENT_ARRAY_BUFFER,0,e.arrayBuffer)}destroy(){let e=this.context.gl;this.buffer&&(e.deleteBuffer(this.buffer),delete this.buffer)}};const Yl={Int8:`BYTE`,Uint8:`UNSIGNED_BYTE`,Int16:`SHORT`,Uint16:`UNSIGNED_SHORT`,Int32:`INT`,Uint32:`UNSIGNED_INT`,Float32:`FLOAT`};var Xl=class{constructor(e,t,n,r){this.length=t.length,this.attributes=n,this.itemSize=t.bytesPerElement,this.dynamicDraw=r,this.context=e;let i=e.gl;this.buffer=i.createBuffer(),e.bindVertexBuffer.set(this.buffer),i.bufferData(i.ARRAY_BUFFER,t.arrayBuffer,this.dynamicDraw?i.DYNAMIC_DRAW:i.STATIC_DRAW),this.dynamicDraw||t.freeBufferAfterUpload()}bind(){this.context.bindVertexBuffer.set(this.buffer)}updateData(e){if(e.length!==this.length)throw Error(`Length of new data is ${e.length}, which doesn't match current length of ${this.length}`);let t=this.context.gl;this.bind(),t.bufferSubData(t.ARRAY_BUFFER,0,e.arrayBuffer)}enableAttributes(e,t){for(let n of this.attributes){let r=t.attributes[n.name];r!==void 0&&e.enableVertexAttribArray(r)}}setVertexAttribPointers(e,t,n){for(let r of this.attributes){let i=t.attributes[r.name];i!==void 0&&e.vertexAttribPointer(i,r.components,e[Yl[r.type]],!1,this.itemSize,r.offset+this.itemSize*(n||0))}}destroy(){let e=this.context.gl;this.buffer&&(e.deleteBuffer(this.buffer),delete this.buffer)}},$=class{constructor(e){this.gl=e.gl,this.default=this.getDefault(),this.current=this.default,this.dirty=!1}get(){return this.current}set(e){}getDefault(){return this.default}setDefault(){this.set(this.default)}},Zl=class extends ${getDefault(){return V.transparent}set(e){let t=this.current;e.r===t.r&&e.g===t.g&&e.b===t.b&&e.a===t.a&&!this.dirty||(this.gl.clearColor(e.r,e.g,e.b,e.a),this.current=e,this.dirty=!1)}},Ql=class extends ${getDefault(){return 1}set(e){e===this.current&&!this.dirty||(this.gl.clearDepth(e),this.current=e,this.dirty=!1)}},$l=class extends ${getDefault(){return 0}set(e){e===this.current&&!this.dirty||(this.gl.clearStencil(e),this.current=e,this.dirty=!1)}},eu=class extends ${getDefault(){return[!0,!0,!0,!0]}set(e){let t=this.current;e[0]===t[0]&&e[1]===t[1]&&e[2]===t[2]&&e[3]===t[3]&&!this.dirty||(this.gl.colorMask(e[0],e[1],e[2],e[3]),this.current=e,this.dirty=!1)}},tu=class extends ${getDefault(){return!0}set(e){e===this.current&&!this.dirty||(this.gl.depthMask(e),this.current=e,this.dirty=!1)}},nu=class extends ${getDefault(){return 255}set(e){e===this.current&&!this.dirty||(this.gl.stencilMask(e),this.current=e,this.dirty=!1)}},ru=class extends ${getDefault(){return{func:this.gl.ALWAYS,ref:0,mask:255}}set(e){let t=this.current;e.func===t.func&&e.ref===t.ref&&e.mask===t.mask&&!this.dirty||(this.gl.stencilFunc(e.func,e.ref,e.mask),this.current=e,this.dirty=!1)}},iu=class extends ${getDefault(){let e=this.gl;return[e.KEEP,e.KEEP,e.KEEP]}set(e){let t=this.current;e[0]===t[0]&&e[1]===t[1]&&e[2]===t[2]&&!this.dirty||(this.gl.stencilOp(e[0],e[1],e[2]),this.current=e,this.dirty=!1)}},au=class extends ${getDefault(){return!1}set(e){if(e===this.current&&!this.dirty)return;let t=this.gl;e?t.enable(t.STENCIL_TEST):t.disable(t.STENCIL_TEST),this.current=e,this.dirty=!1}},ou=class extends ${getDefault(){return[0,1]}set(e){let t=this.current;e[0]===t[0]&&e[1]===t[1]&&!this.dirty||(this.gl.depthRange(e[0],e[1]),this.current=e,this.dirty=!1)}},su=class extends ${getDefault(){return!1}set(e){if(e===this.current&&!this.dirty)return;let t=this.gl;e?t.enable(t.DEPTH_TEST):t.disable(t.DEPTH_TEST),this.current=e,this.dirty=!1}},cu=class extends ${getDefault(){return this.gl.LESS}set(e){e===this.current&&!this.dirty||(this.gl.depthFunc(e),this.current=e,this.dirty=!1)}},lu=class extends ${getDefault(){return!1}set(e){if(e===this.current&&!this.dirty)return;let t=this.gl;e?t.enable(t.BLEND):t.disable(t.BLEND),this.current=e,this.dirty=!1}},uu=class extends ${getDefault(){let e=this.gl;return[e.ONE,e.ZERO]}set(e){let t=this.current;e[0]===t[0]&&e[1]===t[1]&&!this.dirty||(this.gl.blendFunc(e[0],e[1]),this.current=e,this.dirty=!1)}},du=class extends ${getDefault(){return V.transparent}set(e){let t=this.current;e.r===t.r&&e.g===t.g&&e.b===t.b&&e.a===t.a&&!this.dirty||(this.gl.blendColor(e.r,e.g,e.b,e.a),this.current=e,this.dirty=!1)}},fu=class extends ${getDefault(){return this.gl.FUNC_ADD}set(e){e===this.current&&!this.dirty||(this.gl.blendEquation(e),this.current=e,this.dirty=!1)}},pu=class extends ${getDefault(){return!1}set(e){if(e===this.current&&!this.dirty)return;let t=this.gl;e?t.enable(t.CULL_FACE):t.disable(t.CULL_FACE),this.current=e,this.dirty=!1}},mu=class extends ${getDefault(){return this.gl.BACK}set(e){e===this.current&&!this.dirty||(this.gl.cullFace(e),this.current=e,this.dirty=!1)}},hu=class extends ${getDefault(){return this.gl.CCW}set(e){e===this.current&&!this.dirty||(this.gl.frontFace(e),this.current=e,this.dirty=!1)}},gu=class extends ${getDefault(){return null}set(e){e===this.current&&!this.dirty||(this.gl.useProgram(e),this.current=e,this.dirty=!1)}},_u=class extends ${getDefault(){return this.gl.TEXTURE0}set(e){e===this.current&&!this.dirty||(this.gl.activeTexture(e),this.current=e,this.dirty=!1)}},vu=class extends ${getDefault(){let e=this.gl;return[0,0,e.drawingBufferWidth,e.drawingBufferHeight]}set(e){let t=this.current;e[0]===t[0]&&e[1]===t[1]&&e[2]===t[2]&&e[3]===t[3]&&!this.dirty||(this.gl.viewport(e[0],e[1],e[2],e[3]),this.current=e,this.dirty=!1)}},yu=class extends ${getDefault(){return null}set(e){if(e===this.current&&!this.dirty)return;let t=this.gl;t.bindFramebuffer(t.FRAMEBUFFER,e),this.current=e,this.dirty=!1}},bu=class extends ${getDefault(){return null}set(e){if(e===this.current&&!this.dirty)return;let t=this.gl;t.bindRenderbuffer(t.RENDERBUFFER,e),this.current=e,this.dirty=!1}},xu=class extends ${getDefault(){return null}set(e){if(e===this.current&&!this.dirty)return;let t=this.gl;t.bindTexture(t.TEXTURE_2D,e),this.current=e,this.dirty=!1}},Su=class extends ${getDefault(){return null}set(e){if(e===this.current&&!this.dirty)return;let t=this.gl;t.bindBuffer(t.ARRAY_BUFFER,e),this.current=e,this.dirty=!1}},Cu=class extends ${getDefault(){return null}set(e){let t=this.gl;t.bindBuffer(t.ELEMENT_ARRAY_BUFFER,e),this.current=e,this.dirty=!1}},wu=class extends ${getDefault(){return null}set(e){e===this.current&&!this.dirty||(this.gl.bindVertexArray(e),this.current=e,this.dirty=!1)}},Tu=class extends ${getDefault(){return 4}set(e){if(e===this.current&&!this.dirty)return;let t=this.gl;t.pixelStorei(t.UNPACK_ALIGNMENT,e),this.current=e,this.dirty=!1}},Eu=class extends ${getDefault(){return!1}set(e){if(e===this.current&&!this.dirty)return;let t=this.gl;t.pixelStorei(t.UNPACK_PREMULTIPLY_ALPHA_WEBGL,e),this.current=e,this.dirty=!1}},Du=class extends ${getDefault(){return!1}set(e){if(e===this.current&&!this.dirty)return;let t=this.gl;t.pixelStorei(t.UNPACK_FLIP_Y_WEBGL,e),this.current=e,this.dirty=!1}},Ou=class extends ${constructor(e,t){super(e),this.context=e,this.parent=t}getDefault(){return null}},ku=class extends Ou{setDirty(){this.dirty=!0}set(e){if(e===this.current&&!this.dirty)return;this.context.bindFramebuffer.set(this.parent);let t=this.gl;t.framebufferTexture2D(t.FRAMEBUFFER,t.COLOR_ATTACHMENT0,t.TEXTURE_2D,e,0),this.current=e,this.dirty=!1}},Au=class extends Ou{set(e){if(e===this.current&&!this.dirty)return;this.context.bindFramebuffer.set(this.parent);let t=this.gl;t.framebufferRenderbuffer(t.FRAMEBUFFER,t.DEPTH_ATTACHMENT,t.RENDERBUFFER,e),this.current=e,this.dirty=!1}},ju=class extends Ou{set(e){if(e===this.current&&!this.dirty)return;this.context.bindFramebuffer.set(this.parent);let t=this.gl;t.framebufferRenderbuffer(t.FRAMEBUFFER,t.DEPTH_STENCIL_ATTACHMENT,t.RENDERBUFFER,e),this.current=e,this.dirty=!1}},Mu=class{constructor(e,t,n,r,i){this.context=e,this.width=t,this.height=n;let a=e.gl,o=this.framebuffer=a.createFramebuffer();if(this.colorAttachment=new ku(e,o),r)this.depthAttachment=i?new ju(e,o):new Au(e,o);else if(i)throw Error(`Stencil cannot be set without depth`)}destroy(){let e=this.context.gl,t=this.colorAttachment.get();if(t&&e.deleteTexture(t),this.depthAttachment){let t=this.depthAttachment.get();t&&e.deleteRenderbuffer(t)}e.deleteFramebuffer(this.framebuffer)}},Nu=class{constructor(e){this.gl=e,this.clearColor=new Zl(this),this.clearDepth=new Ql(this),this.clearStencil=new $l(this),this.colorMask=new eu(this),this.depthMask=new tu(this),this.stencilMask=new nu(this),this.stencilFunc=new ru(this),this.stencilOp=new iu(this),this.stencilTest=new au(this),this.depthRange=new ou(this),this.depthTest=new su(this),this.depthFunc=new cu(this),this.blend=new lu(this),this.blendFunc=new uu(this),this.blendColor=new du(this),this.blendEquation=new fu(this),this.cullFace=new pu(this),this.cullFaceSide=new mu(this),this.frontFace=new hu(this),this.program=new gu(this),this.activeTexture=new _u(this),this.viewport=new vu(this),this.bindFramebuffer=new yu(this),this.bindRenderbuffer=new bu(this),this.bindTexture=new xu(this),this.bindVertexBuffer=new Su(this),this.bindElementBuffer=new Cu(this),this.bindVertexArray=new wu(this),this.pixelStoreUnpack=new Tu(this),this.pixelStoreUnpackPremultiplyAlpha=new Eu(this),this.pixelStoreUnpackFlipY=new Du(this),this.extTextureFilterAnisotropic=e.getExtension(`EXT_texture_filter_anisotropic`),this.extTextureFilterAnisotropic&&(this.extTextureFilterAnisotropicMax=e.getParameter(this.extTextureFilterAnisotropic.MAX_TEXTURE_MAX_ANISOTROPY_EXT)),this.maxTextureSize=e.getParameter(e.MAX_TEXTURE_SIZE),e.getExtension(`EXT_color_buffer_half_float`),e.getExtension(`EXT_color_buffer_float`)}setDefault(){this.unbindVAO(),this.clearColor.setDefault(),this.clearDepth.setDefault(),this.clearStencil.setDefault(),this.colorMask.setDefault(),this.depthMask.setDefault(),this.stencilMask.setDefault(),this.stencilFunc.setDefault(),this.stencilOp.setDefault(),this.stencilTest.setDefault(),this.depthRange.setDefault(),this.depthTest.setDefault(),this.depthFunc.setDefault(),this.blend.setDefault(),this.blendFunc.setDefault(),this.blendColor.setDefault(),this.blendEquation.setDefault(),this.cullFace.setDefault(),this.cullFaceSide.setDefault(),this.frontFace.setDefault(),this.program.setDefault(),this.activeTexture.setDefault(),this.bindFramebuffer.setDefault(),this.pixelStoreUnpack.setDefault(),this.pixelStoreUnpackPremultiplyAlpha.setDefault(),this.pixelStoreUnpackFlipY.setDefault()}setDirty(){this.clearColor.dirty=!0,this.clearDepth.dirty=!0,this.clearStencil.dirty=!0,this.colorMask.dirty=!0,this.depthMask.dirty=!0,this.stencilMask.dirty=!0,this.stencilFunc.dirty=!0,this.stencilOp.dirty=!0,this.stencilTest.dirty=!0,this.depthRange.dirty=!0,this.depthTest.dirty=!0,this.depthFunc.dirty=!0,this.blend.dirty=!0,this.blendFunc.dirty=!0,this.blendColor.dirty=!0,this.blendEquation.dirty=!0,this.cullFace.dirty=!0,this.cullFaceSide.dirty=!0,this.frontFace.dirty=!0,this.program.dirty=!0,this.activeTexture.dirty=!0,this.viewport.dirty=!0,this.bindFramebuffer.dirty=!0,this.bindRenderbuffer.dirty=!0,this.bindTexture.dirty=!0,this.bindVertexBuffer.dirty=!0,this.bindElementBuffer.dirty=!0,this.bindVertexArray.dirty=!0,this.pixelStoreUnpack.dirty=!0,this.pixelStoreUnpackPremultiplyAlpha.dirty=!0,this.pixelStoreUnpackFlipY.dirty=!0}createIndexBuffer(e,t){return new Jl(this,e,t)}createVertexBuffer(e,t,n){return new Xl(this,e,t,n)}createRenderbuffer(e,t,n){let r=this.gl,i=r.createRenderbuffer();return this.bindRenderbuffer.set(i),r.renderbufferStorage(r.RENDERBUFFER,e,t,n),this.bindRenderbuffer.set(null),i}createFramebuffer(e,t,n,r){return new Mu(this,e,t,n,r)}clear({color:e,depth:t,stencil:n}){let r=this.gl,i=0;e&&(i|=r.COLOR_BUFFER_BIT,this.clearColor.set(e),this.colorMask.set([!0,!0,!0,!0])),t!==void 0&&(i|=r.DEPTH_BUFFER_BIT,this.depthRange.set([0,1]),this.clearDepth.set(t),this.depthMask.set(!0)),n!==void 0&&(i|=r.STENCIL_BUFFER_BIT,this.clearStencil.set(n),this.stencilMask.set(255)),r.clear(i)}setCullFace(e){e.enable===!1?this.cullFace.set(!1):(this.cullFace.set(!0),this.cullFaceSide.set(e.mode),this.frontFace.set(e.frontFace))}setDepthMode(e){e.func===this.gl.ALWAYS&&!e.mask?this.depthTest.set(!1):(this.depthTest.set(!0),this.depthFunc.set(e.func),this.depthMask.set(e.mask),this.depthRange.set(e.range))}setStencilMode(e){e.test.func===this.gl.ALWAYS&&!e.mask?this.stencilTest.set(!1):(this.stencilTest.set(!0),this.stencilMask.set(e.mask),this.stencilOp.set([e.fail,e.depthFail,e.pass]),this.stencilFunc.set({func:e.test.func,ref:e.ref,mask:e.test.mask}))}setColorMode(e){Ze(e.blendFunction,Y.Replace)?this.blend.set(!1):(this.blend.set(!0),this.blendFunc.set(e.blendFunction),this.blendColor.set(e.blendColor)),this.colorMask.set(e.mask)}createVertexArray(){return this.gl.createVertexArray()}deleteVertexArray(e){this.gl.deleteVertexArray(e)}unbindVAO(){this.bindVertexArray.set(null)}};let Pu;function Fu(e,t,n,r,i){let a=e.context,s=e.transform,c=a.gl,l=e.useProgram(`collisionBox`),u=[],d=0,f=0;for(let o of r){let r=t.getTile(o).getBucket(n);if(!r)continue;let p=i?r.textCollisionBox:r.iconCollisionBox,m=r.collisionCircleArray;m.length>0&&(u.push({circleArray:m,circleOffset:f,coord:o}),d+=m.length/4,f=d),p&&l.draw(a,c.LINES,Z.disabled,Q.disabled,e.colorModeForRenderPass(),X.disabled,Qc(e.transform),e.style.map.terrain?.getTerrainData(o),s.getProjectionData({overscaledTileID:o,applyGlobeMatrix:!0,applyTerrainMatrix:!0}),n.id,p.layoutVertexBuffer,p.indexBuffer,p.segments,null,e.transform.zoom,null,null,p.collisionVertexBuffer)}if(!i||!u.length)return;let p=e.useProgram(`collisionCircle`),m=new Ce;m.resize(d*4),m._trim();let h=0;for(let e of u)for(let t=0;tu.getElevation(i,e,t):null;Hu(a,d,f,c,l,g,t,m,_,je(l,e,o,s),i.toUnwrapped(),n)}}}function Vu(e,t,n,r,i,a){let o=t.tileAnchorPoint.add(new z(t.translation[0],t.translation[1]));if(t.pitchWithMap){let e=r.mult(a);n||(e=e.rotate(-i));let s=o.add(e);return io(s.x,s.y,t.pitchedLabelPlaneMatrix,t.getElevation).point}else if(n){let n=mo(t.tileAnchorPoint.x+1,t.tileAnchorPoint.y,t).point.sub(e),i=Math.atan(n.y/n.x)+(n.x<0?Math.PI:0);return e.add(r.rotate(i))}else return e.add(r)}function Hu(e,t,n,r,i,a,o,c,l,u,d,f){let p=e.text.placedSymbolArray,m=e.text.dynamicLayoutVertexArray,h=e.icon.dynamicLayoutVertexArray,g={};m.clear();for(let h=0;h=0&&(g[_.associatedIconIndex]={shiftedAnchor:D,angle:O})}}if(l){h.clear();let t=e.icon.placedSymbolArray;for(let e=0;ee.style.map.terrain.getElevation(c,t,n):null;so(l,e,i,j,t,v,u,n.layout.get(`text-rotation-alignment`)===`map`,c.toUnwrapped(),g.width,g.height,ce,r)}let pe=i&&w||de,me=v?j:e.transform.clipSpaceToPixelsMatrix,he=y||pe?Lu:me,ge=m&&n.paint.get(i?`text-halo-width`:`icon-halo-width`).constantOr(1)!==0,_e;_e=m?l.iconsInText?Ll(C.kind,O,b,v,y,pe,e,he,se,ce,k,A,ee,fe):Il(C.kind,O,b,v,y,pe,e,he,se,ce,i,k,ge,ee,fe):Fl(C.kind,O,b,v,y,pe,e,he,se,ce,i,k,ee,fe);let ve={program:D,buffers:d,uniformValues:_e,projectionData:le,atlasTexture:ne,atlasTextureIcon:ie,atlasInterpolation:re,atlasInterpolationIcon:ae,isSDF:m,hasHalo:ge};if(x&&l.canOverlap){S=!0;let e=d.segments.get();for(let t of e)T.push({segments:new o([t]),sortKey:t.sortKey,state:ve,terrainData:te})}else T.push({segments:d.segments,sortKey:0,state:ve,terrainData:te})}S&&T.sort((e,t)=>e.sortKey-t.sortKey);let E=n.paint.get(i?`text-halo-width`:`icon-halo-width`).constantOr(null)??1/0,D=n.layout.get(`text-letter-spacing`).constantOr(0)*24<0||E>1;for(let t of T){let r=t.state;m.activeTexture.set(h.TEXTURE0),r.atlasTexture.bind(r.atlasInterpolation,h.CLAMP_TO_EDGE),r.atlasTextureIcon&&(m.activeTexture.set(h.TEXTURE1),r.atlasTextureIcon&&r.atlasTextureIcon.bind(r.atlasInterpolationIcon,h.CLAMP_TO_EDGE));let i=r.isSDF&&r.hasHalo;if(i){let i=r.uniformValues;i.u_is_halo=1,D&&(i.u_is_plain=0,Gu(r.buffers,t.segments,n,e,r.program,C,d,f,i,r.projectionData,t.terrainData),i.u_is_halo=0,i.u_is_plain=1)}Gu(r.buffers,t.segments,n,e,r.program,C,d,f,r.uniformValues,r.projectionData,t.terrainData),i&&!D&&(r.uniformValues.u_is_halo=0)}}function Gu(e,t,n,r,i,a,o,s,c,l,u){let d=r.context,f=d.gl;i.draw(d,f.TRIANGLES,a,o,s,X.backCCW,c,u,l,n.id,e.layoutVertexBuffer,e.indexBuffer,t,n.paint,r.transform.zoom,e.programConfigurations.get(n.id),e.dynamicLayoutVertexBuffer,e.opacityVertexBuffer)}function Ku(e,t,n,r,i){if(e.renderPass!==`translucent`)return;let{isRenderingToTexture:a}=i,s=n.paint.get(`circle-opacity`),c=n.paint.get(`circle-stroke-width`),l=n.paint.get(`circle-stroke-opacity`),u=!n.layout.get(`circle-sort-key`).isConstant();if(s.constantOr(1)===0&&(c.constantOr(1)===0||l.constantOr(1)===0))return;let d=e.context,f=d.gl,p=e.transform,m=e.getDepthModeForSublayer(0,Z.ReadOnly),h=Q.disabled,g=e.colorModeForRenderPass(),_=[],v=p.getCircleRadiusCorrection();for(let i of r){let r=t.getTile(i),s=r.getBucket(n);if(!s)continue;let c=je(p,r,n.paint.get(`circle-translate`),n.paint.get(`circle-translate-anchor`)),l=s.programConfigurations.get(n.id),d=e.useProgram(`circle`,l),f=s.layoutVertexBuffer,m=s.indexBuffer,h=e.style.map.terrain?.getTerrainData(i),g={programConfiguration:l,program:d,layoutVertexBuffer:f,indexBuffer:m,uniformValues:Yc(e,r,n,c,v),terrainData:h,projectionData:p.getProjectionData({overscaledTileID:i,applyGlobeMatrix:!a,applyTerrainMatrix:!0})};if(u){let e=s.segments.get();for(let t of e)_.push({segments:new o([t]),sortKey:t.sortKey,state:g})}else _.push({segments:s.segments,sortKey:0,state:g})}u&&_.sort((e,t)=>e.sortKey-t.sortKey);for(let t of _){let{programConfiguration:r,program:i,layoutVertexBuffer:a,indexBuffer:o,uniformValues:s,terrainData:c,projectionData:l}=t.state,u=t.segments;i.draw(d,f.TRIANGLES,m,h,g,X.backCCW,s,c,l,n.id,a,o,u,n.paint,e.transform.zoom,r)}}function qu(e,t,n,r,i){if(n.paint.get(`heatmap-opacity`)===0)return;let a=e.context,{isRenderingToTexture:o,isRenderingGlobe:s}=i;if(e.style.map.terrain){for(let i of r){let r=t.getTile(i);t.hasRenderableParent(i)||(e.renderPass===`offscreen`?Xu(e,r,n,i,s):e.renderPass===`translucent`&&Zu(e,n,i,o,s))}a.viewport.set([0,0,e.width,e.height])}else e.renderPass===`offscreen`?Ju(e,t,n,r):e.renderPass===`translucent`&&Yu(e,n)}function Ju(e,t,n,r){let i=e.context,a=i.gl,o=e.transform,s=Q.disabled,c=new Y([a.ONE,a.ONE],V.transparent,[!0,!0,!0,!0]);Qu(i,e,n),i.clear({color:V.transparent});for(let l of r){if(t.hasRenderableParent(l))continue;let r=t.getTile(l),u=r.getBucket(n);if(!u)continue;let d=u.programConfigurations.get(n.id),f=e.useProgram(`heatmap`,d),p=o.getProjectionData({overscaledTileID:l,applyGlobeMatrix:!0,applyTerrainMatrix:!1}),m=o.getCircleRadiusCorrection();f.draw(i,a.TRIANGLES,Z.disabled,s,c,X.backCCW,il(r,o.zoom,n.paint.get(`heatmap-intensity`),m),null,p,n.id,u.layoutVertexBuffer,u.indexBuffer,u.segments,n.paint,o.zoom,d)}i.viewport.set([0,0,e.width,e.height])}function Yu(e,t){let n=e.context,r=n.gl;n.setColorMode(e.colorModeForRenderPass());let i=t.heatmapFbos.get(An);i&&(n.activeTexture.set(r.TEXTURE0),r.bindTexture(r.TEXTURE_2D,i.colorAttachment.get()),n.activeTexture.set(r.TEXTURE1),ed(n,t).bind(r.LINEAR,r.CLAMP_TO_EDGE),e.useProgram(`heatmapTexture`).draw(n,r.TRIANGLES,Z.disabled,Q.disabled,e.colorModeForRenderPass(),X.disabled,al(e,t,0,1),null,null,t.id,e.viewportBuffer,e.quadTriangleIndexBuffer,e.viewportSegments,t.paint,e.transform.zoom))}function Xu(e,t,n,r,i){let a=e.context,o=a.gl,s=Q.disabled,c=new Y([o.ONE,o.ONE],V.transparent,[!0,!0,!0,!0]),l=t.getBucket(n);if(!l)return;let u=r.key,d=n.heatmapFbos.get(u);d||(d=$u(a,t.tileSize,t.tileSize),n.heatmapFbos.set(u,d)),a.bindFramebuffer.set(d.framebuffer),a.viewport.set([0,0,t.tileSize,t.tileSize]),a.clear({color:V.transparent});let f=l.programConfigurations.get(n.id),p=e.useProgram(`heatmap`,f,!i),m=e.transform.getProjectionData({overscaledTileID:t.tileID,applyGlobeMatrix:!0,applyTerrainMatrix:!0}),h=e.style.map.terrain.getTerrainData(r);p.draw(a,o.TRIANGLES,Z.disabled,s,c,X.disabled,il(t,e.transform.zoom,n.paint.get(`heatmap-intensity`),1),h,m,n.id,l.layoutVertexBuffer,l.indexBuffer,l.segments,n.paint,e.transform.zoom,f)}function Zu(e,t,n,r,i){let a=e.context,o=a.gl,s=e.transform;a.setColorMode(e.colorModeForRenderPass());let c=ed(a,t),l=n.key,u=t.heatmapFbos.get(l);if(!u)return;a.activeTexture.set(o.TEXTURE0),o.bindTexture(o.TEXTURE_2D,u.colorAttachment.get()),a.activeTexture.set(o.TEXTURE1),c.bind(o.LINEAR,o.CLAMP_TO_EDGE);let d=s.getProjectionData({overscaledTileID:n,applyTerrainMatrix:i,applyGlobeMatrix:!r});e.useProgram(`heatmapTexture`).draw(a,o.TRIANGLES,Z.disabled,Q.disabled,e.colorModeForRenderPass(),X.disabled,al(e,t,0,1),null,d,t.id,e.rasterBoundsBuffer,e.quadTriangleIndexBuffer,e.rasterBoundsSegments,t.paint,s.zoom),u.destroy(),t.heatmapFbos.delete(l)}function Qu(e,t,n){let r=e.gl;e.activeTexture.set(r.TEXTURE1),e.viewport.set([0,0,t.width/4,t.height/4]);let i=n.heatmapFbos.get(An);i?(r.bindTexture(r.TEXTURE_2D,i.colorAttachment.get()),e.bindFramebuffer.set(i.framebuffer)):(i=$u(e,t.width/4,t.height/4),n.heatmapFbos.set(An,i))}function $u(e,t,n){let r=e.gl,i=r.createTexture();r.bindTexture(r.TEXTURE_2D,i),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_S,r.CLAMP_TO_EDGE),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_T,r.CLAMP_TO_EDGE),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MIN_FILTER,r.LINEAR),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MAG_FILTER,r.LINEAR),r.texStorage2D(r.TEXTURE_2D,1,r.RGBA16F,t,n);let a=e.createFramebuffer(t,n,!1,!1);return a.colorAttachment.set(i),a}function ed(e,t){return t.colorRampTexture||=new Lt(e,t.colorRamp,e.gl.RGBA),t.colorRampTexture}function td(e,t,n,r){let i=e.context,a=i.bindFramebuffer.get(),o=i.viewport.get(),[,,s,c]=o;return nd(e,s,c),i.viewport.set([0,0,s,c]),i.clear({color:V.transparent,depth:1,stencil:0}),e.currentStencilSource=void 0,e.renderTileClippingMasks(t,n,r),{compositeTarget:a,compositeViewport:o}}function nd(e,t,n){let r=e.context.gl;if(!e.layerOpacityFbo){let i=e.context.createFramebuffer(t,n,!0,!0),a=r.createTexture();r.bindTexture(r.TEXTURE_2D,a),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_S,r.CLAMP_TO_EDGE),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_T,r.CLAMP_TO_EDGE),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MIN_FILTER,r.LINEAR),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MAG_FILTER,r.LINEAR),r.texImage2D(r.TEXTURE_2D,0,r.RGBA,t,n,0,r.RGBA,r.UNSIGNED_BYTE,null),i.colorAttachment.set(a),i.depthAttachment.set(e.context.createRenderbuffer(r.DEPTH_STENCIL,t,n)),e.layerOpacityFbo=i,e.context.bindFramebuffer.set(e.layerOpacityFbo.framebuffer);return}if(e.layerOpacityFbo.width===t&&e.layerOpacityFbo.height===n){e.context.bindFramebuffer.set(e.layerOpacityFbo.framebuffer);return}let i=e.layerOpacityFbo;r.bindTexture(r.TEXTURE_2D,i.colorAttachment.get()),r.texImage2D(r.TEXTURE_2D,0,r.RGBA,t,n,0,r.RGBA,r.UNSIGNED_BYTE,null),e.context.bindRenderbuffer.set(i.depthAttachment.get()),r.renderbufferStorage(r.RENDERBUFFER,r.DEPTH_STENCIL,t,n),e.context.bindRenderbuffer.set(null),i.width=t,i.height=n,e.context.bindFramebuffer.set(i.framebuffer)}function rd(e,t,n,r){let i=e.context,a=i.gl;i.bindFramebuffer.set(n.compositeTarget),i.viewport.set(n.compositeViewport),i.activeTexture.set(a.TEXTURE0),a.bindTexture(a.TEXTURE_2D,e.layerOpacityFbo.colorAttachment.get()),e.useProgram(`layerOpacity`).draw(i,a.TRIANGLES,Z.disabled,Q.disabled,e.colorModeForRenderPass(),X.disabled,El(t,0),null,null,r.id,e.viewportBuffer,e.quadTriangleIndexBuffer,e.viewportSegments,r.paint,e.transform.zoom),e.currentStencilSource=void 0}function id(e,t,n,r,i,a,o,s){let c=256;if(i.stepInterpolant){let r=t.getSource().maxzoom,i=o.canonical.z===r?Math.ceil(1<e.options.anisotropicFilterPitch&&f.texParameterf(f.TEXTURE_2D,d.extTextureFilterAnisotropic.TEXTURE_MAX_ANISOTROPY_EXT,d.extTextureFilterAnisotropicMax);let D=e.getTerrainDataForTile(S,l),O=m.getProjectionData({overscaledTileID:S,aligned:_,applyGlobeMatrix:!l,applyTerrainMatrix:!0}),te=Ol(ee,T,E.fadeMix,n,s),k=h.getMeshFromTileID(d,S.canonical,a,o,`raster`),A=i?i[S.overscaledZ]:Q.disabled;p.draw(d,f.TRIANGLES,r,A,g,c?X.frontCCW:X.backCCW,te,D,O,n.id,k.vertexBuffer,k.indexBuffer,k.segments)}}function Dd(e,t,n,r){let i={parentTile:null,parentScaleBy:1,parentTopLeft:[0,0],fadeValues:{tileOpacity:1,parentTileOpacity:1,fadeMix:{opacity:1,mix:0}}};if(n===0||r)return i;if(e.fadingParentID){let r=t.getLoadedTile(e.fadingParentID);if(!r)return i;let a=2**(r.tileID.overscaledZ-e.tileID.overscaledZ);return{parentTile:r,parentScaleBy:a,parentTopLeft:[e.tileID.canonical.x*a%1,e.tileID.canonical.y*a%1],fadeValues:Od(e,r,n)}}return e.selfFading?{parentTile:null,parentScaleBy:1,parentTopLeft:[0,0],fadeValues:kd(e,n)}:i}function Od(e,t,n){let r=U(),i=(r-e.timeAdded)/n,a=(r-t.timeAdded)/n,o=e.fadingDirection===1,s=j(i,0,1),c=j(1-a,0,1),l=o?s:c;return{tileOpacity:l,parentTileOpacity:o?c:s,fadeMix:{opacity:1,mix:1-l}}}function kd(e,t){let n=j((U()-e.timeAdded)/t,0,1);return{tileOpacity:n,fadeMix:{opacity:n,mix:0}}}function Ad(e,t,n,r,i){let a=n.paint.get(`background-color`),o=n.paint.get(`background-opacity`);if(o===0)return;let{isRenderingToTexture:s}=i,c=e.context,l=c.gl,u=e.style.projection,d=e.transform,f=d.tileSize,p=n.paint.get(`background-pattern`);if(e.isPatternMissing(p))return;let m=!p&&a.a===1&&o===1&&e.opaquePassEnabledForLayer()?`opaque`:`translucent`;if(e.renderPass!==m)return;let h=Q.disabled,g=e.getDepthModeForSublayer(0,m===`opaque`?Z.ReadWrite:Z.ReadOnly),_=e.colorModeForRenderPass(),v=e.useProgram(p?`backgroundPattern`:`background`),y=r||Fa(d,{tileSize:f,terrain:e.style.map.terrain});p&&(c.activeTexture.set(l.TEXTURE0),e.imageManager.bind(e.context));let b=n.getCrossfadeParameters();for(let t of y){let r=d.getProjectionData({overscaledTileID:t,applyGlobeMatrix:!s,applyTerrainMatrix:!0}),i=p?Vl(o,e,p,{tileID:t,tileSize:f},b):Bl(o,a),m=e.getTerrainDataForTile(t,s),y=u.getMeshFromTileID(c,t.canonical,!1,!0,`raster`);v.draw(c,l.TRIANGLES,g,h,_,X.backCCW,i,m,r,n.id,y.vertexBuffer,y.indexBuffer,y.segments)}}const jd=new V(1,0,0,1),Md=new V(0,1,0,1),Nd=new V(0,0,1,1),Pd=new V(1,0,1,1),Fd=new V(0,1,1,1);function Id(e){let t=e.transform.padding;Rd(e,e.transform.height-(t.top||0),3,jd),Rd(e,t.bottom||0,3,Md),zd(e,t.left||0,3,Nd),zd(e,e.transform.width-(t.right||0),3,Pd);let n=e.transform.centerPoint;Ld(e,n.x,e.transform.height-n.y,Fd)}function Ld(e,t,n,r){Bd(e,t-2/2,n-20/2,2,20,r),Bd(e,t-20/2,n-2/2,20,2,r)}function Rd(e,t,n,r){Bd(e,0,t+n/2,e.transform.width,n,r)}function zd(e,t,n,r){Bd(e,t-n/2,0,n,e.transform.height,r)}function Bd(e,t,n,r,i,a){let o=e.context,s=o.gl;s.enable(s.SCISSOR_TEST),s.scissor(t*e.pixelRatio,n*e.pixelRatio,r*e.pixelRatio,i*e.pixelRatio),o.clear({color:a}),s.disable(s.SCISSOR_TEST)}function Vd(e,t,n){for(let r of n)Hd(e,t,r)}function Hd(e,t,n){let r=e.context,i=r.gl,a=e.useProgram(`debug`),o=Z.disabled,s=Q.disabled,c=e.colorModeForRenderPass(),l=`$debug`,u=e.style.map.terrain?.getTerrainData(n);r.activeTexture.set(i.TEXTURE0);let d=t.getTileByID(n.key).latestRawTileData?.byteLength||0,f=Math.floor(d/1024),p=t.getTile(n).tileSize,m=512/Math.min(p,512)*(n.overscaledZ/e.transform.zoom)*.5,h=n.canonical.toString();n.overscaledZ!==n.canonical.z&&(h+=` => ${n.overscaledZ}`),Ud(e,`${h} ${f}kB`);let g=e.transform.getProjectionData({overscaledTileID:n,applyGlobeMatrix:!0,applyTerrainMatrix:!0});a.draw(r,i.TRIANGLES,o,s,Y.alphaBlended,X.disabled,tl(V.transparent,m),null,g,l,e.debugBuffer,e.quadTriangleIndexBuffer,e.debugSegments),a.draw(r,i.LINE_STRIP,o,s,c,X.disabled,tl(V.red),u,g,l,e.debugBuffer,e.tileBorderIndexBuffer,e.debugSegments)}function Ud(e,t){e.initDebugOverlayCanvas();let n=e.debugOverlayCanvas,r=e.context.gl,i=e.debugOverlayCanvas.getContext(`2d`);i.clearRect(0,0,n.width,n.height),i.shadowColor=`white`,i.shadowBlur=2,i.lineWidth=1.5,i.strokeStyle=`white`,i.textBaseline=`top`,i.font=`bold 36px Open Sans, sans-serif`,i.fillText(t,5,5),i.strokeText(t,5,5),e.debugOverlayTexture.update(n),e.debugOverlayTexture.bind(r.LINEAR,r.CLAMP_TO_EDGE)}function Wd(e,t){let n=null,r=Object.values(e._layers).flatMap(n=>n.source&&!n.isHidden(t)?[e.tileManagers[n.source]]:[]),i=r.filter(e=>e.getSource().type===`vector`),a=r.filter(e=>e.getSource().type!==`vector`),o=e=>{(!n||n.getSource().maxzoomc.getProjectionData({overscaledTileID:new ht(e.tileID.canonical.z,e.tileID.wrap??0,e.tileID.canonical.z,e.tileID.canonical.x,e.tileID.canonical.y),aligned:e.aligned,applyGlobeMatrix:e.applyGlobeMatrix,applyTerrainMatrix:e.applyTerrainMatrix})},d=o.renderingMode?o.renderingMode:`2d`;if(e.renderPass===`offscreen`){let t=o.prerender;t&&(e.setCustomLayerDefaults(),a.setColorMode(e.colorModeForRenderPass()),t.call(o,a.gl,u),a.setDirty(),e.setBaseState())}else if(e.renderPass===`translucent`){e.setCustomLayerDefaults(),a.setColorMode(e.colorModeForRenderPass()),a.setStencilMode(Q.disabled);let t=d===`3d`?e.getDepthModeFor3D():e.getDepthModeForSublayer(0,Z.ReadOnly);a.setDepthMode(t),o.render(a.gl,u),a.setDirty(),e.setBaseState(),a.bindFramebuffer.set(null)}}function Kd(e,t){let n=e.context,r=n.gl,i=e.transform,a=Y.unblended,o=new Z(r.LEQUAL,Z.ReadWrite,[0,1]),s=t.tileManager.getRenderableTiles(),c=e.useProgram(`terrainDepth`);n.bindFramebuffer.set(t.getFramebuffer(`depth`).framebuffer),n.viewport.set([0,0,e.width/devicePixelRatio,e.height/devicePixelRatio]),n.clear({color:V.transparent,depth:1});for(let e of s){let s=t.getTerrainMesh(e.tileID),l=t.getTerrainData(e.tileID),u=i.getProjectionData({overscaledTileID:e.tileID,applyTerrainMatrix:!1,applyGlobeMatrix:!0}),d=Oc(t.getSkirtLength(i.zoom));c.draw(n,r.TRIANGLES,o,Q.disabled,a,X.backCCW,d,l,u,`terrain`,s.vertexBuffer,s.indexBuffer,s.segments)}n.bindFramebuffer.set(null),n.viewport.set([0,0,e.width,e.height])}function qd(e,t){let n=e.context,r=n.gl,i=e.transform,a=Y.unblended,o=new Z(r.LEQUAL,Z.ReadWrite,[0,1]),s=t.getCoordsTexture(),c=t.tileManager.getRenderableTiles(),l=e.useProgram(`terrainCoords`);n.bindFramebuffer.set(t.getFramebuffer(`coords`).framebuffer),n.viewport.set([0,0,e.width/devicePixelRatio,e.height/devicePixelRatio]),n.clear({color:V.transparent,depth:1}),t.coordsIndex=[];for(let e of c){let c=t.getTerrainMesh(e.tileID),u=t.getTerrainData(e.tileID);n.activeTexture.set(r.TEXTURE0),r.bindTexture(r.TEXTURE_2D,s.texture);let d=kc(255-t.coordsIndex.length,t.getSkirtLength(i.zoom)),f=i.getProjectionData({overscaledTileID:e.tileID,applyTerrainMatrix:!1,applyGlobeMatrix:!0});l.draw(n,r.TRIANGLES,o,Q.disabled,a,X.backCCW,d,u,f,`terrain`,c.vertexBuffer,c.indexBuffer,c.segments),t.coordsIndex.push(e.tileID.key)}n.bindFramebuffer.set(null),n.viewport.set([0,0,e.width,e.height])}function Jd(e,t,n,r){let{isRenderingGlobe:i}=r,a=e.context,o=a.gl,s=e.transform,c=e.colorModeForRenderPass(),l=e.getDepthModeFor3D(),u=e.useProgram(`terrain`);a.bindFramebuffer.set(null),a.viewport.set([0,0,e.width,e.height]);for(let r of n){let n=t.getTerrainMesh(r.tileID),d=e.renderToTexture.getTexture(r),f=t.getTerrainData(r.tileID);a.activeTexture.set(o.TEXTURE0),o.bindTexture(o.TEXTURE_2D,d.texture);let p=Dc(t.getSkirtLength(s.zoom),s.calculateFogMatrix(r.tileID.toUnwrapped()),e.style.sky,s.pitch,i),m=s.getProjectionData({overscaledTileID:r.tileID,applyTerrainMatrix:!1,applyGlobeMatrix:!0});u.draw(a,o.TRIANGLES,l,Q.disabled,c,X.backCCW,p,f,m,`terrain`,n.vertexBuffer,n.indexBuffer,n.segments)}}function Yd(e,t){if(!t.mesh){let n=new O;n.emplaceBack(-1,-1),n.emplaceBack(1,-1),n.emplaceBack(1,1),n.emplaceBack(-1,1);let r=new He;r.emplaceBack(0,1,2),r.emplaceBack(0,2,3),t.mesh=new us(e.createVertexBuffer(n,ds.members),e.createIndexBuffer(r),o.simpleSegment(0,0,n.length,r.length))}return t.mesh}function Xd(e,t){let n=e.context,r=n.gl,i=Gl(t,e.transform,e.pixelRatio),a=new Z(r.LEQUAL,Z.ReadWrite,[0,1]),o=Q.disabled,s=e.colorModeForRenderPass(),c=e.useProgram(`sky`),l=Yd(n,t);c.draw(n,r.TRIANGLES,a,o,s,X.disabled,i,null,void 0,`sky`,l.vertexBuffer,l.indexBuffer,l.segments)}function Zd(e,t){let n=e.getCartesianPosition();Le(n,n);let r=qt(new Float64Array(16));return e.properties.get(`anchor`)===`map`&&(f(r,r,t.rollInRadians),cr(r,r,-t.pitchInRadians),f(r,r,t.bearingInRadians),cr(r,r,t.center.lat*Math.PI/180),Ue(r,r,-t.center.lng*Math.PI/180)),Bn(n,n,r),n}function Qd(e,t,n){let r=e.context,i=r.gl,a=e.useProgram(`atmosphere`),o=new Z(i.LEQUAL,Z.ReadOnly,[0,1]),s=e.transform,c=Zd(n,e.transform),l=s.getProjectionData({overscaledTileID:null,applyGlobeMatrix:!0,applyTerrainMatrix:!0}),u=t.properties.get(`atmosphere-blend`)*l.projectionTransition;if(u===0)return;let d=Qs(s.worldSize,s.center.lat),f=s.inverseProjectionMatrix,p=new Float64Array(4);p[3]=1,A(p,p,s.modelViewProjectionMatrix),p[0]/=p[3],p[1]/=p[3],p[2]/=p[3],p[3]=1,A(p,p,f),p[0]/=p[3],p[1]/=p[3],p[2]/=p[3],p[3]=1;let m=Ul(c,u,[p[0],p[1],p[2]],d,f),h=Yd(r,t);a.draw(r,i.TRIANGLES,o,Q.disabled,Y.alphaBlended,X.disabled,m,null,null,`atmosphere`,h.vertexBuffer,h.indexBuffer,h.segments)}const $d={symbol:Ru,circle:Ku,heatmap:qu,line:ld,fill:fd,fillExtrusion:gd,hillshade:vd,colorRelief:xd,raster:Td,background:Ad,sky:Xd,atmosphere:Qd,custom:Gd,debug:Vd,debugPadding:Id,terrainDepth:Kd,terrainCoords:qd};var ef=class e{constructor(e,t){this.drawFunctions=$d,this.context=new Nu(e),this.transform=t,this.layerOpacityFbo=null,this._tileTextures={},this._rttObjectRecyclePool=[],this._rttSharedFbo=null,this.terrainFacilitator={depthDirty:!0,coordsDirty:!1,matrix:qt(new Float64Array(16)),renderTime:0},this.setup(),this.numSublayers=qa.maxOverzooming+qa.maxUnderzooming+1,this.depthEpsilon=1/2**16,this.crossTileSymbolIndex=new $o}resize(e,t,n){if(this.width=Math.floor(e*n),this.height=Math.floor(t*n),this.pixelRatio=n,this.context.viewport.set([0,0,this.width,this.height]),this.style)for(let e of this.style._order)this.style._layers[e].resize()}setup(){let e=this.context,t=new O;t.emplaceBack(0,0),t.emplaceBack(M,0),t.emplaceBack(0,M),t.emplaceBack(M,M),this.tileExtentBuffer=e.createVertexBuffer(t,ds.members),this.tileExtentSegments=o.simpleSegment(0,0,4,2);let n=new O;n.emplaceBack(0,0),n.emplaceBack(M,0),n.emplaceBack(0,M),n.emplaceBack(M,M),this.debugBuffer=e.createVertexBuffer(n,ds.members),this.debugSegments=o.simpleSegment(0,0,4,5);let r=new ye;r.emplaceBack(0,0,0,0),r.emplaceBack(M,0,M,0),r.emplaceBack(0,M,0,M),r.emplaceBack(M,M,M,M),this.rasterBoundsBuffer=e.createVertexBuffer(r,xc.members),this.rasterBoundsSegments=o.simpleSegment(0,0,4,2);let i=new O;i.emplaceBack(0,0),i.emplaceBack(M,0),i.emplaceBack(0,M),i.emplaceBack(M,M),this.rasterBoundsBufferPosOnly=e.createVertexBuffer(i,ds.members),this.rasterBoundsSegmentsPosOnly=o.simpleSegment(0,0,4,5);let a=new O;a.emplaceBack(0,0),a.emplaceBack(1,0),a.emplaceBack(0,1),a.emplaceBack(1,1),this.viewportBuffer=e.createVertexBuffer(a,ds.members),this.viewportSegments=o.simpleSegment(0,0,4,2);let s=new De;s.emplaceBack(0),s.emplaceBack(1),s.emplaceBack(3),s.emplaceBack(2),s.emplaceBack(0),this.tileBorderIndexBuffer=e.createIndexBuffer(s);let c=new He;c.emplaceBack(1,0,2),c.emplaceBack(1,2,3),this.quadTriangleIndexBuffer=e.createIndexBuffer(c);let l=this.context.gl;this.stencilClearMode=new Q({func:l.ALWAYS,mask:0},0,255,l.ZERO,l.ZERO,l.ZERO),this.tileExtentMesh=new us(this.tileExtentBuffer,this.quadTriangleIndexBuffer,this.tileExtentSegments)}clearStencil(){let e=this.context,t=e.gl;this.nextStencilID=1,this.currentStencilSource=void 0;let n=Ut();fr(n,0,this.width,this.height,0,0,1),rr(n,n,[t.drawingBufferWidth,t.drawingBufferHeight,0]);let r={mainMatrix:n,tileMercatorCoords:[0,0,1,1],clippingPlane:[0,0,0,0],projectionTransition:0,fallbackMatrix:n};this.useProgram(`clippingMask`,null,!0).draw(e,t.TRIANGLES,Z.disabled,this.stencilClearMode,Y.disabled,X.disabled,null,null,r,`$clipping`,this.viewportBuffer,this.quadTriangleIndexBuffer,this.viewportSegments)}renderTileClippingMasks(e,t,n){if(this.currentStencilSource===e.source||!e.isTileClipped()||!t?.length)return;this.currentStencilSource=e.source,this.nextStencilID+t.length>256&&this.clearStencil();let r=this.context;r.setColorMode(Y.disabled),r.setDepthMode(Z.disabled);let i={};for(let e of t)i[e.key]=this.nextStencilID++;this._renderTileMasks(i,t,n,!0),this._renderTileMasks(i,t,n,!1),this._tileClippingMaskIDs=i}_renderTileMasks(e,t,n,r){let i=this.context,a=i.gl,o=this.style.projection,s=this.transform,c=this.useProgram(`clippingMask`);for(let l of t){let t=e[l.key],u=this.getTerrainDataForTile(l,n),d=o.getMeshFromTileID(this.context,l.canonical,r,!0,`stencil`),f=s.getProjectionData({overscaledTileID:l,applyGlobeMatrix:!n,applyTerrainMatrix:!0});c.draw(i,a.TRIANGLES,Z.disabled,new Q({func:a.ALWAYS,mask:0},t,255,a.KEEP,a.KEEP,a.REPLACE),Y.disabled,n?X.disabled:X.backCCW,null,u,f,`$clipping`,d.vertexBuffer,d.indexBuffer,d.segments)}}getTerrainDataForTile(e,t){return t&&this.style.projection?.name===`mercator`?null:this.style.map.terrain?.getTerrainData(e)||null}_renderTilesDepthBuffer(){let e=this.context,t=e.gl,n=this.style.projection,r=this.transform,i=this.useProgram(`depth`),a=this.getDepthModeFor3D(),o=Fa(r,{tileSize:r.tileSize});for(let s of o){let o=this.style.map.terrain?.getTerrainData(s),c=n.getMeshFromTileID(this.context,s.canonical,!0,!0,`raster`),l=r.getProjectionData({overscaledTileID:s,applyGlobeMatrix:!0,applyTerrainMatrix:!0});i.draw(e,t.TRIANGLES,a,Q.disabled,Y.disabled,X.backCCW,null,o,l,`$clipping`,c.vertexBuffer,c.indexBuffer,c.segments)}}stencilModeFor3D(){this.currentStencilSource=void 0,this.nextStencilID+1>256&&this.clearStencil();let e=this.nextStencilID++,t=this.context.gl;return new Q({func:t.NOTEQUAL,mask:255},e,255,t.KEEP,t.KEEP,t.REPLACE)}stencilModeForClipping(e){let t=this.context.gl;return new Q({func:t.EQUAL,mask:255},this._tileClippingMaskIDs[e.key],0,t.KEEP,t.KEEP,t.REPLACE)}getStencilConfigForOverlapAndUpdateStencilID(e){let t=this.context.gl,n=e.sort((e,t)=>t.overscaledZ-e.overscaledZ),r=n[n.length-1].overscaledZ,i=n[0].overscaledZ-r+1;if(i>1){this.currentStencilSource=void 0,this.nextStencilID+i>256&&this.clearStencil();let e={};for(let n=0;nt.overscaledZ-e.overscaledZ),r=n[n.length-1].overscaledZ,i=n[0].overscaledZ-r+1;if(this.clearStencil(),i>1){let e={},a={};for(let n=0;n0};for(let e in r){let t=r[e];t.used&&t.prepare(this.context),i[e]=t.getVisibleCoordinates(!1),a[e]=i[e].slice().reverse(),o[e]=t.getVisibleCoordinates(!0).reverse()}this.opaquePassCutoff=1/0;for(let e=0;ethis.useProgram(e)}),this.context.viewport.set([0,0,this.width,this.height]),this.context.bindFramebuffer.set(null),this.context.clear({color:t.showOverdrawInspector?V.black:V.transparent,depth:1}),this.clearStencil(),this.style.sky&&this.drawFunctions.sky(this,this.style.sky),this._showOverdrawInspector=t.showOverdrawInspector,this.depthRangeFor3D=[0,1-(e._order.length+2)*this.numSublayers*this.depthEpsilon],!this.renderToTexture)for(this.renderPass=`opaque`,this.currentLayer=n.length-1;this.currentLayer>=0;this.currentLayer--){let e=this.style._layers[n[this.currentLayer]],t=r[e.source],a=i[e.source];this.renderTileClippingMasks(e,a,!1),this.renderLayer(this,t,e,a,s)}this.renderPass=`translucent`;let c=!1;for(this.currentLayer=0;this.currentLayer0?t.pop():null}acquireRTT(e){let t=this.context.gl,n=this._rttObjectRecyclePool.pop();if(n)return n.size!==e&&(t.bindTexture(t.TEXTURE_2D,n.texture.texture),t.texImage2D(t.TEXTURE_2D,0,t.RGBA,e,e,0,t.RGBA,t.UNSIGNED_BYTE,null),n.texture.size=[e,e],n.size=e),n;let r=new Lt(this.context,{width:e,height:e,data:null},t.RGBA);return r.bind(t.LINEAR,t.CLAMP_TO_EDGE),this.context.extTextureFilterAnisotropic&&t.texParameterf(t.TEXTURE_2D,this.context.extTextureFilterAnisotropic.TEXTURE_MAX_ANISOTROPY_EXT,this.context.extTextureFilterAnisotropicMax),{texture:r,size:e}}bindRTT(e){let t=this.context.gl,n=e.size;if(!this._rttSharedFbo){let e=this.context.createFramebuffer(n,n,!0,!0),r=this.context.createRenderbuffer(t.DEPTH_STENCIL,n,n);e.depthAttachment.set(r),this._rttSharedFbo={fbo:e,depthRenderbuffer:r,size:n}}this._rttSharedFbo.size!==n&&(this.context.bindRenderbuffer.set(this._rttSharedFbo.depthRenderbuffer),t.renderbufferStorage(t.RENDERBUFFER,t.DEPTH_STENCIL,n,n),this.context.bindRenderbuffer.set(null),this._rttSharedFbo.fbo.width=n,this._rttSharedFbo.fbo.height=n,this._rttSharedFbo.size=n),this._rttSharedFbo.fbo.colorAttachment.set(e.texture.texture),this.context.bindFramebuffer.set(this._rttSharedFbo.fbo.framebuffer)}releaseRTT(e){this._rttObjectRecyclePool.push(e)}isPatternMissing(e){if(!e)return!1;if(!e.from||!e.to)return!0;let t=this.imageManager.getPattern(e.from.toString()),n=this.imageManager.getPattern(e.to.toString());return!t||!n}useProgram(e,t,n=!1,r=[]){this.cache||={};let i=!!this.style.map.terrain,a=this.style.projection,o=n?ls.projectionMercator:a.shaderPreludeCode,s=n?fs:a.shaderDefine,c=`/${n?ps:a.shaderVariantName}`,l=t?t.cacheKey:``,u=this._showOverdrawInspector?`/overdraw`:``,d=i?`/terrain`:``,f=r?`/${r.join(`/`)}`:``,p=e+l+c+u+d+f;return this.cache[p]||=new Nc(this.context,ls[e],t,ql[e],this._showOverdrawInspector,i,o,s,r),this.cache[p]}setCustomLayerDefaults(){this.context.unbindVAO(),this.context.cullFace.setDefault(),this.context.activeTexture.setDefault(),this.context.pixelStoreUnpack.setDefault(),this.context.pixelStoreUnpackPremultiplyAlpha.setDefault(),this.context.pixelStoreUnpackFlipY.setDefault()}setBaseState(){let e=this.context.gl;this.context.cullFace.set(!1),this.context.viewport.set([0,0,this.width,this.height]),this.context.blendEquation.set(e.FUNC_ADD)}initDebugOverlayCanvas(){if(this.debugOverlayCanvas==null){this.debugOverlayCanvas=document.createElement(`canvas`),this.debugOverlayCanvas.width=512,this.debugOverlayCanvas.height=512;let e=this.context.gl;this.debugOverlayTexture=new Lt(this.context,this.debugOverlayCanvas,e.RGBA)}}destroy(){if(this._tileTextures){for(let e in this._tileTextures){let t=this._tileTextures[e];if(t)for(let e of t)e.destroy()}this._tileTextures={}}for(let e of this._rttObjectRecyclePool)e.texture.destroy();if(this._rttObjectRecyclePool=[],this._rttSharedFbo){this._rttSharedFbo.fbo.colorAttachment.set(null),this._rttSharedFbo.fbo.depthAttachment.set(null);let e=this.context.gl;e.deleteRenderbuffer(this._rttSharedFbo.depthRenderbuffer),e.deleteFramebuffer(this._rttSharedFbo.fbo.framebuffer),this._rttSharedFbo=null}if(this.layerOpacityFbo?.destroy(),this.layerOpacityFbo=null,this.tileExtentBuffer&&this.tileExtentBuffer.destroy(),this.debugBuffer&&this.debugBuffer.destroy(),this.rasterBoundsBuffer&&this.rasterBoundsBuffer.destroy(),this.rasterBoundsBufferPosOnly&&this.rasterBoundsBufferPosOnly.destroy(),this.viewportBuffer&&this.viewportBuffer.destroy(),this.tileBorderIndexBuffer&&this.tileBorderIndexBuffer.destroy(),this.quadTriangleIndexBuffer&&this.quadTriangleIndexBuffer.destroy(),this.tileExtentMesh&&this.tileExtentMesh.vertexBuffer?.destroy(),this.tileExtentMesh&&this.tileExtentMesh.indexBuffer?.destroy(),this.debugOverlayTexture&&this.debugOverlayTexture.destroy(),this.cache){for(let e in this.cache){let t=this.cache[e];t?.program&&this.context.gl.deleteProgram(t.program)}this.cache={}}this.context&&this.context.setDefault()}overLimit(){let{drawingBufferWidth:e,drawingBufferHeight:t}=this.context.gl;return this.width!==e||this.height!==t}},tf=class extends Error{constructor(e,t){super(`WebGL2 is required to display this map. We are sorry, but it seems that your browser does not support WebGL2, a technology for rendering 3D graphics on the web. Read more on https://wiki.openstreetmap.org/wiki/This_map_requires_WebGL`),this.name=`GPUInitializationError`,this.requestedAttributes=e,this.statusMessage=t?.statusMessage??null}};function nf(e,t){let n=!1,r=null,i,a=()=>{r=null,n&&=(e(...i),r=setTimeout(a,t),!1)};return(...e)=>(n=!0,i=e,r||a(),r)}var rf=class{constructor(e){this._getHashParams=()=>new URLSearchParams(window.location.hash.replace(`#`,``)),this._getCurrentHash=()=>{let e=this._getHashParams();return this._hashName?(e.get(this._hashName)||``).split(`/`):([...e.keys()][0]??``).split(`/`)},this._onHashChange=()=>{let e=this._getCurrentHash();if(!this._isValidHash(e))return!1;let t=this._map.dragRotate.isEnabled()&&this._map.touchZoomRotate.isEnabled()?+(e[3]||0):this._map.getBearing();return this._map.jumpTo({center:[+e[2],+e[1]],zoom:+e[0],bearing:t,pitch:+(e[4]||0)}),!0},this._updateHashUnthrottled=()=>{let e=window.location.href.replace(/(#.*)?$/,this.getHashString());window.history.replaceState(window.history.state,null,e)},this._removeHash=()=>{let e=this._getHashParams();if(this._hashName)e.delete(this._hashName);else{let t=Array.from(e.keys());t.length>0&&e.delete(t[0])}let t=decodeURIComponent(e.toString()).replace(/=&/g,`&`).replace(/=$/g,``),n=t?`#${t}`:``,r=window.location.href.replace(/(#.+)?$/,n);r=r.replace(`&&`,`&`),window.history.replaceState(window.history.state,null,r)},this._updateHash=nf(this._updateHashUnthrottled,30*1e3/100),this._hashName=e&&encodeURIComponent(e)}addTo(e){return this._map=e,addEventListener(`hashchange`,this._onHashChange,!1),this._map.on(`moveend`,this._updateHash),this}remove(){return removeEventListener(`hashchange`,this._onHashChange,!1),this._map.off(`moveend`,this._updateHash),clearTimeout(this._updateHash()),this._removeHash(),delete this._map,this}getHashString(e){let t=this._map.getCenter(),n=Math.round(this._map.getZoom()*100)/100,r=10**Math.ceil((n*Math.LN2+Math.log(512/360/.5))/Math.LN10),i=Math.round(t.lng*r)/r,a=Math.round(t.lat*r)/r,o=this._map.getBearing(),s=this._map.getPitch(),c=``;if(e?c+=`/${i}/${a}/${n}`:c+=`${n}/${a}/${i}`,(o||s)&&(c+=`/${Math.round(o*10)/10}`),s&&(c+=`/${Math.round(s)}`),this._hashName){let e=this._getHashParams();return e.set(this._hashName,c),`#${decodeURIComponent(e.toString()).replace(/=&/g,`&`).replace(/=$/g,``)}`}return`#${c}`}_isValidHash(e){if(e.length<3||e.some(e=>isNaN(+e)))return!1;try{new B(+e[2],+e[1])}catch{return!1}let t=+e[0],n=+(e[3]||0),r=+(e[4]||0);return t>=this._map.getMinZoom()&&t<=this._map.getMaxZoom()&&n>=-180&&n<=180&&r>=this._map.getMinPitch()&&r<=this._map.getMaxPitch()}};const af={linearity:.3,easing:E(0,0,.3,1)},of=L({deceleration:2500,maxSpeed:1400},af),sf=L({deceleration:20,maxSpeed:1400},af),cf=L({deceleration:1e3,maxSpeed:360},af),lf=L({deceleration:1e3,maxSpeed:90},af),uf=L({deceleration:1e3,maxSpeed:360},af);var df=class{constructor(e){this._map=e,this.clear()}clear(){this._inertiaBuffer=[]}record(e){this._drainInertiaBuffer(),this._inertiaBuffer.push({time:U(),settings:e})}_drainInertiaBuffer(){let e=this._inertiaBuffer,t=U();for(;e.length>0&&t-e[0].time>160;)e.shift()}_onMoveEnd(e){if(this._drainInertiaBuffer(),this._inertiaBuffer.length<2)return;let t={zoom:0,bearing:0,pitch:0,roll:0,pan:new z(0,0),pinchAround:void 0,around:void 0};for(let{settings:e}of this._inertiaBuffer)t.zoom+=e.zoomDelta||0,t.bearing+=e.bearingDelta||0,t.pitch+=e.pitchDelta||0,t.roll+=e.rollDelta||0,e.panDelta&&t.pan._add(e.panDelta),e.around&&(t.around=e.around),e.pinchAround&&(t.pinchAround=e.pinchAround);let n=this._inertiaBuffer[this._inertiaBuffer.length-1].time-this._inertiaBuffer[0].time,r={};if(t.pan.mag()){let i=pf(t.pan.mag(),n,L({},of,e||{})),a=t.pan.mult(i.amount/t.pan.mag()),o=this._map._camera.cameraHelper.handlePanInertia(a,this._map._camera.transform);r.center=o.easingCenter,r.offset=o.easingOffset,ff(r,i)}if(t.zoom){let e=pf(t.zoom,n,sf);r.zoom=st(this._map.getZoom()+e.amount,this._map.getZoomSnap(),e.amount),ff(r,e)}if(t.bearing){let e=pf(t.bearing,n,cf);r.bearing=this._map.getBearing()+j(e.amount,-179,179),ff(r,e)}if(t.pitch){let e=pf(t.pitch,n,lf);r.pitch=this._map.getPitch()+e.amount,ff(r,e)}if(t.roll){let e=pf(t.roll,n,uf);r.roll=this._map.getRoll()+j(e.amount,-179,179),ff(r,e)}if(r.zoom||r.bearing){let e=t.pinchAround===void 0?t.around:t.pinchAround;r.around=e?this._map.unproject(e):this._map.getCenter()}return this.clear(),L(r,{noMoveStart:!0})}};function ff(e,t){(!e.duration||e.duration=this._clickTolerance||this._map.fire(new Lr(e.type,this._map,e))}dblclick(e){return this._firePreventable(new Lr(e.type,this._map,e))}mouseover(e){this._map.fire(new Lr(e.type,this._map,e))}mouseout(e){this._map.fire(new Lr(e.type,this._map,e))}touchstart(e){return this._firePreventable(new Rr(e.type,this._map,e))}touchmove(e){this._map.fire(new Rr(e.type,this._map,e))}touchend(e){this._map.fire(new Rr(e.type,this._map,e))}touchcancel(e){this._map.fire(new Rr(e.type,this._map,e))}_firePreventable(e){if(this._map.fire(e),e.defaultPrevented)return{}}isEnabled(){return!0}isActive(){return!1}enable(){}disable(){}},hf=class{constructor(e){this._map=e}reset(){this._delayContextMenu=!1,this._ignoreContextMenu=!0,delete this._contextMenuEvent}mousemove(e){this._map.fire(new Lr(e.type,this._map,e))}mousedown(){this._delayContextMenu=!0,this._ignoreContextMenu=!1}mouseup(){this._delayContextMenu=!1,this._contextMenuEvent&&(this._map.fire(new Lr(`contextmenu`,this._map,this._contextMenuEvent)),delete this._contextMenuEvent)}contextmenu(e){this._delayContextMenu?this._contextMenuEvent=e:this._ignoreContextMenu||this._map.fire(new Lr(e.type,this._map,e)),this._map.listens(`contextmenu`)&&e.preventDefault()}isEnabled(){return!0}isActive(){return!1}enable(){}disable(){}},gf=class{constructor(e,t,n){this._map=e,this._tr=n,this._el=e.getCanvasContainer(),this._container=e.getContainer(),this._clickTolerance=t.clickTolerance||1,t.boxZoom&&typeof t.boxZoom==`object`&&(this._boxZoomEnd=t.boxZoom.boxZoomEnd)}isEnabled(){return!!this._enabled}isActive(){return!!this._active}enable(){this.isEnabled()||(this._enabled=!0)}disable(){this.isEnabled()&&(this._enabled=!1)}mousedown(e,t){this.isEnabled()&&e.shiftKey&&e.button===0&&(W.disableDrag(),this._startPos=this._lastPos=t,this._active=!0)}mousemoveWindow(e,t){if(!this._active)return;let n=t;if(this._lastPos.equals(n)||!this._box&&n.dist(this._startPos)e.fitScreenCoordinates(n,r,this._tr.bearing,{linear:!0})}}}keydown(e){this._active&&e.keyCode===27&&(this.reset(),this._fireEvent(`boxzoomcancel`,e))}reset(){this._active=!1,this._container.classList.remove(`maplibregl-crosshair`),this._box&&=(this._box.remove(),null),W.enableDrag(),delete this._startPos,delete this._lastPos}_fireEvent(e,t){return this._map.fire(new Br(e,{originalEvent:t}))}};function _f(e,t){if(e.length!==t.length)throw Error(`The number of touches and points are not equal - touches ${e.length}, points ${t.length}`);let n={};for(let r=0;rthis.numTouches)&&(this.aborted=!0),!this.aborted&&(this.startTime===void 0&&(this.startTime=e.timeStamp),n.length===this.numTouches&&(this.centroid=vf(t),this.touches=_f(n,t)))}touchmove(e,t,n){if(this.aborted||!this.centroid)return;let r=_f(n,t);for(let e in this.touches){let t=this.touches[e],n=r[e];(!n||n.dist(t)>30)&&(this.aborted=!0)}}touchend(e,t,n){if((!this.centroid||e.timeStamp-this.startTime>500)&&(this.aborted=!0),n.length===0){let e=!this.aborted&&this.centroid;if(this.reset(),e)return e}}},bf=class{constructor(e){this.singleTap=new yf(e),this.numTaps=e.numTaps,this.reset()}reset(){this.lastTime=1/0,delete this.lastTap,this.count=0,this.singleTap.reset()}touchstart(e,t,n){this.singleTap.touchstart(e,t,n)}touchmove(e,t,n){this.singleTap.touchmove(e,t,n)}touchend(e,t,n){let r=this.singleTap.touchend(e,t,n);if(r){let t=e.timeStamp-this.lastTime<500,n=!this.lastTap||this.lastTap.dist(r)<30;if((!t||!n)&&this.reset(),this.count++,this.lastTime=e.timeStamp,this.lastTap=r,this.count===this.numTaps)return this.reset(),r}}},xf=class{constructor(e,t){this._tr=t,this._zoomIn=new bf({numTouches:1,numTaps:2}),this._zoomOut=new bf({numTouches:2,numTaps:1}),this.reset()}reset(){this._active=!1,this._zoomIn.reset(),this._zoomOut.reset()}touchstart(e,t,n){this._zoomIn.touchstart(e,t,n),this._zoomOut.touchstart(e,t,n)}touchmove(e,t,n){this._zoomIn.touchmove(e,t,n),this._zoomOut.touchmove(e,t,n)}touchend(e,t,n){let r=this._zoomIn.touchend(e,t,n),i=this._zoomOut.touchend(e,t,n),a=this._tr;if(r)return this._active=!0,e.preventDefault(),setTimeout(()=>this.reset(),0),{cameraAnimation:t=>t.easeTo({duration:300,zoom:st(a.zoom+1,t.getZoomSnap()),around:a.unproject(r)},{originalEvent:e})};if(i)return this._active=!0,e.preventDefault(),setTimeout(()=>this.reset(),0),{cameraAnimation:t=>t.easeTo({duration:300,zoom:st(a.zoom-1,t.getZoomSnap()),around:a.unproject(i)},{originalEvent:e})}}touchcancel(){this.reset()}enable(){this._enabled=!0}disable(){this._enabled=!1,this.reset()}isEnabled(){return this._enabled}isActive(){return this._active}},Sf=class{constructor(e){this._enabled=!!e.enable,this._moveStateManager=e.moveStateManager,this._clickTolerance=e.clickTolerance||1,this._moveFunction=e.move,this._activateOnStart=!!e.activateOnStart,e.assignEvents(this),this.reset()}reset(e){this._active=!1,this._moved=!1,delete this._lastPoint,this._moveStateManager.endMove(e)}_move(...e){let t=this._moveFunction(...e);if(t.bearingDelta||t.pitchDelta||t.rollDelta||t.around||t.panDelta)return this._active=!0,t}dragStart(e,t){!this.isEnabled()||this._lastPoint||this._moveStateManager.isValidStartEvent(e)&&(this._moveStateManager.startMove(e),this._lastPoint=Array.isArray(t)?t[0]:t,this._activateOnStart&&this._lastPoint&&(this._active=!0))}dragMove(e,t){if(!this.isEnabled())return;let n=this._lastPoint;if(!n)return;if(e.preventDefault(),!this._moveStateManager.isValidMoveEvent(e)){this.reset(e);return}let r=Array.isArray(t)?t[0]:t;if(!(!this._moved&&r.dist(n)!0}),t=new Ef){this.mouseMoveStateManager=e,this.oneFingerTouchMoveStateManager=t}_executeRelevantHandler(e,t,n){if(e instanceof MouseEvent)return t(e);if(typeof TouchEvent<`u`&&e instanceof TouchEvent)return n(e)}startMove(e){this._executeRelevantHandler(e,e=>{this.mouseMoveStateManager.startMove(e)},e=>{this.oneFingerTouchMoveStateManager.startMove(e)})}endMove(e){this._executeRelevantHandler(e,e=>{this.mouseMoveStateManager.endMove(e)},e=>{this.oneFingerTouchMoveStateManager.endMove(e)})}isValidStartEvent(e){return!!this._executeRelevantHandler(e,e=>this.mouseMoveStateManager.isValidStartEvent(e),e=>this.oneFingerTouchMoveStateManager.isValidStartEvent(e))}isValidMoveEvent(e){return!!this._executeRelevantHandler(e,e=>this.mouseMoveStateManager.isValidMoveEvent(e),e=>this.oneFingerTouchMoveStateManager.isValidMoveEvent(e))}isValidEndEvent(e){return!!this._executeRelevantHandler(e,e=>this.mouseMoveStateManager.isValidEndEvent(e),e=>this.oneFingerTouchMoveStateManager.isValidEndEvent(e))}};const Of=e=>{e.mousedown=e.dragStart,e.mousemoveWindow=e.dragMove,e.mouseup=e.dragEnd,e.contextmenu=e=>{e.preventDefault()}};function kf({enable:e,clickTolerance:t}){return new Sf({clickTolerance:t,move:(e,t)=>({around:t,panDelta:t.sub(e)}),activateOnStart:!0,moveStateManager:new Tf({checkCorrectEvent:e=>e.button===0&&!e.ctrlKey}),enable:e,assignEvents:Of})}function Af({enable:e,clickTolerance:t,aroundCenter:n=!0,minPixelCenterThreshold:r=100,rotateDegreesPerPixelMoved:i=.8},a){return new Sf({clickTolerance:t,move:(e,t)=>{let o=a();if(n&&Math.abs(o.y-e.y)>r)return{bearingDelta:Kn(new z(e.x,t.y),t,o)};let s=(t.x-e.x)*i;return n&&t.ye.button===0&&e.ctrlKey||e.button===2&&!e.ctrlKey}),enable:e,assignEvents:Of})}function jf({enable:e,clickTolerance:t,pitchDegreesPerPixelMoved:n=-.5}){return new Sf({clickTolerance:t,move:(e,t)=>({pitchDelta:(t.y-e.y)*n}),moveStateManager:new Tf({checkCorrectEvent:e=>e.button===0&&e.ctrlKey||e.button===2}),enable:e,assignEvents:Of})}function Mf({enable:e,clickTolerance:t,rollDegreesPerPixelMoved:n=.3},r){return new Sf({clickTolerance:t,move:(e,t)=>{let i=r(),a=(t.x-e.x)*n;return t.ye.button===2&&e.ctrlKey}),enable:e,assignEvents:Of})}var Nf=class{constructor(e,t){this._clickTolerance=e.clickTolerance||1,this._map=t,this.reset()}reset(){this._active=!1,this._touches={},this._sum=new z(0,0)}_shouldBePrevented(e){return e<(this._map.cooperativeGestures.isEnabled()?2:1)}touchstart(e,t,n){return this._calculateTransform(e,t,n)}touchmove(e,t,n){if(this._active){if(this._shouldBePrevented(n.length)){this._map.cooperativeGestures.notifyGestureBlocked(`touch_pan`,e);return}return e.preventDefault(),this._calculateTransform(e,t,n)}}touchend(e,t,n){this._calculateTransform(e,t,n),this._active&&this._shouldBePrevented(n.length)&&this.reset()}touchcancel(){this.reset()}_calculateTransform(e,t,n){n.length>0&&(this._active=!0);let r=_f(n,t),i=new z(0,0),a=new z(0,0),o=0;for(let e in r){let t=r[e],n=this._touches[e];n&&(i._add(t),a._add(t.sub(n)),o++,r[e]=t)}if(this._touches=r,this._shouldBePrevented(o)||!a.mag())return;let s=a.div(o);if(this._sum._add(s),!(this._sum.mag()Math.abs(e.x)}var Hf=class extends Pf{constructor(e){super(),this._currentTouchCount=0,this._map=e}reset(){super.reset(),this._valid=void 0,delete this._firstMove,delete this._lastPoints}touchstart(e,t,n){super.touchstart(e,t,n),this._currentTouchCount=n.length}_start(e){this._lastPoints=e,Vf(e[0].sub(e[1]))&&(this._valid=!1)}_move(e,t,n){if(this._map.cooperativeGestures.isEnabled()&&this._currentTouchCount<3)return;let r=e[0].sub(this._lastPoints[0]),i=e[1].sub(this._lastPoints[1]);if(this._valid=this.gestureBeginsVertically(r,i,n.timeStamp),this._valid)return this._lastPoints=e,this._active=!0,{pitchDelta:(r.y+i.y)/2*-.5}}gestureBeginsVertically(e,t,n){if(this._valid!==void 0)return this._valid;let r=e.mag()>=2,i=t.mag()>=2;if(!r&&!i)return;if(!r||!i)return this._firstMove===void 0&&(this._firstMove=n),n-this._firstMove<100&&void 0;let a=e.y>0==t.y>0;return Vf(e)&&Vf(t)&&a}};const Uf={panStep:100,bearingStep:15,pitchStep:10};var Wf=class{constructor(e,t){this._tr=t;let n=Uf;this._panStep=n.panStep,this._bearingStep=n.bearingStep,this._pitchStep=n.pitchStep,this._rotationDisabled=!1}reset(){this._active=!1}keydown(e){if(e.altKey||e.ctrlKey||e.metaKey)return;let t=0,n=0,r=0,i=0,a=0;switch(e.keyCode){case 61:case 107:case 171:case 187:t=1;break;case 189:case 109:case 173:t=-1;break;case 37:e.shiftKey?n=-1:(e.preventDefault(),i=-1);break;case 39:e.shiftKey?n=1:(e.preventDefault(),i=1);break;case 38:e.shiftKey?r=1:(e.preventDefault(),a=-1);break;case 40:e.shiftKey?r=-1:(e.preventDefault(),a=1);break;default:return}return this._rotationDisabled&&(n=0,r=0),{cameraAnimation:o=>{let s=this._tr;o.easeTo({duration:300,easeId:`keyboardHandler`,easing:Gf,zoom:t?st(s.zoom+t*(e.shiftKey?2:1),o.getZoomSnap()):s.zoom,bearing:s.bearing+n*this._bearingStep,pitch:s.pitch+r*this._pitchStep,offset:[-i*this._panStep,-a*this._panStep],center:s.center},{originalEvent:e})}}}enable(){this._enabled=!0}disable(){this._enabled=!1,this.reset()}isEnabled(){return this._enabled}isActive(){return this._active}disableRotation(){this._rotationDisabled=!0}enableRotation(){this._rotationDisabled=!1}};function Gf(e){return e*(2-e)}const Kf=4.000244140625;var qf=class{constructor(e,t,n){this._onTimeout=e=>{this._type=`wheel`,this._delta-=this._lastValue,this._active||this._start(e)},this._map=e,this._tr=n,this._triggerRenderFrame=t,this._delta=0,this._defaultZoomRate=.01,this._wheelZoomRate=.0022222222222222222}setZoomRate(e){this._defaultZoomRate=e}setWheelZoomRate(e){this._wheelZoomRate=e}isEnabled(){return!!this._enabled}isActive(){return!!this._active||this._finishTimeout!==void 0}isZooming(){return!!this._zooming}enable(e){this.isEnabled()||(this._enabled=!0,this._aroundCenter=!!e&&e.around===`center`)}disable(){this.isEnabled()&&(this._enabled=!1)}_shouldBePrevented(e){return this._map.cooperativeGestures.isEnabled()?!(e.ctrlKey||this._map.cooperativeGestures.isBypassed(e)):!1}wheel(e){if(!this.isEnabled())return;if(this._shouldBePrevented(e)){this._map.cooperativeGestures.notifyGestureBlocked(`wheel_zoom`,e);return}let t=e.deltaMode===WheelEvent.DOM_DELTA_LINE?e.deltaY*40:e.deltaY,n=U(),r=n-(this._lastWheelEventTime||0);this._lastWheelEventTime=n,t!==0&&t%Kf==0?this._type=`wheel`:t!==0&&Math.abs(t)<4?this._type=`trackpad`:r>400?(this._type=null,this._lastValue=t,this._timeout=setTimeout(this._onTimeout,40,e)):this._type||(this._type=Math.abs(r*t)<200?`trackpad`:`wheel`,this._timeout&&(clearTimeout(this._timeout),this._timeout=null,t+=this._lastValue)),e.shiftKey&&t&&(t/=4),this._type&&(this._lastWheelEvent=e,this._delta-=t,this._active||this._start(e)),e.preventDefault()}_start(e){if(!this._delta)return;this._needsRerender=!1,this._active=!0,this.isZooming()||(this._zooming=!0),this._finishTimeout&&(clearTimeout(this._finishTimeout),delete this._finishTimeout);let t=W.mousePos(this._map.getCanvas(),e),n=this._tr;this._aroundCenter?this._aroundPoint=n.transform.locationToScreenPoint(B.convert(n.center)):this._aroundPoint=t,this._needsRerender||(this._needsRerender=!0,this._triggerRenderFrame())}renderFrame(){if(!this._needsRerender||(this._needsRerender=!1,!this.isActive()))return;let e=this._tr.transform;if(typeof this._lastExpectedZoom==`number`){let t=e.zoom-this._lastExpectedZoom;typeof this._startZoom==`number`&&(this._startZoom+=t),typeof this._targetZoom==`number`&&(this._targetZoom+=t)}if(this._delta!==0){let t=this._type===`wheel`&&Math.abs(this._delta)>Kf?this._wheelZoomRate:this._defaultZoomRate,n=2/(1+Math.exp(-Math.abs(this._delta*t)));this._delta<0&&n!==0&&(n=1/n);let r=typeof this._targetZoom==`number`?Se(this._targetZoom):e.scale,i=e.applyConstrain(e.getCameraLngLat(),ar(r*n)).zoom,a=this._map.getZoomSnap();if(this._type===`wheel`&&a>0){let t=st(e.zoom,a);this._targetZoom=st(i,a,i-t)}else this._targetZoom=i;this._type===`wheel`&&(this._startZoom=e.zoom,this._easing=this._smoothOutEasing(200)),this._delta=0}let t=typeof this._targetZoom==`number`?this._targetZoom:e.zoom,n=this._startZoom,r=this._easing,i=!1,a;if(this._type===`wheel`&&n&&r){let e=U()-this._lastWheelEventTime,o=Math.min((e+5)/200,1),s=r(o);a=Yn.number(n,t,s),o<1?this._needsRerender=!0:i=!0}else a=t,i=!0;return this._active=!0,i&&(this._active=!1,this._finishTimeout=setTimeout(()=>{this._zooming=!1,this._triggerRenderFrame(),delete this._targetZoom,delete this._lastExpectedZoom,delete this._finishTimeout},200)),this._lastExpectedZoom=a,{noInertia:!0,needsRenderFrame:!i,zoomDelta:a-e.zoom,around:this._aroundPoint,originalEvent:this._lastWheelEvent}}_smoothOutEasing(e){let t=rt;if(this._prevEase){let e=this._prevEase,n=(U()-e.start)/e.duration,r=e.easing(n+.01)-e.easing(n),i=.27/Math.sqrt(r*r+1e-4)*.01;t=E(i,Math.sqrt(.27*.27-i*i),.25,1)}return this._prevEase={start:U(),duration:e,easing:t},t}reset(){this._active=!1,this._zooming=!1,delete this._targetZoom,delete this._lastExpectedZoom,this._finishTimeout&&(clearTimeout(this._finishTimeout),delete this._finishTimeout)}},Jf=class{constructor(e,t){this._clickZoom=e,this._tapZoom=t}enable(){this._clickZoom.enable(),this._tapZoom.enable()}disable(){this._clickZoom.disable(),this._tapZoom.disable()}isEnabled(){return this._clickZoom.isEnabled()&&this._tapZoom.isEnabled()}isActive(){return this._clickZoom.isActive()||this._tapZoom.isActive()}},Yf=class{constructor(e,t){this._tr=t,this.reset()}reset(){this._active=!1}dblclick(e,t){return e.preventDefault(),{cameraAnimation:n=>{n.easeTo({duration:300,zoom:st(this._tr.zoom+(e.shiftKey?-1:1),n.getZoomSnap()),around:this._tr.unproject(t)},{originalEvent:e})}}}enable(){this._enabled=!0}disable(){this._enabled=!1,this.reset()}isEnabled(){return this._enabled}isActive(){return this._active}},Xf=class{constructor(){this._tap=new bf({numTouches:1,numTaps:1}),this._zoomRate=1,this.reset()}setZoomRate(e){this._zoomRate=e??1}reset(){this._active=!1,delete this._swipePoint,delete this._swipeTouch,delete this._tapTime,delete this._tapPoint,this._tap.reset()}touchstart(e,t,n){if(!this._swipePoint)if(!this._tapTime)this._tap.touchstart(e,t,n);else{let r=t[0],i=e.timeStamp-this._tapTime<500,a=this._tapPoint.dist(r)<30;!i||!a?this.reset():n.length>0&&(this._swipePoint=r,this._swipeTouch=n[0].identifier)}}touchmove(e,t,n){if(!this._tapTime)this._tap.touchmove(e,t,n);else if(this._swipePoint){if(n[0].identifier!==this._swipeTouch)return;let r=t[0],i=r.y-this._swipePoint.y;return this._swipePoint=r,e.preventDefault(),this._active=!0,{zoomDelta:i/128*this._zoomRate}}}touchend(e,t,n){if(this._tapTime)this._swipePoint&&n.length===0&&this.reset();else{let r=this._tap.touchend(e,t,n);r&&(this._tapTime=e.timeStamp,this._tapPoint=r)}}touchcancel(){this.reset()}enable(){this._enabled=!0}disable(){this._enabled=!1,this.reset()}isEnabled(){return this._enabled}isActive(){return this._active}},Zf=class{constructor(e,t,n){this._el=e,this._mousePan=t,this._touchPan=n}enable(e){this._inertiaOptions=e||{},this._mousePan.enable(),this._touchPan.enable(),this._el.classList.add(`maplibregl-touch-drag-pan`)}disable(){this._mousePan.disable(),this._touchPan.disable(),this._el.classList.remove(`maplibregl-touch-drag-pan`)}isEnabled(){return this._mousePan.isEnabled()&&this._touchPan.isEnabled()}isActive(){return this._mousePan.isActive()||this._touchPan.isActive()}},Qf=class{constructor(e,t,n,r){this._pitchWithRotate=e.pitchWithRotate,this._rollEnabled=e.rollEnabled,this._mouseRotate=t,this._mousePitch=n,this._mouseRoll=r}enable(){this._mouseRotate.enable(),this._pitchWithRotate&&this._mousePitch.enable(),this._rollEnabled&&this._mouseRoll.enable()}disable(){this._mouseRotate.disable(),this._mousePitch.disable(),this._mouseRoll.disable()}isEnabled(){return this._mouseRotate.isEnabled()&&(!this._pitchWithRotate||this._mousePitch.isEnabled())&&(!this._rollEnabled||this._mouseRoll.isEnabled())}isActive(){return this._mouseRotate.isActive()||this._mousePitch.isActive()||this._mouseRoll.isActive()}},$f=class{constructor(e,t,n,r){this._el=e,this._touchZoom=t,this._touchRotate=n,this._tapDragZoom=r,this._rotationDisabled=!1,this._enabled=!0}enable(e){this._touchZoom.enable(e),this._rotationDisabled||this._touchRotate.enable(e),this._tapDragZoom.enable(),this._el.classList.add(`maplibregl-touch-zoom-rotate`)}disable(){this._touchZoom.disable(),this._touchRotate.disable(),this._tapDragZoom.disable(),this._el.classList.remove(`maplibregl-touch-zoom-rotate`)}isEnabled(){return this._touchZoom.isEnabled()&&(this._rotationDisabled||this._touchRotate.isEnabled())&&this._tapDragZoom.isEnabled()}isActive(){return this._touchZoom.isActive()||this._touchRotate.isActive()||this._tapDragZoom.isActive()}setZoomRate(e){this._touchZoom.setZoomRate(e),this._tapDragZoom.setZoomRate(e)}setZoomThreshold(e){this._touchZoom.setZoomThreshold(e)}disableRotation(){this._rotationDisabled=!0,this._touchRotate.disable()}enableRotation(){this._rotationDisabled=!1,this._touchZoom.isEnabled()&&this._touchRotate.enable()}},ep=class{constructor(e,t){this._bypassKey=navigator.userAgent.includes(`Mac`)?`metaKey`:`ctrlKey`,this._map=e,this._options=t,this._enabled=!1}isActive(){return!1}reset(){}_setupUI(){if(this._container)return;let e=this._map.getCanvasContainer();e.classList.add(`maplibregl-cooperative-gestures`),this._container=W.create(`div`,`maplibregl-cooperative-gesture-screen`,e);let t=this._map._getUIString(`CooperativeGesturesHandler.WindowsHelpText`);this._bypassKey===`metaKey`&&(t=this._map._getUIString(`CooperativeGesturesHandler.MacHelpText`));let n=this._map._getUIString(`CooperativeGesturesHandler.MobileHelpText`),r=document.createElement(`div`);r.className=`maplibregl-desktop-message`,r.textContent=t,this._container.appendChild(r);let i=document.createElement(`div`);i.className=`maplibregl-mobile-message`,i.textContent=n,this._container.appendChild(i),this._container.setAttribute(`aria-hidden`,`true`)}_destroyUI(){this._container&&(this._container.remove(),this._map.getCanvasContainer().classList.remove(`maplibregl-cooperative-gestures`)),delete this._container}enable(){this._setupUI(),this._enabled=!0}disable(){this._enabled=!1,this._destroyUI()}isEnabled(){return this._enabled}isBypassed(e){return e[this._bypassKey]}notifyGestureBlocked(e,t){this._enabled&&(this._map.fire(new Pr(`cooperativegestureprevented`,{gestureType:e,originalEvent:t})),this._container.classList.add(`maplibregl-show`),setTimeout(()=>{this._container.classList.remove(`maplibregl-show`)},100))}},tp=class{constructor(e){this._camera=e}get transform(){return this._camera._requestedCameraState||this._camera.transform}get center(){return{lng:this.transform.center.lng,lat:this.transform.center.lat}}get zoom(){return this.transform.zoom}get pitch(){return this.transform.pitch}get bearing(){return this.transform.bearing}unproject(e){return this.transform.screenPointToLocation(z.convert(e),this._camera.terrain)}};const np=e=>e.zoom||e.drag||e.roll||e.pitch||e.rotate;var rp=class extends On{};function ip(e){return e.panDelta?.mag()||e.zoomDelta||e.bearingDelta||e.pitchDelta||e.rollDelta}var ap=class{get _ownerDocument(){return this._el?.ownerDocument||document}get _ownerWindow(){return this._el?.ownerDocument?.defaultView||window}constructor(e,t,n){this.handleWindowEvent=e=>{this.handleEvent(e,`${e.type}Window`)},this.handleEvent=(e,t)=>{if(e.type===`blur`){this.stop(!0);return}this._updatingCamera=!0;let n=e.type===`renderFrame`?void 0:e,r={needsRenderFrame:!1},i={},a={};for(let{handlerName:o,handler:s,allowed:c}of this._handlers){if(!s.isEnabled())continue;let l;if(this._blockedByActive(a,c,o))s.reset();else if(s[t||e.type]){if(Tn(e,t||e.type)){let n=W.mousePos(this._map.getCanvas(),e);l=s[t||e.type](e,n)}else if(Mt(e,t||e.type)){let n=e.touches,r=this._getMapTouches(n),i=W.touchPos(this._map.getCanvas(),r);l=s[t||e.type](e,i,r)}else mn(t||e.type)||(l=s[t||e.type](e));this.mergeHandlerResult(r,i,l,o,n),l?.needsRenderFrame&&this._triggerRenderFrame()}(l||s.isActive())&&(a[o]=s)}let o={};for(let e in this._previousActiveHandlers)a[e]||(o[e]=n);this._previousActiveHandlers=a,(Object.keys(o).length||ip(r))&&(this._changes.push([r,i,o]),this._triggerRenderFrame()),(Object.keys(a).length||ip(r))&&this._camera.stop(!0),this._updatingCamera=!1;let{cameraAnimation:s}=r;s&&(this._inertia.clear(),this._fireEvents({},{},!0),this._changes=[],s(this._map))},this._map=e,this._camera=t,this._transformProvider=new tp(this._camera),this._el=this._map.getCanvasContainer(),this._handlers=[],this._handlersById={},this._changes=[],this._inertia=new df(e),this._bearingSnap=n.bearingSnap,this._previousActiveHandlers={},this._eventsInProgress={},this._addDefaultHandlers(n);let r=this._el;this._listeners=[[r,`touchstart`,{passive:!0}],[r,`touchmove`,{passive:!1}],[r,`touchend`,void 0],[r,`touchcancel`,void 0],[r,`mousedown`,void 0],[r,`mousemove`,void 0],[r,`mouseup`,void 0],[this._ownerDocument,`mousemove`,{capture:!0}],[this._ownerDocument,`mouseup`,void 0],[r,`mouseover`,void 0],[r,`mouseout`,void 0],[r,`dblclick`,void 0],[r,`click`,void 0],[r,`keydown`,{capture:!1}],[r,`keyup`,void 0],[r,`wheel`,{passive:!1}],[r,`contextmenu`,void 0],[this._ownerWindow,`blur`,void 0]];for(let[e,t,n]of this._listeners)e.addEventListener(t,e===this._ownerDocument?this.handleWindowEvent:this.handleEvent,n)}destroy(){for(let[e,t,n]of this._listeners)e.removeEventListener(t,e===this._ownerDocument?this.handleWindowEvent:this.handleEvent,n)}_addDefaultHandlers(e){let t=this._map,n=t.getCanvasContainer();this._add(`mapEvent`,new mf(t,e));let r=t.boxZoom=new gf(t,e,this._transformProvider);this._add(`boxZoom`,r),e.interactive&&e.boxZoom&&r.enable();let i=t.cooperativeGestures=new ep(t,e.cooperativeGestures);this._add(`cooperativeGestures`,i),e.cooperativeGestures&&i.enable();let a=new xf(t,this._transformProvider),o=new Yf(t,this._transformProvider);t.doubleClickZoom=new Jf(o,a),this._add(`tapZoom`,a),this._add(`clickZoom`,o),e.interactive&&e.doubleClickZoom&&t.doubleClickZoom.enable();let s=new Xf;this._add(`tapDragZoom`,s);let c=t.touchPitch=new Hf(t);this._add(`touchPitch`,c),e.interactive&&e.touchPitch&&t.touchPitch.enable(e.touchPitch);let l=()=>t.project(t.getCenter()),u=Af(e,l),d=jf(e),f=Mf(e,l);t.dragRotate=new Qf(e,u,d,f),this._add(`mouseRotate`,u,[`mousePitch`]),this._add(`mousePitch`,d,[`mouseRotate`,`mouseRoll`]),this._add(`mouseRoll`,f,[`mousePitch`]),e.interactive&&e.dragRotate&&t.dragRotate.enable();let p=kf(e),m=new Nf(e,t);t.dragPan=new Zf(n,p,m),this._add(`mousePan`,p),this._add(`touchPan`,m,[`touchZoom`,`touchRotate`]),e.interactive&&e.dragPan&&t.dragPan.enable(e.dragPan);let h=new Bf,g=new Rf;t.touchZoomRotate=new $f(n,g,h,s),this._add(`touchRotate`,h,[`touchPan`,`touchZoom`]),this._add(`touchZoom`,g,[`touchPan`,`touchRotate`]),e.interactive&&e.touchZoomRotate&&t.touchZoomRotate.enable(e.touchZoomRotate),this._add(`blockableMapEvent`,new hf(t));let _=t.scrollZoom=new qf(t,()=>this._triggerRenderFrame(),this._transformProvider);this._add(`scrollZoom`,_,[`mousePan`]),e.interactive&&e.scrollZoom&&t.scrollZoom.enable(e.scrollZoom);let v=t.keyboard=new Wf(t,this._transformProvider);this._add(`keyboard`,v),e.interactive&&e.keyboard&&t.keyboard.enable()}_add(e,t,n){this._handlers.push({handlerName:e,handler:t,allowed:n}),this._handlersById[e]=t}stop(e){if(!this._updatingCamera){for(let{handler:e}of this._handlers)e.reset();this._inertia.clear(),this._fireEvents({},{},e),this._changes=[]}}isActive(){for(let{handler:e}of this._handlers)if(e.isActive())return!0;return!1}isZooming(){return!!this._eventsInProgress.zoom||this._map.scrollZoom.isZooming()}isRotating(){return!!this._eventsInProgress.rotate}isMoving(){return!!np(this._eventsInProgress)||this.isZooming()}_blockedByActive(e,t,n){for(let r in e)if(r!==n&&!t?.includes(r))return!0;return!1}_getMapTouches(e){let t=[];for(let n of e){let e=n.target;this._el.contains(e)&&t.push(n)}return t}mergeHandlerResult(e,t,n,r,i){if(!n)return;L(e,n);let a={handlerName:r,originalEvent:n.originalEvent||i};n.zoomDelta!==void 0&&(t.zoom=a),n.panDelta!==void 0&&(t.drag=a),n.rollDelta!==void 0&&(t.roll=a),n.pitchDelta!==void 0&&(t.pitch=a),n.bearingDelta!==void 0&&(t.rotate=a)}_applyChanges(){let e={},t={},n={};for(let[r,i,a]of this._changes)r.panDelta&&(e.panDelta=(e.panDelta||new z(0,0))._add(r.panDelta)),r.zoomDelta&&(e.zoomDelta=(e.zoomDelta||0)+r.zoomDelta),r.bearingDelta&&(e.bearingDelta=(e.bearingDelta||0)+r.bearingDelta),r.pitchDelta&&(e.pitchDelta=(e.pitchDelta||0)+r.pitchDelta),r.rollDelta&&(e.rollDelta=(e.rollDelta||0)+r.rollDelta),r.around!==void 0&&(e.around=r.around),r.pinchAround!==void 0&&(e.pinchAround=r.pinchAround),r.noInertia&&(e.noInertia=r.noInertia),L(t,i),L(n,a);this._updateMapTransform(e,t,n),this._changes=[]}_updateMapTransform(e,t,n){let r=this._map,i=this._camera.getTransformForUpdate(),a=r.terrain;if(!ip(e)&&!(a&&this._terrainMovement)){this._fireEvents(t,n,!0);return}this._camera.stop(!0);let{panDelta:o,zoomDelta:s,bearingDelta:c,pitchDelta:l,rollDelta:u,around:d,pinchAround:f}=e;f!==void 0&&(d=f),d||=this._camera.transform.centerPoint,a&&!i.isPointOnMapSurface(d)&&(d=i.centerPoint);let p={panDelta:o,zoomDelta:s,rollDelta:u,pitchDelta:l,bearingDelta:c,around:d};this._camera.cameraHelper.useGlobeControls&&!i.isPointOnMapSurface(d)&&(d=i.centerPoint);let m=d.distSqr(i.centerPoint)<.01?i.center:i.screenPointToLocation(o?d.sub(o):d);this._handleMapControls({terrain:a,tr:i,deltasForHelper:p,preZoomAroundLoc:m,combinedEventsInProgress:t,panDelta:o}),this._camera.applyUpdatedTransform(i),this._map._update(),e.noInertia||this._inertia.record(e),this._fireEvents(t,n,!0)}_handleMapControls({terrain:e,tr:t,deltasForHelper:n,preZoomAroundLoc:r,combinedEventsInProgress:i,panDelta:a}){let o=this._camera.cameraHelper;if(o.handleMapControlsRollPitchBearingZoom(n,t),!e){o.handleMapControlsPan(n,t,r);return}if(o.useGlobeControls){!this._terrainMovement&&(i.drag||i.zoom)&&(this._terrainMovement=!0,this._camera.elevationFreeze=!0),o.handleMapControlsPan(n,t,r);return}if(!this._terrainMovement&&(i.drag||i.zoom)){this._terrainMovement=!0,this._camera.elevationFreeze=!0,o.handleMapControlsPan(n,t,r);return}if(i.drag&&this._terrainMovement&&a){t.setCenter(t.screenPointToLocation(t.centerPoint.sub(a)));return}o.handleMapControlsPan(n,t,r)}_fireEvents(e,t,n){let r=np(this._eventsInProgress),i=np(e),a={};for(let t in e){let{originalEvent:n}=e[t];this._eventsInProgress[t]||(a[`${t}start`]=n),this._eventsInProgress[t]=e[t]}!r&&i&&this._fireEvent(`movestart`,i.originalEvent);for(let e in a)this._fireEvent(e,a[e]);i&&this._fireEvent(`move`,i.originalEvent);for(let t in e){let{originalEvent:n}=e[t];this._fireEvent(t,n)}let o={},s;for(let e in this._eventsInProgress){let{handlerName:n,originalEvent:r}=this._eventsInProgress[e];this._handlersById[n].isActive()||(delete this._eventsInProgress[e],s=t[n]||r,o[`${e}end`]=s)}for(let e in o)this._fireEvent(e,o[e]);let c=np(this._eventsInProgress),l=(r||i)&&!c;if(l&&this._terrainMovement){this._camera.elevationFreeze=!1,this._terrainMovement=!1;let e=this._camera.getTransformForUpdate();this._map.getCenterClampedToGround()&&e.recalculateZoomAndCenter(this._map.terrain),this._camera.applyUpdatedTransform(e)}if(n&&l){this._updatingCamera=!0;let e=this._inertia._onMoveEnd(this._map.dragPan._inertiaOptions),t=e=>e!==0&&-this._bearingSnap{delete this._frameId,this.handleEvent(new rp(`renderFrame`,{timeStamp:e})),this._applyChanges()})}_triggerRenderFrame(){this._frameId===void 0&&(this._frameId=this._requestFrame())}},op=class extends _n{constructor(e){super(),this._renderFrameCallback=()=>{let e=Math.min((U()-this._easeStart)/this._easeOptions.duration,1);this._onEaseFrame(this._easeOptions.easing(e)),e<1&&this._easeFrameId?this._easeFrameId=this._requestRenderFrame(this._renderFrameCallback):this.stop()},this.transform=new Es,this.cameraHelper=new As,e.minZoom!==void 0&&this.transform.setMinZoom(e.minZoom),e.maxZoom!==void 0&&this.transform.setMaxZoom(e.maxZoom),e.minPitch!==void 0&&this.transform.setMinPitch(e.minPitch),e.maxPitch!==void 0&&this.transform.setMaxPitch(e.maxPitch),e.renderWorldCopies!==void 0&&this.transform.setRenderWorldCopies(e.renderWorldCopies),e.transformConstrain!==null&&this.transform.setConstrainOverride(e.transformConstrain),this._moving=!1,this._zooming=!1,this._bearingSnap=e.bearingSnap,this._zoomSnap=e.zoomSnap,this._requestRenderFrame=e.requestRenderFrame,this._cancelRenderFrame=e.cancelRenderFrame,this.terrain=e.terrain,this._centerClampedToGround=e.centerClampedToGround??!0,this.transformCameraUpdate=e.transformCameraUpdate??null,this._stopHandlers=e.stopHandlers??(()=>{}),this.on(`moveend`,()=>{delete this._requestedCameraState})}migrateProjection(e,t){e.apply(this.transform,!0),this.transform=e,this.cameraHelper=t}getCenter(){return new B(this.transform.center.lng,this.transform.center.lat)}setCenter(e,t){return this.jumpTo({center:e},t)}getCenterElevation(){return this.transform.elevation}setCenterElevation(e,t){return this.jumpTo({elevation:e},t),this}getCenterClampedToGround(){return this._centerClampedToGround}setCenterClampedToGround(e){this._centerClampedToGround=e}panBy(e,t,n){return e=z.convert(e).mult(-1),this.panTo(this.transform.center,L({offset:e},t),n)}panTo(e,t,n){return this.easeTo(L({center:e},t),n)}getZoom(){return this.transform.zoom}setZoom(e,t){return this.jumpTo({zoom:e},t),this}zoomTo(e,t,n){return this.easeTo(L({zoom:e},t),n)}zoomIn(e,t){return this.zoomTo(st(this.getZoom()+1,this._zoomSnap),e,t),this}zoomOut(e,t){return this.zoomTo(st(this.getZoom()-1,this._zoomSnap),e,t),this}getVerticalFieldOfView(){return this.transform.fov}setVerticalFieldOfView(e,t){return e!=this.transform.fov&&(this.transform.setFov(e),this.fire(new G(`movestart`,t)).fire(new G(`move`,t)).fire(new G(`moveend`,t))),this}getBearing(){return this.transform.bearing}setZoomSnap(e){return this._zoomSnap=e,this}getZoomSnap(){return this._zoomSnap}setBearing(e,t){return this.jumpTo({bearing:e},t),this}getPadding(){return this.transform.padding}setPadding(e,t){return this.jumpTo({padding:e},t),this}rotateTo(e,t,n){return this.easeTo(L({bearing:e},t),n)}resetNorth(e,t){return this.rotateTo(0,L({duration:1e3},e),t),this}resetNorthPitch(e,t){return this.easeTo(L({bearing:0,pitch:0,roll:0,duration:1e3},e),t),this}snapToNorth(e,t){return Math.abs(this.getBearing()){m.easeFunc(r),this.terrain&&!e.freezeElevation&&this._updateElevation(r),this.applyUpdatedTransform(n),this._fireMoveEvents(t)},n=>{this.terrain&&e.freezeElevation&&this._finalizeElevation(),this._afterEase(t,n)},e),this}_prepareEase(e,t,n={}){this._moving=!0,!t&&!n.moving&&this.fire(new G(`movestart`,e)),this._zooming&&!n.zooming&&this.fire(new G(`zoomstart`,e)),this._rotating&&!n.rotating&&this.fire(new G(`rotatestart`,e)),this._pitching&&!n.pitching&&this.fire(new G(`pitchstart`,e)),this._rolling&&!n.rolling&&this.fire(new G(`rollstart`,e))}_prepareElevation(e){this._elevationCenter=e,this._elevationStart=this.transform.elevation,this._elevationTarget=this.terrain.getElevationForLngLatZoom(e,this.transform.tileZoom),this.elevationFreeze=!0}_updateElevation(e){(this._elevationStart===void 0||this._elevationCenter===void 0)&&this._prepareElevation(this.transform.center),this.transform.setMinElevationForCurrentTile(this.terrain.getMinTileElevationForLngLatZoom(this._elevationCenter,this.transform.tileZoom));let t=this.terrain.getElevationForLngLatZoom(this._elevationCenter,this.transform.tileZoom);if(e<1&&t!==this._elevationTarget){let n=this._elevationTarget-this._elevationStart,r=(t-(n*e+this._elevationStart))/(1-e);this._elevationStart+=e*(n-r),this._elevationTarget=t}this.transform.setElevation(Yn.number(this._elevationStart,this._elevationTarget,e))}_finalizeElevation(){this.elevationFreeze=!1,this.getCenterClampedToGround()&&this.transform.recalculateZoomAndCenter(this.terrain)}getTransformForUpdate(){return!this.transformCameraUpdate&&!this.terrain?this.transform:(this._requestedCameraState||=this.transform.clone(),this._requestedCameraState)}_elevateCameraIfInsideTerrain(e){if(!this.terrain&&e.elevation>=0&&e.pitch<=90)return{};let t=e.getCameraLngLat(),n=e.getCameraAltitude(),r=this.terrain?this.terrain.getElevationForLngLatZoom(t,e.zoom):0;if(nthis._elevateCameraIfInsideTerrain(e)),this.transformCameraUpdate&&t.push(e=>this.transformCameraUpdate(e)),!t.length)return;let n=e.clone();for(let e of t){let t=n.clone(),{center:r,zoom:i,roll:a,pitch:o,bearing:s,elevation:c}=e(t);r&&t.setCenter(r),c!==void 0&&t.setElevation(c),i!==void 0&&t.setZoom(i),a!==void 0&&t.setRoll(a),o!==void 0&&t.setPitch(o),s!==void 0&&t.setBearing(s),n.apply(t,!1)}this.transform.apply(n,!1)}_fireMoveEvents(e){this.fire(new G(`move`,e)),this._zooming&&this.fire(new G(`zoom`,e)),this._rotating&&this.fire(new G(`rotate`,e)),this._pitching&&this.fire(new G(`pitch`,e)),this._rolling&&this.fire(new G(`roll`,e))}_afterEase(e,t){if(this._easeId&&t&&this._easeId===t)return;delete this._easeId;let n=this._zooming,r=this._rotating,i=this._pitching,a=this._rolling;this._moving=!1,this._zooming=!1,this._rotating=!1,this._pitching=!1,this._rolling=!1,this._padding=!1,n&&this.fire(new G(`zoomend`,e)),r&&this.fire(new G(`rotateend`,e)),i&&this.fire(new G(`pitchend`,e)),a&&this.fire(new G(`rollend`,e)),this.fire(new G(`moveend`,e))}flyTo(e,t){if(!e.essential&&Dr.prefersReducedMotion){let n=Yt(e,[`center`,`zoom`,`bearing`,`pitch`,`roll`,`elevation`,`padding`]);return this.jumpTo(n,t)}this.stop(),e=L({offset:[0,0],speed:1.2,curve:1.42,easing:rt},e),`zoom`in e&&this._zoomSnap&&(e.zoom=st(e.zoom,this._zoomSnap));let n=this.getTransformForUpdate(),r=n.bearing,i=n.pitch,a=n.roll,o=n.padding,s=`bearing`in e?this._normalizeBearing(e.bearing,r):r,c=`pitch`in e?+e.pitch:i,l=`roll`in e?this._normalizeBearing(e.roll,a):a,u=`padding`in e?e.padding:n.padding,d=z.convert(e.offset),f=n.centerPoint.add(d),p=n.screenPointToLocation(f),m=this.cameraHelper.handleFlyTo(n,{bearing:s,pitch:c,roll:l,padding:u,locationAtOffset:p,offsetAsPoint:d,center:e.center,minZoom:e.minZoom,zoom:e.zoom}),h=e.curve,g=Math.max(n.width,n.height),_=g/m.scaleOfZoom,v=m.pixelPathLength,y=g/m.scaleOfMinZoom;h=Math.min(h,Math.sqrt(y/v*2));let b=h*h;function x(e){let t=(_*_-g*g+(e?-1:1)*b*b*v*v)/(2*(e?_:g)*b*v);return Math.log(Math.sqrt(t*t+1)-t)}function S(e){return(Math.exp(e)-Math.exp(-e))/2}function C(e){return(Math.exp(e)+Math.exp(-e))/2}function w(e){return S(e)/C(e)}let T=x(!1),ee=function(e){return C(T)/C(T+h*e)},E=function(e){return g*((C(T)*w(T+h*e)-S(T))/b)/v},D=(x(!0)-T)/h;if(Math.abs(v)<2e-6||!isFinite(D)){if(Math.abs(g-_)<1e-6)return this.easeTo(e,t);let n=_0,ee=e=>Math.exp(n*h*e)}if(`duration`in e)e.duration=+e.duration;else{let t=`screenSpeed`in e?+e.screenSpeed/h:+e.speed;e.duration=1e3*D/t}return e.maxDuration&&e.duration>e.maxDuration&&(e.duration=0),this._zooming=!0,this._rotating=r!==s,this._pitching=c!==i,this._rolling=l!==a,this._padding=!n.isPaddingEqual(u),this._prepareEase(t,!1),this.terrain&&this._prepareElevation(m.targetCenter),this._ease(p=>{let h=p*D,g=1/ee(h),_=E(h);this._rotating&&n.setBearing(Yn.number(r,s,p)),this._pitching&&n.setPitch(Yn.number(i,c,p)),this._rolling&&n.setRoll(Yn.number(a,l,p)),this._padding&&(n.interpolatePadding(o,u,p),f=n.centerPoint.add(d)),m.easeFunc(p,g,_,f),this.terrain&&!e.freezeElevation&&this._updateElevation(p),this.applyUpdatedTransform(n),this._fireMoveEvents(t)},()=>{this.terrain&&e.freezeElevation&&this._finalizeElevation(),this._afterEase(t)},e),this}isEasing(){return!!this._easeFrameId}stop(e){return this._stop(e)}_stop(e,t){if(this._easeFrameId&&(this._cancelRenderFrame(this._easeFrameId),delete this._easeFrameId,delete this._onEaseFrame),this._onEaseEnd){let e=this._onEaseEnd;delete this._onEaseEnd,e.call(this,t)}return e||this._stopHandlers(),this}_ease(e,t,n){n.animate===!1||n.duration===0?(e(1),t()):(this._easeStart=U(),this._easeOptions=n,this._onEaseFrame=e,this._onEaseEnd=t,this._easeFrameId=this._requestRenderFrame(this._renderFrameCallback))}_normalizeBearing(e,t){e=sn(e,-180,180);let n=Math.abs(e-t);return Math.abs(e-360-t)MapLibre`};var cp=class{constructor(e=sp){this._toggleAttribution=()=>{this._container.classList.contains(`maplibregl-compact`)&&(this._container.classList.contains(`maplibregl-compact-show`)?(this._container.setAttribute(`open`,``),this._container.classList.remove(`maplibregl-compact-show`)):(this._container.classList.add(`maplibregl-compact-show`),this._container.removeAttribute(`open`)))},this._updateData=e=>{e&&(e.type===`terrain`||e.dataType===`style`||e.dataType===`source`&&(e.sourceDataType===`metadata`||e.sourceDataType===`visibility`))&&this._updateAttributions()},this._updateCompact=()=>{this._map.getCanvasContainer().offsetWidth<=640||this._compact?this._compact===!1?this._container.setAttribute(`open`,``):!this._container.classList.contains(`maplibregl-compact`)&&!this._container.classList.contains(`maplibregl-attrib-empty`)&&(this._container.setAttribute(`open`,``),this._container.classList.add(`maplibregl-compact`,`maplibregl-compact-show`)):(this._container.setAttribute(`open`,``),this._container.classList.contains(`maplibregl-compact`)&&this._container.classList.remove(`maplibregl-compact`,`maplibregl-compact-show`))},this._updateCompactMinimize=()=>{this._container.classList.contains(`maplibregl-compact`)&&this._container.classList.contains(`maplibregl-compact-show`)&&this._container.classList.remove(`maplibregl-compact-show`)},this.options=e}getDefaultPosition(){return`bottom-right`}onAdd(e){return this._map=e,this._compact=this.options.compact,this._container=W.create(`details`,`maplibregl-ctrl maplibregl-ctrl-attrib`),this._compactButton=W.create(`summary`,`maplibregl-ctrl-attrib-button`,this._container),this._compactButton.addEventListener(`click`,this._toggleAttribution),this._setElementTitle(this._compactButton,`ToggleAttribution`),this._innerContainer=W.create(`div`,`maplibregl-ctrl-attrib-inner`,this._container),this._updateAttributions(),this._updateCompact(),this._map.on(`styledata`,this._updateData),this._map.on(`sourcedata`,this._updateData),this._map.on(`terrain`,this._updateData),this._map.on(`resize`,this._updateCompact),this._map.on(`drag`,this._updateCompactMinimize),this._container}onRemove(){this._container.remove(),this._map.off(`styledata`,this._updateData),this._map.off(`sourcedata`,this._updateData),this._map.off(`terrain`,this._updateData),this._map.off(`resize`,this._updateCompact),this._map.off(`drag`,this._updateCompactMinimize),this._map=void 0,this._compact=void 0,this._attribHTML=void 0}_setElementTitle(e,t){let n=this._map._getUIString(`AttributionControl.${t}`);e.title=n,e.setAttribute(`aria-label`,n)}_updateAttributions(){if(!this._map.style)return;let e=[];if(this.options.customAttribution&&(Array.isArray(this.options.customAttribution)?e=e.concat(this.options.customAttribution.map(e=>typeof e==`string`?e:``)):typeof this.options.customAttribution==`string`&&e.push(this.options.customAttribution)),this._map.style.stylesheet){let e=this._map.style.stylesheet;this.styleOwner=e.owner,this.styleId=e.id}let t=this._map.style.tileManagers;for(let n in t){let r=t[n];if(r.used||r.usedForTerrain){let t=r.getSource();t.attribution&&!e.includes(t.attribution)&&e.push(t.attribution)}}e=e.filter(e=>String(e).trim()),e.sort((e,t)=>e.length-t.length),e=e.filter((t,n)=>{for(let r=n+1;r{let e=this._container.children;if(e.length){let t=e[0];this._map.getCanvasContainer().offsetWidth<=640||this._compact?this._compact!==!1&&t.classList.add(`maplibregl-compact`):t.classList.remove(`maplibregl-compact`)}},this.options=e}getDefaultPosition(){return`bottom-left`}onAdd(e){this._map=e,this._compact=this.options?.compact,this._container=W.create(`div`,`maplibregl-ctrl`);let t=W.create(`a`,`maplibregl-ctrl-logo`);return t.target=`_blank`,t.rel=`noopener nofollow`,t.href=`https://maplibre.org/`,t.setAttribute(`aria-label`,this._map._getUIString(`LogoControl.Title`)),t.setAttribute(`rel`,`noopener nofollow`),this._container.appendChild(t),this._container.style.display=`block`,this._map.on(`resize`,this._updateCompact),this._updateCompact(),this._container}onRemove(){this._container.remove(),this._map.off(`resize`,this._updateCompact),this._map=void 0,this._compact=void 0}},up=class{constructor(){this._queue=[],this._id=0,this._cleared=!1,this._currentlyRunning=!1}add(e){let t=++this._id;return this._queue.push({callback:e,id:t,cancelled:!1}),t}remove(e){let t=this._currentlyRunning,n=t?this._queue.concat(t):this._queue;for(let t of n)if(t.id===e){t.cancelled=!0;return}}run(e=0){if(this._currentlyRunning)throw Error(`Attempting to run(), but is already running.`);let t=this._currentlyRunning=this._queue;this._queue=[];for(let n of t)if(!n.cancelled&&(n.callback(e),this._cleared))break;this._cleared=!1,this._currentlyRunning=!1}clear(){this._currentlyRunning&&(this._cleared=!0),this._queue=[]}};const dp=yr([{name:`a_pos3d`,type:`Int16`,components:3}]);var fp=class extends _n{constructor(e){super(),this._lastTilesetChange=U(),this.tileManager=e,this._tiles={},this._renderableTilesKeys=[],this._sourceTileCache={},this.minzoom=0,this.maxzoom=22,this.deltaZoom=1,this.tileSize=e._source.tileSize*2**this.deltaZoom,e.usedForTerrain=!0,e.tileSize=this.tileSize}destruct(){this.tileManager.usedForTerrain=!1,this.tileManager.tileSize=null,this.releaseAllRTT()}getSource(){return this.tileManager._source}update(e,t){this.tileManager.update(e,t),this._renderableTilesKeys=[];let n={};for(let r of Fa(e,{tileSize:this.tileSize,minzoom:this.minzoom,maxzoom:this.maxzoom,reparseOverscaled:!1,terrain:t,calculateTileZoom:this.tileManager._source.calculateTileZoom}))n[r.key]=!0,this._renderableTilesKeys.push(r.key),this._tiles[r.key]||(r.terrainRttPosMatrix32f=new Float32Array(16),fr(r.terrainRttPosMatrix32f,0,M,M,0,0,1),this._tiles[r.key]=new ya(r,this.tileSize),this._lastTilesetChange=U());for(let e in this._tiles)n[e]||(this._tiles[e].releaseRTT(this.tileManager.map.painter),delete this._tiles[e])}releaseRTT(e){for(let t in this._tiles){let n=this._tiles[t];(n.tileID.equals(e)||n.tileID.isChildOf(e)||e.isChildOf(n.tileID))&&n.releaseRTT(this.tileManager.map.painter)}}releaseAllRTT(){for(let e in this._tiles)this._tiles[e].releaseRTT(this.tileManager.map.painter)}getRenderableTiles(){return this._renderableTilesKeys.map(e=>this.getTileByID(e))}getTileByID(e){return this._tiles[e]}getTerrainCoords(e,t){return t?this._getTerrainCoordsForTileRanges(e,t):this._getTerrainCoordsForRegularTile(e)}_getTerrainCoordsForRegularTile(e){let t={};for(let n of this._renderableTilesKeys){let r=this._tiles[n].tileID,i=e.clone(),a=c();if(r.canonical.equals(e.canonical))fr(a,0,M,M,0,0,1);else if(r.canonical.isChildOf(e.canonical)){let t=r.canonical.z-e.canonical.z,n=r.canonical.x-(r.canonical.x>>t<>t<>t;fr(a,0,o,o,0,0,1),F(a,a,[-n*o,-i*o,0])}else if(e.canonical.isChildOf(r.canonical)){let t=e.canonical.z-r.canonical.z,n=e.canonical.x-(e.canonical.x>>t<>t<>t;fr(a,0,M,M,0,0,1),F(a,a,[n*o,i*o,0]),rr(a,a,[1/2**t,1/2**t,0])}else continue;i.terrainRttPosMatrix32f=new Float32Array(a),t[n]=i}return t}_getTerrainCoordsForTileRanges(e,t){let n={};for(let r of this._renderableTilesKeys){let i=this._tiles[r].tileID;if(!this._isWithinTileRanges(i,t))continue;let a=e.clone(),o=c();if(i.canonical.z===e.canonical.z){let t=e.canonical.x-i.canonical.x+e.wrap*(1<e.canonical.z){let t=i.canonical.z-e.canonical.z,n=i.canonical.x-(i.canonical.x>>t<>t<>t),s=e.canonical.y-(i.canonical.y>>t),c=M>>t;fr(o,0,c,c,0,0,1),F(o,o,[-n*c+a*M,-r*c+s*M,0])}else{let t=e.canonical.z-i.canonical.z,n=e.canonical.x-(e.canonical.x>>t<>t<>t)-i.canonical.x,s=(e.canonical.y>>t)-i.canonical.y,c=M<n.maxzoom&&(r=n.maxzoom),r=n.minzoom&&!i?.dem;)i=this.findTileInCaches(e.scaledTo(r--).key);return i}findTileInCaches(e){let t=this.tileManager.getTileByID(e);return t||(t=this.tileManager._outOfViewCache.getByKey(e),t)}anyTilesAfterTime(e=U()){return this._lastTilesetChange>=e}_isWithinTileRanges(e,t){let n=t[e.canonical.z];return!!n&&(e.wrap>n.minWrap||e.wrap=n.minTileXWrapped&&e.canonical.x<=n.maxTileXWrapped&&e.canonical.y>=n.minTileY&&e.canonical.y<=n.maxTileY)}},pp=class{constructor(e,t,n,r=`auto`){this._meshCache={},this.painter=e,this.tileManager=new fp(t),this.options=n,this.exaggeration=typeof n.exaggeration==`number`?n.exaggeration:1,this._terrainSkirtLength=r,this.qualityFactor=2,this.meshSize=128,this._demMatrixCache={},this.coordsIndex=[],this._coordsTextureSize=1024}destroy(){this._fbo&&=(this._fbo.destroy(),null),this._fboCoordsTexture&&=(this._fboCoordsTexture.destroy(),null),this._fboDepthTexture&&=(this._fboDepthTexture.destroy(),null),this._emptyDemTexture&&=(this._emptyDemTexture.destroy(),null),this._emptyDepthTexture&&=(this._emptyDepthTexture.destroy(),null),this._coordsTexture&&=(this._coordsTexture.destroy(),null);for(let e in this._meshCache)this._meshCache[e].destroy();this._meshCache={},this.tileManager.destruct()}getDEMElevation(e,t,n,r=M){let i=e.normalizeCoordinates(t,n,r);if(!i)return 0;let a=this.getTerrainData(i.tileID),o=a.tile?.dem;if(!o)return 0;let s=vr([],[i.x/r*M,i.y/r*M],a.u_terrain_matrix),c=[s[0]*o.dim,s[1]*o.dim];return o.sampleBilinear(c[0],c[1])}getElevationForLngLatZoom(e,t){if(!sr(t,e.wrap()))return 0;let{tileID:n,mercatorX:r,mercatorY:i}=this._getOverscaledTileIDFromLngLatZoom(e,t);return this.getElevation(n,r%M,i%M,M)}getElevationForLngLat(e,t){let n=Fa(t,{maxzoom:this.tileManager.maxzoom,minzoom:this.tileManager.minzoom,tileSize:512,terrain:this}),r=0;for(let e of n)e.canonical.z>r&&(r=Math.min(e.canonical.z,this.tileManager.maxzoom));return this.getElevationForLngLatZoom(e,r)}getElevation(e,t,n,r=M){return this.getDEMElevation(e,t,n,r)*this.exaggeration}getTerrainData(e){if(!this._emptyDemTexture){let e=this.painter.context,t=new yt({width:1,height:1},new Uint8Array(4));this._emptyDepthTexture=new Lt(e,t,e.gl.RGBA,{premultiply:!1}),this._emptyDemUnpack=[0,0,0,0],this._emptyDemTexture=new Lt(e,new yt({width:1,height:1}),e.gl.RGBA,{premultiply:!1}),this._emptyDemTexture.bind(e.gl.NEAREST,e.gl.CLAMP_TO_EDGE),this._emptyDemMatrix=qt([])}let t=this.tileManager.getSourceTile(e,!0);if(t?.dem&&(!t.demTexture||t.needsTerrainPrepare)){let e=this.painter.context;t.demTexture||=this.painter.getTileTexture(t.dem.stride),t.demTexture?t.demTexture.update(t.dem.getPixels(),{premultiply:!1}):t.demTexture=new Lt(e,t.dem.getPixels(),e.gl.RGBA,{premultiply:!1}),t.demTexture.bind(e.gl.NEAREST,e.gl.CLAMP_TO_EDGE),t.needsTerrainPrepare=!1}let n=t&&t.toString()+t.tileID.key+e.key;if(n&&!this._demMatrixCache[n]){let r=this.tileManager.getSource().maxzoom,i=e.canonical.z-t.tileID.canonical.z;e.overscaledZ>e.canonical.z&&(e.canonical.z>=r?i=e.canonical.z-r:a(`cannot calculate elevation if elevation maxzoom > source.maxzoom`));let o=e.canonical.x-(e.canonical.x>>i<>i<>8<<4|e>>8,t[n+3]=0;let n=new Lt(e,new yt({width:this._coordsTextureSize,height:this._coordsTextureSize},new Uint8Array(t.buffer)),e.gl.RGBA,{premultiply:!1});return n.bind(e.gl.NEAREST,e.gl.CLAMP_TO_EDGE),this._coordsTexture=n,n}pointCoordinate(e){this.painter.maybeDrawDepth(!0),this.painter.maybeDrawCoords();let t=new Uint8Array(4),n=this.painter.context,r=n.gl,i=Math.round(e.x*this.painter.pixelRatio/devicePixelRatio),a=Math.round(e.y*this.painter.pixelRatio/devicePixelRatio),o=Math.round(this.painter.height/devicePixelRatio);n.bindFramebuffer.set(this.getFramebuffer(`coords`).framebuffer),r.readPixels(i,o-a-1,1,1,r.RGBA,r.UNSIGNED_BYTE,t),n.bindFramebuffer.set(null);let s=t[0]+(t[2]>>4<<8),c=t[1]+((t[2]&15)<<8),l=this.coordsIndex[255-t[3]],u=l&&this.tileManager.getTileByID(l);if(!u)return null;let d=this._coordsTextureSize,f=(1<0,n=t&&e.canonical.y===0,r=t&&e.canonical.y===(1<!e._layers[n].isHidden(t));let n=new Set;for(let t of this._renderableLayerIds){let r=e._layers[t],i=r.source;i&&mp[r.type]&&n.add(i)}this._coordsAscending={},this._rttFingerprints={};for(let t of n){let n=e.tileManagers[t];if(!n)continue;this._coordsAscending[t]={};let r=this._coordsAscending[t],i=n.getSource(),a=i instanceof ia?i.terrainTileRanges:null;for(let e of n.getVisibleCoordinates()){let t=this.terrain.tileManager.getTerrainCoords(e,a);for(let e in t)r[e]||=[],r[e].push(t[e])}this._rttFingerprints[t]={};let o=this._rttFingerprints[t],s=n.getState().revision;for(let e in r)o[e]=`${r[e].map(e=>e.key).sort().join()}#${s}`}for(let e of this._renderableTiles)for(let t in this._rttFingerprints){let n=this._rttFingerprints[t][e.tileID.key];n&&n!==e.rttFingerprint[t]&&e.releaseRTT(this.painter)}}renderLayer(e,t){if(e.isHidden(this.painter.transform.zoom))return!1;let n={...t,isRenderingToTexture:!0},r=e.type,i=this.painter,a=this._renderableLayerIds[this._renderableLayerIds.length-1]===e.id;if(mp[r]&&((!this._prevType||!mp[this._prevType])&&this._stacks.push([]),this._prevType=r,this._stacks[this._stacks.length-1].push(e.id),!a))return!0;if(mp[this._prevType]||mp[r]&&a){this._prevType=r;let e=this._stacks.length-1,t=this._stacks[e]||[];for(let r of this._renderableTiles){if(this._rttTiles.push(r),r.getRTT(e))continue;let a=r.acquireRTT(i,e,this.rttSize);i.bindRTT(a),i.context.clear({color:V.transparent,stencil:0}),i.currentStencilSource=void 0;for(let e of t){let t=i.style._layers[e],a=t.source?this._coordsAscending[t.source][r.tileID.key]:[r.tileID];i.context.viewport.set([0,0,this.rttSize,this.rttSize]),i.renderTileClippingMasks(t,a,!0),i.renderLayer(i,i.style.tileManagers[t.source],t,a,n),t.source&&(r.rttFingerprint[t.source]=this._rttFingerprints[t.source][r.tileID.key])}}return Jd(this.painter,this.terrain,this._rttTiles,n),this._rttTiles=[],mp[r]}return!1}};const gp={"AttributionControl.ToggleAttribution":`Toggle attribution`,"AttributionControl.MapFeedback":`Map feedback`,"FullscreenControl.Enter":`Enter fullscreen`,"FullscreenControl.Exit":`Exit fullscreen`,"GeolocateControl.FindMyLocation":`Find my location`,"GeolocateControl.LocationNotAvailable":`Location not available`,"LogoControl.Title":`MapLibre logo`,"Map.Title":`Map`,"Marker.Title":`Map marker`,"NavigationControl.ResetBearing":`Drag to rotate map, click to reset north`,"NavigationControl.ZoomIn":`Zoom in`,"NavigationControl.ZoomOut":`Zoom out`,"Popup.Close":`Close popup`,"ScaleControl.Feet":`ft`,"ScaleControl.Meters":`m`,"ScaleControl.Kilometers":`km`,"ScaleControl.Miles":`mi`,"ScaleControl.NauticalMiles":`nm`,"GlobeControl.Enable":`Enable globe`,"GlobeControl.Disable":`Disable globe`,"TerrainControl.Enable":`Enable terrain`,"TerrainControl.Disable":`Disable terrain`,"CooperativeGesturesHandler.WindowsHelpText":`Use Ctrl + scroll to zoom the map`,"CooperativeGesturesHandler.MacHelpText":`Use ⌘ + scroll to zoom the map`,"CooperativeGesturesHandler.MobileHelpText":`Use two fingers to move the map`},_p=br,vp={hash:!1,interactive:!0,bearingSnap:7,zoomSnap:0,attributionControl:sp,maplibreLogo:!1,refreshExpiredTiles:!0,canvasContextAttributes:{antialias:!1,preserveDrawingBuffer:!1,powerPreference:`high-performance`,failIfMajorPerformanceCaveat:!1,desynchronized:!1,contextType:void 0},scrollZoom:!0,minZoom:-2,maxZoom:22,minPitch:0,maxPitch:60,boxZoom:!0,dragRotate:!0,dragPan:!0,keyboard:!0,doubleClickZoom:!0,touchZoomRotate:!0,touchPitch:!0,cooperativeGestures:!1,trackResize:!0,center:[0,0],elevation:0,zoom:0,bearing:0,pitch:0,roll:0,renderWorldCopies:!0,maxTileCacheSize:null,maxTileCacheZoomLevels:C.MAX_TILE_CACHE_ZOOM_LEVELS,transformRequest:null,transformCameraUpdate:null,transformConstrain:null,fadeDuration:300,crossSourceCollisions:!0,clickTolerance:3,localIdeographFontFamily:`sans-serif`,pitchWithRotate:!0,rollEnabled:!1,reduceMotion:void 0,validateStyle:!0,maxCanvasSize:[4096,4096],cancelPendingTileRequestsWhileZooming:!0,centerClampedToGround:!0,terrainSkirtLength:`auto`,zoomLevelsToOverscale:4,anisotropicFilterPitch:20};var yp=class extends _n{get _ownerWindow(){return this._container?.ownerDocument?.defaultView||window}constructor(e){super(),this._idleTriggered=!1,this._crossFadingFactor=1,this._renderTaskQueue=new up,this._controls=[],this._mapId=dn(),this._missingStyleImageResolver=null,this._lostContextStyle={style:null,images:null},this._contextLost=e=>{if(e.preventDefault(),this._frameRequest&&=(this._frameRequest.abort(),null),this.painter.destroy(),this._lostContextStyle=this._getStyleAndImages(),!this.style){this.fire(new Ur(`webglcontextlost`,{originalEvent:e}));return}for(let e of Object.values(this.style._layers))if(e.type===`custom`&&console.warn(`Custom layer with id '${e.id}' cannot be restored after WebGL context loss. You will need to re-add it manually after context restoration.`),e._listeners)for(let[t]of Object.entries(e._listeners))console.warn(`Custom layer with id '${e.id}' had event listeners for event '${t}' which cannot be restored after WebGL context loss. You will need to re-add them manually after context restoration.`);this.style.destroy(),this.style=null,this.fire(new Ur(`webglcontextlost`,{originalEvent:e}))},this._contextRestored=e=>{this._lostContextStyle.style&&this.setStyle(this._lostContextStyle.style,{diff:!1}),this._lostContextStyle.images&&this.style&&(this.style.imageManager.images=this._lostContextStyle.images),this._lostContextStyle={style:null,images:null},this._setupPainter(),this.painter&&(this.resize(),this._update(),this._resizeInternal(),this.fire(new Ur(`webglcontextrestored`,{originalEvent:e})))},this._onMapScroll=e=>{if(e.target===this._container)return this._container.scrollTop=0,this._container.scrollLeft=0,!1},this._onWindowOnline=()=>{this._update()};let t={...vp,...e,canvasContextAttributes:{...vp.canvasContextAttributes,...e.canvasContextAttributes}};if(t.minZoom!=null&&t.maxZoom!=null&&t.minZoom>t.maxZoom)throw Error(`maxZoom must be greater than or equal to minZoom`);if(t.minPitch!=null&&t.maxPitch!=null&&t.minPitch>t.maxPitch)throw Error(`maxPitch must be greater than or equal to minPitch`);if(t.minPitch!=null&&t.minPitch<0)throw Error(`minPitch must be greater than or equal to 0`);if(t.maxPitch!=null&&t.maxPitch>180)throw Error(`maxPitch must be less than or equal to 180`);if(this._camera=new op({minZoom:t.minZoom,maxZoom:t.maxZoom,minPitch:t.minPitch,maxPitch:t.maxPitch,bearingSnap:t.bearingSnap,zoomSnap:t.zoomSnap,renderWorldCopies:t.renderWorldCopies,centerClampedToGround:t.centerClampedToGround,terrain:this.terrain,transformConstrain:t.transformConstrain,requestRenderFrame:e=>this._requestRenderFrame(e),cancelRenderFrame:e=>this._cancelRenderFrame(e),transformCameraUpdate:t.transformCameraUpdate,stopHandlers:()=>this._handlers?.stop(!1)}),this._camera.setEventedParent(this),this._interactive=t.interactive,this._maxTileCacheSize=t.maxTileCacheSize,this._maxTileCacheZoomLevels=t.maxTileCacheZoomLevels,this._canvasContextAttributes={...t.canvasContextAttributes},this._trackResize=t.trackResize===!0,this._terrainSkirtLength=t.terrainSkirtLength,this._refreshExpiredTiles=t.refreshExpiredTiles===!0,this._fadeDuration=t.fadeDuration,this._crossSourceCollisions=t.crossSourceCollisions===!0,this._collectResourceTiming=t.collectResourceTiming===!0,this._locale={...gp,...t.locale},this._clickTolerance=t.clickTolerance,this._overridePixelRatio=t.pixelRatio,this._maxCanvasSize=t.maxCanvasSize,this._zoomLevelsToOverscale=t.zoomLevelsToOverscale,this.cancelPendingTileRequestsWhileZooming=t.cancelPendingTileRequestsWhileZooming===!0,this.setAnisotropicFilterPitch(t.anisotropicFilterPitch),t.reduceMotion!==void 0&&(Dr.prefersReducedMotion=t.reduceMotion),this._imageQueueHandle=Mr.addThrottleControl(()=>this.isMoving()),this._requestManager=new Nr(t.transformRequest),this._container=this._resolveContainer(t.container),t.maxBounds&&this.setMaxBounds(t.maxBounds),this._setupContainer(),this._setupPainter(),!this.painter)return;this.on(`move`,()=>this._update(!1)),this.on(`moveend`,()=>this._update(!1)),this.on(`zoom`,()=>this._update(!0)),this.on(`terrain`,()=>{this.painter.terrainFacilitator.depthDirty=!0,this._update(!0)}),this.once(`idle`,()=>this._idleTriggered=!0),this._handlers=new ap(this,this._camera,t),typeof window<`u`&&(this._ownerWindow.addEventListener(`online`,this._onWindowOnline,!1),this._setupResizeObserver());let n=typeof t.hash==`string`&&t.hash||void 0;this._hash=t.hash?new rf(n).addTo(this):void 0,this._hash?._onHashChange()||(this.jumpTo({center:t.center,elevation:t.elevation,zoom:t.zoom,bearing:t.bearing,pitch:t.pitch,roll:t.roll}),t.bounds&&(this.resize(),this.fitBounds(t.bounds,L({},t.fitBoundsOptions,{duration:0}))));let r=typeof t.style==`string`||t.style?.projection?.type!==`globe`;this.resize(null,r),this._localIdeographFontFamily=t.localIdeographFontFamily,this._validateStyle=t.validateStyle,t.style&&this.setStyle(t.style,{localIdeographFontFamily:t.localIdeographFontFamily}),t.attributionControl&&this.addControl(new cp(typeof t.attributionControl==`boolean`?void 0:t.attributionControl)),t.maplibreLogo&&this.addControl(new lp,t.logoPosition),this.on(`style.load`,()=>{if(r||this._resizeTransform(),this._camera.transform.unmodified){let e=Yt(this.style.stylesheet,[`center`,`zoom`,`bearing`,`pitch`,`roll`]);this.jumpTo(e)}}),this.on(`data`,e=>{this._update(e.dataType===`style`),this.fire(e.dataType===`style`?new Ir(`styledata`,e):new K(`sourcedata`,e))}),this.on(`dataloading`,e=>{this.fire(e.dataType===`style`?new Ir(`styledataloading`,e):new K(`sourcedataloading`,e))}),this.on(`dataabort`,e=>{this.fire(new K(`sourcedataabort`,e))})}_getMapId(){return this._mapId}setGlobalStateProperty(e,t){return this.style.setGlobalStateProperty(e,t),this._update(!0)}getGlobalState(){return this.style.getGlobalState()}addControl(e,t){if(t===void 0&&(t=e.getDefaultPosition?e.getDefaultPosition():`top-right`),!e?.onAdd)return this.fire(new R(Error(`Invalid argument to map.addControl(). Argument must be a control with onAdd and onRemove methods.`)));let n=e.onAdd(this);this._controls.push(e);let r=this._controlPositions[t];return t.includes(`bottom`)?r.insertBefore(n,r.firstChild):r.appendChild(n),this}removeControl(e){if(!e?.onRemove)return this.fire(new R(Error(`Invalid argument to map.removeControl(). Argument must be a control with onAdd and onRemove methods.`)));let t=this._controls.indexOf(e);return t>-1&&this._controls.splice(t,1),e.onRemove(this),this}hasControl(e){return this._controls.includes(e)}coveringTiles(e){return Fa(this._camera.transform,e)}setTransformCameraUpdate(e){this._camera.transformCameraUpdate=e}getCenter(){return new B(this._camera.transform.center.lng,this._camera.transform.center.lat)}setCenter(e,t){return this._camera.setCenter(e,t),this}getCenterElevation(){return this._camera.transform.elevation}setCenterElevation(e,t){return this._camera.setCenterElevation(e,t),this}setCenterClampedToGround(e){this._camera.setCenterClampedToGround(e)}panBy(e,t,n){return this._camera.panBy(e,t,n),this}panTo(e,t,n){return this._camera.panTo(e,t,n),this}getZoom(){return this._camera.transform.zoom}setZoom(e,t){return this._camera.setZoom(e,t),this}zoomTo(e,t,n){return this._camera.zoomTo(e,t,n),this}zoomIn(e,t){return this._camera.zoomIn(e,t),this}zoomOut(e,t){return this._camera.zoomOut(e,t),this}getVerticalFieldOfView(){return this._camera.transform.fov}setVerticalFieldOfView(e,t){return this._camera.setVerticalFieldOfView(e,t),this}getBearing(){return this._camera.transform.bearing}setBearing(e,t){return this._camera.setBearing(e,t),this}getZoomSnap(){return this._camera.getZoomSnap()}setZoomSnap(e){return this._camera.setZoomSnap(e),this}getPadding(){return this._camera.transform.padding}setPadding(e,t){return this._camera.setPadding(e,t),this}rotateTo(e,t,n){return this._camera.rotateTo(e,t,n),this}resetNorth(e,t){return this._camera.resetNorth(e,t),this}resetNorthPitch(e,t){return this._camera.resetNorthPitch(e,t),this}snapToNorth(e,t){return this._camera.snapToNorth(e,t),this}getPitch(){return this._camera.transform.pitch}setPitch(e,t){return this._camera.setPitch(e,t),this}getRoll(){return this._camera.transform.roll}setRoll(e,t){return this._camera.setRoll(e,t),this}cameraForBounds(e,t){return this._camera.cameraForBounds(e,t)}fitBounds(e,t,n){return this._camera.fitBounds(e,t,n),this}fitScreenCoordinates(e,t,n,r,i){return this._camera.fitScreenCoordinates(e,t,n,r,i),this}jumpTo(e,t){return this._camera.jumpTo(e,t),this}calculateCameraOptionsFromCameraLngLatAltRotation(e,t,n,r,i){return this._camera.calculateCameraOptionsFromCameraLngLatAltRotation(e,t,n,r,i)}easeTo(e,t){return this._camera.easeTo(e,t),this}flyTo(e,t){return this._camera.flyTo(e,t),this}stop(){return this._camera.stop(),this}queryTerrainElevation(e){return this.terrain?this.terrain.getElevationForLngLat(B.convert(e),this._camera.transform):null}getCenterClampedToGround(){return this._camera.getCenterClampedToGround()}calculateCameraOptionsFromTo(e,t,n,r){return r==null&&this.terrain&&(r=this.terrain.getElevationForLngLat(n,this._camera.transform)),this._camera.calculateCameraOptionsFromTo(e,t,n,r)}resize(e,t=!0){if(this._lostContextStyle.style!==null)return this;this._resizeInternal(t);let n=!this._camera._moving;return n&&(this.stop(),this.fire(new G(`movestart`,e)).fire(new G(`move`,e))),this.fire(new Pr(`resize`,e)),n&&this.fire(new G(`moveend`,e)),this}_resizeInternal(e=!0){let[t,n]=this._containerDimensions(),r=this._getClampedPixelRatio(t,n);if(this._resizeCanvas(t,n,r),this.painter.resize(t,n,r),this.painter.overLimit()){let e=this.painter.context.gl;this._maxCanvasSize=[e.drawingBufferWidth,e.drawingBufferHeight];let r=this._getClampedPixelRatio(t,n);this._resizeCanvas(t,n,r),this.painter.resize(t,n,r)}this._resizeTransform(e)}_resizeTransform(e=!0){let[t,n]=this._containerDimensions();this._camera.transform.resize(t,n,e),this._camera._requestedCameraState?.resize(t,n,e)}_getClampedPixelRatio(e,t){let{0:n,1:r}=this._maxCanvasSize,i=this.getPixelRatio(),a=e*i,o=t*i,s=a>n?n/a:1,c=o>r?r/o:1;return Math.min(s,c)*i}getPixelRatio(){return this._overridePixelRatio??devicePixelRatio}setPixelRatio(e){this._overridePixelRatio=e,this.resize()}getBounds(){return this._camera.transform.getBounds()}getMaxBounds(){return this._camera.transform.getMaxBounds()}setMaxBounds(e){return this._camera.transform.setMaxBounds(Ri.convert(e)),this._update()}setMinZoom(e){if(e??=-2,e>=-2&&e<=this._camera.transform.maxZoom){let t=this._camera.transform.zoom,n=this._camera.getTransformForUpdate();return n.setMinZoom(e),this._camera.applyUpdatedTransform(n),this._update(),t!==this._camera.transform.zoom&&this.fire(new G(`zoomstart`)).fire(new G(`zoom`)).fire(new G(`zoomend`)).fire(new G(`movestart`)).fire(new G(`move`)).fire(new G(`moveend`)),this}else throw Error(`minZoom must be between -2 and the current maxZoom, inclusive`)}getMinZoom(){return this._camera.transform.minZoom}setMaxZoom(e){if(e??=22,e>=this._camera.transform.minZoom){let t=this._camera.transform.zoom,n=this._camera.getTransformForUpdate();return n.setMaxZoom(e),this._camera.applyUpdatedTransform(n),this._update(),t!==this._camera.transform.zoom&&this.fire(new G(`zoomstart`)).fire(new G(`zoom`)).fire(new G(`zoomend`)).fire(new G(`movestart`)).fire(new G(`move`)).fire(new G(`moveend`)),this}else throw Error(`maxZoom must be greater than the current minZoom`)}getMaxZoom(){return this._camera.transform.maxZoom}setMinPitch(e){if(e??=0,e<0)throw Error(`minPitch must be greater than or equal to 0`);if(e>=0&&e<=this._camera.transform.maxPitch){let t=this._camera.transform.pitch,n=this._camera.getTransformForUpdate();return n.setMinPitch(e),this._camera.applyUpdatedTransform(n),this._update(),t!==this._camera.transform.pitch&&this.fire(new G(`pitchstart`)).fire(new G(`pitch`)).fire(new G(`pitchend`)).fire(new G(`movestart`)).fire(new G(`move`)).fire(new G(`moveend`)),this}else throw Error(`minPitch must be between 0 and the current maxPitch, inclusive`)}getMinPitch(){return this._camera.transform.minPitch}setMaxPitch(e){if(e??=60,e>180)throw Error(`maxPitch must be less than or equal to 180`);if(e>=this._camera.transform.minPitch){let t=this._camera.transform.pitch,n=this._camera.getTransformForUpdate();return n.setMaxPitch(e),this._camera.applyUpdatedTransform(n),this._update(),t!==this._camera.transform.pitch&&this.fire(new G(`pitchstart`)).fire(new G(`pitch`)).fire(new G(`pitchend`)).fire(new G(`movestart`)).fire(new G(`move`)).fire(new G(`moveend`)),this}else throw Error(`maxPitch must be greater than the current minPitch`)}getMaxPitch(){return this._camera.transform.maxPitch}getAnisotropicFilterPitch(){return this._anisotropicFilterPitch}setAnisotropicFilterPitch(e){if(e??=20,e>180)throw Error(`anisotropicFilterPitch must be less than or equal to 180`);if(e<0)throw Error(`anisotropicFilterPitch must be greater than or equal to 0`);return this._anisotropicFilterPitch=e,this._update()}getRenderWorldCopies(){return this._camera.transform.renderWorldCopies}setRenderWorldCopies(e){return this._camera.transform.setRenderWorldCopies(e),this._update()}setTransformConstrain(e){return this._camera.transform.setConstrainOverride(e),this._update()}project(e){return this._camera.transform.locationToScreenPoint(B.convert(e),this.style&&this.terrain)}unproject(e){return this._camera.transform.screenPointToLocation(z.convert(e),this.terrain)}isMoving(){return this._camera.isMoving()||this._handlers?.isMoving()||!1}isZooming(){return this._camera.isZooming()||this._handlers?.isZooming()||!1}isRotating(){return this._camera.isRotating()||this._handlers?.isRotating()||!1}_createDelegatedListener(e,t,n){if(e===`mouseenter`||e===`mouseover`){let r=!1;return{layers:t,listener:n,delegates:{mousemove:i=>{let a=t.filter(e=>this.getLayer(e)),o=a.length===0?[]:this.queryRenderedFeatures(i.point,{layers:a});o.length?r||(r=!0,n.call(this,new Lr(e,this,i.originalEvent,{features:o}))):r=!1},mouseout:()=>{r=!1}}}}else if(e===`mouseleave`||e===`mouseout`){let r=!1;return{layers:t,listener:n,delegates:{mousemove:i=>{let a=t.filter(e=>this.getLayer(e));(a.length===0?[]:this.queryRenderedFeatures(i.point,{layers:a})).length?r=!0:r&&(r=!1,n.call(this,new Lr(e,this,i.originalEvent)))},mouseout:t=>{r&&(r=!1,n.call(this,new Lr(e,this,t.originalEvent)))}}}}else{let r=e=>{let r=t.filter(e=>this.getLayer(e)),i=r.length===0?[]:this.queryRenderedFeatures(e.point,{layers:r});i.length&&(e.features=i,n.call(this,e),delete e.features)};return{layers:t,listener:n,delegates:{[e]:r}}}}_saveDelegatedListener(e,t){this._delegatedListeners||={},this._delegatedListeners[e]||=[],this._delegatedListeners[e].push(t)}_removeDelegatedListener(e,t,n){if(!this._delegatedListeners?.[e])return;let r=this._delegatedListeners[e];for(let e=0;et.includes(e))){for(let e in i.delegates)this.off(e,i.delegates[e]);r.splice(e,1);return}}}on(e,t,n){if(n===void 0)return super.on(e,t);let r=typeof t==`string`?[t]:t,i=this._createDelegatedListener(e,r,n);this._saveDelegatedListener(e,i);for(let e in i.delegates)this.on(e,i.delegates[e]);return{unsubscribe:()=>{this._removeDelegatedListener(e,r,n)}}}once(e,t,n){if(n===void 0)return super.once(e,t);let r=typeof t==`string`?[t]:t,i=this._createDelegatedListener(e,r,n);for(let t in i.delegates){let a=i.delegates[t];i.delegates[t]=(...t)=>{this._removeDelegatedListener(e,r,n),a(...t)}}this._saveDelegatedListener(e,i);for(let e in i.delegates)this.once(e,i.delegates[e]);return this}off(e,t,n){if(n===void 0)return super.off(e,t);let r=typeof t==`string`?[t]:t;return this._removeDelegatedListener(e,r,n),this}queryRenderedFeatures(e,t){if(!this.style)return[];let n,r=e instanceof z||Array.isArray(e),i=r?e:[[0,0],[this._camera.transform.width,this._camera.transform.height]];if(t||=(r?{}:e)||{},i instanceof z||typeof i[0]==`number`)n=[z.convert(i)];else{let e=z.convert(i[0]),t=z.convert(i[1]);n=[e,new z(t.x,e.y),t,new z(e.x,t.y),e]}return this.style.queryRenderedFeatures(n,t,this._camera.transform)}querySourceFeatures(e,t){return this.style.querySourceFeatures(e,t)}setStyle(e,t){return t=L({},{localIdeographFontFamily:this._localIdeographFontFamily,validate:this._validateStyle},t),t.diff!==!1&&t.localIdeographFontFamily===this._localIdeographFontFamily&&this.style&&e?(this._diffStyle(e,t),this):(this._localIdeographFontFamily=t.localIdeographFontFamily,this._updateStyle(e,t))}setTransformRequest(e){return this._requestManager.setTransformRequest(e),this}_getUIString(e){let t=this._locale[e];if(t==null)throw Error(`Missing UI string '${e}'`);return t}_updateStyle(e,t){if(this._diffStyleRequest?.abort(),this._diffStyleRequest=null,t.transformStyle&&this.style&&!this.style._loaded){this.style.once(`style.load`,()=>this._updateStyle(e,t));return}let n=this.style&&t.transformStyle?this.style.serialize():void 0;if(this.style&&(this.style.setEventedParent(null),this.style._remove(!e)),e)this.style=new bc(this,t||{});else return this._frameRequest&&=(this._frameRequest.abort(),null),this.style?.projection?.destroy(),delete this.style,this;return this.style.setEventedParent(this,{style:this.style}),typeof e==`string`?this.style.loadURL(e,t,n):this.style.loadJSON(e,t,n),this}_lazyInitEmptyStyle(){this.style||(this.style=new bc(this,{}),this.style.setEventedParent(this,{style:this.style}),this.style.loadEmpty())}async _diffStyle(e,t){if(this._diffStyleRequest?.abort(),typeof e==`string`){let n=e;this._diffStyleRequest=new AbortController;let r=this._diffStyleRequest;try{let e=await this._requestManager.transformRequest(n,`Style`);if(r.signal.aborted){this._diffStyleRequest=null;return}let i=await $n(e,r);this._diffStyleRequest=null,this._updateDiff(i.data,t)}catch(e){this._diffStyleRequest=null,Ae(e)||this.fire(new R(ut(e)))}}else typeof e==`object`&&(this._diffStyleRequest=null,this._updateDiff(e,t))}_updateDiff(e,t){try{this.style.setState(e,t)&&this._update(!0)}catch(n){a(`Unable to perform style diff: ${ut(n).message}. Rebuilding the style from scratch.`),this._updateStyle(e,t)}}getStyle(){if(this.style)return this.style.serialize()}_getStyleAndImages(){return this.style?{style:this.style.serialize(),images:this.style.imageManager.cloneImages()}:{style:null,images:{}}}isStyleLoaded(){if(!this.style){a(`There is no style added to the map.`);return}return this.style.loaded()}addSource(e,t){return this._lazyInitEmptyStyle(),this.style.addSource(e,t),this._update(!0)}isSourceLoaded(e){let t=this.style?.tileManagers[e];if(t===void 0){this.fire(new R(Error(`There is no tile manager with ID '${e}'`)));return}return t.loaded()}setTerrain(e,t={}){if(this.style._checkLoaded(),e&&Re(this,n.terrain,{value:e},t))return this;if(this._terrainDataCallback&&this.style.off(`data`,this._terrainDataCallback),!e)this.terrain&&this.terrain.destroy(),this.terrain=null,this.painter.renderToTexture=null,this._camera.terrain=null,this._camera.transform.setMinElevationForCurrentTile(0),this.getCenterClampedToGround()&&this._camera.transform.setElevation(0);else{let t=this.style.tileManagers[e.source];if(!t)throw Error(`cannot load terrain, because there exists no source with ID: ${e.source}`);this.terrain===null&&t.reload();for(let t in this.style._layers){let n=this.style._layers[t];n.type===`hillshade`&&n.source===e.source&&a(`You are using the same source for a hillshade layer and for 3D terrain. Please consider using two separate sources to improve rendering quality.`),n.type===`color-relief`&&n.source===e.source&&a(`You are using the same source for a color-relief layer and for 3D terrain. Please consider using two separate sources to improve rendering quality.`)}this.terrain=new pp(this.painter,t,e,this._terrainSkirtLength),this.painter.renderToTexture=new hp(this.painter,this.terrain),this._camera.terrain=this.terrain,this._camera.transform.setMinElevationForCurrentTile(this.terrain.getMinTileElevationForLngLatZoom(this._camera.transform.center,this._camera.transform.tileZoom)),this._camera.transform.setElevation(this.terrain.getElevationForLngLatZoom(this._camera.transform.center,this._camera.transform.tileZoom)),this._terrainDataCallback=t=>{t.dataType===`style`?this.terrain.tileManager.releaseAllRTT():t.dataType===`source`&&t.tile&&(t.sourceId===e.source&&!this._camera.elevationFreeze&&(this._camera.transform.setMinElevationForCurrentTile(this.terrain.getMinTileElevationForLngLatZoom(this._camera.transform.center,this._camera.transform.tileZoom)),this.getCenterClampedToGround()&&this._camera.transform.setElevation(this.terrain.getElevationForLngLatZoom(this._camera.transform.center,this._camera.transform.tileZoom))),t.source?.type===`image`?this.terrain.tileManager.releaseAllRTT():this.terrain.tileManager.releaseRTT(t.tile.tileID))},this.style.on(`data`,this._terrainDataCallback)}return this.fire(new Vr({terrain:e})),this}getTerrain(){return this.terrain?.options??null}areTilesLoaded(){let e=this.style?.tileManagers;for(let t of Object.values(e))if(!t.areTilesLoaded())return!1;return!0}removeSource(e){return this.style.removeSource(e),this._update(!0)}getSource(e){return this.style?.getSource(e)}setSourceTileLodParams(e,t,n){if(n){let r=this.getSource(n);if(!r)throw Error(`There is no source with ID "${n}", cannot set LOD parameters`);r.calculateTileZoom=Ma(Math.max(1,e),Math.max(1,t))}else for(let n in this.style.tileManagers)this.style.tileManagers[n].getSource().calculateTileZoom=Ma(Math.max(1,e),Math.max(1,t));return this._update(!0),this}refreshTiles(e,t){let n=this.style.tileManagers[e];if(!n)throw Error(`There is no tile manager with ID "${e}", cannot refresh tile`);t===void 0?n.reload(!0):n.refreshTiles(t.map(e=>new Kt(e.z,e.x,e.y)))}addImage(e,t,n={}){this._lazyInitEmptyStyle();let r=this._createStyleImage(t,n);return r?(this.style.addImage(e,r),r.userImage?.onAdd&&r.userImage.onAdd(this,e),this):this}setMissingStyleImageResolver(e){return this._missingStyleImageResolver=e,this.style?.setMissingImageResolver(e),this}_createStyleImage(e,t={}){let{pixelRatio:n=1,sdf:r=!1,stretchX:i,stretchY:a,content:o,textFitWidth:s,textFitHeight:c}=t;if(e instanceof HTMLImageElement||Ct(e)){let{width:t,height:l,data:u}=Dr.getImageData(e);return{data:new yt({width:t,height:l},u),pixelRatio:n,stretchX:i,stretchY:a,content:o,textFitWidth:s,textFitHeight:c,sdf:r,version:0}}else if(e.width===void 0||e.height===void 0)return this.fire(new R(Error("Invalid arguments to map.addImage(). The second argument must be an `HTMLImageElement`, `ImageData`, `ImageBitmap`, or object with `width`, `height`, and `data` properties with the same format as `ImageData`"))),null;else{let{width:t,height:l,data:u}=e,d=e;return{data:new yt({width:t,height:l},new Uint8Array(u)),pixelRatio:n,stretchX:i,stretchY:a,content:o,textFitWidth:s,textFitHeight:c,sdf:r,version:0,userImage:d}}}updateImage(e,t){let n=this.style.getImage(e);if(!n)return this.fire(new R(Error("The map has no image with that id. If you are adding a new image use `map.addImage(...)` instead.")));let{width:r,height:i,data:a}=t instanceof HTMLImageElement||Ct(t)?Dr.getImageData(t):t;if(r===void 0||i===void 0)return this.fire(new R(Error("Invalid arguments to map.updateImage(). The second argument must be an `HTMLImageElement`, `ImageData`, `ImageBitmap`, or object with `width`, `height`, and `data` properties with the same format as `ImageData`")));if(r!==n.data.width||i!==n.data.height)return this.fire(new R(Error(`The width and height of the updated image must be that same as the previous version of the image`)));let o=!(t instanceof HTMLImageElement||Ct(t));return n.data.replace(a,o),this.style.updateImage(e,n),this}getImage(e){return this.style.getImage(e)}hasImage(e){return e?!!this.style.getImage(e):(this.fire(new R(Error(`Missing required image id`))),!1)}removeImage(e){this.style.removeImage(e)}async loadImage(e){return Mr.getImage(await this._requestManager.transformRequest(e,`Image`),new AbortController)}listImages(){return this.style?.listImages()??[]}addLayer(e,t){return this._lazyInitEmptyStyle(),this.style.addLayer(e,t),this._update(!0)}moveLayer(e,t){return this.style.moveLayer(e,t),this._update(!0)}removeLayer(e){return this.style.removeLayer(e),this._update(!0)}getLayer(e){return this.style?.getLayer(e)}getLayersOrder(){return this.style?.getLayersOrder()??[]}setLayerZoomRange(e,t,n){return this.style.setLayerZoomRange(e,t,n),this._update(!0)}setFilter(e,t,n={}){return this.style?.setFilter(e,t,n),this._update(!0)}getFilter(e){return this.style.getFilter(e)}setPaintProperty(e,t,n,r={}){return this.style?.setPaintProperty(e,t,n,r),this._update(!0)}getPaintProperty(e,t){return this.style.getPaintProperty(e,t)}setLayoutProperty(e,t,n,r={}){return this.style.setLayoutProperty(e,t,n,r),this._update(!0)}getLayoutProperty(e,t){return this.style.getLayoutProperty(e,t)}setGlyphs(e,t={}){return this._lazyInitEmptyStyle(),this.style.setGlyphs(e,t),this._update(!0)}getGlyphs(){return this.style.getGlyphsUrl()}addSprite(e,t,n={}){return this._lazyInitEmptyStyle(),this.style.addSprite(e,t,n,e=>{e||this._update(!0)}),this}removeSprite(e){return this._lazyInitEmptyStyle(),this.style.removeSprite(e),this._update(!0)}getSprite(){return this.style.getSprite()}setSprite(e,t={}){return this._lazyInitEmptyStyle(),this.style.setSprite(e,t,e=>{e||this._update(!0)}),this}setLight(e,t={}){return this._lazyInitEmptyStyle(),this.style.setLight(e,t),this._update(!0)}getLight(){return this.style.getLight()}setSky(e,t={}){return this._lazyInitEmptyStyle(),this.style.setSky(e,t),this._update(!0)}getSky(){return this.style.getSky()}setFeatureState(e,t){return this.style.setFeatureState(e,t),this._update()}removeFeatureState(e,t){return this.style.removeFeatureState(e,t),this._update()}getFeatureState(e){return this.style.getFeatureState(e)}getContainer(){return this._container}getCanvasContainer(){return this._canvasContainer}getCanvas(){return this._canvas}_containerDimensions(){let e=0,t=0;return this._container&&(e=this._container.clientWidth||400,t=this._container.clientHeight||300),[e,t]}_setupResizeObserver(){let e=!1,t=nf(e=>{this._trackResize&&!this._removed&&(this.resize(e),this.redraw())},50),n=this._ownerWindow.ResizeObserver??ResizeObserver;this._resizeObserver=new n(n=>{if(!e){e=!0;return}t(n)}),this._resizeObserver.observe(this._container)}_resolveContainer(e){if(typeof e==`string`){let t=document.getElementById(e);if(!t)throw Error(`Container '${e}' not found.`);return t}if(e instanceof HTMLElement||e&&typeof e==`object`&&e.nodeType===1)return e;throw Error(`Invalid type: 'container' must be a String or HTMLElement.`)}_setupContainer(){let e=this._container;e.classList.add(`maplibregl-map`);let t=this._canvasContainer=W.create(`div`,`maplibregl-canvas-container`,e);this._interactive&&t.classList.add(`maplibregl-interactive`),this._canvas=W.create(`canvas`,`maplibregl-canvas`,t),this._canvas.addEventListener(`webglcontextlost`,this._contextLost,!1),this._canvas.addEventListener(`webglcontextrestored`,this._contextRestored,!1),this._canvas.setAttribute(`tabindex`,this._interactive?`0`:`-1`),this._canvas.setAttribute(`aria-label`,this._getUIString(`Map.Title`)),this._canvas.setAttribute(`role`,`region`);let n=this._containerDimensions(),r=this._getClampedPixelRatio(n[0],n[1]);this._resizeCanvas(n[0],n[1],r);let i=this._controlContainer=W.create(`div`,`maplibregl-control-container`,e),a=this._controlPositions={};for(let e of[`top-left`,`top-right`,`bottom-left`,`bottom-right`])a[e]=W.create(`div`,`maplibregl-ctrl-${e} `,i);this._container.addEventListener(`scroll`,this._onMapScroll,!1)}_resizeCanvas(e,t,n){this._canvas.width=Math.floor(n*e),this._canvas.height=Math.floor(n*t),this._canvas.style.width=`${e}px`,this._canvas.style.height=`${t}px`}_setupPainter(){let e={...this._canvasContextAttributes,alpha:!0,depth:!0,stencil:!0,premultipliedAlpha:!0},t=null;this._canvas.addEventListener(`webglcontextcreationerror`,e=>{t=e},{once:!0});let n=this._canvas.getContext(`webgl2`,e);if(!n){this.fire(new R(new tf(e,t)));return}this.painter=new ef(n,this._camera.transform)}migrateProjection(e,t){this._camera.migrateProjection(e,t),this.painter.transform=e,this.fire(new Hr({newProjection:this.style.projection.name}))}loaded(){return!this._styleDirty&&!this._sourcesDirty&&!!this.style&&this.style.loaded()}_update(e){return this.style?._loaded?(this._styleDirty||=e,this._sourcesDirty=!0,this.triggerRepaint(),this):this}_requestRenderFrame(e){return this._update(),this._renderTaskQueue.add(e)}_cancelRenderFrame(e){this._renderTaskQueue.remove(e)}_render(e){let t=this._idleTriggered?this._fadeDuration:0,n=this.style.projection?.transitionState>0;if(this.painter.context.setDirty(),this.painter.setBaseState(),this._renderTaskQueue.run(e),this._removed)return;let r=!1;if(this.style&&this._styleDirty){this._styleDirty=!1;let e=this._camera.transform.zoom,n=U();this.style.zoomHistory.update(e,n);let i=new ne(e,{now:n,fadeDuration:t,zoomHistory:this.style.zoomHistory,transition:this.style.getTransition()}),a=i.crossFadingFactor();(a!==1||a!==this._crossFadingFactor)&&(r=!0,this._crossFadingFactor=a),this.style.update(i)}let i=this.style.projection?.transitionState>0!==n;this.style.projection?.setErrorQueryLatitudeDegrees(this._camera.transform.center.lat),this._camera.transform.setTransitionState(this.style.projection?.transitionState,this.style.projection?.latitudeErrorCorrectionRadians),this.style&&(this._sourcesDirty||i)&&(this._sourcesDirty=!1,this.style._updateSources(this._camera.transform)),this.terrain?(this.terrain.tileManager.update(this._camera.transform,this.terrain),this._camera.transform.setMinElevationForCurrentTile(this.terrain.getMinTileElevationForLngLatZoom(this._camera.transform.center,this._camera.transform.tileZoom)),!this._camera.elevationFreeze&&this.getCenterClampedToGround()&&this._camera.transform.setElevation(this.terrain.getElevationForLngLatZoom(this._camera.transform.center,this._camera.transform.tileZoom))):(this._camera.transform.setMinElevationForCurrentTile(0),this.getCenterClampedToGround()&&this._camera.transform.setElevation(0)),this._placementDirty=this.style?._updatePlacement(this._camera.transform,this.showCollisionBoxes,t,this._crossSourceCollisions,i),this.painter.render(this.style,{showTileBoundaries:this.showTileBoundaries,showOverdrawInspector:this._showOverdrawInspector,rotating:this.isRotating(),zooming:this.isZooming(),moving:this.isMoving(),fadeDuration:t,showPadding:this.showPadding,anisotropicFilterPitch:this.getAnisotropicFilterPitch()}),this.fire(new Pr(`render`)),this.loaded()&&!this._loaded&&(this._loaded=!0,this.fire(new Pr(`load`))),this.style&&(this.style.hasTransitions()||r)&&(this._styleDirty=!0),this.style&&!this._placementDirty&&this.style._releaseSymbolFadeTiles();let a=this._sourcesDirty||this._styleDirty||this._placementDirty;return a||this._repaint?this.triggerRepaint():!this.isMoving()&&this.loaded()&&this.fire(new Pr(`idle`)),this._loaded&&!this._fullyLoaded&&!a&&(this._fullyLoaded=!0),this}redraw(){return this.style&&(this._frameRequest&&=(this._frameRequest.abort(),null),this._render(0)),this}remove(){this._hash&&this._hash.remove();for(let e of this._controls)e.onRemove(this);this._controls=[],this._frameRequest&&=(this._frameRequest.abort(),null),this._renderTaskQueue.clear(),this._diffStyleRequest?.abort(),this.painter.destroy(),this._handlers.destroy(),this.setStyle(null),typeof window<`u`&&this._ownerWindow.removeEventListener(`online`,this._onWindowOnline,!1),Mr.removeThrottleControl(this._imageQueueHandle),this._resizeObserver?.disconnect();let e=this.painter.context.gl.getExtension(`WEBGL_lose_context`);e?.loseContext&&e.loseContext(),this._canvas.removeEventListener(`webglcontextrestored`,this._contextRestored,!1),this._canvas.removeEventListener(`webglcontextlost`,this._contextLost,!1),this._canvasContainer.remove(),this._controlContainer.remove(),this._container.removeEventListener(`scroll`,this._onMapScroll,!1),this._container.classList.remove(`maplibregl-map`),this._removed=!0,this.fire(new Pr(`remove`))}triggerRepaint(){this.style&&!this._frameRequest&&(this._frameRequest=new AbortController,Dr.frame(this._frameRequest,e=>{this._frameRequest=null;try{this._render(e)}catch(e){if(!Ae(e))throw e}},()=>{},this._ownerWindow))}get showTileBoundaries(){return!!this._showTileBoundaries}set showTileBoundaries(e){this._showTileBoundaries!==e&&(this._showTileBoundaries=e,this._update())}get showPadding(){return!!this._showPadding}set showPadding(e){this._showPadding!==e&&(this._showPadding=e,this._update())}get showCollisionBoxes(){return!!this._showCollisionBoxes}set showCollisionBoxes(e){this._showCollisionBoxes!==e&&(this._showCollisionBoxes=e,e?this.style._generateCollisionBoxes():this._update())}get showOverdrawInspector(){return!!this._showOverdrawInspector}set showOverdrawInspector(e){this._showOverdrawInspector!==e&&(this._showOverdrawInspector=e,this._update())}get repaint(){return!!this._repaint}set repaint(e){this._repaint!==e&&(this._repaint=e,this.triggerRepaint())}get vertices(){return!!this._vertices}set vertices(e){this._vertices=e,this._update()}get version(){return _p}getCameraTargetElevation(){return this._camera.transform.elevation}getProjection(){return this.style.getProjection()}setProjection(e){return this._lazyInitEmptyStyle(),this.style.setProjection(e),this._update(!0)}};const bp={showCompass:!0,showZoom:!0,visualizePitch:!1,visualizeRoll:!0};var xp=class{constructor(e){this._updateZoomButtons=()=>{let e=this._map.getZoom(),t=e===this._map.getMaxZoom(),n=e===this._map.getMinZoom();this._zoomInButton.disabled=t,this._zoomOutButton.disabled=n,this._zoomInButton.setAttribute(`aria-disabled`,t.toString()),this._zoomOutButton.setAttribute(`aria-disabled`,n.toString())},this._rotateCompassArrow=()=>{let e=this._map.getPitch(),t=this._map.getRoll(),n=this._map.getBearing(),r=1/Math.cos(k(e))**.5;if(this.options.visualizePitch&&this.options.visualizeRoll){this._compassIcon.style.transform=`scale(${r}) rotateZ(${-t}deg) rotateX(${e}deg) rotateZ(${-n}deg)`;return}if(this.options.visualizePitch){this._compassIcon.style.transform=`scale(${r}) rotateX(${e}deg) rotateZ(${-n}deg)`;return}if(this.options.visualizeRoll){this._compassIcon.style.transform=`rotate(${-n-t}deg)`;return}this._compassIcon.style.transform=`rotate(${-n}deg)`},this._setButtonTitle=(e,t)=>{let n=this._map._getUIString(`NavigationControl.${t}`);e.title=n,e.setAttribute(`aria-label`,n)},this.options=L({},bp,e),this._container=W.create(`div`,`maplibregl-ctrl maplibregl-ctrl-group`),this._container.addEventListener(`contextmenu`,e=>e.preventDefault()),this.options.showZoom&&(this._zoomInButton=this._createButton(`maplibregl-ctrl-zoom-in`,e=>this._map.zoomIn({},{originalEvent:e})),W.create(`span`,`maplibregl-ctrl-icon`,this._zoomInButton).setAttribute(`aria-hidden`,`true`),this._zoomOutButton=this._createButton(`maplibregl-ctrl-zoom-out`,e=>this._map.zoomOut({},{originalEvent:e})),W.create(`span`,`maplibregl-ctrl-icon`,this._zoomOutButton).setAttribute(`aria-hidden`,`true`)),this.options.showCompass&&(this._compass=this._createButton(`maplibregl-ctrl-compass`,e=>{this.options.visualizePitch?this._map.resetNorthPitch({},{originalEvent:e}):this._map.resetNorth({},{originalEvent:e})}),this._compassIcon=W.create(`span`,`maplibregl-ctrl-icon`,this._compass),this._compassIcon.setAttribute(`aria-hidden`,`true`))}onAdd(e){return this._map=e,this.options.showZoom&&(this._setButtonTitle(this._zoomInButton,`ZoomIn`),this._setButtonTitle(this._zoomOutButton,`ZoomOut`),this._map.on(`zoom`,this._updateZoomButtons),this._updateZoomButtons()),this.options.showCompass&&(this._setButtonTitle(this._compass,`ResetBearing`),this.options.visualizePitch&&this._map.on(`pitch`,this._rotateCompassArrow),this.options.visualizeRoll&&this._map.on(`roll`,this._rotateCompassArrow),this._map.on(`rotate`,this._rotateCompassArrow),this._rotateCompassArrow(),this._handler=new Sp(this._map,this._compass,this.options.visualizePitch)),this._container}onRemove(){this._container.remove(),this.options.showZoom&&this._map.off(`zoom`,this._updateZoomButtons),this.options.showCompass&&(this.options.visualizePitch&&this._map.off(`pitch`,this._rotateCompassArrow),this.options.visualizeRoll&&this._map.off(`roll`,this._rotateCompassArrow),this._map.off(`rotate`,this._rotateCompassArrow),this._handler.off(),delete this._handler),delete this._map}_createButton(e,t){let n=W.create(`button`,e,this._container);return n.type=`button`,n.addEventListener(`click`,t),n}},Sp=class{constructor(e,t,n=!1){this.mousedown=e=>{this.startMove(e,W.mousePos(this.element,e)),window.addEventListener(`mousemove`,this.mousemove),window.addEventListener(`mouseup`,this.mouseup)},this.mousemove=e=>{this.move(e,W.mousePos(this.element,e))},this.mouseup=e=>{this._rotatePitchHandler.dragEnd(e),this.offTemp()},this.touchstart=e=>{e.targetTouches.length===1?(this._startPos=this._lastPos=W.touchPos(this.element,e.targetTouches)[0],this.startMove(e,this._startPos),window.addEventListener(`touchmove`,this.touchmove,{passive:!1}),window.addEventListener(`touchend`,this.touchend)):this.reset()},this.touchmove=e=>{e.targetTouches.length===1?(this._lastPos=W.touchPos(this.element,e.targetTouches)[0],this.move(e,this._lastPos)):this.reset()},this.touchend=e=>{e.targetTouches.length===0&&this._startPos&&this._lastPos&&this._startPos.dist(this._lastPos){this._rotatePitchHandler.reset(),delete this._startPos,delete this._lastPos,this.offTemp()},this._clickTolerance=10,this.element=t;let r=new Df;this._rotatePitchHandler=new Sf({clickTolerance:3,move:(e,r)=>{let i=t.getBoundingClientRect(),a=new z((i.bottom-i.top)/2,(i.right-i.left)/2);return{bearingDelta:Kn(new z(e.x,r.y),r,a),pitchDelta:n?(r.y-e.y)*-.5:void 0}},moveStateManager:r,enable:!0,assignEvents:()=>{}}),this.map=e,t.addEventListener(`mousedown`,this.mousedown),t.addEventListener(`touchstart`,this.touchstart,{passive:!1}),t.addEventListener(`touchcancel`,this.reset)}startMove(e,t){this._rotatePitchHandler.dragStart(e,t),W.disableDrag()}move(e,t){let n=this.map,{bearingDelta:r,pitchDelta:i}=this._rotatePitchHandler.dragMove(e,t)||{};r&&n.setBearing(n.getBearing()+r),i&&n.setPitch(n.getPitch()+i)}off(){let e=this.element;e.removeEventListener(`mousedown`,this.mousedown),e.removeEventListener(`touchstart`,this.touchstart),window.removeEventListener(`touchmove`,this.touchmove),window.removeEventListener(`touchend`,this.touchend),e.removeEventListener(`touchcancel`,this.reset),this.offTemp()}offTemp(){W.enableDrag(),window.removeEventListener(`mousemove`,this.mousemove),window.removeEventListener(`mouseup`,this.mouseup),window.removeEventListener(`touchmove`,this.touchmove),window.removeEventListener(`touchend`,this.touchend)}};let Cp;async function wp(e=!1){if(Cp!==void 0&&!e)return Cp;if(window.navigator.permissions===void 0)return Cp=!!window.navigator.geolocation,Cp;try{Cp=(await window.navigator.permissions.query({name:`geolocation`})).state!==`denied`}catch{Cp=!!window.navigator.geolocation}return Cp}function Tp(e,t,n,r=!1){if(r||!n.getCoveringTilesDetailsProvider().allowWorldCopies())return e?.wrap();let i=new B(e.lng,e.lat);if(e=new B(e.lng,e.lat),t){let r=new B(e.lng-360,e.lat),i=new B(e.lng+360,e.lat),a=n.locationToScreenPoint(e).distSqr(t);n.locationToScreenPoint(r).distSqr(t)180;){let t=n.locationToScreenPoint(e);if(t.x>=0&&t.y>=0&&t.x<=n.width&&t.y<=n.height)break;e.lng>n.center.lng?e.lng-=360:e.lng+=360}return e.lng!==i.lng&&n.isPointOnMapSurface(n.locationToScreenPoint(e))?e:i}const Ep={center:`translate(-50%,-50%)`,top:`translate(-50%,0)`,"top-left":`translate(0,0)`,"top-right":`translate(-100%,0)`,bottom:`translate(-50%,-100%)`,"bottom-left":`translate(0,-100%)`,"bottom-right":`translate(-100%,-100%)`,left:`translate(0,-50%)`,right:`translate(-100%,-50%)`};function Dp(e,t,n){let r=e.classList;for(let e in Ep)r.remove(`maplibregl-${n}-anchor-${e}`);r.add(`maplibregl-${n}-anchor-${t}`)}var Op=class extends On{},kp=class extends On{},Ap=class extends _n{constructor(e){if(super(),this._onClick=e=>{this.fire(new kp(`click`,{originalEvent:e}))},this._onKeyPress=e=>{(e.code===`Space`||e.code===`Enter`)&&this.togglePopup()},this._onMapClick=e=>{let t=e.originalEvent.target,n=this._element;this._popup&&(t===n||n.contains(t))&&this.togglePopup()},this._update=e=>{if(!this._map)return;let t=this._map.loaded()&&!this._map.isMoving();(e?.type===`terrain`||e?.type===`render`&&!t)&&this._map.once(`render`,this._update),this._lngLat=Tp(this._lngLat,this._flatPos,this._map._camera.transform),this._flatPos=this._pos=this._map.project(this._lngLat)._add(this._offset),this._map.terrain&&(this._flatPos=this._map._camera.transform.locationToScreenPoint(this._lngLat)._add(this._offset));let n=``;this._rotationAlignment===`viewport`||this._rotationAlignment===`auto`?n=`rotateZ(${this._rotation}deg)`:this._rotationAlignment===`map`&&(n=`rotateZ(${this._rotation-this._map.getBearing()}deg)`);let r=``;this._pitchAlignment===`viewport`||this._pitchAlignment===`auto`?r=`rotateX(0deg)`:this._pitchAlignment===`map`&&(r=`rotateX(${this._map.getPitch()}deg)`),!this._subpixelPositioning&&(!e||e.type===`moveend`)&&(this._pos=this._pos.round()),this._element.style.transform=`${Ep[this._anchor]} translate(${this._pos.x}px, ${this._pos.y}px) ${r} ${n}`,Dr.frameAsync(new AbortController,this._map._ownerWindow).then(()=>{this._updateOpacity(e?.type===`moveend`)}).catch(()=>{})},this._onMove=e=>{if(!this._isDragging){let t=this._clickTolerance||this._map._clickTolerance;this._isDragging=e.point.dist(this._pointerdownPos)>=t}this._isDragging&&(this._pos=e.point.sub(this._positionDelta),this._lngLat=this._map.unproject(this._pos),this.setLngLat(this._lngLat),this._element.style.pointerEvents=`none`,this._state===`pending`&&(this._state=`active`,this.fire(new Op(`dragstart`))),this.fire(new Op(`drag`)))},this._onUp=()=>{this._element.style.pointerEvents=`auto`,this._positionDelta=null,this._pointerdownPos=null,this._isDragging=!1,this._map.off(`mousemove`,this._onMove),this._map.off(`touchmove`,this._onMove),this._state===`active`&&this.fire(new Op(`dragend`)),this._state=`inactive`},this._addDragHandler=e=>{this._element.contains(e.originalEvent.target)&&(e.preventDefault(),this._positionDelta=e.point.sub(this._pos).add(this._offset),this._pointerdownPos=e.point,this._state=`pending`,this._map.on(`mousemove`,this._onMove),this._map.on(`touchmove`,this._onMove),this._map.once(`mouseup`,this._onUp),this._map.once(`touchend`,this._onUp))},this._anchor=e?.anchor||`center`,this._color=e?.color||`#3FB1CE`,this._scale=e?.scale||1,this._draggable=e?.draggable||!1,this._clickTolerance=e?.clickTolerance||0,this._subpixelPositioning=e?.subpixelPositioning||!1,this._isDragging=!1,this._state=`inactive`,this._rotation=e?.rotation||0,this._rotationAlignment=e?.rotationAlignment||`auto`,this._pitchAlignment=e?.pitchAlignment&&e.pitchAlignment!==`auto`?e.pitchAlignment:this._rotationAlignment,this.setOpacity(e?.opacity,e?.opacityWhenCovered),e?.element)this._element=e.element,this._offset=z.convert(e?.offset||[0,0]);else{this._defaultMarker=!0,this._element=W.create(`div`);let t=W.createNS(`http://www.w3.org/2000/svg`,`svg`);t.setAttributeNS(null,`display`,`block`),t.setAttributeNS(null,`height`,`41px`),t.setAttributeNS(null,`width`,`27px`),t.setAttributeNS(null,`viewBox`,`0 0 27 41`);let n=W.createNS(`http://www.w3.org/2000/svg`,`g`);n.setAttributeNS(null,`stroke`,`none`),n.setAttributeNS(null,`stroke-width`,`1`),n.setAttributeNS(null,`fill`,`none`),n.setAttributeNS(null,`fill-rule`,`evenodd`);let r=W.createNS(`http://www.w3.org/2000/svg`,`g`);r.setAttributeNS(null,`fill-rule`,`nonzero`);let i=W.createNS(`http://www.w3.org/2000/svg`,`g`);i.setAttributeNS(null,`transform`,`translate(3.0, 29.0)`),i.setAttributeNS(null,`fill`,`#000000`);for(let e of[{rx:`10.5`,ry:`5.25002273`},{rx:`10.5`,ry:`5.25002273`},{rx:`9.5`,ry:`4.77275007`},{rx:`8.5`,ry:`4.29549936`},{rx:`7.5`,ry:`3.81822308`},{rx:`6.5`,ry:`3.34094679`},{rx:`5.5`,ry:`2.86367051`},{rx:`4.5`,ry:`2.38636864`}]){let t=W.createNS(`http://www.w3.org/2000/svg`,`ellipse`);t.setAttributeNS(null,`opacity`,`0.04`),t.setAttributeNS(null,`cx`,`10.5`),t.setAttributeNS(null,`cy`,`5.80029008`),t.setAttributeNS(null,`rx`,e.rx),t.setAttributeNS(null,`ry`,e.ry),i.appendChild(t)}let a=W.createNS(`http://www.w3.org/2000/svg`,`g`);a.setAttributeNS(null,`fill`,this._color);let o=W.createNS(`http://www.w3.org/2000/svg`,`path`);o.setAttributeNS(null,`d`,`M27,13.5 C27,19.074644 20.250001,27.000002 14.75,34.500002 C14.016665,35.500004 12.983335,35.500004 12.25,34.500002 C6.7499993,27.000002 0,19.222562 0,13.5 C0,6.0441559 6.0441559,0 13.5,0 C20.955844,0 27,6.0441559 27,13.5 Z`),a.appendChild(o);let s=W.createNS(`http://www.w3.org/2000/svg`,`g`);s.setAttributeNS(null,`opacity`,`0.25`),s.setAttributeNS(null,`fill`,`#000000`);let c=W.createNS(`http://www.w3.org/2000/svg`,`path`);c.setAttributeNS(null,`d`,`M13.5,0 C6.0441559,0 0,6.0441559 0,13.5 C0,19.222562 6.7499993,27 12.25,34.5 C13,35.522727 14.016664,35.500004 14.75,34.5 C20.250001,27 27,19.074644 27,13.5 C27,6.0441559 20.955844,0 13.5,0 Z M13.5,1 C20.415404,1 26,6.584596 26,13.5 C26,15.898657 24.495584,19.181431 22.220703,22.738281 C19.945823,26.295132 16.705119,30.142167 13.943359,33.908203 C13.743445,34.180814 13.612715,34.322738 13.5,34.441406 C13.387285,34.322738 13.256555,34.180814 13.056641,33.908203 C10.284481,30.127985 7.4148684,26.314159 5.015625,22.773438 C2.6163816,19.232715 1,15.953538 1,13.5 C1,6.584596 6.584596,1 13.5,1 Z`),s.appendChild(c);let l=W.createNS(`http://www.w3.org/2000/svg`,`g`);l.setAttributeNS(null,`transform`,`translate(6.0, 7.0)`),l.setAttributeNS(null,`fill`,`#FFFFFF`);let u=W.createNS(`http://www.w3.org/2000/svg`,`g`);u.setAttributeNS(null,`transform`,`translate(8.0, 8.0)`);let d=W.createNS(`http://www.w3.org/2000/svg`,`circle`);d.setAttributeNS(null,`fill`,`#000000`),d.setAttributeNS(null,`opacity`,`0.25`),d.setAttributeNS(null,`cx`,`5.5`),d.setAttributeNS(null,`cy`,`5.5`),d.setAttributeNS(null,`r`,`5.4999962`);let f=W.createNS(`http://www.w3.org/2000/svg`,`circle`);f.setAttributeNS(null,`fill`,`#FFFFFF`),f.setAttributeNS(null,`cx`,`5.5`),f.setAttributeNS(null,`cy`,`5.5`),f.setAttributeNS(null,`r`,`5.4999962`),u.appendChild(d),u.appendChild(f),r.appendChild(i),r.appendChild(a),r.appendChild(s),r.appendChild(l),r.appendChild(u),t.appendChild(r),t.setAttributeNS(null,`height`,`${41*this._scale}px`),t.setAttributeNS(null,`width`,`${27*this._scale}px`),this._element.appendChild(t),this._offset=z.convert(e?.offset||[0,-14])}if(this._element.classList.add(`maplibregl-marker`),this._element.addEventListener(`dragstart`,e=>{e.preventDefault()}),this._element.addEventListener(`mousedown`,e=>{e.preventDefault()}),Dp(this._element,this._anchor,`marker`),e?.className)for(let t of e.className.split(` `))this._element.classList.add(t);this._popup=null}addTo(e){return this.remove(),this._map=e,this._element.hasAttribute(`aria-label`)||this._element.setAttribute(`aria-label`,e._getUIString(`Marker.Title`)),this._element.hasAttribute(`role`)||this._element.setAttribute(`role`,`button`),e.getCanvasContainer().appendChild(this._element),e.on(`move`,this._update),e.on(`moveend`,this._update),e.on(`terrain`,this._update),e.on(`projectiontransition`,this._update),this._element.addEventListener(`click`,this._onClick),this.setDraggable(this._draggable),this._update(),this._map.on(`click`,this._onMapClick),this}remove(){return this._opacityTimeout&&(clearTimeout(this._opacityTimeout),delete this._opacityTimeout),this._map&&(this._map.off(`click`,this._onMapClick),this._map.off(`move`,this._update),this._map.off(`moveend`,this._update),this._map.off(`terrain`,this._update),this._map.off(`projectiontransition`,this._update),this._map.off(`mousedown`,this._addDragHandler),this._map.off(`touchstart`,this._addDragHandler),this._map.off(`mouseup`,this._onUp),this._map.off(`touchend`,this._onUp),this._map.off(`mousemove`,this._onMove),this._map.off(`touchmove`,this._onMove),delete this._map),this._element.removeEventListener(`click`,this._onClick),this._element.remove(),this._popup&&this._popup.remove(),this}getLngLat(){return this._lngLat}setLngLat(e){return this._lngLat=B.convert(e),this._pos=null,this._popup&&this._popup.setLngLat(this._lngLat),this._update(),this}getElement(){return this._element}setPopup(e){if(this._popup&&(this._popup.remove(),this._popup=null,this._element.removeEventListener(`keypress`,this._onKeyPress),this._originalTabIndex||this._element.removeAttribute(`tabindex`)),e){if(!(`offset`in e.options)){let t=41-5.8/2,n=13.5,r=13.5/Math.SQRT2;e.options.offset=this._defaultMarker?{top:[0,0],"top-left":[0,0],"top-right":[0,0],bottom:[0,-38.1],"bottom-left":[r,(t-n+r)*-1],"bottom-right":[-r,(t-n+r)*-1],left:[n,(t-n)*-1],right:[-13.5,(t-n)*-1]}:this._offset}this._popup=e,this._originalTabIndex=this._element.getAttribute(`tabindex`),this._originalTabIndex||this._element.setAttribute(`tabindex`,`0`),this._element.addEventListener(`keypress`,this._onKeyPress)}return this}setSubpixelPositioning(e){return this._subpixelPositioning=e,this}getPopup(){return this._popup}togglePopup(){let e=this._popup;if(this._element.style.opacity===this._opacityWhenCovered)return this;if(e)e.isOpen()?e.remove():(e.setLngLat(this._lngLat),e.addTo(this._map));else return this;return this}_updateOpacity(e=!1){let t=this._map?.terrain,n=this._map._camera.transform.isLocationOccluded(this._lngLat);if(!t||n){let e=n?this._opacityWhenCovered:this._opacity;this._element.style.opacity!==e&&(this._element.style.opacity=e,this._element.classList.toggle(`maplibregl-marker-covered`,n));return}if(e)this._opacityTimeout=null;else{if(this._opacityTimeout)return;this._opacityTimeout=setTimeout(()=>{this._opacityTimeout=null},100)}let r=this._map,i=r.terrain.depthAtPoint(this._pos),a=r.terrain.getElevationForLngLat(this._lngLat,r._camera.transform),o=r._camera.transform.lngLatToCameraDepth(this._lngLat,a),s=.006;if(o-is;this._popup?.isOpen()&&d&&this._popup.remove(),this._element.style.opacity=d?this._opacityWhenCovered:this._opacity,this._element.classList.toggle(`maplibregl-marker-covered`,d)}getOffset(){return this._offset}setOffset(e){return this._offset=z.convert(e),this._update(),this}addClassName(e){this._element.classList.add(e)}removeClassName(e){this._element.classList.remove(e)}toggleClassName(e){return this._element.classList.toggle(e)}setDraggable(e){return this._draggable=!!e,this._map&&(e?(this._map.on(`mousedown`,this._addDragHandler),this._map.on(`touchstart`,this._addDragHandler)):(this._map.off(`mousedown`,this._addDragHandler),this._map.off(`touchstart`,this._addDragHandler))),this}isDraggable(){return this._draggable}setRotation(e){return this._rotation=e||0,this._update(),this}getRotation(){return this._rotation}setRotationAlignment(e){return this._rotationAlignment=e||`auto`,this._update(),this}getRotationAlignment(){return this._rotationAlignment}setPitchAlignment(e){return this._pitchAlignment=e&&e!==`auto`?e:this._rotationAlignment,this._update(),this}getPitchAlignment(){return this._pitchAlignment}setOpacity(e,t){return(this._opacity===void 0||e===void 0&&t===void 0)&&(this._opacity=`1`,this._opacityWhenCovered=`0.2`),e!==void 0&&(this._opacity=String(e)),t!==void 0&&(this._opacityWhenCovered=String(t)),this._map&&this._updateOpacity(!0),this}};const jp={positionOptions:{enableHighAccuracy:!1,maximumAge:0,timeout:6e3},fitBoundsOptions:{maxZoom:15},trackUserLocation:!1,showAccuracyCircle:!0,showUserLocation:!0};let Mp=0,Np=!1;var Pp=class extends On{},Fp=class extends On{},Ip=class extends On{},Lp=class extends _n{constructor(e){super(),this._onSuccess=e=>{if(this._map){if(this._isOutOfMapMaxBounds(e)){this._setErrorState(),this.fire(new Fp(`outofmaxbounds`,e)),this._updateMarker(),this._finish();return}if(this.options.trackUserLocation)switch(this._lastKnownPosition=e,this._watchState){case`WAITING_ACTIVE`:case`ACTIVE_LOCK`:case`ACTIVE_ERROR`:this._watchState=`ACTIVE_LOCK`,this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-waiting`),this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-active-error`),this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-active`);break;case`BACKGROUND`:case`BACKGROUND_ERROR`:this._watchState=`BACKGROUND`,this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-waiting`),this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-background-error`),this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-background`);break;default:throw Error(`Unexpected watchState ${this._watchState}`)}this.options.showUserLocation&&this._watchState!==`OFF`&&this._updateMarker(e),(!this.options.trackUserLocation||this._watchState===`ACTIVE_LOCK`)&&this._updateCamera(e),this.options.showUserLocation&&this._dotElement.classList.remove(`maplibregl-user-location-dot-stale`),this.fire(new Fp(`geolocate`,e)),this._finish()}},this._updateCamera=e=>{let t=new B(e.coords.longitude,e.coords.latitude),n=e.coords.accuracy,r=L({bearing:this._map.getBearing()},this.options.fitBoundsOptions),i=Ri.fromLngLat(t,n);this._map.fitBounds(i,r,{geolocateSource:!0})},this._updateMarker=e=>{if(e){let t=new B(e.coords.longitude,e.coords.latitude);this._accuracyCircleMarker.setLngLat(t).addTo(this._map),this._userLocationDotMarker.setLngLat(t).addTo(this._map),this._accuracy=e.coords.accuracy,this._updateCircleRadiusIfNeeded()}else this._userLocationDotMarker.remove(),this._accuracyCircleMarker.remove()},this._onUpdate=()=>{this._updateCircleRadiusIfNeeded()},this._onError=e=>{if(this._map){if(e.code===1){this._watchState=`OFF`,this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-waiting`),this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-active`),this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-active-error`),this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-background`),this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-background-error`),this._geolocateButton.disabled=!0;let e=this._map._getUIString(`GeolocateControl.LocationNotAvailable`);this._geolocateButton.title=e,this._geolocateButton.setAttribute(`aria-label`,e),this._geolocationWatchID!==void 0&&this._clearWatch()}else if(e.code===3&&Np)return;else this._setErrorState();this._watchState!==`OFF`&&this.options.showUserLocation&&this._dotElement.classList.add(`maplibregl-user-location-dot-stale`),this.fire(new Ip(`error`,e)),this._finish()}},this._finish=()=>{this._timeoutId&&clearTimeout(this._timeoutId),this._timeoutId=void 0},this._onMoveStart=e=>{if(!this._map)return;let t=e?.[0]instanceof ResizeObserverEntry;!e.geolocateSource&&this._watchState===`ACTIVE_LOCK`&&!t&&!this._map.isZooming()&&(this._watchState=`BACKGROUND`,this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-background`),this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-active`),this.fire(new Pp(`trackuserlocationend`)),this.fire(new Pp(`userlocationlostfocus`)))},this._setupUI=()=>{this._map&&(this._container.addEventListener(`contextmenu`,e=>{e.preventDefault()}),this._geolocateButton=W.create(`button`,`maplibregl-ctrl-geolocate`,this._container),W.create(`span`,`maplibregl-ctrl-icon`,this._geolocateButton).setAttribute(`aria-hidden`,`true`),this._geolocateButton.type=`button`,this._geolocateButton.disabled=!0)},this._finishSetupUI=e=>{if(this._map){if(e===!1){a(`Geolocation support is not available so the GeolocateControl will be disabled.`);let e=this._map._getUIString(`GeolocateControl.LocationNotAvailable`);this._geolocateButton.disabled=!0,this._geolocateButton.title=e,this._geolocateButton.setAttribute(`aria-label`,e)}else{let e=this._map._getUIString(`GeolocateControl.FindMyLocation`);this._geolocateButton.disabled=!1,this._geolocateButton.title=e,this._geolocateButton.setAttribute(`aria-label`,e)}this.options.trackUserLocation&&(this._geolocateButton.setAttribute(`aria-pressed`,`false`),this._watchState=`OFF`),this.options.showUserLocation&&(this._dotElement=W.create(`div`,`maplibregl-user-location-dot`),this._userLocationDotMarker=new Ap({element:this._dotElement}),this._circleElement=W.create(`div`,`maplibregl-user-location-accuracy-circle`),this._accuracyCircleMarker=new Ap({element:this._circleElement,pitchAlignment:`map`}),this.options.trackUserLocation&&(this._watchState=`OFF`),this._map.on(`zoom`,this._onUpdate),this._map.on(`move`,this._onUpdate),this._map.on(`rotate`,this._onUpdate),this._map.on(`pitch`,this._onUpdate)),this._geolocateButton.addEventListener(`click`,()=>this.trigger()),this._setup=!0,this.options.trackUserLocation&&this._map.on(`movestart`,this._onMoveStart)}},this.options=L({},jp,e)}onAdd(e){return this._map=e,this._container=W.create(`div`,`maplibregl-ctrl maplibregl-ctrl-group`),this._setupUI(),wp().then(e=>this._finishSetupUI(e)),this._container}onRemove(){this._geolocationWatchID!==void 0&&(window.navigator.geolocation.clearWatch(this._geolocationWatchID),this._geolocationWatchID=void 0),this.options.showUserLocation&&this._userLocationDotMarker&&this._userLocationDotMarker.remove(),this.options.showAccuracyCircle&&this._accuracyCircleMarker&&this._accuracyCircleMarker.remove(),this._container.remove(),this._map.off(`movestart`,this._onMoveStart),this._map.off(`zoom`,this._onUpdate),this._map.off(`move`,this._onUpdate),this._map.off(`rotate`,this._onUpdate),this._map.off(`pitch`,this._onUpdate),this._map=void 0,Mp=0,Np=!1}_isOutOfMapMaxBounds(e){let t=this._map.getMaxBounds(),n=e.coords;return t&&(n.longitudet.getEast()||n.latitudet.getNorth())}_setErrorState(){switch(this._watchState){case`WAITING_ACTIVE`:this._watchState=`ACTIVE_ERROR`,this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-active`),this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-active-error`);break;case`ACTIVE_LOCK`:this._watchState=`ACTIVE_ERROR`,this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-active`),this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-active-error`),this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-waiting`);break;case`BACKGROUND`:this._watchState=`BACKGROUND_ERROR`,this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-background`),this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-background-error`),this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-waiting`);break;case`ACTIVE_ERROR`:case`BACKGROUND_ERROR`:break;case`OFF`:case void 0:break;default:throw Error(`Unexpected watchState ${this._watchState}`)}}_updateCircleRadiusIfNeeded(){let e=this._userLocationDotMarker.getLngLat();if(!this.options.showUserLocation||!this.options.showAccuracyCircle||!this._accuracy||!e)return;let t=this._map.project(e),n=this._map.unproject([t.x+100,t.y]),r=e.distanceTo(n)/100,i=2*this._accuracy/r;this._circleElement.style.width=`${i.toFixed(2)}px`,this._circleElement.style.height=`${i.toFixed(2)}px`}trigger(){if(!this._setup)return a(`Geolocate control triggered before added to a map`),!1;if(this.options.trackUserLocation){switch(this._watchState){case`OFF`:this._watchState=`WAITING_ACTIVE`,this.fire(new Pp(`trackuserlocationstart`));break;case`WAITING_ACTIVE`:case`ACTIVE_LOCK`:case`ACTIVE_ERROR`:case`BACKGROUND_ERROR`:Mp--,Np=!1,this._watchState=`OFF`,this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-waiting`),this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-active`),this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-active-error`),this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-background`),this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-background-error`),this.fire(new Pp(`trackuserlocationend`));break;case`BACKGROUND`:this._watchState=`ACTIVE_LOCK`,this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-background`),this._lastKnownPosition&&this._updateCamera(this._lastKnownPosition),this.fire(new Pp(`trackuserlocationstart`)),this.fire(new Pp(`userlocationfocus`));break;default:throw Error(`Unexpected watchState ${this._watchState}`)}switch(this._watchState){case`WAITING_ACTIVE`:this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-waiting`),this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-active`);break;case`ACTIVE_LOCK`:this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-active`);break;case`OFF`:break;default:throw Error(`Unexpected watchState ${this._watchState}`)}if(this._watchState===`OFF`&&this._geolocationWatchID!==void 0)this._clearWatch();else if(this._geolocationWatchID===void 0){this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-waiting`),this._geolocateButton.setAttribute(`aria-pressed`,`true`),Mp++;let e;Mp>1?(e={maximumAge:6e5,timeout:0},Np=!0):(e=this.options.positionOptions,Np=!1),this._geolocationWatchID=window.navigator.geolocation.watchPosition(this._onSuccess,this._onError,e)}}else window.navigator.geolocation.getCurrentPosition(this._onSuccess,this._onError,this.options.positionOptions),this._timeoutId=setTimeout(this._finish,1e4);return!0}_clearWatch(){window.navigator.geolocation.clearWatch(this._geolocationWatchID),this._geolocationWatchID=void 0,this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-waiting`),this._geolocateButton.setAttribute(`aria-pressed`,`false`),this.options.showUserLocation&&this._updateMarker(null)}};const Rp={maxWidth:100,unit:`metric`};var zp=class{constructor(e){this._onMove=()=>{Bp(this._map,this._container,this.options)},this.setUnit=e=>{this.options.unit=e,Bp(this._map,this._container,this.options)},this.options={...Rp,...e}}getDefaultPosition(){return`bottom-left`}onAdd(e){return this._map=e,this._container=W.create(`div`,`maplibregl-ctrl maplibregl-ctrl-scale`,e.getContainer()),this._map.on(`move`,this._onMove),this._onMove(),this._container}onRemove(){this._container.remove(),this._map.off(`move`,this._onMove),this._map=void 0}};function Bp(e,t,n){let r=n?.maxWidth||100,i=e._container.clientHeight/2,a=e._container.clientWidth/2,o=e.unproject([a-r/2,i]),s=e.unproject([a+r/2,i]),c=Math.round(e.project(s).x-e.project(o).x),l=Math.min(r,c,e._container.clientWidth),u=o.distanceTo(s);if(n?.unit===`imperial`){let n=3.2808*u;n>5280?Vp(t,l,n/5280,e._getUIString(`ScaleControl.Miles`)):Vp(t,l,n,e._getUIString(`ScaleControl.Feet`))}else n?.unit===`nautical`?Vp(t,l,u/1852,e._getUIString(`ScaleControl.NauticalMiles`)):u>=1e3?Vp(t,l,u/1e3,e._getUIString(`ScaleControl.Kilometers`)):Vp(t,l,u,e._getUIString(`ScaleControl.Meters`))}function Vp(e,t,n,r){let i=Up(n),a=i/n;e.style.width=`${t*a}px`,e.innerHTML=`${i} ${r}`}function Hp(e){let t=10**Math.ceil(-Math.log(e)/Math.LN10);return Math.round(e*t)/t}function Up(e){let t=10**(`${Math.floor(e)}`.length-1),n=e/t;return n=n>=10?10:n>=5?5:n>=3?3:n>=2?2:n>=1?1:Hp(n),t*n}var Wp=class extends On{},Gp=class extends _n{constructor(e={}){super(),this._onFullscreenChange=()=>{let e=window.document.fullscreenElement||window.document.webkitFullscreenElement;for(;e?.shadowRoot?.fullscreenElement;)e=e.shadowRoot.fullscreenElement;e===this._container!==this._fullscreen&&this._handleFullscreenChange()},this._onClickFullscreen=()=>{this._isFullscreen()?this._exitFullscreen():this._requestFullscreen()},this._fullscreen=!1,this._pseudo=e.pseudo??!1,e?.container&&(e.container instanceof HTMLElement?this._container=e.container:a(`Full screen control 'container' must be a DOM element.`)),`onfullscreenchange`in document?this._fullscreenchange=`fullscreenchange`:`onmozfullscreenchange`in document?this._fullscreenchange=`mozfullscreenchange`:`onwebkitfullscreenchange`in document?this._fullscreenchange=`webkitfullscreenchange`:`onmsfullscreenchange`in document&&(this._fullscreenchange=`MSFullscreenChange`)}onAdd(e){return this._map=e,this._container||=this._map.getContainer(),this._controlContainer=W.create(`div`,`maplibregl-ctrl maplibregl-ctrl-group`),this._setupUI(),this._controlContainer}onRemove(){this._controlContainer.remove(),this._map=null,window.document.removeEventListener(this._fullscreenchange,this._onFullscreenChange)}_setupUI(){let e=this._fullscreenButton=W.create(`button`,`maplibregl-ctrl-fullscreen`,this._controlContainer);W.create(`span`,`maplibregl-ctrl-icon`,e).setAttribute(`aria-hidden`,`true`),e.type=`button`,this._updateTitle(),this._fullscreenButton.addEventListener(`click`,this._onClickFullscreen),window.document.addEventListener(this._fullscreenchange,this._onFullscreenChange)}_updateTitle(){let e=this._getTitle();this._fullscreenButton.setAttribute(`aria-label`,e),this._fullscreenButton.title=e}_getTitle(){return this._map._getUIString(this._isFullscreen()?`FullscreenControl.Exit`:`FullscreenControl.Enter`)}_isFullscreen(){return this._fullscreen}_handleFullscreenChange(){this._fullscreen=!this._fullscreen,this._fullscreenButton.classList.toggle(`maplibregl-ctrl-shrink`),this._fullscreenButton.classList.toggle(`maplibregl-ctrl-fullscreen`),this._updateTitle(),this._fullscreen?(this.fire(new Wp(`fullscreenstart`)),this._prevCooperativeGesturesEnabled=this._map.cooperativeGestures.isEnabled(),this._map.cooperativeGestures.disable()):(this.fire(new Wp(`fullscreenend`)),this._prevCooperativeGesturesEnabled&&this._map.cooperativeGestures.enable())}_exitFullscreen(){this._pseudo?this._togglePseudoFullScreen():window.document.exitFullscreen?window.document.exitFullscreen():window.document.webkitCancelFullScreen?window.document.webkitCancelFullScreen():this._togglePseudoFullScreen()}_requestFullscreen(){this._pseudo?this._togglePseudoFullScreen():this._container.requestFullscreen?this._container.requestFullscreen():this._container.webkitRequestFullscreen?this._container.webkitRequestFullscreen():this._togglePseudoFullScreen()}_togglePseudoFullScreen(){this._container.classList.toggle(`maplibregl-pseudo-fullscreen`),this._handleFullscreenChange(),this._map.resize()}},Kp=class{constructor(e){this._toggleTerrain=()=>{this._map.getTerrain()?this._map.setTerrain(null):this._map.setTerrain(this.options),this._updateTerrainIcon()},this._updateTerrainIcon=()=>{this._terrainButton.classList.remove(`maplibregl-ctrl-terrain`),this._terrainButton.classList.remove(`maplibregl-ctrl-terrain-enabled`),this._map.terrain?(this._terrainButton.classList.add(`maplibregl-ctrl-terrain-enabled`),this._terrainButton.title=this._map._getUIString(`TerrainControl.Disable`)):(this._terrainButton.classList.add(`maplibregl-ctrl-terrain`),this._terrainButton.title=this._map._getUIString(`TerrainControl.Enable`))},this.options=e}onAdd(e){return this._map=e,this._container=W.create(`div`,`maplibregl-ctrl maplibregl-ctrl-group`),this._terrainButton=W.create(`button`,`maplibregl-ctrl-terrain`,this._container),W.create(`span`,`maplibregl-ctrl-icon`,this._terrainButton).setAttribute(`aria-hidden`,`true`),this._terrainButton.type=`button`,this._terrainButton.addEventListener(`click`,this._toggleTerrain),this._updateTerrainIcon(),this._map.on(`terrain`,this._updateTerrainIcon),this._container}onRemove(){this._container.remove(),this._map.off(`terrain`,this._updateTerrainIcon),this._map=void 0}},qp=class{constructor(){this._toggleProjection=()=>{let e=this._map.getProjection()?.type;e===`mercator`||!e?this._map.setProjection({type:`globe`}):this._map.setProjection({type:`mercator`}),this._updateGlobeIcon()},this._updateGlobeIcon=()=>{this._globeButton.classList.remove(`maplibregl-ctrl-globe`),this._globeButton.classList.remove(`maplibregl-ctrl-globe-enabled`),this._map.getProjection()?.type===`globe`?(this._globeButton.classList.add(`maplibregl-ctrl-globe-enabled`),this._globeButton.title=this._map._getUIString(`GlobeControl.Disable`)):(this._globeButton.classList.add(`maplibregl-ctrl-globe`),this._globeButton.title=this._map._getUIString(`GlobeControl.Enable`))}}onAdd(e){return this._map=e,this._container=W.create(`div`,`maplibregl-ctrl maplibregl-ctrl-group`),this._globeButton=W.create(`button`,`maplibregl-ctrl-globe`,this._container),W.create(`span`,`maplibregl-ctrl-icon`,this._globeButton).setAttribute(`aria-hidden`,`true`),this._globeButton.type=`button`,this._globeButton.addEventListener(`click`,this._toggleProjection),this._updateGlobeIcon(),this._map.on(`styledata`,this._updateGlobeIcon),this._map.on(`projectiontransition`,this._updateGlobeIcon),this._container}onRemove(){this._container.remove(),this._map.off(`styledata`,this._updateGlobeIcon),this._map.off(`projectiontransition`,this._updateGlobeIcon),this._globeButton.removeEventListener(`click`,this._toggleProjection),this._map=void 0}};const Jp={closeButton:!0,closeOnClick:!0,focusAfterOpen:!0,className:``,maxWidth:`240px`,subpixelPositioning:!1,locationOccludedOpacity:void 0,padding:void 0},Yp=[`a[href]`,`[tabindex]:not([tabindex='-1'])`,`[contenteditable]:not([contenteditable='false'])`,`button:not([disabled])`,`input:not([disabled])`,`select:not([disabled])`,`textarea:not([disabled])`].join(`, `);var Xp=class extends On{},Zp=class extends _n{constructor(e){super(),this._updateOpacity=()=>{this.options.locationOccludedOpacity!==void 0&&(this._map._camera.transform.isLocationOccluded(this.getLngLat())?this._container.style.opacity=`${this.options.locationOccludedOpacity}`:this._container.style.opacity=``)},this.remove=()=>(this._content&&this._content.remove(),this._container&&(this._container.remove(),delete this._container),this._map&&(this._map.off(`move`,this._update),this._map.off(`move`,this._onClose),this._map.off(`click`,this._onClose),this._map.off(`remove`,this.remove),this._map.off(`terrain`,this._update),this._map.off(`projectiontransition`,this._update),this._map.off(`mousemove`,this._update),this._map.off(`mouseup`,this._update),this._map.off(`drag`,this._update),this._map._canvasContainer.classList.remove(`maplibregl-track-pointer`),delete this._map,this.fire(new Xp(`close`))),this),this._update=e=>{let t=this._lngLat||this._trackPointer;if(!this._map||!t||!this._content)return;if(!this._container){if(this._container=W.create(`div`,`maplibregl-popup`,this._map.getContainer()),this._tip=W.create(`div`,`maplibregl-popup-tip`,this._container),this._container.appendChild(this._content),this.options.className)for(let e of this.options.className.split(` `))this._container.classList.add(e);this._closeButton&&this._closeButton.setAttribute(`aria-label`,this._map._getUIString(`Popup.Close`)),this._trackPointer&&this._container.classList.add(`maplibregl-popup-track-pointer`)}this.options.maxWidth&&this._container.style.maxWidth!==this.options.maxWidth&&(this._container.style.maxWidth=this.options.maxWidth),this._lngLat=Tp(this._lngLat,this._flatPos,this._map._camera.transform,this._trackPointer);let n;if(e&&`point`in e&&e.point&&(n=e.point),this._trackPointer&&!n)return;let r=this._flatPos=this._pos=this._trackPointer&&n?n:this._map.project(this._lngLat);this._map.terrain&&(this._flatPos=this._trackPointer&&n?n:this._map._camera.transform.locationToScreenPoint(this._lngLat));let i=this.options.anchor,a=Qp(this.options.offset);if(!i){let e=this._container.offsetWidth,t=this._container.offsetHeight,n=$p(this.options.padding),o;o=r.y+a.bottom.ythis._map._camera.transform.height-t-n.bottom?[`bottom`]:[],r.xthis._map._camera.transform.width-e/2-n.right&&o.push(`right`),i=o.length===0?`bottom`:o.join(`-`)}let o=r.add(a[i]);this.options.subpixelPositioning||(o=o.round()),this._container.style.transform=`${Ep[i]} translate(${o.x}px,${o.y}px)`,Dp(this._container,i,`popup`),this._updateOpacity()},this._onClose=()=>{this.remove()},this.options=L(Object.create(Jp),e)}addTo(e){return this._map&&this.remove(),this._map=e,this.options.closeOnClick&&this._map.on(`click`,this._onClose),this.options.closeOnMove&&this._map.on(`move`,this._onClose),this._map.on(`remove`,this.remove),this._map.on(`terrain`,this._update),this._map.on(`projectiontransition`,this._update),this._update(),this._focusFirstElement(),this._trackPointer?(this._map.on(`mousemove`,this._update),this._map.on(`mouseup`,this._update),this._container&&this._container.classList.add(`maplibregl-popup-track-pointer`),this._map._canvasContainer.classList.add(`maplibregl-track-pointer`)):this._map.on(`move`,this._update),this.fire(new Xp(`open`)),this}isOpen(){return!!this._map}getLngLat(){return this._lngLat}setLngLat(e){return this._lngLat=B.convert(e),this._pos=null,this._flatPos=null,this._trackPointer=!1,this._update(),this._map&&(this._map.on(`move`,this._update),this._map.off(`mousemove`,this._update),this._container&&this._container.classList.remove(`maplibregl-popup-track-pointer`),this._map._canvasContainer.classList.remove(`maplibregl-track-pointer`)),this}trackPointer(){return this._trackPointer=!0,this._pos=null,this._flatPos=null,this._update(),this._map&&(this._map.off(`move`,this._update),this._map.on(`mousemove`,this._update),this._map.on(`drag`,this._update),this._container&&this._container.classList.add(`maplibregl-popup-track-pointer`),this._map._canvasContainer.classList.add(`maplibregl-track-pointer`)),this}getElement(){return this._container}setText(e){return this.setDOMContent(document.createTextNode(e))}setHTML(e){let t=document.createDocumentFragment(),n=document.createElement(`body`),r;for(n.innerHTML=e;r=n.firstChild,r;)t.appendChild(r);return this.setDOMContent(t)}getMaxWidth(){return this._container?.style.maxWidth}setMaxWidth(e){return this.options.maxWidth=e,this._update(),this}setDOMContent(e){if(this._content)for(;this._content.hasChildNodes();)this._content.firstChild&&this._content.removeChild(this._content.firstChild);else this._content=W.create(`div`,`maplibregl-popup-content`,this._container);return this._content.appendChild(e),this._createCloseButton(),this._update(),this._focusFirstElement(),this}addClassName(e){return this._container&&this._container.classList.add(e),this}removeClassName(e){return this._container&&this._container.classList.remove(e),this}setOffset(e){return this.options.offset=e,this._update(),this}toggleClassName(e){if(this._container)return this._container.classList.toggle(e)}setSubpixelPositioning(e){this.options.subpixelPositioning=e}setPadding(e){this.options.padding=e,this._update()}_createCloseButton(){this.options.closeButton&&(this._closeButton=W.create(`button`,`maplibregl-popup-close-button`,this._content),this._closeButton.type=`button`,this._closeButton.innerHTML=`×`,this._closeButton.addEventListener(`click`,this._onClose))}_focusFirstElement(){if(!this.options.focusAfterOpen||!this._container)return;let e=this._container.querySelector(Yp);e&&e.focus()}};function Qp(e){if(!e)return Qp(new z(0,0));if(typeof e==`number`){let t=Math.round(Math.abs(e)/Math.SQRT2);return{center:new z(0,0),top:new z(0,e),"top-left":new z(t,t),"top-right":new z(-t,t),bottom:new z(0,-e),"bottom-left":new z(t,-t),"bottom-right":new z(-t,-t),left:new z(e,0),right:new z(-e,0)}}else if(e instanceof z||Array.isArray(e)){let t=z.convert(e);return{center:t,top:t,"top-left":t,"top-right":t,bottom:t,"bottom-left":t,"bottom-right":t,left:t,right:t}}else return{center:z.convert(e.center||[0,0]),top:z.convert(e.top||[0,0]),"top-left":z.convert(e[`top-left`]||[0,0]),"top-right":z.convert(e[`top-right`]||[0,0]),bottom:z.convert(e.bottom||[0,0]),"bottom-left":z.convert(e[`bottom-left`]||[0,0]),"bottom-right":z.convert(e[`bottom-right`]||[0,0]),left:z.convert(e.left||[0,0]),right:z.convert(e.right||[0,0])}}function $p(e){return e?{top:e.top??0,right:e.right??0,bottom:e.bottom??0,left:e.left??0}:{top:0,right:0,bottom:0,left:0}}const em=br;function tm(e,t){return va().setRTLTextPlugin(e,t)}function nm(){return va().getRTLTextPluginStatus()}function rm(){return em}function im(){return vi.workerCount}function am(e){vi.workerCount=e}function om(){return C.MAX_PARALLEL_IMAGE_REQUESTS}function sm(e){C.MAX_PARALLEL_IMAGE_REQUESTS=e}function cm(){return C.WORKER_URL}function lm(e){C.WORKER_URL=e}async function um(e){await Ei().broadcast(`IS`,e)}export{$t as AJAXError,cp as AttributionControl,gf as BoxZoomHandler,ca as CanvasSource,ep as CooperativeGesturesHandler,Jf as DoubleClickZoomHandler,Zf as DragPanHandler,Qf as DragRotateHandler,M as EXTENT,hs as EdgeInsets,R as ErrorEvent,On as Event,_n as Evented,Gp as FullscreenControl,Wp as FullscreenEvent,tf as GPUInitializationError,ra as GeoJSONSource,Lp as GeolocateControl,Ip as GeolocateErrorEvent,Pp as GeolocateEvent,Fp as GeolocatePositionEvent,qp as GlobeControl,rf as Hash,ia as ImageSource,Wf as KeyboardHandler,B as LngLat,Ri as LngLatBounds,lp as LogoControl,yp as Map,yp as MapLibreMap,Br as MapBoxZoomEvent,Ur as MapContextEvent,Pr as MapLibreEvent,Lr as MapMouseEvent,G as MapMovementEvent,Hr as MapProjectionEvent,K as MapSourceDataEvent,Ir as MapStyleDataEvent,Wr as MapStyleImageMissingEvent,Fr as MapStyleLoadEvent,Vr as MapTerrainEvent,Rr as MapTouchEvent,zr as MapWheelEvent,Ap as Marker,kp as MarkerClickEvent,Op as MarkerDragEvent,N as MercatorCoordinate,xp as NavigationControl,z as Point,Zp as Popup,Xp as PopupEvent,Hi as RasterDEMTileSource,Vi as RasterTileSource,zp as ScaleControl,qf as ScrollZoomHandler,bc as Style,Kp as TerrainControl,Hf as TwoFingersTouchPitchHandler,Bf as TwoFingersTouchRotateHandler,Rf as TwoFingersTouchZoomHandler,$f as TwoFingersTouchZoomRotateHandler,Bi as VectorTileSource,sa as VideoSource,p as addProtocol,pa as addSourceType,Ci as clearPrewarmedResources,C as config,Vs as createTileMesh,Ei as getGlobalDispatcher,om as getMaxParallelImageRequests,nm as getRTLTextPluginStatus,rm as getVersion,im as getWorkerCount,cm as getWorkerUrl,um as importScriptInWorkers,jr as isTimeFrozen,U as now,Si as prewarm,qe as removeProtocol,Ar as restoreNow,sm as setMaxParallelImageRequests,kr as setNow,tm as setRTLTextPlugin,am as setWorkerCount,lm as setWorkerUrl}; +`}),{fragmentSource:e,vertexSource:t,staticAttributes:r,staticUniforms:o}}const Zs=`#define PROJECTION_MERCATOR`,Qs=`mercator`;var $s=class{constructor(){this._cachedMesh=null}get name(){return`mercator`}get useSubdivision(){return!1}get shaderVariantName(){return Qs}get shaderDefine(){return Zs}get shaderPreludeCode(){return Xs.projectionMercator}get vertexShaderPreludeCode(){return Xs.projectionMercator.vertexSource}get subdivisionGranularity(){return Lt.noSubdivision}get useGlobeControls(){return!1}get transitionState(){return 0}destroy(){}getMeshFromTileID(e,t,n,r,i){if(this._cachedMesh)return this._cachedMesh;let a=new Un;a.emplaceBack(0,0),a.emplaceBack(N,0),a.emplaceBack(0,N),a.emplaceBack(N,N);let o=e.createVertexBuffer(a,Wa.members),s=ae.simpleSegment(0,0,4,2),c=new gt;c.emplaceBack(1,0,2),c.emplaceBack(1,2,3);let l=e.createIndexBuffer(c);return this._cachedMesh=new Ua(o,l,s),this._cachedMesh}recalculate(){}hasTransition(){return!1}},ec=class e{constructor(e=0,t=0,n=0,r=0){if(isNaN(e)||e<0||isNaN(t)||t<0||isNaN(n)||n<0||isNaN(r)||r<0)throw Error(`Invalid value for edge-insets, top, bottom, left and right must all be numbers`);this.top=e,this.bottom=t,this.left=n,this.right=r}interpolate(e,t,n){return t.top!=null&&e.top!=null&&(this.top=on.number(e.top,t.top,n)),t.bottom!=null&&e.bottom!=null&&(this.bottom=on.number(e.bottom,t.bottom,n)),t.left!=null&&e.left!=null&&(this.left=on.number(e.left,t.left,n)),t.right!=null&&e.right!=null&&(this.right=on.number(e.right,t.right,n)),this}getCenter(e,t){let n=M((this.left+e-this.right)/2,0,e),r=M((this.top+t-this.bottom)/2,0,t);return new l(n,r)}equals(e){return this.top===e.top&&this.bottom===e.bottom&&this.left===e.left&&this.right===e.right}clone(){return new e(this.top,this.bottom,this.left,this.right)}toJSON(){return{top:this.top,bottom:this.bottom,left:this.left,right:this.right}}};function tc(e,t){if(!e.renderWorldCopies||e.lngRange)return;let n=t.lng-e.center.lng;t.lng+=n>180?-360:n<-180?360:0}function nc(e){return Math.max(0,Math.floor(e))}var rc=class{constructor(e,t){this.applyConstrain=(e,t)=>this._constrainOverride===null?this._callbacks.defaultConstrain(e,t):this._constrainOverride(e,t),this._callbacks=e,this._tileSize=512,this._renderWorldCopies=t?.renderWorldCopies===void 0||!!t?.renderWorldCopies,this._minZoom=t?.minZoom||0,this._maxZoom=t?.maxZoom||22,this._minPitch=t?.minPitch===void 0||t?.minPitch===null?0:t?.minPitch,this._maxPitch=t?.maxPitch===void 0||t?.maxPitch===null?60:t?.maxPitch,this._constrainOverride=t?.constrainOverride??null,this.setMaxBounds(),this._width=0,this._height=0,this._center=new V(0,0),this._elevation=0,this._zoom=0,this._tileZoom=nc(this._zoom),this._scale=d(this._zoom),this._bearingInRadians=0,this._fovInRadians=.6435011087932844,this._pitchInRadians=0,this._rollInRadians=0,this._unmodified=!0,this._edgeInsets=new ec,this._minElevationForCurrentTile=0,this._autoCalculateNearFarZ=!0}apply(e,t,n){this._constrainOverride=e.constrainOverride,this._latRange=e.latRange,this._lngRange=e.lngRange,this._width=e.width,this._height=e.height,this._center=e.center,this._elevation=e.elevation,this._minElevationForCurrentTile=e.minElevationForCurrentTile,this._zoom=e.zoom,this._tileZoom=nc(this._zoom),this._scale=d(this._zoom),this._bearingInRadians=e.bearingInRadians,this._fovInRadians=e.fovInRadians,this._pitchInRadians=e.pitchInRadians,this._rollInRadians=e.rollInRadians,this._unmodified=e.unmodified,this._edgeInsets=new ec(e.padding.top,e.padding.bottom,e.padding.left,e.padding.right),this._minZoom=e.minZoom,this._maxZoom=e.maxZoom,this._minPitch=e.minPitch,this._maxPitch=e.maxPitch,this._renderWorldCopies=e.renderWorldCopies,this._cameraToCenterDistance=e.cameraToCenterDistance,this._nearZ=e.nearZ,this._farZ=e.farZ,this._autoCalculateNearFarZ=!n&&e.autoCalculateNearFarZ,t&&this.constrainInternal(),this._calcMatrices()}get pixelsToClipSpaceMatrix(){return this._pixelsToClipSpaceMatrix}get clipSpaceToPixelsMatrix(){return this._clipSpaceToPixelsMatrix}get minElevationForCurrentTile(){return this._minElevationForCurrentTile}setMinElevationForCurrentTile(e){this._minElevationForCurrentTile=e}get tileSize(){return this._tileSize}get tileZoom(){return this._tileZoom}get scale(){return this._scale}get width(){return this._width}get height(){return this._height}get bearingInRadians(){return this._bearingInRadians}get lngRange(){return this._lngRange}get latRange(){return this._latRange}get pixelsToGLUnits(){return this._pixelsToGLUnits}get minZoom(){return this._minZoom}setMinZoom(e){if(this._minZoom===e)return;this._minZoom=e;let t=this._unmodified;this.setZoom(this.applyConstrain(this._center,this.zoom).zoom),this._unmodified=t}get maxZoom(){return this._maxZoom}setMaxZoom(e){if(this._maxZoom===e)return;this._maxZoom=e;let t=this._unmodified;this.setZoom(this.applyConstrain(this._center,this.zoom).zoom),this._unmodified=t}get minPitch(){return this._minPitch}setMinPitch(e){if(this._minPitch===e)return;this._minPitch=e;let t=this._unmodified;this.setPitch(Math.max(this.pitch,e)),this._unmodified=t}get maxPitch(){return this._maxPitch}setMaxPitch(e){if(this._maxPitch===e)return;this._maxPitch=e;let t=this._unmodified;this.setPitch(Math.min(this.pitch,e)),this._unmodified=t}get renderWorldCopies(){return this._renderWorldCopies}setRenderWorldCopies(e){e===void 0?e=!0:e===null&&(e=!1),this._renderWorldCopies=e}get constrainOverride(){return this._constrainOverride}setConstrainOverride(e){e===void 0&&(e=null),this._constrainOverride!==e&&(this._constrainOverride=e,this.constrainInternal(),this._calcMatrices())}get worldSize(){return this._tileSize*this._scale}get centerOffset(){return this.centerPoint._sub(this.size._div(2))}get size(){return new l(this._width,this._height)}get bearing(){return this._bearingInRadians/Math.PI*180}setBearing(e){let t=Or(e,-180,180)*Math.PI/180;this._bearingInRadians!==t&&(this._unmodified=!1,this._bearingInRadians=t,this._calcMatrices(),this._rotationMatrix=jr(),Pr(this._rotationMatrix,this._rotationMatrix,-this._bearingInRadians))}get rotationMatrix(){return this._rotationMatrix}get pitchInRadians(){return this._pitchInRadians}get pitch(){return this._pitchInRadians/Math.PI*180}setPitch(e){let t=M(e,this.minPitch,this.maxPitch)/180*Math.PI;this._pitchInRadians!==t&&(this._unmodified=!1,this._pitchInRadians=t,this._calcMatrices())}get rollInRadians(){return this._rollInRadians}get roll(){return this._rollInRadians/Math.PI*180}setRoll(e){let t=e/180*Math.PI;this._rollInRadians!==t&&(this._unmodified=!1,this._rollInRadians=t,this._calcMatrices())}get fovInRadians(){return this._fovInRadians}get fov(){return E(this._fovInRadians)}setFov(e){e=M(e,.1,150),this.fov!==e&&(this._unmodified=!1,this._fovInRadians=qt(e),this._calcMatrices())}get zoom(){return this._zoom}setZoom(e){let t=this.applyConstrain(this._center,e).zoom;this._zoom!==t&&(this._unmodified=!1,this._zoom=t,this._tileZoom=Math.max(0,Math.floor(t)),this._scale=d(t),this.constrainInternal(),this._calcMatrices())}get center(){return this._center}setCenter(e){(e.lat!==this._center.lat||e.lng!==this._center.lng)&&(this._unmodified=!1,this._center=e,this.constrainInternal(),this._calcMatrices())}get elevation(){return this._elevation}setElevation(e){e!==this._elevation&&(this._elevation=e,this.constrainInternal(),this._calcMatrices())}get padding(){return this._edgeInsets.toJSON()}setPadding(e){this._edgeInsets.equals(e)||(this._unmodified=!1,this._edgeInsets.interpolate(this._edgeInsets,e,1),this._calcMatrices())}get centerPoint(){return this._edgeInsets.getCenter(this._width,this._height)}get pixelsPerMeter(){return this._pixelPerMeter}get unmodified(){return this._unmodified}get cameraToCenterDistance(){return this._cameraToCenterDistance}get nearZ(){return this._nearZ}get farZ(){return this._farZ}get autoCalculateNearFarZ(){return this._autoCalculateNearFarZ}overrideNearFarZ(e,t){this._autoCalculateNearFarZ=!1,this._nearZ=e,this._farZ=t,this._calcMatrices()}clearNearFarZOverride(){this._autoCalculateNearFarZ=!0,this._calcMatrices()}isPaddingEqual(e){return this._edgeInsets.equals(e)}interpolatePadding(e,t,n){this._unmodified=!1,this._edgeInsets.interpolate(e,t,n),this.constrainInternal(),this._calcMatrices()}resize(e,t,n=!0){this._width=e,this._height=t,n&&this.constrainInternal(),this._calcMatrices()}getMaxBounds(){return this._latRange?.length!==2||this._lngRange?.length!==2?null:new _a([this._lngRange[0],this._latRange[0]],[this._lngRange[1],this._latRange[1]])}setMaxBounds(e){e?(this._lngRange=[e.getWest(),e.getEast()],this._latRange=[e.getSouth(),e.getNorth()],this.constrainInternal()):(this._lngRange=null,this._latRange=[-u,u])}getCameraQueryGeometry(e,t){if(t.length===1)return[t[0],e];{let{minX:n,minY:r,maxX:i,maxY:a}=On.fromPoints(t).extend(e);return[new l(n,r),new l(i,r),new l(i,a),new l(n,a),new l(n,r)]}}constrainInternal(){if(!this.center||!this._width||!this._height||this._constraining)return;this._constraining=!0;let e=this._unmodified,{center:t,zoom:n}=this.applyConstrain(this.center,this.zoom);this.setCenter(t),this.setZoom(n),this._unmodified=e,this._constraining=!1}_calcMatrices(){if(this._width&&this._height){this._pixelsToGLUnits=[2/this._width,-2/this._height];let e=$e(new Float64Array(16));ke(e,e,[this._width/2,-this._height/2,1]),Le(e,e,[1,-1,0]),this._clipSpaceToPixelsMatrix=e,e=$e(new Float64Array(16)),ke(e,e,[1,-1,1]),Le(e,e,[-1,-1,0]),ke(e,e,[2/this._width,2/this._height,1]),this._pixelsToClipSpaceMatrix=e;let t=this.fovInRadians/2;this._cameraToCenterDistance=.5/Math.tan(t)*this._height}this._callbacks.calcMatrices()}calculateCenterFromCameraLngLatAlt(e,t,n,r){let i=n===void 0?this.bearing:n,a=r=r===void 0?this.pitch:r,{distanceToCenter:o,clampedElevation:s}=this._distanceToCenterFromAltElevationPitch(t,this.elevation,a),{x:c,y:l}=_t(a,i),u=B.fromLngLat(e,t),d=dn(1,u.y),f,p,m=0;do{if(m+=1,m>10)break;p=o/d;let e=c*p,t=l*p;f=new B(u.x+e,u.y+t),d=1/f.meterInMercatorCoordinateUnits()}while(Math.abs(o-p*d)>1e-12);return{center:f.toLngLat(),elevation:s,zoom:Ee(this.height/2/Math.tan(this.fovInRadians/2)/p/this.tileSize)}}recalculateZoomAndCenter(e){if(this.elevation-e===0)return;let t=1/this.worldSize,n=Tn(1,this.center.lat)*this.worldSize,r=B.fromLngLat(this.center,this.elevation),i=r.x/t,a=r.y/t,o=r.z/t,s=this.pitch,c=this.bearing,{x:l,y:u,z:d}=_t(s,c),f=this.cameraToCenterDistance,p=i+f*-l,m=a+f*-u,h=o+f*d,{distanceToCenter:g,clampedElevation:_}=this._distanceToCenterFromAltElevationPitch(h/n,e,s),v=g*n,y=p+l*v,b=m+u*v,x=new B(y*t,b*t,0).toLngLat(),S=Tn(1,x.lat),C=Ee(this.height/2/Math.tan(this.fovInRadians/2)/g/S/this.tileSize);this._elevation=_,this._center=x,this.setZoom(C)}_distanceToCenterFromAltElevationPitch(e,t,n){let r=-Math.cos(qt(n)),i=e-t,a,o=t;return r*i>=0||Math.abs(r)<.1?(a=1e4,o=e+a*r):a=-i/r,{distanceToCenter:a,clampedElevation:o}}getCameraPoint(){let e=this.pitchInRadians,t=Math.tan(e)*(this.cameraToCenterDistance||1);return this.centerPoint.add(new l(t*Math.sin(this.rollInRadians),t*Math.cos(this.rollInRadians)))}getCameraAltitude(){return Math.cos(this.pitchInRadians)*this._cameraToCenterDistance/this._pixelPerMeter+this.elevation}getCameraLngLat(){return mt(this).toLngLat()}getMercatorTileCoordinates(e){if(!e)return[0,0,1,1];let t=e.canonical.z>=0?1<this.max[0]||e.aabb.min[1]>this.max[1]||e.aabb.min[2]>this.max[2]||e.aabb.max[0]0?(t+=e[r]*this.min[r],n+=e[r]*this.max[r]):(n+=e[r]*this.min[r],t+=e[r]*this.max[r]);return t>=0?2:n<0?0:1}},ac=class{distanceToTile2d(e,t,n,r){let i=r,a=i.distanceX([e,t]),o=i.distanceY([e,t]);return Math.hypot(a,o)}getWrap(e,t,n){return n}getTileBoundingVolume(e,t,n,r){let i=Math.min(0,n),a=Math.max(0,n);if(r?.terrain){let n=new $t(e.z,t,e.z,e.x,e.y),o=r.terrain.getMinMaxElevation(n);i=o.minElevation??i,a=o.maxElevation??a}let o=1<r}allowWorldCopies(){return!0}prepareNextFrame(){}},oc=class e{constructor(e,t,n){this.points=e,this.planes=t,this.aabb=n}static fromInvProjectionMatrix(t,n=1,r=0,i,a){let o=[[-1,1,-1,1],[1,1,-1,1],[1,-1,-1,1],[-1,-1,-1,1],[-1,1,1,1],[1,1,1,1],[1,-1,1,1],[-1,-1,1,1]],s=a?[[6,5,4],[0,1,2],[0,3,7],[2,1,5],[3,2,6],[0,4,5]]:[[0,1,2],[6,5,4],[0,3,7],[2,1,5],[3,2,6],[0,4,5]],c=2**r,l=o.map(e=>sc(e,t,n,c));i&&cc(l,s[0],i,a);let u=s.map(e=>{let t=En([],l[e[0]],l[e[1]]),n=En([],l[e[2]],l[e[1]]),r=Rt([],Gn([],t,n)),i=-ln(r,l[e[1]]);return r.concat(i)}),d=[1/0,1/0,1/0],f=[-1/0,-1/0,-1/0];for(let e of l)for(let t=0;t<3;t++)d[t]=Math.min(d[t],e[t]),f[t]=Math.max(f[t],e[t]);return new e(l,u,new ic(d,f))}};function sc(e,t,n,r){let i=Gt([],e,t),a=1/i[3]/n*r;return He(i,i,[a,a,1/i[3],a])}function cc(e,t,n,r){let i=r?4:0,a=r?0:4,o=0,s=[],c=[];for(let t=0;t<4;t++){let n=En([],e[t+a],e[t+i]),r=Nn(n);Xt(n,n,1/r),s.push(r),c.push(n)}for(let t=0;t<4;t++){let r=x(e[t+i],c[t],n);o=r!==null&&r>=0?Math.max(o,r):Math.max(o,s[t])}let l=lc(e,t),u=uc(n,l);if(u!==null){let e=u/ln(c[0],l);o=Math.min(o,e)}for(let t=0;t<4;t++){let n=Math.min(o,s[t]);e[t+a]=[e[t+i][0]+c[t][0]*n,e[t+i][1]+c[t][1]*n,e[t+i][2]+c[t][2]*n,1]}}function lc(e,t){let n=En([],e[t[0]],e[t[1]]),r=En([],e[t[2]],e[t[1]]),i=[0,0,0,0];return Rt(i,Gn([],n,r)),i[3]=-ln(i,e[t[0]]),i}function uc(e,t){let r=At(e),i=n([],e,1/r),a=En([],t,Xt([],i,ln(t,i))),o=At(a);if(o>0){let e=Math.sqrt(1-i[3]*i[3]),n=Xt([],i,-i[3]),r=$n([],n,Xt([],a,e/o));return tt(t,r)}return null}const dc=wt([{name:`a_pos3d`,type:`Int16`,components:3}]);var fc=class extends h{constructor(e){super(),this._lastTilesetChange=U(),this.tileManager=e,this._tiles={},this._renderableTilesKeys=[],this._sourceTileCache={},this.minzoom=0,this.maxzoom=22,this.deltaZoom=1,this.tileSize=e._source.tileSize*2**this.deltaZoom,e.usedForTerrain=!0,e.tileSize=this.tileSize}destruct(){this.tileManager.usedForTerrain=!1,this.tileManager.tileSize=null,this.releaseAllRTT()}getSource(){return this.tileManager._source}update(e,t){this.tileManager.update(e,t),this._renderableTilesKeys=[];let n={},r=!1;for(let i of So(e,{tileSize:this.tileSize,minzoom:this.minzoom,maxzoom:this.maxzoom,reparseOverscaled:!1,terrain:t,calculateTileZoom:this.tileManager._source.calculateTileZoom}))n[i.key]=!0,this._renderableTilesKeys.push(i.key),this._tiles[i.key]||(i.terrainRttPosMatrix32f=new Float32Array(16),Ne(i.terrainRttPosMatrix32f,0,N,N,0,0,1),this._tiles[i.key]=new po(i,this.tileSize),this._lastTilesetChange=U(),r=!0);for(let e in this._tiles)n[e]||(this._tiles[e].releaseRTT(this.tileManager.map.painter),delete this._tiles[e],r=!0);return r}releaseRTT(e){for(let t in this._tiles){let n=this._tiles[t];(n.tileID.equals(e)||n.tileID.isChildOf(e)||e.isChildOf(n.tileID))&&n.releaseRTT(this.tileManager.map.painter)}}releaseAllRTT(){for(let e in this._tiles)this._tiles[e].releaseRTT(this.tileManager.map.painter)}getRenderableTiles(){return this._renderableTilesKeys.map(e=>this.getTileByID(e))}getTileByID(e){return this._tiles[e]}getTerrainCoords(e,t){return t?this._getTerrainCoordsForTileRanges(e,t):this._getTerrainCoordsForRegularTile(e)}_getTerrainCoordsForRegularTile(e){let t={};for(let n of this._renderableTilesKeys){let r=this._tiles[n].tileID,i=e.clone(),a=vt();if(r.canonical.equals(e.canonical))Ne(a,0,N,N,0,0,1);else if(r.canonical.isChildOf(e.canonical)){let t=r.canonical.z-e.canonical.z,n=r.canonical.x-(r.canonical.x>>t<>t<>t;Ne(a,0,o,o,0,0,1),Le(a,a,[-n*o,-i*o,0])}else if(e.canonical.isChildOf(r.canonical)){let t=e.canonical.z-r.canonical.z,n=e.canonical.x-(e.canonical.x>>t<>t<>t;Ne(a,0,N,N,0,0,1),Le(a,a,[n*o,i*o,0]),ke(a,a,[1/2**t,1/2**t,0])}else continue;i.terrainRttPosMatrix32f=new Float32Array(a),t[n]=i}return t}_getTerrainCoordsForTileRanges(e,t){let n={};for(let r of this._renderableTilesKeys){let i=this._tiles[r].tileID;if(!this._isWithinTileRanges(i,t))continue;let a=e.clone(),o=vt();if(i.canonical.z===e.canonical.z){let t=e.canonical.x-i.canonical.x+e.wrap*(1<e.canonical.z){let t=i.canonical.z-e.canonical.z,n=i.canonical.x-(i.canonical.x>>t<>t<>t),s=e.canonical.y-(i.canonical.y>>t),c=N>>t;Ne(o,0,c,c,0,0,1),Le(o,o,[-n*c+a*N,-r*c+s*N,0])}else{let t=e.canonical.z-i.canonical.z,n=e.canonical.x-(e.canonical.x>>t<>t<>t)-i.canonical.x,s=(e.canonical.y>>t)-i.canonical.y,c=N<n.maxzoom&&(r=n.maxzoom),r=n.minzoom&&!i?.dem;)i=this.findTileInCaches(e.scaledTo(r--).key);return i}findTileInCaches(e){let t=this.tileManager.getTileByID(e);return t||(t=this.tileManager._outOfViewCache.getByKey(e),t)}anyTilesAfterTime(e=U()){return this._lastTilesetChange>=e}_isWithinTileRanges(e,t){let n=t[e.canonical.z];return!!n&&(e.wrap>n.minWrap||e.wrap=n.minTileXWrapped&&e.canonical.x<=n.maxTileXWrapped&&e.canonical.y>=n.minTileY&&e.canonical.y<=n.maxTileY)}};const pc=N*(1-1e-12);var mc=class{constructor(e,t,n,r=`auto`){this._meshCache={},this.painter=e,this.tileManager=new fc(t),this.options=n,this.exaggeration=typeof n.exaggeration==`number`?n.exaggeration:1,this._terrainSkirtLength=r,this.qualityFactor=2,this.meshSize=128,this._demMatrixCache=new Map,this._elevationSamplerCache=new Map}destroy(){this._fbo&&=(this._fbo.destroy(),null),this._fboDepthTexture&&=(this._fboDepthTexture.destroy(),null),this._emptyDemTexture&&=(this._emptyDemTexture.destroy(),null),this._emptyDepthTexture&&=(this._emptyDepthTexture.destroy(),null);for(let e in this._meshCache)this._meshCache[e].destroy();this._meshCache={},this.tileManager.destruct()}getDEMElevation(e,t,n,r=N){let i=e.normalizeCoordinates(t,n,r);if(!i)return 0;let a=this.getElevationSampler(i.tileID);return a?a(i.x,i.y,r):0}getElevationForLngLatZoom(e,t){if(!Nt(t,e.wrap()))return 0;let{tileID:n,mercatorX:r,mercatorY:i}=this._getOverscaledTileIDFromLngLatZoom(e,t);return this.getElevation(n,r%N,i%N,N)}getElevationForLngLat(e,t){let n=this.getCoverageIndex();if(n){let t=B.fromLngLat(e),r=gc(n,this.exaggeration,t.x,t.y);if(r.demLoaded)return r.elevation}let r=So(t,{maxzoom:this.tileManager.maxzoom,minzoom:this.tileManager.minzoom,tileSize:512,terrain:this}),i=0;for(let e of r)e.canonical.z>i&&(i=Math.min(e.canonical.z,this.tileManager.maxzoom));return this.getElevationForLngLatZoom(e,i)}getElevation(e,t,n,r=N){return this.getDEMElevation(e,t,n,r)*this.exaggeration}resetElevationCache(){this._elevationSamplerCache.clear(),this._coverageIndex=void 0}getCoverageIndex(){return this._coverageIndex===void 0&&(this._coverageIndex=this._buildCoverageIndex()),this._coverageIndex}_buildCoverageIndex(){let e=[],t=new Map,n=0,r=0;for(let i of this.tileManager.getRenderableTiles()){if(!i)continue;let{canonical:a,wrap:o}=i.tileID;e.includes(a.z)||e.push(a.z);let s=this.getElevationSampler(i.tileID);t.set(`${o}/${a.z}/${a.x}/${a.y}`,s);let{minElevation:c,maxElevation:l}=this.getMinMaxElevation(i.tileID);n=Math.min(n,c??0),r=Math.max(r,l??0)}return t.size===0?null:(e.sort((e,t)=>t-e),{zooms:e,samplerPerTile:t,minElevation:n-10,maxElevation:r+10})}getElevationSampler(e){let t=e.key,n=this._elevationSamplerCache.get(t);if(n)return n;let r=this.tileManager.getSourceTile(e,!0),i=r?.dem;if(!r||!i)return null;let a=this._getDEMTileMatrix(e,r),o=a[0]*i.dim,s=a[5]*i.dim,c=a[12]*i.dim,l=a[13]*i.dim,u=(e,t,n)=>{let r=n===8192?1:N/n;return i.sampleBilinear(e*r*o+c,t*r*s+l)};return this._elevationSamplerCache.set(t,u),u}_getDEMTileMatrix(e,t){let n=`${t.tileID.key}/${e.key}`,r=this._demMatrixCache.get(n);if(r)return r;let i=this.tileManager.getSource().maxzoom,a=e.canonical.z-t.tileID.canonical.z;e.overscaledZ>e.canonical.z&&(e.canonical.z>=i?a=e.canonical.z-i:I(`cannot calculate elevation if elevation maxzoom > source.maxzoom`));let o=e.canonical.x-(e.canonical.x>>a<>a<0,n=t&&e.canonical.y===0,r=t&&e.canonical.y===(1<=1)return hc;let i=Math.floor(n),a=n-i;for(let n of e.zooms){let o=1<i;a++){let i=(n+r)/2;t(e,i)?r=i:n=i}return{lo:n,hi:r}}var yc=class t{get pixelsToClipSpaceMatrix(){return this._helper.pixelsToClipSpaceMatrix}get clipSpaceToPixelsMatrix(){return this._helper.clipSpaceToPixelsMatrix}get pixelsToGLUnits(){return this._helper.pixelsToGLUnits}get centerOffset(){return this._helper.centerOffset}get size(){return this._helper.size}get rotationMatrix(){return this._helper.rotationMatrix}get centerPoint(){return this._helper.centerPoint}get pixelsPerMeter(){return this._helper.pixelsPerMeter}setMinZoom(e){this._helper.setMinZoom(e)}setMaxZoom(e){this._helper.setMaxZoom(e)}setMinPitch(e){this._helper.setMinPitch(e)}setMaxPitch(e){this._helper.setMaxPitch(e)}setRenderWorldCopies(e){this._helper.setRenderWorldCopies(e)}setBearing(e){this._helper.setBearing(e)}setPitch(e){this._helper.setPitch(e)}setRoll(e){this._helper.setRoll(e)}setFov(e){this._helper.setFov(e)}setZoom(e){this._helper.setZoom(e)}setCenter(e){this._helper.setCenter(e)}setElevation(e){this._helper.setElevation(e)}setMinElevationForCurrentTile(e){this._helper.setMinElevationForCurrentTile(e)}setPadding(e){this._helper.setPadding(e)}interpolatePadding(e,t,n){this._helper.interpolatePadding(e,t,n)}isPaddingEqual(e){return this._helper.isPaddingEqual(e)}resize(e,t,n=!0){this._helper.resize(e,t,n)}getMaxBounds(){return this._helper.getMaxBounds()}setMaxBounds(e){this._helper.setMaxBounds(e)}setConstrainOverride(e){this._helper.setConstrainOverride(e)}overrideNearFarZ(e,t){this._helper.overrideNearFarZ(e,t)}clearNearFarZOverride(){this._helper.clearNearFarZOverride()}getCameraQueryGeometry(e){return this._helper.getCameraQueryGeometry(this.getCameraPoint(),e)}get tileSize(){return this._helper.tileSize}get tileZoom(){return this._helper.tileZoom}get scale(){return this._helper.scale}get worldSize(){return this._helper.worldSize}get width(){return this._helper.width}get height(){return this._helper.height}get lngRange(){return this._helper.lngRange}get latRange(){return this._helper.latRange}get minZoom(){return this._helper.minZoom}get maxZoom(){return this._helper.maxZoom}get zoom(){return this._helper.zoom}get center(){return this._helper.center}get minPitch(){return this._helper.minPitch}get maxPitch(){return this._helper.maxPitch}get pitch(){return this._helper.pitch}get pitchInRadians(){return this._helper.pitchInRadians}get roll(){return this._helper.roll}get rollInRadians(){return this._helper.rollInRadians}get bearing(){return this._helper.bearing}get bearingInRadians(){return this._helper.bearingInRadians}get fov(){return this._helper.fov}get fovInRadians(){return this._helper.fovInRadians}get elevation(){return this._helper.elevation}get minElevationForCurrentTile(){return this._helper.minElevationForCurrentTile}get padding(){return this._helper.padding}get unmodified(){return this._helper.unmodified}get renderWorldCopies(){return this._helper.renderWorldCopies}get cameraToCenterDistance(){return this._helper.cameraToCenterDistance}get constrainOverride(){return this._helper.constrainOverride}get nearZ(){return this._helper.nearZ}get farZ(){return this._helper.farZ}get autoCalculateNearFarZ(){return this._helper.autoCalculateNearFarZ}setTransitionState(e){}constructor(e){this._posMatrixCache=new Map,this._alignedPosMatrixCache=new Map,this._fogMatrixCacheF32=new Map,this.defaultConstrain=(e,t)=>{t=M(+t,this.minZoom,this.maxZoom);let n={center:new V(e.lng,e.lat),zoom:t},r=this._helper._lngRange;!this._helper._renderWorldCopies&&r===null&&(r=[-179.9999999999,180-1e-10]);let i=this.tileSize*d(n.zoom),a=0,o=i,s=0,c=i,u=0,f=0,{x:p,y:m}=this.size;if(this._helper._latRange){let e=this._helper._latRange;a=Vt(e[1])*i,o=Vt(e[0])*i,o-ao&&(v=o-e)}if(r){let e=(s+c)/2,t=h;this._helper._renderWorldCopies&&(t=Or(h,e-i/2,e+i/2));let n=p/2;t-nc&&(_=c-n)}if(_!==void 0||v!==void 0){let e=new l(_??h,v??g);n.center=Mn(i,e).wrap()}return n},this.applyConstrain=(e,t)=>this._helper.applyConstrain(e,t),this._helper=new rc({calcMatrices:()=>this._calcMatrices(),defaultConstrain:(e,t)=>this.defaultConstrain(e,t)},e),this._coveringTilesDetailsProvider=new ac}clone(){let e=new t;return e.apply(this,!1),e}apply(e,t,n){this._helper.apply(e,t,n)}get cameraPosition(){return this._cameraPosition}get projectionMatrix(){return this._projectionMatrix}get modelViewProjectionMatrix(){return this._viewProjMatrix}get inverseProjectionMatrix(){return this._invProjMatrix}get mercatorMatrix(){return this._mercatorMatrix}getVisibleUnwrappedCoordinates(e){let t=[new Tt(0,e)];if(this._helper._renderWorldCopies){let n=this.screenPointToMercatorCoordinate(new l(0,0)),r=this.screenPointToMercatorCoordinate(new l(this._helper._width,0)),i=this.screenPointToMercatorCoordinate(new l(this._helper._width,this._helper._height)),a=this.screenPointToMercatorCoordinate(new l(0,this._helper._height)),o=Math.floor(Math.min(n.x,r.x,i.x,a.x)),s=Math.floor(Math.max(n.x,r.x,i.x,a.x));for(let n=o-1;n<=s+1;n++)n!==0&&t.push(new Tt(n,e))}return t}getCameraFrustum(){return oc.fromInvProjectionMatrix(this._invViewProjMatrix,this.worldSize)}getClippingPlane(){return null}getCoveringTilesDetailsProvider(){return this._coveringTilesDetailsProvider}recalculateZoomAndCenter(e){let t=this.screenPointToLocation(this.centerPoint,e),n=e?e.getElevationForLngLat(t,this):0;this._helper.recalculateZoomAndCenter(n)}setLocationAtPoint(e,t,n=this.elevation){let r=n-this.elevation,i=this.screenPointToMercatorCoordinateAtZ(t,r),a=this.screenPointToMercatorCoordinateAtZ(this.centerPoint,0),o=B.fromLngLat(e),s=new B(o.x-(i.x-a.x),o.y-(i.y-a.y));this.setCenter(s?.toLngLat()),this._helper._renderWorldCopies&&this.setCenter(this.center.wrap())}locationToScreenPoint(e,t){return t?this.coordinatePoint(B.fromLngLat(e),t.getElevationForLngLat(e,this),this._pixelMatrix3D):this.coordinatePoint(B.fromLngLat(e))}screenPointToLocation(e,t){return this.screenPointToMercatorCoordinate(e,t)?.toLngLat()}screenPointToLocationAtElevation(e,t){return this.screenPointToMercatorCoordinateAtZ(e,t-this.elevation)?.toLngLat()}screenPointToMercatorCoordinate(e,t){if(t){let n=this.screenTerrainPointToMercatorCoordinate(e,t);if(n!=null)return n}return this.screenPointToMercatorCoordinateAtZ(e)}screenTerrainPointToMercatorCoordinate(e,t){let n=t.getCoverageIndex();if(!n)return null;let{near:r,far:i}=this.getRaySegmentFromPixel(e),a=this.worldSize,o=i[0]-r[0],s=i[1]-r[1],c=i[2]-r[2],l={index:n,exaggeration:t.exaggeration,near:r,dx:o,dy:s,dz:c,worldSize:a},u=0,d=1;if(c===0){if(r[2]>n.maxElevation||r[2]d)return null}let f=Math.hypot(o,s),p=M(Math.ceil(f*(d-u)/4),1,512),m=0,h=!xc(l,0);for(let e=0;e<=p;e++){let t=u+(d-u)*e/p;if(!h)h=!xc(l,t);else if(xc(l,t)){let{lo:e,hi:n}=vc(l,xc,m,t,.001/f),i=bc(l,e),u=bc(l,n),d=r[2]+e*c-i.elevation,p=r[2]+n*c-u.elevation,h=i.covered&&d>p?M(e+d*(n-e)/(d-p),e,n):n;return new B((r[0]+h*o)/a,(r[1]+h*s)/a,bc(l,h).elevation)}m=t}return null}getRaySegmentFromPixel(e){let t=[e.x,e.y,0,1],n=[e.x,e.y,1,1];Gt(t,t,this._pixelMatrixInverse),Gt(n,n,this._pixelMatrixInverse);let r=t[3],i=n[3],a=this.elevation;return{near:[t[0]/r,t[1]/r,t[2]/r+a],far:[n[0]/i,n[1]/i,n[2]/i+a]}}screenPointToMercatorCoordinateAtZ(e,t){let n=t||0,{near:r,far:i}=this.getRaySegmentFromPixel(e),a=r[2]===i[2]?0:(n+this.elevation-r[2])/(i[2]-r[2]);return new B(on.number(r[0],i[0],a)/this.worldSize,on.number(r[1],i[1],a)/this.worldSize,n)}coordinatePoint(e,t=0,n=this._pixelMatrix){let r=[e.x*this.worldSize,e.y*this.worldSize,t,1];return Gt(r,r,n),new l(r[0]/r[3],r[1]/r[3])}getBounds(){let e=Math.max(0,this._helper._height/2-Be(this));return new _a().extend(this.screenPointToLocation(new l(0,e))).extend(this.screenPointToLocation(new l(this._helper._width,e))).extend(this.screenPointToLocation(new l(this._helper._width,this._helper._height))).extend(this.screenPointToLocation(new l(0,this._helper._height)))}isPointOnMapSurface(e,t){return t?this.screenTerrainPointToMercatorCoordinate(e,t)!=null:e.y>this.height/2-Be(this)}calculatePosMatrix(e,t=!1,n=!1){let r=e.key??sr(e.wrap,e.canonical.z,e.canonical.z,e.canonical.x,e.canonical.y),i=t?this._alignedPosMatrixCache:this._posMatrixCache;if(i.has(r)){let e=i.get(r);return n?e.f32:e.f64}let a=de(e,this.worldSize);y(a,t?this._alignedProjMatrix:this._viewProjMatrix,a);let o={f64:a,f32:new Float32Array(a)};return i.set(r,o),n?o.f32:o.f64}calculateFogMatrix(e){let t=e.key,n=this._fogMatrixCacheF32;if(n.has(t))return n.get(t);let r=de(e,this.worldSize);return y(r,this._fogMatrix,r),n.set(t,new Float32Array(r)),n.get(t)}calculateCenterFromCameraLngLatAlt(e,t,n,r){return this._helper.calculateCenterFromCameraLngLatAlt(e,t,n,r)}_calculateNearFarZIfNeeded(t,n,r){if(!this._helper.autoCalculateNearFarZ)return;let i=Math.min(this.elevation,this.minElevationForCurrentTile,this.getCameraAltitude()-100),a=t-i*this._helper._pixelPerMeter/Math.cos(n),o=i<0?a:t,s=Math.PI/2+this.pitchInRadians,c=qt(this.fov)*(Math.abs(Math.cos(qt(this.roll)))*this.height+Math.abs(Math.sin(qt(this.roll)))*this.width)/this.height*(.5+r.y/this.height),l=Math.sin(c)*o/Math.sin(M(Math.PI-s-c,.01,Math.PI-.01)),u=Be(this),d=Math.atan(u/this._helper.cameraToCenterDistance),f=qt(90-e),p=d>f?2*d*(.5+r.y/(u*2)):f,m=Math.sin(p)*o/Math.sin(M(Math.PI-s-p,.01,Math.PI-.01)),h=Math.min(l,m);this._helper._farZ=(Math.cos(Math.PI/2-n)*h+o)*1.01,this._helper._nearZ=this._helper._height/50}_calcMatrices(){if(!this._helper._height)return;let t=this.centerOffset,n=Jt(this.worldSize,this.center),r=n.x,i=n.y;this._helper._pixelPerMeter=Tn(1,this.center.lat)*this.worldSize;let o=qt(Math.min(this.pitch,e)),s=Math.max(this._helper.cameraToCenterDistance/2,this._helper.cameraToCenterDistance+this._helper._elevation*this._helper._pixelPerMeter/Math.cos(o));this._calculateNearFarZIfNeeded(s,o,t);let c;c=new Float64Array(16),vn(c,this.fovInRadians,this._helper._width/this._helper._height,this._helper._nearZ,this._helper._farZ),this._invProjMatrix=new Float64Array(16),zo(this._invProjMatrix,c),c[8]=-t.x*2/this._helper._width,c[9]=t.y*2/this._helper._height,this._projectionMatrix=Sr(c),ke(c,c,[1,-1,1]),Le(c,c,[0,0,-this._helper.cameraToCenterDistance]),we(c,c,-this.rollInRadians),a(c,c,this.pitchInRadians),we(c,c,-this.bearingInRadians),Le(c,c,[-r,-i,0]),this._mercatorMatrix=ke([],c,[this.worldSize,this.worldSize,this.worldSize]),ke(c,c,[1,1,this._helper._pixelPerMeter]),this._pixelMatrix=y(new Float64Array(16),this.clipSpaceToPixelsMatrix,c),Le(c,c,[0,0,-this.elevation]),this._viewProjMatrix=c,this._invViewProjMatrix=w([],c);let l=[0,0,-1,1];Gt(l,l,this._invViewProjMatrix),this._cameraPosition=[l[0]/l[3],l[1]/l[3],l[2]/l[3]],this._fogMatrix=new Float64Array(16),vn(this._fogMatrix,this.fovInRadians,this.width/this.height,s,this._helper._farZ),this._fogMatrix[8]=-t.x*2/this.width,this._fogMatrix[9]=t.y*2/this.height,ke(this._fogMatrix,this._fogMatrix,[1,-1,1]),Le(this._fogMatrix,this._fogMatrix,[0,0,-this.cameraToCenterDistance]),we(this._fogMatrix,this._fogMatrix,-this.rollInRadians),a(this._fogMatrix,this._fogMatrix,this.pitchInRadians),we(this._fogMatrix,this._fogMatrix,-this.bearingInRadians),Le(this._fogMatrix,this._fogMatrix,[-r,-i,0]),ke(this._fogMatrix,this._fogMatrix,[1,1,this._helper._pixelPerMeter]),Le(this._fogMatrix,this._fogMatrix,[0,0,-this.elevation]),this._pixelMatrix3D=y(new Float64Array(16),this.clipSpaceToPixelsMatrix,c);let u=this._helper._width%2/2,d=this._helper._height%2/2,f=Math.cos(this.bearingInRadians),p=Math.sin(-this.bearingInRadians),m=r-Math.round(r)+f*u+p*d,h=i-Math.round(i)+f*d+p*u,g=new Float64Array(c);if(Le(g,g,[m>.5?m-1:m,h>.5?h-1:h,0]),this._alignedProjMatrix=g,c=w(new Float64Array(16),this._pixelMatrix),!c)throw Error(`failed to invert matrix`);this._pixelMatrixInverse=c,this._clearMatrixCaches()}_clearMatrixCaches(){this._posMatrixCache.clear(),this._alignedPosMatrixCache.clear(),this._fogMatrixCacheF32.clear()}maxPitchScaleFactor(){if(!this._pixelMatrixInverse)return 1;let e=this.screenPointToMercatorCoordinate(new l(0,0)),t=[e.x*this.worldSize,e.y*this.worldSize,0,1];return Gt(t,t,this._pixelMatrix)[3]/this._helper.cameraToCenterDistance}getCameraPoint(){return this._helper.getCameraPoint()}getCameraAltitude(){return this._helper.getCameraAltitude()}getCameraLngLat(){let e=Tn(1,this.center.lat)*this.worldSize,t=this._helper.cameraToCenterDistance/e;return xt(this.center,this.elevation,this.pitch,this.bearing,t).toLngLat()}lngLatToCameraDepth(e,t){let n=B.fromLngLat(e),r=[n.x*this.worldSize,n.y*this.worldSize,t,1];return Gt(r,r,this._viewProjMatrix),r[2]/r[3]}getProjectionData(e){let{overscaledTileID:t,aligned:n,applyTerrainMatrix:r}=e,i=this._helper.getMercatorTileCoordinates(t),a=t?this.calculatePosMatrix(t,n,!0):null,o;return o=t?.terrainRttPosMatrix32f&&r?t.terrainRttPosMatrix32f:a||Vn(),{mainMatrix:o,tileMercatorCoords:i,clippingPlane:[0,0,0,0],projectionTransition:0,fallbackMatrix:o,clipAntimeridian:!1}}isLocationOccluded(e){return!1}getPixelScale(){return 1}getCircleRadiusCorrection(){return 1}getPitchedTextCorrection(e,t,n){return 1}transformLightDirection(e){return kn(e)}getRayDirectionFromPixel(e){throw Error(`Not implemented.`)}projectTileCoordinates(e,t,n,r){let i=this.calculatePosMatrix(n),a;r==null?(a=[e,t,0,1],ls(a,a,i)):(a=[e,t,r,1],Gt(a,a,i));let o=a[3];return{point:new l(a[0]/o,a[1]/o),signedDistanceFromCamera:o,isOccluded:!1}}populateCache(e){for(let t of e)this.calculatePosMatrix(t)}getProjectionDataForCustomLayer(e=!0){let t=new $t(0,0,0,0,0),n=this.getProjectionData({overscaledTileID:t,applyGlobeMatrix:e}),r=de(t,this.worldSize);y(r,this._viewProjMatrix,r);let i=[N,N,this.worldSize/this._helper.pixelsPerMeter],a=vt();return ke(a,r,i),{...n,tileMercatorCoords:[0,0,1,1],fallbackMatrix:a,mainMatrix:a}}getFastPathSimpleProjectionMatrix(e){return this.calculatePosMatrix(e)}};function bc(e,t){return gc(e.index,e.exaggeration,(e.near[0]+t*e.dx)/e.worldSize,(e.near[1]+t*e.dy)/e.worldSize)}function xc(e,t){return _c(bc(e,t),e.near[2]+t*e.dz)}function Sc(){I(`Map cannot fit within canvas with the given bounds, padding, and/or offset.`)}function Cc(e){if(e.useSlerp){if(e.k<1){let t=hn(e.startEulerAngles.roll,e.startEulerAngles.pitch,e.startEulerAngles.bearing),n=hn(e.endEulerAngles.roll,e.endEulerAngles.pitch,e.endEulerAngles.bearing),r=new Float64Array(4);Ct(r,t,n,e.k);let i=Qt(r);e.tr.setRoll(i.roll),e.tr.setPitch(i.pitch),e.tr.setBearing(i.bearing)}else e.tr.setRoll(e.endEulerAngles.roll),e.tr.setPitch(e.endEulerAngles.pitch),e.tr.setBearing(e.endEulerAngles.bearing)}else e.tr.setRoll(on.number(e.startEulerAngles.roll,e.endEulerAngles.roll,e.k)),e.tr.setPitch(on.number(e.startEulerAngles.pitch,e.endEulerAngles.pitch,e.k)),e.tr.setBearing(on.number(e.startEulerAngles.bearing,e.endEulerAngles.bearing,e.k))}function wc(e,t,n,r,i){let a=i.padding,o=Jt(i.worldSize,n.getNorthWest()),s=Jt(i.worldSize,n.getNorthEast()),c=Jt(i.worldSize,n.getSouthEast()),u=Jt(i.worldSize,n.getSouthWest()),f=qt(-r),p=o.rotate(f),m=s.rotate(f),h=c.rotate(f),g=u.rotate(f),_=new l(Math.max(p.x,m.x,g.x,h.x),Math.max(p.y,m.y,g.y,h.y)),v=new l(Math.min(p.x,m.x,g.x,h.x),Math.min(p.y,m.y,g.y,h.y)),y=_.sub(v),b=i.width-(a.left+a.right+t.left+t.right),x=i.height-(a.top+a.bottom+t.top+t.bottom),S=b/y.x,C=x/y.y;if(C<0||S<0){Sc();return}let w=Math.min(Ee(i.scale*Math.min(S,C)),e.maxZoom),T=l.convert(e.offset),E=(t.left-t.right)/2,D=(t.top-t.bottom)/2,ee=new l(E,D).rotate(qt(r)),O=T.add(ee).mult(i.scale/d(w));return{center:Mn(i.worldSize,o.add(c).div(2).sub(O)),zoom:w,bearing:r}}var Tc=class{get useGlobeControls(){return!1}handlePanInertia(e,t){let n=e.mag(),r=Math.abs(Be(t));return{easingOffset:e.mult(Math.min(r*.75/n,1)),easingCenter:t.center}}handleMapControlsRollPitchBearingZoom(e,t){e.bearingDelta&&t.setBearing(t.bearing+e.bearingDelta),e.pitchDelta&&t.setPitch(t.pitch+e.pitchDelta),e.rollDelta&&t.setRoll(t.roll+e.rollDelta),e.zoomDelta&&t.setZoom(t.zoom+e.zoomDelta)}handleMapControlsPan(e,t,n){e.around.distSqr(t.centerPoint)<.01||t.setLocationAtPoint(n,e.around,e.aroundElevation)}cameraForBoxAndBearing(e,t,n,r,i){return wc(e,t,n,r,i)}handleJumpToCenterZoom(e,t){let n=t.zoom===void 0?e.zoom:+t.zoom;e.zoom!==n&&e.setZoom(+t.zoom),t.center!==void 0&&e.setCenter(V.convert(t.center))}handleEaseTo(e,t){let n=e.zoom,r=e.padding,i={roll:e.roll,pitch:e.pitch,bearing:e.bearing},a={roll:t.roll===void 0?e.roll:t.roll,pitch:t.pitch===void 0?e.pitch:t.pitch,bearing:t.bearing===void 0?e.bearing:t.bearing},o=t.zoom!==void 0,c=!e.isPaddingEqual(t.padding),l=!1,u=o?+t.zoom:e.zoom,f=e.centerPoint.add(t.offsetAsPoint),p=e.screenPointToLocation(f),{center:m,zoom:h}=e.applyConstrain(V.convert(t.center||p),u??n);tc(e,m);let g=Jt(e.worldSize,p),_=Jt(e.worldSize,m).sub(g),v=d(h-n);return l=h!==n,{easeFunc:o=>{if(l&&e.setZoom(on.number(n,h,o)),s(i,a)||Cc({startEulerAngles:i,endEulerAngles:a,tr:e,k:o,useSlerp:i.roll!=a.roll}),c&&(e.interpolatePadding(r,t.padding,o),f=e.centerPoint.add(t.offsetAsPoint)),t.around)e.setLocationAtPoint(t.around,t.aroundPoint);else{let t=d(e.zoom-n),r=(h>n?Math.min(2,v):Math.max(.5,v))**(1-o),i=Mn(e.worldSize,g.add(_.mult(o*r)).mult(t));e.setLocationAtPoint(e.renderWorldCopies?i.wrap():i,f)}},isZooming:l,elevationCenter:m}}handleFlyTo(e,t){let n=t.zoom!==void 0,r=e.zoom,i=e.applyConstrain(V.convert(t.center||t.locationAtOffset),n?+t.zoom:r),a=i.center,o=i.zoom;tc(e,a);let s=e.worldSize,c=Jt(s,t.locationAtOffset),l=Jt(s,a).sub(c),u=l.mag(),f=d(o-r),p=t.minZoom===void 0?e.minZoom:+t.minZoom,m=Math.max(p,e.minZoom),h=Math.min(m,r,o),g=e.applyConstrain(a,h).zoom;return{easeFunc:(t,n,i,u)=>{e.setZoom(t===1?o:r+Ee(n));let d=t===1?a:Mn(s,c.add(l.mult(i)));e.setLocationAtPoint(e.renderWorldCopies?d.wrap():d,u)},scaleOfZoom:f,targetCenter:a,scaleOfMinZoom:d(g-r),pixelPathLength:u}}};let Ec;const Dc=()=>Ec||=new Kt({type:new r(Ft.projection.type,`type`)}),Oc=new Lt({fill:new Tr(128,2),line:new Tr(512,0),tile:new Tr(128,32),stencil:new Tr(128,1),circle:3});var kc=class{constructor(){this._tileMeshCache={}}get name(){return`vertical-perspective`}get transitionState(){return 1}get useSubdivision(){return!0}get shaderVariantName(){return`globe`}get shaderDefine(){return`#define GLOBE`}get shaderPreludeCode(){return Xs.projectionGlobe}get vertexShaderPreludeCode(){return Xs.projectionMercator.vertexSource}get subdivisionGranularity(){return Oc}get useGlobeControls(){return!0}destroy(){}_getMeshKey(e){return`${e.granularity.toString(36)}_${e.generateBorders?`b`:``}${e.extendToNorthPole?`n`:``}${e.extendToSouthPole?`s`:``}`}getMeshFromTileID(e,t,n,r,i){let a=(i===`stencil`?Oc.stencil:Oc.tile).getGranularityForZoomLevel(t.z),o=t.y===0&&r,s=t.y===(1<0}get currentProjection(){return this.useGlobeRendering?this._verticalPerspectiveProjection:this._mercatorProjection}get name(){return`globe`}get useSubdivision(){return this.currentProjection.useSubdivision}get shaderVariantName(){return this.currentProjection.shaderVariantName}get shaderDefine(){return this.currentProjection.shaderDefine}get shaderPreludeCode(){return this.currentProjection.shaderPreludeCode}get vertexShaderPreludeCode(){return this.currentProjection.vertexShaderPreludeCode}get subdivisionGranularity(){return this.currentProjection.subdivisionGranularity}get useGlobeControls(){return this.transitionState>0}destroy(){this._mercatorProjection.destroy(),this._verticalPerspectiveProjection.destroy()}getMeshFromTileID(e,t,n,r,i){return this.currentProjection.getMeshFromTileID(e,t,n,r,i)}setProjection(e){this._transitionable.setValue(`type`,e?.type||`mercator`)}updateTransitions(e){this._transitioning=this._transitionable.transitioned(e,this._transitioning)}hasTransition(){return this._transitioning.hasTransition()||this.currentProjection.hasTransition()}recalculate(e){this.properties=this._transitioning.possiblyEvaluate(e)}};function jc(e){let t=Lc(e.worldSize,e.center.lat);return 2*Math.PI*t}function Mc(e,t,n){let r=Ic(t),i=Ic(n),a=ln(r,i),o=Math.acos(a),s=jc(e);return o/(2*Math.PI)*s}function Nc(e,t){return[yr(e*Math.PI*2+Math.PI,Math.PI*2),2*Math.atan(Math.exp(Math.PI-t*Math.PI*2))-Math.PI*.5]}function Pc(e,t){let n=Math.cos(t),r=new Float64Array(3);return r[0]=Math.sin(e)*n,r[1]=Math.sin(t),r[2]=Math.cos(e)*n,r}function Fc(e,t,n,r,i){let a=1/(1<1e-6){let r=e[0]/n,i=e[2]/n,a=Math.acos(i),o=(r>0?a:-a)/Math.PI*180;return new V(Or(o,-180,180),t)}return new V(0,t)}function zc(e,t){return pe(St(),-e.lng,-e.lat,t)}function Bc(e){let t=e[0],n=e[1],r=e[2],i=e[3];return{lng:-Math.atan2(2*(i*t+n*r),1-2*(t*t+n*n))*180/Math.PI,lat:-Math.asin(M(2*(i*n-r*t),-1,1))*180/Math.PI,bearing:Math.atan2(2*(i*r+t*n),1-2*(n*n+r*r))*180/Math.PI}}const Vc=Math.PI*.98;function Hc(e,t){let n=e.cameraPosition,r=Nn(n);if(r<=1)return e.screenPointToLocation(t);let i=L();Rt(i,n);let a=e.getRayDirectionFromPixel(t),o=-ln(a,i),s=L();Ln(s,a,i,o);let c=Nn(s);if(c<1e-9)return e.screenPointToLocation(t);let l=Math.atan2(c,o),u=Math.asin(1/r)*.9;if(l=0?90:-90,s=e.locationToScreenPoint(new V(0,o)),c=t.x-s.x,l=t.y-s.y,d=c*c+l*l,f=M(1-(u-Math.abs(a))/12,0,1),p=f*f*(3-2*f),m=yr(r-i+180,360)-180,h=0;if(p>0&&n){let e=(c*n.y-l*n.x)/Math.max(d,400);h=(o>0?1:-1)*e*180/Math.PI}return i+(1-p)*m+p*h}function Gc(e){let t=L();return t[0]=e[0]*-e[3],t[1]=e[1]*-e[3],t[2]=e[2]*-e[3],{center:t,radius:Math.sqrt(1-e[3]*e[3])}}function Kc(e,t,n){let r=L();En(r,n,e);let i=L();return Ln(i,e,r,t/At(r)),i}function qc(e){return Math.cos(e*Math.PI/180)}function Jc(e,t){let n=qc(e),r=qc(t);return Ee(r/n)}function Yc(e,t){return 360/jc({worldSize:e,center:{lat:t}})}function Xc(e,t){let n=e.rotate(t.bearingInRadians),r=t.zoom+Jc(t.center.lat,0),i=wr(1/qc(t.center.lat),1/qc(Math.min(Math.abs(t.center.lat),60)),bn(r,7,3,0,1)),a=Yc(t.worldSize,t.center.lat);return new V(t.center.lng-n.x*a*i,M(t.center.lat+n.y*a,-u,u))}function Zc(e){let t=.5*e,n=Math.sin(t),r=Math.cos(t);return Math.log(n+r)-Math.log(r-n)}function Qc(e,t,n,r){let i=e.lat+n*r;if(Math.abs(n)>1){let a=e.lat+n,o=(Math.sign(a)===Math.sign(e.lat)?Math.abs(e.lat):-Math.abs(e.lat))*Math.PI/180,s=Math.abs(e.lat+n)*Math.PI/180,c=Zc(o+r*(s-o)),l=Zc(o),u=Zc(s),d=(c-l)/(u-l),f=e.lng+t*d;return new V(f,i)}{let n=e.lng+t*r;return new V(n,i)}}function $c(e,t,n=1){let r=ln(e,t),i=n*n,a=L(),o=L();Xt(o,t,r),En(a,e,o);let s=i-ln(a,a);if(s<0)return null;let c=ln(e,e)-i,l=-r+(r<0?1:-1)*Math.sqrt(s),u=c/l,d=l;return{tMin:Math.min(u,d),tMax:Math.max(u,d)}}var el=class{constructor(e){this._cachePrevious=new Map,this._cache=new Map,this._hadAnyChanges=!1,this._boundingVolumeFactory=e}swapBuffers(){if(!this._hadAnyChanges)return;let e=this._cachePrevious;this._cachePrevious=this._cache,this._cache=e,this._cache.clear(),this._hadAnyChanges=!1}getTileBoundingVolume(e,t,n,r){let i=`${e.z}_${e.x}_${e.y}_${r?.terrain?`t`:``}_${Math.round(n)}`,a=this._cache.get(i);if(a)return a;let o=this._cachePrevious.get(i);if(o)return this._cache.set(i,o),o;let s=this._boundingVolumeFactory(e,t,n,r);return this._cache.set(i,s),this._hadAnyChanges=!0,s}},tl=class e{constructor(e,t,n,r){this.min=n,this.max=r,this.points=e,this.planes=t}static fromAabb(t,n){let r=[];for(let e=0;e<8;e++)r.push([(e>>0&1)==1?n[0]:t[0],(e>>1&1)==1?n[1]:t[1],(e>>2&1)==1?n[2]:t[2]]);return new e(r,[[-1,0,0,n[0]],[1,0,0,-t[0]],[0,-1,0,n[1]],[0,1,0,-t[1]],[0,0,-1,n[2]],[0,0,1,-t[2]]],t,n)}static fromCenterSizeAngles(t,n,r){let i=pe([],r[0],r[1],r[2]),a=Et([],[n[0],0,0],i),o=Et([],[0,n[1],0],i),s=Et([],[0,0,n[2]],i),c=[...t],l=[...t];for(let e=0;e<8;e++)for(let n=0;n<3;n++){let r=t[n]+a[n]*((e>>0&1)==1?1:-1)+o[n]*((e>>1&1)==1?1:-1)+s[n]*((e>>2&1)==1?1:-1);c[n]=Math.min(c[n],r),l[n]=Math.max(l[n],r)}let u=[];for(let e=0;e<8;e++){let n=[...t];$n(n,n,Xt([],a,(e>>0&1)==1?1:-1)),$n(n,n,Xt([],o,(e>>1&1)==1?1:-1)),$n(n,n,Xt([],s,(e>>2&1)==1?1:-1)),u.push(n)}return new e(u,[[...a,-ln(a,u[0])],[...o,-ln(o,u[0])],[...s,-ln(s,u[0])],[-a[0],-a[1],-a[2],-ln(a,u[7])],[-o[0],-o[1],-o[2],-ln(o,u[7])],[-s[0],-s[1],-s[2],-ln(s,u[7])]],c,l)}intersectsFrustum(e){let t=!0,n=this.points.length,r=this.planes.length,i=e.planes.length,a=e.points.length;for(let r=0;r=0&&a++}if(a===0)return 0;a=0&&r++}if(r===0)return 0}return 1}intersectsPlane(e){let t=this.points.length,n=0;for(let r=0;r=0&&n++}return n===t?2:n===0?0:1}};function nl(e,t,n){let r=e-t;return r<0?-r:Math.max(0,r-n)}function rl(e,t,n,r,i){let a=e-n,o;return o=a<0?Math.min(-a,1+a-i):a>i?Math.min(Math.max(a-i,0),1-a):0,Math.max(o,nl(t,r,i))}var il=class{constructor(){this._boundingVolumeCache=new el(this._computeTileBoundingVolume)}prepareNextFrame(){this._boundingVolumeCache.swapBuffers()}distanceToTile2d(e,t,n,r){let i=1<4}allowWorldCopies(){return!1}getTileBoundingVolume(e,t,n,r){return this._boundingVolumeCache.getTileBoundingVolume(e,t,n,r)}_computeTileBoundingVolume(e,t,n,r){let i=Math.min(0,n),a=Math.max(0,n);if(r?.terrain){let n=new $t(e.z,t,e.z,e.x,e.y),o=r.terrain.getMinMaxElevation(n);i=o.minElevation??i,a=o.maxElevation??a}if(i/=Wt,a/=Wt,i+=1,a+=1,e.z<=0)return tl.fromAabb([-a,-a,-a],[a,a,a]);if(e.z===1)return tl.fromAabb([e.x===0?-a:0,e.y===0?0:-a,-a],[e.x===0?0:a,e.y===0?a:0,a]);{let t=[Fc(0,0,e.x,e.y,e.z),Fc(N,0,e.x,e.y,e.z),Fc(N,N,e.x,e.y,e.z),Fc(0,N,e.x,e.y,e.z)],n=[];for(let e of t)n.push(Xt([],e,a));if(a!==i)for(let e of t)n.push(Xt([],e,i));e.y===0&&n.push([0,1,0]),e.y===(1<=(1<{let n=M(e.lat,-u,u),r=M(+t,this.minZoom+Jc(0,n),this.maxZoom);return{center:new V(e.lng,n),zoom:r}},this.applyConstrain=(e,t)=>this._helper.applyConstrain(e,t),this._helper=new rc({calcMatrices:()=>this._calcMatrices(),defaultConstrain:(e,t)=>this.defaultConstrain(e,t)},e),this._coveringTilesDetailsProvider=new il}clone(){let t=new e;return t.apply(this,!1),t}apply(e,t){this._helper.apply(e,t)}get projectionMatrix(){return this._projectionMatrix}get modelViewProjectionMatrix(){return this._globeViewProjMatrixF64}get inverseProjectionMatrix(){return this._globeProjMatrixInverted}get cameraPosition(){let e=L();return e[0]=this._cameraPosition[0],e[1]=this._cameraPosition[1],e[2]=this._cameraPosition[2],e}get cameraToCenterDistance(){return this._helper.cameraToCenterDistance}getProjectionData(e){let{overscaledTileID:t,applyGlobeMatrix:n}=e,r=this._helper.getMercatorTileCoordinates(t);return{mainMatrix:this._globeViewProjMatrix32f,tileMercatorCoords:r,clippingPlane:this._cachedClippingPlane,projectionTransition:+!!n,fallbackMatrix:this._globeViewProjMatrix32f,clipAntimeridian:t?.canonical.z===0}}_computeClippingPlane(e){let t=this.pitchInRadians,n=this.cameraToCenterDistance/e,r=Math.sin(t)*n,i=Math.cos(t)*n+1,a=1/Math.sqrt(r*r+i*i)*1,o=-r,s=i,c=Math.sqrt(o*o+s*s);o/=c,s/=c;let l=[0,o,s];Ht(l,l,[0,0,0],-this.bearingInRadians),Sn(l,l,[0,0,0],-1*this.center.lat*Math.PI/180),ir(l,l,[0,0,0],this.center.lng*Math.PI/180);let u=1/Nn(l);return Xt(l,l,u),[...l,-a*u]}isLocationOccluded(e){return!this.isSurfacePointVisible(Ic(e))}transformLightDirection(e){let t=this._helper._center.lng*Math.PI/180,n=this._helper._center.lat*Math.PI/180,r=Math.cos(n),i=[Math.sin(t)*r,Math.sin(n),Math.cos(t)*r],a=[i[2],0,-i[0]],o=[0,0,0];Gn(o,a,i),Rt(a,a),Rt(o,o);let s=[a[0]*e[0]+o[0]*e[1]+i[0]*e[2],a[1]*e[0]+o[1]*e[1]+i[1]*e[2],a[2]*e[0]+o[2]*e[1]+i[2]*e[2]],c=[0,0,0];return Rt(c,s),c}getPixelScale(){return 1/Math.cos(this._helper._center.lat*Math.PI/180)}getCircleRadiusCorrection(){return Math.cos(this._helper._center.lat*Math.PI/180)}getPitchedTextCorrection(e,t,n){let r=nr(e,t,n.canonical),i=Nc(r.x,r.y);return this.getCircleRadiusCorrection()/Math.cos(i[1])}projectTileCoordinates(e,t,n,r){let i=n.canonical,a=Fc(e,t,i.x,i.y,i.z),o=1+(r??0)/Wt,s=[a[0]*o,a[1]*o,a[2]*o,1];Gt(s,s,this._globeViewProjMatrixF64);let c=this._cachedClippingPlane,u=c[0]*a[0]+c[1]*a[1]+c[2]*a[2]+c[3]<0;return{point:new l(s[0]/s[3],s[1]/s[3]),signedDistanceFromCamera:s[3],isOccluded:u}}_calcMatrices(){if(!this._helper._width||!this._helper._height)return;let e=Lc(this.worldSize,this.center.lat),t=vt();this._helper.autoCalculateNearFarZ&&(this._helper._nearZ=.5,this._helper._farZ=this.cameraToCenterDistance+e*2),vn(t,this.fovInRadians,this.width/this.height,this._helper._nearZ,this._helper._farZ);let n=this.centerOffset;t[8]=-n.x*2/this._helper._width,t[9]=n.y*2/this._helper._height,this._projectionMatrix=Sr(t),this._globeProjMatrixInverted=vt(),w(this._globeProjMatrixInverted,t),Le(t,t,[0,0,-this.cameraToCenterDistance]),we(t,t,this.rollInRadians),a(t,t,-this.pitchInRadians),we(t,t,this.bearingInRadians),Le(t,t,[0,0,-e]);let r=L();r[0]=e,r[1]=e,r[2]=e,a(t,t,this.center.lat*Math.PI/180),pn(t,t,-this.center.lng*Math.PI/180),ke(t,t,r),this._globeViewProjMatrixF64=t,this._globeViewProjMatrix32f=new Float32Array(t),this._globeViewProjMatrixF64Inverted=vt(),w(this._globeViewProjMatrixF64Inverted,t);let i=L();this._cameraPosition=L(),this._cameraPosition[2]=this.cameraToCenterDistance/e,Ht(this._cameraPosition,this._cameraPosition,i,-this.rollInRadians),Sn(this._cameraPosition,this._cameraPosition,i,this.pitchInRadians),Ht(this._cameraPosition,this._cameraPosition,i,-this.bearingInRadians),$n(this._cameraPosition,this._cameraPosition,[0,0,1]),Sn(this._cameraPosition,this._cameraPosition,i,-this.center.lat*Math.PI/180),ir(this._cameraPosition,this._cameraPosition,i,this.center.lng*Math.PI/180),this._cachedClippingPlane=this._computeClippingPlane(e);let o=Sr(this._globeViewProjMatrixF64Inverted);ke(o,o,[1,1,-1]),this._cachedFrustum=oc.fromInvProjectionMatrix(o,1,0,this._cachedClippingPlane,!0)}calculateFogMatrix(e){I(`calculateFogMatrix is not supported on globe projection.`);let t=vt();return $e(t),t}getVisibleUnwrappedCoordinates(e){return[new Tt(0,e)]}getCameraFrustum(){return this._cachedFrustum}getClippingPlane(){return this._cachedClippingPlane}getCoveringTilesDetailsProvider(){return this._coveringTilesDetailsProvider}recalculateZoomAndCenter(e){if(e){I(`terrain is not fully supported on vertical perspective projection.`);return}this._helper.recalculateZoomAndCenter(0)}maxPitchScaleFactor(){return 1}getCameraPoint(){return this._helper.getCameraPoint()}getCameraAltitude(){return this._helper.getCameraAltitude()}getCameraLngLat(){return this._helper.getCameraLngLat()}lngLatToCameraDepth(e,t){if(!this._globeViewProjMatrixF64)return 1;let n=Ic(e);Xt(n,n,1+t/Wt);let r=St();return Gt(r,[n[0],n[1],n[2],1],this._globeViewProjMatrixF64),r[2]/r[3]}populateCache(e){}getBounds(){let e=this.width*.5,t=this.height*.5,n=[new l(0,0),new l(e,0),new l(this.width,0),new l(this.width,t),new l(this.width,this.height),new l(e,this.height),new l(0,this.height),new l(0,t)],r=[];for(let e of n)r.push(this.unprojectScreenPoint(e));let i=0,a=0,o=0,s=0,c=this.center;for(let e of r){let t=tr(c.lng,e.lng),n=tr(c.lat,e.lat);ti&&(i=t),no&&(o=n)}let u=[c.lng+a,c.lat+s,c.lng+i,c.lat+o];return this.isSurfacePointOnScreen([0,1,0])&&(u[3]=90,u[0]=-180,u[2]=180),this.isSurfacePointOnScreen([0,-1,0])&&(u[1]=-90,u[0]=-180,u[2]=180),new _a(u)}calculateCenterFromCameraLngLatAlt(e,t,n,r){return this._helper.calculateCenterFromCameraLngLatAlt(e,t,n,r)}setLocationAtPoint(e,t,n){let r=Ic(this.unprojectScreenPoint(t)),i=Ic(e),a=L();cr(a);let o=L();ir(o,r,a,-this.center.lng*Math.PI/180),Sn(o,o,a,this.center.lat*Math.PI/180);let s=i[0]*i[0]+i[2]*i[2],c=o[0]*o[0];if(s=-_&&m<=_,y=g>=-_&&g<=_,b,x;if(v&&y){let e=this.center.lng*Math.PI/180,t=this.center.lat*Math.PI/180,n=jn(d,e),r=jn(m,t),i=jn(f,e),a=jn(g,t);n+r=0}isSurfacePointOnScreen(e){if(!this.isSurfacePointVisible(e))return!1;let t=St();return Gt(t,[...e,1],this._globeViewProjMatrixF64),t[0]/=t[3],t[1]/=t[3],t[2]/=t[3],t[0]>-1&&t[0]<1&&t[1]>-1&&t[1]<1&&t[2]>-1&&t[2]<1}unprojectScreenPoint(e){let t=this._cameraPosition,n=this.getRayDirectionFromPixel(e),r=$c(t,n);if(r){let e=L();$n(e,t,[n[0]*r.tMin,n[1]*r.tMin,n[2]*r.tMin]);let i=L();return Rt(i,e),Rc(i)}let i=this._cachedClippingPlane,a=i[0]*n[0]+i[1]*n[1]+i[2]*n[2],o=-tt(i,t)/a,s=L();if(o>0)$n(s,t,[n[0]*o,n[1]*o,n[2]*o]);else{let e=L();$n(e,t,[n[0]*2,n[1]*2,n[2]*2]);let r=tt(this._cachedClippingPlane,e);En(s,e,[this._cachedClippingPlane[0]*r,this._cachedClippingPlane[1]*r,this._cachedClippingPlane[2]*r])}let c=Gc(i);return Rc(Kc(c.center,c.radius,s))}getProjectionDataForCustomLayer(e=!0){let t=this.getProjectionData({overscaledTileID:new $t(0,0,0,0,0),applyGlobeMatrix:e});return t.tileMercatorCoords=[0,0,1,1],t}getFastPathSimpleProjectionMatrix(e){}};function sl(e,t){let n=L();Ln(n,e.origin,e.direction,t);let r=Nn(n),i=L();Xt(i,n,1/r);let a=Rc(i),o=B.fromLngLat(a),s=new B(o.x,M(o.y,0,.999999999)),c=gc(e.index,e.exaggeration,s.x,s.y),l=Math.abs(a.lat)>85.051129?0:c.elevation;return{sample:{...c,elevation:l},radius:r,mercator:s}}function cl(e,t){let{sample:n,radius:r}=sl(e,t);return _c(n,(r-1)*Wt)}var ll=class e{get pixelsToClipSpaceMatrix(){return this._helper.pixelsToClipSpaceMatrix}get clipSpaceToPixelsMatrix(){return this._helper.clipSpaceToPixelsMatrix}get pixelsToGLUnits(){return this._helper.pixelsToGLUnits}get centerOffset(){return this._helper.centerOffset}get size(){return this._helper.size}get rotationMatrix(){return this._helper.rotationMatrix}get centerPoint(){return this._helper.centerPoint}get pixelsPerMeter(){return this._helper.pixelsPerMeter}setMinZoom(e){this._helper.setMinZoom(e)}setMaxZoom(e){this._helper.setMaxZoom(e)}setMinPitch(e){this._helper.setMinPitch(e)}setMaxPitch(e){this._helper.setMaxPitch(e)}setRenderWorldCopies(e){this._helper.setRenderWorldCopies(e)}setBearing(e){this._helper.setBearing(e)}setPitch(e){this._helper.setPitch(e)}setRoll(e){this._helper.setRoll(e)}setFov(e){this._helper.setFov(e)}setZoom(e){this._helper.setZoom(e)}setCenter(e){this._helper.setCenter(e)}setElevation(e){this._helper.setElevation(e)}setMinElevationForCurrentTile(e){this._helper.setMinElevationForCurrentTile(e)}setPadding(e){this._helper.setPadding(e)}interpolatePadding(e,t,n){this._helper.interpolatePadding(e,t,n)}isPaddingEqual(e){return this._helper.isPaddingEqual(e)}resize(e,t,n=!0){this._helper.resize(e,t,n)}getMaxBounds(){return this._helper.getMaxBounds()}setMaxBounds(e){this._helper.setMaxBounds(e)}setConstrainOverride(e){this._helper.setConstrainOverride(e)}overrideNearFarZ(e,t){this._helper.overrideNearFarZ(e,t)}clearNearFarZOverride(){this._helper.clearNearFarZOverride()}getCameraQueryGeometry(e){return this._helper.getCameraQueryGeometry(this.getCameraPoint(),e)}get tileSize(){return this._helper.tileSize}get tileZoom(){return this._helper.tileZoom}get scale(){return this._helper.scale}get worldSize(){return this._helper.worldSize}get width(){return this._helper.width}get height(){return this._helper.height}get lngRange(){return this._helper.lngRange}get latRange(){return this._helper.latRange}get minZoom(){return this._helper.minZoom}get maxZoom(){return this._helper.maxZoom}get zoom(){return this._helper.zoom}get center(){return this._helper.center}get minPitch(){return this._helper.minPitch}get maxPitch(){return this._helper.maxPitch}get pitch(){return this._helper.pitch}get pitchInRadians(){return this._helper.pitchInRadians}get roll(){return this._helper.roll}get rollInRadians(){return this._helper.rollInRadians}get bearing(){return this._helper.bearing}get bearingInRadians(){return this._helper.bearingInRadians}get fov(){return this._helper.fov}get fovInRadians(){return this._helper.fovInRadians}get elevation(){return this._helper.elevation}get minElevationForCurrentTile(){return this._helper.minElevationForCurrentTile}get padding(){return this._helper.padding}get unmodified(){return this._helper.unmodified}get renderWorldCopies(){return this._helper.renderWorldCopies}get cameraToCenterDistance(){return this._helper.cameraToCenterDistance}get constrainOverride(){return this._helper.constrainOverride}get nearZ(){return this._helper.nearZ}get farZ(){return this._helper.farZ}get autoCalculateNearFarZ(){return this._helper.autoCalculateNearFarZ}get isGlobeRendering(){return this._globeness>0}setTransitionState(e){this._globeness=e,this._calcMatrices(),this._verticalPerspectiveTransform.getCoveringTilesDetailsProvider().prepareNextFrame(),this._mercatorTransform.getCoveringTilesDetailsProvider().prepareNextFrame()}get currentTransform(){return this.isGlobeRendering?this._verticalPerspectiveTransform:this._mercatorTransform}constructor(e){this._globeness=1,this.defaultConstrain=(e,t)=>this.currentTransform.defaultConstrain(e,t),this.applyConstrain=(e,t)=>this._helper.applyConstrain(e,t),this._helper=new rc({calcMatrices:()=>this._calcMatrices(),defaultConstrain:(e,t)=>this.defaultConstrain(e,t)},e),this._globeness=1,this._mercatorTransform=new yc,this._verticalPerspectiveTransform=new ol}clone(){let t=new e;return t._globeness=this._globeness,t.apply(this,!1),t}apply(e,t){this._helper.apply(e,t),this._mercatorTransform.apply(this,!1),this._verticalPerspectiveTransform.apply(this,!1)}get projectionMatrix(){return this.currentTransform.projectionMatrix}get modelViewProjectionMatrix(){return this.currentTransform.modelViewProjectionMatrix}get inverseProjectionMatrix(){return this.currentTransform.inverseProjectionMatrix}get cameraPosition(){return this.currentTransform.cameraPosition}getProjectionData(e){let t=this._mercatorTransform.getProjectionData(e),n=this._verticalPerspectiveTransform.getProjectionData(e);return{mainMatrix:this.isGlobeRendering?n.mainMatrix:t.mainMatrix,clippingPlane:n.clippingPlane,tileMercatorCoords:n.tileMercatorCoords,projectionTransition:e.applyGlobeMatrix?this._globeness:0,fallbackMatrix:t.fallbackMatrix,clipAntimeridian:n.clipAntimeridian}}isLocationOccluded(e){return this.currentTransform.isLocationOccluded(e)}transformLightDirection(e){return this.currentTransform.transformLightDirection(e)}getPixelScale(){return wr(this._mercatorTransform.getPixelScale(),this._verticalPerspectiveTransform.getPixelScale(),this._globeness)}getCircleRadiusCorrection(){return wr(this._mercatorTransform.getCircleRadiusCorrection(),this._verticalPerspectiveTransform.getCircleRadiusCorrection(),this._globeness)}getPitchedTextCorrection(e,t,n){let r=this._mercatorTransform.getPitchedTextCorrection(e,t,n),i=this._verticalPerspectiveTransform.getPitchedTextCorrection(e,t,n);return wr(r,i,this._globeness)}projectTileCoordinates(e,t,n,r){return this.currentTransform.projectTileCoordinates(e,t,n,r)}_calcMatrices(){!this._helper._width||!this._helper._height||(this._verticalPerspectiveTransform.apply(this,!1),this._helper._nearZ=this._verticalPerspectiveTransform.nearZ,this._helper._farZ=this._verticalPerspectiveTransform.farZ,this._mercatorTransform.apply(this,!0,this.isGlobeRendering),this._helper._nearZ=this._mercatorTransform.nearZ,this._helper._farZ=this._mercatorTransform.farZ)}calculateFogMatrix(e){return this.currentTransform.calculateFogMatrix(e)}getVisibleUnwrappedCoordinates(e){return this.currentTransform.getVisibleUnwrappedCoordinates(e)}getCameraFrustum(){return this.currentTransform.getCameraFrustum()}getClippingPlane(){return this.currentTransform.getClippingPlane()}getCoveringTilesDetailsProvider(){return this.currentTransform.getCoveringTilesDetailsProvider()}recalculateZoomAndCenter(e){this.currentTransform.recalculateZoomAndCenter(e)}maxPitchScaleFactor(){return this._mercatorTransform.maxPitchScaleFactor()}getCameraPoint(){return this._helper.getCameraPoint()}getCameraAltitude(){return this._helper.getCameraAltitude()}getCameraLngLat(){return this._helper.getCameraLngLat()}lngLatToCameraDepth(e,t){return this.currentTransform.lngLatToCameraDepth(e,t)}populateCache(e){this._mercatorTransform.populateCache(e),this._verticalPerspectiveTransform.populateCache(e)}getBounds(){return this.currentTransform.getBounds()}calculateCenterFromCameraLngLatAlt(e,t,n,r){return this._helper.calculateCenterFromCameraLngLatAlt(e,t,n,r)}setLocationAtPoint(e,t,n){if(!this.isGlobeRendering){this._mercatorTransform.setLocationAtPoint(e,t,n),this.apply(this._mercatorTransform,!1);return}this._verticalPerspectiveTransform.setLocationAtPoint(e,t,n),this.apply(this._verticalPerspectiveTransform,!1)}locationToScreenPoint(e,t){return this.currentTransform.locationToScreenPoint(e,t)}screenPointToMercatorCoordinate(e,t){return this.currentTransform.screenPointToMercatorCoordinate(e,t)}screenTerrainPointToMercatorCoordinate(e,t){return this.currentTransform.screenTerrainPointToMercatorCoordinate(e,t)}screenPointToLocation(e,t){return this.currentTransform.screenPointToLocation(e,t)}screenPointToLocationAtElevation(e,t){return this.currentTransform.screenPointToLocationAtElevation(e,t)}isPointOnMapSurface(e,t){return this.currentTransform.isPointOnMapSurface(e,t)}getRayDirectionFromPixel(e){return this._verticalPerspectiveTransform.getRayDirectionFromPixel(e)}getProjectionDataForCustomLayer(e=!0){let t=this._mercatorTransform.getProjectionDataForCustomLayer(e);if(!this.isGlobeRendering)return t;let n=this._verticalPerspectiveTransform.getProjectionDataForCustomLayer(e);return n.fallbackMatrix=t.mainMatrix,n.projectionTransition=this._globeness,n}getFastPathSimpleProjectionMatrix(e){return this.currentTransform.getFastPathSimpleProjectionMatrix(e)}},ul=class e{get useGlobeControls(){return!0}handlePanInertia(e,t){let n=Xc(e,t);return Math.abs(n.lng-t.center.lng)>180&&(n.lng=t.center.lng+179.5*Math.sign(n.lng-t.center.lng)),{easingCenter:n,easingOffset:new l(0,0)}}handleMapControlsRollPitchBearingZoom(e,t){let n=e.around,r=t.screenPointToLocation(n);e.bearingDelta&&t.setBearing(t.bearing+e.bearingDelta),e.pitchDelta&&t.setPitch(t.pitch+e.pitchDelta),e.rollDelta&&t.setRoll(t.roll+e.rollDelta);let i=t.zoom;e.zoomDelta&&t.setZoom(t.zoom+e.zoomDelta);let a=t.zoom-i;if(a===0)return;let o=tr(t.center.lng,r.lng),s=o/(Math.abs(o/180)+1),c=tr(t.center.lat,r.lat),l=t.getRayDirectionFromPixel(n),f=t.cameraPosition,p=ln(f,l)*-1,m=L();$n(m,f,[l[0]*p,l[1]*p,l[2]*p]);let h=Nn(m),g=h-1,_=Math.exp(-Math.max(g-.3,0)*.5),v=bn(h,.95,.999,0,1),y=Lc(t.worldSize,t.center.lat)/Math.min(t.width,t.height),b=bn(y,.9,.5,1,.25),x=Math.min(_,wr(1,b,v)),S=(1-d(-a))*x,C=t.center.lat,w=t.zoom,T=new V(t.center.lng+s*S,M(t.center.lat+c*S,-u,u));t.setLocationAtPoint(r,n);let E=t.center,D=bn(Math.abs(o),45,85,0,1),ee=Math.max(D,v)**.25,O=tr(E.lng,T.lng),k=tr(E.lat,T.lat);t.setCenter(new V(E.lng+O*ee,E.lat+k*ee).wrap()),t.setZoom(w+Jc(C,t.center.lat))}handleMapControlsPan(e,t,n){e.panDelta&&Uc(t,n,t.isPointOnMapSurface(e.around)?e.around:t.centerPoint,e.panDelta)}cameraForBoxAndBearing(t,n,r,i,a){let o=wc(t,n,r,i,a),s=n.left/a.width*2-1,c=(a.width-n.right)/a.width*2-1,l=n.top/a.height*-2+1,u=(a.height-n.bottom)/a.height*-2+1,d=tr(r.getWest(),r.getEast())<0,f=d?r.getEast():r.getWest(),p=d?r.getWest():r.getEast(),m=Math.max(r.getNorth(),r.getSouth()),h=Math.min(r.getNorth(),r.getSouth()),g=f+tr(f,p)*.5,_=m+tr(m,h)*.5,v=a.clone();v.setCenter(o.center),v.setBearing(o.bearing),v.setPitch(0),v.setRoll(0),v.setZoom(o.zoom);let y=v.modelViewProjectionMatrix,b=[Ic(r.getNorthWest()),Ic(r.getNorthEast()),Ic(r.getSouthWest()),Ic(r.getSouthEast()),Ic(new V(p,_)),Ic(new V(f,_)),Ic(new V(g,m)),Ic(new V(g,h))],x=Ic(o.center),S=1/0;for(let t of b)s<0&&(S=e.getLesserNonNegativeNonNull(S,e.solveVectorScale(t,x,y,`x`,s))),c>0&&(S=e.getLesserNonNegativeNonNull(S,e.solveVectorScale(t,x,y,`x`,c))),l>0&&(S=e.getLesserNonNegativeNonNull(S,e.solveVectorScale(t,x,y,`y`,l))),u<0&&(S=e.getLesserNonNegativeNonNull(S,e.solveVectorScale(t,x,y,`y`,u)));if(!Number.isFinite(S)||S===0){Sc();return}return o.zoom=Math.min(v.zoom+Ee(S),t.maxZoom),o}handleJumpToCenterZoom(e,t){let n=e.center.lat,r=e.applyConstrain(t.center?V.convert(t.center):e.center,e.zoom).center;e.setCenter(r.wrap());let i=t.zoom===void 0?e.zoom+Jc(n,r.lat):+t.zoom;e.zoom!==i&&e.setZoom(i)}handleEaseTo(e,t){let n=e.zoom,r=e.center,i=e.padding,a={roll:e.roll,pitch:e.pitch,bearing:e.bearing},o={roll:t.roll===void 0?e.roll:t.roll,pitch:t.pitch===void 0?e.pitch:t.pitch,bearing:t.bearing===void 0?e.bearing:t.bearing},c=t.zoom!==void 0,u=!e.isPaddingEqual(t.padding),f=!1,p=t.center?V.convert(t.center):r,m=e.applyConstrain(p,n).center;tc(e,m);let h=e.clone();h.setCenter(m),h.setZoom(c?+t.zoom:n+Jc(r.lat,p.lat)),h.setBearing(t.bearing);let g=new l(M(e.centerPoint.x+t.offsetAsPoint.x,0,e.width),M(e.centerPoint.y+t.offsetAsPoint.y,0,e.height));h.setLocationAtPoint(m,g);let _=(t.offset&&t.offsetAsPoint.mag())>0?h.center:m,v=c?+t.zoom:n+Jc(r.lat,_.lat),y=n+Jc(r.lat,0),b=v+Jc(_.lat,0),x=tr(r.lng,_.lng),S=tr(r.lat,_.lat),C=d(b-y);return f=v!==n,{easeFunc:n=>{if(s(a,o)||Cc({startEulerAngles:a,endEulerAngles:o,tr:e,k:n,useSlerp:a.roll!=o.roll}),u&&e.interpolatePadding(i,t.padding,n),t.around)I(`Easing around a point is not supported under globe projection.`),e.setLocationAtPoint(t.around,t.aroundPoint);else{let t=n*(b>y?Math.min(2,C):Math.max(.5,C))**(1-n),i=Qc(r,x,S,t);e.setCenter(i.wrap())}if(f){let t=on.number(y,b,n)+Jc(0,e.center.lat);e.setZoom(t)}},isZooming:f,elevationCenter:_}}handleFlyTo(e,t){let n=t.zoom!==void 0,r=e.center,i=e.zoom,a=e.padding,o=!e.isPaddingEqual(t.padding),s=e.applyConstrain(V.convert(t.center||t.locationAtOffset),i).center,c=n?+t.zoom:e.zoom+Jc(e.center.lat,s.lat),u=e.clone();u.setCenter(s),u.setZoom(c),u.setBearing(t.bearing);let f=new l(M(e.centerPoint.x+t.offsetAsPoint.x,0,e.width),M(e.centerPoint.y+t.offsetAsPoint.y,0,e.height));u.setLocationAtPoint(s,f);let p=u.center;tc(e,p);let m=Mc(e,r,p),h=i+Jc(r.lat,0),g=c+Jc(p.lat,0),_=d(g-h),v=typeof t.minZoom==`number`?+t.minZoom:e.minZoom,y=Math.max(v,e.minZoom)+Jc(p.lat,0),b=Math.min(y,h,g)+Jc(0,p.lat),x=e.applyConstrain(p,b).zoom+Jc(p.lat,0),S=d(x-h),C=tr(r.lng,p.lng),w=tr(r.lat,p.lat);return{easeFunc:(n,i,s,l)=>{let u=Qc(r,C,w,s);o&&e.interpolatePadding(a,t.padding,n);let d=n===1?p:u;e.setCenter(d.wrap());let f=h+Ee(i);e.setZoom(n===1?c:f+Jc(0,d.lat))},scaleOfZoom:_,targetCenter:p,scaleOfMinZoom:S,pixelPathLength:m}}static solveVectorScale(e,t,n,r,i){let a=i,o=r===`x`?[n[0],n[4],n[8],n[12]]:[n[1],n[5],n[9],n[13]],s=[n[3],n[7],n[11],n[15]],c=e[0]*o[0]+e[1]*o[1]+e[2]*o[2],l=e[0]*s[0]+e[1]*s[1]+e[2]*s[2],u=t[0]*o[0]+t[1]*o[1]+t[2]*o[2],d=t[0]*s[0]+t[1]*s[1]+t[2]*s[2],f=(u+o[3]-a*d-a*s[3])/(u-c-a*d+a*l);return u+a*l===c+a*d||s[3]*(c-u)+o[3]*(d-l)+c*d===u*l?null:f}static getLesserNonNegativeNonNull(e,t){return t!==null&&t>=0&&t{for(let e in this.tileManagers){let t=this.tileManagers[e].getSource().type;(t===`vector`||t===`geojson`)&&this.tileManagers[e].reload()}},this.map=e,this.dispatcher=new ra(ea(),e._getMapId()),this.dispatcher.registerMessageHandler(`GG`,(e,t)=>this.getGlyphs(e,t)),this.dispatcher.registerMessageHandler(`GI`,(e,t)=>this.getImages(e,t)),this.dispatcher.registerMessageHandler(`GDA`,(e,t)=>this.getDashes(e,t)),this.imageManager=new bi,this.imageManager.setEventedParent(this),this.imageManager.setMissingImageResolver(e._missingStyleImageResolver),this.patternAtlas=new xi(this.imageManager);let n=e._container?.lang||typeof document<`u`&&document.documentElement?.lang||void 0;this.glyphManager=new Ii(e._requestManager,t.localIdeographFontFamily,n),this.lineAtlas=new Ui(256,512),this.crossTileSymbolIndex=new Bs,this._setInitialValues(),this._resetUpdates(),this.dispatcher.broadcast(`SR`,Pe()),fo().on(co,this._rtlPluginLoaded),this.on(`data`,e=>{if(e.dataType!==`source`||e.sourceDataType!==`metadata`)return;let t=this.tileManagers[e.sourceId];if(!t)return;let n=t.getSource();if(n?.vectorLayerIds)for(let e in this._layers){let t=this._layers[e];t.source===n.id&&this._validateLayer(t)}})}_setInitialValues(){this._layers={},this._order=[],this.tileManagers={},this.zoomHistory=new Pn,this._imagesListDirty=!1,this._globalState={},this._serializedLayers={},this.stylesheet=null,this.light=null,this.sky=null,this.projection&&(this.projection.destroy(),delete this.projection),this._loaded=!1,this._changed=!1,this._updatedLayers={},this._updatedSources={},this._changedImages={},this._glyphsDidChange=!1,this._updatedPaintProps={},this._layerOrderChanged=!1,this._symbolPlacementTriggered=!1,this._placedProjectionTransition=void 0,this.crossTileSymbolIndex=new((this.crossTileSymbolIndex?.constructor)||Object),this.pauseablePlacement=void 0,this.placement=void 0,this.z=0}setGlobalStateProperty(e,t){this._checkLoaded();let n=t===null?this.stylesheet.state?.[e]?.default??null:t;if(Ve(n,this._globalState[e]))return this;this._globalState[e]=n,this._applyGlobalStateChanges([e])}getGlobalState(){return this._globalState}setGlobalState(e){this._checkLoaded();let t=[];for(let n in e)Ve(this._globalState[n],e[n].default)||(t.push(n),this._globalState[n]=e[n].default);this._applyGlobalStateChanges(t)}_applyGlobalStateChanges(e){if(e.length===0)return;let t=new Set,n={};for(let r of e){n[r]=this._globalState[r];for(let e in this._layers){let n=this._layers[e],i=n.getLayoutAffectingGlobalStateRefs(),a=n.getPaintAffectingGlobalStateRefs(),o=n.getVisibilityAffectingGlobalStateRefs();if(i.has(r)&&t.add(n.source),a.has(r))for(let{name:e,value:t}of a.get(r))this._updatePaintProperty(n,e,t);o?.has(r)&&(n.recalculateVisibility(),this._updateLayer(n))}}this.dispatcher.broadcast(`UGS`,n);for(let e in this.tileManagers)t.has(e)&&(this._reloadSource(e),this._changed=!0)}async loadURL(e,t={},n){this.fire(new qr(`dataloading`)),t.validate=typeof t.validate!=`boolean`||t.validate,this._loadStyleRequest=new AbortController;let r=this._loadStyleRequest;try{let i=await this.map._requestManager.transformRequest(e,`Style`);Ke(r.signal);let a=await b(i,r);this._loadStyleRequest===r&&(this._loadStyleRequest=null),this._load(a.data,t,n)}catch(e){this._loadStyleRequest===r&&(this._loadStyleRequest=null),e&&!r.signal.aborted&&this.fire(new H(qn(e)))}}loadJSON(e,t={},n){this.fire(new qr(`dataloading`)),this._frameRequest=new AbortController,Rr.frameAsync(this._frameRequest,this.map._ownerWindow).then(()=>{this._frameRequest=null,t.validate=t.validate!==!1,this._load(e,t,n)}).catch(()=>{})}loadEmpty(){this.fire(new qr(`dataloading`)),this._load(pl,{validate:!1})}_load(e,t,n){let r=t.transformStyle?t.transformStyle(n,e):e;if(!(t.validate&&Zt(this,r))){r={...r},this._loaded=!0,this.stylesheet=r;for(let e in r.sources)this.addSource(e,r.sources[e],{validate:!1});r.sprite?this._loadSprite(r.sprite):this.imageManager.setLoaded(!0),this.glyphManager.setURL(r.glyphs),this.glyphManager.setFontFaces(r[`font-faces`]),this._createLayers(),this.light=new zi(this.stylesheet.light??{},this._globalState),this._setProjectionInternal(this.stylesheet.projection?.type||`mercator`),this.sky=new Hi(this.stylesheet.sky,this._globalState),this.map.setTerrain(this.stylesheet.terrain??null,{validate:!1}),this.fire(new qr(`data`)),this.fire(new Kr)}}_createLayers(){let e=ri(this.stylesheet.layers);this.setGlobalState(this.stylesheet.state??null),this.dispatcher.broadcast(`SL`,e),this._order=e.map(e=>e.id),this._layers={},this._serializedLayers=null;for(let t of e){let e=Je(t,this._globalState);if(e.setEventedParent(this,{layer:{id:t.id}}),this._layers[t.id]=e,Ue(e)&&this.tileManagers[e.source]){let n=t.paint?.[`raster-fade-duration`]??e.paint.get(`raster-fade-duration`);this.tileManagers[e.source].setRasterFadeDuration(n)}}}async _loadSprite(e,t=!1,n=void 0){this.imageManager.setLoaded(!1);let r=new AbortController;this._spriteRequest=r;let i;try{let n=await vi(e,this.map._requestManager,this.map.getPixelRatio(),r);if(!n)return;for(let e in n){let{loaded:r,removed:i}=this.imageManager.setSpriteImages(e,n[e]);this._markImagesChanged(i),t&&this._markImagesChanged(r)}}catch(e){i=e,r.signal.aborted||this.fire(new H(i))}finally{this._spriteRequest=null,this.imageManager.setLoaded(!0),t&&(this._changed=!0),this.dispatcher.broadcast(`SI`,this.imageManager.listImages()),this.fire(new qr(`data`)),n?.(i)}}_unloadSprite(){this._markImagesChanged(this.imageManager.removeAllSpriteImages()),this._imagesListDirty=!0,this._changed=!0,this.fire(new qr(`data`))}_validateLayer(e){let t=this.tileManagers[e.source];if(!t)return;let n=e.sourceLayer;if(!n)return;let r=t.getSource();(r.type===`geojson`||r.vectorLayerIds&&!r.vectorLayerIds.includes(n))&&this.fire(new H(Error(`Source layer "${n}" does not exist on source "${r.id}" as specified by style layer "${e.id}".`)))}loaded(){if(!this._loaded||Object.keys(this._updatedSources).length)return!1;for(let e in this.tileManagers)if(!this.tileManagers[e].loaded())return!1;return this.imageManager.isLoaded()}_serializeByIds(e,t=!1){let n=this._serializedAllLayers();if(!e||e.length===0)return Object.values(t?ge(n):n);let r=[];for(let i of e)if(n[i]){let e=t?ge(n[i]):n[i];r.push(e)}return r}_serializedAllLayers(){let e=this._serializedLayers;if(e)return e;e=this._serializedLayers={};let t=Object.keys(this._layers);for(let n of t){let t=this._layers[n];t.type!==`custom`&&(e[n]=t.serialize())}return e}hasTransitions(){if(this.light?.hasTransition()||this.sky?.hasTransition()||this.projection?.hasTransition())return!0;for(let e in this.tileManagers)if(this.tileManagers[e].hasTransition())return!0;for(let e in this._layers)if(this._layers[e].hasTransition())return!0;return!1}_checkLoaded(){if(!this._loaded)throw Error(`Style is not done loading.`)}update(e){if(!this._loaded)return;let t=this._changed;if(t){this._imagesListDirty&&=(this.dispatcher.broadcast(`SI`,this.imageManager.listImages()),!1);let t=Object.keys(this._updatedLayers),n=Object.keys(this._removedLayers);(t.length||n.length)&&this._updateWorkerLayers(t,n);for(let e in this._updatedSources){let t=this._updatedSources[e];if(t===`reload`)this._reloadSource(e);else if(t===`clear`)this._clearSource(e);else throw Error(`Invalid action ${t}`)}this._updateTilesForChangedImages(),this._updateTilesForChangedGlyphs();for(let t in this._updatedPaintProps)this._layers[t].updateTransitions(e);this.light.updateTransitions(e),this.sky.updateTransitions(e),this._resetUpdates()}let n={};for(let e in this.tileManagers){let t=this.tileManagers[e];n[e]=t.used,t.used=!1}let r=this.imageManager.listImages();for(let t of this._order){let n=this._layers[t];n.recalculate(e,r),!n.isHidden(e.zoom)&&n.source&&(this.tileManagers[n.source].used=!0)}for(let e in n){let t=this.tileManagers[e];!!n[e]!=!!t.used&&t.fire(new K(`data`,{sourceDataType:`visibility`,sourceId:e}))}this.light.recalculate(e),this.sky.recalculate(e),this.projection.recalculate(e),this.z=e.zoom,t&&this.fire(new qr(`data`))}_updateTilesForChangedImages(){let e=Object.keys(this._changedImages);if(e.length){for(let t in this.tileManagers)this.tileManagers[t].reloadTilesForDependencies([`icons`,`patterns`],e);this._changedImages={}}}_updateTilesForChangedGlyphs(){if(this._glyphsDidChange){for(let e in this.tileManagers)this.tileManagers[e].reloadTilesForDependencies([`glyphs`],[``]);this._glyphsDidChange=!1}}_updateWorkerLayers(e,t){this.dispatcher.broadcast(`UL`,{layers:this._serializeByIds(e,!1),removedIds:t})}_resetUpdates(){this._changed=!1,this._updatedLayers={},this._removedLayers={},this._updatedSources={},this._updatedPaintProps={},this._changedImages={},this._glyphsDidChange=!1}setState(e,t={}){this._checkLoaded();let n=this.serialize();if(e=t.transformStyle?t.transformStyle(n,e):e,(t.validate??!0)&&Zt(this,e))return!1;e=ge(e),e.layers=ri(e.layers);let r=mi(n,e),i=this._getOperationsToPerform(r);if(i.unimplemented.length>0)throw Error(`Unimplemented: ${i.unimplemented.join(`, `)}.`);if(i.operations.length===0)return!1;for(let e of i.operations)e();return this.stylesheet=e,this._serializedLayers=null,this.fire(new Kr({style:this})),!0}_getOperationsToPerform(e){let t=[],n=[];for(let r of e)switch(r.command){case`setCenter`:case`setZoom`:case`setBearing`:case`setPitch`:case`setRoll`:continue;case`addLayer`:t.push(()=>this.addLayer.apply(this,r.args));break;case`removeLayer`:t.push(()=>this.removeLayer.apply(this,r.args));break;case`setPaintProperty`:t.push(()=>this.setPaintProperty.apply(this,r.args));break;case`setLayoutProperty`:t.push(()=>this.setLayoutProperty.apply(this,r.args));break;case`setFilter`:t.push(()=>this.setFilter.apply(this,r.args));break;case`addSource`:t.push(()=>this.addSource.apply(this,r.args));break;case`removeSource`:t.push(()=>this.removeSource.apply(this,r.args));break;case`setLayerZoomRange`:t.push(()=>this.setLayerZoomRange.apply(this,r.args));break;case`setLight`:t.push(()=>this.setLight.apply(this,r.args));break;case`setGeoJSONSourceData`:t.push(()=>this.setGeoJSONSourceData.apply(this,r.args));break;case`setGlyphs`:t.push(()=>this.setGlyphs.apply(this,r.args));break;case`setFontFaces`:t.push(()=>this.setFontFaces.apply(this,r.args));break;case`setSprite`:t.push(()=>this.setSprite.apply(this,r.args));break;case`setTerrain`:t.push(()=>this.map.setTerrain.apply(this,r.args));break;case`setSky`:t.push(()=>this.setSky.apply(this,r.args));break;case`setProjection`:this.setProjection.apply(this,r.args);break;case`setGlobalState`:t.push(()=>this.setGlobalState.apply(this,r.args));break;case`setTransition`:t.push(()=>{});break;default:n.push(r.command)}return{operations:t,unimplemented:n}}addImage(e,t){if(this.getImage(e)){this.fire(new H(Error(`An image named "${e}" already exists.`)));return}this.imageManager.addImage(e,t),this._afterImageUpdated(e)}updateImage(e,t){this.imageManager.updateImage(e,t)}getImage(e){return this.imageManager.getImage(e)}setMissingImageResolver(e){this.imageManager.setMissingImageResolver(e)}removeImage(e){if(!this.getImage(e)){this.fire(new H(Error(`An image named "${e}" does not exist.`)));return}this.imageManager.removeImage(e),this._afterImageUpdated(e)}_markImagesChanged(e){for(let t of e)this._changedImages[t]=!0}_afterImageUpdated(e){this._changedImages[e]=!0,this._imagesListDirty=!0,this._changed=!0,this.fire(new qr(`data`))}listImages(){return this._checkLoaded(),this.imageManager.listImages()}addSource(e,t,n={}){if(this._checkLoaded(),this.tileManagers[e]!==void 0)throw Error(`Source "${e}" already exists.`);if(!t.type)throw Error(`The type property must be defined, but only the following properties were given: ${Object.keys(t).join(`, `)}.`);if(zt.has(t.type)&&this._validate(Ut.source,`sources.${e}`,t,null,n))return;this.map?._collectResourceTiming&&(t.collectResourceTiming=!0);let r=this.tileManagers[e]=new No(e,t,this.dispatcher);r.style=this,r.setEventedParent(this,()=>({isSourceLoaded:r.loaded(),source:r.serialize(),sourceId:e})),r.onAdd(this.map),this._changed=!0}removeSource(e){if(this._checkLoaded(),this.tileManagers[e]===void 0)throw Error(`There is no source with this ID=${e}`);for(let t in this._layers)if(this._layers[t].source===e)return this.fire(new H(Error(`Source "${e}" cannot be removed while layer "${t}" is using it.`)));let t=this.tileManagers[e];delete this.tileManagers[e],delete this._updatedSources[e],t.fire(new K(`data`,{sourceDataType:`metadata`,sourceId:e})),t.setEventedParent(null),t.onRemove(this.map),this._changed=!0}setGeoJSONSourceData(e,t){if(this._checkLoaded(),this.tileManagers[e]===void 0)throw Error(`There is no source with this ID=${e}`);let n=this.tileManagers[e].getSource();if(n.type!==`geojson`)throw Error(`geojsonSource.type is ${n.type}, which is !== 'geojson`);n.setData(t),this._changed=!0}getSource(e){return this.tileManagers[e]?.getSource()}addLayer(e,t,n={}){this._checkLoaded();let r=e.id;if(this.getLayer(r)){this.fire(new H(Error(`Layer "${r}" already exists on this map.`)));return}let i;if(e.type===`custom`){if(Cn(this,fr(e)))return;i=Je(e,this._globalState)}else{if(`source`in e&&typeof e.source==`object`&&(this.addSource(r,e.source),e=ge(e),e=z(e,{source:r})),this._validate(Ut.layer,`layers.${r}`,e,{arrayIndex:-1},n))return;i=Je(e,this._globalState),this._validateLayer(i),i.setEventedParent(this,{layer:{id:r}})}let a=t?this._order.indexOf(t):this._order.length;if(t&&a===-1){this.fire(new H(Error(`Cannot add layer "${r}" before non-existing layer "${t}".`)));return}if(this._order.splice(a,0,r),this._layerOrderChanged=!0,this._layers[r]=i,this._removedLayers[r]&&i.source&&i.type!==`custom`){let e=this._removedLayers[r];delete this._removedLayers[r],e.type===i.type?(this._updatedSources[i.source]=`reload`,this.tileManagers[i.source].pause()):this._updatedSources[i.source]=`clear`}this._updateLayer(i),i.onAdd&&i.onAdd(this.map)}moveLayer(e,t){if(this._checkLoaded(),this._changed=!0,!this._layers[e]){this.fire(new H(Error(`The layer '${e}' does not exist in the map's style and cannot be moved.`)));return}if(e===t)return;let n=this._order.indexOf(e);this._order.splice(n,1);let r=t?this._order.indexOf(t):this._order.length;if(t&&r===-1){this.fire(new H(Error(`Cannot move layer "${e}" before non-existing layer "${t}".`)));return}this._order.splice(r,0,e),this._layerOrderChanged=!0}removeLayer(e){this._checkLoaded();let t=this._layers[e];if(!t){this.fire(new H(Error(`Cannot remove non-existing layer "${e}".`)));return}t.setEventedParent(null);let n=this._order.indexOf(e);this._order.splice(n,1),this._layerOrderChanged=!0,this._changed=!0,this._removedLayers[e]=t,delete this._layers[e],this._serializedLayers&&delete this._serializedLayers[e],delete this._updatedLayers[e],delete this._updatedPaintProps[e],t.onRemove&&t.onRemove(this.map)}getLayer(e){return this._layers[e]}getLayersOrder(){return[...this._order]}hasLayer(e){return e in this._layers}setLayerZoomRange(e,t,n){this._checkLoaded();let r=this.getLayer(e);if(!r){this.fire(new H(Error(`Cannot set the zoom range of non-existing layer "${e}".`)));return}(r.minzoom!==t||r.maxzoom!==n)&&(t!=null&&(r.minzoom=t),n!=null&&(r.maxzoom=n),this._updateLayer(r))}setFilter(e,t,n={}){this._checkLoaded();let r=this.getLayer(e);if(!r){this.fire(new H(Error(`Cannot filter non-existing layer "${e}".`)));return}if(!Ve(r.filter,t)){if(t==null){r.setFilter(void 0),this._updateLayer(r);return}this._validate(Ut.filter,`layers.${r.id}.filter`,t,null,n)||(r.setFilter(ge(t)),this._updateLayer(r))}}getFilter(e){return ge(this.getLayer(e).filter)}setLayoutProperty(e,t,n,r={}){this._checkLoaded();let i=this.getLayer(e);if(!i){this.fire(new H(Error(`Cannot style non-existing layer "${e}".`)));return}Ve(i.getLayoutProperty(t),n)||(i.setLayoutProperty(t,n,r),this._updateLayer(i))}getLayoutProperty(e,t){let n=this.getLayer(e);if(!n){this.fire(new H(Error(`Cannot get style of non-existing layer "${e}".`)));return}return n.getLayoutProperty(t)}setPaintProperty(e,t,n,r={}){this._checkLoaded();let i=this.getLayer(e);if(!i){this.fire(new H(Error(`Cannot style non-existing layer "${e}".`)));return}Ve(i.getPaintProperty(t),n)||this._updatePaintProperty(i,t,n,r)}_updatePaintProperty(e,t,n,r={}){e.setPaintProperty(t,n,r)&&this._updateLayer(e),Ue(e)&&t===`raster-fade-duration`&&this.tileManagers[e.source].setRasterFadeDuration(n),this._changed=!0,this._updatedPaintProps[e.id]=!0,e.type===`symbol`&&this.triggerSymbolPlacement(),this._serializedLayers=null}getPaintProperty(e,t){return this.getLayer(e).getPaintProperty(t)}setFeatureState(e,t){this._checkLoaded();let n=e.source,r=e.sourceLayer,i=this.tileManagers[n];if(i===void 0){this.fire(new H(Error(`The source '${n}' does not exist in the map's style.`)));return}let a=i.getSource().type;if(a===`geojson`&&r){this.fire(new H(Error(`GeoJSON sources cannot have a sourceLayer parameter.`)));return}if(a===`vector`&&!r){this.fire(new H(Error(`The sourceLayer parameter must be provided for vector source types.`)));return}if(e.id===void 0){this.fire(new H(Error(`The feature id parameter must be provided.`)));return}let o=[`__proto__`,`constructor`,`prototype`];if(t&&Object.keys(t).some(e=>o.includes(e))){this.fire(new H(Error(`The feature state should not include one of the following keys: ${o}`)));return}i.setFeatureState(r,e.id,t)}removeFeatureState(e,t){this._checkLoaded();let n=e.source,r=this.tileManagers[n];if(r===void 0){this.fire(new H(Error(`The source '${n}' does not exist in the map's style.`)));return}let i=r.getSource().type,a=i===`vector`?e.sourceLayer:void 0;if(i===`vector`&&!a){this.fire(new H(Error(`The sourceLayer parameter must be provided for vector source types.`)));return}if(t&&typeof e.id!=`string`&&typeof e.id!=`number`){this.fire(new H(Error(`A feature id is required to remove its specific state property.`)));return}r.removeFeatureState(a,e.id,t)}getFeatureState(e){this._checkLoaded();let t=e.source,n=e.sourceLayer,r=this.tileManagers[t];if(r===void 0){this.fire(new H(Error(`The source '${t}' does not exist in the map's style.`)));return}if(r.getSource().type===`vector`&&!n){this.fire(new H(Error(`The sourceLayer parameter must be provided for vector source types.`)));return}return e.id===void 0&&this.fire(new H(Error(`The feature id parameter must be provided.`))),r.getFeatureState(n,e.id)}getTransition(){return z({duration:300,delay:0},this.stylesheet?.transition)}serialize(){if(!this._loaded)return;let e=It(this.tileManagers,e=>e.serialize()),t=this._serializeByIds(this._order,!0),n=this.map.getTerrain()||void 0,r=this.stylesheet;return Fn({version:r.version,name:r.name,metadata:r.metadata,light:r.light,sky:r.sky,center:r.center,zoom:r.zoom,bearing:r.bearing,pitch:r.pitch,sprite:r.sprite,glyphs:r.glyphs,"font-faces":r[`font-faces`],transition:r.transition,projection:r.projection,state:r.state,sources:e,layers:t,terrain:n},e=>e!==void 0)}_updateLayer(e){this._updatedLayers[e.id]=!0,e.source&&!this._updatedSources[e.source]&&this.tileManagers[e.source].getSource().type!==`raster`&&(this._updatedSources[e.source]=`reload`,this.tileManagers[e.source].pause()),this._serializedLayers=null,this._changed=!0}_flattenAndSortRenderedFeatures(e){let t=e=>this._layers[e].type===`fill-extrusion`,n={},r=[];for(let i=this._order.length-1;i>=0;i--){let a=this._order[i];if(t(a)){n[a]=i;for(let t of e){let e=t[a];if(e)for(let t of e)r.push(t)}}}r.sort((e,t)=>t.intersectionZ-e.intersectionZ);let i=[];for(let a=this._order.length-1;a>=0;a--){let o=this._order[a];if(t(o))for(let e=r.length-1;e>=0;e--){let t=r[e].feature;if(n[t.layer.id]this.map.terrain.getElevation(e,t,n):void 0));return this.placement&&i.push(la(this._layers,a,this.tileManagers,e,s,this.placement.collisionIndex,this.placement.retainedQueryData)),this._flattenAndSortRenderedFeatures(i)}querySourceFeatures(e,t){t?.filter&&this._validate(Ut.filter,`querySourceFeatures.filter`,t.filter,null,t);let n=this.tileManagers[e];return n?ua(n,t?{...t,globalState:this._globalState}:{globalState:this._globalState}):[]}getLight(){return this.light.getLight()}setLight(e,t={}){this._checkLoaded();let n=this.light.getLight(),r=!1;for(let t in e)if(!Ve(e[t],n[t])){r=!0;break}if(!r)return;let i={now:U(),transition:z({duration:300,delay:0},this.stylesheet.transition)};this.light.setLight(e,t),this.light.updateTransitions(i)}getProjection(){return this.stylesheet?.projection}setProjection(e){this._checkLoaded();let t=e??{type:`mercator`};if(this.stylesheet.projection=e,this.projection){if(this.projection.name===t.type)return;this.projection.destroy(),delete this.projection}this._setProjectionInternal(t.type)}getSky(){return this.stylesheet?.sky}setSky(e,t={}){this._checkLoaded();let n=this.getSky(),r=!1;if(!e&&!n)return;if(e&&!n)r=!0;else if(!e&&n)r=!0;else for(let t in e)if(!Ve(e[t],n[t])){r=!0;break}if(!r)return;let i={now:U(),transition:z({duration:300,delay:0},this.stylesheet.transition)};this.stylesheet.sky=e,this.sky.setSky(e,t),this.sky.updateTransitions(i)}_setProjectionInternal(e){let t=fl(e,this.map._camera?.transform.constrainOverride,this._globalState);this.projection=t.projection,this.map.migrateProjection(t.transform,t.cameraHelper);for(let e in this.tileManagers)this.tileManagers[e].reload()}_validate(e,t,n,r,i={}){return i.validate!==!1&&ar(this,e,{key:t,style:this.serialize(),value:n,...r},i)}_remove(e=!0){this._frameRequest&&=(this._frameRequest.abort(),null),this._loadStyleRequest&&=(this._loadStyleRequest.abort(),null),this._spriteRequest&&=(this._spriteRequest.abort(),null),fo().off(co,this._rtlPluginLoaded);for(let e in this._layers)this._layers[e].setEventedParent(null);for(let e in this.tileManagers){let t=this.tileManagers[e];t.setEventedParent(null),t.onRemove(this.map)}this.imageManager.setEventedParent(null),this.setEventedParent(null),e&&this.dispatcher.broadcast(`RM`,void 0),this.dispatcher.remove(e)}_clearSource(e){this.tileManagers[e].clearTiles()}_reloadSource(e){this.tileManagers[e].resume(),this.tileManagers[e].reload()}_updateSources(e){for(let t in this.tileManagers)this.tileManagers[t].update(e,this.map.terrain)}_generateCollisionBoxes(){for(let e in this.tileManagers)this._reloadSource(e)}triggerSymbolPlacement(){this._symbolPlacementTriggered=!0}_placementInputsChanged(e,t,n){let r=this.pauseablePlacement;return!r||this._symbolPlacementTriggered||this._placedProjectionTransition!==this.projection?.transitionState||r._showCollisionBoxes!==t||r.placement.collisionGroups.crossSourceCollisions!==n||r.placement.transform.renderWorldCopies!==e.renderWorldCopies||!m(r.placement.transform.modelViewProjectionMatrix,e.modelViewProjectionMatrix)}_updatePlacement(e,t,n,r,i=!1){let a=!1,o=!1,s={};for(let t of this._order){let n=this._layers[t];if(n.type!==`symbol`)continue;if(!s[n.source]){let e=this.tileManagers[n.source];s[n.source]=e.getRenderableIds(!0).map(t=>e.getTileByID(t)).sort((e,t)=>t.tileID.overscaledZ-e.tileID.overscaledZ||(e.tileID.isLessThan(t.tileID)?-1:1))}let r=this.crossTileSymbolIndex.addLayer(n,s[n.source],e.center.lng);a||=r}this.crossTileSymbolIndex.pruneUnusedLayers(this._order),i||=this._layerOrderChanged||n===0;let c=a||this._placementInputsChanged(e,t,r),l=this.pauseablePlacement?.isDone()&&!this.placement.stillRecent(U(),e.zoom);if((i||!this.pauseablePlacement||l&&(c||this.placement.stale))&&(this._symbolPlacementTriggered=!1,this._placedProjectionTransition=this.projection?.transitionState,this.pauseablePlacement=new Os(e,this.map.terrain,this._order,i,t,n,r,this.placement),this._layerOrderChanged=!1),this.pauseablePlacement.isDone()?c&&this.placement.setStale():(this.pauseablePlacement.continuePlacement(this._order,this._layers,s),this.pauseablePlacement.isDone()&&(this.placement=this.pauseablePlacement.commit(U()),o=!0),a&&this.pauseablePlacement.placement.setStale()),o||a)for(let e of this._order){let t=this._layers[e];t.type===`symbol`&&this.placement.updateLayerOpacities(t,s[t.source])}return!this.pauseablePlacement.isDone()||this.placement.hasTransitions(U())}_releaseSymbolFadeTiles(){for(let e in this.tileManagers)this.tileManagers[e].releaseSymbolFadeTiles()}async getImages(e,t){let n=await this.imageManager.getImages(t.icons);this._updateTilesForChangedImages();let r=this.tileManagers[t.source];return r&&r.setDependencies(t.tileID.key,t.type,t.icons),n}async getGlyphs(e,t){let n=await this.glyphManager.getGlyphs(t.stacks),r=this.tileManagers[t.source];return r&&r.setDependencies(t.tileID.key,t.type,[``]),n}getGlyphsUrl(){return this.stylesheet.glyphs||null}setGlyphs(e,t={}){this._checkLoaded(),!(e&&this._validate(Ut.glyphs,`glyphs`,e,null,t))&&(this._changed=!0,this._glyphsDidChange=!0,this.stylesheet.glyphs=e,this.glyphManager.entries={},this.glyphManager.setURL(e))}getFontFaces(){return this.stylesheet[`font-faces`]||null}setFontFaces(e){this._checkLoaded(),this._changed=!0,this._glyphsDidChange=!0,this.stylesheet[`font-faces`]=e,this.glyphManager.setFontFaces(e)}async getDashes(e,t){let n={};for(let[e,r]of Object.entries(t.dashes))n[e]=this.lineAtlas.getDash(r.dasharray,r.round);return n}addSprite(e,t,n={},r){this._checkLoaded();let i=[{id:e,url:t}],a=[...gi(this.stylesheet.sprite),...i];this._validate(Ut.sprite,`sprite`,a,null,n)||(this.stylesheet.sprite=a,this._loadSprite(i,!0,r))}removeSprite(e){this._checkLoaded();let t=gi(this.stylesheet.sprite);if(!t.find(t=>t.id===e)){this.fire(new H(Error(`Sprite "${e}" doesn't exists on this map.`)));return}let n=this.imageManager.removeSpriteImages(e);this._markImagesChanged(n),t.splice(t.findIndex(t=>t.id===e),1),this.stylesheet.sprite=t.length>0?t:void 0,this._imagesListDirty=!0,this._changed=!0,this.fire(new qr(`data`))}getSprite(){return gi(this.stylesheet.sprite)}setSprite(e,t={},n){this._checkLoaded(),!(e&&this._validate(Ut.sprite,`sprite`,e,null,t))&&(this.stylesheet.sprite=e,e?this._loadSprite(e,!0,n):(this._unloadSprite(),n&&n(null)))}destroy(){this._frameRequest&&=(this._frameRequest.abort(),null),this._loadStyleRequest&&=(this._loadStyleRequest.abort(),null),this._spriteRequest&&=(this._spriteRequest.abort(),null);for(let e in this.tileManagers){let t=this.tileManagers[e];t.setEventedParent(null),t.onRemove(this.map)}this.tileManagers={},this.imageManager&&(this.imageManager.setEventedParent(null),this.imageManager.destroy(),this.patternAtlas.destroy()),this.glyphManager&&this.glyphManager.destroy();for(let e in this._layers){let t=this._layers[e];t.setEventedParent(null),t.onRemove&&t.onRemove(this.map)}this._setInitialValues(),this.setEventedParent(null),this.dispatcher.unregisterMessageHandler(`GG`),this.dispatcher.unregisterMessageHandler(`GI`),this.dispatcher.unregisterMessageHandler(`GDA`),this.dispatcher.remove(!0),this._listeners={},this._oneTimeListeners={}}};const hl=wt([{name:`a_pos`,type:`Int16`,components:2},{name:`a_texture_pos`,type:`Int16`,components:2}]);var gl=class{constructor(){this.boundProgram=null,this.boundLayoutVertexBuffer=null,this.boundPaintVertexBuffers=[],this.boundIndexBuffer=null,this.boundVertexOffset=null,this.boundDynamicVertexBuffer=null,this.vao=null}bind(e,t,n,r,i,a,o,s,c){this.context=e;let l=this.boundPaintVertexBuffers.length!==r.length;for(let e=0;!l&&e({u_depth:new F(e,t.u_depth),u_terrain:new F(e,t.u_terrain),u_terrain_dim:new P(e,t.u_terrain_dim),u_terrain_matrix:new ut(e,t.u_terrain_matrix),u_terrain_unpack:new Ce(e,t.u_terrain_unpack),u_terrain_exaggeration:new P(e,t.u_terrain_exaggeration)}),vl=(e,t)=>({u_texture:new F(e,t.u_texture),u_ele_delta:new P(e,t.u_ele_delta),u_fog_matrix:new ut(e,t.u_fog_matrix),u_fog_color:new qe(e,t.u_fog_color),u_fog_ground_blend:new P(e,t.u_fog_ground_blend),u_fog_ground_blend_opacity:new P(e,t.u_fog_ground_blend_opacity),u_horizon_color:new qe(e,t.u_horizon_color),u_horizon_fog_blend:new P(e,t.u_horizon_fog_blend),u_is_globe_mode:new P(e,t.u_is_globe_mode)}),yl=(e,t)=>({u_ele_delta:new P(e,t.u_ele_delta)}),bl=(e,t,n,r,i)=>({u_texture:0,u_ele_delta:e,u_fog_matrix:t,u_fog_color:n?n.properties.get(`fog-color`):R.white,u_fog_ground_blend:n?n.properties.get(`fog-ground-blend`):1,u_fog_ground_blend_opacity:i?0:n?n.calculateFogBlendOpacity(r):0,u_horizon_color:n?n.properties.get(`horizon-color`):R.white,u_horizon_fog_blend:n?n.properties.get(`horizon-fog-blend`):1,u_is_globe_mode:+!!i}),xl=e=>({u_ele_delta:e}),Sl=(e,t)=>({u_projection_matrix:new ut(e,t.u_projection_matrix),u_projection_tile_mercator_coords:new Ce(e,t.u_projection_tile_mercator_coords),u_projection_clipping_plane:new Ce(e,t.u_projection_clipping_plane),u_projection_transition:new P(e,t.u_projection_transition),u_projection_fallback_matrix:new ut(e,t.u_projection_fallback_matrix),u_projection_clip_antimeridian:new F(e,t.u_projection_clip_antimeridian)}),Cl=e=>({u_projection_matrix:e.mainMatrix,u_projection_tile_mercator_coords:e.tileMercatorCoords,u_projection_clipping_plane:e.clippingPlane,u_projection_transition:e.projectionTransition,u_projection_fallback_matrix:e.fallbackMatrix,u_projection_clip_antimeridian:+!!e.clipAntimeridian});function wl(e){let t=[];for(let n of e){if(n===null)continue;let e=n.split(` `);t.push(e.pop())}return t}function Tl(e,t){let n=new Set([e.INT,e.INT_VEC2,e.INT_VEC3,e.INT_VEC4,e.UNSIGNED_INT,e.UNSIGNED_INT_VEC2,e.UNSIGNED_INT_VEC3,e.UNSIGNED_INT_VEC4]),r=new Set,i=e.getProgramParameter(t,e.ACTIVE_ATTRIBUTES);for(let a=0;a=0&&(this.attributes[e]={location:t,isInteger:T.has(e)})}l.deleteShader(C),l.deleteShader(S);for(let e of v)if(e&&!w[e]){let t=l.getUniformLocation(this.program,e);t&&(w[e]=t)}this.fixedUniforms=r(e,w),this.terrainUniforms=_l(e,w),this.projectionUniforms=Sl(e,w),this.binderUniforms=n?n.getUniforms(e,w):[]}draw(e,t,n,r,i,a,o,s,c,l,u,d,f,p,m,h,g,_,v){let y=e.gl;if(this.failedToCreate)return;if(e.program.set(this.program),e.setDepthMode(n),e.setStencilMode(r),e.setColorMode(i),e.setCullFace(a),s){e.activeTexture.set(y.TEXTURE2),y.bindTexture(y.TEXTURE_2D,s.depthTexture),e.activeTexture.set(y.TEXTURE3),y.bindTexture(y.TEXTURE_2D,s.texture);for(let e in this.terrainUniforms)this.terrainUniforms[e].set(s[e])}if(c){let e=Cl(c);for(let t in this.projectionUniforms)this.projectionUniforms[t].set(e[t])}if(o)for(let e in this.fixedUniforms)this.fixedUniforms[e].set(o[e]);h&&h.setUniforms(e,this.binderUniforms,p,{zoom:m});let b=0;switch(t){case y.LINES:b=2;break;case y.TRIANGLES:b=3;break;case y.LINE_STRIP:b=1}for(let n of f.get())n.vaos||={},n.vaos[l]||=new gl,n.vaos[l].bind(e,this,u,h?h.getPaintVertexBuffers():[],d,n.vertexOffset,g,_,v),y.drawElements(t,n.primitiveLength*b,y.UNSIGNED_SHORT,n.primitiveOffset*b*2)}};function Dl(e,t,n){let r=1/lt(n,1,t.transform.tileZoom),i=2**n.tileID.overscaledZ,a=n.tileSize*2**t.transform.tileZoom/i,o=a*(n.tileID.canonical.x+n.tileID.wrap*i),s=a*n.tileID.canonical.y;return{u_image:0,u_texsize:n.imageAtlasTexture.size,u_scale:[r,e.fromScale,e.toScale],u_fade:e.t,u_pixel_coord_upper:[o>>16,s>>16],u_pixel_coord_lower:[o&65535,s&65535]}}function Ol(e,t,n,r){let i=n.patternAtlas.getPattern(e.from.toString()),a=n.patternAtlas.getPattern(e.to.toString()),{width:o,height:s}=n.patternAtlas.getPixelSize(),c=2**r.tileID.overscaledZ,l=r.tileSize*2**n.transform.tileZoom/c,u=l*(r.tileID.canonical.x+r.tileID.wrap*c),d=l*r.tileID.canonical.y;return{u_image:0,u_pattern_tl_a:i.tl,u_pattern_br_a:i.br,u_pattern_tl_b:a.tl,u_pattern_br_b:a.br,u_texsize:[o,s],u_mix:t.t,u_pattern_size_a:i.displaySize,u_pattern_size_b:a.displaySize,u_scale_a:t.fromScale,u_scale_b:t.toScale,u_tile_units_to_pixels:1/lt(r,1,n.transform.tileZoom),u_pixel_coord_upper:[u>>16,d>>16],u_pixel_coord_lower:[u&65535,d&65535]}}const kl=(e,t)=>({u_lightpos:new ue(e,t.u_lightpos),u_lightpos_globe:new ue(e,t.u_lightpos_globe),u_lightintensity:new P(e,t.u_lightintensity),u_lightcolor:new ue(e,t.u_lightcolor),u_vertical_gradient:new P(e,t.u_vertical_gradient),u_opacity:new P(e,t.u_opacity),u_fill_translate:new j(e,t.u_fill_translate)}),Al=(e,t)=>({u_lightpos:new ue(e,t.u_lightpos),u_lightpos_globe:new ue(e,t.u_lightpos_globe),u_lightintensity:new P(e,t.u_lightintensity),u_lightcolor:new ue(e,t.u_lightcolor),u_vertical_gradient:new P(e,t.u_vertical_gradient),u_height_factor:new P(e,t.u_height_factor),u_opacity:new P(e,t.u_opacity),u_fill_translate:new j(e,t.u_fill_translate),u_image:new F(e,t.u_image),u_texsize:new j(e,t.u_texsize),u_pixel_coord_upper:new j(e,t.u_pixel_coord_upper),u_pixel_coord_lower:new j(e,t.u_pixel_coord_lower),u_scale:new ue(e,t.u_scale),u_fade:new P(e,t.u_fade)}),jl=(e,t,n,r)=>{let i=e.style.light,a=i.getCartesianPosition(),o=O();i.properties.get(`anchor`)===`viewport`&&se(o,e.transform.bearingInRadians),an(a,a,o);let s=e.transform.transformLightDirection(a),c=i.properties.get(`color`);return{u_lightpos:a,u_lightpos_globe:s,u_lightintensity:i.properties.get(`intensity`),u_lightcolor:[c.r,c.g,c.b],u_vertical_gradient:+t,u_opacity:n,u_fill_translate:r}},Ml=(e,t,n,r,i,a,o)=>z(jl(e,t,n,r),Dl(a,e,o),{u_height_factor:-(2**i.overscaledZ)/o.tileSize/8}),Nl=(e,t)=>({u_fill_translate:new j(e,t.u_fill_translate)}),Pl=(e,t)=>({u_image:new F(e,t.u_image),u_texsize:new j(e,t.u_texsize),u_pixel_coord_upper:new j(e,t.u_pixel_coord_upper),u_pixel_coord_lower:new j(e,t.u_pixel_coord_lower),u_scale:new ue(e,t.u_scale),u_fade:new P(e,t.u_fade),u_fill_translate:new j(e,t.u_fill_translate)}),Fl=(e,t)=>({u_world:new j(e,t.u_world),u_fill_translate:new j(e,t.u_fill_translate)}),Il=(e,t)=>({u_world:new j(e,t.u_world),u_image:new F(e,t.u_image),u_texsize:new j(e,t.u_texsize),u_pixel_coord_upper:new j(e,t.u_pixel_coord_upper),u_pixel_coord_lower:new j(e,t.u_pixel_coord_lower),u_scale:new ue(e,t.u_scale),u_fade:new P(e,t.u_fade),u_fill_translate:new j(e,t.u_fill_translate)}),Ll=(e,t,n,r)=>z(Dl(t,e,n),{u_fill_translate:r}),Rl=e=>({u_fill_translate:e}),zl=(e,t)=>({u_world:e,u_fill_translate:t}),Bl=(e,t,n,r,i)=>z(Ll(e,t,n,i),{u_world:r}),Vl=(e,t)=>({u_camera_to_center_distance:new P(e,t.u_camera_to_center_distance),u_scale_with_map:new F(e,t.u_scale_with_map),u_pitch_with_map:new F(e,t.u_pitch_with_map),u_extrude_scale:new j(e,t.u_extrude_scale),u_device_pixel_ratio:new P(e,t.u_device_pixel_ratio),u_globe_extrude_scale:new P(e,t.u_globe_extrude_scale),u_translate:new j(e,t.u_translate)}),Hl=(e,t,n,r,i)=>{let a=e.transform,o,s,c=0;if(n.paint.get(`circle-pitch-alignment`)===`map`){let e=lt(t,1,a.zoom);o=!0,s=[e,e],c=e/(N*2**t.tileID.overscaledZ)*2*Math.PI*i}else o=!1,s=a.pixelsToGLUnits;return{u_camera_to_center_distance:a.cameraToCenterDistance,u_scale_with_map:+(n.paint.get(`circle-pitch-scale`)===`map`),u_pitch_with_map:+o,u_device_pixel_ratio:e.pixelRatio,u_extrude_scale:s,u_globe_extrude_scale:c,u_translate:r}},Ul=(e,t)=>({u_pixel_extrude_scale:new j(e,t.u_pixel_extrude_scale)}),Wl=(e,t)=>({u_viewport_size:new j(e,t.u_viewport_size)}),Gl=e=>({u_pixel_extrude_scale:[1/e.width,1/e.height]}),Kl=e=>({u_viewport_size:[e.width,e.height]}),ql=(e,t)=>({u_color:new qe(e,t.u_color),u_overlay:new F(e,t.u_overlay),u_overlay_scale:new P(e,t.u_overlay_scale)}),Jl=(e,t=1)=>({u_color:e,u_overlay:0,u_overlay_scale:t}),Yl=(e,t)=>({u_extrude_scale:new P(e,t.u_extrude_scale),u_intensity:new P(e,t.u_intensity),u_globe_extrude_scale:new P(e,t.u_globe_extrude_scale)}),Xl=(e,t)=>({u_matrix:new ut(e,t.u_matrix),u_world:new j(e,t.u_world),u_image:new F(e,t.u_image),u_color_ramp:new F(e,t.u_color_ramp),u_opacity:new P(e,t.u_opacity)}),Zl=(e,t,n,r)=>{let i=lt(e,1,t)/(N*2**e.tileID.overscaledZ)*2*Math.PI*r;return{u_extrude_scale:lt(e,1,t),u_intensity:n,u_globe_extrude_scale:i}},Ql=(e,t,n,r)=>{let i=vr();Ne(i,0,e.width,e.height,0,0,1);let a=e.context.gl;return{u_matrix:i,u_world:[a.drawingBufferWidth,a.drawingBufferHeight],u_image:n,u_color_ramp:r,u_opacity:t.paint.get(`heatmap-opacity`)}},$l=(e,t)=>({u_image:new F(e,t.u_image),u_latrange:new j(e,t.u_latrange),u_exaggeration:new P(e,t.u_exaggeration),u_altitudes:new f(e,t.u_altitudes),u_azimuths:new f(e,t.u_azimuths),u_accent:new qe(e,t.u_accent),u_method:new F(e,t.u_method),u_shadows:new kr(e,t.u_shadows),u_highlights:new kr(e,t.u_highlights)}),eu=(e,t)=>({u_matrix:new ut(e,t.u_matrix),u_image:new F(e,t.u_image),u_dimension:new j(e,t.u_dimension),u_zoom:new P(e,t.u_zoom),u_unpack:new Ce(e,t.u_unpack)}),tu=(e,t,n)=>{let r=n.paint.get(`hillshade-accent-color`),i;switch(n.paint.get(`hillshade-method`)){case`basic`:i=4;break;case`combined`:i=1;break;case`igor`:i=2;break;case`multidirectional`:i=3;break;default:i=0}let a=n.getIlluminationProperties();for(let t=0;t{let n=t.stride,r=vr();return Ne(r,0,N,-N,0,0,1),Le(r,r,[0,-N,0]),{u_matrix:r,u_image:1,u_dimension:[n,n],u_zoom:e.overscaledZ,u_unpack:t.getUnpackVector()}};function ru(e,t){let n=2**t.canonical.z,r=t.canonical.y;return[new B(0,r/n).toLngLat().lat,new B(0,(r+1)/n).toLngLat().lat]}const iu=(e,t)=>({u_image:new F(e,t.u_image),u_unpack:new Ce(e,t.u_unpack),u_dimension:new j(e,t.u_dimension),u_elevation_stops:new F(e,t.u_elevation_stops),u_color_stops:new F(e,t.u_color_stops),u_color_ramp_size:new F(e,t.u_color_ramp_size),u_opacity:new P(e,t.u_opacity)}),au=(e,t,n=0)=>({u_image:0,u_unpack:t.getUnpackVector(),u_dimension:[t.stride,t.stride],u_elevation_stops:1,u_color_stops:4,u_color_ramp_size:n,u_opacity:e.paint.get(`color-relief-opacity`)}),ou=(e,t)=>({u_translation:new j(e,t.u_translation),u_ratio:new P(e,t.u_ratio),u_device_pixel_ratio:new P(e,t.u_device_pixel_ratio),u_units_to_pixels:new j(e,t.u_units_to_pixels)}),su=(e,t)=>({u_translation:new j(e,t.u_translation),u_ratio:new P(e,t.u_ratio),u_device_pixel_ratio:new P(e,t.u_device_pixel_ratio),u_units_to_pixels:new j(e,t.u_units_to_pixels),u_image:new F(e,t.u_image),u_image_height:new P(e,t.u_image_height)}),cu=(e,t)=>({u_translation:new j(e,t.u_translation),u_texsize:new j(e,t.u_texsize),u_ratio:new P(e,t.u_ratio),u_device_pixel_ratio:new P(e,t.u_device_pixel_ratio),u_image:new F(e,t.u_image),u_units_to_pixels:new j(e,t.u_units_to_pixels),u_scale:new ue(e,t.u_scale),u_fade:new P(e,t.u_fade)}),lu=(e,t)=>({u_translation:new j(e,t.u_translation),u_ratio:new P(e,t.u_ratio),u_device_pixel_ratio:new P(e,t.u_device_pixel_ratio),u_units_to_pixels:new j(e,t.u_units_to_pixels),u_image:new F(e,t.u_image),u_mix:new P(e,t.u_mix),u_tileratio:new P(e,t.u_tileratio),u_crossfade_from:new P(e,t.u_crossfade_from),u_crossfade_to:new P(e,t.u_crossfade_to),u_lineatlas_width:new P(e,t.u_lineatlas_width),u_lineatlas_height:new P(e,t.u_lineatlas_height)}),uu=(e,t)=>({u_translation:new j(e,t.u_translation),u_ratio:new P(e,t.u_ratio),u_device_pixel_ratio:new P(e,t.u_device_pixel_ratio),u_units_to_pixels:new j(e,t.u_units_to_pixels),u_image:new F(e,t.u_image),u_image_height:new P(e,t.u_image_height),u_tileratio:new P(e,t.u_tileratio),u_crossfade_from:new P(e,t.u_crossfade_from),u_crossfade_to:new P(e,t.u_crossfade_to),u_image_dash:new F(e,t.u_image_dash),u_mix:new P(e,t.u_mix),u_lineatlas_width:new P(e,t.u_lineatlas_width),u_lineatlas_height:new P(e,t.u_lineatlas_height)}),du=(e,t,n,r)=>{let i=e.transform;return{u_translation:_u(e,t,n),u_ratio:r/lt(t,1,i.zoom),u_device_pixel_ratio:e.pixelRatio,u_units_to_pixels:[1/i.pixelsToGLUnits[0],1/i.pixelsToGLUnits[1]]}},fu=(e,t,n,r,i)=>z(du(e,t,n,r),{u_image:0,u_image_height:i}),pu=(e,t,n,r,i)=>{let a=e.transform,o=gu(t,a);return{u_translation:_u(e,t,n),u_texsize:t.imageAtlasTexture.size,u_ratio:r/lt(t,1,a.zoom),u_device_pixel_ratio:e.pixelRatio,u_image:0,u_scale:[o,i.fromScale,i.toScale],u_fade:i.t,u_units_to_pixels:[1/a.pixelsToGLUnits[0],1/a.pixelsToGLUnits[1]]}},mu=(e,t,n,r,i)=>{let a=e.transform,o=gu(t,a);return z(du(e,t,n,r),{u_tileratio:o,u_crossfade_from:i.fromScale,u_crossfade_to:i.toScale,u_image:0,u_mix:i.t,u_lineatlas_width:e.lineAtlas.width,u_lineatlas_height:e.lineAtlas.height})},hu=(e,t,n,r,i,a)=>{let o=e.transform,s=gu(t,o);return z(du(e,t,n,r),{u_image:0,u_image_height:a,u_tileratio:s,u_crossfade_from:i.fromScale,u_crossfade_to:i.toScale,u_image_dash:1,u_mix:i.t,u_lineatlas_width:e.lineAtlas.width,u_lineatlas_height:e.lineAtlas.height})};function gu(e,t){return 1/lt(e,1,t.tileZoom)}function _u(e,t,n){return le(e.transform,t,n.paint.get(`line-translate`),n.paint.get(`line-translate-anchor`))}const vu=(e,t)=>({u_image:new F(e,t.u_image),u_opacity:new P(e,t.u_opacity)}),yu=(e,t)=>({u_image:t,u_opacity:e}),bu=(e,t)=>({u_is_size_zoom_constant:new F(e,t.u_is_size_zoom_constant),u_is_size_feature_constant:new F(e,t.u_is_size_feature_constant),u_size_t:new P(e,t.u_size_t),u_size:new P(e,t.u_size),u_camera_to_center_distance:new P(e,t.u_camera_to_center_distance),u_pitch:new P(e,t.u_pitch),u_rotate_symbol:new F(e,t.u_rotate_symbol),u_aspect_ratio:new P(e,t.u_aspect_ratio),u_fade_change:new P(e,t.u_fade_change),u_label_plane_matrix:new ut(e,t.u_label_plane_matrix),u_coord_matrix:new ut(e,t.u_coord_matrix),u_is_text:new F(e,t.u_is_text),u_pitch_with_map:new F(e,t.u_pitch_with_map),u_is_along_line:new F(e,t.u_is_along_line),u_is_variable_anchor:new F(e,t.u_is_variable_anchor),u_texsize:new j(e,t.u_texsize),u_texture:new F(e,t.u_texture),u_translation:new j(e,t.u_translation),u_pitched_scale:new P(e,t.u_pitched_scale),u_is_offset:new F(e,t.u_is_offset),u_height_anchor_ground:new F(e,t.u_height_anchor_ground)}),xu=(e,t)=>({u_is_size_zoom_constant:new F(e,t.u_is_size_zoom_constant),u_is_size_feature_constant:new F(e,t.u_is_size_feature_constant),u_size_t:new P(e,t.u_size_t),u_size:new P(e,t.u_size),u_camera_to_center_distance:new P(e,t.u_camera_to_center_distance),u_pitch:new P(e,t.u_pitch),u_rotate_symbol:new F(e,t.u_rotate_symbol),u_aspect_ratio:new P(e,t.u_aspect_ratio),u_fade_change:new P(e,t.u_fade_change),u_label_plane_matrix:new ut(e,t.u_label_plane_matrix),u_coord_matrix:new ut(e,t.u_coord_matrix),u_is_text:new F(e,t.u_is_text),u_pitch_with_map:new F(e,t.u_pitch_with_map),u_is_along_line:new F(e,t.u_is_along_line),u_is_variable_anchor:new F(e,t.u_is_variable_anchor),u_texsize:new j(e,t.u_texsize),u_texture:new F(e,t.u_texture),u_gamma_scale:new P(e,t.u_gamma_scale),u_device_pixel_ratio:new P(e,t.u_device_pixel_ratio),u_is_halo:new F(e,t.u_is_halo),u_is_plain:new F(e,t.u_is_plain),u_translation:new j(e,t.u_translation),u_pitched_scale:new P(e,t.u_pitched_scale),u_is_offset:new F(e,t.u_is_offset),u_height_anchor_ground:new F(e,t.u_height_anchor_ground)}),Su=(e,t)=>({u_is_size_zoom_constant:new F(e,t.u_is_size_zoom_constant),u_is_size_feature_constant:new F(e,t.u_is_size_feature_constant),u_size_t:new P(e,t.u_size_t),u_size:new P(e,t.u_size),u_camera_to_center_distance:new P(e,t.u_camera_to_center_distance),u_pitch:new P(e,t.u_pitch),u_rotate_symbol:new F(e,t.u_rotate_symbol),u_aspect_ratio:new P(e,t.u_aspect_ratio),u_fade_change:new P(e,t.u_fade_change),u_label_plane_matrix:new ut(e,t.u_label_plane_matrix),u_coord_matrix:new ut(e,t.u_coord_matrix),u_is_text:new F(e,t.u_is_text),u_pitch_with_map:new F(e,t.u_pitch_with_map),u_is_along_line:new F(e,t.u_is_along_line),u_is_variable_anchor:new F(e,t.u_is_variable_anchor),u_texsize:new j(e,t.u_texsize),u_texsize_icon:new j(e,t.u_texsize_icon),u_texture:new F(e,t.u_texture),u_texture_icon:new F(e,t.u_texture_icon),u_gamma_scale:new P(e,t.u_gamma_scale),u_device_pixel_ratio:new P(e,t.u_device_pixel_ratio),u_is_halo:new F(e,t.u_is_halo),u_translation:new j(e,t.u_translation),u_pitched_scale:new P(e,t.u_pitched_scale),u_is_offset:new F(e,t.u_is_offset),u_height_anchor_ground:new F(e,t.u_height_anchor_ground)}),Cu=(e,t,n,r,i,a,o,s,c,l,u,d,f,p,m)=>{let h=o.transform;return{u_is_size_zoom_constant:+(e===`constant`||e===`source`),u_is_size_feature_constant:+(e===`constant`||e===`camera`),u_size_t:t?t.uSizeT:0,u_size:t?t.uSize:0,u_camera_to_center_distance:h.cameraToCenterDistance,u_pitch:h.pitch/360*2*Math.PI,u_rotate_symbol:+n,u_aspect_ratio:h.width/h.height,u_fade_change:o.options.fadeDuration?o.symbolFadeChange:1,u_label_plane_matrix:s,u_coord_matrix:c,u_is_text:+u,u_pitch_with_map:+r,u_is_along_line:i,u_is_variable_anchor:a,u_texsize:d,u_texture:0,u_translation:l,u_pitched_scale:f,u_is_offset:p,u_height_anchor_ground:+m}},wu=(e,t,n,r,i,a,o,s,c,l,u,d,f,p,m,h)=>{let g=o.transform;return z(Cu(e,t,n,r,i,a,o,s,c,l,u,d,p,m,h),{u_gamma_scale:r?Math.cos(g.pitch*Math.PI/180)*g.cameraToCenterDistance:1,u_device_pixel_ratio:o.pixelRatio,u_is_halo:+!!f,u_is_plain:1})},Tu=(e,t,n,r,i,a,o,s,c,l,u,d,f,p,m)=>z(wu(e,t,n,r,i,a,o,s,c,l,!0,u,!0,f,p,m),{u_texsize_icon:d,u_texture_icon:1}),Eu=(e,t)=>({u_opacity:new P(e,t.u_opacity),u_color:new qe(e,t.u_color)}),Du=(e,t)=>({u_opacity:new P(e,t.u_opacity),u_image:new F(e,t.u_image),u_pattern_tl_a:new j(e,t.u_pattern_tl_a),u_pattern_br_a:new j(e,t.u_pattern_br_a),u_pattern_tl_b:new j(e,t.u_pattern_tl_b),u_pattern_br_b:new j(e,t.u_pattern_br_b),u_texsize:new j(e,t.u_texsize),u_mix:new P(e,t.u_mix),u_pattern_size_a:new j(e,t.u_pattern_size_a),u_pattern_size_b:new j(e,t.u_pattern_size_b),u_scale_a:new P(e,t.u_scale_a),u_scale_b:new P(e,t.u_scale_b),u_pixel_coord_upper:new j(e,t.u_pixel_coord_upper),u_pixel_coord_lower:new j(e,t.u_pixel_coord_lower),u_tile_units_to_pixels:new P(e,t.u_tile_units_to_pixels)}),Ou=(e,t)=>({u_opacity:e,u_color:t}),ku=(e,t,n,r,i)=>z(Ol(n,i,t,r),{u_opacity:e}),Au=(e,t)=>({u_sun_pos:new ue(e,t.u_sun_pos),u_atmosphere_blend:new P(e,t.u_atmosphere_blend),u_globe_position:new ue(e,t.u_globe_position),u_globe_radius:new P(e,t.u_globe_radius),u_inv_proj_matrix:new ut(e,t.u_inv_proj_matrix)}),ju=(e,t,n,r,i)=>({u_sun_pos:e,u_atmosphere_blend:t,u_globe_position:n,u_globe_radius:r,u_inv_proj_matrix:i}),Mu=(e,t)=>({u_sky_color:new qe(e,t.u_sky_color),u_horizon_color:new qe(e,t.u_horizon_color),u_horizon:new j(e,t.u_horizon),u_horizon_normal:new j(e,t.u_horizon_normal),u_sky_horizon_blend:new P(e,t.u_sky_horizon_blend),u_sky_blend:new P(e,t.u_sky_blend)}),Nu=(e,t,n)=>{let r=Math.cos(t.rollInRadians),i=Math.sin(t.rollInRadians),a=Be(t),o=t.getProjectionData({overscaledTileID:null,applyGlobeMatrix:!0,applyTerrainMatrix:!0}).projectionTransition;return{u_sky_color:e.properties.get(`sky-color`),u_horizon_color:e.properties.get(`horizon-color`),u_horizon:[(t.width/2-a*i)*n,(t.height/2+a*r)*n],u_horizon_normal:[-i,r],u_sky_horizon_blend:e.properties.get(`sky-horizon-blend`)*t.height/2*n,u_sky_blend:o}},Pu=(e,t)=>{},Fu={fillExtrusion:kl,fillExtrusionPattern:Al,fill:Nl,fillPattern:Pl,fillOutline:Fl,fillOutlinePattern:Il,circle:Vl,collisionBox:Ul,collisionCircle:Wl,debug:ql,depth:Pu,clippingMask:Pu,heatmap:Yl,heatmapTexture:Xl,hillshade:$l,hillshadePrepare:eu,colorRelief:iu,line:ou,lineGradient:su,linePattern:cu,lineSDF:lu,lineGradientSDF:uu,layerOpacity:vu,raster:Ra,symbolIcon:bu,symbolSDF:xu,symbolTextAndIcon:Su,background:Eu,backgroundPattern:Du,terrain:vl,terrainDepth:yl,atmosphere:Au,sky:Mu};var Iu=class{constructor(e,t,n){this.context=e;let r=e.gl;this.buffer=r.createBuffer(),this.dynamicDraw=!!n,this.context.unbindVAO(),e.bindElementBuffer.set(this.buffer),r.bufferData(r.ELEMENT_ARRAY_BUFFER,t.arrayBuffer,this.dynamicDraw?r.DYNAMIC_DRAW:r.STATIC_DRAW),this.dynamicDraw||t.freeBufferAfterUpload()}bind(){this.context.bindElementBuffer.set(this.buffer)}updateData(e){let t=this.context.gl;if(!this.dynamicDraw)throw Error(`Attempted to update data while not in dynamic mode.`);this.context.unbindVAO(),this.bind(),t.bufferSubData(t.ELEMENT_ARRAY_BUFFER,0,e.arrayBuffer)}destroy(){let e=this.context.gl;this.buffer&&(e.deleteBuffer(this.buffer),delete this.buffer)}};const Lu={Int8:`BYTE`,Uint8:`UNSIGNED_BYTE`,Int16:`SHORT`,Uint16:`UNSIGNED_SHORT`,Int32:`INT`,Uint32:`UNSIGNED_INT`,Float32:`FLOAT`};var Ru=class{constructor(e,t,n,r){this.length=t.length,this.attributes=n,this.itemSize=t.bytesPerElement,this.dynamicDraw=r,this.context=e;let i=e.gl;this.buffer=i.createBuffer(),e.bindVertexBuffer.set(this.buffer),i.bufferData(i.ARRAY_BUFFER,t.arrayBuffer,this.dynamicDraw?i.DYNAMIC_DRAW:i.STATIC_DRAW),this.dynamicDraw||t.freeBufferAfterUpload()}bind(){this.context.bindVertexBuffer.set(this.buffer)}updateData(e){if(e.length!==this.length)throw Error(`Length of new data is ${e.length}, which doesn't match current length of ${this.length}`);let t=this.context.gl;this.bind(),t.bufferSubData(t.ARRAY_BUFFER,0,e.arrayBuffer)}enableAttributes(e,t){for(let n of this.attributes){let r=t.attributes[n.name];r!==void 0&&e.enableVertexAttribArray(r.location)}}setVertexAttribPointers(e,t,n){for(let r of this.attributes){let i=t.attributes[r.name];if(i!==void 0){let t=r.offset+this.itemSize*(n||0);i.isInteger?e.vertexAttribIPointer(i.location,r.components,e[Lu[r.type]],this.itemSize,t):e.vertexAttribPointer(i.location,r.components,e[Lu[r.type]],!1,this.itemSize,t)}}}destroy(){let e=this.context.gl;this.buffer&&(e.deleteBuffer(this.buffer),delete this.buffer)}},X=class{constructor(e){this.gl=e.gl,this.default=this.getDefault(),this.current=this.default,this.dirty=!1}get(){return this.current}set(e){}getDefault(){return this.default}setDefault(){this.set(this.default)}},zu=class extends X{getDefault(){return R.transparent}set(e){let t=this.current;e.r===t.r&&e.g===t.g&&e.b===t.b&&e.a===t.a&&!this.dirty||(this.gl.clearColor(e.r,e.g,e.b,e.a),this.current=e,this.dirty=!1)}},Bu=class extends X{getDefault(){return 1}set(e){e===this.current&&!this.dirty||(this.gl.clearDepth(e),this.current=e,this.dirty=!1)}},Vu=class extends X{getDefault(){return 0}set(e){e===this.current&&!this.dirty||(this.gl.clearStencil(e),this.current=e,this.dirty=!1)}},Hu=class extends X{getDefault(){return[!0,!0,!0,!0]}set(e){let t=this.current;e[0]===t[0]&&e[1]===t[1]&&e[2]===t[2]&&e[3]===t[3]&&!this.dirty||(this.gl.colorMask(e[0],e[1],e[2],e[3]),this.current=e,this.dirty=!1)}},Uu=class extends X{getDefault(){return!0}set(e){e===this.current&&!this.dirty||(this.gl.depthMask(e),this.current=e,this.dirty=!1)}},Wu=class extends X{getDefault(){return 255}set(e){e===this.current&&!this.dirty||(this.gl.stencilMask(e),this.current=e,this.dirty=!1)}},Gu=class extends X{getDefault(){return{func:this.gl.ALWAYS,ref:0,mask:255}}set(e){let t=this.current;e.func===t.func&&e.ref===t.ref&&e.mask===t.mask&&!this.dirty||(this.gl.stencilFunc(e.func,e.ref,e.mask),this.current=e,this.dirty=!1)}},Ku=class extends X{getDefault(){let e=this.gl;return[e.KEEP,e.KEEP,e.KEEP]}set(e){let t=this.current;e[0]===t[0]&&e[1]===t[1]&&e[2]===t[2]&&!this.dirty||(this.gl.stencilOp(e[0],e[1],e[2]),this.current=e,this.dirty=!1)}},qu=class extends X{getDefault(){return!1}set(e){if(e===this.current&&!this.dirty)return;let t=this.gl;e?t.enable(t.STENCIL_TEST):t.disable(t.STENCIL_TEST),this.current=e,this.dirty=!1}},Ju=class extends X{getDefault(){return[0,1]}set(e){let t=this.current;e[0]===t[0]&&e[1]===t[1]&&!this.dirty||(this.gl.depthRange(e[0],e[1]),this.current=e,this.dirty=!1)}},Yu=class extends X{getDefault(){return!1}set(e){if(e===this.current&&!this.dirty)return;let t=this.gl;e?t.enable(t.DEPTH_TEST):t.disable(t.DEPTH_TEST),this.current=e,this.dirty=!1}},Xu=class extends X{getDefault(){return this.gl.LESS}set(e){e===this.current&&!this.dirty||(this.gl.depthFunc(e),this.current=e,this.dirty=!1)}},Zu=class extends X{getDefault(){return!1}set(e){if(e===this.current&&!this.dirty)return;let t=this.gl;e?t.enable(t.BLEND):t.disable(t.BLEND),this.current=e,this.dirty=!1}},Qu=class extends X{getDefault(){let e=this.gl;return[e.ONE,e.ZERO]}set(e){let t=this.current;e[0]===t[0]&&e[1]===t[1]&&!this.dirty||(this.gl.blendFunc(e[0],e[1]),this.current=e,this.dirty=!1)}},$u=class extends X{getDefault(){return R.transparent}set(e){let t=this.current;e.r===t.r&&e.g===t.g&&e.b===t.b&&e.a===t.a&&!this.dirty||(this.gl.blendColor(e.r,e.g,e.b,e.a),this.current=e,this.dirty=!1)}},ed=class extends X{getDefault(){return this.gl.FUNC_ADD}set(e){e===this.current&&!this.dirty||(this.gl.blendEquation(e),this.current=e,this.dirty=!1)}},td=class extends X{getDefault(){return!1}set(e){if(e===this.current&&!this.dirty)return;let t=this.gl;e?t.enable(t.CULL_FACE):t.disable(t.CULL_FACE),this.current=e,this.dirty=!1}},nd=class extends X{getDefault(){return this.gl.BACK}set(e){e===this.current&&!this.dirty||(this.gl.cullFace(e),this.current=e,this.dirty=!1)}},rd=class extends X{getDefault(){return this.gl.CCW}set(e){e===this.current&&!this.dirty||(this.gl.frontFace(e),this.current=e,this.dirty=!1)}},id=class extends X{getDefault(){return null}set(e){e===this.current&&!this.dirty||(this.gl.useProgram(e),this.current=e,this.dirty=!1)}},ad=class extends X{getDefault(){return this.gl.TEXTURE0}set(e){e===this.current&&!this.dirty||(this.gl.activeTexture(e),this.current=e,this.dirty=!1)}},od=class extends X{getDefault(){let e=this.gl;return[0,0,e.drawingBufferWidth,e.drawingBufferHeight]}set(e){let t=this.current;e[0]===t[0]&&e[1]===t[1]&&e[2]===t[2]&&e[3]===t[3]&&!this.dirty||(this.gl.viewport(e[0],e[1],e[2],e[3]),this.current=e,this.dirty=!1)}},sd=class extends X{getDefault(){return null}set(e){if(e===this.current&&!this.dirty)return;let t=this.gl;t.bindFramebuffer(t.FRAMEBUFFER,e),this.current=e,this.dirty=!1}},cd=class extends X{getDefault(){return null}set(e){if(e===this.current&&!this.dirty)return;let t=this.gl;t.bindRenderbuffer(t.RENDERBUFFER,e),this.current=e,this.dirty=!1}},ld=class extends X{getDefault(){return null}set(e){if(e===this.current&&!this.dirty)return;let t=this.gl;t.bindTexture(t.TEXTURE_2D,e),this.current=e,this.dirty=!1}},ud=class extends X{getDefault(){return null}set(e){if(e===this.current&&!this.dirty)return;let t=this.gl;t.bindBuffer(t.ARRAY_BUFFER,e),this.current=e,this.dirty=!1}},dd=class extends X{getDefault(){return null}set(e){let t=this.gl;t.bindBuffer(t.ELEMENT_ARRAY_BUFFER,e),this.current=e,this.dirty=!1}},fd=class extends X{getDefault(){return null}set(e){e===this.current&&!this.dirty||(this.gl.bindVertexArray(e),this.current=e,this.dirty=!1)}},pd=class extends X{getDefault(){return 4}set(e){if(e===this.current&&!this.dirty)return;let t=this.gl;t.pixelStorei(t.UNPACK_ALIGNMENT,e),this.current=e,this.dirty=!1}},md=class extends X{getDefault(){return!1}set(e){if(e===this.current&&!this.dirty)return;let t=this.gl;t.pixelStorei(t.UNPACK_PREMULTIPLY_ALPHA_WEBGL,e),this.current=e,this.dirty=!1}},hd=class extends X{getDefault(){return!1}set(e){if(e===this.current&&!this.dirty)return;let t=this.gl;t.pixelStorei(t.UNPACK_FLIP_Y_WEBGL,e),this.current=e,this.dirty=!1}},gd=class extends X{constructor(e,t){super(e),this.context=e,this.parent=t}getDefault(){return null}},_d=class extends gd{setDirty(){this.dirty=!0}set(e){if(e===this.current&&!this.dirty)return;this.context.bindFramebuffer.set(this.parent);let t=this.gl;t.framebufferTexture2D(t.FRAMEBUFFER,t.COLOR_ATTACHMENT0,t.TEXTURE_2D,e,0),this.current=e,this.dirty=!1}},vd=class extends gd{set(e){if(e===this.current&&!this.dirty)return;this.context.bindFramebuffer.set(this.parent);let t=this.gl;t.framebufferRenderbuffer(t.FRAMEBUFFER,t.DEPTH_ATTACHMENT,t.RENDERBUFFER,e),this.current=e,this.dirty=!1}},yd=class extends gd{set(e){if(e===this.current&&!this.dirty)return;this.context.bindFramebuffer.set(this.parent);let t=this.gl;t.framebufferRenderbuffer(t.FRAMEBUFFER,t.DEPTH_STENCIL_ATTACHMENT,t.RENDERBUFFER,e),this.current=e,this.dirty=!1}},bd=class{constructor(e,t,n,r,i){this.context=e,this.width=t,this.height=n;let a=e.gl,o=this.framebuffer=a.createFramebuffer();if(this.colorAttachment=new _d(e,o),r)this.depthAttachment=i?new yd(e,o):new vd(e,o);else if(i)throw Error(`Stencil cannot be set without depth`)}destroy(){let e=this.context.gl,t=this.colorAttachment.get();if(t&&e.deleteTexture(t),this.depthAttachment){let t=this.depthAttachment.get();t&&e.deleteRenderbuffer(t)}e.deleteFramebuffer(this.framebuffer)}},xd=class{constructor(e,t,n){this.blendFunction=e,this.blendColor=t,this.mask=n}};xd.Replace=[1,0],xd.disabled=new xd(xd.Replace,R.transparent,[!1,!1,!1,!1]),xd.unblended=new xd(xd.Replace,R.transparent,[!0,!0,!0,!0]),xd.alphaBlended=new xd([1,771],R.transparent,[!0,!0,!0,!0]);var Sd=class{constructor(e){this.gl=e,this.clearColor=new zu(this),this.clearDepth=new Bu(this),this.clearStencil=new Vu(this),this.colorMask=new Hu(this),this.depthMask=new Uu(this),this.stencilMask=new Wu(this),this.stencilFunc=new Gu(this),this.stencilOp=new Ku(this),this.stencilTest=new qu(this),this.depthRange=new Ju(this),this.depthTest=new Yu(this),this.depthFunc=new Xu(this),this.blend=new Zu(this),this.blendFunc=new Qu(this),this.blendColor=new $u(this),this.blendEquation=new ed(this),this.cullFace=new td(this),this.cullFaceSide=new nd(this),this.frontFace=new rd(this),this.program=new id(this),this.activeTexture=new ad(this),this.viewport=new od(this),this.bindFramebuffer=new sd(this),this.bindRenderbuffer=new cd(this),this.bindTexture=new ld(this),this.bindVertexBuffer=new ud(this),this.bindElementBuffer=new dd(this),this.bindVertexArray=new fd(this),this.pixelStoreUnpack=new pd(this),this.pixelStoreUnpackPremultiplyAlpha=new md(this),this.pixelStoreUnpackFlipY=new hd(this),this.extTextureFilterAnisotropic=e.getExtension(`EXT_texture_filter_anisotropic`),this.extTextureFilterAnisotropic&&(this.extTextureFilterAnisotropicMax=e.getParameter(this.extTextureFilterAnisotropic.MAX_TEXTURE_MAX_ANISOTROPY_EXT)),this.maxTextureSize=e.getParameter(e.MAX_TEXTURE_SIZE),e.getExtension(`EXT_color_buffer_half_float`),e.getExtension(`EXT_color_buffer_float`)}setDefault(){this.unbindVAO(),this.clearColor.setDefault(),this.clearDepth.setDefault(),this.clearStencil.setDefault(),this.colorMask.setDefault(),this.depthMask.setDefault(),this.stencilMask.setDefault(),this.stencilFunc.setDefault(),this.stencilOp.setDefault(),this.stencilTest.setDefault(),this.depthRange.setDefault(),this.depthTest.setDefault(),this.depthFunc.setDefault(),this.blend.setDefault(),this.blendFunc.setDefault(),this.blendColor.setDefault(),this.blendEquation.setDefault(),this.cullFace.setDefault(),this.cullFaceSide.setDefault(),this.frontFace.setDefault(),this.program.setDefault(),this.activeTexture.setDefault(),this.bindFramebuffer.setDefault(),this.pixelStoreUnpack.setDefault(),this.pixelStoreUnpackPremultiplyAlpha.setDefault(),this.pixelStoreUnpackFlipY.setDefault()}setDirty(){this.clearColor.dirty=!0,this.clearDepth.dirty=!0,this.clearStencil.dirty=!0,this.colorMask.dirty=!0,this.depthMask.dirty=!0,this.stencilMask.dirty=!0,this.stencilFunc.dirty=!0,this.stencilOp.dirty=!0,this.stencilTest.dirty=!0,this.depthRange.dirty=!0,this.depthTest.dirty=!0,this.depthFunc.dirty=!0,this.blend.dirty=!0,this.blendFunc.dirty=!0,this.blendColor.dirty=!0,this.blendEquation.dirty=!0,this.cullFace.dirty=!0,this.cullFaceSide.dirty=!0,this.frontFace.dirty=!0,this.program.dirty=!0,this.activeTexture.dirty=!0,this.viewport.dirty=!0,this.bindFramebuffer.dirty=!0,this.bindRenderbuffer.dirty=!0,this.bindTexture.dirty=!0,this.bindVertexBuffer.dirty=!0,this.bindElementBuffer.dirty=!0,this.bindVertexArray.dirty=!0,this.pixelStoreUnpack.dirty=!0,this.pixelStoreUnpackPremultiplyAlpha.dirty=!0,this.pixelStoreUnpackFlipY.dirty=!0}setCustomLayerDefaults(){this.unbindVAO(),this.cullFace.setDefault(),this.activeTexture.setDefault(),this.pixelStoreUnpack.setDefault(),this.pixelStoreUnpackPremultiplyAlpha.setDefault(),this.pixelStoreUnpackFlipY.setDefault()}createIndexBuffer(e,t){return new Iu(this,e,t)}createVertexBuffer(e,t,n){return new Ru(this,e,t,n)}createRenderbuffer(e,t,n){let r=this.gl,i=r.createRenderbuffer();return this.bindRenderbuffer.set(i),r.renderbufferStorage(r.RENDERBUFFER,e,t,n),this.bindRenderbuffer.set(null),i}createFramebuffer(e,t,n,r){return new bd(this,e,t,n,r)}clear({color:e,depth:t,stencil:n}){let r=this.gl,i=0;e&&(i|=r.COLOR_BUFFER_BIT,this.clearColor.set(e),this.colorMask.set([!0,!0,!0,!0])),t!==void 0&&(i|=r.DEPTH_BUFFER_BIT,this.depthRange.set([0,1]),this.clearDepth.set(t),this.depthMask.set(!0)),n!==void 0&&(i|=r.STENCIL_BUFFER_BIT,this.clearStencil.set(n),this.stencilMask.set(255)),r.clear(i)}setCullFace(e){e.enable===!1?this.cullFace.set(!1):(this.cullFace.set(!0),this.cullFaceSide.set(e.mode),this.frontFace.set(e.frontFace))}setDepthMode(e){e.func===this.gl.ALWAYS&&!e.mask?this.depthTest.set(!1):(this.depthTest.set(!0),this.depthFunc.set(e.func),this.depthMask.set(e.mask),this.depthRange.set(e.range))}setStencilMode(e){e.test.func===this.gl.ALWAYS&&!e.mask?this.stencilTest.set(!1):(this.stencilTest.set(!0),this.stencilMask.set(e.mask),this.stencilOp.set([e.fail,e.depthFail,e.pass]),this.stencilFunc.set({func:e.test.func,ref:e.ref,mask:e.test.mask}))}setColorMode(e){Ve(e.blendFunction,xd.Replace)?this.blend.set(!1):(this.blend.set(!0),this.blendFunc.set(e.blendFunction),this.blendColor.set(e.blendColor)),this.colorMask.set(e.mask)}createVertexArray(){return this.gl.createVertexArray()}deleteVertexArray(e){this.gl.deleteVertexArray(e)}unbindVAO(){this.bindVertexArray.set(null)}},Z=class{constructor(e,t,n){this.func=e,this.mask=t,this.range=n}};Z.ReadOnly=!1,Z.ReadWrite=!0,Z.disabled=new Z(519,Z.ReadOnly,[0,1]);const Cd=7680;var Q=class{constructor(e,t,n,r,i,a){this.test=e,this.ref=t,this.mask=n,this.fail=r,this.depthFail=i,this.pass=a}};Q.disabled=new Q({func:519,mask:0},0,0,Cd,Cd,Cd);const wd=1029,Td=2305;var $=class{constructor(e,t,n){this.enable=e,this.mode=t,this.frontFace=n}};$.disabled=new $(!1,wd,Td),$.backCCW=new $(!0,wd,Td),$.frontCCW=new $(!0,1028,Td);let Ed;function Dd(e,t,n,r,i){let a=e.context,o=e.transform,s=a.gl,c=e.useProgram(`collisionBox`),l=[],u=0,d=0;for(let f of r){let r=t.getTile(f).getBucket(n);if(!r)continue;let p=i?r.textCollisionBox:r.iconCollisionBox,m=r.collisionCircleArray;m.length>0&&(l.push({circleArray:m,circleOffset:d,coord:f}),u+=m.length/4,d=u),p&&c.draw(a,s.LINES,Z.disabled,Q.disabled,e.colorModeForRenderPass(),$.disabled,Gl(e.transform),e.style.map.terrain?.getTerrainData(f),o.getProjectionData({overscaledTileID:f,applyGlobeMatrix:!0,applyTerrainMatrix:!0}),n.id,p.layoutVertexBuffer,p.indexBuffer,p.segments,null,e.transform.zoom,null,null,p.collisionVertexBuffer)}if(!i||!l.length)return;let f=e.useProgram(`collisionCircle`),p=new pt;p.resize(u*4),p._trim();let m=0;for(let e of l)for(let t=0;tu.getElevation(i,e,t):void 0;Pd(a,d,f,c,l,g,t,m,_,le(l,e,o,s),i.toUnwrapped(),r,n.layout.get(`symbol-height-anchor`)===`ground`)}}}function Nd(e,t,n,r,i,a){let o=t.tileAnchorPoint.add(new l(t.translation[0],t.translation[1]));if(t.pitchWithMap){let e=r.mult(a);n||(e=e.rotate(-i));let s=o.add(e);return Ko(s.x,s.y,t.pitchedLabelPlaneMatrix,Go(t,s.x,s.y)).point}if(n){let n=ts(t.tileAnchorPoint.x+1,t.tileAnchorPoint.y,t).point.sub(e),i=Math.atan(n.y/n.x)+(n.x<0?Math.PI:0);return e.add(r.rotate(i))}return e.add(r)}function Pd(e,t,n,r,a,o,s,c,u,d,f,p,m){let h=e.text.placedSymbolArray,g=e.text.dynamicLayoutVertexArray,_=e.icon.dynamicLayoutVertexArray,v={};g.clear();for(let _=0;_=0&&(v[y.associatedIconIndex]={shiftedAnchor:k,angle:A})}}if(u){_.clear();let t=e.icon.placedSymbolArray;for(let e=0;ee.style.map.terrain.getElevation(s,t,n):void 0;Yo(c,e,i,ie,t,_,l,n.layout.get(`text-rotation-alignment`)===`map`,s.toUnwrapped(),h.width,h.height,oe,r)}let pe=i&&C||ue,me=_?ie:e.transform.clipSpaceToPixelsMatrix,he=v||pe?kd:me,ge=p&&n.paint.get(i?`text-halo-width`:`icon-halo-width`).constantOr(1)!==0,_e;_e=p?c.iconsInText?Tu(S.kind,ee,y,_,v,pe,e,he,N,oe,k,A,T,de,fe):wu(S.kind,ee,y,_,v,pe,e,he,N,oe,i,k,ge,T,de,fe):Cu(S.kind,ee,y,_,v,pe,e,he,N,oe,i,k,T,de,fe);let ve={program:D,buffers:u,uniformValues:_e,projectionData:se,atlasTexture:j,atlasTextureIcon:te,atlasInterpolation:M,atlasInterpolationIcon:ne,isSDF:p,hasHalo:ge};if(b&&c.canOverlap){x=!0;let e=u.segments.get();for(let t of e)w.push({segments:new ae([t]),sortKey:t.sortKey,state:ve,terrainData:O})}else w.push({segments:u.segments,sortKey:0,state:ve,terrainData:O})}x&&w.sort((e,t)=>e.sortKey-t.sortKey);let E=n.paint.get(i?`text-halo-width`:`icon-halo-width`).constantOr(null)??1/0,D=n.layout.get(`text-letter-spacing`).constantOr(0)*24<0||E>1;for(let t of w){let r=t.state;p.activeTexture.set(m.TEXTURE0),r.atlasTexture.bind(r.atlasInterpolation,m.CLAMP_TO_EDGE),r.atlasTextureIcon&&(p.activeTexture.set(m.TEXTURE1),r.atlasTextureIcon&&r.atlasTextureIcon.bind(r.atlasInterpolationIcon,m.CLAMP_TO_EDGE));let i=r.isSDF&&r.hasHalo;if(i){let i=r.uniformValues;i.u_is_halo=1,D&&(i.u_is_plain=0,Ld(r.buffers,t.segments,n,e,r.program,S,u,d,i,r.projectionData,t.terrainData),i.u_is_halo=0,i.u_is_plain=1)}Ld(r.buffers,t.segments,n,e,r.program,S,u,d,r.uniformValues,r.projectionData,t.terrainData),i&&!D&&(r.uniformValues.u_is_halo=0)}}function Ld(e,t,n,r,i,a,o,s,c,l,u){let d=r.context,f=d.gl;i.draw(d,f.TRIANGLES,a,o,s,$.backCCW,c,u,l,n.id,e.layoutVertexBuffer,e.indexBuffer,t,n.paint,r.transform.zoom,e.programConfigurations.get(n.id),e.dynamicLayoutVertexBuffer,e.opacityVertexBuffer)}function Rd(e,t,n,r,i){if(e.renderPass!==`translucent`)return;let{isRenderingToTexture:a}=i,o=n.paint.get(`circle-opacity`),s=n.paint.get(`circle-stroke-width`),c=n.paint.get(`circle-stroke-opacity`),l=!n.layout.get(`circle-sort-key`).isConstant();if(o.constantOr(1)===0&&(s.constantOr(1)===0||c.constantOr(1)===0))return;let u=e.context,d=u.gl,f=e.transform,p=e.getDepthModeForSublayer(0,Z.ReadOnly),m=Q.disabled,h=e.colorModeForRenderPass(),g=[],_=f.getCircleRadiusCorrection();for(let i of r){let r=t.getTile(i),o=r.getBucket(n);if(!o)continue;let s=n.paint.get(`circle-translate`),c=n.paint.get(`circle-translate-anchor`),u=le(f,r,s,c),d=o.programConfigurations.get(n.id),p=e.useProgram(`circle`,d),m=o.layoutVertexBuffer,h=o.indexBuffer,v=e.style.map.terrain?.getTerrainData(i),y={programConfiguration:d,program:p,layoutVertexBuffer:m,indexBuffer:h,uniformValues:Hl(e,r,n,u,_),terrainData:v,projectionData:f.getProjectionData({overscaledTileID:i,applyGlobeMatrix:!a,applyTerrainMatrix:!0})};if(l){let e=o.segments.get();for(let t of e)g.push({segments:new ae([t]),sortKey:t.sortKey,state:y})}else g.push({segments:o.segments,sortKey:0,state:y})}l&&g.sort((e,t)=>e.sortKey-t.sortKey);for(let t of g){let{programConfiguration:r,program:i,layoutVertexBuffer:a,indexBuffer:o,uniformValues:s,terrainData:c,projectionData:l}=t.state,f=t.segments;i.draw(u,d.TRIANGLES,p,m,h,$.backCCW,s,c,l,n.id,a,o,f,n.paint,e.transform.zoom,r)}}function zd(e,t,n,r,i){if(n.paint.get(`heatmap-opacity`)===0)return;let a=e.context,{isRenderingToTexture:o,isRenderingGlobe:s}=i;if(e.style.map.terrain){for(let i of r){let r=t.getTile(i);t.hasRenderableParent(i)||(e.renderPass===`offscreen`?Hd(e,r,n,i,s):e.renderPass===`translucent`&&Ud(e,n,i,o,s))}a.viewport.set([0,0,e.width,e.height])}else e.renderPass===`offscreen`?Bd(e,t,n,r):e.renderPass===`translucent`&&Vd(e,n)}function Bd(e,t,n,r){let i=e.context,a=i.gl,o=e.transform,s=Q.disabled,c=new xd([a.ONE,a.ONE],R.transparent,[!0,!0,!0,!0]);Wd(i,e,n),i.clear({color:R.transparent});for(let l of r){if(t.hasRenderableParent(l))continue;let r=t.getTile(l),u=r.getBucket(n);if(!u)continue;let d=u.programConfigurations.get(n.id),f=e.useProgram(`heatmap`,d),p=o.getProjectionData({overscaledTileID:l,applyGlobeMatrix:!0,applyTerrainMatrix:!1}),m=o.getCircleRadiusCorrection();f.draw(i,a.TRIANGLES,Z.disabled,s,c,$.backCCW,Zl(r,o.zoom,n.paint.get(`heatmap-intensity`),m),null,p,n.id,u.layoutVertexBuffer,u.indexBuffer,u.segments,n.paint,o.zoom,d)}i.viewport.set([0,0,e.width,e.height])}function Vd(e,t){let n=e.context,r=n.gl;n.setColorMode(e.colorModeForRenderPass());let i=t.heatmapFbos.get(nt);i&&(n.activeTexture.set(r.TEXTURE0),r.bindTexture(r.TEXTURE_2D,i.colorAttachment.get()),n.activeTexture.set(r.TEXTURE1),Kd(n,t).bind(r.LINEAR,r.CLAMP_TO_EDGE),e.useProgram(`heatmapTexture`).draw(n,r.TRIANGLES,Z.disabled,Q.disabled,e.colorModeForRenderPass(),$.disabled,Ql(e,t,0,1),null,null,t.id,e.viewportBuffer,e.quadTriangleIndexBuffer,e.viewportSegments,t.paint,e.transform.zoom))}function Hd(e,t,n,r,i){let a=e.context,o=a.gl,s=Q.disabled,c=new xd([o.ONE,o.ONE],R.transparent,[!0,!0,!0,!0]),l=t.getBucket(n);if(!l)return;let u=r.key,d=n.heatmapFbos.get(u);d||(d=Gd(a,t.tileSize,t.tileSize),n.heatmapFbos.set(u,d)),a.bindFramebuffer.set(d.framebuffer),a.viewport.set([0,0,t.tileSize,t.tileSize]),a.clear({color:R.transparent});let f=l.programConfigurations.get(n.id),p=e.useProgram(`heatmap`,f,!i),m=e.transform.getProjectionData({overscaledTileID:t.tileID,applyGlobeMatrix:!0,applyTerrainMatrix:!0}),h=e.style.map.terrain.getTerrainData(r);p.draw(a,o.TRIANGLES,Z.disabled,s,c,$.disabled,Zl(t,e.transform.zoom,n.paint.get(`heatmap-intensity`),1),h,m,n.id,l.layoutVertexBuffer,l.indexBuffer,l.segments,n.paint,e.transform.zoom,f)}function Ud(e,t,n,r,i){let a=e.context,o=a.gl,s=e.transform;a.setColorMode(e.colorModeForRenderPass());let c=Kd(a,t),l=n.key,u=t.heatmapFbos.get(l);if(!u)return;a.activeTexture.set(o.TEXTURE0),o.bindTexture(o.TEXTURE_2D,u.colorAttachment.get()),a.activeTexture.set(o.TEXTURE1),c.bind(o.LINEAR,o.CLAMP_TO_EDGE);let d=s.getProjectionData({overscaledTileID:n,applyTerrainMatrix:i,applyGlobeMatrix:!r});e.useProgram(`heatmapTexture`).draw(a,o.TRIANGLES,Z.disabled,Q.disabled,e.colorModeForRenderPass(),$.disabled,Ql(e,t,0,1),null,d,t.id,e.rasterBoundsBuffer,e.quadTriangleIndexBuffer,e.rasterBoundsSegments,t.paint,s.zoom),u.destroy(),t.heatmapFbos.delete(l)}function Wd(e,t,n){let r=e.gl;e.activeTexture.set(r.TEXTURE1),e.viewport.set([0,0,t.width/4,t.height/4]);let i=n.heatmapFbos.get(nt);i?(r.bindTexture(r.TEXTURE_2D,i.colorAttachment.get()),e.bindFramebuffer.set(i.framebuffer)):(i=Gd(e,t.width/4,t.height/4),n.heatmapFbos.set(nt,i))}function Gd(e,t,n){let r=e.gl,i=r.createTexture();r.bindTexture(r.TEXTURE_2D,i),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_S,r.CLAMP_TO_EDGE),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_T,r.CLAMP_TO_EDGE),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MIN_FILTER,r.LINEAR),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MAG_FILTER,r.LINEAR),r.texStorage2D(r.TEXTURE_2D,1,r.RGBA16F,t,n);let a=e.createFramebuffer(t,n,!1,!1);return a.colorAttachment.set(i),a}function Kd(e,t){return t.colorRampTexture||=new _(e,t.colorRamp,e.gl.RGBA),t.colorRampTexture}function qd(e,t,n,r){let i=e.context,a=i.bindFramebuffer.get(),o=i.viewport.get(),[,,s,c]=o;return Jd(e,s,c),i.viewport.set([0,0,s,c]),i.clear({color:R.transparent,depth:1,stencil:0}),e.currentStencilSource=void 0,e.renderTileClippingMasks(t,n,r),{compositeTarget:a,compositeViewport:o}}function Jd(e,t,n){let r=e.context.gl;if(!e.layerOpacityFbo){let i=e.context.createFramebuffer(t,n,!0,!0),a=r.createTexture();r.bindTexture(r.TEXTURE_2D,a),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_S,r.CLAMP_TO_EDGE),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_WRAP_T,r.CLAMP_TO_EDGE),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MIN_FILTER,r.LINEAR),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MAG_FILTER,r.LINEAR),r.texImage2D(r.TEXTURE_2D,0,r.RGBA,t,n,0,r.RGBA,r.UNSIGNED_BYTE,null),i.colorAttachment.set(a),i.depthAttachment.set(e.context.createRenderbuffer(r.DEPTH_STENCIL,t,n)),e.layerOpacityFbo=i,e.context.bindFramebuffer.set(e.layerOpacityFbo.framebuffer);return}if(e.layerOpacityFbo.width===t&&e.layerOpacityFbo.height===n){e.context.bindFramebuffer.set(e.layerOpacityFbo.framebuffer);return}let i=e.layerOpacityFbo;r.bindTexture(r.TEXTURE_2D,i.colorAttachment.get()),r.texImage2D(r.TEXTURE_2D,0,r.RGBA,t,n,0,r.RGBA,r.UNSIGNED_BYTE,null),e.context.bindRenderbuffer.set(i.depthAttachment.get()),r.renderbufferStorage(r.RENDERBUFFER,r.DEPTH_STENCIL,t,n),e.context.bindRenderbuffer.set(null),i.width=t,i.height=n,e.context.bindFramebuffer.set(i.framebuffer)}function Yd(e,t,n,r){let i=e.context,a=i.gl;i.bindFramebuffer.set(n.compositeTarget),i.viewport.set(n.compositeViewport),i.activeTexture.set(a.TEXTURE0),a.bindTexture(a.TEXTURE_2D,e.layerOpacityFbo.colorAttachment.get()),e.useProgram(`layerOpacity`).draw(i,a.TRIANGLES,Z.disabled,Q.disabled,e.colorModeForRenderPass(),$.disabled,yu(t,0),null,null,r.id,e.viewportBuffer,e.quadTriangleIndexBuffer,e.viewportSegments,r.paint,e.transform.zoom),e.currentStencilSource=void 0}function Xd(e,t,n,r,i,a,o,s){let c=256;if(i.stepInterpolant){let r=t.getSource().maxzoom,i=o.canonical.z===r?Math.ceil(1<e.options.anisotropicFilterPitch&&m.texParameterf(m.TEXTURE_2D,p.extTextureFilterAnisotropic.TEXTURE_MAX_ANISOTROPY_EXT,p.extTextureFilterAnisotropicMax);let k=e.getTerrainDataForTile(w,u),A=g.getProjectionData({overscaledTileID:w,aligned:y,applyGlobeMatrix:!u,applyTerrainMatrix:!0}),j=za(ee,D,O.fadeMix,n,s,c),M=d??_.getMeshFromTileID(p,w.canonical,a,o,`raster`),te=i?i[w.overscaledZ]:Q.disabled;h.draw(p,m.TRIANGLES,r,te,v,l?$.frontCCW:$.backCCW,j,k,A,n.id,M.vertexBuffer,M.indexBuffer,M.segments)}}function bf(e,t,n,r){let i={parentTile:null,parentScaleBy:1,parentTopLeft:[0,0],fadeValues:{tileOpacity:1,parentTileOpacity:1,fadeMix:{opacity:1,mix:0}}};if(n===0||r)return i;if(e.fadingParentID){let r=t.getLoadedTile(e.fadingParentID);if(!r)return i;let a=2**(r.tileID.overscaledZ-e.tileID.overscaledZ);return{parentTile:r,parentScaleBy:a,parentTopLeft:[e.tileID.canonical.x*a%1,e.tileID.canonical.y*a%1],fadeValues:xf(e,r,n)}}return e.selfFading?{parentTile:null,parentScaleBy:1,parentTopLeft:[0,0],fadeValues:Sf(e,n)}:i}function xf(e,t,n){let r=U(),i=(r-e.timeAdded)/n,a=(r-t.timeAdded)/n,o=e.fadingDirection===1,s=M(i,0,1),c=M(1-a,0,1),l=o?s:c;return{tileOpacity:l,parentTileOpacity:o?c:s,fadeMix:{opacity:1,mix:1-l}}}function Sf(e,t){let n=(U()-e.timeAdded)/t,r=M(n,0,1);return{tileOpacity:r,fadeMix:{opacity:r,mix:0}}}function Cf(e,t,n,r,i){let a=n.paint.get(`background-color`),o=n.paint.get(`background-opacity`);if(o===0)return;let{isRenderingToTexture:s}=i,c=e.context,l=c.gl,u=e.style.projection,d=e.transform,f=d.tileSize,p=n.paint.get(`background-pattern`);if(e.isPatternMissing(p))return;let m=!p&&a.a===1&&o===1&&e.opaquePassEnabledForLayer()?`opaque`:`translucent`;if(e.renderPass!==m)return;let h=Q.disabled,g=e.getDepthModeForSublayer(0,m===`opaque`?Z.ReadWrite:Z.ReadOnly),_=e.colorModeForRenderPass(),v=e.useProgram(p?`backgroundPattern`:`background`),y=r||So(d,{tileSize:f,terrain:e.style.map.terrain});p&&(c.activeTexture.set(l.TEXTURE0),e.patternAtlas.bind(e.context));let b=n.getCrossfadeParameters();for(let t of y){let r=d.getProjectionData({overscaledTileID:t,applyGlobeMatrix:!s,applyTerrainMatrix:!0}),i=p?ku(o,e,p,{tileID:t,tileSize:f},b):Ou(o,a),m=e.getTerrainDataForTile(t,s),y=u.getMeshFromTileID(c,t.canonical,!1,!0,`raster`);v.draw(c,l.TRIANGLES,g,h,_,$.backCCW,i,m,r,n.id,y.vertexBuffer,y.indexBuffer,y.segments)}}const wf=new R(1,0,0,1),Tf=new R(0,1,0,1),Ef=new R(0,0,1,1),Df=new R(1,0,1,1),Of=new R(0,1,1,1);function kf(e){let t=e.transform.padding;jf(e,e.transform.height-(t.top||0),3,wf),jf(e,t.bottom||0,3,Tf),Mf(e,t.left||0,3,Ef),Mf(e,e.transform.width-(t.right||0),3,Df);let n=e.transform.centerPoint;Af(e,n.x,e.transform.height-n.y,Of)}function Af(e,t,n,r){Nf(e,t-1,n-10,2,20,r),Nf(e,t-10,n-1,20,2,r)}function jf(e,t,n,r){Nf(e,0,t+n/2,e.transform.width,n,r)}function Mf(e,t,n,r){Nf(e,t-n/2,0,n,e.transform.height,r)}function Nf(e,t,n,r,i,a){let o=e.context,s=o.gl;s.enable(s.SCISSOR_TEST),s.scissor(t*e.pixelRatio,n*e.pixelRatio,r*e.pixelRatio,i*e.pixelRatio),o.clear({color:a}),s.disable(s.SCISSOR_TEST)}function Pf(e,t,n){for(let r of n)Ff(e,t,r)}function Ff(e,t,n){let r=e.context,i=r.gl,a=e.useProgram(`debug`),o=Z.disabled,s=Q.disabled,c=e.colorModeForRenderPass(),l=`$debug`,u=e.style.map.terrain?.getTerrainData(n);r.activeTexture.set(i.TEXTURE0);let d=t.getTileByID(n.key).latestRawTileData?.byteLength||0,f=Math.floor(d/1024),p=t.getTile(n).tileSize,m=512/Math.min(p,512)*(n.overscaledZ/e.transform.zoom)*.5,h=n.canonical.toString();n.overscaledZ!==n.canonical.z&&(h+=` => ${n.overscaledZ}`),If(e,`${h} ${f}kB`);let g=e.transform.getProjectionData({overscaledTileID:n,applyGlobeMatrix:!0,applyTerrainMatrix:!0});a.draw(r,i.TRIANGLES,o,s,xd.alphaBlended,$.disabled,Jl(R.transparent,m),null,g,l,e.debugBuffer,e.quadTriangleIndexBuffer,e.debugSegments),a.draw(r,i.LINE_STRIP,o,s,c,$.disabled,Jl(R.red),u,g,l,e.debugBuffer,e.tileBorderIndexBuffer,e.debugSegments)}function If(e,t){e.initDebugOverlayCanvas();let n=e.debugOverlayCanvas,r=e.context.gl,i=e.debugOverlayCanvas.getContext(`2d`);i.clearRect(0,0,n.width,n.height),i.shadowColor=`white`,i.shadowBlur=2,i.lineWidth=1.5,i.strokeStyle=`white`,i.textBaseline=`top`,i.font=`bold 36px Open Sans, sans-serif`,i.fillText(t,5,5),i.strokeText(t,5,5),e.debugOverlayTexture.update(n),e.debugOverlayTexture.bind(r.LINEAR,r.CLAMP_TO_EDGE)}function Lf(e,t){let n=null,r=Object.values(e._layers).flatMap(n=>n.source&&!n.isHidden(t)?[e.tileManagers[n.source]]:[]),i=r.filter(e=>e.getSource().type===`vector`),a=r.filter(e=>e.getSource().type!==`vector`),o=e=>{(!n||n.getSource().maxzoomc.getProjectionData({overscaledTileID:new $t(e.tileID.canonical.z,e.tileID.wrap??0,e.tileID.canonical.z,e.tileID.canonical.x,e.tileID.canonical.y),aligned:e.aligned,applyGlobeMatrix:e.applyGlobeMatrix,applyTerrainMatrix:e.applyTerrainMatrix})},d=o.renderingMode?o.renderingMode:`2d`;if(e.renderPass===`offscreen`){let t=o.prerender;t&&(e.setCustomLayerDefaults(),a.setColorMode(e.colorModeForRenderPass()),t.call(o,a.gl,u),a.setDirty(),e.setBaseState())}else if(e.renderPass===`translucent`){e.setCustomLayerDefaults(),a.setColorMode(e.colorModeForRenderPass()),a.setStencilMode(Q.disabled);let t=d===`3d`?e.getDepthModeFor3D():e.getDepthModeForSublayer(0,Z.ReadOnly);a.setDepthMode(t),o.render(a.gl,u),a.setDirty(),e.setBaseState(),a.bindFramebuffer.set(null)}}function zf(e,t){let n=e.context,r=n.gl,i=e.transform,a=xd.unblended,o=new Z(r.LEQUAL,Z.ReadWrite,[0,1]),s=t.tileManager.getRenderableTiles(),c=e.useProgram(`terrainDepth`);n.bindFramebuffer.set(t.getFramebuffer().framebuffer),n.viewport.set([0,0,e.width/devicePixelRatio,e.height/devicePixelRatio]),n.clear({color:R.white,depth:1});for(let e of s){let s=t.getTerrainMesh(e.tileID),l=t.getTerrainData(e.tileID),u=i.getProjectionData({overscaledTileID:e.tileID,applyTerrainMatrix:!1,applyGlobeMatrix:!0}),d=xl(t.getSkirtLength(i.zoom));c.draw(n,r.TRIANGLES,o,Q.disabled,a,$.backCCW,d,l,u,`terrain`,s.vertexBuffer,s.indexBuffer,s.segments)}n.bindFramebuffer.set(null),n.viewport.set([0,0,e.width,e.height])}function Bf(e,t,n,r){let{isRenderingGlobe:i}=r,a=e.context,o=a.gl,s=e.transform,c=e.colorModeForRenderPass(),l=e.getDepthModeFor3D(),u=e.useProgram(`terrain`);a.bindFramebuffer.set(null),a.viewport.set([0,0,e.width,e.height]);for(let r of n){let n=t.getTerrainMesh(r.tileID),d=e.renderToTexture.getTexture(r),f=t.getTerrainData(r.tileID);a.activeTexture.set(o.TEXTURE0),o.bindTexture(o.TEXTURE_2D,d.texture);let p=t.getSkirtLength(s.zoom),m=s.calculateFogMatrix(r.tileID.toUnwrapped()),h=bl(p,m,e.style.sky,s.pitch,i),g=s.getProjectionData({overscaledTileID:r.tileID,applyTerrainMatrix:!1,applyGlobeMatrix:!0});u.draw(a,o.TRIANGLES,l,Q.disabled,c,$.backCCW,h,f,g,`terrain`,n.vertexBuffer,n.indexBuffer,n.segments)}}function Vf(e,t){if(!t.mesh){let n=new Un;n.emplaceBack(-1,-1),n.emplaceBack(1,-1),n.emplaceBack(1,1),n.emplaceBack(-1,1);let r=new gt;r.emplaceBack(0,1,2),r.emplaceBack(0,2,3),t.mesh=new Ua(e.createVertexBuffer(n,Wa.members),e.createIndexBuffer(r),ae.simpleSegment(0,0,n.length,r.length))}return t.mesh}function Hf(e,t){let n=e.context,r=n.gl,i=Nu(t,e.transform,e.pixelRatio),a=new Z(r.LEQUAL,Z.ReadWrite,[0,1]),o=Q.disabled,s=e.colorModeForRenderPass(),c=e.useProgram(`sky`),l=Vf(n,t);c.draw(n,r.TRIANGLES,a,o,s,$.disabled,i,null,void 0,`sky`,l.vertexBuffer,l.indexBuffer,l.segments)}function Uf(e,t){let n=e.getCartesianPosition();Yn(n,n);let r=$e(new Float64Array(16));return e.properties.get(`anchor`)===`map`&&(we(r,r,t.rollInRadians),a(r,r,-t.pitchInRadians),we(r,r,t.bearingInRadians),a(r,r,t.center.lat*Math.PI/180),pn(r,r,-t.center.lng*Math.PI/180)),en(n,n,r),n}function Wf(e,t,n){let r=e.context,i=r.gl,a=e.useProgram(`atmosphere`),o=new Z(i.LEQUAL,Z.ReadOnly,[0,1]),s=e.transform,c=Uf(n,e.transform),l=s.getProjectionData({overscaledTileID:null,applyGlobeMatrix:!0,applyTerrainMatrix:!0}),u=t.properties.get(`atmosphere-blend`)*l.projectionTransition;if(u===0)return;let d=Lc(s.worldSize,s.center.lat),f=s.inverseProjectionMatrix,p=new Float64Array(4);p[3]=1,Gt(p,p,s.modelViewProjectionMatrix),p[0]/=p[3],p[1]/=p[3],p[2]/=p[3],p[3]=1,Gt(p,p,f),p[0]/=p[3],p[1]/=p[3],p[2]/=p[3],p[3]=1;let m=[p[0],p[1],p[2]],h=ju(c,u,m,d,f),g=Vf(r,t);a.draw(r,i.TRIANGLES,o,Q.disabled,xd.alphaBlended,$.disabled,h,null,null,`atmosphere`,g.vertexBuffer,g.indexBuffer,g.segments)}const Gf={symbol:Ad,circle:Rd,heatmap:zd,line:tf,fill:af,fillExtrusion:lf,hillshade:df,colorRelief:mf,raster:vf,background:Cf,sky:Hf,atmosphere:Wf,custom:Rf,debug:Pf,debugPadding:kf,terrainDepth:zf};var Kf=class e{constructor(e,t){this.drawFunctions=Gf,this.context=new Sd(e),this.transform=t,this.layerOpacityFbo=null,this._tileTextures={},this._rttObjectRecyclePool=[],this._rttSharedFbo=null,this.terrainFacilitator={depthDirty:!0,matrix:$e(new Float64Array(16)),renderTime:0},this.setup(),this.numSublayers=No.maxOverzooming+No.maxUnderzooming+1,this.depthEpsilon=1/2**16,this.crossTileSymbolIndex=new Bs}resize(e,t,n){if(this.width=Math.floor(e*n),this.height=Math.floor(t*n),this.pixelRatio=n,this.context.viewport.set([0,0,this.width,this.height]),this.style)for(let e of this.style._order)this.style._layers[e].resize()}setup(){let e=this.context,t=new Un;t.emplaceBack(0,0),t.emplaceBack(N,0),t.emplaceBack(0,N),t.emplaceBack(N,N),this.tileExtentBuffer=e.createVertexBuffer(t,Wa.members),this.tileExtentSegments=ae.simpleSegment(0,0,4,2);let n=new Un;n.emplaceBack(0,0),n.emplaceBack(N,0),n.emplaceBack(0,N),n.emplaceBack(N,N),this.debugBuffer=e.createVertexBuffer(n,Wa.members),this.debugSegments=ae.simpleSegment(0,0,4,5);let r=new bt;r.emplaceBack(0,0,0,0),r.emplaceBack(N,0,N,0),r.emplaceBack(0,N,0,N),r.emplaceBack(N,N,N,N),this.rasterBoundsBuffer=e.createVertexBuffer(r,hl.members),this.rasterBoundsSegments=ae.simpleSegment(0,0,4,2);let i=new Un;i.emplaceBack(0,0),i.emplaceBack(N,0),i.emplaceBack(0,N),i.emplaceBack(N,N),this.rasterBoundsBufferPosOnly=e.createVertexBuffer(i,Wa.members),this.rasterBoundsSegmentsPosOnly=ae.simpleSegment(0,0,4,5);let a=new Un;a.emplaceBack(0,0),a.emplaceBack(1,0),a.emplaceBack(0,1),a.emplaceBack(1,1),this.viewportBuffer=e.createVertexBuffer(a,Wa.members),this.viewportSegments=ae.simpleSegment(0,0,4,2);let o=new ne;o.emplaceBack(0),o.emplaceBack(1),o.emplaceBack(3),o.emplaceBack(2),o.emplaceBack(0),this.tileBorderIndexBuffer=e.createIndexBuffer(o);let s=new gt;s.emplaceBack(1,0,2),s.emplaceBack(1,2,3),this.quadTriangleIndexBuffer=e.createIndexBuffer(s);let c=this.context.gl;this.stencilClearMode=new Q({func:c.ALWAYS,mask:0},0,255,c.ZERO,c.ZERO,c.ZERO),this.tileExtentMesh=new Ua(this.tileExtentBuffer,this.quadTriangleIndexBuffer,this.tileExtentSegments)}clearStencil(){let e=this.context,t=e.gl;this.nextStencilID=1,this.currentStencilSource=void 0;let n=vr();Ne(n,0,this.width,this.height,0,0,1),ke(n,n,[t.drawingBufferWidth,t.drawingBufferHeight,0]);let r={mainMatrix:n,tileMercatorCoords:[0,0,1,1],clippingPlane:[0,0,0,0],projectionTransition:0,fallbackMatrix:n,clipAntimeridian:!1};this.useProgram(`clippingMask`,null,!0).draw(e,t.TRIANGLES,Z.disabled,this.stencilClearMode,xd.disabled,$.disabled,null,null,r,`$clipping`,this.viewportBuffer,this.quadTriangleIndexBuffer,this.viewportSegments)}renderTileClippingMasks(e,t,n){if(this.currentStencilSource===e.source||!e.isTileClipped()||!t?.length)return;this.currentStencilSource=e.source,this.nextStencilID+t.length>256&&this.clearStencil();let r=this.context;r.setColorMode(xd.disabled),r.setDepthMode(Z.disabled);let i={};for(let e of t)i[e.key]=this.nextStencilID++;this.style.projection.useSubdivision&&this._renderTileMasks(i,t,n,!0),this._renderTileMasks(i,t,n,!1),this._tileClippingMaskIDs=i}_renderTileMasks(e,t,n,r){let i=this.context,a=i.gl,o=this.style.projection,s=this.transform,c=this.useProgram(`clippingMask`);for(let l of t){let t=e[l.key],u=this.getTerrainDataForTile(l,n),d=o.getMeshFromTileID(this.context,l.canonical,r,!0,`stencil`),f=s.getProjectionData({overscaledTileID:l,applyGlobeMatrix:!n,applyTerrainMatrix:!0});c.draw(i,a.TRIANGLES,Z.disabled,new Q({func:a.ALWAYS,mask:0},t,255,a.KEEP,a.KEEP,a.REPLACE),xd.disabled,n?$.disabled:$.backCCW,null,u,f,`$clipping`,d.vertexBuffer,d.indexBuffer,d.segments)}}getTerrainDataForTile(e,t){return t&&this.style.projection?.name===`mercator`?null:this.style.map.terrain?.getTerrainData(e)||null}_renderTilesDepthBuffer(){let e=this.context,t=e.gl,n=this.style.projection,r=this.transform,i=this.useProgram(`depth`),a=this.getDepthModeFor3D(),o=So(r,{tileSize:r.tileSize});for(let s of o){let o=this.style.map.terrain?.getTerrainData(s),c=n.getMeshFromTileID(this.context,s.canonical,!0,!0,`raster`),l=r.getProjectionData({overscaledTileID:s,applyGlobeMatrix:!0,applyTerrainMatrix:!0});i.draw(e,t.TRIANGLES,a,Q.disabled,xd.disabled,$.backCCW,null,o,l,`$clipping`,c.vertexBuffer,c.indexBuffer,c.segments)}}stencilModeFor3D(){this.currentStencilSource=void 0,this.nextStencilID+1>256&&this.clearStencil();let e=this.nextStencilID++,t=this.context.gl;return new Q({func:t.NOTEQUAL,mask:255},e,255,t.KEEP,t.KEEP,t.REPLACE)}stencilModeForClipping(e){let t=this.context.gl;return new Q({func:t.EQUAL,mask:255},this._tileClippingMaskIDs[e.key],0,t.KEEP,t.KEEP,t.REPLACE)}getStencilConfigForOverlapAndUpdateStencilID(e){let t=this.context.gl,n=e.sort((e,t)=>t.overscaledZ-e.overscaledZ),r=n[n.length-1].overscaledZ,i=n[0].overscaledZ-r+1;if(i>1){this.currentStencilSource=void 0,this.nextStencilID+i>256&&this.clearStencil();let e={};for(let n=0;nt.overscaledZ-e.overscaledZ),r=n[n.length-1].overscaledZ,i=n[0].overscaledZ-r+1;if(this.clearStencil(),i>1){let e={},a={};for(let n=0;n0};for(let e in r){let t=r[e];t.used&&t.prepare(this.context),i[e]=t.getVisibleCoordinates(!1),a[e]=i[e].slice().reverse(),o[e]=t.getVisibleCoordinates(!0).reverse()}this.opaquePassCutoff=1/0;for(let e=0;e=0;this.currentLayer--){let e=this.style._layers[n[this.currentLayer]],t=r[e.source],a=i[e.source];this.renderTileClippingMasks(e,a,!1),this.renderLayer(this,t,e,a,s)}this.renderPass=`translucent`;let c=!1;for(this.currentLayer=0;this.currentLayer0?t.pop():null}acquireRTT(e){let t=this.context.gl,n=this._rttObjectRecyclePool.pop();if(n)return n.size!==e&&(t.bindTexture(t.TEXTURE_2D,n.texture.texture),t.texImage2D(t.TEXTURE_2D,0,t.RGBA,e,e,0,t.RGBA,t.UNSIGNED_BYTE,null),n.texture.size=[e,e],n.size=e),n;let r=new _(this.context,{width:e,height:e,data:null},t.RGBA);return r.bind(t.LINEAR,t.CLAMP_TO_EDGE),this.context.extTextureFilterAnisotropic&&t.texParameterf(t.TEXTURE_2D,this.context.extTextureFilterAnisotropic.TEXTURE_MAX_ANISOTROPY_EXT,this.context.extTextureFilterAnisotropicMax),{texture:r,size:e}}bindRTT(e){let t=this.context.gl,n=e.size;if(!this._rttSharedFbo){let e=this.context.createFramebuffer(n,n,!0,!0),r=this.context.createRenderbuffer(t.DEPTH_STENCIL,n,n);e.depthAttachment.set(r),this._rttSharedFbo={fbo:e,depthRenderbuffer:r,size:n}}this._rttSharedFbo.size!==n&&(this.context.bindRenderbuffer.set(this._rttSharedFbo.depthRenderbuffer),t.renderbufferStorage(t.RENDERBUFFER,t.DEPTH_STENCIL,n,n),this.context.bindRenderbuffer.set(null),this._rttSharedFbo.fbo.width=n,this._rttSharedFbo.fbo.height=n,this._rttSharedFbo.size=n),this._rttSharedFbo.fbo.colorAttachment.set(e.texture.texture),this.context.bindFramebuffer.set(this._rttSharedFbo.fbo.framebuffer)}releaseRTT(e){this._rttObjectRecyclePool.push(e)}isPatternMissing(e){if(!e)return!1;if(!e.from||!e.to)return!0;let t=this.patternAtlas.getPattern(e.from.toString()),n=this.patternAtlas.getPattern(e.to.toString());return!t||!n}useProgram(e,t,n=!1,r=[]){this.cache||={};let i=!!this.style.map.terrain,a=this.style.projection,o=n?Xs.projectionMercator:a.shaderPreludeCode,s=n?Zs:a.shaderDefine,c=`/${n?Qs:a.shaderVariantName}`,l=t?t.cacheKey:``,u=this._showOverdrawInspector?`/overdraw`:``,d=i?`/terrain`:``,f=r?`/${r.join(`/`)}`:``,p=e+l+c+u+d+f;return this.cache[p]||=new El(this.context,Xs[e],t,Fu[e],this._showOverdrawInspector,i,o,s,r),this.cache[p]}setCustomLayerDefaults(){this.context.setCustomLayerDefaults()}setBaseState(){let e=this.context.gl;this.context.cullFace.set(!1),this.context.viewport.set([0,0,this.width,this.height]),this.context.blendEquation.set(e.FUNC_ADD)}initDebugOverlayCanvas(){if(this.debugOverlayCanvas==null){this.debugOverlayCanvas=document.createElement(`canvas`),this.debugOverlayCanvas.width=512,this.debugOverlayCanvas.height=512;let e=this.context.gl;this.debugOverlayTexture=new _(this.context,this.debugOverlayCanvas,e.RGBA)}}destroy(){if(this._tileTextures){for(let e in this._tileTextures){let t=this._tileTextures[e];if(t)for(let e of t)e.destroy()}this._tileTextures={}}for(let e of this._rttObjectRecyclePool)e.texture.destroy();if(this._rttObjectRecyclePool=[],this._rttSharedFbo){this._rttSharedFbo.fbo.colorAttachment.set(null),this._rttSharedFbo.fbo.depthAttachment.set(null);let e=this.context.gl;e.deleteRenderbuffer(this._rttSharedFbo.depthRenderbuffer),e.deleteFramebuffer(this._rttSharedFbo.fbo.framebuffer),this._rttSharedFbo=null}if(this.layerOpacityFbo?.destroy(),this.layerOpacityFbo=null,this.tileExtentBuffer&&this.tileExtentBuffer.destroy(),this.debugBuffer&&this.debugBuffer.destroy(),this.rasterBoundsBuffer&&this.rasterBoundsBuffer.destroy(),this.rasterBoundsBufferPosOnly&&this.rasterBoundsBufferPosOnly.destroy(),this.viewportBuffer&&this.viewportBuffer.destroy(),this.tileBorderIndexBuffer&&this.tileBorderIndexBuffer.destroy(),this.quadTriangleIndexBuffer&&this.quadTriangleIndexBuffer.destroy(),this.tileExtentMesh&&this.tileExtentMesh.vertexBuffer?.destroy(),this.tileExtentMesh&&this.tileExtentMesh.indexBuffer?.destroy(),this.debugOverlayTexture&&this.debugOverlayTexture.destroy(),this.cache){for(let e in this.cache){let t=this.cache[e];t?.program&&this.context.gl.deleteProgram(t.program)}this.cache={}}this.context&&this.context.setDefault()}overLimit(){let{drawingBufferWidth:e,drawingBufferHeight:t}=this.context.gl;return this.width!==e||this.height!==t}},qf=class extends Error{constructor(e,t){super(`WebGL2 is required to display this map. We are sorry, but it seems that your browser does not support WebGL2, a technology for rendering 3D graphics on the web. Read more on https://wiki.openstreetmap.org/wiki/This_map_requires_WebGL`),this.name=`GPUInitializationError`,this.requestedAttributes=e,this.statusMessage=t?.statusMessage??null}};function Jf(e,t){let n=!1,r=null,i,a=()=>{r=null,n&&=(e(...i),r=setTimeout(a,t),!1)};return(...e)=>(n=!0,i=e,r||a(),r)}var Yf=class{constructor(e){this._getHashParams=()=>new URLSearchParams(window.location.hash.replace(`#`,``)),this._getCurrentHash=()=>{let e=this._getHashParams();return this._hashName?(e.get(this._hashName)||``).split(`/`):([...e.keys()][0]??``).split(`/`)},this._onHashChange=()=>{let e=this._getCurrentHash();if(!this._isValidHash(e))return!1;let t=this._map.dragRotate.isEnabled()&&this._map.touchZoomRotate.isEnabled()?+(e[3]||0):this._map.getBearing();return this._map.jumpTo({center:[+e[2],+e[1]],zoom:+e[0],bearing:t,pitch:+(e[4]||0)}),!0},this._updateHashUnthrottled=()=>{let e=window.location.href.replace(/(#.*)?$/,this.getHashString());window.history.replaceState(window.history.state,null,e)},this._removeHash=()=>{let e=this._getHashParams();if(this._hashName)e.delete(this._hashName);else{let t=Array.from(e.keys());t.length>0&&e.delete(t[0])}let t=decodeURIComponent(e.toString()).replace(/=&/g,`&`).replace(/=$/g,``),n=t?`#${t}`:``,r=window.location.href.replace(/(#.+)?$/,n);r=r.replace(`&&`,`&`),window.history.replaceState(window.history.state,null,r)},this._updateHash=Jf(this._updateHashUnthrottled,300),this._hashName=e&&encodeURIComponent(e)}addTo(e){return this._map=e,addEventListener(`hashchange`,this._onHashChange,!1),this._map.on(`moveend`,this._updateHash),this}remove(){return removeEventListener(`hashchange`,this._onHashChange,!1),this._map.off(`moveend`,this._updateHash),clearTimeout(this._updateHash()),this._removeHash(),delete this._map,this}getHashString(e){let t=this._map.getCenter(),n=Math.round(this._map.getZoom()*100)/100,r=10**Math.ceil((n*Math.LN2+Math.log(512/360/.5))/Math.LN10),i=Math.round(t.lng*r)/r,a=Math.round(t.lat*r)/r,o=this._map.getBearing(),s=this._map.getPitch(),c=``;if(c+=e?`/${i}/${a}/${n}`:`${n}/${a}/${i}`,(o||s)&&(c+=`/${Math.round(o*10)/10}`),s&&(c+=`/${Math.round(s)}`),this._hashName){let e=this._getHashParams();return e.set(this._hashName,c),`#${decodeURIComponent(e.toString()).replace(/=&/g,`&`).replace(/=$/g,``)}`}return`#${c}`}_isValidHash(e){if(e.length<3||e.some(e=>isNaN(+e)))return!1;try{new V(+e[2],+e[1])}catch{return!1}let t=+e[0],n=+(e[3]||0),r=+(e[4]||0);return t>=this._map.getMinZoom()&&t<=this._map.getMaxZoom()&&n>=-180&&n<=180&&r>=this._map.getMinPitch()&&r<=this._map.getMaxPitch()}};const Xf={linearity:.3,easing:dt(0,0,.3,1)},Zf=z({deceleration:2500,maxSpeed:1400},Xf),Qf=z({deceleration:20,maxSpeed:1400},Xf),$f=z({deceleration:1e3,maxSpeed:360},Xf),ep=z({deceleration:1e3,maxSpeed:90},Xf),tp=z({deceleration:1e3,maxSpeed:360},Xf);var np=class{constructor(e){this._map=e,this.clear()}clear(){this._inertiaBuffer=[]}record(e){this._drainInertiaBuffer(),this._inertiaBuffer.push({time:U(),settings:e})}_drainInertiaBuffer(){let e=this._inertiaBuffer,t=U();for(;e.length>0&&t-e[0].time>160;)e.shift()}_getVelocityEntries(){let e=this._inertiaBuffer,t=U()-60,n=Math.max(0,e.length-2);for(;n>0&&e[n-1].time>=t;)n--;return e.slice(n)}_onMoveEnd(e){this._drainInertiaBuffer();let t=this._getVelocityEntries();if(t.length<2){this.clear();return}let n={zoom:0,bearing:0,pitch:0,roll:0,pan:new l(0,0),pinchAround:void 0,around:void 0};for(let{settings:e}of t)e.around&&(n.around=e.around),e.pinchAround&&(n.pinchAround=e.pinchAround);for(let{settings:e}of t.slice(1))n.zoom+=e.zoomDelta||0,n.bearing+=e.bearingDelta||0,n.pitch+=e.pitchDelta||0,n.roll+=e.rollDelta||0,e.panDelta&&n.pan._add(e.panDelta);if(!n.pan.mag()&&!n.zoom&&!n.bearing&&!n.pitch&&!n.roll){this.clear();return}let r=U()-t[0].time,i={};if(n.pan.mag()){let t=ip(n.pan.mag(),r,z({},Zf,e||{})),a=n.pan.mult(t.amount/n.pan.mag()),o=this._map._camera.cameraHelper.handlePanInertia(a,this._map._camera.transform);i.center=o.easingCenter,i.offset=o.easingOffset,rp(i,t)}if(n.zoom){let e=ip(n.zoom,r,Qf);i.zoom=un(this._map.getZoom()+e.amount,this._map.getZoomSnap(),e.amount),rp(i,e)}if(n.bearing){let e=ip(n.bearing,r,$f);i.bearing=this._map.getBearing()+M(e.amount,-179,179),rp(i,e)}if(n.pitch){let e=ip(n.pitch,r,ep);i.pitch=this._map.getPitch()+e.amount,rp(i,e)}if(n.roll){let e=ip(n.roll,r,tp);i.roll=this._map.getRoll()+M(e.amount,-179,179),rp(i,e)}if(i.zoom||i.bearing){let e=n.pinchAround===void 0?n.around:n.pinchAround;i.around=e?this._map.unproject(e):this._map.getCenter()}return this.clear(),z(i,{noMoveStart:!0})}};function rp(e,t){(!e.duration||e.duration=this._clickTolerance||this._map.fire(new Jr(e.type,this._map,e))}dblclick(e){return this._firePreventable(new Jr(e.type,this._map,e))}mouseover(e){this._map.fire(new Jr(e.type,this._map,e))}mouseout(e){this._map.fire(new Jr(e.type,this._map,e))}touchstart(e){return this._firePreventable(new Yr(e.type,this._map,e))}touchmove(e){this._map.fire(new Yr(e.type,this._map,e))}touchend(e){this._map.fire(new Yr(e.type,this._map,e))}touchcancel(e){this._map.fire(new Yr(e.type,this._map,e))}_firePreventable(e){if(this._map.fire(e),e.defaultPrevented)return{}}isEnabled(){return!0}isActive(){return!1}enable(){}disable(){}},op=class{constructor(e){this._map=e}reset(){this._delayContextMenu=!1,this._ignoreContextMenu=!0,delete this._contextMenuEvent}mousemove(e){this._map.fire(new Jr(e.type,this._map,e))}mousedown(){this._delayContextMenu=!0,this._ignoreContextMenu=!1}mouseup(){this._delayContextMenu=!1,this._contextMenuEvent&&(this._map.fire(new Jr(`contextmenu`,this._map,this._contextMenuEvent)),delete this._contextMenuEvent)}contextmenu(e){this._delayContextMenu?this._contextMenuEvent=e:this._ignoreContextMenu||this._map.fire(new Jr(e.type,this._map,e)),this._map.listens(`contextmenu`)&&e.preventDefault()}isEnabled(){return!0}isActive(){return!1}enable(){}disable(){}},sp=class{constructor(e,t,n){this._map=e,this._tr=n,this._el=e.getCanvasContainer(),this._container=e.getContainer(),this._clickTolerance=t.clickTolerance||1,t.boxZoom&&typeof t.boxZoom==`object`&&(this._boxZoomEnd=t.boxZoom.boxZoomEnd)}isEnabled(){return!!this._enabled}isActive(){return!!this._active}enable(){this.isEnabled()||(this._enabled=!0)}disable(){this.isEnabled()&&(this._enabled=!1)}mousedown(e,t){this.isEnabled()&&e.shiftKey&&e.button===0&&(W.disableDrag(),this._startPos=this._lastPos=t,this._active=!0)}mousemoveWindow(e,t){if(!this._active)return;let n=t;if(this._lastPos.equals(n)||!this._box&&n.dist(this._startPos)e.fitScreenCoordinates(n,r,this._tr.bearing,{linear:!0})}}}keydown(e){this._active&&e.keyCode===27&&(this.reset(),this._fireEvent(`boxzoomcancel`,e))}reset(){this._active=!1,this._container.classList.remove(`maplibregl-crosshair`),this._box&&=(this._box.remove(),null),W.enableDrag(),delete this._startPos,delete this._lastPos}_fireEvent(e,t){return this._map.fire(new Zr(e,{originalEvent:t}))}};function cp(e,t){if(e.length!==t.length)throw Error(`The number of touches and points are not equal - touches ${e.length}, points ${t.length}`);let n={};for(let r=0;rthis.numTouches)&&(this.aborted=!0),!this.aborted&&(this.startTime===void 0&&(this.startTime=e.timeStamp),n.length===this.numTouches&&(this.centroid=lp(t),this.touches=cp(n,t)))}touchmove(e,t,n){if(this.aborted||!this.centroid)return;let r=cp(n,t);for(let e in this.touches){let t=this.touches[e],n=r[e];(!n||n.dist(t)>30)&&(this.aborted=!0)}}touchend(e,t,n){if((!this.centroid||e.timeStamp-this.startTime>500)&&(this.aborted=!0),n.length===0){let e=!this.aborted&&this.centroid;if(this.reset(),e)return e}}},dp=class{constructor(e){this.singleTap=new up(e),this.numTaps=e.numTaps,this.reset()}reset(){this.lastTime=1/0,delete this.lastTap,this.count=0,this.singleTap.reset()}touchstart(e,t,n){this.singleTap.touchstart(e,t,n)}touchmove(e,t,n){this.singleTap.touchmove(e,t,n)}touchend(e,t,n){let r=this.singleTap.touchend(e,t,n);if(r){let t=e.timeStamp-this.lastTime<500,n=!this.lastTap||this.lastTap.dist(r)<30;if((!t||!n)&&this.reset(),this.count++,this.lastTime=e.timeStamp,this.lastTap=r,this.count===this.numTaps)return this.reset(),r}}},fp=class{constructor(e,t){this._tr=t,this._zoomIn=new dp({numTouches:1,numTaps:2}),this._zoomOut=new dp({numTouches:2,numTaps:1}),this.reset()}reset(){this._active=!1,this._zoomIn.reset(),this._zoomOut.reset()}touchstart(e,t,n){this._zoomIn.touchstart(e,t,n),this._zoomOut.touchstart(e,t,n)}touchmove(e,t,n){this._zoomIn.touchmove(e,t,n),this._zoomOut.touchmove(e,t,n)}touchend(e,t,n){let r=this._zoomIn.touchend(e,t,n),i=this._zoomOut.touchend(e,t,n),a=this._tr;if(r)return this._active=!0,e.preventDefault(),setTimeout(()=>this.reset(),0),{cameraAnimation:t=>t.easeTo({duration:300,zoom:un(a.zoom+1,t.getZoomSnap()),around:a.unproject(r)},{originalEvent:e})};if(i)return this._active=!0,e.preventDefault(),setTimeout(()=>this.reset(),0),{cameraAnimation:t=>t.easeTo({duration:300,zoom:un(a.zoom-1,t.getZoomSnap()),around:a.unproject(i)},{originalEvent:e})}}touchcancel(){this.reset()}enable(){this._enabled=!0}disable(){this._enabled=!1,this.reset()}isEnabled(){return this._enabled}isActive(){return this._active}},pp=class{constructor(e){this._enabled=!!e.enable,this._moveStateManager=e.moveStateManager,this._clickTolerance=e.clickTolerance||1,this._moveFunction=e.move,this._activateOnStart=!!e.activateOnStart,e.assignEvents(this),this.reset()}reset(e){this._active=!1,this._moved=!1,delete this._lastPoint,this._moveStateManager.endMove(e)}_move(...e){let t=this._moveFunction(...e);if(t.bearingDelta||t.pitchDelta||t.rollDelta||t.around||t.panDelta)return this._active=!0,t}dragStart(e,t){!this.isEnabled()||this._lastPoint||this._moveStateManager.isValidStartEvent(e)&&(this._moveStateManager.startMove(e),this._lastPoint=Array.isArray(t)?t[0]:t,this._activateOnStart&&this._lastPoint&&(this._active=!0))}dragMove(e,t){if(!this.isEnabled())return;let n=this._lastPoint;if(!n)return;if(e.preventDefault(),!this._moveStateManager.isValidMoveEvent(e)){this.reset(e);return}let r=Array.isArray(t)?t[0]:t;if(!(!this._moved&&r.dist(n)!0}),t=new _p){this.mouseMoveStateManager=e,this.oneFingerTouchMoveStateManager=t}_executeRelevantHandler(e,t,n){if(e instanceof MouseEvent)return t(e);if(typeof TouchEvent<`u`&&e instanceof TouchEvent)return n(e)}startMove(e){this._executeRelevantHandler(e,e=>{this.mouseMoveStateManager.startMove(e)},e=>{this.oneFingerTouchMoveStateManager.startMove(e)})}endMove(e){this._executeRelevantHandler(e,e=>{this.mouseMoveStateManager.endMove(e)},e=>{this.oneFingerTouchMoveStateManager.endMove(e)})}isValidStartEvent(e){return!!this._executeRelevantHandler(e,e=>this.mouseMoveStateManager.isValidStartEvent(e),e=>this.oneFingerTouchMoveStateManager.isValidStartEvent(e))}isValidMoveEvent(e){return!!this._executeRelevantHandler(e,e=>this.mouseMoveStateManager.isValidMoveEvent(e),e=>this.oneFingerTouchMoveStateManager.isValidMoveEvent(e))}isValidEndEvent(e){return!!this._executeRelevantHandler(e,e=>this.mouseMoveStateManager.isValidEndEvent(e),e=>this.oneFingerTouchMoveStateManager.isValidEndEvent(e))}};const yp=e=>{e.mousedown=e.dragStart,e.mousemoveWindow=e.dragMove,e.mouseup=e.dragEnd,e.contextmenu=e=>{e.preventDefault()}};function bp({enable:e,clickTolerance:t}){return new pp({clickTolerance:t,move:(e,t)=>({around:t,panDelta:t.sub(e)}),activateOnStart:!0,moveStateManager:new gp({checkCorrectEvent:e=>e.button===0&&!e.ctrlKey}),enable:e,assignEvents:yp})}function xp({enable:e,clickTolerance:t,aroundCenter:n=!0,minPixelCenterThreshold:r=100,rotateSpeed:i=.8},a){return new pp({clickTolerance:t,move:(e,t)=>{let o=a();if(n&&Math.abs(o.y-e.y)>r)return{bearingDelta:wn(new l(e.x,t.y),t,o)};let s=(t.x-e.x)*i;return n&&t.ye.button===0&&e.ctrlKey||e.button===2&&!e.ctrlKey}),enable:e,assignEvents:yp})}function Sp({enable:e,clickTolerance:t,pitchSpeed:n=-.5}){return new pp({clickTolerance:t,move:(e,t)=>({pitchDelta:(t.y-e.y)*n}),moveStateManager:new gp({checkCorrectEvent:e=>e.button===0&&e.ctrlKey||e.button===2}),enable:e,assignEvents:yp})}function Cp({enable:e,clickTolerance:t,rollDegreesPerPixelMoved:n=.3},r){return new pp({clickTolerance:t,move:(e,t)=>{let i=r(),a=(t.x-e.x)*n;return t.ye.button===2&&e.ctrlKey}),enable:e,assignEvents:yp})}var wp=class{constructor(e,t){this._clickTolerance=e.clickTolerance||1,this._map=t,this.reset()}reset(){this._active=!1,this._touches={},this._sum=new l(0,0)}_shouldBePrevented(e){return e<(this._map.cooperativeGestures.isEnabled()?2:1)}touchstart(e,t,n){return this._calculateTransform(e,t,n)}touchmove(e,t,n){if(this._active){if(this._shouldBePrevented(n.length)){this._map.cooperativeGestures.notifyGestureBlocked(`touch_pan`,e);return}return e.preventDefault(),this._calculateTransform(e,t,n)}}touchend(e,t,n){this._calculateTransform(e,t,n),this._active&&this._shouldBePrevented(n.length)&&this.reset()}touchcancel(){this.reset()}_calculateTransform(e,t,n){n.length>0&&(this._active=!0);let r=cp(n,t),i=new l(0,0),a=new l(0,0),o=0;for(let e in r){let t=r[e],n=this._touches[e];n&&(i._add(t),a._add(t.sub(n)),o++,r[e]=t)}if(this._touches=r,this._shouldBePrevented(o)||!a.mag())return;let s=a.div(o);if(this._sum._add(s),!(this._sum.mag()Math.abs(e.x)}var Np=class extends Tp{constructor(e){super(),this._currentTouchCount=0,this._map=e}reset(){super.reset(),this._valid=void 0,delete this._firstMove,delete this._lastPoints}touchstart(e,t,n){super.touchstart(e,t,n),this._currentTouchCount=n.length}_start(e){this._lastPoints=e,Mp(e[0].sub(e[1]))&&(this._valid=!1)}_move(e,t,n){if(this._map.cooperativeGestures.isEnabled()&&this._currentTouchCount<3)return;let r=e[0].sub(this._lastPoints[0]),i=e[1].sub(this._lastPoints[1]);if(this._valid=this.gestureBeginsVertically(r,i,n.timeStamp),this._valid)return this._lastPoints=e,this._active=!0,{pitchDelta:(r.y+i.y)/2*-.5}}gestureBeginsVertically(e,t,n){if(this._valid!==void 0)return this._valid;let r=e.mag()>=2,i=t.mag()>=2;if(!r&&!i)return;if(!r||!i)return this._firstMove===void 0&&(this._firstMove=n),n-this._firstMove<100&&void 0;let a=e.y>0==t.y>0;return Mp(e)&&Mp(t)&&a}};const Pp={panStep:100,bearingStep:15,pitchStep:10};var Fp=class{constructor(e,t){this._tr=t;let n=Pp;this._panStep=n.panStep,this._bearingStep=n.bearingStep,this._pitchStep=n.pitchStep,this._rotationDisabled=!1}reset(){this._active=!1}keydown(e){if(e.altKey||e.ctrlKey||e.metaKey)return;let t=0,n=0,r=0,i=0,a=0;switch(e.keyCode){case 61:case 107:case 171:case 187:t=1;break;case 189:case 109:case 173:t=-1;break;case 37:e.shiftKey?n=-1:(e.preventDefault(),i=-1);break;case 39:e.shiftKey?n=1:(e.preventDefault(),i=1);break;case 38:e.shiftKey?r=1:(e.preventDefault(),a=-1);break;case 40:e.shiftKey?r=-1:(e.preventDefault(),a=1);break;default:return}return this._rotationDisabled&&(n=0,r=0),{cameraAnimation:o=>{let s=this._tr;o.easeTo({duration:300,easeId:`keyboardHandler`,easing:Ip,zoom:t?un(s.zoom+t*(e.shiftKey?2:1),o.getZoomSnap()):s.zoom,bearing:s.bearing+n*this._bearingStep,pitch:s.pitch+r*this._pitchStep,offset:[-i*this._panStep,-a*this._panStep],center:s.center},{originalEvent:e})}}}enable(){this._enabled=!0}disable(){this._enabled=!1,this.reset()}isEnabled(){return this._enabled}isActive(){return this._active}disableRotation(){this._rotationDisabled=!0}enableRotation(){this._rotationDisabled=!1}};function Ip(e){return e*(2-e)}const Lp=4.000244140625;var Rp=class{constructor(e,t,n){this._onTimeout=e=>{this._type=`wheel`,this._delta-=this._lastValue,this._active||this._start(e)},this._map=e,this._tr=n,this._triggerRenderFrame=t,this._delta=0,this._defaultZoomRate=.01,this._wheelZoomRate=.0022222222222222222}setZoomRate(e){this._defaultZoomRate=e}setWheelZoomRate(e){this._wheelZoomRate=e}isEnabled(){return!!this._enabled}isActive(){return!!this._active||this._finishTimeout!==void 0}isZooming(){return!!this._zooming}enable(e){this.isEnabled()||(this._enabled=!0,this._aroundCenter=!!e&&e.around===`center`)}disable(){this.isEnabled()&&(this._enabled=!1)}_shouldBePrevented(e){return this._map.cooperativeGestures.isEnabled()?!(e.ctrlKey||this._map.cooperativeGestures.isBypassed(e)):!1}wheel(e){if(!this.isEnabled())return;if(this._shouldBePrevented(e)){this._map.cooperativeGestures.notifyGestureBlocked(`wheel_zoom`,e);return}let t=e.deltaMode===WheelEvent.DOM_DELTA_LINE?e.deltaY*40:e.deltaY,n=U(),r=n-(this._lastWheelEventTime||0);this._lastWheelEventTime=n,t!==0&&t%Lp==0?this._type=`wheel`:t!==0&&Math.abs(t)<4?this._type=`trackpad`:r>400?(this._type=null,this._lastValue=t,this._timeout=setTimeout(this._onTimeout,40,e)):this._type||(this._type=Math.abs(r*t)<200?`trackpad`:`wheel`,this._timeout&&(clearTimeout(this._timeout),this._timeout=null,t+=this._lastValue)),e.shiftKey&&t&&(t/=4),this._type&&(this._lastWheelEvent=e,this._delta-=t,this._active||this._start(e)),e.preventDefault()}_start(e){if(!this._delta)return;this._needsRerender=!1,this._active=!0,this.isZooming()||(this._zooming=!0),this._finishTimeout&&(clearTimeout(this._finishTimeout),delete this._finishTimeout);let t=W.mousePos(this._map.getCanvas(),e),n=this._tr;this._aroundPoint=this._aroundCenter?n.transform.locationToScreenPoint(V.convert(n.center)):t,this._needsRerender||(this._needsRerender=!0,this._triggerRenderFrame())}renderFrame(){if(!this._needsRerender||(this._needsRerender=!1,!this.isActive()))return;let e=this._tr.transform;if(typeof this._lastExpectedZoom==`number`){let t=e.zoom-this._lastExpectedZoom;typeof this._startZoom==`number`&&(this._startZoom+=t),typeof this._targetZoom==`number`&&(this._targetZoom+=t)}if(this._delta!==0){let t=this._type===`wheel`&&Math.abs(this._delta)>Lp?this._wheelZoomRate:this._defaultZoomRate,n=2/(1+Math.exp(-Math.abs(this._delta*t)));this._delta<0&&n!==0&&(n=1/n);let r=typeof this._targetZoom==`number`?d(this._targetZoom):e.scale,i=e.applyConstrain(e.getCameraLngLat(),Ee(r*n)).zoom,a=this._map.getZoomSnap();if(this._type===`wheel`&&a>0){let t=un(e.zoom,a);this._targetZoom=un(i,a,i-t)}else this._targetZoom=i;this._type===`wheel`&&(this._startZoom=e.zoom,this._easing=this._smoothOutEasing(200)),this._delta=0}let t=typeof this._targetZoom==`number`?this._targetZoom:e.zoom,n=this._startZoom,r=this._easing,i=!1,a;if(this._type===`wheel`&&n&&r){let e=U()-this._lastWheelEventTime,o=Math.min((e+5)/200,1),s=r(o);a=on.number(n,t,s),o<1?this._needsRerender=!0:i=!0}else a=t,i=!0;return this._active=!0,i&&(this._active=!1,this._finishTimeout=setTimeout(()=>{this._zooming=!1,this._triggerRenderFrame(),delete this._targetZoom,delete this._lastExpectedZoom,delete this._finishTimeout},200)),this._lastExpectedZoom=a,{noInertia:!0,needsRenderFrame:!i,zoomDelta:a-e.zoom,around:this._aroundPoint,originalEvent:this._lastWheelEvent}}_smoothOutEasing(e){let n=t;if(this._prevEase){let e=this._prevEase,t=(U()-e.start)/e.duration,r=e.easing(t+.01)-e.easing(t),i=.27/Math.sqrt(r*r+1e-4)*.01,a=Math.sqrt(.0729-i*i);n=dt(i,a,.25,1)}return this._prevEase={start:U(),duration:e,easing:n},n}reset(){this._active=!1,this._zooming=!1,delete this._targetZoom,delete this._lastExpectedZoom,this._finishTimeout&&(clearTimeout(this._finishTimeout),delete this._finishTimeout)}},zp=class{constructor(e,t){this._clickZoom=e,this._tapZoom=t}enable(){this._clickZoom.enable(),this._tapZoom.enable()}disable(){this._clickZoom.disable(),this._tapZoom.disable()}isEnabled(){return this._clickZoom.isEnabled()&&this._tapZoom.isEnabled()}isActive(){return this._clickZoom.isActive()||this._tapZoom.isActive()}},Bp=class{constructor(e,t){this._tr=t,this.reset()}reset(){this._active=!1}dblclick(e,t){return e.preventDefault(),{cameraAnimation:n=>{n.easeTo({duration:300,zoom:un(this._tr.zoom+(e.shiftKey?-1:1),n.getZoomSnap()),around:this._tr.unproject(t)},{originalEvent:e})}}}enable(){this._enabled=!0}disable(){this._enabled=!1,this.reset()}isEnabled(){return this._enabled}isActive(){return this._active}},Vp=class{constructor(){this._tap=new dp({numTouches:1,numTaps:1}),this._zoomRate=1,this.reset()}setZoomRate(e){this._zoomRate=e??1}reset(){this._active=!1,delete this._swipePoint,delete this._swipeTouch,delete this._tapTime,delete this._tapPoint,this._tap.reset()}touchstart(e,t,n){if(!this._swipePoint){if(!this._tapTime)this._tap.touchstart(e,t,n);else{let r=t[0],i=e.timeStamp-this._tapTime<500,a=this._tapPoint.dist(r)<30;!i||!a?this.reset():n.length>0&&(this._swipePoint=r,this._swipeTouch=n[0].identifier)}}}touchmove(e,t,n){if(!this._tapTime)this._tap.touchmove(e,t,n);else if(this._swipePoint){if(n[0].identifier!==this._swipeTouch)return;let r=t[0],i=r.y-this._swipePoint.y;return this._swipePoint=r,e.preventDefault(),this._active=!0,{zoomDelta:i/128*this._zoomRate}}}touchend(e,t,n){if(this._tapTime)this._swipePoint&&n.length===0&&this.reset();else{let r=this._tap.touchend(e,t,n);r&&(this._tapTime=e.timeStamp,this._tapPoint=r)}}touchcancel(){this.reset()}enable(){this._enabled=!0}disable(){this._enabled=!1,this.reset()}isEnabled(){return this._enabled}isActive(){return this._active}},Hp=class{constructor(e,t,n){this._el=e,this._mousePan=t,this._touchPan=n}enable(e){this._inertiaOptions=e||{},this._mousePan.enable(),this._touchPan.enable(),this._el.classList.add(`maplibregl-touch-drag-pan`)}disable(){this._mousePan.disable(),this._touchPan.disable(),this._el.classList.remove(`maplibregl-touch-drag-pan`)}isEnabled(){return this._mousePan.isEnabled()&&this._touchPan.isEnabled()}isActive(){return this._mousePan.isActive()||this._touchPan.isActive()}},Up=class{constructor(e,t,n,r){this._pitchWithRotate=e.pitchWithRotate,this._rollEnabled=e.rollEnabled,this._mouseRotate=t,this._mousePitch=n,this._mouseRoll=r}enable(){this._mouseRotate.enable(),this._pitchWithRotate&&this._mousePitch.enable(),this._rollEnabled&&this._mouseRoll.enable()}disable(){this._mouseRotate.disable(),this._mousePitch.disable(),this._mouseRoll.disable()}isEnabled(){return this._mouseRotate.isEnabled()&&(!this._pitchWithRotate||this._mousePitch.isEnabled())&&(!this._rollEnabled||this._mouseRoll.isEnabled())}isActive(){return this._mouseRotate.isActive()||this._mousePitch.isActive()||this._mouseRoll.isActive()}},Wp=class{constructor(e,t,n,r){this._el=e,this._touchZoom=t,this._touchRotate=n,this._tapDragZoom=r,this._rotationDisabled=!1,this._enabled=!0}enable(e){this._touchZoom.enable(e),this._rotationDisabled||this._touchRotate.enable(e),this._tapDragZoom.enable(),this._el.classList.add(`maplibregl-touch-zoom-rotate`)}disable(){this._touchZoom.disable(),this._touchRotate.disable(),this._tapDragZoom.disable(),this._el.classList.remove(`maplibregl-touch-zoom-rotate`)}isEnabled(){return this._touchZoom.isEnabled()&&(this._rotationDisabled||this._touchRotate.isEnabled())&&this._tapDragZoom.isEnabled()}isActive(){return this._touchZoom.isActive()||this._touchRotate.isActive()||this._tapDragZoom.isActive()}setZoomRate(e){this._touchZoom.setZoomRate(e),this._tapDragZoom.setZoomRate(e)}setZoomThreshold(e){this._touchZoom.setZoomThreshold(e)}disableRotation(){this._rotationDisabled=!0,this._touchRotate.disable()}enableRotation(){this._rotationDisabled=!1,this._touchZoom.isEnabled()&&this._touchRotate.enable()}},Gp=class{constructor(e,t){this._bypassKey=navigator.userAgent.includes(`Mac`)?`metaKey`:`ctrlKey`,this._map=e,this._options=t,this._enabled=!1}isActive(){return!1}reset(){}_setupUI(){if(this._container)return;let e=this._map.getCanvasContainer();e.classList.add(`maplibregl-cooperative-gestures`),this._container=W.create(`div`,`maplibregl-cooperative-gesture-screen`,e);let t=this._map._getUIString(`CooperativeGesturesHandler.WindowsHelpText`);this._bypassKey===`metaKey`&&(t=this._map._getUIString(`CooperativeGesturesHandler.MacHelpText`));let n=this._map._getUIString(`CooperativeGesturesHandler.MobileHelpText`),r=document.createElement(`div`);r.className=`maplibregl-desktop-message`,r.textContent=t,this._container.appendChild(r);let i=document.createElement(`div`);i.className=`maplibregl-mobile-message`,i.textContent=n,this._container.appendChild(i),this._container.setAttribute(`aria-hidden`,`true`)}_destroyUI(){this._container&&(this._container.remove(),this._map.getCanvasContainer().classList.remove(`maplibregl-cooperative-gestures`)),delete this._container}enable(){this._setupUI(),this._enabled=!0}disable(){this._enabled=!1,this._destroyUI()}isEnabled(){return this._enabled}isBypassed(e){return e[this._bypassKey]}notifyGestureBlocked(e,t){this._enabled&&(this._map.fire(new Gr(`cooperativegestureprevented`,{gestureType:e,originalEvent:t})),this._container.classList.add(`maplibregl-show`),setTimeout(()=>{this._container.classList.remove(`maplibregl-show`)},100))}},Kp=class{constructor(e){this._camera=e}get transform(){return this._camera._requestedCameraState||this._camera.transform}get center(){return{lng:this.transform.center.lng,lat:this.transform.center.lat}}get zoom(){return this.transform.zoom}get pitch(){return this.transform.pitch}get bearing(){return this.transform.bearing}unproject(e){return this.transform.screenPointToLocation(l.convert(e),this._camera.terrain)}};const qp=e=>e.zoom||e.drag||e.roll||e.pitch||e.rotate;var Jp=class extends Xe{};function Yp(e){return e.panDelta?.mag()||e.zoomDelta||e.bearingDelta||e.pitchDelta||e.rollDelta}var Xp=class{get _ownerDocument(){return this._el?.ownerDocument||document}get _ownerWindow(){return this._el?.ownerDocument?.defaultView||window}constructor(e,t,n){this._terrainGestureAnchorElevation=null,this.handleWindowEvent=e=>{this.handleEvent(e,`${e.type}Window`)},this.handleEvent=(e,t)=>{if(e.type===`blur`){this.stop(!0);return}this._updatingCamera=!0;let n=e.type===`renderFrame`?void 0:e,r={needsRenderFrame:!1},i={},a={};for(let{handlerName:o,handler:s,allowed:c}of this._handlers){if(!s.isEnabled())continue;let l;if(this._blockedByActive(a,c,o))s.reset();else if(s[t||e.type]){if(Dn(e,t||e.type)){let n=W.mousePos(this._map.getCanvas(),e);l=s[t||e.type](e,n)}else if(nn(e,t||e.type)){let n=e.touches,r=this._getMapTouches(n),i=W.touchPos(this._map.getCanvas(),r);l=s[t||e.type](e,i,r)}else Dt(t||e.type)||(l=s[t||e.type](e));this.mergeHandlerResult(r,i,l,o,n),l?.needsRenderFrame&&this._triggerRenderFrame()}(l||s.isActive())&&(a[o]=s)}let o={};for(let e in this._previousActiveHandlers)a[e]||(o[e]=n);this._previousActiveHandlers=a,(Object.keys(o).length||Yp(r))&&(this._changes.push([r,i,o]),this._triggerRenderFrame()),(Object.keys(a).length||Yp(r))&&this._camera.stop(!0),this._updatingCamera=!1;let{cameraAnimation:s}=r;s&&(this._inertia.clear(),this._fireEvents({},{},!0),this._changes=[],s(this._map))},this._map=e,this._camera=t,this._transformProvider=new Kp(this._camera),this._el=this._map.getCanvasContainer(),this._handlers=[],this._handlersById={},this._changes=[],this._inertia=new np(e),this._bearingSnap=n.bearingSnap,this._previousActiveHandlers={},this._eventsInProgress={},this._addDefaultHandlers(n);let r=this._el;this._listeners=[[r,`touchstart`,{passive:!0}],[r,`touchmove`,{passive:!1}],[r,`touchend`,void 0],[r,`touchcancel`,void 0],[r,`mousedown`,void 0],[r,`mousemove`,void 0],[r,`mouseup`,void 0],[this._ownerDocument,`mousemove`,{capture:!0}],[this._ownerDocument,`mouseup`,void 0],[r,`mouseover`,void 0],[r,`mouseout`,void 0],[r,`dblclick`,void 0],[r,`click`,void 0],[r,`keydown`,{capture:!1}],[r,`keyup`,void 0],[r,`wheel`,{passive:!1}],[r,`contextmenu`,void 0],[this._ownerWindow,`blur`,void 0]];for(let[e,t,n]of this._listeners)e.addEventListener(t,e===this._ownerDocument?this.handleWindowEvent:this.handleEvent,n)}destroy(){for(let[e,t,n]of this._listeners)e.removeEventListener(t,e===this._ownerDocument?this.handleWindowEvent:this.handleEvent,n)}_addDefaultHandlers(e){let t=this._map,n=t.getCanvasContainer();this._add(`mapEvent`,new ap(t,e));let r=t.boxZoom=new sp(t,e,this._transformProvider);this._add(`boxZoom`,r),e.interactive&&e.boxZoom&&r.enable();let i=t.cooperativeGestures=new Gp(t,e.cooperativeGestures);this._add(`cooperativeGestures`,i),e.cooperativeGestures&&i.enable();let a=new fp(t,this._transformProvider),o=new Bp(t,this._transformProvider);t.doubleClickZoom=new zp(o,a),this._add(`tapZoom`,a),this._add(`clickZoom`,o),e.interactive&&e.doubleClickZoom&&t.doubleClickZoom.enable();let s=new Vp;this._add(`tapDragZoom`,s);let c=t.touchPitch=new Np(t);this._add(`touchPitch`,c),e.interactive&&e.touchPitch&&t.touchPitch.enable(e.touchPitch);let l=()=>t.project(t.getCenter()),u=xp(e,l),d=Sp(e),f=Cp(e,l);t.dragRotate=new Up(e,u,d,f),this._add(`mouseRotate`,u,[`mousePitch`]),this._add(`mousePitch`,d,[`mouseRotate`,`mouseRoll`]),this._add(`mouseRoll`,f,[`mousePitch`]),e.interactive&&e.dragRotate&&t.dragRotate.enable();let p=bp(e),m=new wp(e,t);t.dragPan=new Hp(n,p,m),this._add(`mousePan`,p),this._add(`touchPan`,m,[`touchZoom`,`touchRotate`]),e.interactive&&e.dragPan&&t.dragPan.enable(e.dragPan);let h=new jp,g=new kp;t.touchZoomRotate=new Wp(n,g,h,s),this._add(`touchRotate`,h,[`touchPan`,`touchZoom`]),this._add(`touchZoom`,g,[`touchPan`,`touchRotate`]),e.interactive&&e.touchZoomRotate&&t.touchZoomRotate.enable(e.touchZoomRotate),this._add(`blockableMapEvent`,new op(t));let _=t.scrollZoom=new Rp(t,()=>this._triggerRenderFrame(),this._transformProvider);this._add(`scrollZoom`,_,[`mousePan`]),e.interactive&&e.scrollZoom&&t.scrollZoom.enable(e.scrollZoom);let v=t.keyboard=new Fp(t,this._transformProvider);this._add(`keyboard`,v),e.interactive&&e.keyboard&&t.keyboard.enable()}_add(e,t,n){this._handlers.push({handlerName:e,handler:t,allowed:n}),this._handlersById[e]=t}stop(e){if(!this._updatingCamera){for(let{handler:e}of this._handlers)e.reset();this._inertia.clear(),this._fireEvents({},{},e),this._changes=[]}}isActive(){for(let{handler:e}of this._handlers)if(e.isActive())return!0;return!1}isZooming(){return!!this._eventsInProgress.zoom||this._map.scrollZoom.isZooming()}isRotating(){return!!this._eventsInProgress.rotate}isMoving(){return!!qp(this._eventsInProgress)||this.isZooming()}_blockedByActive(e,t,n){for(let r in e)if(r!==n&&!t?.includes(r))return!0;return!1}_getMapTouches(e){let t=[];for(let n of e){let e=n.target;this._el.contains(e)&&t.push(n)}return t}mergeHandlerResult(e,t,n,r,i){if(!n)return;z(e,n);let a={handlerName:r,originalEvent:n.originalEvent||i};n.zoomDelta!==void 0&&(t.zoom=a),n.panDelta!==void 0&&(t.drag=a),n.rollDelta!==void 0&&(t.roll=a),n.pitchDelta!==void 0&&(t.pitch=a),n.bearingDelta!==void 0&&(t.rotate=a)}_applyChanges(){let e={},t={},n={};for(let[r,i,a]of this._changes)r.panDelta&&(e.panDelta=(e.panDelta||new l(0,0))._add(r.panDelta)),r.zoomDelta&&(e.zoomDelta=(e.zoomDelta||0)+r.zoomDelta),r.bearingDelta&&(e.bearingDelta=(e.bearingDelta||0)+r.bearingDelta),r.pitchDelta&&(e.pitchDelta=(e.pitchDelta||0)+r.pitchDelta),r.rollDelta&&(e.rollDelta=(e.rollDelta||0)+r.rollDelta),r.around!==void 0&&(e.around=r.around),r.pinchAround!==void 0&&(e.pinchAround=r.pinchAround),r.noInertia&&(e.noInertia=r.noInertia),z(t,i),z(n,a);this._updateMapTransform(e,t,n),this._changes=[]}_updateMapTransform(e,t,n){let r=this._map,i=this._camera.getTransformForUpdate(),a=r.terrain;if(!Yp(e)&&!(a&&this._terrainMovement)){this._fireEvents(t,n,!0);return}this._camera.stop(!0);let{panDelta:o,zoomDelta:s,bearingDelta:c,pitchDelta:l,rollDelta:u}=e,{around:d,aroundOnSurface:f}=this._resolveAround(e,a,i),p=a?this._terrainGestureElevation(a,d,f,i,t):void 0,m={panDelta:o,zoomDelta:s,rollDelta:u,pitchDelta:l,bearingDelta:c,around:d,aroundElevation:p};this._camera.cameraHelper.useGlobeControls&&!i.isPointOnMapSurface(d)&&(d=i.centerPoint);let h=this._computePreZoomAroundLoc(i,d,o,p);this._handleMapControls({terrain:a,tr:i,deltasForHelper:m,preZoomAroundLoc:h,combinedEventsInProgress:t,panDelta:o}),this._camera.applyUpdatedTransform(i),this._map._update(),e.noInertia||this._inertia.record(e),this._fireEvents(t,n,!0)}_resolveAround(e,t,n){let r=e.pinchAround===void 0?e.around:e.pinchAround;return r||=this._camera.transform.centerPoint,t&&!n.isPointOnMapSurface(r)?{around:n.centerPoint,aroundOnSurface:!1}:{around:r,aroundOnSurface:!0}}_terrainGestureElevation(e,t,n,r,i){if(!n)return;if(!this._terrainMovement&&(i.drag||i.zoom)){let n=r.screenTerrainPointToMercatorCoordinate(t,e);this._terrainGestureAnchorElevation=n?n.z:null}if(this._terrainGestureAnchorElevation===null)return;let a=this._terrainGestureAnchorElevation;if(!(t.distSqr(r.centerPoint)<.01)&&!(a-r.elevation>=.9*(r.getCameraAltitude()-r.elevation)))return a}_computePreZoomAroundLoc(e,t,n,r){if(t.distSqr(e.centerPoint)<.01)return e.center;let i=n?t.sub(n):t;return r===void 0?e.screenPointToLocation(i):e.screenPointToLocationAtElevation(i,r)}_handleMapControls({terrain:e,tr:t,deltasForHelper:n,preZoomAroundLoc:r,combinedEventsInProgress:i,panDelta:a}){let o=this._camera.cameraHelper;if(o.handleMapControlsRollPitchBearingZoom(n,t),!e){o.handleMapControlsPan(n,t,r);return}if(o.useGlobeControls){!this._terrainMovement&&(i.drag||i.zoom)&&(this._terrainMovement=!0,this._camera.elevationFreeze=!0),o.handleMapControlsPan(n,t,r);return}if(!this._terrainMovement&&(i.drag||i.zoom)){this._terrainMovement=!0,this._camera.elevationFreeze=!0,o.handleMapControlsPan(n,t,r);return}if(n.aroundElevation===void 0&&i.drag&&this._terrainMovement&&a){t.setCenter(t.screenPointToLocation(t.centerPoint.sub(a)));return}o.handleMapControlsPan(n,t,r)}_fireEvents(e,t,n){let r=qp(this._eventsInProgress),i=qp(e),a={};for(let t in e){let{originalEvent:n}=e[t];this._eventsInProgress[t]||(a[`${t}start`]=n),this._eventsInProgress[t]=e[t]}!r&&i&&this._fireEvent(`movestart`,i.originalEvent);for(let e in a)this._fireEvent(e,a[e]);i&&this._fireEvent(`move`,i.originalEvent);for(let t in e){let{originalEvent:n}=e[t];this._fireEvent(t,n)}let o={},s;for(let e in this._eventsInProgress){let{handlerName:n,originalEvent:r}=this._eventsInProgress[e];this._handlersById[n].isActive()||(delete this._eventsInProgress[e],s=t[n]||r,o[`${e}end`]=s)}for(let e in o)this._fireEvent(e,o[e]);let c=qp(this._eventsInProgress),l=(r||i)&&!c;if(l&&this._terrainMovement){this._camera.elevationFreeze=!1,this._terrainMovement=!1,this._terrainGestureAnchorElevation=null;let e=this._camera.getTransformForUpdate();this._map.getCenterClampedToGround()&&e.recalculateZoomAndCenter(this._map.terrain),this._camera.applyUpdatedTransform(e)}if(n&&l){this._updatingCamera=!0;let e=this._inertia._onMoveEnd(this._map.dragPan._inertiaOptions),t=e=>e!==0&&-this._bearingSnap{delete this._frameId,this.handleEvent(new Jp(`renderFrame`,{timeStamp:e})),this._applyChanges()})}_triggerRenderFrame(){this._frameId===void 0&&(this._frameId=this._requestFrame())}},Zp=class extends h{constructor(e){super(),this._renderFrameCallback=()=>{let e=Math.min((U()-this._easeStart)/this._easeOptions.duration,1);this._onEaseFrame(this._easeOptions.easing(e)),e<1&&this._easeFrameId?this._easeFrameId=this._requestRenderFrame(this._renderFrameCallback):this.stop()},this.transform=new yc,this.cameraHelper=new Tc,e.minZoom!==void 0&&this.transform.setMinZoom(e.minZoom),e.maxZoom!==void 0&&this.transform.setMaxZoom(e.maxZoom),e.minPitch!==void 0&&this.transform.setMinPitch(e.minPitch),e.maxPitch!==void 0&&this.transform.setMaxPitch(e.maxPitch),e.renderWorldCopies!==void 0&&this.transform.setRenderWorldCopies(e.renderWorldCopies),e.transformConstrain!==null&&this.transform.setConstrainOverride(e.transformConstrain),this._moving=!1,this._zooming=!1,this._bearingSnap=e.bearingSnap,this._zoomSnap=e.zoomSnap,this._requestRenderFrame=e.requestRenderFrame,this._cancelRenderFrame=e.cancelRenderFrame,this.terrain=e.terrain,this._centerClampedToGround=e.centerClampedToGround??!0,this.transformCameraUpdate=e.transformCameraUpdate??null,this._stopHandlers=e.stopHandlers??(()=>{}),this.on(`moveend`,()=>{delete this._requestedCameraState})}migrateProjection(e,t){e.apply(this.transform,!0),this.transform=e,this.cameraHelper=t}getCenter(){return new V(this.transform.center.lng,this.transform.center.lat)}setCenter(e,t){return this.jumpTo({center:e},t)}getCenterElevation(){return this.transform.elevation}setCenterElevation(e,t){return this.jumpTo({elevation:e},t),this}getCenterClampedToGround(){return this._centerClampedToGround}setCenterClampedToGround(e){this._centerClampedToGround=e}panBy(e,t,n){return e=l.convert(e).mult(-1),this.panTo(this.transform.center,z({offset:e},t),n)}panTo(e,t,n){return this.easeTo(z({center:e},t),n)}getZoom(){return this.transform.zoom}setZoom(e,t){return this.jumpTo({zoom:e},t),this}zoomTo(e,t,n){return this.easeTo(z({zoom:e},t),n)}zoomIn(e,t){return this.zoomTo(un(this.getZoom()+1,this._zoomSnap),e,t),this}zoomOut(e,t){return this.zoomTo(un(this.getZoom()-1,this._zoomSnap),e,t),this}getVerticalFieldOfView(){return this.transform.fov}setVerticalFieldOfView(e,t){return e!=this.transform.fov&&(this.transform.setFov(e),this.fire(new G(`movestart`,t)).fire(new G(`move`,t)).fire(new G(`moveend`,t))),this}getBearing(){return this.transform.bearing}setZoomSnap(e){return this._zoomSnap=e,this}getZoomSnap(){return this._zoomSnap}setBearing(e,t){return this.jumpTo({bearing:e},t),this}getPadding(){return this.transform.padding}setPadding(e,t){return this.jumpTo({padding:e},t),this}rotateTo(e,t,n){return this.easeTo(z({bearing:e},t),n)}resetNorth(e,t){return this.rotateTo(0,z({duration:1e3},e),t),this}resetNorthPitch(e,t){return this.easeTo(z({bearing:0,pitch:0,roll:0,duration:1e3},e),t),this}snapToNorth(e,t){return Math.abs(this.getBearing()){g.easeFunc(t),this.terrain&&!e.freezeElevation&&this._updateElevation(t),this.applyUpdatedTransform(r),this._fireMoveEvents(n)},t=>{this.terrain&&e.freezeElevation&&this._finalizeElevation(),this._afterEase(n,t)},e),this}_prepareEase(e,t,n={}){this._moving=!0,!t&&!n.moving&&this.fire(new G(`movestart`,e)),this._zooming&&!n.zooming&&this.fire(new G(`zoomstart`,e)),this._rotating&&!n.rotating&&this.fire(new G(`rotatestart`,e)),this._pitching&&!n.pitching&&this.fire(new G(`pitchstart`,e)),this._rolling&&!n.rolling&&this.fire(new G(`rollstart`,e))}_prepareElevation(e){this._elevationCenter=e,this._elevationStart=this.transform.elevation,this._elevationTarget=this.terrain.getElevationForLngLat(e,this.transform),this.elevationFreeze=!0}_updateElevation(e){(this._elevationStart===void 0||this._elevationCenter===void 0)&&this._prepareElevation(this.transform.center),this.transform.setMinElevationForCurrentTile(this.terrain.getMinTileElevationForLngLatZoom(this._elevationCenter,this.transform.tileZoom));let t=this.terrain.getElevationForLngLat(this._elevationCenter,this.transform);if(e<1&&t!==this._elevationTarget){let n=this._elevationTarget-this._elevationStart,r=(t-(n*e+this._elevationStart))/(1-e);this._elevationStart+=e*(n-r),this._elevationTarget=t}this.transform.setElevation(on.number(this._elevationStart,this._elevationTarget,e))}_finalizeElevation(){this.elevationFreeze=!1,this.getCenterClampedToGround()&&this.transform.recalculateZoomAndCenter(this.terrain)}getTransformForUpdate(){return!this.transformCameraUpdate&&!this.terrain?this.transform:(this._requestedCameraState||=this.transform.clone(),this._requestedCameraState)}_elevateCameraIfInsideTerrain(e){if(!this.terrain&&e.elevation>=0&&e.pitch<=90)return{};let t=e.getCameraLngLat(),n=e.getCameraAltitude(),r=this.terrain?this.terrain.getElevationForLngLatZoom(t,e.zoom):0;if(nthis._elevateCameraIfInsideTerrain(e)),this.transformCameraUpdate&&t.push(e=>this.transformCameraUpdate(e)),!t.length)return;let n=e.clone();for(let e of t){let t=n.clone(),{center:r,zoom:i,roll:a,pitch:o,bearing:s,elevation:c}=e(t);r&&t.setCenter(r),c!==void 0&&t.setElevation(c),i!==void 0&&t.setZoom(i),a!==void 0&&t.setRoll(a),o!==void 0&&t.setPitch(o),s!==void 0&&t.setBearing(s),n.apply(t,!1)}this.transform.apply(n,!1)}_fireMoveEvents(e){this.fire(new G(`move`,e)),this._zooming&&this.fire(new G(`zoom`,e)),this._rotating&&this.fire(new G(`rotate`,e)),this._pitching&&this.fire(new G(`pitch`,e)),this._rolling&&this.fire(new G(`roll`,e))}_afterEase(e,t){if(this._easeId&&t&&this._easeId===t)return;delete this._easeId;let n=this._zooming,r=this._rotating,i=this._pitching,a=this._rolling;this._moving=!1,this._zooming=!1,this._rotating=!1,this._pitching=!1,this._rolling=!1,this._padding=!1,n&&this.fire(new G(`zoomend`,e)),r&&this.fire(new G(`rotateend`,e)),i&&this.fire(new G(`pitchend`,e)),a&&this.fire(new G(`rollend`,e)),this.fire(new G(`moveend`,e))}flyTo(e,n){if(!e.essential&&Rr.prefersReducedMotion){let t=hr(e,[`center`,`zoom`,`bearing`,`pitch`,`roll`,`elevation`,`padding`]);return this.jumpTo(t,n)}this.stop(),e=z({offset:[0,0],speed:1.2,curve:1.42,easing:t},e),`zoom`in e&&this._zoomSnap&&(e.zoom=un(e.zoom,this._zoomSnap));let r=this.getTransformForUpdate(),i=r.bearing,a=r.pitch,o=r.roll,s=r.padding,c=`bearing`in e?this._normalizeBearing(e.bearing,i):i,u=`pitch`in e?+e.pitch:a,d=`roll`in e?this._normalizeBearing(e.roll,o):o,f=`padding`in e?e.padding:r.padding,p=l.convert(e.offset),m=r.centerPoint.add(p),h=r.screenPointToLocation(m),g=this.cameraHelper.handleFlyTo(r,{bearing:c,pitch:u,roll:d,padding:f,locationAtOffset:h,offsetAsPoint:p,center:e.center,minZoom:e.minZoom,zoom:e.zoom}),_=e.curve,v=Math.max(r.width,r.height),y=v/g.scaleOfZoom,b=g.pixelPathLength,x=v/g.scaleOfMinZoom;_=Math.min(_,Math.sqrt(x/b*2));let S=_*_;function C(e){let t=(y*y-v*v+(e?-1:1)*S*S*b*b)/(2*(e?y:v)*S*b);return Math.log(Math.sqrt(t*t+1)-t)}function w(e){return(Math.exp(e)-Math.exp(-e))/2}function T(e){return(Math.exp(e)+Math.exp(-e))/2}function E(e){return w(e)/T(e)}let D=C(!1),ee=function(e){return T(D)/T(D+_*e)},O=function(e){return v*((T(D)*E(D+_*e)-w(D))/S)/b},k=(C(!0)-D)/_;if(Math.abs(b)<2e-6||!isFinite(k)){if(Math.abs(v-y)<1e-6)return this.easeTo(e,n);let t=y0,ee=e=>Math.exp(t*_*e)}if(`duration`in e)e.duration=+e.duration;else{let t=`screenSpeed`in e?+e.screenSpeed/_:+e.speed;e.duration=1e3*k/t}return e.maxDuration&&e.duration>e.maxDuration&&(e.duration=0),this._zooming=!0,this._rotating=i!==c,this._pitching=u!==a,this._rolling=d!==o,this._padding=!r.isPaddingEqual(f),this._prepareEase(n,!1),this.terrain&&this._prepareElevation(g.targetCenter),this._ease(t=>{let l=t*k,h=1/ee(l),_=O(l);this._rotating&&r.setBearing(on.number(i,c,t)),this._pitching&&r.setPitch(on.number(a,u,t)),this._rolling&&r.setRoll(on.number(o,d,t)),this._padding&&(r.interpolatePadding(s,f,t),m=r.centerPoint.add(p)),g.easeFunc(t,h,_,m),this.terrain&&!e.freezeElevation&&this._updateElevation(t),this.applyUpdatedTransform(r),this._fireMoveEvents(n)},()=>{this.terrain&&e.freezeElevation&&this._finalizeElevation(),this._afterEase(n)},e),this}isEasing(){return!!this._easeFrameId}stop(e){return this._stop(e)}_stop(e,t){if(this._easeFrameId&&(this._cancelRenderFrame(this._easeFrameId),delete this._easeFrameId,delete this._onEaseFrame),this._onEaseEnd){let e=this._onEaseEnd;delete this._onEaseEnd,e.call(this,t)}return e||this._stopHandlers(),this}_ease(e,t,n){n.animate===!1||n.duration===0?(e(1),t()):(this._easeStart=U(),this._easeOptions=n,this._onEaseFrame=e,this._onEaseEnd=t,this._easeFrameId=this._requestRenderFrame(this._renderFrameCallback))}_normalizeBearing(e,t){e=Or(e,-180,180);let n=Math.abs(e-t);return Math.abs(e-360-t)MapLibre`};var $p=class{constructor(e=Qp){this._toggleAttribution=()=>{this._container.classList.contains(`maplibregl-compact`)&&(this._container.classList.contains(`maplibregl-compact-show`)?(this._container.setAttribute(`open`,``),this._container.classList.remove(`maplibregl-compact-show`)):(this._container.classList.add(`maplibregl-compact-show`),this._container.removeAttribute(`open`)))},this._updateData=e=>{e&&(e.type===`terrain`||e.dataType===`style`||e.dataType===`source`&&(e.sourceDataType===`metadata`||e.sourceDataType===`visibility`))&&this._updateAttributions()},this._updateCompact=()=>{this._map.getCanvasContainer().offsetWidth<=640||this._compact?this._compact===!1?this._container.setAttribute(`open`,``):!this._container.classList.contains(`maplibregl-compact`)&&!this._container.classList.contains(`maplibregl-attrib-empty`)&&(this._container.setAttribute(`open`,``),this._container.classList.add(`maplibregl-compact`,`maplibregl-compact-show`)):(this._container.setAttribute(`open`,``),this._container.classList.contains(`maplibregl-compact`)&&this._container.classList.remove(`maplibregl-compact`,`maplibregl-compact-show`))},this._updateCompactMinimize=()=>{this._container.classList.contains(`maplibregl-compact`)&&this._container.classList.contains(`maplibregl-compact-show`)&&this._container.classList.remove(`maplibregl-compact-show`)},this.options=e}getDefaultPosition(){return`bottom-right`}onAdd(e){return this._map=e,this._compact=this.options.compact,this._container=W.create(`details`,`maplibregl-ctrl maplibregl-ctrl-attrib`),this._compactButton=W.create(`summary`,`maplibregl-ctrl-attrib-button`,this._container),this._compactButton.addEventListener(`click`,this._toggleAttribution),this._setElementTitle(this._compactButton,`ToggleAttribution`),this._innerContainer=W.create(`div`,`maplibregl-ctrl-attrib-inner`,this._container),this._updateAttributions(),this._updateCompact(),this._map.on(`styledata`,this._updateData),this._map.on(`sourcedata`,this._updateData),this._map.on(`terrain`,this._updateData),this._map.on(`resize`,this._updateCompact),this._map.on(`drag`,this._updateCompactMinimize),this._container}onRemove(){this._container.remove(),this._map.off(`styledata`,this._updateData),this._map.off(`sourcedata`,this._updateData),this._map.off(`terrain`,this._updateData),this._map.off(`resize`,this._updateCompact),this._map.off(`drag`,this._updateCompactMinimize),this._map=void 0,this._compact=void 0,this._attribHTML=void 0}_setElementTitle(e,t){let n=this._map._getUIString(`AttributionControl.${t}`);e.title=n,e.setAttribute(`aria-label`,n)}_updateAttributions(){if(!this._map.style)return;let e=[];if(this.options.customAttribution&&(Array.isArray(this.options.customAttribution)?e=e.concat(this.options.customAttribution.map(e=>typeof e==`string`?e:``)):typeof this.options.customAttribution==`string`&&e.push(this.options.customAttribution)),this._map.style.stylesheet){let e=this._map.style.stylesheet;this.styleOwner=e.owner,this.styleId=e.id}let t=this._map.style.tileManagers;for(let n in t){let r=t[n];if(r.used||r.usedForTerrain){let t=r.getSource();t.attribution&&!e.includes(t.attribution)&&e.push(t.attribution)}}e=e.filter(e=>String(e).trim()),e.sort((e,t)=>e.length-t.length),e=e.filter((t,n)=>{for(let r=n+1;r{let e=this._container.children;if(e.length){let t=e[0];this._map.getCanvasContainer().offsetWidth<=640||this._compact?this._compact!==!1&&t.classList.add(`maplibregl-compact`):t.classList.remove(`maplibregl-compact`)}},this.options=e}getDefaultPosition(){return`bottom-left`}onAdd(e){this._map=e,this._compact=this.options?.compact,this._container=W.create(`div`,`maplibregl-ctrl`);let t=W.create(`a`,`maplibregl-ctrl-logo`);return t.target=`_blank`,t.rel=`noopener nofollow`,t.href=`https://maplibre.org/`,t.setAttribute(`aria-label`,this._map._getUIString(`LogoControl.Title`)),t.setAttribute(`rel`,`noopener nofollow`),this._container.appendChild(t),this._container.style.display=`block`,this._map.on(`resize`,this._updateCompact),this._updateCompact(),this._container}onRemove(){this._container.remove(),this._map.off(`resize`,this._updateCompact),this._map=void 0,this._compact=void 0}},tm=class{constructor(){this._queue=[],this._id=0,this._cleared=!1,this._currentlyRunning=!1}add(e){let t=++this._id;return this._queue.push({callback:e,id:t,cancelled:!1}),t}remove(e){let t=this._currentlyRunning,n=t?this._queue.concat(t):this._queue;for(let t of n)if(t.id===e){t.cancelled=!0;return}}run(e=0){if(this._currentlyRunning)throw Error(`Attempting to run(), but is already running.`);let t=this._currentlyRunning=this._queue;this._queue=[];for(let n of t)if(!n.cancelled&&(n.callback(e),this._cleared))break;this._cleared=!1,this._currentlyRunning=!1}clear(){this._currentlyRunning&&(this._cleared=!0),this._queue=[]}};const nm={background:!0,fill:!0,line:!0,raster:!0,hillshade:!0,"color-relief":!0};var rm=class{constructor(e,t){this.painter=e,this.terrain=t,this.rttSize=t.tileManager.tileSize*t.qualityFactor}getTexture(e){return e.getRTT(this._stacks.length-1).texture}prepareForRender(e,t){this._stacks=[],this._prevType=null,this._rttTiles=[],this._renderableTiles=this.terrain.tileManager.getRenderableTiles(),this._renderableLayerIds=e._order.filter(n=>!e._layers[n].isHidden(t));let n=new Set;for(let t of this._renderableLayerIds){let r=e._layers[t],i=r.source;i&&nm[r.type]&&n.add(i)}this._coordsAscending={},this._rttFingerprints={};for(let t of n){let n=e.tileManagers[t];if(!n)continue;this._coordsAscending[t]={};let r=this._coordsAscending[t],i=n.getSource(),a=i instanceof Ja?i.terrainTileRanges:null;for(let e of n.getVisibleCoordinates()){let t=this.terrain.tileManager.getTerrainCoords(e,a);for(let e in t)r[e]||=[],r[e].push(t[e])}this._rttFingerprints[t]={};let o=this._rttFingerprints[t],s=n.getState().revision;for(let e in r)o[e]=`${r[e].map(e=>e.key).sort().join()}#${s}`}for(let e of this._renderableTiles)for(let t in this._rttFingerprints){let n=this._rttFingerprints[t][e.tileID.key];n&&n!==e.rttFingerprint[t]&&e.releaseRTT(this.painter)}}renderLayer(e,t){if(e.isHidden(this.painter.transform.zoom))return!1;let n={...t,isRenderingToTexture:!0},r=e.type,i=this.painter,a=this._renderableLayerIds[this._renderableLayerIds.length-1]===e.id;if(nm[r]&&((!this._prevType||!nm[this._prevType])&&this._stacks.push([]),this._prevType=r,this._stacks[this._stacks.length-1].push(e.id),!a))return!0;if(nm[this._prevType]||nm[r]&&a){this._prevType=r;let e=this._stacks.length-1,t=this._stacks[e]||[];for(let r of this._renderableTiles){if(this._rttTiles.push(r),r.getRTT(e))continue;let a=r.acquireRTT(i,e,this.rttSize);i.bindRTT(a),i.context.clear({color:R.transparent,stencil:0}),i.currentStencilSource=void 0;for(let e of t){let t=i.style._layers[e],a=t.source?this._coordsAscending[t.source][r.tileID.key]:[r.tileID];i.context.viewport.set([0,0,this.rttSize,this.rttSize]),i.renderTileClippingMasks(t,a,!0),i.renderLayer(i,i.style.tileManagers[t.source],t,a,n),t.source&&(r.rttFingerprint[t.source]=this._rttFingerprints[t.source][r.tileID.key])}}return Bf(this.painter,this.terrain,this._rttTiles,n),this._rttTiles=[],nm[r]}return!1}};const im={"AttributionControl.ToggleAttribution":`Toggle attribution`,"AttributionControl.MapFeedback":`Map feedback`,"FullscreenControl.Enter":`Enter fullscreen`,"FullscreenControl.Exit":`Exit fullscreen`,"GeolocateControl.FindMyLocation":`Find my location`,"GeolocateControl.LocationNotAvailable":`Location not available`,"LogoControl.Title":`MapLibre logo`,"Map.Title":`Map`,"Marker.Title":`Map marker`,"NavigationControl.ResetBearing":`Drag to rotate map, click to reset north`,"NavigationControl.ZoomIn":`Zoom in`,"NavigationControl.ZoomOut":`Zoom out`,"Popup.Close":`Close popup`,"ScaleControl.Feet":`ft`,"ScaleControl.Meters":`m`,"ScaleControl.Kilometers":`km`,"ScaleControl.Miles":`mi`,"ScaleControl.NauticalMiles":`nm`,"GlobeControl.Enable":`Enable globe`,"GlobeControl.Disable":`Disable globe`,"TerrainControl.Enable":`Enable terrain`,"TerrainControl.Disable":`Disable terrain`,"CooperativeGesturesHandler.WindowsHelpText":`Use Ctrl + scroll to zoom the map`,"CooperativeGesturesHandler.MacHelpText":`Use ⌘ + scroll to zoom the map`,"CooperativeGesturesHandler.MobileHelpText":`Use two fingers to move the map`},am=Ar,om={hash:!1,interactive:!0,bearingSnap:7,zoomSnap:0,attributionControl:Qp,maplibreLogo:!1,refreshExpiredTiles:!0,canvasContextAttributes:{antialias:!1,preserveDrawingBuffer:!1,powerPreference:`high-performance`,failIfMajorPerformanceCaveat:!1,desynchronized:!1,contextType:void 0},scrollZoom:!0,minZoom:-2,maxZoom:22,minPitch:0,maxPitch:60,boxZoom:!0,dragRotate:!0,dragPan:!0,keyboard:!0,doubleClickZoom:!0,touchZoomRotate:!0,touchPitch:!0,cooperativeGestures:!1,trackResize:!0,center:[0,0],elevation:0,zoom:0,bearing:0,pitch:0,roll:0,renderWorldCopies:!0,maxTileCacheSize:null,maxTileCacheZoomLevels:k.MAX_TILE_CACHE_ZOOM_LEVELS,transformRequest:null,transformCameraUpdate:null,transformConstrain:null,fadeDuration:300,crossSourceCollisions:!0,clickTolerance:3,localIdeographFontFamily:`sans-serif`,pitchWithRotate:!0,rollEnabled:!1,rotateSpeed:.8,pitchSpeed:-.5,reduceMotion:void 0,validateStyle:!0,maxCanvasSize:[4096,4096],cancelPendingTileRequestsWhileZooming:!0,centerClampedToGround:!0,terrainSkirtLength:`auto`,zoomLevelsToOverscale:4,anisotropicFilterPitch:20};var sm=class extends h{get _ownerWindow(){return this._container?.ownerDocument?.defaultView||window}constructor(e){super(),this._idleTriggered=!1,this._crossFadingFactor=1,this._renderTaskQueue=new tm,this._controls=[],this._mapId=Se(),this._missingStyleImageResolver=null,this._lostContextStyle={style:null,images:null},this._contextLost=e=>{if(e.preventDefault(),this._frameRequest&&=(this._frameRequest.abort(),null),this.painter.destroy(),this._lostContextStyle=this._getStyleAndImages(),!this.style){this.fire(new ei(`webglcontextlost`,{originalEvent:e}));return}for(let e of Object.values(this.style._layers))if(e.type===`custom`&&console.warn(`Custom layer with id '${e.id}' cannot be restored after WebGL context loss. You will need to re-add it manually after context restoration.`),e._listeners)for(let[t]of Object.entries(e._listeners))console.warn(`Custom layer with id '${e.id}' had event listeners for event '${t}' which cannot be restored after WebGL context loss. You will need to re-add them manually after context restoration.`);this.style.destroy(),this.style=null,this.fire(new ei(`webglcontextlost`,{originalEvent:e}))},this._contextRestored=e=>{if(this._lostContextStyle.style&&this.setStyle(this._lostContextStyle.style,{diff:!1}),this._lostContextStyle.images&&this.style){this.style.imageManager.images=this._lostContextStyle.images;for(let e in this._lostContextStyle.images){let t=this._lostContextStyle.images[e];t.isWebGLImage&&this.style.imageManager.updateImage(e,t,!1)}}this._lostContextStyle={style:null,images:null};try{this._setupPainter()}catch(e){this.fire(new H(e));return}this.resize(),this._update(),this._resizeInternal(),this.fire(new ei(`webglcontextrestored`,{originalEvent:e}))},this._onMapScroll=e=>{if(e.target===this._container)return this._container.scrollTop=0,this._container.scrollLeft=0,!1},this._onWindowOnline=()=>{this._update()};let t={...om,...e,canvasContextAttributes:{...om.canvasContextAttributes,...e.canvasContextAttributes}};if(t.minZoom!=null&&t.maxZoom!=null&&t.minZoom>t.maxZoom)throw Error(`maxZoom must be greater than or equal to minZoom`);if(t.minPitch!=null&&t.maxPitch!=null&&t.minPitch>t.maxPitch)throw Error(`maxPitch must be greater than or equal to minPitch`);if(t.minPitch!=null&&t.minPitch<0)throw Error(`minPitch must be greater than or equal to 0`);if(t.maxPitch!=null&&t.maxPitch>180)throw Error(`maxPitch must be less than or equal to 180`);this._camera=new Zp({minZoom:t.minZoom,maxZoom:t.maxZoom,minPitch:t.minPitch,maxPitch:t.maxPitch,bearingSnap:t.bearingSnap,zoomSnap:t.zoomSnap,renderWorldCopies:t.renderWorldCopies,centerClampedToGround:t.centerClampedToGround,terrain:this.terrain,transformConstrain:t.transformConstrain,requestRenderFrame:e=>this._requestRenderFrame(e),cancelRenderFrame:e=>this._cancelRenderFrame(e),transformCameraUpdate:t.transformCameraUpdate,stopHandlers:()=>this._handlers?.stop(!1)}),this._camera.setEventedParent(this),this._interactive=t.interactive,this._maxTileCacheSize=t.maxTileCacheSize,this._maxTileCacheZoomLevels=t.maxTileCacheZoomLevels,this._canvasContextAttributes={...t.canvasContextAttributes},this._trackResize=t.trackResize===!0,this._terrainSkirtLength=t.terrainSkirtLength,this._refreshExpiredTiles=t.refreshExpiredTiles===!0,this._fadeDuration=t.fadeDuration,this._crossSourceCollisions=t.crossSourceCollisions===!0,this._collectResourceTiming=t.collectResourceTiming===!0,this._locale={...im,...t.locale},this._clickTolerance=t.clickTolerance,this._overridePixelRatio=t.pixelRatio,this._maxCanvasSize=t.maxCanvasSize,this._zoomLevelsToOverscale=t.zoomLevelsToOverscale,this.cancelPendingTileRequestsWhileZooming=t.cancelPendingTileRequestsWhileZooming===!0,this.setAnisotropicFilterPitch(t.anisotropicFilterPitch),t.reduceMotion!==void 0&&(Rr.prefersReducedMotion=t.reduceMotion),this._requestManager=new Wr(t.transformRequest),this._container=this._resolveContainer(t.container),t.maxBounds&&this.setMaxBounds(t.maxBounds),this._setupContainer();try{this._setupPainter()}catch(e){throw this._cleanupContainer(),e}this._imageQueueHandle=Ur.addThrottleControl(()=>this.isMoving()),this.on(`move`,()=>this._update(!1)),this.on(`moveend`,()=>this._update(!1)),this.on(`zoom`,()=>this._update(!0)),this.on(`terrain`,()=>{this.painter.terrainFacilitator.depthDirty=!0,this._update(!0)}),this.once(`idle`,()=>this._idleTriggered=!0),this._handlers=new Xp(this,this._camera,t),typeof window<`u`&&(this._ownerWindow.addEventListener(`online`,this._onWindowOnline,!1),this._setupResizeObserver());let n=typeof t.hash==`string`&&t.hash||void 0;this._hash=t.hash?new Yf(n).addTo(this):void 0,this._hash?._onHashChange()||(this.jumpTo({center:t.center,elevation:t.elevation,zoom:t.zoom,bearing:t.bearing,pitch:t.pitch,roll:t.roll}),t.bounds&&(this.resize(),this.fitBounds(t.bounds,z({},t.fitBoundsOptions,{duration:0}))));let r=typeof t.style==`string`||t.style?.projection?.type!==`globe`;this.resize(null,r),this._localIdeographFontFamily=t.localIdeographFontFamily,this._validateStyle=t.validateStyle,t.style&&this.setStyle(t.style,{localIdeographFontFamily:t.localIdeographFontFamily}),t.attributionControl&&this.addControl(new $p(typeof t.attributionControl==`boolean`?void 0:t.attributionControl)),t.maplibreLogo&&this.addControl(new em,t.logoPosition),this.on(`style.load`,()=>{if(r||this._resizeTransform(),this._camera.transform.unmodified){let e=hr(this.style.stylesheet,[`center`,`zoom`,`bearing`,`pitch`,`roll`]);this.jumpTo(e)}}),this.on(`data`,e=>{this._update(e.dataType===`style`),this.fire(e.dataType===`style`?new qr(`styledata`,e):new K(`sourcedata`,e))}),this.on(`dataloading`,e=>{this.fire(e.dataType===`style`?new qr(`styledataloading`,e):new K(`sourcedataloading`,e))}),this.on(`dataabort`,e=>{this.fire(new K(`sourcedataabort`,e))})}_getMapId(){return this._mapId}setGlobalStateProperty(e,t){return this.style.setGlobalStateProperty(e,t),this._update(!0)}getGlobalState(){return this.style.getGlobalState()}addControl(e,t){if(t===void 0&&(t=e.getDefaultPosition?e.getDefaultPosition():`top-right`),!e?.onAdd)return this.fire(new H(Error(`Invalid argument to map.addControl(). Argument must be a control with onAdd and onRemove methods.`)));let n=e.onAdd(this);this._controls.push(e);let r=this._controlPositions[t];return t.includes(`bottom`)?r.insertBefore(n,r.firstChild):r.appendChild(n),this}removeControl(e){if(!e?.onRemove)return this.fire(new H(Error(`Invalid argument to map.removeControl(). Argument must be a control with onAdd and onRemove methods.`)));let t=this._controls.indexOf(e);return t>-1&&this._controls.splice(t,1),e.onRemove(this),this}hasControl(e){return this._controls.includes(e)}coveringTiles(e){return So(this._camera.transform,e)}setTransformCameraUpdate(e){this._camera.transformCameraUpdate=e}getCenter(){return new V(this._camera.transform.center.lng,this._camera.transform.center.lat)}setCenter(e,t){return this._camera.setCenter(e,t),this}getCenterElevation(){return this._camera.transform.elevation}setCenterElevation(e,t){return this._camera.setCenterElevation(e,t),this}setCenterClampedToGround(e){this._camera.setCenterClampedToGround(e)}panBy(e,t,n){return this._camera.panBy(e,t,n),this}panTo(e,t,n){return this._camera.panTo(e,t,n),this}getZoom(){return this._camera.transform.zoom}setZoom(e,t){return this._camera.setZoom(e,t),this}zoomTo(e,t,n){return this._camera.zoomTo(e,t,n),this}zoomIn(e,t){return this._camera.zoomIn(e,t),this}zoomOut(e,t){return this._camera.zoomOut(e,t),this}getVerticalFieldOfView(){return this._camera.transform.fov}setVerticalFieldOfView(e,t){return this._camera.setVerticalFieldOfView(e,t),this}getBearing(){return this._camera.transform.bearing}setBearing(e,t){return this._camera.setBearing(e,t),this}getZoomSnap(){return this._camera.getZoomSnap()}setZoomSnap(e){return this._camera.setZoomSnap(e),this}getPadding(){return this._camera.transform.padding}setPadding(e,t){return this._camera.setPadding(e,t),this}rotateTo(e,t,n){return this._camera.rotateTo(e,t,n),this}resetNorth(e,t){return this._camera.resetNorth(e,t),this}resetNorthPitch(e,t){return this._camera.resetNorthPitch(e,t),this}snapToNorth(e,t){return this._camera.snapToNorth(e,t),this}getPitch(){return this._camera.transform.pitch}setPitch(e,t){return this._camera.setPitch(e,t),this}getRoll(){return this._camera.transform.roll}setRoll(e,t){return this._camera.setRoll(e,t),this}cameraForBounds(e,t){return this._camera.cameraForBounds(e,t)}fitBounds(e,t,n){return this._camera.fitBounds(e,t,n),this}fitScreenCoordinates(e,t,n,r,i){return this._camera.fitScreenCoordinates(e,t,n,r,i),this}jumpTo(e,t){return this._camera.jumpTo(e,t),this}calculateCameraOptionsFromCameraLngLatAltRotation(e,t,n,r,i){return this._camera.calculateCameraOptionsFromCameraLngLatAltRotation(e,t,n,r,i)}easeTo(e,t){return this._camera.easeTo(e,t),this}flyTo(e,t){return this._camera.flyTo(e,t),this}stop(){return this._camera.stop(),this}queryTerrainElevation(e){return this.terrain?this.terrain.getElevationForLngLat(V.convert(e),this._camera.transform):null}getCenterClampedToGround(){return this._camera.getCenterClampedToGround()}calculateCameraOptionsFromTo(e,t,n,r){return r==null&&this.terrain&&(r=this.terrain.getElevationForLngLat(n,this._camera.transform)),this._camera.calculateCameraOptionsFromTo(e,t,n,r)}resize(e,t=!0){if(this._lostContextStyle.style!==null)return this;this._resizeInternal(t);let n=!this._camera._moving;return n&&(this.stop(),this.fire(new G(`movestart`,e)).fire(new G(`move`,e))),this.fire(new Gr(`resize`,e)),n&&this.fire(new G(`moveend`,e)),this}_resizeInternal(e=!0){let[t,n]=this._containerDimensions(),r=this._getClampedPixelRatio(t,n);if(this._resizeCanvas(t,n,r),this.painter.resize(t,n,r),this.painter.overLimit()){let e=this.painter.context.gl;this._maxCanvasSize=[e.drawingBufferWidth,e.drawingBufferHeight];let r=this._getClampedPixelRatio(t,n);this._resizeCanvas(t,n,r),this.painter.resize(t,n,r)}this._resizeTransform(e)}_resizeTransform(e=!0){let[t,n]=this._containerDimensions();this._camera.transform.resize(t,n,e),this._camera._requestedCameraState?.resize(t,n,e)}_getClampedPixelRatio(e,t){let{0:n,1:r}=this._maxCanvasSize,i=this.getPixelRatio(),a=e*i,o=t*i,s=a>n?n/a:1,c=o>r?r/o:1;return Math.min(s,c)*i}getPixelRatio(){return this._overridePixelRatio??devicePixelRatio}setPixelRatio(e){this._overridePixelRatio=e,this.resize()}getBounds(){return this._camera.transform.getBounds()}getMaxBounds(){return this._camera.transform.getMaxBounds()}setMaxBounds(e){return this._camera.transform.setMaxBounds(_a.convert(e)),this._update()}setMinZoom(e){if(e??=-2,e>=-2&&e<=this._camera.transform.maxZoom){let t=this._camera.transform.zoom,n=this._camera.getTransformForUpdate();return n.setMinZoom(e),this._camera.applyUpdatedTransform(n),this._update(),t!==this._camera.transform.zoom&&this.fire(new G(`zoomstart`)).fire(new G(`zoom`)).fire(new G(`zoomend`)).fire(new G(`movestart`)).fire(new G(`move`)).fire(new G(`moveend`)),this}throw Error(`minZoom must be between -2 and the current maxZoom, inclusive`)}getMinZoom(e=!1){let t=this._camera.transform;return e?t.applyConstrain(t.center,t.minZoom).zoom:t.minZoom}setMaxZoom(e){if(e??=22,e>=this._camera.transform.minZoom){let t=this._camera.transform.zoom,n=this._camera.getTransformForUpdate();return n.setMaxZoom(e),this._camera.applyUpdatedTransform(n),this._update(),t!==this._camera.transform.zoom&&this.fire(new G(`zoomstart`)).fire(new G(`zoom`)).fire(new G(`zoomend`)).fire(new G(`movestart`)).fire(new G(`move`)).fire(new G(`moveend`)),this}throw Error(`maxZoom must be greater than the current minZoom`)}getMaxZoom(){return this._camera.transform.maxZoom}setMinPitch(e){if(e??=0,e<0)throw Error(`minPitch must be greater than or equal to 0`);if(e>=0&&e<=this._camera.transform.maxPitch){let t=this._camera.transform.pitch,n=this._camera.getTransformForUpdate();return n.setMinPitch(e),this._camera.applyUpdatedTransform(n),this._update(),t!==this._camera.transform.pitch&&this.fire(new G(`pitchstart`)).fire(new G(`pitch`)).fire(new G(`pitchend`)).fire(new G(`movestart`)).fire(new G(`move`)).fire(new G(`moveend`)),this}throw Error(`minPitch must be between 0 and the current maxPitch, inclusive`)}getMinPitch(){return this._camera.transform.minPitch}setMaxPitch(e){if(e??=60,e>180)throw Error(`maxPitch must be less than or equal to 180`);if(e>=this._camera.transform.minPitch){let t=this._camera.transform.pitch,n=this._camera.getTransformForUpdate();return n.setMaxPitch(e),this._camera.applyUpdatedTransform(n),this._update(),t!==this._camera.transform.pitch&&this.fire(new G(`pitchstart`)).fire(new G(`pitch`)).fire(new G(`pitchend`)).fire(new G(`movestart`)).fire(new G(`move`)).fire(new G(`moveend`)),this}throw Error(`maxPitch must be greater than the current minPitch`)}getMaxPitch(){return this._camera.transform.maxPitch}getAnisotropicFilterPitch(){return this._anisotropicFilterPitch}setAnisotropicFilterPitch(e){if(e??=20,e>180)throw Error(`anisotropicFilterPitch must be less than or equal to 180`);if(e<0)throw Error(`anisotropicFilterPitch must be greater than or equal to 0`);return this._anisotropicFilterPitch=e,this._update()}getRenderWorldCopies(){return this._camera.transform.renderWorldCopies}setRenderWorldCopies(e){return this._camera.transform.setRenderWorldCopies(e),this._update()}setTransformConstrain(e){return this._camera.transform.setConstrainOverride(e),this._update()}project(e){return this._camera.transform.locationToScreenPoint(V.convert(e),this.style&&this.terrain)}unproject(e){return this._camera.transform.screenPointToLocation(l.convert(e),this.terrain)}isMoving(){return this._camera.isMoving()||this._handlers?.isMoving()||!1}isZooming(){return this._camera.isZooming()||this._handlers?.isZooming()||!1}isRotating(){return this._camera.isRotating()||this._handlers?.isRotating()||!1}_createDelegatedListener(e,t,n){if(e===`mouseenter`||e===`mouseover`){let r=!1;return{layers:t,listener:n,delegates:{mousemove:i=>{let a=t.filter(e=>this.getLayer(e)),o=a.length===0?[]:this.queryRenderedFeatures(i.point,{layers:a});o.length?r||(r=!0,n.call(this,new Jr(e,this,i.originalEvent,{features:o}))):r=!1},mouseout:()=>{r=!1}}}}if(e===`mouseleave`||e===`mouseout`){let r=!1;return{layers:t,listener:n,delegates:{mousemove:i=>{let a=t.filter(e=>this.getLayer(e));(a.length===0?[]:this.queryRenderedFeatures(i.point,{layers:a})).length?r=!0:r&&(r=!1,n.call(this,new Jr(e,this,i.originalEvent)))},mouseout:t=>{r&&(r=!1,n.call(this,new Jr(e,this,t.originalEvent)))}}}}{let r=e=>{let r=t.filter(e=>this.getLayer(e)),i=r.length===0?[]:this.queryRenderedFeatures(e.point,{layers:r});i.length&&(e.features=i,n.call(this,e),delete e.features)};return{layers:t,listener:n,delegates:{[e]:r}}}}_saveDelegatedListener(e,t){this._delegatedListeners||={},this._delegatedListeners[e]||=[],this._delegatedListeners[e].push(t)}_removeDelegatedListener(e,t,n){if(!this._delegatedListeners?.[e])return;let r=this._delegatedListeners[e];for(let e=0;et.includes(e))){for(let e in i.delegates)this.off(e,i.delegates[e]);r.splice(e,1);return}}}on(e,t,n){if(n===void 0)return super.on(e,t);let r=typeof t==`string`?[t]:t,i=this._createDelegatedListener(e,r,n);this._saveDelegatedListener(e,i);for(let e in i.delegates)this.on(e,i.delegates[e]);return{unsubscribe:()=>{this._removeDelegatedListener(e,r,n)}}}once(e,t,n){if(n===void 0)return super.once(e,t);let r=typeof t==`string`?[t]:t,i=this._createDelegatedListener(e,r,n);for(let t in i.delegates){let a=i.delegates[t];i.delegates[t]=(...t)=>{this._removeDelegatedListener(e,r,n),a(...t)}}this._saveDelegatedListener(e,i);for(let e in i.delegates)this.once(e,i.delegates[e]);return this}off(e,t,n){if(n===void 0)return super.off(e,t);let r=typeof t==`string`?[t]:t;return this._removeDelegatedListener(e,r,n),this}queryRenderedFeatures(e,t){if(!this.style)return[];let n,r=e instanceof l||Array.isArray(e),i=r?e:[[0,0],[this._camera.transform.width,this._camera.transform.height]];if(t||=(r?{}:e)||{},i instanceof l||typeof i[0]==`number`)n=[l.convert(i)];else{let e=l.convert(i[0]),t=l.convert(i[1]);n=[e,new l(t.x,e.y),t,new l(e.x,t.y),e]}return this.style.queryRenderedFeatures(n,t,this._camera.transform)}querySourceFeatures(e,t){return this.style.querySourceFeatures(e,t)}setStyle(e,t){return t=z({},{localIdeographFontFamily:this._localIdeographFontFamily,validate:this._validateStyle},t),t.diff!==!1&&t.localIdeographFontFamily===this._localIdeographFontFamily&&this.style&&e?(this._diffStyle(e,t),this):(this._localIdeographFontFamily=t.localIdeographFontFamily,this._updateStyle(e,t))}setTransformRequest(e){return this._requestManager.setTransformRequest(e),this}_getUIString(e){let t=this._locale[e];if(t==null)throw Error(`Missing UI string '${e}'`);return t}_updateStyle(e,t){if(this._diffStyleRequest?.abort(),this._diffStyleRequest=null,t.transformStyle&&this.style&&!this.style._loaded){this.style.once(`style.load`,()=>this._updateStyle(e,t));return}let n=this.style&&t.transformStyle?this.style.serialize():void 0;if(this.style&&(this.style.setEventedParent(null),this.style._remove(!e)),e)this.style=new ml(this,t||{});else return this._frameRequest&&=(this._frameRequest.abort(),null),this.style?.projection?.destroy(),delete this.style,this;return this.style.setEventedParent(this,{style:this.style}),typeof e==`string`?this.style.loadURL(e,t,n):this.style.loadJSON(e,t,n),this}_lazyInitEmptyStyle(){this.style||(this.style=new ml(this,{}),this.style.setEventedParent(this,{style:this.style}),this.style.loadEmpty())}async _diffStyle(e,t){if(this._diffStyleRequest?.abort(),typeof e==`string`){let n=e;this._diffStyleRequest=new AbortController;let r=this._diffStyleRequest;try{let e=await this._requestManager.transformRequest(n,`Style`);if(r.signal.aborted){this._diffStyleRequest=null;return}let i=await b(e,r);this._diffStyleRequest=null,this._updateDiff(i.data,t)}catch(e){this._diffStyleRequest=null,xe(e)||this.fire(new H(qn(e)))}}else typeof e==`object`&&(this._diffStyleRequest=null,this._updateDiff(e,t))}_updateDiff(e,t){try{this.style.setState(e,t)&&this._update(!0)}catch(n){I(`Unable to perform style diff: ${qn(n).message}. Rebuilding the style from scratch.`),this._updateStyle(e,t)}}getStyle(){if(this.style)return this.style.serialize()}_getStyleAndImages(){return this.style?{style:this.style.serialize(),images:this.style.imageManager.cloneImages()}:{style:null,images:{}}}isStyleLoaded(){if(!this.style){I(`There is no style added to the map.`);return}return this.style.loaded()}addSource(e,t){return this._lazyInitEmptyStyle(),this.style.addSource(e,t),this._update(!0)}isSourceLoaded(e){let t=this.style?.tileManagers[e];if(t===void 0){this.fire(new H(Error(`There is no tile manager with ID '${e}'`)));return}return t.loaded()}setTerrain(e,t={}){if(this.style._checkLoaded(),e&&ar(this,Ut.terrain,{value:e},t))return this;if(this._terrainDataCallback&&this.style.off(`data`,this._terrainDataCallback),!e)this.terrain&&this.terrain.destroy(),this.terrain=null,this.painter.renderToTexture=null,this._camera.terrain=null,this._camera.transform.setMinElevationForCurrentTile(0),this.getCenterClampedToGround()&&this._camera.transform.setElevation(0);else{let t=this.style.tileManagers[e.source];if(!t)throw Error(`cannot load terrain, because there exists no source with ID: ${e.source}`);this.terrain===null&&t.reload();for(let t in this.style._layers){let n=this.style._layers[t];n.type===`hillshade`&&n.source===e.source&&I(`You are using the same source for a hillshade layer and for 3D terrain. Please consider using two separate sources to improve rendering quality.`),n.type===`color-relief`&&n.source===e.source&&I(`You are using the same source for a color-relief layer and for 3D terrain. Please consider using two separate sources to improve rendering quality.`)}this.terrain&&this.terrain.destroy(),this.terrain=new mc(this.painter,t,e,this._terrainSkirtLength),this.painter.renderToTexture=new rm(this.painter,this.terrain),this._camera.terrain=this.terrain,this._camera.transform.setMinElevationForCurrentTile(this.terrain.getMinTileElevationForLngLatZoom(this._camera.transform.center,this._camera.transform.tileZoom)),this._camera.transform.setElevation(this.terrain.getElevationForLngLat(this._camera.transform.center,this._camera.transform)),this._terrainDataCallback=t=>this._handleTerrainDataEvent(t,e.source),this.style.on(`data`,this._terrainDataCallback)}return this.style.triggerSymbolPlacement(),this.fire(new Qr({terrain:e})),this}_handleTerrainDataEvent(e,t){if(e.dataType===`style`){this.terrain.tileManager.releaseAllRTT();return}let n=e.sourceId===t;if(n&&(this.terrain.resetElevationCache(),this.style.triggerSymbolPlacement()),n&&e.tile&&!this._camera.elevationFreeze&&(this._camera.transform.setMinElevationForCurrentTile(this.terrain.getMinTileElevationForLngLatZoom(this._camera.transform.center,this._camera.transform.tileZoom)),this.getCenterClampedToGround()&&this._camera.transform.setElevation(this.terrain.getElevationForLngLat(this._camera.transform.center,this._camera.transform))),e.tile){if(e.source?.type===`image`){this.terrain.tileManager.releaseAllRTT();return}this.terrain.tileManager.releaseRTT(e.tile.tileID)}}getTerrain(){return this.terrain?.options??null}areTilesLoaded(){let e=this.style?.tileManagers;for(let t of Object.values(e))if(!t.areTilesLoaded())return!1;return!0}removeSource(e){return this.style.removeSource(e),this._update(!0)}getSource(e){return this.style?.getSource(e)}setSourceTileLodParams(e,t,n){if(n){let r=this.getSource(n);if(!r)throw Error(`There is no source with ID "${n}", cannot set LOD parameters`);r.calculateTileZoom=vo(Math.max(1,e),Math.max(1,t))}else for(let n in this.style.tileManagers)this.style.tileManagers[n].getSource().calculateTileZoom=vo(Math.max(1,e),Math.max(1,t));return this._update(!0),this}refreshTiles(e,t){let n=this.style.tileManagers[e];if(!n)throw Error(`There is no tile manager with ID "${e}", cannot refresh tile`);t===void 0?n.reload(!0):n.refreshTiles(t.map(e=>new rn(e.z,e.x,e.y)))}addImage(e,t,n={}){this._lazyInitEmptyStyle();let r=this._createStyleImage(t,n);return r?(this.style.addImage(e,r),r.userImage?.onAdd&&r.userImage.onAdd(this,e),this):this}setMissingStyleImageResolver(e){return this._missingStyleImageResolver=e,this.style?.setMissingImageResolver(e),this}_createStyleImage(e,t={}){let{pixelRatio:n=1,sdf:r=!1,stretchX:i,stretchY:a,content:o,textFitWidth:s,textFitHeight:c}=t;if(e instanceof HTMLImageElement||zn(e)){let{width:t,height:l,data:u}=Rr.getImageData(e);return{data:new xn({width:t,height:l},u),pixelRatio:n,stretchX:i,stretchY:a,content:o,textFitWidth:s,textFitHeight:c,sdf:r,version:0}}if(e.width===void 0||e.height===void 0)return this.fire(new H(Error("Invalid arguments to map.addImage(). The second argument must be an `HTMLImageElement`, `ImageData`, `ImageBitmap`, or object with `width`, `height`, and `data` properties with the same format as `ImageData`"))),null;{let{width:t,height:l,data:u}=e,d=e,f=ye(d.data);return{data:f?new xn({width:t,height:l}):new xn({width:t,height:l},new Uint8Array(u)),pixelRatio:n,stretchX:i,stretchY:a,content:o,textFitWidth:s,textFitHeight:c,sdf:r,version:0,isWebGLImage:f,userImage:d}}}updateImage(e,t){let n=this.style.getImage(e);if(!n)return this.fire(new H(Error("The map has no image with that id. If you are adding a new image use `map.addImage(...)` instead.")));let{width:r,height:i,data:a}=t instanceof HTMLImageElement||zn(t)?Rr.getImageData(t):t;if(r===void 0||i===void 0)return this.fire(new H(Error("Invalid arguments to map.updateImage(). The second argument must be an `HTMLImageElement`, `ImageData`, `ImageBitmap`, or object with `width`, `height`, and `data` properties with the same format as `ImageData`")));if(r!==n.data.width||i!==n.data.height)return this.fire(new H(Error(`The width and height of the updated image must be that same as the previous version of the image`)));if(n.isWebGLImage=ye(a),n.isWebGLImage)n.userImage=t;else{let e=!(t instanceof HTMLImageElement||zn(t));n.data.replace(a,e)}return this.style.updateImage(e,n),this}getImage(e){return this.style.getImage(e)}hasImage(e){return e?!!this.style.getImage(e):(this.fire(new H(Error(`Missing required image id`))),!1)}removeImage(e){this.style.removeImage(e)}async loadImage(e){return Ur.getImage(await this._requestManager.transformRequest(e,`Image`),new AbortController)}listImages(){return this.style?.listImages()??[]}addLayer(e,t){return this._lazyInitEmptyStyle(),this.style.addLayer(e,t),this._update(!0)}moveLayer(e,t){return this.style.moveLayer(e,t),this._update(!0)}removeLayer(e){return this.style.removeLayer(e),this._update(!0)}getLayer(e){return this.style?.getLayer(e)}getLayersOrder(){return this.style?.getLayersOrder()??[]}setLayerZoomRange(e,t,n){return this.style.setLayerZoomRange(e,t,n),this._update(!0)}setFilter(e,t,n={}){return this.style?.setFilter(e,t,n),this._update(!0)}getFilter(e){return this.style.getFilter(e)}setPaintProperty(e,t,n,r={}){return this.style?.setPaintProperty(e,t,n,r),this._update(!0)}getPaintProperty(e,t){return this.style.getPaintProperty(e,t)}setLayoutProperty(e,t,n,r={}){return this.style.setLayoutProperty(e,t,n,r),this._update(!0)}getLayoutProperty(e,t){return this.style.getLayoutProperty(e,t)}setGlyphs(e,t={}){return this._lazyInitEmptyStyle(),this.style.setGlyphs(e,t),this._update(!0)}getGlyphs(){return this.style.getGlyphsUrl()}setFontFaces(e){return this._lazyInitEmptyStyle(),this.style.setFontFaces(e),this._update(!0)}getFontFaces(){return this.style.getFontFaces()}addSprite(e,t,n={}){return this._lazyInitEmptyStyle(),this.style.addSprite(e,t,n,e=>{e||this._update(!0)}),this}removeSprite(e){return this._lazyInitEmptyStyle(),this.style.removeSprite(e),this._update(!0)}getSprite(){return this.style.getSprite()}setSprite(e,t={}){return this._lazyInitEmptyStyle(),this.style.setSprite(e,t,e=>{e||this._update(!0)}),this}setLight(e,t={}){return this._lazyInitEmptyStyle(),this.style.setLight(e,t),this._update(!0)}getLight(){return this.style.getLight()}setSky(e,t={}){return this._lazyInitEmptyStyle(),this.style.setSky(e,t),this._update(!0)}getSky(){return this.style.getSky()}setFeatureState(e,t){return this.style.setFeatureState(e,t),this._update()}removeFeatureState(e,t){return this.style.removeFeatureState(e,t),this._update()}getFeatureState(e){return this.style.getFeatureState(e)}getContainer(){return this._container}getCanvasContainer(){return this._canvasContainer}getCanvas(){return this._canvas}_containerDimensions(){let e=0,t=0;return this._container&&(e=this._container.clientWidth||400,t=this._container.clientHeight||300),[e,t]}_setupResizeObserver(){let e=!1,t=Jf(e=>{this._trackResize&&!this._removed&&(this.resize(e),this.redraw())},50),n=this._ownerWindow.ResizeObserver??ResizeObserver;this._resizeObserver=new n(n=>{if(!e){e=!0;return}t(n)}),this._resizeObserver.observe(this._container)}_resolveContainer(e){if(typeof e==`string`){let t=document.getElementById(e);if(!t)throw Error(`Container '${e}' not found.`);return t}if(e instanceof HTMLElement||e&&typeof e==`object`&&e.nodeType===1)return e;throw Error(`Invalid type: 'container' must be a String or HTMLElement.`)}_setupContainer(){let e=this._container;e.classList.add(`maplibregl-map`);let t=this._canvasContainer=W.create(`div`,`maplibregl-canvas-container`,e);this._interactive&&t.classList.add(`maplibregl-interactive`),this._canvas=W.create(`canvas`,`maplibregl-canvas`,t),this._canvas.addEventListener(`webglcontextlost`,this._contextLost,!1),this._canvas.addEventListener(`webglcontextrestored`,this._contextRestored,!1),this._canvas.setAttribute(`tabindex`,this._interactive?`0`:`-1`),this._canvas.setAttribute(`aria-label`,this._getUIString(`Map.Title`)),this._canvas.setAttribute(`role`,`region`);let n=this._containerDimensions(),r=this._getClampedPixelRatio(n[0],n[1]);this._resizeCanvas(n[0],n[1],r);let i=this._controlContainer=W.create(`div`,`maplibregl-control-container`,e),a=this._controlPositions={};for(let e of[`top-left`,`top-right`,`bottom-left`,`bottom-right`])a[e]=W.create(`div`,`maplibregl-ctrl-${e} `,i);this._container.addEventListener(`scroll`,this._onMapScroll,!1)}_cleanupContainer(){this._canvas.removeEventListener(`webglcontextrestored`,this._contextRestored,!1),this._canvas.removeEventListener(`webglcontextlost`,this._contextLost,!1),this._canvasContainer.remove(),this._controlContainer.remove(),this._container.removeEventListener(`scroll`,this._onMapScroll,!1),this._container.classList.remove(`maplibregl-map`)}_resizeCanvas(e,t,n){this._canvas.width=Math.floor(n*e),this._canvas.height=Math.floor(n*t),this._canvas.style.width=`${e}px`,this._canvas.style.height=`${t}px`}_setupPainter(){let e={...this._canvasContextAttributes,alpha:!0,depth:!0,stencil:!0,premultipliedAlpha:!0},t=null;this._canvas.addEventListener(`webglcontextcreationerror`,e=>{t=e},{once:!0});let n=this._canvas.getContext(`webgl2`,e);if(!n)throw new qf(e,t);this.painter=new Kf(n,this._camera.transform)}migrateProjection(e,t){this._camera.migrateProjection(e,t),this.painter.transform=e,this.fire(new $r({newProjection:this.style.projection.name}))}loaded(){return!this._styleDirty&&!this._sourcesDirty&&!!this.style&&this.style.loaded()}_update(e){return this.style?._loaded?(this._styleDirty||=e,this._sourcesDirty=!0,this.triggerRepaint(),this):this}_requestRenderFrame(e){return this._update(),this._renderTaskQueue.add(e)}_cancelRenderFrame(e){this._renderTaskQueue.remove(e)}_render(e){let t=this._idleTriggered?this._fadeDuration:0,n=this.style.projection?.transitionState>0;if(this.painter.context.setDirty(),this.painter.setBaseState(),this._renderTaskQueue.run(e),this._removed)return;let r=!1;if(this.style&&this._styleDirty){this._styleDirty=!1;let e=this._camera.transform.zoom,n=U();this.style.zoomHistory.update(e,n);let i=new Kn(e,{now:n,fadeDuration:t,zoomHistory:this.style.zoomHistory,transition:this.style.getTransition()}),a=i.crossFadingFactor();(a!==1||a!==this._crossFadingFactor)&&(r=!0,this._crossFadingFactor=a),this.style.update(i)}let i=this.style.projection?.transitionState>0!==n;this._camera.transform.setTransitionState(this.style.projection?.transitionState),this.style&&(this._sourcesDirty||i)&&(this._sourcesDirty=!1,this.style._updateSources(this._camera.transform)),this.terrain?(this.terrain.tileManager.update(this._camera.transform,this.terrain)&&this.terrain.resetElevationCache(),this._camera.transform.setMinElevationForCurrentTile(this.terrain.getMinTileElevationForLngLatZoom(this._camera.transform.center,this._camera.transform.tileZoom)),!this._camera.elevationFreeze&&this.getCenterClampedToGround()&&this._camera.transform.setElevation(this.terrain.getElevationForLngLat(this._camera.transform.center,this._camera.transform))):(this._camera.transform.setMinElevationForCurrentTile(0),this.getCenterClampedToGround()&&this._camera.transform.setElevation(0)),this._placementDirty=this.style?._updatePlacement(this._camera.transform,this.showCollisionBoxes,t,this._crossSourceCollisions,i),this.painter.render(this.style,{showTileBoundaries:this.showTileBoundaries,showOverdrawInspector:this._showOverdrawInspector,rotating:this.isRotating(),zooming:this.isZooming(),moving:this.isMoving(),fadeDuration:t,showPadding:this.showPadding,anisotropicFilterPitch:this.getAnisotropicFilterPitch()}),this.fire(new Gr(`render`)),this.loaded()&&!this._loaded&&(this._loaded=!0,this.fire(new Gr(`load`))),this.style&&(this.style.hasTransitions()||r)&&(this._styleDirty=!0),this.style&&!this._placementDirty&&this.style._releaseSymbolFadeTiles();let a=this._sourcesDirty||this._styleDirty||this._placementDirty;return a||this._repaint?this.triggerRepaint():!this.isMoving()&&this.loaded()&&this.fire(new Gr(`idle`)),this._loaded&&!this._fullyLoaded&&!a&&(this._fullyLoaded=!0),this}redraw(){return this.style&&(this._frameRequest&&=(this._frameRequest.abort(),null),this._render(0)),this}remove(){this._hash&&this._hash.remove();for(let e of this._controls)e.onRemove(this);this._controls=[],this._frameRequest&&=(this._frameRequest.abort(),null),this._renderTaskQueue.clear(),this._diffStyleRequest?.abort(),this.painter.destroy(),this._handlers.destroy(),this.setStyle(null),typeof window<`u`&&this._ownerWindow.removeEventListener(`online`,this._onWindowOnline,!1),Ur.removeThrottleControl(this._imageQueueHandle),this._resizeObserver?.disconnect();let e=this.painter.context.gl.getExtension(`WEBGL_lose_context`);e?.loseContext&&e.loseContext(),this._cleanupContainer(),this._removed=!0,this.fire(new Gr(`remove`))}triggerRepaint(){this.style&&!this._frameRequest&&(this._frameRequest=new AbortController,Rr.frame(this._frameRequest,e=>{this._frameRequest=null;try{this._render(e)}catch(e){if(!xe(e))throw e}},()=>{},this._ownerWindow))}get showTileBoundaries(){return!!this._showTileBoundaries}set showTileBoundaries(e){this._showTileBoundaries!==e&&(this._showTileBoundaries=e,this._update())}get showPadding(){return!!this._showPadding}set showPadding(e){this._showPadding!==e&&(this._showPadding=e,this._update())}get showCollisionBoxes(){return!!this._showCollisionBoxes}set showCollisionBoxes(e){this._showCollisionBoxes!==e&&(this._showCollisionBoxes=e,e?this.style._generateCollisionBoxes():this._update())}get showOverdrawInspector(){return!!this._showOverdrawInspector}set showOverdrawInspector(e){this._showOverdrawInspector!==e&&(this._showOverdrawInspector=e,this._update())}get repaint(){return!!this._repaint}set repaint(e){this._repaint!==e&&(this._repaint=e,this.triggerRepaint())}get vertices(){return!!this._vertices}set vertices(e){this._vertices=e,this._update()}get version(){return am}getCameraTargetElevation(){return this._camera.transform.elevation}getProjection(){return this.style.getProjection()}setProjection(e){return this._lazyInitEmptyStyle(),this.style.setProjection(e),this._update(!0)}};const cm={showCompass:!0,showZoom:!0,visualizePitch:!1,visualizeRoll:!0};var lm=class{constructor(e){this._updateZoomButtons=()=>{let e=this._map.getZoom(),t=e===this._map.getMaxZoom(),n=e===this._map.getMinZoom(!0);this._zoomInButton.disabled=t,this._zoomOutButton.disabled=n,this._zoomInButton.setAttribute(`aria-disabled`,t.toString()),this._zoomOutButton.setAttribute(`aria-disabled`,n.toString())},this._rotateCompassArrow=()=>{let e=this._map.getPitch(),t=this._map.getRoll(),n=this._map.getBearing(),r=1/Math.cos(qt(e))**.5;if(this.options.visualizePitch&&this.options.visualizeRoll){this._compassIcon.style.transform=`scale(${r}) rotateZ(${-t}deg) rotateX(${e}deg) rotateZ(${-n}deg)`;return}if(this.options.visualizePitch){this._compassIcon.style.transform=`scale(${r}) rotateX(${e}deg) rotateZ(${-n}deg)`;return}if(this.options.visualizeRoll){this._compassIcon.style.transform=`rotate(${-n-t}deg)`;return}this._compassIcon.style.transform=`rotate(${-n}deg)`},this._setButtonTitle=(e,t)=>{let n=this._map._getUIString(`NavigationControl.${t}`);e.title=n,e.setAttribute(`aria-label`,n)},this.options=z({},cm,e),this._container=W.create(`div`,`maplibregl-ctrl maplibregl-ctrl-group`),this._container.addEventListener(`contextmenu`,e=>e.preventDefault()),this.options.showZoom&&(this._zoomInButton=this._createButton(`maplibregl-ctrl-zoom-in`,e=>this._map.zoomIn({},{originalEvent:e})),W.create(`span`,`maplibregl-ctrl-icon`,this._zoomInButton).setAttribute(`aria-hidden`,`true`),this._zoomOutButton=this._createButton(`maplibregl-ctrl-zoom-out`,e=>this._map.zoomOut({},{originalEvent:e})),W.create(`span`,`maplibregl-ctrl-icon`,this._zoomOutButton).setAttribute(`aria-hidden`,`true`)),this.options.showCompass&&(this._compass=this._createButton(`maplibregl-ctrl-compass`,e=>{this.options.visualizePitch?this._map.resetNorthPitch({},{originalEvent:e}):this._map.resetNorth({},{originalEvent:e})}),this._compassIcon=W.create(`span`,`maplibregl-ctrl-icon`,this._compass),this._compassIcon.setAttribute(`aria-hidden`,`true`))}onAdd(e){return this._map=e,this.options.showZoom&&(this._setButtonTitle(this._zoomInButton,`ZoomIn`),this._setButtonTitle(this._zoomOutButton,`ZoomOut`),this._map.on(`move`,this._updateZoomButtons),this._updateZoomButtons()),this.options.showCompass&&(this._setButtonTitle(this._compass,`ResetBearing`),this.options.visualizePitch&&this._map.on(`pitch`,this._rotateCompassArrow),this.options.visualizeRoll&&this._map.on(`roll`,this._rotateCompassArrow),this._map.on(`rotate`,this._rotateCompassArrow),this._rotateCompassArrow(),this._handler=new um(this._map,this._compass,this.options.visualizePitch)),this._container}onRemove(){this._container.remove(),this.options.showZoom&&this._map.off(`move`,this._updateZoomButtons),this.options.showCompass&&(this.options.visualizePitch&&this._map.off(`pitch`,this._rotateCompassArrow),this.options.visualizeRoll&&this._map.off(`roll`,this._rotateCompassArrow),this._map.off(`rotate`,this._rotateCompassArrow),this._handler.off(),delete this._handler),delete this._map}_createButton(e,t){let n=W.create(`button`,e,this._container);return n.type=`button`,n.addEventListener(`click`,t),n}},um=class{constructor(e,t,n=!1){this.mousedown=e=>{this.startMove(e,W.mousePos(this.element,e)),window.addEventListener(`mousemove`,this.mousemove),window.addEventListener(`mouseup`,this.mouseup)},this.mousemove=e=>{this.move(e,W.mousePos(this.element,e))},this.mouseup=e=>{this._rotatePitchHandler.dragEnd(e),this.offTemp()},this.touchstart=e=>{e.targetTouches.length===1?(this._startPos=this._lastPos=W.touchPos(this.element,e.targetTouches)[0],this.startMove(e,this._startPos),window.addEventListener(`touchmove`,this.touchmove,{passive:!1}),window.addEventListener(`touchend`,this.touchend)):this.reset()},this.touchmove=e=>{e.targetTouches.length===1?(this._lastPos=W.touchPos(this.element,e.targetTouches)[0],this.move(e,this._lastPos)):this.reset()},this.touchend=e=>{e.targetTouches.length===0&&this._startPos&&this._lastPos&&this._startPos.dist(this._lastPos){this._rotatePitchHandler.reset(),delete this._startPos,delete this._lastPos,this.offTemp()},this._clickTolerance=10,this.element=t;let r=new vp;this._rotatePitchHandler=new pp({clickTolerance:3,move:(e,r)=>{let i=t.getBoundingClientRect(),a=new l((i.bottom-i.top)/2,(i.right-i.left)/2);return{bearingDelta:wn(new l(e.x,r.y),r,a),pitchDelta:n?(r.y-e.y)*-.5:void 0}},moveStateManager:r,enable:!0,assignEvents:()=>{}}),this.map=e,t.addEventListener(`mousedown`,this.mousedown),t.addEventListener(`touchstart`,this.touchstart,{passive:!1}),t.addEventListener(`touchcancel`,this.reset)}startMove(e,t){this._rotatePitchHandler.dragStart(e,t),W.disableDrag()}move(e,t){let n=this.map,{bearingDelta:r,pitchDelta:i}=this._rotatePitchHandler.dragMove(e,t)||{};r&&n.setBearing(n.getBearing()+r),i&&n.setPitch(n.getPitch()+i)}off(){let e=this.element;e.removeEventListener(`mousedown`,this.mousedown),e.removeEventListener(`touchstart`,this.touchstart),window.removeEventListener(`touchmove`,this.touchmove),window.removeEventListener(`touchend`,this.touchend),e.removeEventListener(`touchcancel`,this.reset),this.offTemp()}offTemp(){W.enableDrag(),window.removeEventListener(`mousemove`,this.mousemove),window.removeEventListener(`mouseup`,this.mouseup),window.removeEventListener(`touchmove`,this.touchmove),window.removeEventListener(`touchend`,this.touchend)}};let dm;async function fm(e=!1){if(dm!==void 0&&!e)return dm;if(window.navigator.permissions===void 0)return dm=!!window.navigator.geolocation,dm;try{dm=(await window.navigator.permissions.query({name:`geolocation`})).state!==`denied`}catch{dm=!!window.navigator.geolocation}return dm}function pm(e,t,n,r=!1){if(r||!n.getCoveringTilesDetailsProvider().allowWorldCopies())return e?.wrap();let i=new V(e.lng,e.lat);if(e=new V(e.lng,e.lat),t){let r=new V(e.lng-360,e.lat),i=new V(e.lng+360,e.lat),a=n.locationToScreenPoint(e).distSqr(t);n.locationToScreenPoint(r).distSqr(t)180;){let t=n.locationToScreenPoint(e);if(t.x>=0&&t.y>=0&&t.x<=n.width&&t.y<=n.height)break;e.lng>n.center.lng?e.lng-=360:e.lng+=360}return e.lng!==i.lng&&n.isPointOnMapSurface(n.locationToScreenPoint(e))?e:i}const mm={center:`translate(-50%,-50%)`,top:`translate(-50%,0)`,"top-left":`translate(0,0)`,"top-right":`translate(-100%,0)`,bottom:`translate(-50%,-100%)`,"bottom-left":`translate(0,-100%)`,"bottom-right":`translate(-100%,-100%)`,left:`translate(0,-50%)`,right:`translate(-100%,-50%)`};function hm(e,t,n){let r=e.classList;for(let e in mm)r.remove(`maplibregl-${n}-anchor-${e}`);r.add(`maplibregl-${n}-anchor-${t}`)}const gm={ArrowLeft:[-1,0],ArrowRight:[1,0],ArrowUp:[0,-1],ArrowDown:[0,1]};var _m=class extends Xe{},vm=class extends Xe{},ym=class extends h{constructor(e){if(super(),this._onClick=e=>{this.fire(new vm(`click`,{originalEvent:e}))},this._onKeyPress=e=>{(e.code===`Space`||e.code===`Enter`)&&this.togglePopup()},this._onKeyDown=e=>{if(!this._defaultMarker||!this._draggable||!this._map||!this._lngLat||e.composedPath()[0]!==this._element||e.altKey||e.ctrlKey||e.metaKey)return;let t=gm[e.key];if(!t)return;e.preventDefault(),e.stopPropagation();let n=e.shiftKey?10:1,r=this._map.project(this._lngLat);this.setLngLat(this._map.unproject(new l(r.x+t[0]*n,r.y+t[1]*n))),this._keyboardDragActive||(this._keyboardDragActive=!0,this.fire(new _m(`dragstart`))),this.fire(new _m(`drag`))},this._onKeyUp=e=>{gm[e.key]&&this._endKeyboardDrag()},this._onBlur=()=>{this._endKeyboardDrag()},this._onMapClick=e=>{let t=e.originalEvent.target,n=this._element;this._popup&&(t===n||n.contains(t))&&this.togglePopup()},this._update=e=>{if(!this._map)return;let t=this._map.loaded()&&!this._map.isMoving();(e?.type===`terrain`||e?.type===`render`&&!t)&&this._map.once(`render`,this._update),this._lngLat=pm(this._lngLat,this._flatPos,this._map._camera.transform),this._flatPos=this._pos=this._map.project(this._lngLat)._add(this._offset),this._map.terrain&&(this._flatPos=this._map._camera.transform.locationToScreenPoint(this._lngLat)._add(this._offset));let n=``;this._rotationAlignment===`viewport`||this._rotationAlignment===`auto`?n=`rotateZ(${this._rotation}deg)`:this._rotationAlignment===`map`&&(n=`rotateZ(${this._rotation-this._map.getBearing()}deg)`);let r=``;this._pitchAlignment===`viewport`||this._pitchAlignment===`auto`?r=`rotateX(0deg)`:this._pitchAlignment===`map`&&(r=`rotateX(${this._map.getPitch()}deg)`),!this._subpixelPositioning&&(!e||e.type===`moveend`)&&(this._pos=this._pos.round()),this._element.style.transform=`${mm[this._anchor]} translate(${this._pos.x}px, ${this._pos.y}px) ${r} ${n}`,Rr.frameAsync(new AbortController,this._map._ownerWindow).then(()=>{this._updateOpacity(e?.type===`moveend`)}).catch(()=>{})},this._onMove=e=>{if(!this._isDragging){let t=this._clickTolerance||this._map._clickTolerance;this._isDragging=e.point.dist(this._pointerdownPos)>=t}this._isDragging&&(this._pos=e.point.sub(this._positionDelta),this._lngLat=this._map.unproject(this._pos),this.setLngLat(this._lngLat),this._element.style.pointerEvents=`none`,this._state===`pending`&&(this._state=`active`,this.fire(new _m(`dragstart`))),this.fire(new _m(`drag`)))},this._onUp=()=>{this._element.style.pointerEvents=`auto`,this._positionDelta=null,this._pointerdownPos=null,this._isDragging=!1,this._map.off(`mousemove`,this._onMove),this._map.off(`touchmove`,this._onMove),this._state===`active`&&this.fire(new _m(`dragend`)),this._state=`inactive`},this._addDragHandler=e=>{this._element.contains(e.originalEvent.target)&&(e.preventDefault(),this._positionDelta=e.point.sub(this._pos).add(this._offset),this._pointerdownPos=e.point,this._state=`pending`,this._map.on(`mousemove`,this._onMove),this._map.on(`touchmove`,this._onMove),this._map.once(`mouseup`,this._onUp),this._map.once(`touchend`,this._onUp))},this._anchor=e?.anchor||`center`,this._color=e?.color||`#3FB1CE`,this._scale=e?.scale||1,this._draggable=e?.draggable||!1,this._clickTolerance=e?.clickTolerance||0,this._subpixelPositioning=e?.subpixelPositioning||!1,this._isDragging=!1,this._roleManaged=!1,this._tabIndexManaged=!1,this._keyboardDragActive=!1,this._state=`inactive`,this._rotation=e?.rotation||0,this._rotationAlignment=e?.rotationAlignment||`auto`,this._pitchAlignment=e?.pitchAlignment&&e.pitchAlignment!==`auto`?e.pitchAlignment:this._rotationAlignment,this.setOpacity(e?.opacity,e?.opacityWhenCovered),e?.element)this._element=e.element,this._offset=l.convert(e?.offset||[0,0]);else{this._defaultMarker=!0,this._element=W.create(`div`);let t=W.createNS(`http://www.w3.org/2000/svg`,`svg`);t.setAttributeNS(null,`display`,`block`),t.setAttributeNS(null,`height`,`41px`),t.setAttributeNS(null,`width`,`27px`),t.setAttributeNS(null,`viewBox`,`0 0 27 41`);let n=W.createNS(`http://www.w3.org/2000/svg`,`g`);n.setAttributeNS(null,`stroke`,`none`),n.setAttributeNS(null,`stroke-width`,`1`),n.setAttributeNS(null,`fill`,`none`),n.setAttributeNS(null,`fill-rule`,`evenodd`);let r=W.createNS(`http://www.w3.org/2000/svg`,`g`);r.setAttributeNS(null,`fill-rule`,`nonzero`);let i=W.createNS(`http://www.w3.org/2000/svg`,`g`);i.setAttributeNS(null,`transform`,`translate(3.0, 29.0)`),i.setAttributeNS(null,`fill`,`#000000`);for(let e of[{rx:`10.5`,ry:`5.25002273`},{rx:`10.5`,ry:`5.25002273`},{rx:`9.5`,ry:`4.77275007`},{rx:`8.5`,ry:`4.29549936`},{rx:`7.5`,ry:`3.81822308`},{rx:`6.5`,ry:`3.34094679`},{rx:`5.5`,ry:`2.86367051`},{rx:`4.5`,ry:`2.38636864`}]){let t=W.createNS(`http://www.w3.org/2000/svg`,`ellipse`);t.setAttributeNS(null,`opacity`,`0.04`),t.setAttributeNS(null,`cx`,`10.5`),t.setAttributeNS(null,`cy`,`5.80029008`),t.setAttributeNS(null,`rx`,e.rx),t.setAttributeNS(null,`ry`,e.ry),i.appendChild(t)}let a=W.createNS(`http://www.w3.org/2000/svg`,`g`);a.setAttributeNS(null,`fill`,this._color);let o=W.createNS(`http://www.w3.org/2000/svg`,`path`);o.setAttributeNS(null,`d`,`M27,13.5 C27,19.074644 20.250001,27.000002 14.75,34.500002 C14.016665,35.500004 12.983335,35.500004 12.25,34.500002 C6.7499993,27.000002 0,19.222562 0,13.5 C0,6.0441559 6.0441559,0 13.5,0 C20.955844,0 27,6.0441559 27,13.5 Z`),a.appendChild(o);let s=W.createNS(`http://www.w3.org/2000/svg`,`g`);s.setAttributeNS(null,`opacity`,`0.25`),s.setAttributeNS(null,`fill`,`#000000`);let c=W.createNS(`http://www.w3.org/2000/svg`,`path`);c.setAttributeNS(null,`d`,`M13.5,0 C6.0441559,0 0,6.0441559 0,13.5 C0,19.222562 6.7499993,27 12.25,34.5 C13,35.522727 14.016664,35.500004 14.75,34.5 C20.250001,27 27,19.074644 27,13.5 C27,6.0441559 20.955844,0 13.5,0 Z M13.5,1 C20.415404,1 26,6.584596 26,13.5 C26,15.898657 24.495584,19.181431 22.220703,22.738281 C19.945823,26.295132 16.705119,30.142167 13.943359,33.908203 C13.743445,34.180814 13.612715,34.322738 13.5,34.441406 C13.387285,34.322738 13.256555,34.180814 13.056641,33.908203 C10.284481,30.127985 7.4148684,26.314159 5.015625,22.773438 C2.6163816,19.232715 1,15.953538 1,13.5 C1,6.584596 6.584596,1 13.5,1 Z`),s.appendChild(c);let u=W.createNS(`http://www.w3.org/2000/svg`,`g`);u.setAttributeNS(null,`transform`,`translate(6.0, 7.0)`),u.setAttributeNS(null,`fill`,`#FFFFFF`);let d=W.createNS(`http://www.w3.org/2000/svg`,`g`);d.setAttributeNS(null,`transform`,`translate(8.0, 8.0)`);let f=W.createNS(`http://www.w3.org/2000/svg`,`circle`);f.setAttributeNS(null,`fill`,`#000000`),f.setAttributeNS(null,`opacity`,`0.25`),f.setAttributeNS(null,`cx`,`5.5`),f.setAttributeNS(null,`cy`,`5.5`),f.setAttributeNS(null,`r`,`5.4999962`);let p=W.createNS(`http://www.w3.org/2000/svg`,`circle`);p.setAttributeNS(null,`fill`,`#FFFFFF`),p.setAttributeNS(null,`cx`,`5.5`),p.setAttributeNS(null,`cy`,`5.5`),p.setAttributeNS(null,`r`,`5.4999962`),d.appendChild(f),d.appendChild(p),r.appendChild(i),r.appendChild(a),r.appendChild(s),r.appendChild(u),r.appendChild(d),t.appendChild(r),t.setAttributeNS(null,`height`,`${41*this._scale}px`),t.setAttributeNS(null,`width`,`${27*this._scale}px`),this._element.appendChild(t),this._offset=l.convert(e?.offset||[0,-14])}if(this._element.classList.add(`maplibregl-marker`),this._element.addEventListener(`dragstart`,e=>{e.preventDefault()}),this._element.addEventListener(`mousedown`,e=>{e.preventDefault()}),hm(this._element,this._anchor,`marker`),e?.className)for(let t of e.className.split(` `))this._element.classList.add(t);this._popup=null}addTo(e){return this.remove(),this._map=e,this._defaultMarker&&!this._element.hasAttribute(`aria-label`)&&this._element.setAttribute(`aria-label`,e._getUIString(`Marker.Title`)),this._updateAccessibilityRole(),e.getCanvasContainer().appendChild(this._element),e.on(`move`,this._update),e.on(`moveend`,this._update),e.on(`terrain`,this._update),e.on(`projectiontransition`,this._update),this._element.addEventListener(`click`,this._onClick),this.setDraggable(this._draggable),this._update(),this._map.on(`click`,this._onMapClick),this}remove(){return this._opacityTimeout&&(clearTimeout(this._opacityTimeout),delete this._opacityTimeout),this._map&&(this._map.off(`click`,this._onMapClick),this._map.off(`move`,this._update),this._map.off(`moveend`,this._update),this._map.off(`terrain`,this._update),this._map.off(`projectiontransition`,this._update),this._map.off(`mousedown`,this._addDragHandler),this._map.off(`touchstart`,this._addDragHandler),this._map.off(`mouseup`,this._onUp),this._map.off(`touchend`,this._onUp),this._map.off(`mousemove`,this._onMove),this._map.off(`touchmove`,this._onMove),delete this._map),this._element.removeEventListener(`click`,this._onClick),this._element.removeEventListener(`keydown`,this._onKeyDown),this._element.removeEventListener(`keyup`,this._onKeyUp),this._element.removeEventListener(`blur`,this._onBlur),this._element.removeEventListener(`keypress`,this._onKeyPress),this._keyboardDragActive=!1,this._element.remove(),this._popup&&this._popup.remove(),this}getLngLat(){return this._lngLat}setLngLat(e){return this._lngLat=V.convert(e),this._pos=null,this._popup&&this._popup.setLngLat(this._lngLat),this._update(),this}getElement(){return this._element}setPopup(e){if(this._popup&&(this._popup.remove(),this._popup=null,this._element.removeEventListener(`keypress`,this._onKeyPress)),e){if(!(`offset`in e.options)){let t=13.5/Math.SQRT2;e.options.offset=this._defaultMarker?{top:[0,0],"top-left":[0,0],"top-right":[0,0],bottom:[0,-38.1],"bottom-left":[t,(24.6+t)*-1],"bottom-right":[-t,(24.6+t)*-1],left:[13.5,-24.6],right:[-13.5,-24.6]}:this._offset}this._popup=e,this._element.addEventListener(`keypress`,this._onKeyPress)}return this._updateTabIndex(),this._updateAccessibilityRole(),this}setSubpixelPositioning(e){return this._subpixelPositioning=e,this}_endKeyboardDrag(){this._keyboardDragActive&&(this._keyboardDragActive=!1,this.fire(new _m(`dragend`)))}getPopup(){return this._popup}togglePopup(){let e=this._popup;if(this._element.style.opacity===this._opacityWhenCovered)return this;if(e)e.isOpen()?e.remove():(e.setLngLat(this._lngLat),e.addTo(this._map));else return this;return this}_updateOpacity(e=!1){let t=this._map?.terrain,n=this._map._camera.transform.isLocationOccluded(this._lngLat);if(!t||n){let e=n?this._opacityWhenCovered:this._opacity;this._element.style.opacity!==e&&(this._element.style.opacity=e,this._element.classList.toggle(`maplibregl-marker-covered`,n));return}if(e)this._opacityTimeout=null;else{if(this._opacityTimeout)return;this._opacityTimeout=setTimeout(()=>{this._opacityTimeout=null},100)}let r=this._map,i=r.terrain.depthAtPoint(this._pos),a=r.terrain.getElevationForLngLat(this._lngLat,r._camera.transform),o=r._camera.transform.lngLatToCameraDepth(this._lngLat,a),s=.006;if(o-is;this._popup?.isOpen()&&f&&this._popup.remove(),this._element.style.opacity=f?this._opacityWhenCovered:this._opacity,this._element.classList.toggle(`maplibregl-marker-covered`,f)}getOffset(){return this._offset}setOffset(e){return this._offset=l.convert(e),this._update(),this}addClassName(e){this._element.classList.add(e)}removeClassName(e){this._element.classList.remove(e)}toggleClassName(e){return this._element.classList.toggle(e)}setDraggable(e){return this._draggable=!!e,this._element.classList.toggle(`maplibregl-marker-draggable`,this._draggable),this._map&&(e?(this._map.on(`mousedown`,this._addDragHandler),this._map.on(`touchstart`,this._addDragHandler)):(this._map.off(`mousedown`,this._addDragHandler),this._map.off(`touchstart`,this._addDragHandler))),this._defaultMarker&&(this._draggable?(this._element.addEventListener(`keydown`,this._onKeyDown),this._element.addEventListener(`keyup`,this._onKeyUp),this._element.addEventListener(`blur`,this._onBlur)):(this._element.removeEventListener(`keydown`,this._onKeyDown),this._element.removeEventListener(`keyup`,this._onKeyUp),this._element.removeEventListener(`blur`,this._onBlur),this._endKeyboardDrag())),this._updateTabIndex(),this._updateAccessibilityRole(),this}isDraggable(){return this._draggable}_updateTabIndex(){this._popup||this._defaultMarker&&this._draggable?this._element.hasAttribute(`tabindex`)||(this._element.setAttribute(`tabindex`,`0`),this._tabIndexManaged=!0):this._tabIndexManaged&&=(this._element.getAttribute(`tabindex`)===`0`&&this._element.removeAttribute(`tabindex`),!1)}_updateAccessibilityRole(){if(!this._defaultMarker||this._element.hasAttribute(`role`)&&!this._roleManaged)return;let e=this._draggable||this._popup?`button`:`img`;this._element.setAttribute(`role`,e),this._roleManaged=!0}setRotation(e){return this._rotation=e||0,this._update(),this}getRotation(){return this._rotation}setRotationAlignment(e){return this._rotationAlignment=e||`auto`,this._update(),this}getRotationAlignment(){return this._rotationAlignment}setPitchAlignment(e){return this._pitchAlignment=e&&e!==`auto`?e:this._rotationAlignment,this._update(),this}getPitchAlignment(){return this._pitchAlignment}setOpacity(e,t){return(this._opacity===void 0||e===void 0&&t===void 0)&&(this._opacity=`1`,this._opacityWhenCovered=`0.2`),e!==void 0&&(this._opacity=String(e)),t!==void 0&&(this._opacityWhenCovered=String(t)),this._map&&this._updateOpacity(!0),this}};const bm={positionOptions:{enableHighAccuracy:!1,maximumAge:0,timeout:6e3},fitBoundsOptions:{maxZoom:15},trackUserLocation:!1,showAccuracyCircle:!0,showUserLocation:!0};let xm=0,Sm=!1;var Cm=class extends Xe{},wm=class extends Xe{},Tm=class extends Xe{},Em=class extends h{constructor(e){super(),this._onSuccess=e=>{if(this._map){if(this._isOutOfMapMaxBounds(e)){this._setErrorState(),this.fire(new wm(`outofmaxbounds`,e)),this._updateMarker(),this._finish();return}if(this.options.trackUserLocation)switch(this._lastKnownPosition=e,this._watchState){case`WAITING_ACTIVE`:case`ACTIVE_LOCK`:case`ACTIVE_ERROR`:this._watchState=`ACTIVE_LOCK`,this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-waiting`),this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-active-error`),this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-active`);break;case`BACKGROUND`:case`BACKGROUND_ERROR`:this._watchState=`BACKGROUND`,this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-waiting`),this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-background-error`),this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-background`);break;default:throw Error(`Unexpected watchState ${this._watchState}`)}this.options.showUserLocation&&this._watchState!==`OFF`&&this._updateMarker(e),(!this.options.trackUserLocation||this._watchState===`ACTIVE_LOCK`)&&this._updateCamera(e),this.options.showUserLocation&&this._dotElement.classList.remove(`maplibregl-user-location-dot-stale`),this.fire(new wm(`geolocate`,e)),this._finish()}},this._updateCamera=e=>{let t=new V(e.coords.longitude,e.coords.latitude),n=e.coords.accuracy,r=this._map.getBearing(),i=z({bearing:r},this.options.fitBoundsOptions),a=_a.fromLngLat(t,n);this._map.fitBounds(a,i,{geolocateSource:!0})},this._updateMarker=e=>{if(e){let t=new V(e.coords.longitude,e.coords.latitude);this._accuracyCircleMarker.setLngLat(t).addTo(this._map),this._userLocationDotMarker.setLngLat(t).addTo(this._map),this._accuracy=e.coords.accuracy,this._updateCircleRadiusIfNeeded()}else this._userLocationDotMarker.remove(),this._accuracyCircleMarker.remove()},this._onUpdate=()=>{this._updateCircleRadiusIfNeeded()},this._onError=e=>{if(this._map){if(e.code===1){this._watchState=`OFF`,this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-waiting`),this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-active`),this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-active-error`),this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-background`),this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-background-error`),this._geolocateButton.disabled=!0;let e=this._map._getUIString(`GeolocateControl.LocationNotAvailable`);this._geolocateButton.title=e,this._geolocateButton.setAttribute(`aria-label`,e),this._geolocationWatchID!==void 0&&this._clearWatch()}else if(e.code===3&&Sm)return;else this._setErrorState();this._watchState!==`OFF`&&this.options.showUserLocation&&this._dotElement.classList.add(`maplibregl-user-location-dot-stale`),this.fire(new Tm(`error`,e)),this._finish()}},this._finish=()=>{this._timeoutId&&clearTimeout(this._timeoutId),this._timeoutId=void 0},this._onMoveStart=e=>{if(!this._map)return;let t=e?.[0]instanceof ResizeObserverEntry;!e.geolocateSource&&this._watchState===`ACTIVE_LOCK`&&!t&&!this._map.isZooming()&&(this._watchState=`BACKGROUND`,this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-background`),this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-active`),this.fire(new Cm(`trackuserlocationend`)),this.fire(new Cm(`userlocationlostfocus`)))},this._setupUI=()=>{this._map&&(this._container.addEventListener(`contextmenu`,e=>{e.preventDefault()}),this._geolocateButton=W.create(`button`,`maplibregl-ctrl-geolocate`,this._container),W.create(`span`,`maplibregl-ctrl-icon`,this._geolocateButton).setAttribute(`aria-hidden`,`true`),this._geolocateButton.type=`button`,this._geolocateButton.disabled=!0)},this._finishSetupUI=e=>{if(this._map){if(e===!1){I(`Geolocation support is not available so the GeolocateControl will be disabled.`);let e=this._map._getUIString(`GeolocateControl.LocationNotAvailable`);this._geolocateButton.disabled=!0,this._geolocateButton.title=e,this._geolocateButton.setAttribute(`aria-label`,e)}else{let e=this._map._getUIString(`GeolocateControl.FindMyLocation`);this._geolocateButton.disabled=!1,this._geolocateButton.title=e,this._geolocateButton.setAttribute(`aria-label`,e)}this.options.trackUserLocation&&(this._geolocateButton.setAttribute(`aria-pressed`,`false`),this._watchState=`OFF`),this.options.showUserLocation&&(this._dotElement=W.create(`div`,`maplibregl-user-location-dot`),this._userLocationDotMarker=new ym({element:this._dotElement}),this._circleElement=W.create(`div`,`maplibregl-user-location-accuracy-circle`),this._accuracyCircleMarker=new ym({element:this._circleElement,pitchAlignment:`map`}),this.options.trackUserLocation&&(this._watchState=`OFF`),this._map.on(`zoom`,this._onUpdate),this._map.on(`move`,this._onUpdate),this._map.on(`rotate`,this._onUpdate),this._map.on(`pitch`,this._onUpdate)),this._geolocateButton.addEventListener(`click`,()=>this.trigger()),this._setup=!0,this.options.trackUserLocation&&this._map.on(`movestart`,this._onMoveStart)}},this.options=z({},bm,e)}onAdd(e){return this._map=e,this._container=W.create(`div`,`maplibregl-ctrl maplibregl-ctrl-group`),this._setupUI(),fm().then(e=>this._finishSetupUI(e)),this._container}onRemove(){this._geolocationWatchID!==void 0&&(window.navigator.geolocation.clearWatch(this._geolocationWatchID),this._geolocationWatchID=void 0),this.options.showUserLocation&&this._userLocationDotMarker&&this._userLocationDotMarker.remove(),this.options.showAccuracyCircle&&this._accuracyCircleMarker&&this._accuracyCircleMarker.remove(),this._container.remove(),this._map.off(`movestart`,this._onMoveStart),this._map.off(`zoom`,this._onUpdate),this._map.off(`move`,this._onUpdate),this._map.off(`rotate`,this._onUpdate),this._map.off(`pitch`,this._onUpdate),this._map=void 0,xm=0,Sm=!1}_isOutOfMapMaxBounds(e){let t=this._map.getMaxBounds(),n=e.coords;return t&&(n.longitudet.getEast()||n.latitudet.getNorth())}_setErrorState(){switch(this._watchState){case`WAITING_ACTIVE`:this._watchState=`ACTIVE_ERROR`,this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-active`),this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-active-error`);break;case`ACTIVE_LOCK`:this._watchState=`ACTIVE_ERROR`,this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-active`),this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-active-error`),this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-waiting`);break;case`BACKGROUND`:this._watchState=`BACKGROUND_ERROR`,this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-background`),this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-background-error`),this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-waiting`);break;case`ACTIVE_ERROR`:case`BACKGROUND_ERROR`:break;case`OFF`:case void 0:break;default:throw Error(`Unexpected watchState ${this._watchState}`)}}_updateCircleRadiusIfNeeded(){let e=this._userLocationDotMarker.getLngLat();if(!this.options.showUserLocation||!this.options.showAccuracyCircle||!this._accuracy||!e)return;let t=this._map.project(e),n=this._map.unproject([t.x+100,t.y]),r=e.distanceTo(n)/100,i=2*this._accuracy/r;this._circleElement.style.width=`${i.toFixed(2)}px`,this._circleElement.style.height=`${i.toFixed(2)}px`}trigger(){if(!this._setup)return I(`Geolocate control triggered before added to a map`),!1;if(this.options.trackUserLocation){switch(this._watchState){case`OFF`:this._watchState=`WAITING_ACTIVE`,this.fire(new Cm(`trackuserlocationstart`));break;case`WAITING_ACTIVE`:case`ACTIVE_LOCK`:case`ACTIVE_ERROR`:case`BACKGROUND_ERROR`:xm--,Sm=!1,this._watchState=`OFF`,this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-waiting`),this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-active`),this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-active-error`),this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-background`),this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-background-error`),this.fire(new Cm(`trackuserlocationend`));break;case`BACKGROUND`:this._watchState=`ACTIVE_LOCK`,this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-background`),this._lastKnownPosition&&this._updateCamera(this._lastKnownPosition),this.fire(new Cm(`trackuserlocationstart`)),this.fire(new Cm(`userlocationfocus`));break;default:throw Error(`Unexpected watchState ${this._watchState}`)}switch(this._watchState){case`WAITING_ACTIVE`:this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-waiting`),this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-active`);break;case`ACTIVE_LOCK`:this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-active`);break;case`OFF`:break;default:throw Error(`Unexpected watchState ${this._watchState}`)}if(this._watchState===`OFF`&&this._geolocationWatchID!==void 0)this._clearWatch();else if(this._geolocationWatchID===void 0){this._geolocateButton.classList.add(`maplibregl-ctrl-geolocate-waiting`),this._geolocateButton.setAttribute(`aria-pressed`,`true`),xm++;let e;xm>1?(e={maximumAge:6e5,timeout:0},Sm=!0):(e=this.options.positionOptions,Sm=!1),this._geolocationWatchID=window.navigator.geolocation.watchPosition(this._onSuccess,this._onError,e)}}else window.navigator.geolocation.getCurrentPosition(this._onSuccess,this._onError,this.options.positionOptions),this._timeoutId=setTimeout(this._finish,1e4);return!0}_clearWatch(){window.navigator.geolocation.clearWatch(this._geolocationWatchID),this._geolocationWatchID=void 0,this._geolocateButton.classList.remove(`maplibregl-ctrl-geolocate-waiting`),this._geolocateButton.setAttribute(`aria-pressed`,`false`),this.options.showUserLocation&&this._updateMarker(null)}};const Dm={maxWidth:100,unit:`metric`};var Om=class{constructor(e){this._onMove=()=>{km(this._map,this._container,this.options)},this.setUnit=e=>{this.options.unit=e,km(this._map,this._container,this.options)},this.options={...Dm,...e}}getDefaultPosition(){return`bottom-left`}onAdd(e){return this._map=e,this._container=W.create(`div`,`maplibregl-ctrl maplibregl-ctrl-scale`,e.getContainer()),this._map.on(`move`,this._onMove),this._onMove(),this._container}onRemove(){this._container.remove(),this._map.off(`move`,this._onMove),this._map=void 0}};function km(e,t,n){let r=n?.maxWidth||100,i=e._container.clientHeight/2,a=e._container.clientWidth/2,o=e.unproject([a-r/2,i]),s=e.unproject([a+r/2,i]),c=Math.round(e.project(s).x-e.project(o).x),l=Math.min(r,c,e._container.clientWidth),u=o.distanceTo(s);if(n?.unit===`imperial`){let n=3.2808*u;n>5280?Am(t,l,n/5280,e._getUIString(`ScaleControl.Miles`)):Am(t,l,n,e._getUIString(`ScaleControl.Feet`))}else n?.unit===`nautical`?Am(t,l,u/1852,e._getUIString(`ScaleControl.NauticalMiles`)):u>=1e3?Am(t,l,u/1e3,e._getUIString(`ScaleControl.Kilometers`)):Am(t,l,u,e._getUIString(`ScaleControl.Meters`))}function Am(e,t,n,r){let i=Mm(n),a=i/n;e.style.width=`${t*a}px`,e.innerHTML=`${i} ${r}`}function jm(e){let t=10**Math.ceil(-Math.log(e)/Math.LN10);return Math.round(e*t)/t}function Mm(e){let t=10**(`${Math.floor(e)}`.length-1),n=e/t;return n=n>=10?10:n>=5?5:n>=3?3:n>=2?2:n>=1?1:jm(n),t*n}var Nm=class extends Xe{},Pm=class extends h{constructor(e={}){super(),this._onFullscreenChange=()=>{let e=window.document.fullscreenElement||window.document.webkitFullscreenElement;for(;e?.shadowRoot?.fullscreenElement;)e=e.shadowRoot.fullscreenElement;e===this._container!==this._fullscreen&&this._handleFullscreenChange()},this._onClickFullscreen=()=>{this._isFullscreen()?this._exitFullscreen():this._requestFullscreen()},this._fullscreen=!1,this._pseudo=e.pseudo??!1,e?.container&&(e.container instanceof HTMLElement?this._container=e.container:I(`Full screen control 'container' must be a DOM element.`)),`onfullscreenchange`in document?this._fullscreenchange=`fullscreenchange`:`onmozfullscreenchange`in document?this._fullscreenchange=`mozfullscreenchange`:`onwebkitfullscreenchange`in document?this._fullscreenchange=`webkitfullscreenchange`:`onmsfullscreenchange`in document&&(this._fullscreenchange=`MSFullscreenChange`)}onAdd(e){return this._map=e,this._container||=this._map.getContainer(),this._controlContainer=W.create(`div`,`maplibregl-ctrl maplibregl-ctrl-group`),this._setupUI(),this._controlContainer}onRemove(){this._controlContainer.remove(),this._map=null,window.document.removeEventListener(this._fullscreenchange,this._onFullscreenChange)}_setupUI(){let e=this._fullscreenButton=W.create(`button`,`maplibregl-ctrl-fullscreen`,this._controlContainer);W.create(`span`,`maplibregl-ctrl-icon`,e).setAttribute(`aria-hidden`,`true`),e.type=`button`,this._updateTitle(),this._fullscreenButton.addEventListener(`click`,this._onClickFullscreen),window.document.addEventListener(this._fullscreenchange,this._onFullscreenChange)}_updateTitle(){let e=this._getTitle();this._fullscreenButton.setAttribute(`aria-label`,e),this._fullscreenButton.title=e}_getTitle(){return this._map._getUIString(this._isFullscreen()?`FullscreenControl.Exit`:`FullscreenControl.Enter`)}_isFullscreen(){return this._fullscreen}_handleFullscreenChange(){this._fullscreen=!this._fullscreen,this._fullscreenButton.classList.toggle(`maplibregl-ctrl-shrink`),this._fullscreenButton.classList.toggle(`maplibregl-ctrl-fullscreen`),this._updateTitle(),this._fullscreen?(this.fire(new Nm(`fullscreenstart`)),this._prevCooperativeGesturesEnabled=this._map.cooperativeGestures.isEnabled(),this._map.cooperativeGestures.disable()):(this.fire(new Nm(`fullscreenend`)),this._prevCooperativeGesturesEnabled&&this._map.cooperativeGestures.enable())}_exitFullscreen(){this._pseudo?this._togglePseudoFullScreen():window.document.exitFullscreen?window.document.exitFullscreen():window.document.webkitCancelFullScreen?window.document.webkitCancelFullScreen():this._togglePseudoFullScreen()}_requestFullscreen(){this._pseudo?this._togglePseudoFullScreen():this._container.requestFullscreen?this._container.requestFullscreen():this._container.webkitRequestFullscreen?this._container.webkitRequestFullscreen():this._togglePseudoFullScreen()}_togglePseudoFullScreen(){this._container.classList.toggle(`maplibregl-pseudo-fullscreen`),this._handleFullscreenChange(),this._map.resize()}},Fm=class{constructor(e){this._toggleTerrain=()=>{this._map.getTerrain()?this._map.setTerrain(null):this._map.setTerrain(this.options),this._updateTerrainIcon()},this._updateTerrainIcon=()=>{this._terrainButton.classList.remove(`maplibregl-ctrl-terrain`),this._terrainButton.classList.remove(`maplibregl-ctrl-terrain-enabled`),this._map.terrain?(this._terrainButton.classList.add(`maplibregl-ctrl-terrain-enabled`),this._terrainButton.title=this._map._getUIString(`TerrainControl.Disable`)):(this._terrainButton.classList.add(`maplibregl-ctrl-terrain`),this._terrainButton.title=this._map._getUIString(`TerrainControl.Enable`))},this.options=e}onAdd(e){return this._map=e,this._container=W.create(`div`,`maplibregl-ctrl maplibregl-ctrl-group`),this._terrainButton=W.create(`button`,`maplibregl-ctrl-terrain`,this._container),W.create(`span`,`maplibregl-ctrl-icon`,this._terrainButton).setAttribute(`aria-hidden`,`true`),this._terrainButton.type=`button`,this._terrainButton.addEventListener(`click`,this._toggleTerrain),this._updateTerrainIcon(),this._map.on(`terrain`,this._updateTerrainIcon),this._container}onRemove(){this._container.remove(),this._map.off(`terrain`,this._updateTerrainIcon),this._map=void 0}},Im=class{constructor(){this._toggleProjection=()=>{let e=this._map.getProjection()?.type;e===`mercator`||!e?this._map.setProjection({type:`globe`}):this._map.setProjection({type:`mercator`}),this._updateGlobeIcon()},this._updateGlobeIcon=()=>{this._globeButton.classList.remove(`maplibregl-ctrl-globe`),this._globeButton.classList.remove(`maplibregl-ctrl-globe-enabled`),this._map.getProjection()?.type===`globe`?(this._globeButton.classList.add(`maplibregl-ctrl-globe-enabled`),this._globeButton.title=this._map._getUIString(`GlobeControl.Disable`)):(this._globeButton.classList.add(`maplibregl-ctrl-globe`),this._globeButton.title=this._map._getUIString(`GlobeControl.Enable`))}}onAdd(e){return this._map=e,this._container=W.create(`div`,`maplibregl-ctrl maplibregl-ctrl-group`),this._globeButton=W.create(`button`,`maplibregl-ctrl-globe`,this._container),W.create(`span`,`maplibregl-ctrl-icon`,this._globeButton).setAttribute(`aria-hidden`,`true`),this._globeButton.type=`button`,this._globeButton.addEventListener(`click`,this._toggleProjection),this._updateGlobeIcon(),this._map.on(`styledata`,this._updateGlobeIcon),this._map.on(`projectiontransition`,this._updateGlobeIcon),this._container}onRemove(){this._container.remove(),this._map.off(`styledata`,this._updateGlobeIcon),this._map.off(`projectiontransition`,this._updateGlobeIcon),this._globeButton.removeEventListener(`click`,this._toggleProjection),this._map=void 0}};const Lm={closeButton:!0,closeOnClick:!0,focusAfterOpen:!0,className:``,maxWidth:`240px`,subpixelPositioning:!1,locationOccludedOpacity:void 0,padding:void 0},Rm=[`a[href]`,`[tabindex]:not([tabindex='-1'])`,`[contenteditable]:not([contenteditable='false'])`,`button:not([disabled])`,`input:not([disabled])`,`select:not([disabled])`,`textarea:not([disabled])`].join(`, `);var zm=class extends Xe{},Bm=class extends h{constructor(e){super(),this._updateOpacity=()=>{this.options.locationOccludedOpacity!==void 0&&(this._map._camera.transform.isLocationOccluded(this.getLngLat())?this._container.style.opacity=`${this.options.locationOccludedOpacity}`:this._container.style.opacity=``)},this.remove=()=>(this._content&&this._content.remove(),this._container&&(this._container.remove(),delete this._container),this._map&&(this._map.off(`move`,this._update),this._map.off(`move`,this._onClose),this._map.off(`click`,this._onClose),this._map.off(`remove`,this.remove),this._map.off(`terrain`,this._update),this._map.off(`projectiontransition`,this._update),this._map.off(`mousemove`,this._update),this._map.off(`mouseup`,this._update),this._map.off(`drag`,this._update),this._map._canvasContainer.classList.remove(`maplibregl-track-pointer`),delete this._map,this.fire(new zm(`close`))),this),this._update=e=>{let t=this._lngLat||this._trackPointer;if(!this._map||!t||!this._content)return;if(!this._container){if(this._container=W.create(`div`,`maplibregl-popup`,this._map.getContainer()),this._tip=W.create(`div`,`maplibregl-popup-tip`,this._container),this._container.appendChild(this._content),this.options.className)for(let e of this.options.className.split(` `))this._container.classList.add(e);this._closeButton&&this._closeButton.setAttribute(`aria-label`,this._map._getUIString(`Popup.Close`)),this._trackPointer&&this._container.classList.add(`maplibregl-popup-track-pointer`)}this.options.maxWidth&&this._container.style.maxWidth!==this.options.maxWidth&&(this._container.style.maxWidth=this.options.maxWidth),this._lngLat=pm(this._lngLat,this._flatPos,this._map._camera.transform,this._trackPointer);let n;if(e&&`point`in e&&e.point&&(n=e.point),this._trackPointer&&!n)return;let r=this._flatPos=this._pos=this._trackPointer&&n?n:this._map.project(this._lngLat);this._map.terrain&&(this._flatPos=this._trackPointer&&n?n:this._map._camera.transform.locationToScreenPoint(this._lngLat));let i=this.options.anchor,a=Vm(this.options.offset);if(!i){let e=this._container.offsetWidth,t=this._container.offsetHeight,n=Hm(this.options.padding),o;o=r.y+a.bottom.ythis._map._camera.transform.height-t-n.bottom?[`bottom`]:[],r.xthis._map._camera.transform.width-e/2-n.right&&o.push(`right`),i=o.length===0?`bottom`:o.join(`-`)}let o=r.add(a[i]);this.options.subpixelPositioning||(o=o.round()),this._container.style.transform=`${mm[i]} translate(${o.x}px,${o.y}px)`,hm(this._container,i,`popup`),this._updateOpacity()},this._onClose=()=>{this.remove()},this.options=z(Object.create(Lm),e)}addTo(e){return this._map&&this.remove(),this._map=e,this.options.closeOnClick&&this._map.on(`click`,this._onClose),this.options.closeOnMove&&this._map.on(`move`,this._onClose),this._map.on(`remove`,this.remove),this._map.on(`terrain`,this._update),this._map.on(`projectiontransition`,this._update),this._update(),this._focusFirstElement(),this._trackPointer?(this._map.on(`mousemove`,this._update),this._map.on(`mouseup`,this._update),this._container&&this._container.classList.add(`maplibregl-popup-track-pointer`),this._map._canvasContainer.classList.add(`maplibregl-track-pointer`)):this._map.on(`move`,this._update),this.fire(new zm(`open`)),this}isOpen(){return!!this._map}getLngLat(){return this._lngLat}setLngLat(e){return this._lngLat=V.convert(e),this._pos=null,this._flatPos=null,this._trackPointer=!1,this._update(),this._map&&(this._map.on(`move`,this._update),this._map.off(`mousemove`,this._update),this._container&&this._container.classList.remove(`maplibregl-popup-track-pointer`),this._map._canvasContainer.classList.remove(`maplibregl-track-pointer`)),this}trackPointer(){return this._trackPointer=!0,this._pos=null,this._flatPos=null,this._update(),this._map&&(this._map.off(`move`,this._update),this._map.on(`mousemove`,this._update),this._map.on(`drag`,this._update),this._container&&this._container.classList.add(`maplibregl-popup-track-pointer`),this._map._canvasContainer.classList.add(`maplibregl-track-pointer`)),this}getElement(){return this._container}setText(e){return this.setDOMContent(document.createTextNode(e))}setHTML(e){let t=document.createDocumentFragment(),n=document.createElement(`body`),r;for(n.innerHTML=e;r=n.firstChild,r;)t.appendChild(r);return this.setDOMContent(t)}getMaxWidth(){return this._container?.style.maxWidth}setMaxWidth(e){return this.options.maxWidth=e,this._update(),this}setDOMContent(e){if(this._content)for(;this._content.hasChildNodes();)this._content.firstChild&&this._content.removeChild(this._content.firstChild);else this._content=W.create(`div`,`maplibregl-popup-content`,this._container);return this._content.appendChild(e),this._createCloseButton(),this._update(),this._focusFirstElement(),this}addClassName(e){return this._container&&this._container.classList.add(e),this}removeClassName(e){return this._container&&this._container.classList.remove(e),this}setOffset(e){return this.options.offset=e,this._update(),this}toggleClassName(e){if(this._container)return this._container.classList.toggle(e)}setSubpixelPositioning(e){this.options.subpixelPositioning=e}setPadding(e){this.options.padding=e,this._update()}_createCloseButton(){this.options.closeButton&&(this._closeButton=W.create(`button`,`maplibregl-popup-close-button`,this._content),this._closeButton.type=`button`,this._closeButton.innerHTML=`×`,this._closeButton.addEventListener(`click`,this._onClose))}_focusFirstElement(){if(!this.options.focusAfterOpen||!this._container)return;let e=this._container.querySelector(Rm);e&&e.focus()}};function Vm(e){if(!e)return Vm(new l(0,0));if(typeof e==`number`){let t=Math.round(Math.abs(e)/Math.SQRT2);return{center:new l(0,0),top:new l(0,e),"top-left":new l(t,t),"top-right":new l(-t,t),bottom:new l(0,-e),"bottom-left":new l(t,-t),"bottom-right":new l(-t,-t),left:new l(e,0),right:new l(-e,0)}}if(e instanceof l||Array.isArray(e)){let t=l.convert(e);return{center:t,top:t,"top-left":t,"top-right":t,bottom:t,"bottom-left":t,"bottom-right":t,left:t,right:t}}return{center:l.convert(e.center||[0,0]),top:l.convert(e.top||[0,0]),"top-left":l.convert(e[`top-left`]||[0,0]),"top-right":l.convert(e[`top-right`]||[0,0]),bottom:l.convert(e.bottom||[0,0]),"bottom-left":l.convert(e[`bottom-left`]||[0,0]),"bottom-right":l.convert(e[`bottom-right`]||[0,0]),left:l.convert(e.left||[0,0]),right:l.convert(e.right||[0,0])}}function Hm(e){return e?{top:e.top??0,right:e.right??0,bottom:e.bottom??0,left:e.left??0}:{top:0,right:0,bottom:0,left:0}}const Um=Ar;function Wm(e,t){return fo().setRTLTextPlugin(e,t)}function Gm(){return fo().getRTLTextPluginStatus()}function Km(){return Um}function qm(){return Zi.workerCount}function Jm(e){Zi.workerCount=e}function Ym(){return k.MAX_PARALLEL_IMAGE_REQUESTS}function Xm(e){k.MAX_PARALLEL_IMAGE_REQUESTS=e}function Zm(){return k.WORKER_URL}function Qm(e){k.WORKER_URL=e}async function $m(e){await aa().broadcast(`IS`,e)}export{mr as AJAXError,$p as AttributionControl,sp as BoxZoomHandler,to as CanvasSource,Gp as CooperativeGesturesHandler,zp as DoubleClickZoomHandler,Hp as DragPanHandler,Up as DragRotateHandler,N as EXTENT,ec as EdgeInsets,H as ErrorEvent,Xe as Event,h as Evented,Pm as FullscreenControl,Nm as FullscreenEvent,qf as GPUInitializationError,Ia as GeoJSONSource,Em as GeolocateControl,Tm as GeolocateErrorEvent,Cm as GeolocateEvent,wm as GeolocatePositionEvent,Im as GlobeControl,Yf as Hash,Ja as ImageSource,Fp as KeyboardHandler,V as LngLat,_a as LngLatBounds,em as LogoControl,sm as Map,sm as MapLibreMap,Zr as MapBoxZoomEvent,ei as MapContextEvent,Gr as MapLibreEvent,Jr as MapMouseEvent,G as MapMovementEvent,$r as MapProjectionEvent,K as MapSourceDataEvent,qr as MapStyleDataEvent,ti as MapStyleImageMissingEvent,Kr as MapStyleLoadEvent,Qr as MapTerrainEvent,Yr as MapTouchEvent,Xr as MapWheelEvent,ym as Marker,vm as MarkerClickEvent,_m as MarkerDragEvent,B as MercatorCoordinate,lm as NavigationControl,l as Point,Bm as Popup,zm as PopupEvent,xa as RasterDEMTileSource,ba as RasterTileSource,Om as ScaleControl,Rp as ScrollZoomHandler,ml as Style,Fm as TerrainControl,Np as TwoFingersTouchPitchHandler,jp as TwoFingersTouchRotateHandler,kp as TwoFingersTouchZoomHandler,Wp as TwoFingersTouchZoomRotateHandler,ya as VectorTileSource,eo as VideoSource,Te as addProtocol,oo as addSourceType,na as clearPrewarmedResources,k as config,qa as createTileMesh,aa as getGlobalDispatcher,Ym as getMaxParallelImageRequests,Gm as getRTLTextPluginStatus,Km as getVersion,qm as getWorkerCount,Zm as getWorkerUrl,$m as importScriptInWorkers,Hr as isTimeFrozen,U as now,ta as prewarm,Re as removeProtocol,Vr as restoreNow,Xm as setMaxParallelImageRequests,Br as setNow,Wm as setRTLTextPlugin,Jm as setWorkerCount,Qm as setWorkerUrl}; //# sourceMappingURL=maplibre-gl.mjs.map \ No newline at end of file From 00d61a19b234003014bef204fadc4b0df5e200f9 Mon Sep 17 00:00:00 2001 From: Claude Opus 5 Date: Fri, 31 Jul 2026 10:57:21 +0200 Subject: [PATCH 07/26] feat(roofmodel): derive PV array geometry from Lantmateriet open geodata A Swedish site can stop typing panel angles in by hand. The new roofmodel/ module reads Lantmateriet LiDAR around the site and recovers each roof face's tilt, azimuth and usable area, which pre-fill weather.pv_arrays. Structured as a separate Python package alongside optimizer/, reached at arm's length: core spawns it, passes coordinates and the operator's own Geotorget credentials, and reads one versioned roof_model.json from stdout. Segmentation drags in a compiled point-cloud stack and runs for minutes, so a time-boxed subprocess keeps it off the control tick and lets it be absent entirely - the normal case, since the data exists only for Sweden. The pipeline follows the SPAN method: iterative RANSAC pulls one surface at a time out of the cloud, then DBSCAN splits faces sharing a plane equation but not a location, because two wings of a building fit the same plane and are not the same roof. Method only - no code is taken from SPAN's GPL QGIS plugin, and the dependencies (numpy, scikit-learn, requests) are all BSD. SWEREF 99 TM is implemented directly rather than via pyproj: it is one projection with fixed parameters, and a full PROJ build is disproportionate for that. Verified against the projection's exact analytic properties - easting on the central meridian is exactly 500000 at every latitude - and by sub-millimetre round trips from Smygehuk to Treriksroeset. Applying derived arrays to config is deliberately a separate act. Derivation is a best guess from a point cloud that may be years old, and silently rewriting an operator's panel config is a change they should make knowingly. Also generalises the __pycache__ ignore rule, which was optimizer-specific and would otherwise need repeating per module. Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com> Signed-off-by: Hugo Karlsson <48095810+HuggeK@users.noreply.github.com> --- .changeset/roofmodel-lantmateriet.md | 33 +++ .gitignore | 6 +- go/cmd/ftw/main.go | 6 + go/internal/api/api.go | 86 +++++++- go/internal/api/api_roofmodel_test.go | 103 +++++++++ go/internal/config/config.go | 29 +++ go/internal/coverage/coverage.go | 13 ++ go/internal/roofmodel/roofmodel.go | 227 +++++++++++++++++++ go/internal/roofmodel/roofmodel_test.go | 279 ++++++++++++++++++++++++ roofmodel/ftw_roofmodel/__init__.py | 18 ++ roofmodel/ftw_roofmodel/__main__.py | 48 ++++ roofmodel/ftw_roofmodel/geotorget.py | 184 ++++++++++++++++ roofmodel/ftw_roofmodel/pipeline.py | 207 ++++++++++++++++++ roofmodel/ftw_roofmodel/segment.py | 243 +++++++++++++++++++++ roofmodel/ftw_roofmodel/sweref.py | 173 +++++++++++++++ roofmodel/pyproject.toml | 26 +++ roofmodel/tests/__init__.py | 0 roofmodel/tests/test_pipeline.py | 265 ++++++++++++++++++++++ roofmodel/tests/test_segment.py | 188 ++++++++++++++++ roofmodel/tests/test_sweref.py | 123 +++++++++++ 20 files changed, 2248 insertions(+), 9 deletions(-) create mode 100644 .changeset/roofmodel-lantmateriet.md create mode 100644 go/internal/api/api_roofmodel_test.go create mode 100644 go/internal/roofmodel/roofmodel.go create mode 100644 go/internal/roofmodel/roofmodel_test.go create mode 100644 roofmodel/ftw_roofmodel/__init__.py create mode 100644 roofmodel/ftw_roofmodel/__main__.py create mode 100644 roofmodel/ftw_roofmodel/geotorget.py create mode 100644 roofmodel/ftw_roofmodel/pipeline.py create mode 100644 roofmodel/ftw_roofmodel/segment.py create mode 100644 roofmodel/ftw_roofmodel/sweref.py create mode 100644 roofmodel/pyproject.toml create mode 100644 roofmodel/tests/__init__.py create mode 100644 roofmodel/tests/test_pipeline.py create mode 100644 roofmodel/tests/test_segment.py create mode 100644 roofmodel/tests/test_sweref.py diff --git a/.changeset/roofmodel-lantmateriet.md b/.changeset/roofmodel-lantmateriet.md new file mode 100644 index 000000000..4143658b5 --- /dev/null +++ b/.changeset/roofmodel-lantmateriet.md @@ -0,0 +1,33 @@ +--- +"ftw": minor +--- + +New optional roof-geometry module derives PV array tilt, azimuth and kWp from +Lantmäteriet open geodata, so a Swedish site can stop typing panel angles in by +hand. + +`roofmodel/` is a separate Python package alongside `optimizer/`, invoked at +arm's length: core spawns it, hands it coordinates and the operator's own +Geotorget credentials, and reads back one versioned `roof_model.json`. LiDAR +segmentation drags in a compiled point-cloud stack and runs for minutes, so +keeping it in a time-boxed subprocess means it cannot stall the control tick or +leak into the daemon — and it can be absent entirely, which is the normal case, +since the data only exists for Sweden. + +The pipeline follows the SPAN method (Yavuzdoğan, *Renewable Energy* 2023): +iterative RANSAC plane fitting pulls one roof surface at a time out of the point +cloud, then DBSCAN splits faces that share a plane equation but not a location — +two wings of a building fit the same plane and are not the same roof. Method +only; no code is taken from SPAN's GPL QGIS plugin, and the module depends on +numpy, scikit-learn and requests, all BSD. + +`GET /api/roofmodel` reports availability and coverage; `POST +/api/roofmodel/derive` runs a derive and returns the proposed arrays. Applying +them to `weather.pv_arrays` is deliberately left as a separate explicit act: +derivation is a best guess from a point cloud that may be years old, and +silently rewriting an operator's panel config is a change they should make +knowingly. Lantmäteriet also joins the `/api/data-sources` registry as a +Sweden-only, credential-gated source. + +Off by default. Absent credentials, absent module or a non-Swedish site all +produce a clean explanation rather than a failure. diff --git a/.gitignore b/.gitignore index 827e4730d..af189ee28 100644 --- a/.gitignore +++ b/.gitignore @@ -71,7 +71,11 @@ node_modules/ release-notes.md optimizer/.venv/ optimizer/.pytest_cache/ -optimizer/**/__pycache__/ +# Python build artefacts from any module, not just the optimizer — roofmodel/ +# is a second one, and a third would otherwise repeat this again. +**/__pycache__/ +**/.pytest_cache/ +*.pyc # TLS material — never commit. *.pem diff --git a/go/cmd/ftw/main.go b/go/cmd/ftw/main.go index 219e7b19c..ad97f7b69 100644 --- a/go/cmd/ftw/main.go +++ b/go/cmd/ftw/main.go @@ -65,6 +65,7 @@ import ( "github.com/srcfl/ftw/go/internal/proxy" "github.com/srcfl/ftw/go/internal/pvmodel" "github.com/srcfl/ftw/go/internal/pvperf" + "github.com/srcfl/ftw/go/internal/roofmodel" "github.com/srcfl/ftw/go/internal/selftune" "github.com/srcfl/ftw/go/internal/selfupdate" "github.com/srcfl/ftw/go/internal/state" @@ -1210,6 +1211,10 @@ func main() { // PV scoring. Nil when the site has no PV geometry to score against. This is // read-only with respect to control: it only fetches weather data and writes // the irradiance_history + pv_performance_daily tables. + // Optional roof-geometry module: nil unless explicitly enabled, and + // stateless — it only runs when an operator asks for a derive. + roofModelSvc := roofmodel.FromConfig(cfg.RoofModel) + pvPerfSvc := pvperf.FromConfig(cfg.Weather, ratedPVW, st, "ftw/"+Version+" github.com/srcfl/ftw") if pvPerfSvc != nil { @@ -2590,6 +2595,7 @@ func main() { Prices: priceSvc, Forecast: forecastSvc, PVPerf: pvPerfSvc, + RoofModel: roofModelSvc, MPC: mpcSvc, PlannerPrefs: plannerPrefs, PVModel: pvSvc, diff --git a/go/internal/api/api.go b/go/internal/api/api.go index 54ab20f59..52dd07ab4 100644 --- a/go/internal/api/api.go +++ b/go/internal/api/api.go @@ -52,6 +52,7 @@ import ( "github.com/srcfl/ftw/go/internal/prices" "github.com/srcfl/ftw/go/internal/pvmodel" "github.com/srcfl/ftw/go/internal/pvperf" + "github.com/srcfl/ftw/go/internal/roofmodel" "github.com/srcfl/ftw/go/internal/scanner" "github.com/srcfl/ftw/go/internal/selftune" "github.com/srcfl/ftw/go/internal/selfupdate" @@ -145,6 +146,10 @@ type Deps struct { // PV geometry to score against (surfaced as {enabled:false}). PVPerf *pvperf.Service + // RoofModel is the optional Lantmäteriet roof-geometry module. nil when the + // module is absent or disabled, which is the normal case outside Sweden. + RoofModel *roofmodel.Service + // Optional: MPC planner. Nil if disabled or a buildMPC gate skipped it. MPC *mpc.Service @@ -503,6 +508,8 @@ func (s *Server) routes() { s.handle("GET /api/forecast", Read, s.handleForecast) s.handle("GET /api/pv/performance", Read, s.handlePVPerformance) s.handle("GET /api/data-sources", Read, s.handleDataSources) + s.handle("GET /api/roofmodel", Read, s.handleRoofModel) + s.handle("POST /api/roofmodel/derive", Configure, s.handleRoofModelDerive) s.handle("GET /api/mpc/plan", Read, s.handleMPCPlan) s.handle("POST /api/mpc/replan", Configure, s.handleMPCReplan) s.handle("GET /api/mpc/diagnose", Read, s.handleMPCDiagnose) @@ -2428,6 +2435,76 @@ func (s *Server) handleForecast(w http.ResponseWriter, r *http.Request) { writeJSON(w, 200, map[string]any{"items": rows, "enabled": true}) } +// ---- /api/roofmodel ---- +// +// GET reports whether roof derivation is available for this site and why not +// when it is unavailable. POST /api/roofmodel/derive runs it. +// +// Deliberately *not* wired: writing the derived arrays straight into +// weather.pv_arrays. Derivation is a best guess from a point cloud that may be +// years old, and silently rewriting an operator's panel config is a change they +// should make knowingly. The endpoint returns the proposal; applying it is a +// separate, explicit act. +func (s *Server) handleRoofModel(w http.ResponseWriter, r *http.Request) { + lat, lon, haveSite := s.siteLocation() + resp := map[string]any{"enabled": s.deps.RoofModel.Enabled()} + if haveSite { + resp["covers"] = coverage.Covers("lantmateriet", lat, lon) + resp["latitude"], resp["longitude"] = lat, lon + } + if src, ok := coverage.ByID("lantmateriet"); ok { + resp["area"] = src.Area + resp["license"] = src.License + resp["note"] = src.Note + } + writeJSON(w, 200, resp) +} + +func (s *Server) handleRoofModelDerive(w http.ResponseWriter, r *http.Request) { + if !s.deps.RoofModel.Enabled() { + writeJSON(w, 200, map[string]any{ + "enabled": false, + "error": "roof model module is not enabled", + }) + return + } + lat, lon, haveSite := s.siteLocation() + if !haveSite { + writeJSON(w, 400, map[string]any{"error": "site latitude/longitude is not configured"}) + return + } + // A derive downloads and segments LiDAR tiles; the service time-boxes it, + // but the request should also die with the client rather than outliving it. + model, err := s.deps.RoofModel.Derive(r.Context(), lat, lon) + if err != nil { + status := 502 + if errors.Is(err, roofmodel.ErrOutsideCoverage) || errors.Is(err, roofmodel.ErrNoCredentials) { + status = 400 + } + writeJSON(w, status, map[string]any{"error": err.Error()}) + return + } + writeJSON(w, 200, map[string]any{ + "enabled": true, + "model": model, + "proposed_arrays": model.ToPVArrays(), + }) +} + +// siteLocation returns the configured site coordinates. +func (s *Server) siteLocation() (lat, lon float64, ok bool) { + if s.deps.CfgMu == nil { + return 0, 0, false + } + s.deps.CfgMu.RLock() + defer s.deps.CfgMu.RUnlock() + if s.deps.Cfg == nil || s.deps.Cfg.Weather == nil { + return 0, 0, false + } + lat, lon = s.deps.Cfg.Weather.Latitude, s.deps.Cfg.Weather.Longitude + return lat, lon, lat != 0 || lon != 0 +} + // ---- /api/data-sources ---- // // Where each external data source works, and whether it covers this site. @@ -2446,14 +2523,7 @@ func (s *Server) handleDataSources(w http.ResponseWriter, r *http.Request) { // Weather is an optional config section, so it is nil on a site that has // never configured one — which is exactly the site most likely to be // looking at this endpoint. - if s.deps.CfgMu != nil { - s.deps.CfgMu.RLock() - if s.deps.Cfg != nil && s.deps.Cfg.Weather != nil { - lat, lon = s.deps.Cfg.Weather.Latitude, s.deps.Cfg.Weather.Longitude - haveSite = lat != 0 || lon != 0 - } - s.deps.CfgMu.RUnlock() - } + lat, lon, haveSite = s.siteLocation() // An explicit ?lat=&lon= overrides the configured site so the Weather tab // can preview coverage for a pin the operator is still dragging around, diff --git a/go/internal/api/api_roofmodel_test.go b/go/internal/api/api_roofmodel_test.go new file mode 100644 index 000000000..d36379896 --- /dev/null +++ b/go/internal/api/api_roofmodel_test.go @@ -0,0 +1,103 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func getJSON(t *testing.T, deps *Deps, method, path string) (int, map[string]any) { + t.Helper() + srv := New(deps) + req := httptest.NewRequest(method, path, nil) + rr := httptest.NewRecorder() + srv.Handler().ServeHTTP(rr, req) + var body map[string]any + if err := json.Unmarshal(rr.Body.Bytes(), &body); err != nil { + t.Fatalf("unreadable body %q: %v", rr.Body.String(), err) + } + return rr.Code, body +} + +// Absent module: the endpoint must answer calmly, the way every other optional +// service does, rather than 404 or 500. +func TestRoofModelDisabledReportsCleanly(t *testing.T) { + code, body := getJSON(t, depsAt(59.33, 18.07), http.MethodGet, "/api/roofmodel") + if code != 200 { + t.Fatalf("status = %d, want 200", code) + } + if body["enabled"] != false { + t.Errorf("enabled = %v, want false", body["enabled"]) + } +} + +// The metadata is the useful part when it is unavailable: it says where the +// data exists at all. +func TestRoofModelReportsCoverageForTheSite(t *testing.T) { + _, sthlm := getJSON(t, depsAt(59.33, 18.07), http.MethodGet, "/api/roofmodel") + if sthlm["covers"] != true { + t.Errorf("Stockholm covers = %v, want true", sthlm["covers"]) + } + if sthlm["area"] != "Sweden" { + t.Errorf("area = %v, want Sweden", sthlm["area"]) + } + + _, berlin := getJSON(t, depsAt(52.52, 13.40), http.MethodGet, "/api/roofmodel") + if berlin["covers"] != false { + t.Errorf("Berlin covers = %v, want false", berlin["covers"]) + } + // Even uncovered, it must still say where the data does exist. + if berlin["area"] != "Sweden" { + t.Errorf("area = %v, want Sweden even when uncovered", berlin["area"]) + } +} + +func TestRoofModelDeriveDisabledDoesNotError(t *testing.T) { + code, body := getJSON(t, depsAt(59.33, 18.07), http.MethodPost, "/api/roofmodel/derive") + if code != 200 { + t.Fatalf("status = %d, want 200", code) + } + if body["enabled"] != false { + t.Errorf("enabled = %v, want false", body["enabled"]) + } + if body["error"] == nil { + t.Error("a disabled derive should say why") + } +} + +// Without a location there is nothing to derive, and that is the operator's +// mistake to fix — so it is a 400, not a silent empty result. +func TestRoofModelDeriveWithoutASiteIsARequestError(t *testing.T) { + code, body := getJSON(t, &Deps{}, http.MethodPost, "/api/roofmodel/derive") + if code == 200 && body["enabled"] == false { + return // module disabled takes precedence, which is also correct + } + if code != 400 { + t.Errorf("status = %d, want 400", code) + } +} + +// Lantmäteriet must appear in the coverage listing alongside every other +// source, so an operator finds it without knowing it exists. +func TestLantmaterietAppearsInDataSources(t *testing.T) { + resp := getDataSources(t, depsAt(59.33, 18.07), "") + lm := find(t, resp, "lantmateriet") + if lm.Kind != "geodata" { + t.Errorf("kind = %q, want geodata", lm.Kind) + } + if !lm.RequiresKey { + t.Error("Geotorget access is credential-gated") + } + if lm.Worldwide { + t.Error("Lantmäteriet is Sweden only") + } + if lm.Covers == nil || !*lm.Covers { + t.Error("should cover Stockholm") + } + + away := getDataSources(t, depsAt(-33.87, 151.21), "") + if s := find(t, away, "lantmateriet"); s.Covers == nil || *s.Covers { + t.Error("must not claim to cover Sydney") + } +} diff --git a/go/internal/config/config.go b/go/internal/config/config.go index ab95bd361..280c21571 100644 --- a/go/internal/config/config.go +++ b/go/internal/config/config.go @@ -32,6 +32,7 @@ type Config struct { State *StateConf `yaml:"state,omitempty" json:"state,omitempty"` Price *Price `yaml:"price,omitempty" json:"price,omitempty"` Weather *Weather `yaml:"weather,omitempty" json:"weather,omitempty"` + RoofModel *RoofModel `yaml:"roofmodel,omitempty" json:"roofmodel,omitempty"` Planner *Planner `yaml:"planner,omitempty" json:"planner,omitempty"` Batteries map[string]Battery `yaml:"batteries,omitempty" json:"batteries,omitempty"` EVCharger *EVCharger `yaml:"ev_charger,omitempty" json:"ev_charger,omitempty"` @@ -1440,6 +1441,34 @@ type Price struct { ExportFloorOreKwh *float64 `yaml:"export_floor_ore_kwh,omitempty" json:"export_floor_ore_kwh,omitempty"` } +// RoofModel configures the optional Lantmäteriet roof-geometry module. +// +// Off by default and off the control tick entirely: it runs only when an +// operator asks for a derive during setup, in its own time-boxed subprocess, +// and its output only ever pre-fills the editable weather.pv_arrays. Absent or +// disabled, everything else behaves normally. +// +// GeotorgetToken is the operator's own credential. It is redacted in API +// responses by the existing sensitive-key rule (any key containing "token"). +type RoofModel struct { + Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"` + // Command is the interpreter used for the module; defaults to "python3". + Command string `yaml:"command,omitempty" json:"command,omitempty"` + ModuleDir string `yaml:"module_dir,omitempty" json:"module_dir,omitempty"` + + GeotorgetUsername string `yaml:"geotorget_username,omitempty" json:"geotorget_username,omitempty"` + GeotorgetToken string `yaml:"geotorget_token,omitempty" json:"geotorget_token,omitempty"` + + // RadiusM is how far around the site to pull LiDAR (default 40 m). + RadiusM float64 `yaml:"radius_m,omitempty" json:"radius_m,omitempty"` + // PackingFactor is the usable fraction of a roof face after ridges, eaves, + // chimneys and walkways (default 0.70). + PackingFactor float64 `yaml:"packing_factor,omitempty" json:"packing_factor,omitempty"` + // TimeoutS bounds a derive. LiDAR tiles are large and this runs on a Pi, + // so an unbounded run could sit on memory indefinitely (default 600). + TimeoutS int `yaml:"timeout_s,omitempty" json:"timeout_s,omitempty"` +} + // Weather is the weather-forecast source config. type Weather struct { Provider string `yaml:"provider" json:"provider"` // met_no | openweather | open_meteo | forecast_solar | none diff --git a/go/internal/coverage/coverage.go b/go/internal/coverage/coverage.go index 12ba85d90..9b65e6cc2 100644 --- a/go/internal/coverage/coverage.go +++ b/go/internal/coverage/coverage.go @@ -25,6 +25,7 @@ const ( KindForecast Kind = "forecast" KindIrradiance Kind = "irradiance" KindPrice Kind = "price" + KindGeodata Kind = "geodata" ) // BBox is an inclusive latitude/longitude bounding box in WGS84 degrees. @@ -115,6 +116,18 @@ var sources = []Source{ License: "CC BY 4.0", Note: "Historical only (1999 to ~1 day ago). Used for PV performance scoring and forecast calibration, never as a forward forecast.", }, + { + ID: "lantmateriet", Kind: KindGeodata, Label: "Lantmäteriet (buildings + LiDAR)", + Area: "Sweden", + Countries: []string{"SE"}, + // Sweden's national extent. Generous at the edges for the same reason + // as STRÅNG: a box cannot trace a coastline, and the upstream STAC + // search returning no tiles is the authoritative answer. + BBox: &BBox{MinLat: 55.0, MinLon: 10.5, MaxLat: 69.5, MaxLon: 24.5}, + RequiresKey: true, + License: "CC BY 4.0", + Note: "Free open data, but gated behind a Geotorget account the operator orders themselves. Used to derive roof tilt/azimuth; everywhere else that stays a manual entry.", + }, { ID: "sourceful", Kind: KindPrice, Label: "Sourceful (cached ENTSO-E)", Area: "Europe", diff --git a/go/internal/roofmodel/roofmodel.go b/go/internal/roofmodel/roofmodel.go new file mode 100644 index 000000000..9c9517cbd --- /dev/null +++ b/go/internal/roofmodel/roofmodel.go @@ -0,0 +1,227 @@ +// Package roofmodel invokes the optional Lantmäteriet roof-geometry module and +// turns its output into candidate PV arrays. +// +// The module is a separate Python package (roofmodel/, sibling to optimizer/) +// reached at arm's length: core spawns it, hands it coordinates and the +// operator's Geotorget credentials on the command line, and reads one versioned +// JSON document back from stdout. Core knows nothing about STAC, LAZ or plane +// fitting, and the module knows nothing about FTW. +// +// That boundary is the point. LiDAR segmentation drags in a compiled point-cloud +// stack, runs for minutes, and is onboarding-time work rather than runtime work. +// Keeping it in a subprocess means it cannot stall the control tick, cannot leak +// memory into the daemon, and can be absent entirely — which is the normal case, +// since the data only exists for Sweden. +// +// Nothing here is authoritative. Derived arrays *pre-fill* the operator's +// editable weather.pv_arrays; the numeric editor stays the final word. +package roofmodel + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "log/slog" + "os/exec" + "time" + + "github.com/srcfl/ftw/go/internal/config" + "github.com/srcfl/ftw/go/internal/coverage" +) + +var ( + // ErrDisabled means no roofmodel section is configured, or it is off. + ErrDisabled = errors.New("roof model module is not enabled") + // ErrOutsideCoverage means Lantmäteriet has no data for this site. + ErrOutsideCoverage = errors.New("outside Lantmäteriet coverage") + // ErrNoCredentials means Geotorget credentials are missing. + ErrNoCredentials = errors.New("Geotorget credentials are required") +) + +const ( + defaultCommand = "python3" + defaultRadiusM = 40.0 + defaultPackingFactor = 0.70 + defaultTimeout = 10 * time.Minute + // A roof model is small; anything larger is a runaway or a wrong command. + maxOutputBytes = 1 << 20 +) + +// Array is one derived candidate PV array. Field names mirror config.PVArray so +// the document can pre-fill weather.pv_arrays directly. +type Array struct { + Name string `json:"name"` + KWp float64 `json:"kwp"` + TiltDeg float64 `json:"tilt_deg"` + AzimuthDeg float64 `json:"azimuth_deg"` + AreaM2 float64 `json:"area_m2"` + SegmentID string `json:"segment_id"` +} + +// Model is the versioned document the module emits. +type Model struct { + SchemaVersion int `json:"schema_version"` + Arrays []Array `json:"arrays"` + PlanesFound int `json:"planes_found"` + Site struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + RadiusM float64 `json:"radius_m"` + } `json:"site"` + Source struct { + Provider string `json:"provider"` + Collection string `json:"collection"` + ItemCount int `json:"item_count"` + DatasetDatetime string `json:"dataset_datetime"` + } `json:"source"` + // CapturedAtMs is when Lantmäteriet flew the LiDAR. Null while their STAC + // datetime backfill is incomplete, which is a missing provenance date and + // not a failure — the UI shows "capture date unknown". + CapturedAtMs *int64 `json:"captured_at_ms"` + DerivedAtMs int64 `json:"derived_at_ms"` +} + +// moduleError is the JSON the module writes to stderr when it fails. +type moduleError struct { + Error string `json:"error"` + Kind string `json:"kind"` +} + +// Service derives roof models. The zero value is unusable; use FromConfig. +type Service struct { + cfg *config.RoofModel +} + +// FromConfig returns a Service, or nil when the module is not configured. A nil +// Service is safe to call: every method reports ErrDisabled. +func FromConfig(cfg *config.RoofModel) *Service { + if cfg == nil || !cfg.Enabled { + return nil + } + return &Service{cfg: cfg} +} + +// Enabled reports whether derives are possible. +func (s *Service) Enabled() bool { return s != nil && s.cfg != nil && s.cfg.Enabled } + +func (s *Service) timeout() time.Duration { + if s.cfg.TimeoutS > 0 { + return time.Duration(s.cfg.TimeoutS) * time.Second + } + return defaultTimeout +} + +func (s *Service) command() string { + if s.cfg.Command != "" { + return s.cfg.Command + } + return defaultCommand +} + +func (s *Service) radius() float64 { + if s.cfg.RadiusM > 0 { + return s.cfg.RadiusM + } + return defaultRadiusM +} + +func (s *Service) packingFactor() float64 { + if s.cfg.PackingFactor > 0 { + return s.cfg.PackingFactor + } + return defaultPackingFactor +} + +// Derive runs the module for one site. +// +// Coverage and credentials are checked before spawning anything: a site outside +// Sweden can never succeed, and a missing credential fails the same way every +// time, so neither is worth an interpreter start and a network round trip. +func (s *Service) Derive(ctx context.Context, lat, lon float64) (*Model, error) { + if !s.Enabled() { + return nil, ErrDisabled + } + if !coverage.Covers("lantmateriet", lat, lon) { + return nil, fmt.Errorf("%w: (%.4f, %.4f) is not in Sweden", ErrOutsideCoverage, lat, lon) + } + if s.cfg.GeotorgetUsername == "" || s.cfg.GeotorgetToken == "" { + return nil, ErrNoCredentials + } + + ctx, cancel := context.WithTimeout(ctx, s.timeout()) + defer cancel() + + args := []string{ + "-m", "ftw_roofmodel", + "--lat", fmt.Sprintf("%.6f", lat), + "--lon", fmt.Sprintf("%.6f", lon), + "--username", s.cfg.GeotorgetUsername, + "--token", s.cfg.GeotorgetToken, + "--radius-m", fmt.Sprintf("%.1f", s.radius()), + "--packing-factor", fmt.Sprintf("%.3f", s.packingFactor()), + } + cmd := exec.CommandContext(ctx, s.command(), args...) + if s.cfg.ModuleDir != "" { + cmd.Env = append(cmd.Environ(), "PYTHONPATH="+s.cfg.ModuleDir) + } + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + start := time.Now() + err := cmd.Run() + elapsed := time.Since(start) + + if ctx.Err() == context.DeadlineExceeded { + return nil, fmt.Errorf("roof model timed out after %s", s.timeout()) + } + if err != nil { + // The module reports failures as JSON on stderr so an operator sees a + // reason ("credentials rejected") rather than a Python traceback. + var me moduleError + if jsonErr := json.Unmarshal(bytes.TrimSpace(stderr.Bytes()), &me); jsonErr == nil && me.Error != "" { + return nil, fmt.Errorf("roof model: %s", me.Error) + } + return nil, fmt.Errorf("roof model failed: %w", err) + } + if stdout.Len() > maxOutputBytes { + return nil, fmt.Errorf("roof model returned %d bytes, refusing", stdout.Len()) + } + + var m Model + if err := json.Unmarshal(stdout.Bytes(), &m); err != nil { + return nil, fmt.Errorf("roof model returned unreadable output: %w", err) + } + if m.SchemaVersion != 1 { + return nil, fmt.Errorf("roof model schema_version %d is not supported", m.SchemaVersion) + } + + slog.Info("roof model derived", + "lat", lat, "lon", lon, "arrays", len(m.Arrays), + "planes", m.PlanesFound, "elapsed", elapsed) + return &m, nil +} + +// ToPVArrays converts derived arrays into config entries ready to be written +// into weather.pv_arrays. +func (m *Model) ToPVArrays() []config.PVArray { + if m == nil { + return nil + } + out := make([]config.PVArray, 0, len(m.Arrays)) + for _, a := range m.Arrays { + // Config keeps tilt and azimuth as pointers so an omitted field cannot + // pass for a valid 0°. A derived array always has both, so both are + // addressed here; the locals keep each entry pointing at its own copy. + tiltDeg, azimuthDeg := a.TiltDeg, a.AzimuthDeg + out = append(out, config.PVArray{ + Name: a.Name, + KWp: a.KWp, + TiltDeg: &tiltDeg, + AzimuthDeg: &azimuthDeg, + }) + } + return out +} diff --git a/go/internal/roofmodel/roofmodel_test.go b/go/internal/roofmodel/roofmodel_test.go new file mode 100644 index 000000000..078103024 --- /dev/null +++ b/go/internal/roofmodel/roofmodel_test.go @@ -0,0 +1,279 @@ +package roofmodel + +import ( + "context" + "errors" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + "time" + + "github.com/srcfl/ftw/go/internal/config" +) + +// stubModule writes a script that stands in for the Python module, so the +// subprocess contract itself is exercised — argument passing, stdout parsing, +// stderr error reporting, timeouts — without needing the geospatial stack +// installed on the machine running the tests. +func stubModule(t *testing.T, script string) (command, dir string) { + t.Helper() + dir = t.TempDir() + var path, interp string + if runtime.GOOS == "windows" { + path = filepath.Join(dir, "stub.bat") + interp = path + script = "@echo off\r\n" + script + } else { + path = filepath.Join(dir, "stub.sh") + interp = "sh" + script = "#!/bin/sh\n" + script + } + if err := os.WriteFile(path, []byte(script), 0o755); err != nil { + t.Fatal(err) + } + if runtime.GOOS == "windows" { + return interp, dir + } + return interp, dir +} + +func svc(t *testing.T, cfg *config.RoofModel) *Service { + t.Helper() + s := FromConfig(cfg) + if s == nil { + t.Fatal("FromConfig returned nil for an enabled config") + } + return s +} + +const stockholmLat, stockholmLon = 59.33, 18.07 + +func TestDisabledWhenAbsentOrOff(t *testing.T) { + if FromConfig(nil) != nil { + t.Error("nil config must not produce a service") + } + if FromConfig(&config.RoofModel{Enabled: false}) != nil { + t.Error("disabled config must not produce a service") + } + // A nil *Service must be safe to call, not a panic. + var s *Service + if s.Enabled() { + t.Error("nil service must report disabled") + } + if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon); !errors.Is(err, ErrDisabled) { + t.Errorf("err = %v, want ErrDisabled", err) + } +} + +// A site outside Sweden can never succeed, so it must fail before spawning an +// interpreter and hitting the network. +func TestDeriveRefusesOutsideSwedenWithoutSpawning(t *testing.T) { + s := svc(t, &config.RoofModel{ + Enabled: true, + Command: "definitely-not-a-real-command", + GeotorgetUsername: "u", GeotorgetToken: "t", + }) + for _, c := range []struct { + name string + lat, lon float64 + }{ + {"Berlin", 52.52, 13.40}, + {"Sydney", -33.87, 151.21}, + {"New York", 40.71, -74.01}, + {"north of Sweden", 71.0, 20.0}, + } { + _, err := s.Derive(context.Background(), c.lat, c.lon) + if !errors.Is(err, ErrOutsideCoverage) { + t.Errorf("%s: err = %v, want ErrOutsideCoverage", c.name, err) + } + } +} + +// Sweden is a long diagonal, so no lat/lon rectangle can trace its border — +// any box containing Sweden also contains parts of Norway and Finland. Oslo is +// the clearest example: it is west of Sweden but inside the box. +// +// This is the same advisory-superset property the coverage package documents +// for STRÅNG, and it is resolved upstream rather than geometrically: the STAC +// search returns no tiles and the module reports "Sweden only". Pinned so the +// box is not "tightened" into something that starts excluding real Swedish +// addresses near the border. +func TestSwedishBoxAdmitsSomeNonSwedishPointsByDesign(t *testing.T) { + s := svc(t, &config.RoofModel{ + Enabled: true, + Command: "definitely-not-a-real-command", + GeotorgetUsername: "u", GeotorgetToken: "t", + }) + _, err := s.Derive(context.Background(), 59.91, 10.75) // Oslo + if errors.Is(err, ErrOutsideCoverage) { + t.Skip("box now excludes Oslo; verify it still admits Strömstad and Haparanda") + } + if err == nil { + t.Fatal("want the spawn to fail, since the command does not exist") + } +} + +// The border towns are the reason the box stays generous: tightening it to +// exclude Oslo would start excluding real Swedish sites. +func TestSwedishBoxCoversBorderTowns(t *testing.T) { + s := svc(t, &config.RoofModel{ + Enabled: true, + Command: "definitely-not-a-real-command", + GeotorgetUsername: "u", GeotorgetToken: "t", + }) + for _, c := range []struct { + name string + lat, lon float64 + }{ + {"Strömstad (west coast, near Norway)", 58.94, 11.17}, + {"Haparanda (east, near Finland)", 65.83, 24.14}, + {"Karesuando (far north)", 68.44, 22.49}, + {"Smygehuk (far south)", 55.34, 13.36}, + } { + _, err := s.Derive(context.Background(), c.lat, c.lon) + if errors.Is(err, ErrOutsideCoverage) { + t.Errorf("%s: must not be excluded", c.name) + } + } +} + +func TestDeriveRequiresCredentials(t *testing.T) { + for _, c := range []struct { + name, user, token string + }{ + {"no username", "", "t"}, + {"no token", "u", ""}, + {"neither", "", ""}, + } { + s := svc(t, &config.RoofModel{Enabled: true, Command: "no-such-command", GeotorgetUsername: c.user, GeotorgetToken: c.token}) + _, err := s.Derive(context.Background(), stockholmLat, stockholmLon) + if !errors.Is(err, ErrNoCredentials) { + t.Errorf("%s: err = %v, want ErrNoCredentials", c.name, err) + } + } +} + +func TestDeriveParsesAModel(t *testing.T) { + doc := `{"schema_version":1,"planes_found":3,` + + `"site":{"latitude":59.33,"longitude":18.07,"radius_m":40},` + + `"source":{"provider":"lantmateriet","item_count":2,"dataset_datetime":"2018-03-01T00:00:00+00:00"},` + + `"arrays":[{"name":"Roof south","kwp":7.2,"tilt_deg":35,"azimuth_deg":180,"area_m2":51.4,"segment_id":"seg-0"}],` + + `"captured_at_ms":1519862400000,"derived_at_ms":1785456000000}` + cmd, dir := stubModule(t, "echo "+quoteForShell(doc)) + s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, ModuleDir: dir, GeotorgetUsername: "u", GeotorgetToken: "t"}) + + m, err := s.Derive(context.Background(), stockholmLat, stockholmLon) + if err != nil { + t.Fatal(err) + } + if m.SchemaVersion != 1 || m.PlanesFound != 3 { + t.Errorf("unexpected model: %+v", m) + } + if len(m.Arrays) != 1 || m.Arrays[0].Name != "Roof south" { + t.Fatalf("arrays = %+v", m.Arrays) + } + if m.CapturedAtMs == nil || *m.CapturedAtMs != 1519862400000 { + t.Errorf("captured_at_ms = %v", m.CapturedAtMs) + } +} + +// The module signals failure as JSON on stderr precisely so an operator sees a +// cause rather than a traceback. +func TestDeriveSurfacesTheModuleErrorMessage(t *testing.T) { + cmd, dir := stubModule(t, `echo {"error":"Geotorget rejected the credentials","kind":"MissingCredentials"} 1>&2 +exit 1`) + s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, ModuleDir: dir, GeotorgetUsername: "u", GeotorgetToken: "t"}) + + _, err := s.Derive(context.Background(), stockholmLat, stockholmLon) + if err == nil { + t.Fatal("want an error") + } + if !strings.Contains(err.Error(), "Geotorget rejected the credentials") { + t.Errorf("err = %v, want the module's own message", err) + } +} + +func TestDeriveRejectsUnknownSchemaVersion(t *testing.T) { + cmd, dir := stubModule(t, "echo "+quoteForShell(`{"schema_version":99,"arrays":[]}`)) + s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, ModuleDir: dir, GeotorgetUsername: "u", GeotorgetToken: "t"}) + + _, err := s.Derive(context.Background(), stockholmLat, stockholmLon) + if err == nil || !strings.Contains(err.Error(), "schema_version") { + t.Errorf("err = %v, want a schema-version rejection", err) + } +} + +func TestDeriveRejectsUnreadableOutput(t *testing.T) { + cmd, dir := stubModule(t, "echo not-json-at-all") + s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, ModuleDir: dir, GeotorgetUsername: "u", GeotorgetToken: "t"}) + + _, err := s.Derive(context.Background(), stockholmLat, stockholmLon) + if err == nil || !strings.Contains(err.Error(), "unreadable") { + t.Errorf("err = %v, want an unreadable-output error", err) + } +} + +// LiDAR tiles are large and this runs on a Pi; an unbounded derive could hold +// memory indefinitely. +func TestDeriveIsTimeBoxed(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("no portable sleep in a .bat stub") + } + cmd, dir := stubModule(t, "sleep 30") + s := svc(t, &config.RoofModel{ + Enabled: true, Command: cmd, ModuleDir: dir, + GeotorgetUsername: "u", GeotorgetToken: "t", TimeoutS: 1, + }) + + start := time.Now() + _, err := s.Derive(context.Background(), stockholmLat, stockholmLon) + if err == nil || !strings.Contains(err.Error(), "timed out") { + t.Errorf("err = %v, want a timeout", err) + } + if elapsed := time.Since(start); elapsed > 10*time.Second { + t.Errorf("took %s, timeout was not enforced", elapsed) + } +} + +func TestToPVArraysMatchesConfigShape(t *testing.T) { + m := &Model{Arrays: []Array{ + {Name: "Roof south", KWp: 7.2, TiltDeg: 35, AzimuthDeg: 180, AreaM2: 51.4}, + {Name: "Roof west", KWp: 4.1, TiltDeg: 35, AzimuthDeg: 270, AreaM2: 29.3}, + }} + got := m.ToPVArrays() + if len(got) != 2 { + t.Fatalf("got %d arrays", len(got)) + } + if got[0].TiltDeg == nil || got[0].AzimuthDeg == nil { + t.Fatalf("derived array must carry both angles, got %+v", got[0]) + } + if got[0].Name != "Roof south" || got[0].KWp != 7.2 || + *got[0].TiltDeg != 35 || *got[0].AzimuthDeg != 180 { + t.Errorf("array 0 = %+v tilt=%v az=%v", got[0], *got[0].TiltDeg, *got[0].AzimuthDeg) + } + // Every entry must own its angles. Sharing one address across the slice + // would make editing one array in the UI silently move the others. + if got[0].AzimuthDeg == got[1].AzimuthDeg { + t.Error("arrays share an azimuth pointer") + } + if *got[1].AzimuthDeg != 270 { + t.Errorf("array 1 azimuth = %v, want 270", *got[1].AzimuthDeg) + } + var nilModel *Model + if nilModel.ToPVArrays() != nil { + t.Error("nil model must yield nil arrays") + } +} + +// quoteForShell wraps a JSON document so both sh and cmd.exe echo it intact. +func quoteForShell(s string) string { + if runtime.GOOS == "windows" { + // cmd.exe has no quoting that survives embedded quotes cleanly; escape + // the shell metacharacters instead. + r := strings.NewReplacer("^", "^^", "&", "^&", "<", "^<", ">", "^>", "|", "^|") + return r.Replace(s) + } + return "'" + s + "'" +} diff --git a/roofmodel/ftw_roofmodel/__init__.py b/roofmodel/ftw_roofmodel/__init__.py new file mode 100644 index 000000000..37aaafd68 --- /dev/null +++ b/roofmodel/ftw_roofmodel/__init__.py @@ -0,0 +1,18 @@ +"""FTW roof-geometry module. + +Derives PV array geometry (tilt, azimuth, kWp) from Lantmaeteriet open geodata. +Optional and independently versioned: core reads only the versioned +`roof_model.json` this module emits, and works normally without it. +""" + +from .pipeline import SCHEMA_VERSION, RoofModelError, derive, planes_to_arrays +from .segment import RoofPlane, segment_roof + +__all__ = [ + "SCHEMA_VERSION", + "RoofModelError", + "RoofPlane", + "derive", + "planes_to_arrays", + "segment_roof", +] diff --git a/roofmodel/ftw_roofmodel/__main__.py b/roofmodel/ftw_roofmodel/__main__.py new file mode 100644 index 000000000..bdcb9c17d --- /dev/null +++ b/roofmodel/ftw_roofmodel/__main__.py @@ -0,0 +1,48 @@ +"""CLI entry point: python -m ftw_roofmodel --lat .. --lon .. + +Core invokes this as a subprocess and reads roof_model.json from stdout, the +same arm's-length pattern the optimizer uses. Errors go to stderr as JSON so +the caller can surface a reason rather than a stack trace. +""" + +from __future__ import annotations + +import argparse +import json +import sys + +from .geotorget import Credentials, GeotorgetError +from .pipeline import RoofModelError, derive + + +def main(argv: list[str] | None = None) -> int: + p = argparse.ArgumentParser(prog="ftw_roofmodel") + p.add_argument("--lat", type=float, required=True) + p.add_argument("--lon", type=float, required=True) + p.add_argument("--username", default="", help="Geotorget username") + p.add_argument("--token", default="", help="Geotorget token/password") + p.add_argument("--radius-m", type=float, default=40.0) + p.add_argument("--packing-factor", type=float, default=0.70) + p.add_argument("--module-w-per-m2", type=float, default=200.0) + args = p.parse_args(argv) + + try: + model = derive( + latitude=args.lat, + longitude=args.lon, + credentials=Credentials(args.username, args.token), + radius_m=args.radius_m, + packing_factor=args.packing_factor, + module_w_per_m2=args.module_w_per_m2, + ) + except (GeotorgetError, RoofModelError) as exc: + json.dump({"error": str(exc), "kind": type(exc).__name__}, sys.stderr) + sys.stderr.write("\n") + return 1 + json.dump(model, sys.stdout) + sys.stdout.write("\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/roofmodel/ftw_roofmodel/geotorget.py b/roofmodel/ftw_roofmodel/geotorget.py new file mode 100644 index 000000000..6bdbbe2e7 --- /dev/null +++ b/roofmodel/ftw_roofmodel/geotorget.py @@ -0,0 +1,184 @@ +"""Lantmaeteriet Geotorget access: authentication and STAC search. + +Two products are used, both free open data (CC BY 4.0) but both gated behind a +Geotorget account the operator orders themselves: + + * *Byggnad Nedladdning, vektor* -- building footprint polygons. + * *Laserdata Nedladdning, Skog* -- airborne LiDAR, 1-2 points/m2, from 2018. + +Credentials are the operator's own and are never shipped, logged or echoed back +through the API. FTW stores them the same way it stores `weather.api_key`, and +redacts them in config responses. + +Only `requests` is used. The STAC API is plain JSON over HTTP, so pulling in +pystac-client would add a dependency for a search body we can write in six +lines -- and a thinner surface is easier to keep working when Lantmaeteriet +moves an endpoint. +""" + +from __future__ import annotations + +import dataclasses +import datetime as dt +from typing import Any, Iterable + +DEFAULT_BASE_URL = "https://api.lantmateriet.se" + +# Collection ids as published in Lantmaeteriet's STAC catalogue. +COLLECTION_BUILDINGS = "byggnad-nedladdning-vektor" +COLLECTION_LIDAR = "laserdata-nedladdning-skog" + + +class GeotorgetError(RuntimeError): + """Any failure talking to Geotorget.""" + + +class MissingCredentials(GeotorgetError): + """No usable credentials were supplied.""" + + +@dataclasses.dataclass(frozen=True) +class Credentials: + username: str + password: str + + def validate(self) -> None: + if not self.username or not self.password: + raise MissingCredentials( + "Geotorget username and token are both required; order access at " + "https://geotorget.lantmateriet.se and set roofmodel.geotorget_username " + "and roofmodel.geotorget_token" + ) + + +@dataclasses.dataclass +class StacItem: + """One STAC item, reduced to what the pipeline needs.""" + + item_id: str + collection: str + assets: dict[str, str] + captured_at: dt.datetime | None + raw: dict[str, Any] = dataclasses.field(default_factory=dict, repr=False) + + def asset_url(self, *preferred: str) -> str | None: + """First matching asset href, trying each preferred key in order.""" + for key in preferred: + if key in self.assets: + return self.assets[key] + return next(iter(self.assets.values()), None) + + +def _parse_datetime(value: str | None) -> dt.datetime | None: + """Parse a STAC RFC 3339 timestamp. + + Lantmaeteriet is backfilling `properties.datetime` across 2026, so it is + routinely absent. That is a missing provenance date, not an error -- the UI + degrades to "capture date unknown" rather than refusing the model. + """ + if not value: + return None + try: + return dt.datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError: + return None + + +def _item_from_feature(feature: dict[str, Any]) -> StacItem: + props = feature.get("properties") or {} + assets = { + name: asset.get("href", "") + for name, asset in (feature.get("assets") or {}).items() + if asset.get("href") + } + captured = _parse_datetime(props.get("datetime")) or _parse_datetime( + props.get("start_datetime") + ) + if captured is None: + # Laser strips carry an acquisition date as `datum` (e.g. "20180301") + # even where the STAC datetime has not been backfilled yet. + datum = props.get("datum") + if datum: + try: + captured = dt.datetime.strptime(str(datum), "%Y%m%d").replace( + tzinfo=dt.timezone.utc + ) + except ValueError: + captured = None + return StacItem( + item_id=feature.get("id", ""), + collection=feature.get("collection", ""), + assets=assets, + captured_at=captured, + raw=feature, + ) + + +class GeotorgetClient: + """Thin STAC client for Lantmaeteriet's download APIs.""" + + def __init__( + self, + credentials: Credentials, + session: Any = None, + base_url: str = DEFAULT_BASE_URL, + timeout: float = 60.0, + ) -> None: + credentials.validate() + self._credentials = credentials + self._base_url = base_url.rstrip("/") + self._timeout = timeout + if session is None: + import requests # imported lazily so tests can inject a fake session + + session = requests.Session() + session.auth = (credentials.username, credentials.password) + self._session = session + + def search( + self, + collection: str, + bbox_sweref: tuple[float, float, float, float], + limit: int = 20, + ) -> list[StacItem]: + """POST /stac/search for one collection over a SWEREF 99 TM bbox. + + bbox is (min_easting, min_northing, max_easting, max_northing); the + catalogue is published in EPSG:3006, so no reprojection happens here. + """ + body = { + "collections": [collection], + "bbox": list(bbox_sweref), + "limit": limit, + } + url = f"{self._base_url}/stac/search" + try: + resp = self._session.post(url, json=body, timeout=self._timeout) + except Exception as exc: # network, DNS, TLS + raise GeotorgetError(f"STAC search failed: {exc}") from exc + if resp.status_code in (401, 403): + raise MissingCredentials( + f"Geotorget rejected the credentials for {collection} " + f"(HTTP {resp.status_code}). Check the account has ordered access " + "to this product." + ) + if resp.status_code != 200: + raise GeotorgetError(f"STAC search returned HTTP {resp.status_code}") + payload = resp.json() + return [_item_from_feature(f) for f in payload.get("features", [])] + + def download(self, url: str) -> bytes: + """Fetch one asset.""" + try: + resp = self._session.get(url, timeout=self._timeout) + except Exception as exc: + raise GeotorgetError(f"asset download failed: {exc}") from exc + if resp.status_code != 200: + raise GeotorgetError(f"asset download returned HTTP {resp.status_code}") + return resp.content + + +def newest_capture(items: Iterable[StacItem]) -> dt.datetime | None: + """Most recent known capture date across items, or None if none carry one.""" + dates = [i.captured_at for i in items if i.captured_at is not None] + return max(dates) if dates else None diff --git a/roofmodel/ftw_roofmodel/pipeline.py b/roofmodel/ftw_roofmodel/pipeline.py new file mode 100644 index 000000000..8431ecd97 --- /dev/null +++ b/roofmodel/ftw_roofmodel/pipeline.py @@ -0,0 +1,207 @@ +"""Derive roof geometry for a site: STAC search -> LiDAR -> planes -> arrays. + +The output contract is a versioned `roof_model.json`, which is the whole reason +this lives in a separate module rather than inside core: core only ever reads +that document, so the heavy geospatial dependencies, their failure modes and +their update cadence stay on this side of the boundary. + +Nothing here is authoritative. The derived arrays *pre-fill* the operator's +editable `weather.pv_arrays`; they are hints, and the numeric editor stays the +final word. A failure produces a clean error and leaves the existing config +untouched. +""" + +from __future__ import annotations + +import dataclasses +import datetime as dt +from typing import Any + +from . import sweref +from .geotorget import ( + COLLECTION_LIDAR, + Credentials, + GeotorgetClient, + GeotorgetError, + StacItem, + newest_capture, +) +from .segment import ( + DEFAULT_MODULE_W_PER_M2, + DEFAULT_PACKING_FACTOR, + RoofPlane, + segment_roof, +) + +SCHEMA_VERSION = 1 + +# How far around the site to pull LiDAR. 40 m comfortably contains a detached +# house and its outbuildings without dragging in the neighbours' roofs. +DEFAULT_RADIUS_M = 40.0 + +# Roof faces smaller than this are dormers, porches and sheds: real surfaces, +# but not worth proposing as a PV array. +MIN_ARRAY_AREA_M2 = 8.0 + +# Lantmaeteriet's Laserdata Skog is specified at 1-2 points/m2. +NOMINAL_POINT_DENSITY = 1.5 + + +class RoofModelError(RuntimeError): + """Derivation failed.""" + + +@dataclasses.dataclass +class DerivedArray: + name: str + kwp: float + tilt_deg: float + azimuth_deg: float + area_m2: float + segment_id: str + + def to_json(self) -> dict[str, Any]: + return { + "name": self.name, + "kwp": round(self.kwp, 2), + "tilt_deg": round(self.tilt_deg, 1), + "azimuth_deg": round(self.azimuth_deg, 1), + "area_m2": round(self.area_m2, 1), + "segment_id": self.segment_id, + } + + +def _compass_name(azimuth_deg: float, tilt_deg: float) -> str: + """Human-readable face name, e.g. "Roof south".""" + if tilt_deg < 5.0: + return "Roof flat" + points = [ + (0, "north"), (45, "north-east"), (90, "east"), (135, "south-east"), + (180, "south"), (225, "south-west"), (270, "west"), (315, "north-west"), + (360, "north"), + ] + best = min(points, key=lambda p: abs(p[0] - azimuth_deg)) + return f"Roof {best[1]}" + + +def planes_to_arrays( + planes: list[RoofPlane], + *, + packing_factor: float = DEFAULT_PACKING_FACTOR, + module_w_per_m2: float = DEFAULT_MODULE_W_PER_M2, + min_area_m2: float = MIN_ARRAY_AREA_M2, +) -> list[DerivedArray]: + """Convert roof planes into candidate PV arrays. + + North-facing pitched roofs are dropped: at Swedish latitudes a north face + at any real pitch yields so little that proposing it as an array would be + noise in the operator's config. Flat roofs are kept -- they are mounted to + face south regardless of which way the building points. + """ + arrays: list[DerivedArray] = [] + used: dict[str, int] = {} + for idx, plane in enumerate(planes): + if plane.area_m2 < min_area_m2: + continue + if plane.tilt_deg >= 5.0 and (plane.azimuth_deg <= 45.0 or plane.azimuth_deg >= 315.0): + continue + name = _compass_name(plane.azimuth_deg, plane.tilt_deg) + used[name] = used.get(name, 0) + 1 + if used[name] > 1: + name = f"{name} {used[name]}" + arrays.append( + DerivedArray( + name=name, + kwp=plane.kwp(packing_factor, module_w_per_m2), + tilt_deg=plane.tilt_deg, + azimuth_deg=plane.azimuth_deg, + area_m2=plane.area_m2, + segment_id=f"seg-{idx}", + ) + ) + return arrays + + +def load_points(data: bytes) -> Any: + """Decode a LAZ/LAS payload into an (N, 3) array of SWEREF 99 TM metres. + + laspy is imported here rather than at module scope so that everything above + -- projection, segmentation, array derivation -- is importable and testable + without the geospatial stack installed. + """ + import io + + try: + import laspy + except ImportError as exc: # pragma: no cover - depends on the install + raise RoofModelError( + "laspy is required to read Lantmaeteriet LiDAR. Install the module's " + "extras: pip install -e roofmodel[geo]" + ) from exc + + import numpy as np + + with laspy.open(io.BytesIO(data)) as reader: + las = reader.read() + return np.column_stack([np.asarray(las.x), np.asarray(las.y), np.asarray(las.z)]) + + +def derive( + *, + latitude: float, + longitude: float, + credentials: Credentials, + client: GeotorgetClient | None = None, + radius_m: float = DEFAULT_RADIUS_M, + packing_factor: float = DEFAULT_PACKING_FACTOR, + module_w_per_m2: float = DEFAULT_MODULE_W_PER_M2, + now: dt.datetime | None = None, +) -> dict[str, Any]: + """Derive a roof model for one site and return it as a JSON-ready dict.""" + if client is None: + client = GeotorgetClient(credentials) + + south, west, north, east = sweref.metre_box_around(latitude, longitude, radius_m) + bbox = sweref.bbox_wgs84_to_sweref99tm(south, west, north, east) + + try: + lidar_items: list[StacItem] = client.search(COLLECTION_LIDAR, bbox) + except GeotorgetError: + raise + if not lidar_items: + raise RoofModelError( + f"no LiDAR tiles cover ({latitude:.5f}, {longitude:.5f}); " + "Lantmaeteriet data is Sweden only" + ) + + points = None + for item in lidar_items: + url = item.asset_url("data", "laz", "copc") + if not url: + continue + points = load_points(client.download(url)) + break + if points is None or len(points) == 0: + raise RoofModelError("LiDAR tiles carried no readable point data") + + planes = segment_roof(points, point_density=NOMINAL_POINT_DENSITY) + arrays = planes_to_arrays( + planes, packing_factor=packing_factor, module_w_per_m2=module_w_per_m2 + ) + + captured = newest_capture(lidar_items) + stamp = now or dt.datetime.now(dt.timezone.utc) + return { + "schema_version": SCHEMA_VERSION, + "site": {"latitude": latitude, "longitude": longitude, "radius_m": radius_m}, + "source": { + "provider": "lantmateriet", + "collection": COLLECTION_LIDAR, + "item_count": len(lidar_items), + "dataset_datetime": captured.isoformat() if captured else None, + }, + "arrays": [a.to_json() for a in arrays], + "planes_found": len(planes), + "captured_at_ms": int(captured.timestamp() * 1000) if captured else None, + "derived_at_ms": int(stamp.timestamp() * 1000), + } diff --git a/roofmodel/ftw_roofmodel/segment.py b/roofmodel/ftw_roofmodel/segment.py new file mode 100644 index 000000000..33d6a13da --- /dev/null +++ b/roofmodel/ftw_roofmodel/segment.py @@ -0,0 +1,243 @@ +"""Roof-plane segmentation from a LiDAR point cloud. + +Implements the method described in the SPAN paper (Yavuzdogan, *Renewable +Energy* 2023, doi:10.1016/j.renene.2023.119022): iterative RANSAC plane fitting +to pull one roof surface at a time out of the cloud, then DBSCAN over each +plane's inliers to split faces that share a plane but not a location -- the two +halves of a gable on opposite wings of a building fit the same equation and are +not the same roof face. + +The method is reimplemented from its description. No code is taken from SPAN's +QGIS plugin, which is GPL; everything here uses numpy and scikit-learn, both +BSD, so this module carries no copyleft obligation. + +Coordinates are SWEREF 99 TM metres as (easting, northing, height). Azimuth +follows FTW's convention: 0 = north, 90 = east, 180 = south, 270 = west. +""" + +from __future__ import annotations + +import dataclasses +import math + +import numpy as np + +# A roof plane must hold at least this many returns to be believed. Below this +# a "plane" is usually a chimney, an aerial, or three points of noise that +# happen to be collinear. +MIN_PLANE_POINTS = 40 + +# Surfaces flatter than this are treated as flat roofs: their azimuth is +# meaningless (the normal is essentially vertical, so its horizontal component +# is numerical noise that can point anywhere). +FLAT_TILT_DEG = 5.0 + +# Roofs steeper than this are walls, dormer cheeks or mis-fits. +MAX_TILT_DEG = 80.0 + +# Fraction of a roof plane's area that can carry modules once you subtract +# ridges, eaves, chimneys, vents and walkways. +DEFAULT_PACKING_FACTOR = 0.70 + +# Module DC rating per square metre of module. ~20% efficiency at 1000 W/m2. +DEFAULT_MODULE_W_PER_M2 = 200.0 + + +@dataclasses.dataclass +class RoofPlane: + """One contiguous roof surface.""" + + tilt_deg: float + azimuth_deg: float + area_m2: float + point_count: int + mean_height_m: float + + def kwp( + self, + packing_factor: float = DEFAULT_PACKING_FACTOR, + module_w_per_m2: float = DEFAULT_MODULE_W_PER_M2, + ) -> float: + """Installable DC capacity for this surface, in kWp.""" + return self.area_m2 * packing_factor * module_w_per_m2 / 1000.0 + + +def _fit_plane(points: np.ndarray) -> np.ndarray: + """Least-squares plane through points; returns a unit normal pointing up. + + Uses the smallest singular vector of the mean-centred points, which is the + total-least-squares fit -- it minimises perpendicular distance rather than + vertical distance, so a steep roof is not biased the way an ordinary + z = ax + by + c regression would bias it. + """ + centred = points - points.mean(axis=0) + _, _, vh = np.linalg.svd(centred, full_matrices=False) + normal = vh[-1] + if normal[2] < 0: + normal = -normal + return normal / np.linalg.norm(normal) + + +def _tilt_azimuth(normal: np.ndarray) -> tuple[float, float]: + """Convert an upward unit normal to (tilt_deg, azimuth_deg).""" + tilt = math.degrees(math.acos(max(-1.0, min(1.0, float(normal[2]))))) + if tilt < FLAT_TILT_DEG: + # Horizontal component is noise at this point; report due south, which + # is what a flat-roof array is normally mounted to face anyway. + return tilt, 180.0 + east, north = float(normal[0]), float(normal[1]) + azimuth = math.degrees(math.atan2(east, north)) % 360.0 + return tilt, azimuth + + +def _convex_hull_area(xy: np.ndarray) -> float: + """Area of the convex hull of 2D points (monotone chain, shoelace). + + Hand-rolled to avoid a scipy dependency for one small routine. The hull + overestimates a concave roof outline, so it is only used as a fallback + when the point count is too low for the density estimate to be stable. + """ + pts = np.unique(xy, axis=0) + if len(pts) < 3: + return 0.0 + order = np.lexsort((pts[:, 1], pts[:, 0])) + pts = pts[order] + + def cross(o, a, b): + return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]) + + lower: list = [] + for p in pts: + while len(lower) >= 2 and cross(lower[-2], lower[-1], p) <= 0: + lower.pop() + lower.append(p) + upper: list = [] + for p in pts[::-1]: + while len(upper) >= 2 and cross(upper[-2], upper[-1], p) <= 0: + upper.pop() + upper.append(p) + hull = np.array(lower[:-1] + upper[:-1]) + if len(hull) < 3: + return 0.0 + x, y = hull[:, 0], hull[:, 1] + return 0.5 * abs(float(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))) + + +def _surface_area(points: np.ndarray, tilt_deg: float, point_density: float | None) -> float: + """Sloped surface area of a roof face, in m2. + + LiDAR density is quoted per square metre of *ground*, so a known density + gives the horizontal footprint directly from the point count; dividing by + cos(tilt) lifts that onto the slope. Without a density we fall back to the + convex hull of the horizontal projection, which is looser -- it fills in + L-shapes and courtyards. + """ + if point_density and point_density > 0: + horizontal = len(points) / point_density + else: + horizontal = _convex_hull_area(points[:, :2]) + cos_t = math.cos(math.radians(min(tilt_deg, MAX_TILT_DEG))) + if cos_t <= 1e-6: + return horizontal + return horizontal / cos_t + + +def _ransac_plane( + points: np.ndarray, + threshold_m: float, + iterations: int, + rng: np.random.Generator, +) -> np.ndarray | None: + """Return a boolean inlier mask for the best plane found, or None. + + Plain RANSAC over point triples. scikit-learn's RANSACRegressor is not used + because it regresses z on (x, y) and so cannot represent a vertical or + near-vertical surface, and weights errors vertically rather than + perpendicular to the plane. + """ + n = len(points) + if n < 3: + return None + best_mask = None + best_count = 0 + for _ in range(iterations): + idx = rng.choice(n, size=3, replace=False) + a, b, c = points[idx] + normal = np.cross(b - a, c - a) + norm = np.linalg.norm(normal) + if norm < 1e-9: + continue # degenerate (collinear) sample + normal = normal / norm + distances = np.abs((points - a) @ normal) + mask = distances < threshold_m + count = int(mask.sum()) + if count > best_count: + best_count, best_mask = count, mask + if best_mask is None or best_count < 3: + return None + return best_mask + + +def segment_roof( + points: np.ndarray, + *, + threshold_m: float = 0.25, + max_planes: int = 8, + min_plane_points: int = MIN_PLANE_POINTS, + cluster_eps_m: float = 1.5, + point_density: float | None = None, + ransac_iterations: int = 200, + seed: int = 0, +) -> list[RoofPlane]: + """Segment a roof point cloud into planes. + + `points` is an (N, 3) array of SWEREF 99 TM (easting, northing, height). + Returns planes ordered by descending area. Determinism is deliberate: the + same cloud must always yield the same arrays, or an operator re-running a + derive would see the geometry shuffle for no reason. + """ + from sklearn.cluster import DBSCAN # imported here to keep import cost off the CLI path + + pts = np.asarray(points, dtype=float) + if pts.ndim != 2 or pts.shape[1] != 3: + raise ValueError(f"points must be (N, 3), got {pts.shape}") + + rng = np.random.default_rng(seed) + remaining = pts + planes: list[RoofPlane] = [] + + for _ in range(max_planes): + if len(remaining) < min_plane_points: + break + mask = _ransac_plane(remaining, threshold_m, ransac_iterations, rng) + if mask is None or int(mask.sum()) < min_plane_points: + break + inliers = remaining[mask] + remaining = remaining[~mask] + + # One plane equation can describe several disjoint faces. Split them. + labels = DBSCAN(eps=cluster_eps_m, min_samples=10).fit(inliers[:, :2]).labels_ + for label in sorted(set(labels)): + if label == -1: + continue # DBSCAN noise + cluster = inliers[labels == label] + if len(cluster) < min_plane_points: + continue + normal = _fit_plane(cluster) + tilt, azimuth = _tilt_azimuth(normal) + if tilt > MAX_TILT_DEG: + continue # a wall, not a roof + planes.append( + RoofPlane( + tilt_deg=round(tilt, 1), + # Round before normalising: 359.97 rounds to 360.0, which is + # the same direction as 0 but reads as an out-of-range value. + azimuth_deg=round(azimuth, 1) % 360.0, + area_m2=round(_surface_area(cluster, tilt, point_density), 1), + point_count=len(cluster), + mean_height_m=round(float(cluster[:, 2].mean()), 2), + ) + ) + + planes.sort(key=lambda p: p.area_m2, reverse=True) + return planes diff --git a/roofmodel/ftw_roofmodel/sweref.py b/roofmodel/ftw_roofmodel/sweref.py new file mode 100644 index 000000000..3c2f699d6 --- /dev/null +++ b/roofmodel/ftw_roofmodel/sweref.py @@ -0,0 +1,173 @@ +"""SWEREF 99 TM <-> WGS84 conversion. + +Lantmaeteriet publishes everything in SWEREF 99 TM (EPSG:3006) while FTW stores +site location as WGS84 latitude/longitude, so every bounding box we send and +every point cloud we read has to cross this boundary. + +This is Lantmaeteriet's own published Gauss conformal projection algorithm +(Krueger series), implemented directly rather than pulled in via pyproj. The +reason is proportion: pyproj ships a full PROJ build for what is, here, exactly +one projection with fixed parameters. The series below is accurate to well under +a millimetre across Sweden, which is several orders of magnitude finer than the +1-2 points/m2 LiDAR it is used to place. + +SWEREF 99 TM is a transverse Mercator on GRS 80 with central meridian 15 deg E, +scale factor 0.9996, false easting 500 000 m and false northing 0. +""" + +from __future__ import annotations + +import math + +# GRS 80 ellipsoid. +_A = 6378137.0 +_F = 1.0 / 298.257222101 + +# SWEREF 99 TM projection parameters. +_CENTRAL_MERIDIAN = 15.0 +_SCALE = 0.9996 +_FALSE_EASTING = 500000.0 +_FALSE_NORTHING = 0.0 + +# Derived constants, computed once. +_E2 = _F * (2.0 - _F) +_N = _F / (2.0 - _F) +_A_HAT = _A / (1.0 + _N) * (1.0 + _N**2 / 4.0 + _N**4 / 64.0) + + +def _forward_coefficients() -> tuple[float, float, float, float]: + n = _N + return ( + n / 2.0 - 2.0 * n**2 / 3.0 + 5.0 * n**3 / 16.0 + 41.0 * n**4 / 180.0, + 13.0 * n**2 / 48.0 - 3.0 * n**3 / 5.0 + 557.0 * n**4 / 1440.0, + 61.0 * n**3 / 240.0 - 103.0 * n**4 / 140.0, + 49561.0 * n**4 / 161280.0, + ) + + +def _inverse_coefficients() -> tuple[float, float, float, float]: + n = _N + return ( + n / 2.0 - 2.0 * n**2 / 3.0 + 37.0 * n**3 / 96.0 - n**4 / 360.0, + n**2 / 48.0 + n**3 / 15.0 - 437.0 * n**4 / 1440.0, + 17.0 * n**3 / 480.0 - 37.0 * n**4 / 840.0, + 4397.0 * n**4 / 161280.0, + ) + + +def wgs84_to_sweref99tm(lat: float, lon: float) -> tuple[float, float]: + """Convert WGS84 degrees to SWEREF 99 TM (northing, easting) in metres.""" + phi = math.radians(lat) + lam = math.radians(lon) + lam0 = math.radians(_CENTRAL_MERIDIAN) + + e2 = _E2 + a_coef = e2 + b_coef = (5.0 * e2**2 - e2**3) / 6.0 + c_coef = (104.0 * e2**3 - 45.0 * e2**4) / 120.0 + d_coef = 1237.0 * e2**4 / 1260.0 + + sin_phi = math.sin(phi) + phi_star = phi - sin_phi * math.cos(phi) * ( + a_coef + + b_coef * sin_phi**2 + + c_coef * sin_phi**4 + + d_coef * sin_phi**6 + ) + + dlam = lam - lam0 + xi_p = math.atan2(math.tan(phi_star), math.cos(dlam)) + eta_p = math.atanh(math.cos(phi_star) * math.sin(dlam)) + + b1, b2, b3, b4 = _forward_coefficients() + scaled = _SCALE * _A_HAT + northing = _FALSE_NORTHING + scaled * ( + xi_p + + b1 * math.sin(2 * xi_p) * math.cosh(2 * eta_p) + + b2 * math.sin(4 * xi_p) * math.cosh(4 * eta_p) + + b3 * math.sin(6 * xi_p) * math.cosh(6 * eta_p) + + b4 * math.sin(8 * xi_p) * math.cosh(8 * eta_p) + ) + easting = _FALSE_EASTING + scaled * ( + eta_p + + b1 * math.cos(2 * xi_p) * math.sinh(2 * eta_p) + + b2 * math.cos(4 * xi_p) * math.sinh(4 * eta_p) + + b3 * math.cos(6 * xi_p) * math.sinh(6 * eta_p) + + b4 * math.cos(8 * xi_p) * math.sinh(8 * eta_p) + ) + return northing, easting + + +def sweref99tm_to_wgs84(northing: float, easting: float) -> tuple[float, float]: + """Convert SWEREF 99 TM (northing, easting) in metres to WGS84 degrees.""" + scaled = _SCALE * _A_HAT + xi = (northing - _FALSE_NORTHING) / scaled + eta = (easting - _FALSE_EASTING) / scaled + + d1, d2, d3, d4 = _inverse_coefficients() + xi_p = ( + xi + - d1 * math.sin(2 * xi) * math.cosh(2 * eta) + - d2 * math.sin(4 * xi) * math.cosh(4 * eta) + - d3 * math.sin(6 * xi) * math.cosh(6 * eta) + - d4 * math.sin(8 * xi) * math.cosh(8 * eta) + ) + eta_p = ( + eta + - d1 * math.cos(2 * xi) * math.sinh(2 * eta) + - d2 * math.cos(4 * xi) * math.sinh(4 * eta) + - d3 * math.cos(6 * xi) * math.sinh(6 * eta) + - d4 * math.cos(8 * xi) * math.sinh(8 * eta) + ) + + phi_star = math.asin(math.sin(xi_p) / math.cosh(eta_p)) + dlam = math.atan2(math.sinh(eta_p), math.cos(xi_p)) + + e2 = _E2 + a_star = e2 + e2**2 + e2**3 + e2**4 + b_star = -(7.0 * e2**2 + 17.0 * e2**3 + 30.0 * e2**4) / 6.0 + c_star = (224.0 * e2**3 + 889.0 * e2**4) / 120.0 + d_star = -(4279.0 * e2**4) / 1260.0 + + sin_ps = math.sin(phi_star) + phi = phi_star + sin_ps * math.cos(phi_star) * ( + a_star + + b_star * sin_ps**2 + + c_star * sin_ps**4 + + d_star * sin_ps**6 + ) + lam = math.radians(_CENTRAL_MERIDIAN) + dlam + return math.degrees(phi), math.degrees(lam) + + +def bbox_wgs84_to_sweref99tm( + min_lat: float, min_lon: float, max_lat: float, max_lon: float +) -> tuple[float, float, float, float]: + """Project a WGS84 bounding box to a SWEREF 99 TM (minE, minN, maxE, maxN). + + All four corners are projected and the extremes taken, rather than just the + two diagonal corners: the projection is not axis-aligned, so a box's + projected edges bow outward and the diagonal-only result would clip. + """ + corners = [ + wgs84_to_sweref99tm(min_lat, min_lon), + wgs84_to_sweref99tm(min_lat, max_lon), + wgs84_to_sweref99tm(max_lat, min_lon), + wgs84_to_sweref99tm(max_lat, max_lon), + ] + northings = [c[0] for c in corners] + eastings = [c[1] for c in corners] + return min(eastings), min(northings), max(eastings), max(northings) + + +def metre_box_around(lat: float, lon: float, radius_m: float) -> tuple[float, float, float, float]: + """Return a WGS84 (min_lat, min_lon, max_lat, max_lon) box of +/- radius_m. + + Built by projecting the centre, stepping in metres in SWEREF 99 TM, and + unprojecting: doing it that way keeps the box square on the ground instead + of stretching with latitude the way a naive degree offset would. + """ + n, e = wgs84_to_sweref99tm(lat, lon) + south, west = sweref99tm_to_wgs84(n - radius_m, e - radius_m) + north, east = sweref99tm_to_wgs84(n + radius_m, e + radius_m) + return south, west, north, east diff --git a/roofmodel/pyproject.toml b/roofmodel/pyproject.toml new file mode 100644 index 000000000..825d4f0fa --- /dev/null +++ b/roofmodel/pyproject.toml @@ -0,0 +1,26 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "ftw-roofmodel" +version = "0.1.0" +description = "Derive PV array geometry from Lantmateriet building and LiDAR open data" +requires-python = ">=3.10" +# Core dependencies are permissive-licensed and light enough to install on a Pi. +dependencies = [ + "numpy>=1.24", + "scikit-learn>=1.3", + "requests>=2.31", +] + +[project.optional-dependencies] +# LAZ decoding pulls a compiled backend, so it is opt-in. Everything except the +# point-cloud read works without it, which keeps the module testable in CI. +geo = ["laspy[lazrs]>=2.5"] + +[tool.setuptools.packages.find] +include = ["ftw_roofmodel*"] + +[tool.pytest.ini_options] +testpaths = ["tests"] diff --git a/roofmodel/tests/__init__.py b/roofmodel/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/roofmodel/tests/test_pipeline.py b/roofmodel/tests/test_pipeline.py new file mode 100644 index 000000000..c597a4a20 --- /dev/null +++ b/roofmodel/tests/test_pipeline.py @@ -0,0 +1,265 @@ +"""Geotorget/STAC and end-to-end derivation tests. + +Lantmaeteriet is credential-gated, so the HTTP layer is exercised against a fake +session rather than the live service. What is verified here is our half of the +contract: the request we send, how we read the response, and how each documented +upstream quirk is handled. +""" + +import datetime as dt +import json + +import numpy as np +import pytest + +from ftw_roofmodel import pipeline +from ftw_roofmodel.geotorget import ( + COLLECTION_LIDAR, + Credentials, + GeotorgetClient, + GeotorgetError, + MissingCredentials, + newest_capture, +) +from ftw_roofmodel.pipeline import RoofModelError, planes_to_arrays +from ftw_roofmodel.segment import RoofPlane +from tests.test_segment import make_plane + + +class FakeResponse: + def __init__(self, status_code=200, payload=None, content=b""): + self.status_code = status_code + self._payload = payload if payload is not None else {} + self.content = content + + def json(self): + return self._payload + + +class FakeSession: + """Records requests and replays canned responses.""" + + def __init__(self, search=None, asset=b"", status=200): + self.search_payload = search if search is not None else {"features": []} + self.asset = asset + self.status = status + self.posts = [] + self.gets = [] + + def post(self, url, json=None, timeout=None): + self.posts.append((url, json)) + return FakeResponse(self.status, self.search_payload) + + def get(self, url, timeout=None): + self.gets.append(url) + return FakeResponse(self.status, content=self.asset) + + +def feature(item_id="tile-1", datetime_value="2023-05-04T10:00:00Z", **props): + p = {"datetime": datetime_value} + p.update(props) + return { + "id": item_id, + "collection": COLLECTION_LIDAR, + "properties": p, + "assets": {"data": {"href": f"https://example.test/{item_id}.laz"}}, + } + + +CREDS = Credentials("user", "token") + + +# --- credentials ----------------------------------------------------------- + +def test_missing_credentials_are_rejected_before_any_request(): + session = FakeSession() + for creds in (Credentials("", "token"), Credentials("user", ""), Credentials("", "")): + with pytest.raises(MissingCredentials): + GeotorgetClient(creds, session=session) + assert session.posts == [], "must not contact Geotorget without credentials" + + +def test_rejected_credentials_say_what_to_check(): + client = GeotorgetClient(CREDS, session=FakeSession(status=403)) + with pytest.raises(MissingCredentials) as exc: + client.search(COLLECTION_LIDAR, (0, 0, 1, 1)) + assert "ordered access" in str(exc.value) + + +def test_server_error_is_not_mistaken_for_an_auth_problem(): + client = GeotorgetClient(CREDS, session=FakeSession(status=500)) + with pytest.raises(GeotorgetError) as exc: + client.search(COLLECTION_LIDAR, (0, 0, 1, 1)) + assert not isinstance(exc.value, MissingCredentials) + + +# --- STAC search ----------------------------------------------------------- + +def test_search_sends_the_collection_and_bbox(): + session = FakeSession(search={"features": [feature()]}) + client = GeotorgetClient(CREDS, session=session) + items = client.search(COLLECTION_LIDAR, (600000.0, 6500000.0, 600100.0, 6500100.0)) + + assert len(items) == 1 + (url, body), = session.posts + assert url.endswith("/stac/search") + assert body["collections"] == [COLLECTION_LIDAR] + assert body["bbox"] == [600000.0, 6500000.0, 600100.0, 6500100.0] + + +def test_capture_date_is_read_from_stac_datetime(): + session = FakeSession(search={"features": [feature(datetime_value="2019-04-02T09:30:00Z")]}) + items = GeotorgetClient(CREDS, session=session).search(COLLECTION_LIDAR, (0, 0, 1, 1)) + assert items[0].captured_at.year == 2019 + assert items[0].captured_at.month == 4 + + +def test_capture_date_falls_back_to_the_laser_strip_datum(): + """Lantmaeteriet is backfilling properties.datetime through 2026; the strip + `datum` is the ground truth available in the meantime.""" + session = FakeSession( + search={"features": [feature(datetime_value=None, datum="20180301")]} + ) + items = GeotorgetClient(CREDS, session=session).search(COLLECTION_LIDAR, (0, 0, 1, 1)) + assert items[0].captured_at is not None + assert items[0].captured_at.strftime("%Y-%m-%d") == "2018-03-01" + + +def test_absent_capture_date_is_not_an_error(): + session = FakeSession(search={"features": [feature(datetime_value=None)]}) + items = GeotorgetClient(CREDS, session=session).search(COLLECTION_LIDAR, (0, 0, 1, 1)) + assert items[0].captured_at is None + assert newest_capture(items) is None + + +def test_newest_capture_picks_the_latest_known_date(): + session = FakeSession( + search={ + "features": [ + feature("a", "2018-01-01T00:00:00Z"), + feature("b", "2021-06-01T00:00:00Z"), + feature("c", None), + ] + } + ) + items = GeotorgetClient(CREDS, session=session).search(COLLECTION_LIDAR, (0, 0, 1, 1)) + assert newest_capture(items).year == 2021 + + +# --- planes -> arrays ------------------------------------------------------ + +def test_north_facing_pitched_roofs_are_not_proposed(): + """At Swedish latitudes a north pitch yields too little to be worth adding + to an operator's config.""" + planes = [ + RoofPlane(tilt_deg=35, azimuth_deg=0, area_m2=60, point_count=200, mean_height_m=6), + RoofPlane(tilt_deg=35, azimuth_deg=180, area_m2=60, point_count=200, mean_height_m=6), + ] + arrays = planes_to_arrays(planes) + assert len(arrays) == 1 + assert arrays[0].azimuth_deg == 180 + + +def test_flat_roofs_are_kept_regardless_of_building_orientation(): + planes = [RoofPlane(tilt_deg=1.0, azimuth_deg=180, area_m2=90, point_count=300, mean_height_m=9)] + assert len(planes_to_arrays(planes)) == 1 + + +def test_tiny_faces_are_dropped(): + """Dormers and porches are real surfaces but not candidate arrays.""" + planes = [RoofPlane(tilt_deg=35, azimuth_deg=180, area_m2=3.0, point_count=50, mean_height_m=5)] + assert planes_to_arrays(planes) == [] + + +def test_arrays_get_readable_and_unique_names(): + planes = [ + RoofPlane(tilt_deg=35, azimuth_deg=180, area_m2=60, point_count=200, mean_height_m=6), + RoofPlane(tilt_deg=35, azimuth_deg=182, area_m2=40, point_count=150, mean_height_m=6), + RoofPlane(tilt_deg=35, azimuth_deg=270, area_m2=30, point_count=120, mean_height_m=6), + ] + arrays = planes_to_arrays(planes) + names = [a.name for a in arrays] + assert len(set(names)) == len(names), names + assert names[0] == "Roof south" + assert "Roof west" in names + + +def test_array_json_matches_the_config_field_names(): + """The document pre-fills weather.pv_arrays, so the keys must line up.""" + planes = [RoofPlane(tilt_deg=35, azimuth_deg=180, area_m2=60, point_count=200, mean_height_m=6)] + payload = planes_to_arrays(planes)[0].to_json() + assert set(payload) >= {"name", "kwp", "tilt_deg", "azimuth_deg"} + assert payload["kwp"] > 0 + + +# --- end to end ------------------------------------------------------------ + +def _patched_points(monkeypatch, cloud): + monkeypatch.setattr(pipeline, "load_points", lambda data: cloud) + + +def test_derive_produces_a_versioned_document(monkeypatch): + gable = np.vstack([ + make_plane(tilt_deg=35, azimuth_deg=180, width=12, depth=6, seed=11), + make_plane(tilt_deg=35, azimuth_deg=0, width=12, depth=6, origin=(0, 6, -4.2), seed=12), + ]) + _patched_points(monkeypatch, gable) + session = FakeSession(search={"features": [feature()]}, asset=b"laz-bytes") + client = GeotorgetClient(CREDS, session=session) + + model = pipeline.derive( + latitude=59.33, longitude=18.07, credentials=CREDS, client=client, + now=dt.datetime(2026, 7, 31, tzinfo=dt.timezone.utc), + ) + + assert model["schema_version"] == pipeline.SCHEMA_VERSION + assert model["source"]["provider"] == "lantmateriet" + assert model["captured_at_ms"] > 0 + assert model["derived_at_ms"] == 1785456000000 + # The gable's north face is dropped, so exactly the south face survives. + assert len(model["arrays"]) == 1 + south = model["arrays"][0] + assert south["azimuth_deg"] == pytest.approx(180.0, abs=3.0) + assert south["tilt_deg"] == pytest.approx(35.0, abs=3.0) + assert south["kwp"] > 0 + # Must be JSON-serialisable for the subprocess contract. + json.dumps(model) + + +def test_derive_searches_the_projected_bbox(monkeypatch): + _patched_points(monkeypatch, make_plane(tilt_deg=35, azimuth_deg=180)) + session = FakeSession(search={"features": [feature()]}, asset=b"x") + pipeline.derive( + latitude=59.33, longitude=18.07, credentials=CREDS, + client=GeotorgetClient(CREDS, session=session), radius_m=40.0, + ) + (_, body), = session.posts + min_e, min_n, max_e, max_n = body["bbox"] + # Stockholm in SWEREF 99 TM, and an 80 m box give or take projection bow. + assert 600_000 < min_e < 700_000, body["bbox"] + assert 6_500_000 < min_n < 6_600_000, body["bbox"] + assert 75 < (max_e - min_e) < 90 + assert 75 < (max_n - min_n) < 90 + + +def test_derive_outside_sweden_says_so(monkeypatch): + """No tiles come back for a site Lantmaeteriet does not cover.""" + session = FakeSession(search={"features": []}) + with pytest.raises(RoofModelError) as exc: + pipeline.derive( + latitude=-33.87, longitude=151.21, credentials=CREDS, + client=GeotorgetClient(CREDS, session=session), + ) + assert "Sweden only" in str(exc.value) + + +def test_derive_reports_unknown_capture_date_without_failing(monkeypatch): + _patched_points(monkeypatch, make_plane(tilt_deg=35, azimuth_deg=180)) + session = FakeSession(search={"features": [feature(datetime_value=None)]}, asset=b"x") + model = pipeline.derive( + latitude=59.33, longitude=18.07, credentials=CREDS, + client=GeotorgetClient(CREDS, session=session), + ) + assert model["captured_at_ms"] is None + assert model["source"]["dataset_datetime"] is None + assert model["arrays"], "a missing provenance date must not block the model" diff --git a/roofmodel/tests/test_segment.py b/roofmodel/tests/test_segment.py new file mode 100644 index 000000000..e8ec35451 --- /dev/null +++ b/roofmodel/tests/test_segment.py @@ -0,0 +1,188 @@ +"""Roof segmentation tests. + +Every case is a synthetic roof whose tilt, azimuth and area are known exactly by +construction, so the assertions check recovered geometry against ground truth +rather than against a previous run's output. +""" + +import math + +import numpy as np +import pytest + +from ftw_roofmodel.segment import RoofPlane, segment_roof + + +def make_plane( + *, + tilt_deg: float, + azimuth_deg: float, + width: float = 10.0, + depth: float = 8.0, + origin=(0.0, 0.0, 0.0), + density: float = 8.0, + noise_m: float = 0.0, + seed: int = 1, +) -> np.ndarray: + """Sample a rectangular sloped surface with a known tilt and azimuth. + + The surface is generated by taking a horizontal grid and raising it along + the downslope direction, which is the inverse of what segment_roof does, so + the test never shares an implementation with the code under test. + """ + rng = np.random.default_rng(seed) + n = max(int(width * depth * density), 60) + x = rng.uniform(0, width, n) + y = rng.uniform(0, depth, n) + + # Aspect points the way the surface faces; height falls off along it. + az = math.radians(azimuth_deg) + east_dir, north_dir = math.sin(az), math.cos(az) + slope = math.tan(math.radians(tilt_deg)) + z = -(x * east_dir + y * north_dir) * slope + + if noise_m: + z = z + rng.normal(0.0, noise_m, n) + + return np.column_stack([x + origin[0], y + origin[1], z + origin[2]]) + + +@pytest.mark.parametrize( + "tilt,azimuth", + [ + (35.0, 180.0), # classic south-facing pitched roof + (30.0, 90.0), # east + (30.0, 270.0), # west + (45.0, 0.0), # north + (20.0, 225.0), # south-west + (60.0, 135.0), # steep south-east + ], +) +def test_recovers_known_tilt_and_azimuth(tilt, azimuth): + cloud = make_plane(tilt_deg=tilt, azimuth_deg=azimuth) + planes = segment_roof(cloud, point_density=8.0) + assert planes, "expected at least one plane" + got = planes[0] + assert got.tilt_deg == pytest.approx(tilt, abs=1.5) + # Compare on the circle so 359.5 vs 0.5 is a 1-degree error, not 359. + delta = abs((got.azimuth_deg - azimuth + 180.0) % 360.0 - 180.0) + assert delta < 2.0, f"azimuth {got.azimuth_deg} vs {azimuth}" + + +def test_flat_roof_reports_flat_and_does_not_invent_an_azimuth(): + """A horizontal surface has no meaningful aspect: its normal is vertical, + so the horizontal component is pure noise and could point anywhere.""" + cloud = make_plane(tilt_deg=0.0, azimuth_deg=180.0, noise_m=0.02) + planes = segment_roof(cloud, point_density=8.0) + assert planes + assert planes[0].tilt_deg < 5.0 + assert planes[0].azimuth_deg == 180.0 + + +def test_gable_roof_splits_into_two_opposing_faces(): + """The canonical case: one ridge, two faces 180 degrees apart.""" + south = make_plane(tilt_deg=35, azimuth_deg=180, depth=6, origin=(0, 0, 0), seed=2) + north = make_plane(tilt_deg=35, azimuth_deg=0, depth=6, origin=(0, 6, -4.2), seed=3) + planes = segment_roof(np.vstack([south, north]), point_density=8.0) + + assert len(planes) >= 2, f"expected two faces, got {len(planes)}" + azimuths = sorted(p.azimuth_deg for p in planes[:2]) + opposed = abs((azimuths[1] - azimuths[0]) - 180.0) + assert opposed < 5.0, f"faces should oppose, got {azimuths}" + for p in planes[:2]: + assert p.tilt_deg == pytest.approx(35.0, abs=2.0) + + +def test_two_separate_buildings_on_the_same_plane_are_split(): + """Two roofs with identical pitch and aspect satisfy one plane equation. + RANSAC alone would merge them; the DBSCAN pass is what separates them.""" + a = make_plane(tilt_deg=30, azimuth_deg=180, origin=(0, 0, 0), seed=4) + b = make_plane(tilt_deg=30, azimuth_deg=180, origin=(60, 0, 0), seed=5) + planes = segment_roof(np.vstack([a, b]), point_density=8.0, cluster_eps_m=1.5) + + assert len(planes) >= 2, "spatially disjoint faces must not be merged" + for p in planes[:2]: + assert p.tilt_deg == pytest.approx(30.0, abs=2.0) + + +def test_area_is_the_sloped_area_not_the_footprint(): + """A 10x8 footprint at 60 degrees covers 80 / cos(60) = 160 m2 of roof.""" + cloud = make_plane(tilt_deg=60.0, azimuth_deg=180.0, width=10, depth=8, density=10) + planes = segment_roof(cloud, point_density=10.0) + assert planes + assert planes[0].area_m2 == pytest.approx(160.0, rel=0.15) + + +def test_area_of_a_flat_roof_matches_its_footprint(): + cloud = make_plane(tilt_deg=0.0, azimuth_deg=180.0, width=12, depth=10, density=10) + planes = segment_roof(cloud, point_density=10.0) + assert planes + assert planes[0].area_m2 == pytest.approx(120.0, rel=0.15) + + +def test_survives_realistic_measurement_noise(): + """1-2 pts/m2 airborne LiDAR carries several centimetres of range noise.""" + cloud = make_plane(tilt_deg=35.0, azimuth_deg=180.0, noise_m=0.05, density=6) + planes = segment_roof(cloud, point_density=6.0, threshold_m=0.3) + assert planes + assert planes[0].tilt_deg == pytest.approx(35.0, abs=3.0) + + +def test_walls_are_rejected(): + """A near-vertical surface is a wall or a dormer cheek, not a roof.""" + cloud = make_plane(tilt_deg=88.0, azimuth_deg=180.0) + planes = segment_roof(cloud, point_density=8.0) + assert all(p.tilt_deg <= 80.0 for p in planes) + + +def test_noise_alone_yields_nothing_believable(): + """A handful of scattered returns must not become a roof.""" + rng = np.random.default_rng(7) + cloud = rng.uniform(0, 10, size=(25, 3)) + assert segment_roof(cloud, point_density=8.0) == [] + + +def test_is_deterministic(): + """Re-running a derive must not shuffle an operator's arrays.""" + cloud = make_plane(tilt_deg=35, azimuth_deg=180) + a = segment_roof(cloud, point_density=8.0) + b = segment_roof(cloud, point_density=8.0) + assert a == b + + +def test_planes_are_ordered_by_area(): + big = make_plane(tilt_deg=30, azimuth_deg=180, width=14, depth=12, origin=(0, 0, 0), seed=8) + small = make_plane(tilt_deg=30, azimuth_deg=180, width=5, depth=4, origin=(70, 0, 0), seed=9) + planes = segment_roof(np.vstack([big, small]), point_density=8.0) + assert len(planes) >= 2 + assert planes[0].area_m2 >= planes[1].area_m2 + + +def test_rejects_malformed_input(): + with pytest.raises(ValueError): + segment_roof(np.zeros((10, 2))) + + +def test_kwp_derives_from_area_with_packing_losses(): + """A 100 m2 face cannot carry 100 m2 of modules.""" + plane = RoofPlane(tilt_deg=35, azimuth_deg=180, area_m2=100.0, point_count=800, mean_height_m=6.0) + kwp = plane.kwp() + assert kwp == pytest.approx(100 * 0.70 * 200 / 1000.0) + assert kwp < 100 * 200 / 1000.0, "packing factor must reduce the raw area" + + +def test_kwp_scales_with_area(): + a = RoofPlane(tilt_deg=35, azimuth_deg=180, area_m2=50.0, point_count=1, mean_height_m=1.0) + b = RoofPlane(tilt_deg=35, azimuth_deg=180, area_m2=100.0, point_count=1, mean_height_m=1.0) + assert b.kwp() == pytest.approx(2 * a.kwp()) + + +def test_azimuth_never_reports_360(): + """A north face fits at ~359.97 deg, which rounds to 360.0 -- the same + direction as 0 but an out-of-range-looking value for anything consuming it. + Caught by the demo, so pinned here.""" + cloud = make_plane(tilt_deg=35, azimuth_deg=0, seed=21) + planes = segment_roof(cloud, point_density=8.0) + assert planes + for p in planes: + assert 0.0 <= p.azimuth_deg < 360.0, p.azimuth_deg diff --git a/roofmodel/tests/test_sweref.py b/roofmodel/tests/test_sweref.py new file mode 100644 index 000000000..f6f20a4b1 --- /dev/null +++ b/roofmodel/tests/test_sweref.py @@ -0,0 +1,123 @@ +"""SWEREF 99 TM projection tests. + +The projection has exact analytic properties at the central meridian and the +equator, which pin the parameters without needing a published coordinate table. +Round-trip accuracy then pins the series expansion itself. +""" + +import math + +import pytest + +from ftw_roofmodel.sweref import ( + bbox_wgs84_to_sweref99tm, + metre_box_around, + sweref99tm_to_wgs84, + wgs84_to_sweref99tm, +) + + +@pytest.mark.parametrize("lat", [0.0, 55.0, 59.33, 63.0, 69.0]) +def test_central_meridian_maps_to_false_easting(lat): + """On the central meridian easting is exactly the false easting, by + definition. Any error in scale, ellipsoid or meridian shows up here.""" + _, easting = wgs84_to_sweref99tm(lat, 15.0) + assert easting == pytest.approx(500000.0, abs=1e-6) + + +def test_equator_on_central_meridian_is_the_projection_origin(): + northing, easting = wgs84_to_sweref99tm(0.0, 15.0) + assert northing == pytest.approx(0.0, abs=1e-6) + assert easting == pytest.approx(500000.0, abs=1e-6) + + +@pytest.mark.parametrize( + "lat,lon", + [ + (55.34, 13.15), # Smygehuk, southernmost Sweden + (59.33, 18.07), # Stockholm + (57.71, 11.97), # Gothenburg + (63.83, 20.26), # Umea + (67.86, 20.23), # Kiruna + (69.06, 20.55), # Treriksroeset, northernmost + ], +) +def test_round_trip_is_sub_millimetre(lat, lon): + """Project and unproject across the full extent of Sweden.""" + n, e = wgs84_to_sweref99tm(lat, lon) + back_lat, back_lon = sweref99tm_to_wgs84(n, e) + # 1e-8 degrees is about 1 mm of latitude. + assert back_lat == pytest.approx(lat, abs=1e-8) + assert back_lon == pytest.approx(lon, abs=1e-8) + + +def test_coordinates_land_in_the_expected_range_for_sweden(): + """Sanity-check magnitudes: Swedish SWEREF 99 TM eastings sit inside + 260-920 km and northings inside 6100-7700 km. A transposed or + wrongly-scaled result would fall far outside.""" + n, e = wgs84_to_sweref99tm(59.33, 18.07) + assert 260_000 < e < 920_000, e + assert 6_100_000 < n < 7_700_000, n + + +def test_east_of_the_meridian_increases_easting(): + _, west = wgs84_to_sweref99tm(59.33, 14.0) + _, east = wgs84_to_sweref99tm(59.33, 16.0) + assert west < 500000.0 < east + + +def test_north_increases_northing(): + south, _ = wgs84_to_sweref99tm(55.0, 15.0) + north, _ = wgs84_to_sweref99tm(65.0, 15.0) + assert north > south + + +def test_one_degree_of_latitude_is_about_111_km(): + a, _ = wgs84_to_sweref99tm(59.0, 15.0) + b, _ = wgs84_to_sweref99tm(60.0, 15.0) + # Scaled by k0 = 0.9996 on the central meridian. + assert 110_000 < (b - a) < 112_000 + + +def test_bbox_uses_all_four_corners(): + """The projection is not axis-aligned, so the projected box must be at + least as large as the one implied by the two diagonal corners.""" + min_lat, min_lon, max_lat, max_lon = 59.0, 17.0, 60.0, 19.0 + min_e, min_n, max_e, max_n = bbox_wgs84_to_sweref99tm( + min_lat, min_lon, max_lat, max_lon + ) + sw_n, sw_e = wgs84_to_sweref99tm(min_lat, min_lon) + ne_n, ne_e = wgs84_to_sweref99tm(max_lat, max_lon) + assert min_e <= sw_e and min_n <= sw_n + assert max_e >= ne_e and max_n >= ne_n + assert min_e < max_e and min_n < max_n + + +def test_metre_box_is_square_on_the_ground(): + """A 100 m box must measure 200 m on both axes regardless of latitude -- + the whole reason it is built in projected metres rather than in degrees.""" + for lat in (55.5, 59.33, 68.0): + south, west, north, east = metre_box_around(lat, 15.0, 100.0) + sn, se = wgs84_to_sweref99tm(south, west) + nn, ne = wgs84_to_sweref99tm(north, east) + assert (ne - se) == pytest.approx(200.0, abs=0.5) + assert (nn - sn) == pytest.approx(200.0, abs=0.5) + + +def test_metre_box_brackets_its_centre(): + south, west, north, east = metre_box_around(59.33, 18.07, 50.0) + assert south < 59.33 < north + assert west < 18.07 < east + + +def test_degree_box_would_have_been_wrong_at_high_latitude(): + """Guards the reason metre_box_around exists: a fixed degree offset gives + wildly different ground distances at Malmoe and Kiruna, so anyone tempted to + simplify it back to degrees has to defeat this test first.""" + span = [] + for lat in (55.5, 68.0): + # What a naive 0.001-degree longitude offset would span, in metres. + _, e0 = wgs84_to_sweref99tm(lat, 15.0) + _, e1 = wgs84_to_sweref99tm(lat, 15.001) + span.append(e1 - e0) + assert span[0] / span[1] > 1.5, span From e66a02b54530589a7e8c54aeb7bf5dc9beab2a39 Mon Sep 17 00:00:00 2001 From: Claude Opus 5 Date: Fri, 31 Jul 2026 14:59:13 +0200 Subject: [PATCH 08/26] test(roofmodel): run the subprocess tests on Linux, and the module in CI The roofmodel tests passed on Windows and failed on Linux, which is where CI runs them. Two separate bugs, both caused by stubbing the module with a shell script: - The stub handed back "sh" as the command, and the service invokes it as ` -m ftw_roofmodel ...`. dash reads that as "run the script named ftw_roofmodel", cannot open it, and exits 2. The stub never ran at all, so five tests failed against an error that had nothing to do with what they were asserting. - The module-error stub echoed an unquoted JSON document. dash strips the double quotes, so `{"error":"..."}` reached the parser as `{error:...}` and could not be unmarshalled. cmd.exe keeps them, which is the only reason that test ever passed. The stub is now this test binary re-executed with an environment variable telling it which behaviour to play. Nothing goes through a shell, so there is no quoting to get wrong and no per-platform behaviour to diverge. The timeout test consequently runs everywhere instead of being skipped on Windows. Two assertions the shell stub could not make: that the site, credentials and radius actually survive the process boundary, and that PYTHONPATH is set -- without it the module is only importable if installed system-wide, which on a Pi it is not. Also that --vostok stays absent unless configured, so a GPL tool is never invoked by default. The Python module's tests did not run in CI at all: the workflow runs `pytest -q optimizer/tests` and nothing else, so 75 tests covering the plane fitting every derived tilt and azimuth depends on were never executed upstream. Added a roofmodel job mirroring the optimizer's, wired into the required-check gate so a failure blocks rather than being reported and ignored. It installs without the `geo` extra on purpose: LAZ decoding pulls a compiled backend, and everything except the point-cloud read is exercised without it. Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com> Signed-off-by: Hugo Karlsson <48095810+HuggeK@users.noreply.github.com> --- .github/scripts/classify-test-changes.sh | 11 +- .github/scripts/test-change-classifier.sh | 23 +-- .github/workflows/test.yml | 31 +++- go/internal/roofmodel/roofmodel_test.go | 195 ++++++++++++++++------ roofmodel/pyproject.toml | 1 + 5 files changed, 197 insertions(+), 64 deletions(-) diff --git a/.github/scripts/classify-test-changes.sh b/.github/scripts/classify-test-changes.sh index d9b9a9708..c4dffe323 100644 --- a/.github/scripts/classify-test-changes.sh +++ b/.github/scripts/classify-test-changes.sh @@ -6,6 +6,7 @@ set -euo pipefail core=false optimizer=false +roofmodel=false web=false drivers=false compose=false @@ -28,6 +29,11 @@ while IFS= read -r file; do optimizer/*|Dockerfile.optimizer|go/internal/mpc/*|go/cmd/ftw/main.go) optimizer=true ;; + # Python roof-geometry module only. Its Go host lives under go/, which + # the core suite already covers. + roofmodel/*) + roofmodel=true + ;; web/*|package.json|package-lock.json) web=true ;; @@ -40,6 +46,7 @@ while IFS= read -r file; do Makefile|.github/workflows/test.yml) core=true optimizer=true + roofmodel=true web=true drivers=true compose=true @@ -52,5 +59,5 @@ while IFS= read -r file; do esac done -printf 'core=%s\noptimizer=%s\nweb=%s\ndrivers=%s\ncompose=%s\n' \ - "${core}" "${optimizer}" "${web}" "${drivers}" "${compose}" +printf 'core=%s\noptimizer=%s\nroofmodel=%s\nweb=%s\ndrivers=%s\ncompose=%s\n' \ + "${core}" "${optimizer}" "${roofmodel}" "${web}" "${drivers}" "${compose}" diff --git a/.github/scripts/test-change-classifier.sh b/.github/scripts/test-change-classifier.sh index dbb5fb2b3..e33e29b92 100644 --- a/.github/scripts/test-change-classifier.sh +++ b/.github/scripts/test-change-classifier.sh @@ -20,33 +20,36 @@ assert_paths() { # This was the release hole: before the special case came first, this path # matched drivers/* and selected only core. assert_paths 'drivers/BUNDLED_SOURCE.json' \ - 'core=true' 'optimizer=false' 'web=false' 'drivers=true' 'compose=false' + 'core=true' 'optimizer=false' 'roofmodel=false' 'web=false' 'drivers=true' 'compose=false' assert_paths 'drivers/example.lua' \ - 'core=true' 'optimizer=false' 'web=false' 'drivers=true' 'compose=false' + 'core=true' 'optimizer=false' 'roofmodel=false' 'web=false' 'drivers=true' 'compose=false' assert_paths 'config.example.yaml' \ - 'core=true' 'optimizer=false' 'web=false' 'drivers=false' 'compose=false' + 'core=true' 'optimizer=false' 'roofmodel=false' 'web=false' 'drivers=false' 'compose=false' assert_paths '.github/workflows/test.yml' \ - 'core=true' 'optimizer=true' 'web=true' 'drivers=true' 'compose=true' + 'core=true' 'optimizer=true' 'roofmodel=true' 'web=true' 'drivers=true' 'compose=true' + +assert_paths 'roofmodel/ftw_roofmodel/pipeline.py' \ + 'core=false' 'optimizer=false' 'roofmodel=true' 'web=false' 'drivers=false' 'compose=false' assert_paths '.github/scripts/classify-test-changes.sh' \ - 'core=false' 'optimizer=false' 'web=false' 'drivers=false' 'compose=true' + 'core=false' 'optimizer=false' 'roofmodel=false' 'web=false' 'drivers=false' 'compose=true' assert_paths '.github/workflows/release-assets.yml' \ - 'core=false' 'optimizer=false' 'web=false' 'drivers=false' 'compose=true' + 'core=false' 'optimizer=false' 'roofmodel=false' 'web=false' 'drivers=false' 'compose=true' assert_paths 'scripts/promote-paired-latest.sh' \ - 'core=false' 'optimizer=false' 'web=false' 'drivers=false' 'compose=true' + 'core=false' 'optimizer=false' 'roofmodel=false' 'web=false' 'drivers=false' 'compose=true' assert_paths 'scripts/github-release-by-id.sh' \ - 'core=false' 'optimizer=false' 'web=false' 'drivers=false' 'compose=true' + 'core=false' 'optimizer=false' 'roofmodel=false' 'web=false' 'drivers=false' 'compose=true' assert_paths 'scripts/test-github-release-by-id.sh' \ - 'core=false' 'optimizer=false' 'web=false' 'drivers=false' 'compose=true' + 'core=false' 'optimizer=false' 'roofmodel=false' 'web=false' 'drivers=false' 'compose=true' assert_paths 'scripts/check-stable-release.py' \ - 'core=false' 'optimizer=false' 'web=false' 'drivers=false' 'compose=true' + 'core=false' 'optimizer=false' 'roofmodel=false' 'web=false' 'drivers=false' 'compose=true' echo "test workflow path classifier contract passed" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 93eb94f4a..57c5fd465 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,6 +22,7 @@ jobs: outputs: core: ${{ steps.paths.outputs.core }} optimizer: ${{ steps.paths.outputs.optimizer }} + roofmodel: ${{ steps.paths.outputs.roofmodel }} web: ${{ steps.paths.outputs.web }} drivers: ${{ steps.paths.outputs.drivers }} compose: ${{ steps.paths.outputs.compose }} @@ -43,6 +44,7 @@ jobs: set -euo pipefail core=false optimizer=false + roofmodel=false web=false drivers=false compose=false @@ -60,6 +62,7 @@ jobs: case "${key}" in core) core="${value}" ;; optimizer) optimizer="${value}" ;; + roofmodel) roofmodel="${value}" ;; web) web="${value}" ;; drivers) drivers="${value}" ;; compose) compose="${value}" ;; @@ -70,6 +73,7 @@ jobs: { echo "core=${core}" echo "optimizer=${optimizer}" + echo "roofmodel=${roofmodel}" echo "web=${web}" echo "drivers=${drivers}" echo "compose=${compose}" @@ -131,6 +135,28 @@ jobs: go test -count=1 ./internal/mpc -run 'TestExternalOptimizer(EndToEnd|PlansMultipleLoadpoints|PlansAndValidatesMultipleStorages)$' + # The roof-geometry module is optional at runtime but not optional to verify: + # it decides the tilt and azimuth every PV forecast is then built on, and a + # silently wrong plane fit looks exactly like a working one. Installed without + # the `geo` extra on purpose -- LAZ decoding pulls a compiled backend, and + # everything except the point-cloud read is exercised without it. + roofmodel: + name: roofmodel (Python) + needs: changes + if: needs.changes.outputs.roofmodel == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: '3.12' + cache: pip + cache-dependency-path: roofmodel/pyproject.toml + - name: Install + run: pip install -e 'roofmodel[test]' + - name: Python tests + run: pytest -q roofmodel/tests + web: name: web needs: changes @@ -480,12 +506,13 @@ jobs: name: go test + vet if: always() needs: - [changes, core, optimizer, web, drivers, device-support-contract, compose, e2e, contract] + [changes, core, optimizer, roofmodel, web, drivers, device-support-contract, compose, e2e, contract] runs-on: ubuntu-latest env: RESULTS: >- ${{ needs.changes.result }} ${{ needs.core.result }} - ${{ needs.optimizer.result }} ${{ needs.web.result }} + ${{ needs.optimizer.result }} ${{ needs.roofmodel.result }} + ${{ needs.web.result }} ${{ needs.drivers.result }} ${{ needs.device-support-contract.result }} ${{ needs.compose.result }} ${{ needs.e2e.result }} ${{ needs.contract.result }} diff --git a/go/internal/roofmodel/roofmodel_test.go b/go/internal/roofmodel/roofmodel_test.go index 078103024..4832b8a83 100644 --- a/go/internal/roofmodel/roofmodel_test.go +++ b/go/internal/roofmodel/roofmodel_test.go @@ -2,10 +2,9 @@ package roofmodel import ( "context" + "encoding/json" "errors" "os" - "path/filepath" - "runtime" "strings" "testing" "time" @@ -13,32 +12,82 @@ import ( "github.com/srcfl/ftw/go/internal/config" ) -// stubModule writes a script that stands in for the Python module, so the -// subprocess contract itself is exercised — argument passing, stdout parsing, -// stderr error reporting, timeouts — without needing the geospatial stack -// installed on the machine running the tests. -func stubModule(t *testing.T, script string) (command, dir string) { - t.Helper() - dir = t.TempDir() - var path, interp string - if runtime.GOOS == "windows" { - path = filepath.Join(dir, "stub.bat") - interp = path - script = "@echo off\r\n" + script - } else { - path = filepath.Join(dir, "stub.sh") - interp = "sh" - script = "#!/bin/sh\n" + script - } - if err := os.WriteFile(path, []byte(script), 0o755); err != nil { - t.Fatal(err) +// The module is a subprocess, so exercising the contract -- argument passing, +// stdout parsing, stderr error reporting, timeouts -- needs something to spawn. +// This test binary stands in for it: with stubEnvVar set, TestMain impersonates +// the Python module instead of running tests. +// +// The first version of this harness wrote little sh and .bat scripts instead. +// It passed on Windows and failed on Linux in two separate ways: the service +// invokes the command as ` -m ftw_roofmodel ...`, which dash reads as +// "run the script named ftw_roofmodel" (exit 2, the stub never ran at all), and +// an unquoted JSON document loses its double quotes to shell word-splitting. +// Re-executing a compiled binary has no shell in the path, so there is no +// quoting to get wrong and nothing that can behave differently per platform. +const ( + stubModeVar = "FTW_ROOFMODEL_TEST_STUB" + stubPayloadVar = "FTW_ROOFMODEL_TEST_PAYLOAD" +) + +func TestMain(m *testing.M) { + if mode, ok := os.LookupEnv(stubModeVar); ok { + os.Exit(runStub(mode, os.Getenv(stubPayloadVar))) + } + os.Exit(m.Run()) +} + +// stubInvocation is what the stub records about how it was called, so a test +// can assert what core actually handed the module. +type stubInvocation struct { + Args []string `json:"args"` + PythonPath string `json:"pythonpath"` +} + +// runStub plays the part of `python3 -m ftw_roofmodel`. +func runStub(mode, payload string) int { + switch mode { + case "stdout": + os.Stdout.WriteString(payload) + return 0 + case "stderr": + // How the real module reports failure: JSON on stderr, non-zero exit. + os.Stderr.WriteString(payload) + return 1 + case "record": + enc, err := json.Marshal(stubInvocation{ + Args: os.Args[1:], + PythonPath: os.Getenv("PYTHONPATH"), + }) + if err != nil { + return 3 + } + if err := os.WriteFile(payload, enc, 0o600); err != nil { + return 3 + } + os.Stdout.WriteString(minimalModel) + return 0 + case "hang": + time.Sleep(30 * time.Second) + return 0 } - if runtime.GOOS == "windows" { - return interp, dir + os.Stderr.WriteString("unknown stub mode " + mode) + return 2 +} + +// stubModule points the service at this test binary running in stub mode. +func stubModule(t *testing.T, mode, payload string) string { + t.Helper() + t.Setenv(stubModeVar, mode) + t.Setenv(stubPayloadVar, payload) + exe, err := os.Executable() + if err != nil { + t.Fatalf("locating the test binary: %v", err) } - return interp, dir + return exe } +const minimalModel = `{"schema_version":1,"arrays":[]}` + func svc(t *testing.T, cfg *config.RoofModel) *Service { t.Helper() s := FromConfig(cfg) @@ -91,7 +140,7 @@ func TestDeriveRefusesOutsideSwedenWithoutSpawning(t *testing.T) { } } -// Sweden is a long diagonal, so no lat/lon rectangle can trace its border — +// Sweden is a long diagonal, so no lat/lon rectangle can trace its border -- // any box containing Sweden also contains parts of Norway and Finland. Oslo is // the clearest example: it is west of Sweden but inside the box. // @@ -161,8 +210,8 @@ func TestDeriveParsesAModel(t *testing.T) { `"source":{"provider":"lantmateriet","item_count":2,"dataset_datetime":"2018-03-01T00:00:00+00:00"},` + `"arrays":[{"name":"Roof south","kwp":7.2,"tilt_deg":35,"azimuth_deg":180,"area_m2":51.4,"segment_id":"seg-0"}],` + `"captured_at_ms":1519862400000,"derived_at_ms":1785456000000}` - cmd, dir := stubModule(t, "echo "+quoteForShell(doc)) - s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, ModuleDir: dir, GeotorgetUsername: "u", GeotorgetToken: "t"}) + cmd := stubModule(t, "stdout", doc) + s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t"}) m, err := s.Derive(context.Background(), stockholmLat, stockholmLon) if err != nil { @@ -179,12 +228,60 @@ func TestDeriveParsesAModel(t *testing.T) { } } +// The site and the operator's credentials have to survive the process boundary, +// or the module derives a roof somewhere else entirely. +func TestDerivePassesTheSiteAndCredentials(t *testing.T) { + dir := t.TempDir() + record := dir + string(os.PathSeparator) + "invocation.json" + cmd := stubModule(t, "record", record) + s := svc(t, &config.RoofModel{ + Enabled: true, Command: cmd, ModuleDir: dir, + GeotorgetUsername: "operator", GeotorgetToken: "secret-token", + RadiusM: 25, + }) + + if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon); err != nil { + t.Fatal(err) + } + + raw, err := os.ReadFile(record) + if err != nil { + t.Fatalf("stub recorded nothing: %v", err) + } + var got stubInvocation + if err := json.Unmarshal(raw, &got); err != nil { + t.Fatal(err) + } + line := strings.Join(got.Args, " ") + for _, want := range []string{ + "-m ftw_roofmodel", + "--lat 59.330000", + "--lon 18.070000", + "--username operator", + "--token secret-token", + "--radius-m 25.0", + } { + if !strings.Contains(line, want) { + t.Errorf("args %q missing %q", line, want) + } + } + // Without PYTHONPATH the module is only importable if it happens to be + // installed system-wide, which on a Pi it is not. + if got.PythonPath != dir { + t.Errorf("PYTHONPATH = %q, want %q", got.PythonPath, dir) + } + // vostok is opt-in: absent config must not silently enable a GPL tool. + if strings.Contains(line, "--vostok") { + t.Errorf("args %q passed --vostok without configuration", line) + } +} + // The module signals failure as JSON on stderr precisely so an operator sees a // cause rather than a traceback. func TestDeriveSurfacesTheModuleErrorMessage(t *testing.T) { - cmd, dir := stubModule(t, `echo {"error":"Geotorget rejected the credentials","kind":"MissingCredentials"} 1>&2 -exit 1`) - s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, ModuleDir: dir, GeotorgetUsername: "u", GeotorgetToken: "t"}) + cmd := stubModule(t, "stderr", + `{"error":"Geotorget rejected the credentials","kind":"MissingCredentials"}`) + s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t"}) _, err := s.Derive(context.Background(), stockholmLat, stockholmLon) if err == nil { @@ -195,9 +292,21 @@ exit 1`) } } +// A crash that is not the module's own JSON must still surface as an error +// rather than being mistaken for a successful empty model. +func TestDeriveReportsNonJSONFailure(t *testing.T) { + cmd := stubModule(t, "stderr", "Traceback (most recent call last):\n MemoryError\n") + s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t"}) + + _, err := s.Derive(context.Background(), stockholmLat, stockholmLon) + if err == nil || !strings.Contains(err.Error(), "roof model failed") { + t.Errorf("err = %v, want a plain failure", err) + } +} + func TestDeriveRejectsUnknownSchemaVersion(t *testing.T) { - cmd, dir := stubModule(t, "echo "+quoteForShell(`{"schema_version":99,"arrays":[]}`)) - s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, ModuleDir: dir, GeotorgetUsername: "u", GeotorgetToken: "t"}) + cmd := stubModule(t, "stdout", `{"schema_version":99,"arrays":[]}`) + s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t"}) _, err := s.Derive(context.Background(), stockholmLat, stockholmLon) if err == nil || !strings.Contains(err.Error(), "schema_version") { @@ -206,8 +315,8 @@ func TestDeriveRejectsUnknownSchemaVersion(t *testing.T) { } func TestDeriveRejectsUnreadableOutput(t *testing.T) { - cmd, dir := stubModule(t, "echo not-json-at-all") - s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, ModuleDir: dir, GeotorgetUsername: "u", GeotorgetToken: "t"}) + cmd := stubModule(t, "stdout", "not-json-at-all") + s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t"}) _, err := s.Derive(context.Background(), stockholmLat, stockholmLon) if err == nil || !strings.Contains(err.Error(), "unreadable") { @@ -218,12 +327,9 @@ func TestDeriveRejectsUnreadableOutput(t *testing.T) { // LiDAR tiles are large and this runs on a Pi; an unbounded derive could hold // memory indefinitely. func TestDeriveIsTimeBoxed(t *testing.T) { - if runtime.GOOS == "windows" { - t.Skip("no portable sleep in a .bat stub") - } - cmd, dir := stubModule(t, "sleep 30") + cmd := stubModule(t, "hang", "") s := svc(t, &config.RoofModel{ - Enabled: true, Command: cmd, ModuleDir: dir, + Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t", TimeoutS: 1, }) @@ -266,14 +372,3 @@ func TestToPVArraysMatchesConfigShape(t *testing.T) { t.Error("nil model must yield nil arrays") } } - -// quoteForShell wraps a JSON document so both sh and cmd.exe echo it intact. -func quoteForShell(s string) string { - if runtime.GOOS == "windows" { - // cmd.exe has no quoting that survives embedded quotes cleanly; escape - // the shell metacharacters instead. - r := strings.NewReplacer("^", "^^", "&", "^&", "<", "^<", ">", "^>", "|", "^|") - return r.Replace(s) - } - return "'" + s + "'" -} diff --git a/roofmodel/pyproject.toml b/roofmodel/pyproject.toml index 825d4f0fa..4e138a246 100644 --- a/roofmodel/pyproject.toml +++ b/roofmodel/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ # LAZ decoding pulls a compiled backend, so it is opt-in. Everything except the # point-cloud read works without it, which keeps the module testable in CI. geo = ["laspy[lazrs]>=2.5"] +test = ["pytest==8.4.2"] [tool.setuptools.packages.find] include = ["ftw_roofmodel*"] From 0298321a8452dad363383a5b99b762bf96e1cc92 Mon Sep 17 00:00:00 2001 From: Claude Opus 5 Date: Fri, 31 Jul 2026 15:39:41 +0200 Subject: [PATCH 09/26] feat(roofmodel): pick your building on the map, read the roof from LiDAR Settings -> Weather gains a Geotorget credential form and a building picker. Press "Find buildings here" and the footprints near the marker are drawn on the map and listed beside it; click yours, press "Read roof from LiDAR", and the PV arrays fill in with one entry per usable roof face. The form is filled but nothing is saved. The operator sees the numbers, corrects what is wrong and presses Save. FTW does not rewrite a panel configuration on its own: the derivation is a guess from a scan that may be years old, and only the operator knows whether that face has panels on it. Picking a building is not cosmetic. Without a footprint the module segments whatever stands inside its search radius, and the plane fitting is global -- a fitted plane is infinite, so a roof at azimuth 180 is z = f(y) with no x term and extends across the whole tile. A second building sharing that ridge orientation lands inside its inlier band however far away it is, and the two lose returns to each other. Measured on a synthetic pair: a detached garage recovered 93% of its true area and split into two fragments while coplanar with the house, against 100% and one clean face once clipped to its own footprint. The clip buffers the outline by a metre first, because roofs overhang their walls and the eaves carry the lowest returns. Frames are detected rather than assumed. GeoJSON mandates WGS84 but Lantmateriet publishes this catalogue in SWEREF 99 TM and its STAC search takes a SWEREF bbox, so ring coordinates are classified by magnitude -- six- and seven-figure numbers are projected metres, degrees never are. New GET /api/roofmodel/buildings lists footprints as GeoJSON, honouring an explicit lat/lon so the picker can search where the marker is rather than where the last save put it. POST /api/roofmodel/derive accepts a building_id. GET /api/roofmodel reports has_credentials so the UI can stop asking. The Geotorget token now masks and restores like every other secret. It never appears in an API response, and saving an unrelated setting no longer wipes it -- the settings form returns the blank it was given, which without PreserveMaskedSecrets would have deleted the stored credential. docs/roof-geometry.md covers ordering the two Geotorget products, what the derived kWp does and does not mean (an upper bound on what fits, not what is installed), and what each failure message is telling you. Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com> Signed-off-by: Hugo Karlsson <48095810+HuggeK@users.noreply.github.com> --- .changeset/roofmodel-building-picker.md | 38 +++ docs/roof-geometry.md | 110 +++++++ go/internal/api/api.go | 93 +++++- go/internal/api/api_roofmodel_test.go | 71 +++++ go/internal/config/config.go | 16 + go/internal/config/roofmodel_secrets_test.go | 89 ++++++ go/internal/roofmodel/roofmodel.go | 96 ++++-- go/internal/roofmodel/roofmodel_test.go | 137 ++++++++- roofmodel/ftw_roofmodel/__main__.py | 51 +++- roofmodel/ftw_roofmodel/buildings.py | 302 +++++++++++++++++++ roofmodel/ftw_roofmodel/pipeline.py | 43 ++- roofmodel/tests/test_buildings.py | 206 +++++++++++++ roofmodel/tests/test_derive_footprint.py | 149 +++++++++ web/index.html | 2 +- web/settings/tabs/weather.js | 235 ++++++++++++++- 15 files changed, 1583 insertions(+), 55 deletions(-) create mode 100644 .changeset/roofmodel-building-picker.md create mode 100644 docs/roof-geometry.md create mode 100644 go/internal/config/roofmodel_secrets_test.go create mode 100644 roofmodel/ftw_roofmodel/buildings.py create mode 100644 roofmodel/tests/test_buildings.py create mode 100644 roofmodel/tests/test_derive_footprint.py diff --git a/.changeset/roofmodel-building-picker.md b/.changeset/roofmodel-building-picker.md new file mode 100644 index 000000000..4514ba210 --- /dev/null +++ b/.changeset/roofmodel-building-picker.md @@ -0,0 +1,38 @@ +--- +"ftw": minor +--- + +Pick your building on the map and read the panel angles off Lantmäteriet's laser +scan, instead of measuring your own roof. + +**Settings → Weather** gains a Geotorget credential form and a building picker. +Press *Find buildings here* and the footprints near the marker are drawn on the +map and listed beside it; click yours, press *Read roof from LiDAR*, and the PV +arrays fill in with one entry per usable roof face. The form is filled but +nothing is saved: the operator sees the numbers, corrects what is wrong and +presses Save. FTW never rewrites a panel configuration on its own, because the +derivation is a guess from a scan that may be years old and only the operator +knows whether that face has panels on it at all. + +Picking a building is not cosmetic. Without a footprint the module segments +whatever stands inside its search radius, and the plane fitting is global — a +fitted plane is infinite, so a roof at azimuth 180° is `z = f(y)` with no `x` +term and extends across the whole tile. A second building sharing that ridge +orientation lands inside its inlier band however far away it is, and the two lose +returns to each other. Measured on a synthetic pair: a detached garage recovered +93% of its true area and split into two fragments while coplanar with the house, +against 100% and one clean face once clipped to its own footprint. Clipping also +buffers the outline by a metre so the eaves, where the lowest roof returns are, +are not shaved off. + +New `GET /api/roofmodel/buildings` lists footprints as GeoJSON, honouring an +explicit `lat`/`lon` so the picker can search where the marker is rather than +where the last save put it. `POST /api/roofmodel/derive` accepts a `building_id`. +`GET /api/roofmodel` reports `has_credentials` so the UI can stop asking. + +The Geotorget token now masks and restores like every other secret: it never +appears in an API response, and saving an unrelated setting no longer wipes it. + +Documented in [docs/roof-geometry.md](../docs/roof-geometry.md), including how to +order the two Geotorget products, what the derived kWp does and does not mean, +and what each failure message is telling you. diff --git a/docs/roof-geometry.md b/docs/roof-geometry.md new file mode 100644 index 000000000..ad10ac9cc --- /dev/null +++ b/docs/roof-geometry.md @@ -0,0 +1,110 @@ +# Roof geometry from Lantmäteriet + +FTW's PV forecast needs the tilt and azimuth of each roof face. Typing them in +means measuring your own roof, and most people estimate. In Sweden the state +already flew a laser over it, so FTW can read the numbers instead. + +This is **optional and Sweden-only**. Everywhere else, and whenever anything +below is missing, the numeric fields in **Settings → Weather → PV arrays** stay +the way they work today. + +## What you need + +A free [Geotorget](https://geotorget.lantmateriet.se) account with access +ordered to two products. Both are open data under CC BY 4.0; the account exists +so Lantmäteriet can see who is downloading, not to charge you. + +| Product | What FTW uses it for | +|---|---| +| [Byggnad Nedladdning, vektor](https://geotorget.lantmateriet.se/geodataprodukter/byggnad-nedladdning-vektor-api) | Building footprints, so you can point at your house | +| [Laserdata Nedladdning, Skog](https://geotorget.lantmateriet.se/geodataprodukter/laserdata-nedladdning-skog-api) | The laser scan the roof planes are fitted to | + +Ordering access is not instant — Lantmäteriet approves it — so do it before you +plan to use this. + +You also need the `roofmodel` module's dependencies on the FTW host: + +```bash +pip install -e roofmodel[geo] +``` + +The `geo` extra pulls the LAZ reader. Without it everything except reading the +point cloud works, which is enough to run the tests but not enough to derive a +real roof. + +## Using it + +1. Open **Settings → Weather**. +2. Put the map marker on your building. +3. Under **Roof geometry from Lantmäteriet**, tick **Enable roof derivation**, + enter your Geotorget username and token, and **Save**. +4. Press **Find buildings here**. Footprints appear on the map and as a list. +5. Click your building. It highlights green. +6. Press **Read roof from LiDAR**. + +The PV arrays above fill in with one entry per usable roof face. **Nothing is +saved yet** — look at the numbers, correct anything that is wrong, then press +Save. FTW never rewrites your panel configuration on its own: the derivation is +a good guess from a scan that may be several years old, and you are the one who +knows whether there are panels on that face at all. + +## What the numbers mean, and what they do not + +- **Tilt and azimuth** come from a plane fitted to the laser returns on your + roof. These are the values worth trusting. +- **kWp** is an *upper bound on what fits*: roof area × a packing factor (0.70 + by default, covering ridges, eaves, chimneys and walkways) × 200 W/m². It is + not what you have installed. If you have six panels on a face that could hold + twenty, correct it. +- **North-facing pitched faces are dropped.** At Swedish latitudes they yield + too little to be worth proposing. Flat roofs are kept, since panels on them + get mounted facing south regardless of which way the building points. +- **Capture date** is shown with the result. Lantmäteriet is still backfilling + the STAC `datetime` field through 2026, so it sometimes reads "date unknown". + A roof built after the scan will not be in the data at all — you will get a + clear error rather than a wrong answer. + +## Why you pick a building + +Without a footprint the module segments everything within its radius: your +neighbour's roof, the garage, the trees. Worse, the plane fitting is *global* — +a fitted plane is infinite, so a roof at azimuth 180° is described by `z = f(y)` +with no `x` term at all and extends right across the tile. A second building +sharing your ridge orientation falls inside its inlier band however far away it +is, and the two lose points to each other. + +Measured on a synthetic pair: a detached garage recovered 93% of its true area +and split into two fragments when it lay on the house's plane, against 100% and +a single clean face once the cloud was clipped to its own footprint. + +Picking a building is what makes the answer yours. + +## Optional: shading + +Tilt and azimuth predict an unobstructed roof. They say nothing about the spruce +to the south, which can cost more than any amount of azimuth. + +If you install [vostok](https://github.com/3dgeo-heidelberg/vostok) and set +`roofmodel.vostok_binary`, each face is additionally run against the surrounding +geometry and gets a `shading_factor`. + +vostok is **GPL-3.0 and is not part of FTW**. FTW never bundles it, never ships +it and never installs it; you install it yourself and point FTW at it. It is run +as a separate process communicating through files, which is what keeps the +licences apart. Without it, faces carry no shading factor at all — deliberately +distinct from a factor of 1.0, because "we did not look" and "we looked and it +is clear" are different claims. + +## When it does not work + +| What you see | What it means | +|---|---| +| "Roof derivation is off" | Tick the box, add credentials, Save, retry | +| "Geotorget rejected the credentials" | Wrong token, or the account has not been granted that product | +| "No buildings found here" | The marker is not on a building, or you are outside Sweden | +| "only N LiDAR returns fall on building" | The building is newer than the scan, or you picked the wrong footprint | +| "No roof faces worth mounting panels on" | Everything found was north-facing or under 8 m² | + +Failures never change your configuration. + +*Data © Lantmäteriet, CC BY 4.0.* diff --git a/go/internal/api/api.go b/go/internal/api/api.go index 52dd07ab4..06c59c175 100644 --- a/go/internal/api/api.go +++ b/go/internal/api/api.go @@ -509,6 +509,7 @@ func (s *Server) routes() { s.handle("GET /api/pv/performance", Read, s.handlePVPerformance) s.handle("GET /api/data-sources", Read, s.handleDataSources) s.handle("GET /api/roofmodel", Read, s.handleRoofModel) + s.handle("GET /api/roofmodel/buildings", Read, s.handleRoofModelBuildings) s.handle("POST /api/roofmodel/derive", Configure, s.handleRoofModelDerive) s.handle("GET /api/mpc/plan", Read, s.handleMPCPlan) s.handle("POST /api/mpc/replan", Configure, s.handleMPCReplan) @@ -2438,13 +2439,13 @@ func (s *Server) handleForecast(w http.ResponseWriter, r *http.Request) { // ---- /api/roofmodel ---- // // GET reports whether roof derivation is available for this site and why not -// when it is unavailable. POST /api/roofmodel/derive runs it. +// when it is unavailable. GET /api/roofmodel/buildings lists footprints to pick +// from; POST /api/roofmodel/derive fits the roof of the picked one. // -// Deliberately *not* wired: writing the derived arrays straight into -// weather.pv_arrays. Derivation is a best guess from a point cloud that may be -// years old, and silently rewriting an operator's panel config is a change they -// should make knowingly. The endpoint returns the proposal; applying it is a -// separate, explicit act. +// The derived arrays are returned, never written. The settings form fills +// itself in with them and the operator saves -- so the panel config still +// changes only when someone looks at the numbers and agrees. Derivation is a +// best guess from a point cloud that may be years old. func (s *Server) handleRoofModel(w http.ResponseWriter, r *http.Request) { lat, lon, haveSite := s.siteLocation() resp := map[string]any{"enabled": s.deps.RoofModel.Enabled()} @@ -2452,6 +2453,7 @@ func (s *Server) handleRoofModel(w http.ResponseWriter, r *http.Request) { resp["covers"] = coverage.Covers("lantmateriet", lat, lon) resp["latitude"], resp["longitude"] = lat, lon } + resp["has_credentials"] = s.roofModelHasCredentials() if src, ok := coverage.ByID("lantmateriet"); ok { resp["area"] = src.Area resp["license"] = src.License @@ -2473,15 +2475,20 @@ func (s *Server) handleRoofModelDerive(w http.ResponseWriter, r *http.Request) { writeJSON(w, 400, map[string]any{"error": "site latitude/longitude is not configured"}) return } + // Optional: which footprint to clip the LiDAR to. An absent or empty body + // keeps the old behaviour of segmenting the whole search radius. + var body struct { + BuildingID string `json:"building_id"` + } + if r.Body != nil { + _ = json.NewDecoder(io.LimitReader(r.Body, 1<<16)).Decode(&body) + } + // A derive downloads and segments LiDAR tiles; the service time-boxes it, // but the request should also die with the client rather than outliving it. - model, err := s.deps.RoofModel.Derive(r.Context(), lat, lon) + model, err := s.deps.RoofModel.Derive(r.Context(), lat, lon, body.BuildingID) if err != nil { - status := 502 - if errors.Is(err, roofmodel.ErrOutsideCoverage) || errors.Is(err, roofmodel.ErrNoCredentials) { - status = 400 - } - writeJSON(w, status, map[string]any{"error": err.Error()}) + writeJSON(w, roofModelErrorStatus(err), map[string]any{"error": err.Error()}) return } writeJSON(w, 200, map[string]any{ @@ -2491,6 +2498,68 @@ func (s *Server) handleRoofModelDerive(w http.ResponseWriter, r *http.Request) { }) } +// handleRoofModelBuildings lists building footprints near the site for the +// picker. Read-only and cheap relative to a derive: it is one STAC search with +// no LiDAR download behind it. +func (s *Server) handleRoofModelBuildings(w http.ResponseWriter, r *http.Request) { + if !s.deps.RoofModel.Enabled() { + writeJSON(w, 200, map[string]any{ + "enabled": false, + "error": "roof model module is not enabled", + }) + return + } + lat, lon, haveSite := s.siteLocation() + // The map lets you drag the pin before saving, so honour an explicit + // coordinate over the stored one. + if v, err := strconv.ParseFloat(r.URL.Query().Get("lat"), 64); err == nil { + if w2, err2 := strconv.ParseFloat(r.URL.Query().Get("lon"), 64); err2 == nil { + lat, lon, haveSite = v, w2, true + } + } + if !haveSite { + writeJSON(w, 400, map[string]any{"error": "site latitude/longitude is not configured"}) + return + } + + list, err := s.deps.RoofModel.Buildings(r.Context(), lat, lon) + if err != nil { + writeJSON(w, roofModelErrorStatus(err), map[string]any{"error": err.Error()}) + return + } + writeJSON(w, 200, map[string]any{ + "enabled": true, + "latitude": lat, + "longitude": lon, + "buildings": list.Buildings, + }) +} + +// roofModelErrorStatus separates "you asked for something impossible" from +// "the module or Lantmateriet failed", so the UI can tell the operator to fix +// their input rather than to retry. +func roofModelErrorStatus(err error) int { + if errors.Is(err, roofmodel.ErrOutsideCoverage) || errors.Is(err, roofmodel.ErrNoCredentials) { + return 400 + } + return 502 +} + +// roofModelHasCredentials reports whether a Geotorget token is stored, without +// revealing it. +func (s *Server) roofModelHasCredentials() bool { + if s.deps.CfgMu == nil { + return false + } + s.deps.CfgMu.RLock() + defer s.deps.CfgMu.RUnlock() + if s.deps.Cfg == nil || s.deps.Cfg.RoofModel == nil { + return false + } + rm := s.deps.Cfg.RoofModel + return rm.GeotorgetUsername != "" && rm.GeotorgetToken != "" +} + // siteLocation returns the configured site coordinates. func (s *Server) siteLocation() (lat, lon float64, ok bool) { if s.deps.CfgMu == nil { diff --git a/go/internal/api/api_roofmodel_test.go b/go/internal/api/api_roofmodel_test.go index d36379896..8e65f8eac 100644 --- a/go/internal/api/api_roofmodel_test.go +++ b/go/internal/api/api_roofmodel_test.go @@ -4,7 +4,11 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "strings" "testing" + + "github.com/srcfl/ftw/go/internal/config" + "github.com/srcfl/ftw/go/internal/roofmodel" ) func getJSON(t *testing.T, deps *Deps, method, path string) (int, map[string]any) { @@ -78,6 +82,73 @@ func TestRoofModelDeriveWithoutASiteIsARequestError(t *testing.T) { } } +func TestRoofModelBuildingsDisabledReportsCleanly(t *testing.T) { + code, body := getJSON(t, depsAt(59.33, 18.07), http.MethodGet, "/api/roofmodel/buildings") + if code != 200 { + t.Fatalf("status = %d, want 200", code) + } + if body["enabled"] != false { + t.Errorf("enabled = %v, want false", body["enabled"]) + } + if body["error"] == nil { + t.Error("a disabled building search should say why") + } +} + +// The map lets the pin be dragged before anything is saved, so the picker has +// to be able to search where the pin is rather than where the config says. +func TestRoofModelBuildingsAcceptsAnExplicitCoordinate(t *testing.T) { + deps := depsAt(59.33, 18.07) + deps.RoofModel = roofmodel.FromConfig(&config.RoofModel{ + Enabled: true, Command: "definitely-not-a-real-command", + GeotorgetUsername: "u", GeotorgetToken: "t", + }) + + // Berlin is outside Lantmateriet coverage; if the query coordinate were + // ignored the stored Stockholm one would be used and this would not be a 400. + code, body := getJSON(t, deps, http.MethodGet, + "/api/roofmodel/buildings?lat=52.52&lon=13.40") + if code != 400 { + t.Fatalf("status = %d, want 400 for a site outside Sweden (body %v)", code, body) + } + + // A malformed pair must fall back to the configured site rather than + // searching at (0, 0), which is in the Atlantic. + _, sthlm := getJSON(t, deps, http.MethodGet, "/api/roofmodel/buildings?lat=abc&lon=def") + if sthlm["error"] == nil { + t.Fatal("want the spawn to fail, since the command does not exist") + } + if msg, _ := sthlm["error"].(string); strings.Contains(msg, "not in Sweden") { + t.Errorf("bad coordinates were used instead of the configured site: %v", msg) + } +} + +// The Geotorget token is the operator's credential. Status may be reported; +// the secret itself must never appear in a response. +func TestRoofModelNeverEchoesTheToken(t *testing.T) { + deps := depsAt(59.33, 18.07) + deps.Cfg.RoofModel = &config.RoofModel{ + Enabled: true, GeotorgetUsername: "operator", GeotorgetToken: "gt_secret_value", + } + deps.RoofModel = roofmodel.FromConfig(deps.Cfg.RoofModel) + + srv := New(deps) + for _, path := range []string{"/api/roofmodel", "/api/roofmodel/buildings"} { + req := httptest.NewRequest(http.MethodGet, path, nil) + rr := httptest.NewRecorder() + srv.Handler().ServeHTTP(rr, req) + if strings.Contains(rr.Body.String(), "gt_secret_value") { + t.Errorf("%s leaked the Geotorget token: %s", path, rr.Body.String()) + } + } + + _, status := getJSON(t, deps, http.MethodGet, "/api/roofmodel") + if status["has_credentials"] != true { + t.Errorf("has_credentials = %v, want true so the UI can stop asking", + status["has_credentials"]) + } +} + // Lantmäteriet must appear in the coverage listing alongside every other // source, so an operator finds it without knowing it exists. func TestLantmaterietAppearsInDataSources(t *testing.T) { diff --git a/go/internal/config/config.go b/go/internal/config/config.go index 280c21571..6837a9959 100644 --- a/go/internal/config/config.go +++ b/go/internal/config/config.go @@ -1458,6 +1458,10 @@ type RoofModel struct { GeotorgetUsername string `yaml:"geotorget_username,omitempty" json:"geotorget_username,omitempty"` GeotorgetToken string `yaml:"geotorget_token,omitempty" json:"geotorget_token,omitempty"` + // HasGeotorgetToken is set only on the masked copy the API returns, so the + // UI can show that a token is stored without ever receiving it. Never + // written to YAML and never read from an incoming config. + HasGeotorgetToken bool `yaml:"-" json:"has_geotorget_token,omitempty"` // RadiusM is how far around the site to pull LiDAR (default 40 m). RadiusM float64 `yaml:"radius_m,omitempty" json:"radius_m,omitempty"` @@ -1604,6 +1608,15 @@ func (c Config) MaskSecrets() Config { cp.APIKey = "" out.Assistant = &cp } + if out.RoofModel != nil { + cp := *out.RoofModel + // The UI has to distinguish "no credential stored" from "one is stored + // but masked", or an operator cannot tell whether they still need to + // paste their Geotorget token in. + cp.HasGeotorgetToken = strings.TrimSpace(cp.GeotorgetToken) != "" + cp.GeotorgetToken = "" + out.RoofModel = &cp + } if out.Notifications != nil { cp := *out.Notifications if cp.Ntfy != nil { @@ -1699,6 +1712,9 @@ func (incoming *Config) PreserveMaskedSecrets(existing *Config) { if incoming.Assistant != nil && existing.Assistant != nil && incoming.Assistant.APIKey == "" { incoming.Assistant.APIKey = existing.Assistant.APIKey } + if incoming.RoofModel != nil && existing.RoofModel != nil && incoming.RoofModel.GeotorgetToken == "" { + incoming.RoofModel.GeotorgetToken = existing.RoofModel.GeotorgetToken + } if incoming.Notifications != nil && existing.Notifications != nil && incoming.Notifications.Ntfy != nil && existing.Notifications.Ntfy != nil { if incoming.Notifications.Ntfy.AccessToken == "" { diff --git a/go/internal/config/roofmodel_secrets_test.go b/go/internal/config/roofmodel_secrets_test.go new file mode 100644 index 000000000..fe1e36d71 --- /dev/null +++ b/go/internal/config/roofmodel_secrets_test.go @@ -0,0 +1,89 @@ +package config + +import "testing" + +// The Geotorget token is the operator's own credential for Lantmateriet. It +// must never come back out of the API, and -- the failure that actually bites -- +// saving the settings form must not wipe it, because the form only ever sends +// back the blank it was given. + +func TestRoofModelMaskSecretsHidesTheTokenButSaysOneExists(t *testing.T) { + c := Config{RoofModel: &RoofModel{ + Enabled: true, + GeotorgetUsername: "operator@example.com", + GeotorgetToken: "gt_secret_value", + }} + m := c.MaskSecrets() + + if m.RoofModel.GeotorgetToken != "" { + t.Errorf("token leaked through the API: %q", m.RoofModel.GeotorgetToken) + } + if !m.RoofModel.HasGeotorgetToken { + t.Error("UI cannot tell a stored token from a missing one") + } + // The username is not a secret, and blanking it would make the form look + // empty when it is not. + if m.RoofModel.GeotorgetUsername != "operator@example.com" { + t.Errorf("username got blanked: %q", m.RoofModel.GeotorgetUsername) + } + if c.RoofModel.GeotorgetToken != "gt_secret_value" { + t.Error("masking mutated the original config") + } +} + +func TestRoofModelMaskSecretsReportsNoTokenWhenUnset(t *testing.T) { + for _, tok := range []string{"", " "} { + c := Config{RoofModel: &RoofModel{Enabled: true, GeotorgetToken: tok}} + if c.MaskSecrets().RoofModel.HasGeotorgetToken { + t.Errorf("token %q reported as stored", tok) + } + } +} + +// Saving any unrelated setting round-trips the whole config, so an empty token +// from the UI means "unchanged", not "delete it". +func TestRoofModelPreserveMaskedSecretsKeepsTheStoredToken(t *testing.T) { + existing := &Config{RoofModel: &RoofModel{ + Enabled: true, GeotorgetUsername: "operator", GeotorgetToken: "gt_secret_value", + }} + incoming := &Config{RoofModel: &RoofModel{ + Enabled: true, GeotorgetUsername: "operator", GeotorgetToken: "", RadiusM: 60, + }} + + incoming.PreserveMaskedSecrets(existing) + + if incoming.RoofModel.GeotorgetToken != "gt_secret_value" { + t.Errorf("token = %q, want it preserved", incoming.RoofModel.GeotorgetToken) + } + if incoming.RoofModel.RadiusM != 60 { + t.Error("the edit being saved was lost") + } +} + +// Pasting a new token has to replace the old one, or a rotated credential +// could never be entered. +func TestRoofModelPreserveMaskedSecretsAcceptsANewToken(t *testing.T) { + existing := &Config{RoofModel: &RoofModel{GeotorgetToken: "old_token"}} + incoming := &Config{RoofModel: &RoofModel{GeotorgetToken: "new_token"}} + + incoming.PreserveMaskedSecrets(existing) + + if incoming.RoofModel.GeotorgetToken != "new_token" { + t.Errorf("token = %q, want the newly entered one", incoming.RoofModel.GeotorgetToken) + } +} + +// Enabling the module for the first time has no existing section to copy from. +func TestRoofModelPreserveMaskedSecretsSurvivesAMissingSection(t *testing.T) { + incoming := &Config{RoofModel: &RoofModel{GeotorgetToken: "first_token"}} + incoming.PreserveMaskedSecrets(&Config{}) + if incoming.RoofModel.GeotorgetToken != "first_token" { + t.Errorf("token = %q", incoming.RoofModel.GeotorgetToken) + } + + none := &Config{} + none.PreserveMaskedSecrets(&Config{RoofModel: &RoofModel{GeotorgetToken: "x"}}) + if none.RoofModel != nil { + t.Error("a section the operator never configured was invented") + } +} diff --git a/go/internal/roofmodel/roofmodel.go b/go/internal/roofmodel/roofmodel.go index 9c9517cbd..fe7453fac 100644 --- a/go/internal/roofmodel/roofmodel.go +++ b/go/internal/roofmodel/roofmodel.go @@ -60,12 +60,38 @@ type Array struct { SegmentID string `json:"segment_id"` } +// BuildingList is what `--mode buildings` emits: GeoJSON features a map can +// draw directly, nearest first. Geometry is passed through as raw JSON because +// core has no business interpreting a polygon -- it only ferries it to the UI. +type BuildingList struct { + SchemaVersion int `json:"schema_version"` + Site struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + } `json:"site"` + Buildings []json.RawMessage `json:"buildings"` +} + +// Building records which footprint a model was derived from, and how much of +// the surrounding cloud survived the clip -- the honest measure of whether the +// footprint and the scan actually agree. +type Building struct { + BuildingID string `json:"building_id"` + AreaM2 float64 `json:"area_m2"` + Footprint json.RawMessage `json:"footprint"` + ReturnsUsed int `json:"returns_used"` + ReturnsInRadius int `json:"returns_in_radius"` +} + // Model is the versioned document the module emits. type Model struct { SchemaVersion int `json:"schema_version"` Arrays []Array `json:"arrays"` PlanesFound int `json:"planes_found"` - Site struct { + // Building is null when the whole search radius was segmented rather than + // one picked footprint. + Building *Building `json:"building"` + Site struct { Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` RadiusM float64 `json:"radius_m"` @@ -134,12 +160,58 @@ func (s *Service) packingFactor() float64 { return defaultPackingFactor } +// Buildings lists candidate building footprints near a site, nearest first, so +// the operator can pick the one their panels are going on. +// +// Without this step a derive segments everything inside its radius: the +// neighbour's roof, the garage, the trees. Worse, RANSAC fits infinite planes, +// so a second building sharing the ridge orientation lands inside the first +// one's inlier band however far away it is and steals its returns. +func (s *Service) Buildings(ctx context.Context, lat, lon float64) (*BuildingList, error) { + out, err := s.run(ctx, lat, lon, "buildings", "") + if err != nil { + return nil, err + } + var list BuildingList + if err := json.Unmarshal(out, &list); err != nil { + return nil, fmt.Errorf("roof model returned unreadable output: %w", err) + } + if list.SchemaVersion != 1 { + return nil, fmt.Errorf("roof model schema_version %d is not supported", list.SchemaVersion) + } + slog.Info("roof model buildings", "lat", lat, "lon", lon, "found", len(list.Buildings)) + return &list, nil +} + // Derive runs the module for one site. // +// buildingID is optional; pass one from Buildings to clip the LiDAR to that +// footprint before segmenting, which is what makes the derived tilt and azimuth +// belong to the operator's own roof rather than to whatever else stood in range. +// // Coverage and credentials are checked before spawning anything: a site outside // Sweden can never succeed, and a missing credential fails the same way every // time, so neither is worth an interpreter start and a network round trip. -func (s *Service) Derive(ctx context.Context, lat, lon float64) (*Model, error) { +func (s *Service) Derive(ctx context.Context, lat, lon float64, buildingID string) (*Model, error) { + out, err := s.run(ctx, lat, lon, "derive", buildingID) + if err != nil { + return nil, err + } + var m Model + if err := json.Unmarshal(out, &m); err != nil { + return nil, fmt.Errorf("roof model returned unreadable output: %w", err) + } + if m.SchemaVersion != 1 { + return nil, fmt.Errorf("roof model schema_version %d is not supported", m.SchemaVersion) + } + slog.Info("roof model derived", + "lat", lat, "lon", lon, "arrays", len(m.Arrays), + "planes", m.PlanesFound, "building", buildingID) + return &m, nil +} + +// run spawns the module and returns its stdout. +func (s *Service) run(ctx context.Context, lat, lon float64, mode, buildingID string) ([]byte, error) { if !s.Enabled() { return nil, ErrDisabled } @@ -155,6 +227,7 @@ func (s *Service) Derive(ctx context.Context, lat, lon float64) (*Model, error) args := []string{ "-m", "ftw_roofmodel", + "--mode", mode, "--lat", fmt.Sprintf("%.6f", lat), "--lon", fmt.Sprintf("%.6f", lon), "--username", s.cfg.GeotorgetUsername, @@ -162,6 +235,9 @@ func (s *Service) Derive(ctx context.Context, lat, lon float64) (*Model, error) "--radius-m", fmt.Sprintf("%.1f", s.radius()), "--packing-factor", fmt.Sprintf("%.3f", s.packingFactor()), } + if buildingID != "" { + args = append(args, "--building-id", buildingID) + } cmd := exec.CommandContext(ctx, s.command(), args...) if s.cfg.ModuleDir != "" { cmd.Env = append(cmd.Environ(), "PYTHONPATH="+s.cfg.ModuleDir) @@ -170,9 +246,7 @@ func (s *Service) Derive(ctx context.Context, lat, lon float64) (*Model, error) cmd.Stdout = &stdout cmd.Stderr = &stderr - start := time.Now() err := cmd.Run() - elapsed := time.Since(start) if ctx.Err() == context.DeadlineExceeded { return nil, fmt.Errorf("roof model timed out after %s", s.timeout()) @@ -189,19 +263,7 @@ func (s *Service) Derive(ctx context.Context, lat, lon float64) (*Model, error) if stdout.Len() > maxOutputBytes { return nil, fmt.Errorf("roof model returned %d bytes, refusing", stdout.Len()) } - - var m Model - if err := json.Unmarshal(stdout.Bytes(), &m); err != nil { - return nil, fmt.Errorf("roof model returned unreadable output: %w", err) - } - if m.SchemaVersion != 1 { - return nil, fmt.Errorf("roof model schema_version %d is not supported", m.SchemaVersion) - } - - slog.Info("roof model derived", - "lat", lat, "lon", lon, "arrays", len(m.Arrays), - "planes", m.PlanesFound, "elapsed", elapsed) - return &m, nil + return stdout.Bytes(), nil } // ToPVArrays converts derived arrays into config entries ready to be written diff --git a/go/internal/roofmodel/roofmodel_test.go b/go/internal/roofmodel/roofmodel_test.go index 4832b8a83..5bb09e6d2 100644 --- a/go/internal/roofmodel/roofmodel_test.go +++ b/go/internal/roofmodel/roofmodel_test.go @@ -111,7 +111,7 @@ func TestDisabledWhenAbsentOrOff(t *testing.T) { if s.Enabled() { t.Error("nil service must report disabled") } - if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon); !errors.Is(err, ErrDisabled) { + if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, ""); !errors.Is(err, ErrDisabled) { t.Errorf("err = %v, want ErrDisabled", err) } } @@ -133,7 +133,7 @@ func TestDeriveRefusesOutsideSwedenWithoutSpawning(t *testing.T) { {"New York", 40.71, -74.01}, {"north of Sweden", 71.0, 20.0}, } { - _, err := s.Derive(context.Background(), c.lat, c.lon) + _, err := s.Derive(context.Background(), c.lat, c.lon, "") if !errors.Is(err, ErrOutsideCoverage) { t.Errorf("%s: err = %v, want ErrOutsideCoverage", c.name, err) } @@ -155,7 +155,7 @@ func TestSwedishBoxAdmitsSomeNonSwedishPointsByDesign(t *testing.T) { Command: "definitely-not-a-real-command", GeotorgetUsername: "u", GeotorgetToken: "t", }) - _, err := s.Derive(context.Background(), 59.91, 10.75) // Oslo + _, err := s.Derive(context.Background(), 59.91, 10.75, "") // Oslo if errors.Is(err, ErrOutsideCoverage) { t.Skip("box now excludes Oslo; verify it still admits Strömstad and Haparanda") } @@ -181,7 +181,7 @@ func TestSwedishBoxCoversBorderTowns(t *testing.T) { {"Karesuando (far north)", 68.44, 22.49}, {"Smygehuk (far south)", 55.34, 13.36}, } { - _, err := s.Derive(context.Background(), c.lat, c.lon) + _, err := s.Derive(context.Background(), c.lat, c.lon, "") if errors.Is(err, ErrOutsideCoverage) { t.Errorf("%s: must not be excluded", c.name) } @@ -197,7 +197,7 @@ func TestDeriveRequiresCredentials(t *testing.T) { {"neither", "", ""}, } { s := svc(t, &config.RoofModel{Enabled: true, Command: "no-such-command", GeotorgetUsername: c.user, GeotorgetToken: c.token}) - _, err := s.Derive(context.Background(), stockholmLat, stockholmLon) + _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "") if !errors.Is(err, ErrNoCredentials) { t.Errorf("%s: err = %v, want ErrNoCredentials", c.name, err) } @@ -213,7 +213,7 @@ func TestDeriveParsesAModel(t *testing.T) { cmd := stubModule(t, "stdout", doc) s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t"}) - m, err := s.Derive(context.Background(), stockholmLat, stockholmLon) + m, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "") if err != nil { t.Fatal(err) } @@ -240,7 +240,7 @@ func TestDerivePassesTheSiteAndCredentials(t *testing.T) { RadiusM: 25, }) - if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon); err != nil { + if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, ""); err != nil { t.Fatal(err) } @@ -276,6 +276,119 @@ func TestDerivePassesTheSiteAndCredentials(t *testing.T) { } } +// Picking a building is the whole point of the picker: the id has to reach the +// module, or the derive silently segments the neighbourhood instead. +func TestDerivePassesThePickedBuilding(t *testing.T) { + dir := t.TempDir() + record := dir + string(os.PathSeparator) + "invocation.json" + cmd := stubModule(t, "record", record) + s := svc(t, &config.RoofModel{ + Enabled: true, Command: cmd, + GeotorgetUsername: "u", GeotorgetToken: "t", + }) + + if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "bldg-42"); err != nil { + t.Fatal(err) + } + + line := strings.Join(readInvocation(t, record).Args, " ") + if !strings.Contains(line, "--building-id bldg-42") { + t.Errorf("args %q did not carry the picked building", line) + } + if !strings.Contains(line, "--mode derive") { + t.Errorf("args %q did not select derive mode", line) + } +} + +// Not picking one must not send an empty flag the module would treat as a +// building named "". +func TestDeriveOmitsTheBuildingFlagWhenNoneIsPicked(t *testing.T) { + dir := t.TempDir() + record := dir + string(os.PathSeparator) + "invocation.json" + cmd := stubModule(t, "record", record) + s := svc(t, &config.RoofModel{ + Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t", + }) + + if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, ""); err != nil { + t.Fatal(err) + } + + if line := strings.Join(readInvocation(t, record).Args, " "); strings.Contains(line, "--building-id") { + t.Errorf("args %q passed an empty building id", line) + } +} + +func TestBuildingsListsFootprints(t *testing.T) { + doc := `{"schema_version":1,"site":{"latitude":59.33,"longitude":18.07},"buildings":[` + + `{"type":"Feature","id":"b1","geometry":{"type":"Polygon","coordinates":[[[18.0,59.3],[18.001,59.3],[18.001,59.301],[18.0,59.3]]]},"properties":{"area_m2":120.5}},` + + `{"type":"Feature","id":"b2","geometry":{"type":"Polygon","coordinates":[[[18.01,59.3],[18.011,59.3],[18.011,59.301],[18.01,59.3]]]},"properties":{"area_m2":64.0}}]}` + cmd := stubModule(t, "stdout", doc) + s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t"}) + + list, err := s.Buildings(context.Background(), stockholmLat, stockholmLon) + if err != nil { + t.Fatal(err) + } + if len(list.Buildings) != 2 { + t.Fatalf("got %d buildings", len(list.Buildings)) + } + // Geometry is ferried to the map untouched, so it must survive intact. + if !strings.Contains(string(list.Buildings[0]), `"id":"b1"`) { + t.Errorf("first feature = %s", list.Buildings[0]) + } +} + +func TestBuildingsUsesBuildingsMode(t *testing.T) { + dir := t.TempDir() + record := dir + string(os.PathSeparator) + "invocation.json" + cmd := stubModule(t, "record", record) + s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t"}) + + // The stub answers with a roof model, not a building list; only the + // invocation matters here. + _, _ = s.Buildings(context.Background(), stockholmLat, stockholmLon) + + if line := strings.Join(readInvocation(t, record).Args, " "); !strings.Contains(line, "--mode buildings") { + t.Errorf("args %q did not select buildings mode", line) + } +} + +// Every guard that protects a derive has to protect a building search too -- +// it is the same credentials and the same country. +func TestBuildingsRefusesOutsideCoverageAndWithoutCredentials(t *testing.T) { + s := svc(t, &config.RoofModel{ + Enabled: true, Command: "definitely-not-a-real-command", + GeotorgetUsername: "u", GeotorgetToken: "t", + }) + if _, err := s.Buildings(context.Background(), 52.52, 13.40); !errors.Is(err, ErrOutsideCoverage) { + t.Errorf("Berlin: err = %v, want ErrOutsideCoverage", err) + } + + noCreds := svc(t, &config.RoofModel{Enabled: true, Command: "no-such-command"}) + if _, err := noCreds.Buildings(context.Background(), stockholmLat, stockholmLon); !errors.Is(err, ErrNoCredentials) { + t.Errorf("err = %v, want ErrNoCredentials", err) + } + + var nilService *Service + if _, err := nilService.Buildings(context.Background(), stockholmLat, stockholmLon); !errors.Is(err, ErrDisabled) { + t.Errorf("err = %v, want ErrDisabled", err) + } +} + +func readInvocation(t *testing.T, path string) stubInvocation { + t.Helper() + raw, err := os.ReadFile(path) + if err != nil { + t.Fatalf("stub recorded nothing: %v", err) + } + var got stubInvocation + if err := json.Unmarshal(raw, &got); err != nil { + t.Fatal(err) + } + return got +} + // The module signals failure as JSON on stderr precisely so an operator sees a // cause rather than a traceback. func TestDeriveSurfacesTheModuleErrorMessage(t *testing.T) { @@ -283,7 +396,7 @@ func TestDeriveSurfacesTheModuleErrorMessage(t *testing.T) { `{"error":"Geotorget rejected the credentials","kind":"MissingCredentials"}`) s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t"}) - _, err := s.Derive(context.Background(), stockholmLat, stockholmLon) + _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "") if err == nil { t.Fatal("want an error") } @@ -298,7 +411,7 @@ func TestDeriveReportsNonJSONFailure(t *testing.T) { cmd := stubModule(t, "stderr", "Traceback (most recent call last):\n MemoryError\n") s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t"}) - _, err := s.Derive(context.Background(), stockholmLat, stockholmLon) + _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "") if err == nil || !strings.Contains(err.Error(), "roof model failed") { t.Errorf("err = %v, want a plain failure", err) } @@ -308,7 +421,7 @@ func TestDeriveRejectsUnknownSchemaVersion(t *testing.T) { cmd := stubModule(t, "stdout", `{"schema_version":99,"arrays":[]}`) s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t"}) - _, err := s.Derive(context.Background(), stockholmLat, stockholmLon) + _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "") if err == nil || !strings.Contains(err.Error(), "schema_version") { t.Errorf("err = %v, want a schema-version rejection", err) } @@ -318,7 +431,7 @@ func TestDeriveRejectsUnreadableOutput(t *testing.T) { cmd := stubModule(t, "stdout", "not-json-at-all") s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t"}) - _, err := s.Derive(context.Background(), stockholmLat, stockholmLon) + _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "") if err == nil || !strings.Contains(err.Error(), "unreadable") { t.Errorf("err = %v, want an unreadable-output error", err) } @@ -334,7 +447,7 @@ func TestDeriveIsTimeBoxed(t *testing.T) { }) start := time.Now() - _, err := s.Derive(context.Background(), stockholmLat, stockholmLon) + _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "") if err == nil || !strings.Contains(err.Error(), "timed out") { t.Errorf("err = %v, want a timeout", err) } diff --git a/roofmodel/ftw_roofmodel/__main__.py b/roofmodel/ftw_roofmodel/__main__.py index bdcb9c17d..7b04e6406 100644 --- a/roofmodel/ftw_roofmodel/__main__.py +++ b/roofmodel/ftw_roofmodel/__main__.py @@ -11,35 +11,64 @@ import json import sys -from .geotorget import Credentials, GeotorgetError -from .pipeline import RoofModelError, derive +from .buildings import DEFAULT_SEARCH_RADIUS_M, search_buildings +from .geotorget import Credentials, GeotorgetClient, GeotorgetError +from .pipeline import SCHEMA_VERSION, RoofModelError, derive def main(argv: list[str] | None = None) -> int: p = argparse.ArgumentParser(prog="ftw_roofmodel") + p.add_argument( + "--mode", + choices=("derive", "buildings"), + default="derive", + help="'buildings' lists footprints to pick from; 'derive' fits the roof", + ) p.add_argument("--lat", type=float, required=True) p.add_argument("--lon", type=float, required=True) + p.add_argument( + "--building-id", + default="", + help="footprint to clip the LiDAR to, from a --mode buildings run", + ) + p.add_argument("--search-radius-m", type=float, default=DEFAULT_SEARCH_RADIUS_M) p.add_argument("--username", default="", help="Geotorget username") p.add_argument("--token", default="", help="Geotorget token/password") p.add_argument("--radius-m", type=float, default=40.0) p.add_argument("--packing-factor", type=float, default=0.70) p.add_argument("--module-w-per-m2", type=float, default=200.0) args = p.parse_args(argv) + credentials = Credentials(args.username, args.token) try: - model = derive( - latitude=args.lat, - longitude=args.lon, - credentials=Credentials(args.username, args.token), - radius_m=args.radius_m, - packing_factor=args.packing_factor, - module_w_per_m2=args.module_w_per_m2, - ) + if args.mode == "buildings": + client = GeotorgetClient(credentials) + found = search_buildings( + client, + latitude=args.lat, + longitude=args.lon, + radius_m=args.search_radius_m, + ) + payload = { + "schema_version": SCHEMA_VERSION, + "site": {"latitude": args.lat, "longitude": args.lon}, + "buildings": [b.to_geojson() for b in found], + } + else: + payload = derive( + latitude=args.lat, + longitude=args.lon, + credentials=credentials, + radius_m=args.radius_m, + packing_factor=args.packing_factor, + module_w_per_m2=args.module_w_per_m2, + building_id=args.building_id or None, + ) except (GeotorgetError, RoofModelError) as exc: json.dump({"error": str(exc), "kind": type(exc).__name__}, sys.stderr) sys.stderr.write("\n") return 1 - json.dump(model, sys.stdout) + json.dump(payload, sys.stdout) sys.stdout.write("\n") return 0 diff --git a/roofmodel/ftw_roofmodel/buildings.py b/roofmodel/ftw_roofmodel/buildings.py new file mode 100644 index 000000000..a0bcc00ca --- /dev/null +++ b/roofmodel/ftw_roofmodel/buildings.py @@ -0,0 +1,302 @@ +"""Building footprints from Lantmaeteriet, and clipping LiDAR to one of them. + +Picking a building matters more than it sounds. Searching LiDAR by a radius +around a coordinate returns the neighbours' roofs, the garage and whatever trees +stand in the garden, and the segmenter has no way to know which returns belong +to the operator's own house. Worse, RANSAC fits *infinite* planes over the whole +tile: an azimuth-180 roof plane is z = f(y) with no x term, so a second building +sharing the ridge orientation falls inside its inlier band however far away it +is, and loses points to it. Measured on a synthetic pair, a detached garage +recovered 93% of its true area and split into two fragments when it lay on the +house's plane, against 100% and one face when it did not. + +Clipping to a chosen footprint removes that whole class of error: the returns +that reach the segmenter are the ones standing on the operator's building. + +Coordinate frames +----------------- +GeoJSON mandates WGS84, but Lantmaeteriet publishes this catalogue in +SWEREF 99 TM (EPSG:3006) and its STAC search takes a SWEREF bbox. Rather than +guess which one a given deployment returns, the frame is detected from the +magnitude of the numbers -- SWEREF eastings and northings are six and seven +figures, WGS84 degrees never are. +""" + +from __future__ import annotations + +import dataclasses +import math +from typing import Any, Iterable + +from . import sweref +from .geotorget import COLLECTION_BUILDINGS, GeotorgetClient, GeotorgetError, StacItem + +# How far around the site to look for candidate buildings. Wide enough to reach +# a house set back from its coordinate, narrow enough not to return a village. +DEFAULT_SEARCH_RADIUS_M = 150.0 + +# A footprint larger than this is a tile boundary or a whole city block, not a +# building someone is about to mount panels on. +MAX_FOOTPRINT_AREA_M2 = 20000.0 +# Below this it is a shed, a bin store or a digitising artefact. +MIN_FOOTPRINT_AREA_M2 = 8.0 + +# Roofs overhang their walls. Clipping exactly on the footprint would shave the +# eaves off every face, and the eaves are where the lowest roof returns are. +DEFAULT_EAVES_BUFFER_M = 1.0 + + +class BuildingLookupError(GeotorgetError): + """The building search succeeded but produced nothing usable.""" + + +@dataclasses.dataclass +class Building: + """One candidate building, ready to hand to a map.""" + + building_id: str + # Ring of (easting, northing) in SWEREF 99 TM -- the frame clipping happens + # in, since the LiDAR arrives in it too. + ring_sweref: list[tuple[float, float]] + area_m2: float + distance_m: float + properties: dict[str, Any] = dataclasses.field(default_factory=dict) + + def centroid_sweref(self) -> tuple[float, float]: + return _centroid(self.ring_sweref) + + def centroid_wgs84(self) -> tuple[float, float]: + e, n = self.centroid_sweref() + return sweref.sweref99tm_to_wgs84(e, n) + + def ring_wgs84(self) -> list[list[float]]: + """GeoJSON ring: [lon, lat] pairs, closed.""" + out = [] + for e, n in self.ring_sweref: + lat, lon = sweref.sweref99tm_to_wgs84(e, n) + out.append([round(lon, 7), round(lat, 7)]) + if out and out[0] != out[-1]: + out.append(out[0]) + return out + + def to_geojson(self) -> dict[str, Any]: + lat, lon = self.centroid_wgs84() + return { + "type": "Feature", + "id": self.building_id, + "geometry": {"type": "Polygon", "coordinates": [self.ring_wgs84()]}, + "properties": { + "building_id": self.building_id, + "area_m2": round(self.area_m2, 1), + "distance_m": round(self.distance_m, 1), + "latitude": round(lat, 6), + "longitude": round(lon, 6), + **self.properties, + }, + } + + +def _looks_like_sweref(ring: Iterable[Iterable[float]]) -> bool: + """True when a ring is projected metres rather than degrees. + + SWEREF 99 TM eastings run roughly 200 000-900 000 and northings 6 100 000- + 7 700 000. No WGS84 coordinate can reach either, so one look at the + magnitude settles which frame a ring is in. + """ + for point in ring: + pt = list(point) + if len(pt) < 2: + continue + if abs(pt[0]) > 180.0 or abs(pt[1]) > 90.0: + return True + return False + + +def _shoelace_area(ring: list[tuple[float, float]]) -> float: + """Planar polygon area in square metres. Ring must be projected.""" + n = len(ring) + if n < 3: + return 0.0 + total = 0.0 + for i in range(n): + x1, y1 = ring[i] + x2, y2 = ring[(i + 1) % n] + total += x1 * y2 - x2 * y1 + return abs(total) / 2.0 + + +def _centroid(ring: list[tuple[float, float]]) -> tuple[float, float]: + """Area centroid of a projected ring, falling back to the vertex mean.""" + n = len(ring) + if n == 0: + return (0.0, 0.0) + if n < 3: + return (sum(p[0] for p in ring) / n, sum(p[1] for p in ring) / n) + cx = cy = a = 0.0 + for i in range(n): + x1, y1 = ring[i] + x2, y2 = ring[(i + 1) % n] + cross = x1 * y2 - x2 * y1 + a += cross + cx += (x1 + x2) * cross + cy += (y1 + y2) * cross + if abs(a) < 1e-9: # degenerate (collinear) ring + return (sum(p[0] for p in ring) / n, sum(p[1] for p in ring) / n) + a *= 0.5 + return (cx / (6.0 * a), cy / (6.0 * a)) + + +def _rings_from_geometry(geometry: dict[str, Any]) -> list[list[tuple[float, float]]]: + """Outer rings of a GeoJSON Polygon or MultiPolygon, in their own frame.""" + if not isinstance(geometry, dict): + return [] + kind = geometry.get("type") + coords = geometry.get("coordinates") or [] + rings: list[list[tuple[float, float]]] = [] + if kind == "Polygon" and coords: + rings.append([(float(p[0]), float(p[1])) for p in coords[0] if len(p) >= 2]) + elif kind == "MultiPolygon": + for poly in coords: + if poly: + rings.append([(float(p[0]), float(p[1])) for p in poly[0] if len(p) >= 2]) + return [r for r in rings if len(r) >= 3] + + +def _to_sweref(ring: list[tuple[float, float]]) -> list[tuple[float, float]]: + if _looks_like_sweref(ring): + return ring + # GeoJSON order is [lon, lat]. + return [sweref.wgs84_to_sweref99tm(lat, lon) for lon, lat in ring] + + +def _features_from_item(item: StacItem) -> list[dict[str, Any]]: + """Every building-like feature an item carries. + + A STAC item may itself be the building, or it may be a tile whose asset + holds them. Inline geometry is preferred: it costs no download. + """ + geom = (item.raw or {}).get("geometry") + if geom: + return [{"geometry": geom, "properties": (item.raw or {}).get("properties") or {}, + "id": item.item_id}] + return [] + + +def buildings_from_features( + features: Iterable[dict[str, Any]], + *, + latitude: float, + longitude: float, + fallback_id: str = "building", +) -> list[Building]: + """Turn GeoJSON-ish features into ranked Building candidates.""" + site_e, site_n = sweref.wgs84_to_sweref99tm(latitude, longitude) + out: list[Building] = [] + for i, feat in enumerate(features): + for j, ring in enumerate(_rings_from_geometry(feat.get("geometry") or {})): + ring_sweref = _to_sweref(ring) + area = _shoelace_area(ring_sweref) + if area < MIN_FOOTPRINT_AREA_M2 or area > MAX_FOOTPRINT_AREA_M2: + continue + cx, cy = _centroid(ring_sweref) + props = dict(feat.get("properties") or {}) + bid = str(feat.get("id") or props.get("objektidentitet") or f"{fallback_id}-{i}") + if j: + bid = f"{bid}-{j}" + out.append(Building( + building_id=bid, + ring_sweref=ring_sweref, + area_m2=area, + distance_m=math.hypot(cx - site_e, cy - site_n), + properties={k: v for k, v in props.items() if isinstance(v, (str, int, float))}, + )) + out.sort(key=lambda b: b.distance_m) + return out + + +def search_buildings( + client: GeotorgetClient, + *, + latitude: float, + longitude: float, + radius_m: float = DEFAULT_SEARCH_RADIUS_M, + limit: int = 50, +) -> list[Building]: + """Building footprints near a site, nearest first.""" + south, west, north, east = sweref.metre_box_around(latitude, longitude, radius_m) + bbox = sweref.bbox_wgs84_to_sweref99tm(south, west, north, east) + items = client.search(COLLECTION_BUILDINGS, bbox, limit=limit) + features: list[dict[str, Any]] = [] + for item in items: + features.extend(_features_from_item(item)) + if not features: + raise BuildingLookupError( + "no building footprints were returned for this site. The Geotorget " + "account needs access to 'Byggnad Nedladdning, vektor', and the data " + "covers Sweden only." + ) + return buildings_from_features( + features, latitude=latitude, longitude=longitude + ) + + +def point_in_ring(x: float, y: float, ring: list[tuple[float, float]]) -> bool: + """Ray-casting point-in-polygon, in the ring's own projected frame.""" + inside = False + n = len(ring) + for i in range(n): + x1, y1 = ring[i] + x2, y2 = ring[(i + 1) % n] + if (y1 > y) != (y2 > y): + if y2 != y1 and x < x1 + (y - y1) * (x2 - x1) / (y2 - y1): + inside = not inside + return inside + + +def inflate_ring(ring: list[tuple[float, float]], metres: float) -> list[tuple[float, float]]: + """Push a ring outward from its centroid by roughly `metres`. + + Approximate on purpose: a true polygon offset needs a geometry library, and + this only has to catch the eaves. For the compact, roughly convex outlines + houses actually have, scaling about the centroid is within a few centimetres + of a proper offset; for a long thin wing it over-buffers the short sides, + which costs a little neighbouring ground rather than losing roof. + """ + if metres <= 0 or len(ring) < 3: + return ring + cx, cy = _centroid(ring) + out = [] + for x, y in ring: + dx, dy = x - cx, y - cy + d = math.hypot(dx, dy) + if d < 1e-9: + out.append((x, y)) + continue + scale = (d + metres) / d + out.append((cx + dx * scale, cy + dy * scale)) + return out + + +def clip_to_footprint(points, ring: list[tuple[float, float]], + *, buffer_m: float = DEFAULT_EAVES_BUFFER_M): + """Keep only the returns standing on one building. + + `points` is (N, 3) in SWEREF 99 TM metres, the frame the LiDAR arrives in. + """ + import numpy as np + + pts = np.asarray(points, dtype=float) + if len(pts) == 0 or len(ring) < 3: + return pts + outline = inflate_ring(ring, buffer_m) + + # Cheap rejection first: most of a tile is nowhere near the building. + xs = [p[0] for p in outline] + ys = [p[1] for p in outline] + box = ( + (pts[:, 0] >= min(xs)) & (pts[:, 0] <= max(xs)) + & (pts[:, 1] >= min(ys)) & (pts[:, 1] <= max(ys)) + ) + candidates = np.nonzero(box)[0] + keep = [i for i in candidates if point_in_ring(pts[i, 0], pts[i, 1], outline)] + return pts[keep] diff --git a/roofmodel/ftw_roofmodel/pipeline.py b/roofmodel/ftw_roofmodel/pipeline.py index 8431ecd97..ae0a6e00c 100644 --- a/roofmodel/ftw_roofmodel/pipeline.py +++ b/roofmodel/ftw_roofmodel/pipeline.py @@ -18,6 +18,7 @@ from typing import Any from . import sweref +from .buildings import Building, clip_to_footprint, search_buildings from .geotorget import ( COLLECTION_LIDAR, Credentials, @@ -46,6 +47,10 @@ # Lantmaeteriet's Laserdata Skog is specified at 1-2 points/m2. NOMINAL_POINT_DENSITY = 1.5 +# Below this, a clipped footprint cannot support a plane fit -- segment_roof +# needs 40 points for a single face, and a roof has at least two. +MIN_POINTS_AFTER_CLIP = 80 + class RoofModelError(RuntimeError): """Derivation failed.""" @@ -155,12 +160,31 @@ def derive( radius_m: float = DEFAULT_RADIUS_M, packing_factor: float = DEFAULT_PACKING_FACTOR, module_w_per_m2: float = DEFAULT_MODULE_W_PER_M2, + building_id: str | None = None, now: dt.datetime | None = None, ) -> dict[str, Any]: - """Derive a roof model for one site and return it as a JSON-ready dict.""" + """Derive a roof model for one site and return it as a JSON-ready dict. + + Pass `building_id` -- one the operator picked from `search_buildings` -- to + clip the LiDAR to that footprint before segmenting. Without it the whole + radius is segmented, which will happily return the neighbour's roof and lets + coplanar buildings steal each other's points; see buildings.py. + """ if client is None: client = GeotorgetClient(credentials) + chosen: Building | None = None + if building_id: + candidates = search_buildings( + client, latitude=latitude, longitude=longitude, radius_m=radius_m + ) + chosen = next((b for b in candidates if b.building_id == building_id), None) + if chosen is None: + raise RoofModelError( + f"building {building_id!r} was not found near this site; it may " + "have been picked against a different coordinate" + ) + south, west, north, east = sweref.metre_box_around(latitude, longitude, radius_m) bbox = sweref.bbox_wgs84_to_sweref99tm(south, west, north, east) @@ -184,6 +208,16 @@ def derive( if points is None or len(points) == 0: raise RoofModelError("LiDAR tiles carried no readable point data") + total_returns = len(points) + if chosen is not None: + points = clip_to_footprint(points, chosen.ring_sweref) + if len(points) < MIN_POINTS_AFTER_CLIP: + raise RoofModelError( + f"only {len(points)} LiDAR returns fall on building " + f"{chosen.building_id!r}. The footprint and the point cloud may " + "be from different years, or the building is newer than the scan." + ) + planes = segment_roof(points, point_density=NOMINAL_POINT_DENSITY) arrays = planes_to_arrays( planes, packing_factor=packing_factor, module_w_per_m2=module_w_per_m2 @@ -200,6 +234,13 @@ def derive( "item_count": len(lidar_items), "dataset_datetime": captured.isoformat() if captured else None, }, + "building": { + "building_id": chosen.building_id, + "area_m2": round(chosen.area_m2, 1), + "footprint": chosen.to_geojson()["geometry"], + "returns_used": len(points), + "returns_in_radius": total_returns, + } if chosen is not None else None, "arrays": [a.to_json() for a in arrays], "planes_found": len(planes), "captured_at_ms": int(captured.timestamp() * 1000) if captured else None, diff --git a/roofmodel/tests/test_buildings.py b/roofmodel/tests/test_buildings.py new file mode 100644 index 000000000..27205ab9f --- /dev/null +++ b/roofmodel/tests/test_buildings.py @@ -0,0 +1,206 @@ +"""Building lookup, frame detection and footprint clipping.""" + +from __future__ import annotations + +import math + +import numpy as np +import pytest + +from ftw_roofmodel import sweref +from ftw_roofmodel.buildings import ( + Building, + BuildingLookupError, + buildings_from_features, + clip_to_footprint, + inflate_ring, + point_in_ring, + search_buildings, +) +from ftw_roofmodel.geotorget import COLLECTION_BUILDINGS, Credentials, GeotorgetClient + +STOCKHOLM = (59.33, 18.07) + + +def square_ring(cx, cy, side): + h = side / 2.0 + return [(cx - h, cy - h), (cx + h, cy - h), (cx + h, cy + h), (cx - h, cy + h)] + + +class FakeResponse: + def __init__(self, payload, status_code=200): + self._payload = payload + self.status_code = status_code + + def json(self): + return self._payload + + +class FakeSession: + """Records what was asked for and replays a canned STAC response.""" + + def __init__(self, payload): + self.payload = payload + self.posts = [] + + def post(self, url, json=None, timeout=None): + self.posts.append((url, json)) + return FakeResponse(self.payload) + + +def stac_feature(ring, feature_id="bldg-1", **props): + return { + "id": feature_id, + "collection": COLLECTION_BUILDINGS, + "geometry": {"type": "Polygon", "coordinates": [[list(p) for p in ring] + [list(ring[0])]]}, + "properties": props, + "assets": {}, + } + + +def test_area_and_centroid_of_a_known_square(): + ring = square_ring(674000.0, 6580000.0, 10.0) + [b] = buildings_from_features( + [{"geometry": {"type": "Polygon", "coordinates": [ring]}, "id": "sq"}], + latitude=STOCKHOLM[0], longitude=STOCKHOLM[1], + ) + assert b.area_m2 == pytest.approx(100.0) + cx, cy = b.centroid_sweref() + assert (cx, cy) == pytest.approx((674000.0, 6580000.0)) + + +def test_wgs84_rings_are_projected_before_measuring(): + """A ring in degrees must be recognised and converted, not measured raw.""" + lat, lon = STOCKHOLM + e, n = sweref.wgs84_to_sweref99tm(lat, lon) + ring_sweref = square_ring(e, n, 12.0) + ring_wgs84 = [] + for x, y in ring_sweref: + blat, blon = sweref.sweref99tm_to_wgs84(x, y) + ring_wgs84.append([blon, blat]) # GeoJSON is [lon, lat] + + [b] = buildings_from_features( + [{"geometry": {"type": "Polygon", "coordinates": [ring_wgs84]}, "id": "deg"}], + latitude=lat, longitude=lon, + ) + # 12 m square, recovered through a full round trip through degrees. + assert b.area_m2 == pytest.approx(144.0, abs=0.5) + + +def test_tiles_and_slivers_are_not_offered_as_buildings(): + lat, lon = STOCKHOLM + e, n = sweref.wgs84_to_sweref99tm(lat, lon) + feats = [ + {"geometry": {"type": "Polygon", "coordinates": [square_ring(e, n, 2500.0)]}, "id": "tile"}, + {"geometry": {"type": "Polygon", "coordinates": [square_ring(e, n, 1.0)]}, "id": "sliver"}, + {"geometry": {"type": "Polygon", "coordinates": [square_ring(e, n, 11.0)]}, "id": "house"}, + ] + got = buildings_from_features(feats, latitude=lat, longitude=lon) + assert [b.building_id for b in got] == ["house"] + + +def test_candidates_come_back_nearest_first(): + lat, lon = STOCKHOLM + e, n = sweref.wgs84_to_sweref99tm(lat, lon) + feats = [ + {"geometry": {"type": "Polygon", "coordinates": [square_ring(e + 60, n, 10.0)]}, "id": "far"}, + {"geometry": {"type": "Polygon", "coordinates": [square_ring(e + 5, n, 10.0)]}, "id": "near"}, + {"geometry": {"type": "Polygon", "coordinates": [square_ring(e + 25, n, 10.0)]}, "id": "mid"}, + ] + got = buildings_from_features(feats, latitude=lat, longitude=lon) + assert [b.building_id for b in got] == ["near", "mid", "far"] + assert got[0].distance_m < got[1].distance_m < got[2].distance_m + + +def test_multipolygon_yields_one_candidate_per_part(): + lat, lon = STOCKHOLM + e, n = sweref.wgs84_to_sweref99tm(lat, lon) + feat = { + "id": "pair", + "geometry": { + "type": "MultiPolygon", + "coordinates": [[square_ring(e, n, 10.0)], [square_ring(e + 30, n, 12.0)]], + }, + } + got = buildings_from_features([feat], latitude=lat, longitude=lon) + assert len(got) == 2 + assert len({b.building_id for b in got}) == 2, "parts must not share an id" + + +def test_search_queries_the_building_collection_and_maps_results(): + lat, lon = STOCKHOLM + e, n = sweref.wgs84_to_sweref99tm(lat, lon) + session = FakeSession({"features": [stac_feature(square_ring(e, n, 10.0), "b1")]}) + client = GeotorgetClient(Credentials("u", "t"), session=session) + + got = search_buildings(client, latitude=lat, longitude=lon) + + assert [b.building_id for b in got] == ["b1"] + (_, body), = session.posts + assert body["collections"] == [COLLECTION_BUILDINGS] + # The bbox must be the SWEREF box around the site, not raw degrees. + assert body["bbox"][0] > 1000 + + +def test_search_says_what_to_do_when_nothing_comes_back(): + client = GeotorgetClient(Credentials("u", "t"), session=FakeSession({"features": []})) + with pytest.raises(BuildingLookupError) as exc: + search_buildings(client, latitude=STOCKHOLM[0], longitude=STOCKHOLM[1]) + assert "Byggnad" in str(exc.value) + + +def test_point_in_ring_handles_edges_and_outside(): + ring = square_ring(0.0, 0.0, 10.0) + assert point_in_ring(0.0, 0.0, ring) + assert point_in_ring(4.9, 4.9, ring) + assert not point_in_ring(5.1, 0.0, ring) + assert not point_in_ring(0.0, 99.0, ring) + + +def test_inflate_ring_grows_the_outline(): + ring = square_ring(0.0, 0.0, 10.0) + bigger = inflate_ring(ring, 1.0) + # Corners sit at radius 7.07; pushing 1 m out puts them at 8.07. + assert math.hypot(*bigger[0]) == pytest.approx(math.hypot(*ring[0]) + 1.0) + assert all(point_in_ring(x, y, bigger) for x, y in ring) + + +def test_clip_keeps_the_building_and_drops_the_neighbours(): + rng = np.random.default_rng(3) + mine = np.column_stack([ + rng.uniform(-4, 4, 400), rng.uniform(-4, 4, 400), rng.uniform(0, 4, 400)]) + theirs = np.column_stack([ + rng.uniform(46, 54, 400), rng.uniform(-4, 4, 400), rng.uniform(0, 4, 400)]) + cloud = np.vstack([mine, theirs]) + + kept = clip_to_footprint(cloud, square_ring(0.0, 0.0, 10.0), buffer_m=0.0) + + assert len(kept) == len(mine) + assert kept[:, 0].max() < 10.0 + + +def test_clip_keeps_the_eaves(): + """Roof returns overhang the wall line; clipping exactly would shave them.""" + ring = square_ring(0.0, 0.0, 10.0) + eaves = np.array([[5.4, 0.0, 3.0], [-5.4, 0.0, 3.0], [0.0, 5.4, 3.0]]) + + assert len(clip_to_footprint(eaves, ring, buffer_m=0.0)) == 0 + assert len(clip_to_footprint(eaves, ring, buffer_m=1.0)) == 3 + + +def test_clip_of_an_empty_cloud_is_empty_not_an_error(): + assert len(clip_to_footprint(np.empty((0, 3)), square_ring(0, 0, 10))) == 0 + + +def test_geojson_feature_is_wgs84_and_closed(): + lat, lon = STOCKHOLM + e, n = sweref.wgs84_to_sweref99tm(lat, lon) + b = Building("b1", square_ring(e, n, 10.0), 100.0, 0.0) + feat = b.to_geojson() + + ring = feat["geometry"]["coordinates"][0] + assert ring[0] == ring[-1], "GeoJSON rings must close" + for x, y in ring: + assert -180 <= x <= 180 and -90 <= y <= 90 + assert feat["properties"]["latitude"] == pytest.approx(lat, abs=1e-3) + assert feat["properties"]["longitude"] == pytest.approx(lon, abs=1e-3) diff --git a/roofmodel/tests/test_derive_footprint.py b/roofmodel/tests/test_derive_footprint.py new file mode 100644 index 000000000..3932e7ef8 --- /dev/null +++ b/roofmodel/tests/test_derive_footprint.py @@ -0,0 +1,149 @@ +"""Deriving against a picked building footprint.""" + +from __future__ import annotations + +import math + +import numpy as np +import pytest + +from ftw_roofmodel import pipeline, sweref +from ftw_roofmodel.buildings import clip_to_footprint +from ftw_roofmodel.geotorget import Credentials, StacItem +from ftw_roofmodel.pipeline import RoofModelError, derive +from ftw_roofmodel.segment import segment_roof + +STOCKHOLM = (59.33, 18.07) + + +def roof_face(tilt, azimuth, w, d, origin, density=8, noise=0.04, seed=1): + """Sample a tilted rectangle. Written as the inverse of what segment.py + computes, so recovering the tilt is a real test rather than a tautology.""" + rng = np.random.default_rng(seed) + n = int(w * d * density) + x = rng.uniform(0, w, n) + y = rng.uniform(0, d, n) + az = math.radians(azimuth) + s = math.tan(math.radians(tilt)) + z = -(x * math.sin(az) + y * math.cos(az)) * s + rng.normal(0, noise, n) + return np.column_stack([x + origin[0], y + origin[1], z + origin[2]]) + + +def ring(cx, cy, w, d): + return [(cx, cy), (cx + w, cy), (cx + w, cy + d), (cx, cy + d)] + + +class FakeClient: + """Stands in for Geotorget: canned buildings, canned LiDAR.""" + + def __init__(self, buildings_payload, points): + self._buildings = buildings_payload + self._points = points + self.searched = [] + + def search(self, collection, bbox, limit=20): + self.searched.append(collection) + if collection == "byggnad-nedladdning-vektor": + return [StacItem(f["id"], collection, {}, None, raw=f) for f in self._buildings] + return [StacItem("lidar-1", collection, {"data": "http://x/tile.laz"}, None, raw={})] + + def download(self, url): + return b"laz-bytes" + + +@pytest.fixture +def scene(monkeypatch): + """A house and a neighbour that share a ridge orientation. + + The neighbour is the point: an azimuth-180 plane is z = f(y) with no x term, + so it extends across the whole tile and the two buildings compete for each + other's returns unless the cloud is clipped first. + """ + e, n = sweref.wgs84_to_sweref99tm(*STOCKHOLM) + mine = np.vstack([ + roof_face(35, 180, 12, 6, (e, n, 0), seed=2), + roof_face(35, 0, 12, 6, (e, n + 6, 4.2), seed=3), + ]) + neighbour = np.vstack([ + roof_face(35, 180, 12, 6, (e + 40, n, 0), seed=4), + roof_face(35, 0, 12, 6, (e + 40, n + 6, 4.2), seed=5), + ]) + cloud = np.vstack([mine, neighbour]) + + buildings = [ + {"id": "mine", "geometry": {"type": "Polygon", + "coordinates": [[list(p) for p in ring(e, n, 12, 12)]]}, "properties": {}}, + {"id": "neighbour", "geometry": {"type": "Polygon", + "coordinates": [[list(p) for p in ring(e + 40, n, 12, 12)]]}, "properties": {}}, + ] + monkeypatch.setattr(pipeline, "load_points", lambda data: cloud) + return FakeClient(buildings, cloud), cloud, (e, n) + + +def test_derive_clips_to_the_picked_building(scene): + client, cloud, _ = scene + model = derive( + latitude=STOCKHOLM[0], longitude=STOCKHOLM[1], + credentials=Credentials("u", "t"), client=client, building_id="mine", + ) + + b = model["building"] + assert b["building_id"] == "mine" + assert b["returns_in_radius"] == len(cloud) + # Roughly half the tile is the neighbour's, and it must be gone. + assert b["returns_used"] < b["returns_in_radius"] * 0.6 + assert model["arrays"], "a clipped house still has a south roof" + + +def test_clipping_recovers_the_true_area_the_neighbour_would_have_stolen(scene): + """The measurable payoff: coplanar buildings stop eating each other.""" + _, cloud, (e, n) = scene + truth = 12 * 6 / math.cos(math.radians(35)) + + whole_tile = [p for p in segment_roof(cloud, point_density=8.0) + if abs(p.azimuth_deg - 180) < 5] + clipped = [p for p in segment_roof(clip_to_footprint(cloud, ring(e, n, 12, 12)), + point_density=8.0) + if abs(p.azimuth_deg - 180) < 5] + + assert len(clipped) == 1, "one building has one south face" + assert clipped[0].area_m2 == pytest.approx(truth, rel=0.10) + # Unclipped, the two south faces are coplanar and merge into one oversized + # segment, so the site's own roof cannot be measured at all. + assert len(whole_tile) != 1 or whole_tile[0].area_m2 > truth * 1.5 + + +def test_derive_without_a_building_id_does_not_search_for_buildings(scene): + client, _, _ = scene + model = derive( + latitude=STOCKHOLM[0], longitude=STOCKHOLM[1], + credentials=Credentials("u", "t"), client=client, + ) + assert "byggnad-nedladdning-vektor" not in client.searched + assert model["building"] is None + + +def test_derive_rejects_a_building_id_that_is_not_there(scene): + client, _, _ = scene + with pytest.raises(RoofModelError) as exc: + derive( + latitude=STOCKHOLM[0], longitude=STOCKHOLM[1], + credentials=Credentials("u", "t"), client=client, building_id="not-a-building", + ) + assert "not found" in str(exc.value) + + +def test_derive_explains_a_footprint_with_no_returns_on_it(monkeypatch, scene): + """A building newer than the scan is a real case and needs a real message.""" + client, _, (e, n) = scene + empty = np.empty((0, 3)) + monkeypatch.setattr(pipeline, "load_points", lambda data: np.vstack([ + roof_face(35, 180, 12, 6, (e + 400, n, 0), seed=9)])) + + with pytest.raises(RoofModelError) as exc: + derive( + latitude=STOCKHOLM[0], longitude=STOCKHOLM[1], + credentials=Credentials("u", "t"), client=client, building_id="mine", + ) + msg = str(exc.value) + assert "fall on building" in msg and "newer than the scan" in msg diff --git a/web/index.html b/web/index.html index 9b629af8f..6f5782e1c 100644 --- a/web/index.html +++ b/web/index.html @@ -995,7 +995,7 @@

Price bars (top of the chart)

- + diff --git a/web/settings/tabs/weather.js b/web/settings/tabs/weather.js index 86cecf2d5..58484a7f7 100644 --- a/web/settings/tabs/weather.js +++ b/web/settings/tabs/weather.js @@ -285,6 +285,233 @@ el.textContent = arraysSummary(n); } + // ---- Roof geometry from Lantmäteriet ------------------------------------- + // + // Typing a tilt and an azimuth means measuring your own roof, and most people + // guess. Sweden publishes the two datasets needed to do it properly, so this + // picks your building off a map and reads the slants out of the LiDAR. + // + // Both datasets are free but sit behind a Geotorget account, so the operator + // brings their own credentials. Everything degrades to the numeric fields + // above when the module, the credentials or the coverage is missing. + + var roofState = { features: [], selectedId: null }; + + function roofFieldset(ctx) { + var field = ctx.field, help = ctx.help, config = ctx.config; + if (!config.roofmodel) config.roofmodel = {}; + var stored = config.roofmodel.has_geotorget_token; + return '
Roof geometry from Lantmäteriet ' + help( + 'Optional and Sweden-only. Reads the tilt and azimuth of each roof face from ' + + 'Lantmäteriet\'s laser scanning data (Laserdata Skog) for a building you pick on ' + + 'the map, and fills in the PV arrays above. Needs a free Geotorget account with ' + + 'access to "Byggnad Nedladdning, vektor" and "Laserdata Nedladdning, Skog".') + + '' + + '' + + '
' + + field("Geotorget username", "roofmodel.geotorget_username", "text", "") + + '
' + + field(stored ? "Geotorget token (stored — type to replace)" : "Geotorget token", + "roofmodel.geotorget_token", "password", "") + + '
' + + '
' + + '' + + '' + + '
' + + '
' + + '
' + + '

' + + 'Data © Lantmäteriet (CC BY 4.0). Derived values are a starting point — check them ' + + 'against your installation before relying on the forecast.' + + '

' + + '
'; + } + + function roofSay(html, tone) { + var el = document.getElementById("roof-status"); + if (!el) return; + el.style.color = tone === "bad" ? "var(--warn, #f59e0b)" : "var(--text-dim)"; + el.innerHTML = html; + } + + function findBuildings(ctx) { + var w = (ctx.config && ctx.config.weather) || {}; + var q = ""; + if (w.latitude != null && w.longitude != null) { + q = "?lat=" + encodeURIComponent(w.latitude) + "&lon=" + encodeURIComponent(w.longitude); + } + roofSay("Searching Lantmäteriet for buildings…"); + return fetch("/api/roofmodel/buildings" + q) + .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); }) + .then(function (res) { + var d = res.d || {}; + if (d.enabled === false) { + roofSay("Roof derivation is off. Tick Enable roof derivation, add your " + + "Geotorget credentials and save, then try again.", "bad"); + return; + } + if (!res.ok || d.error) { + roofSay(ctx.escHtml(d.error || "the building search failed"), "bad"); + return; + } + roofState.features = d.buildings || []; + roofState.selectedId = null; + if (!roofState.features.length) { + roofSay("No buildings found here. Move the marker onto your roof and try again.", "bad"); + return; + } + roofSay("Found " + roofState.features.length + + " building(s). Pick yours on the map or in the list."); + drawBuildings(); + renderBuildingList(ctx); + }) + .catch(function (e) { roofSay(ctx.escHtml(String(e && e.message || e)), "bad"); }); + } + + function featureCollection() { + return { + type: "FeatureCollection", + features: roofState.features.map(function (f) { + var p = Object.assign({}, f.properties || {}); + p.selected = (f.id || p.building_id) === roofState.selectedId; + return { type: "Feature", id: f.id, geometry: f.geometry, properties: p }; + }), + }; + } + + // MapLibre paints with concrete colours and cannot read var(), the same + // problem the canvas charts have. Resolve the theme tokens through a hidden + // probe that inherits :root, exactly as app.js's cssColor does. Resolved once + // per layer creation, so a theme toggle mid-pick keeps the old hue until the + // tab is reopened — the footprints stay legible either way. + var _probe = null; + function themeColor(name, fallback) { + if (!_probe) { + _probe = document.createElement("span"); + _probe.style.cssText = "position:absolute;visibility:hidden;pointer-events:none"; + document.body.appendChild(_probe); + } + _probe.style.color = "var(" + name + ", " + fallback + ")"; + return getComputedStyle(_probe).color || fallback; + } + + function drawBuildings() { + var map = window._weatherMap; + if (!map || !map.isStyleLoaded || !map.isStyleLoaded()) return; + var data = featureCollection(); + var src = map.getSource("roof-buildings"); + if (src) { src.setData(data); return; } + var candidate = themeColor("--cyan", "#38bdf8"); + var picked = themeColor("--accent-e", "#f5b942"); + map.addSource("roof-buildings", { type: "geojson", data: data }); + map.addLayer({ + id: "roof-buildings-fill", type: "fill", source: "roof-buildings", + paint: { + "fill-color": ["case", ["get", "selected"], picked, candidate], + "fill-opacity": ["case", ["get", "selected"], 0.55, 0.25], + }, + }); + map.addLayer({ + id: "roof-buildings-line", type: "line", source: "roof-buildings", + paint: { + "line-color": ["case", ["get", "selected"], picked, candidate], + "line-width": ["case", ["get", "selected"], 2.5, 1], + }, + }); + map.on("click", "roof-buildings-fill", function (e) { + if (!e.features || !e.features.length) return; + var p = e.features[0].properties || {}; + selectBuilding(p.building_id || e.features[0].id); + }); + map.on("mouseenter", "roof-buildings-fill", function () { + map.getCanvas().style.cursor = "pointer"; + }); + map.on("mouseleave", "roof-buildings-fill", function () { + map.getCanvas().style.cursor = ""; + }); + } + + function selectBuilding(id) { + roofState.selectedId = id; + drawBuildings(); + var list = document.getElementById("roof-buildings"); + if (list) { + Array.prototype.forEach.call(list.querySelectorAll("[data-building]"), function (el) { + var on = el.getAttribute("data-building") === id; + el.style.borderColor = on ? "var(--accent-e)" : "var(--line)"; + el.style.background = on ? "var(--ink-sunken)" : "transparent"; + }); + } + var derive = document.getElementById("roof-derive"); + if (derive) derive.disabled = !id; + } + + // Also rendered as a list, so the picker still works where the map does not + // (no WebGL, blocked CDN). + function renderBuildingList(ctx) { + var host = document.getElementById("roof-buildings"); + if (!host) return; + var esc = ctx.escHtml; + host.innerHTML = roofState.features.slice(0, 12).map(function (f) { + var p = f.properties || {}; + var id = f.id || p.building_id; + return ''; + }).join(""); + Array.prototype.forEach.call(host.querySelectorAll("[data-building]"), function (el) { + el.addEventListener("click", function () { + selectBuilding(el.getAttribute("data-building")); + }); + }); + } + + function deriveRoof(ctx) { + if (!roofState.selectedId) return; + roofSay("Reading the laser scan and fitting roof planes… this can take a minute."); + fetch("/api/roofmodel/derive", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ building_id: roofState.selectedId }), + }) + .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); }) + .then(function (res) { + var d = res.d || {}; + if (!res.ok || d.error || d.enabled === false) { + roofSay(ctx.escHtml(d.error || "the derive failed"), "bad"); + return; + } + var arrays = d.proposed_arrays || []; + if (!arrays.length) { + roofSay("No roof faces worth mounting panels on were found on that building. " + + "North-facing and very small faces are dropped.", "bad"); + return; + } + // Fill the form rather than saving: the operator sees the numbers and + // presses Save, so the panel config never changes behind their back. + ctx.config.weather.pv_arrays = arrays.map(function (a) { + return { + name: a.name || "", kwp: a.kwp, + tilt_deg: a.tilt_deg, azimuth_deg: a.azimuth_deg, + }; + }); + renderPVArrays(ctx); + var m = d.model || {}; + var when = m.captured_at_ms + ? new Date(m.captured_at_ms).toISOString().slice(0, 10) + : "date unknown"; + var shade = m.shading && m.shading.evaluated ? ", shading evaluated" : ""; + roofSay("Filled in " + arrays.length + " array(s) from " + m.planes_found + + " roof plane(s). Laser data from " + ctx.escHtml(when) + shade + + ". Review them and press Save."); + }) + .catch(function (e) { roofSay(ctx.escHtml(String(e && e.message || e)), "bad"); }); + } + function initWeatherMap(ctx) { var container = document.getElementById("weather-map"); if (!container) return; @@ -327,7 +554,8 @@ 'Tilt: 0° = flat roof, 35° = typical pitched roof, 90° = wall. Azimuth: 0 = N, 90 = E, 180 = S, 270 = W. ' + 'Rated (W) is watts, same unit as PV rated.' + '

' + - ''; + '' + + roofFieldset(ctx); }, after: function (ctx) { initWeatherMap(ctx); @@ -343,6 +571,11 @@ renderPVArrays(ctx); refreshArraysSummary(ctx.config); }); + roofState = { features: [], selectedId: null }; + var findBtn = document.getElementById("roof-find"); + if (findBtn) findBtn.addEventListener("click", function () { findBuildings(ctx); }); + var deriveBtn = document.getElementById("roof-derive"); + if (deriveBtn) deriveBtn.addEventListener("click", function () { deriveRoof(ctx); }); }, }; From 2585d57ce3004be9b660368ea948be6ce00bba0f Mon Sep 17 00:00:00 2001 From: Claude Opus 5 Date: Sat, 1 Aug 2026 10:54:28 +0200 Subject: [PATCH 10/26] feat(roofmodel): read Lantmateriet in the formats it publishes Both roof-geometry products are STAC APIs behind one Geotorget account, and they differ only in what their items point at: Byggnad Nedladdning, vektor delivers GeoPackage, Laserdata Nedladdning, Skog delivers LAZ organised as COPC. Assets are now selected by declared media type rather than by guessing at asset key names, so a catalogue that calls its asset "punktmoln" instead of "data" keeps working, and a thumbnail is never handed back as a point cloud. Building footprints are read straight out of the GeoPackage with the standard library. A GeoPackage is a SQLite database holding geometry as WKB, both published formats with fixed layouts, so this costs about a hundred lines against a GDAL dependency that will not install on a Pi without a compiler. Previously only inline STAC geometry was handled and the asset-backed case -- the normal one -- returned nothing at all. Because COPC indexes points into an octree, picking a building now also makes the download small: only the octree nodes covering that footprint are range-requested, instead of a 2.5 km tile running to hundreds of megabytes. Plain .laz assets, hosts that ignore Range, and builds of laspy without COPC support all fall back to reading the tile whole -- slower, same answer -- and the model records which path ran as source.fetch, since that also decides what returns_in_radius is counted over. Also corrects the coplanar-absorption measurement quoted in buildings.py and docs/roof-geometry.md. A fresh run does not reproduce the 93%/two-fragment figures: one pass over a house and a garage 40 m apart consumes all 576 of the house's south-face returns and all 256 of the garage's as one surface, and gives identical output at every separation from 3 m to 40 m -- which is the signature of a global infinite-plane fit rather than a proximity effect. Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com> Signed-off-by: Hugo Karlsson <48095810+HuggeK@users.noreply.github.com> --- .changeset/roofmodel-stac-formats.md | 26 +++ docs/roof-geometry.md | 48 +++-- go/internal/roofmodel/roofmodel.go | 5 + go/internal/roofmodel/roofmodel_test.go | 24 +++ roofmodel/ftw_roofmodel/buildings.py | 70 +++++-- roofmodel/ftw_roofmodel/geopackage.py | 228 +++++++++++++++++++++++ roofmodel/ftw_roofmodel/geotorget.py | 104 ++++++++++- roofmodel/ftw_roofmodel/pipeline.py | 79 +++++--- roofmodel/ftw_roofmodel/pointcloud.py | 226 +++++++++++++++++++++++ roofmodel/tests/test_assets.py | 212 +++++++++++++++++++++ roofmodel/tests/test_geopackage.py | 201 ++++++++++++++++++++ roofmodel/tests/test_source_formats.py | 235 ++++++++++++++++++++++++ 12 files changed, 1402 insertions(+), 56 deletions(-) create mode 100644 .changeset/roofmodel-stac-formats.md create mode 100644 roofmodel/ftw_roofmodel/geopackage.py create mode 100644 roofmodel/ftw_roofmodel/pointcloud.py create mode 100644 roofmodel/tests/test_assets.py create mode 100644 roofmodel/tests/test_geopackage.py create mode 100644 roofmodel/tests/test_source_formats.py diff --git a/.changeset/roofmodel-stac-formats.md b/.changeset/roofmodel-stac-formats.md new file mode 100644 index 000000000..874e48c02 --- /dev/null +++ b/.changeset/roofmodel-stac-formats.md @@ -0,0 +1,26 @@ +--- +"ftw": minor +--- + +Read Lantmäteriet's data in the formats it is actually published in. + +Both roof-geometry products are STAC APIs behind one Geotorget account, and they +differ only in what their items point at: *Byggnad Nedladdning, vektor* delivers +**GeoPackage**, *Laserdata Nedladdning, Skog* delivers **LAZ organised as COPC** +(Cloud Optimized Point Cloud). Assets are now chosen by their declared media +type instead of by guessing at asset names, so a catalogue that calls its asset +`punktmoln` rather than `data` still works, and a thumbnail is never mistaken +for a point cloud. + +Building footprints are read straight out of the GeoPackage with the standard +library — a GeoPackage is a SQLite database holding geometry as WKB, both +published formats with fixed layouts, so this needs no GDAL and installs on a Pi +unchanged. Previously only inline STAC geometry was handled, which meant the +normal asset-backed case returned nothing at all. + +Because COPC indexes its points into an octree, picking a building now also +makes the download small: FTW range-requests only the octree nodes covering that +footprint instead of pulling a 2.5 km tile that runs to hundreds of megabytes. +Plain `.laz` assets, servers that ignore `Range`, and builds of laspy without +COPC support all fall back to reading the tile whole — slower, same answer. The +derived model records which path ran as `source.fetch`. diff --git a/docs/roof-geometry.md b/docs/roof-geometry.md index ad10ac9cc..f6d90d10a 100644 --- a/docs/roof-geometry.md +++ b/docs/roof-geometry.md @@ -14,10 +14,15 @@ A free [Geotorget](https://geotorget.lantmateriet.se) account with access ordered to two products. Both are open data under CC BY 4.0; the account exists so Lantmäteriet can see who is downloading, not to charge you. -| Product | What FTW uses it for | -|---|---| -| [Byggnad Nedladdning, vektor](https://geotorget.lantmateriet.se/geodataprodukter/byggnad-nedladdning-vektor-api) | Building footprints, so you can point at your house | -| [Laserdata Nedladdning, Skog](https://geotorget.lantmateriet.se/geodataprodukter/laserdata-nedladdning-skog-api) | The laser scan the roof planes are fitted to | +| Product | Delivered as | What FTW uses it for | +|---|---|---| +| [Byggnad Nedladdning, vektor](https://geotorget.lantmateriet.se/geodataprodukter/byggnad-nedladdning-vektor-api) | STAC → **GeoPackage** | Building footprints, so you can point at your house | +| [Laserdata Nedladdning, Skog](https://geotorget.lantmateriet.se/geodataprodukter/laserdata-nedladdning-skog-api) | STAC → **LAZ as COPC** | The laser scan the roof planes are fitted to | + +Both are STAC APIs behind the same account, so one set of credentials covers +both and FTW searches them the same way. They differ only in what the items +point at, and FTW picks the right asset by its declared media type rather than +by its name — a catalogue that renames `data` to `punktmoln` keeps working. Ordering access is not instant — Lantmäteriet approves it — so do it before you plan to use this. @@ -30,7 +35,22 @@ pip install -e roofmodel[geo] The `geo` extra pulls the LAZ reader. Without it everything except reading the point cloud works, which is enough to run the tests but not enough to derive a -real roof. +real roof. GeoPackage needs nothing extra: it is a SQLite file, and FTW reads it +with the standard library. + +### Why picking a building also makes it fast + +COPC — Cloud Optimized Point Cloud — is LAZ with the points ordered into an +octree and an index at a known offset, so a reader can ask for a region and +fetch only the parts that cover it. Once you have picked a building, FTW asks +for the bounding box of *that footprint* instead of the tile: a Laserdata Skog +tile covers 2.5 km and runs to hundreds of megabytes, and a house is a few tens +of metres across. + +This is best-effort. A plain (non-COPC) `.laz` asset, a server that ignores +`Range` requests, or a laspy without COPC support all fall back to reading the +tile whole — slower, same answer. The result records which path ran as +`source.fetch`: `copc-window` or `whole-tile`. ## Using it @@ -69,15 +89,18 @@ knows whether there are panels on that face at all. Without a footprint the module segments everything within its radius: your neighbour's roof, the garage, the trees. Worse, the plane fitting is *global* — a fitted plane is infinite, so a roof at azimuth 180° is described by `z = f(y)` -with no `x` term at all and extends right across the tile. A second building -sharing your ridge orientation falls inside its inlier band however far away it -is, and the two lose points to each other. +with no `x` term at all and does not stop at your wall. A second building +sharing your pitch and ridge orientation is not on a *similar* plane; it is on +the same one. -Measured on a synthetic pair: a detached garage recovered 93% of its true area -and split into two fragments when it lay on the house's plane, against 100% and -a single clean face once the cloud was clipped to its own footprint. +Measured on a synthetic pair, a single fitting pass over a house and a garage +40 m apart swallowed **all** of both south faces — 576 returns from one and 256 +from the other — as one surface. The result was identical at every separation +from 3 m to 40 m, which is what tells you it is a global effect and not a +proximity one. Only the clustering step afterwards told the two buildings apart. -Picking a building is what makes the answer yours. +Clipping to your footprint first gives exactly the same face you would get by +scanning your building alone. Picking a building is what makes the answer yours. ## Optional: shading @@ -104,6 +127,7 @@ is clear" are different claims. | "No buildings found here" | The marker is not on a building, or you are outside Sweden | | "only N LiDAR returns fall on building" | The building is newer than the scan, or you picked the wrong footprint | | "No roof faces worth mounting panels on" | Everything found was north-facing or under 8 m² | +| "the building tile could not be read" | The GeoPackage asset was not a GeoPackage — usually a changed download URL | Failures never change your configuration. diff --git a/go/internal/roofmodel/roofmodel.go b/go/internal/roofmodel/roofmodel.go index fe7453fac..087b6504a 100644 --- a/go/internal/roofmodel/roofmodel.go +++ b/go/internal/roofmodel/roofmodel.go @@ -101,6 +101,11 @@ type Model struct { Collection string `json:"collection"` ItemCount int `json:"item_count"` DatasetDatetime string `json:"dataset_datetime"` + // Fetch is "copc-window" when only the picked building's neighbourhood + // was pulled from the LiDAR tile, or "whole-tile" when the whole thing + // came across. It also qualifies Building.ReturnsInRadius below, whose + // denominator is the fetched window in the first case. + Fetch string `json:"fetch"` } `json:"source"` // CapturedAtMs is when Lantmäteriet flew the LiDAR. Null while their STAC // datetime backfill is incomplete, which is a missing provenance date and diff --git a/go/internal/roofmodel/roofmodel_test.go b/go/internal/roofmodel/roofmodel_test.go index 5bb09e6d2..97398a798 100644 --- a/go/internal/roofmodel/roofmodel_test.go +++ b/go/internal/roofmodel/roofmodel_test.go @@ -228,6 +228,30 @@ func TestDeriveParsesAModel(t *testing.T) { } } +// Whether the module streamed a window out of the COPC tile or pulled the whole +// thing changes what ReturnsInRadius counts, so the answer has to reach core +// rather than being inferred from the numbers. +func TestDeriveCarriesHowTheLidarWasFetched(t *testing.T) { + doc := `{"schema_version":1,"planes_found":2,` + + `"source":{"provider":"lantmateriet","collection":"laserdata-nedladdning-skog",` + + `"item_count":1,"fetch":"copc-window"},` + + `"building":{"building_id":"b-1","area_m2":144,"returns_used":220,"returns_in_radius":260},` + + `"arrays":[]}` + cmd := stubModule(t, "stdout", doc) + s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t"}) + + m, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "b-1") + if err != nil { + t.Fatal(err) + } + if m.Source.Fetch != "copc-window" { + t.Errorf("fetch = %q, want copc-window", m.Source.Fetch) + } + if m.Building == nil || m.Building.ReturnsUsed != 220 { + t.Fatalf("building = %+v", m.Building) + } +} + // The site and the operator's credentials have to survive the process boundary, // or the module derives a roof somewhere else entirely. func TestDerivePassesTheSiteAndCredentials(t *testing.T) { diff --git a/roofmodel/ftw_roofmodel/buildings.py b/roofmodel/ftw_roofmodel/buildings.py index a0bcc00ca..595a64f15 100644 --- a/roofmodel/ftw_roofmodel/buildings.py +++ b/roofmodel/ftw_roofmodel/buildings.py @@ -4,14 +4,21 @@ around a coordinate returns the neighbours' roofs, the garage and whatever trees stand in the garden, and the segmenter has no way to know which returns belong to the operator's own house. Worse, RANSAC fits *infinite* planes over the whole -tile: an azimuth-180 roof plane is z = f(y) with no x term, so a second building -sharing the ridge orientation falls inside its inlier band however far away it -is, and loses points to it. Measured on a synthetic pair, a detached garage -recovered 93% of its true area and split into two fragments when it lay on the -house's plane, against 100% and one face when it did not. +tile: an azimuth-180 roof plane is z = f(y) with no x term, so it does not stop +at the wall, and a second building sharing the pitch and ridge orientation lies +on *the same* plane rather than a similar one. + +Measured on a synthetic pair, one RANSAC pass over a house and a garage 40 m +apart consumed all 576 of the house's south-face returns and all 256 of the +garage's as a single surface -- and produced identical output at every +separation from 3 m to 40 m, which is the signature of a global fit rather than +a neighbourhood effect. Only the DBSCAN pass afterwards told the two buildings +apart, leaving a clustering parameter as the sole thing standing between a +garage and its neighbour's roof. Clipping to a chosen footprint removes that whole class of error: the returns -that reach the segmenter are the ones standing on the operator's building. +that reach the segmenter are the ones standing on the operator's building, and +the derived face is then identical to segmenting that building in isolation. Coordinate frames ----------------- @@ -29,7 +36,15 @@ from typing import Any, Iterable from . import sweref -from .geotorget import COLLECTION_BUILDINGS, GeotorgetClient, GeotorgetError, StacItem +from .geopackage import GeoPackageError, read_features +from .geotorget import ( + COLLECTION_BUILDINGS, + MEDIA_GEOJSON, + MEDIA_GEOPACKAGE, + GeotorgetClient, + GeotorgetError, + StacItem, +) # How far around the site to look for candidate buildings. Wide enough to reach # a house set back from its coordinate, narrow enough not to return a village. @@ -169,16 +184,49 @@ def _to_sweref(ring: list[tuple[float, float]]) -> list[tuple[float, float]]: return [sweref.wgs84_to_sweref99tm(lat, lon) for lon, lat in ring] -def _features_from_item(item: StacItem) -> list[dict[str, Any]]: +def _features_from_item(item: StacItem, client: GeotorgetClient | None = None) -> list[dict[str, Any]]: """Every building-like feature an item carries. - A STAC item may itself be the building, or it may be a tile whose asset - holds them. Inline geometry is preferred: it costs no download. + A STAC item may *be* the building -- geometry inline, no download -- or it + may be a tile whose asset holds thousands of them. Lantmaeteriet publishes + *Byggnad Nedladdning, vektor* as **GeoPackage**, so the asset path is the + normal one and the inline path is the exception. """ geom = (item.raw or {}).get("geometry") if geom: return [{"geometry": geom, "properties": (item.raw or {}).get("properties") or {}, "id": item.item_id}] + if client is None: + return [] + asset = item.pick(MEDIA_GEOPACKAGE, MEDIA_GEOJSON) + if asset is None or not asset.href: + return [] + media = asset.effective_media_type + payload = client.download(asset.href) + if media == MEDIA_GEOJSON: + return _features_from_geojson(payload) + try: + return read_features(payload) + except GeoPackageError as exc: + raise BuildingLookupError( + f"the building tile for this site could not be read: {exc}" + ) from exc + + +def _features_from_geojson(payload: bytes) -> list[dict[str, Any]]: + """A GeoJSON FeatureCollection asset, for catalogues that publish one.""" + import json + + try: + doc = json.loads(payload.decode("utf-8")) + except (UnicodeDecodeError, ValueError) as exc: + raise BuildingLookupError( + f"the building tile was announced as GeoJSON but did not parse: {exc}" + ) from exc + if isinstance(doc, dict) and doc.get("type") == "FeatureCollection": + return list(doc.get("features") or []) + if isinstance(doc, dict) and doc.get("type") == "Feature": + return [doc] return [] @@ -228,7 +276,7 @@ def search_buildings( items = client.search(COLLECTION_BUILDINGS, bbox, limit=limit) features: list[dict[str, Any]] = [] for item in items: - features.extend(_features_from_item(item)) + features.extend(_features_from_item(item, client)) if not features: raise BuildingLookupError( "no building footprints were returned for this site. The Geotorget " diff --git a/roofmodel/ftw_roofmodel/geopackage.py b/roofmodel/ftw_roofmodel/geopackage.py new file mode 100644 index 000000000..9a7ad342c --- /dev/null +++ b/roofmodel/ftw_roofmodel/geopackage.py @@ -0,0 +1,228 @@ +"""Read polygon features out of a GeoPackage, using only the standard library. + +Lantmaeteriet publishes *Byggnad Nedladdning, vektor* as GeoPackage, so a STAC +item for a building tile carries a `.gpkg` asset rather than inline GeoJSON. + +A GeoPackage is a SQLite database with an agreed set of metadata tables, and +geometry stored as a small binary header followed by standard WKB. Both are +published specifications with fixed layouts, so decoding them here costs about +a hundred lines of `sqlite3` and `struct` -- against a GDAL/fiona/geopandas +dependency that would not install on a Pi without a compiler and pulls in a +second projection stack we already decided not to carry (see sweref.py, which +implements SWEREF 99 TM directly for the same reason). + +Only what a roof model needs is read: polygon and multipolygon rings, in the +file's own coordinate reference system. Curves, triangulated surfaces and the +extended (`GPB` "extended geometry") binary types are rejected explicitly rather +than mis-parsed, because a wrong ring silently clips the wrong LiDAR. + +References +---------- +GeoPackage Encoding Standard (OGC 12-128r19), clause 2.1.3 "BLOB Format". +OpenGIS Simple Features (OGC 06-103r4), clause 8.2 "Well-known Binary". +""" + +from __future__ import annotations + +import os +import sqlite3 +import struct +import tempfile +from typing import Any, Iterator + +# "GP" -- the two magic bytes every GeoPackage geometry blob starts with. +GPKG_MAGIC = b"GP" + +# Envelope sizes in doubles, indexed by the header's envelope indicator. +# 0 = absent, 1 = xy, 2 = xyz, 3 = xym, 4 = xyzm. 5-7 are reserved. +_ENVELOPE_DOUBLES = {0: 0, 1: 4, 2: 6, 3: 6, 4: 8} + +# WKB geometry type codes we can use. The ISO variants add 1000 for Z, 2000 for +# M and 3000 for ZM, so the base code is recovered with % 1000. +_WKB_POLYGON = 3 +_WKB_MULTIPOLYGON = 6 + +# The EWKB flag bits PostGIS adds to the type word. GeoPackage forbids them, but +# files written by other tools do turn up, and reading a flagged type as a +# geometry code would silently produce nonsense. +_EWKB_Z = 0x80000000 +_EWKB_M = 0x40000000 +_EWKB_SRID = 0x20000000 + + +class GeoPackageError(ValueError): + """The file is not a GeoPackage, or holds geometry we will not guess at.""" + + +def _unpack(fmt: str, data: bytes, offset: int) -> tuple[Any, ...]: + size = struct.calcsize(fmt) + if offset + size > len(data): + raise GeoPackageError("geometry blob ended mid-value") + return struct.unpack_from(fmt, data, offset) + + +def _dimensions(type_word: int) -> tuple[int, int]: + """(base geometry code, coordinates per point) for a WKB type word.""" + has_z = bool(type_word & _EWKB_Z) + has_m = bool(type_word & _EWKB_M) + code = type_word & ~(_EWKB_Z | _EWKB_M | _EWKB_SRID) + # ISO style: 1000/2000/3000 offsets carry the same information. + if code >= 3000: + code, has_z, has_m = code - 3000, True, True + elif code >= 2000: + code, has_m = code - 2000, True + elif code >= 1000: + code, has_z = code - 1000, True + return code, 2 + int(has_z) + int(has_m) + + +def _read_ring(data: bytes, offset: int, endian: str, coords: int) -> tuple[list[tuple[float, float]], int]: + (count,) = _unpack(endian + "I", data, offset) + offset += 4 + stride = 8 * coords + ring: list[tuple[float, float]] = [] + for _ in range(count): + x, y = _unpack(endian + "dd", data, offset) + ring.append((x, y)) + offset += stride + return ring, offset + + +def _read_polygon(data: bytes, offset: int, endian: str, coords: int) -> tuple[list[list[tuple[float, float]]], int]: + (n_rings,) = _unpack(endian + "I", data, offset) + offset += 4 + rings = [] + for _ in range(n_rings): + ring, offset = _read_ring(data, offset, endian, coords) + rings.append(ring) + return rings, offset + + +def _read_geometry(data: bytes, offset: int) -> tuple[list[list[list[tuple[float, float]]]], int]: + """Read one WKB geometry, returning it as a list of polygons.""" + (byte_order,) = _unpack("B", data, offset) + endian = "<" if byte_order == 1 else ">" + (type_word,) = _unpack(endian + "I", data, offset + 1) + offset += 5 + if type_word & _EWKB_SRID: + offset += 4 # embedded SRID, which we take from the header instead + code, coords = _dimensions(type_word) + if code == _WKB_POLYGON: + rings, offset = _read_polygon(data, offset, endian, coords) + return [rings], offset + if code == _WKB_MULTIPOLYGON: + (n,) = _unpack(endian + "I", data, offset) + offset += 4 + polys = [] + for _ in range(n): + # Each part carries its own byte order and type word. + part, offset = _read_geometry(data, offset) + polys.extend(part) + return polys, offset + raise GeoPackageError( + f"WKB geometry type {code} is not a polygon; a building footprint must be " + "a Polygon or MultiPolygon" + ) + + +def parse_geometry_blob(blob: bytes) -> dict[str, Any] | None: + """Decode a GeoPackage geometry BLOB into a GeoJSON-shaped geometry. + + Returns None for the empty geometry, which GeoPackage represents with a flag + rather than an absent row. + """ + if len(blob) < 8 or blob[:2] != GPKG_MAGIC: + raise GeoPackageError("not a GeoPackage geometry blob (bad magic)") + flags = blob[3] + if flags & 0x20: + raise GeoPackageError( + "extended (ExtendedGeoPackageBinary) geometry is not supported" + ) + envelope_indicator = (flags >> 1) & 0x07 + if envelope_indicator not in _ENVELOPE_DOUBLES: + raise GeoPackageError(f"reserved envelope indicator {envelope_indicator}") + if flags & 0x10: # empty geometry + return None + offset = 8 + 8 * _ENVELOPE_DOUBLES[envelope_indicator] + polygons, _ = _read_geometry(blob, offset) + if not polygons: + return None + if len(polygons) == 1: + return {"type": "Polygon", "coordinates": [[list(p) for p in r] for r in polygons[0]]} + return { + "type": "MultiPolygon", + "coordinates": [[[list(p) for p in r] for r in poly] for poly in polygons], + } + + +def _feature_tables(conn: sqlite3.Connection) -> list[tuple[str, str]]: + """(table, geometry column) for every feature table in the file.""" + try: + rows = conn.execute( + "SELECT c.table_name, g.column_name FROM gpkg_contents c " + "JOIN gpkg_geometry_columns g ON g.table_name = c.table_name " + "WHERE c.data_type = 'features'" + ).fetchall() + except sqlite3.DatabaseError as exc: + raise GeoPackageError(f"not a readable GeoPackage: {exc}") from exc + return [(str(t), str(c)) for t, c in rows] + + +def read_features(data: bytes, *, limit: int = 5000) -> list[dict[str, Any]]: + """Every polygon feature in a GeoPackage, as GeoJSON-shaped dicts. + + Attributes travel alongside the geometry so the picker can label a building + with whatever the source calls it. + """ + return list(iter_features(data, limit=limit)) + + +def iter_features(data: bytes, *, limit: int = 5000) -> Iterator[dict[str, Any]]: + if not data.startswith(b"SQLite format 3\x00"): + raise GeoPackageError( + "asset is not a GeoPackage (missing the SQLite file header)" + ) + # sqlite3 opens paths, not buffers, and a GeoPackage is random-access by + # design, so the bytes land in a temp file for the life of the read. + fd, path = tempfile.mkstemp(suffix=".gpkg") + try: + with os.fdopen(fd, "wb") as fh: + fh.write(data) + conn = sqlite3.connect(path) + try: + conn.row_factory = sqlite3.Row + yielded = 0 + for table, geom_col in _feature_tables(conn): + cursor = conn.execute(f'SELECT * FROM "{table}"') + for row in cursor: + if yielded >= limit: + return + blob = row[geom_col] + if not isinstance(blob, (bytes, bytearray)): + continue + try: + geometry = parse_geometry_blob(bytes(blob)) + except GeoPackageError: + # One unreadable row must not lose the other buildings + # in the tile. + continue + if geometry is None: + continue + props = { + k: row[k] + for k in row.keys() + if k != geom_col and isinstance(row[k], (str, int, float)) + } + yield { + "id": str(props.get("objektidentitet") or f"{table}-{yielded}"), + "geometry": geometry, + "properties": props, + } + yielded += 1 + finally: + conn.close() + finally: + try: + os.unlink(path) + except OSError: # pragma: no cover - Windows may hold the handle briefly + pass diff --git a/roofmodel/ftw_roofmodel/geotorget.py b/roofmodel/ftw_roofmodel/geotorget.py index 6bdbbe2e7..72553ea0a 100644 --- a/roofmodel/ftw_roofmodel/geotorget.py +++ b/roofmodel/ftw_roofmodel/geotorget.py @@ -24,10 +24,31 @@ DEFAULT_BASE_URL = "https://api.lantmateriet.se" -# Collection ids as published in Lantmaeteriet's STAC catalogue. +# Collection ids as published in Lantmaeteriet's STAC catalogue. Both products +# are STAC APIs over the same base URL and the same credentials; they differ +# only in what their items point at, which is what the media types below say. COLLECTION_BUILDINGS = "byggnad-nedladdning-vektor" COLLECTION_LIDAR = "laserdata-nedladdning-skog" +# Media types, so an asset is chosen by *what it is* rather than by hoping the +# publisher named the key "data". Byggnad-vektor delivers GeoPackage; Laserdata +# Skog delivers LAZ organised as COPC (Cloud Optimized Point Cloud). +MEDIA_GEOPACKAGE = "application/geopackage+sqlite3" +MEDIA_COPC = "application/vnd.laszip+copc" +MEDIA_LAZ = "application/vnd.laszip" +MEDIA_LAS = "application/vnd.las" +MEDIA_GEOJSON = "application/geo+json" + +# Longest suffix first: a COPC file is also a .laz, and reading it as a plain +# one would download the whole tile instead of the part we asked for. +_EXTENSION_MEDIA: tuple[tuple[str, str], ...] = ( + (".copc.laz", MEDIA_COPC), + (".gpkg", MEDIA_GEOPACKAGE), + (".geojson", MEDIA_GEOJSON), + (".laz", MEDIA_LAZ), + (".las", MEDIA_LAS), +) + class GeotorgetError(RuntimeError): """Any failure talking to Geotorget.""" @@ -51,22 +72,85 @@ def validate(self) -> None: ) +def media_type_for(href: str) -> str | None: + """Media type implied by a URL's extension, or None if it says nothing.""" + path = href.split("?", 1)[0].split("#", 1)[0].lower() + for suffix, media in _EXTENSION_MEDIA: + if path.endswith(suffix): + return media + return None + + +@dataclasses.dataclass(frozen=True) +class Asset: + """One STAC asset: where it is, and what it is.""" + + href: str + media_type: str | None = None + roles: tuple[str, ...] = () + title: str = "" + + @property + def effective_media_type(self) -> str | None: + """Declared media type, or the one the extension implies. + + Catalogues are inconsistent about `type`, and an asset with no declared + type is common enough that refusing to guess would mean refusing most + real items. The extension is only consulted when nothing was declared. + """ + return self.media_type or media_type_for(self.href) + + @dataclasses.dataclass class StacItem: """One STAC item, reduced to what the pipeline needs.""" item_id: str collection: str - assets: dict[str, str] + assets: dict[str, Asset] captured_at: dt.datetime | None raw: dict[str, Any] = dataclasses.field(default_factory=dict, repr=False) + def __post_init__(self) -> None: + # A bare href is accepted wherever an Asset is, so callers and tests can + # write {"data": "http://.../tile.copc.laz"} without losing the typing + # that selection depends on -- the extension supplies it. + self.assets = { + name: value if isinstance(value, Asset) else Asset(href=str(value)) + for name, value in (self.assets or {}).items() + } + + def pick(self, *media_types: str) -> Asset | None: + """Best asset for a wanted media type, most preferred type first. + + Where nothing declares a usable type the search widens: an asset with + the `data` role, then a lone asset, since an item carrying exactly one + asset is unambiguous however it is labelled. + + Both fallbacks consider only assets of *unknown* type. Guessing in the + absence of information is reasonable; guessing against it is not, and an + item whose single asset is a thumbnail must not be handed back as a + point cloud. + """ + for wanted in media_types: + for asset in self.assets.values(): + if asset.effective_media_type == wanted: + return asset + untyped = [a for a in self.assets.values() if a.effective_media_type is None] + for asset in untyped: + if "data" in asset.roles: + return asset + if len(untyped) == 1: + return untyped[0] + return None + def asset_url(self, *preferred: str) -> str | None: """First matching asset href, trying each preferred key in order.""" for key in preferred: if key in self.assets: - return self.assets[key] - return next(iter(self.assets.values()), None) + return self.assets[key].href + first = next(iter(self.assets.values()), None) + return first.href if first else None def _parse_datetime(value: str | None) -> dt.datetime | None: @@ -87,7 +171,12 @@ def _parse_datetime(value: str | None) -> dt.datetime | None: def _item_from_feature(feature: dict[str, Any]) -> StacItem: props = feature.get("properties") or {} assets = { - name: asset.get("href", "") + name: Asset( + href=asset.get("href", ""), + media_type=asset.get("type") or None, + roles=tuple(asset.get("roles") or ()), + title=str(asset.get("title") or ""), + ) for name, asset in (feature.get("assets") or {}).items() if asset.get("href") } @@ -135,6 +224,11 @@ def __init__( session.auth = (credentials.username, credentials.password) self._session = session + @property + def session(self) -> Any: + """The authenticated session, for readers that stream their own ranges.""" + return self._session + def search( self, collection: str, diff --git a/roofmodel/ftw_roofmodel/pipeline.py b/roofmodel/ftw_roofmodel/pipeline.py index ae0a6e00c..85f78b48a 100644 --- a/roofmodel/ftw_roofmodel/pipeline.py +++ b/roofmodel/ftw_roofmodel/pipeline.py @@ -17,8 +17,13 @@ import datetime as dt from typing import Any -from . import sweref -from .buildings import Building, clip_to_footprint, search_buildings +from . import geotorget, pointcloud, sweref +from .buildings import ( + DEFAULT_EAVES_BUFFER_M, + Building, + clip_to_footprint, + search_buildings, +) from .geotorget import ( COLLECTION_LIDAR, Credentials, @@ -128,27 +133,47 @@ def planes_to_arrays( def load_points(data: bytes) -> Any: - """Decode a LAZ/LAS payload into an (N, 3) array of SWEREF 99 TM metres. + """Decode a whole LAZ/LAS payload into (N, 3) SWEREF 99 TM metres. - laspy is imported here rather than at module scope so that everything above - -- projection, segmentation, array derivation -- is importable and testable - without the geospatial stack installed. + Re-exported from pointcloud so that laspy stays lazily imported: everything + above -- projection, segmentation, array derivation -- is importable and + testable without the geospatial stack installed. """ - import io - - try: - import laspy - except ImportError as exc: # pragma: no cover - depends on the install - raise RoofModelError( - "laspy is required to read Lantmaeteriet LiDAR. Install the module's " - "extras: pip install -e roofmodel[geo]" - ) from exc - - import numpy as np - - with laspy.open(io.BytesIO(data)) as reader: - las = reader.read() - return np.column_stack([np.asarray(las.x), np.asarray(las.y), np.asarray(las.z)]) + return pointcloud.load_points(data) + + +def _read_lidar( + client: GeotorgetClient, + items: list[StacItem], + chosen: Building | None, +) -> tuple[Any, str]: + """Points for the first readable LiDAR asset, and how they were fetched. + + Laserdata Skog is LAZ organised as COPC, so when the operator has already + picked a building there is no reason to move the rest of a 2.5 km tile + across the network: the footprint's bounding box is exactly the query COPC + is built to answer. Everything about that is best-effort -- a plain LAZ + asset, a host that ignores `Range`, or a laspy without COPC support all + fall back to reading the tile whole. + """ + for item in items: + asset = item.pick( + geotorget.MEDIA_COPC, geotorget.MEDIA_LAZ, geotorget.MEDIA_LAS + ) + if asset is None or not asset.href: + continue + if chosen is not None and asset.effective_media_type == geotorget.MEDIA_COPC: + session = getattr(client, "session", None) + if session is not None: + bounds = pointcloud.bounds_of(chosen.ring_sweref, DEFAULT_EAVES_BUFFER_M) + try: + return pointcloud.read_copc_window(session, asset.href, bounds), "copc-window" + except pointcloud.PointCloudError: + # A slow success beats a failure; the operator gets their + # roof either way, and `fetch` records which path ran. + pass + return load_points(client.download(asset.href)), "whole-tile" + raise RoofModelError("LiDAR tiles carried no readable point data") def derive( @@ -198,13 +223,7 @@ def derive( "Lantmaeteriet data is Sweden only" ) - points = None - for item in lidar_items: - url = item.asset_url("data", "laz", "copc") - if not url: - continue - points = load_points(client.download(url)) - break + points, fetch = _read_lidar(client, lidar_items, chosen) if points is None or len(points) == 0: raise RoofModelError("LiDAR tiles carried no readable point data") @@ -233,6 +252,10 @@ def derive( "collection": COLLECTION_LIDAR, "item_count": len(lidar_items), "dataset_datetime": captured.isoformat() if captured else None, + # "copc-window" means only the footprint's neighbourhood was moved + # across the network, which also makes returns_in_radius below a + # count over that window rather than over the whole search radius. + "fetch": fetch, }, "building": { "building_id": chosen.building_id, diff --git a/roofmodel/ftw_roofmodel/pointcloud.py b/roofmodel/ftw_roofmodel/pointcloud.py new file mode 100644 index 000000000..622c9efc4 --- /dev/null +++ b/roofmodel/ftw_roofmodel/pointcloud.py @@ -0,0 +1,226 @@ +"""Read Lantmaeteriet LiDAR, fetching only the part of the tile we need. + +*Laserdata Nedladdning, Skog* is delivered as LAZ organised as **COPC** (Cloud +Optimized Point Cloud): the points are ordered into an octree and the node index +lives in a VLR at a known offset, so a reader that can issue HTTP range requests +can pull the handful of octree nodes covering one building instead of the whole +2.5 km tile. + +That is worth real money on a Pi. A Laserdata Skog tile is hundreds of megabytes; +a detached house is a few tens of metres across. Since the operator has already +told us *which building* they mean, the bounding box of that footprint is exactly +the query COPC exists to answer. + +The fallbacks are deliberate and ordered, because none of the preconditions are +guaranteed: + + 1. COPC asset + a bounding box + a server that honours `Range` -> spatial query. + 2. Anything else -> download the asset and read it whole. + +A server that ignores `Range` returns 200 with the entire body, which would +otherwise be mistaken for a successful partial read, so that case is detected on +the status code rather than assumed away. +""" + +from __future__ import annotations + +import io +from typing import Any + +__all__ = [ + "PointCloudError", + "HttpRangeFile", + "bounds_of", + "load_points", + "read_copc_window", +] + +# Read this much per range request. COPC chunks are small, and a request per +# chunk would spend more time in round trips than in transfer. +DEFAULT_CHUNK_BYTES = 1 << 20 + + +class PointCloudError(RuntimeError): + """The LiDAR asset could not be read.""" + + +class HttpRangeFile(io.RawIOBase): + """A seekable read-only file over HTTP `Range` requests. + + laspy's COPC reader needs `seek`/`read` and nothing else, so this is the + whole adapter: it turns an HTTP URL into something that behaves like an open + file without ever holding the tile in memory. + """ + + def __init__(self, session: Any, url: str, *, timeout: float = 60.0, + chunk_bytes: int = DEFAULT_CHUNK_BYTES) -> None: + self._session = session + self._url = url + self._timeout = timeout + self._chunk = max(1, chunk_bytes) + self._pos = 0 + self._size: int | None = None + # One cached chunk. COPC reads are clustered -- header, then index, then + # the nodes -- so a single block absorbs most of the repeat traffic. + self._cache: tuple[int, bytes] | None = None + self.requests = 0 + self.bytes_fetched = 0 + + # -- io.RawIOBase ---------------------------------------------------- + def readable(self) -> bool: + return True + + def seekable(self) -> bool: + return True + + def tell(self) -> int: + return self._pos + + def seek(self, offset: int, whence: int = io.SEEK_SET) -> int: + if whence == io.SEEK_SET: + self._pos = offset + elif whence == io.SEEK_CUR: + self._pos += offset + elif whence == io.SEEK_END: + self._pos = self.size + offset + else: # pragma: no cover - io module only defines the three + raise ValueError(f"invalid whence {whence}") + self._pos = max(0, self._pos) + return self._pos + + def read(self, size: int = -1) -> bytes: + if size is None or size < 0: + size = max(0, self.size - self._pos) + if size == 0: + return b"" + data = self._read_at(self._pos, size) + self._pos += len(data) + return data + + def readall(self) -> bytes: + return self.read(-1) + + def readinto(self, buffer) -> int: # type: ignore[override] + data = self.read(len(buffer)) + buffer[: len(data)] = data + return len(data) + + # -- range plumbing -------------------------------------------------- + @property + def size(self) -> int: + if self._size is None: + self._size = self._head_size() + return self._size + + def _head_size(self) -> int: + try: + resp = self._session.head(self._url, timeout=self._timeout) + except Exception as exc: + raise PointCloudError(f"could not stat {self._url}: {exc}") from exc + length = (getattr(resp, "headers", None) or {}).get("Content-Length") + if getattr(resp, "status_code", 0) != 200 or not length: + raise PointCloudError( + "the LiDAR host did not report a size, so it cannot be read in ranges" + ) + return int(length) + + def _read_at(self, offset: int, size: int) -> bytes: + cached = self._from_cache(offset, size) + if cached is not None: + return cached + want = max(size, self._chunk) + end = offset + want - 1 + try: + resp = self._session.get( + self._url, + headers={"Range": f"bytes={offset}-{end}"}, + timeout=self._timeout, + ) + except Exception as exc: + raise PointCloudError(f"range request failed: {exc}") from exc + status = getattr(resp, "status_code", 0) + if status == 200: + # The server ignored Range and sent everything. Honest failure: the + # caller falls back to a whole-tile read rather than silently + # paying for the full download on every seek. + raise PointCloudError("the LiDAR host does not support range requests") + if status != 206: + raise PointCloudError(f"range request returned HTTP {status}") + body = resp.content + self.requests += 1 + self.bytes_fetched += len(body) + self._cache = (offset, body) + return body[:size] + + def _from_cache(self, offset: int, size: int) -> bytes | None: + if self._cache is None: + return None + start, body = self._cache + if offset < start or offset + size > start + len(body): + return None + rel = offset - start + return body[rel : rel + size] + + +def bounds_of(ring: list[tuple[float, float]], pad_m: float = 2.0) -> tuple[float, float, float, float]: + xs = [p[0] for p in ring] + ys = [p[1] for p in ring] + return (min(xs) - pad_m, min(ys) - pad_m, max(xs) + pad_m, max(ys) + pad_m) + + +def _points_from_las(las: Any) -> Any: + import numpy as np + + return np.column_stack([np.asarray(las.x), np.asarray(las.y), np.asarray(las.z)]) + + +def load_points(data: bytes) -> Any: + """Decode a whole LAZ/LAS payload into (N, 3) SWEREF 99 TM metres.""" + laspy = _import_laspy() + with laspy.open(io.BytesIO(data)) as reader: + las = reader.read() + return _points_from_las(las) + + +def _import_laspy(): + try: + import laspy + except ImportError as exc: # pragma: no cover - depends on the install + raise PointCloudError( + "laspy is required to read Lantmaeteriet LiDAR. Install the module's " + "extras: pip install -e roofmodel[geo]" + ) from exc + return laspy + + +def read_copc_window( + session: Any, + url: str, + bounds: tuple[float, float, float, float], + *, + timeout: float = 60.0, +) -> Any: + """Points inside `bounds` from a COPC file, over HTTP range requests. + + Raises PointCloudError if the file or the host cannot support it, so the + caller can fall back to a whole-tile read. + """ + laspy = _import_laspy() + try: + from laspy.copc import Bounds, CopcReader + except ImportError as exc: + raise PointCloudError( + "this laspy build has no COPC support; install laspy[lazrs] 2.5 or newer" + ) from exc + + min_x, min_y, max_x, max_y = bounds + handle = HttpRangeFile(session, url, timeout=timeout) + try: + with CopcReader.open(handle) as reader: + query = Bounds(mins=[min_x, min_y], maxs=[max_x, max_y]) + points = reader.query(query) + except PointCloudError: + raise + except Exception as exc: + raise PointCloudError(f"COPC read failed: {exc}") from exc + return _points_from_las(points) diff --git a/roofmodel/tests/test_assets.py b/roofmodel/tests/test_assets.py new file mode 100644 index 000000000..107a5baef --- /dev/null +++ b/roofmodel/tests/test_assets.py @@ -0,0 +1,212 @@ +"""Choosing STAC assets by what they are, and reading them in ranges. + +Both Lantmaeteriet products are STAC APIs; they differ in what their items point +at. Byggnad-vektor delivers GeoPackage, Laserdata Skog delivers LAZ organised as +COPC. Selecting on media type rather than on an asset key is what keeps that +difference from becoming a pile of special cases. +""" + +from __future__ import annotations + +import io + +import pytest + +from ftw_roofmodel.geotorget import ( + MEDIA_COPC, + MEDIA_GEOJSON, + MEDIA_GEOPACKAGE, + MEDIA_LAZ, + Asset, + StacItem, + _item_from_feature, + media_type_for, +) +from ftw_roofmodel.pointcloud import HttpRangeFile, PointCloudError, bounds_of + + +def item(assets): + return StacItem("i", "c", assets, None, raw={}) + + +def test_a_bare_href_still_gets_a_type_from_its_extension(): + """Tests and simple catalogues pass strings; selection must still work.""" + it = item({"data": "https://x/tile.copc.laz"}) + assert isinstance(it.assets["data"], Asset) + assert it.pick(MEDIA_COPC).href == "https://x/tile.copc.laz" + + +def test_copc_is_recognised_before_plain_laz(): + """A COPC file is also a .laz; reading it as one costs the whole tile.""" + assert media_type_for("https://x/y/tile.copc.laz") == MEDIA_COPC + assert media_type_for("https://x/y/tile.laz") == MEDIA_LAZ + + +def test_query_strings_do_not_hide_the_extension(): + """Signed download URLs carry tokens after a '?'.""" + assert media_type_for("https://x/tile.gpkg?token=abc&x=1") == MEDIA_GEOPACKAGE + + +def test_a_declared_type_beats_the_extension(): + """The catalogue knows better than the filename.""" + a = Asset(href="https://x/download", media_type=MEDIA_GEOPACKAGE) + assert a.effective_media_type == MEDIA_GEOPACKAGE + + +def test_preference_order_is_honoured(): + it = item({ + "laz": Asset("https://x/t.laz", MEDIA_LAZ), + "copc": Asset("https://x/t.copc.laz", MEDIA_COPC), + }) + assert it.pick(MEDIA_COPC, MEDIA_LAZ).effective_media_type == MEDIA_COPC + assert it.pick(MEDIA_LAZ, MEDIA_COPC).effective_media_type == MEDIA_LAZ + + +def test_falls_back_to_the_data_role_when_the_type_is_unknown(): + it = item({ + "thumbnail": Asset("https://x/preview.png", "image/png", roles=("thumbnail",)), + "mystery": Asset("https://x/blob", None, roles=("data",)), + }) + assert it.pick(MEDIA_COPC).href == "https://x/blob" + + +def test_a_lone_asset_is_unambiguous_whatever_it_is_called(): + assert item({"whatever": Asset("https://x/blob")}).pick(MEDIA_COPC).href == "https://x/blob" + + +def test_several_unlabelled_assets_are_refused_rather_than_guessed(): + it = item({"a": Asset("https://x/a"), "b": Asset("https://x/b")}) + assert it.pick(MEDIA_COPC) is None + + +def test_stac_assets_keep_their_type_and_roles(): + feature = { + "id": "tile-1", + "collection": "laserdata-nedladdning-skog", + "assets": { + "data": { + "href": "https://x/t.copc.laz", + "type": MEDIA_COPC, + "roles": ["data"], + "title": "Punktmoln", + } + }, + "properties": {}, + } + parsed = _item_from_feature(feature) + asset = parsed.pick(MEDIA_COPC) + assert asset.media_type == MEDIA_COPC + assert asset.roles == ("data",) + assert asset.title == "Punktmoln" + + +def test_geojson_and_geopackage_are_both_selectable(): + it = item({"gj": Asset("https://x/b.geojson"), "gp": Asset("https://x/b.gpkg")}) + assert it.pick(MEDIA_GEOPACKAGE, MEDIA_GEOJSON).href == "https://x/b.gpkg" + assert it.pick(MEDIA_GEOJSON, MEDIA_GEOPACKAGE).href == "https://x/b.geojson" + + +# --- range reads ------------------------------------------------------------ + + +class FakeResponse: + def __init__(self, status, content=b"", headers=None): + self.status_code = status + self.content = content + self.headers = headers or {} + + +class RangeServer: + """Serves a byte string over Range, and counts what was actually moved.""" + + def __init__(self, body: bytes, *, supports_range: bool = True): + self.body = body + self.supports_range = supports_range + self.requests: list[str] = [] + + def head(self, url, timeout=None): + return FakeResponse(200, headers={"Content-Length": str(len(self.body))}) + + def get(self, url, headers=None, timeout=None): + rng = (headers or {}).get("Range") + if not self.supports_range or not rng: + self.requests.append("full") + return FakeResponse(200, self.body) + self.requests.append(rng) + spec = rng.split("=", 1)[1] + start, end = spec.split("-") + lo = int(start) + hi = min(int(end), len(self.body) - 1) + return FakeResponse(206, self.body[lo : hi + 1]) + + +BODY = bytes(range(256)) * 40 # 10 240 bytes, every offset distinguishable + + +def test_reads_a_window_without_moving_the_whole_file(): + server = RangeServer(BODY) + fh = HttpRangeFile(server, "https://x/t.copc.laz", chunk_bytes=512) + fh.seek(1000) + assert fh.read(16) == BODY[1000:1016] + assert fh.bytes_fetched == 512, "one chunk, not the whole file" + assert len(server.requests) == 1 + + +def test_seek_and_tell_track_the_position(): + fh = HttpRangeFile(RangeServer(BODY), "https://x/t", chunk_bytes=64) + assert fh.seek(100) == 100 and fh.tell() == 100 + fh.read(10) + assert fh.tell() == 110 + assert fh.seek(-10, io.SEEK_END) == len(BODY) - 10 + assert fh.seek(5, io.SEEK_CUR) == len(BODY) - 5 + + +def test_a_second_read_inside_the_chunk_costs_no_request(): + server = RangeServer(BODY) + fh = HttpRangeFile(server, "https://x/t", chunk_bytes=1024) + fh.seek(0) + fh.read(8) + before = len(server.requests) + fh.seek(64) + assert fh.read(8) == BODY[64:72] + assert len(server.requests) == before, "the chunk was already held" + + +def test_reading_across_the_chunk_boundary_fetches_again(): + server = RangeServer(BODY) + fh = HttpRangeFile(server, "https://x/t", chunk_bytes=128) + fh.seek(0) + assert fh.read(8) == BODY[0:8] + fh.seek(4096) + assert fh.read(8) == BODY[4096:4104] + assert len(server.requests) == 2 + + +def test_a_host_that_ignores_range_is_detected_not_trusted(): + """A 200 means the whole body arrived; treating it as partial corrupts.""" + fh = HttpRangeFile(RangeServer(BODY, supports_range=False), "https://x/t") + fh.seek(10) + with pytest.raises(PointCloudError, match="range requests"): + fh.read(4) + + +def test_a_host_that_will_not_report_a_size_is_refused(): + class NoLength: + def head(self, url, timeout=None): + return FakeResponse(200, headers={}) + + with pytest.raises(PointCloudError, match="size"): + HttpRangeFile(NoLength(), "https://x/t").size + + +def test_readinto_fills_the_buffer(): + fh = HttpRangeFile(RangeServer(BODY), "https://x/t", chunk_bytes=256) + fh.seek(32) + buf = bytearray(16) + assert fh.readinto(buf) == 16 + assert bytes(buf) == BODY[32:48] + + +def test_bounds_pad_the_footprint_so_the_eaves_survive(): + ring = [(100.0, 200.0), (110.0, 200.0), (110.0, 220.0), (100.0, 220.0)] + assert bounds_of(ring, 1.0) == (99.0, 199.0, 111.0, 221.0) diff --git a/roofmodel/tests/test_geopackage.py b/roofmodel/tests/test_geopackage.py new file mode 100644 index 000000000..54c49e424 --- /dev/null +++ b/roofmodel/tests/test_geopackage.py @@ -0,0 +1,201 @@ +"""Decoding GeoPackage, the format Lantmaeteriet ships building vectors in. + +The fixtures are built byte by byte from the published layouts rather than by +round-tripping the decoder, so a decoder that agrees with itself but not with +the standard still fails here. +""" + +from __future__ import annotations + +import os +import sqlite3 +import struct +import tempfile + +import pytest + +from ftw_roofmodel.geopackage import ( + GeoPackageError, + parse_geometry_blob, + read_features, +) + +SWEREF = 3006 + + +def wkb_polygon(rings, *, little=True, z=False): + """Standard WKB polygon, per OGC 06-103r4 clause 8.2.""" + e = "<" if little else ">" + code = 1003 if z else 3 + out = struct.pack("B", 1 if little else 0) + struct.pack(e + "I", code) + out += struct.pack(e + "I", len(rings)) + for ring in rings: + out += struct.pack(e + "I", len(ring)) + for point in ring: + out += struct.pack(e + "dd", point[0], point[1]) + if z: + out += struct.pack(e + "d", point[2] if len(point) > 2 else 0.0) + return out + + +def wkb_multipolygon(polygons, *, little=True): + e = "<" if little else ">" + out = struct.pack("B", 1 if little else 0) + struct.pack(e + "I", 6) + out += struct.pack(e + "I", len(polygons)) + for rings in polygons: + out += wkb_polygon(rings, little=little) + return out + + +def gpkg_blob(wkb, *, envelope=None, srs_id=SWEREF, little=True, empty=False): + """GeoPackage geometry BLOB, per OGC 12-128r19 clause 2.1.3. + + Header is magic(2) + version(1) + flags(1) + srs_id(4), then the envelope, + then the WKB. + """ + indicator = 0 if envelope is None else 1 + flags = (1 if little else 0) | (indicator << 1) | (0x10 if empty else 0) + e = "<" if little else ">" + header = b"GP" + bytes([0, flags]) + struct.pack(e + "i", srs_id) + assert len(header) == 8, "the GeoPackage header is 8 bytes before the envelope" + body = b"" + if envelope is not None: + body = struct.pack(e + "dddd", *envelope) + assert len(body) == 32, "an xy envelope is four doubles" + return header + body + wkb + + +SQUARE = [[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0), (0.0, 0.0)]] + + +def test_reads_a_plain_polygon(): + geom = parse_geometry_blob(gpkg_blob(wkb_polygon(SQUARE))) + assert geom["type"] == "Polygon" + assert geom["coordinates"][0][0] == [0.0, 0.0] + assert geom["coordinates"][0][2] == [10.0, 10.0] + assert len(geom["coordinates"][0]) == 5 + + +def test_skips_the_envelope_when_one_is_present(): + """The envelope sits between the header and the WKB and must be stepped over.""" + with_env = parse_geometry_blob( + gpkg_blob(wkb_polygon(SQUARE), envelope=(0.0, 10.0, 0.0, 10.0)) + ) + without = parse_geometry_blob(gpkg_blob(wkb_polygon(SQUARE))) + assert with_env == without + + +def test_reads_big_endian_geometry(): + assert parse_geometry_blob( + gpkg_blob(wkb_polygon(SQUARE, little=False), little=False) + ) == parse_geometry_blob(gpkg_blob(wkb_polygon(SQUARE))) + + +def test_reads_3d_polygons_by_stepping_the_z(): + """Building footprints carry heights; the z must not shift the ring.""" + ring = [[(0.0, 0.0, 12.5), (10.0, 0.0, 12.5), (10.0, 10.0, 12.5), (0.0, 0.0, 12.5)]] + geom = parse_geometry_blob(gpkg_blob(wkb_polygon(ring, z=True))) + assert geom["coordinates"][0] == [[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 0.0]] + + +def test_reads_a_multipolygon_as_several_rings(): + other = [[(20.0, 20.0), (30.0, 20.0), (30.0, 30.0), (20.0, 20.0)]] + geom = parse_geometry_blob(gpkg_blob(wkb_multipolygon([SQUARE, other]))) + assert geom["type"] == "MultiPolygon" + assert len(geom["coordinates"]) == 2 + + +def test_an_interior_ring_survives(): + """A courtyard is a second ring, and dropping it would inflate the roof.""" + hole = [(2.0, 2.0), (4.0, 2.0), (4.0, 4.0), (2.0, 2.0)] + geom = parse_geometry_blob(gpkg_blob(wkb_polygon(SQUARE + [hole]))) + assert len(geom["coordinates"]) == 2 + + +def test_empty_geometry_is_none_not_an_error(): + assert parse_geometry_blob(gpkg_blob(b"", empty=True)) is None + + +def test_rejects_a_blob_that_is_not_a_geopackage_geometry(): + with pytest.raises(GeoPackageError, match="magic"): + parse_geometry_blob(b"XX" + bytes(20)) + + +def test_refuses_extended_geometry_rather_than_guessing(): + blob = bytearray(gpkg_blob(wkb_polygon(SQUARE))) + blob[3] |= 0x20 + with pytest.raises(GeoPackageError, match="extended"): + parse_geometry_blob(bytes(blob)) + + +def test_refuses_a_non_polygon_rather_than_mis_clipping(): + point = struct.pack("B", 1) + struct.pack(" Date: Wed, 5 Aug 2026 13:45:36 +0200 Subject: [PATCH 11/26] fix(roofmodel): one axis order for building rings, wherever they came from Rings read from a GeoPackage arrive x=easting first, the way GIS files store them. Rings converted from inline WGS84 geometry were stored the way wgs84_to_sweref99tm returns them: northing first. Everything downstream assumed the second convention, so every GeoPackage-sourced building -- the normal Lantmateriet case -- reported its centroid near (4 N, 63 E) in the Indian Ocean, an 8 000 km "distance", and a picker map with nothing visible on it. The suite missed it because every assertion was axis-blind: areas and SWEREF centroids survive a consistent swap. It surfaced the first time the module ran against a server and a human looked at where the buildings landed. ring_sweref now always holds (easting, northing): rings from degrees are swapped into it, and projected rings are normalised per point -- eastings stay under a million metres and northings start above six, so each point states its own order and an EPSG-registry-ordered export (north first) is folded in rather than mis-read. The WGS84 accessors and the site distance unpack accordingly. A regression test feeds the same square in both orders and requires real Stockholm coordinates and a sub-50 m distance back from each; the fixture files mis-unpacked the projection the same way the code did, and now match reality (LAZ x is an easting). Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com> Signed-off-by: Hugo Karlsson <48095810+HuggeK@users.noreply.github.com> --- roofmodel/ftw_roofmodel/buildings.py | 39 +++++++++++++++++++--- roofmodel/tests/test_buildings.py | 42 ++++++++++++++++++++---- roofmodel/tests/test_derive_footprint.py | 2 +- roofmodel/tests/test_source_formats.py | 2 +- 4 files changed, 71 insertions(+), 14 deletions(-) diff --git a/roofmodel/ftw_roofmodel/buildings.py b/roofmodel/ftw_roofmodel/buildings.py index 595a64f15..469e11278 100644 --- a/roofmodel/ftw_roofmodel/buildings.py +++ b/roofmodel/ftw_roofmodel/buildings.py @@ -82,13 +82,13 @@ def centroid_sweref(self) -> tuple[float, float]: def centroid_wgs84(self) -> tuple[float, float]: e, n = self.centroid_sweref() - return sweref.sweref99tm_to_wgs84(e, n) + return sweref.sweref99tm_to_wgs84(n, e) def ring_wgs84(self) -> list[list[float]]: """GeoJSON ring: [lon, lat] pairs, closed.""" out = [] for e, n in self.ring_sweref: - lat, lon = sweref.sweref99tm_to_wgs84(e, n) + lat, lon = sweref.sweref99tm_to_wgs84(n, e) out.append([round(lon, 7), round(lat, 7)]) if out and out[0] != out[-1]: out.append(out[0]) @@ -177,11 +177,40 @@ def _rings_from_geometry(geometry: dict[str, Any]) -> list[list[tuple[float, flo return [r for r in rings if len(r) >= 3] +def _normalize_sweref_ring( + ring: list[tuple[float, float]], +) -> list[tuple[float, float]]: + """Force a projected ring into (easting, northing) order. + + GIS files conventionally store x=easting first, but EPSG:3006 formally + declares north-first and some exports follow the registry. The two ranges + cannot collide -- eastings stay under a million metres, northings start + above six million -- so each point states its own order. + """ + out = [] + for point in ring: + x, y = float(point[0]), float(point[1]) + if x > 1_000_000.0 and y < 1_000_000.0: + x, y = y, x + out.append((x, y)) + return out + + def _to_sweref(ring: list[tuple[float, float]]) -> list[tuple[float, float]]: + """Any ring -> (easting, northing) SWEREF, the order ring_sweref documents. + + Both sources must land in the same order: the LiDAR clip compares these + rings against point coordinates, and wgs84_to_sweref99tm returns + (northing, easting) -- stored unswapped it would put every inline-geometry + building's clip window sideways. + """ if _looks_like_sweref(ring): - return ring + return _normalize_sweref_ring(ring) # GeoJSON order is [lon, lat]. - return [sweref.wgs84_to_sweref99tm(lat, lon) for lon, lat in ring] + return [ + (e, n) + for n, e in (sweref.wgs84_to_sweref99tm(lat, lon) for lon, lat in ring) + ] def _features_from_item(item: StacItem, client: GeotorgetClient | None = None) -> list[dict[str, Any]]: @@ -238,7 +267,7 @@ def buildings_from_features( fallback_id: str = "building", ) -> list[Building]: """Turn GeoJSON-ish features into ranked Building candidates.""" - site_e, site_n = sweref.wgs84_to_sweref99tm(latitude, longitude) + site_n, site_e = sweref.wgs84_to_sweref99tm(latitude, longitude) out: list[Building] = [] for i, feat in enumerate(features): for j, ring in enumerate(_rings_from_geometry(feat.get("geometry") or {})): diff --git a/roofmodel/tests/test_buildings.py b/roofmodel/tests/test_buildings.py index 27205ab9f..62232d396 100644 --- a/roofmodel/tests/test_buildings.py +++ b/roofmodel/tests/test_buildings.py @@ -72,11 +72,11 @@ def test_area_and_centroid_of_a_known_square(): def test_wgs84_rings_are_projected_before_measuring(): """A ring in degrees must be recognised and converted, not measured raw.""" lat, lon = STOCKHOLM - e, n = sweref.wgs84_to_sweref99tm(lat, lon) + n, e = sweref.wgs84_to_sweref99tm(lat, lon) ring_sweref = square_ring(e, n, 12.0) ring_wgs84 = [] for x, y in ring_sweref: - blat, blon = sweref.sweref99tm_to_wgs84(x, y) + blat, blon = sweref.sweref99tm_to_wgs84(y, x) # ring is (E, N) ring_wgs84.append([blon, blat]) # GeoJSON is [lon, lat] [b] = buildings_from_features( @@ -89,7 +89,7 @@ def test_wgs84_rings_are_projected_before_measuring(): def test_tiles_and_slivers_are_not_offered_as_buildings(): lat, lon = STOCKHOLM - e, n = sweref.wgs84_to_sweref99tm(lat, lon) + n, e = sweref.wgs84_to_sweref99tm(lat, lon) feats = [ {"geometry": {"type": "Polygon", "coordinates": [square_ring(e, n, 2500.0)]}, "id": "tile"}, {"geometry": {"type": "Polygon", "coordinates": [square_ring(e, n, 1.0)]}, "id": "sliver"}, @@ -101,7 +101,7 @@ def test_tiles_and_slivers_are_not_offered_as_buildings(): def test_candidates_come_back_nearest_first(): lat, lon = STOCKHOLM - e, n = sweref.wgs84_to_sweref99tm(lat, lon) + n, e = sweref.wgs84_to_sweref99tm(lat, lon) feats = [ {"geometry": {"type": "Polygon", "coordinates": [square_ring(e + 60, n, 10.0)]}, "id": "far"}, {"geometry": {"type": "Polygon", "coordinates": [square_ring(e + 5, n, 10.0)]}, "id": "near"}, @@ -114,7 +114,7 @@ def test_candidates_come_back_nearest_first(): def test_multipolygon_yields_one_candidate_per_part(): lat, lon = STOCKHOLM - e, n = sweref.wgs84_to_sweref99tm(lat, lon) + n, e = sweref.wgs84_to_sweref99tm(lat, lon) feat = { "id": "pair", "geometry": { @@ -129,7 +129,7 @@ def test_multipolygon_yields_one_candidate_per_part(): def test_search_queries_the_building_collection_and_maps_results(): lat, lon = STOCKHOLM - e, n = sweref.wgs84_to_sweref99tm(lat, lon) + n, e = sweref.wgs84_to_sweref99tm(lat, lon) session = FakeSession({"features": [stac_feature(square_ring(e, n, 10.0), "b1")]}) client = GeotorgetClient(Credentials("u", "t"), session=session) @@ -194,7 +194,7 @@ def test_clip_of_an_empty_cloud_is_empty_not_an_error(): def test_geojson_feature_is_wgs84_and_closed(): lat, lon = STOCKHOLM - e, n = sweref.wgs84_to_sweref99tm(lat, lon) + n, e = sweref.wgs84_to_sweref99tm(lat, lon) b = Building("b1", square_ring(e, n, 10.0), 100.0, 0.0) feat = b.to_geojson() @@ -204,3 +204,31 @@ def test_geojson_feature_is_wgs84_and_closed(): assert -180 <= x <= 180 and -90 <= y <= 90 assert feat["properties"]["latitude"] == pytest.approx(lat, abs=1e-3) assert feat["properties"]["longitude"] == pytest.approx(lon, abs=1e-3) + + +def test_both_ring_orders_come_back_at_the_real_site(): + """Axis order must not depend on where the ring came from. + + A GeoPackage stores x=easting first; wgs84_to_sweref99tm returns northing + first; EPSG's registry declares EPSG:3006 north-first and some exports + follow it. The first mismatch shipped: GeoPackage-sourced buildings — the + normal Lantmäteriet case — reported their centroids in the Indian Ocean + (lat ≈ 4°) with 8 000 km distances, while every test asserted only areas + and SWEREF centroids, both of which are blind to a consistent swap. + """ + lat, lon = STOCKHOLM + n, e = sweref.wgs84_to_sweref99tm(lat, lon) + ring_en = square_ring(e, n, 10.0) # as a GeoPackage stores it + ring_ne = [(y, x) for x, y in ring_en] # as the EPSG registry says + + for ring in (ring_en, ring_ne): + [b] = buildings_from_features( + [{"geometry": {"type": "Polygon", "coordinates": [ring]}, "id": "b"}], + latitude=lat, longitude=lon, + ) + feat = b.to_geojson() + assert feat["properties"]["latitude"] == pytest.approx(lat, abs=1e-3) + assert feat["properties"]["longitude"] == pytest.approx(lon, abs=1e-3) + assert b.distance_m < 50.0, ( + f"a building drawn around the site is {b.distance_m:.0f} m away" + ) diff --git a/roofmodel/tests/test_derive_footprint.py b/roofmodel/tests/test_derive_footprint.py index 3932e7ef8..5489b2a70 100644 --- a/roofmodel/tests/test_derive_footprint.py +++ b/roofmodel/tests/test_derive_footprint.py @@ -59,7 +59,7 @@ def scene(monkeypatch): so it extends across the whole tile and the two buildings compete for each other's returns unless the cloud is clipped first. """ - e, n = sweref.wgs84_to_sweref99tm(*STOCKHOLM) + n, e = sweref.wgs84_to_sweref99tm(*STOCKHOLM) mine = np.vstack([ roof_face(35, 180, 12, 6, (e, n, 0), seed=2), roof_face(35, 0, 12, 6, (e, n + 6, 4.2), seed=3), diff --git a/roofmodel/tests/test_source_formats.py b/roofmodel/tests/test_source_formats.py index 1bcd434c3..e33b6d929 100644 --- a/roofmodel/tests/test_source_formats.py +++ b/roofmodel/tests/test_source_formats.py @@ -30,7 +30,7 @@ from .test_geopackage import build_gpkg, gpkg_blob, wkb_polygon STOCKHOLM = (59.33, 18.07) -E, N = sweref.wgs84_to_sweref99tm(*STOCKHOLM) +N, E = sweref.wgs84_to_sweref99tm(*STOCKHOLM) def square(cx, cy, w, d): From b94672b67dbce7e1a81fd4fb9c7a208d9508ef24 Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Wed, 5 Aug 2026 14:37:42 +0200 Subject: [PATCH 12/26] fix(web): help bubbles survive quotes and the bottom of the modal The roofmodel section's help text ends with two quoted product names, and the bubble cut off mid-sentence right before them. escHtml used the textContent/innerHTML trick, which never escapes quotes -- so every caller that builds an attribute (data-help, title, value) had its text terminated at the first embedded quote, with the remainder parsed as junk attribute names. Plain string replaces now cover both quote kinds. With the full text rendering, the second failure appeared: the bubble opens downward and the modal body is the scroll container that clips it, so a badge low in the last section has less room below than a long help text needs. settings.js now toggles .help-up on hover when the badge sits in the lower part of the visible modal, and the bubble grows upward into room that exists. Measured on hover because scroll position, not the badge, decides which way is open. Verified in headless Edge: the full text renders at both scroll positions, upward when low, downward when high. Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com> Signed-off-by: Hugo Karlsson <48095810+HuggeK@users.noreply.github.com> --- web/settings.js | 32 +++++++++++++++++++++++++++++--- web/style.css | 10 ++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/web/settings.js b/web/settings.js index cce548233..b3b29ef65 100644 --- a/web/settings.js +++ b/web/settings.js @@ -65,6 +65,24 @@ if (e.target === modal) modal.classList.add("hidden"); }); + // The help bubble is pure CSS and normally opens downward, but the modal + // body is the scroll container that clips it: a badge in the last section + // has less room below it than a long help text needs. Flip the bubble + // upward when the badge sits in the lower part of the visible modal, so it + // grows into space that exists. Measured on hover because scroll position, + // not the badge, decides which way is open. + modal.addEventListener("mouseover", function (e) { + var badge = e.target && e.target.closest && e.target.closest(".help"); + if (!badge) return; + var scroller = badge.closest(".modal-body"); + if (!scroller) return; + var box = scroller.getBoundingClientRect(); + var at = badge.getBoundingClientRect(); + badge.classList.toggle( + "help-up", at.top + at.height / 2 > box.top + box.height * 0.55 + ); + }); + tabsEl.addEventListener("click", function (e) { if (e.target.tagName === "BUTTON" && e.target.dataset.tab) { tabsEl.querySelectorAll("button").forEach(function (b) { @@ -263,9 +281,17 @@ } function escHtml(s) { - var div = document.createElement("div"); - div.textContent = s == null ? "" : String(s); - return div.innerHTML; + // Plain string replaces rather than the textContent/innerHTML trick: that + // trick never escapes quotes, and every caller that builds an attribute + // (value="...", title="...", data-help="...") has its value cut short at + // the first embedded quote — the rest of the text silently becomes junk + // attribute names. + return String(s == null ? "" : s) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); } function renderTab(tab) { diff --git a/web/style.css b/web/style.css index df0e04bf6..037f82aa9 100644 --- a/web/style.css +++ b/web/style.css @@ -1969,6 +1969,16 @@ footer { position: absolute; left: 20px; top: -4px; +} +/* Flipped variant: settings.js adds .help-up when the badge sits low in the + scrollable modal body, so the bubble grows into room that exists instead + of into the clip edge below. */ +.help.help-up:hover::after { + top: auto; + bottom: -4px; +} +.help:hover::after, +.help.help-up:hover::after { background: var(--ink-elevated); border: 1px solid var(--line); border-radius: 4px; From ad5f86eff0f3259435b58db7cde99018810ca76c Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Wed, 19 Aug 2026 14:48:01 +0200 Subject: [PATCH 13/26] fix(roofmodel): emit rated watts after si-core-units Derived arrays pre-fill weather.pv_arrays, which now stores rated_w. The Python module and Go host speak watts; kWp stays a test helper. --- go/internal/roofmodel/roofmodel.go | 4 ++-- go/internal/roofmodel/roofmodel_test.go | 8 ++++---- go/internal/units/consistency_test.go | 12 ++++++++++++ roofmodel/ftw_roofmodel/pipeline.py | 6 +++--- roofmodel/ftw_roofmodel/segment.py | 12 ++++++++++-- roofmodel/tests/test_pipeline.py | 6 +++--- web/settings/tabs/weather.js | 6 +++++- 7 files changed, 39 insertions(+), 15 deletions(-) diff --git a/go/internal/roofmodel/roofmodel.go b/go/internal/roofmodel/roofmodel.go index 087b6504a..4ef5bb6ee 100644 --- a/go/internal/roofmodel/roofmodel.go +++ b/go/internal/roofmodel/roofmodel.go @@ -53,7 +53,7 @@ const ( // the document can pre-fill weather.pv_arrays directly. type Array struct { Name string `json:"name"` - KWp float64 `json:"kwp"` + RatedW float64 `json:"rated_w"` TiltDeg float64 `json:"tilt_deg"` AzimuthDeg float64 `json:"azimuth_deg"` AreaM2 float64 `json:"area_m2"` @@ -285,7 +285,7 @@ func (m *Model) ToPVArrays() []config.PVArray { tiltDeg, azimuthDeg := a.TiltDeg, a.AzimuthDeg out = append(out, config.PVArray{ Name: a.Name, - KWp: a.KWp, + RatedW: a.RatedW, TiltDeg: &tiltDeg, AzimuthDeg: &azimuthDeg, }) diff --git a/go/internal/roofmodel/roofmodel_test.go b/go/internal/roofmodel/roofmodel_test.go index 97398a798..3e70c7292 100644 --- a/go/internal/roofmodel/roofmodel_test.go +++ b/go/internal/roofmodel/roofmodel_test.go @@ -208,7 +208,7 @@ func TestDeriveParsesAModel(t *testing.T) { doc := `{"schema_version":1,"planes_found":3,` + `"site":{"latitude":59.33,"longitude":18.07,"radius_m":40},` + `"source":{"provider":"lantmateriet","item_count":2,"dataset_datetime":"2018-03-01T00:00:00+00:00"},` + - `"arrays":[{"name":"Roof south","kwp":7.2,"tilt_deg":35,"azimuth_deg":180,"area_m2":51.4,"segment_id":"seg-0"}],` + + `"arrays":[{"name":"Roof south","rated_w":7200,"tilt_deg":35,"azimuth_deg":180,"area_m2":51.4,"segment_id":"seg-0"}],` + `"captured_at_ms":1519862400000,"derived_at_ms":1785456000000}` cmd := stubModule(t, "stdout", doc) s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t"}) @@ -482,8 +482,8 @@ func TestDeriveIsTimeBoxed(t *testing.T) { func TestToPVArraysMatchesConfigShape(t *testing.T) { m := &Model{Arrays: []Array{ - {Name: "Roof south", KWp: 7.2, TiltDeg: 35, AzimuthDeg: 180, AreaM2: 51.4}, - {Name: "Roof west", KWp: 4.1, TiltDeg: 35, AzimuthDeg: 270, AreaM2: 29.3}, + {Name: "Roof south", RatedW: 7200, TiltDeg: 35, AzimuthDeg: 180, AreaM2: 51.4}, + {Name: "Roof west", RatedW: 4100, TiltDeg: 35, AzimuthDeg: 270, AreaM2: 29.3}, }} got := m.ToPVArrays() if len(got) != 2 { @@ -492,7 +492,7 @@ func TestToPVArraysMatchesConfigShape(t *testing.T) { if got[0].TiltDeg == nil || got[0].AzimuthDeg == nil { t.Fatalf("derived array must carry both angles, got %+v", got[0]) } - if got[0].Name != "Roof south" || got[0].KWp != 7.2 || + if got[0].Name != "Roof south" || got[0].RatedW != 7200 || *got[0].TiltDeg != 35 || *got[0].AzimuthDeg != 180 { t.Errorf("array 0 = %+v tilt=%v az=%v", got[0], *got[0].TiltDeg, *got[0].AzimuthDeg) } diff --git a/go/internal/units/consistency_test.go b/go/internal/units/consistency_test.go index 053c1e6b5..c0e67f124 100644 --- a/go/internal/units/consistency_test.go +++ b/go/internal/units/consistency_test.go @@ -11,6 +11,7 @@ import ( "github.com/srcfl/ftw/go/internal/loadpoint" "github.com/srcfl/ftw/go/internal/mpc" "github.com/srcfl/ftw/go/internal/pvperf" + "github.com/srcfl/ftw/go/internal/roofmodel" "github.com/srcfl/ftw/go/internal/telemetry" "github.com/srcfl/ftw/go/internal/units" "github.com/srcfl/ftw/go/internal/v2x" @@ -54,6 +55,16 @@ func TestPVPerfArrayHasNoKWp(t *testing.T) { } } +func TestRoofmodelArrayHasNoKWp(t *testing.T) { + typ := reflect.TypeOf(roofmodel.Array{}) + if _, ok := typ.FieldByName("KWp"); ok { + t.Fatal("roofmodel.Array must not have KWp; store RatedW") + } + if _, ok := typ.FieldByName("RatedW"); !ok { + t.Fatal("roofmodel.Array must store RatedW (watts)") + } +} + func TestMPCParamsSoCIsFraction(t *testing.T) { typ := reflect.TypeOf(mpc.Params{}) for _, banned := range []string{"SoCMinPct", "SoCMaxPct", "InitialSoCPct"} { @@ -255,6 +266,7 @@ func TestCoreBannedSoCPercentFieldNames(t *testing.T) { reflect.TypeOf(mpc.SlotDirective{}), reflect.TypeOf(forecast.Array{}), reflect.TypeOf(pvperf.Array{}), + reflect.TypeOf(roofmodel.Array{}), } banned := []string{"CurrentSoCPct", "TargetSoCPct", "PluginSoCPct", "VehicleSoCPct", "SoCPct", "SoCMinPct", "SoCMaxPct", "SoCTargetPct", "LivePVSurplusSoCCapPct", "LoadpointSoCTargetPct", "KWp"} for _, typ := range types { diff --git a/roofmodel/ftw_roofmodel/pipeline.py b/roofmodel/ftw_roofmodel/pipeline.py index 85f78b48a..639d9b657 100644 --- a/roofmodel/ftw_roofmodel/pipeline.py +++ b/roofmodel/ftw_roofmodel/pipeline.py @@ -64,7 +64,7 @@ class RoofModelError(RuntimeError): @dataclasses.dataclass class DerivedArray: name: str - kwp: float + rated_w: float tilt_deg: float azimuth_deg: float area_m2: float @@ -73,7 +73,7 @@ class DerivedArray: def to_json(self) -> dict[str, Any]: return { "name": self.name, - "kwp": round(self.kwp, 2), + "rated_w": round(self.rated_w), "tilt_deg": round(self.tilt_deg, 1), "azimuth_deg": round(self.azimuth_deg, 1), "area_m2": round(self.area_m2, 1), @@ -122,7 +122,7 @@ def planes_to_arrays( arrays.append( DerivedArray( name=name, - kwp=plane.kwp(packing_factor, module_w_per_m2), + rated_w=plane.rated_w(packing_factor, module_w_per_m2), tilt_deg=plane.tilt_deg, azimuth_deg=plane.azimuth_deg, area_m2=plane.area_m2, diff --git a/roofmodel/ftw_roofmodel/segment.py b/roofmodel/ftw_roofmodel/segment.py index 33d6a13da..5a2bc0a11 100644 --- a/roofmodel/ftw_roofmodel/segment.py +++ b/roofmodel/ftw_roofmodel/segment.py @@ -53,13 +53,21 @@ class RoofPlane: point_count: int mean_height_m: float + def rated_w( + self, + packing_factor: float = DEFAULT_PACKING_FACTOR, + module_w_per_m2: float = DEFAULT_MODULE_W_PER_M2, + ) -> float: + """Installable DC capacity for this surface, in watts.""" + return self.area_m2 * packing_factor * module_w_per_m2 + def kwp( self, packing_factor: float = DEFAULT_PACKING_FACTOR, module_w_per_m2: float = DEFAULT_MODULE_W_PER_M2, ) -> float: - """Installable DC capacity for this surface, in kWp.""" - return self.area_m2 * packing_factor * module_w_per_m2 / 1000.0 + """Installable DC capacity for this surface, in kWp (test helper).""" + return self.rated_w(packing_factor, module_w_per_m2) / 1000.0 def _fit_plane(points: np.ndarray) -> np.ndarray: diff --git a/roofmodel/tests/test_pipeline.py b/roofmodel/tests/test_pipeline.py index c597a4a20..570071001 100644 --- a/roofmodel/tests/test_pipeline.py +++ b/roofmodel/tests/test_pipeline.py @@ -188,8 +188,8 @@ def test_array_json_matches_the_config_field_names(): """The document pre-fills weather.pv_arrays, so the keys must line up.""" planes = [RoofPlane(tilt_deg=35, azimuth_deg=180, area_m2=60, point_count=200, mean_height_m=6)] payload = planes_to_arrays(planes)[0].to_json() - assert set(payload) >= {"name", "kwp", "tilt_deg", "azimuth_deg"} - assert payload["kwp"] > 0 + assert set(payload) >= {"name", "rated_w", "tilt_deg", "azimuth_deg"} + assert payload["rated_w"] > 0 # --- end to end ------------------------------------------------------------ @@ -221,7 +221,7 @@ def test_derive_produces_a_versioned_document(monkeypatch): south = model["arrays"][0] assert south["azimuth_deg"] == pytest.approx(180.0, abs=3.0) assert south["tilt_deg"] == pytest.approx(35.0, abs=3.0) - assert south["kwp"] > 0 + assert south["rated_w"] > 0 # Must be JSON-serialisable for the subprocess contract. json.dumps(model) diff --git a/web/settings/tabs/weather.js b/web/settings/tabs/weather.js index 58484a7f7..05a8b7446 100644 --- a/web/settings/tabs/weather.js +++ b/web/settings/tabs/weather.js @@ -494,8 +494,12 @@ // Fill the form rather than saving: the operator sees the numbers and // presses Save, so the panel config never changes behind their back. ctx.config.weather.pv_arrays = arrays.map(function (a) { + var rated = Number(a.rated_w); + if (!(rated > 0) && Number(a.kwp) > 0) { + rated = ratedWattsFromLegacyKwp(a.kwp); + } return { - name: a.name || "", kwp: a.kwp, + name: a.name || "", rated_w: rated || 0, tilt_deg: a.tilt_deg, azimuth_deg: a.azimuth_deg, }; }); From 7cc0df93b9d1c25a8a7977e2f8a4d16f3f32ae77 Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Sat, 29 Aug 2026 18:16:30 +0200 Subject: [PATCH 14/26] feat(roofmodel): Basic-auth credentials, catalog-agnostic STAC access Lantmateriet provides no OAuth for its STAC download APIs, so the credential is the operator's own Geotorget account username and password, sent as HTTP Basic auth. The config keys become roofmodel.stac_username / stac_password; the old geotorget_username/geotorget_token keys keep working as aliases and migrate on the next save through the API. While renaming the keys, the client sheds its Lantmateriet hardcodes: stac_base_url, stac_buildings_collection, stac_lidar_collection and stac_bbox_epsg point FTW at any STAC-conformant catalog (search is the spec's POST {base}/search; the bbox CRS is per-catalog, 4326 per spec, 3006 for Lantmateriet). A custom catalog lifts the Sweden-only gate. Point-cloud data must still arrive in SWEREF 99 TM metres. Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com> --- .changeset/roofmodel-stac-basic-auth.md | 15 +++ docs/roof-geometry.md | 46 ++++++- go/internal/api/api.go | 6 +- go/internal/api/api_roofmodel_test.go | 8 +- go/internal/config/config.go | 75 +++++++++-- go/internal/config/roofmodel_secrets_test.go | 126 +++++++++++++------ go/internal/roofmodel/roofmodel.go | 25 +++- go/internal/roofmodel/roofmodel_test.go | 126 ++++++++++++++++--- roofmodel/ftw_roofmodel/__main__.py | 51 +++++++- roofmodel/ftw_roofmodel/buildings.py | 7 +- roofmodel/ftw_roofmodel/geotorget.py | 67 ++++++---- roofmodel/ftw_roofmodel/pipeline.py | 28 +++-- roofmodel/ftw_roofmodel/sweref.py | 18 +++ roofmodel/tests/test_pipeline.py | 14 +++ roofmodel/tests/test_sweref.py | 25 ++++ web/settings/tabs/weather.js | 15 ++- 16 files changed, 528 insertions(+), 124 deletions(-) create mode 100644 .changeset/roofmodel-stac-basic-auth.md diff --git a/.changeset/roofmodel-stac-basic-auth.md b/.changeset/roofmodel-stac-basic-auth.md new file mode 100644 index 000000000..7bd1bd2b4 --- /dev/null +++ b/.changeset/roofmodel-stac-basic-auth.md @@ -0,0 +1,15 @@ +--- +"ftw": minor +--- + +Roof-geometry credentials are now the Geotorget account username and password, +sent as HTTP Basic auth — Lantmäteriet provides no OAuth for its STAC download +APIs, so there is no issued token to paste. Configs that stored the secret +under the old `geotorget_token` key keep working and migrate to +`roofmodel.stac_password` on their next save. + +The STAC client is catalog-agnostic while it is at it: `stac_base_url`, +`stac_buildings_collection`, `stac_lidar_collection` and `stac_bbox_epsg` +point FTW at any STAC-conformant catalog (search is the spec's +`POST {base}/search`), with Lantmäteriet as the default. A custom catalog also +lifts the Sweden-only coordinate gate. diff --git a/docs/roof-geometry.md b/docs/roof-geometry.md index f6d90d10a..9c9bd1d5c 100644 --- a/docs/roof-geometry.md +++ b/docs/roof-geometry.md @@ -4,9 +4,10 @@ FTW's PV forecast needs the tilt and azimuth of each roof face. Typing them in means measuring your own roof, and most people estimate. In Sweden the state already flew a laser over it, so FTW can read the numbers instead. -This is **optional and Sweden-only**. Everywhere else, and whenever anything -below is missing, the numeric fields in **Settings → Weather → PV arrays** stay -the way they work today. +This is **optional, and Sweden-only by default** (any standard STAC catalog +can stand in — see [Other countries, other catalogs](#other-countries-other-catalogs)). +Everywhere else, and whenever anything below is missing, the numeric fields in +**Settings → Weather → PV arrays** stay the way they work today. ## What you need @@ -24,6 +25,12 @@ both and FTW searches them the same way. They differ only in what the items point at, and FTW picks the right asset by its declared media type rather than by its name — a catalogue that renames `data` to `punktmoln` keeps working. +Authentication is **HTTP Basic with your Geotorget account username and +password**. Lantmäteriet provides no OAuth for these STAC APIs, so there is no +issued token to paste — the account credential itself is what the catalog +accepts. FTW stores the password like every other secret: it is written to the +config file on the host, masked in every API response, and never logged. + Ordering access is not instant — Lantmäteriet approves it — so do it before you plan to use this. @@ -38,6 +45,35 @@ point cloud works, which is enough to run the tests but not enough to derive a real roof. GeoPackage needs nothing extra: it is a SQLite file, and FTW reads it with the standard library. +## Other countries, other catalogs + +The client speaks plain [STAC](https://stacspec.org/) — search is the spec's +`POST {base}/search`, downloads follow asset hrefs, and authentication is +ordinary HTTP Basic. Lantmäteriet is only the default. If another country +publishes building footprints and LiDAR through a STAC API, point FTW at it: + +```yaml +roofmodel: + enabled: true + stac_base_url: https://stac.example.org # STAC API root + stac_buildings_collection: buildings-vector # footprint polygons + stac_lidar_collection: lidar-pointcloud # LAZ/COPC point clouds + stac_bbox_epsg: 4326 # the spec's WGS84; 3006 = SWEREF + stac_username: you # omit both for an open catalog… + stac_password: "…" # …that needs no login +``` + +Setting `stac_base_url` also lifts the Sweden-only coordinate gate, since FTW +cannot know what a third-party catalog covers. + +Two caveats. The search bbox CRS is per-catalog: the STAC spec mandates WGS84 +(`stac_bbox_epsg: 4326`), but Lantmäteriet expects SWEREF 99 TM, which is why +the default stays `3006`. And the *data* itself must arrive in SWEREF 99 TM +metres for now — the plane fitting works in that frame — so a foreign catalog +needs its point clouds delivered in a matching projected CRS before the derived +tilt/azimuth mean anything. Lifting that second limit means carrying a real +projection library, which the module has so far deliberately avoided. + ### Why picking a building also makes it fast COPC — Cloud Optimized Point Cloud — is LAZ with the points ordered into an @@ -57,7 +93,7 @@ tile whole — slower, same answer. The result records which path ran as 1. Open **Settings → Weather**. 2. Put the map marker on your building. 3. Under **Roof geometry from Lantmäteriet**, tick **Enable roof derivation**, - enter your Geotorget username and token, and **Save**. + enter your Geotorget username and password, and **Save**. 4. Press **Find buildings here**. Footprints appear on the map and as a list. 5. Click your building. It highlights green. 6. Press **Read roof from LiDAR**. @@ -123,7 +159,7 @@ is clear" are different claims. | What you see | What it means | |---|---| | "Roof derivation is off" | Tick the box, add credentials, Save, retry | -| "Geotorget rejected the credentials" | Wrong token, or the account has not been granted that product | +| "the STAC catalog rejected the credentials" | Wrong username or password, or the account has not been granted that product | | "No buildings found here" | The marker is not on a building, or you are outside Sweden | | "only N LiDAR returns fall on building" | The building is newer than the scan, or you picked the wrong footprint | | "No roof faces worth mounting panels on" | Everything found was north-facing or under 8 m² | diff --git a/go/internal/api/api.go b/go/internal/api/api.go index 06c59c175..84c21fb58 100644 --- a/go/internal/api/api.go +++ b/go/internal/api/api.go @@ -2545,8 +2545,8 @@ func roofModelErrorStatus(err error) int { return 502 } -// roofModelHasCredentials reports whether a Geotorget token is stored, without -// revealing it. +// roofModelHasCredentials reports whether STAC catalog credentials are +// stored, without revealing them. func (s *Server) roofModelHasCredentials() bool { if s.deps.CfgMu == nil { return false @@ -2557,7 +2557,7 @@ func (s *Server) roofModelHasCredentials() bool { return false } rm := s.deps.Cfg.RoofModel - return rm.GeotorgetUsername != "" && rm.GeotorgetToken != "" + return rm.StacUser() != "" && rm.StacPass() != "" } // siteLocation returns the configured site coordinates. diff --git a/go/internal/api/api_roofmodel_test.go b/go/internal/api/api_roofmodel_test.go index 8e65f8eac..e24c9ff2b 100644 --- a/go/internal/api/api_roofmodel_test.go +++ b/go/internal/api/api_roofmodel_test.go @@ -101,7 +101,7 @@ func TestRoofModelBuildingsAcceptsAnExplicitCoordinate(t *testing.T) { deps := depsAt(59.33, 18.07) deps.RoofModel = roofmodel.FromConfig(&config.RoofModel{ Enabled: true, Command: "definitely-not-a-real-command", - GeotorgetUsername: "u", GeotorgetToken: "t", + StacUsername: "u", StacPassword: "p", }) // Berlin is outside Lantmateriet coverage; if the query coordinate were @@ -123,8 +123,10 @@ func TestRoofModelBuildingsAcceptsAnExplicitCoordinate(t *testing.T) { } } -// The Geotorget token is the operator's credential. Status may be reported; -// the secret itself must never appear in a response. +// The catalog password is the operator's credential. Status may be reported; +// the secret itself must never appear in a response. This test deliberately +// stores it under the legacy geotorget_token key: a config written before the +// basic-auth redesign must stay every bit as private. func TestRoofModelNeverEchoesTheToken(t *testing.T) { deps := depsAt(59.33, 18.07) deps.Cfg.RoofModel = &config.RoofModel{ diff --git a/go/internal/config/config.go b/go/internal/config/config.go index 6837a9959..3038c427d 100644 --- a/go/internal/config/config.go +++ b/go/internal/config/config.go @@ -1448,20 +1448,48 @@ type Price struct { // and its output only ever pre-fills the editable weather.pv_arrays. Absent or // disabled, everything else behaves normally. // -// GeotorgetToken is the operator's own credential. It is redacted in API -// responses by the existing sensitive-key rule (any key containing "token"). +// StacPassword (and its legacy alias GeotorgetToken) is the operator's own +// credential. It is redacted in API responses. type RoofModel struct { Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"` // Command is the interpreter used for the module; defaults to "python3". Command string `yaml:"command,omitempty" json:"command,omitempty"` ModuleDir string `yaml:"module_dir,omitempty" json:"module_dir,omitempty"` + // StacUsername/StacPassword authenticate against the STAC catalog as HTTP + // Basic auth. Lantmäteriet provides no OAuth for its STAC download APIs, + // so for the default Geotorget catalog these are the operator's own + // account username and password. + StacUsername string `yaml:"stac_username,omitempty" json:"stac_username,omitempty"` + StacPassword string `yaml:"stac_password,omitempty" json:"stac_password,omitempty"` + // HasStacPassword is set only on the masked copy the API returns, so the + // UI can show that a password is stored without ever receiving it. Never + // written to YAML and never read from an incoming config. + HasStacPassword bool `yaml:"-" json:"has_stac_password,omitempty"` + + // GeotorgetUsername/GeotorgetToken are legacy aliases from when the + // credential was assumed to be an issued token rather than the account + // password. Still read and still masked; the stac_* keys win if both are + // set, and a config saved through the API migrates to the stac_* keys. GeotorgetUsername string `yaml:"geotorget_username,omitempty" json:"geotorget_username,omitempty"` GeotorgetToken string `yaml:"geotorget_token,omitempty" json:"geotorget_token,omitempty"` - // HasGeotorgetToken is set only on the masked copy the API returns, so the - // UI can show that a token is stored without ever receiving it. Never - // written to YAML and never read from an incoming config. - HasGeotorgetToken bool `yaml:"-" json:"has_geotorget_token,omitempty"` + + // StacBaseURL is the root of a STAC API; empty means Lantmäteriet's + // catalog. The search and download protocol is standard STAC, so any + // catalog that publishes building footprints and LiDAR behind Basic auth + // (or none) can be pointed at. Setting a custom URL also lifts the + // Sweden-only coverage gate, since FTW cannot know what a third-party + // catalog covers. + StacBaseURL string `yaml:"stac_base_url,omitempty" json:"stac_base_url,omitempty"` + // StacBuildingsCollection / StacLidarCollection name the catalog's + // building-footprint and LiDAR collections; empty means the two Geotorget + // products. + StacBuildingsCollection string `yaml:"stac_buildings_collection,omitempty" json:"stac_buildings_collection,omitempty"` + StacLidarCollection string `yaml:"stac_lidar_collection,omitempty" json:"stac_lidar_collection,omitempty"` + // StacBboxEPSG is the CRS of the bbox sent to STAC search. The STAC spec + // mandates WGS84 (4326), but Lantmäteriet's catalog expects SWEREF 99 TM + // (3006), which is the default here. + StacBboxEPSG int `yaml:"stac_bbox_epsg,omitempty" json:"stac_bbox_epsg,omitempty"` // RadiusM is how far around the site to pull LiDAR (default 40 m). RadiusM float64 `yaml:"radius_m,omitempty" json:"radius_m,omitempty"` @@ -1473,6 +1501,24 @@ type RoofModel struct { TimeoutS int `yaml:"timeout_s,omitempty" json:"timeout_s,omitempty"` } +// StacUser returns the catalog username, whichever key it was configured +// under. +func (r *RoofModel) StacUser() string { + if r.StacUsername != "" { + return r.StacUsername + } + return r.GeotorgetUsername +} + +// StacPass returns the catalog Basic-auth password. The legacy +// geotorget_token key holds the same secret under an older name. +func (r *RoofModel) StacPass() string { + if r.StacPassword != "" { + return r.StacPassword + } + return r.GeotorgetToken +} + // Weather is the weather-forecast source config. type Weather struct { Provider string `yaml:"provider" json:"provider"` // met_no | openweather | open_meteo | forecast_solar | none @@ -1612,8 +1658,13 @@ func (c Config) MaskSecrets() Config { cp := *out.RoofModel // The UI has to distinguish "no credential stored" from "one is stored // but masked", or an operator cannot tell whether they still need to - // paste their Geotorget token in. - cp.HasGeotorgetToken = strings.TrimSpace(cp.GeotorgetToken) != "" + // paste their Geotorget password in. The masked copy is also folded to + // the canonical stac_* keys so the UI reads a single shape however old + // the YAML is. + cp.StacUsername = cp.StacUser() + cp.HasStacPassword = strings.TrimSpace(cp.StacPass()) != "" + cp.StacPassword = "" + cp.GeotorgetUsername = "" cp.GeotorgetToken = "" out.RoofModel = &cp } @@ -1712,8 +1763,12 @@ func (incoming *Config) PreserveMaskedSecrets(existing *Config) { if incoming.Assistant != nil && existing.Assistant != nil && incoming.Assistant.APIKey == "" { incoming.Assistant.APIKey = existing.Assistant.APIKey } - if incoming.RoofModel != nil && existing.RoofModel != nil && incoming.RoofModel.GeotorgetToken == "" { - incoming.RoofModel.GeotorgetToken = existing.RoofModel.GeotorgetToken + if incoming.RoofModel != nil && existing.RoofModel != nil && + incoming.RoofModel.StacPassword == "" && incoming.RoofModel.GeotorgetToken == "" { + // Restore under the canonical key wherever the existing secret lives; + // this is also what migrates an old geotorget_token config to + // stac_password on its first save through the API. + incoming.RoofModel.StacPassword = existing.RoofModel.StacPass() } if incoming.Notifications != nil && existing.Notifications != nil && incoming.Notifications.Ntfy != nil && existing.Notifications.Ntfy != nil { diff --git a/go/internal/config/roofmodel_secrets_test.go b/go/internal/config/roofmodel_secrets_test.go index fe1e36d71..b02e3dfa3 100644 --- a/go/internal/config/roofmodel_secrets_test.go +++ b/go/internal/config/roofmodel_secrets_test.go @@ -2,88 +2,140 @@ package config import "testing" -// The Geotorget token is the operator's own credential for Lantmateriet. It -// must never come back out of the API, and -- the failure that actually bites -- -// saving the settings form must not wipe it, because the form only ever sends -// back the blank it was given. +// The STAC catalog password — for Lantmäteriet, the operator's own Geotorget +// account password, since no OAuth is offered for those APIs. It must never +// come back out of the API, and — the failure that actually bites — saving +// the settings form must not wipe it, because the form only ever sends back +// the blank it was given. -func TestRoofModelMaskSecretsHidesTheTokenButSaysOneExists(t *testing.T) { +func TestRoofModelMaskSecretsHidesThePasswordButSaysOneExists(t *testing.T) { c := Config{RoofModel: &RoofModel{ - Enabled: true, - GeotorgetUsername: "operator@example.com", - GeotorgetToken: "gt_secret_value", + Enabled: true, + StacUsername: "operator@example.com", + StacPassword: "gt_secret_value", }} m := c.MaskSecrets() - if m.RoofModel.GeotorgetToken != "" { - t.Errorf("token leaked through the API: %q", m.RoofModel.GeotorgetToken) + if m.RoofModel.StacPassword != "" { + t.Errorf("password leaked through the API: %q", m.RoofModel.StacPassword) } - if !m.RoofModel.HasGeotorgetToken { - t.Error("UI cannot tell a stored token from a missing one") + if !m.RoofModel.HasStacPassword { + t.Error("UI cannot tell a stored password from a missing one") } // The username is not a secret, and blanking it would make the form look // empty when it is not. - if m.RoofModel.GeotorgetUsername != "operator@example.com" { - t.Errorf("username got blanked: %q", m.RoofModel.GeotorgetUsername) + if m.RoofModel.StacUsername != "operator@example.com" { + t.Errorf("username got blanked: %q", m.RoofModel.StacUsername) } - if c.RoofModel.GeotorgetToken != "gt_secret_value" { + if c.RoofModel.StacPassword != "gt_secret_value" { t.Error("masking mutated the original config") } } -func TestRoofModelMaskSecretsReportsNoTokenWhenUnset(t *testing.T) { - for _, tok := range []string{"", " "} { - c := Config{RoofModel: &RoofModel{Enabled: true, GeotorgetToken: tok}} - if c.MaskSecrets().RoofModel.HasGeotorgetToken { - t.Errorf("token %q reported as stored", tok) +// A config written before the basic-auth redesign stores the same secret +// under geotorget_token. It must mask just as hard, and the masked copy must +// present the canonical stac_* shape so the UI only ever reads one thing. +func TestRoofModelMaskSecretsFoldsTheLegacyKeys(t *testing.T) { + c := Config{RoofModel: &RoofModel{ + Enabled: true, + GeotorgetUsername: "operator", + GeotorgetToken: "legacy_secret", + }} + m := c.MaskSecrets() + + if m.RoofModel.GeotorgetToken != "" || m.RoofModel.StacPassword != "" { + t.Errorf("secret leaked: token=%q password=%q", + m.RoofModel.GeotorgetToken, m.RoofModel.StacPassword) + } + if !m.RoofModel.HasStacPassword { + t.Error("a legacy-stored secret was reported as absent") + } + if m.RoofModel.StacUsername != "operator" || m.RoofModel.GeotorgetUsername != "" { + t.Errorf("masked copy not folded to stac_*: stac=%q legacy=%q", + m.RoofModel.StacUsername, m.RoofModel.GeotorgetUsername) + } +} + +func TestRoofModelMaskSecretsReportsNoPasswordWhenUnset(t *testing.T) { + for _, pw := range []string{"", " "} { + c := Config{RoofModel: &RoofModel{Enabled: true, StacPassword: pw}} + if c.MaskSecrets().RoofModel.HasStacPassword { + t.Errorf("password %q reported as stored", pw) } } } -// Saving any unrelated setting round-trips the whole config, so an empty token -// from the UI means "unchanged", not "delete it". -func TestRoofModelPreserveMaskedSecretsKeepsTheStoredToken(t *testing.T) { +// Saving any unrelated setting round-trips the whole config, so an empty +// password from the UI means "unchanged", not "delete it". +func TestRoofModelPreserveMaskedSecretsKeepsTheStoredPassword(t *testing.T) { existing := &Config{RoofModel: &RoofModel{ - Enabled: true, GeotorgetUsername: "operator", GeotorgetToken: "gt_secret_value", + Enabled: true, StacUsername: "operator", StacPassword: "gt_secret_value", }} incoming := &Config{RoofModel: &RoofModel{ - Enabled: true, GeotorgetUsername: "operator", GeotorgetToken: "", RadiusM: 60, + Enabled: true, StacUsername: "operator", StacPassword: "", RadiusM: 60, }} incoming.PreserveMaskedSecrets(existing) - if incoming.RoofModel.GeotorgetToken != "gt_secret_value" { - t.Errorf("token = %q, want it preserved", incoming.RoofModel.GeotorgetToken) + if incoming.RoofModel.StacPassword != "gt_secret_value" { + t.Errorf("password = %q, want it preserved", incoming.RoofModel.StacPassword) } if incoming.RoofModel.RadiusM != 60 { t.Error("the edit being saved was lost") } } -// Pasting a new token has to replace the old one, or a rotated credential +// A secret stored under the legacy key survives a save and migrates to the +// canonical key, which is how old configs move forward without the operator +// retyping anything. +func TestRoofModelPreserveMaskedSecretsMigratesALegacyToken(t *testing.T) { + existing := &Config{RoofModel: &RoofModel{GeotorgetToken: "legacy_secret"}} + incoming := &Config{RoofModel: &RoofModel{}} + + incoming.PreserveMaskedSecrets(existing) + + if incoming.RoofModel.StacPassword != "legacy_secret" { + t.Errorf("password = %q, want the legacy secret under the new key", + incoming.RoofModel.StacPassword) + } +} + +// Pasting a new password has to replace the old one, or a rotated credential // could never be entered. -func TestRoofModelPreserveMaskedSecretsAcceptsANewToken(t *testing.T) { - existing := &Config{RoofModel: &RoofModel{GeotorgetToken: "old_token"}} - incoming := &Config{RoofModel: &RoofModel{GeotorgetToken: "new_token"}} +func TestRoofModelPreserveMaskedSecretsAcceptsANewPassword(t *testing.T) { + existing := &Config{RoofModel: &RoofModel{StacPassword: "old_password"}} + incoming := &Config{RoofModel: &RoofModel{StacPassword: "new_password"}} incoming.PreserveMaskedSecrets(existing) - if incoming.RoofModel.GeotorgetToken != "new_token" { - t.Errorf("token = %q, want the newly entered one", incoming.RoofModel.GeotorgetToken) + if incoming.RoofModel.StacPassword != "new_password" { + t.Errorf("password = %q, want the newly entered one", incoming.RoofModel.StacPassword) } } // Enabling the module for the first time has no existing section to copy from. func TestRoofModelPreserveMaskedSecretsSurvivesAMissingSection(t *testing.T) { - incoming := &Config{RoofModel: &RoofModel{GeotorgetToken: "first_token"}} + incoming := &Config{RoofModel: &RoofModel{StacPassword: "first_password"}} incoming.PreserveMaskedSecrets(&Config{}) - if incoming.RoofModel.GeotorgetToken != "first_token" { - t.Errorf("token = %q", incoming.RoofModel.GeotorgetToken) + if incoming.RoofModel.StacPassword != "first_password" { + t.Errorf("password = %q", incoming.RoofModel.StacPassword) } none := &Config{} - none.PreserveMaskedSecrets(&Config{RoofModel: &RoofModel{GeotorgetToken: "x"}}) + none.PreserveMaskedSecrets(&Config{RoofModel: &RoofModel{StacPassword: "x"}}) if none.RoofModel != nil { t.Error("a section the operator never configured was invented") } } + +// Both credential spellings resolve through the accessors, stac_* winning. +func TestRoofModelCredentialAccessors(t *testing.T) { + r := &RoofModel{GeotorgetUsername: "legacy-u", GeotorgetToken: "legacy-p"} + if r.StacUser() != "legacy-u" || r.StacPass() != "legacy-p" { + t.Errorf("legacy keys not readable: %q %q", r.StacUser(), r.StacPass()) + } + r.StacUsername, r.StacPassword = "new-u", "new-p" + if r.StacUser() != "new-u" || r.StacPass() != "new-p" { + t.Errorf("stac_* keys must win: %q %q", r.StacUser(), r.StacPass()) + } +} diff --git a/go/internal/roofmodel/roofmodel.go b/go/internal/roofmodel/roofmodel.go index 4ef5bb6ee..0955a41fb 100644 --- a/go/internal/roofmodel/roofmodel.go +++ b/go/internal/roofmodel/roofmodel.go @@ -220,10 +220,13 @@ func (s *Service) run(ctx context.Context, lat, lon float64, mode, buildingID st if !s.Enabled() { return nil, ErrDisabled } - if !coverage.Covers("lantmateriet", lat, lon) { + // The Sweden gate belongs to the default Lantmäteriet catalog only. An + // operator pointing at another country's STAC catalog knows what it + // covers; FTW does not, so it stops pretending to. + if s.cfg.StacBaseURL == "" && !coverage.Covers("lantmateriet", lat, lon) { return nil, fmt.Errorf("%w: (%.4f, %.4f) is not in Sweden", ErrOutsideCoverage, lat, lon) } - if s.cfg.GeotorgetUsername == "" || s.cfg.GeotorgetToken == "" { + if s.cfg.StacUser() == "" || s.cfg.StacPass() == "" { return nil, ErrNoCredentials } @@ -235,14 +238,28 @@ func (s *Service) run(ctx context.Context, lat, lon float64, mode, buildingID st "--mode", mode, "--lat", fmt.Sprintf("%.6f", lat), "--lon", fmt.Sprintf("%.6f", lon), - "--username", s.cfg.GeotorgetUsername, - "--token", s.cfg.GeotorgetToken, + "--username", s.cfg.StacUser(), + "--password", s.cfg.StacPass(), "--radius-m", fmt.Sprintf("%.1f", s.radius()), "--packing-factor", fmt.Sprintf("%.3f", s.packingFactor()), } if buildingID != "" { args = append(args, "--building-id", buildingID) } + // A custom catalog replaces the Lantmäteriet defaults piecewise; anything + // left empty falls back to the module's own Geotorget defaults. + if s.cfg.StacBaseURL != "" { + args = append(args, "--stac-base-url", s.cfg.StacBaseURL) + } + if s.cfg.StacBuildingsCollection != "" { + args = append(args, "--buildings-collection", s.cfg.StacBuildingsCollection) + } + if s.cfg.StacLidarCollection != "" { + args = append(args, "--lidar-collection", s.cfg.StacLidarCollection) + } + if s.cfg.StacBboxEPSG != 0 { + args = append(args, "--bbox-epsg", fmt.Sprintf("%d", s.cfg.StacBboxEPSG)) + } cmd := exec.CommandContext(ctx, s.command(), args...) if s.cfg.ModuleDir != "" { cmd.Env = append(cmd.Environ(), "PYTHONPATH="+s.cfg.ModuleDir) diff --git a/go/internal/roofmodel/roofmodel_test.go b/go/internal/roofmodel/roofmodel_test.go index 3e70c7292..a0e670100 100644 --- a/go/internal/roofmodel/roofmodel_test.go +++ b/go/internal/roofmodel/roofmodel_test.go @@ -122,7 +122,7 @@ func TestDeriveRefusesOutsideSwedenWithoutSpawning(t *testing.T) { s := svc(t, &config.RoofModel{ Enabled: true, Command: "definitely-not-a-real-command", - GeotorgetUsername: "u", GeotorgetToken: "t", + StacUsername: "u", StacPassword: "t", }) for _, c := range []struct { name string @@ -153,7 +153,7 @@ func TestSwedishBoxAdmitsSomeNonSwedishPointsByDesign(t *testing.T) { s := svc(t, &config.RoofModel{ Enabled: true, Command: "definitely-not-a-real-command", - GeotorgetUsername: "u", GeotorgetToken: "t", + StacUsername: "u", StacPassword: "t", }) _, err := s.Derive(context.Background(), 59.91, 10.75, "") // Oslo if errors.Is(err, ErrOutsideCoverage) { @@ -170,7 +170,7 @@ func TestSwedishBoxCoversBorderTowns(t *testing.T) { s := svc(t, &config.RoofModel{ Enabled: true, Command: "definitely-not-a-real-command", - GeotorgetUsername: "u", GeotorgetToken: "t", + StacUsername: "u", StacPassword: "t", }) for _, c := range []struct { name string @@ -196,7 +196,7 @@ func TestDeriveRequiresCredentials(t *testing.T) { {"no token", "u", ""}, {"neither", "", ""}, } { - s := svc(t, &config.RoofModel{Enabled: true, Command: "no-such-command", GeotorgetUsername: c.user, GeotorgetToken: c.token}) + s := svc(t, &config.RoofModel{Enabled: true, Command: "no-such-command", StacUsername: c.user, StacPassword: c.token}) _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "") if !errors.Is(err, ErrNoCredentials) { t.Errorf("%s: err = %v, want ErrNoCredentials", c.name, err) @@ -211,7 +211,7 @@ func TestDeriveParsesAModel(t *testing.T) { `"arrays":[{"name":"Roof south","rated_w":7200,"tilt_deg":35,"azimuth_deg":180,"area_m2":51.4,"segment_id":"seg-0"}],` + `"captured_at_ms":1519862400000,"derived_at_ms":1785456000000}` cmd := stubModule(t, "stdout", doc) - s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t"}) + s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, StacUsername: "u", StacPassword: "t"}) m, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "") if err != nil { @@ -238,7 +238,7 @@ func TestDeriveCarriesHowTheLidarWasFetched(t *testing.T) { `"building":{"building_id":"b-1","area_m2":144,"returns_used":220,"returns_in_radius":260},` + `"arrays":[]}` cmd := stubModule(t, "stdout", doc) - s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t"}) + s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, StacUsername: "u", StacPassword: "t"}) m, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "b-1") if err != nil { @@ -260,7 +260,7 @@ func TestDerivePassesTheSiteAndCredentials(t *testing.T) { cmd := stubModule(t, "record", record) s := svc(t, &config.RoofModel{ Enabled: true, Command: cmd, ModuleDir: dir, - GeotorgetUsername: "operator", GeotorgetToken: "secret-token", + StacUsername: "operator", StacPassword: "secret-token", RadiusM: 25, }) @@ -282,7 +282,7 @@ func TestDerivePassesTheSiteAndCredentials(t *testing.T) { "--lat 59.330000", "--lon 18.070000", "--username operator", - "--token secret-token", + "--password secret-token", "--radius-m 25.0", } { if !strings.Contains(line, want) { @@ -308,7 +308,7 @@ func TestDerivePassesThePickedBuilding(t *testing.T) { cmd := stubModule(t, "record", record) s := svc(t, &config.RoofModel{ Enabled: true, Command: cmd, - GeotorgetUsername: "u", GeotorgetToken: "t", + StacUsername: "u", StacPassword: "t", }) if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "bldg-42"); err != nil { @@ -331,7 +331,7 @@ func TestDeriveOmitsTheBuildingFlagWhenNoneIsPicked(t *testing.T) { record := dir + string(os.PathSeparator) + "invocation.json" cmd := stubModule(t, "record", record) s := svc(t, &config.RoofModel{ - Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t", + Enabled: true, Command: cmd, StacUsername: "u", StacPassword: "t", }) if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, ""); err != nil { @@ -348,7 +348,7 @@ func TestBuildingsListsFootprints(t *testing.T) { `{"type":"Feature","id":"b1","geometry":{"type":"Polygon","coordinates":[[[18.0,59.3],[18.001,59.3],[18.001,59.301],[18.0,59.3]]]},"properties":{"area_m2":120.5}},` + `{"type":"Feature","id":"b2","geometry":{"type":"Polygon","coordinates":[[[18.01,59.3],[18.011,59.3],[18.011,59.301],[18.01,59.3]]]},"properties":{"area_m2":64.0}}]}` cmd := stubModule(t, "stdout", doc) - s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t"}) + s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, StacUsername: "u", StacPassword: "t"}) list, err := s.Buildings(context.Background(), stockholmLat, stockholmLon) if err != nil { @@ -367,7 +367,7 @@ func TestBuildingsUsesBuildingsMode(t *testing.T) { dir := t.TempDir() record := dir + string(os.PathSeparator) + "invocation.json" cmd := stubModule(t, "record", record) - s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t"}) + s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, StacUsername: "u", StacPassword: "t"}) // The stub answers with a roof model, not a building list; only the // invocation matters here. @@ -383,7 +383,7 @@ func TestBuildingsUsesBuildingsMode(t *testing.T) { func TestBuildingsRefusesOutsideCoverageAndWithoutCredentials(t *testing.T) { s := svc(t, &config.RoofModel{ Enabled: true, Command: "definitely-not-a-real-command", - GeotorgetUsername: "u", GeotorgetToken: "t", + StacUsername: "u", StacPassword: "t", }) if _, err := s.Buildings(context.Background(), 52.52, 13.40); !errors.Is(err, ErrOutsideCoverage) { t.Errorf("Berlin: err = %v, want ErrOutsideCoverage", err) @@ -418,7 +418,7 @@ func readInvocation(t *testing.T, path string) stubInvocation { func TestDeriveSurfacesTheModuleErrorMessage(t *testing.T) { cmd := stubModule(t, "stderr", `{"error":"Geotorget rejected the credentials","kind":"MissingCredentials"}`) - s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t"}) + s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, StacUsername: "u", StacPassword: "t"}) _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "") if err == nil { @@ -433,7 +433,7 @@ func TestDeriveSurfacesTheModuleErrorMessage(t *testing.T) { // rather than being mistaken for a successful empty model. func TestDeriveReportsNonJSONFailure(t *testing.T) { cmd := stubModule(t, "stderr", "Traceback (most recent call last):\n MemoryError\n") - s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t"}) + s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, StacUsername: "u", StacPassword: "t"}) _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "") if err == nil || !strings.Contains(err.Error(), "roof model failed") { @@ -443,7 +443,7 @@ func TestDeriveReportsNonJSONFailure(t *testing.T) { func TestDeriveRejectsUnknownSchemaVersion(t *testing.T) { cmd := stubModule(t, "stdout", `{"schema_version":99,"arrays":[]}`) - s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t"}) + s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, StacUsername: "u", StacPassword: "t"}) _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "") if err == nil || !strings.Contains(err.Error(), "schema_version") { @@ -453,7 +453,7 @@ func TestDeriveRejectsUnknownSchemaVersion(t *testing.T) { func TestDeriveRejectsUnreadableOutput(t *testing.T) { cmd := stubModule(t, "stdout", "not-json-at-all") - s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, GeotorgetUsername: "u", GeotorgetToken: "t"}) + s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, StacUsername: "u", StacPassword: "t"}) _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "") if err == nil || !strings.Contains(err.Error(), "unreadable") { @@ -467,7 +467,7 @@ func TestDeriveIsTimeBoxed(t *testing.T) { cmd := stubModule(t, "hang", "") s := svc(t, &config.RoofModel{ Enabled: true, Command: cmd, - GeotorgetUsername: "u", GeotorgetToken: "t", TimeoutS: 1, + StacUsername: "u", StacPassword: "t", TimeoutS: 1, }) start := time.Now() @@ -509,3 +509,93 @@ func TestToPVArraysMatchesConfigShape(t *testing.T) { t.Error("nil model must yield nil arrays") } } + +// A config written before the basic-auth redesign still derives: the legacy +// geotorget_* keys resolve through the accessors and the secret reaches the +// module as --password, whatever key it was stored under. +func TestDeriveAcceptsLegacyGeotorgetKeys(t *testing.T) { + dir := t.TempDir() + record := dir + string(os.PathSeparator) + "invocation.json" + cmd := stubModule(t, "record", record) + s := svc(t, &config.RoofModel{ + Enabled: true, Command: cmd, ModuleDir: dir, + GeotorgetUsername: "legacy-op", GeotorgetToken: "legacy-secret", + }) + + if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, ""); err != nil { + t.Fatal(err) + } + + raw, err := os.ReadFile(record) + if err != nil { + t.Fatalf("stub recorded nothing: %v", err) + } + var got stubInvocation + if err := json.Unmarshal(raw, &got); err != nil { + t.Fatal(err) + } + line := strings.Join(got.Args, " ") + for _, want := range []string{"--username legacy-op", "--password legacy-secret"} { + if !strings.Contains(line, want) { + t.Errorf("args %q missing %q", line, want) + } + } + if strings.Contains(line, "--token") { + t.Errorf("args %q still use the retired --token flag", line) + } +} + +// A custom STAC catalog lifts the Sweden-only gate — FTW cannot know what a +// third-party catalog covers — and every stac_* setting must cross the +// process boundary. +func TestDeriveCustomCatalogSkipsSwedenGateAndPassesStacArgs(t *testing.T) { + dir := t.TempDir() + record := dir + string(os.PathSeparator) + "invocation.json" + cmd := stubModule(t, "record", record) + s := svc(t, &config.RoofModel{ + Enabled: true, Command: cmd, ModuleDir: dir, + StacUsername: "u", StacPassword: "p", + StacBaseURL: "https://stac.example.org", + StacBuildingsCollection: "buildings-vector", + StacLidarCollection: "lidar-pointcloud", + StacBboxEPSG: 4326, + }) + + // Berlin: outside Lantmäteriet coverage, fine for a custom catalog. + if _, err := s.Derive(context.Background(), 52.52, 13.40, ""); err != nil { + t.Fatal(err) + } + + raw, err := os.ReadFile(record) + if err != nil { + t.Fatalf("stub recorded nothing: %v", err) + } + var got stubInvocation + if err := json.Unmarshal(raw, &got); err != nil { + t.Fatal(err) + } + line := strings.Join(got.Args, " ") + for _, want := range []string{ + "--stac-base-url https://stac.example.org", + "--buildings-collection buildings-vector", + "--lidar-collection lidar-pointcloud", + "--bbox-epsg 4326", + } { + if !strings.Contains(line, want) { + t.Errorf("args %q missing %q", line, want) + } + } +} + +// Without a custom catalog the Sweden gate still holds — the redesign must +// not have quietly opened the default catalog to the whole planet. +func TestDeriveDefaultCatalogStillRefusesOutsideSweden(t *testing.T) { + s := svc(t, &config.RoofModel{ + Enabled: true, Command: "no-such-command", + StacUsername: "u", StacPassword: "p", + }) + _, err := s.Derive(context.Background(), 52.52, 13.40, "") + if !errors.Is(err, ErrOutsideCoverage) { + t.Errorf("err = %v, want ErrOutsideCoverage", err) + } +} diff --git a/roofmodel/ftw_roofmodel/__main__.py b/roofmodel/ftw_roofmodel/__main__.py index 7b04e6406..b5dc71f81 100644 --- a/roofmodel/ftw_roofmodel/__main__.py +++ b/roofmodel/ftw_roofmodel/__main__.py @@ -12,7 +12,14 @@ import sys from .buildings import DEFAULT_SEARCH_RADIUS_M, search_buildings -from .geotorget import Credentials, GeotorgetClient, GeotorgetError +from .geotorget import ( + COLLECTION_BUILDINGS, + COLLECTION_LIDAR, + DEFAULT_BASE_URL, + Credentials, + GeotorgetClient, + GeotorgetError, +) from .pipeline import SCHEMA_VERSION, RoofModelError, derive @@ -32,22 +39,52 @@ def main(argv: list[str] | None = None) -> int: help="footprint to clip the LiDAR to, from a --mode buildings run", ) p.add_argument("--search-radius-m", type=float, default=DEFAULT_SEARCH_RADIUS_M) - p.add_argument("--username", default="", help="Geotorget username") - p.add_argument("--token", default="", help="Geotorget token/password") + p.add_argument("--username", default="", help="STAC catalog username (Geotorget account)") + p.add_argument( + "--password", + "--token", # legacy spelling, from when the credential was assumed to be a token + dest="password", + default="", + help="STAC catalog password, sent as HTTP Basic auth (Geotorget account password)", + ) p.add_argument("--radius-m", type=float, default=40.0) p.add_argument("--packing-factor", type=float, default=0.70) p.add_argument("--module-w-per-m2", type=float, default=200.0) + p.add_argument( + "--stac-base-url", + default=DEFAULT_BASE_URL, + help="STAC API root; search is POST {base}/search (default: Lantmäteriet)", + ) + p.add_argument( + "--buildings-collection", + default=COLLECTION_BUILDINGS, + help="collection id for building footprints", + ) + p.add_argument( + "--lidar-collection", + default=COLLECTION_LIDAR, + help="collection id for LiDAR point clouds", + ) + p.add_argument( + "--bbox-epsg", + type=int, + choices=(3006, 4326), + default=3006, + help="CRS of the search bbox: 3006 for Lantmäteriet, 4326 per the STAC spec", + ) args = p.parse_args(argv) - credentials = Credentials(args.username, args.token) + credentials = Credentials(args.username, args.password) try: if args.mode == "buildings": - client = GeotorgetClient(credentials) + client = GeotorgetClient(credentials, base_url=args.stac_base_url) found = search_buildings( client, latitude=args.lat, longitude=args.lon, radius_m=args.search_radius_m, + collection=args.buildings_collection, + bbox_epsg=args.bbox_epsg, ) payload = { "schema_version": SCHEMA_VERSION, @@ -63,6 +100,10 @@ def main(argv: list[str] | None = None) -> int: packing_factor=args.packing_factor, module_w_per_m2=args.module_w_per_m2, building_id=args.building_id or None, + base_url=args.stac_base_url, + buildings_collection=args.buildings_collection, + lidar_collection=args.lidar_collection, + bbox_epsg=args.bbox_epsg, ) except (GeotorgetError, RoofModelError) as exc: json.dump({"error": str(exc), "kind": type(exc).__name__}, sys.stderr) diff --git a/roofmodel/ftw_roofmodel/buildings.py b/roofmodel/ftw_roofmodel/buildings.py index 469e11278..1961123c1 100644 --- a/roofmodel/ftw_roofmodel/buildings.py +++ b/roofmodel/ftw_roofmodel/buildings.py @@ -298,11 +298,12 @@ def search_buildings( longitude: float, radius_m: float = DEFAULT_SEARCH_RADIUS_M, limit: int = 50, + collection: str = COLLECTION_BUILDINGS, + bbox_epsg: int = 3006, ) -> list[Building]: """Building footprints near a site, nearest first.""" - south, west, north, east = sweref.metre_box_around(latitude, longitude, radius_m) - bbox = sweref.bbox_wgs84_to_sweref99tm(south, west, north, east) - items = client.search(COLLECTION_BUILDINGS, bbox, limit=limit) + bbox = sweref.stac_search_bbox(latitude, longitude, radius_m, bbox_epsg) + items = client.search(collection, bbox, limit=limit) features: list[dict[str, Any]] = [] for item in items: features.extend(_features_from_item(item, client)) diff --git a/roofmodel/ftw_roofmodel/geotorget.py b/roofmodel/ftw_roofmodel/geotorget.py index 72553ea0a..bca88fc72 100644 --- a/roofmodel/ftw_roofmodel/geotorget.py +++ b/roofmodel/ftw_roofmodel/geotorget.py @@ -1,14 +1,26 @@ -"""Lantmaeteriet Geotorget access: authentication and STAC search. +"""STAC catalog access: authentication, search, and asset download. -Two products are used, both free open data (CC BY 4.0) but both gated behind a -Geotorget account the operator orders themselves: +The default catalog is Lantmaeteriet's Geotorget, where two products are used, +both free open data (CC BY 4.0) but both gated behind a Geotorget account the +operator orders themselves: * *Byggnad Nedladdning, vektor* -- building footprint polygons. * *Laserdata Nedladdning, Skog* -- airborne LiDAR, 1-2 points/m2, from 2018. -Credentials are the operator's own and are never shipped, logged or echoed back -through the API. FTW stores them the same way it stores `weather.api_key`, and -redacts them in config responses. +Authentication is HTTP Basic with the operator's own Geotorget account +username and password: Lantmaeteriet provides no OAuth for its STAC download +APIs, so the account credential is the only door. Credentials are the +operator's own and are never shipped, logged or echoed back through the API. +FTW stores them the same way it stores `weather.api_key`, and redacts them in +config responses. + +Nothing below is Lantmaeteriet-specific beyond the defaults: search is the +standard `POST {base}/search` of the STAC API spec and downloads follow asset +hrefs, so any STAC-conformant catalog behind Basic auth (or none) works by +pointing `base_url` and the collection ids elsewhere. The one non-standard +wrinkle is the search bbox CRS -- the spec mandates WGS84, Lantmaeteriet +expects SWEREF 99 TM -- which is why callers choose the bbox they send (see +`--bbox-epsg` in __main__). Only `requests` is used. The STAC API is plain JSON over HTTP, so pulling in pystac-client would add a dependency for a search body we can write in six @@ -22,7 +34,8 @@ import datetime as dt from typing import Any, Iterable -DEFAULT_BASE_URL = "https://api.lantmateriet.se" +# Root of the STAC API. The standard search endpoint is POST {base}/search. +DEFAULT_BASE_URL = "https://api.lantmateriet.se/stac" # Collection ids as published in Lantmaeteriet's STAC catalogue. Both products # are STAC APIs over the same base URL and the same credentials; they differ @@ -66,9 +79,10 @@ class Credentials: def validate(self) -> None: if not self.username or not self.password: raise MissingCredentials( - "Geotorget username and token are both required; order access at " - "https://geotorget.lantmateriet.se and set roofmodel.geotorget_username " - "and roofmodel.geotorget_token" + "a STAC username and password are both required; for Lantmäteriet, " + "order access at https://geotorget.lantmateriet.se and set " + "roofmodel.stac_username and roofmodel.stac_password to your " + "Geotorget account credentials" ) @@ -203,8 +217,13 @@ def _item_from_feature(feature: dict[str, Any]) -> StacItem: ) -class GeotorgetClient: - """Thin STAC client for Lantmaeteriet's download APIs.""" +class StacClient: + """Thin client for a STAC search-and-download API over HTTP Basic auth. + + Defaults target Lantmaeteriet's Geotorget catalog, but nothing here + depends on it: base_url and the collection ids passed to `search` are the + whole coupling. + """ def __init__( self, @@ -232,29 +251,31 @@ def session(self) -> Any: def search( self, collection: str, - bbox_sweref: tuple[float, float, float, float], + bbox: tuple[float, float, float, float], limit: int = 20, ) -> list[StacItem]: - """POST /stac/search for one collection over a SWEREF 99 TM bbox. + """POST {base}/search for one collection over a bbox. - bbox is (min_easting, min_northing, max_easting, max_northing); the - catalogue is published in EPSG:3006, so no reprojection happens here. + The bbox is (min_x, min_y, max_x, max_y) in whatever CRS the catalog + expects -- the STAC spec says WGS84 lon/lat, Lantmaeteriet expects + SWEREF 99 TM -- so the caller chooses what to send and no reprojection + happens here. """ body = { "collections": [collection], - "bbox": list(bbox_sweref), + "bbox": list(bbox), "limit": limit, } - url = f"{self._base_url}/stac/search" + url = f"{self._base_url}/search" try: resp = self._session.post(url, json=body, timeout=self._timeout) except Exception as exc: # network, DNS, TLS raise GeotorgetError(f"STAC search failed: {exc}") from exc if resp.status_code in (401, 403): raise MissingCredentials( - f"Geotorget rejected the credentials for {collection} " - f"(HTTP {resp.status_code}). Check the account has ordered access " - "to this product." + f"the STAC catalog rejected the credentials for {collection} " + f"(HTTP {resp.status_code}). Check the username and password, and " + "that the account has ordered access to this product." ) if resp.status_code != 200: raise GeotorgetError(f"STAC search returned HTTP {resp.status_code}") @@ -276,3 +297,7 @@ def newest_capture(items: Iterable[StacItem]) -> dt.datetime | None: """Most recent known capture date across items, or None if none carry one.""" dates = [i.captured_at for i in items if i.captured_at is not None] return max(dates) if dates else None + + +# The client predates its generalization; the old name stays importable. +GeotorgetClient = StacClient diff --git a/roofmodel/ftw_roofmodel/pipeline.py b/roofmodel/ftw_roofmodel/pipeline.py index 639d9b657..d31f32286 100644 --- a/roofmodel/ftw_roofmodel/pipeline.py +++ b/roofmodel/ftw_roofmodel/pipeline.py @@ -25,6 +25,7 @@ search_buildings, ) from .geotorget import ( + COLLECTION_BUILDINGS, COLLECTION_LIDAR, Credentials, GeotorgetClient, @@ -187,6 +188,10 @@ def derive( module_w_per_m2: float = DEFAULT_MODULE_W_PER_M2, building_id: str | None = None, now: dt.datetime | None = None, + base_url: str = geotorget.DEFAULT_BASE_URL, + buildings_collection: str = COLLECTION_BUILDINGS, + lidar_collection: str = COLLECTION_LIDAR, + bbox_epsg: int = 3006, ) -> dict[str, Any]: """Derive a roof model for one site and return it as a JSON-ready dict. @@ -194,14 +199,19 @@ def derive( clip the LiDAR to that footprint before segmenting. Without it the whole radius is segmented, which will happily return the neighbour's roof and lets coplanar buildings steal each other's points; see buildings.py. + + The catalog parameters default to Lantmaeteriet; any STAC-conformant + catalog can stand in (see geotorget.py), as long as its data arrives in + SWEREF 99 TM -- the segmentation works in that frame. """ if client is None: - client = GeotorgetClient(credentials) + client = GeotorgetClient(credentials, base_url=base_url) chosen: Building | None = None if building_id: candidates = search_buildings( - client, latitude=latitude, longitude=longitude, radius_m=radius_m + client, latitude=latitude, longitude=longitude, radius_m=radius_m, + collection=buildings_collection, bbox_epsg=bbox_epsg, ) chosen = next((b for b in candidates if b.building_id == building_id), None) if chosen is None: @@ -210,17 +220,17 @@ def derive( "have been picked against a different coordinate" ) - south, west, north, east = sweref.metre_box_around(latitude, longitude, radius_m) - bbox = sweref.bbox_wgs84_to_sweref99tm(south, west, north, east) + bbox = sweref.stac_search_bbox(latitude, longitude, radius_m, bbox_epsg) try: - lidar_items: list[StacItem] = client.search(COLLECTION_LIDAR, bbox) + lidar_items: list[StacItem] = client.search(lidar_collection, bbox) except GeotorgetError: raise if not lidar_items: + hint = "; Lantmaeteriet data is Sweden only" if lidar_collection == COLLECTION_LIDAR else "" raise RoofModelError( - f"no LiDAR tiles cover ({latitude:.5f}, {longitude:.5f}); " - "Lantmaeteriet data is Sweden only" + f"no LiDAR tiles cover ({latitude:.5f}, {longitude:.5f}) in " + f"collection {lidar_collection!r}{hint}" ) points, fetch = _read_lidar(client, lidar_items, chosen) @@ -248,8 +258,8 @@ def derive( "schema_version": SCHEMA_VERSION, "site": {"latitude": latitude, "longitude": longitude, "radius_m": radius_m}, "source": { - "provider": "lantmateriet", - "collection": COLLECTION_LIDAR, + "provider": "lantmateriet" if base_url == geotorget.DEFAULT_BASE_URL else base_url, + "collection": lidar_collection, "item_count": len(lidar_items), "dataset_datetime": captured.isoformat() if captured else None, # "copc-window" means only the footprint's neighbourhood was moved diff --git a/roofmodel/ftw_roofmodel/sweref.py b/roofmodel/ftw_roofmodel/sweref.py index 3c2f699d6..2d5e8f296 100644 --- a/roofmodel/ftw_roofmodel/sweref.py +++ b/roofmodel/ftw_roofmodel/sweref.py @@ -160,6 +160,24 @@ def bbox_wgs84_to_sweref99tm( return min(eastings), min(northings), max(eastings), max(northings) +def stac_search_bbox( + lat: float, lon: float, radius_m: float, bbox_epsg: int = 3006 +) -> tuple[float, float, float, float]: + """Bounding box around a site, in the CRS a STAC catalog expects. + + EPSG:3006 (the default) is what Lantmaeteriet's catalog takes; EPSG:4326 + in lon/lat order is what the STAC spec itself mandates, for catalogs that + follow it. Anything else would need a projection stack this module + deliberately does not carry. + """ + south, west, north, east = metre_box_around(lat, lon, radius_m) + if bbox_epsg == 4326: + return (west, south, east, north) + if bbox_epsg == 3006: + return bbox_wgs84_to_sweref99tm(south, west, north, east) + raise ValueError(f"unsupported bbox EPSG {bbox_epsg}; use 3006 or 4326") + + def metre_box_around(lat: float, lon: float, radius_m: float) -> tuple[float, float, float, float]: """Return a WGS84 (min_lat, min_lon, max_lat, max_lon) box of +/- radius_m. diff --git a/roofmodel/tests/test_pipeline.py b/roofmodel/tests/test_pipeline.py index 570071001..ab7b77b02 100644 --- a/roofmodel/tests/test_pipeline.py +++ b/roofmodel/tests/test_pipeline.py @@ -102,11 +102,25 @@ def test_search_sends_the_collection_and_bbox(): assert len(items) == 1 (url, body), = session.posts + # The default catalog root is Lantmaeteriet's /stac, so the standard + # {base}/search lands on the same URL it always has. assert url.endswith("/stac/search") assert body["collections"] == [COLLECTION_LIDAR] assert body["bbox"] == [600000.0, 6500000.0, 600100.0, 6500100.0] +def test_search_speaks_standard_stac_to_a_custom_catalog(): + """Any STAC-conformant catalog works: base_url and collection ids are the + whole coupling, and search is the spec's POST {base}/search.""" + session = FakeSession(search={"features": []}) + client = GeotorgetClient(CREDS, session=session, base_url="https://stac.example.org/") + client.search("lidar-pointcloud", (5.0, 50.0, 6.0, 51.0)) + + (url, body), = session.posts + assert url == "https://stac.example.org/search" + assert body["collections"] == ["lidar-pointcloud"] + + def test_capture_date_is_read_from_stac_datetime(): session = FakeSession(search={"features": [feature(datetime_value="2019-04-02T09:30:00Z")]}) items = GeotorgetClient(CREDS, session=session).search(COLLECTION_LIDAR, (0, 0, 1, 1)) diff --git a/roofmodel/tests/test_sweref.py b/roofmodel/tests/test_sweref.py index f6f20a4b1..1cd6dfa35 100644 --- a/roofmodel/tests/test_sweref.py +++ b/roofmodel/tests/test_sweref.py @@ -110,6 +110,31 @@ def test_metre_box_brackets_its_centre(): assert west < 18.07 < east +def test_stac_search_bbox_sweref_matches_the_long_form(): + from ftw_roofmodel.sweref import bbox_wgs84_to_sweref99tm, stac_search_bbox + + south, west, north, east = metre_box_around(59.33, 18.07, 40.0) + assert stac_search_bbox(59.33, 18.07, 40.0) == bbox_wgs84_to_sweref99tm( + south, west, north, east + ) + + +def test_stac_search_bbox_wgs84_is_lon_lat_ordered(): + """The STAC spec's bbox is [west, south, east, north] in degrees.""" + from ftw_roofmodel.sweref import stac_search_bbox + + west, south, east, north = stac_search_bbox(59.33, 18.07, 40.0, bbox_epsg=4326) + assert west < 18.07 < east + assert south < 59.33 < north + + +def test_stac_search_bbox_refuses_a_crs_it_cannot_produce(): + from ftw_roofmodel.sweref import stac_search_bbox + + with pytest.raises(ValueError): + stac_search_bbox(59.33, 18.07, 40.0, bbox_epsg=3857) + + def test_degree_box_would_have_been_wrong_at_high_latitude(): """Guards the reason metre_box_around exists: a fixed degree offset gives wildly different ground distances at Malmoe and Kiruna, so anyone tempted to diff --git a/web/settings/tabs/weather.js b/web/settings/tabs/weather.js index 05a8b7446..57edfae52 100644 --- a/web/settings/tabs/weather.js +++ b/web/settings/tabs/weather.js @@ -300,20 +300,23 @@ function roofFieldset(ctx) { var field = ctx.field, help = ctx.help, config = ctx.config; if (!config.roofmodel) config.roofmodel = {}; - var stored = config.roofmodel.has_geotorget_token; + var stored = config.roofmodel.has_stac_password; return '
Roof geometry from Lantmäteriet ' + help( - 'Optional and Sweden-only. Reads the tilt and azimuth of each roof face from ' + + 'Optional. Reads the tilt and azimuth of each roof face from ' + 'Lantmäteriet\'s laser scanning data (Laserdata Skog) for a building you pick on ' + 'the map, and fills in the PV arrays above. Needs a free Geotorget account with ' + - 'access to "Byggnad Nedladdning, vektor" and "Laserdata Nedladdning, Skog".') + + 'access to "Byggnad Nedladdning, vektor" and "Laserdata Nedladdning, Skog" — ' + + 'sign in with the account\'s own username and password (Lantmäteriet offers no ' + + 'OAuth for these APIs). Other countries\' STAC catalogs can be configured in ' + + 'the config file via roofmodel.stac_base_url.') + '' + '' + '
' + - field("Geotorget username", "roofmodel.geotorget_username", "text", "") + + field("Geotorget username", "roofmodel.stac_username", "text", "") + '
' + - field(stored ? "Geotorget token (stored — type to replace)" : "Geotorget token", - "roofmodel.geotorget_token", "password", "") + + field(stored ? "Geotorget password (stored — type to replace)" : "Geotorget password", + "roofmodel.stac_password", "password", "") + '
' + '
' + '' + From 382298c505c80860a410a3631564178484f63013 Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Wed, 2 Sep 2026 12:03:00 +0200 Subject: [PATCH 15/26] feat(roofmodel): anonymous access to open STAC catalogs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Open catalogs are the rule, not the exception: France's LiDAR HD (COPC via the MTD STAC API), Carinthia's KAGIS and swisstopo all answer STAC searches and serve point-cloud assets with no credentials at all. So a custom stac_base_url no longer demands a username and password: with neither half of a credential stored, core omits --username/--password entirely and the module's session goes anonymous. The default Lantmäteriet catalog still requires the operator's own Geotorget account, with the ordering instructions raised before any request. Half a credential stays an error everywhere, and a 401/403 on an anonymous catalog says to configure credentials rather than to check them. has_credentials on GET /api/roofmodel now reports "the catalog is usable as configured", so the UI stops asking for credentials an open catalog does not need. docs/roof-geometry.md gains the table of catalogs verified live today, with the honest caveats per catalog. Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com> --- .changeset/roofmodel-stac-basic-auth.md | 4 ++- docs/roof-geometry.md | 22 ++++++++++++++++ go/internal/api/api.go | 9 +++++-- go/internal/api/api_roofmodel_test.go | 16 +++++++++++ go/internal/roofmodel/roofmodel.go | 13 ++++++--- go/internal/roofmodel/roofmodel_test.go | 35 +++++++++++++++++++++++++ roofmodel/ftw_roofmodel/geotorget.py | 19 +++++++++++--- roofmodel/tests/test_pipeline.py | 28 ++++++++++++++++++++ web/settings/tabs/weather.js | 3 ++- 9 files changed, 139 insertions(+), 10 deletions(-) diff --git a/.changeset/roofmodel-stac-basic-auth.md b/.changeset/roofmodel-stac-basic-auth.md index 7bd1bd2b4..e19a5cb86 100644 --- a/.changeset/roofmodel-stac-basic-auth.md +++ b/.changeset/roofmodel-stac-basic-auth.md @@ -12,4 +12,6 @@ The STAC client is catalog-agnostic while it is at it: `stac_base_url`, `stac_buildings_collection`, `stac_lidar_collection` and `stac_bbox_epsg` point FTW at any STAC-conformant catalog (search is the spec's `POST {base}/search`), with Lantmäteriet as the default. A custom catalog also -lifts the Sweden-only coordinate gate. +lifts the Sweden-only coordinate gate, and needs no credentials at all when it +is open — anonymous access is only refused for the default Lantmäteriet +catalog, which always requires the operator's own account. diff --git a/docs/roof-geometry.md b/docs/roof-geometry.md index 9c9bd1d5c..c20020477 100644 --- a/docs/roof-geometry.md +++ b/docs/roof-geometry.md @@ -66,6 +66,28 @@ roofmodel: Setting `stac_base_url` also lifts the Sweden-only coordinate gate, since FTW cannot know what a third-party catalog covers. +### Catalogs verified to answer (2026-09-02) + +Each row was checked live: the STAC landing page, an item search over a real +bbox, and an anonymous HEAD on a returned asset. + +| Catalog | LiDAR | Auth | Licence | Caveat | +|---|---|---|---|---| +| [Lantmäteriet](https://api.lantmateriet.se/stac) (default) | LAZ/COPC | HTTP Basic (Geotorget account) | CC BY 4.0 | Sweden; bbox in EPSG:3006 | +| [IGN LiDAR HD via MTD](https://api.stac.teledetection.fr) (`lidarhd`) | COPC, served by IGN itself | none | etalab-2.0 | France; catalog is run by Université de Montpellier, not IGN, and IGN's download host rate-limits (~1 req/s) | +| [KAGIS Carinthia](https://gis.ktn.gv.at/api/stac/v1/) (`KAGIS_coll_ALS2_pc_*`) | COPC | none | CC BY 4.0 | four Alpine regions only; ~700 MB tiles, so the COPC window path matters | +| [swisstopo](https://data.geo.admin.ch/api/stac/v1) (`ch.swisstopo.swisssurface3d`) | LAS zipped as `.las.zip` | none | swisstopo open data terms | Switzerland; the zip wrapper is not yet unpacked by the module, so this one is search-verified but not derive-ready | + +USGS 3DEP (`3dep-lidar-copc` on the [Planetary +Computer](https://planetarycomputer.microsoft.com/api/stac/v1)) is one small +extension away: search is anonymous, but asset downloads need a SAS token +fetched from an open endpoint and appended to the URL. + +Building footprints are the scarce half. No other verified catalog serves them +as GeoPackage or GeoJSON — swisstopo publishes DXF/FileGDB, Microsoft and +Overture publish GeoParquet — so outside Sweden the practical path today is a +LiDAR-only catalog plus drawing the outline yourself on the map. + Two caveats. The search bbox CRS is per-catalog: the STAC spec mandates WGS84 (`stac_bbox_epsg: 4326`), but Lantmäteriet expects SWEREF 99 TM, which is why the default stays `3006`. And the *data* itself must arrive in SWEREF 99 TM diff --git a/go/internal/api/api.go b/go/internal/api/api.go index 84c21fb58..a5d0f3fbb 100644 --- a/go/internal/api/api.go +++ b/go/internal/api/api.go @@ -2545,8 +2545,10 @@ func roofModelErrorStatus(err error) int { return 502 } -// roofModelHasCredentials reports whether STAC catalog credentials are -// stored, without revealing them. +// roofModelHasCredentials reports whether the configured STAC catalog can be +// asked, without revealing any secret: credentials are stored, or a custom +// catalog is configured — open catalogs need none, and the module goes +// anonymous. The default Lantmäteriet catalog always needs credentials. func (s *Server) roofModelHasCredentials() bool { if s.deps.CfgMu == nil { return false @@ -2557,6 +2559,9 @@ func (s *Server) roofModelHasCredentials() bool { return false } rm := s.deps.Cfg.RoofModel + if rm.StacBaseURL != "" { + return true + } return rm.StacUser() != "" && rm.StacPass() != "" } diff --git a/go/internal/api/api_roofmodel_test.go b/go/internal/api/api_roofmodel_test.go index e24c9ff2b..13d5df76b 100644 --- a/go/internal/api/api_roofmodel_test.go +++ b/go/internal/api/api_roofmodel_test.go @@ -151,6 +151,22 @@ func TestRoofModelNeverEchoesTheToken(t *testing.T) { } } +// An open custom catalog is usable with no stored secret at all, and the UI +// must not keep asking for credentials it does not need. +func TestRoofModelCustomCatalogCountsAsCredentialed(t *testing.T) { + deps := depsAt(52.52, 13.40) + deps.Cfg.RoofModel = &config.RoofModel{ + Enabled: true, StacBaseURL: "https://stac.example.org", + } + deps.RoofModel = roofmodel.FromConfig(deps.Cfg.RoofModel) + + _, status := getJSON(t, deps, http.MethodGet, "/api/roofmodel") + if status["has_credentials"] != true { + t.Errorf("has_credentials = %v, want true for an anonymous open catalog", + status["has_credentials"]) + } +} + // Lantmäteriet must appear in the coverage listing alongside every other // source, so an operator finds it without knowing it exists. func TestLantmaterietAppearsInDataSources(t *testing.T) { diff --git a/go/internal/roofmodel/roofmodel.go b/go/internal/roofmodel/roofmodel.go index 0955a41fb..0bb839d92 100644 --- a/go/internal/roofmodel/roofmodel.go +++ b/go/internal/roofmodel/roofmodel.go @@ -226,7 +226,10 @@ func (s *Service) run(ctx context.Context, lat, lon float64, mode, buildingID st if s.cfg.StacBaseURL == "" && !coverage.Covers("lantmateriet", lat, lon) { return nil, fmt.Errorf("%w: (%.4f, %.4f) is not in Sweden", ErrOutsideCoverage, lat, lon) } - if s.cfg.StacUser() == "" || s.cfg.StacPass() == "" { + // Lantmäteriet always needs the operator's own Geotorget credentials. A + // custom catalog may be open — many national STAC catalogs are — so with a + // base URL set, absent credentials mean anonymous access, not a mistake. + if s.cfg.StacBaseURL == "" && (s.cfg.StacUser() == "" || s.cfg.StacPass() == "") { return nil, ErrNoCredentials } @@ -238,14 +241,18 @@ func (s *Service) run(ctx context.Context, lat, lon float64, mode, buildingID st "--mode", mode, "--lat", fmt.Sprintf("%.6f", lat), "--lon", fmt.Sprintf("%.6f", lon), - "--username", s.cfg.StacUser(), - "--password", s.cfg.StacPass(), "--radius-m", fmt.Sprintf("%.1f", s.radius()), "--packing-factor", fmt.Sprintf("%.3f", s.packingFactor()), } if buildingID != "" { args = append(args, "--building-id", buildingID) } + if u := s.cfg.StacUser(); u != "" { + args = append(args, "--username", u) + } + if p := s.cfg.StacPass(); p != "" { + args = append(args, "--password", p) + } // A custom catalog replaces the Lantmäteriet defaults piecewise; anything // left empty falls back to the module's own Geotorget defaults. if s.cfg.StacBaseURL != "" { diff --git a/go/internal/roofmodel/roofmodel_test.go b/go/internal/roofmodel/roofmodel_test.go index a0e670100..bc40c7120 100644 --- a/go/internal/roofmodel/roofmodel_test.go +++ b/go/internal/roofmodel/roofmodel_test.go @@ -587,6 +587,41 @@ func TestDeriveCustomCatalogSkipsSwedenGateAndPassesStacArgs(t *testing.T) { } } +// An open catalog needs no credentials: with a custom base URL and neither +// half of a credential stored, the derive runs anonymously and no empty +// --username/--password ever reaches the command line. +func TestDeriveCustomCatalogWorksAnonymously(t *testing.T) { + dir := t.TempDir() + record := dir + string(os.PathSeparator) + "invocation.json" + cmd := stubModule(t, "record", record) + s := svc(t, &config.RoofModel{ + Enabled: true, Command: cmd, ModuleDir: dir, + StacBaseURL: "https://stac.example.org", + }) + + if _, err := s.Derive(context.Background(), 52.52, 13.40, ""); err != nil { + t.Fatal(err) + } + + raw, err := os.ReadFile(record) + if err != nil { + t.Fatalf("stub recorded nothing: %v", err) + } + var got stubInvocation + if err := json.Unmarshal(raw, &got); err != nil { + t.Fatal(err) + } + line := strings.Join(got.Args, " ") + if !strings.Contains(line, "--stac-base-url https://stac.example.org") { + t.Errorf("args %q missing the base URL", line) + } + for _, banned := range []string{"--username", "--password"} { + if strings.Contains(line, banned) { + t.Errorf("args %q carry %s despite no stored credential", line, banned) + } + } +} + // Without a custom catalog the Sweden gate still holds — the redesign must // not have quietly opened the default catalog to the whole planet. func TestDeriveDefaultCatalogStillRefusesOutsideSweden(t *testing.T) { diff --git a/roofmodel/ftw_roofmodel/geotorget.py b/roofmodel/ftw_roofmodel/geotorget.py index bca88fc72..9b1ddabc3 100644 --- a/roofmodel/ftw_roofmodel/geotorget.py +++ b/roofmodel/ftw_roofmodel/geotorget.py @@ -232,15 +232,22 @@ def __init__( base_url: str = DEFAULT_BASE_URL, timeout: float = 60.0, ) -> None: - credentials.validate() - self._credentials = credentials self._base_url = base_url.rstrip("/") + # Lantmäteriet always demands the operator's Geotorget account, so + # missing credentials there deserve the ordering instructions early. + # A custom catalog may be open: no credentials means anonymous access, + # while half a credential is still an error either way. + self._anonymous = not (credentials.username or credentials.password) + if not self._anonymous or self._base_url == DEFAULT_BASE_URL: + credentials.validate() + self._credentials = credentials self._timeout = timeout if session is None: import requests # imported lazily so tests can inject a fake session session = requests.Session() - session.auth = (credentials.username, credentials.password) + if not self._anonymous: + session.auth = (credentials.username, credentials.password) self._session = session @property @@ -272,6 +279,12 @@ def search( except Exception as exc: # network, DNS, TLS raise GeotorgetError(f"STAC search failed: {exc}") from exc if resp.status_code in (401, 403): + if self._anonymous: + raise MissingCredentials( + f"the STAC catalog requires credentials for {collection} " + f"(HTTP {resp.status_code}). Set roofmodel.stac_username " + "and roofmodel.stac_password for this catalog." + ) raise MissingCredentials( f"the STAC catalog rejected the credentials for {collection} " f"(HTTP {resp.status_code}). Check the username and password, and " diff --git a/roofmodel/tests/test_pipeline.py b/roofmodel/tests/test_pipeline.py index ab7b77b02..c05d8406a 100644 --- a/roofmodel/tests/test_pipeline.py +++ b/roofmodel/tests/test_pipeline.py @@ -79,6 +79,34 @@ def test_missing_credentials_are_rejected_before_any_request(): assert session.posts == [], "must not contact Geotorget without credentials" +def test_open_catalog_needs_no_credentials(): + session = FakeSession(search={"features": []}) + client = GeotorgetClient( + Credentials("", ""), session=session, base_url="https://stac.example.org" + ) + client.search(COLLECTION_LIDAR, (0, 0, 1, 1)) + assert session.posts, "anonymous search must reach an open catalog" + + +def test_open_catalog_still_refuses_half_a_credential(): + for creds in (Credentials("u", ""), Credentials("", "p")): + with pytest.raises(MissingCredentials): + GeotorgetClient( + creds, session=FakeSession(), base_url="https://stac.example.org" + ) + + +def test_anonymous_rejection_says_to_configure_credentials(): + client = GeotorgetClient( + Credentials("", ""), + session=FakeSession(status=401), + base_url="https://stac.example.org", + ) + with pytest.raises(MissingCredentials) as exc: + client.search(COLLECTION_LIDAR, (0, 0, 1, 1)) + assert "stac_username" in str(exc.value) + + def test_rejected_credentials_say_what_to_check(): client = GeotorgetClient(CREDS, session=FakeSession(status=403)) with pytest.raises(MissingCredentials) as exc: diff --git a/web/settings/tabs/weather.js b/web/settings/tabs/weather.js index 57edfae52..79ee20913 100644 --- a/web/settings/tabs/weather.js +++ b/web/settings/tabs/weather.js @@ -308,7 +308,8 @@ 'access to "Byggnad Nedladdning, vektor" and "Laserdata Nedladdning, Skog" — ' + 'sign in with the account\'s own username and password (Lantmäteriet offers no ' + 'OAuth for these APIs). Other countries\' STAC catalogs can be configured in ' + - 'the config file via roofmodel.stac_base_url.') + + 'the config file via roofmodel.stac_base_url — open catalogs need no ' + + 'credentials, so both fields stay empty.') + '' + '' + From 160eca2a8a7c4e76b43cefb32f1c5c142900d2f4 Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Wed, 2 Sep 2026 19:01:41 +0200 Subject: [PATCH 16/26] fix(roofmodel): follow config hot-reload, and read the module's error line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two faults that live testing with a real Geotorget account surfaced. Credentials typed into Settings never reached the service: it was built once at boot — nil when disabled — and no reload ever touched it, while GET /api/roofmodel read the live config and claimed has_credentials. The service is now always constructed, keeps its config behind a mutex, and the hot-reload applier swaps it like every other service. And when the module failed for a real reason, the operator saw 'exit status 1': requests' RequestsDependencyWarning shares stderr with the module's JSON error document, and parsing the whole stream failed. The contract is one JSON document as the final line, so parse that. Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com> --- go/cmd/ftw/main.go | 16 +++- go/internal/roofmodel/roofmodel.go | 120 ++++++++++++++++-------- go/internal/roofmodel/roofmodel_test.go | 62 +++++++++++- 3 files changed, 153 insertions(+), 45 deletions(-) diff --git a/go/cmd/ftw/main.go b/go/cmd/ftw/main.go index ad97f7b69..108519b48 100644 --- a/go/cmd/ftw/main.go +++ b/go/cmd/ftw/main.go @@ -883,6 +883,12 @@ func main() { // Assigned where the OCPP server starts (optional; nil-guarded). var ocppSrv *ocpp.Server + // Forward-declared so the reload callback can hand hot-edited roofmodel + // config to the service. Geotorget credentials arrive through Settings + // while the process is running; a boot-time snapshot stranded them until + // a restart nothing announced. Assigned later (roofmodel.FromConfig). + var roofModelSvc *roofmodel.Service + // ---- Config hot-reload watcher ---- // Named because two callers share it: the fsnotify watcher created // below and POST /api/config (Deps.ConfigApplier), so a config saved @@ -990,6 +996,11 @@ func main() { }) } + // Roof-derivation module: swap in the fresh config so the enable + // toggle and Geotorget credentials saved through the API work on + // the next click. Nil-receiver safe until the service is built. + roofModelSvc.Reconfigure(newCfg.RoofModel) + // Site-meter swap propagation. The configreload watcher // already updated ctrl.SiteMeterDriver under ctrlMu before // this applier ran, so the dispatch loop reads from the @@ -1211,9 +1222,10 @@ func main() { // PV scoring. Nil when the site has no PV geometry to score against. This is // read-only with respect to control: it only fetches weather data and writes // the irradiance_history + pv_performance_daily tables. - // Optional roof-geometry module: nil unless explicitly enabled, and + // Optional roof-geometry module: always constructed (so a hot-reloaded + // enable works without a restart) but inert until config enables it, and // stateless — it only runs when an operator asks for a derive. - roofModelSvc := roofmodel.FromConfig(cfg.RoofModel) + roofModelSvc = roofmodel.FromConfig(cfg.RoofModel) pvPerfSvc := pvperf.FromConfig(cfg.Weather, ratedPVW, st, "ftw/"+Version+" github.com/srcfl/ftw") diff --git a/go/internal/roofmodel/roofmodel.go b/go/internal/roofmodel/roofmodel.go index 0bb839d92..f6ae772de 100644 --- a/go/internal/roofmodel/roofmodel.go +++ b/go/internal/roofmodel/roofmodel.go @@ -25,6 +25,7 @@ import ( "fmt" "log/slog" "os/exec" + "sync" "time" "github.com/srcfl/ftw/go/internal/config" @@ -122,45 +123,74 @@ type moduleError struct { // Service derives roof models. The zero value is unusable; use FromConfig. type Service struct { + mu sync.RWMutex cfg *config.RoofModel } -// FromConfig returns a Service, or nil when the module is not configured. A nil -// Service is safe to call: every method reports ErrDisabled. +// FromConfig returns a Service even when the module is disabled or +// unconfigured, so a later Reconfigure can enable it without a restart. A +// Service without a usable config — and a nil Service — is safe to call: +// every method reports ErrDisabled. func FromConfig(cfg *config.RoofModel) *Service { - if cfg == nil || !cfg.Enabled { + return &Service{cfg: cfg} +} + +// Reconfigure swaps the module's config so enablement or credentials saved +// through the API count on the next call rather than the next restart. The +// service used to keep its boot-time snapshot, which silently ignored +// Geotorget credentials typed into Settings while GET /api/roofmodel — +// reading the live config — reported them present. +func (s *Service) Reconfigure(cfg *config.RoofModel) { + if s == nil { + return + } + s.mu.Lock() + s.cfg = cfg + s.mu.Unlock() +} + +// config returns the current snapshot. Loaded config structs are never +// mutated in place — a reload builds a fresh one and Reconfigure swaps the +// pointer — so the snapshot stays coherent for the length of a call. +func (s *Service) config() *config.RoofModel { + if s == nil { return nil } - return &Service{cfg: cfg} + s.mu.RLock() + defer s.mu.RUnlock() + return s.cfg } // Enabled reports whether derives are possible. -func (s *Service) Enabled() bool { return s != nil && s.cfg != nil && s.cfg.Enabled } +func (s *Service) Enabled() bool { + c := s.config() + return c != nil && c.Enabled +} -func (s *Service) timeout() time.Duration { - if s.cfg.TimeoutS > 0 { - return time.Duration(s.cfg.TimeoutS) * time.Second +func timeoutFrom(cfg *config.RoofModel) time.Duration { + if cfg.TimeoutS > 0 { + return time.Duration(cfg.TimeoutS) * time.Second } return defaultTimeout } -func (s *Service) command() string { - if s.cfg.Command != "" { - return s.cfg.Command +func commandFrom(cfg *config.RoofModel) string { + if cfg.Command != "" { + return cfg.Command } return defaultCommand } -func (s *Service) radius() float64 { - if s.cfg.RadiusM > 0 { - return s.cfg.RadiusM +func radiusFrom(cfg *config.RoofModel) float64 { + if cfg.RadiusM > 0 { + return cfg.RadiusM } return defaultRadiusM } -func (s *Service) packingFactor() float64 { - if s.cfg.PackingFactor > 0 { - return s.cfg.PackingFactor +func packingFactorFrom(cfg *config.RoofModel) float64 { + if cfg.PackingFactor > 0 { + return cfg.PackingFactor } return defaultPackingFactor } @@ -217,23 +247,24 @@ func (s *Service) Derive(ctx context.Context, lat, lon float64, buildingID strin // run spawns the module and returns its stdout. func (s *Service) run(ctx context.Context, lat, lon float64, mode, buildingID string) ([]byte, error) { - if !s.Enabled() { + cfg := s.config() + if cfg == nil || !cfg.Enabled { return nil, ErrDisabled } // The Sweden gate belongs to the default Lantmäteriet catalog only. An // operator pointing at another country's STAC catalog knows what it // covers; FTW does not, so it stops pretending to. - if s.cfg.StacBaseURL == "" && !coverage.Covers("lantmateriet", lat, lon) { + if cfg.StacBaseURL == "" && !coverage.Covers("lantmateriet", lat, lon) { return nil, fmt.Errorf("%w: (%.4f, %.4f) is not in Sweden", ErrOutsideCoverage, lat, lon) } // Lantmäteriet always needs the operator's own Geotorget credentials. A // custom catalog may be open — many national STAC catalogs are — so with a // base URL set, absent credentials mean anonymous access, not a mistake. - if s.cfg.StacBaseURL == "" && (s.cfg.StacUser() == "" || s.cfg.StacPass() == "") { + if cfg.StacBaseURL == "" && (cfg.StacUser() == "" || cfg.StacPass() == "") { return nil, ErrNoCredentials } - ctx, cancel := context.WithTimeout(ctx, s.timeout()) + ctx, cancel := context.WithTimeout(ctx, timeoutFrom(cfg)) defer cancel() args := []string{ @@ -241,35 +272,35 @@ func (s *Service) run(ctx context.Context, lat, lon float64, mode, buildingID st "--mode", mode, "--lat", fmt.Sprintf("%.6f", lat), "--lon", fmt.Sprintf("%.6f", lon), - "--radius-m", fmt.Sprintf("%.1f", s.radius()), - "--packing-factor", fmt.Sprintf("%.3f", s.packingFactor()), + "--radius-m", fmt.Sprintf("%.1f", radiusFrom(cfg)), + "--packing-factor", fmt.Sprintf("%.3f", packingFactorFrom(cfg)), } if buildingID != "" { args = append(args, "--building-id", buildingID) } - if u := s.cfg.StacUser(); u != "" { + if u := cfg.StacUser(); u != "" { args = append(args, "--username", u) } - if p := s.cfg.StacPass(); p != "" { + if p := cfg.StacPass(); p != "" { args = append(args, "--password", p) } // A custom catalog replaces the Lantmäteriet defaults piecewise; anything // left empty falls back to the module's own Geotorget defaults. - if s.cfg.StacBaseURL != "" { - args = append(args, "--stac-base-url", s.cfg.StacBaseURL) + if cfg.StacBaseURL != "" { + args = append(args, "--stac-base-url", cfg.StacBaseURL) } - if s.cfg.StacBuildingsCollection != "" { - args = append(args, "--buildings-collection", s.cfg.StacBuildingsCollection) + if cfg.StacBuildingsCollection != "" { + args = append(args, "--buildings-collection", cfg.StacBuildingsCollection) } - if s.cfg.StacLidarCollection != "" { - args = append(args, "--lidar-collection", s.cfg.StacLidarCollection) + if cfg.StacLidarCollection != "" { + args = append(args, "--lidar-collection", cfg.StacLidarCollection) } - if s.cfg.StacBboxEPSG != 0 { - args = append(args, "--bbox-epsg", fmt.Sprintf("%d", s.cfg.StacBboxEPSG)) + if cfg.StacBboxEPSG != 0 { + args = append(args, "--bbox-epsg", fmt.Sprintf("%d", cfg.StacBboxEPSG)) } - cmd := exec.CommandContext(ctx, s.command(), args...) - if s.cfg.ModuleDir != "" { - cmd.Env = append(cmd.Environ(), "PYTHONPATH="+s.cfg.ModuleDir) + cmd := exec.CommandContext(ctx, commandFrom(cfg), args...) + if cfg.ModuleDir != "" { + cmd.Env = append(cmd.Environ(), "PYTHONPATH="+cfg.ModuleDir) } var stdout, stderr bytes.Buffer cmd.Stdout = &stdout @@ -278,13 +309,24 @@ func (s *Service) run(ctx context.Context, lat, lon float64, mode, buildingID st err := cmd.Run() if ctx.Err() == context.DeadlineExceeded { - return nil, fmt.Errorf("roof model timed out after %s", s.timeout()) + return nil, fmt.Errorf("roof model timed out after %s", timeoutFrom(cfg)) } if err != nil { // The module reports failures as JSON on stderr so an operator sees a - // reason ("credentials rejected") rather than a Python traceback. + // reason ("credentials rejected") rather than a Python traceback. It + // is not alone on that stream: third-party libraries write warnings + // there too (requests' RequestsDependencyWarning buried the real + // message behind a generic "exit status 1" in live testing), so when + // the whole stream doesn't parse, the module's contract — one JSON + // document as the final line — still does. var me moduleError - if jsonErr := json.Unmarshal(bytes.TrimSpace(stderr.Bytes()), &me); jsonErr == nil && me.Error != "" { + errOut := bytes.TrimSpace(stderr.Bytes()) + if jsonErr := json.Unmarshal(errOut, &me); jsonErr != nil || me.Error == "" { + if i := bytes.LastIndexByte(errOut, '\n'); i >= 0 { + _ = json.Unmarshal(bytes.TrimSpace(errOut[i+1:]), &me) + } + } + if me.Error != "" { return nil, fmt.Errorf("roof model: %s", me.Error) } return nil, fmt.Errorf("roof model failed: %w", err) diff --git a/go/internal/roofmodel/roofmodel_test.go b/go/internal/roofmodel/roofmodel_test.go index bc40c7120..e75eda32c 100644 --- a/go/internal/roofmodel/roofmodel_test.go +++ b/go/internal/roofmodel/roofmodel_test.go @@ -100,11 +100,17 @@ func svc(t *testing.T, cfg *config.RoofModel) *Service { const stockholmLat, stockholmLon = 59.33, 18.07 func TestDisabledWhenAbsentOrOff(t *testing.T) { - if FromConfig(nil) != nil { - t.Error("nil config must not produce a service") + // The service is always constructed — a later Reconfigure may enable it — + // but stays inert until config says otherwise. + if FromConfig(nil).Enabled() { + t.Error("nil config must report disabled") } - if FromConfig(&config.RoofModel{Enabled: false}) != nil { - t.Error("disabled config must not produce a service") + off := FromConfig(&config.RoofModel{Enabled: false}) + if off.Enabled() { + t.Error("disabled config must report disabled") + } + if _, err := off.Derive(context.Background(), stockholmLat, stockholmLon, ""); !errors.Is(err, ErrDisabled) { + t.Errorf("err = %v, want ErrDisabled", err) } // A nil *Service must be safe to call, not a panic. var s *Service @@ -114,6 +120,33 @@ func TestDisabledWhenAbsentOrOff(t *testing.T) { if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, ""); !errors.Is(err, ErrDisabled) { t.Errorf("err = %v, want ErrDisabled", err) } + s.Reconfigure(&config.RoofModel{Enabled: true}) // no-op, not a panic +} + +// Geotorget credentials arrive through Settings while the process runs; the +// service used to keep its boot-time config, so the very save that stored them +// changed nothing until a restart nothing asked for. Reconfigure is what the +// hot-reload applier calls — it must both deliver credentials and flip +// enablement, in both directions. +func TestReconfigureAppliesCredentialsWithoutRestart(t *testing.T) { + s := svc(t, &config.RoofModel{Enabled: true}) + if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, ""); !errors.Is(err, ErrNoCredentials) { + t.Fatalf("before reconfigure: err = %v, want ErrNoCredentials", err) + } + + s.Reconfigure(&config.RoofModel{ + Enabled: true, + Command: stubModule(t, "stdout", minimalModel), + StacUsername: "operator", StacPassword: "secret", + }) + if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, ""); err != nil { + t.Fatalf("after reconfigure with credentials: %v", err) + } + + s.Reconfigure(&config.RoofModel{Enabled: false}) + if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, ""); !errors.Is(err, ErrDisabled) { + t.Fatalf("after disabling: err = %v, want ErrDisabled", err) + } } // A site outside Sweden can never succeed, so it must fail before spawning an @@ -429,6 +462,27 @@ func TestDeriveSurfacesTheModuleErrorMessage(t *testing.T) { } } +// Third-party libraries write warnings to stderr above the module's JSON — +// requests' RequestsDependencyWarning did exactly this in live testing and +// reduced a real "STAC search returned HTTP 404" to "exit status 1". The +// module's contract is one JSON document as the final line; the parse must +// hold whatever gets printed above it. +func TestModuleErrorSurvivesLibraryWarnings(t *testing.T) { + cmd := stubModule(t, "stderr", + "site-packages/requests/__init__.py:113: RequestsDependencyWarning: urllib3 mismatch\n"+ + " warnings.warn(\n"+ + `{"error":"STAC search returned HTTP 404","kind":"GeotorgetError"}`+"\n") + s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, StacUsername: "u", StacPassword: "t"}) + + _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "") + if err == nil { + t.Fatal("want an error") + } + if !strings.Contains(err.Error(), "STAC search returned HTTP 404") { + t.Errorf("err = %v, want the module's own message despite the warnings above it", err) + } +} + // A crash that is not the module's own JSON must still surface as an error // rather than being mistaken for a successful empty model. func TestDeriveReportsNonJSONFailure(t *testing.T) { From 6ba05b7dbb1ba207e7e8bb715ceb478609acd38e Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Wed, 2 Sep 2026 19:01:53 +0200 Subject: [PATCH 17/26] fix(roofmodel): match the live Geotorget STAC service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Verified end-to-end against the real API with a real account: the catalogue differs from what the module assumed in four ways. The service is two STAC roots, not one — byggnader on stac-vektor/v1, the point clouds on stac-hojd/v1 — so the pipeline runs one client per root for the defaults (a custom single-root catalog is unchanged). Laserdata Skog is published as dsm-skoglig-copc. Searches take the spec's WGS84 bbox, which the live probe confirmed. And buildings arrive as one ZIP-wrapped GeoPackage per municipality: Stockholm's holds 93,716 features with no per-row envelopes, so the reader unwraps the zip and clips rows to the search window by parsed geometry bounds — without that, the row limit truncated the table before it reached the site. A tile item's own geometry (the municipality outline) is no longer read as a building; a data asset wins, inline geometry is the fallback for catalogues whose items are buildings. Live result: 'Find buildings here' at the demo site returns 39 real footprints, nearest first, in ~4 s including the authenticated 13 MB municipality download. Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com> --- .changeset/roofmodel-live-geotorget.md | 23 +++++++ docs/roof-geometry.md | 18 +++-- roofmodel/ftw_roofmodel/__main__.py | 5 +- roofmodel/ftw_roofmodel/buildings.py | 83 +++++++++++++++++++----- roofmodel/ftw_roofmodel/geopackage.py | 81 ++++++++++++++++++++++- roofmodel/ftw_roofmodel/geotorget.py | 38 +++++++---- roofmodel/ftw_roofmodel/pipeline.py | 16 ++++- roofmodel/tests/test_buildings.py | 6 +- roofmodel/tests/test_derive_footprint.py | 6 +- roofmodel/tests/test_pipeline.py | 63 ++++++++++++++---- roofmodel/tests/test_source_formats.py | 81 ++++++++++++++++++++++- 11 files changed, 358 insertions(+), 62 deletions(-) create mode 100644 .changeset/roofmodel-live-geotorget.md diff --git a/.changeset/roofmodel-live-geotorget.md b/.changeset/roofmodel-live-geotorget.md new file mode 100644 index 000000000..9eb7b8cce --- /dev/null +++ b/.changeset/roofmodel-live-geotorget.md @@ -0,0 +1,23 @@ +--- +"ftw": patch +--- + +Roof derivation now works against the real Lantmäteriet service, verified +end-to-end with a live Geotorget account. The catalogue turned out to differ +from its paper description in four ways, each now handled: the STAC service +is two roots (`stac-vektor/v1` for buildings, `stac-hojd/v1` for the point +clouds) rather than one; the collections are `byggnader` and +`dsm-skoglig-copc`; searches take the STAC spec's WGS84 bbox; and buildings +arrive as one ZIP-wrapped GeoPackage per municipality — 90k+ features with no +per-row envelopes, so the reader now clips to the search window by parsed +geometry bounds instead of truncating at a row limit, and a tile item's own +geometry (the municipality outline) is no longer mistaken for a building. + +Credentials typed into Settings also apply immediately: the roofmodel service +follows config hot-reload instead of keeping its boot-time snapshot, and a +module error on stderr survives third-party library warnings above it. + +The roof section gains a data-catalog picker: Lantmäteriet (default), the +live-verified open catalogs (IGN LiDAR HD France, KAGIS Carinthia), or any +custom STAC endpoint with its own collections — open catalogs need no +credentials. diff --git a/docs/roof-geometry.md b/docs/roof-geometry.md index c20020477..42ec61a21 100644 --- a/docs/roof-geometry.md +++ b/docs/roof-geometry.md @@ -7,7 +7,7 @@ already flew a laser over it, so FTW can read the numbers instead. This is **optional, and Sweden-only by default** (any standard STAC catalog can stand in — see [Other countries, other catalogs](#other-countries-other-catalogs)). Everywhere else, and whenever anything below is missing, the numeric fields in -**Settings → Weather → PV arrays** stay the way they work today. +**Settings → Planner → PV arrays** stay the way they work today. ## What you need @@ -20,9 +20,15 @@ so Lantmäteriet can see who is downloading, not to charge you. | [Byggnad Nedladdning, vektor](https://geotorget.lantmateriet.se/geodataprodukter/byggnad-nedladdning-vektor-api) | STAC → **GeoPackage** | Building footprints, so you can point at your house | | [Laserdata Nedladdning, Skog](https://geotorget.lantmateriet.se/geodataprodukter/laserdata-nedladdning-skog-api) | STAC → **LAZ as COPC** | The laser scan the roof planes are fitted to | -Both are STAC APIs behind the same account, so one set of credentials covers -both and FTW searches them the same way. They differ only in what the items -point at, and FTW picks the right asset by its declared media type rather than +Both sit behind the same account, so one set of credentials covers both and +FTW searches them the same way. As verified against the live service +(2026-09-02), they are **two separate STAC roots**: buildings are collection +`byggnader` on `api.lantmateriet.se/stac-vektor/v1` — one item per +municipality whose asset is a ZIP holding the GeoPackage — and Laserdata Skog +is collection `dsm-skoglig-copc` on `api.lantmateriet.se/stac-hojd/v1`. Both +searches take the STAC spec's WGS84 bbox, the catalogue metadata is readable +anonymously, and the credentials are enforced where it matters — on the asset +downloads. FTW picks the right asset by its declared media type rather than by its name — a catalogue that renames `data` to `punktmoln` keeps working. Authentication is **HTTP Basic with your Geotorget account username and @@ -73,7 +79,7 @@ bbox, and an anonymous HEAD on a returned asset. | Catalog | LiDAR | Auth | Licence | Caveat | |---|---|---|---|---| -| [Lantmäteriet](https://api.lantmateriet.se/stac) (default) | LAZ/COPC | HTTP Basic (Geotorget account) | CC BY 4.0 | Sweden; bbox in EPSG:3006 | +| [Lantmäteriet](https://api.lantmateriet.se/stac-vektor/v1) (default; LiDAR on [stac-hojd](https://api.lantmateriet.se/stac-hojd/v1)) | buildings as zipped GeoPackage + LAZ/COPC | HTTP Basic (Geotorget account), enforced on downloads | CC BY 4.0 | Sweden; WGS84 bbox per the spec; end-to-end verified with a real account 2026-09-02 | | [IGN LiDAR HD via MTD](https://api.stac.teledetection.fr) (`lidarhd`) | COPC, served by IGN itself | none | etalab-2.0 | France; catalog is run by Université de Montpellier, not IGN, and IGN's download host rate-limits (~1 req/s) | | [KAGIS Carinthia](https://gis.ktn.gv.at/api/stac/v1/) (`KAGIS_coll_ALS2_pc_*`) | COPC | none | CC BY 4.0 | four Alpine regions only; ~700 MB tiles, so the COPC window path matters | | [swisstopo](https://data.geo.admin.ch/api/stac/v1) (`ch.swisstopo.swisssurface3d`) | LAS zipped as `.las.zip` | none | swisstopo open data terms | Switzerland; the zip wrapper is not yet unpacked by the module, so this one is search-verified but not derive-ready | @@ -112,7 +118,7 @@ tile whole — slower, same answer. The result records which path ran as ## Using it -1. Open **Settings → Weather**. +1. Open **Settings → Planner**. 2. Put the map marker on your building. 3. Under **Roof geometry from Lantmäteriet**, tick **Enable roof derivation**, enter your Geotorget username and password, and **Save**. diff --git a/roofmodel/ftw_roofmodel/__main__.py b/roofmodel/ftw_roofmodel/__main__.py index b5dc71f81..5cded24b4 100644 --- a/roofmodel/ftw_roofmodel/__main__.py +++ b/roofmodel/ftw_roofmodel/__main__.py @@ -69,8 +69,9 @@ def main(argv: list[str] | None = None) -> int: "--bbox-epsg", type=int, choices=(3006, 4326), - default=3006, - help="CRS of the search bbox: 3006 for Lantmäteriet, 4326 per the STAC spec", + default=4326, + help="CRS of the search bbox: 4326 per the STAC spec (Lantmäteriet " + "included, verified live), 3006 for a catalog that wants SWEREF", ) args = p.parse_args(argv) credentials = Credentials(args.username, args.password) diff --git a/roofmodel/ftw_roofmodel/buildings.py b/roofmodel/ftw_roofmodel/buildings.py index 1961123c1..de9078559 100644 --- a/roofmodel/ftw_roofmodel/buildings.py +++ b/roofmodel/ftw_roofmodel/buildings.py @@ -22,8 +22,9 @@ Coordinate frames ----------------- -GeoJSON mandates WGS84, but Lantmaeteriet publishes this catalogue in -SWEREF 99 TM (EPSG:3006) and its STAC search takes a SWEREF bbox. Rather than +GeoJSON mandates WGS84, and the live Lantmaeteriet STAC search takes a WGS84 +bbox per the spec -- but the GeoPackages it hands out store their rings in +SWEREF 99 TM (EPSG:3006), and another catalogue may do either. Rather than guess which one a given deployment returns, the frame is detected from the magnitude of the numbers -- SWEREF eastings and northings are six and seven figures, WGS84 degrees never are. @@ -41,6 +42,7 @@ COLLECTION_BUILDINGS, MEDIA_GEOJSON, MEDIA_GEOPACKAGE, + MEDIA_ZIP, GeotorgetClient, GeotorgetError, StacItem, @@ -213,29 +215,66 @@ def _to_sweref(ring: list[tuple[float, float]]) -> list[tuple[float, float]]: ] -def _features_from_item(item: StacItem, client: GeotorgetClient | None = None) -> list[dict[str, Any]]: +def _gpkg_from_zip(payload: bytes) -> bytes: + """The GeoPackage inside a ZIP asset. + + Lantmaeteriet's live `byggnader` collection carries one item per + municipality whose only asset is `byggnad_kn.zip` with the + GeoPackage inside. The largest `.gpkg` member wins if there are several; + anything else in the archive (metadata PDFs, licence texts) is ignored. + """ + import io + import zipfile + + try: + with zipfile.ZipFile(io.BytesIO(payload)) as zf: + members = [i for i in zf.infolist() if i.filename.lower().endswith(".gpkg")] + if not members: + raise BuildingLookupError( + "the building tile ZIP holds no GeoPackage" + ) + member = max(members, key=lambda i: i.file_size) + return zf.read(member) + except zipfile.BadZipFile as exc: + raise BuildingLookupError( + f"the building tile was announced as ZIP but did not open: {exc}" + ) from exc + + +def _features_from_item( + item: StacItem, + client: GeotorgetClient | None = None, + window_boxes: list[tuple[float, float, float, float]] | None = None, +) -> list[dict[str, Any]]: """Every building-like feature an item carries. A STAC item may *be* the building -- geometry inline, no download -- or it - may be a tile whose asset holds thousands of them. Lantmaeteriet publishes - *Byggnad Nedladdning, vektor* as **GeoPackage**, so the asset path is the - normal one and the inline path is the exception. + may be a tile whose asset holds thousands of them. Lantmaeteriet's live + catalogue publishes `byggnader` as one zipped **GeoPackage** per + municipality, so the asset path is the normal one and the inline path the + exception — and `window_boxes` (the search window, in each frame the file + could be in) keeps a whole city from being decoded for a 150 m search. """ - geom = (item.raw or {}).get("geometry") - if geom: - return [{"geometry": geom, "properties": (item.raw or {}).get("properties") or {}, - "id": item.item_id}] - if client is None: - return [] - asset = item.pick(MEDIA_GEOPACKAGE, MEDIA_GEOJSON) - if asset is None or not asset.href: + # A usable data asset wins over the item's own geometry: a tile item's + # geometry is the TILE's outline — for Lantmaeteriet, the whole + # municipality — and reading it as a building both invents a footprint + # nobody has and skips the real ones in the asset. Inline geometry is the + # fallback for catalogues whose items *are* the buildings. + asset = item.pick(MEDIA_GEOPACKAGE, MEDIA_ZIP, MEDIA_GEOJSON) + if asset is None or not asset.href or client is None: + geom = (item.raw or {}).get("geometry") + if geom: + return [{"geometry": geom, "properties": (item.raw or {}).get("properties") or {}, + "id": item.item_id}] return [] media = asset.effective_media_type payload = client.download(asset.href) if media == MEDIA_GEOJSON: return _features_from_geojson(payload) + if payload[:4] == b"PK\x03\x04": + payload = _gpkg_from_zip(payload) try: - return read_features(payload) + return read_features(payload, bboxes=window_boxes) except GeoPackageError as exc: raise BuildingLookupError( f"the building tile for this site could not be read: {exc}" @@ -299,14 +338,24 @@ def search_buildings( radius_m: float = DEFAULT_SEARCH_RADIUS_M, limit: int = 50, collection: str = COLLECTION_BUILDINGS, - bbox_epsg: int = 3006, + bbox_epsg: int = 4326, ) -> list[Building]: """Building footprints near a site, nearest first.""" bbox = sweref.stac_search_bbox(latitude, longitude, radius_m, bbox_epsg) + # The same window in every frame and axis order a tile could be stored + # in. Coordinate magnitude keeps the frames apart (degrees never look + # like metres), and carrying both axis orders costs nothing but a few + # extra decoded rows — a box only ever widens what is kept. + def swapped(b): + return (b[1], b[0], b[3], b[2]) + + wgs = sweref.stac_search_bbox(latitude, longitude, radius_m, 4326) + swe = sweref.stac_search_bbox(latitude, longitude, radius_m, 3006) + window_boxes = [wgs, swapped(wgs), swe, swapped(swe)] items = client.search(collection, bbox, limit=limit) features: list[dict[str, Any]] = [] for item in items: - features.extend(_features_from_item(item, client)) + features.extend(_features_from_item(item, client, window_boxes)) if not features: raise BuildingLookupError( "no building footprints were returned for this site. The Geotorget " diff --git a/roofmodel/ftw_roofmodel/geopackage.py b/roofmodel/ftw_roofmodel/geopackage.py index 9a7ad342c..76e3c757c 100644 --- a/roofmodel/ftw_roofmodel/geopackage.py +++ b/roofmodel/ftw_roofmodel/geopackage.py @@ -168,16 +168,82 @@ def _feature_tables(conn: sqlite3.Connection) -> list[tuple[str, str]]: return [(str(t), str(c)) for t, c in rows] -def read_features(data: bytes, *, limit: int = 5000) -> list[dict[str, Any]]: +def _blob_envelope(blob: bytes) -> tuple[float, float, float, float] | None: + """The header envelope of a geometry blob as (minx, miny, maxx, maxy). + + GeoPackage stores it as [minx, maxx, miny, maxy] doubles right after the + 8-byte header (OGC 12-128r19, clause 2.1.3), in the header's own byte + order. None when the writer chose not to include one. + """ + if len(blob) < 8 or blob[:2] != GPKG_MAGIC: + return None + flags = blob[3] + if ((flags >> 1) & 0x07) == 0 or flags & 0x10: + return None + endian = "<" if flags & 0x01 else ">" + try: + minx, maxx, miny, maxy = _unpack(endian + "dddd", blob, 8) + except GeoPackageError: + return None + return (minx, miny, maxx, maxy) + + +def _intersects(a: tuple[float, float, float, float], b: tuple[float, float, float, float]) -> bool: + return a[0] <= b[2] and a[2] >= b[0] and a[1] <= b[3] and a[3] >= b[1] + + +def _geometry_bounds(geometry: dict[str, Any]) -> tuple[float, float, float, float] | None: + """(minx, miny, maxx, maxy) over every ring of a parsed geometry. + + The fallback when a writer omitted the header envelope — Lantmaeteriet's + municipality files do (flags 0x00 on every blob), so without this the + bbox filter would keep all 90k+ rows and the row limit would truncate the + table before it ever reached the site. + """ + polys = geometry.get("coordinates") or [] + if geometry.get("type") == "Polygon": + polys = [polys] + xs: list[float] = [] + ys: list[float] = [] + for poly in polys: + for ring in poly: + for point in ring: + xs.append(point[0]) + ys.append(point[1]) + if not xs: + return None + return (min(xs), min(ys), max(xs), max(ys)) + + +def read_features( + data: bytes, + *, + limit: int = 5000, + bboxes: list[tuple[float, float, float, float]] | None = None, +) -> list[dict[str, Any]]: """Every polygon feature in a GeoPackage, as GeoJSON-shaped dicts. Attributes travel alongside the geometry so the picker can label a building with whatever the source calls it. + + `bboxes` filters by the geometry blobs' header envelopes: a row is kept + when its envelope intersects ANY of the boxes (each (minx, miny, maxx, + maxy)). More than one box exists because the file's CRS isn't declared to + this reader — the caller passes the same window in every frame the file + could be in, and the frames' coordinate magnitudes are so far apart + (degrees vs. six-figure metres) that only the matching one can intersect. + Lantmaeteriet ships one GeoPackage per *municipality*, so without a filter + a 150 m search would decode a whole city. """ - return list(iter_features(data, limit=limit)) + return list(iter_features(data, limit=limit, bboxes=bboxes)) -def iter_features(data: bytes, *, limit: int = 5000) -> Iterator[dict[str, Any]]: +def iter_features( + data: bytes, + *, + limit: int = 5000, + bboxes: list[tuple[float, float, float, float]] | None = None, +) -> Iterator[dict[str, Any]]: if not data.startswith(b"SQLite format 3\x00"): raise GeoPackageError( "asset is not a GeoPackage (missing the SQLite file header)" @@ -200,6 +266,11 @@ def iter_features(data: bytes, *, limit: int = 5000) -> Iterator[dict[str, Any]] blob = row[geom_col] if not isinstance(blob, (bytes, bytearray)): continue + env = None + if bboxes: + env = _blob_envelope(bytes(blob)) + if env is not None and not any(_intersects(env, b) for b in bboxes): + continue try: geometry = parse_geometry_blob(bytes(blob)) except GeoPackageError: @@ -208,6 +279,10 @@ def iter_features(data: bytes, *, limit: int = 5000) -> Iterator[dict[str, Any]] continue if geometry is None: continue + if bboxes and env is None: + bounds = _geometry_bounds(geometry) + if bounds is not None and not any(_intersects(bounds, b) for b in bboxes): + continue props = { k: row[k] for k in row.keys() diff --git a/roofmodel/ftw_roofmodel/geotorget.py b/roofmodel/ftw_roofmodel/geotorget.py index 9b1ddabc3..bbe4a13e6 100644 --- a/roofmodel/ftw_roofmodel/geotorget.py +++ b/roofmodel/ftw_roofmodel/geotorget.py @@ -34,23 +34,32 @@ import datetime as dt from typing import Any, Iterable -# Root of the STAC API. The standard search endpoint is POST {base}/search. -DEFAULT_BASE_URL = "https://api.lantmateriet.se/stac" - -# Collection ids as published in Lantmaeteriet's STAC catalogue. Both products -# are STAC APIs over the same base URL and the same credentials; they differ -# only in what their items point at, which is what the media types below say. -COLLECTION_BUILDINGS = "byggnad-nedladdning-vektor" -COLLECTION_LIDAR = "laserdata-nedladdning-skog" +# Roots of the STAC APIs, as verified against the live service (2026-09-02): +# Lantmaeteriet does not serve one catalogue — vector products and elevation +# products have separate STAC roots. The standard search endpoint is +# POST {base}/search on each, it takes a WGS84 (lon/lat) bbox per the STAC +# spec, and the catalogue metadata is anonymously readable; the Geotorget +# credentials are enforced on the asset downloads (dl1.lantmateriet.se +# answers 401 without them). +DEFAULT_BASE_URL = "https://api.lantmateriet.se/stac-vektor/v1" +DEFAULT_LIDAR_BASE_URL = "https://api.lantmateriet.se/stac-hojd/v1" + +# Collection ids as published in the live catalogues. Buildings are one item +# per municipality whose asset is a ZIP holding a GeoPackage; "Laserdata Skog" +# is published as `dsm-skoglig-copc` — a surface-model point cloud in +# LAZ/COPC — on the elevation root. +COLLECTION_BUILDINGS = "byggnader" +COLLECTION_LIDAR = "dsm-skoglig-copc" # Media types, so an asset is chosen by *what it is* rather than by hoping the -# publisher named the key "data". Byggnad-vektor delivers GeoPackage; Laserdata -# Skog delivers LAZ organised as COPC (Cloud Optimized Point Cloud). +# publisher named the key "data". Byggnader delivers a zipped GeoPackage; +# Laserdata Skog delivers LAZ organised as COPC (Cloud Optimized Point Cloud). MEDIA_GEOPACKAGE = "application/geopackage+sqlite3" MEDIA_COPC = "application/vnd.laszip+copc" MEDIA_LAZ = "application/vnd.laszip" MEDIA_LAS = "application/vnd.las" MEDIA_GEOJSON = "application/geo+json" +MEDIA_ZIP = "application/zip" # Longest suffix first: a COPC file is also a .laz, and reading it as a plain # one would download the whole tile instead of the part we asked for. @@ -60,6 +69,7 @@ (".geojson", MEDIA_GEOJSON), (".laz", MEDIA_LAZ), (".las", MEDIA_LAS), + (".zip", MEDIA_ZIP), ) @@ -238,7 +248,7 @@ def __init__( # A custom catalog may be open: no credentials means anonymous access, # while half a credential is still an error either way. self._anonymous = not (credentials.username or credentials.password) - if not self._anonymous or self._base_url == DEFAULT_BASE_URL: + if not self._anonymous or self._base_url in (DEFAULT_BASE_URL, DEFAULT_LIDAR_BASE_URL): credentials.validate() self._credentials = credentials self._timeout = timeout @@ -264,9 +274,9 @@ def search( """POST {base}/search for one collection over a bbox. The bbox is (min_x, min_y, max_x, max_y) in whatever CRS the catalog - expects -- the STAC spec says WGS84 lon/lat, Lantmaeteriet expects - SWEREF 99 TM -- so the caller chooses what to send and no reprojection - happens here. + expects -- the STAC spec says WGS84 lon/lat, and the live Lantmaeteriet + service follows it -- so the caller chooses what to send and no + reprojection happens here. """ body = { "collections": [collection], diff --git a/roofmodel/ftw_roofmodel/pipeline.py b/roofmodel/ftw_roofmodel/pipeline.py index d31f32286..e76ecdcc7 100644 --- a/roofmodel/ftw_roofmodel/pipeline.py +++ b/roofmodel/ftw_roofmodel/pipeline.py @@ -191,7 +191,7 @@ def derive( base_url: str = geotorget.DEFAULT_BASE_URL, buildings_collection: str = COLLECTION_BUILDINGS, lidar_collection: str = COLLECTION_LIDAR, - bbox_epsg: int = 3006, + bbox_epsg: int = 4326, ) -> dict[str, Any]: """Derive a roof model for one site and return it as a JSON-ready dict. @@ -204,8 +204,18 @@ def derive( catalog can stand in (see geotorget.py), as long as its data arrives in SWEREF 99 TM -- the segmentation works in that frame. """ + # Lantmaeteriet splits its STAC service per product family: buildings live + # on the vector root, the point clouds on the elevation root. An injected + # client (tests, the demo) serves both; so does a custom single-root + # catalog. Only the Lantmaeteriet default needs the second client. + lidar_client = client if client is None: client = GeotorgetClient(credentials, base_url=base_url) + lidar_client = client + if base_url == geotorget.DEFAULT_BASE_URL: + lidar_client = GeotorgetClient( + credentials, base_url=geotorget.DEFAULT_LIDAR_BASE_URL + ) chosen: Building | None = None if building_id: @@ -223,7 +233,7 @@ def derive( bbox = sweref.stac_search_bbox(latitude, longitude, radius_m, bbox_epsg) try: - lidar_items: list[StacItem] = client.search(lidar_collection, bbox) + lidar_items: list[StacItem] = lidar_client.search(lidar_collection, bbox) except GeotorgetError: raise if not lidar_items: @@ -233,7 +243,7 @@ def derive( f"collection {lidar_collection!r}{hint}" ) - points, fetch = _read_lidar(client, lidar_items, chosen) + points, fetch = _read_lidar(lidar_client, lidar_items, chosen) if points is None or len(points) == 0: raise RoofModelError("LiDAR tiles carried no readable point data") diff --git a/roofmodel/tests/test_buildings.py b/roofmodel/tests/test_buildings.py index 62232d396..90805abc7 100644 --- a/roofmodel/tests/test_buildings.py +++ b/roofmodel/tests/test_buildings.py @@ -138,8 +138,10 @@ def test_search_queries_the_building_collection_and_maps_results(): assert [b.building_id for b in got] == ["b1"] (_, body), = session.posts assert body["collections"] == [COLLECTION_BUILDINGS] - # The bbox must be the SWEREF box around the site, not raw degrees. - assert body["bbox"][0] > 1000 + # The bbox is WGS84 lon/lat per the STAC spec — verified against the live + # Lantmaeteriet service, which follows it. + assert 17.9 < body["bbox"][0] < 18.2, body["bbox"] + assert 59.2 < body["bbox"][1] < 59.5, body["bbox"] def test_search_says_what_to_do_when_nothing_comes_back(): diff --git a/roofmodel/tests/test_derive_footprint.py b/roofmodel/tests/test_derive_footprint.py index 5489b2a70..0ca6e161e 100644 --- a/roofmodel/tests/test_derive_footprint.py +++ b/roofmodel/tests/test_derive_footprint.py @@ -9,7 +9,7 @@ from ftw_roofmodel import pipeline, sweref from ftw_roofmodel.buildings import clip_to_footprint -from ftw_roofmodel.geotorget import Credentials, StacItem +from ftw_roofmodel.geotorget import COLLECTION_BUILDINGS, Credentials, StacItem from ftw_roofmodel.pipeline import RoofModelError, derive from ftw_roofmodel.segment import segment_roof @@ -43,7 +43,7 @@ def __init__(self, buildings_payload, points): def search(self, collection, bbox, limit=20): self.searched.append(collection) - if collection == "byggnad-nedladdning-vektor": + if collection == COLLECTION_BUILDINGS: return [StacItem(f["id"], collection, {}, None, raw=f) for f in self._buildings] return [StacItem("lidar-1", collection, {"data": "http://x/tile.laz"}, None, raw={})] @@ -119,7 +119,7 @@ def test_derive_without_a_building_id_does_not_search_for_buildings(scene): latitude=STOCKHOLM[0], longitude=STOCKHOLM[1], credentials=Credentials("u", "t"), client=client, ) - assert "byggnad-nedladdning-vektor" not in client.searched + assert COLLECTION_BUILDINGS not in client.searched assert model["building"] is None diff --git a/roofmodel/tests/test_pipeline.py b/roofmodel/tests/test_pipeline.py index c05d8406a..93fbfb5cd 100644 --- a/roofmodel/tests/test_pipeline.py +++ b/roofmodel/tests/test_pipeline.py @@ -12,7 +12,7 @@ import numpy as np import pytest -from ftw_roofmodel import pipeline +from ftw_roofmodel import pipeline, sweref from ftw_roofmodel.geotorget import ( COLLECTION_LIDAR, Credentials, @@ -130,9 +130,9 @@ def test_search_sends_the_collection_and_bbox(): assert len(items) == 1 (url, body), = session.posts - # The default catalog root is Lantmaeteriet's /stac, so the standard - # {base}/search lands on the same URL it always has. - assert url.endswith("/stac/search") + # The client's default root is the live vector catalogue; search is the + # spec's POST {base}/search on whatever root the client was given. + assert url.endswith("/stac-vektor/v1/search") assert body["collections"] == [COLLECTION_LIDAR] assert body["bbox"] == [600000.0, 6500000.0, 600100.0, 6500100.0] @@ -268,7 +268,7 @@ def test_derive_produces_a_versioned_document(monkeypatch): json.dumps(model) -def test_derive_searches_the_projected_bbox(monkeypatch): +def test_derive_searches_the_spec_bbox(monkeypatch): _patched_points(monkeypatch, make_plane(tilt_deg=35, azimuth_deg=180)) session = FakeSession(search={"features": [feature()]}, asset=b"x") pipeline.derive( @@ -276,12 +276,53 @@ def test_derive_searches_the_projected_bbox(monkeypatch): client=GeotorgetClient(CREDS, session=session), radius_m=40.0, ) (_, body), = session.posts - min_e, min_n, max_e, max_n = body["bbox"] - # Stockholm in SWEREF 99 TM, and an 80 m box give or take projection bow. - assert 600_000 < min_e < 700_000, body["bbox"] - assert 6_500_000 < min_n < 6_600_000, body["bbox"] - assert 75 < (max_e - min_e) < 90 - assert 75 < (max_n - min_n) < 90 + # WGS84 lon/lat per the STAC spec, the live service's frame — and still + # square on the ground: built by stepping metres in SWEREF and + # unprojecting, which is what stac_search_bbox does. + expected = sweref.stac_search_bbox(59.33, 18.07, 40.0, 4326) + assert list(body["bbox"]) == pytest.approx(list(expected)) + + +def test_derive_uses_both_lantmateriet_roots_by_default(monkeypatch): + """The live service splits its STAC per product family: buildings on the + vector root, point clouds on the elevation root. The default derive must + build a client for each.""" + made = [] + + class RecordingClient: + def __init__(self, credentials, base_url=None, **kw): + made.append(base_url) + + def search(self, collection, bbox, limit=20): + return [] + + monkeypatch.setattr(pipeline, "GeotorgetClient", RecordingClient) + with pytest.raises(RoofModelError): + pipeline.derive(latitude=59.33, longitude=18.07, credentials=CREDS) + assert made == [ + pipeline.geotorget.DEFAULT_BASE_URL, + pipeline.geotorget.DEFAULT_LIDAR_BASE_URL, + ] + + +def test_derive_uses_one_client_for_a_custom_catalog(monkeypatch): + """A custom single-root catalog serves both collections itself.""" + made = [] + + class RecordingClient: + def __init__(self, credentials, base_url=None, **kw): + made.append(base_url) + + def search(self, collection, bbox, limit=20): + return [] + + monkeypatch.setattr(pipeline, "GeotorgetClient", RecordingClient) + with pytest.raises(RoofModelError): + pipeline.derive( + latitude=59.33, longitude=18.07, credentials=CREDS, + base_url="https://stac.example.org", + ) + assert made == ["https://stac.example.org"] def test_derive_outside_sweden_says_so(monkeypatch): diff --git a/roofmodel/tests/test_source_formats.py b/roofmodel/tests/test_source_formats.py index e33b6d929..db41dff69 100644 --- a/roofmodel/tests/test_source_formats.py +++ b/roofmodel/tests/test_source_formats.py @@ -16,6 +16,7 @@ from ftw_roofmodel import pipeline, sweref from ftw_roofmodel.buildings import BuildingLookupError, search_buildings from ftw_roofmodel.geotorget import ( + COLLECTION_BUILDINGS, MEDIA_COPC, MEDIA_GEOJSON, MEDIA_GEOPACKAGE, @@ -66,7 +67,7 @@ def __init__(self, building_asset=None, lidar_asset=None, payloads=None, self.downloaded: list[str] = [] def search(self, collection, bbox, limit=20): - if collection == "byggnad-nedladdning-vektor": + if collection == COLLECTION_BUILDINGS: if self._building_asset is None: return [] return [StacItem("tile-b", collection, {"data": self._building_asset}, None, @@ -104,6 +105,84 @@ def test_buildings_come_out_of_a_geopackage_asset(): assert client.downloaded == [url] +def test_buildings_come_out_of_a_zipped_geopackage_asset(): + """The live Lantmaeteriet shape: one item per municipality whose only + asset is byggnad_kn.zip with the GeoPackage inside.""" + import io + import zipfile + + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w") as zf: + zf.writestr("byggnad_kn0180.gpkg", a_geopackage_of_two_buildings()) + zf.writestr("licens.txt", "CC BY 4.0") + url = "https://dl1.lantmateriet.se/byggnadsverk/byggnad_kn0180.zip" + client = FakeClient( + building_asset=Asset(url, "application/zip"), + payloads={url: buf.getvalue()}, + ) + found = search_buildings(client, latitude=STOCKHOLM[0], longitude=STOCKHOLM[1]) + + assert [b.building_id for b in found] == ["house-1", "shed-1"] + assert client.downloaded == [url] + + +def test_a_tile_asset_wins_over_the_tile_outline(): + """The live municipality items carry their own geometry — the TILE's + outline, not a building. With a data asset present, the asset must be + read and the outline ignored; treating the outline as a building both + invented a giant footprint and skipped the 90k real ones (found live: + 'found=0' with no error, because the outline failed the area filter).""" + url = "https://dl1.lantmateriet.se/byggnadsverk/byggnad_kn0180.zip" + import io + import zipfile + + buf = io.BytesIO() + with zipfile.ZipFile(buf, "w") as zf: + zf.writestr("byggnad_kn0180.gpkg", a_geopackage_of_two_buildings()) + kommun_outline = { + "type": "Polygon", + "coordinates": [[[E - 20000, N - 20000], [E + 20000, N - 20000], + [E + 20000, N + 20000], [E - 20000, N + 20000], + [E - 20000, N - 20000]]], + } + + class TileClient(FakeClient): + def search(self, collection, bbox, limit=20): + return [StacItem("0180", collection, {"data": Asset(url, "application/zip")}, + None, raw={"id": "0180", "geometry": kommun_outline})] + + client = TileClient(payloads={url: buf.getvalue()}) + found = search_buildings(client, latitude=STOCKHOLM[0], longitude=STOCKHOLM[1]) + + assert [b.building_id for b in found] == ["house-1", "shed-1"] + assert client.downloaded == [url] + + +def test_a_municipality_tile_is_filtered_to_the_search_window(): + """One GeoPackage covers a whole municipality — Stockholm's has 90k+ + buildings — so rows outside the search window must be dropped, or the + reader's row limit truncates the table before it reaches the site. + + Both drop paths matter: a stored header envelope skips the row before + its WKB is decoded, and the live Lantmaeteriet files write NO envelopes + (flags 0x00), where the parsed geometry's own bounds must do it.""" + far_e, far_n = E + 5000, N + 5000 + gpkg = build_gpkg([ + # Near the site, no envelope: kept via its parsed bounds. + ("near-1", "Bostad", gpkg_blob(wkb_polygon(square(E - 6, N - 3, 12, 12)))), + # Far away with an envelope: dropped before the WKB is decoded. + ("far-1", "Bostad", gpkg_blob(wkb_polygon(square(far_e, far_n, 12, 12)), + envelope=(far_e, far_e + 12, far_n, far_n + 12))), + # Far away without an envelope: dropped via its parsed bounds. + ("far-2", "Bostad", gpkg_blob(wkb_polygon(square(far_e, far_n - 60, 12, 12)))), + ]) + url = "https://api.lantmateriet.se/x/byggnad_kn0180.gpkg" + client = FakeClient(building_asset=Asset(url, MEDIA_GEOPACKAGE), payloads={url: gpkg}) + found = search_buildings(client, latitude=STOCKHOLM[0], longitude=STOCKHOLM[1]) + + assert [b.building_id for b in found] == ["near-1"] + + def test_buildings_come_out_of_a_geojson_asset_too(): """Some catalogues publish GeoJSON; both are handled by media type.""" url = "https://api.lantmateriet.se/x/byggnad.geojson" From 26fcd13cfca47389550874ac4266a844b60c1856 Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Wed, 2 Sep 2026 19:02:02 +0200 Subject: [PATCH 18/26] feat(web): pick the STAC catalog from a list, or bring your own MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The roof section gains a data-catalog select: Lantmäteriet (the default, Geotorget account), the live-verified open catalogs — IGN LiDAR HD for France, KAGIS for Carinthia — and a custom entry that reveals the STAC API root and collection fields. Presets fill the same roofmodel.stac_* config keys the config file uses; open catalogs need no credentials, and the credential labels and the attribution line follow the pick. Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com> --- web/settings/tabs/weather.js | 103 +++++++++++++++++++++++++++++++---- 1 file changed, 91 insertions(+), 12 deletions(-) diff --git a/web/settings/tabs/weather.js b/web/settings/tabs/weather.js index 79ee20913..559a67e2f 100644 --- a/web/settings/tabs/weather.js +++ b/web/settings/tabs/weather.js @@ -297,26 +297,80 @@ var roofState = { features: [], selectedId: null }; + // Known point-cloud catalogs, verified live 2026-09-02. `base: ""` means + // the module's built-in Lantmäteriet defaults; `base: null` marks the + // custom entry, which leaves whatever the operator typed alone. + var STAC_CATALOGS = [ + { key: "lantmateriet", label: "Lantmäteriet (Sweden — Geotorget account)", + base: "", buildings: "", lidar: "" }, + { key: "ign-france", label: "IGN LiDAR HD (France — open, no account)", + base: "https://api.stac.teledetection.fr", buildings: "", lidar: "lidarhd" }, + { key: "kagis", label: "KAGIS ALS (Austria/Carinthia — open, no account)", + base: "https://gis.ktn.gv.at/api/stac/v1", buildings: "", lidar: "" }, + { key: "custom", label: "Custom STAC endpoint…", base: null }, + ]; + + // Set when the operator picks "Custom" while the base URL is still empty, + // so the re-render doesn't snap the select back to Lantmäteriet before + // they had a chance to type the root. + var roofCustomCatalog = false; + + function catalogPresetKey(rm) { + var base = String(rm.stac_base_url || "").replace(/\/+$/, ""); + if (!base) return roofCustomCatalog ? "custom" : "lantmateriet"; + for (var i = 0; i < STAC_CATALOGS.length; i++) { + var c = STAC_CATALOGS[i]; + if (c.base && c.base.replace(/\/+$/, "") === base) return c.key; + } + return "custom"; + } + function roofFieldset(ctx) { var field = ctx.field, help = ctx.help, config = ctx.config; if (!config.roofmodel) config.roofmodel = {}; var stored = config.roofmodel.has_stac_password; - return '
Roof geometry from Lantmäteriet ' + help( - 'Optional. Reads the tilt and azimuth of each roof face from ' + - 'Lantmäteriet\'s laser scanning data (Laserdata Skog) for a building you pick on ' + - 'the map, and fills in the PV arrays above. Needs a free Geotorget account with ' + - 'access to "Byggnad Nedladdning, vektor" and "Laserdata Nedladdning, Skog" — ' + - 'sign in with the account\'s own username and password (Lantmäteriet offers no ' + - 'OAuth for these APIs). Other countries\' STAC catalogs can be configured in ' + - 'the config file via roofmodel.stac_base_url — open catalogs need no ' + - 'credentials, so both fields stay empty.') + + var preset = catalogPresetKey(config.roofmodel); + var isDefault = preset === "lantmateriet"; + var options = STAC_CATALOGS.map(function (c) { + return ''; + }).join(""); + return '
Roof geometry from LiDAR ' + help( + 'Optional. Reads the tilt and azimuth of each roof face out of a ' + + 'LiDAR point cloud for a building you pick on the map, and fills in ' + + 'the PV arrays above. The default catalog is Lantmäteriet (Sweden), ' + + 'which needs a free Geotorget account — sign in with the account\'s ' + + 'own username and password. The open catalogs need no account, and ' + + 'any STAC-conformant endpoint can be entered as a custom catalog.') + '' + '' + + '' + + '' + + '
' + + field("STAC API root", "roofmodel.stac_base_url", "text", "") + '
' + - field("Geotorget username", "roofmodel.stac_username", "text", "") + + field("Buildings collection", "roofmodel.stac_buildings_collection", "text", "", + "STAC collection id for building footprints. Leave empty if the catalog has none — the derive then reads the whole search radius.") + '
' + - field(stored ? "Geotorget password (stored — type to replace)" : "Geotorget password", + field("LiDAR collection", "roofmodel.stac_lidar_collection", "text", "", + "STAC collection id for the point cloud.") + + '
' + + '
' + + '
' + + field(isDefault ? "Geotorget username" : "Catalog username (empty for open catalogs)", + "roofmodel.stac_username", "text", "") + + '
' + + field((isDefault ? "Geotorget password" : "Catalog password") + + (stored ? " (stored — type to replace)" : ""), "roofmodel.stac_password", "password", "") + '
' + '
' + @@ -326,7 +380,10 @@ '
' + '
' + '

' + - 'Data © Lantmäteriet (CC BY 4.0). Derived values are a starting point — check them ' + + (isDefault + ? 'Data © Lantmäteriet (CC BY 4.0). ' + : 'Check the catalog\'s licence and attribution terms. ') + + 'Derived values are a starting point — check them ' + 'against your installation before relying on the forecast.' + '

' + '
'; @@ -580,6 +637,28 @@ refreshArraysSummary(ctx.config); }); roofState = { features: [], selectedId: null }; + var catalogSel = document.getElementById("roof-catalog"); + if (catalogSel) catalogSel.addEventListener("change", function () { + var chosen = null; + for (var i = 0; i < STAC_CATALOGS.length; i++) { + if (STAC_CATALOGS[i].key === catalogSel.value) chosen = STAC_CATALOGS[i]; + } + if (!chosen) return; + roofCustomCatalog = chosen.key === "custom"; + // Keep everything already typed on this tab, then swap the catalog + // fields to the preset. Custom leaves the operator's values alone — + // it only reveals the fields. + ctx.captureCurrentTab(); + if (!ctx.config.roofmodel) ctx.config.roofmodel = {}; + if (chosen.base !== null) { + ctx.setByPath(ctx.config, "roofmodel.stac_base_url", chosen.base); + ctx.setByPath(ctx.config, "roofmodel.stac_buildings_collection", chosen.buildings || ""); + ctx.setByPath(ctx.config, "roofmodel.stac_lidar_collection", chosen.lidar || ""); + } + // Re-render so the fields, labels and attribution follow the pick. + var active = document.querySelector("#settings-tabs button.active"); + if (ctx.renderTab && active) ctx.renderTab(active.dataset.tab); + }); var findBtn = document.getElementById("roof-find"); if (findBtn) findBtn.addEventListener("click", function () { findBuildings(ctx); }); var deriveBtn = document.getElementById("roof-derive"); From ac7ccf67eaa4a93ee7041136d7da1c994cd6f438 Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Wed, 2 Sep 2026 19:37:04 +0200 Subject: [PATCH 19/26] fix(web): found buildings draw on the map and a click picks one MapLibre rejected both footprint layers without throwing: the case condition needed a typed boolean, and the theme's oklch() tokens are unparseable to its colour parser, so themeColor now bakes the resolved value to sRGB bytes through a 1x1 canvas. A find also fits the camera to the nearby candidates (city zoom leaves a footprint smaller than a pixel), retries the draw if the style is still loading, and a click on a footprint selects that building instead of dragging the site pin - and the saved coordinates - to wherever you clicked. Verified in headless Edge against the live Geotorget service: 39 footprints rendered, map-click selection enables the derive button, pin coordinates unchanged. Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com> --- .changeset/roof-buildings-on-the-map.md | 14 ++++ web/index.html | 2 +- web/settings/tabs/weather.js | 96 ++++++++++++++++++++++--- web/settings/tabs/weather.test.mjs | 65 +++++++++++++++++ 4 files changed, 166 insertions(+), 11 deletions(-) create mode 100644 .changeset/roof-buildings-on-the-map.md diff --git a/.changeset/roof-buildings-on-the-map.md b/.changeset/roof-buildings-on-the-map.md new file mode 100644 index 000000000..6a1c5079f --- /dev/null +++ b/.changeset/roof-buildings-on-the-map.md @@ -0,0 +1,14 @@ +--- +"ftw": patch +--- + +Found buildings now actually appear on the map, and clicking one selects it. +Three defects hid them: MapLibre silently rejected both footprint layers +because the styling used a value-typed `["get", "selected"]` where a typed +boolean is required, and — once typed — because the theme's `oklch()` colour +tokens reached MapLibre's parser unconverted, so the resolved colours are now +baked to sRGB through a canvas probe. Finally the camera never moved: the map +sat at city zoom, where a footprint is smaller than a pixel. A find now zooms +to the nearby candidates, and clicking a footprint selects that building +without also dragging the site pin (and its saved coordinates) to wherever +you clicked. diff --git a/web/index.html b/web/index.html index 6f5782e1c..b24235a06 100644 --- a/web/index.html +++ b/web/index.html @@ -995,7 +995,7 @@

Price bars (top of the chart)

- + diff --git a/web/settings/tabs/weather.js b/web/settings/tabs/weather.js index 559a67e2f..62cfa6293 100644 --- a/web/settings/tabs/weather.js +++ b/web/settings/tabs/weather.js @@ -203,6 +203,13 @@ setCoord(ll.lat, ll.lng); }); map.on("click", function (e) { + // A click on a building footprint selects that building (the layer's own + // handler). It must not also drag the site pin there and silently + // rewrite the saved coordinates. + if (map.getLayer("roof-buildings-fill") && + map.queryRenderedFeatures(e.point, { layers: ["roof-buildings-fill"] }).length) { + return; + } marker.setLngLat(e.lngLat); setCoord(e.lngLat.lat, e.lngLat.lng); }); @@ -425,7 +432,10 @@ roofSay("Found " + roofState.features.length + " building(s). Pick yours on the map or in the list."); drawBuildings(); + fitToBuildings(d.latitude, d.longitude); renderBuildingList(ctx); + var mapEl = document.getElementById("weather-map"); + if (mapEl && mapEl.scrollIntoView) mapEl.scrollIntoView({ block: "nearest" }); }) .catch(function (e) { roofSay(ctx.escHtml(String(e && e.message || e)), "bad"); }); } @@ -443,9 +453,12 @@ // MapLibre paints with concrete colours and cannot read var(), the same // problem the canvas charts have. Resolve the theme tokens through a hidden - // probe that inherits :root, exactly as app.js's cssColor does. Resolved once - // per layer creation, so a theme toggle mid-pick keeps the old hue until the - // tab is reopened — the footprints stay legible either way. + // probe that inherits :root, exactly as app.js's cssColor does. The theme + // authors its tokens in oklch(), which getComputedStyle passes through + // verbatim and MapLibre's parser rejects — so bake the resolved colour to + // sRGB bytes through a 1x1 canvas, whose getImageData is sRGB by contract. + // Resolved once per layer creation, so a theme toggle mid-pick keeps the old + // hue until the tab is reopened — the footprints stay legible either way. var _probe = null; function themeColor(name, fallback) { if (!_probe) { @@ -454,12 +467,35 @@ document.body.appendChild(_probe); } _probe.style.color = "var(" + name + ", " + fallback + ")"; - return getComputedStyle(_probe).color || fallback; + var resolved = getComputedStyle(_probe).color || fallback; + try { + var ctx = document.createElement("canvas").getContext("2d", { willReadFrequently: true }); + ctx.fillStyle = fallback; // an unparseable resolved value leaves this + ctx.fillStyle = resolved; + ctx.fillRect(0, 0, 1, 1); + var px = ctx.getImageData(0, 0, 1, 1).data; + return "rgb(" + px[0] + "," + px[1] + "," + px[2] + ")"; + } catch (e) { + return fallback; + } + } + + // A "case" condition must be a typed boolean: a bare ["get", ...] is value- + // typed, and MapLibre rejects the whole layer through its error event without + // throwing — the picker then looks enabled while the map stays empty. + function whenSelected(then, otherwise) { + return ["case", ["boolean", ["get", "selected"], false], then, otherwise]; } function drawBuildings() { var map = window._weatherMap; - if (!map || !map.isStyleLoaded || !map.isStyleLoaded()) return; + if (!map) return; + if (!map.isStyleLoaded || !map.isStyleLoaded()) { + // A find can win the race against the style. Idempotent, so a stacked + // retry only costs a setData with identical data. + map.once("load", drawBuildings); + return; + } var data = featureCollection(); var src = map.getSource("roof-buildings"); if (src) { src.setData(data); return; } @@ -469,15 +505,15 @@ map.addLayer({ id: "roof-buildings-fill", type: "fill", source: "roof-buildings", paint: { - "fill-color": ["case", ["get", "selected"], picked, candidate], - "fill-opacity": ["case", ["get", "selected"], 0.55, 0.25], + "fill-color": whenSelected(picked, candidate), + "fill-opacity": whenSelected(0.55, 0.25), }, }); map.addLayer({ id: "roof-buildings-line", type: "line", source: "roof-buildings", paint: { - "line-color": ["case", ["get", "selected"], picked, candidate], - "line-width": ["case", ["get", "selected"], 2.5, 1], + "line-color": whenSelected(picked, candidate), + "line-width": whenSelected(2.5, 1), }, }); map.on("click", "roof-buildings-fill", function (e) { @@ -493,6 +529,42 @@ }); } + // [[west, south], [east, north]] around the buildings someone would actually + // pick — the nearby ones the list also shows — plus the site pin. Fitting + // the whole search radius leaves every footprint a few pixels wide. + function buildingsBounds(features, siteLat, siteLon) { + var west = null, south = null, east = null, north = null; + function extend(lon, lat) { + if (typeof lon !== "number" || typeof lat !== "number") return; + if (west === null || lon < west) west = lon; + if (east === null || lon > east) east = lon; + if (south === null || lat < south) south = lat; + if (north === null || lat > north) north = lat; + } + extend(siteLon, siteLat); + var near = features.filter(function (f) { + return ((f.properties || {}).distance_m || 0) <= 150; + }); + if (near.length < 3) near = features; + near.forEach(function (f) { + var rings = (f.geometry && f.geometry.coordinates) || []; + (rings[0] || []).forEach(function (pt) { extend(pt[0], pt[1]); }); + }); + if (west === null) return null; + return [[west, south], [east, north]]; + } + + // The picker opens at city zoom, where a footprint is smaller than a pixel. + // Zoom to the search results once per find; selection redraws leave the + // camera where the operator put it. + function fitToBuildings(siteLat, siteLon) { + var map = window._weatherMap; + if (!map || !roofState.features.length) return; + var bounds = buildingsBounds(roofState.features, siteLat, siteLon); + if (!bounds) return; + map.fitBounds(bounds, { padding: 48, maxZoom: 17.5, duration: 600 }); + } + function selectBuilding(id) { roofState.selectedId = id; drawBuildings(); @@ -666,5 +738,9 @@ }, }; - S.tabs.weather._pure = { arraysSummary: arraysSummary }; + S.tabs.weather._pure = { + arraysSummary: arraysSummary, + whenSelected: whenSelected, + buildingsBounds: buildingsBounds, + }; })(); diff --git a/web/settings/tabs/weather.test.mjs b/web/settings/tabs/weather.test.mjs index 592fcd781..8cdebb481 100644 --- a/web/settings/tabs/weather.test.mjs +++ b/web/settings/tabs/weather.test.mjs @@ -82,3 +82,68 @@ describe("weather household path", () => { assert.doesNotMatch(source, /observed.*pv_rated_w|peak.*pv_rated_w/i); }); }); + +describe("roof buildings on the map", () => { + const { whenSelected, buildingsBounds } = tab._pure; + + function footprint(distanceM, ring) { + return { + type: "Feature", + geometry: { type: "Polygon", coordinates: [ring] }, + properties: { distance_m: distanceM }, + }; + } + + it("types the selected flag as a boolean, or MapLibre rejects the layer", () => { + // A bare ["get", ...] case condition is value-typed; MapLibre then drops + // the whole layer through its error event without throwing, and the map + // silently stays empty. + assert.deepEqual(whenSelected("#picked", "#other"), [ + "case", ["boolean", ["get", "selected"], false], "#picked", "#other", + ]); + }); + + it("zooms to the near buildings plus the site, not the whole radius", () => { + const bounds = buildingsBounds([ + footprint(40, [[18.06, 59.32], [18.07, 59.33], [18.06, 59.33], [18.06, 59.32]]), + footprint(90, [[18.065, 59.325], [18.066, 59.326], [18.065, 59.326], [18.065, 59.325]]), + footprint(120, [[18.068, 59.328], [18.069, 59.329], [18.068, 59.329], [18.068, 59.328]]), + footprint(800, [[18.20, 59.40], [18.21, 59.41], [18.20, 59.41], [18.20, 59.40]]), + ], 59.335, 18.05); + assert.deepEqual(bounds, [[18.05, 59.32], [18.07, 59.335]]); + }); + + it("falls back to every footprint when almost nothing is near", () => { + const bounds = buildingsBounds([ + footprint(800, [[18.20, 59.40], [18.21, 59.41], [18.20, 59.41], [18.20, 59.40]]), + footprint(900, [[18.30, 59.42], [18.31, 59.43], [18.30, 59.43], [18.30, 59.42]]), + ], 59.33, 18.07); + assert.deepEqual(bounds, [[18.07, 59.33], [18.31, 59.43]]); + }); + + it("survives features with no usable geometry", () => { + assert.equal(buildingsBounds([], undefined, undefined), null); + assert.deepEqual( + buildingsBounds([{ type: "Feature", properties: { distance_m: 5 } }, + { type: "Feature", properties: { distance_m: 6 } }, + { type: "Feature", properties: { distance_m: 7 } }], 59.33, 18.07), + [[18.07, 59.33], [18.07, 59.33]], + ); + }); + + it("bakes theme colours to sRGB — MapLibre cannot parse oklch()", () => { + assert.match(source, /getImageData\(0, 0, 1, 1\)/); + assert.match(source, /"rgb\(" \+ px\[0\]/); + }); + + it("keeps a building click from dragging the site pin", () => { + assert.match( + source, + /queryRenderedFeatures\(e\.point, \{ layers: \["roof-buildings-fill"\] \}\)/, + ); + }); + + it("retries the draw once the style has loaded", () => { + assert.match(source, /map\.once\("load", drawBuildings\)/); + }); +}); From 249411b779ad214ed8a3a2e8c79d709cb457d756 Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Wed, 2 Sep 2026 19:59:30 +0200 Subject: [PATCH 20/26] fix(roofmodel): COPC window queries must not trust Lantmateriet z keys Live derive with laspy[lazrs] returned 47 points for a 4,760 m2 window of a 217M-point tile. The tile's octree nodes carry z voxel keys from a different origin than the file's own cube - a level-6 node keyed to a slab at -1698..-1542 m holds points at +18..+42 m, x/y keys exact - so laspy's bounds pruning discarded every dense level and kept the sparse preview. The window query now spans the octree cube vertically; x/y pruning and laspy's exact post-filter still bound the read. Verified live end to end afterwards: the picked building derived 8 arrays from 9 roof planes out of the 2021-03-23 scan, over HTTP range requests (~7 MB moved, not the multi-GB tile). Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com> --- .changeset/copc-window-z-keys.md | 11 ++++++++++ roofmodel/ftw_roofmodel/pointcloud.py | 21 ++++++++++++++++++- roofmodel/tests/test_pointcloud.py | 29 +++++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 .changeset/copc-window-z-keys.md create mode 100644 roofmodel/tests/test_pointcloud.py diff --git a/.changeset/copc-window-z-keys.md b/.changeset/copc-window-z-keys.md new file mode 100644 index 000000000..aa896238a --- /dev/null +++ b/.changeset/copc-window-z-keys.md @@ -0,0 +1,11 @@ +--- +"ftw": patch +--- + +The LiDAR derive now reads the full-density point cloud. Lantmäteriet's COPC +files key their octree nodes with a broken z origin (a node whose key implies +a slab 1.7 km underground holds the roof points), which made the windowed +query prune every dense level and return ~47 preview points — too few to fit +any plane. The query now spans the octree cube vertically, so pruning happens +on x/y only. Verified live: a picked Stockholm building went from "only 34 +LiDAR returns" to 8 proposed arrays from 9 roof planes out of the 2021 scan. diff --git a/roofmodel/ftw_roofmodel/pointcloud.py b/roofmodel/ftw_roofmodel/pointcloud.py index 622c9efc4..c49927dea 100644 --- a/roofmodel/ftw_roofmodel/pointcloud.py +++ b/roofmodel/ftw_roofmodel/pointcloud.py @@ -31,6 +31,7 @@ "PointCloudError", "HttpRangeFile", "bounds_of", + "copc_query_z_range", "load_points", "read_copc_window", ] @@ -193,6 +194,22 @@ def _import_laspy(): return laspy +def copc_query_z_range(center_z: float, halfsize: float) -> tuple[float, float]: + """The vertical range a windowed COPC query must span: the whole cube. + + Lantmäteriet's COPC writer emits octree z-keys measured from some other + origin than the file's own cube (observed live on Laserdata Skog: a level-6 + node keyed z=19, implying a -1698..-1542 m slab, holds points at +18..+42 m + while its x/y keys are exact). A 2D query lets laspy fill z from the header + and prune nodes by those broken slabs, which silently discards every dense + deep level and leaves only the sparse preview points. Spanning the full + cube keeps z from ever pruning; x/y pruning and the exact post-filter still + bound the read. + """ + pad = abs(halfsize) + 1.0 + return (center_z - 2.0 * pad, center_z + 2.0 * pad) + + def read_copc_window( session: Any, url: str, @@ -217,7 +234,9 @@ def read_copc_window( handle = HttpRangeFile(session, url, timeout=timeout) try: with CopcReader.open(handle) as reader: - query = Bounds(mins=[min_x, min_y], maxs=[max_x, max_y]) + info = reader.copc_info + z_lo, z_hi = copc_query_z_range(float(info.center[2]), float(info.halfsize)) + query = Bounds(mins=[min_x, min_y, z_lo], maxs=[max_x, max_y, z_hi]) points = reader.query(query) except PointCloudError: raise diff --git a/roofmodel/tests/test_pointcloud.py b/roofmodel/tests/test_pointcloud.py new file mode 100644 index 000000000..ea3f6a101 --- /dev/null +++ b/roofmodel/tests/test_pointcloud.py @@ -0,0 +1,29 @@ +"""The COPC window query must never prune on z. + +Lantmäteriet's COPC writer keys octree nodes with a z origin that does not +match the file's own cube (seen live: a node keyed to a slab 1.7 km underground +holding points at +18..+42 m). Pruning by those keys silently drops the dense +deep levels, so the query's vertical range has to cover every slab the cube +could describe. +""" + +from ftw_roofmodel.pointcloud import copc_query_z_range + + +def test_the_query_covers_the_whole_octree_cube(): + for center, half in [(332.51, 5000.005), (0.0, 128.0), (-250.0, 4096.0)]: + z_lo, z_hi = copc_query_z_range(center, half) + assert z_lo < center - half + assert z_hi > center + half + + +def test_the_query_stays_bounded_for_scaled_int_filters(): + # laspy's post-filter casts scaled bounds to int32; the range must stay + # proportional to the cube, never an arbitrary huge sentinel. + z_lo, z_hi = copc_query_z_range(332.51, 5000.005) + assert z_hi - z_lo < 10 * 5000.005 + + +def test_a_degenerate_cube_still_yields_a_range(): + z_lo, z_hi = copc_query_z_range(100.0, 0.0) + assert z_lo < 100.0 < z_hi From 43924e165edd29338dd0055ce6237e1715b295f1 Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Wed, 2 Sep 2026 21:47:47 +0200 Subject: [PATCH 21/26] fix(roofmodel): the derive searches where the picker searched Reported live from a 522 m2 barn at an unsaved pin: 'building ... was not found near this site'. Two causes, both fixed: the derive now sends the picker's own (possibly dragged, unsaved) coordinates instead of silently using the stored site, and its re-find of the picked footprint reaches as far as the picker's 150 m search did rather than the 40 m LiDAR radius. The LiDAR tile lookup centres on the picked building, not the pin. The module also accepts --footprint-json, a hand-drawn [lon, lat] ring that clips exactly like a picked building - the UI for it rides the drawing PR above this one. Verified live at the reported site: the exact failing pick now derives 2 arrays from 2 roof planes out of the 2019-04-04 scan. Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com> --- .changeset/derive-follows-the-pin.md | 14 ++++ go/internal/api/api.go | 29 ++++++-- go/internal/api/api_roofmodel_test.go | 40 +++++++++++ go/internal/roofmodel/roofmodel.go | 16 +++-- go/internal/roofmodel/roofmodel_test.go | 73 +++++++++++++------- roofmodel/ftw_roofmodel/__main__.py | 17 +++++ roofmodel/ftw_roofmodel/buildings.py | 39 +++++++++++ roofmodel/ftw_roofmodel/pipeline.py | 33 +++++++-- roofmodel/tests/test_derive_footprint.py | 85 +++++++++++++++++++++++- web/index.html | 2 +- web/settings/tabs/weather.js | 23 +++++-- web/settings/tabs/weather.test.mjs | 9 +++ 12 files changed, 332 insertions(+), 48 deletions(-) create mode 100644 .changeset/derive-follows-the-pin.md diff --git a/.changeset/derive-follows-the-pin.md b/.changeset/derive-follows-the-pin.md new file mode 100644 index 000000000..0776c0594 --- /dev/null +++ b/.changeset/derive-follows-the-pin.md @@ -0,0 +1,14 @@ +--- +"ftw": patch +--- + +Deriving a picked building no longer fails with "not found near this site" +in the two ways it could. The derive now searches where the picker searched: +the same (possibly dragged, unsaved) map coordinates are sent with the +request instead of silently using the stored site. And the derive's re-find +of the picked footprint now reaches as far as the picker's own search did — +it used to re-search with the 40 m LiDAR radius, so anything the 150 m picker +had found beyond that reach vanished on derive. The LiDAR tile lookup also +centres on the picked building rather than the pin. Verified live on the +reported case: a 522 m² barn picked via an unsaved pin now derives 2 arrays +from 2 roof planes. diff --git a/go/internal/api/api.go b/go/internal/api/api.go index a5d0f3fbb..c93efc73c 100644 --- a/go/internal/api/api.go +++ b/go/internal/api/api.go @@ -2471,22 +2471,37 @@ func (s *Server) handleRoofModelDerive(w http.ResponseWriter, r *http.Request) { return } lat, lon, haveSite := s.siteLocation() - if !haveSite { - writeJSON(w, 400, map[string]any{"error": "site latitude/longitude is not configured"}) - return - } // Optional: which footprint to clip the LiDAR to. An absent or empty body - // keeps the old behaviour of segmenting the whole search radius. + // keeps the old behaviour of segmenting the whole search radius. The + // coordinates mirror the buildings endpoint: the map lets you drag the + // pin before saving, and the derive must search where the picker + // searched, or the picked id is "not found near this site". var body struct { - BuildingID string `json:"building_id"` + BuildingID string `json:"building_id"` + Latitude *float64 `json:"latitude"` + Longitude *float64 `json:"longitude"` + Footprint [][]float64 `json:"footprint"` } if r.Body != nil { _ = json.NewDecoder(io.LimitReader(r.Body, 1<<16)).Decode(&body) } + if body.Latitude != nil && body.Longitude != nil { + lat, lon, haveSite = *body.Latitude, *body.Longitude, true + } + if !haveSite { + writeJSON(w, 400, map[string]any{"error": "site latitude/longitude is not configured"}) + return + } + // A hand-drawn footprint replaces the picked building where the catalog + // has no building dataset. Shape errors are the operator's to fix. + if n := len(body.Footprint); n > 0 && n < 3 { + writeJSON(w, 400, map[string]any{"error": "a drawn footprint needs at least three [lon, lat] corners"}) + return + } // A derive downloads and segments LiDAR tiles; the service time-boxes it, // but the request should also die with the client rather than outliving it. - model, err := s.deps.RoofModel.Derive(r.Context(), lat, lon, body.BuildingID) + model, err := s.deps.RoofModel.Derive(r.Context(), lat, lon, body.BuildingID, body.Footprint) if err != nil { writeJSON(w, roofModelErrorStatus(err), map[string]any{"error": err.Error()}) return diff --git a/go/internal/api/api_roofmodel_test.go b/go/internal/api/api_roofmodel_test.go index 13d5df76b..d1f947cd3 100644 --- a/go/internal/api/api_roofmodel_test.go +++ b/go/internal/api/api_roofmodel_test.go @@ -123,6 +123,46 @@ func TestRoofModelBuildingsAcceptsAnExplicitCoordinate(t *testing.T) { } } +// The derive must search where the picker searched. The picker honours the +// dragged, unsaved pin; if the derive silently used the stored site instead, +// every pick made after moving the pin would die with "not found near this +// site". +func TestRoofModelDeriveAcceptsAnExplicitCoordinate(t *testing.T) { + deps := depsAt(59.33, 18.07) + deps.RoofModel = roofmodel.FromConfig(&config.RoofModel{ + Enabled: true, Command: "definitely-not-a-real-command", + StacUsername: "u", StacPassword: "p", + }) + srv := New(deps) + + // Berlin is outside Lantmateriet coverage; if the body coordinates were + // ignored the stored Stockholm ones would be used and this would not 400. + req := httptest.NewRequest(http.MethodPost, "/api/roofmodel/derive", + strings.NewReader(`{"building_id":"b1","latitude":52.52,"longitude":13.40}`)) + req.Header.Set("Content-Type", "application/json") + rr := httptest.NewRecorder() + srv.Handler().ServeHTTP(rr, req) + if rr.Code != 400 { + t.Fatalf("status = %d, want 400 for a site outside Sweden (body %s)", rr.Code, rr.Body.String()) + } + + // Without coordinates in the body the stored site still serves. + req = httptest.NewRequest(http.MethodPost, "/api/roofmodel/derive", + strings.NewReader(`{"building_id":"b1"}`)) + req.Header.Set("Content-Type", "application/json") + rr = httptest.NewRecorder() + srv.Handler().ServeHTTP(rr, req) + var body map[string]any + _ = json.Unmarshal(rr.Body.Bytes(), &body) + msg, _ := body["error"].(string) + if msg == "" { + t.Fatal("want the spawn to fail, since the command does not exist") + } + if strings.Contains(msg, "not in Sweden") { + t.Errorf("the stored Stockholm site was not used: %v", msg) + } +} + // The catalog password is the operator's credential. Status may be reported; // the secret itself must never appear in a response. This test deliberately // stores it under the legacy geotorget_token key: a config written before the diff --git a/go/internal/roofmodel/roofmodel.go b/go/internal/roofmodel/roofmodel.go index f6ae772de..93cd95b5a 100644 --- a/go/internal/roofmodel/roofmodel.go +++ b/go/internal/roofmodel/roofmodel.go @@ -203,7 +203,7 @@ func packingFactorFrom(cfg *config.RoofModel) float64 { // so a second building sharing the ridge orientation lands inside the first // one's inlier band however far away it is and steals its returns. func (s *Service) Buildings(ctx context.Context, lat, lon float64) (*BuildingList, error) { - out, err := s.run(ctx, lat, lon, "buildings", "") + out, err := s.run(ctx, lat, lon, "buildings", "", nil) if err != nil { return nil, err } @@ -223,12 +223,15 @@ func (s *Service) Buildings(ctx context.Context, lat, lon float64) (*BuildingLis // buildingID is optional; pass one from Buildings to clip the LiDAR to that // footprint before segmenting, which is what makes the derived tilt and azimuth // belong to the operator's own roof rather than to whatever else stood in range. +// footprint is the hand-drawn alternative — a [lon, lat] ring traced on the +// map, for catalogs that publish no building dataset to pick from. It wins +// over buildingID. // // Coverage and credentials are checked before spawning anything: a site outside // Sweden can never succeed, and a missing credential fails the same way every // time, so neither is worth an interpreter start and a network round trip. -func (s *Service) Derive(ctx context.Context, lat, lon float64, buildingID string) (*Model, error) { - out, err := s.run(ctx, lat, lon, "derive", buildingID) +func (s *Service) Derive(ctx context.Context, lat, lon float64, buildingID string, footprint [][]float64) (*Model, error) { + out, err := s.run(ctx, lat, lon, "derive", buildingID, footprint) if err != nil { return nil, err } @@ -246,7 +249,7 @@ func (s *Service) Derive(ctx context.Context, lat, lon float64, buildingID strin } // run spawns the module and returns its stdout. -func (s *Service) run(ctx context.Context, lat, lon float64, mode, buildingID string) ([]byte, error) { +func (s *Service) run(ctx context.Context, lat, lon float64, mode, buildingID string, footprint [][]float64) ([]byte, error) { cfg := s.config() if cfg == nil || !cfg.Enabled { return nil, ErrDisabled @@ -278,6 +281,11 @@ func (s *Service) run(ctx context.Context, lat, lon float64, mode, buildingID st if buildingID != "" { args = append(args, "--building-id", buildingID) } + if len(footprint) > 0 { + // Marshalling [][]float64 cannot fail; the module validates the shape. + fp, _ := json.Marshal(footprint) + args = append(args, "--footprint-json", string(fp)) + } if u := cfg.StacUser(); u != "" { args = append(args, "--username", u) } diff --git a/go/internal/roofmodel/roofmodel_test.go b/go/internal/roofmodel/roofmodel_test.go index e75eda32c..72beb1d99 100644 --- a/go/internal/roofmodel/roofmodel_test.go +++ b/go/internal/roofmodel/roofmodel_test.go @@ -109,7 +109,7 @@ func TestDisabledWhenAbsentOrOff(t *testing.T) { if off.Enabled() { t.Error("disabled config must report disabled") } - if _, err := off.Derive(context.Background(), stockholmLat, stockholmLon, ""); !errors.Is(err, ErrDisabled) { + if _, err := off.Derive(context.Background(), stockholmLat, stockholmLon, "", nil); !errors.Is(err, ErrDisabled) { t.Errorf("err = %v, want ErrDisabled", err) } // A nil *Service must be safe to call, not a panic. @@ -117,7 +117,7 @@ func TestDisabledWhenAbsentOrOff(t *testing.T) { if s.Enabled() { t.Error("nil service must report disabled") } - if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, ""); !errors.Is(err, ErrDisabled) { + if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "", nil); !errors.Is(err, ErrDisabled) { t.Errorf("err = %v, want ErrDisabled", err) } s.Reconfigure(&config.RoofModel{Enabled: true}) // no-op, not a panic @@ -130,7 +130,7 @@ func TestDisabledWhenAbsentOrOff(t *testing.T) { // enablement, in both directions. func TestReconfigureAppliesCredentialsWithoutRestart(t *testing.T) { s := svc(t, &config.RoofModel{Enabled: true}) - if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, ""); !errors.Is(err, ErrNoCredentials) { + if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "", nil); !errors.Is(err, ErrNoCredentials) { t.Fatalf("before reconfigure: err = %v, want ErrNoCredentials", err) } @@ -139,12 +139,12 @@ func TestReconfigureAppliesCredentialsWithoutRestart(t *testing.T) { Command: stubModule(t, "stdout", minimalModel), StacUsername: "operator", StacPassword: "secret", }) - if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, ""); err != nil { + if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "", nil); err != nil { t.Fatalf("after reconfigure with credentials: %v", err) } s.Reconfigure(&config.RoofModel{Enabled: false}) - if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, ""); !errors.Is(err, ErrDisabled) { + if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "", nil); !errors.Is(err, ErrDisabled) { t.Fatalf("after disabling: err = %v, want ErrDisabled", err) } } @@ -166,7 +166,7 @@ func TestDeriveRefusesOutsideSwedenWithoutSpawning(t *testing.T) { {"New York", 40.71, -74.01}, {"north of Sweden", 71.0, 20.0}, } { - _, err := s.Derive(context.Background(), c.lat, c.lon, "") + _, err := s.Derive(context.Background(), c.lat, c.lon, "", nil) if !errors.Is(err, ErrOutsideCoverage) { t.Errorf("%s: err = %v, want ErrOutsideCoverage", c.name, err) } @@ -188,7 +188,7 @@ func TestSwedishBoxAdmitsSomeNonSwedishPointsByDesign(t *testing.T) { Command: "definitely-not-a-real-command", StacUsername: "u", StacPassword: "t", }) - _, err := s.Derive(context.Background(), 59.91, 10.75, "") // Oslo + _, err := s.Derive(context.Background(), 59.91, 10.75, "", nil) // Oslo if errors.Is(err, ErrOutsideCoverage) { t.Skip("box now excludes Oslo; verify it still admits Strömstad and Haparanda") } @@ -214,7 +214,7 @@ func TestSwedishBoxCoversBorderTowns(t *testing.T) { {"Karesuando (far north)", 68.44, 22.49}, {"Smygehuk (far south)", 55.34, 13.36}, } { - _, err := s.Derive(context.Background(), c.lat, c.lon, "") + _, err := s.Derive(context.Background(), c.lat, c.lon, "", nil) if errors.Is(err, ErrOutsideCoverage) { t.Errorf("%s: must not be excluded", c.name) } @@ -230,7 +230,7 @@ func TestDeriveRequiresCredentials(t *testing.T) { {"neither", "", ""}, } { s := svc(t, &config.RoofModel{Enabled: true, Command: "no-such-command", StacUsername: c.user, StacPassword: c.token}) - _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "") + _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "", nil) if !errors.Is(err, ErrNoCredentials) { t.Errorf("%s: err = %v, want ErrNoCredentials", c.name, err) } @@ -246,7 +246,7 @@ func TestDeriveParsesAModel(t *testing.T) { cmd := stubModule(t, "stdout", doc) s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, StacUsername: "u", StacPassword: "t"}) - m, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "") + m, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "", nil) if err != nil { t.Fatal(err) } @@ -273,7 +273,7 @@ func TestDeriveCarriesHowTheLidarWasFetched(t *testing.T) { cmd := stubModule(t, "stdout", doc) s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, StacUsername: "u", StacPassword: "t"}) - m, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "b-1") + m, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "b-1", nil) if err != nil { t.Fatal(err) } @@ -297,7 +297,7 @@ func TestDerivePassesTheSiteAndCredentials(t *testing.T) { RadiusM: 25, }) - if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, ""); err != nil { + if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "", nil); err != nil { t.Fatal(err) } @@ -344,7 +344,7 @@ func TestDerivePassesThePickedBuilding(t *testing.T) { StacUsername: "u", StacPassword: "t", }) - if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "bldg-42"); err != nil { + if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "bldg-42", nil); err != nil { t.Fatal(err) } @@ -357,6 +357,31 @@ func TestDerivePassesThePickedBuilding(t *testing.T) { } } +// A hand-drawn footprint is the picker for catalogs with no building dataset; +// it must reach the module as the exact [lon, lat] ring that was traced. +func TestDerivePassesTheDrawnFootprint(t *testing.T) { + dir := t.TempDir() + record := dir + string(os.PathSeparator) + "invocation.json" + cmd := stubModule(t, "record", record) + s := svc(t, &config.RoofModel{ + Enabled: true, Command: cmd, + StacUsername: "u", StacPassword: "t", + }) + + ring := [][]float64{{18.06, 59.32}, {18.07, 59.32}, {18.07, 59.33}} + if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "", ring); err != nil { + t.Fatal(err) + } + + line := strings.Join(readInvocation(t, record).Args, " ") + if !strings.Contains(line, `--footprint-json [[18.06,59.32],[18.07,59.32],[18.07,59.33]]`) { + t.Errorf("args %q did not carry the drawn footprint", line) + } + if strings.Contains(line, "--building-id") { + t.Errorf("args %q sent a building id nobody picked", line) + } +} + // Not picking one must not send an empty flag the module would treat as a // building named "". func TestDeriveOmitsTheBuildingFlagWhenNoneIsPicked(t *testing.T) { @@ -367,7 +392,7 @@ func TestDeriveOmitsTheBuildingFlagWhenNoneIsPicked(t *testing.T) { Enabled: true, Command: cmd, StacUsername: "u", StacPassword: "t", }) - if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, ""); err != nil { + if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "", nil); err != nil { t.Fatal(err) } @@ -453,7 +478,7 @@ func TestDeriveSurfacesTheModuleErrorMessage(t *testing.T) { `{"error":"Geotorget rejected the credentials","kind":"MissingCredentials"}`) s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, StacUsername: "u", StacPassword: "t"}) - _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "") + _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "", nil) if err == nil { t.Fatal("want an error") } @@ -474,7 +499,7 @@ func TestModuleErrorSurvivesLibraryWarnings(t *testing.T) { `{"error":"STAC search returned HTTP 404","kind":"GeotorgetError"}`+"\n") s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, StacUsername: "u", StacPassword: "t"}) - _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "") + _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "", nil) if err == nil { t.Fatal("want an error") } @@ -489,7 +514,7 @@ func TestDeriveReportsNonJSONFailure(t *testing.T) { cmd := stubModule(t, "stderr", "Traceback (most recent call last):\n MemoryError\n") s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, StacUsername: "u", StacPassword: "t"}) - _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "") + _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "", nil) if err == nil || !strings.Contains(err.Error(), "roof model failed") { t.Errorf("err = %v, want a plain failure", err) } @@ -499,7 +524,7 @@ func TestDeriveRejectsUnknownSchemaVersion(t *testing.T) { cmd := stubModule(t, "stdout", `{"schema_version":99,"arrays":[]}`) s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, StacUsername: "u", StacPassword: "t"}) - _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "") + _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "", nil) if err == nil || !strings.Contains(err.Error(), "schema_version") { t.Errorf("err = %v, want a schema-version rejection", err) } @@ -509,7 +534,7 @@ func TestDeriveRejectsUnreadableOutput(t *testing.T) { cmd := stubModule(t, "stdout", "not-json-at-all") s := svc(t, &config.RoofModel{Enabled: true, Command: cmd, StacUsername: "u", StacPassword: "t"}) - _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "") + _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "", nil) if err == nil || !strings.Contains(err.Error(), "unreadable") { t.Errorf("err = %v, want an unreadable-output error", err) } @@ -525,7 +550,7 @@ func TestDeriveIsTimeBoxed(t *testing.T) { }) start := time.Now() - _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "") + _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "", nil) if err == nil || !strings.Contains(err.Error(), "timed out") { t.Errorf("err = %v, want a timeout", err) } @@ -576,7 +601,7 @@ func TestDeriveAcceptsLegacyGeotorgetKeys(t *testing.T) { GeotorgetUsername: "legacy-op", GeotorgetToken: "legacy-secret", }) - if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, ""); err != nil { + if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "", nil); err != nil { t.Fatal(err) } @@ -616,7 +641,7 @@ func TestDeriveCustomCatalogSkipsSwedenGateAndPassesStacArgs(t *testing.T) { }) // Berlin: outside Lantmäteriet coverage, fine for a custom catalog. - if _, err := s.Derive(context.Background(), 52.52, 13.40, ""); err != nil { + if _, err := s.Derive(context.Background(), 52.52, 13.40, "", nil); err != nil { t.Fatal(err) } @@ -653,7 +678,7 @@ func TestDeriveCustomCatalogWorksAnonymously(t *testing.T) { StacBaseURL: "https://stac.example.org", }) - if _, err := s.Derive(context.Background(), 52.52, 13.40, ""); err != nil { + if _, err := s.Derive(context.Background(), 52.52, 13.40, "", nil); err != nil { t.Fatal(err) } @@ -683,7 +708,7 @@ func TestDeriveDefaultCatalogStillRefusesOutsideSweden(t *testing.T) { Enabled: true, Command: "no-such-command", StacUsername: "u", StacPassword: "p", }) - _, err := s.Derive(context.Background(), 52.52, 13.40, "") + _, err := s.Derive(context.Background(), 52.52, 13.40, "", nil) if !errors.Is(err, ErrOutsideCoverage) { t.Errorf("err = %v, want ErrOutsideCoverage", err) } diff --git a/roofmodel/ftw_roofmodel/__main__.py b/roofmodel/ftw_roofmodel/__main__.py index 5cded24b4..3deb25863 100644 --- a/roofmodel/ftw_roofmodel/__main__.py +++ b/roofmodel/ftw_roofmodel/__main__.py @@ -38,6 +38,13 @@ def main(argv: list[str] | None = None) -> int: default="", help="footprint to clip the LiDAR to, from a --mode buildings run", ) + p.add_argument( + "--footprint-json", + default="", + help="hand-drawn footprint to clip the LiDAR to, as a JSON array of " + "[lon, lat] pairs — for catalogs that publish no building dataset. " + "Wins over --building-id.", + ) p.add_argument("--search-radius-m", type=float, default=DEFAULT_SEARCH_RADIUS_M) p.add_argument("--username", default="", help="STAC catalog username (Geotorget account)") p.add_argument( @@ -93,6 +100,15 @@ def main(argv: list[str] | None = None) -> int: "buildings": [b.to_geojson() for b in found], } else: + footprint = None + if args.footprint_json: + try: + footprint = json.loads(args.footprint_json) + except ValueError as exc: + json.dump({"error": f"--footprint-json is not valid JSON: {exc}", + "kind": "ValueError"}, sys.stderr) + sys.stderr.write("\n") + return 1 payload = derive( latitude=args.lat, longitude=args.lon, @@ -101,6 +117,7 @@ def main(argv: list[str] | None = None) -> int: packing_factor=args.packing_factor, module_w_per_m2=args.module_w_per_m2, building_id=args.building_id or None, + footprint=footprint, base_url=args.stac_base_url, buildings_collection=args.buildings_collection, lidar_collection=args.lidar_collection, diff --git a/roofmodel/ftw_roofmodel/buildings.py b/roofmodel/ftw_roofmodel/buildings.py index de9078559..2cdc40c8f 100644 --- a/roofmodel/ftw_roofmodel/buildings.py +++ b/roofmodel/ftw_roofmodel/buildings.py @@ -129,6 +129,45 @@ def _looks_like_sweref(ring: Iterable[Iterable[float]]) -> bool: return False +def building_from_drawn_footprint(points: list[Any]) -> Building: + """A hand-drawn outline as a Building, for catalogs with no footprints. + + `points` are GeoJSON-style [lon, lat] pairs traced on the map. The + operator's drawing stands in for the picker where no building dataset is + published (IGN's LiDAR HD, for instance, ships point clouds but no + footprints). The ring is projected to the pipeline's working frame; where + the point cloud arrives in another projection the clip fails loudly with + "no returns fall on the footprint" rather than clipping the wrong spot. + """ + ring: list[tuple[float, float]] = [] + for pt in points or []: + seq = list(pt) + if len(seq) < 2: + continue + lon, lat = float(seq[0]), float(seq[1]) + n, e = sweref.wgs84_to_sweref99tm(lat, lon) + ring.append((e, n)) + if len(ring) >= 2 and ring[0] == ring[-1]: + ring.pop() + if len(ring) < 3: + raise BuildingLookupError( + "a drawn footprint needs at least three corners" + ) + area = _shoelace_area(ring) + if area < MIN_FOOTPRINT_AREA_M2: + raise BuildingLookupError( + f"the drawn footprint encloses {area:.1f} m2, too small to hold a " + "roof; draw the building's outline, not a point" + ) + return Building( + building_id="drawn-footprint", + ring_sweref=ring, + area_m2=area, + distance_m=0.0, + properties={"source": "drawn"}, + ) + + def _shoelace_area(ring: list[tuple[float, float]]) -> float: """Planar polygon area in square metres. Ring must be projected.""" n = len(ring) diff --git a/roofmodel/ftw_roofmodel/pipeline.py b/roofmodel/ftw_roofmodel/pipeline.py index e76ecdcc7..f8f764e3c 100644 --- a/roofmodel/ftw_roofmodel/pipeline.py +++ b/roofmodel/ftw_roofmodel/pipeline.py @@ -20,7 +20,9 @@ from . import geotorget, pointcloud, sweref from .buildings import ( DEFAULT_EAVES_BUFFER_M, + DEFAULT_SEARCH_RADIUS_M, Building, + building_from_drawn_footprint, clip_to_footprint, search_buildings, ) @@ -187,6 +189,7 @@ def derive( packing_factor: float = DEFAULT_PACKING_FACTOR, module_w_per_m2: float = DEFAULT_MODULE_W_PER_M2, building_id: str | None = None, + footprint: list[Any] | None = None, now: dt.datetime | None = None, base_url: str = geotorget.DEFAULT_BASE_URL, buildings_collection: str = COLLECTION_BUILDINGS, @@ -196,9 +199,12 @@ def derive( """Derive a roof model for one site and return it as a JSON-ready dict. Pass `building_id` -- one the operator picked from `search_buildings` -- to - clip the LiDAR to that footprint before segmenting. Without it the whole - radius is segmented, which will happily return the neighbour's roof and lets - coplanar buildings steal each other's points; see buildings.py. + clip the LiDAR to that footprint before segmenting, or `footprint` -- a + hand-drawn [lon, lat] ring -- where the catalog publishes no building + dataset to pick from. A drawn footprint wins over a building id. Without + either the whole radius is segmented, which will happily return the + neighbour's roof and lets coplanar buildings steal each other's points; + see buildings.py. The catalog parameters default to Lantmaeteriet; any STAC-conformant catalog can stand in (see geotorget.py), as long as its data arrives in @@ -218,9 +224,16 @@ def derive( ) chosen: Building | None = None - if building_id: + if footprint: + chosen = building_from_drawn_footprint(footprint) + elif building_id: + # Re-find the picked footprint with the same reach the picker's own + # search had. `radius_m` is the LiDAR/segmentation radius (tens of + # metres); a building the operator picked from the 150 m search would + # vanish here if it alone bounded the re-search. candidates = search_buildings( - client, latitude=latitude, longitude=longitude, radius_m=radius_m, + client, latitude=latitude, longitude=longitude, + radius_m=max(radius_m, DEFAULT_SEARCH_RADIUS_M), collection=buildings_collection, bbox_epsg=bbox_epsg, ) chosen = next((b for b in candidates if b.building_id == building_id), None) @@ -230,7 +243,13 @@ def derive( "have been picked against a different coordinate" ) - bbox = sweref.stac_search_bbox(latitude, longitude, radius_m, bbox_epsg) + # The LiDAR lookup centres on the roof being derived: for a picked + # building that is its centroid, not the site pin — a barn at the edge of + # the search radius must find *its* tile, not the pin's. + lidar_lat, lidar_lon = latitude, longitude + if chosen is not None: + lidar_lat, lidar_lon = chosen.centroid_wgs84() + bbox = sweref.stac_search_bbox(lidar_lat, lidar_lon, radius_m, bbox_epsg) try: lidar_items: list[StacItem] = lidar_client.search(lidar_collection, bbox) @@ -239,7 +258,7 @@ def derive( if not lidar_items: hint = "; Lantmaeteriet data is Sweden only" if lidar_collection == COLLECTION_LIDAR else "" raise RoofModelError( - f"no LiDAR tiles cover ({latitude:.5f}, {longitude:.5f}) in " + f"no LiDAR tiles cover ({lidar_lat:.5f}, {lidar_lon:.5f}) in " f"collection {lidar_collection!r}{hint}" ) diff --git a/roofmodel/tests/test_derive_footprint.py b/roofmodel/tests/test_derive_footprint.py index 0ca6e161e..466b8fae5 100644 --- a/roofmodel/tests/test_derive_footprint.py +++ b/roofmodel/tests/test_derive_footprint.py @@ -8,7 +8,7 @@ import pytest from ftw_roofmodel import pipeline, sweref -from ftw_roofmodel.buildings import clip_to_footprint +from ftw_roofmodel.buildings import BuildingLookupError, clip_to_footprint from ftw_roofmodel.geotorget import COLLECTION_BUILDINGS, Credentials, StacItem from ftw_roofmodel.pipeline import RoofModelError, derive from ftw_roofmodel.segment import segment_roof @@ -40,9 +40,11 @@ def __init__(self, buildings_payload, points): self._buildings = buildings_payload self._points = points self.searched = [] + self.searched_boxes = [] def search(self, collection, bbox, limit=20): self.searched.append(collection) + self.searched_boxes.append((collection, bbox)) if collection == COLLECTION_BUILDINGS: return [StacItem(f["id"], collection, {}, None, raw=f) for f in self._buildings] return [StacItem("lidar-1", collection, {"data": "http://x/tile.laz"}, None, raw={})] @@ -147,3 +149,84 @@ def test_derive_explains_a_footprint_with_no_returns_on_it(monkeypatch, scene): ) msg = str(exc.value) assert "fall on building" in msg and "newer than the scan" in msg + + +def test_derive_clips_to_a_drawn_footprint(scene): + """Where the catalog has no building dataset, the operator traces the + outline by hand — and that must clip exactly like a picked building, + without any building search happening at all.""" + client, cloud, (e, n) = scene + corners = [] + for ee, nn in ring(e, n, 12, 12): + lat, lon = sweref.sweref99tm_to_wgs84(nn, ee) + corners.append([lon, lat]) + corners.append(list(corners[0])) # GeoJSON rings close themselves + + model = derive( + latitude=STOCKHOLM[0], longitude=STOCKHOLM[1], + credentials=Credentials("u", "t"), client=client, footprint=corners, + ) + + assert COLLECTION_BUILDINGS not in client.searched + b = model["building"] + assert b["building_id"] == "drawn-footprint" + assert b["returns_used"] < b["returns_in_radius"] * 0.6 + assert model["arrays"], "a traced house still has a south roof" + + +def test_a_drawn_footprint_needs_three_corners(scene): + client, _, _ = scene + with pytest.raises(Exception) as exc: + derive( + latitude=STOCKHOLM[0], longitude=STOCKHOLM[1], + credentials=Credentials("u", "t"), client=client, + footprint=[[18.06, 59.33], [18.07, 59.33]], + ) + assert "three corners" in str(exc.value) + + +def test_the_re_search_reaches_as_far_as_the_picker_did(monkeypatch, scene): + """A barn 100 m out is inside the picker's 150 m search but outside the + 40 m LiDAR radius. The derive's re-find must use the picker's reach, or + every such pick dies with "not found near this site".""" + client, _, _ = scene + real_search = pipeline.search_buildings + radii = [] + + def windowed(client_, *, latitude, longitude, radius_m, **kw): + radii.append(radius_m) + # Model the real windowing: the barn only comes back when the search + # reaches at least as far as the picker's default. + if radius_m < 150.0: + raise BuildingLookupError("nothing inside this window") + return real_search(client_, latitude=latitude, longitude=longitude, + radius_m=radius_m, **kw) + + monkeypatch.setattr(pipeline, "search_buildings", windowed) + model = derive( + latitude=STOCKHOLM[0], longitude=STOCKHOLM[1], + credentials=Credentials("u", "t"), client=client, building_id="mine", + ) + assert model["building"]["building_id"] == "mine" + assert radii and min(radii) >= 150.0 + + +def test_the_lidar_lookup_centres_on_the_picked_building(scene): + """The tile search must cover the roof being derived, not the pin: a + building at the search edge can sit on a different tile.""" + client, _, (e, n) = scene + derive( + latitude=STOCKHOLM[0], longitude=STOCKHOLM[1], + credentials=Credentials("u", "t"), client=client, building_id="neighbour", + ) + lidar_bboxes = [b for c, b in getattr(client, "searched_boxes", []) + if c != COLLECTION_BUILDINGS] + assert lidar_bboxes, "the LiDAR collection was searched" + west, south, east, north = lidar_bboxes[-1] + centre_n, centre_e = sweref.wgs84_to_sweref99tm( + (south + north) / 2.0, (west + east) / 2.0 + ) + # The neighbour's centroid is 40 m east of the site; the bbox centre must + # follow it rather than stay on the pin. + assert centre_e == pytest.approx(e + 40 + 6, abs=15.0) + assert centre_n == pytest.approx(n + 6, abs=15.0) diff --git a/web/index.html b/web/index.html index b24235a06..f1577a0b3 100644 --- a/web/index.html +++ b/web/index.html @@ -995,7 +995,7 @@

Price bars (top of the chart)

- + diff --git a/web/settings/tabs/weather.js b/web/settings/tabs/weather.js index 62cfa6293..8c409e24a 100644 --- a/web/settings/tabs/weather.js +++ b/web/settings/tabs/weather.js @@ -302,7 +302,7 @@ // brings their own credentials. Everything degrades to the numeric fields // above when the module, the credentials or the coverage is missing. - var roofState = { features: [], selectedId: null }; + var roofState = { features: [], selectedId: null, drawnFootprint: null }; // Known point-cloud catalogs, verified live 2026-09-02. `base: ""` means // the module's built-in Lantmäteriet defaults; `base: null` marks the @@ -567,6 +567,9 @@ function selectBuilding(id) { roofState.selectedId = id; + // A picked building and a drawn footprint answer the same question; + // the newest answer wins. + roofState.drawnFootprint = null; drawBuildings(); var list = document.getElementById("roof-buildings"); if (list) { @@ -604,12 +607,24 @@ } function deriveRoof(ctx) { - if (!roofState.selectedId) return; + if (!roofState.selectedId && !roofState.drawnFootprint) return; roofSay("Reading the laser scan and fitting roof planes… this can take a minute."); + // Send the same coordinates the building search used — the live form + // state, saved or not. Deriving against the stored site while the pin + // has moved makes the picked id "not found near this site". + var w = (ctx.config && ctx.config.weather) || {}; + var payload = {}; + if (roofState.selectedId) payload.building_id = roofState.selectedId; + else payload.footprint = roofState.drawnFootprint; + var lat = parseFloat(w.latitude), lon = parseFloat(w.longitude); + if (isFinite(lat) && isFinite(lon)) { + payload.latitude = lat; + payload.longitude = lon; + } fetch("/api/roofmodel/derive", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ building_id: roofState.selectedId }), + body: JSON.stringify(payload), }) .then(function (r) { return r.json().then(function (d) { return { ok: r.ok, d: d }; }); }) .then(function (res) { @@ -708,7 +723,7 @@ renderPVArrays(ctx); refreshArraysSummary(ctx.config); }); - roofState = { features: [], selectedId: null }; + roofState = { features: [], selectedId: null, drawnFootprint: null }; var catalogSel = document.getElementById("roof-catalog"); if (catalogSel) catalogSel.addEventListener("change", function () { var chosen = null; diff --git a/web/settings/tabs/weather.test.mjs b/web/settings/tabs/weather.test.mjs index 8cdebb481..6966a00b3 100644 --- a/web/settings/tabs/weather.test.mjs +++ b/web/settings/tabs/weather.test.mjs @@ -146,4 +146,13 @@ describe("roof buildings on the map", () => { it("retries the draw once the style has loaded", () => { assert.match(source, /map\.once\("load", drawBuildings\)/); }); + + it("derives at the same coordinates the picker searched", () => { + // Both requests read the live form state; deriving against the stored + // site while the pin has moved makes the picked building "not found + // near this site". + assert.match(source, /payload\.latitude = lat/); + assert.match(source, /payload\.longitude = lon/); + assert.match(source, /body: JSON\.stringify\(payload\)/); + }); }); From 37ea70a83cfc3b5aa6d3ce698bfe9d811440e616 Mon Sep 17 00:00:00 2001 From: Claude Opus 5 Date: Wed, 5 Aug 2026 12:59:48 +0200 Subject: [PATCH 22/26] feat(web): draw your PV arrays on the map Typing tilt, azimuth and kWp for a roof you can see out of the window is the worst part of setting FTW up, and the numbers people type are guesses. The Weather tab's existing Leaflet map now takes a rectangle drawn over the panels and turns it into a weather.pv_arrays entry. The rectangle is angled rather than square to north, because the angle is where the answer lives: its long edge follows the ridge, and the face is perpendicular to that. Two directions are perpendicular to a ridge and an overhead outline genuinely does not say which, so the equatorward one is offered as a default with a one-click flip, never as a measurement. Tilt cannot be seen from above at all, so it is typed once before drawing -- and it is also what converts the outline into panel area. What you trace on a map is the horizontal projection of a sloped rectangle, so a 35 deg roof carries about 22% more panel than its outline suggests; without that division every drawn array would be quietly under-sized. Capacity uses the same 0.70 packing factor and 200 W/m2 module density as the Lantmateriet roof model, so a drawn array and a derived one compare. The geometry is a pure ES module, tested against fixtures built from the standard ellipsoidal metres-per-degree series rather than from its own spherical projection -- a shape round-tripped through the code under test would have agreed with itself and proved nothing. Terra Draw and its Leaflet adapter (both MIT) ship UMD builds, so they lazy-load behind real SRI hashes instead of the bare version pinning an ES module would have forced. The adapter captures window.leaflet as it evaluates while Leaflet only ever defines window.L, so that alias is load-bearing rather than tidiness. If either bundle fails to load, the numeric editor is untouched and the page says so. Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com> Signed-off-by: Hugo Karlsson <48095810+HuggeK@users.noreply.github.com> --- .changeset/pv-array-draw-on-map.md | 11 ++ web/components/pv-array-geometry.js | 208 ++++++++++++++++++++++ web/components/pv-array-geometry.test.mjs | 203 +++++++++++++++++++++ web/index.html | 2 +- web/settings/tabs/weather.js | 164 ++++++++++++++++- 5 files changed, 586 insertions(+), 2 deletions(-) create mode 100644 .changeset/pv-array-draw-on-map.md create mode 100644 web/components/pv-array-geometry.js create mode 100644 web/components/pv-array-geometry.test.mjs diff --git a/.changeset/pv-array-draw-on-map.md b/.changeset/pv-array-draw-on-map.md new file mode 100644 index 000000000..c018ec157 --- /dev/null +++ b/.changeset/pv-array-draw-on-map.md @@ -0,0 +1,11 @@ +--- +"ftw": minor +--- + +PV arrays can be drawn on the map instead of measured by hand. The Weather tab gains a rectangle tool over the existing MapLibre map: draw over your panels and it becomes a `weather.pv_arrays` entry, with area from the shape and azimuth from the way the shape is turned. Capacity is stored as `rated_w` (watts). + +The rectangle is drawn at an angle rather than square to north, because the angle is the point — the long edge follows the ridge, and the face is perpendicular to it. Two directions are perpendicular to a ridge and a flat outline genuinely does not say which, so the equatorward one is offered as a default with a one-click flip, never as a measurement. + +Tilt is the one number an overhead outline cannot contain, so it is typed once before drawing — and it is also what turns the outline into panel area. What you trace on a map is the horizontal projection of a sloped rectangle, so a 35° roof carries about 22 % more panel than its outline suggests; ignoring that would quietly under-size every drawn array. Capacity uses the same packing factor and module density as the Lantmäteriet roof model, so a drawn array and a derived one are comparable. + +Drawing is progressive enhancement: [Terra Draw](https://github.com/JamesLMilner/terra-draw) and its MapLibre adapter (both MIT) are lazy-loaded from CDN with SRI hashes only when the tool is first used, and if either fails to load the numeric editor is untouched and says so. The editor stays the final word for drawn and typed arrays alike. diff --git a/web/components/pv-array-geometry.js b/web/components/pv-array-geometry.js new file mode 100644 index 000000000..110f903e6 --- /dev/null +++ b/web/components/pv-array-geometry.js @@ -0,0 +1,208 @@ +// Turning a rectangle drawn on the map into a PV array. +// +// Drawing supplies two of the three numbers an array needs. Area comes from +// the shape; azimuth from how the shape is turned. Tilt cannot be seen from +// directly above at all, so it stays typed — and it is also what converts the +// drawn outline into real panel area, because what you trace on a map is the +// *horizontal projection* of a sloped rectangle, not the rectangle itself. +// +// Everything here is pure so it can be tested without a browser or a map. + +export const DEFAULT_PACKING_FACTOR = 0.7; +export const DEFAULT_MODULE_W_PER_M2 = 200; +export const DEFAULT_TILT_DEG = 35; + +// IUGG mean Earth radius. At the scale of one roof the radius matters far +// less than the flat-Earth approximation below, which is exact enough for a +// 20 m rectangle and meaningless across a county. +const EARTH_RADIUS_M = 6371008.8; +const DEG = Math.PI / 180; + +// Beyond this a roof is a wall: cos(tilt) approaches zero and the plan-area +// division runs away. A wall has no horizontal projection to trace anyway, so +// clamping here bounds the arithmetic instead of returning Infinity. +const MAX_TILT_FOR_PROJECTION_DEG = 89; + +function stripClosingVertex(ring) { + if (ring.length > 1) { + const first = ring[0]; + const last = ring[ring.length - 1]; + if (first[0] === last[0] && first[1] === last[1]) return ring.slice(0, -1); + } + return ring.slice(); +} + +/** + * Project a WGS84 ring ([[lon, lat], …]) to metres about its own centroid. + * + * A local tangent plane, not a real projection: over one building the error + * is well under the precision anyone draws with, and it avoids carrying a + * projection library into the settings page. + */ +export function toLocalMetres(ring) { + const pts = stripClosingVertex(ring || []); + if (pts.length === 0) return []; + let lon0 = 0; + let lat0 = 0; + for (const [lon, lat] of pts) { + lon0 += lon; + lat0 += lat; + } + lon0 /= pts.length; + lat0 /= pts.length; + const mPerDegLat = EARTH_RADIUS_M * DEG; + const mPerDegLon = mPerDegLat * Math.cos(lat0 * DEG); + return pts.map(([lon, lat]) => [(lon - lon0) * mPerDegLon, (lat - lat0) * mPerDegLat]); +} + +/** Area of the drawn outline in m², as seen from above. */ +export function planAreaM2(ring) { + const p = toLocalMetres(ring); + if (p.length < 3) return 0; + let twiceArea = 0; + for (let i = 0; i < p.length; i++) { + const [x1, y1] = p[i]; + const [x2, y2] = p[(i + 1) % p.length]; + twiceArea += x1 * y2 - x2 * y1; + } + return Math.abs(twiceArea) / 2; +} + +/** Compass bearing of a local vector, 0 = north, 90 = east. */ +function bearingDeg(dx, dy) { + return (((Math.atan2(dx, dy) / DEG) % 360) + 360) % 360; +} + +/** + * Direction of the ring's longest edge, as a line in [0, 180). + * + * For a panel rectangle that edge runs along the ridge, which is a line and + * not an arrow — calling it "north" rather than "south" would be a + * distinction the drawing does not contain. + */ +export function ridgeAzimuthDeg(ring) { + const p = toLocalMetres(ring); + if (p.length < 2) return null; + let longest = 0; + let bx = 0; + let by = 0; + for (let i = 0; i < p.length; i++) { + const [x1, y1] = p[i]; + const [x2, y2] = p[(i + 1) % p.length]; + const dx = x2 - x1; + const dy = y2 - y1; + const len = Math.hypot(dx, dy); + if (len > longest) { + longest = len; + bx = dx; + by = dy; + } + } + if (longest <= 0) return null; + return bearingDeg(bx, by) % 180; +} + +/** Shortest angle between two compass bearings, in degrees. */ +export function angularDistanceDeg(a, b) { + const d = Math.abs((((a - b) % 360) + 360) % 360); + return d > 180 ? 360 - d : d; +} + +/** Turn an azimuth to face the opposite way. */ +export function flipAzimuthDeg(azimuthDeg) { + return ((((azimuthDeg + 180) % 360) + 360) % 360); +} + +/** + * The two directions the face could point: perpendicular to the ridge, either + * side of it. A flat outline genuinely does not say which. + */ +export function faceAzimuthCandidates(ring) { + const ridge = ridgeAzimuthDeg(ring); + if (ridge === null) return []; + return [(ridge + 90) % 360, (ridge + 270) % 360]; +} + +/** + * The candidate a panel is more likely to use: the equatorward one. + * + * This is a default, not a measurement. Both perpendiculars fit the drawing + * equally well, so the UI offers a flip rather than pretending to know. + */ +export function preferredAzimuthDeg(ring, latitudeDeg) { + const candidates = faceAzimuthCandidates(ring); + if (candidates.length === 0) return null; + const target = (latitudeDeg || 0) >= 0 ? 180 : 0; + const [a, b] = candidates; + return angularDistanceDeg(a, target) <= angularDistanceDeg(b, target) ? a : b; +} + +/** + * Real panel area from the traced outline. + * + * A sloped rectangle of area A casts a shadow of A·cos(tilt) on the map, so + * recovering it divides that back out. A 35° roof carries about 22 % more + * panel than its outline suggests, which is the difference between a + * believable rating and a quietly low one. + */ +export function slopeAreaM2(planArea, tiltDeg) { + const tilt = Math.min(Math.max(tiltDeg || 0, 0), MAX_TILT_FOR_PROJECTION_DEG); + return planArea / Math.cos(tilt * DEG); +} + +/** Installable DC capacity in watts for a roof area, matching the roof model's basis. */ +export function ratedWFromSlopeArea(areaM2, packingFactor, moduleWPerM2) { + const packing = packingFactor == null ? DEFAULT_PACKING_FACTOR : packingFactor; + const wPerM2 = moduleWPerM2 == null ? DEFAULT_MODULE_W_PER_M2 : moduleWPerM2; + return areaM2 * packing * wPerM2; +} + +/** Human-readable face name, mirroring the roof model's naming. */ +export function compassName(azimuthDeg, tiltDeg) { + if (tiltDeg < 5) return "Roof flat"; + const points = [ + [0, "north"], [45, "north-east"], [90, "east"], [135, "south-east"], + [180, "south"], [225, "south-west"], [270, "west"], [315, "north-west"], + [360, "north"], + ]; + let best = points[0]; + for (const p of points) { + if (Math.abs(p[0] - azimuthDeg) < Math.abs(best[0] - azimuthDeg)) best = p; + } + return `Roof ${best[1]}`; +} + +function round(value, places) { + const factor = 10 ** places; + return Math.round(value * factor) / factor; +} + +/** + * Everything a drawn rectangle says about one array. + * + * Returns the config-shaped entry separately from the measurements, so only + * the four fields weather.pv_arrays actually defines are ever written back. + */ +export function arrayFromRing(ring, options) { + const opts = options || {}; + const plan = planAreaM2(ring); + if (!(plan > 0)) return null; + const tiltDeg = opts.tiltDeg == null ? DEFAULT_TILT_DEG : opts.tiltDeg; + const candidates = faceAzimuthCandidates(ring); + const azimuth = opts.azimuthDeg == null + ? preferredAzimuthDeg(ring, opts.latitude) + : opts.azimuthDeg; + const azimuthDeg = azimuth == null ? 180 : Math.round(azimuth); + const slope = slopeAreaM2(plan, tiltDeg); + return { + array: { + name: opts.name || compassName(azimuthDeg, tiltDeg), + rated_w: Math.round(ratedWFromSlopeArea(slope, opts.packingFactor, opts.moduleWPerM2)), + tilt_deg: tiltDeg, + azimuth_deg: azimuthDeg, + }, + planAreaM2: round(plan, 1), + slopeAreaM2: round(slope, 1), + azimuthCandidates: candidates.map((c) => Math.round(c)), + }; +} diff --git a/web/components/pv-array-geometry.test.mjs b/web/components/pv-array-geometry.test.mjs new file mode 100644 index 000000000..56b112cfb --- /dev/null +++ b/web/components/pv-array-geometry.test.mjs @@ -0,0 +1,203 @@ +// node --test web/components/pv-array-geometry.test.mjs + +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { + DEFAULT_MODULE_W_PER_M2, + DEFAULT_PACKING_FACTOR, + angularDistanceDeg, + arrayFromRing, + compassName, + faceAzimuthCandidates, + flipAzimuthDeg, + ratedWFromSlopeArea, + planAreaM2, + preferredAzimuthDeg, + ridgeAzimuthDeg, + slopeAreaM2, +} from "./pv-array-geometry.js"; + +// Fixtures are built with the standard ellipsoidal metres-per-degree series, +// deliberately *not* with the module's own spherical projection — a shape +// round-tripped through the code under test would agree with itself and prove +// nothing. The two disagree by roughly half a percent in area at this +// latitude, which is the known bias of a spherical Earth against WGS84 and is +// far below the precision anyone draws a roof with. +const STOCKHOLM = { lat: 59.3293, lon: 18.0686 }; + +function metresPerDegree(latDeg) { + const p = (latDeg * Math.PI) / 180; + return { + lat: 111132.92 - 559.82 * Math.cos(2 * p) + 1.175 * Math.cos(4 * p) + - 0.0023 * Math.cos(6 * p), + lon: 111412.84 * Math.cos(p) - 93.5 * Math.cos(3 * p) + 0.118 * Math.cos(5 * p), + }; +} + +/** Build a WGS84 ring from local east/north offsets in metres. */ +function ringFromMetres(offsets, origin) { + const m = metresPerDegree(origin.lat); + return offsets.map(([x, y]) => [origin.lon + x / m.lon, origin.lat + y / m.lat]); +} + +/** Rotate local offsets counter-clockwise in the east/north plane. */ +function rotate(offsets, degrees) { + const r = (degrees * Math.PI) / 180; + const c = Math.cos(r); + const s = Math.sin(r); + return offsets.map(([x, y]) => [x * c - y * s, x * s + y * c]); +} + +// 10 m along the ridge (east-west) by 6 m down the slope. +const RECT_10x6 = [[-5, -3], [5, -3], [5, 3], [-5, 3]]; + +describe("plan area", () => { + it("recovers the drawn size in square metres", () => { + const ring = ringFromMetres(RECT_10x6, STOCKHOLM); + const area = planAreaM2(ring); + assert.ok(Math.abs(area - 60) < 0.6, `area ${area} should be ~60 m²`); + }); + + it("does not care whether the ring repeats its first point", () => { + const ring = ringFromMetres(RECT_10x6, STOCKHOLM); + const closed = [...ring, ring[0]]; + assert.ok(Math.abs(planAreaM2(ring) - planAreaM2(closed)) < 1e-9); + }); + + it("is unsigned, so winding order cannot produce a negative roof", () => { + const ring = ringFromMetres(RECT_10x6, STOCKHOLM); + assert.ok(Math.abs(planAreaM2(ring) - planAreaM2([...ring].reverse())) < 1e-9); + }); + + it("treats a shape with no area as no array", () => { + assert.equal(planAreaM2([]), 0); + assert.equal(planAreaM2([[18, 59], [18.001, 59]]), 0); + assert.equal(arrayFromRing([[18, 59], [18.001, 59]], {}), null); + }); +}); + +describe("orientation", () => { + it("reads the ridge from the longest edge", () => { + const ring = ringFromMetres(RECT_10x6, STOCKHOLM); + // The 10 m edges run east-west: a ridge bearing of 90°. + assert.ok(Math.abs(ridgeAzimuthDeg(ring) - 90) < 0.5); + }); + + it("offers both faces the outline permits, and no others", () => { + const ring = ringFromMetres(RECT_10x6, STOCKHOLM); + const [a, b] = faceAzimuthCandidates(ring).map(Math.round); + assert.deepEqual([a, b].sort((x, y) => x - y), [0, 180]); + }); + + it("defaults to the equatorward face, per hemisphere", () => { + const north = ringFromMetres(RECT_10x6, STOCKHOLM); + assert.ok(Math.abs(preferredAzimuthDeg(north, STOCKHOLM.lat) - 180) < 0.5); + + const south = ringFromMetres(RECT_10x6, { lat: -33.87, lon: 151.21 }); + const picked = preferredAzimuthDeg(south, -33.87); + assert.ok(angularDistanceDeg(picked, 0) < 0.5, `expected ~0°, got ${picked}`); + }); + + it("follows the rectangle round as it turns", () => { + // Turning the shape 30° counter-clockwise swings the ridge from 90° to + // 60°, so the faces move with it: 150° and 330°, and south-ish wins. + const ring = ringFromMetres(rotate(RECT_10x6, 30), STOCKHOLM); + assert.ok(Math.abs(ridgeAzimuthDeg(ring) - 60) < 0.5); + assert.ok(Math.abs(preferredAzimuthDeg(ring, STOCKHOLM.lat) - 150) < 0.5); + }); + + it("keeps the ridge a line rather than an arrow", () => { + // Drawing the same rectangle the other way round is the same roof. + const ring = ringFromMetres(RECT_10x6, STOCKHOLM); + const reversed = ringFromMetres([...RECT_10x6].reverse(), STOCKHOLM); + assert.ok(Math.abs(ridgeAzimuthDeg(ring) - ridgeAzimuthDeg(reversed)) < 0.5); + }); + + it("flips to the opposite face", () => { + assert.equal(flipAzimuthDeg(180), 0); + assert.equal(flipAzimuthDeg(0), 180); + assert.equal(flipAzimuthDeg(270), 90); + assert.equal(flipAzimuthDeg(350), 170); + }); + + it("measures the shorter way round the compass", () => { + assert.equal(angularDistanceDeg(350, 10), 20); + assert.equal(angularDistanceDeg(10, 350), 20); + assert.equal(angularDistanceDeg(0, 180), 180); + }); +}); + +describe("tilt turns an outline into panel area", () => { + it("leaves a flat roof alone", () => { + assert.ok(Math.abs(slopeAreaM2(60, 0) - 60) < 1e-9); + }); + + it("recovers the area hidden by the slope", () => { + // cos 60° = 0.5, so a 60 m² shadow is cast by 120 m² of roof. + assert.ok(Math.abs(slopeAreaM2(60, 60) - 120) < 1e-9); + // A 35° roof carries ~22 % more panel than its outline suggests. + assert.ok(Math.abs(slopeAreaM2(60, 35) - 73.24) < 0.05); + }); + + it("stays finite at a wall, where there is no outline to trace", () => { + assert.ok(Number.isFinite(slopeAreaM2(60, 90))); + }); + + it("means a steeper roof is a bigger array for the same drawing", () => { + const ring = ringFromMetres(RECT_10x6, STOCKHOLM); + const flat = arrayFromRing(ring, { latitude: STOCKHOLM.lat, tiltDeg: 0 }); + const steep = arrayFromRing(ring, { latitude: STOCKHOLM.lat, tiltDeg: 45 }); + assert.ok(steep.array.rated_w > flat.array.rated_w, + `${steep.array.rated_w} should exceed ${flat.array.rated_w}`); + }); +}); + +describe("capacity", () => { + it("uses the same basis as the roof model", () => { + // 60 m² × 0.70 packing × 200 W/m² = 8400 W. + assert.ok(Math.abs(ratedWFromSlopeArea(60) - 8400) < 1e-9); + assert.equal(DEFAULT_PACKING_FACTOR, 0.7); + assert.equal(DEFAULT_MODULE_W_PER_M2, 200); + }); + + it("honours an overridden packing factor", () => { + assert.ok(Math.abs(ratedWFromSlopeArea(60, 0.5, 200) - 6000) < 1e-9); + }); +}); + +describe("the entry written back to config", () => { + it("carries only the four fields weather.pv_arrays defines", () => { + const ring = ringFromMetres(RECT_10x6, STOCKHOLM); + const out = arrayFromRing(ring, { latitude: STOCKHOLM.lat }); + assert.deepEqual( + Object.keys(out.array).sort(), + ["azimuth_deg", "name", "rated_w", "tilt_deg"], + ); + }); + + it("describes a south-facing 35° roof from the drawing alone", () => { + const ring = ringFromMetres(RECT_10x6, STOCKHOLM); + const out = arrayFromRing(ring, { latitude: STOCKHOLM.lat }); + assert.equal(out.array.azimuth_deg, 180); + assert.equal(out.array.tilt_deg, 35); + assert.equal(out.array.name, "Roof south"); + assert.ok(Math.abs(out.planAreaM2 - 60) < 0.6); + assert.ok(Math.abs(out.slopeAreaM2 - 73.2) < 0.6); + assert.ok(Math.abs(out.array.rated_w - 10250) < 100); + assert.deepEqual(out.azimuthCandidates.slice().sort((a, b) => a - b), [0, 180]); + }); + + it("lets an explicit azimuth override the guess", () => { + const ring = ringFromMetres(RECT_10x6, STOCKHOLM); + const out = arrayFromRing(ring, { latitude: STOCKHOLM.lat, azimuthDeg: 0 }); + assert.equal(out.array.azimuth_deg, 0); + assert.equal(out.array.name, "Roof north"); + }); + + it("names a flat roof for what it is", () => { + assert.equal(compassName(180, 0), "Roof flat"); + assert.equal(compassName(90, 35), "Roof east"); + assert.equal(compassName(225, 35), "Roof south-west"); + }); +}); diff --git a/web/index.html b/web/index.html index f1577a0b3..fc50c95ad 100644 --- a/web/index.html +++ b/web/index.html @@ -995,7 +995,7 @@

Price bars (top of the chart)

- + diff --git a/web/settings/tabs/weather.js b/web/settings/tabs/weather.js index 8c409e24a..db66604ec 100644 --- a/web/settings/tabs/weather.js +++ b/web/settings/tabs/weather.js @@ -45,6 +45,45 @@ return maplibreLoading; } + // Terra Draw (MIT) supplies the drawing. Both bundles are UMD, so unlike an + // ES module they can carry a real integrity hash: one self-contained file + // each, with no sub-imports for SRI to silently miss. + var TERRA_DRAW = { + src: "https://unpkg.com/terra-draw@1.32.2/dist/terra-draw.umd.js", + integrity: "sha384-TYV8O/5VLLcJCLall6+2ipTEOgCQ0Fy4YkAoL3AU15s0qmNqs20zeWGiIJyMiztH", + }; + var TERRA_DRAW_MAPLIBRE = { + src: "https://unpkg.com/terra-draw-maplibre-gl-adapter@1.4.1/dist/terra-draw-maplibre-gl-adapter.umd.js", + integrity: "sha384-A56++Zl2ljSDy1B+lcDdkRT3BVbzDkMolLzWffRCxiU5rGUWcUYDMvg/Dxr0JR+N", + }; + + function loadScript(spec) { + return new Promise(function (resolve, reject) { + var script = document.createElement("script"); + script.src = spec.src; + script.integrity = spec.integrity; + script.crossOrigin = "anonymous"; + script.async = true; + script.onload = function () { resolve(); }; + script.onerror = function () { reject(new Error("could not load " + spec.src)); }; + document.head.appendChild(script); + }); + } + + var terraDrawLoading = null; + function loadTerraDraw() { + if (window.terraDrawMaplibreGlAdapter) return Promise.resolve(); + if (terraDrawLoading) return terraDrawLoading; + // The adapter's UMD only needs the terra-draw global — the map instance + // is handed to it at construction — but the map library must already be + // up, which loadMapLibre guarantees before any drawing can start. + terraDrawLoading = loadMapLibre() + .then(function () { return loadScript(TERRA_DRAW); }) + .then(function () { return loadScript(TERRA_DRAW_MAPLIBRE); }) + .catch(function (e) { terraDrawLoading = null; throw e; }); + return terraDrawLoading; + } + var pvArraysModulePromise = null; var pvArraysModuleFailed = false; function ensurePvArraysComponent() { @@ -141,6 +180,103 @@ }; } + // --- drawing arrays on the map ------------------------------------------- + // A drawn rectangle answers two of the three questions an array asks: how + // big it is, and which way it is turned. Tilt is the one thing an overhead + // outline cannot show, so it is typed once before drawing and used to turn + // the outline into real panel area. + var drawInstance = null; + var drawGeometry = null; + var drawHandled = {}; + var lastDrawnArray = null; + + function drawStatus(html) { + var el = document.getElementById("pv-draw-status"); + if (el) el.innerHTML = html; + } + + function drawnTiltDeg() { + var el = document.getElementById("pv-draw-tilt"); + var v = el ? parseFloat(el.value) : NaN; + return isNaN(v) ? drawGeometry.DEFAULT_TILT_DEG : Math.min(Math.max(v, 0), 90); + } + + function onRectangleFinished(ctx, id) { + if (drawHandled[id]) return; + drawHandled[id] = true; + var snapshot = drawInstance.getSnapshot() || []; + var feature = null; + for (var i = 0; i < snapshot.length; i++) { + if (snapshot[i] && snapshot[i].id === id) feature = snapshot[i]; + } + if (!feature || !feature.geometry || feature.geometry.type !== "Polygon") return; + var weather = ctx.config.weather; + var derived = drawGeometry.arrayFromRing(feature.geometry.coordinates[0], { + latitude: weather.latitude, + tiltDeg: drawnTiltDeg(), + }); + if (!derived) { + drawStatus("That outline enclosed no area — draw it again."); + return; + } + weather.pv_arrays.push(derived.array); + lastDrawnArray = derived.array; + renderPVArrays(ctx); + drawStatus( + "" + ctx.escHtml(derived.array.name) + " added: " + + derived.planAreaM2 + " m² outline is " + derived.slopeAreaM2 + " m² of roof at " + + derived.array.tilt_deg + "°, about " + derived.array.rated_w + " W. " + + "Facing " + derived.array.azimuth_deg + "° — the outline fits " + + derived.azimuthCandidates.join("° and ") + "° equally well. " + + ' ' + + "Draw another, or edit the numbers below." + ); + } + + function startArrayDrawing(ctx) { + var map = window._weatherMap; + if (!map) { + drawStatus("The map has to finish loading before you can draw on it."); + return; + } + drawStatus("Loading the drawing tools…"); + Promise.all([loadTerraDraw(), import("/components/pv-array-geometry.js")]) + .then(function (loaded) { + drawGeometry = loaded[1]; + if (!drawInstance) { + drawInstance = new window.terraDraw.TerraDraw({ + adapter: new window.terraDrawMaplibreGlAdapter.TerraDrawMapLibreGLAdapter({ + map: map, + }), + modes: [new window.terraDraw.TerraDrawAngledRectangleMode()], + }); + drawInstance.start(); + drawInstance.on("finish", function (id) { onRectangleFinished(ctx, id); }); + } + drawInstance.setMode("angled-rectangle"); + var container = document.getElementById("weather-map"); + if (container && container.scrollIntoView) { + container.scrollIntoView({ block: "nearest" }); + } + drawStatus( + "Click one corner of your panels, click along the ridge to set the " + + "angle, then click again to finish the rectangle." + ); + }) + .catch(function (e) { + drawStatus("Drawing is unavailable (" + ctx.escHtml(e.message) + + "). The numbers below still work."); + }); + } + + function stopArrayDrawing() { + if (!drawInstance) return; + // "static" keeps what has been drawn on the map while stopping new + // drawing; stop() would take the outlines with it. + try { drawInstance.setMode("static"); } catch (e) { /* stays in draw mode */ } + drawStatus("Drawing finished. The arrays below are yours to edit."); + } + // Raster style assembled inline from the same OpenStreetMap tiles the old // Leaflet picker used: swapping the renderer changes neither the tile source // nor the attribution. Raster-only also means no glyph or sprite server is @@ -701,8 +837,19 @@ 'Optional. Open-Meteo uses these per-plane values to project shortwave radiation onto each array. ' + 'Forecast.Solar uses them for its site-calibrated forecast. Leave empty unless you are debugging a multi-plane site.') + '
' + '
' + - '' + + '
' + + '' + + '' + + '' + + '
' + + '' + + '
' + + '
' + + '

' + '

' + + 'Drawing gives the size and the direction; tilt is the one thing an overhead ' + + 'outline cannot show, so set it above before you draw — a 35° roof holds about ' + + '22 % more panel than its outline suggests. ' + 'Tilt: 0° = flat roof, 35° = typical pitched roof, 90° = wall. Azimuth: 0 = N, 90 = E, 180 = S, 270 = W. ' + 'Rated (W) is watts, same unit as PV rated.' + '

' + @@ -750,6 +897,21 @@ if (findBtn) findBtn.addEventListener("click", function () { findBuildings(ctx); }); var deriveBtn = document.getElementById("roof-derive"); if (deriveBtn) deriveBtn.addEventListener("click", function () { deriveRoof(ctx); }); + var drawBtn = document.getElementById("pv-array-draw"); + if (drawBtn) drawBtn.addEventListener("click", function () { startArrayDrawing(ctx); }); + var doneBtn = document.getElementById("pv-array-draw-done"); + if (doneBtn) doneBtn.addEventListener("click", stopArrayDrawing); + var status = document.getElementById("pv-draw-status"); + if (status) status.addEventListener("click", function (e) { + if (!e.target || e.target.id !== "pv-draw-flip" || !lastDrawnArray) return; + // Flip the object, not an index: removing another row above it would + // otherwise silently turn a different roof around. + lastDrawnArray.azimuth_deg = drawGeometry.flipAzimuthDeg(lastDrawnArray.azimuth_deg); + lastDrawnArray.name = drawGeometry.compassName( + lastDrawnArray.azimuth_deg, lastDrawnArray.tilt_deg); + renderPVArrays(ctx); + drawStatus("Now facing " + lastDrawnArray.azimuth_deg + "°."); + }); }, }; From ae8791fa3dfff57b8da9f6b493a30c02bb72760c Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Sat, 29 Aug 2026 18:27:57 +0200 Subject: [PATCH 23/26] feat(web): vendor Terra Draw; the PV-array editor loads nothing from a CDN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same policy as the vendored MapLibre it draws on: both UMD bundles move into web/vendor/terra-draw (verified byte-identical to the SRI-pinned CDN copies), the loader goes same-origin, and terra-draw-vendor.test.mjs pins the contract. SRI attributes go away with the CDN — we ship the files. Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com> --- web/settings/tabs/weather.js | 31 +++++++------------ web/terra-draw-vendor.test.mjs | 26 ++++++++++++++++ web/vendor/terra-draw/LICENSE | 8 +++++ web/vendor/terra-draw/README.md | 19 ++++++++++++ .../terra-draw-maplibre-gl-adapter.umd.js | 2 ++ web/vendor/terra-draw/terra-draw.umd.js | 2 ++ 6 files changed, 69 insertions(+), 19 deletions(-) create mode 100644 web/terra-draw-vendor.test.mjs create mode 100644 web/vendor/terra-draw/LICENSE create mode 100644 web/vendor/terra-draw/README.md create mode 100644 web/vendor/terra-draw/terra-draw-maplibre-gl-adapter.umd.js create mode 100644 web/vendor/terra-draw/terra-draw.umd.js diff --git a/web/settings/tabs/weather.js b/web/settings/tabs/weather.js index db66604ec..96be70da6 100644 --- a/web/settings/tabs/weather.js +++ b/web/settings/tabs/weather.js @@ -45,27 +45,20 @@ return maplibreLoading; } - // Terra Draw (MIT) supplies the drawing. Both bundles are UMD, so unlike an - // ES module they can carry a real integrity hash: one self-contained file - // each, with no sub-imports for SRI to silently miss. - var TERRA_DRAW = { - src: "https://unpkg.com/terra-draw@1.32.2/dist/terra-draw.umd.js", - integrity: "sha384-TYV8O/5VLLcJCLall6+2ipTEOgCQ0Fy4YkAoL3AU15s0qmNqs20zeWGiIJyMiztH", - }; - var TERRA_DRAW_MAPLIBRE = { - src: "https://unpkg.com/terra-draw-maplibre-gl-adapter@1.4.1/dist/terra-draw-maplibre-gl-adapter.umd.js", - integrity: "sha384-A56++Zl2ljSDy1B+lcDdkRT3BVbzDkMolLzWffRCxiU5rGUWcUYDMvg/Dxr0JR+N", - }; - - function loadScript(spec) { + // Terra Draw (MIT) supplies the drawing, vendored under /vendor/terra-draw + // for the same reason MapLibre above ships on the box: no third-party CDN + // JS, and the drawing tools load without internet. Both bundles are UMD — + // one self-contained file each. + var TERRA_DRAW_SRC = "/vendor/terra-draw/terra-draw.umd.js"; + var TERRA_DRAW_MAPLIBRE_SRC = "/vendor/terra-draw/terra-draw-maplibre-gl-adapter.umd.js"; + + function loadScript(src) { return new Promise(function (resolve, reject) { var script = document.createElement("script"); - script.src = spec.src; - script.integrity = spec.integrity; - script.crossOrigin = "anonymous"; + script.src = src; script.async = true; script.onload = function () { resolve(); }; - script.onerror = function () { reject(new Error("could not load " + spec.src)); }; + script.onerror = function () { reject(new Error("could not load " + src)); }; document.head.appendChild(script); }); } @@ -78,8 +71,8 @@ // is handed to it at construction — but the map library must already be // up, which loadMapLibre guarantees before any drawing can start. terraDrawLoading = loadMapLibre() - .then(function () { return loadScript(TERRA_DRAW); }) - .then(function () { return loadScript(TERRA_DRAW_MAPLIBRE); }) + .then(function () { return loadScript(TERRA_DRAW_SRC); }) + .then(function () { return loadScript(TERRA_DRAW_MAPLIBRE_SRC); }) .catch(function (e) { terraDrawLoading = null; throw e; }); return terraDrawLoading; } diff --git a/web/terra-draw-vendor.test.mjs b/web/terra-draw-vendor.test.mjs new file mode 100644 index 000000000..a3e172a73 --- /dev/null +++ b/web/terra-draw-vendor.test.mjs @@ -0,0 +1,26 @@ +import assert from 'node:assert/strict'; +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import test from 'node:test'; +import { fileURLToPath } from 'node:url'; + +const webRoot = dirname(fileURLToPath(import.meta.url)); +const weather = readFileSync(join(webRoot, 'settings', 'tabs', 'weather.js'), 'utf8'); +const vendor = join(webRoot, 'vendor', 'terra-draw'); + +test('PV-array drawing loads Terra Draw from the vendored copy, not a CDN', () => { + assert.doesNotMatch(weather, /unpkg\.com/); + assert.match(weather, /\/vendor\/terra-draw\/terra-draw\.umd\.js/); + assert.match(weather, /\/vendor\/terra-draw\/terra-draw-maplibre-gl-adapter\.umd\.js/); +}); + +test('vendored Terra Draw files are present', () => { + for (const rel of [ + 'terra-draw.umd.js', + 'terra-draw-maplibre-gl-adapter.umd.js', + 'LICENSE', + 'README.md', + ]) { + assert.ok(existsSync(join(vendor, rel)), rel + ' must be vendored'); + } +}); diff --git a/web/vendor/terra-draw/LICENSE b/web/vendor/terra-draw/LICENSE new file mode 100644 index 000000000..407a4b959 --- /dev/null +++ b/web/vendor/terra-draw/LICENSE @@ -0,0 +1,8 @@ +Copyright 2022 James Milner + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + diff --git a/web/vendor/terra-draw/README.md b/web/vendor/terra-draw/README.md new file mode 100644 index 000000000..1817aa3ae --- /dev/null +++ b/web/vendor/terra-draw/README.md @@ -0,0 +1,19 @@ +# Terra Draw, vendored + +Terra Draw 1.32.2 and terra-draw-maplibre-gl-adapter 1.4.1, MIT. See +`LICENSE` (the npm packages ship no license file; the text comes from the +project repository, which covers both packages). + +Vendored rather than pulled from a CDN, for the same reason as +`/vendor/maplibre/` next door: the box UI must not execute third-party JS +from a CDN, and the PV-array drawing tools have to load even when the +gateway cannot reach the internet. + +| File | Why | +|---|---| +| `terra-draw.umd.js` | the drawing engine (self-contained UMD build) | +| `terra-draw-maplibre-gl-adapter.umd.js` | binds Terra Draw to a MapLibre map | + +To upgrade: bump the versions in this file, replace the two files from the +packages' `dist/`, and update `web/settings/tabs/weather.js` if the layout +changed. `web/terra-draw-vendor.test.mjs` pins the contract. diff --git a/web/vendor/terra-draw/terra-draw-maplibre-gl-adapter.umd.js b/web/vendor/terra-draw/terra-draw-maplibre-gl-adapter.umd.js new file mode 100644 index 000000000..29409f27b --- /dev/null +++ b/web/vendor/terra-draw/terra-draw-maplibre-gl-adapter.umd.js @@ -0,0 +1,2 @@ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("terra-draw")):"function"==typeof define&&define.amd?define(["exports","terra-draw"],t):t((e||self).terraDrawMaplibreGlAdapter={},e.terraDraw)}(this,function(e,t){function i(){return i=Object.assign?Object.assign.bind():function(e){for(var t=1;tl:o!==d?o>d:n[2]>=r[2]},o._addGeoJSONSource=function(e,t){this._map.addSource(e,{type:"geojson",data:{type:"FeatureCollection",features:t},tolerance:0})},o._addFillLayer=function(e){return this._map.addLayer({id:e,source:e,type:"fill",layout:{"fill-sort-key":["get","zIndex"]},paint:{"fill-color":["get","polygonFillColor"],"fill-opacity":["get","polygonFillOpacity"]}})},o._addFillOutlineLayer=function(e){return this._map.addLayer({id:e+"-outline",source:e,type:"line",layout:{"line-sort-key":["get","zIndex"]},paint:{"line-width":["get","polygonOutlineWidth"],"line-color":["get","polygonOutlineColor"],"line-opacity":["get","polygonOutlineOpacity"]}})},o._addLineLayer=function(e){var t={};return this.isMapLibreAtLeast("5.8.0")&&(t["line-dasharray"]=["coalesce",["get","lineStringDash"],["literal",[1,0]]]),this._map.addLayer({id:e,source:e,type:"line",layout:{"line-sort-key":["get","zIndex"]},paint:i({},t,{"line-width":["get","lineStringWidth"],"line-color":["get","lineStringColor"],"line-opacity":["get","lineStringOpacity"]})})},o._addPointLayer=function(e){return this._map.addLayer({id:e,source:e,type:"circle",layout:{"circle-sort-key":["get","zIndex"]},paint:{"circle-stroke-color":["get","pointOutlineColor"],"circle-stroke-width":["get","pointOutlineWidth"],"circle-stroke-opacity":["get","pointOutlineOpacity"],"circle-radius":["get","pointWidth"],"circle-color":["get","pointColor"],"circle-opacity":["get","pointOpacity"]}})},o._addMarkerLayer=function(e){return this._map.addLayer({id:e+"-marker",source:e,type:"symbol",filter:["has","markerId"],layout:{"icon-image":["image",["get","markerId"]],"icon-anchor":"bottom","icon-allow-overlap":!0}})},o._addLayer=function(e,t){"Point"===t&&(this._addPointLayer(e),this._addMarkerLayer(e)),"LineString"===t&&this._addLineLayer(e),"Polygon"===t&&(this._addFillLayer(e),this._addFillOutlineLayer(e))},o._addGeoJSONLayer=function(e,t){var i=this._prefixId+"-"+e.toLowerCase();return this._addGeoJSONSource(i,t),this._addLayer(i,e),i},o._setGeoJSONLayerData=function(e,t){var i=this._prefixId+"-"+e.toLowerCase();return this._map.getSource(i).setData({type:"FeatureCollection",features:t}),i},o.updateChangedIds=function(e){var t=this;[].concat(e.updated,e.created).forEach(function(e){"Point"===e.geometry.type?t.changedIds.points=!0:"LineString"===e.geometry.type?t.changedIds.linestrings=!0:"Polygon"===e.geometry.type&&(t.changedIds.polygons=!0)}),e.deletedIds.length>0&&(this.changedIds.deletion=!0),0===e.created.length&&0===e.updated.length&&0===e.deletedIds.length&&(this.changedIds.styling=!0)},o.getLngLatFromEvent=function(e){var t=this._container.getBoundingClientRect();return this.unproject(e.clientX-t.left,e.clientY-t.top)},o.getMapEventElement=function(){return this._map.getCanvas()},o.setDraggability=function(e){e?(this._initialDragRotate&&this._map.dragRotate.enable(),this._initialDragPan&&this._map.dragPan.enable()):(this._initialDragRotate&&this._map.dragRotate.disable(),this._initialDragPan&&this._map.dragPan.disable())},o.project=function(e,t){var i=this._map.project({lng:e,lat:t});return{x:i.x,y:i.y}},o.unproject=function(e,t){var i=this._map.unproject({x:e,y:t});return{lng:i.lng,lat:i.lat}},o.setCursor=function(e){var t=this._map.getCanvas();"unset"===e?t.style.removeProperty("cursor"):t.style.cursor=e},o.setDoubleClickToZoom=function(e){e?this._map.doubleClickZoom.enable():this._map.doubleClickZoom.disable()},o.render=function(e,t){var i=this;this.updateChangedIds(e),this._nextRender&&cancelAnimationFrame(this._nextRender),this._nextRender=requestAnimationFrame(function(){if(i._currentModeCallbacks){for(var n=[].concat(e.created,e.updated,e.unchanged),r=[],a=[],o=[],l=function(){var e=n[d],l=e.properties,s=t[l.mode](e);if(l.zIndex=s.zIndex,l.zIndex=s.zIndex,"Point"===e.geometry.type){l.pointColor=s.pointColor,l.pointOutlineColor=s.pointOutlineColor,l.pointOutlineWidth=s.pointOutlineWidth;var p=s.pointOutlineOpacity;l.pointOutlineOpacity=void 0===p?1:p,l.pointWidth=s.pointWidth;var c=s.pointOpacity;if(l.pointOpacity=void 0===c?1:c,s.markerUrl&&s.markerWidth&&s.markerHeight){var g="marker-"+i.hashCode(s.markerUrl);i._map.hasImage(g)||i.resizeImage(s.markerUrl,s.markerWidth,s.markerHeight,function(e){i._map.loadImage(e).then(function(e){i._map.hasImage(g)||i._map.addImage(g,e.data)})}),l.markerId=g,l.pointWidth=0}r.push(e)}else if("LineString"===e.geometry.type){l.lineStringDash=i.toGlDashArrayFromPixels(s.lineStringDash,s.lineStringWidth),l.lineStringColor=s.lineStringColor,l.lineStringWidth=s.lineStringWidth;var h=s.lineStringOpacity;l.lineStringOpacity=void 0===h?1:h,a.push(e)}else if("Polygon"===e.geometry.type){var u=s.polygonOutlineOpacity;l.polygonFillColor=s.polygonFillColor,l.polygonFillOpacity=s.polygonFillOpacity,l.polygonOutlineOpacity=void 0===u?1:u,l.polygonOutlineColor=s.polygonOutlineColor,l.polygonOutlineWidth=s.polygonOutlineWidth,o.push(e)}},d=0;dt.length)&&(e=t.length);for(var i=0,n=Array(e);i=t.length?{done:!0}:{done:!1,value:t[o++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function r(){return r=Object.assign?Object.assign.bind():function(t){for(var e=1;e0;function a(t){return t<0||t>1}function d(t,n,o,r){var s,d=e[t][n],u=e[t][n+1],h=e[o][r],l=e[o][r+1],c=function(t,e,i,n){if(z(t,i)||z(t,n)||z(e,i)||z(n,i))return null;var o=t[0],r=t[1],s=e[0],a=e[1],d=i[0],u=i[1],h=n[0],l=n[1],c=(o-s)*(u-l)-(r-a)*(d-h);return 0===c?null:[((o*a-r*s)*(d-h)-(o-s)*(d*l-u*h))/c,((o*a-r*s)*(u-l)-(r-a)*(d*l-u*h))/c]}(d,u,h,l);null!==c&&(s=l[0]!==h[0]?(c[0]-h[0])/(l[0]-h[0]):(c[1]-h[1])/(l[1]-h[1]),a(u[0]!==d[0]?(c[0]-d[0])/(u[0]-d[0]):(c[1]-d[1])/(u[1]-d[1]))||a(s)||(c.toString(),i.push(c)))}}function z(t,e){return t[0]===e[0]&&t[1]===e[1]}function H(t,e){return G(t[0])<=e&&G(t[1])<=e}function V(t){return 2===t.length&&"number"==typeof t[0]&&"number"==typeof t[1]&&Infinity!==t[0]&&Infinity!==t[1]&&(i=t[0])>=-180&&i<=180&&(e=t[1])>=-90&&e<=90;var e,i}function G(t){for(var e=1,i=0;Math.round(t*e)/e!==t;)e*=10,i++;return i}var j="Feature has holes",K="Feature has less than 4 coordinates",Y="Feature has invalid coordinates",X="Feature coordinates are not closed";function q(t,e){if("Polygon"!==t.geometry.type)return{valid:!1,reason:"Feature is not a Polygon"};if(1!==t.geometry.coordinates.length)return{valid:!1,reason:j};if(t.geometry.coordinates[0].length<4)return{valid:!1,reason:K};for(var i=0;i=a)throw new RangeError("Index "+t+" (normalized to "+e+") is out of bounds");return e},u=new Array(a).fill(void 0),h=Array.from({length:a},function(){return[]}),l=Array.from({length:a},function(){return[]}),c=[],p=o(e);!(i=p()).done;){var g=i.value;if(g.type!==Q&&g.type!==tt){var f=d(g.index);u[f]=r({},g,{index:f})}else{var y=g.index,v=y<0?a+y:y;if(v<0||v>a)throw new RangeError("Index "+g.index+" (normalized to "+v+") is out of bounds");if(g.type===Q){if(v>=a)throw new RangeError("INSERT_BEFORE index "+g.index+" (normalized to "+v+") is out of bounds for length "+a);h[v].push(g)}else v===a?c.push(g):l[v].push(g)}}for(var m=[],C=0;C=i.length)throw new RangeError("Index "+e+" (normalized to "+n+") is out of bounds");return i[n]},i.getProperties=function(t){return this.store.getPropertiesCopy(t)},i.hasFeature=function(t){return this.store.has(t)},i.getAllFeatureIdsWhere=function(t){return this.store.copyAllWhere(t).map(function(t){return t.id})},e}(J),lt={cancel:"Escape",finish:"Enter"},ct={start:"crosshair",close:"pointer"},pt=/*#__PURE__*/function(t){function e(e){var i;return(i=t.call(this,e,!0)||this).mode="freehand",i.canClose=!1,i.currentId=void 0,i.closingPointId=void 0,i.minDistance=20,i.keyEvents=lt,i.cursors=ct,i.preventPointsNearClose=!0,i.autoClose=!1,i.autoCloseTimeout=500,i.hasLeftStartingPoint=!1,i.preventNewFeature=!1,i.drawInteraction="click-move",i.drawType=void 0,i.smoothing=0,i.mutateFeature=void 0,i.readFeature=void 0,i.updateOptions(e),i}s(e,t);var i=e.prototype;return i.updateOptions=function(e){t.prototype.updateOptions.call(this,e),null!=e&&e.minDistance&&(this.minDistance=e.minDistance),void 0!==(null==e?void 0:e.smoothing)&&(this.smoothing=Math.min(Math.max(e.smoothing,0),.999)),void 0!==(null==e?void 0:e.preventPointsNearClose)&&(this.preventPointsNearClose=e.preventPointsNearClose),void 0!==(null==e?void 0:e.autoClose)&&(this.autoClose=e.autoClose),null!=e&&e.autoCloseTimeout&&(this.autoCloseTimeout=e.autoCloseTimeout),null===(null==e?void 0:e.keyEvents)?this.keyEvents={cancel:null,finish:null}:null!=e&&e.keyEvents&&(this.keyEvents=r({},this.keyEvents,e.keyEvents)),null!=e&&e.cursors&&(this.cursors=r({},this.cursors,e.cursors)),null!=e&&e.drawInteraction&&(this.drawInteraction=e.drawInteraction)},i.moveDrawAllowed=function(){return"click-move"===this.drawInteraction||"click-move-or-drag"===this.drawInteraction},i.dragDrawAllowed=function(){return"click-drag"===this.drawInteraction||"click-move-or-drag"===this.drawInteraction},i.beginDrawing=function(t,e){var i;void 0===e&&(e="click");var n=this.mutateFeature.createPolygon({coordinates:[[t.lng,t.lat],[t.lng,t.lat],[t.lng,t.lat],[t.lng,t.lat]],properties:(i={mode:this.mode},i[y.CURRENTLY_DRAWING]=!0,i)});this.currentId=n.id,this.drawType=e,this.closingPointId=this.mutateFeature.createGuidancePoint({coordinate:[t.lng,t.lat],type:y.CLOSING_POINT}),this.canClose=!0,"drawing"!==this.state&&this.setDrawing()},i.addCoordinate=function(t){var e=this;if(void 0!==this.currentId&&!1!==this.canClose){var i=this.readFeature.getCoordinate(this.currentId,-2),n=i[0],o=i[1],r=this.project(n,o),s=dt({x:r.x,y:r.y},{x:t.containerX,y:t.containerY}),a=this.readFeature.getCoordinate(this.currentId,0),d=this.project(a[0],a[1]);if(dt({x:d.x,y:d.y},{x:t.containerX,y:t.containerY})180?o-=360:o<-180&&(o+=360),o}function St(t){return(t+360)%360}function Ft(t,e,i){for(var n,o,r,s=[],a=t.length,d=0,u=0;u=d&&u===t.length-1);u++){if(d>e&&0===s.length){if(!(n=e-d))return s.push(t[u]),s;o=Pt(t[u],t[u-1])-180,r=mt(t[u],n,o),s.push(r)}if(d>=i)return(n=i-d)?(o=Pt(t[u],t[u-1])-180,r=mt(t[u],n,o),s.push(r),s):(s.push(t[u]),s);if(d>=e&&s.push(t[u]),u===t.length-1)return s;d+=w(t[u],t[u+1])}if(dj||Dt(M,x)>j?w(bt(x),bt(O))<=w(bt(x),bt(M))?[bt(O),!0,!1]:[bt(M),!1,!0]:[bt(x),!1,!1])[0])&&(c=w(t,d))0&&Array.isArray(t[0])&&Array.isArray(t[0][0])}var Rt=function(t){return Nt(t)?t[0].slice(0,-1):t},Lt=function(t){return Nt(t)?t[0]:t},Wt=/*#__PURE__*/function(t){function e(e,i,n,o){var r;return(r=t.call(this,e)||this).config=void 0,r.pixelDistance=void 0,r.mutateFeatureBehavior=void 0,r.readFeatureBehavior=void 0,r._startEndPoints=[],r.config=e,r.pixelDistance=i,r.mutateFeatureBehavior=n,r.readFeatureBehavior=o,r}s(e,t);var i=e.prototype;return i.create=function(t){if(this.ids.length)throw new Error("Opening and closing points already created");var e=Nt(t),i=Lt(t);if(e){if(i.length<=3)throw new Error("Requires at least 4 coordinates");this._startEndPoints=this.mutateFeatureBehavior.createGuidancePoints({coordinates:[i[0],i[i.length-2]],type:y.CLOSING_POINT})}else this._startEndPoints=[this.mutateFeatureBehavior.createGuidancePoint({coordinate:i[i.length-2],type:y.CLOSING_POINT})]},i.delete=function(){this.ids.length&&(this.mutateFeatureBehavior.deleteFeaturesIfPresent(this.ids),this._startEndPoints=[])},i.updateOne=function(t,e){this.mutateFeatureBehavior.updateGuidancePoints([{featureId:this.ids[t],coordinate:e}])},i.update=function(t){var e=Lt(t);1!==this.ids.length?2===this.ids.length&&this.mutateFeatureBehavior.updateGuidancePoints([{featureId:this.ids[0],coordinate:e[0]},{featureId:this.ids[1],coordinate:e[e.length-3]}]):this.mutateFeatureBehavior.updateGuidancePoints([{featureId:this.ids[0],coordinate:e[e.length-2]}])},i.isLineStringClosingPoint=function(t){if(1!==this.ids.length)return{isClosing:!1};var e=this.readFeatureBehavior.getGeometry(this.ids[0]);return{isClosing:this.pixelDistance.measure(t,e.coordinates)this.maxStackSize;)t.shift()},e.pushUndoEntry=function(t){0!==this.maxStackSize&&(this.undoHistory.push(t),this.trimHistoryToMax(this.undoHistory))},e.pushRedoEntry=function(t){0!==this.maxStackSize&&(this.redoHistory.push(t),this.trimHistoryToMax(this.redoHistory))},e.cloneRecursively=function(t){var e=this;return Array.isArray(t)?t.map(function(t){return e.cloneRecursively(t)}):null!==t&&"object"==typeof t?r({},t):t},e.cloneCoordinates=function(t){return this.cloneCoordinatesFunction(t)},e.cloneEntry=function(t){return{featureCoordinates:this.cloneCoordinates(t.featureCoordinates),currentCoordinate:t.currentCoordinate}},e.clear=function(){this.undoHistory=[],this.redoHistory=[]},e.undoSize=function(){return this.undoHistory.length},e.redoSize=function(){return this.redoHistory.length},e.recordSnapshot=function(t){this.pushUndoEntry(this.cloneEntry(t)),this.redoHistory=[]},e.beginUndo=function(){var t=this.undoHistory.pop();if(t){var e=this.cloneEntry(t);this.pushRedoEntry(e);var i=this.undoHistory[this.undoHistory.length-1];return{undoneEntry:e,previousEntry:i?this.cloneEntry(i):void 0}}},e.takeRedo=function(){var t=this.redoHistory.pop();if(t)return this.cloneEntry(t)},e.commitRedo=function(t){this.pushUndoEntry(this.cloneEntry(t))},t}(),Bt={cancel:"Escape",finish:"Enter"},zt={start:"crosshair",close:"pointer",dragStart:"grabbing",dragEnd:"crosshair"},Ht=/*#__PURE__*/function(t){function e(e){var i;return(i=t.call(this,e,!0)||this).mode="linestring",i.currentCoordinate=0,i.currentId=void 0,i.keyEvents=Bt,i.snapping=void 0,i.cursors=zt,i.mouseMove=!1,i.insertCoordinates=void 0,i.lastCommittedCoordinates=void 0,i.snappedPointId=void 0,i.lastMouseMoveEvent=void 0,i.showCoordinatePoints=!1,i.finishOnNthCoordinate=void 0,i.editable=!1,i.editedFeatureId=void 0,i.editedFeatureCoordinateIndex=void 0,i.editedSnapType=void 0,i.editedInsertIndex=void 0,i.editedPointId=void 0,i.coordinateSnapping=void 0,i.insertPoint=void 0,i.lineSnapping=void 0,i.featureSnapping=void 0,i.pixelDistance=void 0,i.clickBoundingBox=void 0,i.mutateFeature=void 0,i.readFeature=void 0,i.closingPoints=void 0,i.coordinatePoints=void 0,i.undoRedo=void 0,i.updateOptions(e),i}s(e,t);var i=e.prototype;return i.updateOptions=function(e){var i=this;if(t.prototype.updateOptions.call(this,e),void 0!==(null==e?void 0:e.finishOnNthCoordinate)&&Number.isInteger(e.finishOnNthCoordinate)&&e.finishOnNthCoordinate>1&&(this.finishOnNthCoordinate=Math.floor(e.finishOnNthCoordinate)),null!=e&&e.cursors&&(this.cursors=r({},this.cursors,e.cursors)),null!=e&&e.snapping&&(this.snapping=e.snapping),null===(null==e?void 0:e.keyEvents)?this.keyEvents={cancel:null,finish:null}:null!=e&&e.keyEvents&&(this.keyEvents=r({},this.keyEvents,e.keyEvents)),null!=e&&e.insertCoordinates&&(this.insertCoordinates=e.insertCoordinates),e&&e.editable&&(this.editable=e.editable),void 0!==(null==e?void 0:e.showCoordinatePoints))if(this.showCoordinatePoints=e.showCoordinatePoints,this.coordinatePoints&&!0===e.showCoordinatePoints)this.store.copyAllWhere(function(t){return t.mode===i.mode}).forEach(function(t){i.coordinatePoints.createOrUpdate({featureId:t.id,featureCoordinates:t.geometry.coordinates})});else if(this.coordinatePoints&&!1===this.showCoordinatePoints){var n=this.store.copyAllWhere(function(t){var e;return t.mode===i.mode&&Boolean(null==(e=t[y.COORDINATE_POINT_IDS])?void 0:e.length)});this.coordinatePoints.deletePointsByFeatureIds(n.map(function(t){return t.id}))}},i.shouldFinishOnCommit=function(t){return!!this.finishOnNthCoordinate&&Math.max(0,t.coordinates.length-1)>=this.finishOnNthCoordinate},i.updateSnappedCoordinate=function(t){var e=this.snapCoordinate(t);return e?(this.snappedPointId?this.mutateFeature.updateGuidancePoints([{featureId:this.snappedPointId,coordinate:e}]):this.snappedPointId=this.mutateFeature.createGuidancePoint({coordinate:e,type:y.SNAPPING_POINT}),t.lng=e[0],t.lat=e[1]):this.snappedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.snappedPointId),this.snappedPointId=void 0),e},i.close=function(){var t;if(void 0!==this.currentId){var e=this.mutateFeature.updateLineString({featureId:this.currentId,context:{updateType:u.Finish,action:h},coordinateMutations:[{type:it,index:-1}],propertyMutations:(t={},t[y.CURRENTLY_DRAWING]=void 0,t)});if(e){this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:this.currentId,featureCoordinates:e.geometry.coordinates});var i=this.currentId;this.currentCoordinate=0,this.currentId=void 0,this.lastCommittedCoordinates=void 0,this.undoRedo.clear(),"drawing"===this.state&&this.setStarted(),this.closingPoints.delete(),this.snappedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.snappedPointId),this.snappedPointId=void 0),this.editedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.editedPointId),this.editedPointId=void 0,this.editedFeatureId=void 0,this.editedFeatureCoordinateIndex=void 0,this.editedInsertIndex=void 0,this.editedSnapType=void 0),this.onFinish(i,{mode:this.mode,action:h})}}},i.generateInsertCoordinates=function(t,e){if(!this.insertCoordinates||!this.lastCommittedCoordinates)throw new Error("Not able to insert coordinates");if("amount"!==this.insertCoordinates.strategy)throw new Error("Strategy does not exist");var i=w(t,e)/(this.insertCoordinates.value+1),n=[];return"globe"===this.projection?n=this.insertPoint.generateInsertionGeodesicCoordinates(t,e,i):"web-mercator"===this.projection&&(n=this.insertPoint.generateInsertionCoordinates(t,e,i)),n},i.createLine=function(t){var e,i=this.mutateFeature.createLineString({coordinates:[t,t],properties:(e={mode:this.mode},e[y.CURRENTLY_DRAWING]=!0,e)});this.lastCommittedCoordinates=i.geometry.coordinates,this.currentId=i.id,this.currentCoordinate++,this.pushHistorySnapshot(this.currentId,this.currentCoordinate),this.setDrawing(),this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:this.currentId,featureCoordinates:i.geometry.coordinates})},i.firstUpdateToLine=function(t){if(this.currentId){this.setCursor(this.cursors.close);var e=this.mutateFeature.updateLineString({featureId:this.currentId,context:{updateType:u.Commit},coordinateMutations:[{type:tt,index:-1,coordinate:t}]});e&&(this.closingPoints.create(e.geometry.coordinates),this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:this.currentId,featureCoordinates:e.geometry.coordinates}),this.lastCommittedCoordinates=e.geometry.coordinates,this.currentCoordinate++,this.pushHistorySnapshot(this.currentId,this.currentCoordinate),this.shouldFinishOnCommit(e.geometry)&&this.close())}},i.updateToLine=function(t,e){if(this.currentId)if(this.closingPoints.isLineStringClosingPoint(t).isClosing)this.close();else{this.setCursor(this.cursors.close);var i=this.mutateFeature.updateLineString({featureId:this.currentId,context:{updateType:u.Commit},coordinateMutations:[{type:tt,index:-1,coordinate:e}]});i&&(this.closingPoints.update(i.geometry.coordinates),this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:this.currentId,featureCoordinates:i.geometry.coordinates}),this.lastCommittedCoordinates=i.geometry.coordinates,this.currentCoordinate++,this.pushHistorySnapshot(this.currentId,this.currentCoordinate),this.shouldFinishOnCommit(i.geometry)&&this.close())}},i.undoSize=function(){return this.undoRedo.undoSize()},i.clearHistory=function(){this.undoRedo.clear()},i.pushHistorySnapshot=function(t,e){var i=this.readFeature.getGeometry(t);this.undoRedo.recordSnapshot({featureCoordinates:i.coordinates,currentCoordinate:e})},i.updateSnappedGuidancePointFromLastMouseMove=function(){this.snapping&&this.lastMouseMoveEvent?this.updateSnappedCoordinate(this.lastMouseMoveEvent):this.snappedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.snappedPointId),this.snappedPointId=void 0)},i.syncClosingPoints=function(t){this.currentCoordinate>=2?this.closingPoints.ids.length?this.closingPoints.update(t):this.closingPoints.create(t):this.closingPoints.delete()},i.undo=function(){var t;if("drawing"===this.state&&this.currentId){var e=this.undoRedo.beginUndo();if(e){var i=e.previousEntry;if(!i){var n=this.currentId;return this.currentId=void 0,this.currentCoordinate=0,this.lastCommittedCoordinates=void 0,this.closingPoints.delete(),"drawing"===this.state&&this.setStarted(),this.showCoordinatePoints&&this.coordinatePoints.deletePointsByFeatureIds([n]),this.mutateFeature.deleteFeatureIfPresent(n),void this.updateSnappedGuidancePointFromLastMouseMove()}var o=this.mutateFeature.updateLineString({featureId:this.currentId,coordinateMutations:{type:nt,coordinates:i.featureCoordinates},propertyMutations:(t={},t[y.CURRENTLY_DRAWING]=!0,t),context:{updateType:u.Commit}});o&&(this.currentCoordinate=i.currentCoordinate,this.lastCommittedCoordinates=o.geometry.coordinates,this.syncClosingPoints(o.geometry.coordinates),this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:this.currentId,featureCoordinates:o.geometry.coordinates}),this.updateSnappedGuidancePointFromLastMouseMove())}}},i.redoSize=function(){return this.undoRedo.redoSize()},i.redo=function(){var t=this.undoRedo.takeRedo();if(t){if(this.currentId){var e,i=this.mutateFeature.updateLineString({featureId:this.currentId,coordinateMutations:{type:nt,coordinates:t.featureCoordinates},propertyMutations:(e={},e[y.CURRENTLY_DRAWING]=!0,e),context:{updateType:u.Commit}});if(!i)return;this.currentCoordinate=t.currentCoordinate,this.lastCommittedCoordinates=i.geometry.coordinates,this.syncClosingPoints(i.geometry.coordinates),this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:this.currentId,featureCoordinates:i.geometry.coordinates})}else{var n,o=this.mutateFeature.createLineString({coordinates:t.featureCoordinates,properties:(n={mode:this.mode},n[y.CURRENTLY_DRAWING]=!0,n)}),r=o.id,s=o.geometry;this.currentId=r,this.currentCoordinate=t.currentCoordinate,this.lastCommittedCoordinates=s.coordinates,"started"===this.state&&this.setDrawing(),this.syncClosingPoints(s.coordinates),this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:r,featureCoordinates:s.coordinates})}this.undoRedo.commitRedo(t),this.updateSnappedGuidancePointFromLastMouseMove()}},i.registerBehaviors=function(t){this.insertPoint=new Mt(t),this.clickBoundingBox=new ft(t),this.pixelDistance=new yt(t),this.lineSnapping=new _t(t,this.pixelDistance,this.clickBoundingBox),this.coordinateSnapping=new vt(t,this.pixelDistance,this.clickBoundingBox),this.featureSnapping=new Tt(this.coordinateSnapping,this.lineSnapping),this.readFeature=new ht(t),this.mutateFeature=new ot(t,{validate:this.validate}),this.closingPoints=new Wt(t,this.pixelDistance,this.mutateFeature,this.readFeature),this.coordinatePoints=new Ut(t,this.readFeature,this.mutateFeature),this.undoRedo=new At({maxStackSize:t.undoRedoMaxStackSize})},i.start=function(){this.setStarted(),this.setCursor(this.cursors.start)},i.stop=function(){this.cleanUp(),this.setStopped(),this.setCursor("unset")},i.onMouseMove=function(t){this.mouseMove=!0,this.setCursor(this.cursors.start),this.lastMouseMoveEvent=t;var e=this.updateSnappedCoordinate(t)||[t.lng,t.lat];if(void 0!==this.currentId&&0!==this.currentCoordinate){this.closingPoints.isLineStringClosingPoint(t).isClosing&&this.setCursor(this.cursors.close);var i=[{type:et,index:-1,coordinate:e}];if(this.insertCoordinates){var n=this.getInsertCoordinates(e);n&&(i={type:nt,coordinates:n})}var o=this.mutateFeature.updateLineString({coordinateMutations:i,featureId:this.currentId,context:{updateType:u.Provisional}});o&&this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:this.currentId,featureCoordinates:o.geometry.coordinates})}},i.getInsertCoordinates=function(t){if(this.lastCommittedCoordinates){var e=this.lastCommittedCoordinates[this.lastCommittedCoordinates.length-1];if(!ut(e,t)){var i=this.generateInsertCoordinates(e,t),n=this.lastCommittedCoordinates.slice(0,-1);return[].concat(n,i,[t])}}},i.onRightClick=function(t){var e=this;if(this.editable&&"started"===this.state){var i=this.coordinateSnapping.getSnappable(t,function(t){return e.lineStringFilter(t)}),n=i.featureId,o=i.featureCoordinateIndex;if(n&&void 0!==o){var r=this.readFeature.getGeometry(n);if("LineString"===r.type&&!(r.coordinates.length<=2)){var s=this.mutateFeature.updateLineString({featureId:n,coordinateMutations:[{type:it,index:o}],context:{updateType:u.Finish,action:l}});s&&this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:n,featureCoordinates:s.geometry.coordinates}),this.snappedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.snappedPointId),this.snappedPointId=void 0),this.editedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.editedPointId),this.editedPointId=void 0,this.editedFeatureId=void 0,this.editedFeatureCoordinateIndex=void 0,this.editedInsertIndex=void 0,this.editedSnapType=void 0),this.closingPoints.delete(),this.onFinish(n,{mode:this.mode,action:c})}}}},i.onLeftClick=function(t){this.snappedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.snappedPointId),this.snappedPointId=void 0);var e=this.snapCoordinate(t)||[t.lng,t.lat];0===this.currentCoordinate?this.createLine(e):1===this.currentCoordinate&&this.currentId?this.firstUpdateToLine(e):this.currentId&&this.updateToLine(t,e)},i.onClick=function(t){void 0===this.currentId||this.readFeature.hasFeature(this.currentId)||this.cleanUp(),("right"===t.button&&this.allowPointerEvent(this.pointerEvents.rightClick,t)||"left"===t.button&&this.allowPointerEvent(this.pointerEvents.leftClick,t)||t.isContextMenu&&this.allowPointerEvent(this.pointerEvents.contextMenu,t))&&(this.currentCoordinate>0&&!this.mouseMove&&this.onMouseMove(t),this.mouseMove=!1,"right"===t.button?this.onRightClick(t):"left"===t.button&&this.onLeftClick(t))},i.onKeyDown=function(){},i.onKeyUp=function(t){t.key===this.keyEvents.cancel&&this.cleanUp(),t.key===this.keyEvents.finish&&this.close()},i.onDragStart=function(t,e){var i=this;if(this.allowPointerEvent(this.pointerEvents.onDragStart,t)&&this.editable){var n=void 0;if("started"===this.state){var o=this.lineSnapping.getSnappable(t,function(t){return i.lineStringFilter(t)});o.coordinate&&(this.editedSnapType="line",this.editedFeatureCoordinateIndex=o.featureCoordinateIndex,this.editedFeatureId=o.featureId,n=o.coordinate);var r=this.coordinateSnapping.getSnappable(t,function(t){return i.lineStringFilter(t)});r.coordinate&&(this.editedSnapType="coordinate",this.editedFeatureCoordinateIndex=r.featureCoordinateIndex,this.editedFeatureId=r.featureId,n=r.coordinate)}this.editedFeatureId&&n&&(this.editedPointId||(this.editedPointId=this.mutateFeature.createGuidancePoint({coordinate:n,type:y.EDITED})),this.setCursor(this.cursors.dragStart),e(!1))}},i.onDrag=function(t,e){var i;if(this.allowPointerEvent(this.pointerEvents.onDrag,t)&&void 0!==this.editedFeatureId&&void 0!==this.editedFeatureCoordinateIndex){if("coordinate"===this.editedSnapType||"line"===this.editedSnapType&&void 0!==this.editedInsertIndex){var n=this.mutateFeature.updateLineString({featureId:this.editedFeatureId,context:{updateType:u.Provisional},coordinateMutations:[{type:et,index:this.editedFeatureCoordinateIndex,coordinate:[t.lng,t.lat]}]});if(!n)return;this.showCoordinatePoints&&(void 0!==this.editedInsertIndex?this.coordinatePoints.createOrUpdate({featureId:this.editedFeatureId,featureCoordinates:n.geometry.coordinates}):this.coordinatePoints.updateOneAtIndex(this.editedFeatureId,this.editedFeatureCoordinateIndex,[t.lng,t.lat]))}else if("line"===this.editedSnapType&&void 0===this.editedInsertIndex){this.editedInsertIndex=this.editedFeatureCoordinateIndex+1;var o=this.mutateFeature.updateLineString({featureId:this.editedFeatureId,context:{updateType:u.Provisional}});if(!o)return;this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:this.editedFeatureId,featureCoordinates:o.geometry.coordinates}),this.editedFeatureCoordinateIndex++}this.snapping&&this.snappedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.snappedPointId),this.snappedPointId=void 0),this.editedPointId&&this.mutateFeature.updateGuidancePoints([{featureId:this.editedPointId,coordinate:[t.lng,t.lat]}]),this.mutateFeature.updateLineString({featureId:this.editedFeatureId,context:{updateType:u.Provisional},propertyMutations:(i={},i[y.EDITED]=!0,i)})}},i.onDragEnd=function(t,e){var i;if(this.allowPointerEvent(this.pointerEvents.onDragEnd,t)&&void 0!==this.editedFeatureId&&(this.setCursor(this.cursors.dragEnd),this.mutateFeature.updateLineString({featureId:this.editedFeatureId,propertyMutations:(i={},i[y.EDITED]=!1,i),context:{updateType:u.Finish,action:l}}))){var n=this.editedFeatureId;e(!0),this.snappedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.snappedPointId),this.snappedPointId=void 0),this.editedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.editedPointId),this.editedPointId=void 0,this.editedFeatureId=void 0,this.editedFeatureCoordinateIndex=void 0,this.editedInsertIndex=void 0,this.editedSnapType=void 0),this.closingPoints.delete(),this.onFinish(n,{mode:this.mode,action:l})}},i.cleanUp=function(){var t=this.currentId,e=this.snappedPointId;this.snappedPointId=void 0,this.currentId=void 0,this.currentCoordinate=0,this.lastCommittedCoordinates=void 0,this.undoRedo.clear(),"drawing"===this.state&&this.setStarted(),t&&this.showCoordinatePoints&&this.coordinatePoints.deletePointsByFeatureIds([t]),this.mutateFeature.deleteFeatureIfPresent(t),this.mutateFeature.deleteFeatureIfPresent(e),this.closingPoints.delete()},i.styleFeature=function(t){var e=r({},{polygonFillColor:"#3f97e0",polygonOutlineColor:"#3f97e0",polygonOutlineWidth:4,polygonOutlineOpacity:1,polygonFillOpacity:.3,pointColor:"#3f97e0",pointOpacity:1,pointOutlineColor:"#ffffff",pointOutlineOpacity:1,pointOutlineWidth:0,pointWidth:6,lineStringColor:"#3f97e0",lineStringWidth:4,lineStringOpacity:1,zIndex:0,markerUrl:void 0,markerHeight:void 0,markerWidth:void 0,lineStringDash:void 0});if("Feature"===t.type&&"LineString"===t.geometry.type&&t.properties.mode===this.mode)return e.lineStringDash=this.getDashArrayStylingValue(this.styles.lineStringDash,void 0,t),e.lineStringColor=this.getHexColorStylingValue(this.styles.lineStringColor,e.lineStringColor,t),e.lineStringOpacity=this.getNumericStylingValue(this.styles.lineStringOpacity,void 0===e.lineStringOpacity?1:e.lineStringOpacity,t),e.lineStringWidth=this.getNumericStylingValue(this.styles.lineStringWidth,e.lineStringWidth,t),e.zIndex=v,e;if("Feature"===t.type&&"Point"===t.geometry.type&&t.properties.mode===this.mode){var i=t.properties[y.COORDINATE_POINT],n=t.properties[y.CLOSING_POINT]?"closingPoint":t.properties[y.SNAPPING_POINT]?"snappingPoint":i?"coordinatePoint":void 0;if(!n)return e;var o={closingPoint:{width:this.styles.closingPointWidth,color:this.styles.closingPointColor,opacity:this.styles.closingPointOpacity,outlineColor:this.styles.closingPointOutlineColor,outlineWidth:this.styles.closingPointOutlineWidth,outlineOpacity:this.styles.closingPointOutlineOpacity},snappingPoint:{width:this.styles.snappingPointWidth,color:this.styles.snappingPointColor,opacity:this.styles.snappingPointOpacity,outlineColor:this.styles.snappingPointOutlineColor,outlineWidth:this.styles.snappingPointOutlineWidth,outlineOpacity:this.styles.snappingPointOutlineOpacity},coordinatePoint:{width:this.styles.coordinatePointWidth,color:this.styles.coordinatePointColor,opacity:this.styles.coordinatePointOpacity,outlineColor:this.styles.coordinatePointOutlineColor,outlineWidth:this.styles.coordinatePointOutlineWidth,outlineOpacity:this.styles.coordinatePointOutlineOpacity}};return e.pointWidth=this.getNumericStylingValue(o[n].width,e.pointWidth,t),e.pointOpacity=this.getNumericStylingValue(o[n].opacity,1,t),e.pointColor=this.getHexColorStylingValue(o[n].color,e.pointColor,t),e.pointOutlineColor=this.getHexColorStylingValue(o[n].outlineColor,"#ffffff",t),e.pointOutlineWidth=this.getNumericStylingValue(o[n].outlineWidth,2,t),e.pointOutlineOpacity=this.getNumericStylingValue(o[n].outlineOpacity,1,t),e.zIndex=i?20:50,e}return e},i.validateFeature=function(t){var e=this;return this.validateModeFeature(t,function(t){return wt(t,e.coordinatePrecision)})},i.lineStringFilter=function(t){return Boolean("LineString"===t.geometry.type&&t.properties&&t.properties.mode===this.mode)},i.snapCoordinate=function(t){var e,i,n,o,r,s,a,d=this;if(null!=(e=this.snapping)&&e.toLine&&(s=this.currentId?this.lineSnapping.getSnappableCoordinate(t,this.currentId):this.lineSnapping.getSnappableCoordinateFirstClick(t))&&(r=s),null!=(i=this.snapping)&&i.toCoordinate&&(a=this.currentId?this.coordinateSnapping.getSnappableCoordinate(t,this.currentId):this.coordinateSnapping.getSnappableCoordinateFirstClick(t))&&(r=a),null!=(n=this.snapping)&&n.toCustom){var u=this.snapping.toCustom(t,{currentCoordinate:this.currentCoordinate,currentId:this.currentId,getCurrentGeometrySnapshot:this.currentId?function(){return d.readFeature.getGeometry(d.currentId)}:function(){return null},project:this.project,unproject:this.unproject});u&&(r=u)}if(null!=(o=this.snapping)&&o.toFeature){var h=this.featureSnapping.getSnappable(t,this.currentId,this.snapping.toFeature.filter,{toLine:this.snapping.toFeature.toLine,toCoordinate:this.snapping.toFeature.toCoordinate});h.coordinate&&(r=h.coordinate)}return r},i.afterFeatureUpdated=function(t){this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:t.id,featureCoordinates:t.geometry.coordinates}),this.editedFeatureId===t.id&&this.editedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.editedPointId),this.editedPointId=void 0,this.editedFeatureId=void 0,this.editedFeatureCoordinateIndex=void 0,this.editedSnapType=void 0),this.snappedPointId&&this.lastMouseMoveEvent&&this.updateSnappedCoordinate(this.lastMouseMoveEvent),this.currentId===t.id&&(this.closingPoints.delete(),this.currentCoordinate=0,this.currentId=void 0,this.lastCommittedCoordinates=void 0,this.undoRedo.clear(),"drawing"===this.state&&this.setStarted())},i.afterFeatureAdded=function(t){this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:t.id,featureCoordinates:t.geometry.coordinates})},e}(O),Vt={cancel:"Escape",finish:"Enter"},Gt={start:"crosshair",close:"pointer"},jt=/*#__PURE__*/function(t){function e(e){var i;return(i=t.call(this,e,!0)||this).mode="polyline",i.currentCoordinate=0,i.currentId=void 0,i.keyEvents=Vt,i.cursors=Gt,i.mouseMove=!1,i.snapping=void 0,i.snappedPointId=void 0,i.mutateFeature=void 0,i.readFeature=void 0,i.pixelDistance=void 0,i.closingPoints=void 0,i.clickBoundingBox=void 0,i.lineSnapping=void 0,i.coordinateSnapping=void 0,i.featureSnapping=void 0,i.updateOptions(e),i}s(e,t);var i=e.prototype;return i.updateOptions=function(e){t.prototype.updateOptions.call(this,e),null!=e&&e.cursors&&(this.cursors=r({},this.cursors,e.cursors)),null!=e&&e.snapping&&(this.snapping=e.snapping),null===(null==e?void 0:e.keyEvents)?this.keyEvents={cancel:null,finish:null}:null!=e&&e.keyEvents&&(this.keyEvents=r({},this.keyEvents,e.keyEvents))},i.registerBehaviors=function(t){this.clickBoundingBox=new ft(t),this.pixelDistance=new yt(t),this.lineSnapping=new _t(t,this.pixelDistance,this.clickBoundingBox),this.coordinateSnapping=new vt(t,this.pixelDistance,this.clickBoundingBox),this.featureSnapping=new Tt(this.coordinateSnapping,this.lineSnapping),this.readFeature=new ht(t),this.mutateFeature=new ot(t,{validate:this.validate}),this.closingPoints=new Wt(t,this.pixelDistance,this.mutateFeature,this.readFeature)},i.start=function(){this.setStarted(),this.setCursor(this.cursors.start)},i.stop=function(){this.cleanUp(),this.setStopped(),this.setCursor("unset")},i.finishLine=function(){var t;if(this.currentId&&this.mutateFeature.updateLineString({featureId:this.currentId,context:{updateType:u.Finish,action:h},coordinateMutations:[{type:it,index:-1}],propertyMutations:(t={},t[y.CURRENTLY_DRAWING]=void 0,t)})){var e=this.currentId;this.currentCoordinate=0,this.currentId=void 0,this.closingPoints.delete(),this.mutateFeature.deleteFeatureIfPresent(this.snappedPointId),this.snappedPointId=void 0,"drawing"===this.state&&this.setStarted(),this.onFinish(e,{mode:this.mode,action:h})}},i.toPolygonLikeCoordinates=function(t){return 0===t.length?[t]:[[].concat(t,[t[0]])]},i.closeAsPolygon=function(){if(this.currentId){var t=this.readFeature.getGeometry(this.currentId).coordinates.slice(0,-1);if(!(t.length<3)){var e=this.currentId,i=[].concat(t,[t[0]]),n=this.mutateFeature.createPolygon({coordinates:i,properties:{mode:this.mode},context:{updateType:u.Finish,action:h}});n&&(this.mutateFeature.deleteFeatureIfPresent(e),this.currentCoordinate=0,this.currentId=void 0,this.closingPoints.delete(),this.mutateFeature.deleteFeatureIfPresent(this.snappedPointId),this.snappedPointId=void 0,"drawing"===this.state&&this.setStarted(),this.onFinish(n.id,{mode:this.mode,action:h}))}}},i.onMouseMove=function(t){if(this.mouseMove=!0,this.setCursor(this.cursors.start),this.updateSnappedCoordinate(t),this.currentId&&0!==this.currentCoordinate&&this.mutateFeature.updateLineString({featureId:this.currentId,coordinateMutations:[{type:et,index:-1,coordinate:[t.lng,t.lat]}],context:{updateType:u.Provisional}})){var e=this.closingPoints.isPolygonClosingPoints(t);(e.isClosing&&this.currentCoordinate>=3||e.isPreviousClosing&&this.currentCoordinate>=2)&&this.setCursor(this.cursors.close)}},i.onLeftClick=function(t){this.updateSnappedCoordinate(t);var e=[t.lng,t.lat];if(0===this.currentCoordinate){var i,n=this.mutateFeature.createLineString({coordinates:[e,e],properties:(i={mode:this.mode},i[y.CURRENTLY_DRAWING]=!0,i)});return this.currentId=n.id,this.currentCoordinate=1,void this.setDrawing()}if(this.currentId){var o=this.closingPoints.isPolygonClosingPoints(t),r=o.isPreviousClosing;if(o.isClosing&&this.currentCoordinate>=3)this.closeAsPolygon();else if(r&&this.currentCoordinate>=2)this.finishLine();else{var s=this.mutateFeature.updateLineString({featureId:this.currentId,context:{updateType:u.Commit},coordinateMutations:[{type:tt,index:-1,coordinate:e}]});if(s&&(this.currentCoordinate++,this.currentCoordinate>=2)){var a=this.toPolygonLikeCoordinates(s.geometry.coordinates);0===this.closingPoints.ids.length?this.closingPoints.create(a):this.closingPoints.update(a)}}}},i.onClick=function(t){"left"===t.button&&this.allowPointerEvent(this.pointerEvents.leftClick,t)&&(this.currentCoordinate>0&&!this.mouseMove&&this.onMouseMove(t),this.mouseMove=!1,this.onLeftClick(t))},i.onKeyUp=function(t){t.key===this.keyEvents.cancel?this.cleanUp():t.key===this.keyEvents.finish&&this.finishLine()},i.onKeyDown=function(){},i.onDragStart=function(){},i.onDrag=function(){},i.onDragEnd=function(){},i.cleanUp=function(){var t=this.currentId;this.currentId=void 0,this.currentCoordinate=0,"drawing"===this.state&&this.setStarted(),this.mutateFeature.deleteFeatureIfPresent(t),this.mutateFeature.deleteFeatureIfPresent(this.snappedPointId),this.snappedPointId=void 0,this.closingPoints.delete()},i.updateSnappedCoordinate=function(t){var e=this.snapCoordinate(t);e?(this.snappedPointId?this.mutateFeature.updateGuidancePoints([{featureId:this.snappedPointId,coordinate:e}]):this.snappedPointId=this.mutateFeature.createGuidancePoint({coordinate:e,type:y.SNAPPING_POINT}),t.lng=e[0],t.lat=e[1]):this.snappedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.snappedPointId),this.snappedPointId=void 0)},i.snapCoordinate=function(t){var e,i,n,o,r,s,a,d=this;if(null!=(e=this.snapping)&&e.toLine&&(s=this.currentId?this.lineSnapping.getSnappableCoordinate(t,this.currentId):this.lineSnapping.getSnappableCoordinateFirstClick(t))&&(r=s),null!=(i=this.snapping)&&i.toCoordinate&&(a=this.currentId?this.coordinateSnapping.getSnappableCoordinate(t,this.currentId):this.coordinateSnapping.getSnappableCoordinateFirstClick(t))&&(r=a),null!=(n=this.snapping)&&n.toFeature){var u=this.featureSnapping.getSnappable(t,this.currentId,this.snapping.toFeature.filter,{toLine:this.snapping.toFeature.toLine,toCoordinate:this.snapping.toFeature.toCoordinate});u.coordinate&&(r=u.coordinate)}if(null!=(o=this.snapping)&&o.toCustom){var h=this.snapping.toCustom(t,{currentCoordinate:this.currentCoordinate,currentId:this.currentId,getCurrentGeometrySnapshot:this.currentId?function(){return d.readFeature.getGeometry(d.currentId)}:function(){return null},project:this.project,unproject:this.unproject});h&&(r=h)}return r},i.styleFeature=function(t){var e=r({},{polygonFillColor:"#3f97e0",polygonOutlineColor:"#3f97e0",polygonOutlineWidth:4,polygonOutlineOpacity:1,polygonFillOpacity:.3,pointColor:"#3f97e0",pointOpacity:1,pointOutlineColor:"#ffffff",pointOutlineOpacity:1,pointOutlineWidth:0,pointWidth:6,lineStringColor:"#3f97e0",lineStringWidth:4,lineStringOpacity:1,zIndex:0,markerUrl:void 0,markerHeight:void 0,markerWidth:void 0,lineStringDash:void 0});if(t.properties.mode!==this.mode)return e;if("LineString"===t.geometry.type)return e.lineStringColor=this.getHexColorStylingValue(this.styles.lineStringColor,e.lineStringColor,t),e.lineStringWidth=this.getNumericStylingValue(this.styles.lineStringWidth,e.lineStringWidth,t),e.lineStringOpacity=this.getNumericStylingValue(this.styles.lineStringOpacity,1,t),e.lineStringDash=this.getDashArrayStylingValue(this.styles.lineStringDash,void 0,t),e.zIndex=v,e;if("Polygon"===t.geometry.type)return e.polygonFillColor=this.getHexColorStylingValue(this.styles.polygonFillColor,e.polygonFillColor,t),e.polygonFillOpacity=this.getNumericStylingValue(this.styles.polygonFillOpacity,e.polygonFillOpacity,t),e.polygonOutlineColor=this.getHexColorStylingValue(this.styles.polygonOutlineColor,e.polygonOutlineColor,t),e.polygonOutlineWidth=this.getNumericStylingValue(this.styles.polygonOutlineWidth,e.polygonOutlineWidth,t),e.polygonOutlineOpacity=this.getNumericStylingValue(this.styles.polygonOutlineOpacity,1,t),e.zIndex=v,e;if("Point"===t.geometry.type){var i=!0===t.properties[y.CLOSING_POINT];if(!i&&!0!==t.properties[y.SNAPPING_POINT])return e;e.pointColor=this.getHexColorStylingValue(i?this.styles.closingPointColor:this.styles.snappingPointColor,e.pointColor,t),e.pointWidth=this.getNumericStylingValue(i?this.styles.closingPointWidth:this.styles.snappingPointWidth,e.pointWidth,t),e.pointOpacity=this.getNumericStylingValue(i?this.styles.closingPointOpacity:this.styles.snappingPointOpacity,1,t),e.pointOutlineColor=this.getHexColorStylingValue(i?this.styles.closingPointOutlineColor:this.styles.snappingPointOutlineColor,e.pointOutlineColor,t),e.pointOutlineWidth=this.getNumericStylingValue(i?this.styles.closingPointOutlineWidth:this.styles.snappingPointOutlineWidth,2,t),e.pointOutlineOpacity=this.getNumericStylingValue(i?this.styles.closingPointOutlineOpacity:this.styles.snappingPointOutlineOpacity,1,t),e.zIndex=30}return e},i.afterFeatureAdded=function(t){},i.afterFeatureUpdated=function(t){this.snappedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.snappedPointId),this.snappedPointId=void 0),this.currentId===t.id&&(this.currentCoordinate=0,this.currentId=void 0,this.closingPoints.delete(),"drawing"===this.state&&this.setStarted())},i.validateFeature=function(t){var e=this;return this.validateModeFeature(t,function(t){return"LineString"===t.geometry.type?wt(t,e.coordinatePrecision):"Polygon"===t.geometry.type?q(t,e.coordinatePrecision):{valid:!1,reason:"Only LineString or Polygon features are valid"}})},e}(O),Kt="Feature is not a Point",Yt="Feature has invalid coordinates",Xt="Feature has coordinates with excessive precision";function qt(t,e){return"Point"!==t.geometry.type?{valid:!1,reason:Kt}:V(t.geometry.coordinates)?H(t.geometry.coordinates,e)?{valid:!0}:{valid:!1,reason:Xt}:{valid:!1,reason:Yt}}var Zt=/*#__PURE__*/function(t){function e(e,i,n){var o;return(o=t.call(this,e)||this).pixelDistance=void 0,o.clickBoundingBox=void 0,o.pixelDistance=i,o.clickBoundingBox=n,o}return s(e,t),e.prototype.getNearestPointFeature=function(t){for(var e=this.clickBoundingBox.create(t),i=this.store.search(e),n=Infinity,o=void 0,r=0;rn||a>this.pointerDistance||(n=a,o=s)}}return o},e}(J),Jt={create:"crosshair",dragStart:"grabbing",dragEnd:"crosshair"},$t=/*#__PURE__*/function(t){function e(e){var i;return(i=t.call(this,e,!0)||this).mode="point",i.cursors=Jt,i.editable=!1,i.editedFeatureId=void 0,i.pixelDistance=void 0,i.clickBoundingBox=void 0,i.pointSearch=void 0,i.mutateFeature=void 0,i.updateOptions(e),i}s(e,t);var i=e.prototype;return i.updateOptions=function(e){t.prototype.updateOptions.call(this,e),null!=e&&e.cursors&&(this.cursors=r({},this.cursors,e.cursors)),null!=e&&e.editable&&(this.editable=e.editable)},i.start=function(){this.setStarted(),this.setCursor(this.cursors.create)},i.stop=function(){this.cleanUp(),this.setStopped(),this.setCursor("unset")},i.onClick=function(t){"right"===t.button&&this.allowPointerEvent(this.pointerEvents.rightClick,t)||t.isContextMenu&&this.allowPointerEvent(this.pointerEvents.contextMenu,t)?this.onRightClick(t):"left"===t.button&&this.allowPointerEvent(this.pointerEvents.leftClick,t)&&this.onLeftClick(t)},i.onMouseMove=function(){},i.onKeyDown=function(){},i.onKeyUp=function(){},i.cleanUp=function(){this.editedFeatureId=void 0},i.onDragStart=function(t,e){if(this.allowPointerEvent(this.pointerEvents.onDragStart,t)){if(this.editable){var i=this.pointSearch.getNearestPointFeature(t);this.editedFeatureId=null==i?void 0:i.id}this.editedFeatureId&&(this.setCursor(this.cursors.dragStart),e(!1))}},i.onDrag=function(t,e){var i;this.allowPointerEvent(this.pointerEvents.onDrag,t)&&void 0!==this.editedFeatureId&&this.mutateFeature.updatePoint({featureId:this.editedFeatureId,coordinateMutations:{type:nt,coordinates:[t.lng,t.lat]},propertyMutations:(i={},i[y.EDITED]=!0,i),context:{updateType:u.Provisional}})},i.onDragEnd=function(t,e){var i;if(this.allowPointerEvent(this.pointerEvents.onDragEnd,t)&&void 0!==this.editedFeatureId&&this.mutateFeature.updatePoint({featureId:this.editedFeatureId,propertyMutations:(i={mode:this.mode},i[y.EDITED]=!1,i),context:{updateType:u.Finish,action:"edit"}})){var n=this.editedFeatureId;this.setCursor(this.cursors.dragEnd),this.editedFeatureId=void 0,e(!0),this.onFinish(n,{mode:this.mode,action:h})}},i.registerBehaviors=function(t){this.pixelDistance=new yt(t),this.clickBoundingBox=new ft(t),this.pointSearch=new Zt(t,this.pixelDistance,this.clickBoundingBox),this.mutateFeature=new ot(t,{validate:this.validate})},i.styleFeature=function(t){var e=r({},{polygonFillColor:"#3f97e0",polygonOutlineColor:"#3f97e0",polygonOutlineWidth:4,polygonOutlineOpacity:1,polygonFillOpacity:.3,pointColor:"#3f97e0",pointOpacity:1,pointOutlineColor:"#ffffff",pointOutlineOpacity:1,pointOutlineWidth:0,pointWidth:6,lineStringColor:"#3f97e0",lineStringWidth:4,lineStringOpacity:1,zIndex:0,markerUrl:void 0,markerHeight:void 0,markerWidth:void 0,lineStringDash:void 0});if("Feature"===t.type&&"Point"===t.geometry.type&&t.properties.mode===this.mode){var i=Boolean(t.id&&this.editedFeatureId===t.id);e.pointWidth=this.getNumericStylingValue(i?this.styles.editedPointWidth:this.styles.pointWidth,e.pointWidth,t),e.pointOpacity=this.getNumericStylingValue(this.styles.pointOpacity,void 0===e.pointOpacity?1:e.pointOpacity,t),e.pointColor=this.getHexColorStylingValue(i?this.styles.editedPointColor:this.styles.pointColor,e.pointColor,t),e.pointOutlineColor=this.getHexColorStylingValue(i?this.styles.editedPointOutlineColor:this.styles.pointOutlineColor,e.pointOutlineColor,t),e.pointOutlineOpacity=this.getNumericStylingValue(this.styles.pointOutlineOpacity,void 0===e.pointOutlineOpacity?1:e.pointOutlineOpacity,t),e.pointOutlineWidth=this.getNumericStylingValue(i?this.styles.editedPointOutlineWidth:this.styles.pointOutlineWidth,2,t),e.zIndex=30}return e},i.validateFeature=function(t){var e=this;return this.validateModeFeature(t,function(t){return qt(t,e.coordinatePrecision)})},i.onLeftClick=function(t){var e=this.mutateFeature.createPoint({coordinates:[t.lng,t.lat],properties:{mode:this.mode},context:{updateType:u.Finish,action:h}});e&&this.onFinish(e.id,{mode:this.mode,action:h})},i.onRightClick=function(t){if(this.editable){var e=this.pointSearch.getNearestPointFeature(t);e&&this.mutateFeature.deleteFeatureIfPresent(e.id)}},i.afterFeatureUpdated=function(t){this.editedFeatureId===t.id&&(this.editedFeatureId=void 0,this.setCursor(this.cursors.create))},e}(O),Qt={cancel:"Escape",finish:"Enter"},te={start:"crosshair",close:"pointer",dragStart:"grabbing",dragEnd:"crosshair"},ee=/*#__PURE__*/function(t){function e(e){var i;return(i=t.call(this,e,!0)||this).mode="polygon",i.currentCoordinate=0,i.currentId=void 0,i.keyEvents=Qt,i.cursors=te,i.mouseMove=!1,i.showCoordinatePoints=!1,i.lastMouseMoveEvent=void 0,i.snapping=void 0,i.snappedPointId=void 0,i.editable=!1,i.editedFeatureId=void 0,i.editedFeatureCoordinateIndex=void 0,i.editedSnapType=void 0,i.editedInsertIndex=void 0,i.editedPointId=void 0,i.coordinatePoints=void 0,i.lineSnapping=void 0,i.coordinateSnapping=void 0,i.featureSnapping=void 0,i.pixelDistance=void 0,i.closingPoints=void 0,i.clickBoundingBox=void 0,i.mutateFeature=void 0,i.readFeature=void 0,i.undoRedo=void 0,i.updateOptions(e),i}s(e,t);var i=e.prototype;return i.updateOptions=function(e){var i=this;if(t.prototype.updateOptions.call(this,e),null!=e&&e.cursors&&(this.cursors=r({},this.cursors,e.cursors)),null===(null==e?void 0:e.keyEvents)?this.keyEvents={cancel:null,finish:null}:null!=e&&e.keyEvents&&(this.keyEvents=r({},this.keyEvents,e.keyEvents)),null!=e&&e.snapping&&(this.snapping=e.snapping),void 0!==(null==e?void 0:e.editable)&&(this.editable=e.editable),void 0!==(null==e?void 0:e.pointerEvents)&&(this.pointerEvents=e.pointerEvents),void 0!==(null==e?void 0:e.showCoordinatePoints))if(this.showCoordinatePoints=e.showCoordinatePoints,this.coordinatePoints&&!0===e.showCoordinatePoints)this.store.copyAllWhere(function(t){return t.mode===i.mode}).filter(function(t){return"Polygon"===t.geometry.type}).forEach(function(t){i.coordinatePoints.createOrUpdate({featureId:t.id,featureCoordinates:t.geometry.coordinates})});else if(this.coordinatePoints&&!1===this.showCoordinatePoints){var n=this.store.copyAllWhere(function(t){return t.mode===i.mode&&Boolean(t[y.COORDINATE_POINT_IDS])}).filter(function(t){return"Polygon"===t.geometry.type});this.coordinatePoints.deletePointsByFeatureIds(n.map(function(t){return t.id}))}},i.close=function(){var t;if(void 0!==this.currentId&&!(this.readFeature.getCoordinates(this.currentId).length<5)){var e=this.mutateFeature.updatePolygon({featureId:this.currentId,coordinateMutations:[{type:it,index:-2}],propertyMutations:(t={},t[y.CURRENTLY_DRAWING]=void 0,t[y.COMMITTED_COORDINATE_COUNT]=void 0,t[y.PROVISIONAL_COORDINATE_COUNT]=void 0,t),context:{updateType:u.Finish,action:h}});if(e){this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:this.currentId,featureCoordinates:e.geometry.coordinates}),"drawing"===this.state&&this.setStarted(),this.editedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.editedPointId),this.editedPointId=void 0),this.snappedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.snappedPointId),this.snappedPointId=void 0),this.closingPoints.delete();var i=this.currentId;this.currentCoordinate=0,this.currentId=void 0,this.undoRedo.clear(),this.onFinish(i,{mode:this.mode,action:h})}}},i.registerBehaviors=function(t){this.readFeature=new ht(t),this.mutateFeature=new ot(t,{validate:this.validate}),this.clickBoundingBox=new ft(t),this.pixelDistance=new yt(t),this.lineSnapping=new _t(t,this.pixelDistance,this.clickBoundingBox),this.coordinateSnapping=new vt(t,this.pixelDistance,this.clickBoundingBox),this.featureSnapping=new Tt(this.coordinateSnapping,this.lineSnapping),this.closingPoints=new Wt(t,this.pixelDistance,this.mutateFeature,this.readFeature),this.coordinatePoints=new Ut(t,this.readFeature,this.mutateFeature),this.undoRedo=new At({maxStackSize:t.undoRedoMaxStackSize})},i.start=function(){this.setStarted(),this.setCursor(this.cursors.start)},i.stop=function(){this.cleanUp(),this.setStopped(),this.setCursor("unset")},i.updateSnappedCoordinate=function(t){var e=this.snapCoordinate(t);e?(this.snappedPointId?this.mutateFeature.updateGuidancePoints([{featureId:this.snappedPointId,coordinate:e}]):this.snappedPointId=this.mutateFeature.createGuidancePoint({coordinate:e,type:y.SNAPPING_POINT}),t.lng=e[0],t.lat=e[1]):this.snappedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.snappedPointId),this.snappedPointId=void 0)},i.undoSize=function(){return this.undoRedo.undoSize()},i.clearHistory=function(){this.undoRedo.clear()},i.pushHistorySnapshot=function(t,e){var i=this.readFeature.getGeometry(t);this.undoRedo.recordSnapshot({featureCoordinates:i.coordinates,currentCoordinate:e})},i.updateSnappedGuidancePointFromLastMouseMove=function(){this.snapping&&this.lastMouseMoveEvent?this.updateSnappedCoordinate(this.lastMouseMoveEvent):this.snappedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.snappedPointId),this.snappedPointId=void 0)},i.syncClosingPoints=function(t){this.currentCoordinate>=3?this.closingPoints.ids.length?this.closingPoints.update(t):this.closingPoints.create(t):this.closingPoints.delete()},i.undo=function(){var t;if("drawing"===this.state&&this.currentId){var e=this.undoRedo.beginUndo();if(e){var i=e.previousEntry;if(!i){var n=this.currentId;return this.currentId=void 0,this.currentCoordinate=0,this.closingPoints.delete(),"drawing"===this.state&&this.setStarted(),this.showCoordinatePoints&&this.coordinatePoints.deletePointsByFeatureIds([n]),this.mutateFeature.deleteFeatureIfPresent(n),void this.updateSnappedGuidancePointFromLastMouseMove()}var o=this.mutateFeature.updatePolygon({featureId:this.currentId,coordinateMutations:{type:nt,coordinates:i.featureCoordinates},propertyMutations:(t={},t[y.CURRENTLY_DRAWING]=!0,t[y.COMMITTED_COORDINATE_COUNT]=i.currentCoordinate,t[y.PROVISIONAL_COORDINATE_COUNT]=i.currentCoordinate,t),context:{updateType:u.Commit}});o&&(this.currentCoordinate=i.currentCoordinate,this.syncClosingPoints(o.geometry.coordinates),this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:this.currentId,featureCoordinates:o.geometry.coordinates}),this.updateSnappedGuidancePointFromLastMouseMove())}}},i.redoSize=function(){return this.undoRedo.redoSize()},i.redo=function(){var t=this.undoRedo.takeRedo();if(t){if(this.currentId){var e,i=this.mutateFeature.updatePolygon({featureId:this.currentId,coordinateMutations:{type:nt,coordinates:t.featureCoordinates},propertyMutations:(e={},e[y.CURRENTLY_DRAWING]=!0,e[y.COMMITTED_COORDINATE_COUNT]=t.currentCoordinate,e[y.PROVISIONAL_COORDINATE_COUNT]=t.currentCoordinate,e),context:{updateType:u.Commit}});if(!i)return;this.currentCoordinate=t.currentCoordinate,this.syncClosingPoints(i.geometry.coordinates),this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:this.currentId,featureCoordinates:i.geometry.coordinates})}else{var n,o=this.undoRedo.cloneCoordinates(t.featureCoordinates)[0],r=this.mutateFeature.createPolygon({coordinates:o,properties:(n={mode:this.mode},n[y.CURRENTLY_DRAWING]=!0,n[y.COMMITTED_COORDINATE_COUNT]=t.currentCoordinate,n[y.PROVISIONAL_COORDINATE_COUNT]=t.currentCoordinate,n)}),s=r.id,a=r.geometry;this.currentId=s,this.currentCoordinate=t.currentCoordinate,"started"===this.state&&this.setDrawing(),this.syncClosingPoints(a.coordinates),this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:s,featureCoordinates:a.coordinates})}this.undoRedo.commitRedo(t),this.updateSnappedGuidancePointFromLastMouseMove()}},i.onMouseMove=function(t){var e;if(this.mouseMove=!0,this.setCursor(this.cursors.start),this.lastMouseMoveEvent=t,this.updateSnappedCoordinate(t),void 0!==this.currentId&&0!==this.currentCoordinate){var i,n=this.readFeature.getCoordinate(this.currentId,0),o=[t.lng,t.lat];if(1===this.currentCoordinate)i=[{type:et,index:1,coordinate:o},{type:et,index:2,coordinate:[t.lng,t.lat]}];else if(2===this.currentCoordinate)i=[{type:et,index:2,coordinate:o}];else{var r=this.closingPoints.isPolygonClosingPoints(t);r.isPreviousClosing||r.isClosing?(this.snappedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.snappedPointId),this.snappedPointId=void 0),this.setCursor(this.cursors.close),i=[{type:et,index:-1,coordinate:n},{type:et,index:-2,coordinate:n}]):i=[{type:et,index:-2,coordinate:o},{type:et,index:-1,coordinate:n}]}var s=this.mutateFeature.updatePolygon({featureId:this.currentId,coordinateMutations:i,propertyMutations:(e={},e[y.PROVISIONAL_COORDINATE_COUNT]=this.currentCoordinate+1,e),context:{updateType:u.Provisional}});s&&this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:this.currentId,featureCoordinates:s.geometry.coordinates})}},i.snapCoordinate=function(t){var e,i,n,o,r,s,a=this,d=void 0;if(null!=(e=this.snapping)&&e.toLine&&(r=this.currentId?this.lineSnapping.getSnappableCoordinate(t,this.currentId):this.lineSnapping.getSnappableCoordinateFirstClick(t))&&(d=r),null!=(i=this.snapping)&&i.toCoordinate&&(s=this.currentId?this.coordinateSnapping.getSnappableCoordinate(t,this.currentId):this.coordinateSnapping.getSnappableCoordinateFirstClick(t))&&(d=s),null!=(n=this.snapping)&&n.toFeature){var u,h=this.featureSnapping.getSnappable(t,this.currentId,null==(u=this.snapping)?void 0:u.toFeature.filter,{toLine:this.snapping.toFeature.toLine,toCoordinate:this.snapping.toFeature.toCoordinate});h.coordinate&&(d=h.coordinate)}if(null!=(o=this.snapping)&&o.toCustom){var l=this.snapping.toCustom(t,{currentCoordinate:this.currentCoordinate,currentId:this.currentId,getCurrentGeometrySnapshot:this.currentId?function(){return a.readFeature.getGeometry(a.currentId)}:function(){return null},project:this.project,unproject:this.unproject});l&&(d=l)}return d},i.polygonFilter=function(t){return Boolean("Polygon"===t.geometry.type&&t.properties&&t.properties.mode===this.mode)},i.onRightClick=function(t){var e=this;if(this.editable&&"started"===this.state){var i=this.coordinateSnapping.getSnappable(t,function(t){return e.polygonFilter(t)}),n=i.featureId,o=i.featureCoordinateIndex;if(n&&void 0!==o){var r=this.readFeature.getGeometry(n);if("Polygon"===r.type){var s=r.coordinates[0];if(!(s.length<=4)){var a=this.mutateFeature.updatePolygon({featureId:n,coordinateMutations:0===o||o===s.length-1?[{type:it,index:0},{type:it,index:-1},{type:tt,index:-1,coordinate:s[1]}]:[{type:it,index:o}],context:{updateType:u.Finish,action:l}});if(a){if(this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:n,featureCoordinates:a.geometry.coordinates}),this.snappedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.snappedPointId),this.snappedPointId=void 0,this.snapping)){var d=this.snapCoordinate(t);if(d){var h=this.mutateFeature.createGuidancePoints({type:y.SNAPPING_POINT,coordinates:[d]});this.snappedPointId=h[0]}}this.onFinish(n,{mode:this.mode,action:l})}}}}}},i.onLeftClick=function(t){this.snappedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.snappedPointId),this.snappedPointId=void 0);var e=this.snapCoordinate(t)||[t.lng,t.lat];if(0===this.currentCoordinate){var i,n=this.mutateFeature.createPolygon({coordinates:[e,e,e,e],properties:(i={mode:this.mode},i[y.CURRENTLY_DRAWING]=!0,i[y.COMMITTED_COORDINATE_COUNT]=this.currentCoordinate+1,i[y.PROVISIONAL_COORDINATE_COUNT]=this.currentCoordinate+1,i)}),o=n.id;this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:o,featureCoordinates:n.geometry.coordinates}),this.currentId=o,this.currentCoordinate++,this.pushHistorySnapshot(this.currentId,this.currentCoordinate),this.setDrawing()}else if(1===this.currentCoordinate&&this.currentId){var r;if(this.readFeature.coordinateAtIndexIsIdentical({featureId:this.currentId,newCoordinate:e,index:0}))return;var s=this.mutateFeature.updatePolygon({featureId:this.currentId,coordinateMutations:[{type:et,index:1,coordinate:e},{type:et,index:2,coordinate:e}],propertyMutations:(r={},r[y.COMMITTED_COORDINATE_COUNT]=this.currentCoordinate+1,r),context:{updateType:u.Commit}});if(!s)return;this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:this.currentId,featureCoordinates:s.geometry.coordinates}),this.currentCoordinate++,this.pushHistorySnapshot(this.currentId,this.currentCoordinate)}else if(2===this.currentCoordinate&&this.currentId){var a;if(this.readFeature.coordinateAtIndexIsIdentical({featureId:this.currentId,newCoordinate:e,index:1}))return;var d=this.mutateFeature.updatePolygon({featureId:this.currentId,coordinateMutations:[{type:et,index:2,coordinate:e},{type:tt,index:2,coordinate:e}],propertyMutations:(a={},a[y.COMMITTED_COORDINATE_COUNT]=this.currentCoordinate+1,a),context:{updateType:u.Commit}});if(!d)return;this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:this.currentId,featureCoordinates:d.geometry.coordinates}),2===this.currentCoordinate&&this.closingPoints.create(d.geometry.coordinates),this.currentCoordinate++,this.pushHistorySnapshot(this.currentId,this.currentCoordinate)}else if(this.currentId){var h=this.closingPoints.isPolygonClosingPoints(t);if(h.isPreviousClosing||h.isClosing)this.close();else{var l;if(this.readFeature.coordinateAtIndexIsIdentical({featureId:this.currentId,newCoordinate:e,index:this.currentCoordinate-1}))return;var c=this.mutateFeature.updatePolygon({featureId:this.currentId,coordinateMutations:[{type:Q,index:-1,coordinate:e}],propertyMutations:(l={},l[y.COMMITTED_COORDINATE_COUNT]=this.currentCoordinate+1,l),context:{updateType:u.Commit}});if(!c)return;this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:this.currentId,featureCoordinates:c.geometry.coordinates}),this.currentCoordinate++,this.pushHistorySnapshot(this.currentId,this.currentCoordinate),this.closingPoints.ids.length&&this.closingPoints.update(c.geometry.coordinates)}}},i.onClick=function(t){this.currentCoordinate>0&&!this.mouseMove&&this.onMouseMove(t),this.mouseMove=!1,"right"===t.button&&this.allowPointerEvent(this.pointerEvents.rightClick,t)||t.isContextMenu&&this.allowPointerEvent(this.pointerEvents.contextMenu,t)?this.onRightClick(t):"left"===t.button&&this.allowPointerEvent(this.pointerEvents.leftClick,t)&&this.onLeftClick(t)},i.onKeyUp=function(t){t.key===this.keyEvents.cancel?this.cleanUp():t.key===this.keyEvents.finish&&this.close()},i.onKeyDown=function(){},i.onDragStart=function(t,e){var i=this;if(this.allowPointerEvent(this.pointerEvents.onDragStart,t)&&this.editable){var n=void 0;if("started"===this.state){var o=this.lineSnapping.getSnappable(t,function(t){return i.polygonFilter(t)});o.coordinate&&(this.editedSnapType="line",this.editedFeatureCoordinateIndex=o.featureCoordinateIndex,this.editedFeatureId=o.featureId,n=o.coordinate);var r=this.coordinateSnapping.getSnappable(t,function(t){return i.polygonFilter(t)});r.coordinate&&(this.editedSnapType="coordinate",this.editedFeatureCoordinateIndex=r.featureCoordinateIndex,this.editedFeatureId=r.featureId,n=r.coordinate)}this.editedFeatureId&&n&&(this.editedPointId||(this.editedPointId=this.mutateFeature.createGuidancePoint({coordinate:n,type:y.EDITED})),this.setCursor(this.cursors.dragStart),e(!1))}},i.onDrag=function(t,e){var i;if(this.allowPointerEvent(this.pointerEvents.onDrag,t)&&void 0!==this.editedFeatureId&&void 0!==this.editedFeatureCoordinateIndex){var n=this.readFeature.getGeometry(this.editedFeatureId),o=[t.lng,t.lat],r=[];if("coordinate"===this.editedSnapType||"line"===this.editedSnapType&&void 0!==this.editedInsertIndex?r=0===this.editedFeatureCoordinateIndex||this.editedFeatureCoordinateIndex===n.coordinates[0].length-1?[{type:et,index:0,coordinate:o},{type:et,index:-1,coordinate:o}]:[{type:et,index:this.editedFeatureCoordinateIndex,coordinate:o}]:"line"===this.editedSnapType&&void 0===this.editedInsertIndex&&(this.editedInsertIndex=this.editedFeatureCoordinateIndex+1,r=[{type:Q,index:this.editedInsertIndex,coordinate:o}],this.editedFeatureCoordinateIndex++),0!==r.length){var s=this.mutateFeature.updatePolygon({featureId:this.editedFeatureId,coordinateMutations:r,propertyMutations:(i={},i[y.EDITED]=!0,i),context:{updateType:u.Provisional}});s&&(this.showCoordinatePoints&&(this.editedInsertIndex?this.coordinatePoints.createOrUpdate({featureId:this.editedFeatureId,featureCoordinates:s.geometry.coordinates}):this.coordinatePoints.updateOneAtIndex(this.editedFeatureId,this.editedFeatureCoordinateIndex,s.geometry.coordinates[0][this.editedFeatureCoordinateIndex])),this.snapping&&this.snappedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.snappedPointId),this.snappedPointId=void 0),this.editedPointId&&this.mutateFeature.updateGuidancePoints([{featureId:this.editedPointId,coordinate:o}]))}}},i.onDragEnd=function(t,e){var i;if(this.allowPointerEvent(this.pointerEvents.onDragEnd,t)&&void 0!==this.editedFeatureId&&(this.setCursor(this.cursors.dragEnd),this.mutateFeature.updatePolygon({featureId:this.editedFeatureId,propertyMutations:(i={},i[y.EDITED]=!1,i),context:{updateType:u.Finish,action:l}}))){var n=this.editedFeatureId;this.editedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.editedPointId),this.editedPointId=void 0),this.snappedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.snappedPointId),this.snappedPointId=void 0),this.editedFeatureId=void 0,this.editedFeatureCoordinateIndex=void 0,this.editedInsertIndex=void 0,this.editedSnapType=void 0,e(!0),this.onFinish(n,{mode:this.mode,action:l})}},i.cleanUp=function(){var t=this.currentId,e=this.snappedPointId,i=this.editedPointId;this.currentId=void 0,this.snappedPointId=void 0,this.editedPointId=void 0,this.editedFeatureId=void 0,this.editedFeatureCoordinateIndex=void 0,this.editedInsertIndex=void 0,this.editedSnapType=void 0,this.currentCoordinate=0,this.undoRedo.clear(),"drawing"===this.state&&this.setStarted(),t&&this.coordinatePoints.deletePointsByFeatureIds([t]),this.mutateFeature.deleteFeatureIfPresent(t),this.mutateFeature.deleteFeatureIfPresent(i),this.mutateFeature.deleteFeatureIfPresent(e),this.closingPoints.ids.length&&this.closingPoints.delete()},i.styleFeature=function(t){var e=r({},{polygonFillColor:"#3f97e0",polygonOutlineColor:"#3f97e0",polygonOutlineWidth:4,polygonOutlineOpacity:1,polygonFillOpacity:.3,pointColor:"#3f97e0",pointOpacity:1,pointOutlineColor:"#ffffff",pointOutlineOpacity:1,pointOutlineWidth:0,pointWidth:6,lineStringColor:"#3f97e0",lineStringWidth:4,lineStringOpacity:1,zIndex:0,markerUrl:void 0,markerHeight:void 0,markerWidth:void 0,lineStringDash:void 0});if(t.properties.mode===this.mode){if("Polygon"===t.geometry.type)return e.polygonFillColor=this.getHexColorStylingValue(this.styles.fillColor,e.polygonFillColor,t),e.polygonOutlineColor=this.getHexColorStylingValue(this.styles.outlineColor,e.polygonOutlineColor,t),e.polygonOutlineWidth=this.getNumericStylingValue(this.styles.outlineWidth,e.polygonOutlineWidth,t),e.polygonFillOpacity=this.getNumericStylingValue(this.styles.fillOpacity,e.polygonFillOpacity,t),e.polygonOutlineOpacity=this.getNumericStylingValue(this.styles.outlineOpacity,1,t),e.zIndex=v,e;if("Point"===t.geometry.type){var i=t.properties[y.EDITED],n=t.properties[y.COORDINATE_POINT],o=i?"editedPoint":t.properties[y.CLOSING_POINT]?"closingPoint":t.properties[y.SNAPPING_POINT]?"snappingPoint":n?"coordinatePoint":void 0;if(!o)return e;var s={editedPoint:{width:this.styles.editedPointOutlineWidth,color:this.styles.editedPointColor,opacity:this.styles.editedPointOpacity,outlineColor:this.styles.editedPointOutlineColor,outlineWidth:this.styles.editedPointOutlineWidth,outlineOpacity:this.styles.editedPointOutlineOpacity},closingPoint:{width:this.styles.closingPointWidth,color:this.styles.closingPointColor,opacity:this.styles.closingPointOpacity,outlineColor:this.styles.closingPointOutlineColor,outlineWidth:this.styles.closingPointOutlineWidth,outlineOpacity:this.styles.closingPointOutlineOpacity},snappingPoint:{width:this.styles.snappingPointWidth,color:this.styles.snappingPointColor,opacity:this.styles.snappingPointOpacity,outlineColor:this.styles.snappingPointOutlineColor,outlineWidth:this.styles.snappingPointOutlineWidth,outlineOpacity:this.styles.snappingPointOutlineOpacity},coordinatePoint:{width:this.styles.coordinatePointWidth,color:this.styles.coordinatePointColor,opacity:this.styles.coordinatePointOpacity,outlineColor:this.styles.coordinatePointOutlineColor,outlineWidth:this.styles.coordinatePointOutlineWidth,outlineOpacity:this.styles.coordinatePointOutlineOpacity}};return e.pointWidth=this.getNumericStylingValue(s[o].width,e.pointWidth,t),e.pointOpacity=this.getNumericStylingValue(s[o].opacity,1,t),e.pointColor=this.getHexColorStylingValue(s[o].color,e.pointColor,t),e.pointOutlineColor=this.getHexColorStylingValue(s[o].outlineColor,e.pointOutlineColor,t),e.pointOutlineOpacity=this.getNumericStylingValue(s[o].outlineOpacity,1,t),e.pointOutlineWidth=this.getNumericStylingValue(s[o].outlineWidth,2,t),e.zIndex=i?40:n?20:30,e}}return e},i.afterFeatureAdded=function(t){this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:t.id,featureCoordinates:t.geometry.coordinates})},i.afterFeatureUpdated=function(t){this.showCoordinatePoints&&this.coordinatePoints.createOrUpdate({featureId:t.id,featureCoordinates:t.geometry.coordinates}),this.editedFeatureId===t.id&&this.editedPointId&&(this.mutateFeature.deleteFeatureIfPresent(this.editedPointId),this.editedPointId=void 0,this.editedFeatureId=void 0,this.editedFeatureCoordinateIndex=void 0,this.editedSnapType=void 0),this.snappedPointId&&this.lastMouseMoveEvent&&this.updateSnappedCoordinate(this.lastMouseMoveEvent),this.currentId===t.id&&(this.currentCoordinate=0,this.currentId=void 0,this.undoRedo.clear(),this.closingPoints.delete(),"drawing"===this.state&&this.setStarted())},i.validateFeature=function(t){var e=this;return this.validateModeFeature(t,function(t){return q(t,e.coordinatePrecision)})},e}(O),ie={cancel:"Escape",finish:"Enter"},ne={start:"crosshair"},oe=/*#__PURE__*/function(t){function e(e){var i;return(i=t.call(this,e,!0)||this).mode="rectangle",i.startPosition=void 0,i.endPosition=void 0,i.currentRectangleId=void 0,i.keyEvents=ie,i.cursors=ne,i.drawInteraction="click-move",i.drawType=void 0,i.mutateFeature=void 0,i.readFeature=void 0,i.updateOptions(e),i}s(e,t);var i=e.prototype;return i.updateOptions=function(e){t.prototype.updateOptions.call(this,e),null!=e&&e.cursors&&(this.cursors=r({},this.cursors,e.cursors)),null===(null==e?void 0:e.keyEvents)?this.keyEvents={cancel:null,finish:null}:null!=e&&e.keyEvents&&(this.keyEvents=r({},this.keyEvents,e.keyEvents)),null!=e&&e.drawInteraction&&(this.drawInteraction=e.drawInteraction)},i.updateRectangle=function(t,e){var i;if(this.startPosition&&this.currentRectangleId){var n=e===u.Finish;return this.mutateFeature.updatePolygon({featureId:this.currentRectangleId,coordinateMutations:[{type:et,index:1,coordinate:[t[0],this.startPosition[1]]},{type:et,index:2,coordinate:t},{type:et,index:3,coordinate:[this.startPosition[0],t[1]]}],propertyMutations:n?(i={},i[y.CURRENTLY_DRAWING]=void 0,i):{},context:n?{updateType:e,action:h}:{updateType:e}})}},i.close=function(){if(this.currentRectangleId&&this.endPosition&&this.updateRectangle(this.endPosition,u.Finish)){var t=this.currentRectangleId;this.startPosition=void 0,this.currentRectangleId=void 0,this.drawType=void 0,"drawing"===this.state&&this.setStarted(),this.onFinish(t,{mode:this.mode,action:h})}},i.beginDrawing=function(t,e){var i;void 0===e&&(e="click"),this.startPosition=[t.lng,t.lat],this.endPosition=[t.lng,t.lat];var n=this.mutateFeature.createPolygon({coordinates:[[t.lng,t.lat],[t.lng,t.lat],[t.lng,t.lat],[t.lng,t.lat],[t.lng,t.lat]],properties:(i={mode:this.mode},i[y.CURRENTLY_DRAWING]=!0,i)});this.currentRectangleId=n.id,this.drawType=e,this.setDrawing()},i.moveDrawAllowed=function(){return"click-move"===this.drawInteraction||"click-move-or-drag"===this.drawInteraction},i.dragDrawAllowed=function(){return"click-drag"===this.drawInteraction||"click-move-or-drag"===this.drawInteraction},i.start=function(){this.setStarted(),this.setCursor(this.cursors.start)},i.stop=function(){this.cleanUp(),this.setStopped(),this.setCursor("unset")},i.onClick=function(t){this.moveDrawAllowed()&&("right"===t.button&&this.allowPointerEvent(this.pointerEvents.rightClick,t)||"left"===t.button&&this.allowPointerEvent(this.pointerEvents.leftClick,t)||t.isContextMenu&&this.allowPointerEvent(this.pointerEvents.contextMenu,t))&&(this.startPosition?(this.endPosition=[t.lng,t.lat],this.close()):this.beginDrawing(t))},i.onMouseMove=function(t){this.endPosition=[t.lng,t.lat],this.updateRectangle(this.endPosition,u.Provisional)},i.onKeyDown=function(){},i.onKeyUp=function(t){t.key===this.keyEvents.cancel?this.cleanUp():t.key===this.keyEvents.finish&&this.close()},i.onDragStart=function(t,e){"drawing"!==this.state&&this.allowPointerEvent(this.pointerEvents.onDragStart,t)&&this.dragDrawAllowed()&&(this.beginDrawing(t,"drag"),e(!1))},i.onDrag=function(t,e){this.allowPointerEvent(this.pointerEvents.onDrag,t)&&this.dragDrawAllowed()&&"drag"===this.drawType&&(this.endPosition=[t.lng,t.lat],this.updateRectangle(this.endPosition,u.Provisional))},i.onDragEnd=function(t,e){this.allowPointerEvent(this.pointerEvents.onDragEnd,t)&&this.dragDrawAllowed()&&"drag"===this.drawType&&(this.endPosition=[t.lng,t.lat],this.close(),e(!0))},i.cleanUp=function(){var t=this.currentRectangleId;this.startPosition=void 0,this.currentRectangleId=void 0,this.drawType=void 0,"drawing"===this.state&&this.setStarted(),this.mutateFeature.deleteFeatureIfPresent(t)},i.styleFeature=function(t){var e=r({},{polygonFillColor:"#3f97e0",polygonOutlineColor:"#3f97e0",polygonOutlineWidth:4,polygonOutlineOpacity:1,polygonFillOpacity:.3,pointColor:"#3f97e0",pointOpacity:1,pointOutlineColor:"#ffffff",pointOutlineOpacity:1,pointOutlineWidth:0,pointWidth:6,lineStringColor:"#3f97e0",lineStringWidth:4,lineStringOpacity:1,zIndex:0,markerUrl:void 0,markerHeight:void 0,markerWidth:void 0,lineStringDash:void 0});return"Feature"===t.type&&"Polygon"===t.geometry.type&&t.properties.mode===this.mode?(e.polygonFillColor=this.getHexColorStylingValue(this.styles.fillColor,e.polygonFillColor,t),e.polygonOutlineColor=this.getHexColorStylingValue(this.styles.outlineColor,e.polygonOutlineColor,t),e.polygonOutlineOpacity=this.getNumericStylingValue(this.styles.outlineOpacity,1,t),e.polygonOutlineWidth=this.getNumericStylingValue(this.styles.outlineWidth,e.polygonOutlineWidth,t),e.polygonFillOpacity=this.getNumericStylingValue(this.styles.fillOpacity,e.polygonFillOpacity,t),e.zIndex=v,e):e},i.validateFeature=function(t){var e=this;return this.validateModeFeature(t,function(t){return Z(t,e.coordinatePrecision)})},i.afterFeatureUpdated=function(t){this.currentRectangleId===t.id&&(this.startPosition=void 0,this.currentRectangleId=void 0,this.drawType=void 0,"drawing"===this.state&&this.setStarted())},i.registerBehaviors=function(t){this.readFeature=new ht(t),this.mutateFeature=new ot(t,{validate:this.validate})},e}(O),re=/*#__PURE__*/function(t){function e(e){var i;if(!e.modeName)throw new Error("Mode name is required for TerraDrawRenderMode");return(i=t.call(this,e,!0)||this).type=I.Render,i.mode="render",i.updateOptions(e),i}s(e,t);var i=e.prototype;return i.updateOptions=function(e){t.prototype.updateOptions.call(this,e)},i.registerBehaviors=function(t){this.mode=t.mode},i.start=function(){this.setStarted()},i.stop=function(){this.setStopped()},i.onKeyUp=function(){},i.onKeyDown=function(){},i.onClick=function(){},i.onDragStart=function(){},i.onDrag=function(){},i.onDragEnd=function(){},i.onMouseMove=function(){},i.cleanUp=function(){},i.styleFeature=function(t){return{pointColor:this.getHexColorStylingValue(this.styles.pointColor,"#3f97e0",t),pointWidth:this.getNumericStylingValue(this.styles.pointWidth,6,t),pointOpacity:this.getNumericStylingValue(this.styles.pointOpacity,1,t),pointOutlineColor:this.getHexColorStylingValue(this.styles.pointOutlineColor,"#ffffff",t),pointOutlineWidth:this.getNumericStylingValue(this.styles.pointOutlineWidth,0,t),pointOutlineOpacity:this.getNumericStylingValue(this.styles.pointOutlineOpacity,1,t),polygonFillColor:this.getHexColorStylingValue(this.styles.polygonFillColor,"#3f97e0",t),polygonFillOpacity:this.getNumericStylingValue(this.styles.polygonFillOpacity,.3,t),polygonOutlineColor:this.getHexColorStylingValue(this.styles.polygonOutlineColor,"#3f97e0",t),polygonOutlineWidth:this.getNumericStylingValue(this.styles.polygonOutlineWidth,4,t),lineStringWidth:this.getNumericStylingValue(this.styles.lineStringWidth,4,t),lineStringColor:this.getHexColorStylingValue(this.styles.lineStringColor,"#3f97e0",t),lineStringOpacity:this.getNumericStylingValue(this.styles.lineStringOpacity,1,t),zIndex:this.getNumericStylingValue(this.styles.zIndex,0,t),lineStringDash:void 0}},i.validateFeature=function(e){var i=t.prototype.validateFeature.call(this,e);if(i.valid){var n=e,o=qt(n,this.coordinatePrecision).valid||q(n,this.coordinatePrecision).valid||wt(n,this.coordinatePrecision).valid;return o?{valid:!0}:{valid:o,reason:"Feature is not a valid Point, Polygon or LineString feature"}}return i},e}(O);function se(t,e){var i=t,n=e,o=D(i[1]),r=D(n[1]),s=D(n[0]-i[0]);s>Math.PI&&(s-=2*Math.PI),s<-Math.PI&&(s+=2*Math.PI);var a=Math.log(Math.tan(r/2+Math.PI/4)/Math.tan(o/2+Math.PI/4)),d=(b(Math.atan2(s,a))+360)%360;return d>180?-(360-d):d}function ae(t,e,i){var n=e;e<0&&(n=-Math.abs(n));var o=n/E,r=t[0]*Math.PI/180,s=D(t[1]),a=D(i),d=o*Math.cos(a),u=s+d;Math.abs(u)>Math.PI/2&&(u=u>0?Math.PI-u:-Math.PI-u);var h=Math.log(Math.tan(u/2+Math.PI/4)/Math.tan(s/2+Math.PI/4)),l=Math.abs(h)>1e-11?d/h:Math.cos(s),c=[(180*(r+o*Math.sin(a)/l)/Math.PI+540)%360-180,180*u/Math.PI];return c[0]+=c[0]-t[0]>180?-360:t[0]-c[0]>180?360:0,c}function de(t,e,i,n,o){var r=n(t[0],t[1]),s=n(e[0],e[1]),a=o((r.x+s.x)/2,(r.y+s.y)/2),d=a.lat;return[_(a.lng,i),_(d,i)]}function ue(t,e,i){var n=ae(t,1e3*w(t,e)/2,se(t,e));return[_(n[0],i),_(n[1],i)]}function he(t){for(var e=t.featureCoords,i=t.precision,n=t.unproject,o=t.project,r=t.projection,s=[],a=0;a(i=t)[1]!=(o=d[l])[1]>i[1]&&i[0]<(o[0]-n[0])*(i[1]-n[1])/(o[1]-n[1])+n[0]&&(r=!r);return r}var ge=function(t,e,i){var n=function(t){return t*t},o=function(t,e){return n(t.x-e.x)+n(t.y-e.y)};return Math.sqrt(function(t,e,i){var n=o(e,i);if(0===n)return o(t,e);var r=((t.x-e.x)*(i.x-e.x)+(t.y-e.y)*(i.y-e.y))/n;return r=Math.max(0,Math.min(1,r)),o(t,{x:e.x+r*(i.x-e.x),y:e.y+r*(i.y-e.y)})}(t,e,i))},fe=/*#__PURE__*/function(t){function e(e,i,n){var o;return(o=t.call(this,e)||this).config=void 0,o.createClickBoundingBox=void 0,o.pixelDistance=void 0,o.config=e,o.createClickBoundingBox=i,o.pixelDistance=n,o}return s(e,t),e.prototype.find=function(t,e){for(var i=void 0,n=Infinity,o=void 0,r=Infinity,s=void 0,a=this.createClickBoundingBox.create(t),d=this.store.search(a),u=0;u180||a<-180||d>90||d<-90)return!1;n[r]=[a,d]}"Polygon"===e.type&&(n[n.length-1]=[n[0][0],n[0][1]]);var y=this.draggedFeatureId,v=null;if("Polygon"===e.type)v=this.mutateFeature.updatePolygon({featureId:y,coordinateMutations:{type:nt,coordinates:[n]},context:{updateType:u.Provisional}});else{if("LineString"!==e.type)return;v=this.mutateFeature.updateLineString({featureId:y,coordinateMutations:{type:nt,coordinates:n},context:{updateType:u.Provisional}})}if(!v)return!1;var m=v.geometry.coordinates;this.midPoints.updateAllInPlace({featureCoordinates:m}),this.selectionPoints.updateAllInPlace({featureCoordinates:m}),this.coordinatePoints.updateAllInPlace({featureId:y,featureCoordinates:m}),this.dragPosition=[t.lng,t.lat]}else"Point"===e.type&&(this.mutateFeature.updatePoint({featureId:this.draggedFeatureId,coordinateMutations:{type:nt,coordinates:i},context:{updateType:u.Provisional}}),this.dragPosition=[t.lng,t.lat])}},e}(J),ve=/*#__PURE__*/function(t){function e(e,i,n,o,r,s,a,d,u){var h;return(h=t.call(this,e)||this).config=void 0,h.pixelDistance=void 0,h.selectionPoints=void 0,h.midPoints=void 0,h.coordinatePoints=void 0,h.coordinateSnapping=void 0,h.lineSnapping=void 0,h.readFeature=void 0,h.mutateFeature=void 0,h.featureSnapping=void 0,h.draggedCoordinate={id:null,index:-1},h.config=e,h.pixelDistance=i,h.selectionPoints=n,h.midPoints=o,h.coordinatePoints=r,h.coordinateSnapping=s,h.lineSnapping=a,h.readFeature=d,h.mutateFeature=u,h.featureSnapping=new Tt(h.coordinateSnapping,h.lineSnapping),h}s(e,t);var i=e.prototype;return i.getClosestCoordinate=function(t,e){var i,n={dist:Infinity,index:-1,isFirstOrLastPolygonCoord:!1};if("LineString"===e.type)i=e.coordinates;else{if("Polygon"!==e.type)return n;i=e.coordinates[0]}for(var o=0;o180||t.lng<-180||t.lat>90||t.lat<-90)return!1;if(d){var l=a.length-1;a[0]=h,a[l]=h}else a[o]=h;if("Point"!==r.type&&!e&&B({type:"Feature",geometry:r,properties:{}}))return!1;var c=n,p=null;return"Polygon"===r.type?p=this.mutateFeature.updatePolygon({featureId:c,coordinateMutations:{type:nt,coordinates:[a]},context:{updateType:u.Provisional}}):"LineString"===r.type&&(p=this.mutateFeature.updateLineString({featureId:c,coordinateMutations:{type:nt,coordinates:a},context:{updateType:u.Provisional}})),!!p&&(this.midPoints.updateOneAtIndex(o>0?o-1:-1,a),this.midPoints.updateOneAtIndex(o,a),this.selectionPoints.updateOneAtIndex(o,h),this.coordinatePoints.updateOneAtIndex(c,o,h),!0)},i.isDragging=function(){return null!==this.draggedCoordinate.id},i.startDragging=function(t,e){this.draggedCoordinate={id:t,index:e}},i.stopDragging=function(){this.draggedCoordinate={id:null,index:-1}},e}(J);function me(t){var e=0,i=0,n=0;return("Polygon"===t.geometry.type?t.geometry.coordinates[0].slice(0,-1):t.geometry.coordinates).forEach(function(t){e+=t[0],i+=t[1],n++},!0),[e/n,i/n]}var Ce=function(t,e){if(0===e||360===e||-360===e)return t;var i=.017453292519943295*e,n=("Polygon"===t.geometry.type?t.geometry.coordinates[0]:t.geometry.coordinates).map(function(t){return L(t[0],t[1])}),o=n.reduce(function(t,e){return{x:t.x+e.x,y:t.y+e.y}},{x:0,y:0});o.x/=n.length,o.y/=n.length;var r=n.map(function(t){return{x:o.x+(t.x-o.x)*Math.cos(i)-(t.y-o.y)*Math.sin(i),y:o.y+(t.x-o.x)*Math.sin(i)+(t.y-o.y)*Math.cos(i)}}).map(function(t){var e=t.x,i=t.y;return[W(e,i).lng,W(e,i).lat]});return"Polygon"===t.geometry.type?t.geometry.coordinates[0]=r:t.geometry.coordinates=r,t};function Pe(t){var e=("Polygon"===t.geometry.type?t.geometry.coordinates[0]:t.geometry.coordinates).map(function(t){var e=L(t[0],t[1]);return[e.x,e.y]});return"Polygon"===t.geometry.type?function(t){for(var e=0,i=0,n=0,o=t.length,r=0;r180?-360:e[0]-t[0]>180?360:0;var i=E,n=e[1]*Math.PI/180,o=t[1]*Math.PI/180,r=o-n,s=Math.abs(t[0]-e[0])*Math.PI/180;s>Math.PI&&(s-=2*Math.PI);var a=Math.log(Math.tan(o/2+Math.PI/4)/Math.tan(n/2+Math.PI/4)),d=Math.abs(a)>1e-11?r/a:Math.cos(n);return Math.sqrt(r*r+d*d*s*s)*i}(i,t),r=ae(i,o,n);t[0]=r[0],t[1]=r[1]})}(s,-(this.lastBearing-(o+180)))}var d="Polygon"===n.type?n.coordinates[0]:n.coordinates;d.forEach(function(t){t[0]=_(t[0],i.coordinatePrecision),t[1]=_(t[1],i.coordinatePrecision)});var h={featureId:e,coordinateMutations:{type:nt,coordinates:"Polygon"===n.type?[d]:d},context:{updateType:u.Provisional}},l=null;if("Polygon"===s.geometry.type)l=this.mutateFeature.updatePolygon(h);else{if("LineString"!==s.geometry.type)return;l=this.mutateFeature.updateLineString(h)}if(!l)return!1;var c=l.geometry.coordinates;this.midPoints.updateAllInPlace({featureCoordinates:c}),this.selectionPoints.updateAllInPlace({featureCoordinates:c}),this.coordinatePoints.updateAllInPlace({featureId:e,featureCoordinates:c}),"web-mercator"===this.projection?this.lastBearing=o:"globe"===this.projection&&(this.lastBearing=o+180)}},e}(J),Se=/*#__PURE__*/function(t){function e(e,i){var n;return(n=t.call(this,e)||this).config=void 0,n.dragCoordinateResizeBehavior=void 0,n.config=e,n.dragCoordinateResizeBehavior=i,n}s(e,t);var i=e.prototype;return i.scale=function(t,e){if(!this.dragCoordinateResizeBehavior.isDragging()){var i=this.dragCoordinateResizeBehavior.getDraggableIndex(t,e);this.dragCoordinateResizeBehavior.startDragging(e,i)}this.dragCoordinateResizeBehavior.drag(t,"center-fixed")},i.reset=function(){this.dragCoordinateResizeBehavior.stopDragging()},e}(J);function Fe(t){var e=t.originX,i=t.originY,n=t.xScale,o=t.yScale;1===n&&1===o||t.coordinates.forEach(function(t){var r=L(t[0],t[1]),s=W(e+(r.x-e)*n,i+(r.y-i)*o),a=s.lat;t[0]=s.lng,t[1]=a})}var xe=/*#__PURE__*/function(t){function e(e,i,n,o,r,s,a){var d;return(d=t.call(this,e)||this).config=void 0,d.pixelDistance=void 0,d.selectionPoints=void 0,d.midPoints=void 0,d.coordinatePoints=void 0,d.readFeature=void 0,d.mutateFeature=void 0,d.minimumScale=1e-4,d.draggedCoordinate={id:null,index:-1},d.boundingBoxMaps={opposite:{0:4,1:5,2:6,3:7,4:0,5:1,6:2,7:3}},d.config=e,d.pixelDistance=i,d.selectionPoints=n,d.midPoints=o,d.coordinatePoints=r,d.readFeature=s,d.mutateFeature=a,d}s(e,t);var i=e.prototype;return i.getClosestCoordinate=function(t,e){var i,n={dist:Infinity,index:-1,isFirstOrLastPolygonCoord:!1};if("LineString"===e.type)i=e.coordinates;else{if("Polygon"!==e.type)return n;i=e.coordinates[0]}for(var o=0;o=0)return!1;break;case 1:if(i>=0)return!1;break;case 2:if(e>=0||i>=0)return!1;break;case 3:if(e>=0)return!1;break;case 4:if(e>=0||i<=0)return!1;break;case 5:if(i<=0)return!1;break;case 6:if(e<=0||i<=0)return!1;break;case 7:if(e<=0)return!1}return!0},i.getSelectedFeatureDataWebMercator=function(){if(!this.draggedCoordinate.id||-1===this.draggedCoordinate.index)return null;var t=this.getFeature(this.draggedCoordinate.id);if(!t)return null;var e=this.getNormalisedCoordinates(t.geometry);return{boundingBox:this.getBBoxWebMercator(e),feature:t,updatedCoords:e,selectedCoordinate:e[this.draggedCoordinate.index]}},i.centerWebMercatorDrag=function(t){var e=this.getSelectedFeatureDataWebMercator();if(!e)return null;var i=e.boundingBox,n=e.updatedCoords,o=e.selectedCoordinate,r=Pe(e.feature);if(!r)return null;var s=L(o[0],o[1]),a=this.getIndexesWebMercator(i,s).closestBBoxIndex,d=L(t.lng,t.lat);return this.scaleWebMercator({closestBBoxIndex:a,updatedCoords:n,webMercatorCursor:d,webMercatorSelected:s,webMercatorOrigin:r}),n},i.centerFixedWebMercatorDrag=function(t){var e=this.getSelectedFeatureDataWebMercator();if(!e)return null;var i=e.boundingBox,n=e.updatedCoords,o=e.selectedCoordinate,r=Pe(e.feature);if(!r)return null;var s=L(o[0],o[1]),a=this.getIndexesWebMercator(i,s).closestBBoxIndex,d=L(t.lng,t.lat);return this.scaleFixedWebMercator({closestBBoxIndex:a,updatedCoords:n,webMercatorCursor:d,webMercatorSelected:s,webMercatorOrigin:r}),n},i.scaleFixedWebMercator=function(t){var e=t.webMercatorOrigin,i=t.webMercatorSelected,n=t.webMercatorCursor,o=t.updatedCoords;if(!this.isValidDragWebMercator(t.closestBBoxIndex,e.x-n.x,e.y-n.y))return null;var r=dt(e,n)/dt(e,i);return r<0&&(r=this.minimumScale),Fe({coordinates:o,originX:e.x,originY:e.y,xScale:r,yScale:r}),o},i.oppositeFixedWebMercatorDrag=function(t){var e=this.getSelectedFeatureDataWebMercator();if(!e)return null;var i=e.boundingBox,n=e.updatedCoords,o=e.selectedCoordinate,r=L(o[0],o[1]),s=this.getIndexesWebMercator(i,r),a=s.oppositeBboxIndex,d=s.closestBBoxIndex,u={x:i[a][0],y:i[a][1]},h=L(t.lng,t.lat);return this.scaleFixedWebMercator({closestBBoxIndex:d,updatedCoords:n,webMercatorCursor:h,webMercatorSelected:r,webMercatorOrigin:u}),n},i.oppositeWebMercatorDrag=function(t){var e=this.getSelectedFeatureDataWebMercator();if(!e)return null;var i=e.boundingBox,n=e.updatedCoords,o=e.selectedCoordinate,r=L(o[0],o[1]),s=this.getIndexesWebMercator(i,r),a=s.oppositeBboxIndex,d=s.closestBBoxIndex,u={x:i[a][0],y:i[a][1]},h=L(t.lng,t.lat);return this.scaleWebMercator({closestBBoxIndex:d,updatedCoords:n,webMercatorCursor:h,webMercatorSelected:r,webMercatorOrigin:u}),n},i.scaleWebMercator=function(t){var e=t.closestBBoxIndex,i=t.webMercatorOrigin,n=t.webMercatorSelected,o=t.webMercatorCursor,r=t.updatedCoords,s=i.x-o.x,a=i.y-o.y;if(!this.isValidDragWebMercator(e,s,a))return null;var d=1;0!==s&&1!==e&&5!==e&&(d=1-(i.x-n.x-s)/s);var u=1;return 0!==a&&3!==e&&7!==e&&(u=1-(i.y-n.y-a)/a),this.validateScale(d,u)?(d<0&&(d=this.minimumScale),u<0&&(u=this.minimumScale),this.performWebMercatorScale(r,i.x,i.y,d,u),r):null},i.getFeature=function(t){if(null===this.draggedCoordinate.id)return null;var e=this.readFeature.getGeometry(t);return"Polygon"!==e.type&&"LineString"!==e.type?null:{id:t,type:"Feature",geometry:e,properties:{}}},i.getNormalisedCoordinates=function(t){return"Polygon"===t.type?t.coordinates[0]:t.coordinates},i.validateScale=function(t,e){var i=!isNaN(t)&&ee[2]&&(e[2]=i),n>e[3]&&(e[3]=n)});var i=e[0],n=e[1],o=e[2],r=e[3];return[[i,r],[(i+o)/2,r],[o,r],[o,r+(n-r)/2],[o,n],[(i+o)/2,n],[i,n],[i,r+(n-r)/2]]},i.getIndexesWebMercator=function(t,e){for(var i,n=Infinity,o=0;o0).clickedFeature,i=this.midPoints.getNearestMidPoint(t),n=this.selected[0];if(n){var o,r=this.getSelectedFlags(n).featureFlags;if(null!=r&&null!=(o=r.coordinates)&&o.midpoints&&i){if(r.coordinates.draggable){var s=this.pixelDistance.measure(t,this.readFeature.getGeometry(i).coordinates),a=this.dragCoordinate.getDraggable(t,n).dist;if(void 0!==a&&s>a)return}return this.midPoints.insert({featureId:n,midPointId:i}),void this.onFinish(this.selected[0],{action:p,mode:this.mode})}}if(null!=e&&e.id)this.allowManualSelection&&this.select(e.id,!0);else if(this.selected.length&&this.allowManualDeselection)return void this.deselect(this.selected[0])},i.start=function(){this.setStarted(),this.setSelecting()},i.stop=function(){this.cleanUp(),this.setStarted(),this.setStopped()},i.onClick=function(t){"right"===t.button&&this.allowPointerEvent(this.pointerEvents.rightClick,t)||t.isContextMenu&&this.allowPointerEvent(this.pointerEvents.contextMenu,t)?this.onRightClick(t):"left"===t.button&&this.allowPointerEvent(this.pointerEvents.leftClick,t)&&this.onLeftClick(t)},i.canScale=function(t){return this.keyEvents.scale&&this.keyEvents.scale.every(function(e){return t.heldKeys.includes(e)})},i.canRotate=function(t){return this.keyEvents.rotate&&this.keyEvents.rotate.every(function(e){return t.heldKeys.includes(e)})},i.preventDefaultKeyEvent=function(t){var e=this.canRotate(t),i=this.canScale(t);(e||i)&&t.preventDefault()},i.onKeyDown=function(t){this.preventDefaultKeyEvent(t)},i.onKeyUp=function(t){if(this.preventDefaultKeyEvent(t),this.keyEvents.delete&&t.key===this.keyEvents.delete){if(!this.selected.length)return;var e=this.selected[0];this.onDeselect(this.selected[0]),this.coordinatePoints.deletePointsByFeatureIds([e]),this.deleteSelected(),this.selectionPoints.delete(),this.midPoints.delete()}else this.keyEvents.deselect&&t.key===this.keyEvents.deselect&&this.cleanUp()},i.cleanUp=function(){this.selected.length&&this.deselect(this.selected[0])},i.onDragStart=function(t,e){if(this.allowPointerEvent(this.pointerEvents.onDragStart,t)){var i=this.selected[0];if(i){var n=this.getSelectedFlags(i),o=n.featureFlags,r=n.coordinatesFlags;if(n.hasDraggableFlags){this.dragEventCount=0;var s="none"!==this.dragTarget.type&&this.dragTarget.featureId===i?this.dragTarget:{type:"none"},a="coordinate"===s.type?s.coordinateIndex:this.dragCoordinate.getDraggableIndex(t,i),d="resize"===s.type?s.coordinateIndex:this.dragCoordinateResizeFeature.getDraggableIndex(t,i),u=(null==r?void 0:r.resizable)&&-1!==d,h=(null==r?void 0:r.draggable)&&-1!==a,l=r&&"object"==typeof r.midpoints&&r.midpoints.draggable,c=(null==o?void 0:o.draggable)&&("feature"===s.type||this.dragFeature.canDrag(t,i));if(u)return this.setCursor(this.cursors.dragStart),this.dragCoordinateResizeFeature.startDragging(i,d),void e(!1);if(h)return this.setCursor(this.cursors.dragStart),this.dragCoordinate.startDragging(i,a),void e(!1);if(l){var g="midpoint"===s.type?s.midPointId:this.midPoints.getNearestMidPoint(t);if(this.selected.length&&g){this.midPoints.insert({featureId:i,midPointId:g}),this.onFinish(this.selected[0],{action:p,mode:this.mode});var f=this.dragCoordinate.getDraggableIndex(t,i);return this.dragCoordinate.startDragging(i,f),void e(!1)}}if(c)return this.setCursor(this.cursors.dragStart),this.dragFeature.startDragging(t,i),void e(!1);this.setCursor("unset")}}}},i.onDrag=function(t,e){if(this.allowPointerEvent(this.pointerEvents.onDrag,t)){var i=this.selected[0];if(i){var n=this.readFeature.getProperties(i),o=this.flags[n.mode],r=!0===(o&&o.feature&&o.feature.selfIntersectable);if(this.dragEventCount++,this.dragEventCount%this.dragEventThrottle!=0){if(o&&o.feature&&o.feature.rotateable&&this.canRotate(t))return e(!1),void this.rotateFeature.rotate(t,i);if(o&&o.feature&&o.feature.scaleable&&this.canScale(t))return e(!1),void this.scaleFeature.scale(t,i);if(this.dragCoordinateResizeFeature.isDragging()&&o.feature&&o.feature.coordinates&&o.feature.coordinates.resizable){if("globe"===this.projection)throw new Error("Globe is currently unsupported projection for resizable");return e(!1),void this.dragCoordinateResizeFeature.drag(t,o.feature.coordinates.resizable)}if(this.dragCoordinate.isDragging()){var s,a=null==(s=o.feature)||null==(s=s.coordinates)?void 0:s.snappable,d={toCoordinate:!1};return!0===a?d={toCoordinate:!0}:"object"==typeof a&&(d=a),void this.dragCoordinate.drag(t,r,d)}this.dragFeature.isDragging()?this.dragFeature.drag(t):e(!0)}}}},i.onDragEnd=function(t,e){this.allowPointerEvent(this.pointerEvents.onDragEnd,t)&&(this.setCursor(this.cursors.dragEnd),this.dragCoordinate.isDragging()?this.onFinish(this.selected[0],{mode:this.mode,action:"dragCoordinate"}):this.dragFeature.isDragging()?this.onFinish(this.selected[0],{mode:this.mode,action:"dragFeature"}):this.dragCoordinateResizeFeature.isDragging()&&this.onFinish(this.selected[0],{mode:this.mode,action:"dragCoordinateResize"}),this.dragCoordinate.stopDragging(),this.dragFeature.stopDragging(),this.dragCoordinateResizeFeature.stopDragging(),this.rotateFeature.reset(),this.scaleFeature.reset(),e(!0))},i.onMouseMove=function(t){var e=this.selected[0];if(e){if(!(this.dragFeature.isDragging()||this.dragCoordinate.isDragging()||this.dragCoordinateResizeFeature.isDragging())){var i=this.getSelectedFlags(e).featureFlags;if(i){var n=void 0,o=i.coordinates;if(null!=o&&o.midpoints&&(n=this.midPoints.getNearestMidPoint(t))&&(this.dragTarget={type:"midpoint",featureId:e,midPointId:n},this.setCursor(this.cursors.insertMidpoint)),o&&o.draggable){var r=this.dragCoordinate.getDraggable(t,e),s=r.index,a=r.dist;if(s>-1){if(n&&this.pixelDistance.measure(t,this.readFeature.getGeometry(n).coordinates)-1)return this.dragTarget={type:"resize",featureId:e,coordinateIndex:d},void this.setCursor(this.getPointerOverResizeHandleCursor())}if(i.draggable&&this.dragFeature.canDrag(t,e)){if(n)return;return this.dragTarget={type:"feature",featureId:e},void this.setCursor(this.getPointerOverFeatureCursor())}n||this.clearDragTargetAndCursor()}else this.clearDragTargetAndCursor()}}else this.clearDragTargetAndCursor()},i.styleFeature=function(t){var e=r({},{polygonFillColor:"#3f97e0",polygonOutlineColor:"#3f97e0",polygonOutlineWidth:4,polygonOutlineOpacity:1,polygonFillOpacity:.3,pointColor:"#3f97e0",pointOpacity:1,pointOutlineColor:"#ffffff",pointOutlineOpacity:1,pointOutlineWidth:0,pointWidth:6,lineStringColor:"#3f97e0",lineStringWidth:4,lineStringOpacity:1,zIndex:0,markerUrl:void 0,markerHeight:void 0,markerWidth:void 0,lineStringDash:void 0});if(t.properties.mode===this.mode&&"Point"===t.geometry.type){if(t.properties[f.SELECTION_POINT])return e.pointColor=this.getHexColorStylingValue(this.styles.selectionPointColor,e.pointColor,t),e.pointOpacity=this.getNumericStylingValue(this.styles.selectionPointOpacity,1,t),e.pointOutlineColor=this.getHexColorStylingValue(this.styles.selectionPointOutlineColor,e.pointOutlineColor,t),e.pointWidth=this.getNumericStylingValue(this.styles.selectionPointWidth,e.pointWidth,t),e.pointOutlineOpacity=this.getNumericStylingValue(this.styles.selectionPointOutlineOpacity,1,t),e.pointOutlineWidth=this.getNumericStylingValue(this.styles.selectionPointOutlineWidth,2,t),e.zIndex=30,e;if(t.properties[f.MID_POINT])return e.pointColor=this.getHexColorStylingValue(this.styles.midPointColor,e.pointColor,t),e.pointOpacity=this.getNumericStylingValue(this.styles.midPointOpacity,1,t),e.pointOutlineColor=this.getHexColorStylingValue(this.styles.midPointOutlineColor,e.pointOutlineColor,t),e.pointWidth=this.getNumericStylingValue(this.styles.midPointWidth,4,t),e.pointOutlineOpacity=this.getNumericStylingValue(this.styles.midPointOutlineOpacity,1,t),e.pointOutlineWidth=this.getNumericStylingValue(this.styles.midPointOutlineWidth,2,t),e.zIndex=50,e}else if(t.properties[f.SELECTED]){if("Point"===t.geometry.type&&t.properties[y.MARKER])return e.markerUrl=this.getUrlStylingValue(this.styles.selectedMarkerUrl,g,t),e.markerHeight=this.getNumericStylingValue(this.styles.selectedMarkerHeight,40,t),e.markerWidth=this.getNumericStylingValue(this.styles.selectedMarkerWidth,32,t),e;if("Polygon"===t.geometry.type)return e.polygonFillColor=this.getHexColorStylingValue(this.styles.selectedPolygonColor,e.polygonFillColor,t),e.polygonOutlineWidth=this.getNumericStylingValue(this.styles.selectedPolygonOutlineWidth,e.polygonOutlineWidth,t),e.polygonOutlineColor=this.getHexColorStylingValue(this.styles.selectedPolygonOutlineColor,e.polygonOutlineColor,t),e.polygonOutlineOpacity=this.getNumericStylingValue(this.styles.selectedPolygonOutlineOpacity,1,t),e.polygonFillOpacity=this.getNumericStylingValue(this.styles.selectedPolygonFillOpacity,e.polygonFillOpacity,t),e.zIndex=v,e;if("LineString"===t.geometry.type)return e.lineStringColor=this.getHexColorStylingValue(this.styles.selectedLineStringColor,e.lineStringColor,t),e.lineStringWidth=this.getNumericStylingValue(this.styles.selectedLineStringWidth,e.lineStringWidth,t),e.lineStringOpacity=this.getNumericStylingValue(this.styles.selectedLineStringOpacity,1,t),e.lineStringDash=this.getDashArrayStylingValue(this.styles.selectedLineStringDash,void 0,t),e.zIndex=v,e;if("Point"===t.geometry.type)return e.pointWidth=this.getNumericStylingValue(this.styles.selectedPointWidth,e.pointWidth,t),e.pointColor=this.getHexColorStylingValue(this.styles.selectedPointColor,e.pointColor,t),e.pointOpacity=this.getNumericStylingValue(this.styles.selectedPointOpacity,1,t),e.pointOutlineColor=this.getHexColorStylingValue(this.styles.selectedPointOutlineColor,e.pointOutlineColor,t),e.pointOutlineOpacity=this.getNumericStylingValue(this.styles.selectedPointOutlineOpacity,1,t),e.pointOutlineWidth=this.getNumericStylingValue(this.styles.selectedPointOutlineWidth,e.pointOutlineWidth,t),e.zIndex=v,e}return e},i.afterFeatureUpdated=function(t){if(this.selected.length&&t.id===this.selected[0]){var e,i,n=this.flags[t.properties.mode];if(null==n||null==(e=n.feature)||!e.coordinates)return;var o=t.geometry.type,r=t.id;if(this.selectionPoints.delete(),this.midPoints.delete(),"LineString"!==o&&"Polygon"!==o)return;var s=t.geometry.coordinates;this.selectionPoints.create({featureCoordinates:s,featureId:r}),null!=n&&null!=(i=n.feature)&&null!=(i=i.coordinates)&&i.midpoints&&this.midPoints.create({featureCoordinates:s,featureId:r})}},e}(M),Ee=/*#__PURE__*/function(t){function e(){for(var e,i=arguments.length,n=new Array(i),o=0;oi;){if(n-i>600){var r=n-i+1,s=e-i+1,a=Math.log(r),d=.5*Math.exp(2*a/3),u=.5*Math.sqrt(a*d*(r-d)/r)*(s-r/2<0?-1:1);De(t,e,Math.max(i,Math.floor(e-s*d/r+u)),Math.min(n,Math.floor(e+(r-s)*d/r+u)),o)}var h=t[e],l=i,c=n;for(ke(t,i,e),o(t[n],h)>0&&ke(t,i,n);l0;)c--}0===o(t[i],h)?ke(t,i,c):ke(t,++c,n),c<=e&&(i=c+1),e<=c&&(n=c-1)}}function ke(t,e,i){var n=t[e];t[e]=t[i],t[i]=n}function be(t,e){_e(t,0,t.children.length,e,t)}function _e(t,e,i,n,o){o||(o=Be([])),o.minX=Infinity,o.minY=Infinity,o.maxX=-Infinity,o.maxY=-Infinity;for(var r=e;r=t.minX&&e.maxY>=t.minY}function Be(t){return{children:t,height:1,leaf:!0,minX:Infinity,minY:Infinity,maxX:-Infinity,maxY:-Infinity}}function ze(t,e,i,n,o){for(var r=[e,i];r.length;)if(!((i=r.pop())-(e=r.pop())<=n)){var s=e+Math.ceil((i-e)/n/2)*n;De(t,s,e,i,o),r.push(e,s,s,i)}}var He=/*#__PURE__*/function(){function t(t){this._maxEntries=void 0,this._minEntries=void 0,this.data=void 0,this._maxEntries=Math.max(4,t),this._minEntries=Math.max(2,Math.ceil(.4*this._maxEntries)),this.clear()}var e=t.prototype;return e.search=function(t){var e=this.data,i=[];if(!Ae(t,e))return i;for(var n=this.toBBox,o=[];e;){for(var r=0;r=0&&o[e].children.length>this._maxEntries;)this._split(o,e),e--;this._adjustParentBBoxes(n,o,e)},e._split=function(t,e){var i=t[e],n=i.children.length,o=this._minEntries;this._chooseSplitAxis(i,o,n);var r=this._chooseSplitIndex(i,o,n),s=Be(i.children.splice(r,i.children.length-r));s.height=i.height,s.leaf=i.leaf,be(i,this.toBBox),be(s,this.toBBox),e?t[e-1].children.push(s):this._splitRoot(i,s)},e._splitRoot=function(t,e){this.data=Be([t,e]),this.data.height=t.height+1,this.data.leaf=!1,be(this.data,this.toBBox)},e._chooseSplitIndex=function(t,e,i){for(var n,o,r,s,a,d,u,h=Infinity,l=Infinity,c=e;c<=i-e;c++){var p=_e(t,0,c,this.toBBox),g=_e(t,c,i,this.toBBox),f=(o=p,r=g,s=Math.max(o.minX,r.minX),a=Math.max(o.minY,r.minY),d=Math.min(o.maxX,r.maxX),u=Math.min(o.maxY,r.maxY),Math.max(0,d-s)*Math.max(0,u-a)),y=Le(p)+Le(g);f=e;h--){var l=t.children[h];Te(s,t.leaf?o(l):l),a+=We(s)}return a},e._adjustParentBBoxes=function(t,e,i){for(var n=i;n>=0;n--)Te(e[n],t)},e._condense=function(t){for(var e,i=t.length-1;i>=0;i--)0===t[i].children.length?i>0?(e=t[i-1].children).splice(e.indexOf(t[i]),1):this.clear():be(t[i],this.toBBox)},t}(),Ve=/*#__PURE__*/function(){function t(t){this.tree=void 0,this.idToNode=void 0,this.nodeToId=void 0,this.tree=new He(t&&t.maxEntries?t.maxEntries:9),this.idToNode=new Map,this.nodeToId=new Map}var e=t.prototype;return e.setMaps=function(t,e){this.idToNode.set(t.id,e),this.nodeToId.set(e,t.id)},e.toBBox=function(t){var e,i=[],n=[];if("Polygon"===t.geometry.type)e=t.geometry.coordinates[0];else if("LineString"===t.geometry.type)e=t.geometry.coordinates;else{if("Point"!==t.geometry.type)throw new Error("Not a valid feature to turn into a bounding box");e=[t.geometry.coordinates]}for(var o=0;o0&&(this._onChange(d,"create",n),i&&a.forEach(function(t){i(t)})),s},e.search=function(t,e){var i=this,n=this.spatialIndex.search(t).map(function(t){return i.store[t]});return this.clone(e?n.filter(e):n)},e.registerOnChange=function(t){this._onChange=function(e,i,n){t(e,i,n)}},e.getGeometryCopy=function(t){var e=this.store[t];if(!e)throw new Error("No feature with this id ("+t+"), can not get geometry copy");return this.clone(e.geometry)},e.getPropertiesCopy=function(t){var e=this.store[t];if(!e)throw new Error("No feature with this id ("+t+"), can not get properties copy");return this.clone(e.properties)},e.updateProperty=function(t,e){var i=this,n=new Set;t.forEach(function(t){var e=t.id,o=t.property,r=t.value,s=i.store[e];if(!s)throw new Error("No feature with this ("+e+"), can not update geometry");s.properties[o]!==r&&(n.add(e),void 0===r?delete s.properties[o]:s.properties[o]=r,i.tracked&&(s.properties.updatedAt=+new Date))}),this._onChange&&n.size>0&&this._onChange(Array.from(n),"update",e?r({},e,Ke):Ke)},e.updateGeometry=function(t,e){var i=this,n=new Set;t.forEach(function(t){var e=t.id,o=t.geometry;n.add(e);var r=i.store[e];if(!r)throw new Error("No feature with this ("+e+"), can not update geometry");r.geometry=i.clone(o),i.spatialIndex.update(r),i.tracked&&(r.properties.updatedAt=+new Date)}),this._onChange&&n.size>0&&this._onChange(Array.from(n),"update",e?r({},e,je):je)},e.create=function(t,e){var i=this,n=[];return t.forEach(function(t){var e,o=t.geometry,s=t.properties,a=r({},s);i.tracked&&(e=+new Date,s?(a.createdAt="number"==typeof s.createdAt?s.createdAt:e,a.updatedAt="number"==typeof s.updatedAt?s.updatedAt:e):a={createdAt:e,updatedAt:e});var d=i.getId(),u={id:d,type:"Feature",geometry:o,properties:a};i.store[d]=u,i.spatialIndex.insert(u),n.push(d)}),this._onChange&&this._onChange([].concat(n),"create",e),n},e.delete=function(t,e){var i=this;t.forEach(function(t){if(!i.store[t])throw new Error("No feature with id "+t+", can not delete");delete i.store[t],i.spatialIndex.remove(t)}),this._onChange&&this._onChange([].concat(t),"delete",e)},e.copy=function(t){return this.clone(this.store[t])},e.copyAll=function(){var t=this;return this.clone(Object.keys(this.store).map(function(e){return t.store[e]}))},e.copyAllWhere=function(t){var e=this;return this.clone(Object.keys(this.store).map(function(t){return e.store[t]}).filter(function(e){return e.properties&&t(e.properties)}))},e.clear=function(t){var e=Object.keys(this.store);this.store={},this.spatialIndex.clear(),this._onChange(e,"delete",t)},e.size=function(){return Object.keys(this.store).length},t}();function Xe(t){var e=t.coordinates,i=0;if(e&&e.length>0){i+=Math.abs(Je(e[0]));for(var n=1;n=e?(n+2)%e:n+2][0]*Ze-t[n][0]*Ze)*Math.sin(t[n+1===e?0:n+1][1]*Ze),n++;return i*qe}var $e="Feature is smaller than the minimum area",Qe="Feature is not a Polygon or LineString",ti="Feature intersects itself";function ei(t,e,i){var n=It(t,e),o=It(e,i)-n;return o<0&&(o+=360),180-Math.abs(o-90-90)}var ii={cancel:"Escape",finish:"Enter"},ni={start:"crosshair",close:"pointer"},oi=/*#__PURE__*/function(t){function e(e){var i;return(i=t.call(this,e,!0)||this).mode="angled-rectangle",i.currentCoordinate=0,i.currentId=void 0,i.keyEvents=ii,i.cursors=ni,i.mouseMove=!1,i.mutateFeature=void 0,i.readFeature=void 0,i.updateOptions(e),i}s(e,t);var i=e.prototype;return i.updateOptions=function(e){t.prototype.updateOptions.call(this,e),null!=e&&e.cursors&&(this.cursors=r({},this.cursors,e.cursors)),null===(null==e?void 0:e.keyEvents)?this.keyEvents={cancel:null,finish:null}:null!=e&&e.keyEvents&&(this.keyEvents=r({},this.keyEvents,e.keyEvents))},i.close=function(){var t;if(void 0!==this.currentId&&this.mutateFeature.updatePolygon({featureId:this.currentId,propertyMutations:(t={},t[y.CURRENTLY_DRAWING]=void 0,t),context:{updateType:u.Finish,action:h}})){var e=this.currentId;this.currentCoordinate=0,this.currentId=void 0,"drawing"===this.state&&this.setStarted(),this.onFinish(e,{mode:this.mode,action:h})}},i.start=function(){this.setStarted(),this.setCursor(this.cursors.start)},i.stop=function(){this.cleanUp(),this.setStopped(),this.setCursor("unset")},i.onMouseMove=function(t){if(this.mouseMove=!0,this.setCursor(this.cursors.start),void 0!==this.currentId&&0!==this.currentCoordinate){var e=[];if(1===this.currentCoordinate)e=this.getUpdateForSecondCoordinate(t);else{if(2!==this.currentCoordinate)return;e=this.getNewSecondAndThirdCoordinates(t)}this.mutateFeature.updatePolygon({featureId:this.currentId,coordinateMutations:e,context:{updateType:u.Provisional}})}},i.getUpdateForSecondCoordinate=function(t){return[{type:et,index:1,coordinate:[t.lng,t.lat]},{type:et,index:2,coordinate:[t.lng,t.lat]}]},i.getNewSecondAndThirdCoordinates=function(t){if(!this.currentId)throw new Error("No current feature being drawn");var e,i,n,o,r=this.readFeature.getCoordinate(this.currentId,0),s=this.readFeature.getCoordinate(this.currentId,1),a=de(r,s,this.coordinatePrecision,this.project,this.unproject),d=L(r[0],r[1]),u=L(a[0],a[1]),h=L(s[0],s[1]),l=L(t.lng,t.lat),c=dt(l,d)1e-10?"left":o<-1e-10?"right":"left")?-90:90),m=Ct(d,y,v),C=Ct(h,y,v),P=W(m.x,m.y),I=W(C.x,C.y);return[{type:et,index:2,coordinate:[_(I.lng,this.coordinatePrecision),_(I.lat,this.coordinatePrecision)]},{type:et,index:3,coordinate:[_(P.lng,this.coordinatePrecision),_(P.lat,this.coordinatePrecision)]}]},i.onClick=function(t){if("right"===t.button&&this.allowPointerEvent(this.pointerEvents.rightClick,t)||"left"===t.button&&this.allowPointerEvent(this.pointerEvents.leftClick,t)||t.isContextMenu&&this.allowPointerEvent(this.pointerEvents.contextMenu,t))if(this.currentCoordinate>0&&!this.mouseMove&&this.onMouseMove(t),this.mouseMove=!1,0===this.currentCoordinate){var e,i=this.mutateFeature.createPolygon({coordinates:[[t.lng,t.lat],[t.lng,t.lat],[t.lng,t.lat],[t.lng,t.lat]],properties:(e={mode:this.mode},e[y.CURRENTLY_DRAWING]=!0,e)});this.currentId=i.id,this.currentCoordinate++,this.setDrawing()}else if(1===this.currentCoordinate&&this.currentId){var n=this.readFeature.getCoordinate(this.currentId,0);if(ut([t.lng,t.lat],n))return;if(!this.mutateFeature.updatePolygon({featureId:this.currentId,coordinateMutations:[{type:et,index:1,coordinate:[t.lng,t.lat]},{type:tt,index:1,coordinate:[t.lng,t.lat]}],context:{updateType:u.Commit}}))return;this.currentCoordinate++}else 2===this.currentCoordinate&&this.currentId&&this.close()},i.onKeyUp=function(t){if(t.key===this.keyEvents.cancel)this.cleanUp();else if(t.key===this.keyEvents.finish){if(this.currentCoordinate<2)return void this.cleanUp();this.close()}},i.onKeyDown=function(){},i.onDragStart=function(){},i.onDrag=function(){},i.onDragEnd=function(){},i.cleanUp=function(){var t=this.currentId;this.currentId=void 0,this.currentCoordinate=0,"drawing"===this.state&&this.setStarted(),this.mutateFeature.deleteFeatureIfPresent(t)},i.styleFeature=function(t){var e=r({},{polygonFillColor:"#3f97e0",polygonOutlineColor:"#3f97e0",polygonOutlineWidth:4,polygonOutlineOpacity:1,polygonFillOpacity:.3,pointColor:"#3f97e0",pointOpacity:1,pointOutlineColor:"#ffffff",pointOutlineOpacity:1,pointOutlineWidth:0,pointWidth:6,lineStringColor:"#3f97e0",lineStringWidth:4,lineStringOpacity:1,zIndex:0,markerUrl:void 0,markerHeight:void 0,markerWidth:void 0,lineStringDash:void 0});return t.properties.mode===this.mode&&"Polygon"===t.geometry.type&&(e.polygonFillColor=this.getHexColorStylingValue(this.styles.fillColor,e.polygonFillColor,t),e.polygonOutlineColor=this.getHexColorStylingValue(this.styles.outlineColor,e.polygonOutlineColor,t),e.polygonOutlineWidth=this.getNumericStylingValue(this.styles.outlineWidth,e.polygonOutlineWidth,t),e.polygonOutlineOpacity=this.getNumericStylingValue(this.styles.outlineOpacity,1,t),e.polygonFillOpacity=this.getNumericStylingValue(this.styles.fillOpacity,e.polygonFillOpacity,t),e.zIndex=v),e},i.validateFeature=function(t){var e=this;return this.validateModeFeature(t,function(t){return Z(t,e.coordinatePrecision)})},i.afterFeatureUpdated=function(t){this.currentId===t.id&&(this.currentId=void 0,this.currentCoordinate=0,"drawing"===this.state&&this.setStarted())},i.registerBehaviors=function(t){this.readFeature=new ht(t),this.mutateFeature=new ot(t,{validate:this.validate})},e}(O);function ri(t,e,i){return(e.x-t.x)*(i.y-t.y)-(e.y-t.y)*(i.x-t.x)<=0}var si={cancel:"Escape",finish:"Enter"},ai={start:"crosshair",close:"pointer"},di=/*#__PURE__*/function(t){function e(e){var i;return(i=t.call(this,e,!0)||this).mode="sector",i.currentCoordinate=0,i.currentId=void 0,i.keyEvents=si,i.direction=void 0,i.arcPoints=64,i.cursors=ai,i.mouseMove=!1,i.readFeature=void 0,i.mutateFeature=void 0,i.updateOptions(e),i}s(e,t);var i=e.prototype;return i.updateOptions=function(e){t.prototype.updateOptions.call(this,e),null!=e&&e.cursors&&(this.cursors=r({},this.cursors,e.cursors)),null===(null==e?void 0:e.keyEvents)?this.keyEvents={cancel:null,finish:null}:null!=e&&e.keyEvents&&(this.keyEvents=r({},this.keyEvents,e.keyEvents)),null!=e&&e.arcPoints&&(this.arcPoints=e.arcPoints)},i.close=function(){var t;if(void 0!==this.currentId&&this.mutateFeature.updatePolygon({featureId:this.currentId,propertyMutations:(t={},t[y.CURRENTLY_DRAWING]=void 0,t),coordinateMutations:{coordinates:this.readFeature.getGeometry(this.currentId).coordinates,type:nt},context:{updateType:u.Finish,action:h}})){var e=this.currentId;this.currentCoordinate=0,this.currentId=void 0,this.direction=void 0,"drawing"===this.state&&this.setStarted(),this.onFinish(e,{mode:this.mode,action:h})}},i.getSectorCoordinates=function(t){var e=this.readFeature.getCoordinates(this.currentId),i=e[0],n=e[1],o=[t.lng,t.lat],r=L(i[0],i[1]),s=L(n[0],n[1]),a=L(o[0],o[1]);if(void 0===this.direction){var d=ri(r,s,a);this.direction=d?"clockwise":"anticlockwise"}var u,h=dt(r,s),l=It(r,s),c=It(r,a),p=this.arcPoints,g=[i],f=St(l),y=St(c);"anticlockwise"===this.direction?(u=y-f)<0&&(u+=360):(u=f-y)<0&&(u+=360);var v=("anticlockwise"===this.direction?1:-1)*u/p;g.push(n);for(var m=0;m<=p;m++){var C=Ct(r,h,f+m*v),P=W(C.x,C.y),I=P.lat,S=[_(P.lng,this.coordinatePrecision),_(I,this.coordinatePrecision)];S[0]!==g[g.length-1][0]&&S[1]!==g[g.length-1][1]&&g.push(S)}return g.push(i),g},i.start=function(){this.setStarted(),this.setCursor(this.cursors.start)},i.stop=function(){this.cleanUp(),this.setStopped(),this.setCursor("unset")},i.onMouseMove=function(t){if(this.mouseMove=!0,this.setCursor(this.cursors.start),void 0!==this.currentId&&0!==this.currentCoordinate){var e;if(1===this.currentCoordinate)e=[{type:et,index:1,coordinate:[t.lng,t.lat]},{type:et,index:2,coordinate:[t.lng,t.lat]}];else{if(2!==this.currentCoordinate)return;var i=this.getSectorCoordinates(t);if(!i)return;e={type:nt,coordinates:[i]}}this.mutateFeature.updatePolygon({featureId:this.currentId,coordinateMutations:e,context:{updateType:u.Provisional}})}},i.onClick=function(t){if("right"===t.button&&this.allowPointerEvent(this.pointerEvents.rightClick,t)||"left"===t.button&&this.allowPointerEvent(this.pointerEvents.leftClick,t)||t.isContextMenu&&this.allowPointerEvent(this.pointerEvents.contextMenu,t))if(this.currentCoordinate>0&&!this.mouseMove&&this.onMouseMove(t),this.mouseMove=!1,0===this.currentCoordinate){var e,i=this.mutateFeature.createPolygon({coordinates:[[t.lng,t.lat],[t.lng,t.lat],[t.lng,t.lat],[t.lng,t.lat]],properties:(e={mode:this.mode},e[y.CURRENTLY_DRAWING]=!0,e)});this.currentId=null==i?void 0:i.id,this.currentCoordinate++,this.setDrawing()}else if(1===this.currentCoordinate&&this.currentId){if(this.readFeature.coordinateAtIndexIsIdentical({featureId:this.currentId,index:0,newCoordinate:[t.lng,t.lat]}))return;if(!this.mutateFeature.updatePolygon({featureId:this.currentId,coordinateMutations:[{type:et,index:1,coordinate:[t.lng,t.lat]},{type:et,index:2,coordinate:[t.lng,t.lat]}],context:{updateType:u.Provisional}}))return;this.currentCoordinate++}else 2===this.currentCoordinate&&this.currentId&&this.close()},i.onKeyUp=function(t){t.key===this.keyEvents.cancel?this.cleanUp():t.key===this.keyEvents.finish&&this.close()},i.onKeyDown=function(){},i.onDragStart=function(){},i.onDrag=function(){},i.onDragEnd=function(){},i.cleanUp=function(){var t=this.currentId;this.currentId=void 0,this.direction=void 0,this.currentCoordinate=0,"drawing"===this.state&&this.setStarted(),this.mutateFeature.deleteFeatureIfPresent(t)},i.styleFeature=function(t){var e=r({},{polygonFillColor:"#3f97e0",polygonOutlineColor:"#3f97e0",polygonOutlineWidth:4,polygonOutlineOpacity:1,polygonFillOpacity:.3,pointColor:"#3f97e0",pointOpacity:1,pointOutlineColor:"#ffffff",pointOutlineOpacity:1,pointOutlineWidth:0,pointWidth:6,lineStringColor:"#3f97e0",lineStringWidth:4,lineStringOpacity:1,zIndex:0,markerUrl:void 0,markerHeight:void 0,markerWidth:void 0,lineStringDash:void 0});return t.properties.mode===this.mode&&"Polygon"===t.geometry.type&&(e.polygonFillColor=this.getHexColorStylingValue(this.styles.fillColor,e.polygonFillColor,t),e.polygonOutlineColor=this.getHexColorStylingValue(this.styles.outlineColor,e.polygonOutlineColor,t),e.polygonOutlineWidth=this.getNumericStylingValue(this.styles.outlineWidth,e.polygonOutlineWidth,t),e.polygonOutlineOpacity=this.getNumericStylingValue(this.styles.outlineOpacity,1,t),e.polygonFillOpacity=this.getNumericStylingValue(this.styles.fillOpacity,e.polygonFillOpacity,t),e.zIndex=v),e},i.validateFeature=function(t){var e=this;return this.validateModeFeature(t,function(t){return Z(t,e.coordinatePrecision)})},i.afterFeatureUpdated=function(t){this.currentId===t.id&&(this.currentId=void 0,this.direction=void 0,this.currentCoordinate=0,"drawing"===this.state&&this.setStarted())},i.registerBehaviors=function(t){this.readFeature=new ht(t),this.mutateFeature=new ot(t,{validate:this.validate})},e}(O),ui={cancel:"Escape",finish:"Enter"},hi={start:"crosshair",close:"pointer"},li=/*#__PURE__*/function(t){function e(e){var i;return(i=t.call(this,e,!0)||this).mode="sensor",i.currentCoordinate=0,i.currentId=void 0,i.currentInitialArcId=void 0,i.currentStartingPointId=void 0,i.keyEvents=ui,i.direction=void 0,i.arcPoints=64,i.cursors=hi,i.mouseMove=!1,i.readFeature=void 0,i.mutateFeature=void 0,i.updateOptions(e),i}s(e,t);var i=e.prototype;return i.updateOptions=function(e){t.prototype.updateOptions.call(this,e),null!=e&&e.cursors&&(this.cursors=r({},this.cursors,e.cursors)),null===(null==e?void 0:e.keyEvents)?this.keyEvents={cancel:null,finish:null}:null!=e&&e.keyEvents&&(this.keyEvents=r({},this.keyEvents,e.keyEvents)),null!=e&&e.arcPoints&&(this.arcPoints=e.arcPoints)},i.start=function(){this.setStarted(),this.setCursor(this.cursors.start)},i.stop=function(){this.cleanUp(),this.setStopped(),this.setCursor("unset")},i.onMouseMove=function(t){if(this.mouseMove=!0,this.setCursor(this.cursors.start),void 0!==this.currentInitialArcId&&void 0!==this.currentStartingPointId&&0!==this.currentCoordinate)if(2===this.currentCoordinate){var e=this.getUpdatedLineStringCoordinates(t);if(!e)return;this.mutateFeature.updateLineString({featureId:this.currentInitialArcId,coordinateMutations:{type:nt,coordinates:e},context:{updateType:u.Provisional}})}else if(3===this.currentCoordinate){var i=this.getUpdatedPolygonCoordinates(t);if(!i)return;if(this.currentId)this.mutateFeature.updatePolygon({featureId:this.currentId,coordinateMutations:{type:nt,coordinates:[i]},context:{updateType:u.Provisional}});else{var n,o=this.mutateFeature.createPolygon({coordinates:i,properties:(n={mode:this.mode},n[y.CURRENTLY_DRAWING]=!0,n)});if(!o)return;this.currentId=o.id}}},i.onClick=function(t){if("right"===t.button&&this.allowPointerEvent(this.pointerEvents.rightClick,t)||"left"===t.button&&this.allowPointerEvent(this.pointerEvents.leftClick,t)||t.isContextMenu&&this.allowPointerEvent(this.pointerEvents.contextMenu,t))if(this.currentCoordinate>0&&!this.mouseMove&&this.onMouseMove(t),this.mouseMove=!1,0===this.currentCoordinate){var e=this.mutateFeature.createPoint({coordinates:[t.lng,t.lat],properties:{mode:this.mode}});if(!e)return;this.currentStartingPointId=e.id,this.currentCoordinate++,this.setDrawing()}else if(1===this.currentCoordinate&&this.currentStartingPointId){var i=this.mutateFeature.createLineString({coordinates:[[t.lng,t.lat],[t.lng,t.lat]],properties:{mode:this.mode}});if(!i)return;this.currentInitialArcId=i.id,this.currentCoordinate++}else 2===this.currentCoordinate&&this.currentStartingPointId?this.currentCoordinate++:3===this.currentCoordinate&&this.currentStartingPointId&&this.close()},i.onKeyUp=function(t){t.key===this.keyEvents.cancel?this.cleanUp():t.key===this.keyEvents.finish&&this.close()},i.onKeyDown=function(){},i.onDragStart=function(){},i.onDrag=function(){},i.onDragEnd=function(){},i.cleanUp=function(){this.mutateFeature.deleteFeatureIfPresent(this.currentStartingPointId),this.mutateFeature.deleteFeatureIfPresent(this.currentInitialArcId),this.mutateFeature.deleteFeatureIfPresent(this.currentId),this.currentStartingPointId=void 0,this.direction=void 0,this.currentId=void 0,this.currentCoordinate=0,"drawing"===this.state&&this.setStarted()},i.styleFeature=function(t){var e=r({},{polygonFillColor:"#3f97e0",polygonOutlineColor:"#3f97e0",polygonOutlineWidth:4,polygonOutlineOpacity:1,polygonFillOpacity:.3,pointColor:"#3f97e0",pointOpacity:1,pointOutlineColor:"#ffffff",pointOutlineOpacity:1,pointOutlineWidth:0,pointWidth:6,lineStringColor:"#3f97e0",lineStringWidth:4,lineStringOpacity:1,zIndex:0,markerUrl:void 0,markerHeight:void 0,markerWidth:void 0,lineStringDash:void 0});return t.properties.mode===this.mode&&("Polygon"===t.geometry.type?(e.polygonFillColor=this.getHexColorStylingValue(this.styles.fillColor,e.polygonFillColor,t),e.polygonOutlineColor=this.getHexColorStylingValue(this.styles.outlineColor,e.polygonOutlineColor,t),e.polygonOutlineWidth=this.getNumericStylingValue(this.styles.outlineWidth,e.polygonOutlineWidth,t),e.polygonOutlineOpacity=this.getNumericStylingValue(this.styles.outlineOpacity,1,t),e.polygonFillOpacity=this.getNumericStylingValue(this.styles.fillOpacity,e.polygonFillOpacity,t),e.zIndex=v):"LineString"===t.geometry.type?(e.lineStringColor=this.getHexColorStylingValue(this.styles.outlineColor,e.polygonOutlineColor,t),e.lineStringWidth=this.getNumericStylingValue(this.styles.outlineWidth,e.polygonOutlineWidth,t),e.zIndex=v):"Point"===t.geometry.type&&(e.pointColor=this.getHexColorStylingValue(this.styles.centerPointColor,e.pointColor,t),e.pointOpacity=this.getNumericStylingValue(this.styles.centerPointOpacity,1,t),e.pointWidth=this.getNumericStylingValue(this.styles.centerPointWidth,e.pointWidth,t),e.pointOutlineColor=this.getHexColorStylingValue(this.styles.centerPointOutlineColor,e.pointOutlineColor,t),e.pointOutlineOpacity=this.getNumericStylingValue(this.styles.centerPointOutlineOpacity,1,t),e.pointOutlineWidth=this.getNumericStylingValue(this.styles.centerPointOutlineWidth,e.pointOutlineWidth,t),e.zIndex=20)),e},i.validateFeature=function(t){var e=this;return this.validateModeFeature(t,function(t){return Z(t,e.coordinatePrecision)})},i.afterFeatureUpdated=function(t){this.currentId===t.id&&(this.mutateFeature.deleteFeatureIfPresent(this.currentStartingPointId),this.mutateFeature.deleteFeatureIfPresent(this.currentInitialArcId),this.currentStartingPointId=void 0,this.direction=void 0,this.currentId=void 0,this.currentCoordinate=0,"drawing"===this.state&&this.setStarted())},i.registerBehaviors=function(t){this.readFeature=new ht(t),this.mutateFeature=new ot(t,{validate:this.validate})},i.close=function(){if(void 0!==this.currentStartingPointId){var t,e=this.currentStartingPointId,i=this.currentInitialArcId;if(this.currentId&&!this.mutateFeature.updatePolygon({featureId:this.currentId,propertyMutations:(t={},t[y.CURRENTLY_DRAWING]=void 0,t),coordinateMutations:{coordinates:this.readFeature.getGeometry(this.currentId).coordinates,type:nt},context:{updateType:u.Finish,action:h}}))return;var n=this.currentId;this.mutateFeature.deleteFeatureIfPresent(e),this.mutateFeature.deleteFeatureIfPresent(i),this.currentCoordinate=0,this.currentStartingPointId=void 0,this.currentInitialArcId=void 0,this.currentId=void 0,this.direction=void 0,"drawing"===this.state&&this.setStarted(),n&&this.onFinish(n,{mode:this.mode,action:h})}},i.getUpdatedPolygonCoordinates=function(t){if(!(void 0===this.currentInitialArcId||void 0===this.currentStartingPointId||this.currentCoordinate<3)){var e=this.readFeature.getCoordinates(this.currentInitialArcId);if(!(e.length<2)&&this.direction){var i=this.readFeature.getGeometry(this.currentStartingPointId).coordinates,n=e[0],o=e[e.length-1],r=L(t.lng,t.lat),s=L(n[0],n[1]),a=L(o[0],o[1]),d=L(i[0],i[1]),u=dt(d,s),h=dt(d,r)=i&&e<=n:e>=i||e<=n:i>=n?e<=i&&e>=n:e<=i||e>=n},e}(O),ci=function(t){var e=this,i=t.name,n=t.callback,o=t.unregister,r=t.register;this.name=void 0,this.callback=void 0,this.registered=!1,this.register=void 0,this.unregister=void 0,this.name=i,this.register=function(){e.registered||(e.registered=!0,r(n))},this.unregister=function(){e.register&&(e.registered=!1,o(n))},this.callback=n},pi={__proto__:null,GeoJSONStore:Ye,TerraDrawBaseDrawMode:O,TerraDrawBaseSelectMode:M,TerraDrawBaseAdapter:/*#__PURE__*/function(){function t(t){this._nextKeyUpIsContextMenu=!1,this._lastPointerDownEventTarget=void 0,this._ignoreMismatchedPointerEvents=!1,this._minPixelDragDistance=void 0,this._minPixelDragDistanceDrawing=void 0,this._minPixelDragDistanceSelecting=void 0,this._lastDrawEvent=void 0,this._coordinatePrecision=void 0,this._heldKeys=new Set,this._listeners=[],this._dragState="not-dragging",this._currentModeCallbacks=void 0,this._ignoreMismatchedPointerEvents="boolean"==typeof t.ignoreMismatchedPointerEvents&&t.ignoreMismatchedPointerEvents,this._minPixelDragDistance="number"==typeof t.minPixelDragDistance?t.minPixelDragDistance:1,this._minPixelDragDistanceSelecting="number"==typeof t.minPixelDragDistanceSelecting?t.minPixelDragDistanceSelecting:1,this._minPixelDragDistanceDrawing="number"==typeof t.minPixelDragDistanceDrawing?t.minPixelDragDistanceDrawing:8,this._coordinatePrecision="number"==typeof t.coordinatePrecision?t.coordinatePrecision:9}var e=t.prototype;return e.getButton=function(t){return-1===t.button?"neither":0===t.button?"left":1===t.button?"middle":2===t.button?"right":"neither"},e.getMapElementXYPosition=function(t){var e=this.getMapEventElement(t.type).getBoundingClientRect();return{containerX:t.clientX-e.left,containerY:t.clientY-e.top}},e.getDrawEventFromEvent=function(t,e){void 0===e&&(e=!1);var i=this.getLngLatFromEvent(t);if(!i)return null;var n=i.lng,o=i.lat,r=this.getMapElementXYPosition(t),s=r.containerX,a=r.containerY,d=this.getButton(t),u=Array.from(this._heldKeys);return{lng:_(n,this._coordinatePrecision),lat:_(o,this._coordinatePrecision),containerX:s,containerY:a,button:d,heldKeys:u,isContextMenu:e}},e.register=function(t){this._currentModeCallbacks=t,this._listeners=this.getAdapterListeners(),this._listeners.forEach(function(t){t.register()})},e.getCoordinatePrecision=function(){return this._coordinatePrecision},e.getAdapterListeners=function(){var t=this;return[new ci({name:"pointerdown",callback:function(e){if(t._currentModeCallbacks&&e.isPrimary){var i=t.getDrawEventFromEvent(e);i&&(t._dragState="pre-dragging",t._lastDrawEvent=i,t._lastPointerDownEventTarget=e.target?e.target:void 0)}},register:function(e){t.getMapEventElement("pointerdown").addEventListener("pointerdown",e)},unregister:function(e){t.getMapEventElement("pointerdown").removeEventListener("pointerdown",e)}}),new ci({name:"pointermove",callback:function(e){if(t._currentModeCallbacks&&e.isPrimary){e.preventDefault();var i=t.getDrawEventFromEvent(e);if(i)if("not-dragging"===t._dragState)t._currentModeCallbacks.onMouseMove(i),t._lastDrawEvent=i;else if("pre-dragging"===t._dragState){if(!t._lastDrawEvent)return;var n={x:t._lastDrawEvent.containerX,y:t._lastDrawEvent.containerY},o={x:i.containerX,y:i.containerY},r=t._currentModeCallbacks.getState(),s=dt(n,o);if("drawing"===r?s0},e.canRedo=function(){return!!this.inDrawingState()&&this.getHistorySizes().redoSize>0},e.undo=function(){return!(!this.canUndo()||!this.undoMode||(this.undoMode(),this.emitHistoryChange(Mi),0))},e.redo=function(){return!(!this.canRedo()||!this.redoMode||(this.redoMode(),this.emitHistoryChange(wi),0))},e.clearHistory=function(){this.clearModeHistory&&this.clearModeHistory(),this.lastHistorySizes={undoSize:0,redoSize:0}},e.getHistorySizes=function(){return this.getModeHistorySizes?this.getModeHistorySizes():{undoSize:0,redoSize:0}},e.undoSize=function(){return this.getHistorySizes().undoSize},e.redoSize=function(){return this.getHistorySizes().redoSize},e.emitPushIfHistoryChangedFromLastSnapshot=function(){if(this.inDrawingState()){var t=this.getHistorySizes();t.undoSize===this.lastHistorySizes.undoSize&&t.redoSize===this.lastHistorySizes.redoSize||this.emitHistoryChange(Ei)}},e.emitPushIfHistoryChanged=function(t){if(this.inDrawingState()){var e=this.getHistorySizes();e.undoSize===t.undoSize&&e.redoSize===t.redoSize||this.emitHistoryChange(Ei)}},e.emitHistoryChange=function(t){if(this.onHistoryChange){var e=this.getHistorySizes(),i=e.undoSize,n=e.redoSize;this.lastHistorySizes={undoSize:i,redoSize:n},this.onHistoryChange({cause:t,stack:Di,undoStackSize:i,redoStackSize:n})}},t}(),_i=/*#__PURE__*/function(){function t(t){var e=this;this.draw=void 0,this.onHistoryChange=void 0,this.maxStackSize=void 0,this.historyById={},this.undoStack=[],this.ignoreProgrammaticCreate={},this.ignoreProgrammaticDelete={},this.deletedFeatureIds={},this.redoStack=[],this.isReplayingHistory=!1,this.emitStackChange=function(t){e.onHistoryChange&&e.onHistoryChange({cause:t,stack:ki,undoStackSize:e.undoStack.length,redoStackSize:e.redoStack.length})},this.handleChange=function(t,i,n){if(e.draw&&!e.isDrawing()&&0!==e.maxStackSize)if("update"!==i){if("delete"===i||"create"===i)if("create"!==i){for(var r,s=!1,a=[],d=o(Array.isArray(t)?t:[t]);!(r=d()).done;){var u=r.value,h=String(u);if(e.ignoreProgrammaticDelete[u])delete e.ignoreProgrammaticDelete[u];else if(e.historyById[h]){var l=e.historyById[h].length-1;if(l>=0){var c=e.historyById[h][l];if(!c)continue;a.push({id:u,toIndex:l,snapshot:c}),e.deletedFeatureIds[u]=!0,s=!0}}}if(a.length>1)e.pushUndoStackEntry({id:a[0].id,toIndex:a[0].toIndex,action:"batch-delete",metadata:{entries:a}});else if(1===a.length){var p=a[0];e.pushUndoStackEntry({id:p.id,toIndex:p.toIndex,action:"single"})}s&&(e.redoStack.length=0,e.emitStackChange(Ei))}else{if(void 0===n||!("origin"in n)||"api"!==n.origin)return;for(var g,f=!1,y=[],v=o(Array.isArray(t)?t:[t]);!(g=v()).done;){var m=g.value;if(e.ignoreProgrammaticCreate[m])delete e.ignoreProgrammaticCreate[m],delete e.deletedFeatureIds[m];else{var C=String(m),P=e.draw.getSnapshotFeature(m);P&&(e.deletedFeatureIds[m]&&(e.historyById[C]=[],delete e.deletedFeatureIds[m]),e.historyById[C]||(e.historyById[C]=[]),e.historyById[C].push(P),y.push({id:m,toIndex:e.historyById[C].length-1,snapshot:P}),f=!0)}}if(y.length>1)e.pushUndoStackEntry({id:y[0].id,toIndex:y[0].toIndex,action:"batch-create",metadata:{entries:y}});else if(1===y.length){var I=y[0];e.pushUndoStackEntry({id:I.id,toIndex:I.toIndex,action:"single"})}f&&(e.redoStack.length=0,e.emitStackChange(Ei))}}else{if(void 0===n||!("origin"in n)||"api"!==n.origin||e.isReplayingHistory)return;for(var S,F=!1,x=o(Array.isArray(t)?t:[t]);!(S=x()).done;){var O=S.value;if(null!=O){var M=String(O),w=e.draw.getSnapshotFeature(O);w&&(e.historyById[M]||(e.historyById[M]=[]),e.historyById[M].push(w),e.pushUndoStackEntry({id:O,toIndex:e.historyById[M].length-1,action:"single"}),F=!0)}}F&&(e.redoStack.length=0,e.emitStackChange(Ei))}},this.handleFinish=function(t){if(e.draw&&0!==e.maxStackSize&&!e.isReplayingHistory)for(var i,n=!1,r=o(Array.isArray(t)?t:[t]);!(i=r()).done;){var s=i.value;if(null!=s){var a=String(s),d=e.draw.getSnapshotFeature(s);d&&(e.historyById[a]||(e.historyById[a]=[]),e.historyById[a].push(d),n||(e.redoStack.length=0,n=!0),e.pushUndoStackEntry({id:s,toIndex:e.historyById[a].length-1,action:"single"}),e.emitStackChange(Ei))}}},this.maxStackSize=Oi(null==t?void 0:t.maxStackSize)}var e=t.prototype;return e.register=function(t){this.draw!==t.draw?(this.draw&&(this.draw.off("change",this.handleChange),this.draw.off("finish",this.handleFinish)),this.draw=t.draw,this.draw.on("change",this.handleChange),this.draw.on("finish",this.handleFinish),this.onHistoryChange=t.onHistoryChange):this.onHistoryChange=t.onHistoryChange},e.pushUndoStackEntry=function(t){0!==this.maxStackSize&&(this.undoStack.push(t),this.undoStack.length>this.maxStackSize&&this.undoStack.shift())},e.pushRedoStackEntry=function(t){0!==this.maxStackSize&&(this.redoStack.push(t),this.redoStack.length>this.maxStackSize&&this.redoStack.shift())},e.isDrawing=function(){return!!this.draw&&"drawing"===this.draw.getModeState()},e.applySnapshotDuringReplay=function(t,e){if(this.draw){this.isReplayingHistory=!0;try{this.draw.hasFeature(t)&&(this.ignoreProgrammaticDelete[t]=!0,this.draw.removeFeatures([t])),this.ignoreProgrammaticCreate[t]=!0,delete this.deletedFeatureIds[t],this.draw.addFeatures([e])}finally{this.isReplayingHistory=!1}}},e.canUndo=function(){return!(!this.draw||this.isDrawing())&&this.undoStack.length>0},e.canRedo=function(){return!(!this.draw||this.isDrawing())&&this.redoStack.length>0},e.undo=function(){var t=this;if(!this.canUndo())return!1;if(!this.draw)return!1;var e=this.undoStack.pop();if(!e)return this.emitStackChange(Mi),!1;if("batch-create"===e.action){var i,n=(null==(i=e.metadata)?void 0:i.entries)||[];if(0===n.length)return this.emitStackChange(Mi),!1;var o=n.map(function(t){return t.id});return o.forEach(function(e){t.ignoreProgrammaticDelete[e]=!0,t.deletedFeatureIds[e]=!0}),this.draw.removeFeatures(o),this.pushRedoStackEntry({id:n[0].id,toIndex:n[0].toIndex,action:"batch-create",metadata:{entries:n}}),this.emitStackChange(Mi),!0}if("batch-delete"===e.action){var r,s=(null==(r=e.metadata)?void 0:r.entries)||[];if(0===s.length)return this.emitStackChange(Mi),!1;var a=s.map(function(t){return t.snapshot}).filter(function(t){return void 0!==t});return a.length>0&&(s.forEach(function(e){t.ignoreProgrammaticCreate[e.id]=!0,delete t.deletedFeatureIds[e.id]}),this.draw.addFeatures(a)),this.pushRedoStackEntry({id:s[0].id,toIndex:s[0].toIndex,action:"batch-delete",metadata:{entries:s}}),this.emitStackChange(Mi),!0}var d=e.id,u=e.toIndex,h=String(d),l=this.historyById[h];if(!l||0===l.length)return this.emitStackChange(Mi),!1;var c=Math.min(u,l.length-1);if(!this.draw.hasFeature(d)){var p=l[c];return p?(this.ignoreProgrammaticCreate[d]=!0,delete this.deletedFeatureIds[d],this.draw.addFeatures([p]),this.pushRedoStackEntry({id:d,toIndex:c,action:"delete",snapshot:p}),this.emitStackChange(Mi),!0):(this.emitStackChange(Mi),!1)}if(c<=0)return this.pushRedoStackEntry({id:d,toIndex:0,action:"create"}),this.ignoreProgrammaticDelete[d]=!0,this.deletedFeatureIds[d]=!0,this.draw.removeFeatures([d]),this.undoStack=this.undoStack.filter(function(t){return t.id!==d}),this.emitStackChange(Mi),!0;var g=l[c],f=l[c-1];return g&&this.pushRedoStackEntry({id:d,toIndex:c,snapshot:g,action:"update"}),this.applySnapshotDuringReplay(d,f),l.length=c,this.emitStackChange(Mi),!0},e.redo=function(){var t=this;if(!this.canRedo())return!1;if(!this.draw)return!1;var e=this.redoStack.pop(),i=e.id,n=e.toIndex,o=e.snapshot,r=e.action,s=e.metadata;if("batch-create"===r){var a=(null==s?void 0:s.entries)||[];if(0===a.length)return this.emitStackChange(wi),!1;var d=a.map(function(t){return t.snapshot}).filter(function(t){return void 0!==t});return d.length>0&&(a.forEach(function(e){t.ignoreProgrammaticCreate[e.id]=!0}),this.draw.addFeatures(d)),this.pushUndoStackEntry({id:a[0].id,toIndex:a[0].toIndex,action:"batch-create",metadata:{entries:a}}),this.emitStackChange(wi),!0}if("batch-delete"===r){var u=(null==s?void 0:s.entries)||[];if(0===u.length)return this.emitStackChange(wi),!1;var h=u.map(function(t){return t.id});return h.forEach(function(e){t.ignoreProgrammaticDelete[e]=!0,t.deletedFeatureIds[e]=!0}),this.draw.removeFeatures(h),this.pushUndoStackEntry({id:u[0].id,toIndex:u[0].toIndex,action:"batch-delete",metadata:{entries:u}}),this.emitStackChange(wi),!0}var l=String(i),c=this.historyById[l]||(this.historyById[l]=[]);if("delete"===r)return this.ignoreProgrammaticDelete[i]=!0,this.deletedFeatureIds[i]=!0,this.draw.removeFeatures([i]),this.pushUndoStackEntry({id:i,toIndex:n,action:"single"}),this.emitStackChange(wi),!0;if(n<=0){var p=c[0];return!!p&&(this.ignoreProgrammaticCreate[i]=!0,this.draw.addFeatures([p]),this.pushUndoStackEntry({id:i,toIndex:0,action:"single"}),this.emitStackChange(wi),!0)}var g=o||c[n];return!!g&&(c.length===n?c.push(g):(c[n]=g,c.length=n+1),this.applySnapshotDuringReplay(i,g),this.pushUndoStackEntry({id:i,toIndex:n,action:"single"}),this.emitStackChange(wi),!0)},e.clearHistory=function(){var t={};if(this.draw&&!this.isDrawing())for(var e,i=o(this.draw.getSnapshot());!(e=i()).done;){var n=e.value;t[String(n.id)]=[n]}this.historyById=t,this.undoStack=[],this.ignoreProgrammaticCreate={},this.ignoreProgrammaticDelete={},this.deletedFeatureIds={},this.redoStack=[]},e.undoSize=function(){return this.undoStack.length},e.redoSize=function(){return this.redoStack.length},t}(),Ti=/*#__PURE__*/function(){function t(t){var e;this.modeLevel=void 0,this.sessionLevel=void 0,this.shouldPreferMode=void 0,this.onHistoryChange=void 0,this.shouldEmitHistoryChange=void 0,this.modeLevel=t.modeLevel,this.sessionLevel=t.sessionLevel,this.shouldPreferMode=t.shouldPreferMode,this.onHistoryChange=t.onHistoryChange,this.shouldEmitHistoryChange=null!=(e=t.shouldEmitHistoryChange)?e:function(){return!0}}var e=t.prototype;return e.emitStackHistoryChange=function(t){this.shouldEmitHistoryChange()&&this.onHistoryChange&&this.onHistoryChange({cause:t.cause,stack:t.stack,undoSize:t.undoStackSize,redoSize:t.redoStackSize})},e.hasSessionUndo=function(){return Boolean(this.sessionLevel&&this.sessionLevel.canUndo())},e.hasSessionRedo=function(){return Boolean(this.sessionLevel&&this.sessionLevel.canRedo())},e.activeStackForUndo=function(){var t,e;return this.shouldPreferMode()&&null!=(t=this.modeLevel)&&t.canUndo()?Di:this.hasSessionUndo()?ki:null!=(e=this.modeLevel)&&e.canUndo()?Di:void 0},e.activeStackForRedo=function(){var t,e;return this.shouldPreferMode()&&null!=(t=this.modeLevel)&&t.canRedo()?Di:this.hasSessionRedo()?ki:null!=(e=this.modeLevel)&&e.canRedo()?Di:void 0},e.canUndo=function(){return void 0!==this.activeStackForUndo()},e.canRedo=function(){return void 0!==this.activeStackForRedo()},e.undo=function(){var t=this.activeStackForUndo();return!!t&&(t===Di?!!this.modeLevel&&this.modeLevel.undo():!(!this.sessionLevel||!this.sessionLevel.canUndo())&&this.sessionLevel.undo())},e.redo=function(){var t=this.activeStackForRedo();return!!t&&(t===Di?!!this.modeLevel&&this.modeLevel.redo():!(!this.sessionLevel||!this.sessionLevel.canRedo())&&this.sessionLevel.redo())},e.clearHistory=function(){this.modeLevel&&this.modeLevel.clearHistory(),this.sessionLevel&&this.sessionLevel.clearHistory()},e.emitHistoryPushForCompletedAction=function(){this.sessionLevel?this.emitStackHistoryChange({cause:Ei,undoStackSize:this.sessionLevel.undoSize(),redoStackSize:this.sessionLevel.redoSize(),stack:ki}):this.modeLevel&&this.emitStackHistoryChange({cause:Ei,undoStackSize:this.modeLevel.undoSize(),redoStackSize:this.modeLevel.redoSize(),stack:Di})},t}();t.TerraDraw=/*#__PURE__*/function(){function t(t){var e,i,n,o,s=this;this._modes=void 0,this._mode=void 0,this._adapter=void 0,this._enabled=!1,this._store=void 0,this._eventListeners=void 0,this._instanceSelectModes=void 0,this.sessionUndoRedoEnabled=!1,this.keyboardShortcutsMatcher=void 0,this.drawingUndoRedo=void 0,this.sessionUndoRedo=void 0,this.undoRedoCoordinator=void 0,this._adapter=t.adapter,this._instanceSelectModes=[];var a=null==t||null==(e=t.undoRedo)?void 0:e.modeLevel;a&&(this.drawingUndoRedo=a);var d=null==t||null==(i=t.undoRedo)?void 0:i.keyboardShortcuts;d&&(this.keyboardShortcutsMatcher=d),this.sessionUndoRedoEnabled=Boolean(null==t||null==(n=t.undoRedo)?void 0:n.sessionLevel);var u=null==t||null==(o=t.undoRedo)?void 0:o.sessionLevel;this._mode=new Ee;var h=new Set,l=t.modes.reduce(function(t,e){if(h.has(e.mode))throw new Error("There is already a "+e.mode+" mode provided");return h.add(e.mode),t[e.mode]=e,t},{}),c=Object.keys(l);if(0===c.length)throw new Error("No modes provided");c.forEach(function(t){l[t].type===I.Select&&s._instanceSelectModes.push(t)}),this._modes=r({},l,{static:this._mode}),this._eventListeners={change:[],select:[],deselect:[],finish:[],ready:[],history:[]},this._store=new Ye({tracked:!!t.tracked,idStrategy:t.idStrategy?t.idStrategy:void 0});var p=function(t){var e=[],i=s._store.copyAll().filter(function(i){return!t.includes(i.id)||(e.push(i),!1)});return{changed:e,unchanged:i}},g=function(t,e){var i;s._enabled&&(s._eventListeners.finish.forEach(function(i){i(t,e)}),null==(i=s.undoRedoCoordinator)||i.emitHistoryPushForCompletedAction())},f=function(t,e,i){if(s._enabled){s._eventListeners.change.forEach(function(n){n(t,e,i)}),s.emitDrawingPushIfHistoryChangedFromLastSnapshot();var n=p(t),o=n.changed,r=n.unchanged;"create"===e?s._adapter.render({created:o,deletedIds:[],unchanged:r,updated:[]},s.getModeStyles()):"update"===e?s._adapter.render({created:[],deletedIds:[],unchanged:r,updated:o},s.getModeStyles()):"delete"===e?s._adapter.render({created:[],deletedIds:t,unchanged:r,updated:[]},s.getModeStyles()):"styling"===e&&s._adapter.render({created:[],deletedIds:[],unchanged:r,updated:[]},s.getModeStyles())}},y=function(t){if(s._enabled){s._eventListeners.select.forEach(function(e){e(t)});var e=p([t]);s._adapter.render({created:[],deletedIds:[],unchanged:e.unchanged,updated:e.changed},s.getModeStyles())}},v=function(t){if(s._enabled){s._eventListeners.deselect.forEach(function(e){e(t)});var e=p([t]),i=e.changed;i&&s._adapter.render({created:[],deletedIds:[],unchanged:e.unchanged,updated:i},s.getModeStyles())}};Object.keys(this._modes).forEach(function(t){var e;s._modes[t].register({mode:t,store:s._store,setCursor:s._adapter.setCursor.bind(s._adapter),project:s._adapter.project.bind(s._adapter),unproject:s._adapter.unproject.bind(s._adapter),setDoubleClickToZoom:s._adapter.setDoubleClickToZoom.bind(s._adapter),onChange:f,onSelect:y,onDeselect:v,onFinish:g,coordinatePrecision:s._adapter.getCoordinatePrecision(),undoRedoMaxStackSize:null==(e=s.drawingUndoRedo)||null==e.getMaxStackSize?void 0:e.getMaxStackSize()})}),this.sessionUndoRedoEnabled&&u&&(this.sessionUndoRedo=u,u.register({draw:this,onHistoryChange:function(t){var e;null==(e=s.undoRedoCoordinator)||e.emitStackHistoryChange(t)}})),this.drawingUndoRedo&&this.drawingUndoRedo.register({getModeState:function(){return s.getModeState()},getModeHistorySizes:function(){return s.getDrawingHistorySizes()},undoMode:function(){s._mode.undo&&s._mode.undo()},redoMode:function(){s._mode.redo&&s._mode.redo()},clearModeHistory:function(){var t=s._mode;t.clearHistory&&t.clearHistory()},onHistoryChange:function(t){var e;null==(e=s.undoRedoCoordinator)||e.emitStackHistoryChange(t)}}),this.undoRedoCoordinator=new Ti({modeLevel:this.drawingUndoRedo,sessionLevel:this.sessionUndoRedo,shouldPreferMode:function(){return"drawing"===s.getModeState()},onHistoryChange:function(t){s._eventListeners.history.forEach(function(e){e(t)})},shouldEmitHistoryChange:function(){return s._enabled}})}var e=t.prototype;return e.checkEnabled=function(){if(!this._enabled)throw new Error("Terra Draw is not enabled")},e.handleUndoRedoKeyboardShortcut=function(t){if(!this.drawingUndoRedo&&!this.sessionUndoRedoEnabled)return!1;if(!this.keyboardShortcutsMatcher)return!1;var e=this.keyboardShortcutsMatcher.isUndoKeyboardShortcut(t),i=this.keyboardShortcutsMatcher.isRedoKeyboardShortcut(t);if(e){if(!this.canUndo())return!1;var n=this.undo();return n&&t.preventDefault(),n}if(i){if(!this.canRedo())return!1;var o=this.redo();return o&&t.preventDefault(),o}return!1},e.getDrawingHistorySizes=function(){return{undoSize:this._mode.undoSize&&"function"==typeof this._mode.undoSize?this._mode.undoSize():0,redoSize:this._mode.redoSize&&"function"==typeof this._mode.redoSize?this._mode.redoSize():0}},e.emitDrawingPushIfHistoryChangedFromLastSnapshot=function(){this.drawingUndoRedo&&this.drawingUndoRedo.emitPushIfHistoryChangedFromLastSnapshot()},e.emitDrawingPushIfHistoryChanged=function(t){this.drawingUndoRedo&&this.drawingUndoRedo.emitPushIfHistoryChanged(t)},e.getModeStyles=function(){var t=this,e={},i=this._instanceSelectModes.includes(this._mode.mode)?this._mode.mode:void 0;return Object.keys(this._modes).forEach(function(n){e[n]=function(e){return i&&e.properties[f.SELECTED]?t._modes[i].styleFeature.bind(t._modes[i])(e):t._modes[n].styleFeature.bind(t._modes[n])(e)}}),e},e.featuresAtLocation=function(t,e){var i=t.lng,n=t.lat,r=e&&void 0!==e.pointerDistance?e.pointerDistance:30,s=!e||void 0===e.ignoreSelectFeatures||e.ignoreSelectFeatures,a=!(!e||void 0===e.ignoreCoordinatePoints)&&e.ignoreCoordinatePoints,d=!(!e||void 0===e.ignoreCurrentlyDrawing)&&e.ignoreCurrentlyDrawing,u=!(!e||void 0===e.ignoreClosingPoints)&&e.ignoreClosingPoints,h=!(!e||void 0===e.ignoreSnappingPoints)&&e.ignoreSnappingPoints,l=this._adapter.unproject.bind(this._adapter),c=this._adapter.project.bind(this._adapter),p=c(i,n),g=gt({unproject:l,point:p,pointerDistance:r});return this._store.search(g).filter(function(t){if(s&&(t.properties[f.MID_POINT]||t.properties[f.SELECTION_POINT]))return!1;if(a&&t.properties[y.COORDINATE_POINT])return!1;if(u&&t.properties[y.CLOSING_POINT])return!1;if(d&&t.properties[y.CURRENTLY_DRAWING])return!1;if(h&&t.properties[y.SNAPPING_POINT])return!1;if("Point"===t.geometry.type){var l=t.geometry.coordinates,g=c(l[0],l[1]);return dt(p,g)e?{valid:!1,reason:"Feature is larger than the maximum area"}:{valid:!0}},t.ValidateMinAreaSquareMeters=function(t,e){return"Polygon"!==t.geometry.type?{valid:!1,reason:S}:Xe(t.geometry) Date: Sat, 29 Aug 2026 18:37:25 +0200 Subject: [PATCH 24/26] ci: retrigger after unrelated notifications-test flake Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com> From ba1da86fa9db65bae34b504afc0b00e7087f8b63 Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Wed, 2 Sep 2026 12:38:58 +0200 Subject: [PATCH 25/26] ci: retrigger after unrelated app-link roster flake Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com> From 735b7e765c37430a1c5f3ce2383b3071f2bccf01 Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Wed, 2 Sep 2026 21:50:14 +0200 Subject: [PATCH 26/26] feat(web): draw the building footprint where no catalog has one The roof section gains 'Draw the footprint on the map': a Terra Draw polygon traced over the building, clipped exactly like a picked footprint. Optional by design - it stands in for 'Find buildings here' when the catalog publishes no building dataset over STAC, which is most of the open LiDAR catalogs. The ring travels as footprint [lon,lat] pairs through POST /api/roofmodel/derive into the module's --footprint-json (already landed below in the stack); a drawn footprint and a picked building answer the same question, so the newest one wins. Verified live: a footprint traced with real map clicks over the same barn the picker had found derived 2 arrays from 3 roof planes. Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com> --- .changeset/drawn-footprint.md | 13 +++++ docs/roof-geometry.md | 5 ++ web/settings/tabs/weather.js | 91 ++++++++++++++++++++++++++---- web/settings/tabs/weather.test.mjs | 10 ++++ 4 files changed, 108 insertions(+), 11 deletions(-) create mode 100644 .changeset/drawn-footprint.md diff --git a/.changeset/drawn-footprint.md b/.changeset/drawn-footprint.md new file mode 100644 index 000000000..72ae68406 --- /dev/null +++ b/.changeset/drawn-footprint.md @@ -0,0 +1,13 @@ +--- +"ftw": patch +--- + +The roof section gains "Draw the footprint on the map": trace your +building's outline and the LiDAR derive clips to it, exactly as if the +footprint had come from a catalog. It is optional and stands in for "Find +buildings here" where the catalog publishes no building dataset over STAC — +the open LiDAR catalogs (IGN LiDAR HD, KAGIS) mostly ship point clouds only. +The traced ring travels as `footprint` ([lon, lat] pairs) through +POST /api/roofmodel/derive and `--footprint-json` into the module; a drawn +footprint and a picked building answer the same question, so the newest one +wins. diff --git a/docs/roof-geometry.md b/docs/roof-geometry.md index 42ec61a21..a0478b39f 100644 --- a/docs/roof-geometry.md +++ b/docs/roof-geometry.md @@ -126,6 +126,11 @@ tile whole — slower, same answer. The result records which path ran as 5. Click your building. It highlights green. 6. Press **Read roof from LiDAR**. +Where the catalog publishes no building footprints over STAC — the open +LiDAR catalogs mostly ship point clouds only — press **Draw the footprint on +the map** instead of step 4 and trace your building's outline; the laser scan +is clipped to what you drew. Drawing is optional everywhere else. + The PV arrays above fill in with one entry per usable roof face. **Nothing is saved yet** — look at the numbers, correct anything that is wrong, then press Save. FTW never rewrites your panel configuration on its own: the derivation is diff --git a/web/settings/tabs/weather.js b/web/settings/tabs/weather.js index 96be70da6..5332bdccc 100644 --- a/web/settings/tabs/weather.js +++ b/web/settings/tabs/weather.js @@ -182,6 +182,10 @@ var drawGeometry = null; var drawHandled = {}; var lastDrawnArray = null; + // What the next finished shape means: a PV array rectangle, or the + // building footprint the LiDAR should be clipped to. One Terra Draw + // instance serves both; the entry points set the purpose. + var drawPurpose = "array"; function drawStatus(html) { var el = document.getElementById("pv-draw-status"); @@ -194,7 +198,7 @@ return isNaN(v) ? drawGeometry.DEFAULT_TILT_DEG : Math.min(Math.max(v, 0), 90); } - function onRectangleFinished(ctx, id) { + function onShapeFinished(ctx, id) { if (drawHandled[id]) return; drawHandled[id] = true; var snapshot = drawInstance.getSnapshot() || []; @@ -203,6 +207,10 @@ if (snapshot[i] && snapshot[i].id === id) feature = snapshot[i]; } if (!feature || !feature.geometry || feature.geometry.type !== "Polygon") return; + if (drawPurpose === "footprint") { + onFootprintFinished(feature); + return; + } var weather = ctx.config.weather; var derived = drawGeometry.arrayFromRing(feature.geometry.coordinates[0], { latitude: weather.latitude, @@ -226,6 +234,21 @@ ); } + function ensureDrawInstance(ctx, map) { + if (drawInstance) return; + drawInstance = new window.terraDraw.TerraDraw({ + adapter: new window.terraDrawMaplibreGlAdapter.TerraDrawMapLibreGLAdapter({ + map: map, + }), + modes: [ + new window.terraDraw.TerraDrawAngledRectangleMode(), + new window.terraDraw.TerraDrawPolygonMode(), + ], + }); + drawInstance.start(); + drawInstance.on("finish", function (id) { onShapeFinished(ctx, id); }); + } + function startArrayDrawing(ctx) { var map = window._weatherMap; if (!map) { @@ -236,16 +259,8 @@ Promise.all([loadTerraDraw(), import("/components/pv-array-geometry.js")]) .then(function (loaded) { drawGeometry = loaded[1]; - if (!drawInstance) { - drawInstance = new window.terraDraw.TerraDraw({ - adapter: new window.terraDrawMaplibreGlAdapter.TerraDrawMapLibreGLAdapter({ - map: map, - }), - modes: [new window.terraDraw.TerraDrawAngledRectangleMode()], - }); - drawInstance.start(); - drawInstance.on("finish", function (id) { onRectangleFinished(ctx, id); }); - } + ensureDrawInstance(ctx, map); + drawPurpose = "array"; drawInstance.setMode("angled-rectangle"); var container = document.getElementById("weather-map"); if (container && container.scrollIntoView) { @@ -262,6 +277,50 @@ }); } + function startFootprintDrawing(ctx) { + var map = window._weatherMap; + if (!map) { + roofSay("The map has to finish loading before you can draw on it.", "bad"); + return; + } + roofSay("Loading the drawing tools…"); + loadTerraDraw() + .then(function () { + ensureDrawInstance(ctx, map); + drawPurpose = "footprint"; + drawInstance.setMode("polygon"); + var container = document.getElementById("weather-map"); + if (container && container.scrollIntoView) { + container.scrollIntoView({ block: "nearest" }); + } + roofSay( + "Click each corner of the building on the map, then click the " + + "first corner again to close the outline." + ); + }) + .catch(function (e) { + roofSay("Drawing is unavailable (" + ctx.escHtml(e.message) + ").", "bad"); + }); + } + + function onFootprintFinished(feature) { + var ring = (feature.geometry.coordinates || [])[0] || []; + if (ring.length < 4) { // GeoJSON rings close themselves: 3 corners = 4 points + roofSay("That outline has too few corners — draw it again.", "bad"); + return; + } + roofState.drawnFootprint = ring; + // A drawn footprint and a picked building answer the same question; + // the newest answer wins. + roofState.selectedId = null; + drawBuildings(); + try { drawInstance.setMode("static"); } catch (e) { /* keeps drawing */ } + var derive = document.getElementById("roof-derive"); + if (derive) derive.disabled = false; + roofSay("Footprint drawn (" + (ring.length - 1) + " corners). " + + "Read roof from LiDAR will clip the laser scan to it."); + } + function stopArrayDrawing() { if (!drawInstance) return; // "static" keeps what has been drawn on the map while stopping new @@ -511,8 +570,16 @@ '
' + '
' + '' + + '' + '' + '
' + + '

' + + 'Drawing the footprint yourself is optional: it stands in for ' + + 'Find buildings here when the catalog publishes no building ' + + 'footprints over STAC for your region — the open LiDAR catalogs ' + + 'mostly ship point clouds only. Trace your building\'s outline and ' + + 'the laser scan is clipped to it.' + + '

' + '
' + '
' + '

' + @@ -888,6 +955,8 @@ }); var findBtn = document.getElementById("roof-find"); if (findBtn) findBtn.addEventListener("click", function () { findBuildings(ctx); }); + var footprintBtn = document.getElementById("roof-draw-footprint"); + if (footprintBtn) footprintBtn.addEventListener("click", function () { startFootprintDrawing(ctx); }); var deriveBtn = document.getElementById("roof-derive"); if (deriveBtn) deriveBtn.addEventListener("click", function () { deriveRoof(ctx); }); var drawBtn = document.getElementById("pv-array-draw"); diff --git a/web/settings/tabs/weather.test.mjs b/web/settings/tabs/weather.test.mjs index 6966a00b3..687b16d4f 100644 --- a/web/settings/tabs/weather.test.mjs +++ b/web/settings/tabs/weather.test.mjs @@ -147,6 +147,16 @@ describe("roof buildings on the map", () => { assert.match(source, /map\.once\("load", drawBuildings\)/); }); + it("offers footprint drawing as the fallback for catalogs without buildings", () => { + const html = tab.render(stubCtx()); + assert.ok(html.includes('id="roof-draw-footprint"')); + assert.ok(html.includes("Drawing the footprint yourself is optional")); + // The drawn ring must reach the derive, and beat a stale building pick. + assert.match(source, /payload\.footprint = roofState\.drawnFootprint/); + assert.match(source, /TerraDrawPolygonMode/); + assert.match(source, /roofState\.drawnFootprint = null/); + }); + it("derives at the same coordinates the picker searched", () => { // Both requests read the live form state; deriving against the stored // site while the pin has moved makes the picked building "not found