diff --git a/.prettierignore b/.prettierignore index 805dd09..d30b4be 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,3 +8,4 @@ coverage helm reports .release-please-manifest.json +HANDOFF.md diff --git a/README.md b/README.md index aa757fb..4e601b6 100644 --- a/README.md +++ b/README.md @@ -1,124 +1,53 @@ -# Map Colonies typescript service template +# 3D Operations Trigger ---------------------------------- -This is a basic repo template for building new MapColonies web services in Typescript. +Entry point for 3D operations — **ingest, delete, update, publish/unpublish**. It validates incoming requests (light & fast) and triggers the corresponding [Jobnik](https://mapcolonies.github.io/infra-portal/docs/knowledge-base/jobnik) jobs. Successor to the legacy `3d-gateway` (and, in the near future, `store-trigger`). -> [!IMPORTANT] -> To regenerate the types on openapi change run the command `npm run generate:openapi-types`. +> Epic: MAPCO-11833 · Design: [3D Ingestion High level architecture](https://mapcolonies.atlassian.net/wiki/spaces/MAPConflicResolution/pages/3342172161) · See [docs/PRD.md](./docs/PRD.md) -> [!WARNING] -> After creating a new repo based on this template, you should delete the CODEOWNERS file. - - -## Development -When in development you should use the command `npm run start:dev`. The main benefits are that it enables offline mode for the config package, and source map support for NodeJS errors. - -### Template Features: - -- eslint configuration by [@map-colonies/eslint-config](https://github.com/MapColonies/eslint-config) - -- prettier configuration by [@map-colonies/prettier-config](https://github.com/MapColonies/prettier-config) - -- jest - -- .nvmrc - -- Multi stage production-ready Dockerfile - -- commitlint - -- git hooks - -- logging by [@map-colonies/js-logger](https://github.com/MapColonies/js-logger) - -- OpenAPI request validation - -- config load with [node-config](https://www.npmjs.com/package/node-config) - -- Tracing and metrics by [@map-colonies/telemetry](https://github.com/MapColonies/telemetry) - -- github templates - -- bug report - -- feature request - -- pull request - -- github actions - -- on pull_request - -- LGTM +## API -- test +| Endpoint | Method | Summary | +| --- | --- | --- | +| `/record` | POST | Start an ingestion flow (validate, create Jobnik ingestion job) | +| `/record/{id}` | DELETE | Validate deletability, create Jobnik delete job | +| `/record/{id}` | PATCH | Update metadata for a record | +| `/record/status/{id}` | PATCH | Publish / unpublish a record | -- lint +Full OpenAPI spec: [openapi3.yaml](/openapi3.yaml). Regenerate types on spec change with `npm run generate:openapi-types`. -- snyk +## Requirements -## API -Checkout the OpenAPI spec [here](/openapi3.yaml) +- **Node.js ≥ 24** (required by `@map-colonies/jobnik-sdk`). Use the pinned version via `nvm use`. ## Installation -Install deps with npm - ```bash npm install ``` -## Run Locally - -Clone the project - -```bash - -git clone https://link-to-project - -``` - -Go to the project directory - -```bash - -cd my-project - -``` - -Install dependencies +> During development this service consumes `@map-colonies/3d-shared` (and, later, mc-models v2) via local `file:` links until those packages are published to npm. -```bash - -npm install - -``` - -Start the server +## Run Locally ```bash - -npm run start - +npm run start # build + run +npm run start:dev # offline config + source maps ``` ## Running Tests -To run tests, run the following command - ```bash - -npm run test - +npm run test # all +npm run test:unit # unit only +npm run test:integration # integration only ``` -To only run unit tests: -```bash -npm run test:unit -``` +## Development notes -To only run integration tests: -```bash -npm run test:integration -``` +- eslint / prettier via `@map-colonies/eslint-config` and `@map-colonies/prettier-config` +- vitest for tests +- OpenAPI request validation at the middleware layer +- config via [node-config](https://www.npmjs.com/package/node-config) +- tracing & metrics via `@map-colonies/telemetry` diff --git a/bundledApi.yaml b/bundledApi.yaml new file mode 100644 index 0000000..9d62ccf --- /dev/null +++ b/bundledApi.yaml @@ -0,0 +1,285 @@ +openapi: 3.0.1 +info: + title: 3d-ops-trigger + description: Entry point for 3D operations — validates requests and triggers Jobnik jobs + version: 1.0.0 + license: + name: MIT + url: https://opensource.org/licenses/MIT +security: + - {} +paths: + /records: + post: + operationId: createRecord + tags: + - records + summary: Invoke a new model ingestion flow + description: Validates the request (light & fast) and creates a Jobnik ingestion job. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ingestionPayload' + responses: + '201': + description: Ingestion job created + content: + application/json: + schema: + $ref: '#/components/schemas/jobResponse' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/error' + /records/validate: + post: + operationId: validateRecord + tags: + - records + summary: Invoke a new model ingestion validation + description: Runs the full ingestion validation and returns the result without triggering a Jobnik job. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ingestionPayload' + responses: + '200': + description: Validation result + content: + application/json: + schema: + $ref: '#/components/schemas/validationResultResponse' + /records/canDelete/{recordId}: + get: + operationId: canDeleteRecord + tags: + - records + summary: Validate if a record can be removed + description: Runs the delete validation and returns the result without creating a job. + parameters: + - $ref: '#/components/parameters/recordId' + responses: + '200': + description: Validation result + content: + application/json: + schema: + $ref: '#/components/schemas/validationResultResponse' + /records/{recordId}: + delete: + operationId: deleteRecord + tags: + - records + summary: Delete a model by record id + description: Validates the record can be deleted and creates a Jobnik delete job. + parameters: + - $ref: '#/components/parameters/recordId' + responses: + '200': + description: Delete job created + content: + application/json: + schema: + $ref: '#/components/schemas/jobResponse' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/error' + '404': + description: Record not found + content: + application/json: + schema: + $ref: '#/components/schemas/error' + /metadata/{identifier}: + patch: + operationId: updateMetadata + tags: + - metadata + summary: Update metadata of a model + parameters: + - $ref: '#/components/parameters/identifier' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/updatePayload' + responses: + '200': + description: Metadata updated + content: + application/json: + schema: + $ref: '#/components/schemas/ackResponse' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/error' + '404': + description: Record not found + content: + application/json: + schema: + $ref: '#/components/schemas/error' + /metadata/status/{identifier}: + patch: + operationId: updateMetadataStatus + tags: + - metadata + summary: Update the publish/unpublish status of a model + parameters: + - $ref: '#/components/parameters/identifier' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/statusPayload' + responses: + '200': + description: Status updated + content: + application/json: + schema: + $ref: '#/components/schemas/ackResponse' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/error' + '404': + description: Record not found + content: + application/json: + schema: + $ref: '#/components/schemas/error' + /jobStatus/{jobId}: + get: + operationId: getJobStatus + tags: + - jobStatus + summary: Get the status and progress of a job + parameters: + - $ref: '#/components/parameters/jobId' + responses: + '200': + description: Job status + content: + application/json: + schema: + $ref: '#/components/schemas/jobStatusResponse' + '404': + description: Job not found + content: + application/json: + schema: + $ref: '#/components/schemas/error' +components: + parameters: + identifier: + name: identifier + in: path + required: true + description: The record identifier + schema: + type: string + recordId: + name: recordId + in: path + required: true + description: The record identifier + schema: + type: string + jobId: + name: jobId + in: path + required: true + description: The job identifier + schema: + type: string + schemas: + error: + type: object + required: + - message + properties: + message: + type: string + ingestionPayload: + type: object + required: + - modelPath + - tilesetFilename + - metadata + properties: + modelPath: + type: string + description: Path on the shared storage to the 3DTiles folder or 3TZ archive + tilesetFilename: + type: string + description: The tileset entry name (e.g. tileset.json) + metadata: + type: object + description: 3D record metadata (business validation applied downstream) + additionalProperties: true + updatePayload: + type: object + description: Partial metadata fields to update + additionalProperties: true + statusPayload: + type: object + required: + - status + properties: + status: + type: string + enum: + - PUBLISHED + - UNPUBLISHED + jobResponse: + type: object + required: + - jobId + - status + properties: + jobId: + type: string + status: + type: string + ackResponse: + type: object + required: + - message + properties: + message: + type: string + jobStatusResponse: + type: object + required: + - status + properties: + status: + type: string + percentage: + type: number + validationResultResponse: + type: object + required: + - isValid + properties: + isValid: + type: boolean + message: + type: string diff --git a/catalog-info.yaml b/catalog-info.yaml index 55a51de..b4ab881 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -1,17 +1,17 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: - name: ts-server-boilerplate - description: A boilerplate github repo for a REST API service in NodeJS for MapColonies + name: 3d-ops-trigger + description: Entry point for 3D operations (ingest, delete, update, publish/unpublish) — validates requests and triggers Jobnik jobs annotations: - github.com/project-slug: MapColonies/ts-server-boilerplate + github.com/project-slug: MapColonies/3d-ops-trigger tags: - nodejs - typescript - expressjs - - boilerplate + - 3d spec: type: service lifecycle: production owner: DevInfra - system: boilerplate + system: 3d diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json new file mode 100644 index 0000000..e7c5e28 --- /dev/null +++ b/config/custom-environment-variables.json @@ -0,0 +1,55 @@ +{ + "externalServices": { + "catalog": "CATALOG_URL", + "extractable": "EXTRACTABLE_URL", + "lookupTables": { + "url": "LOOKUP_TABLES_URL", + "subUrl": "LOOKUP_TABLES_SUB_URL" + } + }, + "jobManager": { + "url": "JOB_MANAGER_URL", + "ingestion": { + "jobType": "JOB_INGESTION_TYPE", + "taskType": "TASK_INGESTION_TYPE", + "batches": { + "__name": "INGESTION_TASK_BATCHES", + "__format": "number" + } + }, + "delete": { + "jobType": "JOB_DELETE_TYPE", + "taskType": "TASK_DELETE_TYPE" + } + }, + "validation": { + "percentageLimit": { + "__name": "VALIDATION_PERCENTAGE_LIMIT", + "__format": "number" + }, + "basePath": "VALIDATION_BASE_PATH" + }, + "provider": "PROVIDER_FROM", + "NFS": { + "pvPath": "PV_SOURCE_PATH" + }, + "S3": { + "accessKeyId": "S3_ACCESS_KEY_ID", + "secretAccessKey": "S3_SECRET_ACCESS_KEY", + "endpointUrl": "S3_ENDPOINT_URL", + "bucket": "S3_BUCKET", + "region": "S3_REGION", + "forcePathStyle": { + "__name": "S3_FORCE_PATH_STYLE", + "__format": "boolean" + }, + "sslEnabled": { + "__name": "S3_SSL_ENABLED", + "__format": "boolean" + }, + "maxAttempts": { + "__name": "S3_MAX_ATTEMPTS", + "__format": "number" + } + } +} diff --git a/config/default.json b/config/default.json index 02f9345..3aa5098 100644 --- a/config/default.json +++ b/config/default.json @@ -32,5 +32,43 @@ "options": null } } + }, + "externalServices": { + "catalog": "http://127.0.0.1:8080", + "extractable": "http://127.0.0.1:8080", + "lookupTables": { + "url": "http://127.0.0.1:8080", + "subUrl": "lookup-tables/lookupData" + } + }, + "jobManager": { + "url": "http://127.0.0.1:8080", + "ingestion": { + "jobType": "Ingestion_New_3D", + "taskType": "tilesCopying", + "batches": 100 + }, + "delete": { + "jobType": "Delete_3D", + "taskType": "deleteModel" + } + }, + "validation": { + "percentageLimit": 10, + "basePath": "/app/models" + }, + "provider": "NFS", + "NFS": { + "pvPath": "/app/models" + }, + "S3": { + "accessKeyId": "minio", + "secretAccessKey": "minio", + "endpointUrl": "http://127.0.0.1:9000", + "bucket": "3dtiles", + "region": "us-east-1", + "forcePathStyle": true, + "sslEnabled": false, + "maxAttempts": 3 } } diff --git a/helm/Chart.yaml b/helm/Chart.yaml index b1f2c0f..853bc57 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 -name: ts-server-boilerplate -description: A Helm chart for ts-server-boilerplate service +name: 3d-ops-trigger +description: A Helm chart for the 3d-ops-trigger service type: application version: 1.0.0 appVersion: 1.0.0 diff --git a/helm/templates/configmap.yaml b/helm/templates/configmap.yaml index 0cd9c61..ed7a095 100644 --- a/helm/templates/configmap.yaml +++ b/helm/templates/configmap.yaml @@ -30,6 +30,38 @@ data: {{- end }} {{- end }} npm_config_cache: /tmp/ + {{- with .Values.externalServices }} + CATALOG_URL: {{ .catalog | quote }} + EXTRACTABLE_URL: {{ .extractable | quote }} + {{- with .lookupTables }} + LOOKUP_TABLES_URL: {{ .url | quote }} + LOOKUP_TABLES_SUB_URL: {{ .subUrl | quote }} + {{- end }} + {{- end }} + {{- with .Values.jobManager }} + JOB_MANAGER_URL: {{ .url | quote }} + JOB_INGESTION_TYPE: {{ .ingestion.jobType | quote }} + TASK_INGESTION_TYPE: {{ .ingestion.taskType | quote }} + INGESTION_TASK_BATCHES: {{ .ingestion.batches | quote }} + JOB_DELETE_TYPE: {{ .delete.jobType | quote }} + TASK_DELETE_TYPE: {{ .delete.taskType | quote }} + {{- end }} + VALIDATION_PERCENTAGE_LIMIT: {{ .Values.validation.percentageLimit | quote }} + VALIDATION_BASE_PATH: {{ .Values.validation.basePath | quote }} + PROVIDER_FROM: {{ .Values.provider | quote }} + {{- if eq .Values.provider "NFS" }} + PV_SOURCE_PATH: {{ .Values.NFS.pvPath | quote }} + {{- end }} + {{- if eq .Values.provider "S3" }} + {{- with .Values.S3 }} + S3_ENDPOINT_URL: {{ .endpointUrl | quote }} + S3_BUCKET: {{ .bucket | quote }} + S3_REGION: {{ .region | quote }} + S3_FORCE_PATH_STYLE: {{ .forcePathStyle | quote }} + S3_SSL_ENABLED: {{ .sslEnabled | quote }} + S3_MAX_ATTEMPTS: {{ .maxAttempts | quote }} + {{- end }} + {{- end }} {{- with .Values.configManagement }} CONFIG_NAME: {{ .name | quote }} CONFIG_VERSION: {{ .version | quote }} diff --git a/helm/values.yaml b/helm/values.yaml index f982729..4a9b5c2 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -7,7 +7,7 @@ global: mclabels: component: backend - partOf: boilerplates + partOf: 3d owner: common prometheus: enabled: true @@ -23,7 +23,7 @@ fullnameOverride: "" configManagement: offlineMode: false - name: 'service-name' + name: '3d-ops-trigger' version: 'latest' serverUrl: 'http://localhost:8080/api' @@ -66,7 +66,7 @@ caPath: '/usr/local/share/ca-certificates' caKey: 'ca.crt' image: - repository: ts-server-boilerplate + repository: 3d-ops-trigger # If commented, appVersion will be taken. See: _helpers.tpl # tag: 'latest' pullPolicy: IfNotPresent @@ -78,6 +78,38 @@ server: responseCompressionEnabled: true requestPayloadLimit: 1mb +externalServices: + catalog: '' + extractable: '' + lookupTables: + url: '' + subUrl: '' + +jobManager: + url: + ingestion: + jobType: '' + taskType: '' + batches: + delete: + jobType: '' + taskType: '' + +validation: + percentageLimit: 10 + basePath: '' + +provider: 'NFS' +NFS: + pvPath: '' +S3: + endpointUrl: '' + bucket: '' + region: '' + forcePathStyle: true + sslEnabled: false + maxAttempts: 3 + telemetry: logger: level: info diff --git a/openapi3.yaml b/openapi3.yaml index 55ee8ee..491b9d1 100644 --- a/openapi3.yaml +++ b/openapi3.yaml @@ -1,77 +1,214 @@ openapi: 3.0.1 info: - title: service-name - description: basic template for map colonies service + title: 3d-ops-trigger + description: Entry point for 3D operations — validates requests and triggers Jobnik jobs version: 1.0.0 license: name: MIT url: https://opensource.org/licenses/MIT paths: - /anotherResource: + /records: + post: + operationId: createRecord + tags: + - records + summary: Invoke a new model ingestion flow + description: Validates the request (light & fast) and creates a Jobnik ingestion job. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ingestionPayload' + responses: + 201: + description: Ingestion job created + content: + application/json: + schema: + $ref: '#/components/schemas/jobResponse' + 400: + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/error' + /records/validate: + post: + operationId: validateRecord + tags: + - records + summary: Invoke a new model ingestion validation + description: Runs the full ingestion validation and returns the result without triggering a Jobnik job. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ingestionPayload' + responses: + 200: + description: Validation result + content: + application/json: + schema: + $ref: '#/components/schemas/validationResultResponse' + /records/canDelete/{recordId}: get: - operationId: getAnotherResource + operationId: canDeleteRecord tags: - - anotherResource - summary: gets the resource + - records + summary: Validate if a record can be removed + description: Runs the delete validation and returns the result without creating a job. + parameters: + - $ref: '#/components/parameters/recordId' responses: 200: - description: OK + description: Validation result content: application/json: schema: - $ref: '#/components/schemas/anotherResource' + $ref: '#/components/schemas/validationResultResponse' + /records/{recordId}: + delete: + operationId: deleteRecord + tags: + - records + summary: Delete a model by record id + description: Validates the record can be deleted and creates a Jobnik delete job. + parameters: + - $ref: '#/components/parameters/recordId' + responses: + 200: + description: Delete job created + content: + application/json: + schema: + $ref: '#/components/schemas/jobResponse' 400: description: Bad Request content: application/json: schema: $ref: '#/components/schemas/error' - /resourceName: - get: - operationId: getResourceName + 404: + description: Record not found + content: + application/json: + schema: + $ref: '#/components/schemas/error' + /metadata/{identifier}: + patch: + operationId: updateMetadata tags: - - resourceName - summary: gets the resource + - metadata + summary: Update metadata of a model + parameters: + - $ref: '#/components/parameters/identifier' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/updatePayload' responses: 200: - description: OK + description: Metadata updated content: application/json: schema: - $ref: '#/components/schemas/resource' + $ref: '#/components/schemas/ackResponse' 400: description: Bad Request content: application/json: schema: $ref: '#/components/schemas/error' - post: - operationId: createResource + 404: + description: Record not found + content: + application/json: + schema: + $ref: '#/components/schemas/error' + /metadata/status/{identifier}: + patch: + operationId: updateMetadataStatus tags: - - resourceName - summary: creates a new record of type resource + - metadata + summary: Update the publish/unpublish status of a model + parameters: + - $ref: '#/components/parameters/identifier' requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/resource' + $ref: '#/components/schemas/statusPayload' responses: - 201: - description: created + 200: + description: Status updated content: application/json: schema: - $ref: '#/components/schemas/resource' + $ref: '#/components/schemas/ackResponse' 400: description: Bad Request content: application/json: schema: $ref: '#/components/schemas/error' + 404: + description: Record not found + content: + application/json: + schema: + $ref: '#/components/schemas/error' + /jobStatus/{jobId}: + get: + operationId: getJobStatus + tags: + - jobStatus + summary: Get the status and progress of a job + parameters: + - $ref: '#/components/parameters/jobId' + responses: + 200: + description: Job status + content: + application/json: + schema: + $ref: '#/components/schemas/jobStatusResponse' + 404: + description: Job not found + content: + application/json: + schema: + $ref: '#/components/schemas/error' security: - {} components: + parameters: + identifier: + name: identifier + in: path + required: true + description: The record identifier + schema: + type: string + recordId: + name: recordId + in: path + required: true + description: The record identifier + schema: + type: string + jobId: + name: jobId + in: path + required: true + description: The job identifier + schema: + type: string schemas: error: type: object @@ -80,27 +217,69 @@ components: properties: message: type: string - resource: + ingestionPayload: type: object required: - - id - - name - - description + - modelPath + - tilesetFilename + - metadata properties: - id: - type: number - format: int64 - name: + modelPath: type: string - description: + description: Path on the shared storage to the 3DTiles folder or 3TZ archive + tilesetFilename: type: string - anotherResource: + description: The tileset entry name (e.g. tileset.json) + metadata: + type: object + description: 3D record metadata (business validation applied downstream) + additionalProperties: true + updatePayload: + type: object + description: Partial metadata fields to update + additionalProperties: true + statusPayload: + type: object + required: + - status + properties: + status: + type: string + enum: + - PUBLISHED + - UNPUBLISHED + jobResponse: + type: object + required: + - jobId + - status + properties: + jobId: + type: string + status: + type: string + ackResponse: + type: object + required: + - message + properties: + message: + type: string + jobStatusResponse: type: object required: - - kind - - isAlive + - status properties: - kind: + status: type: string - isAlive: + percentage: + type: number + validationResultResponse: + type: object + required: + - isValid + properties: + isValid: type: boolean + message: + type: string diff --git a/package-lock.json b/package-lock.json index 6ee91ea..d48d080 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,21 @@ { - "name": "service-name", + "name": "3d-ops-trigger", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "service-name", + "name": "3d-ops-trigger", "version": "1.0.0", "license": "ISC", "dependencies": { + "@aws-sdk/client-s3": "^3.1142.0", "@godaddy/terminus": "^4.12.1", + "@map-colonies/3d-shared": "file:../../3d-general/3d-shared", "@map-colonies/config": "^4.0.1", "@map-colonies/error-express-handler": "^4.0.0", "@map-colonies/express-access-log-middleware": "^5.0.0", + "@map-colonies/jobnik-sdk": "^1.0.1", "@map-colonies/js-logger": "^5.0.0", "@map-colonies/openapi-express-viewer": "^5.0.0", "@map-colonies/prometheus": "^1.0.0", @@ -20,14 +23,19 @@ "@map-colonies/schemas": "^1.26.0", "@map-colonies/tracing": "^1.0.0", "@map-colonies/tracing-utils": "^2.0.0", + "@map-colonies/types": "^1.4.0", "@opentelemetry/api": "^1.9.1", + "@turf/turf": "^7.4.0", + "axios": "^1.20.0", "compression": "^1.8.0", "express": "^4.21.2", "express-openapi-validator": "^5.6.2", "http-status-codes": "^2.3.0", + "mime-types": "^2.1.35", "prom-client": "^15.1.3", "reflect-metadata": "^0.2.2", - "tsyringe": "^4.8.0" + "tsyringe": "^4.8.0", + "zod": "^3.24.1" }, "devDependencies": { "@commitlint/cli": "^21.2.2", @@ -60,6 +68,51 @@ "tsc-alias": "^1.9.1", "typescript": "^5.9.3", "vitest": "^4.1.10" + }, + "engines": { + "node": ">=24.0.0" + } + }, + "../../3d-general/3d-shared": { + "name": "@map-colonies/3d-shared", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@map-colonies/types": "^1.4.0", + "@turf/turf": "^7.4.0", + "geojson": "^0.5.0", + "mime-types": "^2.1.35", + "proj4": "^2.22.0", + "zod": "^3.24.1" + }, + "devDependencies": { + "@commitlint/cli": "^19.6.1", + "@commitlint/config-conventional": "^19.6.0", + "@map-colonies/eslint-config": "^4.0.0", + "@map-colonies/prettier-config": "^0.0.1", + "@swc/core": "^1.7.26", + "@swc/jest": "^0.2.36", + "@types/geojson": "^7946.0.14", + "@types/jest": "^29.4.0", + "@types/node": "^20.0.0", + "@types/proj4": "^2.5.6", + "@typescript-eslint/eslint-plugin": "^6.0.0", + "@typescript-eslint/parser": "^6.0.0", + "commitlint": "^19.6.1", + "eslint": "^8.36.0", + "husky": "^9.1.7", + "jest": "^29.0.0", + "jest-html-reporters": "^3.1.4", + "prettier": "^3.4.2", + "pretty-quick": "^4.0.0", + "rimraf": "^5.0.0", + "standard-version": "^9.5.0", + "ts-jest": "^29.0.0", + "typedoc": "^0.27.6", + "typescript": "^5.0.0" + }, + "engines": { + "node": ">=24.0.0" } }, "node_modules/@apideck/better-ajv-errors": { @@ -95,11 +148,420 @@ "url": "https://github.com/sponsors/philsturgeon" } }, + "node_modules/@aws-sdk/checksums": { + "version": "3.1001.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/checksums/-/checksums-3.1001.1.tgz", + "integrity": "sha512-x12Q17KYlJAd3nKf8LV5LV0vt8sh8/6YfQLGPtrGnQf/tW4jqxPGq5GPpuVitpQYM3eUR4XB7CbxZf751NMbLw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/checksums/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/@aws-sdk/client-s3": { + "version": "3.1142.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1142.0.tgz", + "integrity": "sha512-OC9AcGMFOsBc95YSPWH3O7DE6RUTy7jqeOpVvzO2Dv3rw3w4vQRK+YdLXIbO2yXfmx3N59Y55yAJdDRJFMQK3Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/checksums": "^3.1001.1", + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/credential-provider-node": "^3.972.84", + "@aws-sdk/middleware-sdk-s3": "^3.972.77", + "@aws-sdk/signature-v4-multi-region": "^3.996.47", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3/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/@aws-sdk/core": { + "version": "3.978.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.1.tgz", + "integrity": "sha512-LbY9aGsEiznDWmUc30Nwv3aIX/+dbwTx8KfS0yOC3NPYMO+O91e6jkT1azf34FwjOndq8/Q+RcVVZz5xnerwdg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.6", + "@aws-sdk/xml-builder": "^3.972.41", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.35.0", + "@smithy/signature-v4": "^5.7.3", + "@smithy/types": "^4.19.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core/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/@aws-sdk/credential-provider-env": { + "version": "3.972.72", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.72.tgz", + "integrity": "sha512-xTKO/FWJPozTIXbozVnVGoNBhaGba8TBcx+KyUjRVeOlXE+dUc7GTR1cLvu0uTdIdmemzaFbqqCshXeZA1fZew==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env/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/@aws-sdk/credential-provider-http": { + "version": "3.972.74", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.74.tgz", + "integrity": "sha512-u91E/hT8f4d1xy0Jl7VG4nVKJ3lxbrZkoBTeSVoJdWBiSEUMwMS/9+e0H/aJVQV//Lt5wuzP+E69v4aRSsNTmw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http/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/@aws-sdk/credential-provider-ini": { + "version": "3.973.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.17.tgz", + "integrity": "sha512-ged4KXdBkvIC81bLvNHHuQKdKak/VXhQTR1NWYTTqW0474nlmsxy9O/vlgTIohDDWH3xpBdtVMZRyjb+DnocDA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/credential-provider-env": "^3.972.72", + "@aws-sdk/credential-provider-http": "^3.972.74", + "@aws-sdk/credential-provider-login": "^3.972.79", + "@aws-sdk/credential-provider-process": "^3.972.72", + "@aws-sdk/credential-provider-sso": "^3.973.16", + "@aws-sdk/credential-provider-web-identity": "^3.972.78", + "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/credential-provider-imds": "^4.5.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini/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/@aws-sdk/credential-provider-login": { + "version": "3.972.79", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.79.tgz", + "integrity": "sha512-L+Z85anONJd8MaiuraO4wRxATCdEejBZ3K3eymzWI5JPXa9sOS9CkIm72PBKqXKX+Z9p9NGMX5AIMXm0LEflgw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login/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/@aws-sdk/credential-provider-node": { + "version": "3.972.84", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.84.tgz", + "integrity": "sha512-oHt854odINVwzwsh+c5x69j0ajm4DbqqqVJ+O1ECsCIZeMDAbzFpXItaqP7UZstJj/ATdTk/KFSH0LaNAgV+kA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.72", + "@aws-sdk/credential-provider-http": "^3.972.74", + "@aws-sdk/credential-provider-ini": "^3.973.17", + "@aws-sdk/credential-provider-process": "^3.972.72", + "@aws-sdk/credential-provider-sso": "^3.973.16", + "@aws-sdk/credential-provider-web-identity": "^3.972.78", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/credential-provider-imds": "^4.5.2", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node/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/@aws-sdk/credential-provider-process": { + "version": "3.972.72", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.72.tgz", + "integrity": "sha512-rLIp2xbMjX/k9/od7APpqq1ZgXXnV0pOL1Th3ZsL8Wu0TRtBsDTVS8iPqcfRFcHakFxPvR04OSTv2ka2qOb/2A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process/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/@aws-sdk/credential-provider-sso": { + "version": "3.973.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.16.tgz", + "integrity": "sha512-IGihaJfFZYacJJr/odqILCoK7W/mvrZ7cuK7ECn3sAu4vLC6u0V8bS7mCGbdugJ8Aum2tnvqmx0F2MRFp2rn9g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/token-providers": "3.1138.0", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso/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/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.78", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.78.tgz", + "integrity": "sha512-/y9WvNtlcPBGLR0qc1a+9J/xtYZfVczvLUOuXaVWylzttH7ewsxwHtjmiJSolNrVSDorIxHGHMU61CbonRkmwA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity/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/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.77", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.77.tgz", + "integrity": "sha512-E7W2UOeUoc+lg3uIfR/dM7ZwusHwhBQrKMnlkRv4EXRR+C0YtV1pg25xC7GdZIhXH+NAMgZPCbE7o5to2cjFiw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/signature-v4-multi-region": "^3.996.47", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3/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/@aws-sdk/nested-clients": { + "version": "3.997.46", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.46.tgz", + "integrity": "sha512-oRxtBcka/JGHGs9l9p9IVajGoTP8vTPmoAzdHGy4Qcy9P5vPnDf6nhIeM/COQNY9k/OahImTRaLkHftoXvfcmQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/signature-v4-multi-region": "^3.996.47", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.12.1", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients/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/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.47", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.47.tgz", + "integrity": "sha512-Zk08macMvQTHzQJCLJVkOlviVoqwYMrpXv4lmLN7b7sAbiMoOK7Go0NYdR5UeF+MW8LIbRmwrNy9u/5VvX1U5g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.6", + "@smithy/signature-v4": "^5.7.3", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region/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/@aws-sdk/token-providers": { + "version": "3.1138.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1138.0.tgz", + "integrity": "sha512-GpyAr0DD63YOEmYFM6Df+gJuIgC92MMTiBK4FTKfxii5MJ9ge20epR7LyroulscYlG89J+ZB2ivFDPjvfQhzdw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.1", + "@aws-sdk/nested-clients": "^3.997.46", + "@aws-sdk/types": "^3.974.6", + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers/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/@aws-sdk/types": { + "version": "3.974.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.6.tgz", + "integrity": "sha512-v/clNZzZnDxGyvpHMOGpJKVXFAExJzUNAAjaWGdcx8QAcXLGwTaOkw33p5SHAi0YAioK32xB3hWwOekRVfmfKg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types/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/@aws-sdk/xml-builder": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.41.tgz", + "integrity": "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder/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/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", @@ -114,7 +576,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, "license": "MIT" }, "node_modules/@babel/helper-string-parser": { @@ -131,7 +592,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -157,7 +617,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -888,7 +1347,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/@humanwhocodes/momoa/-/momoa-2.0.4.tgz", "integrity": "sha512-RE815I4arJFtt+FVeU1Tgp9/Xvecacji8w/V6XtXsWWH/wz/eNkNbhb+ny/+PlVZjV0rxQpRSQKNKE3lcktHEA==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=10.10.0" @@ -969,6 +1427,10 @@ "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", "license": "MIT" }, + "node_modules/@map-colonies/3d-shared": { + "resolved": "../../3d-general/3d-shared", + "link": true + }, "node_modules/@map-colonies/commitlint-config": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@map-colonies/commitlint-config/-/commitlint-config-2.0.1.tgz", @@ -1134,47 +1596,122 @@ "node": ">=24" } }, - "node_modules/@map-colonies/js-logger": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@map-colonies/js-logger/-/js-logger-5.0.0.tgz", - "integrity": "sha512-kPEJwJo+bzZprq6Q91+ozJQpP2sPvgjfUF2BMxuXQXXmfY+USU2XcZJdfU4dowV5E275XhFTG9nQT2gJZvdQbg==", + "node_modules/@map-colonies/jobnik-sdk": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@map-colonies/jobnik-sdk/-/jobnik-sdk-1.0.1.tgz", + "integrity": "sha512-ieV91q/+F+5QWaKjW3rmvNTfENw/1mLOM/LlMqb6khYs7ODQMCQcXapEYM3VIifYMc51E46XezHNXOxQKBUtfA==", + "license": "ISC", "dependencies": { "@map-colonies/read-pkg": "^2.0.0", - "@opentelemetry/resource-detector-container": "^0.8.4", - "@opentelemetry/resources": "^2.6.0", - "@opentelemetry/semantic-conventions": "^1.40.0", - "pino": "^10.1.0", - "pino-caller": "^4.0.0", - "pino-opentelemetry-transport": "^2.0.0", - "pino-pretty": "^13.1.2" + "@map-colonies/semantic-conventions": "^1.0.0", + "@readme/openapi-parser": "^4.0.1", + "date-fns": "^4.1.0", + "http-status-codes": "^2.3.0", + "openapi-fetch": "^0.14.0", + "opossum": "^9.0.0", + "undici": "^7.19.1", + "yaml": "^2.8.1" }, "engines": { "node": ">=24" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "prom-client": "^15.0.0" } }, - "node_modules/@map-colonies/openapi-express-types": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@map-colonies/openapi-express-types/-/openapi-express-types-0.2.0.tgz", - "integrity": "sha512-qKVbd4kPGzbE9zUxgxahTc5TLlRvV2XJywpW9V/EsCqczVODvWULqawg0QO0BgT0kYXdH8O64KzD2uLoZhqKPA==", - "dev": true, + "node_modules/@map-colonies/jobnik-sdk/node_modules/@apidevtools/json-schema-ref-parser": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-13.0.5.tgz", + "integrity": "sha512-xfh4xVJD62gG6spIc7lwxoWT+l16nZu1ELyU8FkjaP/oD2yP09EvLAU6KhtudN9aML2Khhs9pY6Slr7KGTES3w==", + "license": "MIT", "dependencies": { - "ts-essentials": "^10.1.1" + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" }, "engines": { - "node": ">=24" + "node": ">= 16" }, - "peerDependencies": { - "@types/express": "^4.17.21" + "funding": { + "url": "https://github.com/sponsors/philsturgeon" } }, - "node_modules/@map-colonies/openapi-express-viewer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@map-colonies/openapi-express-viewer/-/openapi-express-viewer-5.0.0.tgz", - "integrity": "sha512-xDKLxFz+w00NN4sxs5qd+QzHnmmOhaEk5e2qtU1/XsnSWMu0JSi6G34WZwxq+6IhYZ4hZn4whe1lfSMzPXIkAw==", + "node_modules/@map-colonies/jobnik-sdk/node_modules/@readme/openapi-parser": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@readme/openapi-parser/-/openapi-parser-4.1.2.tgz", + "integrity": "sha512-lAFH88r/CHs5VZDUocEda0OSMSQsr6801sziIjOKyVA+0hSFN+BPuelPF5XvkMROHecnPd+XEJN1iNQqCgER/g==", + "license": "MIT", "dependencies": { - "http-status-codes": "^2.3.0", - "js-yaml": "^4.1.0", - "swagger-ui-express": "^5.0.1" + "@apidevtools/json-schema-ref-parser": "^13.0.5", + "@readme/better-ajv-errors": "^2.3.2", + "@readme/openapi-schemas": "^3.1.0", + "@types/json-schema": "^7.0.15", + "ajv": "^8.12.0", + "ajv-draft-04": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@map-colonies/jobnik-sdk/node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/@map-colonies/js-logger": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@map-colonies/js-logger/-/js-logger-5.0.0.tgz", + "integrity": "sha512-kPEJwJo+bzZprq6Q91+ozJQpP2sPvgjfUF2BMxuXQXXmfY+USU2XcZJdfU4dowV5E275XhFTG9nQT2gJZvdQbg==", + "dependencies": { + "@map-colonies/read-pkg": "^2.0.0", + "@opentelemetry/resource-detector-container": "^0.8.4", + "@opentelemetry/resources": "^2.6.0", + "@opentelemetry/semantic-conventions": "^1.40.0", + "pino": "^10.1.0", + "pino-caller": "^4.0.0", + "pino-opentelemetry-transport": "^2.0.0", + "pino-pretty": "^13.1.2" + }, + "engines": { + "node": ">=24" + } + }, + "node_modules/@map-colonies/openapi-express-types": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@map-colonies/openapi-express-types/-/openapi-express-types-0.2.0.tgz", + "integrity": "sha512-qKVbd4kPGzbE9zUxgxahTc5TLlRvV2XJywpW9V/EsCqczVODvWULqawg0QO0BgT0kYXdH8O64KzD2uLoZhqKPA==", + "dev": true, + "dependencies": { + "ts-essentials": "^10.1.1" + }, + "engines": { + "node": ">=24" + }, + "peerDependencies": { + "@types/express": "^4.17.21" + } + }, + "node_modules/@map-colonies/openapi-express-viewer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@map-colonies/openapi-express-viewer/-/openapi-express-viewer-5.0.0.tgz", + "integrity": "sha512-xDKLxFz+w00NN4sxs5qd+QzHnmmOhaEk5e2qtU1/XsnSWMu0JSi6G34WZwxq+6IhYZ4hZn4whe1lfSMzPXIkAw==", + "dependencies": { + "http-status-codes": "^2.3.0", + "js-yaml": "^4.1.0", + "swagger-ui-express": "^5.0.1" }, "engines": { "node": ">=24" @@ -1281,6 +1818,14 @@ "integrity": "sha512-XUQ2/gvC6d6/nGMKIGVyZo/kemGH5uGOy4xhkYCVLNV/CJsHvJXmhiov7QpqVpb58x+/2WVVUThAStj9ogipug==", "license": "MIT" }, + "node_modules/@map-colonies/semantic-conventions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@map-colonies/semantic-conventions/-/semantic-conventions-1.0.0.tgz", + "integrity": "sha512-QcR+hHVcH3GyHtOXjmsWZXWWR6GG4Kx8+xWdkjuLlBAEG6sLX6hQObLJ1H5YhLZiw3HKCA39hjLwXyl+6ExUNg==", + "engines": { + "node": ">=24" + } + }, "node_modules/@map-colonies/tracing": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@map-colonies/tracing/-/tracing-1.0.0.tgz", @@ -1326,6 +1871,19 @@ "node": ">=24" } }, + "node_modules/@map-colonies/types": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@map-colonies/types/-/types-1.10.0.tgz", + "integrity": "sha512-FwVrBAYzjnpmd9Jnq7Mex7axl8KR6o8JjKVGu3kDCGHz26Ru9qSCpzkA+zUVV8fKNxWVZSmDi7IiHEe9WsOalw==", + "license": "ISC", + "dependencies": { + "@types/geojson": "^7946.0.16", + "@types/mime-types": "^2.1.1", + "copyfiles": "^2.4.1", + "epsg-index": "^1.3.0", + "ts-morph": "^18.0.0" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", @@ -1362,7 +1920,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -1376,7 +1933,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -1386,7 +1942,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -3543,7 +4098,6 @@ "version": "2.4.0", "resolved": "https://registry.npmjs.org/@readme/better-ajv-errors/-/better-ajv-errors-2.4.0.tgz", "integrity": "sha512-9WODaOAKSl/mU+MYNZ2aHCrkoRSvmQ+1YkLj589OEqqjOAhbn8j7Z+ilYoiTu/he6X63/clsxxAB4qny9/dDzg==", - "dev": true, "license": "Apache-2.0", "dependencies": { "@babel/code-frame": "^7.22.5", @@ -3611,7 +4165,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@readme/openapi-schemas/-/openapi-schemas-3.1.0.tgz", "integrity": "sha512-9FC/6ho8uFa8fV50+FPy/ngWN53jaUu4GRXlAjcxIRrzhltJnpKkBG2Tp0IDraFJeWrOpk84RJ9EMEEYzaI1Bw==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -4095,15 +4648,2937 @@ "node": ">=22" }, "funding": { - "url": "https://ko-fi.com/dangreen" + "url": "https://ko-fi.com/dangreen" + } + }, + "node_modules/@smithy/core": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.35.0.tgz", + "integrity": "sha512-zRMhfkByhT2snNdr1si24vJitU6Cr9ix2MikUfWmkAgp4jrNP0GcKSP5YvwQ+TlI8AZXER5QOGJn3JsVtSD9/A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core/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/@smithy/credential-provider-imds": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz", + "integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds/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/@smithy/fetch-http-handler": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz", + "integrity": "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler/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/@smithy/node-http-handler": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz", + "integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler/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/@smithy/signature-v4": { + "version": "5.7.4", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.4.tgz", + "integrity": "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.35.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4/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/@smithy/types": { + "version": "4.19.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.19.0.tgz", + "integrity": "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types/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/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ts-morph/common": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.19.0.tgz", + "integrity": "sha512-Unz/WHmd4pGax91rdIKWi51wnVUW11QttMEPpBiBgIewnc9UQIX7UDLxr5vRlqeByXCwhkF6VabSsI0raWcyAQ==", + "license": "MIT", + "dependencies": { + "fast-glob": "^3.2.12", + "minimatch": "^7.4.3", + "mkdirp": "^2.1.6", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@ts-morph/common/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/@ts-morph/common/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@ts-morph/common/node_modules/minimatch": { + "version": "7.4.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.9.tgz", + "integrity": "sha512-Brg/fp/iAVDOQoHxkuN5bEYhyQlZhxddI78yWsCbeEwTHXQjlNLtiJDUsp1GIptVqMI7/gkJMz4vVAc01mpoBw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@ts-morph/common/node_modules/mkdirp": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-2.1.6.tgz", + "integrity": "sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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/along/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/@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/angle/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/@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/area/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/@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-clip/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/@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/bbox-polygon/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/@turf/bbox/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/@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/bearing/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/@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/bezier-spline/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/@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-clockwise/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/@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-concave/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/@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-contains/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/@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-crosses/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/@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-disjoint/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/@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-equal/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/@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-intersects/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/@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-overlap/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/@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-parallel/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/@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-in-polygon/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/@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-point-on-line/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/@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-touches/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/@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-valid/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/@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/boolean-within/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/@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/buffer/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/@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-mean/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/@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-median/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/@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/center-of-mass/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/@turf/center/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/@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/centroid/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/@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/circle/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/@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/clean-coords/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/@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/clone/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/@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-dbscan/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/@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/clusters-kmeans/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/@turf/clusters/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/@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/collect/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/@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/combine/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/@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/concave/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/@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/convex/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/@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/destination/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/@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/difference/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/@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/directional-mean/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/@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/dissolve/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/@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/distance-weight/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/@turf/distance/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/@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/ellipse/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/@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/envelope/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/@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/explode/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/@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/flatten/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/@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/flip/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/@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/geojson-rbush/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/@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/great-circle/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/@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/helpers/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/@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/hex-grid/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/@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/interpolate/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/@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/intersect/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/@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/invariant/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/@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/isobands/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/@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/isolines/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/@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/kinks/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/@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/length/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/@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-arc/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/@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-chunk/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/@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-intersect/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/@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-offset/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/@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-overlap/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/@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-segment/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/@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-slice-along/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/@turf/line-slice/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/@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-split/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/@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/line-to-polygon/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/@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/mask/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/@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/meta/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/@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/midpoint/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/@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/moran-index/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/@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-neighbor-analysis/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/@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-on-line/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/@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/nearest-point-to-line/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/@turf/nearest-point/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/@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/planepoint/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/@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-grid/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/@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-on-feature/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/@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-line-distance/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/@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/point-to-polygon-distance/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/@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/points-within-polygon/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/@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-smooth/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/@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-tangents/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/@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/polygon-to-line/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/@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/polygonize/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/@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/projection/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/@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/quadrat-analysis/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/@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/random/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/@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/rectangle-grid/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/@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/rewind/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/@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-bearing/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/@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-destination/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/@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/rhumb-distance/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/@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/sample/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/@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/sector/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/@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/shortest-path/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/@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/simplify/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/@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/square-grid/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/@turf/square/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/@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/standard-deviational-ellipse/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/@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/tag/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/@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/tesselate/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/@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/tin/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/@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-rotate/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/@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-scale/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/@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/transform-translate/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/@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/triangle-grid/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/@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/truncate/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/@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/turf/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/@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/union/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/@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/unkink-polygon/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/@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/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" + "node_modules/@turf/voronoi/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/@tybys/wasm-util": { "version": "0.10.3", @@ -4194,6 +7669,12 @@ "dev": true, "license": "MIT" }, + "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/deep-eql": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -4201,6 +7682,12 @@ "dev": true, "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/esrecurse": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -4239,6 +7726,12 @@ "@types/send": "*" } }, + "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/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -4278,6 +7771,12 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "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/lodash": { "version": "4.17.24", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", @@ -4306,6 +7805,12 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "license": "MIT" }, + "node_modules/@types/mime-types": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", + "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", + "license": "MIT" + }, "node_modules/@types/multer": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.2.0.tgz", @@ -5406,6 +8911,14 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "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/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -5474,7 +8987,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, "node_modules/at-least-node": { @@ -5497,13 +9009,40 @@ } }, "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "dev": true, + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.16.0", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" } }, "node_modules/balanced-match": { @@ -5596,6 +9135,12 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "5.0.7", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", @@ -5613,7 +9158,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -5948,6 +9492,12 @@ "node": ">=8" } }, + "node_modules/code-block-writer": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-12.0.0.tgz", + "integrity": "sha512-q4dMFMlXtKR3XNBHyMHt/3pwYNA69EDk00lloMOaaUMKPUXBw6lpXtbu3MMVG6/uOihGnRDOlkyqsONEUj60+w==", + "license": "MIT" + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -5978,7 +9528,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -6073,7 +9622,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, "license": "MIT" }, "node_modules/concat-stream": { @@ -6091,6 +9639,18 @@ "typedarray": "^0.0.6" } }, + "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/config": { "version": "3.3.12", "resolved": "https://registry.npmjs.org/config/-/config-3.3.12.tgz", @@ -6200,7 +9760,6 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/copyfiles/-/copyfiles-2.4.1.tgz", "integrity": "sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==", - "dev": true, "license": "MIT", "dependencies": { "glob": "^7.0.5", @@ -6220,14 +9779,12 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/copyfiles/node_modules/brace-expansion": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -6238,7 +9795,6 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dev": true, "license": "ISC", "dependencies": { "string-width": "^4.2.0", @@ -6250,7 +9806,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -6263,7 +9818,6 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -6278,7 +9832,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -6291,7 +9844,6 @@ "version": "16.2.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz", "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==", - "dev": true, "license": "MIT", "dependencies": { "cliui": "^7.0.2", @@ -6310,7 +9862,6 @@ "version": "20.2.9", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -6334,7 +9885,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true, "license": "MIT" }, "node_modules/cosmiconfig": { @@ -6415,6 +9965,40 @@ "node": ">= 8" } }, + "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/date-fns": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", + "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/dateformat": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", @@ -6461,7 +10045,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -6544,6 +10127,12 @@ "node": ">= 0.4" } }, + "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/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -6591,6 +10180,15 @@ "node": ">=6" } }, + "node_modules/epsg-index": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/epsg-index/-/epsg-index-1.3.0.tgz", + "integrity": "sha512-pWJ7lkMbH0Au+MJyBV4GxiX+KmOknPc6c5FqDAYD61Ju3LblR60VGmOlteBhK9Dm3oy++Jxa2kUimONeSvsKRA==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -6642,7 +10240,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7249,7 +10846,6 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -7266,7 +10862,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -7315,7 +10910,6 @@ "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", - "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" @@ -7363,7 +10957,6 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -7460,7 +11053,6 @@ "version": "1.16.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", - "dev": true, "funding": [ { "type": "individual", @@ -7481,7 +11073,6 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -7556,7 +11147,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -7613,6 +11203,39 @@ "node": ">=14" } }, + "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/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -7690,7 +11313,6 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", - "dev": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -7724,14 +11346,12 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, "license": "MIT" }, "node_modules/glob/node_modules/brace-expansion": { "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", - "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -7742,7 +11362,6 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -7842,7 +11461,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -8044,7 +11662,6 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -8067,6 +11684,12 @@ "node": "^20.17.0 || >=22.9.0" } }, + "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/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -8126,7 +11749,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -8145,7 +11767,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -8171,7 +11792,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -8219,7 +11839,6 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "dev": true, "license": "MIT" }, "node_modules/isexe": { @@ -8470,6 +12089,15 @@ "node": ">=0.10.0" } }, + "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/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -8934,7 +12562,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -8953,7 +12580,6 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -8967,7 +12593,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", - "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -9070,7 +12695,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" @@ -9262,7 +12886,6 @@ "version": "0.0.0", "resolved": "https://registry.npmjs.org/noms/-/noms-0.0.0.tgz", "integrity": "sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==", - "dev": true, "license": "ISC", "dependencies": { "inherits": "^2.0.1", @@ -9273,7 +12896,6 @@ "version": "1.0.34", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", "integrity": "sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==", - "dev": true, "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -9286,7 +12908,6 @@ "version": "0.10.31", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", - "dev": true, "license": "MIT" }, "node_modules/normalize-path": { @@ -9481,6 +13102,15 @@ "@jsdevtools/ono": "7.1.3" } }, + "node_modules/openapi-fetch": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.14.1.tgz", + "integrity": "sha512-l7RarRHxlEZYjMLd/PR0slfMVse2/vvIAGm75/F7J6MlQ8/b9uUQmUF2kCPrQhJqMXSxmYWObVgeYXbFYzZR+A==", + "license": "MIT", + "dependencies": { + "openapi-typescript-helpers": "^0.0.15" + } + }, "node_modules/openapi-response-validator": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/openapi-response-validator/-/openapi-response-validator-9.3.1.tgz", @@ -9541,7 +13171,6 @@ "version": "12.1.3", "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", - "dev": true, "license": "MIT" }, "node_modules/openapi-typescript": { @@ -9566,6 +13195,12 @@ "typescript": "^5.x" } }, + "node_modules/openapi-typescript-helpers": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.15.tgz", + "integrity": "sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==", + "license": "MIT" + }, "node_modules/openapi-typescript/node_modules/parse-json": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", @@ -9643,6 +13278,25 @@ "@types/node": "*" } }, + "node_modules/openapi-validator/node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/opossum": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/opossum/-/opossum-9.0.0.tgz", + "integrity": "sha512-K76U0QkxOfUZamneQuzz+AP0fyfTJcCplZ2oZL93nxeupuJbN4s6uFNbmVCt4eWqqGqRnnowdFuBicJ1fLMVxw==", + "license": "Apache-2.0", + "engines": { + "node": "^24 || ^22 || ^20" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -9999,6 +13653,12 @@ "node": ">= 0.8" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -10013,7 +13673,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -10115,7 +13774,6 @@ "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": { @@ -10258,6 +13916,37 @@ "node": ">=4" } }, + "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.19", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.19.tgz", @@ -10424,7 +14113,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true, "license": "MIT" }, "node_modules/process-warning": { @@ -10492,6 +14180,15 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -10542,7 +14239,6 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, "funding": [ { "type": "github", @@ -10565,6 +14261,12 @@ "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", "license": "MIT" }, + "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/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -10589,6 +14291,15 @@ "node": ">= 0.8" } }, + "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/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -10756,7 +14467,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -10801,6 +14511,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "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.1.5", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", @@ -10839,7 +14555,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, "funding": [ { "type": "github", @@ -11179,6 +14894,12 @@ "node": ">=18" } }, + "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/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -11227,6 +14948,12 @@ "source-map": "^0.6.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", @@ -11547,7 +15274,6 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, "license": "MIT", "dependencies": { "readable-stream": "~2.3.6", @@ -11558,14 +15284,12 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true, "license": "MIT" }, "node_modules/through2/node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", @@ -11581,14 +15305,12 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, "license": "MIT" }, "node_modules/through2/node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" @@ -11628,6 +15350,12 @@ "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/tinyrainbow": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", @@ -11642,7 +15370,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -11660,6 +15387,44 @@ "node": ">=0.6" } }, + "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-client/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/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/topojson-server/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/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -11704,6 +15469,16 @@ } } }, + "node_modules/ts-morph": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-18.0.0.tgz", + "integrity": "sha512-Kg5u0mk19PIIe4islUI/HWRvm9bC1lHejK4S0oh1zaZ77TMZAEmQC0sHQYiu2RgCQFZKXz1fMVi/7nOOeirznA==", + "license": "MIT", + "dependencies": { + "@ts-morph/common": "~0.19.0", + "code-block-writer": "^12.0.0" + } + }, "node_modules/tsc-alias": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.9.1.tgz", @@ -11931,7 +15706,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -12450,6 +16224,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index f080ea9..33030e1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "service-name", + "name": "3d-ops-trigger", "version": "1.0.0", - "description": "This is template for map colonies typescript service", + "description": "Entry point for 3D operations (ingest, delete, update, publish/unpublish) — validates requests and triggers Jobnik jobs", "main": "./src/index.ts", "scripts": { "test:unit": "vitest run --coverage.enabled=false --project unit", @@ -14,6 +14,7 @@ "prelint:fix": "npm run format:fix", "prelint": "npm run format", "lint:openapi": "redocly lint openapi3.yaml", + "bundleOpenApi": "redocly bundle openapi3.yaml -o bundledApi.yaml", "lint": "eslint .", "lint:fix": "eslint --fix .", "prebuild": "npm run clean && npm run generate:openapi-types", @@ -30,11 +31,17 @@ }, "author": "MapColonies", "license": "ISC", + "engines": { + "node": ">=24.0.0" + }, "dependencies": { + "@aws-sdk/client-s3": "^3.1142.0", "@godaddy/terminus": "^4.12.1", + "@map-colonies/3d-shared": "file:../../3d-general/3d-shared", "@map-colonies/config": "^4.0.1", "@map-colonies/error-express-handler": "^4.0.0", "@map-colonies/express-access-log-middleware": "^5.0.0", + "@map-colonies/jobnik-sdk": "^1.0.1", "@map-colonies/js-logger": "^5.0.0", "@map-colonies/openapi-express-viewer": "^5.0.0", "@map-colonies/prometheus": "^1.0.0", @@ -42,14 +49,19 @@ "@map-colonies/schemas": "^1.26.0", "@map-colonies/tracing": "^1.0.0", "@map-colonies/tracing-utils": "^2.0.0", + "@map-colonies/types": "^1.4.0", "@opentelemetry/api": "^1.9.1", + "@turf/turf": "^7.4.0", + "axios": "^1.20.0", "compression": "^1.8.0", "express": "^4.21.2", "express-openapi-validator": "^5.6.2", "http-status-codes": "^2.3.0", + "mime-types": "^2.1.35", "prom-client": "^15.1.3", "reflect-metadata": "^0.2.2", - "tsyringe": "^4.8.0" + "tsyringe": "^4.8.0", + "zod": "^3.24.1" }, "devDependencies": { "@commitlint/cli": "^21.2.2", diff --git a/src/anotherResource/controllers/anotherResourceController.ts b/src/anotherResource/controllers/anotherResourceController.ts deleted file mode 100644 index 56f3879..0000000 --- a/src/anotherResource/controllers/anotherResourceController.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { Logger } from '@map-colonies/js-logger'; -import { type Registry, Counter } from 'prom-client'; -import httpStatus from 'http-status-codes'; -import { injectable, inject } from 'tsyringe'; -import type { TypedRequestHandlers } from '@openapi'; -import { SERVICES } from '@common/constants'; -import { AnotherResourceManager } from '../models/anotherResourceManager'; - -@injectable() -export class AnotherResourceController { - private readonly getResourceCounter: Counter; - - public constructor( - @inject(SERVICES.LOGGER) private readonly logger: Logger, - @inject(AnotherResourceManager) private readonly manager: AnotherResourceManager, - @inject(SERVICES.METRICS) private readonly metricsRegistry: Registry - ) { - this.getResourceCounter = new Counter({ - name: 'get_resource', - help: 'number of get resource requests', - registers: [this.metricsRegistry], - }); - } - - public getResource: TypedRequestHandlers['getAnotherResource'] = (req, res) => { - this.getResourceCounter.inc(1); - return res.status(httpStatus.OK).json(this.manager.getResource()); - }; -} diff --git a/src/anotherResource/models/anotherResourceManager.ts b/src/anotherResource/models/anotherResourceManager.ts deleted file mode 100644 index f54fcde..0000000 --- a/src/anotherResource/models/anotherResourceManager.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Logger } from '@map-colonies/js-logger'; -import { inject, injectable } from 'tsyringe'; -import { components } from '@src/openapi'; -import { SERVICES } from '@common/constants'; - -const resourceInstance: IAnotherResourceModel = { - kind: 'avi', - isAlive: false, -}; - -export type IAnotherResourceModel = components['schemas']['anotherResource']; - -@injectable() -export class AnotherResourceManager { - public constructor(@inject(SERVICES.LOGGER) private readonly logger: Logger) {} - public getResource(): IAnotherResourceModel { - this.logger.info('logging'); - return resourceInstance; - } -} diff --git a/src/anotherResource/routes/anotherResourceRouter.ts b/src/anotherResource/routes/anotherResourceRouter.ts deleted file mode 100644 index 937ba6d..0000000 --- a/src/anotherResource/routes/anotherResourceRouter.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Router } from 'express'; -import type { FactoryFunction } from 'tsyringe'; -import { AnotherResourceController } from '../controllers/anotherResourceController'; - -const anotherResourceRouterFactory: FactoryFunction = (dependencyContainer) => { - const router = Router(); - const controller = dependencyContainer.resolve(AnotherResourceController); - - router.get('/', controller.getResource); - - return router; -}; - -export const ANOTHER_RESOURCE_ROUTER_SYMBOL = Symbol('anotherResourceRouterFactory'); - -export { anotherResourceRouterFactory }; diff --git a/src/common/appError.ts b/src/common/appError.ts new file mode 100644 index 0000000..26b6212 --- /dev/null +++ b/src/common/appError.ts @@ -0,0 +1,20 @@ +import type { StatusCodes } from 'http-status-codes'; + +export class AppError extends Error { + public override readonly name: string; + public readonly status: StatusCodes; + public readonly isOperational: boolean; + + public constructor(name: string, status: StatusCodes, description: string, isOperational: boolean) { + super(description); + + Object.setPrototypeOf(this, new.target.prototype); + + this.name = name; + this.status = status; + this.isOperational = isOperational; + this.message = description; + + Error.captureStackTrace(this); + } +} diff --git a/src/common/config.ts b/src/common/config.ts index 3541a48..0f28da5 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -1,21 +1,156 @@ import { type ConfigInstance, config } from '@map-colonies/config'; import { commonBoilerplateV3, type commonBoilerplateV3Type } from '@map-colonies/schemas'; -// Choose here the type of the config instance and import this type from the entire application -type ConfigType = ConfigInstance; +interface LookupTablesConfig { + url: string; + subUrl: string; +} + +interface ExternalServicesConfig { + lookupTables: LookupTablesConfig; + catalog: string; + extractable: string; +} + +interface JobManagerConfig { + url: string; + ingestion: { + jobType: string; + taskType: string; + batches: number; + }; + delete: { + jobType: string; + taskType: string; + }; +} + +interface ValidationConfig { + percentageLimit: number; + basePath: string; +} + +interface NFSConfig { + pvPath: string; +} + +interface S3Config { + accessKeyId: string; + secretAccessKey: string; + endpointUrl: string; + bucket: string; + region: string; + forcePathStyle: boolean; + sslEnabled: boolean; + maxAttempts: number; +} + +type ProviderSource = 'NFS' | 'S3'; + +type OpsTriggerConfigType = commonBoilerplateV3Type & { + externalServices: ExternalServicesConfig; + jobManager: JobManagerConfig; + validation: ValidationConfig; + provider: ProviderSource; + // eslint-disable-next-line @typescript-eslint/naming-convention + NFS: NFSConfig; + // eslint-disable-next-line @typescript-eslint/naming-convention + S3: S3Config; +}; + +type ConfigType = ConfigInstance; + +const opsTriggerConfigSchema = { + $id: 'https://mapcolonies.com/3d/opsTrigger/v1', + type: 'object', + allOf: [ + { $ref: commonBoilerplateV3.$id }, + { + type: 'object', + required: ['externalServices', 'jobManager', 'validation', 'provider'], + properties: { + provider: { type: 'string', enum: ['NFS', 'S3'] }, + validation: { + type: 'object', + required: ['percentageLimit', 'basePath'], + properties: { + percentageLimit: { type: 'number' }, + basePath: { type: 'string' }, + }, + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + NFS: { + type: 'object', + required: ['pvPath'], + properties: { pvPath: { type: 'string' } }, + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + S3: { + type: 'object', + required: ['accessKeyId', 'secretAccessKey', 'endpointUrl', 'bucket', 'region', 'forcePathStyle', 'sslEnabled', 'maxAttempts'], + properties: { + accessKeyId: { type: 'string' }, + secretAccessKey: { type: 'string' }, + endpointUrl: { type: 'string' }, + bucket: { type: 'string' }, + region: { type: 'string' }, + forcePathStyle: { type: 'boolean' }, + sslEnabled: { type: 'boolean' }, + maxAttempts: { type: 'number' }, + }, + }, + externalServices: { + type: 'object', + required: ['lookupTables', 'catalog', 'extractable'], + properties: { + lookupTables: { + type: 'object', + required: ['url', 'subUrl'], + properties: { + url: { type: 'string' }, + subUrl: { type: 'string' }, + }, + }, + catalog: { type: 'string' }, + extractable: { type: 'string' }, + }, + }, + jobManager: { + type: 'object', + required: ['url', 'ingestion', 'delete'], + properties: { + url: { type: 'string' }, + ingestion: { + type: 'object', + required: ['jobType'], + properties: { + jobType: { type: 'string' }, + taskType: { type: 'string' }, + batches: { type: 'number' }, + }, + }, + delete: { + type: 'object', + required: ['jobType'], + properties: { + jobType: { type: 'string' }, + taskType: { type: 'string' }, + }, + }, + }, + }, + }, + }, + ], +}; let configInstance: ConfigType | undefined; -/** - * Initializes the configuration by fetching it from the server. - * This should only be called from the instrumentation file. - * @returns A Promise that resolves when the configuration is successfully initialized. - */ async function initConfig(offlineMode?: boolean): Promise { - configInstance = await config({ - schema: commonBoilerplateV3, + configInstance = (await config({ + schema: opsTriggerConfigSchema as unknown as typeof commonBoilerplateV3, offlineMode, - }); + })) as unknown as ConfigType; } function getConfig(): ConfigType { @@ -26,4 +161,4 @@ function getConfig(): ConfigType { } export { getConfig, initConfig }; -export type { ConfigType }; +export type { ConfigType, LookupTablesConfig, ExternalServicesConfig, JobManagerConfig, ValidationConfig, NFSConfig, S3Config, ProviderSource }; diff --git a/src/common/constants.ts b/src/common/constants.ts index 6e7c100..9715684 100644 --- a/src/common/constants.ts +++ b/src/common/constants.ts @@ -1,4 +1,5 @@ import { readPackageJsonSync } from '@map-colonies/read-pkg'; +import type { Job } from '@map-colonies/jobnik-sdk'; export const SERVICE_NAME = readPackageJsonSync().name ?? 'unknown_service'; export const DEFAULT_SERVER_PORT = 80; @@ -6,11 +7,28 @@ export const DEFAULT_SERVER_PORT = 80; export const IGNORED_OUTGOING_TRACE_ROUTES = [/^.*\/v1\/metrics.*$/]; export const IGNORED_INCOMING_TRACE_ROUTES = [/^.*\/docs.*$/]; +export const DOMAIN = '3D'; + +/* eslint-disable @typescript-eslint/naming-convention */ +export const STAGE_TYPES = { + DATA_EXTRACTION: 'data-extraction', + VALIDATION: 'validation', + CREATE_UPLOAD_MODEL_TASKS: 'create-upload-model-tasks', + UPLOAD_MODEL_DATA: 'upload-model-data', + CLEAR_DATA: 'clear-data', + UPLOAD_MODEL_PARTS: 'upload-model-parts', + INGESTION_FINALIZER: 'ingestion-finalizer', +} satisfies Record; +/* eslint-enable @typescript-eslint/naming-convention */ + +export const IN_FLIGHT_JOB_STATUSES: readonly Job['status'][] = ['PENDING', 'IN_PROGRESS', 'PAUSED', 'CREATED']; + /* eslint-disable @typescript-eslint/naming-convention */ export const SERVICES = { LOGGER: Symbol('Logger'), CONFIG: Symbol('Config'), TRACER: Symbol('Tracer'), METRICS: Symbol('METRICS'), + PROVIDER: Symbol('Provider'), } satisfies Record; /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/src/common/interfaces.ts b/src/common/interfaces.ts index 455054c..879c230 100644 --- a/src/common/interfaces.ts +++ b/src/common/interfaces.ts @@ -1,4 +1,25 @@ +import type { components } from '@openapi'; + +export type IngestionPayload = components['schemas']['ingestionPayload']; +export type UpdatePayload = components['schemas']['updatePayload']; +export type StatusPayload = components['schemas']['statusPayload']; +export type JobResponse = components['schemas']['jobResponse']; +export type AckResponse = components['schemas']['ackResponse']; +export type ValidationResultResponse = components['schemas']['validationResultResponse']; +export type JobStatusResponse = components['schemas']['jobStatusResponse']; + export interface IConfig { get: (setting: string) => T; has: (setting: string) => boolean; } + +export interface LogContext { + fileName: string; + class: string; + function?: string; +} + +export interface ValidationResponse { + isValid: boolean; + message?: string; +} diff --git a/src/common/util.ts b/src/common/util.ts new file mode 100644 index 0000000..f4b4f2f --- /dev/null +++ b/src/common/util.ts @@ -0,0 +1,13 @@ +import type { Polygon, Position } from 'geojson'; + +const TWO_DIMENSIONS = 2; + +export const is3tz = (modelPath: string): boolean => modelPath.toLowerCase().endsWith('.3tz'); + +export const buildModelFilePath = (modelPath: string, tilesetFilename: string): string => + is3tz(modelPath) ? modelPath : `${modelPath}/${tilesetFilename}`; + +export const convertPolygonTo2DPolygon = (polygon: Polygon): Polygon => ({ + ...polygon, + coordinates: polygon.coordinates.map((ring: Position[]) => ring.map((coordinate: Position) => coordinate.slice(0, TWO_DIMENSIONS))), +}); diff --git a/src/containerConfig.ts b/src/containerConfig.ts index ecd03fa..75be200 100644 --- a/src/containerConfig.ts +++ b/src/containerConfig.ts @@ -6,8 +6,10 @@ import { jsLogger } from '@map-colonies/js-logger'; import { type InjectionObject, registerDependencies } from '@common/dependencyRegistration'; import { SERVICES, SERVICE_NAME } from '@common/constants'; import { getTracing } from '@common/tracing'; -import { resourceNameRouterFactory, RESOURCE_NAME_ROUTER_SYMBOL } from './resourceName/routes/resourceNameRouter'; -import { anotherResourceRouterFactory, ANOTHER_RESOURCE_ROUTER_SYMBOL } from './anotherResource/routes/anotherResourceRouter'; +import { recordsRouterFactory, RECORDS_ROUTER_SYMBOL } from './records/routes/recordsRouter'; +import { jobStatusRouterFactory, JOB_STATUS_ROUTER_SYMBOL } from './jobStatus/routes/jobStatusRouter'; +import { metadataRouterFactory, METADATA_ROUTER_SYMBOL } from './metadata/routes/metadataRouter'; +import { providerFactory } from './providers/getProvider'; import { getConfig } from './common/config'; export interface RegisterOptions { @@ -31,8 +33,10 @@ export const registerExternalValues = async (options?: RegisterOptions): Promise { token: SERVICES.LOGGER, provider: { useValue: logger } }, { token: SERVICES.TRACER, provider: { useValue: tracer } }, { token: SERVICES.METRICS, provider: { useValue: metricsRegistry } }, - { token: RESOURCE_NAME_ROUTER_SYMBOL, provider: { useFactory: resourceNameRouterFactory } }, - { token: ANOTHER_RESOURCE_ROUTER_SYMBOL, provider: { useFactory: anotherResourceRouterFactory } }, + { token: SERVICES.PROVIDER, provider: { useFactory: providerFactory } }, + { token: RECORDS_ROUTER_SYMBOL, provider: { useFactory: recordsRouterFactory } }, + { token: JOB_STATUS_ROUTER_SYMBOL, provider: { useFactory: jobStatusRouterFactory } }, + { token: METADATA_ROUTER_SYMBOL, provider: { useFactory: metadataRouterFactory } }, { token: 'onSignal', provider: { diff --git a/src/externalServices/catalog/catalogCall.ts b/src/externalServices/catalog/catalogCall.ts new file mode 100644 index 0000000..90696d0 --- /dev/null +++ b/src/externalServices/catalog/catalogCall.ts @@ -0,0 +1,111 @@ +import axios from 'axios'; +import { inject, injectable } from 'tsyringe'; +import type { Logger } from '@map-colonies/js-logger'; +import { StatusCodes } from 'http-status-codes'; +import { SERVICES } from '@common/constants'; +import { AppError } from '@common/appError'; +import type { ConfigType } from '@common/config'; +import type { LogContext } from '@common/interfaces'; +import type { CatalogStatusPayload, CatalogUpdatePayload, IFindRecordsPayload, Record3D } from './interfaces'; + +@injectable() +export class CatalogCall { + private readonly logContext: LogContext; + private readonly catalog: string; + + public constructor( + @inject(SERVICES.CONFIG) private readonly config: ConfigType, + @inject(SERVICES.LOGGER) private readonly logger: Logger + ) { + this.catalog = this.config.get('externalServices.catalog'); + this.logContext = { + fileName: __filename, + class: CatalogCall.name, + }; + } + + public async findRecords(payload: IFindRecordsPayload): Promise { + const logContext = { ...this.logContext, function: this.findRecords.name }; + this.logger.debug({ msg: `Searching in catalog ${this.catalog}/metadata/find`, logContext, payload }); + return this.run(logContext, 'Something went wrong in catalog when trying to find records', 'Problem with catalog find', {}, async () => { + const response = await axios.post(`${this.catalog}/metadata/find`, payload); + if (response.status !== StatusCodes.OK.valueOf()) { + this.logger.error({ msg: `Catalog returned unexpected status: ${response.status}`, logContext }); + throw new AppError('catalog', StatusCodes.INTERNAL_SERVER_ERROR, 'Problem with catalog during record lookup', true); + } + + const records = response.data; + if (!Array.isArray(records)) { + return []; + } + + this.logger.debug({ msg: `Found ${records.length} record(s) in catalog`, logContext }); + return records; + }); + } + + public async getRecord(identifier: string): Promise { + const logContext = { ...this.logContext, function: this.getRecord.name }; + this.logger.debug({ msg: `Getting record ${identifier} from catalog`, logContext }); + return this.run( + logContext, + 'Something went wrong in catalog when getting a record', + 'Problem with catalog during record lookup', + { identifier }, + async () => { + const response = await axios.get(`${this.catalog}/metadata/${identifier}`, { + validateStatus: (status) => status === StatusCodes.OK.valueOf() || status === StatusCodes.NOT_FOUND.valueOf(), + }); + return response.status === StatusCodes.NOT_FOUND.valueOf() ? undefined : response.data; + } + ); + } + + public async patchMetadata(identifier: string, payload: CatalogUpdatePayload): Promise { + const logContext = { ...this.logContext, function: this.patchMetadata.name }; + this.logger.debug({ msg: `Updating metadata for record ${identifier} in catalog`, logContext }); + return this.run( + logContext, + 'Something went wrong in catalog when updating metadata', + 'Problem with catalog during metadata update', + { identifier }, + async () => { + const response = await axios.patch(`${this.catalog}/metadata/${identifier}`, payload); + return response.data; + } + ); + } + + public async changeStatus(identifier: string, payload: CatalogStatusPayload): Promise { + const logContext = { ...this.logContext, function: this.changeStatus.name }; + this.logger.debug({ msg: `Changing status for record ${identifier} in catalog`, logContext }); + return this.run( + logContext, + 'Something went wrong in catalog when changing status', + 'Problem with catalog during status change', + { identifier }, + async () => { + const response = await axios.patch(`${this.catalog}/metadata/status/${identifier}`, payload); + return response.data; + } + ); + } + + private async run( + logContext: LogContext, + errorLog: string, + failMessage: string, + details: Record, + fn: () => Promise + ): Promise { + try { + return await fn(); + } catch (err) { + if (err instanceof AppError) { + throw err; + } + this.logger.error({ msg: errorLog, logContext, ...details, err }); + throw new AppError('catalog', StatusCodes.INTERNAL_SERVER_ERROR, failMessage, true); + } + } +} diff --git a/src/externalServices/catalog/interfaces.ts b/src/externalServices/catalog/interfaces.ts new file mode 100644 index 0000000..e3987df --- /dev/null +++ b/src/externalServices/catalog/interfaces.ts @@ -0,0 +1,25 @@ +export interface Record3D { + id: string; + productId?: string; + productName?: string; + productType?: string; + productVersion?: number; + producerName?: string; + productStatus?: string; + links?: string; +} + +export interface IFindRecordsPayload { + id?: string; + productId?: string; + productName?: string; + productType?: string; + classification?: string; + productStatus?: string; +} + +export type CatalogUpdatePayload = Record; + +export interface CatalogStatusPayload { + productStatus: string; +} diff --git a/src/externalServices/extractableManagement/extractableCall.ts b/src/externalServices/extractableManagement/extractableCall.ts new file mode 100644 index 0000000..02dbd22 --- /dev/null +++ b/src/externalServices/extractableManagement/extractableCall.ts @@ -0,0 +1,39 @@ +import axios from 'axios'; +import { inject, injectable } from 'tsyringe'; +import type { Logger } from '@map-colonies/js-logger'; +import { StatusCodes } from 'http-status-codes'; +import { SERVICES } from '@common/constants'; +import { AppError } from '@common/appError'; +import type { ConfigType } from '@common/config'; +import type { LogContext } from '@common/interfaces'; + +@injectable() +export class ExtractableCall { + private readonly logContext: LogContext; + private readonly extractable: string; + + public constructor( + @inject(SERVICES.CONFIG) private readonly config: ConfigType, + @inject(SERVICES.LOGGER) private readonly logger: Logger + ) { + this.extractable = this.config.get('externalServices.extractable'); + this.logContext = { + fileName: __filename, + class: ExtractableCall.name, + }; + } + + public async isExtractableRecordExists(recordName: string): Promise { + const logContext = { ...this.logContext, function: this.isExtractableRecordExists.name }; + this.logger.debug({ msg: `Checking record '${recordName}' in extractable service`, logContext }); + try { + const response = await axios.get(`${this.extractable}/records/${encodeURIComponent(recordName)}`, { + validateStatus: (status) => status === StatusCodes.OK.valueOf() || status === StatusCodes.NOT_FOUND.valueOf(), + }); + return response.status === StatusCodes.OK.valueOf(); + } catch (err) { + this.logger.error({ msg: 'Something went wrong in extractable when checking a record', logContext, recordName, err }); + throw new AppError('extractable', StatusCodes.INTERNAL_SERVER_ERROR, 'Problem with extractable during record lookup', true); + } + } +} diff --git a/src/externalServices/jobnik/jobnikClient.ts b/src/externalServices/jobnik/jobnikClient.ts new file mode 100644 index 0000000..52c1d69 --- /dev/null +++ b/src/externalServices/jobnik/jobnikClient.ts @@ -0,0 +1,152 @@ +import { inject, singleton } from 'tsyringe'; +import { StatusCodes } from 'http-status-codes'; +import type { Logger } from '@map-colonies/js-logger'; +import type { Registry } from 'prom-client'; +import { JobnikSDK } from '@map-colonies/jobnik-sdk'; +import type { JobId } from '@map-colonies/jobnik-sdk'; +import { IN_FLIGHT_JOB_STATUSES, SERVICES, STAGE_TYPES } from '@common/constants'; +import { is3tz } from '@common/util'; +import { AppError } from '@common/appError'; +import type { ConfigType, JobManagerConfig } from '@common/config'; +import type { IngestionPayload, JobResponse, JobStatusResponse, LogContext } from '@common/interfaces'; +import type { Record3D } from '../catalog/interfaces'; + +interface StageDescriptor { + type: string; + data: Record; + task?: Record; + only3tz?: boolean; +} + +@singleton() +export class JobnikClient { + private readonly logContext: LogContext; + private readonly producer: ReturnType; + private readonly apiClient: ReturnType; + private readonly jobManager: JobManagerConfig; + + public constructor( + @inject(SERVICES.CONFIG) private readonly config: ConfigType, + @inject(SERVICES.LOGGER) private readonly logger: Logger, + @inject(SERVICES.METRICS) private readonly metricsRegistry: Registry + ) { + this.jobManager = this.config.get('jobManager'); + const sdk = new JobnikSDK({ baseUrl: this.jobManager.url, metricsRegistry: this.metricsRegistry }); + this.producer = sdk.getProducer(); + this.apiClient = sdk.getApiClient(); + this.logContext = { + fileName: __filename, + class: JobnikClient.name, + }; + } + + public async hasInFlightIngestionJob(productName: string): Promise { + const logContext = { ...this.logContext, function: this.hasInFlightIngestionJob.name }; + const pageSize = 100; + + for (let page = 1; ; page++) { + const { data, error } = await this.apiClient.GET('/v1/jobs', { + // eslint-disable-next-line @typescript-eslint/naming-convention -- Jobnik query params are snake_case + params: { query: { job_name: this.jobManager.ingestion.jobType, page, page_size: pageSize } }, + }); + if (error !== undefined) { + this.logger.error({ msg: 'failed querying Jobnik for in-flight jobs', logContext, productName, err: error }); + throw new AppError('jobnik', StatusCodes.INTERNAL_SERVER_ERROR, 'failed querying Jobnik for in-flight jobs', false); + } + + const jobs = data.items; + const match = jobs.some((job) => { + const metadata = (job.data as { metadata?: { productName?: string } }).metadata; + return IN_FLIGHT_JOB_STATUSES.includes(job.status) && metadata?.productName === productName; + }); + if (match) { + this.logger.debug({ msg: 'in-flight ingestion job found', logContext, productName }); + return true; + } + if (jobs.length < pageSize) { + return false; + } + } + } + + public async getJobStatus(jobId: string): Promise { + const logContext = { ...this.logContext, function: this.getJobStatus.name }; + const { data, error, response } = await this.apiClient.GET('/v1/jobs/{jobId}', { params: { path: { jobId: jobId as JobId } } }); + if (error !== undefined) { + if (response.status === StatusCodes.NOT_FOUND.valueOf()) { + throw new AppError('badRequest', StatusCodes.NOT_FOUND, `job ${jobId} was not found`, true); + } + this.logger.error({ msg: 'failed querying Jobnik for job status', logContext, jobId, err: error }); + throw new AppError('jobnik', StatusCodes.INTERNAL_SERVER_ERROR, 'failed querying Jobnik for job status', false); + } + + return { status: data.status, percentage: data.percentage }; + } + + public async createIngestionJob(payload: IngestionPayload, modelId: string): Promise { + const logContext = { ...this.logContext, function: this.createIngestionJob.name }; + const isArchive = is3tz(payload.modelPath); + + const job = await this.producer.createJob({ + name: this.jobManager.ingestion.jobType, + data: { modelId, modelPath: payload.modelPath, tilesetFilename: payload.tilesetFilename, metadata: payload.metadata }, + }); + + const stages: StageDescriptor[] = [ + { + type: STAGE_TYPES.DATA_EXTRACTION, + data: { modelPath: payload.modelPath }, + task: { modelPath: payload.modelPath, tilesetFilename: payload.tilesetFilename }, + only3tz: true, + }, + { + type: STAGE_TYPES.VALIDATION, + data: {}, + task: { modelPath: payload.modelPath, tilesetFilename: payload.tilesetFilename, metadata: payload.metadata }, + }, + { + type: STAGE_TYPES.CREATE_UPLOAD_MODEL_TASKS, + data: {}, + task: { modelPath: payload.modelPath, tilesetFilename: payload.tilesetFilename }, + }, + { type: STAGE_TYPES.UPLOAD_MODEL_DATA, data: {} }, + { type: STAGE_TYPES.CLEAR_DATA, data: { modelPath: payload.modelPath }, task: { modelPath: payload.modelPath }, only3tz: true }, + { type: STAGE_TYPES.UPLOAD_MODEL_PARTS, data: {}, task: { metadata: payload.metadata } }, + { type: STAGE_TYPES.INGESTION_FINALIZER, data: { metadata: payload.metadata }, task: { metadata: payload.metadata } }, + ]; + + for (const descriptor of stages) { + if (descriptor.only3tz === true && !isArchive) { + continue; + } + const stage = await this.producer.createStage(job.id, { type: descriptor.type, data: descriptor.data }); + if (descriptor.task !== undefined) { + await this.producer.createTasks(stage.id, descriptor.type, [{ data: descriptor.task }]); + } + } + + this.logger.info({ msg: 'ingestion job created', logContext, jobId: job.id, is3tz: isArchive }); + + return { jobId: job.id, status: job.status }; + } + + public async createDeleteJob(record: Record3D): Promise { + const logContext = { ...this.logContext, function: this.createDeleteJob.name }; + + const job = await this.producer.createJob({ + name: this.jobManager.delete.jobType, + data: { + modelId: record.id, + productId: record.productId, + productVersion: record.productVersion, + productName: record.productName, + productType: record.productType, + producerName: record.producerName, + }, + }); + + this.logger.info({ msg: 'delete job created', logContext, jobId: job.id, recordId: record.id }); + + return { jobId: job.id, status: job.status }; + } +} diff --git a/src/externalServices/lookupTables/interfaces.ts b/src/externalServices/lookupTables/interfaces.ts new file mode 100644 index 0000000..99c30e4 --- /dev/null +++ b/src/externalServices/lookupTables/interfaces.ts @@ -0,0 +1,5 @@ +export interface ILookupOption { + value: string; + translationCode: string; + properties?: Record; +} diff --git a/src/externalServices/lookupTables/lookupTablesCall.ts b/src/externalServices/lookupTables/lookupTablesCall.ts new file mode 100644 index 0000000..7125e3a --- /dev/null +++ b/src/externalServices/lookupTables/lookupTablesCall.ts @@ -0,0 +1,41 @@ +import axios from 'axios'; +import { inject, injectable } from 'tsyringe'; +import type { Logger } from '@map-colonies/js-logger'; +import { StatusCodes } from 'http-status-codes'; +import { SERVICES } from '@common/constants'; +import { AppError } from '@common/appError'; +import type { ConfigType, LookupTablesConfig } from '@common/config'; +import type { LogContext } from '@common/interfaces'; +import type { ILookupOption } from './interfaces'; + +@injectable() +export class LookupTablesCall { + private readonly logContext: LogContext; + private readonly lookupTables: LookupTablesConfig; + + public constructor( + @inject(SERVICES.CONFIG) private readonly config: ConfigType, + @inject(SERVICES.LOGGER) private readonly logger: Logger + ) { + this.lookupTables = this.config.get('externalServices.lookupTables'); + this.logContext = { + fileName: __filename, + class: LookupTablesCall.name, + }; + } + + public async getClassifications(): Promise { + const logContext = { ...this.logContext, function: this.getClassifications.name }; + this.logger.debug({ msg: 'Get Classifications from lookup-tables service', logContext }); + try { + const response = await axios.get(`${this.lookupTables.url}/${this.lookupTables.subUrl}/classification`); + const classifications = response.data.map((item) => item.value); + this.logger.debug({ msg: 'Got Classifications', logContext, classifications }); + + return classifications; + } catch (err) { + this.logger.error({ msg: 'something went wrong with lookup-tables service', logContext, err }); + throw new AppError('lookup-tables', StatusCodes.INTERNAL_SERVER_ERROR, 'there is a problem with lookup-tables', true); + } + } +} diff --git a/src/job/models/jobManager.ts b/src/job/models/jobManager.ts new file mode 100644 index 0000000..21949b3 --- /dev/null +++ b/src/job/models/jobManager.ts @@ -0,0 +1,26 @@ +import type { Logger } from '@map-colonies/js-logger'; +import { inject, injectable } from 'tsyringe'; +import { SERVICES } from '@common/constants'; +import type { JobStatusResponse, LogContext } from '@common/interfaces'; +import { JobnikClient } from '../../externalServices/jobnik/jobnikClient'; + +@injectable() +export class JobManager { + private readonly logContext: LogContext; + + public constructor( + @inject(SERVICES.LOGGER) private readonly logger: Logger, + @inject(JobnikClient) private readonly jobnik: JobnikClient + ) { + this.logContext = { + fileName: __filename, + class: JobManager.name, + }; + } + + public async getStatus(jobId: string): Promise { + const logContext = { ...this.logContext, function: this.getStatus.name }; + this.logger.info({ msg: 'fetching job status', logContext, jobId }); + return this.jobnik.getJobStatus(jobId); + } +} diff --git a/src/jobStatus/controllers/jobStatusController.ts b/src/jobStatus/controllers/jobStatusController.ts new file mode 100644 index 0000000..968b570 --- /dev/null +++ b/src/jobStatus/controllers/jobStatusController.ts @@ -0,0 +1,34 @@ +import type { Logger } from '@map-colonies/js-logger'; +import { StatusCodes } from 'http-status-codes'; +import { injectable, inject } from 'tsyringe'; +import type { TypedRequestHandlers } from '@openapi'; +import { SERVICES } from '@common/constants'; +import type { LogContext } from '@common/interfaces'; +import { JobManager } from '../../job/models/jobManager'; + +@injectable() +export class JobStatusController { + private readonly logContext: LogContext; + + public constructor( + @inject(SERVICES.LOGGER) private readonly logger: Logger, + @inject(JobManager) private readonly manager: JobManager + ) { + this.logContext = { + fileName: __filename, + class: JobStatusController.name, + }; + } + + public getJobStatus: TypedRequestHandlers['getJobStatus'] = async (req, res, next) => { + const logContext = { ...this.logContext, function: this.getJobStatus.name }; + const { jobId } = req.params; + try { + const status = await this.manager.getStatus(jobId); + return res.status(StatusCodes.OK).json(status); + } catch (err) { + this.logger.error({ msg: 'failed to get job status', logContext, err, jobId }); + return next(err); + } + }; +} diff --git a/src/jobStatus/routes/jobStatusRouter.ts b/src/jobStatus/routes/jobStatusRouter.ts new file mode 100644 index 0000000..9d3230a --- /dev/null +++ b/src/jobStatus/routes/jobStatusRouter.ts @@ -0,0 +1,16 @@ +import { Router } from 'express'; +import type { FactoryFunction } from 'tsyringe'; +import { JobStatusController } from '../controllers/jobStatusController'; + +const jobStatusRouterFactory: FactoryFunction = (dependencyContainer) => { + const router = Router(); + const controller = dependencyContainer.resolve(JobStatusController); + + router.get('/:jobId', controller.getJobStatus); + + return router; +}; + +export const JOB_STATUS_ROUTER_SYMBOL = Symbol('jobStatusRouterFactory'); + +export { jobStatusRouterFactory }; diff --git a/src/metadata/controllers/metadataController.ts b/src/metadata/controllers/metadataController.ts new file mode 100644 index 0000000..c7da02e --- /dev/null +++ b/src/metadata/controllers/metadataController.ts @@ -0,0 +1,46 @@ +import type { Logger } from '@map-colonies/js-logger'; +import { StatusCodes } from 'http-status-codes'; +import { injectable, inject } from 'tsyringe'; +import type { TypedRequestHandlers } from '@openapi'; +import { SERVICES } from '@common/constants'; +import type { LogContext } from '@common/interfaces'; +import { RecordManager } from '../../records/models/recordManager'; + +@injectable() +export class MetadataController { + private readonly logContext: LogContext; + + public constructor( + @inject(SERVICES.LOGGER) private readonly logger: Logger, + @inject(RecordManager) private readonly manager: RecordManager + ) { + this.logContext = { + fileName: __filename, + class: MetadataController.name, + }; + } + + public updateMetadata: TypedRequestHandlers['updateMetadata'] = async (req, res, next) => { + const logContext = { ...this.logContext, function: this.updateMetadata.name }; + const { identifier } = req.params; + try { + const ack = await this.manager.updateMetadata(identifier, req.body); + return res.status(StatusCodes.OK).json(ack); + } catch (err) { + this.logger.error({ msg: 'failed to update record metadata', logContext, err, recordId: identifier }); + return next(err); + } + }; + + public updateMetadataStatus: TypedRequestHandlers['updateMetadataStatus'] = async (req, res, next) => { + const logContext = { ...this.logContext, function: this.updateMetadataStatus.name }; + const { identifier } = req.params; + try { + const ack = await this.manager.updateStatus(identifier, req.body); + return res.status(StatusCodes.OK).json(ack); + } catch (err) { + this.logger.error({ msg: 'failed to update record status', logContext, err, recordId: identifier }); + return next(err); + } + }; +} diff --git a/src/metadata/routes/metadataRouter.ts b/src/metadata/routes/metadataRouter.ts new file mode 100644 index 0000000..c8ca7ed --- /dev/null +++ b/src/metadata/routes/metadataRouter.ts @@ -0,0 +1,17 @@ +import { Router } from 'express'; +import type { FactoryFunction } from 'tsyringe'; +import { MetadataController } from '../controllers/metadataController'; + +const metadataRouterFactory: FactoryFunction = (dependencyContainer) => { + const router = Router(); + const controller = dependencyContainer.resolve(MetadataController); + + router.patch('/status/:identifier', controller.updateMetadataStatus); + router.patch('/:identifier', controller.updateMetadata); + + return router; +}; + +export const METADATA_ROUTER_SYMBOL = Symbol('metadataRouterFactory'); + +export { metadataRouterFactory }; diff --git a/src/openapi.d.ts b/src/openapi.d.ts index eb9ba76..07455f1 100644 --- a/src/openapi.d.ts +++ b/src/openapi.d.ts @@ -4,15 +4,58 @@ import type { TypedRequestHandlers as ImportedTypedRequestHandlers } from '@map-colonies/openapi-express-types'; export type paths = { - '/anotherResource': { + '/records': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** gets the resource */ - get: operations['getAnotherResource']; + get?: never; + put?: never; + /** + * Invoke a new model ingestion flow + * @description Validates the request (light & fast) and creates a Jobnik ingestion job. + */ + post: operations['createRecord']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/records/validate': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Invoke a new model ingestion validation + * @description Runs the full ingestion validation and returns the result without triggering a Jobnik job. + */ + post: operations['validateRecord']; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/records/canDelete/{recordId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Validate if a record can be removed + * @description Runs the delete validation and returns the result without creating a job. + */ + get: operations['canDeleteRecord']; put?: never; post?: never; delete?: never; @@ -21,18 +64,71 @@ export type paths = { patch?: never; trace?: never; }; - '/resourceName': { + '/records/{recordId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Delete a model by record id + * @description Validates the record can be deleted and creates a Jobnik delete job. + */ + delete: operations['deleteRecord']; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + '/metadata/{identifier}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** Update metadata of a model */ + patch: operations['updateMetadata']; + trace?: never; + }; + '/metadata/status/{identifier}': { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - /** gets the resource */ - get: operations['getResourceName']; + get?: never; put?: never; - /** creates a new record of type resource */ - post: operations['createResource']; + post?: never; + delete?: never; + options?: never; + head?: never; + /** Update the publish/unpublish status of a model */ + patch: operations['updateMetadataStatus']; + trace?: never; + }; + '/jobStatus/{jobId}': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get the status and progress of a job */ + get: operations['getJobStatus']; + put?: never; + post?: never; delete?: never; options?: never; head?: never; @@ -46,41 +142,75 @@ export type components = { error: { message: string; }; - resource: { - /** Format: int64 */ - id: number; - name: string; - description: string; + ingestionPayload: { + /** @description Path on the shared storage to the 3DTiles folder or 3TZ archive */ + modelPath: string; + /** @description The tileset entry name (e.g. tileset.json) */ + tilesetFilename: string; + /** @description 3D record metadata (business validation applied downstream) */ + metadata: { + [key: string]: unknown; + }; + }; + /** @description Partial metadata fields to update */ + updatePayload: { + [key: string]: unknown; + }; + statusPayload: { + /** @enum {string} */ + status: 'PUBLISHED' | 'UNPUBLISHED'; + }; + jobResponse: { + jobId: string; + status: string; + }; + ackResponse: { + message: string; + }; + jobStatusResponse: { + status: string; + percentage?: number; }; - anotherResource: { - kind: string; - isAlive: boolean; + validationResultResponse: { + isValid: boolean; + message?: string; }; }; responses: never; - parameters: never; + parameters: { + /** @description The record identifier */ + identifier: string; + /** @description The record identifier */ + recordId: string; + /** @description The job identifier */ + jobId: string; + }; requestBodies: never; headers: never; pathItems: never; }; export type $defs = Record; export interface operations { - getAnotherResource: { + createRecord: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; - requestBody?: never; + requestBody: { + content: { + 'application/json': components['schemas']['ingestionPayload']; + }; + }; responses: { - /** @description OK */ - 200: { + /** @description Ingestion job created */ + 201: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['anotherResource']; + 'application/json': components['schemas']['jobResponse']; }; }; /** @description Bad Request */ @@ -94,22 +224,72 @@ export interface operations { }; }; }; - getResourceName: { + validateRecord: { parameters: { query?: never; header?: never; path?: never; cookie?: never; }; + requestBody: { + content: { + 'application/json': components['schemas']['ingestionPayload']; + }; + }; + responses: { + /** @description Validation result */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['validationResultResponse']; + }; + }; + }; + }; + canDeleteRecord: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The record identifier */ + recordId: components['parameters']['recordId']; + }; + cookie?: never; + }; requestBody?: never; responses: { - /** @description OK */ + /** @description Validation result */ 200: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['resource']; + 'application/json': components['schemas']['validationResultResponse']; + }; + }; + }; + }; + deleteRecord: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The record identifier */ + recordId: components['parameters']['recordId']; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Delete job created */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['jobResponse']; }; }; /** @description Bad Request */ @@ -121,28 +301,85 @@ export interface operations { 'application/json': components['schemas']['error']; }; }; + /** @description Record not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['error']; + }; + }; }; }; - createResource: { + updateMetadata: { parameters: { query?: never; header?: never; - path?: never; + path: { + /** @description The record identifier */ + identifier: components['parameters']['identifier']; + }; cookie?: never; }; requestBody: { content: { - 'application/json': components['schemas']['resource']; + 'application/json': components['schemas']['updatePayload']; }; }; responses: { - /** @description created */ - 201: { + /** @description Metadata updated */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ackResponse']; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['error']; + }; + }; + /** @description Record not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['error']; + }; + }; + }; + }; + updateMetadataStatus: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The record identifier */ + identifier: components['parameters']['identifier']; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['statusPayload']; + }; + }; + responses: { + /** @description Status updated */ + 200: { headers: { [name: string]: unknown; }; content: { - 'application/json': components['schemas']['resource']; + 'application/json': components['schemas']['ackResponse']; }; }; /** @description Bad Request */ @@ -154,6 +391,47 @@ export interface operations { 'application/json': components['schemas']['error']; }; }; + /** @description Record not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['error']; + }; + }; + }; + }; + getJobStatus: { + parameters: { + query?: never; + header?: never; + path: { + /** @description The job identifier */ + jobId: components['parameters']['jobId']; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Job status */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['jobStatusResponse']; + }; + }; + /** @description Job not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['error']; + }; + }; }; }; } diff --git a/src/providers/getProvider.ts b/src/providers/getProvider.ts new file mode 100644 index 0000000..26e6d30 --- /dev/null +++ b/src/providers/getProvider.ts @@ -0,0 +1,11 @@ +import type { FactoryFunction } from 'tsyringe'; +import { SERVICES } from '@common/constants'; +import type { ConfigType } from '@common/config'; +import { NFSProvider } from './nfsProvider'; +import { S3Provider } from './s3Provider'; +import type { Provider } from './interfaces'; + +export const providerFactory: FactoryFunction = (dependencyContainer) => { + const config = dependencyContainer.resolve(SERVICES.CONFIG); + return config.get('provider') === 'S3' ? dependencyContainer.resolve(S3Provider) : dependencyContainer.resolve(NFSProvider); +}; diff --git a/src/providers/interfaces.ts b/src/providers/interfaces.ts new file mode 100644 index 0000000..87ad465 --- /dev/null +++ b/src/providers/interfaces.ts @@ -0,0 +1,3 @@ +export interface Provider { + fileExists: (relativePath: string) => Promise; +} diff --git a/src/providers/nfsProvider.ts b/src/providers/nfsProvider.ts new file mode 100644 index 0000000..a4cb296 --- /dev/null +++ b/src/providers/nfsProvider.ts @@ -0,0 +1,43 @@ +import { access } from 'node:fs/promises'; +import { join } from 'node:path'; +import { inject, injectable } from 'tsyringe'; +import type { Logger } from '@map-colonies/js-logger'; +import { StatusCodes } from 'http-status-codes'; +import { SERVICES } from '@common/constants'; +import type { ConfigType } from '@common/config'; +import type { LogContext } from '@common/interfaces'; +import { AppError } from '@common/appError'; +import type { Provider } from './interfaces'; + +@injectable() +export class NFSProvider implements Provider { + private readonly logContext: LogContext; + private readonly pvPath: string; + + public constructor( + @inject(SERVICES.CONFIG) private readonly config: ConfigType, + @inject(SERVICES.LOGGER) private readonly logger: Logger + ) { + this.pvPath = this.config.get('NFS').pvPath; + this.logContext = { + fileName: __filename, + class: NFSProvider.name, + }; + } + + public async fileExists(relativePath: string): Promise { + const logContext = { ...this.logContext, function: this.fileExists.name }; + const fullPath = join(this.pvPath, relativePath); + try { + await access(fullPath); + return true; + } catch (err) { + if ((err as NodeJS.ErrnoException).code === 'ENOENT') { + this.logger.debug({ msg: `path does not exist: ${fullPath}`, logContext }); + return false; + } + this.logger.error({ msg: 'something went wrong with the NFS provider', logContext, fullPath, err }); + throw new AppError('nfs', StatusCodes.INTERNAL_SERVER_ERROR, 'there is a problem with the NFS provider', true); + } + } +} diff --git a/src/providers/s3Provider.ts b/src/providers/s3Provider.ts new file mode 100644 index 0000000..7f95301 --- /dev/null +++ b/src/providers/s3Provider.ts @@ -0,0 +1,56 @@ +import { inject, injectable } from 'tsyringe'; +import type { Logger } from '@map-colonies/js-logger'; +import { S3Client, HeadObjectCommand, type S3ClientConfig } from '@aws-sdk/client-s3'; +import { StatusCodes } from 'http-status-codes'; +import { SERVICES } from '@common/constants'; +import type { ConfigType, S3Config } from '@common/config'; +import type { LogContext } from '@common/interfaces'; +import { AppError } from '@common/appError'; +import type { Provider } from './interfaces'; + +@injectable() +export class S3Provider implements Provider { + private readonly logContext: LogContext; + private readonly s3Config: S3Config; + private readonly s3: S3Client; + + public constructor( + @inject(SERVICES.CONFIG) private readonly config: ConfigType, + @inject(SERVICES.LOGGER) private readonly logger: Logger + ) { + this.s3Config = this.config.get('S3'); + const s3ClientConfig: S3ClientConfig = { + endpoint: this.s3Config.endpointUrl, + forcePathStyle: this.s3Config.forcePathStyle, + credentials: { + accessKeyId: this.s3Config.accessKeyId, + secretAccessKey: this.s3Config.secretAccessKey, + }, + region: this.s3Config.region, + maxAttempts: this.s3Config.maxAttempts, + tls: this.s3Config.sslEnabled, + }; + this.s3 = new S3Client(s3ClientConfig); + this.logContext = { + fileName: __filename, + class: S3Provider.name, + }; + } + + public async fileExists(key: string): Promise { + const logContext = { ...this.logContext, function: this.fileExists.name }; + try { + // eslint-disable-next-line @typescript-eslint/naming-convention + await this.s3.send(new HeadObjectCommand({ Bucket: this.s3Config.bucket, Key: key })); + return true; + } catch (err) { + const statusCode = (err as { $metadata?: { httpStatusCode?: number } }).$metadata?.httpStatusCode; + if (statusCode === StatusCodes.NOT_FOUND) { + this.logger.debug({ msg: `key does not exist: ${key}`, logContext }); + return false; + } + this.logger.error({ msg: 'something went wrong with S3', logContext, key, err }); + throw new AppError('s3', StatusCodes.INTERNAL_SERVER_ERROR, 'there is a problem with S3', true); + } + } +} diff --git a/src/records/controllers/recordsController.ts b/src/records/controllers/recordsController.ts new file mode 100644 index 0000000..7bca530 --- /dev/null +++ b/src/records/controllers/recordsController.ts @@ -0,0 +1,77 @@ +import type { Logger } from '@map-colonies/js-logger'; +import { StatusCodes } from 'http-status-codes'; +import { injectable, inject } from 'tsyringe'; +import { type Registry, Counter } from 'prom-client'; +import type { TypedRequestHandlers } from '@openapi'; +import { SERVICES } from '@common/constants'; +import type { LogContext } from '@common/interfaces'; +import { RecordManager } from '../models/recordManager'; + +@injectable() +export class RecordsController { + private readonly logContext: LogContext; + private readonly ingestionJobCounter: Counter; + + public constructor( + @inject(SERVICES.LOGGER) private readonly logger: Logger, + @inject(RecordManager) private readonly manager: RecordManager, + @inject(SERVICES.METRICS) private readonly metricsRegistry: Registry + ) { + this.logContext = { + fileName: __filename, + class: RecordsController.name, + }; + this.ingestionJobCounter = new Counter({ + name: 'ingestion_jobs_created', + help: 'number of ingestion jobs created', + registers: [this.metricsRegistry], + }); + } + + public createRecord: TypedRequestHandlers['createRecord'] = async (req, res, next) => { + const logContext = { ...this.logContext, function: this.createRecord.name }; + try { + const job = await this.manager.createIngestion(req.body); + this.ingestionJobCounter.inc(1); + return res.status(StatusCodes.CREATED).json(job); + } catch (err) { + this.logger.error({ msg: 'failed to create ingestion job', logContext, err }); + return next(err); + } + }; + + public validateRecord: TypedRequestHandlers['validateRecord'] = async (req, res, next) => { + const logContext = { ...this.logContext, function: this.validateRecord.name }; + try { + const result = await this.manager.validateIngestion(req.body); + return res.status(StatusCodes.OK).json(result); + } catch (err) { + this.logger.error({ msg: 'failed to validate ingestion request', logContext, err }); + return next(err); + } + }; + + public canDeleteRecord: TypedRequestHandlers['canDeleteRecord'] = async (req, res, next) => { + const logContext = { ...this.logContext, function: this.canDeleteRecord.name }; + const { recordId } = req.params; + try { + const result = await this.manager.canDelete(recordId); + return res.status(StatusCodes.OK).json(result); + } catch (err) { + this.logger.error({ msg: 'failed to validate record deletability', logContext, err, recordId }); + return next(err); + } + }; + + public deleteRecord: TypedRequestHandlers['deleteRecord'] = async (req, res, next) => { + const logContext = { ...this.logContext, function: this.deleteRecord.name }; + const { recordId } = req.params; + try { + const job = await this.manager.deleteRecord(recordId); + return res.status(StatusCodes.OK).json(job); + } catch (err) { + this.logger.error({ msg: 'failed to create delete job', logContext, err, recordId }); + return next(err); + } + }; +} diff --git a/src/records/models/recordManager.ts b/src/records/models/recordManager.ts new file mode 100644 index 0000000..8fb11c8 --- /dev/null +++ b/src/records/models/recordManager.ts @@ -0,0 +1,128 @@ +import { randomUUID } from 'node:crypto'; +import type { Logger } from '@map-colonies/js-logger'; +import { inject, injectable } from 'tsyringe'; +import { trace } from '@opentelemetry/api'; +import { StatusCodes } from 'http-status-codes'; +import type { Polygon } from 'geojson'; +import { RecordStatus } from '@map-colonies/types'; +import { SERVICES } from '@common/constants'; +import type { + AckResponse, + IngestionPayload, + JobResponse, + LogContext, + StatusPayload, + UpdatePayload, + ValidationResultResponse, +} from '@common/interfaces'; +import { AppError } from '@common/appError'; +import { convertPolygonTo2DPolygon } from '@common/util'; +import { IngestionValidator } from '../../validator/ingestionValidator'; +import { MetadataValidator } from '../../validator/metadataValidator'; +import { JobnikClient } from '../../externalServices/jobnik/jobnikClient'; +import { CatalogCall } from '../../externalServices/catalog/catalogCall'; + +@injectable() +export class RecordManager { + private readonly logContext: LogContext; + + public constructor( + @inject(SERVICES.LOGGER) private readonly logger: Logger, + @inject(IngestionValidator) private readonly ingestionValidator: IngestionValidator, + @inject(MetadataValidator) private readonly metadataValidator: MetadataValidator, + @inject(JobnikClient) private readonly jobnik: JobnikClient, + @inject(CatalogCall) private readonly catalog: CatalogCall + ) { + this.logContext = { + fileName: __filename, + class: RecordManager.name, + }; + } + + public async createIngestion(payload: IngestionPayload): Promise { + const logContext = { ...this.logContext, function: this.createIngestion.name }; + const modelId = randomUUID(); + this.logger.info({ msg: 'creating ingestion job', logContext, modelId, modelPath: payload.modelPath, tilesetFilename: payload.tilesetFilename }); + trace.getActiveSpan()?.setAttribute('catalogId', modelId); + + try { + await this.ingestionValidator.validateIngestion(payload); + + const metadata = payload.metadata as Record; + if (metadata.footprint !== undefined) { + metadata.footprint = convertPolygonTo2DPolygon(metadata.footprint as Polygon); + } + + return await this.jobnik.createIngestionJob(payload, modelId); + } catch (err) { + if (err instanceof AppError) { + throw err; + } + this.logger.error({ msg: 'unexpected error while creating ingestion job', logContext, modelId, err }); + throw new AppError('error', StatusCodes.INTERNAL_SERVER_ERROR, String(err), true); + } + } + + public async validateIngestion(payload: IngestionPayload): Promise { + const logContext = { ...this.logContext, function: this.validateIngestion.name }; + this.logger.info({ msg: 'validating ingestion request', logContext, modelPath: payload.modelPath }); + try { + await this.ingestionValidator.validateIngestion(payload); + return { isValid: true }; + } catch (err) { + if (err instanceof AppError) { + return { isValid: false, message: err.message }; + } + throw err; + } + } + + public async canDelete(id: string): Promise { + const logContext = { ...this.logContext, function: this.canDelete.name }; + this.logger.info({ msg: 'validating record deletability', logContext, recordId: id }); + try { + await this.metadataValidator.validateDelete(id); + return { isValid: true }; + } catch (err) { + if (err instanceof AppError) { + return { isValid: false, message: err.message }; + } + throw err; + } + } + + public async deleteRecord(id: string): Promise { + const logContext = { ...this.logContext, function: this.deleteRecord.name }; + this.logger.info({ msg: 'creating delete job', logContext, recordId: id }); + const record = await this.metadataValidator.validateDelete(id); + const job = await this.jobnik.createDeleteJob(record); + await this.catalog.changeStatus(id, { productStatus: RecordStatus.BEING_DELETED }); + return job; + } + + public async updateMetadata(id: string, update: UpdatePayload): Promise { + const logContext = { ...this.logContext, function: this.updateMetadata.name }; + this.logger.info({ msg: 'updating record metadata', logContext, recordId: id }); + const record = await this.metadataValidator.validateUpdate(id, update); + await this.metadataValidator.ensureRecordAbsentFromExtractable(record); + + const payload: Record = { ...update }; + if (payload.footprint !== undefined) { + payload.footprint = convertPolygonTo2DPolygon(payload.footprint as Polygon); + } + await this.catalog.patchMetadata(id, payload); + + return { message: `metadata updated for record ${id}` }; + } + + public async updateStatus(id: string, payload: StatusPayload): Promise { + const logContext = { ...this.logContext, function: this.updateStatus.name }; + this.logger.info({ msg: 'updating record status', logContext, recordId: id, status: payload.status }); + const record = await this.metadataValidator.validateStatusChange(id); + await this.metadataValidator.ensureRecordAbsentFromExtractable(record); + + await this.catalog.changeStatus(id, { productStatus: payload.status }); + + return { message: `status ${payload.status} accepted for record ${id}` }; + } +} diff --git a/src/records/routes/recordsRouter.ts b/src/records/routes/recordsRouter.ts new file mode 100644 index 0000000..c22f629 --- /dev/null +++ b/src/records/routes/recordsRouter.ts @@ -0,0 +1,19 @@ +import { Router } from 'express'; +import type { FactoryFunction } from 'tsyringe'; +import { RecordsController } from '../controllers/recordsController'; + +const recordsRouterFactory: FactoryFunction = (dependencyContainer) => { + const router = Router(); + const controller = dependencyContainer.resolve(RecordsController); + + router.post('/', controller.createRecord); + router.post('/validate', controller.validateRecord); + router.get('/canDelete/:recordId', controller.canDeleteRecord); + router.delete('/:recordId', controller.deleteRecord); + + return router; +}; + +export const RECORDS_ROUTER_SYMBOL = Symbol('recordsRouterFactory'); + +export { recordsRouterFactory }; diff --git a/src/resourceName/controllers/resourceNameController.ts b/src/resourceName/controllers/resourceNameController.ts deleted file mode 100644 index c194ea6..0000000 --- a/src/resourceName/controllers/resourceNameController.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { Logger } from '@map-colonies/js-logger'; -import httpStatus from 'http-status-codes'; -import { injectable, inject } from 'tsyringe'; -import { type Registry, Counter } from 'prom-client'; -import type { TypedRequestHandlers } from '@openapi'; -import { SERVICES } from '@common/constants'; - -import { ResourceNameManager } from '../models/resourceNameManager'; - -@injectable() -export class ResourceNameController { - private readonly createdResourceCounter: Counter; - - public constructor( - @inject(SERVICES.LOGGER) private readonly logger: Logger, - @inject(ResourceNameManager) private readonly manager: ResourceNameManager, - @inject(SERVICES.METRICS) private readonly metricsRegistry: Registry - ) { - this.createdResourceCounter = new Counter({ - name: 'created_resource', - help: 'number of created resources', - registers: [this.metricsRegistry], - }); - } - - public getResource: TypedRequestHandlers['getResourceName'] = (req, res) => { - return res.status(httpStatus.OK).json(this.manager.getResource()); - }; - - public createResource: TypedRequestHandlers['POST /resourceName'] = (req, res) => { - const createdResource = this.manager.createResource(req.body); - this.createdResourceCounter.inc(1); - return res.status(httpStatus.CREATED).json(createdResource); - }; -} diff --git a/src/resourceName/models/resourceNameManager.ts b/src/resourceName/models/resourceNameManager.ts deleted file mode 100644 index be5aa5a..0000000 --- a/src/resourceName/models/resourceNameManager.ts +++ /dev/null @@ -1,36 +0,0 @@ -import type { Logger } from '@map-colonies/js-logger'; -import { inject, injectable } from 'tsyringe'; -import type { components } from '@openapi'; -import { SERVICES } from '@common/constants'; - -const resourceInstance: IResourceNameModel = { - id: 1, - name: 'ronin', - description: 'can you do a logistics run?', -}; - -function generateRandomId(): number { - const rangeOfIds = 100; - return Math.floor(Math.random() * rangeOfIds); -} - -export type IResourceNameModel = components['schemas']['resource']; - -@injectable() -export class ResourceNameManager { - public constructor(@inject(SERVICES.LOGGER) private readonly logger: Logger) {} - - public getResource(): IResourceNameModel { - this.logger.info({ msg: 'getting resource', resourceId: resourceInstance.id }); - - return resourceInstance; - } - - public createResource(resource: IResourceNameModel): IResourceNameModel { - const resourceId = generateRandomId(); - - this.logger.info({ msg: 'creating resource', resourceId }); - - return { ...resource, id: resourceId }; - } -} diff --git a/src/resourceName/routes/resourceNameRouter.ts b/src/resourceName/routes/resourceNameRouter.ts deleted file mode 100644 index a0e3f4a..0000000 --- a/src/resourceName/routes/resourceNameRouter.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Router } from 'express'; -import type { FactoryFunction } from 'tsyringe'; -import { ResourceNameController } from '../controllers/resourceNameController'; - -const resourceNameRouterFactory: FactoryFunction = (dependencyContainer) => { - const router = Router(); - const controller = dependencyContainer.resolve(ResourceNameController); - - router.get('/', controller.getResource); - router.post('/', controller.createResource); - - return router; -}; - -export const RESOURCE_NAME_ROUTER_SYMBOL = Symbol('resourceNameRouterFactory'); - -export { resourceNameRouterFactory }; diff --git a/src/serverBuilder.ts b/src/serverBuilder.ts index 5554627..900566d 100644 --- a/src/serverBuilder.ts +++ b/src/serverBuilder.ts @@ -10,8 +10,9 @@ import { collectMetricsExpressMiddleware } from '@map-colonies/prometheus'; import { Registry } from 'prom-client'; import type { ConfigType } from '@common/config'; import { SERVICES } from '@common/constants'; -import { RESOURCE_NAME_ROUTER_SYMBOL } from './resourceName/routes/resourceNameRouter'; -import { ANOTHER_RESOURCE_ROUTER_SYMBOL } from './anotherResource/routes/anotherResourceRouter'; +import { RECORDS_ROUTER_SYMBOL } from './records/routes/recordsRouter'; +import { JOB_STATUS_ROUTER_SYMBOL } from './jobStatus/routes/jobStatusRouter'; +import { METADATA_ROUTER_SYMBOL } from './metadata/routes/metadataRouter'; @injectable() export class ServerBuilder { @@ -21,8 +22,9 @@ export class ServerBuilder { @inject(SERVICES.CONFIG) private readonly config: ConfigType, @inject(SERVICES.LOGGER) private readonly logger: Logger, @inject(SERVICES.METRICS) private readonly metricsRegistry: Registry, - @inject(RESOURCE_NAME_ROUTER_SYMBOL) private readonly resourceNameRouter: Router, - @inject(ANOTHER_RESOURCE_ROUTER_SYMBOL) private readonly anotherResourceRouter: Router + @inject(RECORDS_ROUTER_SYMBOL) private readonly recordsRouter: Router, + @inject(JOB_STATUS_ROUTER_SYMBOL) private readonly jobStatusRouter: Router, + @inject(METADATA_ROUTER_SYMBOL) private readonly metadataRouter: Router ) { this.serverInstance = express(); } @@ -45,8 +47,9 @@ export class ServerBuilder { } private buildRoutes(): void { - this.serverInstance.use('/resourceName', this.resourceNameRouter); - this.serverInstance.use('/anotherResource', this.anotherResourceRouter); + this.serverInstance.use('/records', this.recordsRouter); + this.serverInstance.use('/jobStatus', this.jobStatusRouter); + this.serverInstance.use('/metadata', this.metadataRouter); this.buildDocsRoutes(); } diff --git a/src/tileset/tilesetReader.ts b/src/tileset/tilesetReader.ts new file mode 100644 index 0000000..365e616 --- /dev/null +++ b/src/tileset/tilesetReader.ts @@ -0,0 +1,108 @@ +import { readFile, open } from 'node:fs/promises'; +import { inflateRawSync } from 'node:zlib'; +import { inject, injectable } from 'tsyringe'; +import type { Logger } from '@map-colonies/js-logger'; +import { StatusCodes } from 'http-status-codes'; +import { SERVICES } from '@common/constants'; +import { AppError } from '@common/appError'; +import { is3tz, buildModelFilePath } from '@common/util'; +import type { LogContext } from '@common/interfaces'; + +const LOCAL_FILE_HEADER_SIGNATURE = 0x04034b50; +const GENERAL_PURPOSE_FLAG_OFFSET = 6; +const COMPRESSION_METHOD_OFFSET = 8; +const COMPRESSED_SIZE_OFFSET = 18; +const NAME_LENGTH_OFFSET = 26; +const EXTRA_LENGTH_OFFSET = 28; +const HEADER_FIXED_LENGTH = 30; +const COMPRESSION_STORED = 0; +const COMPRESSION_DEFLATED = 8; +const DATA_DESCRIPTOR_FLAG = 0x08; +const ZIP64_SIZE_SENTINEL = 0xffffffff; + +export const ERROR_TILESET_NOT_FIRST_ENTRY = 'tileset file is not the first entry of the 3tz archive'; +export const ERROR_TILESET_UNSUPPORTED_COMPRESSION = 'tileset entry uses an unsupported compression method'; +export const ERROR_TILESET_UNSUPPORTED_ENCODING = 'tileset entry size is not available in the local header (data descriptor or ZIP64)'; +export const ERROR_TILESET_READ_FAILED = 'failed reading the tileset file'; + +@injectable() +export class TilesetReader { + private readonly logContext: LogContext; + + public constructor(@inject(SERVICES.LOGGER) private readonly logger: Logger) { + this.logContext = { + fileName: __filename, + class: TilesetReader.name, + }; + } + + public async readTilesetJson(modelPath: string, tilesetFilename: string): Promise { + return is3tz(modelPath) ? this.readFromArchive(modelPath, tilesetFilename) : this.readFromFolder(modelPath, tilesetFilename); + } + + private async readFromFolder(modelPath: string, tilesetFilename: string): Promise { + const logContext = { ...this.logContext, function: this.readFromFolder.name }; + const path = buildModelFilePath(modelPath, tilesetFilename); + try { + return await readFile(path, 'utf-8'); + } catch (err) { + this.logger.error({ msg: 'failed reading tileset from folder', logContext, path, err }); + throw new AppError('tileset', StatusCodes.INTERNAL_SERVER_ERROR, ERROR_TILESET_READ_FAILED, false); + } + } + + private async readFromArchive(archivePath: string, tilesetFilename: string): Promise { + const logContext = { ...this.logContext, function: this.readFromArchive.name }; + let handle; + try { + handle = await open(archivePath, 'r'); + } catch (err) { + this.logger.error({ msg: 'failed opening 3tz archive', logContext, archivePath, err }); + throw new AppError('tileset', StatusCodes.INTERNAL_SERVER_ERROR, ERROR_TILESET_READ_FAILED, false); + } + + try { + const header = Buffer.alloc(HEADER_FIXED_LENGTH); + await handle.read(header, 0, HEADER_FIXED_LENGTH, 0); + + if (header.readUInt32LE(0) !== LOCAL_FILE_HEADER_SIGNATURE) { + throw new AppError('badRequest', StatusCodes.BAD_REQUEST, ERROR_TILESET_NOT_FIRST_ENTRY, true); + } + + const flags = header.readUInt16LE(GENERAL_PURPOSE_FLAG_OFFSET); + const method = header.readUInt16LE(COMPRESSION_METHOD_OFFSET); + const compressedSize = header.readUInt32LE(COMPRESSED_SIZE_OFFSET); + const nameLength = header.readUInt16LE(NAME_LENGTH_OFFSET); + const extraLength = header.readUInt16LE(EXTRA_LENGTH_OFFSET); + + if ((flags & DATA_DESCRIPTOR_FLAG) !== 0 || compressedSize === 0 || compressedSize === ZIP64_SIZE_SENTINEL) { + throw new AppError('badRequest', StatusCodes.BAD_REQUEST, ERROR_TILESET_UNSUPPORTED_ENCODING, true); + } + + const nameBuffer = Buffer.alloc(nameLength); + await handle.read(nameBuffer, 0, nameLength, HEADER_FIXED_LENGTH); + if (nameBuffer.toString('utf-8') !== tilesetFilename) { + this.logger.error({ + msg: 'first 3tz entry is not the expected tileset file', + logContext, + entryName: nameBuffer.toString('utf-8'), + tilesetFilename, + }); + throw new AppError('badRequest', StatusCodes.BAD_REQUEST, ERROR_TILESET_NOT_FIRST_ENTRY, true); + } + + const raw = Buffer.alloc(compressedSize); + await handle.read(raw, 0, compressedSize, HEADER_FIXED_LENGTH + nameLength + extraLength); + + if (method === COMPRESSION_STORED) { + return raw.toString('utf-8'); + } + if (method === COMPRESSION_DEFLATED) { + return inflateRawSync(raw).toString('utf-8'); + } + throw new AppError('badRequest', StatusCodes.BAD_REQUEST, ERROR_TILESET_UNSUPPORTED_COMPRESSION, true); + } finally { + await handle.close(); + } + } +} diff --git a/src/validator/classification.ts b/src/validator/classification.ts new file mode 100644 index 0000000..91db638 --- /dev/null +++ b/src/validator/classification.ts @@ -0,0 +1,24 @@ +import type { Logger } from '@map-colonies/js-logger'; +import { StatusCodes } from 'http-status-codes'; +import type { LogContext } from '@common/interfaces'; +import { AppError } from '@common/appError'; +import type { LookupTablesCall } from '../externalServices/lookupTables/lookupTablesCall'; + +export const validateClassification = async ( + lookupTables: LookupTablesCall, + logger: Logger, + logContext: LogContext, + classification: string +): Promise => { + const classifications = await lookupTables.getClassifications(); + logger.debug({ msg: 'classification validation', logContext, classifications }); + + if (!classifications.includes(classification)) { + throw new AppError( + 'badRequest', + StatusCodes.BAD_REQUEST, + `classification is not a valid value. Optional values: ${classifications.join()}`, + true + ); + } +}; diff --git a/src/validator/errors.ts b/src/validator/errors.ts new file mode 100644 index 0000000..5817937 --- /dev/null +++ b/src/validator/errors.ts @@ -0,0 +1,18 @@ +export const ERROR_METADATA_DATE = 'imagingTimeBeginUTC must not be later than imagingTimeEndUTC'; +export const ERROR_METADATA_MISSING_DATE = 'imagingTimeBeginUTC and imagingTimeEndUTC are required'; +export const ERROR_METADATA_INVALID_DATE = 'imagingTimeBeginUTC and imagingTimeEndUTC must be valid dates'; +export const ERROR_METADATA_FOOTPRINT = 'Invalid footprint! Must be a GeoJSON Polygon or MultiPolygon with all-2D or all-3D coordinates'; +export const ERROR_METADATA_PRODUCT_NAME_UNIQUE = 'product name is not unique!'; +export const ERROR_PRODUCT_NAME_IN_FLIGHT = 'product name already has an in-flight ingestion job'; +export const ERROR_DELETE_RECORD_NOT_FOUND = "recordId doesn't match exactly one existing record"; +export const ERROR_DELETE_PRODUCT_TYPE = 'Cannot delete a record whose productType is "QuantizedMeshDTMBest"'; +export const ERROR_DELETE_STATUS = 'Cannot delete a record whose productStatus is not "UNPUBLISHED"'; +export const ERROR_FILE_NOT_FOUND = 'The model files do not exist in the agreed storage'; +export const ERROR_TILESET_INVALID = 'The tileset file is not a valid 3DTiles tileset'; +export const ERROR_FOOTPRINT_FAR_FROM_MODEL = "Wrong footprint! footprint's coordinates is not even close to the model!"; +export const ERROR_INTERSECTION_FAILED = 'An error caused during the validation of the intersection'; +export const ERROR_RECORD_NOT_FOUND = "record with the given identifier doesn't exist"; +export const ERROR_RECORD_BEING_DELETED = 'cannot change a record that is being deleted'; +export const ERROR_EXTRACTABLE_CONFLICT = 'the record exists in the extractable-management service and cannot be changed here'; +export const ERROR_MODEL_PATH_INVALID = 'Unknown model path! The model is not under the agreed base path'; +export const ERROR_PRODUCT_ID_EXISTS = 'a record with this productId already exists in the catalog'; diff --git a/src/validator/ingestionValidator.ts b/src/validator/ingestionValidator.ts new file mode 100644 index 0000000..7c4b0c7 --- /dev/null +++ b/src/validator/ingestionValidator.ts @@ -0,0 +1,138 @@ +import type { Logger } from '@map-colonies/js-logger'; +import { StatusCodes } from 'http-status-codes'; +import { inject, injectable } from 'tsyringe'; +import { new3DLayerMetadataSchema, geometrySchema } from '@map-colonies/3d-shared'; +import { SERVICES } from '@common/constants'; +import type { IngestionPayload, LogContext } from '@common/interfaces'; +import { AppError } from '@common/appError'; +import { buildModelFilePath } from '@common/util'; +import type { ConfigType } from '@common/config'; +import { LookupTablesCall } from '../externalServices/lookupTables/lookupTablesCall'; +import { CatalogCall } from '../externalServices/catalog/catalogCall'; +import { JobnikClient } from '../externalServices/jobnik/jobnikClient'; +import type { Provider } from '../providers/interfaces'; +import { TilesetValidator } from './tilesetValidator'; +import { validateClassification } from './classification'; +import { + ERROR_FILE_NOT_FOUND, + ERROR_METADATA_DATE, + ERROR_METADATA_FOOTPRINT, + ERROR_METADATA_INVALID_DATE, + ERROR_METADATA_MISSING_DATE, + ERROR_METADATA_PRODUCT_NAME_UNIQUE, + ERROR_MODEL_PATH_INVALID, + ERROR_PRODUCT_ID_EXISTS, + ERROR_PRODUCT_NAME_IN_FLIGHT, +} from './errors'; + +@injectable() +export class IngestionValidator { + private readonly logContext: LogContext; + private readonly basePath: string; + + public constructor( + @inject(SERVICES.CONFIG) private readonly config: ConfigType, + @inject(SERVICES.LOGGER) private readonly logger: Logger, + @inject(LookupTablesCall) private readonly lookupTables: LookupTablesCall, + @inject(CatalogCall) private readonly catalog: CatalogCall, + @inject(JobnikClient) private readonly jobnik: JobnikClient, + @inject(SERVICES.PROVIDER) private readonly provider: Provider, + @inject(TilesetValidator) private readonly tileset: TilesetValidator + ) { + this.basePath = this.config.get('validation.basePath'); + this.logContext = { + fileName: __filename, + class: IngestionValidator.name, + }; + } + + public async validateIngestion(payload: IngestionPayload): Promise { + const logContext = { ...this.logContext, function: this.validateIngestion.name }; + this.logger.info({ msg: 'ingestion validation start', logContext }); + + const metadata = payload.metadata; + const parsed = new3DLayerMetadataSchema.safeParse(metadata); + if (!parsed.success) { + const message = parsed.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join('; '); + throw new AppError('badRequest', StatusCodes.BAD_REQUEST, message, true); + } + + const footprintResult = geometrySchema.safeParse(metadata.footprint); + if (!footprintResult.success) { + throw new AppError('badRequest', StatusCodes.BAD_REQUEST, ERROR_METADATA_FOOTPRINT, true); + } + + this.validateDates(metadata.imagingTimeBeginUTC, metadata.imagingTimeEndUTC); + this.validateModelPath(payload.modelPath); + + await validateClassification(this.lookupTables, this.logger, logContext, parsed.data.classification); + await this.validateProductNameUnique(parsed.data.productName); + await this.validateProductNameNotInFlight(parsed.data.productName); + await this.validateProductIdUnique(parsed.data.productId); + await this.validateFileExists(payload.modelPath, payload.tilesetFilename); + await this.tileset.validateTileset(payload, footprintResult.data); + } + + private validateModelPath(modelPath: string): void { + if (!modelPath.startsWith(this.basePath)) { + throw new AppError('badRequest', StatusCodes.BAD_REQUEST, `${ERROR_MODEL_PATH_INVALID} (basePath: ${this.basePath})`, true); + } + } + + private async validateProductIdUnique(productId: string): Promise { + const logContext = { ...this.logContext, function: this.validateProductIdUnique.name }; + const records = await this.catalog.findRecords({ productId }); + this.logger.debug({ msg: 'product id uniqueness validation', logContext, productId, matches: records.length }); + + if (records.length > 0) { + throw new AppError('conflict', StatusCodes.CONFLICT, ERROR_PRODUCT_ID_EXISTS, true); + } + } + + private async validateFileExists(modelPath: string, tilesetFilename: string): Promise { + const logContext = { ...this.logContext, function: this.validateFileExists.name }; + const path = buildModelFilePath(modelPath, tilesetFilename); + const exists = await this.provider.fileExists(path); + this.logger.debug({ msg: 'file existence validation', logContext, path, exists }); + + if (!exists) { + throw new AppError('badRequest', StatusCodes.BAD_REQUEST, ERROR_FILE_NOT_FOUND, true); + } + } + + private async validateProductNameUnique(productName: string): Promise { + const logContext = { ...this.logContext, function: this.validateProductNameUnique.name }; + const records = await this.catalog.findRecords({ productName }); + this.logger.debug({ msg: 'product name uniqueness validation', logContext, productName, matches: records.length }); + + if (records.length > 0) { + throw new AppError('badRequest', StatusCodes.BAD_REQUEST, ERROR_METADATA_PRODUCT_NAME_UNIQUE, true); + } + } + + private async validateProductNameNotInFlight(productName: string): Promise { + const logContext = { ...this.logContext, function: this.validateProductNameNotInFlight.name }; + const inFlight = await this.jobnik.hasInFlightIngestionJob(productName); + this.logger.debug({ msg: 'in-flight duplicate validation', logContext, productName, inFlight }); + + if (inFlight) { + throw new AppError('conflict', StatusCodes.CONFLICT, ERROR_PRODUCT_NAME_IN_FLIGHT, true); + } + } + + private validateDates(start: unknown, end: unknown): void { + if (start === undefined || end === undefined) { + throw new AppError('badRequest', StatusCodes.BAD_REQUEST, ERROR_METADATA_MISSING_DATE, true); + } + + const startTime = new Date(start as string).getTime(); + const endTime = new Date(end as string).getTime(); + if (Number.isNaN(startTime) || Number.isNaN(endTime)) { + throw new AppError('badRequest', StatusCodes.BAD_REQUEST, ERROR_METADATA_INVALID_DATE, true); + } + + if (startTime > endTime) { + throw new AppError('badRequest', StatusCodes.BAD_REQUEST, ERROR_METADATA_DATE, true); + } + } +} diff --git a/src/validator/metadataValidator.ts b/src/validator/metadataValidator.ts new file mode 100644 index 0000000..35cb207 --- /dev/null +++ b/src/validator/metadataValidator.ts @@ -0,0 +1,127 @@ +import type { Logger } from '@map-colonies/js-logger'; +import { StatusCodes } from 'http-status-codes'; +import { inject, injectable } from 'tsyringe'; +import { new3DLayerMetadataSchema, geometrySchema } from '@map-colonies/3d-shared'; +import { ProductType, RecordStatus } from '@map-colonies/types'; +import { SERVICES } from '@common/constants'; +import type { LogContext, UpdatePayload } from '@common/interfaces'; +import { AppError } from '@common/appError'; +import { LookupTablesCall } from '../externalServices/lookupTables/lookupTablesCall'; +import { CatalogCall } from '../externalServices/catalog/catalogCall'; +import { ExtractableCall } from '../externalServices/extractableManagement/extractableCall'; +import type { Record3D } from '../externalServices/catalog/interfaces'; +import { validateClassification } from './classification'; +import { + ERROR_DELETE_PRODUCT_TYPE, + ERROR_DELETE_RECORD_NOT_FOUND, + ERROR_DELETE_STATUS, + ERROR_EXTRACTABLE_CONFLICT, + ERROR_METADATA_FOOTPRINT, + ERROR_METADATA_PRODUCT_NAME_UNIQUE, + ERROR_RECORD_BEING_DELETED, + ERROR_RECORD_NOT_FOUND, +} from './errors'; + +const BLOCKED_DELETE_PRODUCT_TYPE = ProductType.QUANTIZED_MESH_DTM_BEST; + +@injectable() +export class MetadataValidator { + private readonly logContext: LogContext; + + public constructor( + @inject(SERVICES.LOGGER) private readonly logger: Logger, + @inject(LookupTablesCall) private readonly lookupTables: LookupTablesCall, + @inject(CatalogCall) private readonly catalog: CatalogCall, + @inject(ExtractableCall) private readonly extractable: ExtractableCall + ) { + this.logContext = { + fileName: __filename, + class: MetadataValidator.name, + }; + } + + public async validateDelete(recordId: string): Promise { + const logContext = { ...this.logContext, function: this.validateDelete.name }; + const records = await this.catalog.findRecords({ id: recordId }); + this.logger.debug({ msg: 'delete validation', logContext, recordId, matches: records.length }); + + const [record] = records; + if (records.length !== 1 || record === undefined) { + throw new AppError('badRequest', StatusCodes.BAD_REQUEST, ERROR_DELETE_RECORD_NOT_FOUND, true); + } + + if (record.productType === BLOCKED_DELETE_PRODUCT_TYPE) { + throw new AppError('badRequest', StatusCodes.BAD_REQUEST, ERROR_DELETE_PRODUCT_TYPE, true); + } + + if (record.productStatus !== RecordStatus.UNPUBLISHED) { + throw new AppError('badRequest', StatusCodes.BAD_REQUEST, ERROR_DELETE_STATUS, true); + } + + return record; + } + + public async validateUpdate(identifier: string, payload: UpdatePayload): Promise { + const logContext = { ...this.logContext, function: this.validateUpdate.name }; + const record = await this.catalog.getRecord(identifier); + if (record === undefined) { + throw new AppError('notFound', StatusCodes.NOT_FOUND, ERROR_RECORD_NOT_FOUND, true); + } + if (record.productStatus === RecordStatus.BEING_DELETED) { + throw new AppError('badRequest', StatusCodes.BAD_REQUEST, ERROR_RECORD_BEING_DELETED, true); + } + + const parsed = new3DLayerMetadataSchema.partial().safeParse(payload); + if (!parsed.success) { + const message = parsed.error.issues.map((issue) => `${issue.path.join('.')}: ${issue.message}`).join('; '); + throw new AppError('badRequest', StatusCodes.BAD_REQUEST, message, true); + } + + const footprint = (payload as { footprint?: unknown }).footprint; + if (footprint !== undefined && !geometrySchema.safeParse(footprint).success) { + throw new AppError('badRequest', StatusCodes.BAD_REQUEST, ERROR_METADATA_FOOTPRINT, true); + } + + const data = parsed.data; + if (data.classification !== undefined) { + await validateClassification(this.lookupTables, this.logger, logContext, data.classification); + } + if (data.productName !== undefined) { + await this.validateProductNameUniqueExcept(data.productName, identifier); + } + + return record; + } + + public async validateStatusChange(identifier: string): Promise { + const record = await this.catalog.getRecord(identifier); + if (record === undefined) { + throw new AppError('notFound', StatusCodes.NOT_FOUND, ERROR_RECORD_NOT_FOUND, true); + } + if (record.productStatus === RecordStatus.BEING_DELETED) { + throw new AppError('badRequest', StatusCodes.BAD_REQUEST, ERROR_RECORD_BEING_DELETED, true); + } + + return record; + } + + public async ensureRecordAbsentFromExtractable(record: Record3D): Promise { + const logContext = { ...this.logContext, function: this.ensureRecordAbsentFromExtractable.name }; + if (record.productName === undefined) { + return; + } + + const exists = await this.extractable.isExtractableRecordExists(record.productName); + this.logger.debug({ msg: 'extractable existence validation', logContext, productName: record.productName, exists }); + if (exists) { + throw new AppError('conflict', StatusCodes.CONFLICT, ERROR_EXTRACTABLE_CONFLICT, true); + } + } + + private async validateProductNameUniqueExcept(productName: string, identifier: string): Promise { + const records = await this.catalog.findRecords({ productName }); + if (records.some((existing) => existing.id !== identifier)) { + throw new AppError('badRequest', StatusCodes.BAD_REQUEST, ERROR_METADATA_PRODUCT_NAME_UNIQUE, true); + } + } +} diff --git a/src/validator/tilesetValidator.ts b/src/validator/tilesetValidator.ts new file mode 100644 index 0000000..fed1fad --- /dev/null +++ b/src/validator/tilesetValidator.ts @@ -0,0 +1,98 @@ +import type { Logger } from '@map-colonies/js-logger'; +import { StatusCodes } from 'http-status-codes'; +import { inject, injectable } from 'tsyringe'; +import { calculatePolygonFromTileset, type TileSetJson } from '@map-colonies/3d-shared'; +import { area, feature, featureCollection, intersect, union } from '@turf/turf'; +import type { Feature, MultiPolygon, Polygon } from 'geojson'; +import { SERVICES } from '@common/constants'; +import type { IngestionPayload, LogContext, ValidationResponse } from '@common/interfaces'; +import { AppError } from '@common/appError'; +import type { ConfigType } from '@common/config'; +import { TilesetReader } from '../tileset/tilesetReader'; +import { ERROR_FOOTPRINT_FAR_FROM_MODEL, ERROR_INTERSECTION_FAILED, ERROR_TILESET_INVALID } from './errors'; + +const FULL_COVERAGE_PERCENT = 100; + +@injectable() +export class TilesetValidator { + private readonly logContext: LogContext; + private readonly percentageLimit: number; + + public constructor( + @inject(SERVICES.CONFIG) private readonly config: ConfigType, + @inject(SERVICES.LOGGER) private readonly logger: Logger, + @inject(TilesetReader) private readonly tilesetReader: TilesetReader + ) { + this.percentageLimit = this.config.get('validation.percentageLimit'); + this.logContext = { + fileName: __filename, + class: TilesetValidator.name, + }; + } + + public async validateTileset(payload: IngestionPayload, footprint: Polygon | MultiPolygon): Promise { + const content = await this.tilesetReader.readTilesetJson(payload.modelPath, payload.tilesetFilename); + + const { modelPolygon, response: polygonResponse } = this.getTilesetModelPolygon(content); + if (!polygonResponse.isValid || modelPolygon === undefined) { + this.rejectValidation(polygonResponse); + } + + const intersectionResponse = this.isFootprintAndModelIntersects(footprint, modelPolygon); + if (!intersectionResponse.isValid) { + this.rejectValidation(intersectionResponse); + } + } + + private getTilesetModelPolygon(content: string): { modelPolygon?: Polygon; response: ValidationResponse } { + const logContext = { ...this.logContext, function: this.getTilesetModelPolygon.name }; + try { + const tilesetJson = JSON.parse(content) as TileSetJson; + const modelPolygon = calculatePolygonFromTileset(tilesetJson); + this.logger.debug({ msg: 'extracted tileset model polygon', logContext, modelPolygon }); + + return { modelPolygon, response: { isValid: true } }; + } catch (err) { + const message = err instanceof Error ? err.message : ERROR_TILESET_INVALID; + this.logger.error({ msg: 'tileset polygon extraction failed', logContext, err }); + return { response: { isValid: false, message } }; + } + } + + private isFootprintAndModelIntersects(footprint: Polygon | MultiPolygon, modelPolygon: Polygon): ValidationResponse { + const logContext = { ...this.logContext, function: this.isFootprintAndModelIntersects.name }; + try { + const footprintFeature = feature(footprint); + const modelFeature: Feature = feature(modelPolygon); + + const intersection = intersect(featureCollection([footprintFeature, modelFeature])); + if (intersection === null) { + return { isValid: false, message: ERROR_FOOTPRINT_FAR_FROM_MODEL }; + } + + const combined = union(featureCollection([footprintFeature, modelFeature])); + const combinedArea = combined === null ? 0 : area(combined); + const coverage = combinedArea === 0 ? 0 : (FULL_COVERAGE_PERCENT * area(footprintFeature)) / combinedArea; + this.logger.debug({ msg: 'calculated footprint coverage of the model', logContext, coverage, percentageLimit: this.percentageLimit }); + + if (coverage < this.percentageLimit) { + return { + isValid: false, + message: `The footprint intersectection with the model doesn't reach minimum required threshhold, the coverage is: ${coverage}% when the minimum coverage is ${this.percentageLimit}%`, + }; + } + return { isValid: true }; + } catch (err) { + this.logger.error({ msg: ERROR_INTERSECTION_FAILED, logContext, err }); + return { isValid: false, message: ERROR_INTERSECTION_FAILED }; + } + } + + private rejectValidation(response: ValidationResponse): never { + const logContext = { ...this.logContext, function: this.rejectValidation.name }; + const message = response.message ?? ERROR_TILESET_INVALID; + this.logger.warn({ msg: 'ingestion validation failed', logContext, isValid: response.isValid, message }); + + throw new AppError('badRequest', StatusCodes.BAD_REQUEST, message, true); + } +} diff --git a/tests/helpers/metadata.ts b/tests/helpers/metadata.ts new file mode 100644 index 0000000..c319f46 --- /dev/null +++ b/tests/helpers/metadata.ts @@ -0,0 +1,30 @@ +/* eslint-disable @typescript-eslint/no-magic-numbers */ +import { LAYER_3D_PRODUCT_TYPE_LIST } from '@map-colonies/3d-shared'; + +export const buildValidMetadata = (): Record => ({ + productId: 'p-1', + productName: 'afula', + productType: LAYER_3D_PRODUCT_TYPE_LIST[0], + classification: 'abc123', + srsId: '4326', + srsName: 'WGS84GEO', + region: ['ישראל'], + producerName: 'IDFMU', + productionSystem: 'sys', + productionSystemVersion: '1', + productionDate: '2025-07-08T11:26:00.000Z', + footprint: { + type: 'Polygon', + coordinates: [ + [ + [35.17, 32.9], + [35.18, 32.9], + [35.18, 32.91], + [35.17, 32.91], + [35.17, 32.9], + ], + ], + }, + imagingTimeBeginUTC: '2025-07-06T11:10:00.000Z', + imagingTimeEndUTC: '2025-07-10T11:10:00.000Z', +}); diff --git a/tests/helpers/tilesets/folder/tileset.json b/tests/helpers/tilesets/folder/tileset.json new file mode 100644 index 0000000..c9d2f7d --- /dev/null +++ b/tests/helpers/tilesets/folder/tileset.json @@ -0,0 +1,35 @@ +{ + "asset": { "version": "1.1" }, + "geometricError": 500.0, + "root": { + "boundingVolume": { "region": [0.613745031463806, 0.5741784173210945, 0.614094097314205, 0.5744402167088937, -10.0, 200.0] }, + "geometricError": 500.0, + "refine": "ADD", + "children": [ + { + "boundingVolume": { "region": [0.613745031463806, 0.5741784173210945, 0.614094097314205, 0.5744402167088937, -10.0, 200.0] }, + "geometricError": 500.0, + "refine": "ADD", + "content": { "uri": "./terrain/tileset.json" } + }, + { + "boundingVolume": { "region": [0.613745031463806, 0.5741784173210945, 0.614094097314205, 0.5744402167088937, -10.0, 200.0] }, + "geometricError": 500.0, + "refine": "ADD", + "content": { "uri": "./buildings/tileset.json" } + }, + { + "boundingVolume": { "region": [0.613745031463806, 0.5741784173210945, 0.614094097314205, 0.5744402167088937, -10.0, 200.0] }, + "geometricError": 500.0, + "refine": "ADD", + "content": { "uri": "./roof_furniture/tileset.json" } + }, + { + "boundingVolume": { "region": [0.613745031463806, 0.5741784173210945, 0.614094097314205, 0.5744402167088937, -10.0, 200.0] }, + "geometricError": 500.0, + "refine": "ADD", + "content": { "uri": "./trees/tileset.json" } + } + ] + } +} diff --git a/tests/helpers/tilesets/region.3tz b/tests/helpers/tilesets/region.3tz new file mode 100644 index 0000000..88714e6 Binary files /dev/null and b/tests/helpers/tilesets/region.3tz differ diff --git a/tests/integration/anotherResource/anotherResourceName.spec.ts b/tests/integration/anotherResource/anotherResourceName.spec.ts deleted file mode 100644 index fce250d..0000000 --- a/tests/integration/anotherResource/anotherResourceName.spec.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { jsLogger } from '@map-colonies/js-logger'; -import { describe, beforeEach, it, expect, beforeAll } from 'vitest'; -import { trace } from '@opentelemetry/api'; -import httpStatusCodes from 'http-status-codes'; -import { createRequestSender, type RequestSender } from '@map-colonies/openapi-supertest'; -import type { paths, operations } from '@openapi'; -import { getApp } from '@src/app'; -import { SERVICES } from '@src/common/constants'; -import { initConfig } from '@src/common/config'; - -describe('anotherResourceName', function () { - let requestSender: RequestSender; - - beforeAll(async function () { - await initConfig(true); - }); - - beforeEach(async function () { - const [app] = await getApp({ - override: [ - { token: SERVICES.LOGGER, provider: { useValue: await jsLogger({ enabled: false }) } }, - { token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } }, - ], - useChild: true, - }); - requestSender = await createRequestSender('openapi3.yaml', app); - }); - - describe('Happy Path', function () { - it('should return 200 status code and the resource', async function () { - const response = await requestSender.getAnotherResource(); - - expect(response.status).toBe(httpStatusCodes.OK); - expect(response).toSatisfyApiSpec(); - - const resource = response.body as paths['/anotherResource']['get']['responses'][200]['content']['application/json']; - - expect(resource.kind).toBe('avi'); - expect(resource.isAlive).toBe(false); - }); - }); - - describe('Bad Path', function () { - // All requests with status code of 400 - it('should in theory test 400 status code', function () { - expect(true).toBe(true); - }); - }); - - describe('Sad Path', function () { - // All requests with status code 4XX-5XX - it('should in theory test 500 status code', function () { - expect(true).toBe(true); - }); - }); -}); diff --git a/tests/integration/record/record.spec.ts b/tests/integration/record/record.spec.ts new file mode 100644 index 0000000..64e7ad5 --- /dev/null +++ b/tests/integration/record/record.spec.ts @@ -0,0 +1,209 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { jsLogger } from '@map-colonies/js-logger'; +import { describe, beforeEach, it, expect, beforeAll, vi } from 'vitest'; +import { trace } from '@opentelemetry/api'; +import httpStatusCodes from 'http-status-codes'; +import { createRequestSender, type RequestSender } from '@map-colonies/openapi-supertest'; +import type { paths, operations } from '@openapi'; +import { getApp } from '@src/app'; +import { SERVICES } from '@common/constants'; +import { initConfig } from '@src/common/config'; +import { LookupTablesCall } from '@src/externalServices/lookupTables/lookupTablesCall'; +import { CatalogCall } from '@src/externalServices/catalog/catalogCall'; +import { JobnikClient } from '@src/externalServices/jobnik/jobnikClient'; +import { TilesetReader } from '@src/tileset/tilesetReader'; +import { ExtractableCall } from '@src/externalServices/extractableManagement/extractableCall'; +import { buildValidMetadata } from '@tests/helpers/metadata'; + +const regionTilesetJson = readFileSync(join(__dirname, '../../helpers/tilesets/folder/tileset.json'), 'utf-8'); + +const deletableRecord = { + id: 'rec-1', + productId: 'p-1', + productName: 'afula', + productType: '3DPhotoRealistic', + productVersion: 1, + producerName: 'IDFMU', + productStatus: 'UNPUBLISHED', +}; + +const lookupStub = { getClassifications: vi.fn().mockResolvedValue(['abc123']) } as unknown as LookupTablesCall; +const catalogStub = { + findRecords: vi.fn().mockImplementation((payload: { id?: string }) => (payload.id !== undefined ? [deletableRecord] : [])), + getRecord: vi.fn().mockResolvedValue(deletableRecord), + patchMetadata: vi.fn().mockResolvedValue(deletableRecord), + changeStatus: vi.fn().mockResolvedValue(deletableRecord), +} as unknown as CatalogCall; +const jobnikStub = { + createIngestionJob: vi.fn().mockResolvedValue({ jobId: 'job-1', status: 'PENDING' }), + createDeleteJob: vi.fn().mockResolvedValue({ jobId: 'del-1', status: 'PENDING' }), + hasInFlightIngestionJob: vi.fn().mockResolvedValue(false), + getJobStatus: vi.fn().mockResolvedValue({ status: 'IN_PROGRESS', percentage: 42 }), +} as unknown as JobnikClient; +const tilesetReaderStub = { readTilesetJson: vi.fn().mockResolvedValue(regionTilesetJson) } as unknown as TilesetReader; +const extractableStub = { isExtractableRecordExists: vi.fn().mockResolvedValue(false) } as unknown as ExtractableCall; +const providerStub = { fileExists: vi.fn().mockResolvedValue(true) }; + +const validMetadata = buildValidMetadata(); + +const validIngestionPayload = { + modelPath: '/app/models/afula', + tilesetFilename: 'tileset.json', + metadata: validMetadata, +}; + +describe('3d-ops-trigger', function () { + let requestSender: RequestSender; + + beforeAll(async function () { + await initConfig(true); + }); + + beforeEach(async function () { + const [app] = await getApp({ + override: [ + { token: SERVICES.LOGGER, provider: { useValue: await jsLogger({ enabled: false }) } }, + { token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } }, + { token: LookupTablesCall, provider: { useValue: lookupStub } }, + { token: CatalogCall, provider: { useValue: catalogStub } }, + { token: JobnikClient, provider: { useValue: jobnikStub } }, + { token: TilesetReader, provider: { useValue: tilesetReaderStub } }, + { token: ExtractableCall, provider: { useValue: extractableStub } }, + { token: SERVICES.PROVIDER, provider: { useValue: providerStub } }, + ], + useChild: true, + }); + requestSender = await createRequestSender('openapi3.yaml', app); + }); + + describe('POST /records', function () { + it('should return 201 and a job response for a valid ingestion request', async function () { + const response = await requestSender.createRecord({ requestBody: validIngestionPayload }); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.CREATED); + + const body = response.body as paths['/records']['post']['responses']['201']['content']['application/json']; + + expect(body.jobId).toBeTypeOf('string'); + expect(body.status).toBeTypeOf('string'); + }); + + it('should return 400 when a required field is missing', async function () { + const response = await requestSender.createRecord({ + // @ts-expect-error intentionally invalid: missing tilesetFilename and metadata + requestBody: { modelPath: '/app/models/afula' }, + }); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.BAD_REQUEST); + }); + + it('should return 400 when metadata fails business validation', async function () { + const response = await requestSender.createRecord({ + requestBody: { ...validIngestionPayload, metadata: { ...validMetadata, productType: 'NOT_A_3D_TYPE' } }, + }); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.BAD_REQUEST); + }); + }); + + describe('POST /records/validate', function () { + it('should return 200 with isValid true for a valid request without creating a job', async function () { + const response = await requestSender.validateRecord({ requestBody: validIngestionPayload }); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.OK); + + const body = response.body; + + expect(body.isValid).toBe(true); + }); + + it('should return 200 with isValid false and a message for an invalid request', async function () { + const response = await requestSender.validateRecord({ + requestBody: { ...validIngestionPayload, metadata: { ...validMetadata, productType: 'NOT_A_3D_TYPE' } }, + }); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.OK); + + const body = response.body; + + expect(body.isValid).toBe(false); + expect(body.message).toBeTypeOf('string'); + }); + }); + + describe('GET /records/canDelete/{recordId}', function () { + it('should return 200 with isValid true for a deletable record', async function () { + const response = await requestSender.canDeleteRecord({ pathParams: { recordId: 'rec-1' } }); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.OK); + + const body = response.body; + + expect(body.isValid).toBe(true); + }); + }); + + describe('DELETE /records/{recordId}', function () { + it('should return 200 and a job response', async function () { + const response = await requestSender.deleteRecord({ pathParams: { recordId: 'rec-1' } }); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.OK); + + const body = response.body as paths['/records/{recordId}']['delete']['responses']['200']['content']['application/json']; + + expect(body.jobId).toBeTypeOf('string'); + }); + }); + + describe('PATCH /metadata/{identifier}', function () { + it('should return 200 and an ack for a metadata update', async function () { + const response = await requestSender.updateMetadata({ pathParams: { identifier: 'rec-1' }, requestBody: { description: 'updated' } }); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.OK); + expect(response.body.message).toBeTypeOf('string'); + }); + }); + + describe('PATCH /metadata/status/{identifier}', function () { + it('should return 200 and an ack for a valid status change', async function () { + const response = await requestSender.updateMetadataStatus({ pathParams: { identifier: 'rec-1' }, requestBody: { status: 'PUBLISHED' } }); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.OK); + }); + + it('should return 400 for an invalid status value', async function () { + const response = await requestSender.updateMetadataStatus({ + pathParams: { identifier: 'rec-1' }, + // @ts-expect-error intentionally invalid status enum value + requestBody: { status: 'NOT_A_STATUS' }, + }); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.BAD_REQUEST); + }); + }); + + describe('GET /jobStatus/{jobId}', function () { + it('should return 200 with the job status and percentage', async function () { + const response = await requestSender.getJobStatus({ pathParams: { jobId: 'job-1' } }); + + expect(response).toSatisfyApiSpec(); + expect(response.status).toBe(httpStatusCodes.OK); + + const body = response.body as paths['/jobStatus/{jobId}']['get']['responses']['200']['content']['application/json']; + + expect(body.status).toBeTypeOf('string'); + expect(body.percentage).toBe(42); + }); + }); +}); diff --git a/tests/integration/resourceName/resourceName.spec.ts b/tests/integration/resourceName/resourceName.spec.ts deleted file mode 100644 index be26f69..0000000 --- a/tests/integration/resourceName/resourceName.spec.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { jsLogger } from '@map-colonies/js-logger'; -import { describe, beforeEach, it, expect, beforeAll } from 'vitest'; -import { trace } from '@opentelemetry/api'; -import httpStatusCodes from 'http-status-codes'; -import { createRequestSender, type RequestSender } from '@map-colonies/openapi-supertest'; -import type { paths, operations } from '@openapi'; -import { getApp } from '@src/app'; -import { SERVICES } from '@common/constants'; -import { initConfig } from '@src/common/config'; - -describe('resourceName', function () { - let requestSender: RequestSender; - - beforeAll(async function () { - await initConfig(true); - }); - - beforeEach(async function () { - const [app] = await getApp({ - override: [ - { token: SERVICES.LOGGER, provider: { useValue: await jsLogger({ enabled: false }) } }, - { token: SERVICES.TRACER, provider: { useValue: trace.getTracer('testTracer') } }, - ], - useChild: true, - }); - requestSender = await createRequestSender('openapi3.yaml', app); - }); - - describe('Happy Path', function () { - it('should return 200 status code and the resource', async function () { - const response = await requestSender.getResourceName(); - - expect(response.status).toBe(httpStatusCodes.OK); - - const resource = response.body as paths['/resourceName']['get']['responses'][200]['content']['application/json']; - - expect(response).toSatisfyApiSpec(); - expect(resource.id).toBe(1); - expect(resource.name).toBe('ronin'); - expect(resource.description).toBe('can you do a logistics run?'); - }); - - it('should return 200 status code and create the resource', async function () { - const response = await requestSender.createResource({ - requestBody: { - description: 'aaa', - id: 1, - name: 'aaa', - }, - }); - - expect(response).toSatisfyApiSpec(); - expect(response.status).toBe(httpStatusCodes.CREATED); - }); - }); - - describe('Bad Path', function () { - // All requests with status code of 400 - it('should in theory test 400 status code', function () { - expect(true).toBe(true); - }); - }); - - describe('Sad Path', function () { - // All requests with status code 4XX-5XX - it('should in theory test 500 status code', function () { - expect(true).toBe(true); - }); - }); -}); diff --git a/tests/unit/anotherResource/models/anotherResourceManager.spec.ts b/tests/unit/anotherResource/models/anotherResourceManager.spec.ts deleted file mode 100644 index c086bb4..0000000 --- a/tests/unit/anotherResource/models/anotherResourceManager.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { jsLogger } from '@map-colonies/js-logger'; -import { describe, beforeEach, it, expect } from 'vitest'; -import { AnotherResourceManager } from '@src/anotherResource/models/anotherResourceManager'; - -let anotherResourceManager: AnotherResourceManager; - -describe('ResourceNameManager', () => { - beforeEach(async function () { - anotherResourceManager = new AnotherResourceManager(await jsLogger({ enabled: false })); - }); - - describe('#getResource', () => { - it('should return resource of kind avi', function () { - // action - const resource = anotherResourceManager.getResource(); - - // expectation - expect(resource.kind).toBe('avi'); - expect(resource.isAlive).toBe(false); - }); - }); -}); diff --git a/tests/unit/externalServices/catalog/catalogCall.spec.ts b/tests/unit/externalServices/catalog/catalogCall.spec.ts new file mode 100644 index 0000000..94d45ec --- /dev/null +++ b/tests/unit/externalServices/catalog/catalogCall.spec.ts @@ -0,0 +1,36 @@ +import axios from 'axios'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { jsLogger } from '@map-colonies/js-logger'; +import { StatusCodes } from 'http-status-codes'; +import { CatalogCall } from '@src/externalServices/catalog/catalogCall'; +import { AppError } from '@src/common/appError'; +import type { ConfigType } from '@src/common/config'; + +vi.mock('axios'); +const mockedAxios = vi.mocked(axios, true); + +const configStub = { get: (): unknown => 'http://catalog' } as unknown as ConfigType; + +describe('CatalogCall', function () { + let client: CatalogCall; + + beforeEach(async function () { + vi.clearAllMocks(); + client = new CatalogCall(configStub, await jsLogger({ enabled: false })); + }); + + it('should return the records found by the catalog service', async function () { + mockedAxios.post.mockResolvedValue({ status: StatusCodes.OK, data: [{ id: '1', productName: 'afula' }] }); + + const records = await client.findRecords({ productName: 'afula' }); + + expect(records).toHaveLength(1); + expect(records[0].productName).toBe('afula'); + }); + + it('should throw an AppError when the catalog service fails', async function () { + mockedAxios.post.mockRejectedValue(new Error('service down')); + + await expect(client.findRecords({ productName: 'afula' })).rejects.toThrow(AppError); + }); +}); diff --git a/tests/unit/externalServices/jobnik/jobnikClient.spec.ts b/tests/unit/externalServices/jobnik/jobnikClient.spec.ts new file mode 100644 index 0000000..ccb0fed --- /dev/null +++ b/tests/unit/externalServices/jobnik/jobnikClient.spec.ts @@ -0,0 +1,168 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { jsLogger } from '@map-colonies/js-logger'; +import { Registry } from 'prom-client'; +import { JobnikClient } from '@src/externalServices/jobnik/jobnikClient'; +import { STAGE_TYPES } from '@src/common/constants'; +import type { ConfigType } from '@src/common/config'; +import type { IngestionPayload } from '@src/common/interfaces'; + +const jobManagerConfig = { + url: 'http://job-manager', + ingestion: { jobType: 'Ingestion_New_3D', taskType: 'tilesCopying', batches: 100 }, + delete: { jobType: 'Delete_3D', taskType: 'deleteModel' }, +}; + +const producerMock = vi.hoisted(() => ({ + createJob: vi.fn(), + createStage: vi.fn(), + createTasks: vi.fn(), +})); + +const apiClientMock = vi.hoisted(() => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention -- mirrors the SDK ApiClient.GET method + GET: vi.fn(), +})); + +vi.mock('@map-colonies/jobnik-sdk', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + JobnikSDK: class { + public getProducer(): typeof producerMock { + return producerMock; + } + public getApiClient(): typeof apiClientMock { + return apiClientMock; + } + }, +})); + +const configStub = { get: (): unknown => jobManagerConfig } as unknown as ConfigType; + +const buildPayload = (modelPath: string): IngestionPayload => ({ + modelPath, + tilesetFilename: 'tileset.json', + metadata: { productName: 'afula' }, +}); + +describe('JobnikClient', function () { + let client: JobnikClient; + + beforeEach(async function () { + vi.clearAllMocks(); + producerMock.createJob.mockResolvedValue({ id: 'job-1', status: 'PENDING' }); + producerMock.createStage.mockResolvedValue({ id: 'stage-1' }); + producerMock.createTasks.mockResolvedValue([]); + apiClientMock.GET.mockResolvedValue({ data: { total: 0, items: [] } }); + client = new JobnikClient(configStub, await jsLogger({ enabled: false }), new Registry()); + }); + + it('should create the ingestion job and a validation stage for a folder input', async function () { + const result = await client.createIngestionJob(buildPayload('/shared/models/afula'), 'model-1'); + + const createJobArg = (producerMock.createJob.mock.calls as [{ name: string; data: { modelId: string } }][])[0][0]; + + expect(createJobArg.name).toBe(jobManagerConfig.ingestion.jobType); + expect(createJobArg.data.modelId).toBe('model-1'); + + const folderStageTypes = (producerMock.createStage.mock.calls as [string, { type: string }][]).map((call) => call[1].type); + + expect(folderStageTypes).toEqual([ + STAGE_TYPES.VALIDATION, + STAGE_TYPES.CREATE_UPLOAD_MODEL_TASKS, + STAGE_TYPES.UPLOAD_MODEL_DATA, + STAGE_TYPES.UPLOAD_MODEL_PARTS, + STAGE_TYPES.INGESTION_FINALIZER, + ]); + + const validationTaskCall = (producerMock.createTasks.mock.calls as [string, string, { data: { metadata?: unknown } }[]][]).find( + (call) => call[1] === STAGE_TYPES.VALIDATION + ); + + expect(validationTaskCall?.[2][0]?.data.metadata).toEqual({ productName: 'afula' }); + expect(result).toEqual({ jobId: 'job-1', status: 'PENDING' }); + }); + + it('should also create the data-extraction and clear-data stages for a 3tz input', async function () { + await client.createIngestionJob(buildPayload('/shared/models/afula.3tz'), 'model-1'); + + const stageTypes = (producerMock.createStage.mock.calls as [string, { type: string }][]).map((call) => call[1].type); + + expect(stageTypes).toEqual([ + STAGE_TYPES.DATA_EXTRACTION, + STAGE_TYPES.VALIDATION, + STAGE_TYPES.CREATE_UPLOAD_MODEL_TASKS, + STAGE_TYPES.UPLOAD_MODEL_DATA, + STAGE_TYPES.CLEAR_DATA, + STAGE_TYPES.UPLOAD_MODEL_PARTS, + STAGE_TYPES.INGESTION_FINALIZER, + ]); + }); + + it('should create a delete job with the record details', async function () { + const result = await client.createDeleteJob({ id: 'rec-1', productId: 'p-1', productName: 'afula', productType: '3DPhotoRealistic' }); + + expect(producerMock.createJob).toHaveBeenCalledWith(expect.objectContaining({ name: jobManagerConfig.delete.jobType })); + expect(result).toEqual({ jobId: 'job-1', status: 'PENDING' }); + }); + + describe('hasInFlightIngestionJob', function () { + it('should query jobs by the ingestion job name', async function () { + await client.hasInFlightIngestionJob('afula'); + + expect(apiClientMock.GET).toHaveBeenCalledWith('/v1/jobs', { + // eslint-disable-next-line @typescript-eslint/naming-convention -- Jobnik query params are snake_case + params: { query: { job_name: jobManagerConfig.ingestion.jobType, page: 1, page_size: 100 } }, + }); + }); + + it('should return true when a non-terminal job matches the product name', async function () { + apiClientMock.GET.mockResolvedValueOnce({ + data: { total: 1, items: [{ status: 'IN_PROGRESS', data: { metadata: { productName: 'afula' } } }] }, + }); + + await expect(client.hasInFlightIngestionJob('afula')).resolves.toBe(true); + }); + + it('should return false when the only matching job is in a terminal status', async function () { + apiClientMock.GET.mockResolvedValueOnce({ + data: { total: 1, items: [{ status: 'COMPLETED', data: { metadata: { productName: 'afula' } } }] }, + }); + + await expect(client.hasInFlightIngestionJob('afula')).resolves.toBe(false); + }); + + it('should return false when no in-flight job matches the product name', async function () { + apiClientMock.GET.mockResolvedValueOnce({ + data: { total: 1, items: [{ status: 'PENDING', data: { metadata: { productName: 'haifa' } } }] }, + }); + + await expect(client.hasInFlightIngestionJob('afula')).resolves.toBe(false); + }); + + it('should page through results and match a job on a later page', async function () { + const fullPage = Array.from({ length: 100 }, () => ({ status: 'IN_PROGRESS', data: { metadata: { productName: 'haifa' } } })); + apiClientMock.GET.mockResolvedValueOnce({ data: { total: 101, items: fullPage } }); + apiClientMock.GET.mockResolvedValueOnce({ + data: { total: 101, items: [{ status: 'PENDING', data: { metadata: { productName: 'afula' } } }] }, + }); + + await expect(client.hasInFlightIngestionJob('afula')).resolves.toBe(true); + expect(apiClientMock.GET).toHaveBeenNthCalledWith(2, '/v1/jobs', { + // eslint-disable-next-line @typescript-eslint/naming-convention -- Jobnik query params are snake_case + params: { query: { job_name: jobManagerConfig.ingestion.jobType, page: 2, page_size: 100 } }, + }); + }); + + it('should stop paging once a page is shorter than the page size', async function () { + apiClientMock.GET.mockResolvedValueOnce({ data: { total: 1, items: [{ status: 'PENDING', data: { metadata: { productName: 'haifa' } } }] } }); + + await expect(client.hasInFlightIngestionJob('afula')).resolves.toBe(false); + expect(apiClientMock.GET).toHaveBeenCalledTimes(1); + }); + + it('should throw an AppError when the jobs query returns an error', async function () { + apiClientMock.GET.mockResolvedValueOnce({ error: { message: 'boom' } }); + + await expect(client.hasInFlightIngestionJob('afula')).rejects.toThrow(/failed querying Jobnik/); + }); + }); +}); diff --git a/tests/unit/externalServices/lookupTables/lookupTablesCall.spec.ts b/tests/unit/externalServices/lookupTables/lookupTablesCall.spec.ts new file mode 100644 index 0000000..17ae1f8 --- /dev/null +++ b/tests/unit/externalServices/lookupTables/lookupTablesCall.spec.ts @@ -0,0 +1,37 @@ +import axios from 'axios'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { jsLogger } from '@map-colonies/js-logger'; +import { LookupTablesCall } from '@src/externalServices/lookupTables/lookupTablesCall'; +import { AppError } from '@src/common/appError'; +import type { ConfigType } from '@src/common/config'; + +vi.mock('axios'); +const mockedAxios = vi.mocked(axios, true); + +const configStub = { get: (): unknown => ({ url: 'http://lookup', subUrl: 'lookup-tables/lookupData' }) } as unknown as ConfigType; + +describe('LookupTablesCall', function () { + let client: LookupTablesCall; + + beforeEach(async function () { + vi.clearAllMocks(); + client = new LookupTablesCall(configStub, await jsLogger({ enabled: false })); + }); + + it('should return the classification values from the service', async function () { + mockedAxios.get.mockResolvedValue({ + data: [ + { value: 'a', translationCode: 'x' }, + { value: 'b', translationCode: 'y' }, + ], + }); + + await expect(client.getClassifications()).resolves.toEqual(['a', 'b']); + }); + + it('should throw an AppError when the lookup-tables service fails', async function () { + mockedAxios.get.mockRejectedValue(new Error('service down')); + + await expect(client.getClassifications()).rejects.toThrow(AppError); + }); +}); diff --git a/tests/unit/providers/nfsProvider.spec.ts b/tests/unit/providers/nfsProvider.spec.ts new file mode 100644 index 0000000..a37add4 --- /dev/null +++ b/tests/unit/providers/nfsProvider.spec.ts @@ -0,0 +1,38 @@ +import { access } from 'node:fs/promises'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { jsLogger } from '@map-colonies/js-logger'; +import { NFSProvider } from '@src/providers/nfsProvider'; +import { AppError } from '@src/common/appError'; +import type { ConfigType } from '@src/common/config'; + +vi.mock('node:fs/promises', () => ({ access: vi.fn() })); +const mockedAccess = vi.mocked(access); + +const configStub = { get: (): unknown => ({ pvPath: '/data/models' }) } as unknown as ConfigType; + +describe('NFSProvider', function () { + let provider: NFSProvider; + + beforeEach(async function () { + vi.clearAllMocks(); + provider = new NFSProvider(configStub, await jsLogger({ enabled: false })); + }); + + it('should return true when the path is accessible', async function () { + mockedAccess.mockResolvedValue(undefined); + + await expect(provider.fileExists('afula/tileset.json')).resolves.toBe(true); + }); + + it('should return false when the path does not exist (ENOENT)', async function () { + mockedAccess.mockRejectedValue(Object.assign(new Error('not found'), { code: 'ENOENT' })); + + await expect(provider.fileExists('missing/tileset.json')).resolves.toBe(false); + }); + + it('should throw an AppError on non-ENOENT errors', async function () { + mockedAccess.mockRejectedValue(Object.assign(new Error('permission denied'), { code: 'EACCES' })); + + await expect(provider.fileExists('locked/tileset.json')).rejects.toThrow(AppError); + }); +}); diff --git a/tests/unit/providers/s3Provider.spec.ts b/tests/unit/providers/s3Provider.spec.ts new file mode 100644 index 0000000..4e0f1e9 --- /dev/null +++ b/tests/unit/providers/s3Provider.spec.ts @@ -0,0 +1,59 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { jsLogger } from '@map-colonies/js-logger'; +import { StatusCodes } from 'http-status-codes'; +import { S3Provider } from '@src/providers/s3Provider'; +import { AppError } from '@src/common/appError'; +import type { ConfigType } from '@src/common/config'; + +const sendMock = vi.hoisted(() => vi.fn()); + +vi.mock('@aws-sdk/client-s3', () => ({ + // eslint-disable-next-line @typescript-eslint/naming-convention + S3Client: class { + public send = sendMock; + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + HeadObjectCommand: class { + public constructor(public readonly input: unknown) {} + }, +})); + +const configStub = { + get: (): unknown => ({ + accessKeyId: 'k', + secretAccessKey: 's', + endpointUrl: 'http://s3', + bucket: 'b', + region: 'us-east-1', + forcePathStyle: true, + sslEnabled: false, + maxAttempts: 3, + }), +} as unknown as ConfigType; + +describe('S3Provider', function () { + let provider: S3Provider; + + beforeEach(async function () { + vi.clearAllMocks(); + provider = new S3Provider(configStub, await jsLogger({ enabled: false })); + }); + + it('should return true when the object exists', async function () { + sendMock.mockResolvedValue({}); + + await expect(provider.fileExists('afula/tileset.json')).resolves.toBe(true); + }); + + it('should return false when the object is not found (404)', async function () { + sendMock.mockRejectedValue({ $metadata: { httpStatusCode: StatusCodes.NOT_FOUND } }); + + await expect(provider.fileExists('missing')).resolves.toBe(false); + }); + + it('should throw an AppError on other S3 errors', async function () { + sendMock.mockRejectedValue({ $metadata: { httpStatusCode: StatusCodes.INTERNAL_SERVER_ERROR } }); + + await expect(provider.fileExists('boom')).rejects.toThrow(AppError); + }); +}); diff --git a/tests/unit/records/models/recordManager.spec.ts b/tests/unit/records/models/recordManager.spec.ts new file mode 100644 index 0000000..e2dc26b --- /dev/null +++ b/tests/unit/records/models/recordManager.spec.ts @@ -0,0 +1,79 @@ +import { jsLogger } from '@map-colonies/js-logger'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { RecordManager } from '@src/records/models/recordManager'; +import type { IngestionPayload } from '@src/common/interfaces'; +import type { IngestionValidator } from '@src/validator/ingestionValidator'; +import type { MetadataValidator } from '@src/validator/metadataValidator'; +import type { JobnikClient } from '@src/externalServices/jobnik/jobnikClient'; +import type { CatalogCall } from '@src/externalServices/catalog/catalogCall'; + +const ingestionValidatorStub = { + validateIngestion: vi.fn().mockResolvedValue(undefined), +} as unknown as IngestionValidator; +const metadataValidatorStub = { + validateDelete: vi.fn().mockResolvedValue({ id: 'rec-1', productName: 'afula' }), + validateUpdate: vi.fn().mockResolvedValue({ id: 'rec-1', productName: 'afula' }), + validateStatusChange: vi.fn().mockResolvedValue({ id: 'rec-1', productName: 'afula' }), + ensureRecordAbsentFromExtractable: vi.fn().mockResolvedValue(undefined), +} as unknown as MetadataValidator; +const jobnikStub = { + createIngestionJob: vi.fn().mockResolvedValue({ jobId: 'job-1', status: 'PENDING' }), + createDeleteJob: vi.fn().mockResolvedValue({ jobId: 'del-1', status: 'PENDING' }), +} as unknown as JobnikClient; +const patchMetadataMock = vi.fn().mockResolvedValue({ id: 'rec-1' }); +const changeStatusMock = vi.fn().mockResolvedValue({ id: 'rec-1' }); +const catalogStub = { + patchMetadata: patchMetadataMock, + changeStatus: changeStatusMock, +} as unknown as CatalogCall; + +describe('RecordManager', function () { + let manager: RecordManager; + + beforeEach(async function () { + manager = new RecordManager(await jsLogger({ enabled: false }), ingestionValidatorStub, metadataValidatorStub, jobnikStub, catalogStub); + }); + + describe('createIngestion', function () { + it('should return a job response with a jobId and status', async function () { + const payload: IngestionPayload = { + modelPath: '/shared/models/afula', + tilesetFilename: 'tileset.json', + metadata: { productName: 'afula' }, + }; + + const result = await manager.createIngestion(payload); + + expect(result.jobId).toBeTypeOf('string'); + expect(result.status).toBeTypeOf('string'); + }); + }); + + describe('deleteRecord', function () { + it('should create the delete job and set the catalog status to BEING_DELETED', async function () { + const result = await manager.deleteRecord('rec-1'); + + expect(result.jobId).toBeTypeOf('string'); + expect(result.status).toBeTypeOf('string'); + expect(changeStatusMock).toHaveBeenCalledWith('rec-1', { productStatus: 'BEING_DELETED' }); + }); + }); + + describe('updateMetadata', function () { + it('should validate, guard extractable, patch the catalog and return an ack', async function () { + const result = await manager.updateMetadata('rec-1', { description: 'x' }); + + expect(patchMetadataMock).toHaveBeenCalledWith('rec-1', expect.objectContaining({ description: 'x' })); + expect(result.message).toContain('rec-1'); + }); + }); + + describe('updateStatus', function () { + it('should validate, guard extractable, change the catalog status and return an ack', async function () { + const result = await manager.updateStatus('rec-1', { status: 'PUBLISHED' }); + + expect(changeStatusMock).toHaveBeenCalledWith('rec-1', { productStatus: 'PUBLISHED' }); + expect(result.message).toContain('PUBLISHED'); + }); + }); +}); diff --git a/tests/unit/resourceName/models/resourceNameModel.spec.ts b/tests/unit/resourceName/models/resourceNameModel.spec.ts deleted file mode 100644 index 61d9bbe..0000000 --- a/tests/unit/resourceName/models/resourceNameModel.spec.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { jsLogger } from '@map-colonies/js-logger'; -import { describe, beforeEach, it, expect } from 'vitest'; -import { ResourceNameManager } from '@src/resourceName/models/resourceNameManager'; - -let resourceNameManager: ResourceNameManager; - -describe('ResourceNameManager', () => { - beforeEach(async function () { - resourceNameManager = new ResourceNameManager(await jsLogger({ enabled: false })); - }); - - describe('#getResource', () => { - it('should return the resource of id 1', function () { - // action - const resource = resourceNameManager.getResource(); - - // expectation - expect(resource.id).toBe(1); - expect(resource.name).toBe('ronin'); - expect(resource.description).toBe('can you do a logistics run?'); - }); - }); - - describe('#createResource', () => { - it('should return the resource of id 1', function () { - // action - const resource = resourceNameManager.createResource({ description: 'meow', id: 1, name: 'cat' }); - - // expectation - expect(resource.id).toBeLessThanOrEqual(100); - expect(resource.id).toBeGreaterThanOrEqual(0); - expect(resource).toHaveProperty('name', 'cat'); - expect(resource).toHaveProperty('description', 'meow'); - }); - }); -}); diff --git a/tests/unit/tileset/tilesetReader.spec.ts b/tests/unit/tileset/tilesetReader.spec.ts new file mode 100644 index 0000000..42cb6fb --- /dev/null +++ b/tests/unit/tileset/tilesetReader.spec.ts @@ -0,0 +1,77 @@ +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { writeFile, mkdtemp, rm } from 'node:fs/promises'; +import { describe, it, expect, beforeEach } from 'vitest'; +import { jsLogger } from '@map-colonies/js-logger'; +import { + TilesetReader, + ERROR_TILESET_NOT_FIRST_ENTRY, + ERROR_TILESET_READ_FAILED, + ERROR_TILESET_UNSUPPORTED_ENCODING, +} from '@src/tileset/tilesetReader'; +import { AppError } from '@src/common/appError'; + +const fixtures = join(__dirname, '../../helpers/tilesets'); + +const buildLocalHeader = (name: string, flags: number): Buffer => { + const nameBuffer = Buffer.from(name, 'utf-8'); + const header = Buffer.alloc(30 + nameBuffer.length); + header.writeUInt32LE(0x04034b50, 0); + header.writeUInt16LE(flags, 6); + header.writeUInt16LE(0, 8); + header.writeUInt32LE(0, 18); + header.writeUInt16LE(nameBuffer.length, 26); + header.writeUInt16LE(0, 28); + nameBuffer.copy(header, 30); + return header; +}; + +describe('TilesetReader', function () { + let reader: TilesetReader; + + beforeEach(async function () { + reader = new TilesetReader(await jsLogger({ enabled: false })); + }); + + it('should read tileset.json from the first entry of a 3tz archive.', async function () { + const content = await reader.readTilesetJson(join(fixtures, 'region.3tz'), 'tileset.json'); + const parsed = JSON.parse(content) as { root: { boundingVolume: { region?: number[] } } }; + + expect(parsed.root.boundingVolume.region).toBeDefined(); + }); + + it('should read tileset.json from a folder input.', async function () { + const content = await reader.readTilesetJson(join(fixtures, 'folder'), 'tileset.json'); + const parsed = JSON.parse(content) as { root: { boundingVolume: { region?: number[] } } }; + + expect(parsed.root.boundingVolume.region).toBeDefined(); + }); + + it('should throw a 400 when the requested entry is not the first entry of the archive.', async function () { + await expect(reader.readTilesetJson(join(fixtures, 'region.3tz'), 'other.json')).rejects.toThrow(ERROR_TILESET_NOT_FIRST_ENTRY); + }); + + it('should throw a 400 when the archive entry uses a data descriptor (size absent from local header).', async function () { + const dir = await mkdtemp(join(tmpdir(), 'tileset-reader-')); + const archive = join(dir, 'descriptor.3tz'); + await writeFile(archive, buildLocalHeader('tileset.json', 0x08)); + + try { + await expect(reader.readTilesetJson(archive, 'tileset.json')).rejects.toThrow(ERROR_TILESET_UNSUPPORTED_ENCODING); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); + + it('should throw when the folder tileset file is missing.', async function () { + let thrown: unknown; + try { + await reader.readTilesetJson(join(fixtures, 'folder'), 'missing.json'); + } catch (err) { + thrown = err; + } + + expect(thrown).toBeInstanceOf(AppError); + expect((thrown as AppError).message).toBe(ERROR_TILESET_READ_FAILED); + }); +}); diff --git a/tests/unit/validator/ingestionValidator.spec.ts b/tests/unit/validator/ingestionValidator.spec.ts new file mode 100644 index 0000000..2c4da59 --- /dev/null +++ b/tests/unit/validator/ingestionValidator.spec.ts @@ -0,0 +1,224 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { jsLogger } from '@map-colonies/js-logger'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { StatusCodes } from 'http-status-codes'; +import { IngestionValidator } from '@src/validator/ingestionValidator'; +import { TilesetValidator } from '@src/validator/tilesetValidator'; +import { + ERROR_METADATA_DATE, + ERROR_METADATA_FOOTPRINT, + ERROR_METADATA_MISSING_DATE, + ERROR_METADATA_INVALID_DATE, + ERROR_FILE_NOT_FOUND, + ERROR_PRODUCT_NAME_IN_FLIGHT, + ERROR_FOOTPRINT_FAR_FROM_MODEL, + ERROR_MODEL_PATH_INVALID, +} from '@src/validator/errors'; +import { AppError } from '@src/common/appError'; +import type { ConfigType } from '@src/common/config'; +import type { LookupTablesCall } from '@src/externalServices/lookupTables/lookupTablesCall'; +import type { CatalogCall } from '@src/externalServices/catalog/catalogCall'; +import type { JobnikClient } from '@src/externalServices/jobnik/jobnikClient'; +import type { TilesetReader } from '@src/tileset/tilesetReader'; +import type { Provider } from '@src/providers/interfaces'; +import type { IngestionPayload } from '@src/common/interfaces'; +import { buildValidMetadata } from '@tests/helpers/metadata'; + +const regionTilesetJson = readFileSync(join(__dirname, '../../helpers/tilesets/folder/tileset.json'), 'utf-8'); +const boxTilesetJson = JSON.stringify({ root: { boundingVolume: { box: [0, 0, 0, 100, 0, 0, 0, 100, 0, 0, 0, 100] } } }); + +const lookupStub = { getClassifications: vi.fn().mockResolvedValue(['abc123']) } as unknown as LookupTablesCall; +const configStub = { + get: vi.fn((key: string) => (key === 'validation.basePath' ? '/shared/models' : 10)), +} as unknown as ConfigType; + +const ingest = (metadata: Record): IngestionPayload => ({ + modelPath: '/shared/models/afula', + tilesetFilename: 'tileset.json', + metadata, +}); + +describe('IngestionValidator', function () { + let validator: IngestionValidator; + let catalogStub: CatalogCall; + let jobnikStub: JobnikClient; + let tilesetReaderStub: TilesetReader; + let providerStub: Provider; + + beforeEach(async function () { + catalogStub = { + findRecords: vi.fn().mockResolvedValue([]), + } as unknown as CatalogCall; + jobnikStub = { hasInFlightIngestionJob: vi.fn().mockResolvedValue(false) } as unknown as JobnikClient; + tilesetReaderStub = { readTilesetJson: vi.fn().mockResolvedValue(regionTilesetJson) } as unknown as TilesetReader; + providerStub = { fileExists: vi.fn().mockResolvedValue(true) }; + const logger = await jsLogger({ enabled: false }); + const tilesetValidator = new TilesetValidator(configStub, logger, tilesetReaderStub); + validator = new IngestionValidator(configStub, logger, lookupStub, catalogStub, jobnikStub, providerStub, tilesetValidator); + }); + + it('should pass a fully valid ingestion payload', async function () { + await expect(validator.validateIngestion(ingest(buildValidMetadata()))).resolves.toBeUndefined(); + }); + + it('should throw 400 when a required core field is missing', async function () { + const metadata = buildValidMetadata(); + delete metadata.productName; + + await expect(validator.validateIngestion(ingest(metadata))).rejects.toThrow(AppError); + }); + + it('should throw 400 for an invalid productType', async function () { + const metadata = { ...buildValidMetadata(), productType: 'NOT_A_3D_TYPE' }; + + let thrown: unknown; + try { + await validator.validateIngestion(ingest(metadata)); + } catch (err) { + thrown = err; + } + + expect(thrown).toBeInstanceOf(AppError); + expect((thrown as AppError).status).toBe(StatusCodes.BAD_REQUEST); + }); + + it('should throw the footprint error for a malformed geometry', async function () { + const metadata = { ...buildValidMetadata(), footprint: { type: 'Point', coordinates: [34.45, 31.48] } }; + + await expect(validator.validateIngestion(ingest(metadata))).rejects.toThrow(ERROR_METADATA_FOOTPRINT); + }); + + it('should throw the date error when imagingTimeBeginUTC is after imagingTimeEndUTC', async function () { + const metadata = { ...buildValidMetadata(), imagingTimeBeginUTC: '2025-07-11T00:00:00.000Z' }; + + await expect(validator.validateIngestion(ingest(metadata))).rejects.toThrow(ERROR_METADATA_DATE); + }); + + it('should throw the missing-date error when a source date is absent', async function () { + const metadata = buildValidMetadata(); + delete metadata.imagingTimeEndUTC; + + await expect(validator.validateIngestion(ingest(metadata))).rejects.toThrow(ERROR_METADATA_MISSING_DATE); + }); + + it('should throw the invalid-date error for an unparseable date', async function () { + const metadata = { ...buildValidMetadata(), imagingTimeBeginUTC: 'not-a-date' }; + + await expect(validator.validateIngestion(ingest(metadata))).rejects.toThrow(ERROR_METADATA_INVALID_DATE); + }); + + it('should throw 400 when the classification is not in the lookup table', async function () { + const metadata = { ...buildValidMetadata(), classification: 'not-a-real-classification' }; + + await expect(validator.validateIngestion(ingest(metadata))).rejects.toThrow(AppError); + }); + + it('should throw 400 when the product name already exists in the catalog', async function () { + (catalogStub.findRecords as ReturnType).mockResolvedValueOnce([{ id: 'existing', productName: 'afula' }]); + + await expect(validator.validateIngestion(ingest(buildValidMetadata()))).rejects.toThrow(AppError); + }); + + it('should throw 409 when the product name has an in-flight ingestion job', async function () { + (jobnikStub.hasInFlightIngestionJob as ReturnType).mockResolvedValueOnce(true); + + let thrown: unknown; + try { + await validator.validateIngestion(ingest(buildValidMetadata())); + } catch (err) { + thrown = err; + } + + expect(thrown).toBeInstanceOf(AppError); + expect((thrown as AppError).status).toBe(StatusCodes.CONFLICT); + expect((thrown as AppError).message).toBe(ERROR_PRODUCT_NAME_IN_FLIGHT); + }); + + it('should throw 400 when the model files do not exist', async function () { + (providerStub.fileExists as ReturnType).mockResolvedValueOnce(false); + + await expect(validator.validateIngestion(ingest(buildValidMetadata()))).rejects.toThrow(ERROR_FILE_NOT_FOUND); + }); + + it('should throw 400 when the model path is not under the agreed base path', async function () { + const payload = { ...ingest(buildValidMetadata()), modelPath: '/etc/passwd' }; + + await expect(validator.validateIngestion(payload)).rejects.toThrow(ERROR_MODEL_PATH_INVALID); + }); + + it('should throw 409 when the productId already exists in the catalog', async function () { + (catalogStub.findRecords as ReturnType).mockImplementation((query: { productId?: string; productName?: string }) => + query.productId !== undefined ? [{ id: 'existing', productId: query.productId }] : [] + ); + + let thrown: unknown; + try { + await validator.validateIngestion(ingest(buildValidMetadata())); + } catch (err) { + thrown = err; + } + + expect(thrown).toBeInstanceOf(AppError); + expect((thrown as AppError).status).toBe(StatusCodes.CONFLICT); + }); + + it('should throw 400 when the footprint does not intersect the tileset model', async function () { + const metadata = buildValidMetadata(); + metadata.footprint = { + type: 'Polygon', + coordinates: [ + [ + [10, 10], + [10.01, 10], + [10.01, 10.01], + [10, 10.01], + [10, 10], + ], + ], + }; + + await expect(validator.validateIngestion(ingest(metadata))).rejects.toThrow(ERROR_FOOTPRINT_FAR_FROM_MODEL); + }); + + it('should throw 400 when the footprint coverage of the model is below the threshold', async function () { + const metadata = buildValidMetadata(); + metadata.footprint = { + type: 'Polygon', + coordinates: [ + [ + [35.17, 32.9], + [35.172, 32.9], + [35.172, 32.902], + [35.17, 32.902], + [35.17, 32.9], + ], + ], + }; + + let thrown: unknown; + try { + await validator.validateIngestion(ingest(metadata)); + } catch (err) { + thrown = err; + } + + expect(thrown).toBeInstanceOf(AppError); + expect((thrown as AppError).status).toBe(StatusCodes.BAD_REQUEST); + expect((thrown as AppError).message).toMatch(/minimum required threshhold/); + }); + + it('should throw 400 when the tileset bounding volume is an unsupported box', async function () { + (tilesetReaderStub.readTilesetJson as ReturnType).mockResolvedValueOnce(boxTilesetJson); + + let thrown: unknown; + try { + await validator.validateIngestion(ingest(buildValidMetadata())); + } catch (err) { + thrown = err; + } + + expect(thrown).toBeInstanceOf(AppError); + expect((thrown as AppError).status).toBe(StatusCodes.BAD_REQUEST); + }); +}); diff --git a/tests/unit/validator/metadataValidator.spec.ts b/tests/unit/validator/metadataValidator.spec.ts new file mode 100644 index 0000000..27d441f --- /dev/null +++ b/tests/unit/validator/metadataValidator.spec.ts @@ -0,0 +1,126 @@ +import { jsLogger } from '@map-colonies/js-logger'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { StatusCodes } from 'http-status-codes'; +import { MetadataValidator } from '@src/validator/metadataValidator'; +import { + ERROR_DELETE_RECORD_NOT_FOUND, + ERROR_DELETE_PRODUCT_TYPE, + ERROR_DELETE_STATUS, + ERROR_RECORD_NOT_FOUND, + ERROR_RECORD_BEING_DELETED, + ERROR_EXTRACTABLE_CONFLICT, +} from '@src/validator/errors'; +import { AppError } from '@src/common/appError'; +import type { LookupTablesCall } from '@src/externalServices/lookupTables/lookupTablesCall'; +import type { CatalogCall } from '@src/externalServices/catalog/catalogCall'; +import type { ExtractableCall } from '@src/externalServices/extractableManagement/extractableCall'; + +const lookupStub = { getClassifications: vi.fn().mockResolvedValue(['abc123']) } as unknown as LookupTablesCall; + +describe('MetadataValidator', function () { + let validator: MetadataValidator; + let catalogStub: CatalogCall; + let extractableStub: ExtractableCall; + + beforeEach(async function () { + catalogStub = { + findRecords: vi.fn().mockResolvedValue([]), + getRecord: vi.fn().mockResolvedValue({ id: 'rec-1', productName: 'afula' }), + } as unknown as CatalogCall; + extractableStub = { isExtractableRecordExists: vi.fn().mockResolvedValue(false) } as unknown as ExtractableCall; + validator = new MetadataValidator(await jsLogger({ enabled: false }), lookupStub, catalogStub, extractableStub); + }); + + describe('validateUpdate', function () { + it('should return the record for a valid update payload', async function () { + await expect(validator.validateUpdate('rec-1', { description: 'x' })).resolves.toEqual({ id: 'rec-1', productName: 'afula' }); + }); + + it('should throw 404 when the record does not exist', async function () { + (catalogStub.getRecord as ReturnType).mockResolvedValueOnce(undefined); + + let thrown: unknown; + try { + await validator.validateUpdate('missing', { description: 'x' }); + } catch (err) { + thrown = err; + } + + expect(thrown).toBeInstanceOf(AppError); + expect((thrown as AppError).status).toBe(StatusCodes.NOT_FOUND); + expect((thrown as AppError).message).toBe(ERROR_RECORD_NOT_FOUND); + }); + + it('should throw 400 when the record is being deleted', async function () { + (catalogStub.getRecord as ReturnType).mockResolvedValueOnce({ id: 'rec-1', productStatus: 'BEING_DELETED' }); + + await expect(validator.validateUpdate('rec-1', { description: 'x' })).rejects.toThrow(ERROR_RECORD_BEING_DELETED); + }); + + it('should throw 400 when a renamed product name collides with another record', async function () { + (catalogStub.findRecords as ReturnType).mockResolvedValueOnce([{ id: 'other', productName: 'haifa' }]); + + await expect(validator.validateUpdate('rec-1', { productName: 'haifa' })).rejects.toThrow(AppError); + }); + }); + + describe('validateStatusChange', function () { + it('should throw 404 when the record does not exist', async function () { + (catalogStub.getRecord as ReturnType).mockResolvedValueOnce(undefined); + + await expect(validator.validateStatusChange('missing')).rejects.toThrow(ERROR_RECORD_NOT_FOUND); + }); + }); + + describe('ensureRecordAbsentFromExtractable', function () { + it('should throw 409 when the record exists in extractable-management', async function () { + (extractableStub.isExtractableRecordExists as ReturnType).mockResolvedValueOnce(true); + + let thrown: unknown; + try { + await validator.ensureRecordAbsentFromExtractable({ id: 'rec-1', productName: 'afula' }); + } catch (err) { + thrown = err; + } + + expect(thrown).toBeInstanceOf(AppError); + expect((thrown as AppError).status).toBe(StatusCodes.CONFLICT); + expect((thrown as AppError).message).toBe(ERROR_EXTRACTABLE_CONFLICT); + }); + + it('should pass when the record is absent from extractable-management', async function () { + await expect(validator.ensureRecordAbsentFromExtractable({ id: 'rec-1', productName: 'afula' })).resolves.toBeUndefined(); + }); + }); + + describe('validateDelete', function () { + it('should return the record when it is deletable', async function () { + const record = { id: 'rec-1', productType: '3DPhotoRealistic', productStatus: 'UNPUBLISHED' }; + (catalogStub.findRecords as ReturnType).mockResolvedValueOnce([record]); + + await expect(validator.validateDelete('rec-1')).resolves.toEqual(record); + }); + + it('should throw when the record is not found', async function () { + (catalogStub.findRecords as ReturnType).mockResolvedValueOnce([]); + + await expect(validator.validateDelete('missing')).rejects.toThrow(ERROR_DELETE_RECORD_NOT_FOUND); + }); + + it('should throw when the productType is blocked', async function () { + (catalogStub.findRecords as ReturnType).mockResolvedValueOnce([ + { id: 'rec-1', productType: 'QuantizedMeshDTMBest', productStatus: 'UNPUBLISHED' }, + ]); + + await expect(validator.validateDelete('rec-1')).rejects.toThrow(ERROR_DELETE_PRODUCT_TYPE); + }); + + it('should throw when the record is not unpublished', async function () { + (catalogStub.findRecords as ReturnType).mockResolvedValueOnce([ + { id: 'rec-1', productType: '3DPhotoRealistic', productStatus: 'PUBLISHED' }, + ]); + + await expect(validator.validateDelete('rec-1')).rejects.toThrow(ERROR_DELETE_STATUS); + }); + }); +});