diff --git a/.github/workflows/field-dispatch.yml b/.github/workflows/field-dispatch.yml new file mode 100644 index 00000000..531f3e67 --- /dev/null +++ b/.github/workflows/field-dispatch.yml @@ -0,0 +1,25 @@ +name: Field dispatch +on: + pull_request: + paths: ['applications/field-dispatch/**', '.github/workflows/field-dispatch.yml'] + push: + paths: ['applications/field-dispatch/**', '.github/workflows/field-dispatch.yml'] +permissions: + contents: read +jobs: + native: + runs-on: ubuntu-latest + defaults: + run: + working-directory: applications/field-dispatch + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version-file: applications/field-dispatch/.node-version + cache: npm + cache-dependency-path: applications/field-dispatch/package-lock.json + - run: npm ci --ignore-scripts --strict-peer-deps + - run: npm test + - run: npm run check + - run: npm run build diff --git a/applications/field-dispatch/.env.example b/applications/field-dispatch/.env.example new file mode 100644 index 00000000..fb5bd401 --- /dev/null +++ b/applications/field-dispatch/.env.example @@ -0,0 +1,4 @@ +DATABASE_URL=postgresql://dispatch_app:replace-me@replace-me:5432/postgres +DATABASE_CA_PATH=.deployment/ca.pem +APP_ORIGIN=http://127.0.0.1:3002 +PORT=3002 diff --git a/applications/field-dispatch/.gitignore b/applications/field-dispatch/.gitignore new file mode 100644 index 00000000..3f4ea5ad --- /dev/null +++ b/applications/field-dispatch/.gitignore @@ -0,0 +1,7 @@ +.env +.deployment/ +node_modules/ +dist/ +test-results/ +playwright-report/ +__pycache__/ diff --git a/applications/field-dispatch/.node-version b/applications/field-dispatch/.node-version new file mode 100644 index 00000000..df6ae337 --- /dev/null +++ b/applications/field-dispatch/.node-version @@ -0,0 +1 @@ +24.21.0 diff --git a/applications/field-dispatch/README.md b/applications/field-dispatch/README.md new file mode 100644 index 00000000..56839c6e --- /dev/null +++ b/applications/field-dispatch/README.md @@ -0,0 +1,240 @@ +# Field dispatch + +A synthetic maintenance desk built with **React, MapLibre GL JS, deck.gl, Turf.js, +PMTiles and PostGIS** on [ClickHouse Managed Postgres](https://clickhouse.com/cloud/postgres). +Pan the town, review jobs within the visible viewport, preview service coverage, +assign the right crew and save the map view for your next session. + +**[Start a ClickHouse Cloud trial with $300 in credits](https://clickhouse.com/cloud).** + +| Integration | Visible use | +| --- | --- | +| React | Dispatch list, coverage review, layer/area selection and saved-view feedback | +| MapLibre GL JS | Real WebGL map, vector basemap, service boundary layer, viewport events | +| PMTiles | Checked-in v3 archive, range requests and MapLibre protocol; no token | +| deck.gl | Pickable job points and arcs between assigned jobs and crew depots | +| Turf.js | Service area in km², point-in-polygon preview and straight-line depot distance | +| PostGIS | Indexed viewport queries and authoritative stored-boundary assignment checks | + +The archive contains wholly synthetic blocks, parks, roads and a river. All jobs +and boundaries are fictional. Arcs represent depot-to-job relationships, not +road routes. Coordinates are `[longitude, latitude]`, SRID 4326; SQL viewports use +degrees. Turf computes the displayed area and distance units. + +## Native setup + +Use an isolated Linux VM, Node **24.21.0**, npm, `psql`, OpenSSL and `jq`. +Application dependencies and browser binaries belong inside the VM. + +```sh +git clone https://github.com/ClickHouse/examples.git +cd examples/applications/field-dispatch +npm ci --ignore-scripts --strict-peer-deps +npm test +npm run check +npm run build +``` + +Native tests read the real PMTiles archive through the app's HTTP range endpoint +and reject unsupported viewports/revisions. They do not require a database. +The pinned stack uses React 19.3.0, MapLibre 6.12.0, deck.gl 9.4.0, +Turf 7.4.0, PMTiles 4.5.0, pg 8.23.1, Zod 4.6.5 and TypeScript 6.0.3. +The dedicated `@deck.gl/maplibre` adapter supports MapLibre 6 and shares its +WebGL2 context. The lockfile resolves compatible luma/loaders peers without +forcing installation. + +## Create Postgres with clickhousectl + +Use [clickhousectl](https://clickhouse.com/docs/interfaces/cli) on your provisioning +machine. Cloud management credentials are separate from application credentials. +Install the CLI if needed and authenticate interactively with an Admin API key: + +```sh +curl -fsSL https://clickhouse.com/cli | sh +export PATH="$HOME/.local/bin:$PATH" +clickhousectl --version +clickhousectl cloud auth login --interactive +clickhousectl cloud auth status +clickhousectl cloud org list +umask 077 +mkdir -p .deployment +``` + +Create a key using the [Cloud API guide](https://clickhouse.com/docs/cloud/manage/openapi). +In an editor, create `.deployment/resources.env` with an organization ID and an +available AWS region/size: + +```dotenv +CH_ORG_ID=YOUR_ORGANIZATION_UUID +DEPLOYMENT_NAME=field-dispatch-example +CLOUD_REGION=YOUR_AVAILABLE_REGION +PG_SIZE=YOUR_AVAILABLE_POSTGRES_SIZE +``` + +This example uses one Postgres 18 service with no HA. Check current +[pricing](https://clickhouse.com/pricing). Database charges continue until the +service is deleted; stopping Node or the VM does not stop them. + +```sh +source .deployment/resources.env +clickhousectl cloud postgres create \ + --org-id "$CH_ORG_ID" --name "$DEPLOYMENT_NAME" \ + --provider aws --region "$CLOUD_REGION" --size "$PG_SIZE" \ + --pg-version 18 --ha-type none --tag project=field-dispatch --json \ + > .deployment/postgres-create.json +PG_SERVICE_ID="$(jq -er '.id' .deployment/postgres-create.json)" +printf 'PG_SERVICE_ID=%s\n' "$PG_SERVICE_ID" >> .deployment/resources.env +``` + +The receipt contains the initial administrator password, returned once. Keep it +private. If creation is interrupted, inspect the service list before retrying. +A missing receipt does not prove creation failed. + +Repeat inspection until `state` is `running`, then download its CA: + +```sh +clickhousectl cloud postgres get "$PG_SERVICE_ID" \ + --org-id "$CH_ORG_ID" --json > .deployment/postgres-status.json +jq '{id,state,hostname,username,postgresVersion,size}' .deployment/postgres-status.json +clickhousectl cloud postgres certs get "$PG_SERVICE_ID" \ + --org-id "$CH_ORG_ID" --output .deployment/postgres-ca.pem +``` + +If provisioning runs outside the VM, transfer only the private database receipt +and CA into the VM's app `.deployment/` directory. Keep Cloud API keys outside +the application. Continue the following steps in that app directory in Linux. + +## Roles, schema and seed + +Generate separate migration and runtime passwords once and preserve them for +setup retries. Generated hex passwords are safe in the connection URL: + +```sh +umask 077 +cat > .deployment/passwords.env < .env < .deployment/inventory-after.json +jq --arg id "$PG_SERVICE_ID" '[.. | objects | select(.id? == $id)]' .deployment/inventory-after.json +``` diff --git a/applications/field-dispatch/THIRD_PARTY_NOTICES.md b/applications/field-dispatch/THIRD_PARTY_NOTICES.md new file mode 100644 index 00000000..1358208a --- /dev/null +++ b/applications/field-dispatch/THIRD_PARTY_NOTICES.md @@ -0,0 +1,23 @@ +# Third-party notices + +This example pins dependencies in package-lock.json. Their upstream licenses +remain applicable; installation retains each package's license files. + +| Demonstrated package | License | Upstream | +| --- | --- | --- | +| React / ReactDOM | MIT | https://github.com/facebook/react | +| MapLibre GL JS | BSD-3-Clause | https://github.com/maplibre/maplibre-gl-js | +| deck.gl / MapLibre adapter | MIT | https://github.com/visgl/deck.gl | +| Turf.js | MIT | https://github.com/Turfjs/turf | +| PMTiles JavaScript | BSD-3-Clause | https://github.com/protomaps/PMTiles | +| node-postgres / pg | MIT | https://github.com/brianc/node-postgres | +| Zod | MIT | https://github.com/colinhacks/zod | +| PMTiles Python 3.8.1 (optional fixture tool) | BSD-3-Clause | https://pypi.org/project/pmtiles/ | +| mapbox-vector-tile 2.2.0 (optional fixture tool) | MIT | https://github.com/tilezen/mapbox-vector-tile | + +PostGIS is the database extension and is GPL-2.0-or-later: +https://postgis.net/docs/manual-3.6/using_postgis_dbmanagement.html + +The checked-in PMTiles archive and geographic fixtures are synthetic data created +for this example. No OpenStreetMap or commercial map data is copied. The map +attribution identifies the synthetic source. No Mapbox-hosted style or token is used. diff --git a/applications/field-dispatch/docs/dispatch-desktop.png b/applications/field-dispatch/docs/dispatch-desktop.png new file mode 100644 index 00000000..6e078787 Binary files /dev/null and b/applications/field-dispatch/docs/dispatch-desktop.png differ diff --git a/applications/field-dispatch/docs/dispatch-mobile.png b/applications/field-dispatch/docs/dispatch-mobile.png new file mode 100644 index 00000000..10cb65c5 Binary files /dev/null and b/applications/field-dispatch/docs/dispatch-mobile.png differ diff --git a/applications/field-dispatch/index.html b/applications/field-dispatch/index.html new file mode 100644 index 00000000..b8150b42 --- /dev/null +++ b/applications/field-dispatch/index.html @@ -0,0 +1,2 @@ + +Field dispatch | Maintenance desk
diff --git a/applications/field-dispatch/package-lock.json b/applications/field-dispatch/package-lock.json new file mode 100644 index 00000000..d25d1e2d --- /dev/null +++ b/applications/field-dispatch/package-lock.json @@ -0,0 +1,4596 @@ +{ + "name": "field-dispatch", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "field-dispatch", + "dependencies": { + "@deck.gl/core": "9.4.0", + "@deck.gl/layers": "9.4.0", + "@deck.gl/maplibre": "9.4.0", + "@loaders.gl/core": "4.5.2", + "@luma.gl/core": "9.4.0", + "@luma.gl/engine": "9.4.0", + "@turf/turf": "7.4.0", + "maplibre-gl": "6.12.0", + "pg": "8.23.1", + "pmtiles": "4.5.0", + "react": "19.3.0", + "react-dom": "19.3.0", + "zod": "4.6.5" + }, + "devDependencies": { + "@playwright/test": "1.63.0", + "@types/geojson": "7946.0.16", + "@types/node": "26.6.4", + "@types/pg": "8.23.1", + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@vitejs/plugin-react": "6.1.1", + "prettier": "3.9.9", + "tsx": "4.23.15", + "typescript": "6.0.3", + "vite": "8.3.2" + }, + "engines": { + "node": ">=24.21.0 <25" + } + }, + "node_modules/@deck.gl/core": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@deck.gl/core/-/core-9.4.0.tgz", + "integrity": "sha512-0isP+x2eGNxteO7lT650lYO7oiAYXAu4aF1jG+BMi8aKYh38HtPugLq1TlQTravIXzBSi5Oe1oZNuVj8h4ZEjA==", + "license": "MIT", + "dependencies": { + "@loaders.gl/core": "^4.4.3", + "@loaders.gl/images": "^4.4.3", + "@luma.gl/core": "^9.4.0", + "@luma.gl/engine": "^9.4.0", + "@luma.gl/gpgpu": "^9.4.0", + "@luma.gl/shadertools": "^9.4.0", + "@luma.gl/webgl": "^9.4.0", + "@math.gl/core": "^4.1.0", + "@math.gl/sun": "^4.1.0", + "@math.gl/types": "^4.1.0", + "@math.gl/web-mercator": "^4.1.0", + "@probe.gl/env": "^4.1.1", + "@probe.gl/log": "^4.1.1", + "@probe.gl/stats": "^4.1.1", + "@types/offscreencanvas": "^2019.6.4", + "gl-matrix": "^3.0.0", + "mjolnir.js": "^3.1.1" + } + }, + "node_modules/@deck.gl/core/node_modules/@loaders.gl/images": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@loaders.gl/images/-/images-4.5.2.tgz", + "integrity": "sha512-Xw9pxuOrxISg2aCVtFmiytMFuxAHQc+KGy5roVGDfryZDYi494SGtgJaVAmWGY3pxOXkZ3em2BWJleG5W/jzXg==", + "license": "MIT", + "dependencies": { + "@loaders.gl/loader-utils": "4.5.2" + }, + "peerDependencies": { + "@loaders.gl/core": "~4.5.0" + } + }, + "node_modules/@deck.gl/layers": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@deck.gl/layers/-/layers-9.4.0.tgz", + "integrity": "sha512-jSL/xje2Ez7DmnP8AAIdhhg6IAtfXMmnXiUAKNgpl/TTaEL+iL5TAnagCdt39H3azmKVcCG33kOlIoWFAuLuRg==", + "license": "MIT", + "dependencies": { + "@loaders.gl/images": "^4.4.3", + "@loaders.gl/schema": "^4.4.3", + "@luma.gl/shadertools": "^9.4.0", + "@mapbox/tiny-sdf": "^2.0.5", + "@math.gl/core": "^4.1.0", + "@math.gl/polygon": "^4.1.0", + "@math.gl/web-mercator": "^4.1.0", + "@types/geojson": "^7946.0.16", + "earcut": "^2.2.4" + }, + "peerDependencies": { + "@deck.gl/core": "~9.4.0", + "@loaders.gl/core": "^4.4.3", + "@luma.gl/core": "~9.4.0", + "@luma.gl/engine": "~9.4.0" + } + }, + "node_modules/@deck.gl/layers/node_modules/@loaders.gl/images": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@loaders.gl/images/-/images-4.5.2.tgz", + "integrity": "sha512-Xw9pxuOrxISg2aCVtFmiytMFuxAHQc+KGy5roVGDfryZDYi494SGtgJaVAmWGY3pxOXkZ3em2BWJleG5W/jzXg==", + "license": "MIT", + "dependencies": { + "@loaders.gl/loader-utils": "4.5.2" + }, + "peerDependencies": { + "@loaders.gl/core": "~4.5.0" + } + }, + "node_modules/@deck.gl/maplibre": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@deck.gl/maplibre/-/maplibre-9.4.0.tgz", + "integrity": "sha512-ck7Hxzt0t9M7iVgbVwelZiuUEx+e2HtmhGyPmZulURV0Ct5QtIjDYc6SWWUYZqASQnyDiBgFVaSaA9DisU8ziw==", + "license": "MIT", + "peerDependencies": { + "@deck.gl/core": "~9.4.0", + "@luma.gl/core": "~9.4.0", + "maplibre-gl": "^4.5.1 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@loaders.gl/core": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@loaders.gl/core/-/core-4.5.2.tgz", + "integrity": "sha512-Al1NEd/tCz0KLWslqmEaGka9F/VpBLGUFyjb0Oc7zgmgvzJEAOMWac7nGgO8Zqobl4uVcC8HTkNSFCMV4PKIQw==", + "license": "MIT", + "dependencies": { + "@loaders.gl/loader-utils": "4.5.2", + "@loaders.gl/schema": "4.5.1", + "@loaders.gl/schema-utils": "4.5.2", + "@loaders.gl/worker-utils": "4.5.2", + "@probe.gl/log": "^4.1.1" + } + }, + "node_modules/@loaders.gl/loader-utils": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@loaders.gl/loader-utils/-/loader-utils-4.5.2.tgz", + "integrity": "sha512-ov5se9HOmvgJasOHq9v2dyEfYGClSVQFyFwaD78BD8Nqz1xRxU5O5TRk19WpIExs9MFpNyqVgUHq1yE1PTwWLQ==", + "license": "MIT", + "dependencies": { + "@loaders.gl/schema": "4.5.1", + "@loaders.gl/worker-utils": "4.5.2", + "@probe.gl/log": "^4.1.1", + "@probe.gl/stats": "^4.1.1" + } + }, + "node_modules/@loaders.gl/schema": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@loaders.gl/schema/-/schema-4.5.1.tgz", + "integrity": "sha512-qrMqcwIHQRCfAx/zkujuI8ZfiwaOXvh82y5OXVfPkGtAw5EhLBM3fQxqdNOpfVhy0m4GCC6QvlXyurJXcx++yQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "^7946.0.7", + "apache-arrow": ">= 17.0.0" + } + }, + "node_modules/@loaders.gl/schema-utils": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@loaders.gl/schema-utils/-/schema-utils-4.5.2.tgz", + "integrity": "sha512-giaz4ZzmxuxhWNV1TSOazUksbgFBgnjtxFMHbikaDeBHYQNzHsGWGoG3SLsvmnKCJ5q/fUwFJZqRGqTVdBcoKg==", + "license": "MIT", + "dependencies": { + "@loaders.gl/schema": "4.5.1", + "@math.gl/types": "^4.1.0", + "@types/geojson": "^7946.0.7", + "apache-arrow": ">= 17.0.0" + }, + "peerDependencies": { + "@loaders.gl/core": "~4.5.0" + } + }, + "node_modules/@loaders.gl/worker-utils": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@loaders.gl/worker-utils/-/worker-utils-4.5.2.tgz", + "integrity": "sha512-w5zWjtWQZRYME9Gk7v+u5tAs5bHa+09at10106vZ7wNI0YP4kbcVu424+vxP2dfppR54U2goiS1RY4QGYGXXcQ==", + "license": "MIT", + "peerDependencies": { + "@loaders.gl/core": "~4.5.0" + } + }, + "node_modules/@luma.gl/core": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@luma.gl/core/-/core-9.4.0.tgz", + "integrity": "sha512-2SIB77DoZ6LMZW/aPS/M3hKHP8UR0WR6f5R2+Sir5FdNVdOvFiOHAxPCafjVTxTqFWWHnh1NSCo5k/qq6sdFQw==", + "license": "MIT", + "dependencies": { + "@math.gl/types": "^4.1.0", + "@probe.gl/env": "^4.1.1", + "@probe.gl/log": "^4.1.1", + "@probe.gl/stats": "^4.1.1", + "@types/offscreencanvas": "^2019.7.3" + } + }, + "node_modules/@luma.gl/engine": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/@luma.gl/engine/-/engine-9.4.0.tgz", + "integrity": "sha512-VK1kvdbGKzUmxNC0caR2YB65Q9VV5JA2bMOZwE3k6+mwrSymUrgIgjsNZHasjwe8QnSPlZNpMBsQDVVrdG9P1g==", + "license": "MIT", + "dependencies": { + "@math.gl/core": "^4.1.0", + "@math.gl/types": "^4.1.0", + "@probe.gl/log": "^4.1.1", + "@probe.gl/stats": "^4.1.1" + }, + "peerDependencies": { + "@luma.gl/core": "~9.4.0", + "@luma.gl/shadertools": "~9.4.0" + } + }, + "node_modules/@luma.gl/gpgpu": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@luma.gl/gpgpu/-/gpgpu-9.4.2.tgz", + "integrity": "sha512-LoEqoViPZqMHqQzvq/txFuOLsxI6sn8xmv+IZyovRFBqcWaV184PE/8Ieufww0pjVyJMfubhUCQmGuGrOME8oA==", + "license": "MIT", + "dependencies": { + "@math.gl/core": "^4.1.0", + "@math.gl/types": "^4.1.0", + "@probe.gl/log": "^4.0.8", + "@probe.gl/stats": "^4.0.8" + }, + "peerDependencies": { + "@luma.gl/core": "~9.4.0", + "@luma.gl/engine": "~9.4.0", + "@luma.gl/shadertools": "~9.4.0", + "@luma.gl/webgpu": "~9.4.0" + } + }, + "node_modules/@luma.gl/shadertools": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@luma.gl/shadertools/-/shadertools-9.4.2.tgz", + "integrity": "sha512-0/6pLZSfnQnvcWdYePKKSwVet+gFtAAnKeGXNfxpgp3I8E3WsF/SbD6oEzgU7jdnRqONwGsoACWNpQHO0zZvIQ==", + "license": "MIT", + "dependencies": { + "@math.gl/core": "^4.1.0", + "@math.gl/types": "^4.1.0" + }, + "peerDependencies": { + "@luma.gl/core": "~9.4.0" + } + }, + "node_modules/@luma.gl/webgl": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@luma.gl/webgl/-/webgl-9.4.2.tgz", + "integrity": "sha512-D3GqnxTgQI+vsyH56hX5+jwqdVUhvhbpnPurTwbQOSlSQ8mshhNKcRdrZytTy10qiodDwrcjTnndPEAP6UgCzA==", + "license": "MIT", + "dependencies": { + "@math.gl/types": "^4.1.0", + "@probe.gl/env": "^4.1.1" + }, + "peerDependencies": { + "@luma.gl/core": "~9.4.0" + } + }, + "node_modules/@luma.gl/webgpu": { + "version": "9.4.2", + "resolved": "https://registry.npmjs.org/@luma.gl/webgpu/-/webgpu-9.4.2.tgz", + "integrity": "sha512-24gDOmLyL81fVHxn+UXajnYN5jVln7XgB9fvobc1Yh3PAtXjlcAkmVTqYRvULVGeEamuqvqPDscAkaK/T+R1iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@probe.gl/env": "^4.1.1", + "@webgpu/types": "^0.1.69" + }, + "peerDependencies": { + "@luma.gl/core": "~9.4.0", + "@luma.gl/shadertools": "~9.4.0" + } + }, + "node_modules/@mapbox/jsonlint-lines-primitives": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.3.tgz", + "integrity": "sha512-0SElaV0uMxEnxzBhhX9WTuPyUeMsAN/SS0i16tjuba4/mio63MG9khjC1a0JAiPGXAwvwm4UfHJURCN7nyudQg==", + "license": "MIT", + "engines": { + "node": ">= 22" + } + }, + "node_modules/@mapbox/point-geometry": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz", + "integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==", + "license": "ISC" + }, + "node_modules/@mapbox/tiny-sdf": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.2.0.tgz", + "integrity": "sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/unitbezier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-1.0.0.tgz", + "integrity": "sha512-fqd515fjBmANKGGsQ286E2Wvj/XvDFpGzwJxq4CI6jMQue6Oy04uCKp+JWKF00xRTmk6cEu1jPJ9p3xqH8YWqQ==", + "license": "BSD-2-Clause" + }, + "node_modules/@mapbox/vector-tile": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-3.0.0.tgz", + "integrity": "sha512-Qf10S1uIHMk20ri/IVBnpS+esUEkVaR5Hftmz88jTInrpmWgPGJfPe3LVjjlE77trLx8tH6qjTG7uWH9hIq/0Q==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "~1.1.0", + "@types/geojson": "^7946.0.16", + "pbf": "^5.0.0" + } + }, + "node_modules/@maplibre/geojson-vt": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-6.1.2.tgz", + "integrity": "sha512-2ZYhobdAY+y4/P/z1xe0698165LywD0yDdWugdu3NRUrajgVWut+tLkcz7i/RJguMTURwK7gxwmwYMFJbgbQFw==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.1.0" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "26.4.4", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-26.4.4.tgz", + "integrity": "sha512-hG4RZR4PBd/+6A3ODdZ3AnQRF106ZQk1y3bhwDg8bH/A4Oz2tnRUJVVWs9BZa6gXZ1d3UKe89C+Mm+olGBELvA==", + "license": "ISC", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "^2.0.3", + "@mapbox/unitbezier": "^1.0.0", + "json-stringify-pretty-compact": "^4.0.0", + "minimist": "^1.2.8", + "quickselect": "^3.0.0", + "tinyqueue": "^3.0.0" + }, + "bin": { + "gl-style-format": "dist/gl-style-format.mjs", + "gl-style-migrate": "dist/gl-style-migrate.mjs", + "gl-style-validate": "dist/gl-style-validate.mjs" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, + "node_modules/@maplibre/mlt": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@maplibre/mlt/-/mlt-1.3.0.tgz", + "integrity": "sha512-7M2O7ABM80Oi8XPkfkrjKB8mOBIxGoVgkA52Mu4RMWoBvvMdagQ522+WxVYujZsMpqIr3BpzrTbXrm4k8lCzCQ==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0" + } + }, + "node_modules/@maplibre/vt-pbf": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@maplibre/vt-pbf/-/vt-pbf-4.3.2.tgz", + "integrity": "sha512-j6p0AdjvAR19Z3XaCysle7A4ZSo08tYOzxD0Y9NQylwPAkwJJeYub5b2eVucdeDh7erhv69DahoLOevDRERRUw==", + "license": "MIT", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0", + "@types/geojson": "^7946.0.16", + "pbf": "^5.1.0" + } + }, + "node_modules/@math.gl/core": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@math.gl/core/-/core-4.1.0.tgz", + "integrity": "sha512-FrdHBCVG3QdrworwrUSzXIaK+/9OCRLscxI2OUy6sLOHyHgBMyfnEGs99/m3KNvs+95BsnQLWklVfpKfQzfwKA==", + "license": "MIT", + "dependencies": { + "@math.gl/types": "4.1.0" + } + }, + "node_modules/@math.gl/polygon": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@math.gl/polygon/-/polygon-4.1.0.tgz", + "integrity": "sha512-YA/9PzaCRHbIP5/0E9uTYrqe+jsYTQoqoDWhf6/b0Ixz8bPZBaGDEafLg3z7ffBomZLacUty9U3TlPjqMtzPjA==", + "license": "MIT", + "dependencies": { + "@math.gl/core": "4.1.0" + } + }, + "node_modules/@math.gl/sun": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@math.gl/sun/-/sun-4.1.0.tgz", + "integrity": "sha512-i3q6OCBLSZ5wgZVhXg+X7gsjY/TUtuFW/2KBiq/U1ypLso3S4sEykoU/MGjxUv1xiiGtr+v8TeMbO1OBIh/HmA==", + "license": "MIT" + }, + "node_modules/@math.gl/types": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@math.gl/types/-/types-4.1.0.tgz", + "integrity": "sha512-clYZdHcmRvMzVK5fjeDkQlHUzXQSNdZ7s4xOqC3nJPgz4C/TZkUecTo9YS4PruZqtDda/ag4erndP0MIn40dGA==", + "license": "MIT" + }, + "node_modules/@math.gl/web-mercator": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@math.gl/web-mercator/-/web-mercator-4.1.0.tgz", + "integrity": "sha512-HZo3vO5GCMkXJThxRJ5/QYUYRr3XumfT8CzNNCwoJfinxy5NtKUd7dusNTXn7yJ40UoB8FMIwkVwNlqaiRZZAw==", + "license": "MIT", + "dependencies": { + "@math.gl/core": "4.1.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", + "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@playwright/test": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.63.0.tgz", + "integrity": "sha512-oxMK4vllB9RK5NQ2l1pq1IfOf2AvnEuj/vYGDj0H2nMtmtZpKtCwt/l00GEO6xjGfpBNAvjovvYdCm50dRQkpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@probe.gl/env": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@probe.gl/env/-/env-4.1.2.tgz", + "integrity": "sha512-KIcdjrEiEZVIGX9nh7pZSmTMIKrd+wCb09gyXqpYYI4hs9WMqrQHvKZ7KD8ASutf6kwARwgd4WNQn8aucLSz3g==", + "license": "MIT" + }, + "node_modules/@probe.gl/log": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@probe.gl/log/-/log-4.1.2.tgz", + "integrity": "sha512-2LqSq4CHhc/HJQasHMPOzA6c7rprlGz8CCREGebo19jxcuKdvXQo0DEmlfId076U6UldMp6jGhWvDHEFLX111Q==", + "license": "MIT", + "dependencies": { + "@probe.gl/env": "4.1.2" + } + }, + "node_modules/@probe.gl/stats": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@probe.gl/stats/-/stats-4.1.2.tgz", + "integrity": "sha512-Hqc0UIHd0IZ2+JbovzTz7lYG7CY9D8yCB81zyP92Zl9UDSPcw2cLbTySW14keAQeAo1sjGjUw0+KHxr0Y2h/hA==", + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", + "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz", + "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz", + "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz", + "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz", + "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz", + "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz", + "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz", + "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz", + "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz", + "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz", + "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz", + "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz", + "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz", + "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz", + "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@turf/along": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/along/-/along-7.4.0.tgz", + "integrity": "sha512-uWh26sph1bJm3eO1toaoq9fbPRNxtbQlW/u0m04n7m6uFyGrUu4qE+8DTDQeF+cHGWbMQft4zshHZy0rz4Nyog==", + "license": "MIT", + "dependencies": { + "@turf/bearing": "7.4.0", + "@turf/destination": "7.4.0", + "@turf/distance": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/angle": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/angle/-/angle-7.4.0.tgz", + "integrity": "sha512-QcylOejONtVUQVG25/GznC2URvYLta6HCDLPB12wT/AEsG3pEKvUh5RGJDcFwk8WKfKPRp9C24kzsbp1Nwp9XQ==", + "license": "MIT", + "dependencies": { + "@turf/bearing": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/rhumb-bearing": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/area": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/area/-/area-7.4.0.tgz", + "integrity": "sha512-B7q5f6QwKIxxbl4/L59lAYA+FF7h2hv6st96PuKue0TGCYGSxAG/lV8LHSPUSOuOrLywN4jytJcKK3vOFBs7Nw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/bbox": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/bbox/-/bbox-7.4.0.tgz", + "integrity": "sha512-yOX9lALc2GmjYUIE99+nRj7eZA7eDtx7Ixu/hrgIo+YqHvL1reaPVTC9rQWMHMm0pH0/WHjzk8uOpAHjptrudQ==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/bbox-clip": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/bbox-clip/-/bbox-clip-7.4.0.tgz", + "integrity": "sha512-Vxi8XznT/vhLHfqE9C1IMw0wi73+6EP3Bn51Vm0v9AWLUEv1zKPqR9/a4cj+s7f3diPE1g238rS48+BB5nYKSQ==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/bbox-polygon": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/bbox-polygon/-/bbox-polygon-7.4.0.tgz", + "integrity": "sha512-hF0ylAOhzwk9i7m/PCmTtg+TP5AnFb9B+bMXgEPIYd/C3BOVet1YaA8nF6ViN5bNF0GSkWFYivvclSgWbtZyqg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/bearing": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/bearing/-/bearing-7.4.0.tgz", + "integrity": "sha512-utMyjTU5U3QfHWLRIKUAvJerCMmC6NLI7X6cmL6PA5+KYdzul/ymxL6CQrwJugrf9zt4Hnzvipwz0j5H1Yo87w==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/bezier-spline": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/bezier-spline/-/bezier-spline-7.4.0.tgz", + "integrity": "sha512-ctzTB46Mo5umz658IFh2C9zB8fs/FtqDOjcozAcUm7TUs6h4hdrXsCMRLb4a9NIX7ekPB84xQ6ZrISXwI+6/yQ==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-clockwise": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-clockwise/-/boolean-clockwise-7.4.0.tgz", + "integrity": "sha512-3wO+W9P63JZMK1Ud5ER0kKXpiZaeXkOR5Omq5USKlPcopwZ6TP/mJjt8lUYYWsB2WGDXuUhlYmSVec43z3wwow==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-concave": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-concave/-/boolean-concave-7.4.0.tgz", + "integrity": "sha512-uSu01JxLsivXIyT8R7fN7ujyjyP0A8qTLaC/YlA2FSKDjpW1876KbWua203R9JxqZ4hgNUPCE7dhk9uTRXuZIg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-contains": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-contains/-/boolean-contains-7.4.0.tgz", + "integrity": "sha512-vR8sfdKhSlDy6Jd6UAI2Bt27ZAU287G8lhLieIGfxKCpoUa2V8v3y0I9FH+ZpFen07I33aysHIZ4dMH92hBO3Q==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "7.4.0", + "@turf/boolean-point-in-polygon": "7.4.0", + "@turf/boolean-point-on-line": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/line-split": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-crosses": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-crosses/-/boolean-crosses-7.4.0.tgz", + "integrity": "sha512-athvZ8yB04BcuT53plyRJnn4jMmbiwlGk/pdxj28JZtsKipi+Ti6xmWDklFnrXpGfeaSHpPvKVt7XRduTB/qVA==", + "license": "MIT", + "dependencies": { + "@turf/boolean-equal": "7.4.0", + "@turf/boolean-point-in-polygon": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/line-intersect": "7.4.0", + "@turf/polygon-to-line": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-disjoint": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-disjoint/-/boolean-disjoint-7.4.0.tgz", + "integrity": "sha512-B6nXGFHa1+m181BZ5MOKa1l7/otws/NonUpOd0CKWTuBUzrlc6e60z/kRp5ceRYmE2LFOH5ZZxItnrsWgKc2VA==", + "license": "MIT", + "dependencies": { + "@turf/boolean-point-in-polygon": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/line-intersect": "7.4.0", + "@turf/meta": "7.4.0", + "@turf/polygon-to-line": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-equal": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-equal/-/boolean-equal-7.4.0.tgz", + "integrity": "sha512-OZWSaWvG7tZn90/7sBclZxpLyjrbxh8oNfOqrmdJigTY/XzAyjCE+jaweVTY8NGRkytC53kkDO0Q2DPZptpUcw==", + "license": "MIT", + "dependencies": { + "@turf/clean-coords": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@types/geojson": "^7946.0.10", + "geojson-equality-ts": "^1.0.2", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-intersects": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-intersects/-/boolean-intersects-7.4.0.tgz", + "integrity": "sha512-tV30WLBgLh89uGYpWsKjU29eMhdyhSh2Sc1IKmv1W0YFHKwp6oDm2eRajMzMgdtN8cl6Jyb8oFCmw4LbMMuWAQ==", + "license": "MIT", + "dependencies": { + "@turf/boolean-disjoint": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-overlap": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-overlap/-/boolean-overlap-7.4.0.tgz", + "integrity": "sha512-WcwQC042FnqDbZJCTm67NoQqV3bWz3C5M89b39MJtEPDWbO6kWwgMBt3Rm9w6gwrYljPE43vVB4pLiaSdNgKSQ==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/line-intersect": "7.4.0", + "@turf/line-overlap": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "geojson-equality-ts": "^1.0.2", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-parallel": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-parallel/-/boolean-parallel-7.4.0.tgz", + "integrity": "sha512-EUjTkvbd1zumtXwP3vEQPsjdlny9UjDkWRzNrZjBpc0MVY9g0clSslMBgHbcJAtOz4lP3+6fqOgwOkjA6XJBLQ==", + "license": "MIT", + "dependencies": { + "@turf/clean-coords": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/line-segment": "7.4.0", + "@turf/rhumb-bearing": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-point-in-polygon": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-7.4.0.tgz", + "integrity": "sha512-GQrHc6WSDHhf32GB/b9AGyOeC05AQ3nGN7ooWTrlCYefKn7X9/2wG9BNexF47AuyDq5jPdOvITVR26BLFWV+mA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@types/geojson": "^7946.0.10", + "point-in-polygon-hao": "^1.1.0", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-point-on-line": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-point-on-line/-/boolean-point-on-line-7.4.0.tgz", + "integrity": "sha512-5i5UBeZf/LuzgavkIj8L8L5/8SYWItWzQQcE9ymFZr60lV5lM8eFvQDfzAz2R3WQFCrxXExPqNvAVZOKGUyvoA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-touches": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-touches/-/boolean-touches-7.4.0.tgz", + "integrity": "sha512-ociH8gYuQ/jpTIFDAr3krbh5lMWjfbJXG1diGdmV03smutui/LhnMF8AvqQ46/nzo1IgD0640gbdzkQn91090g==", + "license": "MIT", + "dependencies": { + "@turf/boolean-point-in-polygon": "7.4.0", + "@turf/boolean-point-on-line": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-valid": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-valid/-/boolean-valid-7.4.0.tgz", + "integrity": "sha512-Gop9r5YA3Asz80bPDCs3PjO4YPY5fyE55wlQDqLBSr6ZxbOaJXIoKrMgaBb1RBcM+9BFYnBlnpekMG3C8Gscrg==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "7.4.0", + "@turf/boolean-crosses": "7.4.0", + "@turf/boolean-disjoint": "7.4.0", + "@turf/boolean-overlap": "7.4.0", + "@turf/boolean-point-in-polygon": "7.4.0", + "@turf/boolean-point-on-line": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/line-intersect": "7.4.0", + "@types/geojson": "^7946.0.10", + "geojson-polygon-self-intersections": "^1.2.1", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/boolean-within": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/boolean-within/-/boolean-within-7.4.0.tgz", + "integrity": "sha512-wld+Og3HZlSe+QcovDY4JKLDkEqOx6gW52jkvAWIvbr4fDB/HUV95zX6SY78reJwm6hNwmnuX3nSriWooLU4HA==", + "license": "MIT", + "dependencies": { + "@turf/boolean-contains": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/buffer": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/buffer/-/buffer-7.4.0.tgz", + "integrity": "sha512-ewYjC7NmmP+BZ8JcjX1TglzYH1FCMWAh5L08j0iAP4lbn3/AutVjjbwIXedZZy5He1OfgmXf+rIBcNXdeEYceg==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "7.4.0", + "@turf/center": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/jsts": "^2.7.1", + "@turf/meta": "7.4.0", + "@turf/projection": "7.4.0", + "@types/geojson": "^7946.0.10", + "d3-geo": "^2.0.2", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/center": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/center/-/center-7.4.0.tgz", + "integrity": "sha512-JVuTaIdnQtmTV8eQfVxqza1okKiaFzgVeIA6Xoy/u6aBMxg2+oBpS6GcHZkEqR8OapPWValB1AYYqimUSiyOXw==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "7.4.0", + "@turf/helpers": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/center-mean": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/center-mean/-/center-mean-7.4.0.tgz", + "integrity": "sha512-M4PG7NPiy5OoxueLkgw/OIe3driLic37xSsfs6KSjg7u/ZNBysymThy9a6lYUlygS/Z1xhqSfjOsOcxHtxKTzA==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/center-median": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/center-median/-/center-median-7.4.0.tgz", + "integrity": "sha512-R2ap2S789WRFzbk54gv3hSN0vnT+djRL0nqultWmvhYBJE2FsN5Z0libZNetP+VyOV/mg5AF8CFLbkmnmqJg3Q==", + "license": "MIT", + "dependencies": { + "@turf/center-mean": "7.4.0", + "@turf/centroid": "7.4.0", + "@turf/distance": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/center-of-mass": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/center-of-mass/-/center-of-mass-7.4.0.tgz", + "integrity": "sha512-wpa22gBJvwYmvh+I3idMds1mtSEYXqLFTu9s9U7TZTy2HrglIIe+6iz0zSm4is58W7TNXQQlfeCmyvURU2+kdQ==", + "license": "MIT", + "dependencies": { + "@turf/centroid": "7.4.0", + "@turf/convex": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/centroid": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/centroid/-/centroid-7.4.0.tgz", + "integrity": "sha512-WglCFe+TnMqeYa/LcbUp37NgIF0zHTNYmlxwUm40fyyzyLsYeDq4dgFWLTYpvRNqb5oIMV6xsBjSf+vCPtVJ6w==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/circle": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/circle/-/circle-7.4.0.tgz", + "integrity": "sha512-H7dXQP7WRpIE/UwZFFGYoSewsali5rjk9yzHPNZ03Bc9F11PWA3H+lwznEWmi/wLMMx91uMU+NkyGtJUeQr9cg==", + "license": "MIT", + "dependencies": { + "@turf/destination": "7.4.0", + "@turf/helpers": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/clean-coords": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/clean-coords/-/clean-coords-7.4.0.tgz", + "integrity": "sha512-xl3lVPey/FyVc3Z+drAWtVuVtvt737RHKGUMGyuskmKYW0JUeFDgqSR0OnrDfxXWb58Im/65BgPd1B+TUA9WfA==", + "license": "MIT", + "dependencies": { + "@turf/boolean-point-on-line": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/clone": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/clone/-/clone-7.4.0.tgz", + "integrity": "sha512-IfYnuil7XYJauy3crzIYEr26QkmBiTgFdGfYwUUe3S6dawX+lyb3vhuaYyssEPcCJq83g0G7hwxZy7141Mmzjg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/clusters": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/clusters/-/clusters-7.4.0.tgz", + "integrity": "sha512-ZY8ZXONy88EnsHCTdaAILkZjkR3gd8xYx4Sc8TpRFuRTDzI2abzU6ET9zezMd+U8QWs7//QJctcfapzCpXBXQg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/clusters-dbscan": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/clusters-dbscan/-/clusters-dbscan-7.4.0.tgz", + "integrity": "sha512-6uKPUvL7VNx3MueOqgGTx/pnGe5p5rgy/tA/6HOgRFLlDu0oYUTUdNg2JE0ylYOc0HW5hxCcmsRR0lz6fxw1KA==", + "license": "MIT", + "dependencies": { + "@turf/clone": "7.4.0", + "@turf/distance": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "rbush": "^3.0.1", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/clusters-kmeans": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/clusters-kmeans/-/clusters-kmeans-7.4.0.tgz", + "integrity": "sha512-fGSapN0nNzQ8NdKSsVgwPUQ5vkwlVK4M/zGN91jY29VcKb6ua8CotH7DNGKutCByr0c7WBefhUGeYVeZEgd9tQ==", + "license": "MIT", + "dependencies": { + "@turf/clone": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "skmeans": "0.9.7", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/collect": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/collect/-/collect-7.4.0.tgz", + "integrity": "sha512-vQ0NH9SCUacKtQGValYxF8Q1mv4g4vLdWSwuVjIDbsH7NTgGUCFQwKXeHuGqh3U9r2PCQ+3ZbsMK61ok8U75fg==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "7.4.0", + "@turf/boolean-point-in-polygon": "7.4.0", + "@turf/helpers": "7.4.0", + "@types/geojson": "^7946.0.10", + "rbush": "^3.0.1", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/combine": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/combine/-/combine-7.4.0.tgz", + "integrity": "sha512-G3tEctgA3Kqz9T3PjF3lQdMUodQxOOLLBy8s5Y//8qyx7IRJqOazxdCaZRo2lq7C8eEdoUud6PIAXLz0gHue4w==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/concave": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/concave/-/concave-7.4.0.tgz", + "integrity": "sha512-EzCHDEbGRVbo9vR/HJriC8U7mpxgTYgyluBDNzcaT7kX53eH7/eS1U7rp/Q36e5RXMmlAWBknqtVseGMvKE5tw==", + "license": "MIT", + "dependencies": { + "@turf/clone": "7.4.0", + "@turf/distance": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/meta": "7.4.0", + "@turf/tin": "7.4.0", + "@types/geojson": "^7946.0.10", + "topojson-client": "3.x", + "topojson-server": "3.x", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/convex": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/convex/-/convex-7.4.0.tgz", + "integrity": "sha512-MKKAKBMZAQJLicdJSck0rPkwvf/Yz4IFl672iNq9ZoofQxgAqQSEGtBGy4Zgnr5edDoIexzLRAoqm6woyx4Lzw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "concaveman": "^1.2.1", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/destination": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/destination/-/destination-7.4.0.tgz", + "integrity": "sha512-+jFpOUtzCiN8l9ZOvr66rxi/UxizphXaBDldaw9NfxlX/HcMn3ZDzC8IiCHiNXAilVh4WpCH0qqmH9ykOgdBKA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/difference": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/difference/-/difference-7.4.0.tgz", + "integrity": "sha512-4zV7Xi3CwUdyNsDzrPty08PiUeYYVBdbCnNVlSf8sk5Z7SL7seA42RabRG8ByTzcrbLATeDU4oTEthVhOGPlyw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "polyclip-ts": "^0.16.8", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/directional-mean": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/directional-mean/-/directional-mean-7.4.0.tgz", + "integrity": "sha512-TEgKLz71yLVAvjP9ozRAtB1gk7ziSq+a7LGePkMWLMxLtoJxb7eapAqcDk/QqRUBUJ4Dw/41qtRRwkVX0hQE/A==", + "license": "MIT", + "dependencies": { + "@turf/bearing": "7.4.0", + "@turf/centroid": "7.4.0", + "@turf/destination": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/length": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/dissolve": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/dissolve/-/dissolve-7.4.0.tgz", + "integrity": "sha512-UHx+XiReD+qHXN0dKOyOxlB+NF0L1PkUHlERCPZcYNqd4JWl2uqOjMEBYxlVeBj6SO3yQPTM9kvWXPE3HqLanQ==", + "license": "MIT", + "dependencies": { + "@turf/flatten": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "polyclip-ts": "^0.16.8", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/distance": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/distance/-/distance-7.4.0.tgz", + "integrity": "sha512-ODkopQDG1m/U6Mx7OMmShncaCvneomwX3lZY1CDA7x40bhDdTTRYP/n/6LicZVyIO4/oK+aXkov4NOtWYthA2g==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/distance-weight": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/distance-weight/-/distance-weight-7.4.0.tgz", + "integrity": "sha512-HUS57zzrSJAUUUlpEZYAsjK+BuCisgRf5/9pW5uW9TfegSbysA0T0C800GYqZcSPIpv22zVF5LE/hFnnu8WjHA==", + "license": "MIT", + "dependencies": { + "@turf/centroid": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/ellipse": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/ellipse/-/ellipse-7.4.0.tgz", + "integrity": "sha512-m5s/fwYUbNN1MW/nW33sZ/jD58lMU1kGP6muU4inGAP7OxKbS15yz+Qg2aXsfyKcb0z19AFjhnCLQGWHU0sl1w==", + "license": "MIT", + "dependencies": { + "@turf/destination": "7.4.0", + "@turf/distance": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/transform-rotate": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/envelope": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/envelope/-/envelope-7.4.0.tgz", + "integrity": "sha512-BNpwP70dFElOrYAVxLvjd9kMnxdowJB0ojsTs2OTvaC8Hp+qfrVTzTK5ZZ6Dzix/UpgdRlC5cDrEmbXTdvDpjA==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "7.4.0", + "@turf/bbox-polygon": "7.4.0", + "@turf/helpers": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/explode": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/explode/-/explode-7.4.0.tgz", + "integrity": "sha512-kTWoryCTAiBwFvyJYNQWLJCaFx4bSRpyCjJWmuLoBEQRh9Fi4JLDEug+6JZxygMxCMWGXBH8/R3ICl+soLPFAg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/flatten": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/flatten/-/flatten-7.4.0.tgz", + "integrity": "sha512-yrXpvXtmPlXIIoRuonQ7z+Uj47Sw9KYlH9oh9NBb3QRfz53LN+OTNndkGeXPaI00BDXxO/aZ7wEGmzuoCaDn1w==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/flip": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/flip/-/flip-7.4.0.tgz", + "integrity": "sha512-SlLwkHlY0MzFXmn/3pFNT7JILsEZer+KzLIxVxebiu6nd3aQaIOqGvxLHVFRbIXYd48UrORg1L40sWsw8z1Egg==", + "license": "MIT", + "dependencies": { + "@turf/clone": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/geojson-rbush": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/geojson-rbush/-/geojson-rbush-7.4.0.tgz", + "integrity": "sha512-o+debVj+6KQ2p0VebH+FgcTig2jUuG6mWMgLYvb34c7cOHoEuY6ICvKRKtHn/XcHa6eyoSk0TvnLEJAfFTQDMg==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "rbush": "^3.0.1", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/great-circle": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/great-circle/-/great-circle-7.4.0.tgz", + "integrity": "sha512-dV8qrH9rpvtUsqrXgUKWojCCJTdgAKgX6eyE+zm9GUtzq0pYUxG2JXVIxWBT65ucQPigBSbS5hxx6lAOpoD/Ew==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@types/geojson": "^7946.0.10", + "arc": "^0.2.0", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/helpers": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-7.4.0.tgz", + "integrity": "sha512-7PAwLZqOdRzTI5g9bHvUwlloAXPDH/mlajtryk0tw4ZwGMtmXsAyF4QundsAMfy4u48Fyd3AUqMXY0SMxqJGWg==", + "license": "MIT", + "dependencies": { + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/hex-grid": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/hex-grid/-/hex-grid-7.4.0.tgz", + "integrity": "sha512-IQ35KzK2N9KZ7W0xVeZlDVcXURE2h+GD0CHru/LlWW7hkjVLNYvWjm+ZlMA141As038+4XremSGNzHAZxp3xsQ==", + "license": "MIT", + "dependencies": { + "@turf/distance": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/intersect": "7.4.0", + "@turf/invariant": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/interpolate": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/interpolate/-/interpolate-7.4.0.tgz", + "integrity": "sha512-QRY0i2CW5ZhEhvDa1HDCO5gjun25HSjo4X7/Q2gC8BkjwpL12Ogo883BGAlt5Qq0JZq5uVFU9Ae7W7yVjfIPfw==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "7.4.0", + "@turf/centroid": "7.4.0", + "@turf/clone": "7.4.0", + "@turf/distance": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/hex-grid": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/meta": "7.4.0", + "@turf/point-grid": "7.4.0", + "@turf/square-grid": "7.4.0", + "@turf/triangle-grid": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/intersect": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/intersect/-/intersect-7.4.0.tgz", + "integrity": "sha512-n/wbZfDPoM1JKwrql8mHBoS9cdRWUC7/VE9IBZOttVPv8Guo0B6+Mzc2Z2e1PdIkSP/1R2yH6lxPpWMuuBfVEA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "polyclip-ts": "^0.16.8", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/invariant": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-7.4.0.tgz", + "integrity": "sha512-OAsc3qdNx+tRqzWmMNFMnVlWWACIqnYqIejZKvb2oKkKPYJJrURPXZ6OdrGD58ljJMolLoqwIZGSx3VndaEjxg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/isobands": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/isobands/-/isobands-7.4.0.tgz", + "integrity": "sha512-ClOkFMwcgQmRCespoQavY4AA+gH5qkbcQgrGsKn12VrgbresgA2hBM9bVMNDaEmDauN7RdDBoNcNhkakhD/ZOQ==", + "license": "MIT", + "dependencies": { + "@turf/area": "7.4.0", + "@turf/bbox": "7.4.0", + "@turf/boolean-point-in-polygon": "7.4.0", + "@turf/explode": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/isolines": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/isolines/-/isolines-7.4.0.tgz", + "integrity": "sha512-Pu0i03/GqAcYT4yJ9fj3NWa4Z/c4Viv3Duhzua05XLJ9c70wWJmb+eniLg2l+qiuVKi4ob0Xl2AKkgkKYiezgg==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/jsts": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/@turf/jsts/-/jsts-2.7.2.tgz", + "integrity": "sha512-zAezGlwWHPyU0zxwcX2wQY3RkRpwuoBmhhNE9HY9kWhFDkCxZ3aWK5URKwa/SWKJbj9aztO+8vtdiBA28KVJFg==", + "license": "(EDL-1.0 OR EPL-1.0)", + "dependencies": { + "jsts": "2.7.1" + } + }, + "node_modules/@turf/kinks": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/kinks/-/kinks-7.4.0.tgz", + "integrity": "sha512-M2deZpJq45WASRru5MmGwDJkVIj+JL/rcS6LLZBQyhSna47pxOQIVDC/tIx6xyFezxXw3lhX9VsoRst3irDdAQ==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/length": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/length/-/length-7.4.0.tgz", + "integrity": "sha512-vQsJLuL7uy8m6HAMuJJKoiOHYY1de1Hd6mY/fM90qfu2RyIrkoF8J4bRUb72XZNCVnvob/eHpUMdhZeoUhSyfQ==", + "license": "MIT", + "dependencies": { + "@turf/distance": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-arc": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/line-arc/-/line-arc-7.4.0.tgz", + "integrity": "sha512-fkqfm3S1nRIBoKoK3LfpjDVGswyOw1cRb0W1qV6Na02ccrCJfJ7P9C4SkKZz2iakZioFCot4emraWno3B9YsYg==", + "license": "MIT", + "dependencies": { + "@turf/circle": "7.4.0", + "@turf/destination": "7.4.0", + "@turf/helpers": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-chunk": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/line-chunk/-/line-chunk-7.4.0.tgz", + "integrity": "sha512-vGytKbvTc+PtRimeEJez8nGoIxuu5uQiqSrK7DD7JB4ULLnBtABAMEr0EaEQQ2fyvE1zkTsbWSQPoFlQ3M52cQ==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/length": "7.4.0", + "@turf/line-slice-along": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-intersect": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/line-intersect/-/line-intersect-7.4.0.tgz", + "integrity": "sha512-5qNmEJ/0tQEaUUKSt2zIZGIS6eYGn3T7Z32yN6+Py0ANs8ln3dQNWaDyOEMms/qVY7BFvSaO+8iBmD8vlHdNcA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@types/geojson": "^7946.0.10", + "robust-predicates": "^2.0.4", + "tinyqueue": "^2.0.3", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-offset": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/line-offset/-/line-offset-7.4.0.tgz", + "integrity": "sha512-vAF3tNvHfo6efcrDwhjSL7UgAt2QjPyCKhIWO0g5xvuGMw9v3DZDErla3fVNT/b+DG01UmNmYQWvMzlUWcTLoQ==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-overlap": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/line-overlap/-/line-overlap-7.4.0.tgz", + "integrity": "sha512-40ZFjTk103uXFF5MfkhqzhowvBNozMcm18yR24Z38BbLC+4BLUNUslNDqyXStGZPXQ1X0MH3LeE2TbIpw9wKRA==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "7.4.0", + "@turf/boolean-point-on-line": "7.4.0", + "@turf/geojson-rbush": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/line-segment": "7.4.0", + "@turf/meta": "7.4.0", + "@turf/nearest-point-on-line": "7.4.0", + "@types/geojson": "^7946.0.10", + "fast-deep-equal": "^3.1.3", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-segment": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/line-segment/-/line-segment-7.4.0.tgz", + "integrity": "sha512-v4Gnxpj6XqWbl4d/aVZHYwvSFzaTGGBW4eQQ9Ozqcgt+bvignOMdtVogFDHSWKDj9Gr2tbS2jYp57c8OTa+Olw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-slice": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/line-slice/-/line-slice-7.4.0.tgz", + "integrity": "sha512-urpVBcce/Z2Gprhv6cRniiaOr0L2UVjBqzEpp3esrW20iu25aDXor43d2KAqAP3Yhlv+vNMFlAJAGPcX/nTSzA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/nearest-point-on-line": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-slice-along": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/line-slice-along/-/line-slice-along-7.4.0.tgz", + "integrity": "sha512-N3rBS/E/N2v2udmglBU1XTfWIQlxkSDuzGCjbGnWUMX2ClVgJvptQXI1zQ29aIeOTbcqKNiSbEyqBg8e+fugPw==", + "license": "MIT", + "dependencies": { + "@turf/bearing": "7.4.0", + "@turf/destination": "7.4.0", + "@turf/distance": "7.4.0", + "@turf/helpers": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-split": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/line-split/-/line-split-7.4.0.tgz", + "integrity": "sha512-bAij7u19sFVdYoAp3bQnu/cVzlORxM/W3FM+WeFN/VvGkZ6YiAtcPq/lLP4WwK3WxthTcavED3wbd32iXGpzZw==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "7.4.0", + "@turf/geojson-rbush": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/line-intersect": "7.4.0", + "@turf/line-segment": "7.4.0", + "@turf/meta": "7.4.0", + "@turf/nearest-point-on-line": "7.4.0", + "@turf/truncate": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/line-to-polygon": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/line-to-polygon/-/line-to-polygon-7.4.0.tgz", + "integrity": "sha512-wUsYNTP+tN+2J0+tzakKVHdo6Y1Pe8Ne1rJUuCxZRumwOFyML6/aIQvQaXdbAjN0gmfuoZTCv78RDSZBtsAPAQ==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "7.4.0", + "@turf/clone": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/mask": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/mask/-/mask-7.4.0.tgz", + "integrity": "sha512-NVED/D8XcTHJTi080l9ezH3f+BzFbnBdVa0s5G9Q+sZQcrabSS5BOL1kdvQhu1LS49QhAGYNBtcfKB9e0hvwJg==", + "license": "MIT", + "dependencies": { + "@turf/clone": "7.4.0", + "@turf/helpers": "7.4.0", + "@types/geojson": "^7946.0.10", + "polyclip-ts": "^0.16.8", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/meta": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-7.4.0.tgz", + "integrity": "sha512-3cLUvlEyDuSnMSzrjhaLAEiYR8xhbfWyTVQlrlEw40xL81d4KF4PqUWbjTXKpXZStdYbet2GCurl79KMypNw6g==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/midpoint": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/midpoint/-/midpoint-7.4.0.tgz", + "integrity": "sha512-+poTGThn9F2tveuUDjg1z5TE3QG8zp/IHVqpzkFwFLWX3xpeys0mev9H3LqlbzRU7waawZJ/lsqYlHg3mIWHJw==", + "license": "MIT", + "dependencies": { + "@turf/bearing": "7.4.0", + "@turf/destination": "7.4.0", + "@turf/distance": "7.4.0", + "@turf/helpers": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/moran-index": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/moran-index/-/moran-index-7.4.0.tgz", + "integrity": "sha512-lZqEUOhpmV2Fngjv6O16Pl3zbDlRUudEeuv/iinkT5adesOAKqngIrg4wYOT0kkNaSdaqNqbKyJGZ8mAlP4Lig==", + "license": "MIT", + "dependencies": { + "@turf/distance-weight": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/nearest-neighbor-analysis": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/nearest-neighbor-analysis/-/nearest-neighbor-analysis-7.4.0.tgz", + "integrity": "sha512-WBZbuH+f7/ezEdBGMToT2yYzONdYqhE79yxHTvoJMk1+o0eakg4DY+/owM5FWYgB3GnJCp260//Uz1D/q6wFlg==", + "license": "MIT", + "dependencies": { + "@turf/area": "7.4.0", + "@turf/bbox": "7.4.0", + "@turf/bbox-polygon": "7.4.0", + "@turf/centroid": "7.4.0", + "@turf/distance": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@turf/nearest-point": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/nearest-point": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/nearest-point/-/nearest-point-7.4.0.tgz", + "integrity": "sha512-rH+M5H/rWiOXStiHkV3Cs5OS+Tg/531Tonr7tm2pUjVy1RRLmDqSzKn+iFs3S6ybvjSK2pJa1BbTXjYlc8Pv3Q==", + "license": "MIT", + "dependencies": { + "@turf/clone": "7.4.0", + "@turf/distance": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/nearest-point-on-line": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/nearest-point-on-line/-/nearest-point-on-line-7.4.0.tgz", + "integrity": "sha512-eKTKf/qODIsPankYMZrNfI3LjLaVtQvYV/1hcj9kH/Zr3GfLUszevjhTRDZtMgTFrv8+awW3+MyhmN+PJGuLdQ==", + "license": "MIT", + "dependencies": { + "@turf/distance": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/nearest-point-to-line": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/nearest-point-to-line/-/nearest-point-to-line-7.4.0.tgz", + "integrity": "sha512-PQjS+7/OIjib5HGHtrtDuIH0B6ifT8b4b8ADmcvKcKEMaOk5wjc+Z40oPc0n5OwRoddvbd2K2yV59+fh1cXxeA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/meta": "7.4.0", + "@turf/point-to-line-distance": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/planepoint": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/planepoint/-/planepoint-7.4.0.tgz", + "integrity": "sha512-4TQxrbFpbEjIEv9C+5MO5BbnL/Ay2wrJlLxaiIbJ61LaFFuMx6ZCWu1rBYMDo72Ik5+oK4Pik1J5e8rKW3YGRg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/point-grid": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/point-grid/-/point-grid-7.4.0.tgz", + "integrity": "sha512-Lx7M6JkLHT8A3hPc+811rALohOJ9mJBjAh+NQz9Mj/QdFmnhzGa8aia4F4agy3uSyJP/7EFXUXlPRK4Yc6/4tw==", + "license": "MIT", + "dependencies": { + "@turf/boolean-within": "7.4.0", + "@turf/distance": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/point-on-feature": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/point-on-feature/-/point-on-feature-7.4.0.tgz", + "integrity": "sha512-H7YamrW510QJLnhWIT9oecm1siwhwvb4SZrZSqu6hUOS+pFxumJjUYogE+B6kL+Ymtui2+h6cOtOHJnrNrKaEA==", + "license": "MIT", + "dependencies": { + "@turf/boolean-point-in-polygon": "7.4.0", + "@turf/center": "7.4.0", + "@turf/explode": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/nearest-point": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/point-to-line-distance": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/point-to-line-distance/-/point-to-line-distance-7.4.0.tgz", + "integrity": "sha512-2Egagkj3BK6SrcUxMBJ40PHhW/numknLsJQhlv4kl3d3t83HChhUtH2mXnhqcdi97V3jXSRV1SZIhZKc8TrwOg==", + "license": "MIT", + "dependencies": { + "@turf/bearing": "7.4.0", + "@turf/distance": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/meta": "7.4.0", + "@turf/nearest-point-on-line": "7.4.0", + "@turf/projection": "7.4.0", + "@turf/rhumb-bearing": "7.4.0", + "@turf/rhumb-distance": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/point-to-polygon-distance": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/point-to-polygon-distance/-/point-to-polygon-distance-7.4.0.tgz", + "integrity": "sha512-37h+HzsMgXMu9MJhfQJk11aJ8ZSK4Db/kccvrIyRFv+SX7bCjFON6P4FC4+4PVACajK9VJ52h+KRufaCZqlxdA==", + "license": "MIT", + "dependencies": { + "@turf/boolean-point-in-polygon": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/meta": "7.4.0", + "@turf/point-to-line-distance": "7.4.0", + "@turf/polygon-to-line": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/points-within-polygon": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/points-within-polygon/-/points-within-polygon-7.4.0.tgz", + "integrity": "sha512-8TtUvUeM7USPD11rEPPR/C+aADov1G8wxmgP8ExE2KUDCg4X6cnYTxpWgOHe3uijpv4QRHGhiz12xNmB4ysyyA==", + "license": "MIT", + "dependencies": { + "@turf/boolean-point-in-polygon": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/polygon-smooth": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/polygon-smooth/-/polygon-smooth-7.4.0.tgz", + "integrity": "sha512-UxcEo+OrHbl4+YWuhiIT2lLXvA6rohNdbRBqnZY/X9uQ41JYhh5gMYwlQxbsLTgdYA2pE6wJO52PDHvpqsPu4g==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/polygon-tangents": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/polygon-tangents/-/polygon-tangents-7.4.0.tgz", + "integrity": "sha512-QXKxDhRIDeW/+FQ8Eh6gQMPJFpyf6iXal6tdbL7+5jw9Zpm8DTFVvcPzERBZ3gey6P3CSqP/GD1zZT8gQ+mZng==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "7.4.0", + "@turf/boolean-within": "7.4.0", + "@turf/explode": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/nearest-point": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/polygon-to-line": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/polygon-to-line/-/polygon-to-line-7.4.0.tgz", + "integrity": "sha512-LCHZ9bojUj99et83ZLGpSR/PFx48c0tcjtfuI1nn66a6Q+EGy7GO4NsHv4rWZsC8Lljy5ldbJ3dd8+WGnAk7dw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/polygonize": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/polygonize/-/polygonize-7.4.0.tgz", + "integrity": "sha512-SwEazUYz4KFXllNNXGUt2rP6XpPBfNEOnx+1uP0rcioIsNocFfgwfCUcgHXYM+GZhG0JTD7jQB9RXeNGb6hNIg==", + "license": "MIT", + "dependencies": { + "@turf/boolean-point-in-polygon": "7.4.0", + "@turf/envelope": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/projection": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/projection/-/projection-7.4.0.tgz", + "integrity": "sha512-ZS932qM+18V1UBvNlrNXUCD7mh9CzQqZzw/ElVtb0kAk6N00FYQ9bo26Cca08UZKXh3rZqG/e4eCK74JNGS4Zg==", + "license": "MIT", + "dependencies": { + "@turf/clone": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/quadrat-analysis": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/quadrat-analysis/-/quadrat-analysis-7.4.0.tgz", + "integrity": "sha512-p+Af0+LpwnsVQuGSL1BWuLxgMjd6dYaSs+PxwJ2Y05nSXlsIFpWy+/T55r0Sfun7SoypclKURRCmDy3Q/RVdJw==", + "license": "MIT", + "dependencies": { + "@turf/area": "7.4.0", + "@turf/bbox": "7.4.0", + "@turf/bbox-polygon": "7.4.0", + "@turf/centroid": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/point-grid": "7.4.0", + "@turf/random": "7.4.0", + "@turf/square-grid": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/random": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/random/-/random-7.4.0.tgz", + "integrity": "sha512-ttoPPPQppLYww3ZSeOSlbZlaH4Ki4Mxrdu7wMuDWNrINEhrkr/PuqWyIDISTpILsSARqT0Zzfjwdp0ptkUyakw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/rectangle-grid": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/rectangle-grid/-/rectangle-grid-7.4.0.tgz", + "integrity": "sha512-JeRq5pgmn543DmZU6x4nSX+KVNRGEz3iJQIgbKgSMnzZeiJxfMStINvASz7mz6yky2EwUHvQOBRRlhgCqWViuQ==", + "license": "MIT", + "dependencies": { + "@turf/boolean-intersects": "7.4.0", + "@turf/distance": "7.4.0", + "@turf/helpers": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/rewind": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/rewind/-/rewind-7.4.0.tgz", + "integrity": "sha512-Q2qOcLzymxXyIAYXM9BZQT09L63EhzgneCBMka0dPf9jVShxMeA3oF1kY20ChWkZvTGOAbM7YU9hghphbCxo2w==", + "license": "MIT", + "dependencies": { + "@turf/boolean-clockwise": "7.4.0", + "@turf/clone": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/rhumb-bearing": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/rhumb-bearing/-/rhumb-bearing-7.4.0.tgz", + "integrity": "sha512-y5omgg5SqHwz46fZlra2/70z5eOKXSp/ZzRNIDYKJVYF5oNT2YbG9Z6lj4Az51bjqJQESXUoR6wRmFnpliIp0w==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/rhumb-destination": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/rhumb-destination/-/rhumb-destination-7.4.0.tgz", + "integrity": "sha512-3x4dNLheJJIK/6daH9U7dDkFRSq3wqvDIdqgSxsqgFHoNEI6xDACKGrx47JFWVbg/zh9osG5JSv96e70Y4P5XA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/rhumb-distance": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/rhumb-distance/-/rhumb-distance-7.4.0.tgz", + "integrity": "sha512-29pFCftS/yxEzrYWB777I23Mob+yw9WAECJNw+uabbJi6fwFphJfqWApMXY2ZnudVSJFCGJbByVAHVDTvOGG+w==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/sample": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/sample/-/sample-7.4.0.tgz", + "integrity": "sha512-RfU8+bp5RzxxDq5ldKk8qR+E/ADboOhlcVjFu2p99SVivlnbkMBNXlFRAwdmaDeyqI6bTuwymc7DsCxE0ZA1Uw==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/sector": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/sector/-/sector-7.4.0.tgz", + "integrity": "sha512-BGTbeT/LHW+6H43by4EQfAQaql94rOg5sxIN1T9/PeQR13KV6CnCVzxEmMzPVEZvy7VDfssv9eMerY0WGkjfYg==", + "license": "MIT", + "dependencies": { + "@turf/circle": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/line-arc": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/shortest-path": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/shortest-path/-/shortest-path-7.4.0.tgz", + "integrity": "sha512-Bq/4/yN+CFWo0TcxXKO/VW7/BB5EgrHj9lwP4Kbk00i43flF0EpnlABWoO/MDUVFBPAkBCyWUgcCUW5MZbid6w==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "7.4.0", + "@turf/bbox-polygon": "7.4.0", + "@turf/boolean-point-in-polygon": "7.4.0", + "@turf/clean-coords": "7.4.0", + "@turf/distance": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/meta": "7.4.0", + "@turf/transform-scale": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/simplify": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/simplify/-/simplify-7.4.0.tgz", + "integrity": "sha512-HTolHFuXLReKHJoh3iz92+WpF5EsuCmmZR7fNv3hWnA4hZfkuEzbGB/WZqRdQpvDs99IUS8QIc6BAp6FHzQbBQ==", + "license": "MIT", + "dependencies": { + "@turf/clean-coords": "7.4.0", + "@turf/clone": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/square": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/square/-/square-7.4.0.tgz", + "integrity": "sha512-XjL7taeIZdh3iIS1vertswqa+dgQEgBlByfInjTj3uDDjrpHYG13Yxg6EHuOW/y1SeTWp2fTAKLRQFexC+kwGw==", + "license": "MIT", + "dependencies": { + "@turf/distance": "7.4.0", + "@turf/helpers": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/square-grid": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/square-grid/-/square-grid-7.4.0.tgz", + "integrity": "sha512-jq4pxYs+FQ8Y1m3RhpTBYvSLzSwj75hurT53T1jXboEYIyeFGtGdlOy2o3yx/2cxUK+AP1KtKrQGX5v7sdpzVg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/rectangle-grid": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/standard-deviational-ellipse": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/standard-deviational-ellipse/-/standard-deviational-ellipse-7.4.0.tgz", + "integrity": "sha512-02Erb1A1BARE0MKxLMxsSL3BVHZWZrW/edZISvqZUF1eBOc7FMpIy6Pu55COkFkN3YYOQHysrc4COAZdYEjcwg==", + "license": "MIT", + "dependencies": { + "@turf/center-mean": "7.4.0", + "@turf/ellipse": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/meta": "7.4.0", + "@turf/points-within-polygon": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/tag": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/tag/-/tag-7.4.0.tgz", + "integrity": "sha512-IBd4Lz01FWyTnyxmzcyK3DypMkd7vMzptZIQFlkC/LDeMnPMnmT1oJD7wMdZCJIV1YGVC5abpXoNwY3xnj/eTw==", + "license": "MIT", + "dependencies": { + "@turf/boolean-point-in-polygon": "7.4.0", + "@turf/clone": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/tesselate": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/tesselate/-/tesselate-7.4.0.tgz", + "integrity": "sha512-73Jj0+9GPUYoegut9J1ex9kVYe+WAIV+tBm6EXXtaZzmobIp8fPsAvspyTzXMQvPtlUsf/St+L0W4KFwWe2KrQ==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@types/earcut": "^2.1.4", + "@types/geojson": "^7946.0.10", + "earcut": "^2.2.4", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/tin": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/tin/-/tin-7.4.0.tgz", + "integrity": "sha512-54uBeNKIP8GufZCdeMUMy33e0blR4w0JoE6sYO8I6ZPshNRbVbI24ck+kREVMjzNyeuhL+l7DUrNEn/pcDgBRg==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/transform-rotate": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/transform-rotate/-/transform-rotate-7.4.0.tgz", + "integrity": "sha512-JsXtvydU23rJdOlS5u9Fpdi6mPtbRCo8/6y3QSjkg37EubFTd357M+lX8+lQ+LHjs8hXxvp9LKBZwTujlqsDcw==", + "license": "MIT", + "dependencies": { + "@turf/centroid": "7.4.0", + "@turf/clone": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/meta": "7.4.0", + "@turf/rhumb-bearing": "7.4.0", + "@turf/rhumb-destination": "7.4.0", + "@turf/rhumb-distance": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/transform-scale": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/transform-scale/-/transform-scale-7.4.0.tgz", + "integrity": "sha512-lPsvppxBfK+5z9fC1R3F/7y4113cwZ7hLG9dR97EUYaUhphQveVShrFK18S8+AXxs0J4PnDm3HptTfuJyD7bMQ==", + "license": "MIT", + "dependencies": { + "@turf/bbox": "7.4.0", + "@turf/center": "7.4.0", + "@turf/centroid": "7.4.0", + "@turf/clone": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/meta": "7.4.0", + "@turf/rhumb-bearing": "7.4.0", + "@turf/rhumb-destination": "7.4.0", + "@turf/rhumb-distance": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/transform-translate": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/transform-translate/-/transform-translate-7.4.0.tgz", + "integrity": "sha512-tMHS7lovcHRpKtM0A9w1Y337ZM279YALzPmEvbLA/1Piy6Ik2osecxGB90T+HaobYkWVWhiQ70XP48f49qFc8g==", + "license": "MIT", + "dependencies": { + "@turf/clone": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/meta": "7.4.0", + "@turf/rhumb-destination": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/triangle-grid": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/triangle-grid/-/triangle-grid-7.4.0.tgz", + "integrity": "sha512-3aPtajJH67JkuEe+9WAEecRHBs9Q6lK0KrJIhs8QDx4VJ4nGjRazomz+UuxbhBRtn37lQ/cIxWn0nH9JYFembw==", + "license": "MIT", + "dependencies": { + "@turf/distance": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/intersect": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/truncate": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/truncate/-/truncate-7.4.0.tgz", + "integrity": "sha512-eTXO62l1mD++mKlhUqLNHGcgCQvhxXJGB/EUL1KSzpVIeMYtgcmItAneWdHW+gEpmHfjeDjBuvtbZWA23r7Eig==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/turf": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/turf/-/turf-7.4.0.tgz", + "integrity": "sha512-enR/94yESzZb4t7lj/DSqk6oMdsSfPhMnXHn+f4umpdtZuMmFSbB/mPWxMFoyWCD0z+luwPwtJ7kdtHO5nGi9w==", + "license": "MIT", + "dependencies": { + "@turf/along": "7.4.0", + "@turf/angle": "7.4.0", + "@turf/area": "7.4.0", + "@turf/bbox": "7.4.0", + "@turf/bbox-clip": "7.4.0", + "@turf/bbox-polygon": "7.4.0", + "@turf/bearing": "7.4.0", + "@turf/bezier-spline": "7.4.0", + "@turf/boolean-clockwise": "7.4.0", + "@turf/boolean-concave": "7.4.0", + "@turf/boolean-contains": "7.4.0", + "@turf/boolean-crosses": "7.4.0", + "@turf/boolean-disjoint": "7.4.0", + "@turf/boolean-equal": "7.4.0", + "@turf/boolean-intersects": "7.4.0", + "@turf/boolean-overlap": "7.4.0", + "@turf/boolean-parallel": "7.4.0", + "@turf/boolean-point-in-polygon": "7.4.0", + "@turf/boolean-point-on-line": "7.4.0", + "@turf/boolean-touches": "7.4.0", + "@turf/boolean-valid": "7.4.0", + "@turf/boolean-within": "7.4.0", + "@turf/buffer": "7.4.0", + "@turf/center": "7.4.0", + "@turf/center-mean": "7.4.0", + "@turf/center-median": "7.4.0", + "@turf/center-of-mass": "7.4.0", + "@turf/centroid": "7.4.0", + "@turf/circle": "7.4.0", + "@turf/clean-coords": "7.4.0", + "@turf/clone": "7.4.0", + "@turf/clusters": "7.4.0", + "@turf/clusters-dbscan": "7.4.0", + "@turf/clusters-kmeans": "7.4.0", + "@turf/collect": "7.4.0", + "@turf/combine": "7.4.0", + "@turf/concave": "7.4.0", + "@turf/convex": "7.4.0", + "@turf/destination": "7.4.0", + "@turf/difference": "7.4.0", + "@turf/directional-mean": "7.4.0", + "@turf/dissolve": "7.4.0", + "@turf/distance": "7.4.0", + "@turf/distance-weight": "7.4.0", + "@turf/ellipse": "7.4.0", + "@turf/envelope": "7.4.0", + "@turf/explode": "7.4.0", + "@turf/flatten": "7.4.0", + "@turf/flip": "7.4.0", + "@turf/geojson-rbush": "7.4.0", + "@turf/great-circle": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/hex-grid": "7.4.0", + "@turf/interpolate": "7.4.0", + "@turf/intersect": "7.4.0", + "@turf/invariant": "7.4.0", + "@turf/isobands": "7.4.0", + "@turf/isolines": "7.4.0", + "@turf/kinks": "7.4.0", + "@turf/length": "7.4.0", + "@turf/line-arc": "7.4.0", + "@turf/line-chunk": "7.4.0", + "@turf/line-intersect": "7.4.0", + "@turf/line-offset": "7.4.0", + "@turf/line-overlap": "7.4.0", + "@turf/line-segment": "7.4.0", + "@turf/line-slice": "7.4.0", + "@turf/line-slice-along": "7.4.0", + "@turf/line-split": "7.4.0", + "@turf/line-to-polygon": "7.4.0", + "@turf/mask": "7.4.0", + "@turf/meta": "7.4.0", + "@turf/midpoint": "7.4.0", + "@turf/moran-index": "7.4.0", + "@turf/nearest-neighbor-analysis": "7.4.0", + "@turf/nearest-point": "7.4.0", + "@turf/nearest-point-on-line": "7.4.0", + "@turf/nearest-point-to-line": "7.4.0", + "@turf/planepoint": "7.4.0", + "@turf/point-grid": "7.4.0", + "@turf/point-on-feature": "7.4.0", + "@turf/point-to-line-distance": "7.4.0", + "@turf/point-to-polygon-distance": "7.4.0", + "@turf/points-within-polygon": "7.4.0", + "@turf/polygon-smooth": "7.4.0", + "@turf/polygon-tangents": "7.4.0", + "@turf/polygon-to-line": "7.4.0", + "@turf/polygonize": "7.4.0", + "@turf/projection": "7.4.0", + "@turf/quadrat-analysis": "7.4.0", + "@turf/random": "7.4.0", + "@turf/rectangle-grid": "7.4.0", + "@turf/rewind": "7.4.0", + "@turf/rhumb-bearing": "7.4.0", + "@turf/rhumb-destination": "7.4.0", + "@turf/rhumb-distance": "7.4.0", + "@turf/sample": "7.4.0", + "@turf/sector": "7.4.0", + "@turf/shortest-path": "7.4.0", + "@turf/simplify": "7.4.0", + "@turf/square": "7.4.0", + "@turf/square-grid": "7.4.0", + "@turf/standard-deviational-ellipse": "7.4.0", + "@turf/tag": "7.4.0", + "@turf/tesselate": "7.4.0", + "@turf/tin": "7.4.0", + "@turf/transform-rotate": "7.4.0", + "@turf/transform-scale": "7.4.0", + "@turf/transform-translate": "7.4.0", + "@turf/triangle-grid": "7.4.0", + "@turf/truncate": "7.4.0", + "@turf/union": "7.4.0", + "@turf/unkink-polygon": "7.4.0", + "@turf/voronoi": "7.4.0", + "@types/geojson": "^7946.0.10", + "@types/kdbush": "^3.0.5", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/union": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/union/-/union-7.4.0.tgz", + "integrity": "sha512-SYVtsCW9IznTyKlcGWxssRrf90p/hdpch1UeIcZc31pkSVdmeLOyuzPx0ef3UPmaF//fgXAphMph6JWA/FpKbA==", + "license": "MIT", + "dependencies": { + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "polyclip-ts": "^0.16.8", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/unkink-polygon": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/unkink-polygon/-/unkink-polygon-7.4.0.tgz", + "integrity": "sha512-GAQdiRmfR3Q64LoOqorr2Ec9eKoaVssSE1Rq34ZVTGmf2H49vjOPTXfTBp9ZVva4apVUkNLPPv3r4VaUHmABog==", + "license": "MIT", + "dependencies": { + "@turf/area": "7.4.0", + "@turf/boolean-point-in-polygon": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/meta": "7.4.0", + "@types/geojson": "^7946.0.10", + "rbush": "^3.0.1", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@turf/voronoi": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/@turf/voronoi/-/voronoi-7.4.0.tgz", + "integrity": "sha512-Uw/gyd0vuRonM/zCzgKqsb4Pt8gLl3oMPPscGVtmtyH0vsdlaZuaimJFC9Xdo9sWkLxn8eokhFnkJ3MZSl2UTA==", + "license": "MIT", + "dependencies": { + "@turf/clone": "7.4.0", + "@turf/helpers": "7.4.0", + "@turf/invariant": "7.4.0", + "@types/d3-voronoi": "^1.1.12", + "@types/geojson": "^7946.0.10", + "d3-voronoi": "1.1.2", + "tslib": "^2.8.1" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, + "node_modules/@types/d3-voronoi": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@types/d3-voronoi/-/d3-voronoi-1.1.12.tgz", + "integrity": "sha512-DauBl25PKZZ0WVJr42a6CNvI6efsdzofl9sajqZr2Gf5Gu733WkDdUGiPkUHXiUvYGzNNlFQde2wdZdfQPG+yw==", + "license": "MIT" + }, + "node_modules/@types/earcut": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/earcut/-/earcut-2.1.4.tgz", + "integrity": "sha512-qp3m9PPz4gULB9MhjGID7wpo3gJ4bTGXm7ltNDsmOvsPduTeHp8wSW9YckBj3mljeOh4F0m2z/0JKAALRKbmLQ==", + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/kdbush": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/kdbush/-/kdbush-3.0.5.tgz", + "integrity": "sha512-tdJz7jaWFu4nR+8b2B+CdPZ6811ighYylWsu2hpsivapzW058yP0KdfZuNY89IiRe5jbKvBGXN3LQdN2KPXVdQ==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", + "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/offscreencanvas": { + "version": "2019.7.3", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", + "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==", + "license": "MIT" + }, + "node_modules/@types/pg": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.3.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz", + "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/@webgpu/types": { + "version": "0.1.74", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.74.tgz", + "integrity": "sha512-lgiI4hbuLcI9unnm2cL/tvCaQU45dp0xcLWJh5uB/9MBGvIA4F8XIk7nSiBeg+K4xWGYwgZKn80LmERI5CxoTA==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/apache-arrow": { + "version": "21.2.0", + "resolved": "https://registry.npmjs.org/apache-arrow/-/apache-arrow-21.2.0.tgz", + "integrity": "sha512-Hxe6Agq26gQOM954qpzYSllJBPJl+e16U5CkfuMUhLrNba+5nKkttIVlflaovN6oaTratqMGAO8H5u/aNhmHWQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^25.2.0", + "flatbuffers": "^25.1.24", + "json-with-bigint": "^3.5.3", + "tslib": "^2.6.2" + }, + "bin": { + "arrow2csv": "bin/arrow2csv.js" + } + }, + "node_modules/apache-arrow/node_modules/@types/node": { + "version": "25.9.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.9.tgz", + "integrity": "sha512-b4e2xxj/yMeT2hNlD6x7zNFiQK2Dlmkb9CQ6v3/1G2RezKN4YS2lcEv8lzGm1mhs4f2lM4yNAMI/zUDBZBBt7w==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/apache-arrow/node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/arc": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/arc/-/arc-0.2.0.tgz", + "integrity": "sha512-8NFOo126uYKQJyXNSLY/jSklgfLQL+XWAcPXGo876JwEQ8nSOPXWNI3TV2jLZMN8QEw8uksJ1ZwS4npjBca8MA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/bidi-js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.1.0.tgz", + "integrity": "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/concaveman": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/concaveman/-/concaveman-1.2.1.tgz", + "integrity": "sha512-PwZYKaM/ckQSa8peP5JpVr7IMJ4Nn/MHIaWUjP4be+KoZ7Botgs8seAZGpmaOM+UZXawcdYRao/px9ycrCihHw==", + "license": "ISC", + "dependencies": { + "point-in-polygon": "^1.1.0", + "rbush": "^3.0.1", + "robust-predicates": "^2.0.4", + "tinyqueue": "^2.0.3" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-geo": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-2.0.2.tgz", + "integrity": "sha512-8pM1WGMLGFuhq9S+FpPURxic+gKzjluCD/CHTuUF3mXMeiCo0i6R0tO1s4+GArRFde96SLcW/kOFRjoAosPsFA==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "^2.5.0" + } + }, + "node_modules/d3-voronoi": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.2.tgz", + "integrity": "sha512-RhGS1u2vavcO7ay7ZNAPo4xeDh/VYeGof3x5ZLJBQgYhLegxr3s5IykvWmJ94FTU6mcbtp4sloqZ54mP6R4Utw==", + "license": "BSD-3-Clause" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/earcut": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", + "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==", + "license": "ISC" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, + "node_modules/flatbuffers": { + "version": "25.9.23", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-25.9.23.tgz", + "integrity": "sha512-MI1qs7Lo4Syw0EOzUl0xjs2lsoeqFku44KpngfIduHBYvzm8h2+7K8YMQh1JtVVVrUvhLpNwqVi4DERegUJhPQ==", + "license": "Apache-2.0" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/geojson-equality-ts": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/geojson-equality-ts/-/geojson-equality-ts-1.0.2.tgz", + "integrity": "sha512-h3Ryq+0mCSN/7yLs0eDgrZhvc9af23o/QuC4aTiuuzP/MRCtd6mf5rLsLRY44jX0RPUfM8c4GqERQmlUxPGPoQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "^7946.0.14" + } + }, + "node_modules/geojson-polygon-self-intersections": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/geojson-polygon-self-intersections/-/geojson-polygon-self-intersections-1.2.2.tgz", + "integrity": "sha512-6XRNF4CsRHYmR9z5YuIk5f/aOototnDf0dgMqYGcS7y1l57ttt6MAIAxl3rXyas6lq1HEbTuLMh4PgvO+OV42w==", + "license": "MIT", + "dependencies": { + "rbush": "^2.0.1" + } + }, + "node_modules/geojson-polygon-self-intersections/node_modules/quickselect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-1.1.1.tgz", + "integrity": "sha512-qN0Gqdw4c4KGPsBOQafj6yj/PA6c/L63f6CaZ/DCF/xF4Esu3jVmKLUDYxghFx8Kb/O7y9tI7x2RjTSXwdK1iQ==", + "license": "ISC" + }, + "node_modules/geojson-polygon-self-intersections/node_modules/rbush": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-2.0.2.tgz", + "integrity": "sha512-XBOuALcTm+O/H8G90b6pzu6nX6v2zCKiFG4BJho8a+bY6AER6t8uQUZdi5bomQc0AprCWhEGa7ncAbbRap0bRA==", + "license": "MIT", + "dependencies": { + "quickselect": "^1.0.1" + } + }, + "node_modules/gl-matrix": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.4.tgz", + "integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==", + "license": "MIT" + }, + "node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" + }, + "node_modules/json-with-bigint": { + "version": "3.5.12", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.12.tgz", + "integrity": "sha512-uwbF/wSSuOgC7qqlq27Xp5B6a2MHVug3t0idZdTqu0JnlFvgJuH7ju+KAk/J06C7GfhoYy2gnb9wz2INqcne7w==", + "license": "MIT" + }, + "node_modules/jsts": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/jsts/-/jsts-2.7.1.tgz", + "integrity": "sha512-x2wSZHEBK20CY+Wy+BPE7MrFQHW6sIsdaGUMEqmGAio+3gFzQaBYPwLRonUfQf9Ak8pBieqj9tUofX1+WtAEIg==", + "license": "(EDL-1.0 OR EPL-1.0)", + "engines": { + "node": ">= 12" + } + }, + "node_modules/kdbush": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz", + "integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==", + "license": "ISC" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/maplibre-gl": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-6.12.0.tgz", + "integrity": "sha512-DwgganVi2BhNxOpD7ob3lJC0dQQz4HfJfuQQV3XxCY3XdOzFrMqp9ylMXhRzryRsOnIezdZcklWXIIs/Q8JPjA==", + "license": "BSD-3-Clause", + "dependencies": { + "@mapbox/point-geometry": "^1.1.0", + "@mapbox/tiny-sdf": "^2.2.0", + "@mapbox/unitbezier": "^1.0.0", + "@mapbox/vector-tile": "^3.0.0", + "@maplibre/geojson-vt": "^6.1.1", + "@maplibre/maplibre-gl-style-spec": "^26.4.4", + "@maplibre/mlt": "^1.3.0", + "@maplibre/vt-pbf": "^4.3.2", + "@types/geojson": "^7946.0.16", + "bidi-js": "^1.1.0", + "earcut": "^3.2.4", + "gl-matrix": "^3.4.4", + "kdbush": "^4.1.0", + "murmurhash-js": "^1.0.0", + "pbf": "^5.1.2", + "potpack": "^2.1.0", + "quickselect": "^3.0.0", + "tinyqueue": "^3.0.0" + }, + "engines": { + "node": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" + } + }, + "node_modules/maplibre-gl/node_modules/earcut": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.2.4.tgz", + "integrity": "sha512-FHc6yNpnTORB0tM5kmZ03iD2mDuNakDE2lvf4Hw1XkfxtEHIGVi01B/GqUSwx+NcC1WGjidAfpTXzeALtPtfpg==", + "license": "ISC" + }, + "node_modules/maplibre-gl/node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mjolnir.js": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/mjolnir.js/-/mjolnir.js-3.1.1.tgz", + "integrity": "sha512-Em3+0p6n2RamaiiDenHByvaWNolr+K0fWsFpgUgUXRssFI59ramCMdp0crU6V75clbA4fDJBVTBXfBF5rlPB9A==", + "license": "MIT" + }, + "node_modules/murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pbf": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-5.1.2.tgz", + "integrity": "sha512-mnvGdvOrIvJOBGUEdGkrVXjN8E/VkIJCkf2eS1DH2yv82ORUlLttmDt0rWY38yYZmVwciZwBUvHM20qxBZf40w==", + "license": "BSD-3-Clause", + "dependencies": { + "resolve-protobuf-schema": "^2.1.0" + }, + "bin": { + "pbf": "bin/pbf" + } + }, + "node_modules/pg": { + "version": "8.23.1", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.23.1.tgz", + "integrity": "sha512-aL96AHANtWjPLDOLqnhx+ngp9+UK7ETEU8VJrDCGvsSSi/mGLcWYsS6Herg7lmaBJe4uwrfqsa7gTEFaSizDoQ==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.14.1", + "pg-pool": "^3.14.0", + "pg-protocol": "^1.16.1", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.4.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.1.tgz", + "integrity": "sha512-6PQbsFWZcp9EmJEwy5cGQ2La+AMWpP46lgbb8X+U/XsHIUweYDNCpeuKck5RxL2MdVFi7krbbEi5nX4Zh7JhrQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.14.1.tgz", + "integrity": "sha512-qR3kGNPBLpCNtz0evbKA0Y/MRFXwSSdT+pTJvYp/bXTcReZbvX1kzF0IyTc1QnxqF7AZbOeBhNL8R5mYQZV/MA==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.14.0.tgz", + "integrity": "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.16.1.tgz", + "integrity": "sha512-p9VOFMiHB/ZbJATetbg+99PxssTVSQRnyuPSQ67mN1+1KBOjZaZ83ZQzltnxPhJwSsC3nwVjJ10DVJlerbFzLg==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.63.0.tgz", + "integrity": "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.63.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/playwright-core": { + "version": "1.63.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.63.0.tgz", + "integrity": "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/pmtiles": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/pmtiles/-/pmtiles-4.5.0.tgz", + "integrity": "sha512-CBeD4SoUluFziGdy/8k7FOjQxQy486n+929W/tWophauvMICpkZ26vGBWhDt/6b1FZQWNaf4jFdT/5+q0Wii5w==", + "license": "BSD-3-Clause", + "dependencies": { + "fflate": "^0.8.2" + } + }, + "node_modules/point-in-polygon": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/point-in-polygon/-/point-in-polygon-1.1.0.tgz", + "integrity": "sha512-3ojrFwjnnw8Q9242TzgXuTD+eKiutbzyslcq1ydfu82Db2y+Ogbmyrkpv0Hgj31qwT3lbS9+QAAO/pIQM35XRw==", + "license": "MIT" + }, + "node_modules/point-in-polygon-hao": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/point-in-polygon-hao/-/point-in-polygon-hao-1.2.4.tgz", + "integrity": "sha512-x2pcvXeqhRHlNRdhLs/tgFapAbSSe86wa/eqmj1G6pWftbEs5aVRJhRGM6FYSUERKu0PjekJzMq0gsI2XyiclQ==", + "license": "MIT", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/point-in-polygon-hao/node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, + "node_modules/polyclip-ts": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/polyclip-ts/-/polyclip-ts-0.16.8.tgz", + "integrity": "sha512-JPtKbDRuPEuAjuTdhR62Gph7Is2BS1Szx69CFOO3g71lpJDFo78k4tFyi+qFOMVPePEzdSKkpGU3NBXPHHjvKQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.1.0", + "splaytree-ts": "^1.0.2" + } + }, + "node_modules/postcss": { + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.19", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/potpack": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.1.0.tgz", + "integrity": "sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==", + "license": "ISC" + }, + "node_modules/prettier": { + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.9.tgz", + "integrity": "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/protocol-buffers-schema": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.1.tgz", + "integrity": "sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==", + "license": "MIT" + }, + "node_modules/quickselect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" + }, + "node_modules/rbush": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/rbush/-/rbush-3.0.1.tgz", + "integrity": "sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==", + "license": "MIT", + "dependencies": { + "quickselect": "^2.0.0" + } + }, + "node_modules/rbush/node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", + "license": "ISC" + }, + "node_modules/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz", + "integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz", + "integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.28.0" + }, + "peerDependencies": { + "react": "^19.3.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-protobuf-schema": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", + "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", + "license": "MIT", + "dependencies": { + "protocol-buffers-schema": "^3.3.1" + } + }, + "node_modules/robust-predicates": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-2.0.4.tgz", + "integrity": "sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg==", + "license": "Unlicense" + }, + "node_modules/rolldown": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz", + "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.152.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.12", + "@rolldown/binding-android-arm64": "1.2.12", + "@rolldown/binding-darwin-arm64": "1.2.12", + "@rolldown/binding-darwin-x64": "1.2.12", + "@rolldown/binding-freebsd-x64": "1.2.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", + "@rolldown/binding-linux-arm64-gnu": "1.2.12", + "@rolldown/binding-linux-arm64-musl": "1.2.12", + "@rolldown/binding-linux-ppc64-gnu": "1.2.12", + "@rolldown/binding-linux-s390x-gnu": "1.2.12", + "@rolldown/binding-linux-x64-gnu": "1.2.12", + "@rolldown/binding-linux-x64-musl": "1.2.12", + "@rolldown/binding-openharmony-arm64": "1.2.12", + "@rolldown/binding-win32-arm64-msvc": "1.2.12", + "@rolldown/binding-win32-x64-msvc": "1.2.12" + } + }, + "node_modules/scheduler": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz", + "integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==", + "license": "MIT" + }, + "node_modules/skmeans": { + "version": "0.9.7", + "resolved": "https://registry.npmjs.org/skmeans/-/skmeans-0.9.7.tgz", + "integrity": "sha512-hNj1/oZ7ygsfmPZ7ZfN5MUBRoGg1gtpnImuJBgLO0ljQ67DtJuiQaiYdS4lUA6s0KCwnPhGivtC/WRwIZLkHyg==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/splaytree-ts": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/splaytree-ts/-/splaytree-ts-1.0.2.tgz", + "integrity": "sha512-0kGecIZNIReCSiznK3uheYB8sbstLjCZLiwcQwbmLhgHJj2gz6OnSPkVzJQCMnmEz1BQ4gPK59ylhBoEWOhGNA==", + "license": "BDS-3-Clause" + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyqueue": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", + "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==", + "license": "ISC" + }, + "node_modules/topojson-client": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz", + "integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==", + "license": "ISC", + "dependencies": { + "commander": "2" + }, + "bin": { + "topo2geo": "bin/topo2geo", + "topomerge": "bin/topomerge", + "topoquantize": "bin/topoquantize" + } + }, + "node_modules/topojson-server": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/topojson-server/-/topojson-server-3.0.1.tgz", + "integrity": "sha512-/VS9j/ffKr2XAOjlZ9CgyyeLmgJ9dMwq6Y0YEON8O7p/tGGk+dCWnrE03zEdu7i4L7YsFZLEPZPzCvcB7lEEXw==", + "license": "ISC", + "dependencies": { + "commander": "2" + }, + "bin": { + "geo2topo": "bin/geo2topo" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.23.15", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", + "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.2.tgz", + "integrity": "sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.11", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/zod": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/applications/field-dispatch/package.json b/applications/field-dispatch/package.json new file mode 100644 index 00000000..b97b147e --- /dev/null +++ b/applications/field-dispatch/package.json @@ -0,0 +1,44 @@ +{ + "name": "field-dispatch", + "private": true, + "type": "module", + "engines": { + "node": ">=24.21.0 <25" + }, + "scripts": { + "build": "vite build", + "check": "tsc --noEmit", + "start": "node --env-file=.env --import tsx server/index.ts", + "test": "tsx --test tests/native.test.ts", + "test:integration": "node --env-file=.env --import tsx --test tests/database.test.ts", + "test:browser": "node --env-file=.env ./node_modules/@playwright/test/cli.js test" + }, + "dependencies": { + "react": "19.3.0", + "react-dom": "19.3.0", + "maplibre-gl": "6.12.0", + "@deck.gl/core": "9.4.0", + "@deck.gl/layers": "9.4.0", + "@deck.gl/maplibre": "9.4.0", + "@luma.gl/core": "9.4.0", + "@luma.gl/engine": "9.4.0", + "@loaders.gl/core": "4.5.2", + "@turf/turf": "7.4.0", + "pmtiles": "4.5.0", + "pg": "8.23.1", + "zod": "4.6.5" + }, + "devDependencies": { + "@types/react": "19.3.0", + "@types/react-dom": "19.3.0", + "@types/geojson": "7946.0.16", + "@types/node": "26.6.4", + "@types/pg": "8.23.1", + "@vitejs/plugin-react": "6.1.1", + "vite": "8.3.2", + "typescript": "6.0.3", + "tsx": "4.23.15", + "@playwright/test": "1.63.0", + "prettier": "3.9.9" + } +} diff --git a/applications/field-dispatch/playwright.config.ts b/applications/field-dispatch/playwright.config.ts new file mode 100644 index 00000000..1afbd22c --- /dev/null +++ b/applications/field-dispatch/playwright.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from "@playwright/test"; +export default defineConfig({ + testDir: "tests/browser", + workers: 1, + retries: 0, + timeout: 180_000, + use: { + actionTimeout: 10_000, + baseURL: process.env.APP_ORIGIN ?? "http://127.0.0.1:3002", + viewport: { width: 1440, height: 1050 }, + trace: "retain-on-failure", + launchOptions: { + args: [ + "--use-gl=angle", + "--use-angle=swiftshader", + "--enable-unsafe-swiftshader", + ], + }, + }, +}); diff --git a/applications/field-dispatch/public/maintenance-town.pmtiles b/applications/field-dispatch/public/maintenance-town.pmtiles new file mode 100644 index 00000000..1a4e4409 Binary files /dev/null and b/applications/field-dispatch/public/maintenance-town.pmtiles differ diff --git a/applications/field-dispatch/scripts/generate-basemap.py b/applications/field-dispatch/scripts/generate-basemap.py new file mode 100644 index 00000000..2da22014 --- /dev/null +++ b/applications/field-dispatch/scripts/generate-basemap.py @@ -0,0 +1,76 @@ +"""Generate a tiny, wholly synthetic vector PMTiles basemap (no OSM data).""" +import gzip +import math +from pathlib import Path + +import mapbox_vector_tile +from pmtiles.tile import Compression, TileType, zxy_to_tileid +from pmtiles.writer import Writer +from shapely.geometry import LineString, box +from shapely.ops import transform + +ROOT = Path(__file__).resolve().parents[1] +WORLD = 20037508.342789244 +EXTENT = (-1.51, 53.33, -1.41, 53.41) + + +def mercator(lon, lat): + return lon * WORLD / 180, math.log(math.tan(math.pi / 4 + math.radians(lat) / 2)) * WORLD / math.pi + + +def tile_xy(lon, lat, zoom): + scale = 2 ** zoom + return int((lon + 180) / 360 * scale), int((1 - math.asinh(math.tan(math.radians(lat))) / math.pi) / 2 * scale) + + +def feature(geometry, kind, identifier): + return {"geometry": transform(mercator, geometry), "properties": {"kind": kind}, "id": identifier} + + +roads = [] +for i in range(10): + lon = -1.495 + i * 0.006 + roads.append(feature(LineString([(lon, 53.343), (lon + 0.003, 53.400)]), "minor", i + 1)) +for i in range(9): + lat = 53.346 + i * 0.006 + roads.append(feature(LineString([(-1.501, lat), (-1.435, lat + 0.001)]), "main" if i in (2, 6) else "minor", i + 20)) +land = [] +for x in range(8): + for y in range(7): + lon, lat = -1.493 + x * 0.006, 53.349 + y * 0.006 + land.append(feature(box(lon, lat, lon + 0.003, lat + 0.002), "park" if (x + y) % 7 == 0 else "block", 100 + x * 10 + y)) +water = [feature(LineString([(-1.469, 53.340), (-1.466, 53.360), (-1.462, 53.380), (-1.458, 53.402)]), "river", 500)] + +output = ROOT / "public" / "maintenance-town.pmtiles" +output.parent.mkdir(exist_ok=True) +with output.open("wb") as stream: + writer = Writer(stream) + tiles = [] + for zoom in range(12, 15): + west, south, east, north = EXTENT + min_x, min_y = tile_xy(west, north, zoom) + max_x, max_y = tile_xy(east, south, zoom) + for x in range(min_x, max_x + 1): + for y in range(min_y, max_y + 1): + size = 2 * WORLD / 2 ** zoom + bounds = (-WORLD + x * size, WORLD - (y + 1) * size, -WORLD + (x + 1) * size, WORLD - y * size) + clip = box(*bounds) + layers = [] + for name, source in [("roads", roads), ("land", land), ("water", water)]: + clipped = [] + for original in source: + geometry = original["geometry"].intersection(clip) + if not geometry.is_empty and geometry.geom_type in ("Polygon", "MultiPolygon", "LineString", "MultiLineString"): + clipped.append({**original, "geometry": geometry}) + layers.append({"name": name, "features": clipped}) + encoded = mapbox_vector_tile.encode(layers, default_options={"quantize_bounds": bounds, "extents": 4096}) + tiles.append((zxy_to_tileid(zoom, x, y), gzip.compress(encoded, mtime=0))) + for tileid, encoded in sorted(tiles): + writer.write_tile(tileid, encoded) + writer.finalize({"tile_type": TileType.MVT, "tile_compression": Compression.GZIP, + "min_lon_e7": int(EXTENT[0] * 1e7), "min_lat_e7": int(EXTENT[1] * 1e7), + "max_lon_e7": int(EXTENT[2] * 1e7), "max_lat_e7": int(EXTENT[3] * 1e7), + "center_zoom": 14, "center_lon_e7": -14670000, "center_lat_e7": 533730000}, + {"name": "Synthetic maintenance town", "description": "Generated fictional streets, blocks and parks. No real map data.", + "attribution": "Synthetic map data · ClickHouse examples", "vector_layers": [{"id": name, "fields": {"kind": "String"}, "minzoom": 12, "maxzoom": 14} for name in ["roads", "land", "water"]]}) +print(f"Generated {len(tiles)} vector tiles, {output.stat().st_size} bytes: {output.name}") diff --git a/applications/field-dispatch/server/database.ts b/applications/field-dispatch/server/database.ts new file mode 100644 index 00000000..822bfa82 --- /dev/null +++ b/applications/field-dispatch/server/database.ts @@ -0,0 +1,35 @@ +import { readFileSync } from "node:fs"; +import { Pool } from "pg"; +let pool: Pool | undefined; +export function databaseConfig() { + const connectionString = process.env.DATABASE_URL; + const caPath = process.env.DATABASE_CA_PATH; + if (!connectionString || !caPath) + throw new Error("DATABASE_URL and DATABASE_CA_PATH are required."); + const url = new URL(connectionString); + if ( + !["postgresql:", "postgres:"].includes(url.protocol) || + url.search || + url.hash || + process.env.NODE_TLS_REJECT_UNAUTHORIZED === "0" + ) + throw new Error("Use a direct Postgres URL without SSL query parameters."); + return { + connectionString, + max: 5, + connectionTimeoutMillis: 10_000, + query_timeout: 15_000, + ssl: { + ca: readFileSync(caPath, "utf8"), + rejectUnauthorized: true, + servername: url.hostname, + }, + }; +} +export function database() { + return (pool ??= new Pool(databaseConfig())); +} +export async function closeDatabase() { + await pool?.end(); + pool = undefined; +} diff --git a/applications/field-dispatch/server/dispatch.ts b/applications/field-dispatch/server/dispatch.ts new file mode 100644 index 00000000..b7e13046 --- /dev/null +++ b/applications/field-dispatch/server/dispatch.ts @@ -0,0 +1,153 @@ +import type { PoolClient } from "pg"; +import { database } from "./database"; +import { + assignmentSchema, + mapStateSchema, + viewportSchema, + type Area, + type Bootstrap, + type Job, + type MapState, + type Team, + type Viewport, +} from "../src/domain"; + +export class DispatchError extends Error { + constructor( + message: string, + public status = 400, + ) { + super(message); + } +} +async function transaction(work: (client: PoolClient) => Promise) { + const client = await database().connect(); + try { + await client.query("BEGIN"); + const result = await work(client); + await client.query("COMMIT"); + return result; + } catch (error) { + await client.query("ROLLBACK"); + throw error; + } finally { + client.release(); + } +} +export async function bootstrap(): Promise { + return transaction(async (client) => { + await client.query( + "SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY", + ); + const areas = ( + await client.query(` + SELECT id, name, color, jsonb_build_object( + 'type','Feature','properties',jsonb_build_object('id',id,'name',name), + 'geometry',public.ST_AsGeoJSON(boundary)::jsonb) AS feature + FROM dispatch.areas ORDER BY name + `) + ).rows; + const teams = ( + await client.query(` + SELECT id,name,area_id AS "areaId", + jsonb_build_array(public.ST_X(depot),public.ST_Y(depot)) AS position + FROM dispatch.teams ORDER BY name + `) + ).rows; + const state = ( + await client.query(` + SELECT longitude,latitude,zoom,area_id AS "areaId",mode,revision + FROM dispatch.map_state WHERE id='dispatch' + `) + ).rows[0]; + return { areas, teams, state }; + }); +} +export async function viewportJobs(input: unknown) { + const bounds = viewportSchema.parse(input); + const rows = ( + await database().query( + ` + SELECT id,code,title,priority,team_id AS "teamId",revision, + jsonb_build_array(public.ST_X(geom),public.ST_Y(geom)) AS position, + (count(*) OVER ())::integer AS total + FROM dispatch.jobs + WHERE public.ST_Intersects(geom,public.ST_MakeEnvelope($1,$2,$3,$4,4326)) + ORDER BY CASE priority WHEN 'urgent' THEN 0 ELSE 1 END,code LIMIT 100 + `, + [bounds.west, bounds.south, bounds.east, bounds.north], + ) + ).rows; + const total = rows[0]?.total ?? 0; + return { + jobs: rows.map(({ total: _total, ...job }) => job), + total, + capped: total > rows.length, + }; +} +export async function assign(input: { + jobId: string; + teamId: string; + revision: number; +}) { + const data = assignmentSchema.parse(input); + return transaction(async (client) => { + const result = await client.query<{ revision: number; inside: boolean }>( + ` + SELECT j.revision,public.ST_Covers(a.boundary,j.geom) AS inside + FROM dispatch.jobs j + JOIN dispatch.teams t ON t.id=$2 + JOIN dispatch.areas a ON a.id=t.area_id + WHERE j.id=$1 FOR UPDATE OF j + `, + [data.jobId, data.teamId], + ); + const job = result.rows[0]; + if (!job) throw new DispatchError("Job or team not found.", 404); + if (job.revision !== data.revision) + throw new DispatchError( + "This job changed in another tab. Refresh before assigning.", + 409, + ); + if (!job.inside) + throw new DispatchError( + "This job lies outside the team’s service area. Assignment was not saved.", + ); + await client.query( + ` + UPDATE dispatch.jobs SET team_id=$2,revision=revision+1 WHERE id=$1 + `, + [data.jobId, data.teamId], + ); + return { revision: job.revision + 1 }; + }); +} +export async function saveMap(input: MapState) { + const state = mapStateSchema.parse(input); + return transaction(async (client) => { + const result = await client.query(` + SELECT revision FROM dispatch.map_state WHERE id='dispatch' FOR UPDATE + `); + if (result.rows[0].revision !== state.revision) + throw new DispatchError( + "The saved view changed in another tab. Reload before saving.", + 409, + ); + if ( + !( + await client.query("SELECT 1 FROM dispatch.areas WHERE id=$1", [ + state.areaId, + ]) + ).rowCount + ) + throw new DispatchError("Select an existing service area."); + await client.query( + ` + UPDATE dispatch.map_state SET longitude=$1,latitude=$2,zoom=$3, + area_id=$4,mode=$5,revision=revision+1 WHERE id='dispatch' + `, + [state.longitude, state.latitude, state.zoom, state.areaId, state.mode], + ); + return { revision: state.revision + 1 }; + }); +} diff --git a/applications/field-dispatch/server/http.ts b/applications/field-dispatch/server/http.ts new file mode 100644 index 00000000..9e86001c --- /dev/null +++ b/applications/field-dispatch/server/http.ts @@ -0,0 +1,149 @@ +import { + createServer, + type IncomingMessage, + type ServerResponse, +} from "node:http"; +import { createReadStream } from "node:fs"; +import { stat } from "node:fs/promises"; +import { resolve, sep, extname } from "node:path"; +import { z } from "zod"; +import { + bootstrap, + viewportJobs, + assign, + saveMap, + DispatchError, +} from "./dispatch"; + +export function parseRange(value: string, size: number) { + const match = /^bytes=(\d+)-(\d*)$/.exec(value); + if (!match) throw new DispatchError("Use one forward byte range.", 416); + const start = Number(match[1]); + const end = match[2] ? Math.min(Number(match[2]), size - 1) : size - 1; + if ( + !Number.isSafeInteger(start) || + !Number.isSafeInteger(end) || + start > end || + start >= size + ) + throw new DispatchError("Byte range is outside the archive.", 416); + return { start, end }; +} +function json(response: ServerResponse, status: number, value: unknown) { + response.writeHead(status, { + "content-type": "application/json; charset=utf-8", + "cache-control": "no-store", + }); + response.end(JSON.stringify(value)); +} +async function body(request: IncomingMessage) { + if (request.headers["content-type"]?.split(";")[0] !== "application/json") + throw new DispatchError("Send a JSON request body.", 415); + const chunks: Buffer[] = []; + let bytes = 0; + for await (const chunk of request) { + bytes += chunk.length; + if (bytes > 8192) + throw new DispatchError("Request body is too large.", 413); + chunks.push(chunk); + } + try { + return JSON.parse(Buffer.concat(chunks).toString("utf8")); + } catch { + throw new DispatchError("Request body is not valid JSON."); + } +} +async function staticFile( + request: IncomingMessage, + response: ServerResponse, + pathname: string, +) { + const archive = pathname === "/maintenance-town.pmtiles"; + const root = resolve(archive ? "public" : "dist"); + let path = resolve(root, pathname === "/" ? "index.html" : `.${pathname}`); + if (!path.startsWith(root + sep)) + throw new DispatchError("File not found.", 404); + let info; + try { + info = await stat(path); + } catch { + throw new DispatchError("File not found.", 404); + } + if (!info.isFile()) throw new DispatchError("File not found.", 404); + const mime: Record = { + ".html": "text/html; charset=utf-8", + ".js": "text/javascript", + ".mjs": "text/javascript", + ".css": "text/css", + ".svg": "image/svg+xml", + ".pmtiles": "application/octet-stream", + }; + const headers: Record = { + "content-type": mime[extname(path)] ?? "application/octet-stream", + "cache-control": archive ? "public, max-age=3600" : "no-cache", + "content-length": info.size, + }; + if (archive) headers["accept-ranges"] = "bytes"; + if (archive && request.headers.range) { + const range = parseRange(request.headers.range, info.size); + headers["content-range"] = `bytes ${range.start}-${range.end}/${info.size}`; + headers["content-length"] = range.end - range.start + 1; + response.writeHead(206, headers); + if (request.method === "HEAD") response.end(); + else createReadStream(path, range).pipe(response); + } else { + response.writeHead(200, headers); + if (request.method === "HEAD") response.end(); + else createReadStream(path).pipe(response); + } +} +export function createApplication() { + return createServer((request, response) => { + void (async () => { + const origin = process.env.APP_ORIGIN; + if ( + !origin || + !["127.0.0.1", "localhost", "[::1]"].includes(new URL(origin).hostname) + ) + throw new DispatchError("Configure a loopback APP_ORIGIN.", 503); + if ( + request.headers.host !== new URL(origin).host || + (!["GET", "HEAD"].includes(request.method ?? "") && + request.headers.origin !== origin) + ) + throw new DispatchError("Use the configured dispatch address.", 403); + const url = new URL(request.url ?? "/", origin); + if (request.method === "GET" && url.pathname === "/api/bootstrap") + return json(response, 200, await bootstrap()); + if (request.method === "GET" && url.pathname === "/api/jobs") + return json( + response, + 200, + await viewportJobs(Object.fromEntries(url.searchParams)), + ); + if (request.method === "POST" && url.pathname === "/api/assign") + return json(response, 200, await assign(await body(request))); + if (request.method === "POST" && url.pathname === "/api/map") + return json(response, 200, await saveMap(await body(request))); + if (url.pathname.startsWith("/api/")) + throw new DispatchError("API route not found.", 404); + if (!["GET", "HEAD"].includes(request.method ?? "")) + throw new DispatchError("Method not allowed.", 405); + await staticFile(request, response, url.pathname); + })().catch((error) => { + if (error instanceof z.ZodError) + return json(response, 400, { + error: "Use valid coordinates, identifiers and revisions.", + }); + if (error instanceof DispatchError) + return json(response, error.status, { error: error.message }); + console.error( + "Dispatch request failed.", + error.code ?? error.constructor.name, + ); + json(response, 503, { + error: "Request failed. Check the database connection.", + }); + }); + }); +} diff --git a/applications/field-dispatch/server/index.ts b/applications/field-dispatch/server/index.ts new file mode 100644 index 00000000..52f7dc5a --- /dev/null +++ b/applications/field-dispatch/server/index.ts @@ -0,0 +1,19 @@ +import { createApplication } from "./http"; +import { closeDatabase } from "./database"; +const port = Number(process.env.PORT ?? 3002); +if (!Number.isInteger(port) || port < 1 || port > 65535) + throw new Error("Use a valid PORT."); +const server = createApplication(); +server.listen(port, "127.0.0.1", () => + console.log(`Field dispatch at http://127.0.0.1:${port}`), +); +process.once("SIGTERM", () => + server.close(() => { + void closeDatabase().then(() => process.exit(0)); + }), +); +process.once("SIGINT", () => + server.close(() => { + void closeDatabase().then(() => process.exit(0)); + }), +); diff --git a/applications/field-dispatch/sql/bootstrap.sql b/applications/field-dispatch/sql/bootstrap.sql new file mode 100644 index 00000000..1bef14cc --- /dev/null +++ b/applications/field-dispatch/sql/bootstrap.sql @@ -0,0 +1,13 @@ +\set ON_ERROR_STOP on +\getenv migration_password PG_MIGRATION_PASSWORD +\getenv app_password PG_APP_PASSWORD +BEGIN; +CREATE EXTENSION IF NOT EXISTS postgis WITH SCHEMA public; +CREATE ROLE dispatch_owner NOLOGIN; +CREATE ROLE dispatch_migrator LOGIN PASSWORD :'migration_password'; +GRANT dispatch_owner TO dispatch_migrator; +CREATE ROLE dispatch_app LOGIN PASSWORD :'app_password'; +CREATE SCHEMA dispatch AUTHORIZATION dispatch_owner; +REVOKE ALL ON SCHEMA dispatch FROM PUBLIC; +GRANT USAGE ON SCHEMA dispatch, public TO dispatch_app; +COMMIT; diff --git a/applications/field-dispatch/sql/grants.sql b/applications/field-dispatch/sql/grants.sql new file mode 100644 index 00000000..aea8b782 --- /dev/null +++ b/applications/field-dispatch/sql/grants.sql @@ -0,0 +1,7 @@ +\set ON_ERROR_STOP on +BEGIN; +SET LOCAL ROLE dispatch_owner; +GRANT SELECT ON dispatch.areas, dispatch.teams, dispatch.jobs, dispatch.map_state TO dispatch_app; +GRANT UPDATE (team_id, revision) ON dispatch.jobs TO dispatch_app; +GRANT UPDATE (longitude, latitude, zoom, area_id, mode, revision) ON dispatch.map_state TO dispatch_app; +COMMIT; diff --git a/applications/field-dispatch/sql/migrations/001_initial.sql b/applications/field-dispatch/sql/migrations/001_initial.sql new file mode 100644 index 00000000..b76c6c43 --- /dev/null +++ b/applications/field-dispatch/sql/migrations/001_initial.sql @@ -0,0 +1,37 @@ +\set ON_ERROR_STOP on +BEGIN; +SET LOCAL ROLE dispatch_owner; +CREATE TABLE dispatch.areas ( + id uuid PRIMARY KEY, + name text NOT NULL CHECK (length(name) BETWEEN 1 AND 80), + color text NOT NULL CHECK (color ~ '^#[0-9a-fA-F]{6}$'), + boundary public.geometry(Polygon, 4326) NOT NULL, + CHECK (public.ST_IsValid(boundary) AND NOT public.ST_IsEmpty(boundary)) +); +CREATE INDEX areas_boundary_gist ON dispatch.areas USING gist (boundary); +CREATE TABLE dispatch.teams ( + id uuid PRIMARY KEY, + name text NOT NULL, + area_id uuid NOT NULL REFERENCES dispatch.areas(id), + depot public.geometry(Point, 4326) NOT NULL +); +CREATE TABLE dispatch.jobs ( + id uuid PRIMARY KEY, + code text NOT NULL UNIQUE, + title text NOT NULL, + priority text NOT NULL CHECK (priority IN ('routine', 'urgent')), + geom public.geometry(Point, 4326) NOT NULL, + team_id uuid REFERENCES dispatch.teams(id), + revision integer NOT NULL DEFAULT 1 CHECK (revision > 0) +); +CREATE INDEX jobs_geom_gist ON dispatch.jobs USING gist (geom); +CREATE TABLE dispatch.map_state ( + id text PRIMARY KEY CHECK (id = 'dispatch'), + longitude double precision NOT NULL CHECK (longitude BETWEEN -1.52 AND -1.40), + latitude double precision NOT NULL CHECK (latitude BETWEEN 53.32 AND 53.42), + zoom double precision NOT NULL CHECK (zoom BETWEEN 12 AND 16), + area_id uuid NOT NULL REFERENCES dispatch.areas(id), + mode text NOT NULL CHECK (mode IN ('assignments', 'priority')), + revision integer NOT NULL DEFAULT 1 CHECK (revision > 0) +); +COMMIT; diff --git a/applications/field-dispatch/sql/seed.sql b/applications/field-dispatch/sql/seed.sql new file mode 100644 index 00000000..dad55fb1 --- /dev/null +++ b/applications/field-dispatch/sql/seed.sql @@ -0,0 +1,58 @@ +\set ON_ERROR_STOP on +BEGIN; +SET LOCAL ROLE dispatch_owner; +INSERT INTO dispatch.areas (id,name,color,boundary) VALUES + ('20000000-0000-4000-8000-000000000001','West garden','#648662', + public.ST_GeomFromText('POLYGON((-1.495 53.345,-1.465 53.345,-1.465 53.379,-1.495 53.379,-1.495 53.345))',4326)), + ('20000000-0000-4000-8000-000000000002','Riverside','#46858d', + public.ST_GeomFromText('POLYGON((-1.465 53.345,-1.435 53.345,-1.435 53.379,-1.465 53.379,-1.465 53.345))',4326)), + ('20000000-0000-4000-8000-000000000003','North common','#b28544', + public.ST_GeomFromText('POLYGON((-1.49 53.376,-1.44 53.376,-1.44 53.402,-1.49 53.402,-1.49 53.376))',4326)) +ON CONFLICT (id) DO NOTHING; +INSERT INTO dispatch.teams (id,name,area_id,depot) VALUES + ('30000000-0000-4000-8000-000000000001','Garden crew','20000000-0000-4000-8000-000000000001',public.ST_SetSRID(public.ST_MakePoint(-1.482,53.36),4326)), + ('30000000-0000-4000-8000-000000000002','River crew','20000000-0000-4000-8000-000000000002',public.ST_SetSRID(public.ST_MakePoint(-1.451,53.354),4326)), + ('30000000-0000-4000-8000-000000000003','Common crew','20000000-0000-4000-8000-000000000003',public.ST_SetSRID(public.ST_MakePoint(-1.469,53.392),4326)) +ON CONFLICT (id) DO NOTHING; +INSERT INTO dispatch.jobs (id,code,title,priority,geom) VALUES + ('40000000-0000-4000-8000-000000000001','FD-001','Repair garden gate','urgent', + public.ST_SetSRID(public.ST_MakePoint(-1.481,53.361),4326)), + ('40000000-0000-4000-8000-000000000002','FD-002','Clear riverside drain','urgent', + public.ST_SetSRID(public.ST_MakePoint(-1.448,53.36),4326)), + ('40000000-0000-4000-8000-000000000003','FD-003','Replace path light','routine', + public.ST_SetSRID(public.ST_MakePoint(-1.467,53.391),4326)), + ('40000000-0000-4000-8000-000000000004','FD-004','Inspect common bench','routine', + public.ST_SetSRID(public.ST_MakePoint(-1.475,53.377),4326)), + ('40000000-0000-4000-8000-000000000005','FD-005','Repair play-area fence','routine', + public.ST_SetSRID(public.ST_MakePoint(-1.486,53.352),4326)), + ('40000000-0000-4000-8000-000000000006','FD-006','Clear fallen branch','urgent', + public.ST_SetSRID(public.ST_MakePoint(-1.442,53.373),4326)), + ('40000000-0000-4000-8000-000000000007','FD-007','Fix fountain valve','routine', + public.ST_SetSRID(public.ST_MakePoint(-1.478,53.367),4326)), + ('40000000-0000-4000-8000-000000000008','FD-008','Reset crossing bollard','routine', + public.ST_SetSRID(public.ST_MakePoint(-1.452,53.367),4326)), + ('40000000-0000-4000-8000-000000000009','FD-009','Replace notice board','routine', + public.ST_SetSRID(public.ST_MakePoint(-1.482,53.389),4326)), + ('40000000-0000-4000-8000-000000000010','FD-010','Collect damaged bin','urgent', + public.ST_SetSRID(public.ST_MakePoint(-1.462,53.381),4326)), + ('40000000-0000-4000-8000-000000000011','FD-011','Repair picnic table','routine', + public.ST_SetSRID(public.ST_MakePoint(-1.473,53.356),4326)), + ('40000000-0000-4000-8000-000000000012','FD-012','Clear footbridge debris','routine', + public.ST_SetSRID(public.ST_MakePoint(-1.454,53.353),4326)), + ('40000000-0000-4000-8000-000000000013','FD-013','Inspect gravel path','routine', + public.ST_SetSRID(public.ST_MakePoint(-1.449,53.393),4326)), + ('40000000-0000-4000-8000-000000000014','FD-014','Replace bike rack','routine', + public.ST_SetSRID(public.ST_MakePoint(-1.49,53.373),4326)), + ('40000000-0000-4000-8000-000000000015','FD-015','Repair allotment tap','routine', + public.ST_SetSRID(public.ST_MakePoint(-1.479,53.398),4326)), + ('40000000-0000-4000-8000-000000000016','FD-016','Clear blocked culvert','urgent', + public.ST_SetSRID(public.ST_MakePoint(-1.44,53.382),4326)), + ('40000000-0000-4000-8000-000000000017','FD-017','Inspect boundary sign','routine', + public.ST_SetSRID(public.ST_MakePoint(-1.501,53.375),4326)), + ('40000000-0000-4000-8000-000000000018','FD-018','Repair orchard fence','routine', + public.ST_SetSRID(public.ST_MakePoint(-1.501,53.39),4326)) +ON CONFLICT (id) DO NOTHING; +INSERT INTO dispatch.map_state (id,longitude,latitude,zoom,area_id,mode) +VALUES ('dispatch',-1.467,53.373,13,'20000000-0000-4000-8000-000000000001','assignments') +ON CONFLICT (id) DO NOTHING; +COMMIT; diff --git a/applications/field-dispatch/src/MapView.tsx b/applications/field-dispatch/src/MapView.tsx new file mode 100644 index 00000000..c897ee54 --- /dev/null +++ b/applications/field-dispatch/src/MapView.tsx @@ -0,0 +1,275 @@ +import { useEffect, useRef, useState } from "react"; +import { + Map, + NavigationControl, + addProtocol, + removeProtocol, + setWorkerUrl, +} from "maplibre-gl"; +import workerUrl from "maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url"; +import { Protocol } from "pmtiles"; +import { MapLibreOverlay } from "@deck.gl/maplibre"; +import { ArcLayer, ScatterplotLayer } from "@deck.gl/layers"; +import type { Color } from "@deck.gl/core"; +import type { Bootstrap, Job, MapState, Viewport } from "./domain"; +import "maplibre-gl/dist/maplibre-gl.css"; + +setWorkerUrl(workerUrl); +function rgb(hex: string): Color { + return [ + parseInt(hex.slice(1, 3), 16), + parseInt(hex.slice(3, 5), 16), + parseInt(hex.slice(5, 7), 16), + 230, + ]; +} +type Props = { + data: Bootstrap; + jobs: Job[]; + selectedJob: string | null; + areaId: string; + mode: MapState["mode"]; + onJob: (id: string) => void; + onViewport: (bounds: Viewport) => void; + onCamera: (camera: Pick) => void; + onError: (message: string) => void; +}; +export function MapView(props: Props) { + const container = useRef(null); + const mapRef = useRef(null); + const overlayRef = useRef(null); + const [ready, setReady] = useState(false); + useEffect(() => { + const protocol = new Protocol(); + addProtocol("pmtiles", protocol.tile); + const map = new Map({ + container: container.current!, + center: [props.data.state.longitude, props.data.state.latitude], + zoom: props.data.state.zoom, + minZoom: 12, + maxZoom: 16, + maxBounds: [ + [-1.51, 53.33], + [-1.41, 53.41], + ], + dragRotate: false, + pitchWithRotate: false, + touchPitch: false, + canvasContextAttributes: { preserveDrawingBuffer: true, antialias: true }, + style: { + version: 8, + sources: { + town: { + type: "vector", + url: `pmtiles://${location.origin}/maintenance-town.pmtiles`, + attribution: "Synthetic map data · ClickHouse examples", + }, + }, + layers: [ + { + id: "paper", + type: "background", + paint: { "background-color": "#eff0e5" }, + }, + { + id: "blocks", + type: "fill", + source: "town", + "source-layer": "land", + filter: ["==", ["get", "kind"], "block"], + paint: { "fill-color": "#d8d4c3", "fill-opacity": 0.7 }, + }, + { + id: "parks", + type: "fill", + source: "town", + "source-layer": "land", + filter: ["==", ["get", "kind"], "park"], + paint: { "fill-color": "#c0d2a4" }, + }, + { + id: "river", + type: "line", + source: "town", + "source-layer": "water", + paint: { "line-color": "#8cb7c3", "line-width": 7 }, + }, + { + id: "roads", + type: "line", + source: "town", + "source-layer": "roads", + paint: { + "line-color": "#faf9f2", + "line-width": ["case", ["==", ["get", "kind"], "main"], 5, 3], + }, + }, + ], + }, + }); + mapRef.current = map; + map.addControl(new NavigationControl({ showCompass: false }), "top-right"); + const captureView = () => { + const bounds = map.getBounds(); + props.onViewport({ + west: bounds.getWest(), + south: bounds.getSouth(), + east: bounds.getEast(), + north: bounds.getNorth(), + }); + const center = map.getCenter(); + props.onCamera({ + longitude: center.lng, + latitude: center.lat, + zoom: map.getZoom(), + }); + }; + map.on("load", () => { + map.addSource("areas", { + type: "geojson", + data: { + type: "FeatureCollection", + features: props.data.areas.map((area) => ({ + ...area.feature, + properties: { ...area.feature.properties, color: area.color }, + })), + }, + }); + map.addLayer({ + id: "service-fill", + type: "fill", + source: "areas", + paint: { "fill-color": ["get", "color"], "fill-opacity": 0.07 }, + }); + map.addLayer({ + id: "service-outline", + type: "line", + source: "areas", + paint: { + "line-color": ["get", "color"], + "line-width": 2, + "line-dasharray": [4, 2], + }, + }); + const overlay = new MapLibreOverlay({ interleaved: true, layers: [] }); + map.addControl(overlay); + overlayRef.current = overlay; + map.on("click", (event) => { + const picked = overlay.pickObject({ + x: event.point.x, + y: event.point.y, + radius: 5, + layerIds: ["maintenance-jobs"], + }); + if (picked?.object) props.onJob((picked.object as Job).id); + }); + setReady(true); + captureView(); + }); + map.on("moveend", captureView); + map.on("error", () => + props.onError( + "The map could not render. Check WebGL and the local basemap archive.", + ), + ); + return () => { + setReady(false); + overlayRef.current?.finalize(); + overlayRef.current = null; + map.remove(); + mapRef.current = null; + removeProtocol("pmtiles"); + }; + }, [props.data, props.onViewport, props.onCamera, props.onError]); + useEffect(() => { + if (!ready || !mapRef.current) return; + mapRef.current.setPaintProperty("service-fill", "fill-opacity", [ + "case", + ["==", ["get", "id"], props.areaId], + 0.2, + 0.04, + ]); + mapRef.current.setPaintProperty("service-outline", "line-width", [ + "case", + ["==", ["get", "id"], props.areaId], + 3, + 1, + ]); + }, [ready, props.areaId]); + useEffect(() => { + if (!ready || !overlayRef.current) return; + const teams = new globalThis.Map( + props.data.teams.map((team) => [team.id, team]), + ); + const areas = new globalThis.Map( + props.data.areas.map((area) => [area.id, area]), + ); + const color = (job: Job): Color => + props.mode === "priority" + ? job.priority === "urgent" + ? [178, 86, 42, 240] + : [88, 105, 120, 220] + : job.teamId + ? rgb(areas.get(teams.get(job.teamId)!.areaId)!.color) + : [93, 103, 113, 235]; + overlayRef.current.setProps({ + layers: [ + new ArcLayer({ + id: "assignment-links", + data: + props.mode === "assignments" + ? props.jobs.filter((job) => job.teamId && teams.has(job.teamId)) + : [], + getSourcePosition: (job) => teams.get(job.teamId!)!.position, + getTargetPosition: (job) => job.position, + getSourceColor: color, + getTargetColor: color, + getWidth: 2, + getHeight: 0.12, + pickable: false, + }), + new ScatterplotLayer({ + id: "maintenance-jobs", + data: props.jobs, + pickable: true, + getPosition: (job) => job.position, + getFillColor: color, + getRadius: (job) => (job.priority === "urgent" ? 34 : 24), + radiusMinPixels: 7, + radiusMaxPixels: 18, + stroked: true, + getLineColor: (job) => + job.id === props.selectedJob + ? [255, 255, 255, 255] + : [49, 65, 71, 220], + getLineWidth: (job) => (job.id === props.selectedJob ? 4 : 1), + lineWidthUnits: "pixels", + updateTriggers: { + getFillColor: [props.mode], + getLineColor: [props.selectedJob], + getLineWidth: [props.selectedJob], + }, + }), + ], + }); + }, [ + ready, + props.data, + props.jobs, + props.selectedJob, + props.mode, + props.onJob, + ]); + return ( +
+
+ + {ready ? "Map ready · Synthetic town" : "Loading local map…"} + +
+ ); +} diff --git a/applications/field-dispatch/src/domain.ts b/applications/field-dispatch/src/domain.ts new file mode 100644 index 00000000..89d34205 --- /dev/null +++ b/applications/field-dispatch/src/domain.ts @@ -0,0 +1,59 @@ +import { z } from "zod"; +import type { Feature, Polygon } from "geojson"; + +export const viewportSchema = z + .object({ + west: z.coerce.number().finite().min(-1.53).max(-1.39), + south: z.coerce.number().finite().min(53.31).max(53.43), + east: z.coerce.number().finite().min(-1.53).max(-1.39), + north: z.coerce.number().finite().min(53.31).max(53.43), + }) + .refine( + (v) => + v.east > v.west && + v.north > v.south && + v.east - v.west <= 0.14 && + v.north - v.south <= 0.12, + "Use a bounded viewport around the synthetic town.", + ); +export const assignmentSchema = z + .object({ + jobId: z.uuid(), + teamId: z.uuid(), + revision: z.number().int().positive().max(2147483646), + }) + .strict(); +export const mapStateSchema = z + .object({ + longitude: z.number().finite().min(-1.52).max(-1.4), + latitude: z.number().finite().min(53.32).max(53.42), + zoom: z.number().finite().min(12).max(16), + areaId: z.uuid(), + mode: z.enum(["assignments", "priority"]), + revision: z.number().int().positive().max(2147483646), + }) + .strict(); +export type Viewport = z.infer; +export type MapState = z.infer; +export type Area = { + id: string; + name: string; + color: string; + feature: Feature; +}; +export type Team = { + id: string; + name: string; + areaId: string; + position: [number, number]; +}; +export type Job = { + id: string; + code: string; + title: string; + priority: "routine" | "urgent"; + position: [number, number]; + teamId: string | null; + revision: number; +}; +export type Bootstrap = { areas: Area[]; teams: Team[]; state: MapState }; diff --git a/applications/field-dispatch/src/main.tsx b/applications/field-dispatch/src/main.tsx new file mode 100644 index 00000000..7c03ebbe --- /dev/null +++ b/applications/field-dispatch/src/main.tsx @@ -0,0 +1,346 @@ +import { createRoot } from "react-dom/client"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { area, booleanPointInPolygon, distance, point } from "@turf/turf"; +import { MapView } from "./MapView"; +import type { Bootstrap, Job, MapState, Viewport } from "./domain"; +import "./styles.css"; + +async function api(path: string, options?: RequestInit): Promise { + const response = await fetch(path, options); + const value = await response.json(); + if (!response.ok) + throw new Error(value.error ?? "The request could not be completed."); + return value as T; +} +function App() { + const [data, setData] = useState(null); + const [jobs, setJobs] = useState([]); + const [total, setTotal] = useState(0); + const [capped, setCapped] = useState(false); + const [jobsStatus, setJobsStatus] = useState<"loading" | "ready" | "error">( + "loading", + ); + const [selectedJob, setSelectedJob] = useState(null); + const [areaId, setAreaId] = useState(""); + const [mode, setMode] = useState("assignments"); + const [camera, setCamera] = useState | null>(null); + const [savedRevision, setSavedRevision] = useState(1); + const [message, setMessage] = useState(""); + const [error, setError] = useState(""); + const [busy, setBusy] = useState(false); + const viewport = useRef(null); + const request = useRef(null); + const fetchJobs = useCallback((bounds: Viewport) => { + viewport.current = bounds; + setJobs([]); + setTotal(0); + setCapped(false); + setJobsStatus("loading"); + request.current?.abort(); + const controller = new AbortController(); + request.current = controller; + const query = new URLSearchParams( + Object.entries(bounds).map(([key, value]) => [key, String(value)]), + ); + void api<{ jobs: Job[]; total: number; capped: boolean }>( + `/api/jobs?${query}`, + { signal: controller.signal }, + ) + .then((result) => { + if (controller.signal.aborted) return; + setJobs(result.jobs); + setTotal(result.total); + setCapped(result.capped); + setJobsStatus("ready"); + setError(""); + }) + .catch((err) => { + if (!controller.signal.aborted) { + setError(err.message); + setJobsStatus("error"); + } + }); + }, []); + useEffect(() => { + let active = true; + void api("/api/bootstrap") + .then((value) => { + if (!active) return; + setData(value); + setAreaId(value.state.areaId); + setMode(value.state.mode); + setSavedRevision(value.state.revision); + }) + .catch((err) => { + if (active) setError(err.message); + }); + return () => { + active = false; + request.current?.abort(); + }; + }, []); + const service = data?.areas.find((value) => value.id === areaId); + const team = data?.teams.find((value) => value.areaId === areaId); + const job = jobs.find((value) => value.id === selectedJob); + const inside = !!( + service && + job && + booleanPointInPolygon(point(job.position), service.feature) + ); + const previewArea = service ? area(service.feature) / 1_000_000 : 0; + const previewDistance = + job && team + ? distance(point(team.position), point(job.position), { + units: "kilometers", + }) + : 0; + async function assignJob() { + if (!job || !team) return; + setBusy(true); + setMessage(""); + setError(""); + try { + await api("/api/assign", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + jobId: job.id, + teamId: team.id, + revision: job.revision, + }), + }); + if (viewport.current) fetchJobs(viewport.current); + setMessage(`${team.name} assigned to ${job.code}.`); + } catch (err) { + setError((err as Error).message); + } finally { + setBusy(false); + } + } + async function saveView() { + if (!camera) return; + setBusy(true); + setMessage(""); + setError(""); + try { + const result = await api<{ revision: number }>("/api/map", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + ...camera, + areaId, + mode, + revision: savedRevision, + }), + }); + setSavedRevision(result.revision); + setMessage( + "Map view saved. Reload to restore this area, camera and layer.", + ); + } catch (err) { + setError((err as Error).message); + } finally { + setBusy(false); + } + } + return ( +
+
+
+

FIELD OPERATIONS / MAINTENANCE DESK

+

A place for every job.

+

+ Review the work in view, check service coverage and send the right + crew. +

+
+ + Synthetic town +
+ Morning dispatch +
+
+
+ + +
+ AREA PREVIEW + {previewArea.toFixed(2)} km² +
+ +
+ {message && ( +

+ {message} +

+ )} + {error && ( +

+ {error} +

+ )} +
+ +
+ {data ? ( + + ) : ( +
Loading the saved dispatch view…
+ )} +
+ + + Unassigned / routine + + {mode === "assignments" ? ( + data?.teams.map((t) => ( + + a.id === t.areaId) + ?.color, + }} + /> + {t.name} + + )) + ) : ( + + + Urgent work + + )} + + Lines link assigned jobs to crew depots; they are not road routes. + +
+
+
+ 02 / COVERAGE BEFORE COMMIT +

