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/.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/.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/.changeset/maplibre-670.md b/.changeset/maplibre-670.md new file mode 100644 index 000000000..4d7f5c7fa --- /dev/null +++ b/.changeset/maplibre-670.md @@ -0,0 +1,8 @@ +--- +"ftw": patch +--- + +The vendored MapLibre GL JS is now 6.7.0 (was 6.0.0). Same four-file +code-split layout, and the per-request `referrerPolicy` the OSM tiles rely on +still works; the weather map, the building picker and the PV array drawing +tool were re-verified in a browser on the new build. 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/.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/.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/.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/.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/.changeset/roofmodel-stac-basic-auth.md b/.changeset/roofmodel-stac-basic-auth.md new file mode 100644 index 000000000..e19a5cb86 --- /dev/null +++ b/.changeset/roofmodel-stac-basic-auth.md @@ -0,0 +1,17 @@ +--- +"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, 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/.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/.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/.changeset/vendor-maplibre.md b/.changeset/vendor-maplibre.md new file mode 100644 index 000000000..e25ec21da --- /dev/null +++ b/.changeset/vendor-maplibre.md @@ -0,0 +1,13 @@ +--- +"ftw": patch +--- + +The weather map's MapLibre GL JS is vendored on the box instead of loaded +from unpkg, so the location picker works without internet access and the UI +executes no third-party CDN JavaScript — the same policy that vendored +Leaflet, whose now-unused copy is removed. + +Static assets are also served with pinned Content-Types instead of whatever +the host OS's MIME table says: on a Windows host whose registry maps `.mjs` +to text/plain, the browser (correctly, under `nosniff`) refused the vendored +module and the map died with "failed to fetch dynamically imported module". 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/.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/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/docs/roof-geometry.md b/docs/roof-geometry.md new file mode 100644 index 000000000..a0478b39f --- /dev/null +++ b/docs/roof-geometry.md @@ -0,0 +1,203 @@ +# 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 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 → Planner → 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 | 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 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 +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. + +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. 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. + +### 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-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 | + +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 +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 +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 + +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**. +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**. + +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 +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 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 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. + +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 + +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 | +| "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² | +| "the building tile could not be read" | The GeoPackage asset was not a GeoPackage — usually a changed download URL | + +Failures never change your configuration. + +*Data © Lantmäteriet, CC BY 4.0.* diff --git a/go/cmd/ftw/main.go b/go/cmd/ftw/main.go index c8d35f144..108519b48 100644 --- a/go/cmd/ftw/main.go +++ b/go/cmd/ftw/main.go @@ -64,6 +64,8 @@ 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/roofmodel" "github.com/srcfl/ftw/go/internal/selftune" "github.com/srcfl/ftw/go/internal/selfupdate" "github.com/srcfl/ftw/go/internal/state" @@ -881,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 @@ -988,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 @@ -1204,6 +1217,31 @@ 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. + // 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) + + 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 +2606,8 @@ func main() { SnapshotDir: filepath.Join(filepath.Dir(statePath), "snapshots"), 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 40bfec1b3..c93efc73c 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,8 @@ 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/roofmodel" "github.com/srcfl/ftw/go/internal/scanner" "github.com/srcfl/ftw/go/internal/selftune" "github.com/srcfl/ftw/go/internal/selfupdate" @@ -139,6 +142,14 @@ 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 + + // 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 @@ -495,6 +506,11 @@ 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/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) s.handle("GET /api/mpc/diagnose", Read, s.handleMPCDiagnose) @@ -2420,6 +2436,292 @@ 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. GET /api/roofmodel/buildings lists footprints to pick +// from; POST /api/roofmodel/derive fits the roof of the picked one. +// +// 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()} + if haveSite { + 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 + 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() + // Optional: which footprint to clip the LiDAR to. An absent or empty body + // 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"` + 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, body.Footprint) + if err != nil { + writeJSON(w, roofModelErrorStatus(err), map[string]any{"error": err.Error()}) + return + } + writeJSON(w, 200, map[string]any{ + "enabled": true, + "model": model, + "proposed_arrays": model.ToPVArrays(), + }) +} + +// 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 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 + } + 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 + if rm.StacBaseURL != "" { + return true + } + return rm.StacUser() != "" && rm.StacPass() != "" +} + +// 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. +// 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. + 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, + // 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 { @@ -2960,6 +3262,23 @@ func (s *Server) handlePVModelReset(w http.ResponseWriter, r *http.Request) { // ---- static ---- +// staticContentTypes pins the Content-Type of every asset kind the web tree +// ships. Without it, http.ServeFile asks the operating system's MIME table — +// the registry on Windows — and a host that maps .mjs (or .js) to text/plain +// does not merely mislabel the file: the app sends X-Content-Type-Options: +// nosniff, so the browser is required to refuse it, and the vendored MapLibre +// dies with "failed to fetch dynamically imported module". ES modules are the +// strictest case; the rest are pinned so no asset depends on host state. +var staticContentTypes = map[string]string{ + ".html": "text/html; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".mjs": "text/javascript; charset=utf-8", + ".svg": "image/svg+xml", + ".png": "image/png", + ".jpg": "image/jpeg", +} + func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) { path := r.URL.Path if path == "/" { @@ -2978,6 +3297,9 @@ func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) { } // Always-revalidate so version bumps land immediately w.Header().Set("Cache-Control", "no-cache, must-revalidate") + if ct, ok := staticContentTypes[strings.ToLower(filepath.Ext(clean))]; ok { + w.Header().Set("Content-Type", ct) + } http.ServeFile(w, r, clean) } 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/api/api_roofmodel_test.go b/go/internal/api/api_roofmodel_test.go new file mode 100644 index 000000000..d1f947cd3 --- /dev/null +++ b/go/internal/api/api_roofmodel_test.go @@ -0,0 +1,232 @@ +package api + +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) { + 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) + } +} + +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", + StacUsername: "u", StacPassword: "p", + }) + + // 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 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 +// 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{ + 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"]) + } +} + +// 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) { + 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/api/api_static_mime_test.go b/go/internal/api/api_static_mime_test.go new file mode 100644 index 000000000..b0801ce09 --- /dev/null +++ b/go/internal/api/api_static_mime_test.go @@ -0,0 +1,52 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" +) + +// A browser must refuse an ES module served with a non-JavaScript +// Content-Type once X-Content-Type-Options: nosniff is set — and FTW sets it. +// http.ServeFile alone asks the operating system's MIME table (the registry +// on Windows), so a host that maps .mjs to text/plain would break the +// vendored MapLibre import outright. The served type must come from the app. +func TestStaticAssetsServeWithPinnedContentTypes(t *testing.T) { + dir := t.TempDir() + sub := filepath.Join(dir, "vendor", "maplibre") + if err := os.MkdirAll(sub, 0o755); err != nil { + t.Fatal(err) + } + files := map[string]string{ + filepath.Join(sub, "maplibre-gl.mjs"): "export default {};", + filepath.Join(dir, "app.js"): "// classic script", + filepath.Join(dir, "style.css"): "body{}", + filepath.Join(dir, "logo.svg"): ``, + } + for p, content := range files { + if err := os.WriteFile(p, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + } + srv := New(&Deps{Version: "test", WebDir: dir}) + + for path, want := range map[string]string{ + "/vendor/maplibre/maplibre-gl.mjs": "text/javascript", + "/app.js": "text/javascript", + "/style.css": "text/css", + "/logo.svg": "image/svg+xml", + } { + req := httptest.NewRequest(http.MethodGet, path, nil) + rr := httptest.NewRecorder() + srv.Handler().ServeHTTP(rr, req) + if rr.Code != 200 { + t.Fatalf("%s: status = %d, want 200", path, rr.Code) + } + if got := rr.Header().Get("Content-Type"); !strings.HasPrefix(got, want) { + t.Errorf("%s: Content-Type = %q, want prefix %q", path, got, want) + } + } +} diff --git a/go/internal/config/config.go b/go/internal/config/config.go index ab95bd361..3038c427d 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,84 @@ 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. +// +// 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"` + + // 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"` + // 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"` +} + +// 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 @@ -1575,6 +1654,20 @@ 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 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 + } if out.Notifications != nil { cp := *out.Notifications if cp.Ntfy != nil { @@ -1670,6 +1763,13 @@ 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.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 { 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..b02e3dfa3 --- /dev/null +++ b/go/internal/config/roofmodel_secrets_test.go @@ -0,0 +1,141 @@ +package config + +import "testing" + +// 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 TestRoofModelMaskSecretsHidesThePasswordButSaysOneExists(t *testing.T) { + c := Config{RoofModel: &RoofModel{ + Enabled: true, + StacUsername: "operator@example.com", + StacPassword: "gt_secret_value", + }} + m := c.MaskSecrets() + + if m.RoofModel.StacPassword != "" { + t.Errorf("password leaked through the API: %q", m.RoofModel.StacPassword) + } + 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.StacUsername != "operator@example.com" { + t.Errorf("username got blanked: %q", m.RoofModel.StacUsername) + } + if c.RoofModel.StacPassword != "gt_secret_value" { + t.Error("masking mutated the original config") + } +} + +// 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 +// password from the UI means "unchanged", not "delete it". +func TestRoofModelPreserveMaskedSecretsKeepsTheStoredPassword(t *testing.T) { + existing := &Config{RoofModel: &RoofModel{ + Enabled: true, StacUsername: "operator", StacPassword: "gt_secret_value", + }} + incoming := &Config{RoofModel: &RoofModel{ + Enabled: true, StacUsername: "operator", StacPassword: "", RadiusM: 60, + }} + + incoming.PreserveMaskedSecrets(existing) + + 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") + } +} + +// 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 TestRoofModelPreserveMaskedSecretsAcceptsANewPassword(t *testing.T) { + existing := &Config{RoofModel: &RoofModel{StacPassword: "old_password"}} + incoming := &Config{RoofModel: &RoofModel{StacPassword: "new_password"}} + + incoming.PreserveMaskedSecrets(existing) + + 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{StacPassword: "first_password"}} + incoming.PreserveMaskedSecrets(&Config{}) + if incoming.RoofModel.StacPassword != "first_password" { + t.Errorf("password = %q", incoming.RoofModel.StacPassword) + } + + none := &Config{} + 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/coverage/coverage.go b/go/internal/coverage/coverage.go new file mode 100644 index 000000000..9b65e6cc2 --- /dev/null +++ b/go/internal/coverage/coverage.go @@ -0,0 +1,201 @@ +// 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" + KindGeodata Kind = "geodata" +) + +// 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: "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", + 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/roofmodel/roofmodel.go b/go/internal/roofmodel/roofmodel.go new file mode 100644 index 000000000..93cd95b5a --- /dev/null +++ b/go/internal/roofmodel/roofmodel.go @@ -0,0 +1,368 @@ +// 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" + "sync" + "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"` + RatedW float64 `json:"rated_w"` + TiltDeg float64 `json:"tilt_deg"` + AzimuthDeg float64 `json:"azimuth_deg"` + AreaM2 float64 `json:"area_m2"` + 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"` + // 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"` + } `json:"site"` + Source struct { + Provider string `json:"provider"` + 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 + // 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 { + mu sync.RWMutex + cfg *config.RoofModel +} + +// 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 { + 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 + } + s.mu.RLock() + defer s.mu.RUnlock() + return s.cfg +} + +// Enabled reports whether derives are possible. +func (s *Service) Enabled() bool { + c := s.config() + return c != nil && c.Enabled +} + +func timeoutFrom(cfg *config.RoofModel) time.Duration { + if cfg.TimeoutS > 0 { + return time.Duration(cfg.TimeoutS) * time.Second + } + return defaultTimeout +} + +func commandFrom(cfg *config.RoofModel) string { + if cfg.Command != "" { + return cfg.Command + } + return defaultCommand +} + +func radiusFrom(cfg *config.RoofModel) float64 { + if cfg.RadiusM > 0 { + return cfg.RadiusM + } + return defaultRadiusM +} + +func packingFactorFrom(cfg *config.RoofModel) float64 { + if cfg.PackingFactor > 0 { + return cfg.PackingFactor + } + 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", "", nil) + 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. +// 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, footprint [][]float64) (*Model, error) { + out, err := s.run(ctx, lat, lon, "derive", buildingID, footprint) + 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, footprint [][]float64) ([]byte, error) { + 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 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 cfg.StacBaseURL == "" && (cfg.StacUser() == "" || cfg.StacPass() == "") { + return nil, ErrNoCredentials + } + + ctx, cancel := context.WithTimeout(ctx, timeoutFrom(cfg)) + defer cancel() + + args := []string{ + "-m", "ftw_roofmodel", + "--mode", mode, + "--lat", fmt.Sprintf("%.6f", lat), + "--lon", fmt.Sprintf("%.6f", lon), + "--radius-m", fmt.Sprintf("%.1f", radiusFrom(cfg)), + "--packing-factor", fmt.Sprintf("%.3f", packingFactorFrom(cfg)), + } + 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) + } + 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 cfg.StacBaseURL != "" { + args = append(args, "--stac-base-url", cfg.StacBaseURL) + } + if cfg.StacBuildingsCollection != "" { + args = append(args, "--buildings-collection", cfg.StacBuildingsCollection) + } + if cfg.StacLidarCollection != "" { + args = append(args, "--lidar-collection", cfg.StacLidarCollection) + } + if cfg.StacBboxEPSG != 0 { + args = append(args, "--bbox-epsg", fmt.Sprintf("%d", cfg.StacBboxEPSG)) + } + 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 + cmd.Stderr = &stderr + + err := cmd.Run() + + if ctx.Err() == context.DeadlineExceeded { + 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. 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 + 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) + } + if stdout.Len() > maxOutputBytes { + return nil, fmt.Errorf("roof model returned %d bytes, refusing", stdout.Len()) + } + return stdout.Bytes(), 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, + RatedW: a.RatedW, + 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..72beb1d99 --- /dev/null +++ b/go/internal/roofmodel/roofmodel_test.go @@ -0,0 +1,715 @@ +package roofmodel + +import ( + "context" + "encoding/json" + "errors" + "os" + "strings" + "testing" + "time" + + "github.com/srcfl/ftw/go/internal/config" +) + +// 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 + } + 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 exe +} + +const minimalModel = `{"schema_version":1,"arrays":[]}` + +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) { + // 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") + } + off := FromConfig(&config.RoofModel{Enabled: false}) + if off.Enabled() { + t.Error("disabled config must report disabled") + } + 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. + var s *Service + if s.Enabled() { + t.Error("nil service must report disabled") + } + 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 +} + +// 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, "", nil); !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, "", 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, "", nil); !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 +// interpreter and hitting the network. +func TestDeriveRefusesOutsideSwedenWithoutSpawning(t *testing.T) { + s := svc(t, &config.RoofModel{ + Enabled: true, + Command: "definitely-not-a-real-command", + StacUsername: "u", StacPassword: "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, "", nil) + 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", + StacUsername: "u", StacPassword: "t", + }) + _, 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") + } + 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", + StacUsername: "u", StacPassword: "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, "", nil) + 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", StacUsername: c.user, StacPassword: c.token}) + _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "", nil) + 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","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, StacUsername: "u", StacPassword: "t"}) + + m, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "", nil) + 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) + } +} + +// 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, StacUsername: "u", StacPassword: "t"}) + + m, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "b-1", nil) + 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) { + 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: "operator", StacPassword: "secret-token", + RadiusM: 25, + }) + + if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "", nil); 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", + "--password 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) + } +} + +// 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, + StacUsername: "u", StacPassword: "t", + }) + + if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "bldg-42", nil); 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) + } +} + +// 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) { + 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", + }) + + if _, err := s.Derive(context.Background(), stockholmLat, stockholmLon, "", nil); 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, StacUsername: "u", StacPassword: "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, StacUsername: "u", StacPassword: "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", + 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) + } + + 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) { + cmd := stubModule(t, "stderr", + `{"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, "", nil) + 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) + } +} + +// 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, "", nil) + 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) { + 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, "", nil) + 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 := 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, "", nil) + 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 := 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, "", nil) + 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) { + cmd := stubModule(t, "hang", "") + s := svc(t, &config.RoofModel{ + Enabled: true, Command: cmd, + StacUsername: "u", StacPassword: "t", TimeoutS: 1, + }) + + start := time.Now() + _, 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) + } + 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", 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 { + 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].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) + } + // 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") + } +} + +// 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, "", nil); 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, "", nil); 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) + } + } +} + +// 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, "", nil); 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) { + s := svc(t, &config.RoofModel{ + Enabled: true, Command: "no-such-command", + StacUsername: "u", StacPassword: "p", + }) + _, 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/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..e0f65b593 --- /dev/null +++ b/go/internal/strang/strang.go @@ -0,0 +1,235 @@ +// Package strang fetches historical solar irradiance from SMHI's STRÅNG +// mesoscale model. The model is described at +// https://www.smhi.se/forskning/modeller-och-data/strang---en-modell-for-solstralning +// and the API is documented at https://opendata.smhi.se/metanalys/strang. +// +// 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 taken on faith from the documentation, which names the quantities but +// not the code-to-quantity mapping: 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..c0e67f124 100644 --- a/go/internal/units/consistency_test.go +++ b/go/internal/units/consistency_test.go @@ -10,6 +10,8 @@ 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/roofmodel" "github.com/srcfl/ftw/go/internal/telemetry" "github.com/srcfl/ftw/go/internal/units" "github.com/srcfl/ftw/go/internal/v2x" @@ -43,6 +45,26 @@ 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 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"} { @@ -243,6 +265,8 @@ func TestCoreBannedSoCPercentFieldNames(t *testing.T) { reflect.TypeOf(mpc.Plan{}), 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/__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..3deb25863 --- /dev/null +++ b/roofmodel/ftw_roofmodel/__main__.py @@ -0,0 +1,136 @@ +"""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 .buildings import DEFAULT_SEARCH_RADIUS_M, search_buildings +from .geotorget import ( + COLLECTION_BUILDINGS, + COLLECTION_LIDAR, + DEFAULT_BASE_URL, + 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( + "--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( + "--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=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) + + try: + if args.mode == "buildings": + 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, + "site": {"latitude": args.lat, "longitude": args.lon}, + "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, + 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, + footprint=footprint, + 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) + sys.stderr.write("\n") + return 1 + json.dump(payload, sys.stdout) + sys.stdout.write("\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/roofmodel/ftw_roofmodel/buildings.py b/roofmodel/ftw_roofmodel/buildings.py new file mode 100644 index 000000000..2cdc40c8f --- /dev/null +++ b/roofmodel/ftw_roofmodel/buildings.py @@ -0,0 +1,468 @@ +"""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 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, and +the derived face is then identical to segmenting that building in isolation. + +Coordinate frames +----------------- +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. +""" + +from __future__ import annotations + +import dataclasses +import math +from typing import Any, Iterable + +from . import sweref +from .geopackage import GeoPackageError, read_features +from .geotorget import ( + COLLECTION_BUILDINGS, + MEDIA_GEOJSON, + MEDIA_GEOPACKAGE, + MEDIA_ZIP, + 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(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(n, e) + 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 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) + 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 _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 _normalize_sweref_ring(ring) + # GeoJSON order is [lon, lat]. + return [ + (e, n) + for n, e in (sweref.wgs84_to_sweref99tm(lat, lon) for lon, lat in ring) + ] + + +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'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. + """ + # 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, bboxes=window_boxes) + 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 [] + + +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_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 {})): + 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, + collection: str = COLLECTION_BUILDINGS, + 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, window_boxes)) + 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/geopackage.py b/roofmodel/ftw_roofmodel/geopackage.py new file mode 100644 index 000000000..76e3c757c --- /dev/null +++ b/roofmodel/ftw_roofmodel/geopackage.py @@ -0,0 +1,303 @@ +"""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 _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, bboxes=bboxes)) + + +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)" + ) + # 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 + 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: + # One unreadable row must not lose the other buildings + # in the tile. + 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() + 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 new file mode 100644 index 000000000..bbe4a13e6 --- /dev/null +++ b/roofmodel/ftw_roofmodel/geotorget.py @@ -0,0 +1,326 @@ +"""STAC catalog access: authentication, search, and asset download. + +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. + +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 +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 + +# 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". 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. +_EXTENSION_MEDIA: tuple[tuple[str, str], ...] = ( + (".copc.laz", MEDIA_COPC), + (".gpkg", MEDIA_GEOPACKAGE), + (".geojson", MEDIA_GEOJSON), + (".laz", MEDIA_LAZ), + (".las", MEDIA_LAS), + (".zip", MEDIA_ZIP), +) + + +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( + "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" + ) + + +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, 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].href + first = next(iter(self.assets.values()), None) + return first.href if first else 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( + 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") + } + 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 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, + credentials: Credentials, + session: Any = None, + base_url: str = DEFAULT_BASE_URL, + timeout: float = 60.0, + ) -> None: + 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 in (DEFAULT_BASE_URL, DEFAULT_LIDAR_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() + if not self._anonymous: + 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, + bbox: tuple[float, float, float, float], + limit: int = 20, + ) -> list[StacItem]: + """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, and the live Lantmaeteriet + service follows it -- so the caller chooses what to send and no + reprojection happens here. + """ + body = { + "collections": [collection], + "bbox": list(bbox), + "limit": limit, + } + 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): + 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 " + "that 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 + + +# 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 new file mode 100644 index 000000000..f8f764e3c --- /dev/null +++ b/roofmodel/ftw_roofmodel/pipeline.py @@ -0,0 +1,310 @@ +"""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 geotorget, pointcloud, sweref +from .buildings import ( + DEFAULT_EAVES_BUFFER_M, + DEFAULT_SEARCH_RADIUS_M, + Building, + building_from_drawn_footprint, + clip_to_footprint, + search_buildings, +) +from .geotorget import ( + COLLECTION_BUILDINGS, + 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 + +# 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.""" + + +@dataclasses.dataclass +class DerivedArray: + name: str + rated_w: float + tilt_deg: float + azimuth_deg: float + area_m2: float + segment_id: str + + def to_json(self) -> dict[str, Any]: + return { + "name": self.name, + "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), + "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, + 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, + segment_id=f"seg-{idx}", + ) + ) + return arrays + + +def load_points(data: bytes) -> Any: + """Decode a whole LAZ/LAS payload into (N, 3) SWEREF 99 TM metres. + + 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. + """ + 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( + *, + 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, + 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, + lidar_collection: str = COLLECTION_LIDAR, + bbox_epsg: int = 4326, +) -> dict[str, Any]: + """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, 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 + 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 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=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) + 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" + ) + + # 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) + 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 ({lidar_lat:.5f}, {lidar_lon:.5f}) in " + f"collection {lidar_collection!r}{hint}" + ) + + 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") + + 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 + ) + + 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" 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 + # 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, + "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, + "derived_at_ms": int(stamp.timestamp() * 1000), + } diff --git a/roofmodel/ftw_roofmodel/pointcloud.py b/roofmodel/ftw_roofmodel/pointcloud.py new file mode 100644 index 000000000..c49927dea --- /dev/null +++ b/roofmodel/ftw_roofmodel/pointcloud.py @@ -0,0 +1,245 @@ +"""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", + "copc_query_z_range", + "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 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, + 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: + 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 + except Exception as exc: + raise PointCloudError(f"COPC read failed: {exc}") from exc + return _points_from_las(points) diff --git a/roofmodel/ftw_roofmodel/segment.py b/roofmodel/ftw_roofmodel/segment.py new file mode 100644 index 000000000..5a2bc0a11 --- /dev/null +++ b/roofmodel/ftw_roofmodel/segment.py @@ -0,0 +1,251 @@ +"""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 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 (test helper).""" + return self.rated_w(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..2d5e8f296 --- /dev/null +++ b/roofmodel/ftw_roofmodel/sweref.py @@ -0,0 +1,191 @@ +"""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 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. + + 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..4e138a246 --- /dev/null +++ b/roofmodel/pyproject.toml @@ -0,0 +1,27 @@ +[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"] +test = ["pytest==8.4.2"] + +[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_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_buildings.py b/roofmodel/tests/test_buildings.py new file mode 100644 index 000000000..90805abc7 --- /dev/null +++ b/roofmodel/tests/test_buildings.py @@ -0,0 +1,236 @@ +"""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 + 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(y, x) # ring is (E, N) + 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 + 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"}, + {"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 + 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"}, + {"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 + n, e = 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 + 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) + + 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 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(): + 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 + 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() + + 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) + + +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 new file mode 100644 index 000000000..466b8fae5 --- /dev/null +++ b/roofmodel/tests/test_derive_footprint.py @@ -0,0 +1,232 @@ +"""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 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 + +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 = [] + 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={})] + + 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. + """ + 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), + ]) + 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 COLLECTION_BUILDINGS 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 + + +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/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(" 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", "rated_w", "tilt_deg", "azimuth_deg"} + assert payload["rated_w"] > 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["rated_w"] > 0 + # Must be JSON-serialisable for the subprocess contract. + json.dumps(model) + + +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( + latitude=59.33, longitude=18.07, credentials=CREDS, + client=GeotorgetClient(CREDS, session=session), radius_m=40.0, + ) + (_, body), = session.posts + # 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): + """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_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 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_source_formats.py b/roofmodel/tests/test_source_formats.py new file mode 100644 index 000000000..db41dff69 --- /dev/null +++ b/roofmodel/tests/test_source_formats.py @@ -0,0 +1,314 @@ +"""The two Lantmaeteriet products end to end, in the formats they ship in. + +Both are STAC APIs over one client and one credential. What differs is the +payload: *Byggnad Nedladdning, vektor* is GeoPackage, *Laserdata Nedladdning, +Skog* is LAZ organised as COPC. +""" + +from __future__ import annotations + +import json +import math + +import numpy as np +import pytest + +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, + MEDIA_LAZ, + Asset, + Credentials, + StacItem, +) +from ftw_roofmodel.pipeline import derive +from ftw_roofmodel.pointcloud import PointCloudError + +from .test_geopackage import build_gpkg, gpkg_blob, wkb_polygon + +STOCKHOLM = (59.33, 18.07) +N, E = sweref.wgs84_to_sweref99tm(*STOCKHOLM) + + +def square(cx, cy, w, d): + return [[(cx, cy), (cx + w, cy), (cx + w, cy + d), (cx, cy + d), (cx, cy)]] + + +def roof_face(tilt, azimuth, w, d, origin, density=8, noise=0.04, seed=1): + 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]]) + + +HOUSE = 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), +]) + + +class FakeClient: + """A Geotorget stand-in that serves typed assets.""" + + def __init__(self, building_asset=None, lidar_asset=None, payloads=None, + session=None): + self._building_asset = building_asset + self._lidar_asset = lidar_asset + self._payloads = payloads or {} + self.session = session + self.downloaded: list[str] = [] + + def search(self, collection, bbox, limit=20): + if collection == COLLECTION_BUILDINGS: + if self._building_asset is None: + return [] + return [StacItem("tile-b", collection, {"data": self._building_asset}, None, + raw={"id": "tile-b"})] + if self._lidar_asset is None: + return [] + return [StacItem("tile-l", collection, {"data": self._lidar_asset}, None, + raw={"id": "tile-l"})] + + def download(self, url): + self.downloaded.append(url) + return self._payloads[url] + + +def a_geopackage_of_two_buildings(): + return build_gpkg([ + ("house-1", "Bostad", gpkg_blob(wkb_polygon(square(E - 6, N - 3, 12, 12)))), + ("shed-1", "Komplementbyggnad", + gpkg_blob(wkb_polygon(square(E + 40, N + 40, 8, 8)))), + ]) + + +def test_buildings_come_out_of_a_geopackage_asset(): + """The normal Byggnad-vektor path: the item points at a .gpkg tile.""" + url = "https://api.lantmateriet.se/x/byggnad.gpkg" + client = FakeClient( + building_asset=Asset(url, MEDIA_GEOPACKAGE), + payloads={url: a_geopackage_of_two_buildings()}, + ) + found = search_buildings(client, latitude=STOCKHOLM[0], longitude=STOCKHOLM[1]) + + assert [b.building_id for b in found] == ["house-1", "shed-1"], "nearest first" + assert found[0].area_m2 == pytest.approx(144.0, rel=0.01) + assert found[0].properties["andamal"] == "Bostad" + 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" + doc = { + "type": "FeatureCollection", + "features": [{ + "type": "Feature", + "id": "gj-1", + "geometry": {"type": "Polygon", + "coordinates": [[list(p) for p in square(E - 6, N - 3, 12, 12)[0]]]}, + "properties": {"andamal": "Bostad"}, + }], + } + client = FakeClient( + building_asset=Asset(url, MEDIA_GEOJSON), + payloads={url: json.dumps(doc).encode()}, + ) + found = search_buildings(client, latitude=STOCKHOLM[0], longitude=STOCKHOLM[1]) + assert [b.building_id for b in found] == ["gj-1"] + + +def test_an_unreadable_building_tile_says_what_is_wrong(): + url = "https://api.lantmateriet.se/x/byggnad.gpkg" + client = FakeClient( + building_asset=Asset(url, MEDIA_GEOPACKAGE), + payloads={url: b"this is not a database"}, + ) + with pytest.raises(BuildingLookupError, match="could not be read"): + search_buildings(client, latitude=STOCKHOLM[0], longitude=STOCKHOLM[1]) + + +def test_an_item_with_no_usable_asset_is_skipped_not_fatal(): + client = FakeClient(building_asset=Asset("https://x/preview.png", "image/png")) + with pytest.raises(BuildingLookupError, match="no building footprints"): + search_buildings(client, latitude=STOCKHOLM[0], longitude=STOCKHOLM[1]) + + +# --- LiDAR: COPC windowing ------------------------------------------------- + + +@pytest.fixture +def lidar_scene(monkeypatch): + """A client whose LiDAR asset is COPC, with both read paths instrumented.""" + gpkg_url = "https://api.lantmateriet.se/x/byggnad.gpkg" + copc_url = "https://api.lantmateriet.se/x/tile.copc.laz" + client = FakeClient( + building_asset=Asset(gpkg_url, MEDIA_GEOPACKAGE), + lidar_asset=Asset(copc_url, MEDIA_COPC), + payloads={gpkg_url: a_geopackage_of_two_buildings(), copc_url: b"laz-bytes"}, + session=object(), + ) + calls: dict[str, object] = {} + + def fake_window(session, url, bounds, timeout=60.0): + calls["window"] = {"url": url, "bounds": bounds} + return HOUSE + + monkeypatch.setattr(pipeline.pointcloud, "read_copc_window", fake_window) + monkeypatch.setattr(pipeline, "load_points", lambda data: HOUSE) + return client, calls, copc_url + + +def test_a_picked_building_is_read_as_a_copc_window(lidar_scene): + """The payoff: a footprint costs a window, not a 2.5 km tile.""" + client, calls, copc_url = lidar_scene + model = derive( + latitude=STOCKHOLM[0], longitude=STOCKHOLM[1], + credentials=Credentials("u", "t"), client=client, building_id="house-1", + ) + + assert model["source"]["fetch"] == "copc-window" + assert calls["window"]["url"] == copc_url + min_x, min_y, max_x, max_y = calls["window"]["bounds"] + # The footprint is 12 m square around the site, plus the eaves buffer. + assert min_x == pytest.approx(E - 7, abs=0.5) + assert max_x == pytest.approx(E + 7, abs=0.5) + assert copc_url not in client.downloaded, "the tile was never downloaded whole" + + +def test_without_a_picked_building_the_tile_is_read_whole(lidar_scene): + """No footprint means no window to ask for.""" + client, calls, copc_url = lidar_scene + model = derive( + latitude=STOCKHOLM[0], longitude=STOCKHOLM[1], + credentials=Credentials("u", "t"), client=client, + ) + assert model["source"]["fetch"] == "whole-tile" + assert "window" not in calls + assert copc_url in client.downloaded + + +def test_a_host_without_range_support_falls_back_to_the_whole_tile(monkeypatch, lidar_scene): + """Best-effort: the operator still gets their roof, just slower.""" + client, _, copc_url = lidar_scene + + def refuses(session, url, bounds, timeout=60.0): + raise PointCloudError("the LiDAR host does not support range requests") + + monkeypatch.setattr(pipeline.pointcloud, "read_copc_window", refuses) + model = derive( + latitude=STOCKHOLM[0], longitude=STOCKHOLM[1], + credentials=Credentials("u", "t"), client=client, building_id="house-1", + ) + assert model["source"]["fetch"] == "whole-tile" + assert copc_url in client.downloaded + assert model["arrays"], "and the roof still comes out" + + +def test_a_plain_laz_asset_is_read_whole_even_with_a_building(monkeypatch): + """Only COPC can be windowed; plain LAZ has no index to seek with.""" + gpkg_url = "https://api.lantmateriet.se/x/byggnad.gpkg" + laz_url = "https://api.lantmateriet.se/x/tile.laz" + client = FakeClient( + building_asset=Asset(gpkg_url, MEDIA_GEOPACKAGE), + lidar_asset=Asset(laz_url, MEDIA_LAZ), + payloads={gpkg_url: a_geopackage_of_two_buildings(), laz_url: b"laz-bytes"}, + session=object(), + ) + monkeypatch.setattr(pipeline, "load_points", lambda data: HOUSE) + called = [] + monkeypatch.setattr(pipeline.pointcloud, "read_copc_window", + lambda *a, **k: called.append(1)) + + model = derive( + latitude=STOCKHOLM[0], longitude=STOCKHOLM[1], + credentials=Credentials("u", "t"), client=client, building_id="house-1", + ) + assert model["source"]["fetch"] == "whole-tile" + assert not called diff --git a/roofmodel/tests/test_sweref.py b/roofmodel/tests/test_sweref.py new file mode 100644 index 000000000..1cd6dfa35 --- /dev/null +++ b/roofmodel/tests/test_sweref.py @@ -0,0 +1,148 @@ +"""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_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 + 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 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/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 1e46ac6d0..fc50c95ad 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)}}},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 new file mode 100644 index 000000000..2c85b53a0 --- /dev/null +++ b/web/vendor/maplibre/maplibre-gl.css @@ -0,0 +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%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 new file mode 100644 index 000000000..16c40880e --- /dev/null +++ b/web/vendor/maplibre/maplibre-gl.mjs @@ -0,0 +1,806 @@ +/** +* MapLibre GL JS +* @license 3-Clause BSD. Full text of license: https://github.com/maplibre/maplibre-gl-js/blob/v6.7.0/LICENSE.txt +*/ +import{$ as e,$n as t,$r as n,$t as r,A as i,Ai as a,An as o,Ar as s,At as c,Bi as l,Bn as u,Br as d,Bt as f,C as p,Ci as m,Cn as h,Cr as g,Ct as _,D as v,Di as y,Dn as b,Dr as x,Dt as S,E as C,Ei as w,En as T,Er as E,Et as D,F as ee,Fi as O,Fn as k,Fr as A,Ft as j,Gn as M,Gr as te,Gt as ne,H as re,Hn as ie,Hr as N,Ht as ae,I as oe,Ii as se,In as ce,Ir as le,It as ue,J as de,Jn as fe,Jr as pe,Jt as me,K as he,Kn as ge,Kr as _e,Kt as ve,L as ye,Li as be,Ln as xe,Lr as Se,Lt as Ce,Mi as we,Mn as Te,Mr as Ee,Mt as De,N as Oe,Ni as ke,Nn as Ae,Nr as je,Nt as P,O as Me,Oi as Ne,On as Pe,Or as Fe,Ot as Ie,Pi as Le,Pn as Re,Pr as ze,Pt as F,Q as Be,Qn as Ve,Qr as He,Qt as Ue,R as We,Ri as Ge,Rn as Ke,Rr as I,Rt as qe,S as Je,Si as Ye,Sn as Xe,Sr as Ze,T as Qe,Ti as $e,Tn as et,Tr as tt,Tt as nt,U as rt,Un as it,Ur as at,Ut as ot,V as st,Vn as ct,Vr as lt,Vt as ut,Wn as dt,Wr as ft,Wt as pt,X as mt,Xn as L,Xr as ht,Xt as gt,Y as _t,Yn as vt,Yr as yt,Yt as bt,Z as xt,Zn as St,Zr as Ct,Zt as wt,_ as Tt,_i as Et,_n as R,_r as Dt,_t as Ot,a as kt,ai as At,an as jt,ar as z,at as Mt,b as Nt,bi as Pt,bn as Ft,br as It,bt as Lt,ci as Rt,cn as zt,cr as Bt,ct as Vt,di as Ht,dn as Ut,dt as Wt,ei as Gt,en as Kt,er as qt,et as Jt,f as Yt,fi as Xt,fn as Zt,fr as Qt,g as $t,gi as en,gn as tn,gr as nn,h as rn,hi as an,hn as on,hr as sn,ht as cn,ii as ln,ir as un,it as dn,j as fn,ji as pn,jn as mn,jr as hn,jt as gn,k as _n,ki as vn,kn as yn,kr as bn,kt as xn,li as Sn,ln as Cn,lr as wn,lt as Tn,mi as En,mr as Dn,mt as On,ni as kn,nn as An,nr as jn,nt as Mn,oi as Nn,on as Pn,or as Fn,ot as In,pi as Ln,pn as Rn,pr as zn,pt as Bn,qn as Vn,qr as Hn,qt as Un,r as Wn,ri as Gn,rn as Kn,rr as qn,rt as B,s as Jn,si as Yn,sr as Xn,st as Zn,t as Qn,ti as $n,tn as er,tr,tt as nr,u as rr,ui as ir,un as ar,ur as or,ut as V,v as sr,vi as cr,vn as lr,vr as ur,vt as dr,w as fr,wi as pr,wn as mr,wr as hr,wt as gr,x as _r,xi as vr,xn as H,xr as yr,xt as br,y as xr,yi as Sr,yn as Cr,yr as wr,yt as Tr,z as Er,zi as Dr,zr as Or,zt as kr}from"./maplibre-gl-shared.mjs";var Ar=`6.7.0`;function jr(){var e=new be(4);return be!=Float32Array&&(e[1]=0,e[2]=0),e[0]=1,e[3]=1,e}function Mr(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 Nr(e){return e[0]*e[3]-e[2]*e[1]}function Pr(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 Fr,Ir,Lr;const Rr={frame(e,t,n,r){let i=r||window,a=i.requestAnimationFrame(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 +uniform sampler2D u_terrain;uniform float u_terrain_dim;uniform mat4 u_terrain_matrix;uniform vec4 u_terrain_unpack;uniform float u_terrain_exaggeration;uniform highp sampler2D u_depth; +#endif +const highp vec4 bitSh=vec4(256.*256.*256.,256.*256.,256.,1.);const highp vec4 bitShifts=vec4(1.)/bitSh;highp float unpack(highp vec4 color) {return dot(color,bitShifts);}highp float depthOpacity(vec3 frag) { +#ifdef TERRAIN3D +highp float d=unpack(texture(u_depth,frag.xy*0.5+0.5))+0.0001-frag.z;return 1.0-max(0.0,min(1.0,-d*500.0)); +#else +return 1.0; +#endif +}float calculate_visibility(vec4 pos) { +#ifdef TERRAIN3D +vec3 frag=pos.xyz/pos.w;highp float d=depthOpacity(frag);if (d > 0.95) return 1.0;return (d+depthOpacity(frag+vec3(0.0,0.01,0.0)))/2.0; +#else +return 1.0; +#endif +}float ele(ivec2 pos) { +#ifdef TERRAIN3D +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 +}float get_elevation(vec2 pos) { +#ifdef TERRAIN3D +#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);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;`,Us=`uniform vec4 u_color;uniform float u_opacity;void main() {fragColor=u_color*u_opacity; +#ifdef OVERDRAW_INSPECTOR +fragColor=vec4(1.0); +#endif +}`,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 +}`,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 +#pragma maplibre: define lowp float opacity +#pragma maplibre: define highp vec4 stroke_color +#pragma maplibre: define mediump float stroke_width +#pragma maplibre: define lowp float stroke_opacity +void main() { +#pragma maplibre: initialize highp vec4 color +#pragma maplibre: initialize mediump float radius +#pragma maplibre: initialize lowp float blur +#pragma maplibre: initialize lowp float opacity +#pragma maplibre: initialize highp vec4 stroke_color +#pragma maplibre: initialize mediump float stroke_width +#pragma maplibre: initialize lowp float stroke_opacity +vec2 extrude=v_data.xy;float extrude_length=length(extrude);float antialiased_blur=v_data.z;float opacity_t=smoothstep(0.0,antialiased_blur,extrude_length-1.0);float color_t=stroke_width < 0.01 ? 0.0 : smoothstep(antialiased_blur,0.0,extrude_length-radius/(radius+stroke_width));fragColor=v_visibility*opacity_t*mix(color*opacity,stroke_color*stroke_opacity,color_t);const float epsilon=0.5/255.0;if (fragColor.r < epsilon && fragColor.g < epsilon && fragColor.b < epsilon && fragColor.a < epsilon) {discard;} +#ifdef OVERDRAW_INSPECTOR +fragColor=vec4(1.0); +#endif +}`,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 +#pragma maplibre: define lowp float opacity +#pragma maplibre: define highp vec4 stroke_color +#pragma maplibre: define mediump float stroke_width +#pragma maplibre: define lowp float stroke_opacity +void main(void) { +#pragma maplibre: initialize highp vec4 color +#pragma maplibre: initialize mediump float radius +#pragma maplibre: initialize lowp float blur +#pragma maplibre: initialize lowp float opacity +#pragma maplibre: initialize highp vec4 stroke_color +#pragma maplibre: initialize mediump float stroke_width +#pragma maplibre: initialize lowp float stroke_opacity +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 +float angle_scale=u_globe_extrude_scale;vec2 corner_position=circle_center;if (u_scale_with_map) {angle_scale*=(radius+stroke_width);corner_position+=extrude*u_extrude_scale*(radius+stroke_width);} else { +#ifdef GLOBE +vec4 projected_center=interpolateProjection(circle_center,center_vector,ele); +#else +vec4 projected_center=projectTileWithElevation(circle_center,ele); +#endif +corner_position+=extrude*u_extrude_scale*(radius+stroke_width)*(projected_center.w/u_camera_to_center_distance);angle_scale*=(radius+stroke_width)*(projected_center.w/u_camera_to_center_distance);} +#ifdef GLOBE +vec2 angles=extrude*angle_scale;vec3 corner_vector=globeRotateVector(center_vector,angles);gl_Position=interpolateProjection(corner_position,corner_vector,ele); +#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);}`,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() { +#pragma maplibre: initialize highp float weight +float d=-0.5*3.0*3.0*dot(v_extrude,v_extrude);float val=weight*u_intensity*GAUSS_COEF*exp(d);fragColor=vec4(val,1.0,1.0,1.0); +#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 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; +#define GAUSS_COEF 0.3989422804014327 +void main(void) { +#pragma maplibre: initialize highp float weight +#pragma maplibre: initialize mediump float radius +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: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: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) {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: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:Y(`#pragma maplibre: define highp vec4 color +#pragma maplibre: define lowp float opacity +void main() { +#pragma maplibre: initialize highp vec4 color +#pragma maplibre: initialize lowp float opacity +fragColor=color*opacity; +#ifdef OVERDRAW_INSPECTOR +fragColor=vec4(1.0); +#endif +}`,`uniform vec2 u_fill_translate;layout(location=0) in vec2 a_pos; +#pragma maplibre: define highp vec4 color +#pragma maplibre: define lowp float opacity +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:Y(`in vec2 v_pos; +#ifdef GLOBE +in float v_depth; +#endif +#pragma maplibre: define highp vec4 outline_color +#pragma maplibre: define lowp float opacity +void main() { +#pragma maplibre: initialize highp vec4 outline_color +#pragma maplibre: initialize lowp float opacity +float dist=length(v_pos-gl_FragCoord.xy);float alpha=1.0-smoothstep(0.0,1.0,dist);fragColor=outline_color*(alpha*opacity); +#ifdef GLOBE +if (v_depth > 1.0) {discard;} +#endif +#ifdef OVERDRAW_INSPECTOR +fragColor=vec4(1.0); +#endif +}`,`uniform vec2 u_world;uniform vec2 u_fill_translate;layout(location=0) in vec2 a_pos;out vec2 v_pos; +#ifdef GLOBE +out float v_depth; +#endif +#pragma maplibre: define highp vec4 outline_color +#pragma maplibre: define lowp float opacity +void main() { +#pragma maplibre: initialize highp vec4 outline_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);v_pos=(gl_Position.xy/gl_Position.w+1.0)/2.0*u_world; +#ifdef GLOBE +v_depth=gl_Position.z/gl_Position.w; +#endif +}`),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 +#pragma maplibre: define lowp float opacity +#pragma maplibre: define lowp vec4 pattern_from +#pragma maplibre: define lowp vec4 pattern_to +void main() { +#pragma maplibre: initialize lowp float opacity +#pragma maplibre: initialize mediump vec4 pattern_from +#pragma maplibre: initialize mediump vec4 pattern_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;vec2 imagecoord=mod(v_pos_a,1.0);vec2 pos=mix(pattern_tl_a/u_texsize,pattern_br_a/u_texsize,imagecoord);vec4 color1=texture(u_image,pos);vec2 imagecoord_b=mod(v_pos_b,1.0);vec2 pos2=mix(pattern_tl_b/u_texsize,pattern_br_b/u_texsize,imagecoord_b);vec4 color2=texture(u_image,pos2);float dist=length(v_pos-gl_FragCoord.xy);float alpha=1.0-smoothstep(0.0,1.0,dist);fragColor=mix(color1,color2,u_fade)*alpha*opacity; +#ifdef GLOBE +if (v_depth > 1.0) {discard;} +#endif +#ifdef OVERDRAW_INSPECTOR +fragColor=vec4(1.0); +#endif +}`,`uniform vec2 u_world;uniform vec2 u_pixel_coord_upper;uniform vec2 u_pixel_coord_lower;uniform vec3 u_scale;uniform vec2 u_fill_translate;layout(location=0) in vec2 a_pos;out vec2 v_pos_a;out vec2 v_pos_b;out vec2 v_pos; +#ifdef GLOBE +out float v_depth; +#endif +#pragma maplibre: define lowp float opacity +#pragma maplibre: define lowp vec4 pattern_from +#pragma maplibre: define lowp vec4 pattern_to +#pragma maplibre: define lowp float pixel_ratio_from +#pragma maplibre: define lowp float pixel_ratio_to +void main() { +#pragma maplibre: initialize lowp float opacity +#pragma maplibre: initialize mediump vec4 pattern_from +#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 tileRatio=u_scale.x;float fromScale=u_scale.y;float toScale=u_scale.z;gl_Position=projectTile(a_pos+u_fill_translate,a_pos);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;v_pos_a=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,fromScale*display_size_a,tileRatio,a_pos);v_pos_b=get_pattern_pos(u_pixel_coord_upper,u_pixel_coord_lower,toScale*display_size_b,tileRatio,a_pos);v_pos=(gl_Position.xy/gl_Position.w+1.0)/2.0*u_world; +#ifdef GLOBE +v_depth=gl_Position.z/gl_Position.w; +#endif +}`),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; +#pragma maplibre: define lowp float opacity +#pragma maplibre: define lowp vec4 pattern_from +#pragma maplibre: define lowp vec4 pattern_to +void main() { +#pragma maplibre: initialize lowp float opacity +#pragma maplibre: initialize mediump vec4 pattern_from +#pragma maplibre: initialize mediump vec4 pattern_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;vec2 imagecoord=mod(v_pos_a,1.0);vec2 pos=mix(pattern_tl_a/u_texsize,pattern_br_a/u_texsize,imagecoord);vec4 color1=texture(u_image,pos);vec2 imagecoord_b=mod(v_pos_b,1.0);vec2 pos2=mix(pattern_tl_b/u_texsize,pattern_br_b/u_texsize,imagecoord_b);vec4 color2=texture(u_image,pos2);fragColor=mix(color1,color2,u_fade)*opacity; +#ifdef OVERDRAW_INSPECTOR +fragColor=vec4(1.0); +#endif +}`,`uniform vec2 u_pixel_coord_upper;uniform vec2 u_pixel_coord_lower;uniform vec3 u_scale;uniform vec2 u_fill_translate;layout(location=0) in vec2 a_pos;out vec2 v_pos_a;out vec2 v_pos_b; +#pragma maplibre: define lowp float opacity +#pragma maplibre: define lowp vec4 pattern_from +#pragma maplibre: define lowp vec4 pattern_to +#pragma maplibre: define lowp float pixel_ratio_from +#pragma maplibre: define lowp float pixel_ratio_to +void main() { +#pragma maplibre: initialize lowp float opacity +#pragma maplibre: initialize mediump vec4 pattern_from +#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: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 ivec4 a_normal_ed; +#ifdef TERRAIN3D +layout(location=2) in vec2 a_centroid; +#endif +out vec4 v_color; +#pragma maplibre: define highp float base +#pragma maplibre: define highp float height +#pragma maplibre: define highp vec4 color +void main() { +#pragma maplibre: initialize highp float base +#pragma maplibre: initialize highp float height +#pragma maplibre: initialize highp vec4 color +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=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 +gl_Position=u_projection_matrix*vec4(posInTile,elevation,1.0); +#endif +float colorvalue=color.r*0.2126+color.g*0.7152+color.b*0.0722;v_color=vec4(0.0,0.0,0.0,1.0);vec4 ambientlight=vec4(0.03,0.03,0.03,1.0);color+=ambientlight;vec3 normalForLighting=normal/16384.0;float directional=clamp(dot(normalForLighting,u_lightpos),0.0,1.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: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 +#pragma maplibre: define lowp vec4 pattern_to +#pragma maplibre: define lowp float pixel_ratio_from +#pragma maplibre: define lowp float pixel_ratio_to +void main() { +#pragma maplibre: initialize lowp float base +#pragma maplibre: initialize lowp float height +#pragma maplibre: initialize mediump vec4 pattern_from +#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;vec2 imagecoord=mod(v_pos_a,1.0);vec2 pos=mix(pattern_tl_a/u_texsize,pattern_br_a/u_texsize,imagecoord);vec4 color1=texture(u_image,pos);vec2 imagecoord_b=mod(v_pos_b,1.0);vec2 pos2=mix(pattern_tl_b/u_texsize,pattern_br_b/u_texsize,imagecoord_b);vec4 color2=texture(u_image,pos2);vec4 mixedColor=mix(color1,color2,u_fade);fragColor=mixedColor*v_lighting; +#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 ivec4 a_normal_ed; +#ifdef TERRAIN3D +layout(location=2) in vec2 a_centroid; +#endif +#ifdef GLOBE +out vec3 v_sphere_pos; +#endif +out vec2 v_pos_a;out vec2 v_pos_b;out vec4 v_lighting; +#pragma maplibre: define lowp float base +#pragma maplibre: define lowp float height +#pragma maplibre: define lowp vec4 pattern_from +#pragma maplibre: define lowp vec4 pattern_to +#pragma maplibre: define lowp float pixel_ratio_from +#pragma maplibre: define lowp float pixel_ratio_to +void main() { +#pragma maplibre: initialize lowp float base +#pragma maplibre: initialize lowp float height +#pragma maplibre: initialize mediump vec4 pattern_from +#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=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=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=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: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(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: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 +#define IGOR 2 +#define MULTIDIRECTIONAL 3 +#define BASIC 4 +float get_aspect(vec2 deriv){return deriv.x !=0.0 ? atan(deriv.y,-deriv.x) : PI/2.0*(deriv.y > 0.0 ? 1.0 :-1.0);}void igor_hillshade(vec2 deriv){deriv=deriv*u_exaggeration*2.0;float aspect=get_aspect(deriv);float azimuth=u_azimuths[0]+PI;float slope_stength=atan(length(deriv))*2.0/PI;float aspect_strength=1.0-abs(mod((aspect+azimuth)/PI+0.5,2.0)-1.0);float shadow_strength=slope_stength*aspect_strength;float highlight_strength=slope_stength*(1.0-aspect_strength);fragColor=u_shadows[0]*shadow_strength+u_highlights[0]*highlight_strength;}void standard_hillshade(vec2 deriv){float azimuth=u_azimuths[0]+PI;float slope=atan(0.625*length(deriv));float aspect=get_aspect(deriv);float intensity=u_exaggeration;float base=1.875-intensity*1.75;float maxValue=0.5*PI;float scaledSlope=intensity !=0.5 ? ((pow(base,slope)-1.0)/(pow(base,maxValue)-1.0))*maxValue : slope;float accent=cos(scaledSlope);vec4 accent_color=(1.0-accent)*u_accent*clamp(intensity*2.0,0.0,1.0);float shade=abs(mod((aspect+azimuth)/PI+0.5,2.0)-1.0);vec4 shade_color=mix(u_shadows[0],u_highlights[0],shade)*sin(scaledSlope)*clamp(intensity*2.0,0.0,1.0);fragColor=accent_color*(1.0-shade_color.a)+shade_color;}void basic_hillshade(vec2 deriv){deriv=deriv*u_exaggeration*2.0;float azimuth=u_azimuths[0]+PI;float cos_az=cos(azimuth);float sin_az=sin(azimuth);float cos_alt=cos(u_altitudes[0]);float sin_alt=sin(u_altitudes[0]);float cang=(sin_alt-(deriv.y*cos_az*cos_alt-deriv.x*sin_az*cos_alt))/sqrt(1.0+dot(deriv,deriv));float shade=clamp(cang,0.0,1.0);if(shade > 0.5){fragColor=u_highlights[0]*(2.0*shade-1.0);}else +{fragColor=u_shadows[0]*(1.0-2.0*shade);}}void multidirectional_hillshade(vec2 deriv){deriv=deriv*u_exaggeration*2.0;fragColor=vec4(0,0,0,0);for(int i=0; i < NUM_ILLUMINATION_SOURCES; i++){float cos_alt=cos(u_altitudes[i]);float sin_alt=sin(u_altitudes[i]);float cos_az=-cos(u_azimuths[i]);float sin_az=-sin(u_azimuths[i]);float cang=(sin_alt-(deriv.y*cos_az*cos_alt-deriv.x*sin_az*cos_alt))/sqrt(1.0+dot(deriv,deriv));float shade=clamp(cang,0.0,1.0);if(shade > 0.5){fragColor+=u_highlights[i]*(2.0*shade-1.0)/float(NUM_ILLUMINATION_SOURCES);}else +{fragColor+=u_shadows[i]*(1.0-2.0*shade)/float(NUM_ILLUMINATION_SOURCES);}}}void combined_hillshade(vec2 deriv){deriv=deriv*u_exaggeration*2.0;float azimuth=u_azimuths[0]+PI;float cos_az=cos(azimuth);float sin_az=sin(azimuth);float cos_alt=cos(u_altitudes[0]);float sin_alt=sin(u_altitudes[0]);float cang=acos((sin_alt-(deriv.y*cos_az*cos_alt-deriv.x*sin_az*cos_alt))/sqrt(1.0+dot(deriv,deriv)));cang=clamp(cang,0.0,PI/2.0);float shade=cang*atan(length(deriv))*4.0/PI/PI;float highlight=(PI/2.0-cang)*atan(length(deriv))*4.0/PI/PI;fragColor=u_shadows[0]*shade+u_highlights[0]*highlight;}void main() {vec4 pixel=texture(u_image,v_pos);float scaleFactor=cos(radians((u_latrange[0]-u_latrange[1])*(1.0-v_pos.y)+u_latrange[1]));vec2 deriv=((pixel.rg*8.0)-4.0)/scaleFactor;if (u_method==BASIC) {basic_hillshade(deriv);} else if (u_method==COMBINED) {combined_hillshade(deriv);} else if (u_method==IGOR) {igor_hillshade(deriv);} else if (u_method==MULTIDIRECTIONAL) {multidirectional_hillshade(deriv);} else if (u_method==STANDARD) {standard_hillshade(deriv);} else {standard_hillshade(deriv);} +#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: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 +#pragma maplibre: define highp vec4 color +#pragma maplibre: define lowp float blur +#pragma maplibre: define lowp float opacity +void main() { +#pragma maplibre: initialize highp vec4 color +#pragma maplibre: initialize lowp float blur +#pragma maplibre: initialize lowp float 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 +#ifdef OVERDRAW_INSPECTOR +fragColor=vec4(1.0); +#endif +}`,` +#define scale 0.015873016 +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 +#pragma maplibre: define highp vec4 color +#pragma maplibre: define lowp float blur +#pragma maplibre: define lowp float opacity +#pragma maplibre: define mediump float gapwidth +#pragma maplibre: define lowp float offset +#pragma maplibre: define mediump float width +void main() { +#pragma maplibre: initialize highp vec4 color +#pragma maplibre: initialize lowp float blur +#pragma maplibre: initialize lowp float opacity +#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=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 +#ifdef TERRAIN3D +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: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 +#pragma maplibre: define lowp float blur +#pragma maplibre: define lowp float opacity +void main() { +#pragma maplibre: initialize lowp float blur +#pragma maplibre: initialize lowp float 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 +#ifdef OVERDRAW_INSPECTOR +fragColor=vec4(1.0); +#endif +}`,` +#define scale 0.015873016 +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 +#pragma maplibre: define lowp float blur +#pragma maplibre: define lowp float opacity +#pragma maplibre: define mediump float gapwidth +#pragma maplibre: define lowp float offset +#pragma maplibre: define mediump float width +void main() { +#pragma maplibre: initialize lowp float blur +#pragma maplibre: initialize lowp float opacity +#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=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 +#ifdef TERRAIN3D +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: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; +#ifdef GLOBE +in float v_depth; +#endif +#pragma maplibre: define lowp vec4 pattern_from +#pragma maplibre: define lowp vec4 pattern_to +#pragma maplibre: define lowp float pixel_ratio_from +#pragma maplibre: define lowp float pixel_ratio_to +#pragma maplibre: define lowp float blur +#pragma maplibre: define lowp float opacity +void main() { +#pragma maplibre: initialize mediump vec4 pattern_from +#pragma maplibre: initialize mediump vec4 pattern_to +#pragma maplibre: initialize lowp float pixel_ratio_from +#pragma maplibre: initialize lowp float pixel_ratio_to +#pragma maplibre: initialize lowp float blur +#pragma maplibre: initialize lowp float 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 +#ifdef OVERDRAW_INSPECTOR +fragColor=vec4(1.0); +#endif +}`,` +#define scale 0.015873016 +#define LINE_DISTANCE_SCALE 2.0 +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 +#pragma maplibre: define lowp float blur +#pragma maplibre: define lowp float opacity +#pragma maplibre: define lowp float offset +#pragma maplibre: define mediump float gapwidth +#pragma maplibre: define mediump float width +#pragma maplibre: define lowp float floorwidth +#pragma maplibre: define lowp vec4 pattern_from +#pragma maplibre: define lowp vec4 pattern_to +#pragma maplibre: define lowp float pixel_ratio_from +#pragma maplibre: define lowp float pixel_ratio_to +void main() { +#pragma maplibre: initialize lowp float blur +#pragma maplibre: initialize lowp float opacity +#pragma maplibre: initialize lowp float offset +#pragma maplibre: initialize mediump float gapwidth +#pragma maplibre: initialize mediump float width +#pragma maplibre: initialize lowp float floorwidth +#pragma maplibre: initialize mediump vec4 pattern_from +#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=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 +#ifdef TERRAIN3D +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: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 +#pragma maplibre: define highp vec4 color +#pragma maplibre: define lowp float blur +#pragma maplibre: define lowp float opacity +#pragma maplibre: define mediump float width +#pragma maplibre: define lowp float floorwidth +#pragma maplibre: define mediump vec4 dasharray_from +#pragma maplibre: define mediump vec4 dasharray_to +void main() { +#pragma maplibre: initialize highp vec4 color +#pragma maplibre: initialize lowp float blur +#pragma maplibre: initialize lowp float opacity +#pragma maplibre: initialize mediump float width +#pragma maplibre: initialize lowp float floorwidth +#pragma maplibre: initialize mediump vec4 dasharray_from +#pragma maplibre: initialize mediump vec4 dasharray_to +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 +#ifdef OVERDRAW_INSPECTOR +fragColor=vec4(1.0); +#endif +}`,` +#define scale 0.015873016 +#define LINE_DISTANCE_SCALE 2.0 +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 +#pragma maplibre: define highp vec4 color +#pragma maplibre: define lowp float blur +#pragma maplibre: define lowp float opacity +#pragma maplibre: define mediump float gapwidth +#pragma maplibre: define lowp float offset +#pragma maplibre: define mediump float width +#pragma maplibre: define lowp float floorwidth +#pragma maplibre: define mediump vec4 dasharray_from +#pragma maplibre: define mediump vec4 dasharray_to +void main() { +#pragma maplibre: initialize highp vec4 color +#pragma maplibre: initialize lowp float blur +#pragma maplibre: initialize lowp float opacity +#pragma maplibre: initialize mediump float gapwidth +#pragma maplibre: initialize lowp float offset +#pragma maplibre: initialize mediump float width +#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=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 +#ifdef TERRAIN3D +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: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 +#pragma maplibre: define lowp float blur +#pragma maplibre: define lowp float opacity +#pragma maplibre: define mediump float width +#pragma maplibre: define lowp float floorwidth +#pragma maplibre: define mediump vec4 dasharray_from +#pragma maplibre: define mediump vec4 dasharray_to +void main() { +#pragma maplibre: initialize lowp float blur +#pragma maplibre: initialize lowp float opacity +#pragma maplibre: initialize mediump float width +#pragma maplibre: initialize lowp float floorwidth +#pragma maplibre: initialize mediump vec4 dasharray_from +#pragma maplibre: initialize mediump vec4 dasharray_to +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 +#ifdef OVERDRAW_INSPECTOR +fragColor=vec4(1.0); +#endif +}`,` +#define scale 0.015873016 +#define LINE_DISTANCE_SCALE 2.0 +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 +#pragma maplibre: define lowp float blur +#pragma maplibre: define lowp float opacity +#pragma maplibre: define mediump float gapwidth +#pragma maplibre: define lowp float offset +#pragma maplibre: define mediump float width +#pragma maplibre: define lowp float floorwidth +#pragma maplibre: define mediump vec4 dasharray_from +#pragma maplibre: define mediump vec4 dasharray_to +void main() { +#pragma maplibre: initialize lowp float blur +#pragma maplibre: initialize lowp float opacity +#pragma maplibre: initialize mediump float gapwidth +#pragma maplibre: initialize lowp float offset +#pragma maplibre: initialize mediump float width +#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=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 +#ifdef TERRAIN3D +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: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: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 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) {texturePos.y=0.0;}if (a_pos.y > 32766.5) {texturePos.y=1.0;} +#endif +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 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=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: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 +#pragma maplibre: define lowp float halo_width +#pragma maplibre: define lowp float halo_blur +void main() { +#pragma maplibre: initialize highp vec4 fill_color +#pragma maplibre: initialize highp vec4 halo_color +#pragma maplibre: initialize lowp float halo_width +#pragma maplibre: initialize lowp float halo_blur +float EDGE_GAMMA=0.105/u_device_pixel_ratio;vec2 tex=v_data0.xy;float gamma_scale=v_data1.x;float size=v_data1.y;float total_opacity=v_data1[2];float fontScale=u_is_text ? size/24.0 : size;highp float gamma=EDGE_GAMMA/(fontScale*u_gamma_scale);lowp float inner_edge=(256.0-64.0)/256.0;lowp float dist=texture(u_texture,tex).a;lowp vec4 color_alpha_out_text,color_alpha_out_halo;if (u_is_plain){highp float gamma_scaled=gamma*gamma_scale;highp float alpha=smoothstep(inner_edge-gamma_scaled,inner_edge+gamma_scaled,dist);color_alpha_out_text=total_opacity*alpha*fill_color;}if (u_is_halo) {float gamma_halo=(halo_blur*1.19/SDF_PX+EDGE_GAMMA)/(fontScale*u_gamma_scale);float inner_edge_halo=inner_edge+gamma_halo*gamma_scale;highp float gamma_scaled_halo=gamma_halo*gamma_scale;highp float alpha_halo=smoothstep(inner_edge_halo-gamma_scaled_halo,inner_edge_halo+gamma_scaled_halo,dist);highp float halo_edge=(6.0-halo_width/fontScale)/SDF_PX;alpha_halo= min(smoothstep(halo_edge-gamma_scaled_halo,halo_edge+gamma_scaled_halo,dist),1.0-alpha_halo);color_alpha_out_halo=total_opacity*alpha_halo*halo_color;}if (u_is_plain && u_is_halo) {fragColor=color_alpha_out_text+(1.-color_alpha_out_text.a)*color_alpha_out_halo;} else if (u_is_halo){fragColor=color_alpha_out_halo;} else {fragColor=color_alpha_out_text;} +#ifdef OVERDRAW_INSPECTOR +fragColor=vec4(1.0); +#endif +}`,`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 +#pragma maplibre: define lowp float halo_width +#pragma maplibre: define lowp float halo_blur +void main() { +#pragma maplibre: initialize highp vec4 fill_color +#pragma maplibre: initialize highp vec4 halo_color +#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=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: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; +#pragma maplibre: define highp vec4 fill_color +#pragma maplibre: define highp vec4 halo_color +#pragma maplibre: define lowp float halo_width +#pragma maplibre: define lowp float halo_blur +void main() { +#pragma maplibre: initialize highp vec4 fill_color +#pragma maplibre: initialize highp vec4 halo_color +#pragma maplibre: initialize lowp float halo_width +#pragma maplibre: initialize lowp float halo_blur +float total_opacity=v_data1[2];if (v_is_sdf==ICON) {vec2 tex_icon=v_data0.zw;fragColor=texture(u_texture_icon,tex_icon)*total_opacity; +#ifdef OVERDRAW_INSPECTOR +fragColor=vec4(1.0); +#endif +return;}vec2 tex=v_data0.xy;float EDGE_GAMMA=0.105/u_device_pixel_ratio;float gamma_scale=v_data1.x;float size=v_data1.y;float fontScale=size/24.0;highp float gamma=EDGE_GAMMA/(fontScale*u_gamma_scale);lowp float buff=(256.0-64.0)/256.0;lowp float dist=texture(u_texture,tex).a;lowp vec4 color_alpha_out,color_alpha_out_halo;if (u_is_text) {highp float gamma_scaled=gamma*gamma_scale;highp float alpha=smoothstep(buff-gamma_scaled,buff+gamma_scaled,dist);color_alpha_out=fill_color*(alpha*total_opacity);}if (u_is_halo) {highp float gamma_halo=(halo_blur*1.19/SDF_PX+EDGE_GAMMA)/(fontScale*u_gamma_scale);lowp float buff_halo=(6.0-halo_width/fontScale)/SDF_PX;highp float gamma_scaled_halo=gamma_halo*gamma_scale;highp float alpha_halo=smoothstep(buff_halo-gamma_scaled_halo,buff_halo+gamma_scaled_halo,dist);color_alpha_out_halo=halo_color*(alpha_halo*total_opacity);}if (u_is_text && u_is_halo) {fragColor=color_alpha_out+(1.-color_alpha_out.a)*color_alpha_out_halo;} else if (u_is_halo) {fragColor=color_alpha_out_halo;} else {fragColor=color_alpha_out;} +#ifdef OVERDRAW_INSPECTOR +fragColor=vec4(1.0); +#endif +}`,`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 +#pragma maplibre: define lowp float halo_width +#pragma maplibre: define lowp float halo_blur +void main() { +#pragma maplibre: initialize highp vec4 fill_color +#pragma maplibre: initialize highp vec4 halo_color +#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=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: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: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 +uniform ${n} ${r} u_${i}; +#endif +`:` +#ifdef HAS_UNIFORM_u_${i} + ${n} ${r} ${i} = u_${i}; +#endif +`)),t=t.replace(n,(e,t,n,r,i)=>{let a=r===`float`?`vec2`:`vec4`,o=i.match(/color/)?`color`:a;return c[i]?t===`define`?` +#ifndef HAS_UNIFORM_u_${i} +uniform lowp float u_${i}_t; +layout(location = ${s++}) in ${n} ${a} a_${i}; +out ${n} ${r} ${i}; +#else +uniform ${n} ${r} u_${i}; +#endif +`:o===`vec4`?` +#ifndef HAS_UNIFORM_u_${i} + ${i} = a_${i}; +#else + ${n} ${r} ${i} = u_${i}; +#endif +`:` +#ifndef HAS_UNIFORM_u_${i} + ${i} = unpack_mix_${o}(a_${i}, u_${i}_t); +#else + ${n} ${r} ${i} = u_${i}; +#endif +`:t===`define`?` +#ifndef HAS_UNIFORM_u_${i} +uniform lowp float u_${i}_t; +layout(location = ${s++}) in ${n} ${a} a_${i}; +#else +uniform ${n} ${r} u_${i}; +#endif +`:o===`vec4`?` +#ifndef HAS_UNIFORM_u_${i} + ${n} ${r} ${i} = a_${i}; +#else + ${n} ${r} ${i} = u_${i}; +#endif +`:` +#ifndef HAS_UNIFORM_u_${i} + ${n} ${r} ${i} = unpack_mix_${o}(a_${i}, u_${i}_t); +#else + ${n} ${r} ${i} = u_${i}; +#endif +`}),{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 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)