+ {job + ? `${job.code} · ${job.title}` + : "Select a job on the map or in the list"} +

+ {job && ( +

+ {inside ? "Inside" : "Outside"} {service?.name} ·{" "} + {previewDistance.toFixed(2)} km straight-line from{" "} + {team?.name} depot. +

+ )} +

+ The browser previews coverage. PostGIS checks the stored + boundary before saving. +

+
+ +
+
+
+
+ Field dispatch · Fictional jobs and service boundaries · Coordinates + [longitude, latitude], SRID 4326 +
+
+ ); +} +createRoot(document.getElementById("root")!).render(); diff --git a/applications/field-dispatch/src/styles.css b/applications/field-dispatch/src/styles.css new file mode 100644 index 00000000..b6d4c1aa --- /dev/null +++ b/applications/field-dispatch/src/styles.css @@ -0,0 +1,393 @@ +:root { + font-family: Arial, sans-serif; + color: #253f49; + background: #f2f1eb; + font-synthesis: none; +} +* { + box-sizing: border-box; +} +body { + margin: 0; +} +button, +select { + font: inherit; +} +button { + cursor: pointer; +} +button:disabled { + cursor: default; + opacity: 0.52; +} +button:focus-visible, +select:focus-visible { + outline: 3px solid #cc9b49; + outline-offset: 3px; +} +main { + max-width: 1500px; + margin: 0 auto; + padding: 40px 36px 22px; +} +header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 24px; + margin-bottom: 32px; +} +h1, +h2, +p { + margin: 0; +} +h1 { + font-family: Georgia, serif; + font-size: clamp(32px, 4vw, 54px); + font-weight: normal; + letter-spacing: -0.04em; + margin: 10px 0; +} +h2 { + font-family: Georgia, serif; + font-size: 24px; + font-weight: normal; + line-height: 1.2; +} +.eyebrow { + font-size: 10px; + font-weight: bold; + letter-spacing: 0.15em; + color: #687a75; +} +.intro { + color: #73817f; + font-size: 14px; + line-height: 1.6; +} +.edition { + text-align: right; + line-height: 1.5; + font-size: 12px; + color: #6c7a76; + border-right: 3px solid #af8648; + padding-right: 16px; +} +.toolbar { + background: #203e4a; + color: #f7f7ef; + display: flex; + gap: 24px; + align-items: center; + padding: 18px 22px; + border-radius: 8px; + margin-bottom: 24px; +} +.toolbar label { + display: flex; + flex-direction: column; + gap: 7px; + font-size: 10px; + font-weight: bold; + letter-spacing: 0.06em; +} +select { + min-width: 170px; + font-size: 13px; + font-weight: normal; + border: 1px solid #617980; + color: #fff; + background: #2b4b58; + border-radius: 5px; + padding: 10px 30px 10px 12px; +} +.coverage { + display: flex; + flex-direction: column; + gap: 6px; + margin-left: auto; +} +.coverage span { + font-size: 9px; + letter-spacing: 0.12em; + color: #a2bbb8; +} +.coverage strong { + font-size: 19px; + font-weight: normal; +} +.save, +.review-panel button { + padding: 12px 18px; + border: 0; + border-radius: 5px; + background: #c89a52; + color: #183441; + font-size: 12px; + font-weight: bold; +} +.notice { + padding: 14px 18px; + margin: 0 0 18px; + border-radius: 5px; + font-size: 13px; + line-height: 1.5; +} +.success { + background: #e5eddf; + border: 1px solid #c9d6bd; +} +.failure { + background: #fae5dc; + border: 1px solid #deb6a3; + color: #883d2b; +} +.desk { + display: grid; + grid-template-columns: 300px minmax(0, 1fr); + gap: 24px; +} +aside { + border: 1px solid #dddcd3; + border-radius: 8px; + background: #fffefa; + overflow: hidden; +} +.panel-heading { + padding: 24px 20px 18px; + border-bottom: 1px solid #e6e5da; +} +.panel-heading h2 { + margin: 10px 0 8px; +} +.panel-heading p { + font-size: 12px; + color: #79857c; + line-height: 1.5; +} +.jobs { + max-height: 680px; + overflow: auto; + padding: 12px; +} +.job { + display: block; + text-align: left; + width: 100%; + border: 1px solid transparent; + border-bottom-color: #e4e4da; + background: transparent; + padding: 14px 12px; + border-radius: 5px; + color: #2b424a; +} +.job:hover { + background: #f5f5ed; +} +.job.selected { + background: #edf0e6; + border-color: #8ba488; +} +.job-top { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + font-size: 10px; + letter-spacing: 0.05em; +} +.priority { + font-size: 9px; + border-radius: 12px; + padding: 3px 7px; + letter-spacing: 0; +} +.priority.urgent { + background: #f7e5d2; + color: #a55c2e; +} +.priority.routine { + background: #e8eceb; + color: #6b787f; +} +.job-title { + display: block; + font-size: 14px; + line-height: 1.45; + margin: 8px 0 6px; +} +.assignment { + font-size: 10px; + color: #748274; +} +.map-column { + min-width: 0; +} +.map-frame { + position: relative; + overflow: hidden; + border-radius: 8px; + border: 1px solid #c7cdbb; +} +.map { + height: 570px; + width: 100%; +} +.map-status { + position: absolute; + left: 12px; + top: 12px; + padding: 7px 11px; + border-radius: 4px; + font-size: 10px; + background: #fffff4e8; + box-shadow: 0 1px 6px #243a4620; +} +.loading { + height: 570px; + display: grid; + place-content: center; + background: #e1e7db; + border-radius: 8px; + color: #6c7f73; +} +.legend { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 14px; + padding: 14px 3px; + font-size: 10px; + color: #69796e; +} +.legend > span { + display: flex; + align-items: center; + gap: 6px; +} +.legend small { + display: block; + width: 100%; + font-size: 10px; +} +.swatch { + width: 9px; + height: 9px; + border-radius: 50%; + display: inline-block; +} +.unassigned { + background: #5d6771; +} +.swatch.urgent { + background: #b2562a; +} +.review-panel { + background: #fffefa; + padding: 24px; + border-radius: 8px; + border: 1px solid #dddcd3; + display: flex; + justify-content: space-between; + align-items: center; + gap: 24px; +} +.review-panel h2 { + margin: 10px 0; + font-size: 23px; +} +.review-panel p { + font-size: 12px; + line-height: 1.5; +} +.review-panel .hint { + margin-top: 7px; + font-size: 10px; + color: #788275; +} +.review-panel button { + flex-shrink: 0; +} +footer { + padding: 24px 2px 0; + color: #8a9387; + font-size: 10px; + line-height: 1.5; +} +@media (max-width: 900px) { + .desk { + grid-template-columns: 240px minmax(0, 1fr); + gap: 16px; + } + main { + padding: 28px 20px 18px; + } + .toolbar { + gap: 16px; + } + .review-panel { + flex-direction: column; + align-items: flex-start; + padding: 18px; + } + .toolbar select { + min-width: 140px; + } +} +@media (max-width: 680px) { + header { + align-items: flex-start; + } + .edition { + display: none; + } + .toolbar { + flex-wrap: wrap; + padding: 16px; + gap: 16px; + } + .toolbar label { + flex: 1; + } + select { + width: 100%; + } + .coverage { + margin-left: 0; + } + .save { + margin-left: auto; + } + .desk { + grid-template-columns: 1fr; + } + .jobs { + max-height: 220px; + } + .map { + height: 390px; + } + .panel-heading { + padding: 18px; + } + .legend { + gap: 10px; + } +} + +.panel-heading button { + margin-top: 10px; + padding: 8px 12px; + border: 1px solid #bfc9bd; + border-radius: 5px; + background: #edf1e8; + color: #29443d; + font: inherit; + cursor: pointer; +} +.panel-heading button:disabled { + opacity: 0.55; + cursor: wait; +} diff --git a/applications/field-dispatch/tests/browser/dispatch.spec.ts b/applications/field-dispatch/tests/browser/dispatch.spec.ts new file mode 100644 index 00000000..71473406 --- /dev/null +++ b/applications/field-dispatch/tests/browser/dispatch.spec.ts @@ -0,0 +1,308 @@ +import { test, expect } from "@playwright/test"; +import { spawn, type ChildProcess } from "node:child_process"; +import { once } from "node:events"; +import { mkdirSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { database, closeDatabase } from "../../server/database"; +import { bootstrap, saveMap } from "../../server/dispatch"; +const base = process.env.APP_ORIGIN ?? "http://127.0.0.1:3002"; +const evidence = process.env.EVIDENCE_DIR ?? "test-results"; +let server: ChildProcess; +async function startServer() { + server = spawn( + process.execPath, + ["--env-file=.env", "--import", "tsx", "server/index.ts"], + { stdio: "ignore", env: process.env }, + ); + for (let i = 0; i < 80; i++) { + if (server.exitCode !== null) throw new Error("Dispatch server exited."); + try { + if ((await fetch(base)).ok) return; + } catch {} + await new Promise((resolve) => setTimeout(resolve, 250)); + } + throw new Error("Dispatch server did not start."); +} +async function stopServer() { + if (server && server.exitCode === null) { + const exited = once(server, "exit"); + server.kill("SIGTERM"); + await exited; + } +} +test.beforeAll(async () => { + mkdirSync(evidence, { recursive: true }); + // Dedicated demo test fixture only: restore two jobs and the singleton view. + await database().query( + "UPDATE dispatch.jobs SET team_id=NULL,revision=revision+1 WHERE code IN ('FD-001','FD-002')", + ); + const state = (await bootstrap()).state; + await saveMap({ + ...state, + longitude: -1.467, + latitude: 53.373, + zoom: 13, + areaId: "20000000-0000-4000-8000-000000000001", + mode: "assignments", + }); + await startServer(); +}); +test.afterAll(async () => { + await stopServer(); + await closeDatabase(); +}); +test("actual WebGL, PMTiles, deck picking, coverage, assignment, viewport retry and saved-view process restart", async ({ + page, + request, +}) => { + let viewportRequest = ""; + page.on("request", (req) => { + if (req.url().includes("/api/jobs?")) viewportRequest = req.url(); + }); + + const errors: string[] = []; + page.on("pageerror", (error) => errors.push(error.message)); + const ranges: { status: number; range: string | null }[] = []; + page.on("response", (response) => { + if (response.url().endsWith(".pmtiles")) + ranges.push({ + status: response.status(), + range: response.headers()["content-range"] ?? null, + }); + }); + await page.goto("/"); + await expect( + page.getByText("Map ready · Synthetic town", { exact: true }), + ).toBeVisible(); + await expect(page.getByRole("button", { name: /FD-001/ })).toBeVisible(); + await expect + .poll(() => ranges.filter((value) => value.status === 206).length) + .toBeGreaterThan(0); + const canvas = page.locator("canvas.maplibregl-canvas"); + const rendered = await canvas.evaluate((element: HTMLCanvasElement) => { + const gl = element.getContext("webgl2")!; + if (!gl) throw new Error("No actual WebGL2 context."); + const pixels = new Uint8Array( + gl.drawingBufferWidth * gl.drawingBufferHeight * 4, + ); + gl.readPixels( + 0, + 0, + gl.drawingBufferWidth, + gl.drawingBufferHeight, + gl.RGBA, + gl.UNSIGNED_BYTE, + pixels, + ); + const colors = new Set(); + for (let i = 0; i < pixels.length; i += 400) + colors.add( + `${pixels[i]},${pixels[i + 1]},${pixels[i + 2]},${pixels[i + 3]}`, + ); + return { + webgl: gl.getParameter(gl.VERSION), + renderer: gl.getParameter(gl.RENDERER), + colors: colors.size, + }; + }); + expect(rendered.colors).toBeGreaterThan(5); + const state = (await (await request.get("/api/bootstrap")).json()).state; + const size = await canvas.boundingBox(); + expect(size).not.toBeNull(); + const query = new URL(viewportRequest).searchParams; + const bounds = Object.fromEntries( + [...query].map(([key, value]) => [key, Number(value)]), + ); + const projected = await page.evaluate( + ({ bounds, width, height }) => { + const mercator = (lat: number) => + Math.log(Math.tan(Math.PI / 4 + (lat * Math.PI) / 360)); + return { + x: ((-1.481 - bounds.west) / (bounds.east - bounds.west)) * width, + y: + ((mercator(bounds.north) - mercator(53.361)) / + (mercator(bounds.north) - mercator(bounds.south))) * + height, + }; + }, + { bounds, width: size!.width, height: size!.height }, + ); + writeFileSync( + join(evidence, "picking-debug.json"), + JSON.stringify({ state, size, projected, rendered }), + ); + await page.screenshot({ + path: join(evidence, "picking-debug.png"), + fullPage: true, + }); + await page.waitForTimeout(1000); + await page.mouse.click(size!.x + projected.x, size!.y + projected.y); + await expect(page.locator(".review-panel h2")).toContainText("FD-001"); + await expect(page.locator(".review-panel")).toContainText( + "Inside West garden", + ); + await page + .getByRole("button", { name: "Assign Garden crew", exact: true }) + .click(); + await expect(page.getByRole("status")).toContainText( + "Garden crew assigned to FD-001", + ); + await expect(page.getByRole("button", { name: /FD-001/ })).toContainText( + "Garden crew", + ); + const jobs = ( + await ( + await request.get( + "/api/jobs?west=-1.51&south=53.33&east=-1.41&north=53.41", + ) + ).json() + ).jobs; + expect( + ( + await request.post("/api/map", { + headers: { origin: "https://wrong-origin.invalid" }, + data: state, + }) + ).status(), + ).toBe(403); + const first = jobs.find((job: any) => job.code === "FD-001"); + const stale = await request.post("/api/assign", { + headers: { origin: base }, + data: { + jobId: first.id, + teamId: first.teamId, + revision: first.revision - 1, + }, + }); + expect(stale.status()).toBe(409); + await page.getByRole("button", { name: /FD-002/ }).click(); + await expect(page.locator(".review-panel")).toContainText( + "Outside West garden", + ); + await expect( + page.getByRole("button", { name: "Assign Garden crew", exact: true }), + ).toBeDisabled(); + const river = jobs.find((job: any) => job.code === "FD-002"); + expect( + ( + await request.post("/api/assign", { + headers: { origin: base }, + data: { + jobId: river.id, + teamId: first.teamId, + revision: river.revision, + }, + }) + ).status(), + ).toBe(400); + await page + .getByRole("combobox", { name: "Service area", exact: true }) + .selectOption("20000000-0000-4000-8000-000000000002"); + await expect(page.locator(".review-panel")).toContainText("Inside Riverside"); + await page + .getByRole("button", { name: "Assign River crew", exact: true }) + .click(); + await expect(page.getByRole("status")).toContainText( + "River crew assigned to FD-002", + ); + await page + .getByRole("combobox", { name: "Map layer", exact: true }) + .selectOption("priority"); + await page.route("**/api/jobs?**", (route) => + route.fulfill({ + status: 503, + contentType: "application/json", + body: JSON.stringify({ error: "Controlled viewport failure" }), + }), + ); + await page.getByRole("button", { name: "Refresh jobs" }).click(); + await expect( + page.getByRole("heading", { name: "Jobs unavailable" }), + ).toBeVisible(); + await expect(page.locator(".jobs button")).toHaveCount(0); + await page.unroute("**/api/jobs?**"); + await page.getByRole("button", { name: "Refresh jobs" }).click(); + await expect(page.getByRole("button", { name: /FD-001/ })).toBeVisible(); + await expect(page.getByRole("alert")).toHaveCount(0); + const before = (await (await request.get("/api/bootstrap")).json()).state; + await page.mouse.move(size!.x + size!.width / 2, size!.y + size!.height / 2); + await page.mouse.down(); + await page.mouse.move( + size!.x + size!.width / 2 + 90, + size!.y + size!.height / 2 + 35, + { steps: 12 }, + ); + await page.mouse.up(); + await page.waitForTimeout(700); + await page.getByRole("button", { name: "Save map view" }).click(); + await expect(page.getByRole("status")).toContainText("Map view saved"); + const saved = (await (await request.get("/api/bootstrap")).json()).state; + expect(saved.longitude).not.toBe(before.longitude); + expect(saved.mode).toBe("priority"); + expect(saved.areaId).toBe("20000000-0000-4000-8000-000000000002"); + await page.screenshot({ + path: join(evidence, "dispatch-desktop.png"), + fullPage: true, + }); + const originalPid = server.pid; + await stopServer(); + await startServer(); + await page.reload(); + await expect( + page.getByText("Map ready · Synthetic town", { exact: true }), + ).toBeVisible(); + await expect( + page.getByRole("combobox", { name: "Map layer", exact: true }), + ).toHaveValue("priority"); + expect((await (await request.get("/api/bootstrap")).json()).state).toEqual( + saved, + ); + const restored = ( + await ( + await request.get( + "/api/jobs?west=-1.51&south=53.33&east=-1.41&north=53.41", + ) + ).json() + ).jobs; + expect(restored.find((job: any) => job.code === "FD-001").teamId).toBe( + first.teamId, + ); + await page.setViewportSize({ width: 390, height: 844 }); + await page.waitForTimeout(500); + await page.screenshot({ + path: join(evidence, "dispatch-mobile.png"), + fullPage: true, + }); + expect( + await page.evaluate( + () => document.documentElement.scrollWidth <= window.innerWidth, + ), + ).toBe(true); + expect(errors).toEqual([]); + writeFileSync( + join(evidence, "restart.json"), + JSON.stringify( + { + originalPid, + replacementPid: server.pid, + saved, + assignmentsRestored: true, + }, + null, + 2, + ), + ); + writeFileSync( + join(evidence, "webgl.json"), + JSON.stringify( + { + rendered, + ranges, + picking: "FD-001 actual deck point", + viewportFailureCleared: true, + }, + null, + 2, + ), + ); +}); diff --git a/applications/field-dispatch/tests/database.test.ts b/applications/field-dispatch/tests/database.test.ts new file mode 100644 index 00000000..cc957e4f --- /dev/null +++ b/applications/field-dispatch/tests/database.test.ts @@ -0,0 +1,183 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { lookup } from "node:dns/promises"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { execFileSync } from "node:child_process"; +import { Pool } from "pg"; +import { database, databaseConfig, closeDatabase } from "../server/database"; +import { + assign, + bootstrap, + viewportJobs, + saveMap, + DispatchError, +} from "../server/dispatch"; +const id = (prefix: string, suffix: number) => + `${prefix}0000000-0000-4000-8000-${String(suffix).padStart(12, "0")}`; +test("managed PostGIS: indexed viewport, stored coverage, revision races, grants and verified TLS", async () => { + try { + const identity = ( + await database().query( + "SELECT current_user,version() AS version,public.postgis_full_version() AS postgis", + ) + ).rows[0]; + assert.equal(identity.current_user, "dispatch_app"); + console.log(JSON.stringify(identity)); + const limited = await viewportJobs({ + west: -1.485, + south: 53.355, + east: -1.475, + north: 53.365, + }); + assert.ok(limited.jobs.some((job) => job.code === "FD-001")); + assert.ok( + limited.jobs.every( + (job) => + job.position[0] >= -1.485 && + job.position[0] <= -1.475 && + job.position[1] >= 53.355 && + job.position[1] <= 53.365, + ), + ); + const river = ( + await database().query( + "SELECT team_id,revision FROM dispatch.jobs WHERE id=$1", + [id("4", 2)], + ) + ).rows[0]; + await assert.rejects( + assign({ + jobId: id("4", 2), + teamId: id("3", 1), + revision: river.revision, + }), + (error) => error instanceof DispatchError && error.status === 400, + ); + assert.deepEqual( + ( + await database().query( + "SELECT team_id,revision FROM dispatch.jobs WHERE id=$1", + [id("4", 2)], + ) + ).rows[0], + river, + ); + const revision = ( + await database().query("SELECT revision FROM dispatch.jobs WHERE id=$1", [ + id("4", 4), + ]) + ).rows[0].revision; + const race = await Promise.allSettled([ + assign({ jobId: id("4", 4), teamId: id("3", 1), revision }), + assign({ jobId: id("4", 4), teamId: id("3", 3), revision }), + ]); + assert.equal( + race.filter((result) => result.status === "fulfilled").length, + 1, + ); + assert.ok( + race.some( + (result) => + result.status === "rejected" && + result.reason instanceof DispatchError && + result.reason.status === 409, + ), + ); + const state = (await bootstrap()).state; + const saves = await Promise.allSettled([ + saveMap({ ...state, mode: "priority" }), + saveMap({ ...state, mode: "assignments" }), + ]); + assert.equal( + saves.filter((result) => result.status === "fulfilled").length, + 1, + ); + assert.ok( + saves.some( + (result) => + result.status === "rejected" && + result.reason instanceof DispatchError && + result.reason.status === 409, + ), + ); + for (const sql of [ + "CREATE TABLE dispatch.forbidden(id integer)", + "ALTER TABLE dispatch.jobs ADD COLUMN forbidden text", + "DELETE FROM dispatch.jobs", + "UPDATE dispatch.jobs SET geom=public.ST_SetSRID(public.ST_MakePoint(0,0),4326)", + "INSERT INTO dispatch.jobs(id,code,title,priority,geom) VALUES(gen_random_uuid(),'forbidden','Forbidden','routine',public.ST_SetSRID(public.ST_MakePoint(0,0),4326))", + ]) + await assert.rejects( + database().query(sql), + (error: any) => error.code === "42501", + ); + const config = databaseConfig(); + const ip = new URL(process.env.DATABASE_URL!); + ip.hostname = (await lookup(ip.hostname, { family: 4 })).address; + const wrongName = new Pool({ + ...config, + connectionString: ip.toString(), + max: 1, + ssl: { ...(config.ssl as object), servername: "wrong-host.invalid" }, + }); + try { + await assert.rejects( + wrongName.query("SELECT 1"), + (err: any) => err.code === "ERR_TLS_CERT_ALTNAME_INVALID", + ); + } finally { + await wrongName.end(); + } + const directory = mkdtempSync(join(tmpdir(), "dispatch-ca-")); + try { + execFileSync( + "openssl", + [ + "req", + "-x509", + "-newkey", + "rsa:2048", + "-nodes", + "-keyout", + join(directory, "key.pem"), + "-out", + join(directory, "ca.pem"), + "-days", + "1", + "-subj", + "/CN=unrelated-dispatch-root", + ], + { stdio: "ignore" }, + ); + const wrongCa = new Pool({ + ...config, + max: 1, + ssl: { + ...(config.ssl as object), + ca: readFileSync(join(directory, "ca.pem"), "utf8"), + }, + }); + try { + await assert.rejects(wrongCa.query("SELECT 1"), (err: any) => + [ + "UNABLE_TO_VERIFY_LEAF_SIGNATURE", + "UNABLE_TO_GET_ISSUER_CERT_LOCALLY", + "SELF_SIGNED_CERT_IN_CHAIN", + "DEPTH_ZERO_SELF_SIGNED_CERT", + ].includes(err.code), + ); + } finally { + await wrongCa.end(); + } + } finally { + rmSync(directory, { recursive: true, force: true }); + } + console.log( + "Passed PostGIS viewport, outside-area rollback, competing job/map writers, restricted grants and pg wrong-CA/hostname controls.", + ); + } finally { + await closeDatabase(); + } +}); diff --git a/applications/field-dispatch/tests/native.test.ts b/applications/field-dispatch/tests/native.test.ts new file mode 100644 index 00000000..a2da08a1 --- /dev/null +++ b/applications/field-dispatch/tests/native.test.ts @@ -0,0 +1,82 @@ +import test from "node:test"; +import { get } from "node:http"; +import assert from "node:assert/strict"; +import { PMTiles } from "pmtiles"; +import { createApplication } from "../server/http"; +import { + assignmentSchema, + mapStateSchema, + viewportSchema, +} from "../src/domain"; + +test("the checked-in PMTiles archive is read through actual HTTP byte ranges", async () => { + process.env.APP_ORIGIN = "http://127.0.0.1:3002"; + const server = createApplication(); + await new Promise((resolve) => + server.listen(3002, "127.0.0.1", resolve), + ); + try { + const url = `${process.env.APP_ORIGIN}/maintenance-town.pmtiles`; + const response = await fetch(url, { headers: { range: "bytes=0-126" } }); + assert.equal(response.status, 206); + assert.equal(response.headers.get("content-range"), "bytes 0-126/7995"); + assert.equal((await response.arrayBuffer()).byteLength, 127); + const archive = new PMTiles(url); + const header = await archive.getHeader(); + assert.equal(header.specVersion, 3); + assert.equal(header.numAddressedTiles, 61); + assert.equal(header.minZoom, 12); + const metadata = await archive.getMetadata(); + assert.equal( + (metadata as { vector_layers: unknown[] }).vector_layers.length, + 3, + ); + const tile = await archive.getZxy(12, 2031, 1327); + assert.ok(tile && tile.data.byteLength > 0); + for (const range of ["bytes=-10", "bytes=8000-", "bytes=0-1,3-5"]) { + assert.equal((await fetch(url, { headers: { range } })).status, 416); + } + const wrongHost = await new Promise((resolve) => + get(url, { headers: { host: "unexpected.example" } }, (response) => { + response.resume(); + resolve(response.statusCode); + }), + ); + assert.equal(wrongHost, 403); + } finally { + await new Promise((resolve, reject) => + server.close((error) => (error ? reject(error) : resolve())), + ); + } +}); + +test("public payloads reject world viewports and non-incrementable PostgreSQL revisions", () => { + assert.equal( + viewportSchema.safeParse({ west: -180, south: -90, east: 180, north: 90 }) + .success, + false, + ); + const ids = { + jobId: "40000000-0000-4000-8000-000000000001", + teamId: "30000000-0000-4000-8000-000000000001", + }; + assert.equal( + assignmentSchema.safeParse({ ...ids, revision: 2147483647 }).success, + false, + ); + assert.equal( + assignmentSchema.safeParse({ ...ids, revision: 2147483646 }).success, + true, + ); + assert.equal( + mapStateSchema.safeParse({ + longitude: -1.467, + latitude: 53.373, + zoom: 13, + areaId: "20000000-0000-4000-8000-000000000001", + mode: "priority", + revision: 2147483647, + }).success, + false, + ); +}); diff --git a/applications/field-dispatch/tsconfig.json b/applications/field-dispatch/tsconfig.json new file mode 100644 index 00000000..085b655d --- /dev/null +++ b/applications/field-dispatch/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true, + "lib": [ + "ES2022", + "DOM", + "DOM.Iterable" + ], + "types": [ + "node", + "vite/client" + ] + }, + "include": [ + "src", + "server", + "tests", + "vite.config.ts", + "playwright.config.ts" + ] +} diff --git a/applications/field-dispatch/vite.config.ts b/applications/field-dispatch/vite.config.ts new file mode 100644 index 00000000..50eb7916 --- /dev/null +++ b/applications/field-dispatch/vite.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +export default defineConfig({ plugins: [react()], server: { host: '127.0.0.1' } });