diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 00000000..8b30f3bf --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "dev", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "dev"], + "port": 5173 + } + ] +} diff --git a/.env.development.example b/.env.development.example index dd40bd54..c6af9753 100644 --- a/.env.development.example +++ b/.env.development.example @@ -1,9 +1,7 @@ VITE_APP_TITLE="Ocotillo (Dev)" VITE_NMBGMR_AMP_API_URL= -VITE_NMBGMR_GEOTHERMAL_API_URL= VITE_OCOTILLO_API_URL= VITE_REFINE_PROJECT_ID= -VITE_MAPBOX_TOKEN= VITE_POSTHOG_KEY= VITE_POSTHOG_HOST=https://us.i.posthog.com VITE_APP_TIMEZONE=America/Denver @@ -12,3 +10,11 @@ VITE_AUTHENTIK_CLIENT_ID= VITE_AUTHENTIK_URL= VITE_AUTHENTIK_REDIRECT_URI= VITE_TEST_AUTH=true + +# Wellntel analytics API (direct access, ported from wellpy) +VITE_WELLNTEL_API_URL= +VITE_WELLNTEL_API_KEY= + +# Diver-HUB (VanEssen GroundwaterOnline) API +VITE_DIVERHUB_API_URL= +VITE_DIVERHUB_API_KEY= diff --git a/.env.devserver.example b/.env.devserver.example index a3e3abbb..45cb722b 100644 --- a/.env.devserver.example +++ b/.env.devserver.example @@ -1,10 +1,8 @@ VITE_APP_TITLE="Ocotillo" VITE_NMBGMR_AMP_API_URL= -VITE_NMBGMR_GEOTHERMAL_API_URL= VITE_OCOTILLO_API_URL= VITE_API_URL= VITE_REFINE_PROJECT_ID= -VITE_MAPBOX_TOKEN= VITE_POSTHOG_KEY= VITE_POSTHOG_HOST=https://us.i.posthog.com VITE_APP_ENV=development @@ -14,3 +12,11 @@ VITE_APP_TIMEZONE=America/Denver VITE_AUTHENTIK_CLIENT_ID= VITE_AUTHENTIK_URL= VITE_AUTHENTIK_REDIRECT_URI= + +# Wellntel analytics API (direct access, ported from wellpy) +VITE_WELLNTEL_API_URL= +VITE_WELLNTEL_API_KEY= + +# Diver-HUB (VanEssen GroundwaterOnline) API +VITE_DIVERHUB_API_URL= +VITE_DIVERHUB_API_KEY= diff --git a/.env.production.example b/.env.production.example index 361965e0..d31dc096 100644 --- a/.env.production.example +++ b/.env.production.example @@ -1,10 +1,8 @@ VITE_APP_TITLE="Ocotillo" VITE_NMBGMR_AMP_API_URL= -VITE_NMBGMR_GEOTHERMAL_API_URL= VITE_OCOTILLO_API_URL= VITE_API_URL= VITE_REFINE_PROJECT_ID= -VITE_MAPBOX_TOKEN= VITE_POSTHOG_KEY= VITE_POSTHOG_HOST=https://us.i.posthog.com VITE_APP_ENV=production @@ -16,3 +14,11 @@ VITE_APP_TIMEZONE=America/Denver VITE_AUTHENTIK_CLIENT_ID= VITE_AUTHENTIK_URL= VITE_AUTHENTIK_REDIRECT_URI= + +# Wellntel analytics API (direct access, ported from wellpy) +VITE_WELLNTEL_API_URL= +VITE_WELLNTEL_API_KEY= + +# Diver-HUB (VanEssen GroundwaterOnline) API +VITE_DIVERHUB_API_URL= +VITE_DIVERHUB_API_KEY= diff --git a/.github/preview/api-service.tmpl.yaml b/.github/preview/api-service.tmpl.yaml new file mode 100644 index 00000000..dd301250 --- /dev/null +++ b/.github/preview/api-service.tmpl.yaml @@ -0,0 +1,168 @@ +# Cloud Run service spec for an ephemeral preview backend. +# +# This is the docker-compose stack the Cypress job runs (OcotilloAPI "app" + +# postgis "db") re-expressed as a single multi-container Cloud Run service. +# Sidecars share a network namespace, so "db" becomes 127.0.0.1:5432. +# +# Rendered by _preview_deploy.yml with envsubst and an explicit variable +# allowlist -- every other $VAR below is meant to be evaluated at runtime by the +# container shell, not at render time. Render-time placeholders all carry a +# TPL_ prefix precisely so they cannot collide with a runtime variable: +# envsubst substitutes $VAR as readily as ${VAR}, so an allowlisted name that +# also appears in the startup script would get frozen at render time. +# +# The database is an in-memory tmpfs. Everything in it is lost when the instance +# restarts. That is the point: these previews are disposable. +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: ${TPL_API_SERVICE_NAME} + labels: + preview: "true" + preview-branch: ${TPL_SANITIZED_BRANCH} + preview-role: api + preview-expires: "${TPL_EXPIRES_AT}" + annotations: + run.googleapis.com/ingress: all +spec: + template: + metadata: + annotations: + # One always-warm instance: the database lives in that instance's + # memory, so scaling to zero or out to two would lose or fork the data. + autoscaling.knative.dev/minScale: "1" + autoscaling.knative.dev/maxScale: "1" + # Postgres needs CPU between requests to run its background workers. + run.googleapis.com/cpu-throttling: "false" + run.googleapis.com/execution-environment: gen2 + run.googleapis.com/container-dependencies: '{"api":["db"]}' + spec: + containerConcurrency: 40 + timeoutSeconds: 300 + containers: + - name: db + image: postgis/postgis:17-3.5 + env: + - name: POSTGRES_USER + value: postgres + - name: POSTGRES_PASSWORD + value: postgres + - name: POSTGRES_DB + value: ocotillo_preview + # PGDATA has to be a subdirectory of the mount: the postgis + # entrypoint refuses to initialise into a non-empty directory, and + # a fresh mount point is not guaranteed to look empty to it. + - name: PGDATA + value: /var/lib/postgresql/data/pgdata + volumeMounts: + - name: pgdata + mountPath: /var/lib/postgresql/data + resources: + limits: + cpu: "1" + memory: 2Gi + startupProbe: + tcpSocket: + port: 5432 + periodSeconds: 5 + # Cloud Run rejects a probe whose timeout exceeds its period. + timeoutSeconds: 3 + failureThreshold: 60 + + - name: api + image: ${TPL_API_IMAGE} + ports: + - name: http1 + containerPort: 8000 + env: + - name: MODE + value: development + - name: POSTGRES_HOST + value: 127.0.0.1 + - name: POSTGRES_PORT + value: "5432" + - name: POSTGRES_USER + value: postgres + - name: POSTGRES_PASSWORD + value: postgres + - name: POSTGRES_DB + value: ocotillo_preview + - name: AUTHENTIK_DISABLE_AUTHENTICATION + value: "${TPL_DISABLE_AUTH}" + - name: AUTHENTIK_URL + value: https://authentik.newmexicowaterdata.org/application/o/ocotillo/ + - name: AUTHENTIK_AUTHORIZE_URL + value: https://authentik.newmexicowaterdata.org/application/o/authorize/ + - name: AUTHENTIK_TOKEN_URL + value: https://authentik.newmexicowaterdata.org/application/o/token/ + - name: AUTHENTIK_CLIENT_ID + value: "${TPL_AUTHENTIK_CLIENT_ID}" + # create_api_app() mounts pygeoapi unconditionally -- the + # MOUNT_PYGEOAPI_IN_API flag in OcotilloAPI's app.yaml reads like an + # opt-out but no Python reads it. Of these, only the password is + # actually required: host/port/db/user each fall back to their + # POSTGRES_* equivalent, while the password is checked for None with + # no fallback and aborts boot. Set the full block anyway, to match + # docker-compose.yml and to keep the next reader out of pygeoapi.py. + - name: PYGEOAPI_POSTGRES_HOST + value: 127.0.0.1 + - name: PYGEOAPI_POSTGRES_PORT + value: "5432" + - name: PYGEOAPI_POSTGRES_DB + value: ocotillo_preview + - name: PYGEOAPI_POSTGRES_USER + value: postgres + - name: PYGEOAPI_POSTGRES_PASSWORD + value: postgres + - name: APP_VERSION + value: "${TPL_API_SOURCE_SHA}" + - name: RUN_SEED + value: "${TPL_RUN_SEED}" + # Replaces entrypoint.sh so the seed can run between the migration and + # uvicorn. Cloud Run has no exec, so this is the only hook available. + command: + - sh + - -c + args: + - | + set -e + + until PGPASSWORD="$POSTGRES_PASSWORD" pg_isready \ + -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" \ + -U "$POSTGRES_USER" -d "$POSTGRES_DB"; do + echo "Waiting for postgres at $POSTGRES_HOST:$POSTGRES_PORT..." + sleep 2 + done + + echo "Applying migrations..." + alembic upgrade head + + if [ "$RUN_SEED" = "true" ]; then + echo "Seeding preview database..." + # A seed failure leaves an empty-but-working API, which is more + # useful for a preview than no API at all. + python -m transfers.seed || echo "WARNING: seed failed; continuing with an empty database" + fi + + echo "Starting the application..." + exec uvicorn main:app --host 0.0.0.0 --port 8000 + # A preview serves a handful of reviewers, and the expensive part of + # startup (migrate + seed) measured in seconds, not minutes. Sized to + # keep the always-warm instance cheap rather than fast. + resources: + limits: + cpu: "1" + memory: 1536Mi + startupProbe: + tcpSocket: + port: 8000 + periodSeconds: 10 + timeoutSeconds: 5 + # Migration + seed can take several minutes on a cold instance. + failureThreshold: 90 + + volumes: + - name: pgdata + emptyDir: + medium: Memory + sizeLimit: 1Gi diff --git a/.github/workflows/CD_preview.yml b/.github/workflows/CD_preview.yml index 977c66f0..2e7cff06 100644 --- a/.github/workflows/CD_preview.yml +++ b/.github/workflows/CD_preview.yml @@ -1,3 +1,9 @@ +# PR-triggered preview. The build/deploy/teardown mechanics live in the two +# reusable workflows so this file and CD_preview_ondemand.yml cannot drift. +# +# By default a PR preview talks to the staging API. Add the "preview-backend" +# label to the PR to get a throwaway API + database instead -- use that when the +# branch depends on unreleased OcotilloAPI changes. name: Preview deploy to Cloud Run on: @@ -7,210 +13,47 @@ on: - production jobs: - deploy-preview: + deploy: + if: github.event.action != 'closed' + uses: ./.github/workflows/_preview_deploy.yml + secrets: inherit + with: + branch: ${{ github.head_ref }} + # Build the merge result, not the head commit, which is what + # actions/checkout does by default on pull_request. + checkout_ref: refs/pull/${{ github.event.pull_request.number }}/merge + backend: ${{ contains(github.event.pull_request.labels.*.name, 'preview-backend') && 'ephemeral' || 'staging' }} + # No TTL: the teardown job below owns the lifetime of a PR preview, so the + # nightly sweep must not delete one out from under a long-lived PR. + ttl_hours: 0 + + comment: + needs: deploy + if: github.event.action != 'closed' runs-on: ubuntu-latest - environment: staging - - env: - GCP_PROJECT_ID: waterdatainitiative-271000 - GCP_REGION: us-central1 - AUTHENTIK_BASE_URL: https://authentik.newmexicowaterdata.org - AUTHENTIK_PROVIDER_ID: ${{ secrets.AUTHENTIK_PROVIDER_ID }} - AUTHENTIK_API_TOKEN: ${{ secrets.AUTHENTIK_API_TOKEN }} - + permissions: + pull-requests: write steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Compute preview service name - env: - BRANCH_NAME: ${{ github.head_ref || github.ref_name }} - run: | - set -euo pipefail - - SANITIZED=$(echo "$BRANCH_NAME" \ - | sed 's/[^a-zA-Z0-9-]/-/g' \ - | tr '[:upper:]' '[:lower:]' \ - | cut -c1-40) - - SERVICE_NAME="preview-$SANITIZED" - - echo "SANITIZED_BRANCH=$SANITIZED" >> "$GITHUB_ENV" - echo "SERVICE_NAME=$SERVICE_NAME" >> "$GITHUB_ENV" - - echo "Computed service: $SERVICE_NAME" - - - name: Setup Node - if: github.event.action != 'closed' - uses: actions/setup-node@v4 - with: - node-version: 22.x - cache: npm - cache-dependency-path: package-lock.json - - - name: Authenticate to Google Cloud - uses: google-github-actions/auth@v2 - with: - credentials_json: ${{ secrets.GCP_SA_KEY }} - - - name: Configure Docker for Artifact Registry - if: github.event.action != 'closed' - run: | - gcloud config set project "$GCP_PROJECT_ID" - gcloud auth configure-docker us-central1-docker.pkg.dev --quiet - - - name: Load secrets - if: github.event.action != 'closed' - run: | - set -euo pipefail - echo "VITE_PUBLIC_POSTHOG_KEY=$(gcloud secrets versions access latest --secret=VITE_PUBLIC_POSTHOG_KEY --project=$GCP_PROJECT_ID)" >> $GITHUB_ENV - echo "VITE_PUBLIC_POSTHOG_HOST=$(gcloud secrets versions access latest --secret=VITE_PUBLIC_POSTHOG_HOST --project=$GCP_PROJECT_ID)" >> $GITHUB_ENV - - - name: Build and push - if: github.event.action != 'closed' - env: - IMAGE_TAG: ${{ github.sha }} - NODE_OPTIONS: --max-old-space-size=6144 - VITE_POSTHOG_KEY: ${{ env.VITE_PUBLIC_POSTHOG_KEY }} - VITE_POSTHOG_HOST: ${{ env.VITE_PUBLIC_POSTHOG_HOST }} - VITE_APP_ENV: preview - VITE_APP_VERSION: ${{ github.sha }} - run: | - set -euo pipefail - - npm ci - npx tsc - npx vite build --mode development - - docker build \ - --build-arg MODE=development \ - --build-arg VITE_APP_TITLE="Ocotillo Preview" \ - --build-arg VITE_BASE_URL="/" \ - --build-arg VITE_AUTHENTIK_CLIENT_ID=${{ vars.VITE_AUTHENTIK_CLIENT_ID }} \ - --build-arg VITE_AUTHENTIK_URL=${{ vars.VITE_AUTHENTIK_URL }} \ - --build-arg VITE_OCOTILLO_API_URL=${{ vars.VITE_OCOTILLO_API_URL }} \ - --build-arg VITE_MAPBOX_TOKEN=${{ secrets.VITE_MAPBOX_TOKEN }} \ - --build-arg VITE_PUBLIC_POSTHOG_KEY=${{ env.VITE_PUBLIC_POSTHOG_KEY }} \ - --build-arg VITE_PUBLIC_POSTHOG_HOST=${{ env.VITE_PUBLIC_POSTHOG_HOST }} \ - --build-arg VITE_APP_ENV=preview \ - --build-arg VITE_APP_VERSION=${{ github.sha }} \ - -t "us-central1-docker.pkg.dev/$GCP_PROJECT_ID/ocotillo-previews/${SERVICE_NAME}:$IMAGE_TAG" . - - docker push "us-central1-docker.pkg.dev/$GCP_PROJECT_ID/ocotillo-previews/${SERVICE_NAME}:$IMAGE_TAG" - - - name: Deploy to Cloud Run - if: github.event.action != 'closed' - env: - IMAGE_TAG: ${{ github.sha }} - run: | - set -euo pipefail - - gcloud run deploy "${SERVICE_NAME}" \ - --image "us-central1-docker.pkg.dev/$GCP_PROJECT_ID/ocotillo-previews/${SERVICE_NAME}:$IMAGE_TAG" \ - --platform managed \ - --region "$GCP_REGION" \ - --allow-unauthenticated \ - --port 8080 \ - --memory 512Mi \ - --cpu 1 - - - name: Get Cloud Run URL - if: github.event.action != 'closed' - id: cloudrun-url - run: | - set -euo pipefail - - URL=$(gcloud run services describe "${SERVICE_NAME}" \ - --platform managed \ - --region "$GCP_REGION" \ - --format 'value(status.url)') - - echo "Cloud Run URL: $URL" - echo "url=$URL" >> "$GITHUB_OUTPUT" - - - name: Add exact preview origin to authentik provider - if: github.event.action != 'closed' - env: - PREVIEW_ORIGIN: ${{ steps.cloudrun-url.outputs.url }} - run: | - set -euo pipefail - - echo "Ensuring authentik redirect/origin exists for: $PREVIEW_ORIGIN" - - tmp="$(mktemp)" - payload="$(mktemp)" - - curl -fsS \ - -H "Authorization: Bearer ${AUTHENTIK_API_TOKEN}" \ - "${AUTHENTIK_BASE_URL}/api/v3/providers/oauth2/${AUTHENTIK_PROVIDER_ID}/" \ - > "$tmp" - - jq --arg origin "$PREVIEW_ORIGIN" ' - .redirect_uris |= ( - . + [{"matching_mode":"strict","url":$origin}] - | unique_by(.matching_mode + "|" + .url) - ) - | {redirect_uris: .redirect_uris} - ' "$tmp" > "$payload" - - curl -fsS -X PATCH \ - -H "Authorization: Bearer ${AUTHENTIK_API_TOKEN}" \ - -H "Content-Type: application/json" \ - --data @"$payload" \ - "${AUTHENTIK_BASE_URL}/api/v3/providers/oauth2/${AUTHENTIK_PROVIDER_ID}/" - - echo "Added/verified strict preview origin in authentik." - - name: Comment PR with preview URL - if: github.event_name == 'pull_request' && github.event.action != 'closed' env: - PREVIEW_URL: ${{ steps.cloudrun-url.outputs.url }} + PREVIEW_URL: ${{ needs.deploy.outputs.preview_url }} + API_URL: ${{ needs.deploy.outputs.api_url }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - gh pr comment ${{ github.event.pull_request.number }} --body "$(cat < "$tmp" - - jq --arg origin "$PREVIEW_ORIGIN" ' - .redirect_uris |= map(select(.url != $origin)) - | {redirect_uris: .redirect_uris} - ' "$tmp" > "$payload" - - curl -fsS -X PATCH \ - -H "Authorization: Bearer ${AUTHENTIK_API_TOKEN}" \ - -H "Content-Type: application/json" \ - --data @"$payload" \ - "${AUTHENTIK_BASE_URL}/api/v3/providers/oauth2/${AUTHENTIK_PROVIDER_ID}/" - - echo "Removed strict preview origin from authentik." - - - name: Delete preview on PR close - if: github.event_name == 'pull_request' && github.event.action == 'closed' - run: | - gcloud run services delete "${SERVICE_NAME}" \ - --platform managed \ - --region "$GCP_REGION" \ - --quiet || true - + teardown: + if: github.event.action == 'closed' + uses: ./.github/workflows/_preview_teardown.yml + secrets: inherit + with: + branch: ${{ github.head_ref }} diff --git a/.github/workflows/CD_preview_ondemand.yml b/.github/workflows/CD_preview_ondemand.yml new file mode 100644 index 00000000..88fe9457 --- /dev/null +++ b/.github/workflows/CD_preview_ondemand.yml @@ -0,0 +1,58 @@ +# On-demand preview deploy (BDMS-1173). Unlike CD_preview.yml this needs no pull +# request: pick any branch in the Actions UI, or +# +# gh workflow run CD_preview_ondemand.yml --ref my-branch -f backend=ephemeral +# +# Note that workflow_dispatch always reads the workflow definition from the +# default branch, but --ref selects the branch that gets built. +name: Preview deploy (on demand) + +on: + workflow_dispatch: + inputs: + backend: + description: Which API the preview talks to + type: choice + default: staging + options: + - staging + - ephemeral + backend_ref: + description: OcotilloAPI ref to build (ephemeral backend only) + type: string + default: staging + backend_auth: + description: >- + Authentik enforcement on the ephemeral API. A seeded preview database + has no permission rows, so "enabled" locks everyone out. + type: choice + default: disabled + options: + - disabled + - enabled + seed: + description: Seed the ephemeral database with fake data + type: boolean + default: true + ttl_hours: + description: Hours before the nightly sweep may tear this preview down + type: number + default: 48 + +# One preview per branch. A second dispatch supersedes an in-flight one rather +# than racing it onto the same Cloud Run service. +concurrency: + group: preview-${{ github.ref_name }} + cancel-in-progress: true + +jobs: + deploy: + uses: ./.github/workflows/_preview_deploy.yml + secrets: inherit + with: + branch: ${{ github.ref_name }} + backend: ${{ inputs.backend }} + backend_ref: ${{ inputs.backend_ref }} + backend_auth: ${{ inputs.backend_auth }} + seed: ${{ inputs.seed }} + ttl_hours: ${{ fromJSON(inputs.ttl_hours) }} diff --git a/.github/workflows/CD_preview_teardown.yml b/.github/workflows/CD_preview_teardown.yml new file mode 100644 index 00000000..80d39bc5 --- /dev/null +++ b/.github/workflows/CD_preview_teardown.yml @@ -0,0 +1,123 @@ +# Preview teardown (BDMS-1173). Three ways in: +# +# 1. Manual -- gh workflow run CD_preview_teardown.yml -f branch=my-branch +# 2. Branch deletion -- deleting a branch removes its preview +# 3. Nightly sweep -- removes previews past their TTL, and previews whose +# branch no longer exists +# +# PR-close teardown lives in CD_preview.yml, which calls the same reusable. +# +# The "delete" and "schedule" triggers only fire from the repository's default +# branch, so those two paths do nothing until this file is merged there. +name: Preview teardown + +on: + workflow_dispatch: + inputs: + branch: + description: Branch whose preview should be torn down + type: string + required: true + delete_images: + description: Also delete the branch's images from Artifact Registry + type: boolean + default: true + delete: + schedule: + # 09:00 UTC == 02:00 or 03:00 Mountain, depending on DST. + - cron: '0 9 * * *' + +jobs: + manual: + if: github.event_name == 'workflow_dispatch' + uses: ./.github/workflows/_preview_teardown.yml + secrets: inherit + with: + branch: ${{ inputs.branch }} + delete_images: ${{ inputs.delete_images }} + + on-branch-delete: + if: github.event_name == 'delete' && github.event.ref_type == 'branch' + uses: ./.github/workflows/_preview_teardown.yml + secrets: inherit + with: + branch: ${{ github.event.ref }} + + discover-expired: + if: github.event_name == 'schedule' + runs-on: ubuntu-latest + environment: staging + outputs: + branches: ${{ steps.scan.outputs.branches }} + steps: + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_SA_KEY }} + + - name: Find previews to sweep + id: scan + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GCP_PROJECT_ID: waterdatainitiative-271000 + GCP_REGION: us-central1 + run: | + set -euo pipefail + + gcloud config set project "$GCP_PROJECT_ID" + + # Live branches, sanitized the same way _preview_deploy.yml names + # services, so an orphan check can compare label to label. + gh api --paginate \ + "repos/${{ github.repository }}/branches?per_page=100" \ + --jq '.[].name' \ + | sed 's/[^a-zA-Z0-9-]/-/g' \ + | tr '[:upper:]' '[:lower:]' \ + | cut -c1-40 \ + | sort -u > /tmp/live-branches.txt + + gcloud run services list \ + --platform managed \ + --region "$GCP_REGION" \ + --filter 'metadata.labels.preview=true AND metadata.labels.preview-role=frontend' \ + --format 'value(metadata.labels.preview-branch,metadata.labels.preview-expires)' \ + > /tmp/previews.txt + + now=$(date -u +%s) + : > /tmp/doomed.txt + + while IFS=$'\t' read -r branch expires; do + [ -n "$branch" ] || continue + + # preview-expires=0 means "no TTL" -- PR previews set it so the + # sweep cannot delete a preview an open PR still points at. + reason="" + if [ -n "$expires" ] && [ "$expires" != "0" ] && [ "$expires" -lt "$now" ] 2>/dev/null; then + reason="TTL expired" + elif ! grep -qxF "$branch" /tmp/live-branches.txt; then + reason="branch no longer exists" + fi + + if [ -n "$reason" ]; then + echo "sweep $branch ($reason)" + echo "$branch" >> /tmp/doomed.txt + else + echo "keep $branch" + fi + done < /tmp/previews.txt + + BRANCHES=$(jq -R -s -c 'split("\n") | map(select(length > 0))' < /tmp/doomed.txt) + echo "branches=$BRANCHES" >> "$GITHUB_OUTPUT" + echo "Sweeping: $BRANCHES" + + sweep: + needs: discover-expired + if: needs.discover-expired.outputs.branches != '[]' + strategy: + fail-fast: false + matrix: + branch: ${{ fromJSON(needs.discover-expired.outputs.branches) }} + uses: ./.github/workflows/_preview_teardown.yml + secrets: inherit + with: + branch: ${{ matrix.branch }} diff --git a/.github/workflows/CD_production.yml b/.github/workflows/CD_production.yml index db12cbec..9362e9c3 100644 --- a/.github/workflows/CD_production.yml +++ b/.github/workflows/CD_production.yml @@ -43,7 +43,6 @@ jobs: - name: Install deps & build env: VITE_APP_TITLE: "Ocotillo" - VITE_MAPBOX_TOKEN: ${{ secrets.VITE_MAPBOX_TOKEN }} VITE_BASE_URL: '/' VITE_AUTHENTIK_CLIENT_ID: ${{ vars.VITE_AUTHENTIK_CLIENT_ID }} VITE_AUTHENTIK_URL: ${{ vars.VITE_AUTHENTIK_URL }} diff --git a/.github/workflows/CD_staging.yml b/.github/workflows/CD_staging.yml index 829963d9..38531ae1 100644 --- a/.github/workflows/CD_staging.yml +++ b/.github/workflows/CD_staging.yml @@ -3,6 +3,7 @@ name: CD deploy staging to GAE on: push: branches: [ staging ] + workflow_dispatch: jobs: deploy-dev: @@ -43,7 +44,6 @@ jobs: - name: Install deps & build env: VITE_APP_TITLE: "Ocotillo (Staging)" - VITE_MAPBOX_TOKEN: ${{ secrets.VITE_MAPBOX_TOKEN }} VITE_BASE_URL: '/' VITE_AUTHENTIK_CLIENT_ID: ${{ vars.VITE_AUTHENTIK_CLIENT_ID }} VITE_AUTHENTIK_URL: ${{ vars.VITE_AUTHENTIK_URL }} diff --git a/.github/workflows/CI_cypress.yml b/.github/workflows/CI_cypress.yml index 827bb11d..7e89131a 100644 --- a/.github/workflows/CI_cypress.yml +++ b/.github/workflows/CI_cypress.yml @@ -80,17 +80,28 @@ jobs: working-directory: ./api-repo run: docker compose logs --tail=200 app || true + # Give up as soon as the app container dies. Polling a container that has + # already exited used to burn the full timeout before the job failed. - name: Wait for FastAPI to be ready working-directory: ./api-repo run: | echo "Waiting for FastAPI to be ready..." - timeout 720 bash -c ' - until curl -sf http://localhost:8000/docs; do - echo "FastAPI not up yet, retrying..." - sleep 3 - done - ' - echo "FastAPI is up and healthy" + for _ in $(seq 1 60); do + if curl -sf http://localhost:8000/docs >/dev/null; then + echo "FastAPI is up and healthy" + exit 0 + fi + if [ -z "$(docker compose ps --status running -q app)" ]; then + echo "The app container stopped before serving requests:" + docker compose logs --tail=100 app + exit 1 + fi + echo "FastAPI not up yet, retrying..." + sleep 3 + done + echo "FastAPI did not become ready within 180s:" + docker compose logs --tail=100 app + exit 1 - name: Show API logs after readiness probe if: always() diff --git a/.github/workflows/_preview_deploy.yml b/.github/workflows/_preview_deploy.yml new file mode 100644 index 00000000..fc9ca7e1 --- /dev/null +++ b/.github/workflows/_preview_deploy.yml @@ -0,0 +1,452 @@ +# Reusable preview deploy. Both the PR-triggered preview (CD_preview.yml) and the +# on-demand preview (CD_preview_ondemand.yml) call this so there is exactly one +# copy of the build/deploy/authentik logic. +name: _preview deploy + +on: + workflow_call: + inputs: + branch: + description: >- + Branch the preview belongs to. Service names and teardown are keyed off + this, so it must be a real branch name even when checkout_ref is set. + type: string + required: true + checkout_ref: + description: >- + Ref to actually build, when it differs from the branch. PR previews + pass the merge ref so the preview shows the merged result rather than + the head commit alone. Defaults to the branch. + type: string + default: '' + backend: + description: >- + "staging" points the preview at the shared staging API. + "ephemeral" spins up a throwaway API + postgis for this branch only. + type: string + default: staging + backend_ref: + description: Ref of DataIntegrationGroup/OcotilloAPI to build when backend=ephemeral. + type: string + default: staging + backend_auth: + description: >- + "disabled" runs the ephemeral API with AUTHENTIK_DISABLE_AUTHENTICATION=1. + A seeded preview database has no permission rows, so an auth-enabled + ephemeral backend locks every user out. Ignored when backend=staging. + type: string + default: disabled + seed: + description: Run transfers.seed against the ephemeral database on startup. + type: boolean + default: true + ttl_hours: + description: >- + Hours before the nightly sweep is allowed to tear this preview down. + 0 exempts the preview from the TTL sweep entirely, which is what PR + previews use -- those are torn down when the PR closes instead. + type: number + default: 48 + outputs: + preview_url: + description: Public URL of the preview frontend. + value: ${{ jobs.deploy.outputs.preview_url }} + api_url: + description: API URL the preview frontend was built against. + value: ${{ jobs.deploy.outputs.api_url }} + service_name: + description: Cloud Run service name of the preview frontend. + value: ${{ jobs.deploy.outputs.service_name }} + expires_at: + description: UTC timestamp after which the sweep may tear this preview down. + value: ${{ jobs.deploy.outputs.expires_at }} + +jobs: + deploy: + runs-on: ubuntu-latest + environment: staging + + outputs: + preview_url: ${{ steps.cloudrun-url.outputs.url }} + api_url: ${{ steps.api-url.outputs.url }} + service_name: ${{ steps.names.outputs.service_name }} + expires_at: ${{ steps.names.outputs.expires_human }} + + env: + GCP_PROJECT_ID: waterdatainitiative-271000 + GCP_REGION: us-central1 + AR_REPO: us-central1-docker.pkg.dev/waterdatainitiative-271000/ocotillo-previews + AUTHENTIK_BASE_URL: https://authentik.newmexicowaterdata.org + AUTHENTIK_PROVIDER_ID: ${{ secrets.AUTHENTIK_PROVIDER_ID }} + AUTHENTIK_API_TOKEN: ${{ secrets.AUTHENTIK_API_TOKEN }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + ref: ${{ inputs.checkout_ref != '' && inputs.checkout_ref || inputs.branch }} + + - name: Compute preview names and expiry + id: names + env: + BRANCH_NAME: ${{ inputs.branch }} + TTL_HOURS: ${{ inputs.ttl_hours }} + run: | + set -euo pipefail + + # Keep this sanitizer byte-for-byte in sync with _preview_teardown.yml, + # or teardown will compute a different service name than deploy did. + SANITIZED=$(echo "$BRANCH_NAME" \ + | sed 's/[^a-zA-Z0-9-]/-/g' \ + | tr '[:upper:]' '[:lower:]' \ + | cut -c1-40) + + # 0 is the "never sweep" sentinel the nightly sweep looks for. + if [ "$TTL_HOURS" -eq 0 ]; then + EXPIRES_AT=0 + EXPIRES_HUMAN="never (torn down when the PR closes)" + else + EXPIRES_AT=$(( $(date -u +%s) + TTL_HOURS * 3600 )) + EXPIRES_HUMAN=$(date -u -d "@$EXPIRES_AT" +'%Y-%m-%dT%H:%M:%SZ') + fi + + { + echo "SANITIZED_BRANCH=$SANITIZED" + echo "SERVICE_NAME=preview-$SANITIZED" + echo "API_SERVICE_NAME=preview-api-$SANITIZED" + echo "EXPIRES_AT=$EXPIRES_AT" + } >> "$GITHUB_ENV" + + { + echo "service_name=preview-$SANITIZED" + echo "expires_human=$EXPIRES_HUMAN" + } >> "$GITHUB_OUTPUT" + + echo "Frontend service: preview-$SANITIZED" + echo "Backend mode: ${{ inputs.backend }}" + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22.x + cache: npm + cache-dependency-path: package-lock.json + + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_SA_KEY }} + + - name: Configure Docker for Artifact Registry + run: | + gcloud config set project "$GCP_PROJECT_ID" + gcloud auth configure-docker us-central1-docker.pkg.dev --quiet + + # ---------------------------------------------------------------- backend + # The ephemeral backend reuses the same two images the Cypress job runs + # under docker compose -- OcotilloAPI's docker/app/Dockerfile and the stock + # postgis image -- but wires them together as Cloud Run sidecars instead of + # compose services. Sidecars share a network namespace, so the API reaches + # postgres on 127.0.0.1 the same way it reaches "db" locally. + - name: Checkout OcotilloAPI + if: inputs.backend == 'ephemeral' + uses: actions/checkout@v4 + with: + repository: DataIntegrationGroup/OcotilloAPI + ref: ${{ inputs.backend_ref }} + path: api-repo + + - name: Build and push API image + if: inputs.backend == 'ephemeral' + working-directory: ./api-repo + run: | + set -euo pipefail + + API_SHA=$(git rev-parse --short HEAD) + echo "API_IMAGE=$AR_REPO/${API_SERVICE_NAME}:$API_SHA" >> "$GITHUB_ENV" + echo "API_SOURCE_SHA=$API_SHA" >> "$GITHUB_ENV" + + # INSTALL_DEV=true because transfers.seed imports faker, a dev dependency. + docker build \ + --build-arg INSTALL_DEV=true \ + -f docker/app/Dockerfile \ + -t "$AR_REPO/${API_SERVICE_NAME}:$API_SHA" \ + . + + docker push "$AR_REPO/${API_SERVICE_NAME}:$API_SHA" + + - name: Deploy ephemeral backend to Cloud Run + if: inputs.backend == 'ephemeral' + env: + BACKEND_AUTH: ${{ inputs.backend_auth }} + TPL_RUN_SEED: ${{ inputs.seed }} + TPL_AUTHENTIK_CLIENT_ID: ${{ vars.VITE_AUTHENTIK_CLIENT_ID }} + run: | + set -euo pipefail + + if [ "$BACKEND_AUTH" = "disabled" ]; then + TPL_DISABLE_AUTH=1 + else + TPL_DISABLE_AUTH=0 + fi + + # The template's placeholders are TPL_-prefixed and the values here + # come from unprefixed GITHUB_ENV entries, so bridge the two. + export TPL_DISABLE_AUTH + export TPL_API_SERVICE_NAME="$API_SERVICE_NAME" + export TPL_API_IMAGE="$API_IMAGE" + export TPL_EXPIRES_AT="$EXPIRES_AT" + export TPL_SANITIZED_BRANCH="$SANITIZED_BRANCH" + export TPL_API_SOURCE_SHA="$API_SOURCE_SHA" + + # envsubst gets an explicit allowlist so the $VARS inside the container + # startup script in the template survive rendering untouched. + # shellcheck disable=SC2016 # envsubst wants the literal names, unexpanded + envsubst '${TPL_API_SERVICE_NAME} ${TPL_API_IMAGE} ${TPL_EXPIRES_AT} ${TPL_SANITIZED_BRANCH} ${TPL_DISABLE_AUTH} ${TPL_API_SOURCE_SHA} ${TPL_RUN_SEED} ${TPL_AUTHENTIK_CLIENT_ID}' \ + < .github/preview/api-service.tmpl.yaml \ + > /tmp/api-service.yaml + + cat /tmp/api-service.yaml + + gcloud run services replace /tmp/api-service.yaml \ + --region "$GCP_REGION" \ + --platform managed + + # The browser calls this API directly and cannot present a Google + # identity, so the service has to be publicly invokable. + gcloud run services add-iam-policy-binding "${API_SERVICE_NAME}" \ + --region "$GCP_REGION" \ + --platform managed \ + --member=allUsers \ + --role=roles/run.invoker \ + --quiet + + # "services replace" reports only that the revision never became ready. + # Which of the two containers failed, and why, is in Cloud Logging -- + # fetch it here rather than making the next person go digging in the + # console. Both containers land in the same revision log stream, tagged + # by container name. + - name: Dump ephemeral backend logs + if: failure() && inputs.backend == 'ephemeral' + run: | + set -uo pipefail + + echo "::group::Cloud Run logs for ${API_SERVICE_NAME}" + gcloud logging read \ + "resource.type=cloud_run_revision AND resource.labels.service_name=${API_SERVICE_NAME}" \ + --project "$GCP_PROJECT_ID" \ + --freshness 30m \ + --limit 300 \ + --order asc \ + --format 'value(timestamp, labels."run.googleapis.com/container_name", textPayload)' \ + || echo "Could not read logs." + echo "::endgroup::" + + echo "::group::Revision status for ${API_SERVICE_NAME}" + gcloud run revisions list \ + --service "${API_SERVICE_NAME}" \ + --region "$GCP_REGION" \ + --platform managed \ + --format 'yaml(metadata.name, status.conditions)' \ + || echo "No revisions found." + echo "::endgroup::" + + # A preview that switches back to the staging API -- someone drops the + # preview-backend label, or redeploys on demand without it -- would + # otherwise strand this branch's ephemeral API. That service pins an + # always-warm instance, so it bills continuously until the PR closes or + # the nightly sweep catches it. Reap it as soon as it is unwanted. + - name: Remove stale ephemeral backend + if: inputs.backend != 'ephemeral' + run: | + set -euo pipefail + + if gcloud run services describe "${API_SERVICE_NAME}" \ + --platform managed \ + --region "$GCP_REGION" >/dev/null 2>&1; then + echo "Deleting stale ephemeral backend ${API_SERVICE_NAME}" + gcloud run services delete "${API_SERVICE_NAME}" \ + --platform managed \ + --region "$GCP_REGION" \ + --quiet + else + echo "No stale ephemeral backend for this branch." + fi + + - name: Resolve API URL + id: api-url + run: | + set -euo pipefail + + if [ "${{ inputs.backend }}" = "ephemeral" ]; then + URL=$(gcloud run services describe "${API_SERVICE_NAME}" \ + --platform managed \ + --region "$GCP_REGION" \ + --format 'value(status.url)') + else + URL='${{ vars.VITE_OCOTILLO_API_URL }}' + fi + + if [ -z "$URL" ]; then + echo "Could not resolve an API URL for backend=${{ inputs.backend }}." >&2 + exit 1 + fi + + echo "API URL: $URL" + echo "API_URL=$URL" >> "$GITHUB_ENV" + echo "url=$URL" >> "$GITHUB_OUTPUT" + + - name: Wait for ephemeral backend + if: inputs.backend == 'ephemeral' + run: | + set -euo pipefail + + # Migrations plus the optional seed run before uvicorn binds, so the + # first successful response can be several minutes out. + for _ in $(seq 1 60); do + if curl -sf "$API_URL/docs" >/dev/null; then + echo "Ephemeral API is serving at $API_URL" + exit 0 + fi + echo "API not up yet, retrying..." + sleep 10 + done + + echo "Ephemeral API never became ready. Recent logs:" >&2 + gcloud run services logs read "${API_SERVICE_NAME}" \ + --region "$GCP_REGION" --limit 100 || true + exit 1 + + # --------------------------------------------------------------- frontend + - name: Load secrets + run: | + set -euo pipefail + echo "VITE_PUBLIC_POSTHOG_KEY=$(gcloud secrets versions access latest --secret=VITE_PUBLIC_POSTHOG_KEY --project="$GCP_PROJECT_ID")" >> "$GITHUB_ENV" + echo "VITE_PUBLIC_POSTHOG_HOST=$(gcloud secrets versions access latest --secret=VITE_PUBLIC_POSTHOG_HOST --project="$GCP_PROJECT_ID")" >> "$GITHUB_ENV" + + - name: Build and push + env: + IMAGE_TAG: ${{ github.sha }} + NODE_OPTIONS: --max-old-space-size=6144 + VITE_POSTHOG_KEY: ${{ env.VITE_PUBLIC_POSTHOG_KEY }} + VITE_POSTHOG_HOST: ${{ env.VITE_PUBLIC_POSTHOG_HOST }} + VITE_APP_ENV: preview + VITE_APP_VERSION: ${{ github.sha }} + run: | + set -euo pipefail + + npm ci + npx tsc + npx vite build --mode development + + docker build \ + --build-arg MODE=development \ + --build-arg VITE_APP_TITLE="Ocotillo Preview" \ + --build-arg VITE_BASE_URL="/" \ + --build-arg VITE_AUTHENTIK_CLIENT_ID=${{ vars.VITE_AUTHENTIK_CLIENT_ID }} \ + --build-arg VITE_AUTHENTIK_URL=${{ vars.VITE_AUTHENTIK_URL }} \ + --build-arg VITE_OCOTILLO_API_URL="$API_URL" \ + --build-arg VITE_PUBLIC_POSTHOG_KEY=${{ env.VITE_PUBLIC_POSTHOG_KEY }} \ + --build-arg VITE_PUBLIC_POSTHOG_HOST=${{ env.VITE_PUBLIC_POSTHOG_HOST }} \ + --build-arg VITE_APP_ENV=preview \ + --build-arg VITE_APP_VERSION=${{ github.sha }} \ + -t "$AR_REPO/${SERVICE_NAME}:$IMAGE_TAG" . + + docker push "$AR_REPO/${SERVICE_NAME}:$IMAGE_TAG" + + - name: Deploy to Cloud Run + env: + IMAGE_TAG: ${{ github.sha }} + run: | + set -euo pipefail + + gcloud run deploy "${SERVICE_NAME}" \ + --image "$AR_REPO/${SERVICE_NAME}:$IMAGE_TAG" \ + --platform managed \ + --region "$GCP_REGION" \ + --allow-unauthenticated \ + --port 8080 \ + --memory 512Mi \ + --cpu 1 \ + --labels "preview=true,preview-branch=${SANITIZED_BRANCH},preview-role=frontend,preview-expires=${EXPIRES_AT}" + + - name: Get Cloud Run URL + id: cloudrun-url + run: | + set -euo pipefail + + URL=$(gcloud run services describe "${SERVICE_NAME}" \ + --platform managed \ + --region "$GCP_REGION" \ + --format 'value(status.url)') + + if [ -z "$URL" ]; then + echo "Deploy succeeded but ${SERVICE_NAME} has no URL." >&2 + exit 1 + fi + + echo "Cloud Run URL: $URL" + echo "url=$URL" >> "$GITHUB_OUTPUT" + + - name: Add exact preview origin to authentik provider + env: + PREVIEW_ORIGIN: ${{ steps.cloudrun-url.outputs.url }} + run: | + set -euo pipefail + + echo "Ensuring authentik redirect/origin exists for: $PREVIEW_ORIGIN" + + tmp="$(mktemp)" + payload="$(mktemp)" + + curl -fsS \ + -H "Authorization: Bearer ${AUTHENTIK_API_TOKEN}" \ + "${AUTHENTIK_BASE_URL}/api/v3/providers/oauth2/${AUTHENTIK_PROVIDER_ID}/" \ + > "$tmp" + + jq --arg origin "$PREVIEW_ORIGIN" ' + .redirect_uris |= ( + . + [{"matching_mode":"strict","url":$origin}] + | unique_by(.matching_mode + "|" + .url) + ) + | {redirect_uris: .redirect_uris} + ' "$tmp" > "$payload" + + # -o /dev/null is load-bearing: authentik answers a PATCH with the full + # provider object, client_secret included, and anything on stdout here + # lands in the run log where every repo reader can see it. + curl -fsS -X PATCH \ + -o /dev/null \ + -H "Authorization: Bearer ${AUTHENTIK_API_TOKEN}" \ + -H "Content-Type: application/json" \ + --data @"$payload" \ + "${AUTHENTIK_BASE_URL}/api/v3/providers/oauth2/${AUTHENTIK_PROVIDER_ID}/" + + echo "Added/verified strict preview origin in authentik." + + - name: Job summary + if: always() + env: + PREVIEW_URL: ${{ steps.cloudrun-url.outputs.url }} + run: | + { + echo "## Preview deployment" + echo + echo "| | |" + echo "|---|---|" + echo "| Branch | \`${{ inputs.branch }}\` |" + echo "| Preview URL | ${PREVIEW_URL:-_deploy failed_} |" + echo "| Backend | \`${{ inputs.backend }}\` |" + echo "| API URL | ${API_URL:-_unresolved_} |" + echo "| Expires after | ${{ steps.names.outputs.expires_human }} |" + echo "| Teardown | \`gh workflow run CD_preview_teardown.yml -f branch=${{ inputs.branch }}\` |" + if [ "${{ inputs.backend }}" = "ephemeral" ]; then + echo + echo "> **Ephemeral backend.** Data lives in an in-memory postgis sidecar and is" + echo "> lost whenever the instance restarts. Do not load real data into it." + if [ "${{ inputs.backend_auth }}" = "disabled" ]; then + echo ">" + echo "> **Authentication is disabled on this API and it is publicly reachable.**" + fi + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/_preview_teardown.yml b/.github/workflows/_preview_teardown.yml new file mode 100644 index 00000000..9c774578 --- /dev/null +++ b/.github/workflows/_preview_teardown.yml @@ -0,0 +1,168 @@ +# Reusable preview teardown. Removes the frontend service, the ephemeral backend +# service if one exists, their Artifact Registry images, and the authentik +# redirect URI. Every step is idempotent: tearing down a preview that was never +# deployed succeeds and does nothing. +name: _preview teardown + +on: + workflow_call: + inputs: + branch: + description: Branch whose preview resources should be removed. + type: string + required: true + delete_images: + description: Also delete the branch's images from Artifact Registry. + type: boolean + default: true + +jobs: + teardown: + runs-on: ubuntu-latest + environment: staging + + env: + GCP_PROJECT_ID: waterdatainitiative-271000 + GCP_REGION: us-central1 + AR_REPO: us-central1-docker.pkg.dev/waterdatainitiative-271000/ocotillo-previews + AUTHENTIK_BASE_URL: https://authentik.newmexicowaterdata.org + AUTHENTIK_PROVIDER_ID: ${{ secrets.AUTHENTIK_PROVIDER_ID }} + AUTHENTIK_API_TOKEN: ${{ secrets.AUTHENTIK_API_TOKEN }} + + steps: + - name: Compute preview service names + env: + BRANCH_NAME: ${{ inputs.branch }} + run: | + set -euo pipefail + + # Keep this sanitizer byte-for-byte in sync with _preview_deploy.yml. + SANITIZED=$(echo "$BRANCH_NAME" \ + | sed 's/[^a-zA-Z0-9-]/-/g' \ + | tr '[:upper:]' '[:lower:]' \ + | cut -c1-40) + + { + echo "SANITIZED_BRANCH=$SANITIZED" + echo "SERVICE_NAME=preview-$SANITIZED" + echo "API_SERVICE_NAME=preview-api-$SANITIZED" + } >> "$GITHUB_ENV" + + echo "Tearing down preview-$SANITIZED and preview-api-$SANITIZED" + + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v2 + with: + credentials_json: ${{ secrets.GCP_SA_KEY }} + + - name: Configure gcloud project + run: gcloud config set project "$GCP_PROJECT_ID" + + # Resolve the URL before deleting: Cloud Run URLs embed a generated hash, + # so once the service is gone the authentik entry cannot be reconstructed. + - name: Get Cloud Run URL + id: cloudrun-url + run: | + set -euo pipefail + + URL=$(gcloud run services describe "${SERVICE_NAME}" \ + --platform managed \ + --region "$GCP_REGION" \ + --format 'value(status.url)' 2>/dev/null || true) + + if [ -z "$URL" ]; then + echo "No Cloud Run service named ${SERVICE_NAME}" + else + echo "Cloud Run URL: $URL" + fi + + echo "url=$URL" >> "$GITHUB_OUTPUT" + + - name: Remove exact preview origin from authentik provider + env: + PREVIEW_ORIGIN: ${{ steps.cloudrun-url.outputs.url }} + run: | + set -euo pipefail + + echo "Removing authentik redirect/origin for ${SERVICE_NAME}: ${PREVIEW_ORIGIN:-}" + + tmp="$(mktemp)" + payload="$(mktemp)" + + curl -fsS \ + -H "Authorization: Bearer ${AUTHENTIK_API_TOKEN}" \ + "${AUTHENTIK_BASE_URL}/api/v3/providers/oauth2/${AUTHENTIK_PROVIDER_ID}/" \ + > "$tmp" + + # Match the exact URL when it resolved, and additionally sweep this + # service's own host. The sweep cleans up entries stranded by earlier + # runs, when the URL came back empty and nothing was removed. + # + # The trailing "-[0-9]+\." is load-bearing: it pins the match to the + # project-number segment of a Cloud Run hostname. A bare + # "^https://-" prefix would also match a longer sibling + # service, so closing preview-foo would strip preview-foo-bar's origin + # and break an unrelated open PR. + jq --arg origin "$PREVIEW_ORIGIN" --arg service "$SERVICE_NAME" ' + def is_preview($u): + ($origin != "" and $u == $origin) + or ($u | test("^https://" + $service + "-[0-9]+\\.")); + + {redirect_uris: (.redirect_uris | map(select(is_preview(.url) | not)))} + ' "$tmp" > "$payload" + + before=$(jq '.redirect_uris | length' "$tmp") + after=$(jq '.redirect_uris | length' "$payload") + + if [ "$before" -eq "$after" ]; then + echo "No matching authentik redirect URI for ${SERVICE_NAME}; nothing to remove." + exit 0 + fi + + # -o /dev/null is load-bearing: authentik answers a PATCH with the full + # provider object, client_secret included, and anything on stdout here + # lands in the run log where every repo reader can see it. + curl -fsS -X PATCH \ + -o /dev/null \ + -H "Authorization: Bearer ${AUTHENTIK_API_TOKEN}" \ + -H "Content-Type: application/json" \ + --data @"$payload" \ + "${AUTHENTIK_BASE_URL}/api/v3/providers/oauth2/${AUTHENTIK_PROVIDER_ID}/" + + echo "Removed $((before - after)) preview origin(s) from authentik." + + - name: Delete Cloud Run services + run: | + set -euo pipefail + + for svc in "${SERVICE_NAME}" "${API_SERVICE_NAME}"; do + gcloud run services delete "$svc" \ + --platform managed \ + --region "$GCP_REGION" \ + --quiet || echo "No service $svc to delete." + done + + - name: Delete Artifact Registry images + if: inputs.delete_images + run: | + set -euo pipefail + + for img in "${SERVICE_NAME}" "${API_SERVICE_NAME}"; do + gcloud artifacts docker images delete "$AR_REPO/$img" \ + --delete-tags \ + --quiet || echo "No images for $img to delete." + done + + - name: Job summary + if: always() + run: | + { + echo "## Preview teardown" + echo + echo "| | |" + echo "|---|---|" + echo "| Branch | \`${{ inputs.branch }}\` |" + echo "| Frontend service | \`${SERVICE_NAME}\` |" + echo "| Backend service | \`${API_SERVICE_NAME}\` |" + echo "| Images deleted | ${{ inputs.delete_images }} |" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitignore b/.gitignore index f95353d8..20bf9a01 100644 --- a/.gitignore +++ b/.gitignore @@ -33,8 +33,13 @@ mise.toml # But keep .env.example and .env.*.example !.env.example !.env.*.example +mise.toml cypress/e2e/1-getting-started/* cypress/e2e/2-advanced-examples/* cypress/screenshots/* cypress/downloads/* + +# Claude Code: personal/ephemeral files (shared config like launch.json stays tracked) +.claude/settings.local.json +.claude/worktrees/ diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 66e7e941..00000000 --- a/.prettierrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "trailingComma": "es5", - "tabWidth": 2, - "semi": false, - "singleQuote": true -} \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 5bc0e652..699ed733 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,3 @@ { - "recommendations": [ - "esbenp.prettier-vscode", - "dbaeumer.vscode-eslint" - ] + "recommendations": ["biomejs.biome"] } diff --git a/.vscode/settings.json b/.vscode/settings.json index ad20b4fb..24f74aa5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,11 +1,8 @@ { "editor.formatOnSave": true, - "editor.defaultFormatter": "esbenp.prettier-vscode", - "eslint.useFlatConfig": true, - "eslint.validate": [ - "javascript", - "javascriptreact", - "typescript", - "typescriptreact" - ] + "editor.defaultFormatter": "biomejs.biome", + "editor.codeActionsOnSave": { + "source.fixAll.biome": "explicit", + "source.organizeImports.biome": "explicit" + } } diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..0b69e684 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,170 @@ +# AGENTS.md + +Guidance for AI coding agents working in the OcotilloUI repository. + +OcotilloUI is the admin dashboard for the New Mexico Bureau of Geology & Mineral Resources (NMBGMR) — React 18 + TypeScript on Refine.dev, built with Vite, deployed to Google App Engine. + +## Documentation rule: record the model + +**Every document generated by an AI agent in this repo must state which model produced it.** This applies to ADRs, design docs, runbooks, migration notes, research summaries, and any other prose artifact an agent writes — not to source code, tests, or commit messages. + +Put the attribution in a YAML frontmatter block at the top of the file: + +```markdown +--- +generated-by: claude-opus-5 +generated-on: 2026-08-06 +prompted-by: jakeross +--- + +# ADR 0007: Replace Yup with Zod for form validation +``` + +Rules for the field: + +- Use the exact model ID, not a friendly name — `claude-opus-5`, not "Claude" or "Opus". +- If a human substantially rewrote the document, keep the field and add `human-edited: true`. +- If several models contributed, list them: `generated-by: [claude-opus-5, claude-sonnet-5]`. +- Never remove the field when editing an existing generated doc. Update `generated-on` instead. + +The point is traceability: when a doc turns out to be wrong or stale, the reader should know what produced it and be able to weigh it accordingly. + +ADRs go in `docs/adr/` as `NNNN-kebab-title.md`, numbered sequentially. Other docs go in `docs/`. + +## Commands + +```bash +npm run dev # Vite dev server on :5173 +npm run test # Vitest watch +npm run test:run # Vitest once — use this in CI-like checks +npm run test:coverage # Vitest with v8 coverage +npm run lint # Biome lint +npm run lint:fix # Biome lint with safe autofix +npm run format # Biome format, writes +npm run format:check # Biome format, check only +npm run check # Biome lint + format together +npm run check:fix # Biome lint + format, writes +npm run typecheck # tsc, no emit to dist +npm run build # Production build +npm run build:ci # typecheck + build, sourcemaps off, 4GB heap +``` + +Before declaring work done, run `npm run lint`, `npm run typecheck`, and `npm run test:run`. The Lint workflow runs `lint` and `typecheck` on every PR, and Vitest runs in its own workflow, so these catch what CI would. + +The `:unsafe` variants (`lint:fix:unsafe`, `check:fix:unsafe`) apply Biome fixes that can change behavior. Do not reach for them to clear a warning — read the warning instead. + +Cypress E2E needs a server and the mock API: + +```bash +npm run mock:server:cypress # Prism mock of openapi-auth.json on :4010 +npx cypress run # against :5173 locally, :4173 in CI +``` + +## Do not hand-edit generated code + +`src/generated/` is emitted by `@hey-api/openapi-ts` from `openapi-auth.json` and is wiped clean on every regeneration (`output.clean: true`). Never edit `types.gen.ts` or `zod.gen.ts` — changes are silently lost on the next codegen run. + +To change generated types, update `openapi-auth.json` (or pull a fresh spec from the API), then: + +```bash +npm run openapi:generate +``` + +Commit the regenerated output alongside the spec change. + +## Layout + +``` +src/ + pages/ Route-level components + components/ Shared presentational + container components + resources/ Refine resource definitions (ocotillo, geothermal, st2, …) + routes/ Route tree, wired to accessControl + config/ navigation, auth, storage keys, units, time, pdf + contexts/ React contexts + hooks/ Shared hooks + providers/ Refine data/auth/accessControl providers + generated/ AUTO-GENERATED — do not edit + test/ Vitest suites, mirroring the src tree +cypress/e2e/ End-to-end specs +docs/ Prose docs (see documentation rule above) +scripts/ scaffold_resource.py — generates a new Refine resource +``` + +Adding a resource touches `src/resources/`, `src/routes/`, and `src/config/navigation.ts` together. Navigation entries are access-control aware — a page invisible to a role is usually a `navigation.ts` or `accessControl` issue, not a routing bug. + +## Style + +**Biome does both linting and formatting**, configured in `biome.json`. There is no ESLint and no Prettier here — do not run `npx prettier` or `npx eslint`. Without a config to find, they will happily reformat the entire file to a style the repo does not use, producing a diff that buries the real change. + +The formatter settings are non-obvious in two places: **no semicolons** (`semicolons: "asNeeded"`) and **single quotes** (`quoteStyle: "single"`), plus 2-space indent, 80-column width, and `es5` trailing commas. Match the surrounding file; do not reformat unrelated lines. + +Linter scope is narrower than formatter scope: the linter covers `src/**/*.ts`, `src/**/*.tsx`, and root `*.ts` only, while `src/generated`, `cypress`, `dist`, and `coverage` are excluded from both. Key rules — `useHookAtTopLevel` is an **error**; `noExplicitAny`, `noImplicitAnyLet`, `noUnusedVariables`, and `useExhaustiveDependencies` are **warnings**, deliberately, to allow gradual cleanup. See `LINTING.md` for the full ruleset and how to suppress a rule properly. + +Note that `production` still carries the pre-migration `eslint.config.ts` and `.prettierrc`. Those files are historical; on `staging` and anything branched from it, Biome is the only formatter. + +UI layering is documented in `FRONTEND.md` — read it before adding styles. Short version: MUI owns components and theming, Refine owns CRUD scaffolding and routing, Tailwind v4 is available but should not fight the MUI theme. Prefer theme tokens over hard-coded colors. + +## Branches and deploys + +| Branch | Trigger | Target | +|---|---|---| +| feature branch → PR | `pull_request` | Cloud Run preview deploy | +| `staging` | push | App Engine staging (`CD_staging.yml`) | +| `production` | push | App Engine production (`CD_production.yml`) | + +### Where unfinished work gets exercised + +**Preview deploys are the sandbox. `staging` is a pre-production release branch — what is on it is a candidate for `production`, not an experiment.** Anything not ready to ship gets exercised on its own PR preview (see `docs/preview-deployments.md`), which can run against an ephemeral API nobody else shares. + +In code, that means WIP surfaces gate on dev or preview, never on staging: + +```ts +export const SHOW_WIP_FEATURES = + import.meta.env.DEV || import.meta.env.VITE_APP_ENV === 'preview' +``` + +`recordsGridLogic.ts` gates exactly this way. Do not add `'staging'` to that check, and do not add a staging arm to a new one. + +### Where to branch from + +**Everything bases off `staging` and targets `staging` in its PR — feature, fix, chore, docs, and CI work alike.** The single exception is a hotfix, which bases off `production` and targets `production`. + +```bash +# feature, fix, chore, docs, ci — the normal case +git fetch origin && git checkout -b BDMS-1234-short-description origin/staging +git fetch origin && git checkout -b chore/tighten-preview-cleanup origin/staging + +# hotfix only +git fetch origin && git checkout -b hotfix/v1.2.3 origin/production +``` + +Existing branch names use a mix of conventions — a `BDMS-####-description` ticket prefix, or a `feat/`, `fix/`, `chore/`, `task/`, `hotfix/` type prefix. Either is fine; match whichever the work resembles. The base branch is what matters, not the name. + +This matters more than it looks. `production` is the repository's default branch, so a branch cut without thinking starts there — and a PR from a `production`-based branch into `staging` drags every commit `production` has that `staging` does not into the diff, burying the actual change. If a PR shows files you never touched, this is why: rebase onto `origin/staging` rather than trying to resolve it in review. + +Never push directly to `staging` or `production`; both are deploy triggers. + +`CD_staging.yml` also accepts `workflow_dispatch`, so a staging deploy can be re-run manually without a dummy commit: + +```bash +gh workflow run CD_staging.yml --repo DataIntegrationGroup/OcotilloUI --ref staging +``` + +`CD_production.yml` is push-only — a production redeploy currently requires a new commit on `production`. + +PR checks: Lint, Vitest, Cypress, and PR Build Test all run on `pull_request`. + +## Secrets and environment + +Env files follow `.env.development.example`, `.env.devserver.example`, and `.env.production.example`. Copy the example, never commit a filled-in `.env`. + +Deploy-time secrets (`GCP_SA_KEY`, PostHog keys) come from GitHub Actions secrets and Google Secret Manager. Do not inline credentials, tokens, or service-account JSON into source, tests, fixtures, or docs — including as "example" values that look real. + +## Working agreements + +- Match existing patterns in the file you are editing over introducing a new one. +- Tests live in `src/test/`, mirroring the source path of what they cover. +- Do not bump dependency versions as a side effect of unrelated work. +- Do not commit or push unless asked. When asked, branch off `origin/staging` (see [Where to branch from](#where-to-branch-from)) rather than committing to a deploy branch. +- If a task is blocked or partially done, say which parts were left out and why rather than reporting completion. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..84eeefdf --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,21 @@ +--- +generated-by: claude-opus-5 +generated-on: 2026-08-22 +prompted-by: jakeross +--- + +# CLAUDE.md + +Guidance for Claude Code in the OcotilloUI repository. The full agent guide lives in `AGENTS.md`, imported below — read it before making changes. + +## Branching, in short + +**Feature, fix, chore, docs, and CI branches all base off `origin/staging` and target `staging` in their PR.** Only a hotfix bases off `production`. + +```bash +git fetch origin && git checkout -b chore/bdms-1234-short-description origin/staging +``` + +`production` is the repository's default branch, so a branch cut without passing an explicit base starts there — and a `production`-based PR into `staging` drags unrelated commits into the diff. Always pass `origin/staging` explicitly. See [Where to branch from](AGENTS.md#where-to-branch-from) for the full rule. + +@AGENTS.md diff --git a/Dockerfile b/Dockerfile index 25348105..cc8ed4de 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,6 @@ ARG VITE_AUTHENTIK_CLIENT_ID ARG VITE_AUTHENTIK_URL ARG VITE_AUTHENTIK_REDIRECT_URI ARG VITE_OCOTILLO_API_URL -ARG VITE_MAPBOX_TOKEN ARG VITE_PUBLIC_POSTHOG_KEY ARG VITE_PUBLIC_POSTHOG_HOST ARG VITE_APP_ENV=preview diff --git a/FRONTEND.md b/FRONTEND.md index 49dc3f21..3c6083e5 100644 --- a/FRONTEND.md +++ b/FRONTEND.md @@ -393,54 +393,120 @@ Apply the same pattern to any other detail page where the record name adds meani ## Color System -Colors are defined in `src/theme.ts` and sourced entirely from the [Tailwind CSS v3 color palette](https://v3.tailwindcss.com/docs/customizing-colors). +There are **two** colour pipelines, and they must be kept in step: -### How it works +| Pipeline | Source of truth | Consumed by | +| --- | --- | --- | +| MUI | `src/theme.ts` (`palette` block) | Everything rendered by MUI / Refine | +| Tailwind + shadcn/ui | `src/index.css` (CSS custom properties) | `className="bg-primary"`, shadcn components | -`tailwindcss@3` is installed as a devDependency **only as a color value source** — there is no Tailwind CSS processing, no `tailwind.config.js`, no utility classes in templates. The package exports a `colors` object with every Tailwind color as a plain hex value, which is imported directly into `theme.ts`: +Both express the same palette. Change a colour in one and you must change it in +the other, or light and dark mode will drift apart between the two halves of the +app. -```ts -import colors from 'tailwindcss/colors' +### The brand ramp -colors.blue[700] // '#1d4ed8' -colors.stone[200] // '#e7e5e4' -colors.zinc[950] // '#09090b' -``` +`primary` is **Ocotillo blue** — a bespoke 50–950 ramp defined at the top of the +`colors` object in `src/theme.ts`. It is the only ramp that is not from +Tailwind. The values were sampled from the product's own artwork: the pixel +water-splash mark (`public/images/pixel/ocotillo-splash.svg`) and the high-desert +sky in `src/img/ocotillo.jpeg`. In OKLCH the ramp sits at hue ~235–245 — a +water/sky blue with no violet cast. + +| Stop | Hex | Role | +| --- | --- | --- | +| `brand[200]` | `#b8dff6` | `primary.dark` (dark mode — hover lightens) | +| `brand[300]` | `#83c6ee` | `primary.main` dark mode / `primary.light` light mode | +| `brand[400]` | `#47a6dd` | `primary.light` dark mode | +| `brand[600]` | `#0e6da8` | `primary.main` light mode | +| `brand[700]` | `#0f5786` | `primary.dark` light mode | -The theme uses these values directly for the MUI `palette`. **Do not add Tailwind utility classes (`className="text-blue-700"`)** — they will not work. All styling goes through MUI's `sx` prop and `theme.ts`. +Every other ramp still comes from the [Tailwind v3 palette](https://v3.tailwindcss.com/docs/customizing-colors), +inlined as hex in the same `colors` object. Tailwind v4 returns `oklch()` +strings from `tailwindcss/colors`, which MUI's palette parser cannot read — that +is why the hex values are inlined rather than imported. ### Palette slots +Unlike the other slots, `primary` is **mode-aware**: a mid-dark blue that works +on white is far too heavy on `zinc-900`, so dark mode steps up the ramp. Note +that MUI uses the `dark` slot as the hover/emphasis token, so in dark mode +`primary.dark` resolves *lighter* than `primary.main`, not darker. + | Slot | Light mode | Dark mode | Used for | | --- | --- | --- | --- | -| `primary` | `blue[300/700/900]` | same | Buttons, links, focus rings | +| `primary` | `brand[300/600/700]` | `brand[400/300/200]` | Buttons, links, focus rings | | `secondary` | `amber[300/600/800]` | same | Secondary actions | | `error` | `red[300/600/800]` | same | Validation errors | | `warning` | `orange[300/500/700]` | same | Warning alerts | | `success` | `emerald[300/700/900]` | same | Success states | -| `info` | `cyan[300/600/800]` | same | Info alerts | -| `background.default` | `stone[200]` | `zinc[950]` | Page background, sidebar | +| `info` | `teal[300/700/900]` | same | Info alerts | +| `background.default` | `zinc[50]` | `zinc[900]` | Page background, sidebar | | `background.paper` | `white` | `zinc[700]` | Cards, DataGrid, inputs | -| `background.wrapper` | `stone[100]` | `zinc[800]` | List page wrapper card | +| `background.wrapper` | `zinc[50]` | `zinc[800]` | List page wrapper card | | `divider` | `stone[300]` | `zinc[700]` | Borders, dividers | | `text.primary` | `slate[900]` | `slate[100]` | Body text | | `text.secondary` | `slate[500]` | `slate[400]` | Descriptions, labels | -### Changing a color +`info` is teal rather than cyan: cyan sits only ~20 degrees of hue from the brand +blue, and an info alert next to a primary button read as the same colour. + +### Brand identity colours (not semantic) + +Two more ramps exist in the `colors` object, and they play by different rules: + +| Token | Hex | What it is | +| --- | --- | --- | +| `bloom[500]` | `#e2552e` | The scarlet of the ocotillo flower | +| `sand[100]` | `#f2e9d2` | The bone/sand of the stems | -Open `src/theme.ts` and find the relevant slot in the `palette` block. Change the Tailwind color reference: +These are **brand identity, not semantic slots, and must never be wired to one.** +In OKLCH the bloom sits at hue 36 — 8.7 degrees from `error` (red-600, hue 27.3) +and 11.6 from `warning` (orange-500, hue 47.6). Anything painted in it inside the +UI chrome will read as an alarm. Use it for brand surfaces only: the favicon, +artwork, splash screens. + +This is also why `secondary` stays amber. At hue 58.3 it clears `warning` by 10.7 +degrees, which is already tight; promoting the bloom into that slot would be +worse, not better. + +The matching CSS variables are `--bloom` and `--sand` (utilities `bg-bloom`, +`text-sand`). Unlike every other token they are **mode-independent** — declared +once in `:root` with no `.dark` override, because a logo does not change colour +with the theme. + +### Contrast + +The brand ramp is chosen so every primary-on-surface pairing clears WCAG AA +(4.5:1 for text, 3:1 for UI): + +| Pairing | Ratio | +| --- | --- | +| `brand[600]` on white — light links, contained buttons | 5.57 | +| `brand[700]` on white — light hover | 7.70 | +| `brand[300]` on `zinc[900]` — dark links | 9.51 | +| `brand[300]` on `zinc[700]` — dark links on a card | 5.61 | +| `brand[200]` on `zinc[900]` — dark hover | 12.59 | + +If you restop the ramp, re-check these. Anything landing under 4.5 against +`zinc[700]` (the dark-mode card surface) is the first thing to break. + +### Changing a colour + +1. Edit the slot in the `palette` block of `src/theme.ts`. +2. Edit the matching CSS variable in **both** `:root` and `.dark` in + `src/index.css`. Those are `oklch()`, so convert the hex first. +3. Grep for hardcoded hex — e.g. the gradient border in `src/components/AppShell.tsx`. ```ts -// Change primary from blue to sky +// Change primary from the brand ramp to Tailwind sky primary: { light: colors.sky[300], - main: colors.sky[700], - dark: colors.sky[900], + main: colors.sky[600], + dark: colors.sky[800], }, ``` -Refer to the [Tailwind v3 color palette](https://v3.tailwindcss.com/docs/customizing-colors) for all available color names and numeric stops (50 through 950). Lighter numbers are lighter colors; darker numbers are darker. - ### Custom background tokens The `TypeBackground` interface is augmented in `src/theme.ts` to add `wrapper` as a custom slot alongside `default` and `paper`. To add another custom token: diff --git a/LINTING.md b/LINTING.md index a7be3bb5..777819c0 100644 --- a/LINTING.md +++ b/LINTING.md @@ -1,6 +1,6 @@ # Linting -OcotilloUI uses [ESLint](https://eslint.org/) with the flat config format (`eslint.config.ts`) and [Prettier](https://prettier.io/) for formatting. +OcotilloUI uses [Biome](https://biomejs.dev/) for linting and formatting. ## Running locally @@ -11,48 +11,32 @@ npm run lint # Auto-fix fixable issues npm run lint:fix -# Check formatting only (Prettier) -npx prettier --check . +# Check formatting only +npm run format:check -# Auto-format all files -npx prettier --write . +# Auto-format all supported files +npm run format ``` ## Ruleset -The config lives in `eslint.config.ts` at the repo root. It applies to all `*.ts` and `*.tsx` files and covers: +The config lives in `biome.json` at the repo root. It applies the current TypeScript/React baseline and skips generated/build output: -| Plugin | Purpose | -|--------|---------| -| `@eslint/js` | Core JS recommended rules | -| `typescript-eslint` | TypeScript type-aware rules | -| `eslint-plugin-react` | React best practices | -| `eslint-plugin-react-hooks` | Enforces the rules of hooks | -| `eslint-plugin-react-refresh` | Vite HMR compatibility | -| `eslint-config-prettier` | Disables rules that conflict with Prettier | +- `dist/**` +- `node_modules/**` +- `src/generated/**` +- `coverage/**` +- `cypress/**` ### Key rules -- `@typescript-eslint/no-explicit-any` — **warn**. Explicit `any` is a signal to improve typing. Not an error yet to allow gradual cleanup. -- `@typescript-eslint/no-unused-vars` — **warn**. Variables prefixed with `_` are exempt. -- `react-hooks/rules-of-hooks` — **error**. Hooks called outside components or conditionally break React. -- `react-hooks/exhaustive-deps` — **warn**. Missing `useEffect` deps are a common source of stale closure bugs. +- `noExplicitAny` and `noImplicitAnyLet` — **warn**. Unsafe `any` usage is a signal to improve typing. Not an error yet to allow gradual cleanup. +- `noUnusedVariables` — **warn**. Unused variables should be cleaned up, but do not block the initial Biome migration. +- `useHookAtTopLevel` — **error**. Hooks called outside components or conditionally break React. +- `useExhaustiveDependencies` — **warn**. Missing hook dependencies are a common source of stale closure bugs. ## CI enforcement -[`CI_lint.yml`](.github/workflows/CI_lint.yml) runs `npm run lint` and `npm run typecheck` on every pull request. The build fails on any ESLint **error**. Warnings are reported but do not block merge. +[`CI_lint.yml`](.github/workflows/CI_lint.yml) runs `npm run lint` and `npm run typecheck` on every pull request. The build fails on any Biome **error**. Warnings are reported but do not block merge. The goal is to graduate all current warnings to errors once the codebase is clean. See [Epic 1, Ticket 1.2](../nm-water-data/tickets/epics/epic-01-code-health.md) for the full plan. Epic 1 status and strict-mode progress are tracked in [code health and quality](https://github.com/DataIntegrationGroup/the-brain/blob/main/docs/process/code-health-and-quality.md) in the-brain repo. - -## Prettier config - -Prettier is configured in `.prettierrc`: - -```json -{ - "trailingComma": "es5", - "tabWidth": 2, - "semi": false, - "singleQuote": true -} -``` diff --git a/README.md b/README.md index 3081c4da..7db93880 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ NMBGMR data sources, including NM aquifer, Pychron, NM wells, and ST2 data. - CRUD operations for Ocotillo system through a unified Admin Dashboard - User authentication and authorization via Authentik -- Interactive map visualizations using Mapbox GL +- Interactive map visualizations using MapLibre GL - Data validation with React Hook Form and Zod - Theming and layout via Material UI @@ -36,7 +36,7 @@ NMBGMR data sources, including NM aquifer, Pychron, NM wells, and ST2 data. - Vite (Next-generation frontend build tool) - Material UI (UI components) - React Hook Form & Zod (Forms & validation) -- Mapbox GL (Map visualizations) +- MapLibre GL (Map visualizations) - Authentik (Authentication) - Cypress (E2E Testing) @@ -84,10 +84,8 @@ This application uses Vite environment variables. The following variables are re ```bash VITE_APP_TITLE="Ocotillo (Dev)" VITE_NMBGMR_AMP_API_URL="https://your-amp-development-api-url" -VITE_NMBGMR_GEOTHERMAL_API_URL="https://your-geothermal-development-api-url" VITE_OCOTILLO_API_URL="https://your-ocotillo-development-api-url" VITE_REFINE_PROJECT_ID="your-refine-project-id" -VITE_MAPBOX_TOKEN="your-mapbox-token" VITE_POSTHOG_KEY="your-posthog-project-api-key" VITE_POSTHOG_HOST="https://us.i.posthog.com" VITE_AUTHENTIK_CLIENT_ID="your-authentik-client-id" @@ -182,6 +180,8 @@ npm run start Deploy the contents of the `dist/` folder to any static hosting provider (e.g. GCP). Ensure environment variables are configured on the hosting platform. +For throwaway review deployments off any branch — including ones that spin up their own API and database — see [docs/preview-deployments.md](docs/preview-deployments.md). + ## License This project is licensed under the Apache 2.0 License - see the [LICENSE](./LICENSE) file for details. @@ -194,6 +194,6 @@ New Mexico Bureau of Geology & Mineral Resources ## Acknowledgements - [Refine.dev](https://refine.dev) -- [Mapbox GL](https://docs.mapbox.com/mapbox-gl-js/) +- [MapLibre GL](https://maplibre.org/maplibre-gl-js/docs/) - [Material UI](https://mui.com) - [Cypress](https://www.cypress.io/) diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..ce16548a --- /dev/null +++ b/biome.json @@ -0,0 +1,49 @@ +{ + "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", + "files": { + "ignoreUnknown": true, + "includes": [ + "**", + "!dist", + "!node_modules", + "!src/generated", + "!coverage", + "!cypress" + ] + }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineWidth": 80 + }, + "javascript": { + "formatter": { + "quoteStyle": "single", + "semicolons": "asNeeded", + "trailingCommas": "es5" + }, + "jsxRuntime": "reactClassic" + }, + "json": { + "formatter": { + "trailingCommas": "none" + } + }, + "linter": { + "enabled": true, + "includes": ["src/**/*.ts", "src/**/*.tsx", "*.ts"], + "rules": { + "preset": "none", + "correctness": { + "noUnusedVariables": "warn", + "useExhaustiveDependencies": "warn", + "useHookAtTopLevel": "error" + }, + "suspicious": { + "noExplicitAny": "warn", + "noImplicitAnyLet": "warn" + } + } + } +} diff --git a/cypress/e2e/ocotillo/list-pages.cy.ts b/cypress/e2e/ocotillo/list-pages.cy.ts new file mode 100644 index 00000000..260974b4 --- /dev/null +++ b/cypress/e2e/ocotillo/list-pages.cy.ts @@ -0,0 +1,85 @@ +/// + +import { + interceptOcotilloListFixtures, + projectAlpha, + projectBeta, + wellOne, + wellTwo, +} from '../../support/ocotillo-fixtures' + +describe('Ocotillo List Pages', () => { + beforeEach(() => { + cy.viewport(1600, 900) + interceptOcotilloListFixtures() + cy.login() + }) + + it('renders the wells list with search, actions, columns, and rows', () => { + cy.visit('/ocotillo/well') + cy.wait('@getWells') + + cy.contains('h3', /^Wells$/).should('be.visible') + cy.get('input[aria-label="Search wells by well name"]').should( + 'have.attr', + 'placeholder', + 'Search by well name' + ) + cy.get('button') + .contains(/batch field sheets/i) + .should('be.visible') + cy.get('button') + .contains(/export/i) + .should('be.visible') + + cy.contains('[role="columnheader"]', 'Name').should('be.visible') + cy.contains('[role="columnheader"]', 'Site name').should('be.visible') + cy.contains('[role="columnheader"]', 'Monitoring').should('be.visible') + cy.contains('[role="columnheader"]', 'Well Status').should('be.visible') + + cy.contains('[role="row"]', wellOne.name).should('be.visible') + cy.contains('[role="row"]', wellTwo.name).should('be.visible') + cy.contains(projectAlpha.name).should('be.visible') + }) + + it('renders the project list with project rows and navigation targets', () => { + cy.visit('/ocotillo/well/projects') + cy.wait('@getProjects') + + cy.contains('h3', /^Projects$/).should('be.visible') + cy.get('input[aria-label="Filter rows on this page"]').should( + 'have.attr', + 'placeholder', + 'Filter this page...' + ) + + cy.contains('[role="columnheader"]', 'Name').should('be.visible') + cy.contains('[role="columnheader"]', 'Description').should('be.visible') + cy.contains('[role="columnheader"]', 'Release Status').should('be.visible') + cy.contains('[role="columnheader"]', 'Type').should('be.visible') + + cy.contains('[role="row"]', projectAlpha.name).should('be.visible') + cy.contains('[role="row"]', projectBeta.name).should('be.visible') + cy.contains(projectAlpha.description).should('be.visible') + }) + + it('renders the contacts list with contact method and associated site columns', () => { + cy.visit('/ocotillo/contact') + cy.wait('@getContacts') + + cy.contains('h3', /contacts & owners/i).should('be.visible') + cy.get('input[aria-label="Filter rows on this page"]').should('be.visible') + + cy.contains('[role="columnheader"]', 'Name').should('be.visible') + cy.contains('[role="columnheader"]', 'Organization').should('be.visible') + cy.contains('[role="columnheader"]', 'Role').should('be.visible') + cy.contains('[role="columnheader"]', 'Contact Type').should('be.visible') + cy.contains('[role="columnheader"]', 'Associated Sites').should( + 'be.visible' + ) + + cy.contains('[role="row"]', 'Alex Contact').should('be.visible') + cy.contains('[role="row"]', 'Jordan Manager').should('be.visible') + cy.contains(wellOne.name).should('be.visible') + }) +}) diff --git a/cypress/e2e/ocotillo/map_list.cy.ts b/cypress/e2e/ocotillo/map_list.cy.ts index f7b8c5c0..b44bd738 100644 --- a/cypress/e2e/ocotillo/map_list.cy.ts +++ b/cypress/e2e/ocotillo/map_list.cy.ts @@ -7,7 +7,7 @@ describe('Map Page', () => { }) it('should render the map page UI without errors', () => { - cy.get('canvas.mapboxgl-canvas', { timeout: 20000 }).should('be.visible') + cy.get('canvas.maplibregl-canvas', { timeout: 20000 }).should('be.visible') cy.get('[data-testid="ocotillo-map-container"]').should('exist') }) }) diff --git a/cypress/e2e/ocotillo/show-pages.cy.ts b/cypress/e2e/ocotillo/show-pages.cy.ts new file mode 100644 index 00000000..baa20c92 --- /dev/null +++ b/cypress/e2e/ocotillo/show-pages.cy.ts @@ -0,0 +1,65 @@ +/// + +import { + contactOne, + interceptContactShowFixtures, + interceptProjectShowFixtures, + projectAlpha, + wellOne, + wellTwo, +} from '../../support/ocotillo-fixtures' + +describe('Ocotillo Show Pages', () => { + it('renders the project show page with details, map, and associated wells', () => { + interceptProjectShowFixtures() + cy.login() + cy.visit('/ocotillo/well/projects/show/10') + cy.wait('@getProject') + + cy.contains('h3', projectAlpha.name).should('be.visible') + cy.contains(projectAlpha.group_type).should('be.visible') + cy.contains('Project Details').should('be.visible') + cy.contains('Project Details') + .closest('[class*="MuiPaper-root"]') + .within(() => { + cy.contains('Release Status').should('be.visible') + cy.contains(projectAlpha.release_status).should('be.visible') + }) + cy.contains(projectAlpha.description).should('be.visible') + cy.contains('Created By').should('be.visible') + cy.contains(projectAlpha.created_by_name).should('be.visible') + + cy.contains('Project Map').should('be.visible') + cy.get('[data-testid="ocotillo-map-container"]', { + timeout: 20000, + }).should('exist') + cy.contains('Associated Wells').scrollIntoView().should('be.visible') + cy.get('a') + .contains('View all 2 wells') + .should('have.attr', 'href') + .and('include', '/ocotillo/well?projectId=10') + cy.contains('[role="row"]', wellOne.name).should('be.visible') + cy.contains('[role="row"]', wellTwo.name).should('be.visible') + }) + + it('renders the contact show page with details and associated sites', () => { + interceptContactShowFixtures() + cy.login() + cy.visit('/ocotillo/contact/show/1') + cy.wait('@getContact') + + cy.contains('h3', contactOne.name).should('be.visible') + cy.contains(contactOne.role).should('be.visible') + cy.contains(contactOne.organization).should('be.visible') + cy.contains('Contact Details').should('be.visible') + cy.contains(contactOne.emails[0].email).should('be.visible') + cy.contains('(505) 555-1212').should('be.visible') + cy.contains('801 Leroy Place').should('be.visible') + + cy.contains('Associated Sites').should('be.visible') + cy.contains(wellOne.name).should('be.visible') + cy.contains('Depth to water').should('be.visible') + cy.contains('42.5 ft bgs').should('be.visible') + cy.contains('Associated Sites Map').should('be.visible') + }) +}) diff --git a/cypress/e2e/ocotillo/well-show.cy.ts b/cypress/e2e/ocotillo/well-show.cy.ts index a04c6bd8..6579b0d8 100644 --- a/cypress/e2e/ocotillo/well-show.cy.ts +++ b/cypress/e2e/ocotillo/well-show.cy.ts @@ -1,17 +1,43 @@ /// +import { + contactOne, + interceptWellShowFixtures, + projectAlpha, + wellOne, +} from '../../support/ocotillo-fixtures' + describe('Thing Well Show Page', () => { beforeEach(() => { + interceptWellShowFixtures() cy.login() - - cy.intercept('GET', '**/thing/*').as('getWell') cy.visit('/ocotillo/well/show/1') - cy.wait('@getWell') + cy.wait('@getWellDetails') }) - it('should render the well show page UI without errors', () => { - cy.get('[data-testid="ocotillo-map-container"]', { timeout: 20000 }).should( - 'exist' - ) + it('renders the current well detail UI with core cards and related data', () => { + cy.contains('h3', wellOne.name).should('be.visible') + cy.contains(wellOne.site_name).should('exist') + cy.contains(wellOne.monitoring_status).should('exist') + cy.contains(wellOne.well_status).should('exist') + + cy.contains('Hole Depth').should('be.visible') + cy.contains('210 ft').should('be.visible') + cy.contains('Well Depth').should('be.visible') + cy.contains('198 ft').should('be.visible') + cy.contains('Measuring Point').should('be.visible') + cy.contains('Top of casing | 2.5 ft').should('be.visible') + + cy.get('[data-testid="ocotillo-map-container"]', { + timeout: 20000, + }).should('exist') + cy.contains('Hydrograph').should('exist') + cy.contains('Recent Water Level Observations').should('exist') + cy.contains('Alternate IDs').should('exist') + + cy.contains(contactOne.name).should('exist') + cy.contains(contactOne.organization).should('exist') + cy.contains(contactOne.emails[0].email).should('exist') + cy.contains(projectAlpha.name.toUpperCase()).should('exist') }) }) diff --git a/cypress/support/ocotillo-fixtures.ts b/cypress/support/ocotillo-fixtures.ts new file mode 100644 index 00000000..c30408f3 --- /dev/null +++ b/cypress/support/ocotillo-fixtures.ts @@ -0,0 +1,288 @@ +/// + +export const projectAlpha = { + id: 10, + name: 'Rio Grande Monitoring', + description: 'Long-term water level monitoring in the middle Rio Grande.', + group_type: 'Project', + release_status: 'public', + parent_group_id: null, + project_area: null, + well_count: 2, + created_at: '2026-01-05T12:00:00Z', + created_by_name: 'Data Team', +} + +export const projectBeta = { + id: 11, + name: 'Chuska Reconnaissance', + description: 'Reconnaissance wells near Chuska.', + group_type: 'Project', + release_status: 'draft', + parent_group_id: null, + project_area: null, + well_count: 1, + created_at: '2026-02-10T12:00:00Z', + created_by_name: 'Field Team', +} + +export const wellOne = { + id: 1, + name: 'RG-001', + site_name: 'Rio Grande Site 1', + created_at: '2026-01-15T12:00:00Z', + release_status: 'public', + thing_type: 'water well', + location_id: 101, + monitoring_status: 'Active', + well_status: 'In use', + hole_depth: 210, + hole_depth_unit: 'ft', + well_depth: 198, + well_depth_unit: 'ft', + well_completion_date: '2025-12-10', + well_driller_name: 'Mesa Drilling', + measuring_point_description: 'Top of casing', + measuring_point_height: 2.5, + measuring_point_height_unit: 'ft', + first_visit_date: '2026-01-20', + groups: [projectAlpha], + contacts: [ + { + id: 1, + name: 'Alex Contact', + organization: 'NMBGMR', + role: 'Owner', + contact_type: 'Primary', + release_status: 'public', + }, + ], + aquifers: [ + { aquifer_system: 'Santa Fe Group', aquifer_types: ['basin fill'] }, + ], + alternate_ids: [ + { + id: 1001, + alternate_organization: 'USGS', + alternate_id: '08300000', + relation: 'site id', + }, + ], + current_location: { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-106.65, 35.08, 5000], + }, + properties: { + elevation: 5000, + elevation_unit: 'ft', + }, + }, +} + +export const wellTwo = { + id: 2, + name: 'RG-002', + site_name: 'Rio Grande Site 2', + created_at: '2026-01-20T12:00:00Z', + release_status: 'public', + thing_type: 'water well', + location_id: 102, + monitoring_status: 'Inactive', + well_status: 'Plugged', + hole_depth: 150, + hole_depth_unit: 'ft', + well_depth: 140, + well_depth_unit: 'ft', + groups: [projectAlpha, projectBeta], + contacts: [], + current_location: { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [-106.7, 35.12, 5020], + }, + properties: { + elevation: 5020, + elevation_unit: 'ft', + }, + }, +} + +export const contactOne = { + id: 1, + name: 'Alex Contact', + organization: 'NMBGMR', + role: 'Owner', + contact_type: 'Primary', + release_status: 'public', + created_at: '2026-01-01T00:00:00Z', + things: [wellOne], + phones: [ + { + id: 1, + contact_id: 1, + phone_type: 'Primary', + phone_number: '5055551212', + release_status: 'public', + created_at: '2026-01-01T00:00:00Z', + }, + ], + emails: [ + { + id: 1, + contact_id: 1, + email_type: 'Primary', + email: 'alex@example.org', + release_status: 'public', + created_at: '2026-01-01T00:00:00Z', + }, + ], + addresses: [ + { + id: 1, + contact_id: 1, + address_type: 'Mailing', + address_line_1: '801 Leroy Place', + city: 'Socorro', + state: 'NM', + postal_code: '87801', + country: 'USA', + release_status: 'public', + created_at: '2026-01-01T00:00:00Z', + }, + ], +} + +export const contactTwo = { + id: 2, + name: 'Jordan Manager', + organization: 'Bureau of Geology', + role: 'Manager', + contact_type: 'Secondary', + release_status: 'public', + created_at: '2026-02-01T00:00:00Z', + things: [wellTwo], + phones: [], + emails: [], + addresses: [], +} + +const listResponse = (items: unknown[]) => ({ + items, + total: items.length, +}) + +export const interceptOcotilloListFixtures = () => { + cy.intercept('GET', 'http://localhost:8000/thing/water-well*', (req) => { + const query = String(req.query.name_contains ?? '').toLowerCase() + const groupFilter = ([] as string[]).concat(req.query.filter ?? []) + const wantsProjectAlpha = groupFilter.some( + (filter) => filter.includes('"groups"') && filter.includes('"10"') + ) + + let wells = [wellOne, wellTwo] + if (wantsProjectAlpha) + wells = wells.filter((well) => well.groups.some((g) => g.id === 10)) + if (query) + wells = wells.filter((well) => well.name.toLowerCase().includes(query)) + + req.reply({ statusCode: 200, body: listResponse(wells) }) + }).as('getWells') + + cy.intercept('GET', 'http://localhost:8000/group*', { + statusCode: 200, + body: listResponse([projectAlpha, projectBeta]), + }).as('getProjects') + + cy.intercept('GET', 'http://localhost:8000/contact*', { + statusCode: 200, + body: listResponse([contactOne, contactTwo]), + }).as('getContacts') +} + +export const interceptWellShowFixtures = () => { + cy.intercept('GET', 'http://localhost:8000/thing/water-well/1/details**', { + statusCode: 200, + body: { + well: wellOne, + contacts: [contactOne], + sensors: [], + deployments: [], + well_screens: [], + field_events: [], + first_field_event: null, + }, + }).as('getWellDetails') + + cy.intercept('GET', 'http://localhost:8000/asset*', { + statusCode: 200, + body: listResponse([]), + }).as('getWellAssets') + + cy.intercept('GET', 'http://localhost:8000/thing/1/id-link*', { + statusCode: 200, + body: listResponse(wellOne.alternate_ids), + }).as('getWellIdLinks') + + cy.intercept('GET', 'http://localhost:8000/observation/groundwater-level*', { + statusCode: 200, + body: listResponse([]), + }).as('getGroundwaterLevels') + + cy.intercept( + 'GET', + 'http://localhost:8000/observation/transducer-groundwater-level*', + { + statusCode: 200, + body: listResponse([]), + } + ).as('getTransducerGroundwaterLevels') +} + +export const interceptProjectShowFixtures = () => { + cy.intercept('GET', 'http://localhost:8000/group/10**', { + statusCode: 200, + body: projectAlpha, + }).as('getProject') + + cy.intercept('GET', 'http://localhost:8000/thing/water-well*', { + statusCode: 200, + body: listResponse([wellOne, wellTwo]), + }).as('getProjectWells') +} + +export const interceptContactShowFixtures = () => { + cy.intercept('GET', 'http://localhost:8000/contact/1**', { + statusCode: 200, + body: contactOne, + }).as('getContact') + + cy.intercept('GET', 'http://localhost:8000/thing/1**', { + statusCode: 200, + body: wellOne, + }).as('getAssociatedWell') + + cy.intercept('GET', 'http://localhost:8000/observation/groundwater-level*', { + statusCode: 200, + body: listResponse([ + { + id: 501, + sample_id: 701, + observation_datetime: '2026-03-01T10:00:00Z', + depth_to_water_bgs: 42.5, + }, + ]), + }).as('getContactWellObservations') + + cy.intercept('GET', 'http://localhost:8000/sample/701**', { + statusCode: 200, + body: { + id: 701, + sample_date: '2026-03-01T10:00:00Z', + sampler_name: 'Field Staff', + contact: contactOne, + }, + }).as('getContactWellSample') +} diff --git a/docs/BDMS-878-inline-grid-plan.md b/docs/BDMS-878-inline-grid-plan.md new file mode 100644 index 00000000..570fa5b1 --- /dev/null +++ b/docs/BDMS-878-inline-grid-plan.md @@ -0,0 +1,100 @@ +# BDMS-878 — Inline Spreadsheet Grid for Geothermal Data Upload + +**Status:** Proposed approach / WIP +**Scope (corrected):** A spreadsheet-like grid to review, correct, and enter **Geothermal** data directly into Ocotillo — no separate upload step, keyboard navigation per spreadsheet conventions. + +--- + +## 1. Goal + +Give a data manager an inline Glide DataGrid to edit existing geothermal records and enter new ones, writing straight through to the geothermal API. Replace the current round-trip through external spreadsheets + Python scripts. + +Acceptance criteria mapped to work: + +| AC | Delivered by | +|----|--------------| +| Edit data inline from a grid, spreadsheet-like | Phase 2 (editable Glide grid on real data) | +| Changes saved directly to Ocotillo, no separate upload step | Phase 3 (write-back to provider `create`/`update`) | +| Keyboard navigation per spreadsheet conventions | Glide built-in (arrows, tab, enter, copy/paste) — verified Phase 2 | + +--- + +## 2. Current state (recon findings) + +**Assets that exist:** +- `@glideapps/glide-data-grid@^6.0.3` already a dependency. +- Working Glide patterns in the temporary demo `src/pages/example/DataGridPage.tsx` — cell kinds, `onCellEdited`, light/dark theme hook (`useGdgTheme` off `ColorModeContext`), ResizeObserver sizing, a bulk-add modal. **But** all write-back / CSV / submit in the demo are stubs (local state only). +- Geothermal data provider `src/providers/geothermal-data-provider.ts` already has `getList`, `getMany`, `getOne`, `create` (POST `{resource}`), `update` (PATCH `{resource}/{id}`). Registered as `geothermal` in `src/AppProviders.tsx:82`. +- Geothermal resources/pages scaffolded: `wells` list/show (`src/pages/geothermal/wells/`), well records shown via nested endpoint `wells/{id}/records`. + +**Gaps that block anything from rendering (fix first):** +1. `geothermalResources` is **not** registered in the resources barrel `src/resources/index.tsx` (only ocotillo is spread in). +2. `GeothermalRoutes` (`src/routes/geothermal.tsx`) is **never mounted** in `src/App.tsx` — geothermal UI is currently unreachable. +3. No `create`/grid route registered for geothermal. + +**Data-model gaps:** +- `src/interfaces/geothermal/IWell.ts` has only `OBJECTID`, yet pages already read `WellDataID`, `County`. Interface under-specifies the real API shape. +- `IWellRecord.ts` = 11 `string` fields (`API_suffix`, `ActionDate`, `Comments`, `EnteredBy`, `EntryDate`, `OBJECTID`, `RecrdSetID`, `SourceID`, `WellDataID`, `WellName`, `WellNumber`). No OpenAPI codegen for geothermal — columns must be authored by hand from the true API contract. + +**Provider gaps vs Ocotillo:** +- Geothermal provider uses bare `fetch` with **no auth** (no Bearer token). Ocotillo injects a token + refresh. → confirm whether the geothermal write API requires auth. +- Geothermal `getList` returns a bare array (`total = data.length`), not an `{items,total}` envelope — fine for a grid, but no server-side paging/total. +- No Pydantic 422/409 → refine `fieldErrors` mapping (Ocotillo has one at `ocotillo-data-provider.ts:190-224`, copyable) — needed for inline cell validation feedback. + +**Access control:** +- `canEditGeothermal` = Editor|Admin; `canManageGeothermal` = Admin only. +- Discrepancy: `canAccessResource` policy requires **Admin** for `create`, **Editor** for `edit`. So a grid that *creates* new rows is admin-only under current policy, editing existing rows is editor-ok. Must resolve before gating the "enter new records" path. + +--- + +## 3. Decisions + +1. **Auth — YES.** The geothermal write API requires a token. The geothermal provider must get the same Bearer-token + refresh interceptor as Ocotillo before any save works. This is a prerequisite for Phase 3. +2. **Role — ADMIN.** Both editing and entering rows in the grid gate on `Geothermal.Admin` (`canManageGeothermal`). No editor-level access to the grid. Resolves the earlier helper-vs-policy discrepancy in favor of admin-only. +3. **Save — EXPLICIT BATCH.** No per-cell autosave. Edits accumulate in grid state; a "Save changes" action flushes all dirty rows. No server bulk endpoint → client-side loop over single-record `create`/`update` with per-row success/error tracking. + +### Still open (deferred, not blocking scaffolding) +- **Target entity** — `wells` vs nested **well records** (`wells/{id}/records`). Deferred; decide before Phase 2 column authoring. +- **API contract** — true field list/types/validation. Deferred; needed before Phase 2. + +--- + +## 4. Proposed approach — phased + +### Phase 0 — Wiring & auth (unblock) +- Register `geothermalResources` in `src/resources/index.tsx`; mount `GeothermalRoutes` in `src/App.tsx`; add a grid route (e.g. `/geothermal/wells/grid` or a `create`/`bulk` route). +- **Add Bearer-token + refresh interceptor to the geothermal provider** (auth = YES). Convert it to the Ocotillo axios pattern, or inject `getAccessToken()` into the existing `fetcher` headers + wire `axios-auth-refresh`-equivalent. Prerequisite for all saves. +- Contract/target entity deferred — correct `IWell`/`IWellRecord` interfaces once decided (before Phase 2). + +### Phase 1 — Extract a reusable grid component +- Lift the Glide patterns out of the throwaway `DataGridPage.tsx` into a reusable, entity-agnostic `EditableDataGrid` under `src/components/` (or `src/components/grid/`): theme hook, ResizeObserver sizing, cell-kind dispatch, `onCellEdited`, column-def model. +- Keep it typed/generic over a row shape + a column spec (id, title, editable, cell kind, validator). +- The temporary demo stays as reference until this lands, then delete it. + +### Phase 2 — Read + inline edit existing geothermal records +- New page (e.g. `src/pages/geothermal/wells/grid.tsx`) using `EditableDataGrid` fed by `useList`/`useDataGrid` with `dataProviderName: 'geothermal'` on the target resource. +- Editable columns per the real schema; read-only for keys/IDs. Verify keyboard nav (arrows/tab/enter, copy/paste) — Glide gives this for free. +- Gate render + edit on **`canManageGeothermal` (Admin)** via `` / capability check. +- `onCellEdited` writes to local grid state + marks the row dirty. **No API call here** (batch save, not autosave). + +### Phase 3 — Explicit batch save (no upload step) +- Track dirty rows in grid state. A **"Save changes"** action flushes them: client-side loop over provider `update` (PATCH) for existing rows, `create` (POST) for new rows. No server bulk endpoint exists. +- Per-row success/error tracking; optimistic UI with rollback on failed rows; keep dirty state for rows that failed so the user can retry. Show a summary (n saved / m failed). +- Add Pydantic 422/409 → `fieldErrors` mapping to the geothermal provider (copy Ocotillo pattern `ocotillo-data-provider.ts:190-224`) so failed cells surface inline. + +### Phase 4 — Enter new records (bulk-add) +- Adapt the demo's `BulkAddModal` (blank N-row grid, grouped columns, paste-from-Excel) to the geothermal entity. New rows join the dirty set and flush through the same Phase 3 batch save (`create`). +- Gate on **`canManageGeothermal` (Admin)**. +- Real paste-from-Excel/CSV: Glide's built-in copy/paste covers cell-range paste; a CSV import button would need a parser (no `papaparse`/`xlsx` in deps yet) — treat as stretch. + +--- + +## 5. Risks / notes +- Demo `DataGridPage.tsx` advertises CSV upload / "Open in Google Sheets" / "Create Wells" — all **non-functional placeholders**. Do not assume they work. +- No server-side bulk endpoint → large uploads become N single requests; consider a real batch endpoint on the API side if volume is high. +- Geothermal provider currently unauthenticated — a write path without auth is a security gap; confirm before shipping any save. + +--- + +## 6. First concrete deliverable (suggested MVP) +Phase 0 (wiring + geothermal auth interceptor) → Phase 1 (extract reusable `EditableDataGrid`) → Phase 2 (read/inline-edit existing rows, admin-gated) → Phase 3 (explicit batch save). Target entity + true column schema get pinned before Phase 2 authoring. diff --git a/docs/access-control-ruleset.md b/docs/access-control-ruleset.md index b63825bc..4da44649 100644 --- a/docs/access-control-ruleset.md +++ b/docs/access-control-ruleset.md @@ -45,7 +45,6 @@ This mapping is temporary and should be removed before the v1 release once all u - Can view confidential data ### AMP.Admin Only -- Can access `Sandbox` - Can access unfinished / WIP AMP resources ### Geothermal Roles @@ -73,8 +72,7 @@ This mapping is temporary and should be removed before the v1 release once all u - `ocotillo.thing-well-batch-export` -> AMP view access required - `ocotillo.groundwater-level-observation` -> AMP view access required -### AMP / Sandbox -- `Sandbox` -> `AMP.Admin` only +### AMP / Water - `water.*` -> AMP access required - WIP `water.*` resources -> `AMP.Admin` only - `water.locations` -> `AMP.Admin` or `Geothermal.Admin` diff --git a/docs/adr/0001-contextual-media-mapping.md b/docs/adr/0001-contextual-media-mapping.md new file mode 100644 index 00000000..a3d67040 --- /dev/null +++ b/docs/adr/0001-contextual-media-mapping.md @@ -0,0 +1,703 @@ +# ADR 0001 — Contextual Media Mapping (non-well photographs) + +**Status:** Proposed +**Ticket:** [BDMS-901](https://nmbgmr.atlassian.net/browse/BDMS-901) (epic: BDMS-815 Ocotillo Feature Requests) +**Date:** 2026-08-06 +**Author:** Jake Ross +**Requester:** Stacy Timmons (raised 2026-05-05, AMP/WDI Coordination meeting) +**Deciders:** Data owner (Stacy Timmons), Ocotillo product (Liz Lyons), backend; Amy Trivitt if the Photo Archive route is taken + +--- + +## 1. Context + +### The request + +Field and office staff accumulate photographs that are **not of a well**: +landscapes, outcrops, people working, site context, regional scenery. They are +wanted for reports, guidebooks, and other publications. The retrieval pattern is +by **place and subject**, not by well: + +- "I want all the photos in the Taos area." +- "I want photos of the salt basin." + +Three separable capabilities are being asked for: + +| # | Capability | Ocotillo today | +|---|---|---| +| C1 | **Store** a photo that has no well | Partial — uploadable with no `thing_id`, lands in an orphan bucket | +| C2 | **Describe** it (place, subject, date, photographer, rights) | No — none of these fields exist | +| C3 | **Retrieve** it by area or subject, in bulk, for publication | No — assets are only queryable by `thing_id` | + +### Ocotillo's media model today + +- `AssetResponse` ([src/generated/types.gen.ts:56](src/generated/types.gen.ts:56)): + `name`, `label`, `storage_path`, `mime_type`, `size`, `uri`, `id`, + `created_at`, `release_status`, `storage_service`, `signed_url`. That is the + entire descriptive surface. +- `CreateAsset` ([src/generated/types.gen.ts:240](src/generated/types.gen.ts:240)) adds exactly + one relationship: `thing_id?: number | null`. +- `GET /asset` ([src/generated/types.gen.ts:4025](src/generated/types.gen.ts:4025)) accepts exactly + one filter: `thing_id`, plus `page`/`size`. +- Assets surface in two places: the per-well **Attachments** card + ([src/components/WellShow/Attachments.tsx](src/components/WellShow/Attachments.tsx)) and the + **Unassociated Assets** list + ([src/pages/ocotillo/asset/unassociated.tsx](src/pages/ocotillo/asset/unassociated.tsx)), which exists to + *drain* the orphan bucket onto wells rather than to curate a standing + collection. +- Global search indexes assets by file path and related well names only + ([docs/search.md](docs/search.md)). A photo of the Taos gorge with a + camera-generated filename is unfindable by design. +- There is no tag model, no asset geometry, no capture date (`created_at` is + upload time), no photographer, and no rights field. `release_status` is a + lifecycle enum, not a publication licence. + +**Ocotillo currently models media as an attachment to a thing. The request is +for media as a first-class, place-anchored, subject-tagged object.** Those are +different data models, not a missing filter. + +### Product context + +Ocotillo's intended scope is a **holistic data management platform for the +Bureau — the model being USGS ScienceBase**: a catalogue of heterogeneous +items, each carrying arbitrary descriptive metadata, attached files, spatial +footprints, and access controls, discoverable through faceted and spatial search +over an API. + +This matters for the decision, and §3/D4 develops it. Under that vision, a +place-anchored, subject-tagged asset catalogue is not scope creep — it is a +component of the target architecture that has not been built yet. + +### Stack facts that constrain the options + +- Frontend: React + Refine + MUI, served as static files from **Google App + Engine** ([app.yaml](app.yaml)). +- Backend: FastAPI, Postgres. +- Object storage is already pluggable — `storage_service` is typed + `'gcs' | 's3' | string` + ([src/interfaces/ocotillo/SearchResult.ts:19](src/interfaces/ocotillo/SearchResult.ts:19)) — and asset + reads go through 15-minute signed URLs. +- We therefore already operate a bucket, a signed-URL pattern, and an + OpenAPI-typed client generator. + +--- + +## 2. The incumbent: photoarchive.nmt.edu + +The Bureau operates — the "NMBGMR Photo & +Document Archive," running **ResourceSpace**. Observed 2026-08-06 by loading it +anonymously: + +- **Public.** Content browses and searches with no login. +- **Populated and curated.** Featured collections include *Photo Archive* + ("Historical photos of fieldwork, mining, and bureau publication photos"), + *Historic Document Archive*, *Sample Data Repository*, *Subsurface Library + Logs*, *Core Repository*, **Fieldwork**, *Thin Sections*, *Chip Sets*, + *Sample Descriptions*, *Hand Samples*, *SOP Documentation*. +- **Geographic search works** — `/pages/geo_search.php`, a Leaflet map with + "Drag to select a search area." +- **Tag browse, advanced search, and workflow states** are all present. +- **Bulk tooling exists** — search results offer *Edit all resources* and *CSV + Export - metadata*, with page sizes to 240. + +On capability alone this answers BDMS-901. The constraints are what make it +complicated. + +### 2.1 Ownership, support, and cost + +**The Photo Archive is owned by Amy Trivitt. Additional support is provided +through the Bureau's ICASA database maintenance contract.** + +This is a real and ongoing cost to the Bureau, and it should not be treated as +free simply because it does not appear on the Ocotillo team's budget line. Three +consequences follow, and they bear directly on the options in §5: + +1. **The cost is real, just borne elsewhere.** "Use the existing system" is not a + zero-cost option; it is a decision to keep paying an existing cost and to add + load to it. Any comparison that scores ResourceSpace as "no operational + burden" is wrong. +2. **A named owner has finite capacity.** Routing a new stream of AMP/WDI field + photography at the archive is a request on one person's time, not on an + abstract service. +3. **Contract-mediated support constrains the change cycle.** When meaningful + changes require scoping against a maintenance contract, iteration is slow and + discretionary work is expensive. This is a structural explanation for the + friction described below — it is not a system one iterates with, and + "customization is not permitted" is the natural consequence of that support + model rather than an arbitrary policy. + +### 2.2 Constraints + +Reported by the Ocotillo team: + +1. **No customization.** No custom metadata fields, no schema changes, no + plugins — so no "Ocotillo thing ID" field and no AMP/WDI-specific structure. +2. **Serious friction uploading content.** +3. **Serious friction integrating programmatically.** + +Constraint 1 would be survivable on its own: what BDMS-901 asks for is stock +ResourceSpace. Constraints 2 and 3 are different in kind, because ingest and +retrieval *are* the request. §2.4 measures both. + +### 2.3 The decisive data point + +| Query | Results | +|---|---| +| `Taos` | **340** — "View south from the Rio…", "Winter at Questa…", "Taos Plaza. Taos Pueblo…", "Frances and Dick Jahns at…", "Landslide scars in…" | +| `salt basin` | **0** | + +Same system, same working geographic search, same working tag browse. + +The 340 is what makes this informative. **Curation demonstrably happens here** — +somebody ingested, described, and keyworded hundreds of historical photographs. +The organisation is willing and able to curate. So the zero is not a motivation +gap or a staffing gap. It is the gap between *that* workflow — an archivist, +working a defined historical collection, inside their own group's tooling — and +*this* one: a field geologist with 400 photos on a phone and an SD card, outside +that group. + +> **The photos aren't in the archive because the people who take them have no +> adequate, accessible way to put them there.** + +This diagnosis determines the decision: + +| If the constraint is… | Then… | +|---|---| +| Curation staffing | Software choice barely matters. Pick the cheapest option, assign a curator, done. | +| **Tooling and access** | **Software choice is the primary lever.** Ingest ergonomics outranks every query feature, and any option that fails to put a usable path in field staff's hands fails regardless of how good its search is. | + +This ADR adopts the second reading, and §2.4 substantiates it. + +### 2.4 Measured performance + +Measured 2026-08-06 from one client, 2–3 repetitions, single location and time +of day. Magnitudes are solid; exact figures are indicative. + +**Baselines:** DNS 4 ms · TCP connect 65 ms · TLS handshake 220 ms. A static +thumbnail from `/filestore/` returns in **85 ms** warm, 286 ms cold. The network +path and file server are both healthy. + +**Every dynamic request costs ~2.3–2.5 s regardless of what it does:** + +| Request | Response size | Time | +|---|---|---| +| `/filestore/…145thm_….jpg` (static) | 7 KB | **85 ms** | +| `/pages/ajax/reload_searchbar.php` | 8 KB | 2.31 s | +| `/api/?function=…` → `401 Invalid signature` | **17 B** | **2.23–2.50 s** | +| `/pages/search.php?search=zzqqxx` (0 results) | 69 KB | 2.37–2.41 s | +| `/pages/geo_search.php` | 88 KB | 2.32–2.34 s | +| `/pages/home.php` | 66 KB | 2.34–2.44 s | +| `/pages/search.php?search=Taos` (340 results) | 237 KB | 2.49–2.53 s | +| `/pages/search.php?search=Taos&per_page=240` | 862 KB | 2.94 s and 10.0 s | + +**A 17-byte error response takes 2.25 seconds.** The latency is not query cost, +result-set size, payload, or network — it is a **fixed ~2.2 s bootstrap tax on +every dynamic request**. A zero-result search costs the same as a 340-result +one. The server reports `Microsoft-IIS/10.0`. + +**Full page load** (`search.php?search=Taos`, warm cache, Navigation Timing): + +- TTFB **3.10 s**, response complete 3.32 s +- **DOMContentLoaded / load: 5.76 s** +- 126 subresources — 36 scripts, 25 stylesheets, 50 images, 4 XHR, each XHR + paying the full ~2.3 s tax again + +**Cold first visit** adds the bot gate below: interstitial (~2.5 s) + a +hard-coded 1 s delay + full reload (~5.8 s) ≈ **9 s before anything usable +appears.** + +#### The browser-check gate + +Every `/pages/*` URL requested without JavaScript returns a 1,217-byte +interstitial — "Performing browser checks…" — that computes a +`browser_check_cookie` in obfuscated JS and reloads after a fixed 1 s delay. +Plain HTTP clients (`curl`, `axios`, `requests`) receive this indefinitely and +never reach content. **This is the mechanical explanation for the reported +integration friction:** scripting against the HTML interface cannot work. + +Two qualifications, both established by testing: + +- **`/api/` is exempt.** Plain curl with no JS and no cookie reaches it and + receives a genuine `401 Invalid signature`. The ResourceSpace API is enabled + and reachable — it needs a key, not a workaround. +- **CORS is locked to its own origin** + (`access-control-allow-origin: https://photoarchive.nmt.edu`). Ocotillo cannot + call it from the browser; integration must be **server-to-server** through the + FastAPI backend. + +#### Interpretation + +1. **The friction is real, measurable, and structural** — not a training + problem. A tool where every click costs 2.5 s, a results page takes 5.8 s, + and a first visit takes ~9 s will not attract someone with 400 photos to + upload. +2. **Bulk work is worst hit.** The 240-per-page view — where a real curation + session would live — returns 862 KB and took 2.9 s and 10.0 s on consecutive + tries. A field season's metadata work means hundreds of such round trips. +3. **Integration is possible but constrained:** server-to-server, keyed, ~2.5 s + per call. Adequate for a nightly sync; not for a live "related media" panel. +4. **The cause is likely fixable, and not by us.** A fixed per-request bootstrap + cost alongside fast static delivery points at PHP process startup, opcode + caching, session handling, or the IIS/PHP configuration — not at capacity. + Per §2.1, however, acting on it means scoping work against the ICASA + contract, which is precisely the slow, expensive path. + +--- + +## 3. Decision drivers + +- **D1 — Retrieval by place is the requirement.** Ocotillo cannot do it at all. + Whatever wins must answer a bbox or named-place query. +- **D2 — Publication use implies rights management.** Guidebooks and reports + need photographer, credit line, and usage terms. Legal exposure, not polish. +- **D3 — The binding constraint is the absence of adequate, accessible tooling + for staff** (§2.3, §2.4). **Adequate**: describing a photo costs seconds, not + minutes; bulk operations exist; EXIF/GPS is harvested rather than retyped. + **Accessible**: the person who took the photo can deposit it themselves, + today, without an account request or a gatekeeper. Rank every option on this + first. +- **D4 — A described, place-anchored asset catalogue is on-mission for + Ocotillo.** Ocotillo is intended as a holistic Bureau data management platform + on the ScienceBase model (§1). ScienceBase is precisely a catalogue of items + with arbitrary metadata, attached files, spatial footprints, faceted and + spatial search, permissions, and an API — and USGS built it rather than + delegating that role to a digital asset manager. Under this vision, C1–C3 are + **capabilities the platform is expected to have**, not a foreign concern + bolted onto a well database. This driver argues *for* Ocotillo holding this + data, and it is the strongest single argument in the document. +- **D5 — One home per asset.** Two systems both claiming "the photos" guarantees + drift, duplicate storage, and "which copy is current?" Combined with D4, this + argues that the home should be the platform intended to be holistic. +- **D6 — Some non-well photos are scientific context.** The access road, the + wellhead surroundings, the crew installing a transducer — that is evidence + about a site visit, and Ocotillo is the only system that holds it in context. +- **D7 — Integration cost is a first-class criterion.** Any option must be + reachable from an OpenAPI-typed FastAPI/React stack. Per §2.4, the incumbent + is reachable only server-to-server, keyed, at ~2.5 s per call. +- **D8 — Total Bureau cost, not team cost.** Per §2.1, ResourceSpace consumes + Amy Trivitt's time plus ICASA contract capacity. Options must be compared on + what the Bureau spends and on who is blocked, not on which budget line the + cost appears against. +- **D9 — Build cost and permanence are real.** D4 makes an Ocotillo catalogue + on-mission; it does not make it small. §8 is genuine multi-sprint work across + backend, frontend, and search, and every future asset change will carry these + use cases as constraints. ScienceBase is a substantial system with a + substantial team behind it. + +--- + +## 4. The boundary rule + +Independent of which system holds the files. "Not of the well" is used for two +different things: + +- **Not of the well, and not about any site** — landscapes, regional scenery, + outcrops, people working, "the Taos area" → the general asset catalogue. +- **Not of the wellhead, but about a specific site or visit** — the access road, + surrounding terrain, the crew installing equipment at a named well → attached + to the well or field activity. This is provenance (D6). + +Instructing staff that "non-well photos go elsewhere" without this distinction +would lose site-context photography that has real scientific value. + +This rule is a deliverable of this ADR alongside any code, and it can be written +and socialised immediately. + +--- + +## 5. Options + +### Option A — Extend Ocotillo into a described, place-anchored asset catalogue + +Add tags, geometry, capture date, photographer, rights, and spatial/faceted +browse to the asset model. Sketch in §8. + +- **Pros:** **on-mission (D4)** — this is a component of the ScienceBase-style + platform Ocotillo is meant to be, not a diversion from it, and USGS's own + answer to the same problem was to build rather than delegate. Total schema + control, which no external system offers. No cross-team dependency and no + contract-mediated change cycle (D8). Nothing to integrate (D7). Media sits + beside the scientific record; one login, one UI, one search. Ingest ergonomics + are entirely within our control, which is the only way to guarantee D3 is + actually met. +- **Cons:** substantial, permanent build and maintenance (D9). Adds a second + Bureau-level home for photographs unless the relationship with the Photo + Archive is explicitly settled. Does not by itself solve the historical + backlog. + +### Option B — Fix the ResourceSpace relationship + +Keep photoarchive.nmt.edu and address constraints 2 and 3: a bulk-ingest path, +upload accounts for AMP/WDI staff, an API key, and the §2.4 latency. + +- **Pros:** no new system, and everything is already live and public, so outside + requesters get a URL rather than asking staff for an export. Preserves a + working archive with real curated content. +- **Cons:** dependent on Amy Trivitt's capacity and on ICASA contract scope + (D8), which makes the change cycle slow and each change discretionary. + Constraint 1 persists even in the best case — no custom fields, ever, so no + Ocotillo linkage and no AMP/WDI-specific structure. Leaves C1–C3 outside the + platform that is supposed to be holistic (D4). +- **Access is not tooling (D3).** Handing staff logins to the same system + addresses *accessible* and leaves *adequate* untouched. Given §2.4, accounts + alone change nothing; the per-request latency must be fixed too, and per §2.1 + that fix runs through the contract. + +### Option C — Adopt a third-party DAM that Ocotillo can integrate with + +Stand up a separate system chosen for ingest ergonomics and API quality. +Candidates in §6. + +- **Pros:** better ingest and integration than the incumbent without building a + catalogue from scratch. Most candidates are a deployment and a configuration. +- **Cons:** a third home for Bureau imagery, with its own operations, patching, + backups, and cost (D8). Cuts directly against D4 and D5 — it moves platform + capability *out* of the platform. Weakest strategic fit of the three, and + worth pursuing mainly as a component or a fallback rather than as a + destination. + +### Option D — The public website + +Publish curated galleries as web content. + +- **Pros:** no new systems; adequate for a hand-picked "best of" set. +- **Cons:** a publishing surface, not a repository. No queryable metadata, no + embargo handling, no bulk retrieval, no provenance. Useful downstream of + whatever wins, not instead of it. + +### Option E — Capture and describe in Ocotillo, decide custody separately + +Treat "how do photos get described?" and "where do photos ultimately live?" as +separate questions, and answer the first one now. + +Ocotillo becomes the **capture surface**: bulk drop-upload, EXIF/GPS harvested +automatically, place and subject applied across a selection in one action, +photographer defaulted from the logged-in user. Where the files ultimately live +— Ocotillo's own catalogue (A), the Photo Archive (B), or a third-party DAM (C) +— stays open. + +- **Pros:** attacks *adequate* and *accessible* simultaneously (D3), in the one + place where we control the ergonomics completely. **Metadata is cheapest at + the moment of capture and grows more expensive monotonically afterwards** — + EXIF GPS and timestamp are in the file now, the photographer is known now, and + which basin it is is in someone's head now; a week later that costs an + interview, a year later it is unrecoverable. It is the first increment of + Option A under any reading, so no work is wasted, and it composes with B and C + as a push source if either is chosen instead. +- **Cons:** on its own it defers the custody question rather than answering it, + so the §4 boundary rule must be crisp from day one. If custody lands outside + Ocotillo, a push integration is still required (D7). + +--- + +## 6. Reference model and third-party candidates + +### ScienceBase as the reference model + +Since Ocotillo's target is a ScienceBase-style platform, it is worth naming what +that model actually provides, because it is close to a specification for Option +A: + +- items with **arbitrary descriptive metadata**, not a fixed schema; +- **file attachments** on items, with the item — not the file — as the unit of + description; +- **spatial footprints** and map-based discovery; +- **hierarchical collections** and faceted browse; +- **permissions** per item and per collection; +- a **REST API** as a first-class interface, not an afterthought. + +Mapped onto §8: tags and place are the metadata layer, `asset.geometry` is the +footprint, collections are the hierarchy, `release_status` plus +`asset.usage_rights` are the permission and rights layer, and the API extensions +are the interface. The notable point is that USGS built this rather than +adopting a DAM — the same conclusion D4 points at here. + +### Third-party candidates (Option C, or as components) + +> Product details are from general knowledge; licences and feature sets change. +> Verify current terms before committing. The evaluation criteria are the +> durable part. + +**Directus** — open-source (BSL) headless data platform, Postgres-backed. An +asset library where **you define the metadata fields**, with REST and GraphQL +over a real OpenAPI spec, so Ocotillo's existing `openapi-ts` generator produces +a typed client for free (D7). GCS and S3 adapters reuse the bucket pattern +already in place; on-the-fly transforms replace the derivative work in §8; bulk +upload and bulk edit address D3. Watch the Business Source licence (free below a +revenue threshold a state agency clears comfortably — confirm), and note that +geographic query is a PostGIS concern with the map UI still ours to build. +**Most interesting not as a separate archive but as a possible implementation +substrate for Option A's metadata layer**, which would trade build effort for an +operational dependency. + +**Cloudinary** — SaaS media API. Zero operations, strong delivery and +transforms, structured metadata, a real search API, and **AI auto-tagging**, +which attacks D3 in a way no self-hosted option does. Usage-based cost scaling +with bandwidth, and not an archival or preservation system, so originals should +remain in the Bureau's own bucket. Best considered as a delivery and +enrichment layer over our own storage rather than as the system of record. + +**STAC + `stac-fastapi`/`pgstac`** — the request is fundamentally spatiotemporal +asset search, and STAC makes "all photos in the Taos area between these dates" a +first-class standards-based query on the FastAPI/PostGIS stack the backend +already runs. But it is a catalog spec, not a DAM: no upload UI, no curation +workflow, no bulk editor, no rights. Excellent on D1 and D7, contributes nothing +to D3. Worth considering as an **interoperability layer over Option A**, not as +a store. + +**Omeka S** — digital-collections publishing with item sets, Dublin Core, linked +data, a REST API, and a mapping module. Curation-first rather than +bulk-ingest-first. Good for *published collections*, weaker as a working +repository for a field season's raw output. + +**InvenioRDM** — research data repository behind Zenodo. Relevant only if photo +sets should be **citable** (DOIs, versioning, embargoes). Heavy to operate and +deposit-oriented. + +**Lower priority:** Payload CMS (MIT, Node/TS, good if the media service should +live in a TypeScript codebase adjacent to the frontend); GeoNode +(domain-appropriate but layer-oriented and heavy); Nuxeo, Pimcore, and +CollectiveAccess (capable, high configuration burden). **Not recommended:** +Bynder, Canto, Brandfolder, Acquia DAM — brand-asset oriented, expensive, +procurement-heavy, weak on geoscience metadata. + +### Comparison + +Columns are ordered by weight. Ingest ergonomics and self-service are decisive +(D3); strategic fit reflects D4 and D5. **Operational burden is scored as cost +to the Bureau, not to the Ocotillo team** (D8). + +| | Ingest (D3) | Staff self-serve (D3) | Integration (D7) | Place query (D1) | Rights (D2) | Bureau ops cost (D8) | Strategic fit (D4/D5) | +|---|---|---|---|---|---|---|---| +| Ocotillo catalogue (A) | ✅ fully ours to design | ✅ already logged in | ✅ native | 🔴 to build | 🔴 to build | ⚠️ our build + run (D9) | ✅ **on-mission** | +| Ocotillo capture (E) | ✅ fully ours to design | ✅ already logged in | ✅ native | — deferred | — deferred | ✅ low | ✅ first increment of A | +| ResourceSpace (incumbent) | 🔴 ~2.5 s/request, 5.8 s pages | 🔴 gated by another group | 🔴 keyed + slow; HTML unscriptable | ✅ live geo search | ✅ | ⚠️ **Amy Trivitt + ICASA contract** | 🔴 capability outside the platform | +| Directus | ✅ bulk upload + bulk edit | ✅ we control accounts | ✅ OpenAPI/REST/GraphQL, GCS adapter | ⚠️ PostGIS, map UI ours | ✅ custom fields | ⚠️ another system to run | ⚠️ unless used as A's substrate | +| Cloudinary | ✅ + AI auto-tagging | ✅ we control accounts | ✅ strong API/SDKs | ⚠️ metadata-based | ⚠️ custom fields | ⚠️ usage-based spend | ⚠️ layer, not system of record | +| STAC + stac-fastapi | 🔴 no ingest UI | 🔴 none | ✅ same stack | ✅ native bbox + datetime | 🔴 | ⚠️ medium | ⚠️ layer over A | +| Omeka S | ⚠️ curation-first | ⚠️ archivist-oriented | ✅ REST API | ⚠️ mapping module | ⚠️ | ⚠️ medium | 🔴 third home | +| InvenioRDM | ⚠️ deposit-oriented | ⚠️ deposit ceremony | ✅ REST API | 🔴 | ✅ + DOIs | 🔴 high | 🔴 third home | + +--- + +## 7. Decision + +**Build the capture path now (Option E) as the first increment of Option A, and +settle the relationship with the Photo Archive in parallel.** + +1. **Adopt and publish the §4 boundary rule.** Costs nothing, depends on nobody, + prevents further orphan accumulation. Ocotillo work: an upload-time hint on + [AttachmentsUploadDialog.tsx](src/components/WellShow/AttachmentsUploadDialog.tsx) and a third + disposition on + [the Unassociated Assets page](src/pages/ocotillo/asset/unassociated.tsx), which currently + offers only attach-or-delete. + +2. **Build the capture and describe path in Ocotillo** — bulk drop-upload for + photos with no `thing_id`; **automatic EXIF extraction** (GPS → geometry, + timestamp → `captured_at`, camera and author where present); place and + subject applied across a multi-selection in one action; photographer + defaulted from the logged-in user. This is the direct answer to D3, it is + on-mission under D4, and it is the only step that depends on nobody outside + this team. It is the 🅔 subset of §8. + +3. **Settle the Photo Archive relationship, in parallel and without blocking + step 2.** Two questions for Amy Trivitt: whether AMP/WDI field photography + should be routed there at all, and whether the ~2.2 s per-request tax can be + addressed within ICASA contract scope. The answers determine whether the + Bureau ends up with one photo home or two, and that is a governance question + the data owner should decide explicitly rather than by default (D5, D8). + +4. **Complete Option A** — browse, faceted and spatial search, rights fields, + bulk export — as the platform's asset-catalogue capability, scheduled against + the broader ScienceBase-model roadmap rather than as a one-off response to + this ticket. §8 lists the work; D9 is the honest counterweight, and the + sequencing should reflect it. + +5. **Consider third-party components rather than third-party destinations.** + Directus as a possible substrate for A's metadata layer, Cloudinary as a + delivery and auto-tagging layer, STAC as an interoperability layer — each + evaluated on whether it reduces §8's build without moving platform capability + out of the platform. A standalone third-party DAM (Option C as a destination) + is the weakest strategic fit and should not be pursued unless steps 2 and 4 + both prove infeasible. + +Step 2 is deliberately not gated on step 3. Gating it would leave the actual +constraint unaddressed for the duration of a cross-team negotiation, and photos +taken in the interim would lose their cheapest-to-capture metadata permanently. + +The §4 boundary rule holds throughout: media whose subject is a well, spring, +location, or field activity attaches to that record; media whose subject is a +place or a scene goes to the general catalogue. + +--- + +## 8. Option A implementation sketch + +Items marked **🅔** are the capture slice built in step 2. They are the smaller +half and the only half that addresses D3. + +### Data model + +- 🅔 `asset.tags` — many-to-many to a controlled vocabulary. Free-text tags + become unusable within a year; a `lexicon`-style table has precedent + ([src/interfaces/ocotillo/ILexicon.ts](src/interfaces/ocotillo/ILexicon.ts)). +- 🅔 `asset.geometry` — point (optionally extent) with a spatial index, from + EXIF GPS on upload where present and manual placement otherwise. Reuse the + `LocationGeoJsonResponse` shape so existing map components consume it + unchanged. +- 🅔 `asset.captured_at` — distinct from `created_at`, which is upload time. +- 🅔 `asset.photographer` — plausibly a FK to `contact`, which already exists. +- `asset.usage_rights` — licence enum plus free-text restrictions. + `release_status` must not be overloaded for this; it is a lifecycle field, and + conflating the two produces a rights bug (D2). +- `asset.place_id` — a gazetteer of named informal areas ("Taos area", "salt + basin"). Without it, "the salt basin" is answerable only as a hand-drawn + bounding box. The component most likely to be underestimated, and the one that + most directly serves the original request. + +### API + +- Extend `GET /asset` beyond `thing_id`: `tag`, `bbox`, `place_id`, + `captured_after`/`captured_before`, `mime_type`. +- Bulk-download endpoint (zip of a selection). The publication workflow is "give + me all of these"; one-at-a-time downloads make it unusable. +- Derivative sizes (thumb / web / original). The 15-minute signed-URL pattern + needs revisiting for galleries of hundreds of images. +- Extend the search index to cover tags, captions, and place names; today it + covers storage path and related well names only ([docs/search.md](docs/search.md)). + +### UI + +- A **Media** section in navigation ([src/config/navigation.ts](src/config/navigation.ts)), + separate from the well-scoped Attachments card. +- Gallery browse with tag facets and date filter; map browse via the existing + OGC layer components; multi-select to bulk download. +- 🅔 Metadata editor: tags, place, capture date, photographer, rights. **Bulk + edit is mandatory** — per-file editing will not survive a 400-photo field + season. +- 🅔 Upload flow reworked for the no-thing case; today's path assumes a well + ([AttachmentsUploadDialog.tsx](src/components/WellShow/AttachmentsUploadDialog.tsx)) and everything + else falls into the unassociated bucket. + +--- + +## 9. Open questions + +1. **🔴 Should AMP/WDI field photography be routed to the Photo Archive at all, + and does the Bureau want one photo home or two?** For Amy Trivitt and the + data owner jointly. This is a governance decision that should be made + explicitly (D5, D8), and it determines steps 3–5 of §7. +2. **Can the ~2.2 s per-request tax be addressed within ICASA contract scope?** + Narrow and concrete: static files serve in 85 ms while a 17-byte API error + takes 2.25 s, pointing at PHP bootstrap, opcode caching, session handling, or + the IIS/PHP configuration rather than capacity. Worth asking regardless of + which option wins, since the archive continues to serve the historical + collection either way. +3. **What is the current cost and capacity of Photo Archive support?** Needed to + compare options honestly under D8 — the ICASA contract line plus Amy + Trivitt's time, against the build and run cost of §8. +4. **What else went wrong on upload?** §2.4 measures the site from outside, but + the upload path requires a login. Watch one person load a field season's + photos and time it. The answer indicates what *not* to reproduce in step 2. +5. **How does the asset catalogue fit the broader ScienceBase-model roadmap?** + If items with arbitrary metadata and spatial footprints are coming anyway, + §8's data model should be designed as the general case rather than as a + photo-specific feature (D4). This materially affects the schema. +6. **Who curates the historical backlog?** Worth naming a person, but scoped + correctly — per §2.3 this is a backlog question, not the explanation for the + `salt basin` zero going forward. It should not substitute for fixing the + tooling. +7. **Volume and backlog.** How many non-well photos exist, and where — personal + drives, shared drives, Ocotillo's unassociated bucket? Hundreds versus tens + of thousands changes the sequencing. +8. **Rights and licensing posture.** Works-for-hire owned by the Bureau? + Third-party or contributed photos with constraints? What credit line do + publications need? (D2.) +9. **Does the §4 boundary rule survive contact with staff?** Walk a real mixed + batch from a recent field season past it. If people cannot apply it + consistently, rewrite it before publishing it. + +--- + +## 10. Consequences + +**Common to every branch:** + +- Staff learn one boundary rule. It must appear at upload time or it becomes + folklore and the orphan bucket refills. +- Tooling is the constraint, so tooling work is the lever (D3). A cheap option + that leaves ingest friction in place is not actually cheap — it defers the + cost onto staff, where it is invisible and paid in photos that never get + archived. +- The Photo Archive continues to hold the historical collection and continues to + cost Amy Trivitt's time and ICASA contract capacity regardless of what + Ocotillo builds (D8). This ADR does not propose retiring it. + +**Step 2 (capture path):** field staff get a usable path in the tool they +already have open, and capture-time metadata — GPS, timestamp, photographer, +context — stops being lost. Realised regardless of how custody resolves, and it +depends on nobody else. + +**Completing Option A:** Ocotillo gains a platform capability it is expected to +have under the ScienceBase model (D4), with full schema control and no +contract-mediated change cycle. The cost is a genuine multi-sprint build and a +permanent maintenance obligation (D9), and every future asset change will carry +these use cases as constraints. The Bureau should decide deliberately whether +this makes the Photo Archive the historical-collections system and Ocotillo the +active-data platform, or whether both are expected to serve the same purpose. + +**If the Photo Archive route is taken instead:** no new system to run, and a +public URL to hand to outside requesters — but C1–C3 stay outside the platform +that is meant to be holistic, no Ocotillo linkage is possible without custom +fields, and both the latency fix and any future change run through the ICASA +contract. + +**Revisit this ADR if** the ScienceBase-model roadmap changes, if the §2.4 +latency is resolved, if Photo Archive ownership or support arrangements change, +if backlog volume proves an order of magnitude off the assumption, or if staff +cannot apply the §4 boundary rule consistently. + +--- + +## Appendix A — ADR conventions + +First ADR in this repository. Convention: `docs/adr/NNNN-kebab-title.md`, +four-digit sequence, never renumbered. Status is one of `Draft`, `Proposed`, +`Accepted`, `Rejected`, or `Superseded by NNNN`. Superseding ADRs link back; +superseded ones are kept, not deleted. + +## Appendix B — Evidence and method + +**Observed directly**, 2026-08-06, by loading photoarchive.nmt.edu anonymously +(no login, read-only): + +| Observation | Source | +|---|---| +| Runs ResourceSpace | "Powered by ResourceSpace" footer link | +| Public read access | Content browses with the "Log in" link unused | +| Geographic search | `/pages/geo_search.php` — Leaflet, "Drag to select a search area" | +| Advanced search | `/pages/search_advanced.php` | +| Tag browse, workflow states | Left navigation | +| Collections | Photo Archive, Historic Document Archive, Sample Data Repository, Subsurface Library Logs, Core Repository, Fieldwork, Thin Sections, SOP Documentation, Chip Sets, Sample Descriptions, Hand Samples | +| Bulk tooling | Search results → Actions: "Edit all resources", "CSV Export - metadata"; up to 240 per page | +| `search.php?search=Taos` | 340 results | +| `search.php?search=salt+basin` | 0 results | + +**Performance method (§2.4):** request timings via `curl` write-out (DNS, TCP, +TLS, TTFB, total) and via in-page `fetch()` with `cache: 'no-store'`, 2–3 +repetitions per endpoint; page-level figures from the Navigation Timing and +Resource Timing APIs on a warm cache. Single client, single location, one time +of day. Response headers and the browser-check interstitial were read directly +from `curl` output. No authentication was used and no access control was +circumvented; the JavaScript challenge was satisfied only by a real browser +loading the site normally. + +**Taken as given** (reported by the Ocotillo team, not independently verified): +Photo Archive ownership by Amy Trivitt and support via the ICASA database +maintenance contract; no customization permitted; friction uploading content and +integrating programmatically; and Ocotillo's intended scope as a holistic +ScienceBase-model data platform. §2.4 corroborates the integration and +performance claims and identifies mechanisms. + +**Product claims in §6** are from general knowledge and were not tested against +this stack. Licences and feature sets change; verify before committing. diff --git a/docs/adr/0002-well-id-minting-service.md b/docs/adr/0002-well-id-minting-service.md new file mode 100644 index 00000000..9fe7a07f --- /dev/null +++ b/docs/adr/0002-well-id-minting-service.md @@ -0,0 +1,178 @@ +# 0002 — Well ID Minting Service + +**Status:** Draft +**Ticket:** none yet. +**Generated by:** Claude Opus 5 (research + draft), Claude Sonnet 5 (revision after AMP/geothermal scope correction) — reviewed and directed by Jake Ross. +**Scope:** cross-repo — `OcotilloAPI` (backend, new table + endpoint) and `OcotilloUI` (frontend, this repo). **AMP-inventoried wells only** (the general water-well inventory, role-gated `AMP.*`) — not geothermal. Geothermal has its own, separate, not-yet-live well model and is out of scope here. + +## Summary + +Field technicians currently type a well's identifier by hand into a free-text field, often offline. The identifier follows a house convention — a short prefix plus a zero-padded sequence number, e.g. `WL-0001`, `WL-0002` — but nothing enforces it and nothing enforces uniqueness. Two technicians can independently pick `WL-0047` and collide. This ADR proposes a **minting service**: a backend endpoint that atomically reserves unique, unused well IDs ahead of time, backed by a new database table, driven from a dedicated **Field Planning** page where technicians mint a batch of IDs before a trip and review every minted ID and its status. Not every minted ID gets used, and not every well needs one pre-minted — minting is a collision-avoidance mechanism, not a mandatory gate. + +## Context + +### What "well ID" means here + +The identifier in question is the `name` field on a well `Thing` — e.g. `WL-0001` (confirmed format). The UI already documents this field as the "*Official well identifier used in bureau records (for example county prefix and local ID)*" ([`src/pages/ocotillo/thing/list.tsx:229`](../../src/pages/ocotillo/thing/list.tsx)) — so the team already treats it as structured, prefix + sequence, even though nothing today enforces that structure or its uniqueness. + +This is a **different** identifier from `well_data_id`/`id` (the server-assigned integer PK — collision-free by construction) and is **unrelated to geothermal's own well-ID scheme** (the legacy `Well_ID`/API-number concatenation discussed for the geothermal migration) — that's a separate identifier space, a separate legacy dataset, and a well model that isn't live in the backend yet. This ADR is scoped to AMP wells only. + +### How well creation actually works today (backend, verified) + +- There is **no dedicated `Well` model**. Wells are rows in the polymorphic `Thing` table (`thing_type = "water well"`), defined at `OcotilloAPI/db/thing.py:61-403`. +- The identifier lives in `Thing.name` (`db/thing.py:88-91`) — free-text, `nullable=False`, **no length limit, no uniqueness constraint**. It sits directly under a developer's own `# TODO: should \`name\` be unique?` (`db/thing.py:87`). +- `POST /thing/water-well` (`OcotilloAPI/api/thing.py:458-477`) is the **live** create endpoint (unlike geothermal's endpoints, which are commented out and not registered — irrelevant here since this ADR doesn't touch geothermal). `name` is entirely client-supplied (`schemas/thing.py:107`), with no server-side uniqueness or format check. `find_water_wells_by_name` (`services/thing_helper.py:88-102`) exists but is only called from CSV-import matching — **not** from the create path. +- No uniqueness constraint exists anywhere close to this: the `thing` table declares only `PrimaryKeyConstraint("id")` in the initial migration (`alembic/versions/66ac1af4ba69_initial_migration.py:1384-1508`) — no `UniqueConstraint` on `name`. +- **No mint/reserve/sequence/counter identifier-issuing pattern exists anywhere in the codebase today**, for any entity. +- **No rate-limiting exists anywhere in the backend** (grepped `ratelimit`/`throttle`/`slowapi`/`limiter` — no hits). + +### Permissions — already in place, no gap here + +Per [`docs/access-control-ruleset.md`](../access-control-ruleset.md), `ocotillo.thing-well` requires AMP view access, and the legacy role mapping (`access-control-ruleset.md:19-21`) makes the backend's generic `Admin`/`Editor` permission strings **equivalent to** `AMP.Admin`/`AMP.Editor`. `create_well` is gated by `admin_dependency` (`api/thing.py:467`, built from `authenticated(permissions=["Admin"])` at `core/dependencies.py:41-43,70`) — so the permission this feature needs already exists and is already enforced on the endpoint it needs to extend. (Unrelated open item, not a blocker: the backend also defines separate `amp_admin_function`/`amp_editor_function`/`amp_viewer_function` — `core/dependencies.py:48-50` — that are **not** the ones gating `create_well`; worth understanding why two AMP-adjacent permission sets exist before building on either, see Risks.) + +### The prefix isn't formally modeled anywhere + +The `WL-0001` shape (prefix + sequence) is convention only. A legacy, already-deleted frontend concept — `IProject { Project, PointIDPrefix }` ([`src/interfaces/amp/IProject.ts`](../../src/interfaces/amp/IProject.ts), from the `/amp/` page tree removed in `5bc22be` — see [[jeremy-disputed-changes]]) — paired a project with its prefix, but that code is gone and was never backed by this backend. The modern replacement for "project" appears to be the `Group` model (`db/group.py:32-...` — `name`, `group_type`, `project_area` geometry; recent "Projects View" work groups wells this way), but `Group` has **no prefix column**. So today, "what prefix does this well belong to" lives only inside the free-text string a technician types — nowhere structured. + +## Decision Drivers + +- **Prevent collisions at assignment time**, not just at write time — a technician needs an ID they know is theirs before they lose connectivity in the field. +- **Don't block wells that already have a real, known-good identifier** — CSV/bulk imports of already-named wells (e.g. legacy data migration) must not be forced through minting. +- **Cheap and simple.** No existing sequence/reservation infrastructure, no existing rate-limiter — the smallest mechanism that actually removes the race condition. +- **Abuse resistance without existing infrastructure to lean on.** Nothing in this app rate-limits anything today; minting must not become a way to exhaust or scrape a prefix's number space. +- **Don't silently reuse numbers already in use.** Plenty of `WL-####`-style names already exist in the live `thing` table from ordinary (non-minted) creates; a naive "count and add one" scheme would immediately collide with them. +- **Auditability** — who minted what, when, whether it was ever used — using the existing `AuditMixin`/`AutoBaseMixin` conventions already in the codebase. + +## Considered Options + +### Option A — Reservation table + minting endpoint, prefix-scoped atomic sequence (recommended) + +A new `well_id_mint` table records every minted ID as a row (`reserved` → `used` / `expired`), with a real Postgres sequence per prefix guaranteeing atomic allocation under concurrent requests. The uniqueness guarantee lives in the database (`UNIQUE (prefix, sequence_number)`), not just in application logic. + +- **Pros:** the DB, not app code, is the source of truth for "has this number been given out" — survives concurrent requests, restarts, bugs. Naturally supports "not all minted IDs get used" (rows sit in `reserved`) and "not all wells need minting" (creates without a minted reference skip validation entirely, unchanged from today). Auditable by construction. +- **Cons:** new table + migration + endpoint + expiry/cleanup logic — the most implementation work of the options considered. + +### Option B — Bare Postgres `SEQUENCE` per prefix, no reservation table + +Call `nextval('well_id_seq_wl')` directly when requested, hand back the formatted ID. + +- **Pros:** minimal schema. +- **Cons:** no record of who holds a given number or whether it was ever used — "not all minted IDs get used" becomes unrecoverable gaps with zero audit trail. No way to validate at consume-time that a submitted ID was one actually issued (vs. guessed to look plausible). No basis for an abuse cap (no row to count against). + +### Option C — Client-side random/high-entropy suffix, no server coordination + +Generate IDs client-side so no round-trip is needed before going offline. + +- **Pros:** works fully offline with zero backend change. +- **Cons:** doesn't produce the `WL-0001`-style sequential format the team already uses; turns collisions from *impossible* into merely *unlikely*; solves a different problem (offline generation) than the one asked for (a minting mechanism). + +### Option D — Do nothing; rely on process/coordination + +Keep manual assignment; mitigate via shared spreadsheet or verbal coordination. + +- **Pros:** zero engineering cost. +- **Cons:** this is the status quo causing collisions today. + +## Decision + +Adopt **Option A**. + +### Backend (`OcotilloAPI`) + +- **New table `well_id_mint`** (Alembic-managed): `id`, `prefix`, `sequence_number`, `formatted_id` (stored, e.g. `WL-0048`), `status` (`reserved` / `used` / `expired`), `minted_by`, `minted_at`, `used_by`, `used_at`, `expires_at` (nullable), `thing_id` (nullable FK to `thing.id`, set on consume). `UNIQUE (prefix, sequence_number)` at the DB level — the actual collision guard. +- **Prefix sequences seeded from existing data, not zero.** Before this ships, a one-time step scans existing `thing.name` values matching `^{PREFIX}-(\d+)$` per prefix and seeds that prefix's starting sequence at the observed max, so newly minted IDs can never collide with IDs already assigned the old way. +- **`POST /thing/well-id/mint`** (under the already-live `/thing` router, `api/thing.py`): accepts `{prefix, count}`, returns `count` newly reserved `formatted_id`s. Gated by `admin_dependency`/`editor_dependency` — the same permission that already gates well create/update today (§Context; no new role needed). +- **Consumption on well create**: `POST /thing/water-well` checks whether the submitted `name` matches a `reserved` row for its prefix; if so, marks it `used` and stamps `thing_id`/`used_at`/`used_by`. If the submitted `name` matches no mint row, creation proceeds exactly as it does today, unminted-and-unvalidated — this is what keeps CSV/bulk import and any other non-minting path working unchanged. +- **Ownership is tracking, not a constraint.** `minted_by` records who reserved an ID; `used_by` records who actually consumed it. The two are **expected to differ** — the common field pattern is a primary technician who plans the trip and mints the IDs, and a secondary technician who does the actual well inventorying and consumes them. The consume path therefore does **not** check that the caller matches `minted_by`; any user with write access may consume any `reserved` ID. Keeping both columns is what makes the handoff visible after the fact. +- **Abuse protections**, given no rate-limiter exists anywhere in this app today: + - Cap IDs per mint request (e.g. 25). + - Cap outstanding (`reserved`, unconsumed) rows per user (e.g. 100). + - `expires_at` TTL (proposed 90 days); expired-and-unused rows become eligible for reuse. + - A scoped, local cap — not a substitute for the app-wide rate-limiting gap noted in Risks. + +### Frontend (`OcotilloUI`, this repo) + +A **dedicated Field Planning page** is the home for this feature — minting is a planning activity done before going out, not a step inside the well-create form. The create form is only lightly touched, and only so a technician can pick an ID they already minted. + +> **Mockup:** [`field-planning-well-id-minting-mockup.html`](https://github.com/DataIntegrationGroup/OcotilloMockups/blob/main/field-planning-well-id-minting-mockup.html) in the `OcotilloMockups` repo. Where the mockup and this text disagree, the mockup is the more current picture of layout and wording; this section is the authority on behavior. + +**Honesty check first:** this app has no offline mode. It's a normal SPA — no service worker, no IndexedDB, no local write queue (checked; none exists). So "field techs are offline" doesn't mean the app works offline — it means the technician has no signal to open the app at all while they're out. What minting actually buys them: mint a batch of guaranteed-unique IDs on the Field Planning page **before** leaving connectivity, take them into the field (printed, exported, or written down), and create the wells later, back online. This ADR does not propose making well creation work offline. + +#### 1. Field Planning page (new route — the primary surface) + +- **Route:** `/ocotillo/field-planning`, registered in [`src/routes/ocotillo.tsx`](../../src/routes/ocotillo.tsx) wrapped in `ProtectedRoute` with a new resource `ocotillo.field-planning`, following the same shape as the existing `batch-export` and `projects` routes. +- **Nav:** a **"Field Planning"** entry in `RESOURCE_NAV` ([`src/config/navigation.ts:104-168`](../../src/config/navigation.ts)), sitting alongside "Field Sheets" (its closest sibling in purpose — both are pre-trip prep). Gated to Editor-and-above, not `viewerAndAbove`, since minting is a write action. + +The page has two sections: + +**Section A — Mint new IDs.** A prefix input (free text, autocompleting against prefixes already present in existing `thing.name` values — no separate prefix registry, per the open question in Risks) and a count stepper (capped at the backend's per-request limit). A "Mint IDs" button calls `POST /thing/well-id/mint`. On success, the newly minted IDs appear immediately in a compact result list — e.g. `WL-0048` … `WL-0052` — with copy-all and a print/export affordance, so the technician can carry them out on paper. This result list is the "IDs associated with this minting run" view: it shows what *this* action just produced, distinct from the full table below. + +**Section B — All minted IDs table.** A table of every minted ID with its status, so a technician (or a manager) can see the whole picture at a glance: + +| Column | Notes | +|---|---| +| Well ID | e.g. `WL-0048` | +| Prefix | for grouping/filtering | +| Status | `Minted` (reserved, not yet used) / `In use` (consumed by a real well) / `Expired` | +| Minted by | who reserved it — tracking only, does not restrict who may use it | +| Minted date | when | +| Used by | who actually consumed it; blank until used, and routinely a different person than "Minted by" | +| Well | for `In use` rows, a link to the well that consumed it (`thing_id`) | + +Filterable by status, prefix, and minter, searchable by ID, sorted newest-first by default. Built on the existing `ListPage` / DataGrid pattern the app already uses for Wells and Contacts, so it inherits the standard search, sort, and export behavior rather than inventing a new table. + +The table is **team-wide** — every technician sees every minted ID regardless of who minted it. That is the point: "has anyone already got `WL-0048`?" is the collision question this feature exists to answer, and a per-user view couldn't answer it. A "minted by me" filter is a convenience, not a boundary. + +#### 2. Using a minted ID at well-create time (light touch) + +The create form is *not* where minting happens; it only needs to let a technician select an ID they already hold. + +- The **only** field this touches is "Well Name" in the shared `CreateEditWell` component ([`src/components/form/thing/CreateEditWell.tsx:49-55`](../../src/components/form/thing/CreateEditWell.tsx)) — used by both create paths, the standalone `WellCreate` ([`src/pages/ocotillo/thing/create.tsx:86-98`](../../src/pages/ocotillo/thing/create.tsx)) and the multi-step `WellInventoryForm` ([`src/pages/ocotillo/well-inventory-form/index.tsx`](../../src/pages/ocotillo/well-inventory-form/index.tsx)) — so building it once covers both. +- Today it's a plain `ControlledTextField`. It becomes a `freeSolo` combobox offering **all** `Minted`-status IDs as options — not just the signed-in user's — since the technician doing the inventorying is often not the one who minted the ID. Options show who minted each ID as secondary text, so the secondary technician can find the batch their primary planned for them. Typing a value that isn't in the list is still allowed and behaves exactly as today (unminted, unvalidated) — this is what keeps "not all wells need a minted ID" intact, with zero change for anyone who never visits the Field Planning page. +- On submit, `name` goes to `POST /thing/water-well` exactly as it does today — no new payload shape. The backend does the consume-or-pass-through check (§Backend); a consumed ID flips to `In use` in the Field Planning table. +- **Failure** (the ID was consumed elsewhere, or expired since minting): the existing form error path surfaces it inline on the Well Name field and the technician picks another — not a generic save failure. +- New provider method for the mint call and the minted-ID list, following the existing `fieldErrors`-mapping pattern already used in this codebase for 422/409 responses. + +## Consequences + +### Positive + +- Collisions become structurally impossible for any ID that went through minting — enforced by a DB unique constraint, not technician discipline. +- Reservation can happen while online, ahead of a field visit with no connectivity. +- Unminted creates (CSV import, any other source) are untouched. +- Every issued ID is attributable and its fate tracked, unlike a bare sequence. + +### Negative / costs + +- New table, mint + list endpoints, migration, a whole new frontend page (route + nav + two sections), the create-form picker, plus expiry/cleanup logic — real scope across both repos. +- New caps-and-TTL design that has to be tuned — too tight blocks legitimate batch prep, too loose doesn't stop abuse. +- Prefix has no formal home server-side today (§Context) — this feature partially formalizes it (as rows in `well_id_mint`) without deciding whether `Group`/Project should also gain a prefix column. + +## Risks and Open Questions + +- **Two AMP-adjacent permission paths exist in the backend** (`admin_function`/`editor_function` vs. `amp_admin_function`/`amp_editor_function`, `core/dependencies.py:41-50`) and only the former currently gates well create. Confirm which one minting should use, and why two exist, before building on either. +- **Prefix isn't tied to any entity.** Is `prefix` on `well_id_mint` a free string chosen at mint time, or should it be validated against (or formalized on) `Group`? Affects whether a technician can mint under a nonexistent/typo'd prefix. +- **Two technicians could grab the same `Minted` ID.** Since ownership isn't a constraint, nothing stops two people from independently picking `WL-0048` from the picker on the same day. The DB still prevents a *duplicate well* (the second create fails the consume check and errors inline), so this is a UX annoyance, not a data-integrity hole — but it's worth deciding whether the picker should soft-signal intent (e.g. an "assigned to" hint on the mint run) rather than letting two people discover the clash at save time. +- **Expiry duration** — 90 days is a placeholder, not a decision. +- **Bulk/CSV import interaction** — confirm bulk-imported wells should bypass mint validation entirely (this ADR assumes yes, consistent with "not all wells need minting"), and shouldn't accidentally consume a `reserved` row that happens to string-match. +- **App-wide rate limiting** — this ADR proposes a narrow, local cap on minting specifically; it does not address the broader fact that no rate-limiting exists anywhere in the backend. Flagging, not solving, as a separate concern. +- **Manual release** — can a technician release an unused reservation back to the pool before its TTL expires (e.g. they minted 10, used 4, know they won't need the rest)? Not designed here; v1 as described only reclaims via expiry. +- **`GET` endpoint for the table** — the Field Planning table needs a list endpoint (`GET /thing/well-id/mint` with status/prefix filters and pagination) that the backend section above doesn't yet spell out. Straightforward, but it is additional API surface beyond the mint call itself. + +## Acceptance Criteria + +*(Draft — nothing is built yet; these are the bar for a first landing, not a file-by-file plan.)* + +- A DB-level `UNIQUE` constraint exists such that two concurrent mint requests for the same prefix can never return the same `formatted_id`. +- Minted-and-unconsumed IDs are visible as `reserved`; consumed ones flip to `used` and carry the `thing_id` they resolved to. +- A well create that references a valid `reserved` ID consumes it; one that references no minted ID at all still succeeds unchanged from today's behavior. +- A technician can consume an ID minted by a *different* technician, and the resulting row shows both `minted_by` and `used_by` distinctly. +- Newly minted IDs, for any prefix with existing wells, never collide with `name` values already present in `thing`. +- Exceeding the per-request or per-user outstanding cap returns a clear error, not a silent partial mint. +- Expired, unused reservations become available for re-minting. + +## Notes + +- This ADR documents a decision spanning both `OcotilloAPI` (backend) and `OcotilloUI` (frontend, this repo). The `OcotilloAPI` repo has its own flat `ADR1.md`–`ADR3.md` at its root, predating this convention; if this decision is adopted, it should be mirrored there when backend work begins — this repo's branch (`well-id-minting`, off `staging`) does not touch the backend repo. +- Explicitly **out of scope**: geothermal wells. Geothermal has a separate legacy well-ID scheme (state-county-sequence API numbers) and no live create endpoint in the backend today (`api/geothermal.py` is fully commented out). If geothermal ever needs its own minting, it should be a separate ADR — the two ID spaces, entities, and permission models don't overlap. +- Related in this repo: [`docs/access-control-ruleset.md`](../access-control-ruleset.md) (the AMP role model this leans on). diff --git a/docs/adr/0003-offline-well-viewing.md b/docs/adr/0003-offline-well-viewing.md new file mode 100644 index 00000000..29c562a7 --- /dev/null +++ b/docs/adr/0003-offline-well-viewing.md @@ -0,0 +1,380 @@ +--- +generated-by: claude-opus-5 +generated-on: 2026-08-07 +prompted-by: jakeross +--- + +# ADR 0003 — Offline Well Viewing + +**Status:** Draft +**Ticket:** none yet. +**Date:** 2026-08-07 +**Deciders:** OcotilloUI frontend team; security/data owner for the at-rest PII question (Q5) +**Scope:** `OcotilloUI` only. No backend change is required for this ADR — the API +dependencies it names (bundled well download, conditional requests) are optimizations, +not prerequisites. +**Related:** [ADR 0004](0004-offline-field-data-capture.md) covers the write path and +depends on this one. + +## Context + +Field staff visit wells in parts of New Mexico with no usable cellular data. Today +OcotilloUI is a pure online SPA: every route render issues live HTTP requests through +Refine data providers, and every map tile is fetched from a public tile host. With no +network, the app shell itself fails to load (nginx `try_files` never runs), so the user +gets a browser error page rather than a degraded app. + +Two concrete requirements drive this ADR: + +1. **Explicit pinning** — a "Use Offline" control on the wells list + ([list.tsx](../../src/pages/ocotillo/thing/list.tsx)) lets a user deliberately mark a well + for offline availability before leaving connectivity. +2. **Implicit caching** — any well opened in Ocotillo + ([well-show.tsx](../../src/pages/ocotillo/thing/well-show.tsx)) becomes viewable offline + afterward, without the user asking. + +### Relevant facts about the current system + +| Area | Current state | Consequence for offline | +| --- | --- | --- | +| App shell | Vite SPA served by nginx; hashed assets `Cache-Control: immutable`, `index.html` uncached ([nginx.conf](../../nginx.conf)) | Shell is precacheable, but nothing registers a service worker today | +| Data access | Refine v5 data providers, axios instance with bearer token ([ocotillo-data-provider.ts](../../src/providers/ocotillo-data-provider.ts)) | Single choke point exists for read interception | +| Well detail | `GET thing/water-well/{id}/details` via `dataProvider.custom()` inside [useWellDetails.ts](../../src/hooks/useWellDetails.ts) | One request carries well, contacts, sensors, deployments, screens — a natural bundle unit | +| Attachments | `asset` list returns **signed URLs valid 15 minutes**, refetched every 10 min ([well-show.tsx:104](../../src/pages/ocotillo/thing/well-show.tsx:104)) | Caching the URL is useless offline; bytes must be captured at pin time | +| Query cache | Refine constructs its **own** `QueryClient` internally; the app *also* mounts a nested `QueryClientProvider` inside `` ([AppProviders.tsx](../../src/AppProviders.tsx)) | Two clients coexist; any persistence layer must target one deliberately | +| Auth | Authentik OIDC, access/refresh tokens in `localStorage`, refresh interceptor on 401 ([authentik-provider.ts](../../src/providers/authentik-provider.ts)) | Offline token refresh is impossible; auth must not hard-fail offline | +| Access control | CASL rules derived from token groups ([access-control-provider.ts](../../src/providers/access-control-provider.ts)) | Permissions must be readable offline or every card renders as denied | +| Maps | MapLibre GL; OpenFreeMap vector + USGS raster tiles, key-free ([basemaps.ts](../../src/basemaps.ts)) | Tiles are plain URL GETs with no auth — cacheable by a service worker | +| Version banner | Polls `/version.json` every 5 min ([useNewVersion.ts](../../src/hooks/useNewVersion.ts)) | Must coexist with a service worker update lifecycle, not fight it | + +### Non-goals for this ADR + +- **Offline writes.** Editing, the groundwater-level stepper form, and the well + inventory form stay online-only. A queue-and-sync design has different failure modes + (conflict resolution, idempotency, partial submission) and deserves its own ADR. +- Offline availability of AMP, ST2, geothermal, geochronology, or OGC API resources + beyond what a pinned well bundle explicitly includes. +- Full-catalog offline search over all wells. + +## Decision + +Adopt a **two-layer hybrid**: + +**Layer 1 — Service worker (Workbox via `vite-plugin-pwa`)** owns everything addressed +purely by URL and served without an `Authorization` header: + +- Precache the app shell and hashed build assets (`index.html` served + `NetworkFirst` so deploys still propagate; JS/CSS/fonts `CacheFirst`). +- Runtime-cache basemap tiles and glyphs (`CacheFirst`, bounded by an + `ExpirationPlugin` entry cap and max-age). +- Make the app installable (web app manifest) so field users get a standalone icon. + +**Layer 2 — Application-level offline store (IndexedDB)** owns authenticated API data, +because it needs semantics a URL cache cannot express: bundling many requests into one +"well", pinning that survives eviction, per-well sync status, and a manageable UI. + +- A `withOfflineCache(dataProvider)` decorator wraps `ocotilloDataProvider` in + [AppProviders.tsx](../../src/AppProviders.tsx). It intercepts `getOne`, `getList`, and + `custom` (the last is what `useWellDetails` uses), writing successful responses into + IndexedDB and reading from IndexedDB when the network is unavailable or a request + fails with a network error. +- A `wellOfflineBundle` record is the unit of caching: + + ``` + { + wellId, pinned: boolean, pinnedAt, lastSyncedAt, schemaVersion, sizeBytes, + details, // thing/water-well/{id}/details payload + assets: [{ id, filename, contentType, blob }], // bytes, not signed URLs + manualObservations, // full series -- see Q3 + transducerObservations, // trailing 12 months only -- see Q3 + tileKeys[] + } + ``` + +- **Pinned** bundles (`pinned: true`) are never evicted automatically and are + refreshed opportunistically when connectivity returns. +- **Viewed** bundles (`pinned: false`) are written on every successful well-show load + and evicted LRU beyond a cap (proposed: 50 wells or a storage budget, whichever + binds first). +- Pinning is a **superset of viewing**: pinning fetches the full bundle eagerly, + including asset bytes and map tiles; viewing captures whatever the page already + fetched plus a background asset/tile fill. + +### Supporting decisions + +1. **Consolidate the query client.** Create one `QueryClient` and hand it to Refine via + `options.reactQuery.clientConfig`, then delete the nested `QueryClientProvider` + inside ``. Without this, Refine hooks and `useWellDetails` can resolve + different clients and cache behavior becomes non-deterministic. This is a + prerequisite, not an optional cleanup. +2. **Do not persist the whole TanStack Query cache.** `@tanstack/query-persist-client` + is tempting but ties offline durability to `gcTime` and serializes blobs poorly. + IndexedDB bundles are the source of truth; the query cache stays in memory and is + *hydrated from* bundles. +3. **Offline auth grace window.** When `navigator.onLine === false` (or a refresh + attempt fails with a network error), the auth provider returns `authenticated: + true` if a stored session exists and its recorded `offlineGraceExpiresAt` (7 days, + capped by refresh-token expiry — see [Q1](#q1--offline-auth-grace-window)) has not + passed. CASL rules are snapshotted + into IndexedDB on each successful login/refresh and replayed offline. On explicit + logout, or when a refresh returns a real `invalid_grant`, **all offline stores are + wiped**, since the bundles contain authenticated data. +4. **Signed asset URLs are never cached as URLs.** Pinning downloads asset bytes into + IndexedDB and the UI serves them via `URL.createObjectURL`. Assets above a size + threshold (proposed: 25 MB each) are skipped with a visible note rather than + silently blowing the quota. +5. **Map tiles for a pinned well are bounded and prefetched.** At pin time, compute the + tile set covering a 2 km radius around the well point, from z10 up to the active + basemap's own `maxzoom`, and `cache.addAll()` them into a dedicated Cache Storage + bucket, recording the keys on the bundle so unpinning can release them. See + [Q2](#q2--tile-radius-and-zoom-range) for the sizing derivation: ~131 raster tiles + (≤8 MB) or ~25 vector tiles plus style/sprite/glyphs (~2 MB) per well. +6. **Storage is explicitly negotiated.** Call `navigator.storage.persist()` on first + pin and surface `navigator.storage.estimate()` in the offline management UI. Refuse + new pins past a budget with an actionable message rather than failing mid-write. +7. **Offline state is visible, never silent.** A global offline indicator, plus + per-page "Showing cached data from {lastSyncedAt}" banners. Cards whose data was not + captured render an explicit "unavailable offline" state instead of an empty or + perpetually-loading card. + +### User-facing surfaces + +| Surface | Behavior | +| --- | --- | +| Wells list row action + bulk action ([list.tsx](../../src/pages/ocotillo/thing/list.tsx)) | "Use Offline" toggle; shows progress while the bundle downloads, then a pinned indicator | +| Well show header ([OcotilloPageHeader](../../src/components/OcotilloPageHeader)) | Same toggle, plus last-synced timestamp | +| New `/offline` management page | Pinned wells, per-well size, last sync, total storage used, "sync all", unpin | +| Offline banner | App-wide connectivity state and staleness | + +### Rollout phases + +1. **Phase 1 — Shell.** `vite-plugin-pwa`, manifest, precache, tile runtime cache, SW + update flow reconciled with `useNewVersion`. App opens offline; data is empty. +2. **Phase 2 — Implicit well cache.** Query client consolidation, IndexedDB store, + provider decorator, offline banner. Viewed wells replay offline (no asset bytes). +3. **Phase 3 — Explicit pin.** "Use Offline" on list and show, asset byte capture, tile + prefetch, `/offline` page, storage negotiation. +4. **Phase 4 — Auth grace + sync.** Offline auth window, CASL snapshot, background + refresh of pinned bundles on reconnect. + +Each phase ships behind a feature flag so field testing can precede general exposure. + +## Alternatives Considered + +**A. Service worker only — cache authenticated API GETs with Workbox.** +Rejected. Zero app-code changes is attractive, but a URL cache cannot express "this +well is pinned", cannot bundle a well's several requests as a unit, cannot survive +signed-URL expiry, and gives the UI nothing to render sync state from. Cache keys also +ignore the `Authorization` header, so a user switch would serve another user's data +unless caches are wiped on logout anyway. + +**B. Persist the TanStack Query cache to IndexedDB (`persistQueryClient`).** +Rejected as the primary mechanism. It is the cheapest path to "viewed wells work +offline", but durability is coupled to `gcTime`, blobs do not serialize, pinning has no +natural representation, and the double-`QueryClient` situation makes the blast radius +unclear. May still be used *inside* Phase 2 as an implementation detail for hydration. + +**C. Local replica database (RxDB / PouchDB / SQLite-WASM) with sync.** +Rejected for now. It is the right shape if and when offline *writes* land, but it +implies a sync protocol on the API side that does not exist, and it is heavy for a +read-only requirement. + +**D. Generate an offline PDF per well instead of caching the app.** +Rejected as a substitute — [well-show-pdf-preview.tsx](../../src/pages/ocotillo/thing/well-show-pdf-preview.tsx) +already exists and is a reasonable stopgap, but it is static, loses the map and +hydrograph interactivity, and does not satisfy "any well viewed is cached". + +## Consequences + +### Positive + +- Field staff keep working with no signal; the app opens and pinned wells are complete. +- One interception point (the data provider decorator) covers both Refine hooks and the + hand-rolled `useWellDetails` query. +- Installability and precaching improve cold-start latency on poor connections even + when fully online. +- The query-client consolidation removes existing latent ambiguity regardless of + offline work. + +### Negative / costs + +- A service worker is a permanent operational hazard: a bad SW can pin users to a stale + build. Mitigated by `NetworkFirst` on `index.html`, a tested skip-waiting/update + prompt, and a documented kill switch (ship an SW that unregisters itself). +- Authenticated well data — including attachment bytes — now sits on the device at + rest, unencrypted, subject to the grace window. This is a security posture change and + needs sign-off; wipe-on-logout is necessary but not sufficient against a lost device. +- The offline auth grace window means a user whose access is revoked server-side keeps + reading cached wells until the window lapses. +- Stale data risk: a cached well may show a water level that was superseded. The + last-synced banner is the mitigation and is mandatory, not decorative. +- Cypress and Vitest suites grow an offline dimension; service workers interfere with + `cy.intercept` and must be disabled in most specs and exercised deliberately in a few. +- Storage quotas differ sharply across browsers and are hostile on iOS Safari, where + eviction can occur without user action even with `persist()`. + +### Follow-on work this unblocks or requires + +- [ADR 0004 — Offline Field Data Capture](0004-offline-field-data-capture.md): the + write path for the groundwater-level and well-inventory forms. +- API support for a single bundled well-download endpoint, to replace N requests per + pin with one. +- API support for conditional requests (`ETag` / `If-Modified-Since`) so reconnect sync + is cheap. + +## Validation + +- **Unit (Vitest):** bundle serialization round-trip, LRU eviction, quota refusal, + offline auth grace boundary (just inside / just outside), wipe-on-logout. +- **E2E (Cypress):** pin a well online → force offline via CDP + `Network.emulateNetworkConditions` → reload → assert the well show page renders from + cache with a staleness banner; assert an unpinned, never-viewed well renders an + explicit unavailable state rather than a spinner. +- **Manual field test:** airplane mode on a real device, full pin-then-drive workflow, + before general rollout. +- **Telemetry:** PostHog `captureEvent` on pin, unpin, offline page view, cache hit, + and quota refusal — to learn whether pinning is used and how large real bundles get. + +## Resolved Questions + +### Q1 — Offline auth grace window + +**7 days, hard-capped by the refresh token's own expiry, reset by any successful online +session.** + +Store `offlineGraceExpiresAt = min(lastSuccessfulRefresh + 7d, refreshTokenExp)` at +every successful token refresh in [authentik-provider.ts](../../src/providers/authentik-provider.ts). +The cap matters: a grace window longer than the refresh token's life produces a user who +can read cached wells but is forced through a full re-auth the moment they reconnect — +strictly worse than expiring the cache with the token. + +Seven days is chosen against the actual failure it must survive: a multi-day field +trip. Longer buys little (trips over a week almost always touch connectivity +somewhere), and every extra day extends how long a revoked account keeps reading +authenticated data. Shorter is hostile — a 24-hour window would strand a Tuesday +departure by Wednesday. + +Expiry is not silent. At `expires - 24h` the app surfaces a "reconnect within 1 day to +keep offline access" prompt; past expiry, bundles are wiped and the user sees the login +screen with an explanation, not an empty app. + +Make the 7 days a build-time constant, not a user setting — a user-adjustable security +window will be set to maximum by everyone and reviewed by no one. + +### Q2 — Tile radius and zoom range + +**2 km radius, z10 up to each basemap's own `maxzoom`, active basemap only, plus the +themed default (`light`) as a fallback.** + +Derivation at NM latitude (φ ≈ 34°, so a tile spans `40,075,017 · cos φ / 2^z` metres); +a 2 km radius is a 4 km box, and worst-case grid alignment needs `floor(4000/span) + 2` +tiles per axis: + +| Zoom | Tile span | Tiles per axis | Tiles | +| --- | --- | --- | --- | +| z10 | 32.4 km | 2 | 4 | +| z11 | 16.2 km | 2 | 4 | +| z12 | 8.1 km | 2 | 4 | +| z13 | 4.1 km | 2 | 4 | +| z14 | 2.03 km | 3 | 9 | +| z15 | 1.01 km | 5 | 25 | +| z16 | 507 m | 9 | 81 | +| | | **Total** | **131** | + +- **Raster basemaps** (USGS imagery/topo, `maxzoom` 16; shaded relief, 15) reach the + full 131 tiles. At 30–60 KB per 256px JPEG that is **4–8 MB per well**. +- **Vector basemaps** (OpenFreeMap) serve source tiles only to z14 and overzoom above + it, so prefetch stops at z14: **25 tiles, ~2 MB**, plus the style JSON, sprite sheet, + and the glyph PBF ranges the app's symbol layers actually request + (`DEFAULT_TEXT_FONT` = Noto Sans Regular — one fontstack, a handful of ranges). + +2 km, not 1 km, because the well point is often not where the truck parks — access +roads and gates need to be on-screen. Not 5 km, because z16 tile count scales with the +square of the radius: 5 km would be ~500 tiles and ~30 MB per well. + +**Budget: 250 MB total, ~10 MB per pinned well worst case → ~25 wells.** Warn at 80%, +refuse new pins at 100% with an actionable message naming the wells to unpin. + +iOS Safari is the binding constraint, and it argues for shipping Phase 1's web app +manifest before Phase 3's pinning: an installed PWA gets materially better eviction +treatment than a plain tab, where storage can be reclaimed after a stretch of disuse +regardless of `navigator.storage.persist()`. + +### Q3 — Hydrograph history in a pinned bundle + +**Full manual series; trailing 12 months of transducer data only.** + +The current page fetches *everything* — [well-show.tsx:217](../../src/pages/ocotillo/thing/well-show.tsx:217) +pages through `observation/groundwater-level` at 1000/page and +`observation/transducer-groundwater-level` at 5000/page until exhausted. Those two are +not remotely the same size: + +- **Manual** groundwater-level readings are field measurements — monthly to quarterly. + Decades of record is still only hundreds to low thousands of rows, well under a + megabyte of JSON. Cache all of it; the whole point of a hydrograph is the long trend. +- **Transducer** data is logger output. At a 15-minute interval that is ~35,000 rows + per year; a ten-year record is ~350,000 rows and tens of megabytes — several times + the entire tile budget, for one well. + +Twelve months of transducer data preserves a full seasonal cycle, which is what a field +user is actually checking against ("is this level normal for August?"), and keeps the +bundle in the low single-digit megabytes. + +The offline chart must label the truncation — "transducer record truncated to 12 months +for offline use" — rather than silently drawing a shorter line than the same well shows +online. + +The real fix is server-side downsampling (LTTB or time-bucketed averages) so the full +record fits in a fixed row budget. Track that as an API request; until it exists, the +12-month window is the mitigation. Note that this also makes pinning *cheaper than the +online page load*, which is a nice side effect worth keeping. + +### Q4 — Auto-refresh on reconnect + +**Foreground + explicit user action, or Wi-Fi. Never silent cellular.** + +Auto-sync pinned bundles only when all hold: the app is online, the tab is foregrounded, +and either the connection is known Wi-Fi or the user has opted into cellular sync in the +`/offline` page. `navigator.connection.effectiveType` / `saveData` supply the signal +where available (Chromium/Android); Safari and Firefox do not implement it, so **absent +signal is treated as cellular** — the conservative default. + +Rationale: a bundle refresh is multi-megabyte and 25 pinned wells is a ~250 MB sync. +Doing that unprompted on a state-issued metered plan the moment a truck rolls back into +coverage is a bill the user did not agree to. + +A manual "Sync all" button in `/offline` is always enabled regardless of connection +type — an explicit tap is consent. Staleness is always visible per well, so a user who +skips syncing is never misled about what they are looking at. + +### Q5 — Encryption of cached attachments + +**No app-level encryption in v1.** Ship wipe-on-logout, wipe-on-user-switch, and +wipe-on-schema-mismatch instead, and require FDE + MDM on devices that pin. + +App-level crypto here would be theater: the key has to live somewhere the app can read +without a server round trip — IndexedDB or `localStorage`, right next to the ciphertext. +Anyone who can read the origin's storage can read the key. It would defeat casual +inspection of the IndexedDB viewer in devtools and nothing else, while adding real +complexity to every read path. + +What actually mitigates a lost device is OS full-disk encryption plus a remote-wipe +policy, both of which are device-management concerns, not app concerns. + +Concrete requirements this does impose: + +- Wipe every offline store on explicit logout, on refresh failing with a real + `invalid_grant`, and when the incoming token's `sub` claim differs from the one that + wrote the bundles (user switch on a shared truck tablet). +- Wipe on `schemaVersion` mismatch after a deploy rather than attempting migration. +- **Flag for security review before Phase 3:** the `details` payload includes contacts + ([ContactsCard](../../src/components/WellShow)), i.e. landowner names and phone numbers. + Pinning therefore writes third-party PII to the device at rest. Field staff plausibly + need it, so the recommendation is to include it and bring pinning devices formally + in scope for the organization's PII handling policy — but that is a call for the data + owner, not the frontend team. If the answer is no, contacts are dropped from the + bundle and that card renders "unavailable offline". + +Revisit only if a real requirement appears (a regulated attachment class, or a +compliance regime that names at-rest encryption) — and then the answer is likely an +OS-backed key store via a native wrapper, not WebCrypto in the SPA. diff --git a/docs/adr/0004-offline-field-data-capture.md b/docs/adr/0004-offline-field-data-capture.md new file mode 100644 index 00000000..91984923 --- /dev/null +++ b/docs/adr/0004-offline-field-data-capture.md @@ -0,0 +1,337 @@ +--- +generated-by: claude-opus-5 +generated-on: 2026-08-07 +prompted-by: jakeross +--- + +# ADR 0004 — Offline Field Data Capture (Groundwater Level + Well Inventory) + +**Status:** Draft +**Ticket:** none yet. +**Date:** 2026-08-07 +**Deciders:** OcotilloUI frontend team, Ocotillo API team +**Scope:** cross-repo — `OcotilloUI` (this repo, outbox + form refactors) and +`OcotilloAPI` (idempotency contract, without which this ADR should not ship — see D2). +**Depends on:** [ADR 0003 — Offline Well Viewing](0003-offline-well-viewing.md) + +## Context + +ADR 0003 makes wells *readable* without a network. It explicitly excludes writes. This +ADR covers the other half of the field workflow: recording a measurement or inventorying +a new well while standing at a site with no signal. + +Two forms are in scope: + +- **Groundwater level** ([groundwater-level-form](../../src/pages/ocotillo/groundwater-level-form)) — + the high-frequency case. A technician visits a known well and records a depth-to-water + reading. +- **Well inventory** ([well-inventory-form](../../src/pages/ocotillo/well-inventory-form)) — + the low-frequency, high-effort case. A new well is characterized from scratch: + location, construction, screens, landowner contacts, photos. + +Today both fail hard offline: the submit mutation throws, the user sees "Failed to +Submit Form — please check your input and try again later", and the entered data exists +only in React state until the tab is closed. The de facto workaround is paper. + +### What makes this hard + +Neither form is a single request. Both are **client-orchestrated, non-transactional +chains where each step depends on an ID the server just generated.** + +**Groundwater level** ([groundwater-level-form.service.ts](../../src/pages/ocotillo/groundwater-level-form/groundwater-level-form.service.ts)) — +two steps: + +``` +POST sample -> sampleResponse.data.id +POST observation/groundwater-level (sample_id = that id) +``` + +**Well inventory** ([well_inventory.service.ts](../../src/pages/ocotillo/well-inventory-form/well_inventory.service.ts)) — +`1 + 1 + N + M + P` steps, all sequential: + +``` +POST ocotillo.location (only when locationMode === 'new') -> locationId +POST ocotillo.thing/water-well (location_id) -> wellId +POST ocotillo.thing/well-screen x N (thing_id = wellId) +POST ocotillo.contact x M (thing_id = wellId) +POST ocotillo.asset x P (thing_id = wellId, storage_path, uri) +``` + +A failure at step *k* leaves steps `0..k-1` committed on the server with no rollback. +That is already true online — offline replay makes it far more likely, and adds +duplicate-on-retry as a new failure mode. + +### Other constraints found in the code + +| Fact | Location | Implication | +| --- | --- | --- | +| Asset files upload via multipart `POST asset/upload` **at file-selection time**, not at submit; the returned `storage_path`/`uri` is then written into the asset record | [CreateEditAsset.tsx:78](../../src/components/form/asset/CreateEditAsset.tsx:78) | Offline, there is nothing to upload *to*. Blobs must be held locally and uploaded at drain time | +| `@TODO change the well inventory form to only upload new asset on form submit, not on file selection` | [index.tsx:638](../../src/pages/ocotillo/well-inventory-form/index.tsx:638) | Already a known defect; becomes a hard prerequisite here | +| Well selection in the GWL form queries the API for things | [SelectThingComponent](../../src/components/form/thing/SelectThingComponent.tsx) | Offline the picker must be backed by the ADR 0003 bundle store | +| Submission is a bare `useMutation` with notification side effects and `setCreatedWellId` on success | [index.tsx:155](../../src/pages/ocotillo/well-inventory-form/index.tsx:155) | "Success" currently means "server committed". Queuing changes what success means, and the UI must say so | +| Every resource carries a `release_status` | both services | Queued records need provenance distinguishable from live ones | +| `deleteOne` is unimplemented in the data provider | [ocotillo-data-provider.ts:327](../../src/providers/ocotillo-data-provider.ts:327) | No client-side compensating delete is available to unwind a partial chain | + +### Non-goals + +- **Offline edits.** `PATCH` against an existing record has genuine conflict semantics + (two technicians, one well, divergent values). v1 queues **creates only**. This + restriction is what keeps the design tractable — it means there is no merge, no + last-write-wins policy, and no vector clocks. +- Offline delete (the provider does not implement it at all). +- Bulk/CSV import while offline. +- Any change to how *online* submissions behave, beyond the asset-upload timing fix. + +## Decision + +Adopt a **durable outbox of whole form submissions**, replayed by re-running the existing +service functions when connectivity returns. + +### D1 — The queued unit is a submission intent, not an HTTP request + +Store the *validated form payload* — the same object `createGroundwaterLevelForm` or +`createWellInventoryForm` already takes — in IndexedDB, together with its file blobs. +At drain time, run the real service function against the live API, resolving +server-generated IDs then, in order, as it does online. + +``` +outboxSubmission { + id, // client UUID; also the idempotency key + kind, // 'groundwater-level' | 'well-inventory' + payload, // the validated IGroundwaterLevelForm / IWellInventoryForm + files: [{ fieldPath, filename, contentType, blob }], + capturedAt, // when the technician hit submit, in the field + status, // 'pending' | 'draining' | 'failed' | 'done' + attempts, lastError, + progress: { location?: id, well?: id, screens: [id], contacts: [id], assets: [id] }, + schemaVersion +} +``` + +The rejected alternative — queuing individual HTTP calls with client-generated primary +keys and remapping IDs on drain — requires the server to accept client-supplied IDs. It +does not, and asking for that is a much larger API change than asking for idempotency +keys. + +### D2 — Every submission carries an idempotency key, and the API must honor it + +The client generates a UUID at queue time and sends it on **every request in the chain** +(header `Idempotency-Key`, scoped per step, or a `client_submission_id` column — the API +team's call). Without server-side deduplication, a request that succeeds but whose +response is lost to a dying connection produces a duplicate record on retry, and +duplicate water-level observations corrupt the science. + +**This is the critical-path dependency for the whole ADR.** If it cannot ship, the +fallback is a pre-drain "does a record with this `capturedAt` + `thing_id` already +exist?" probe plus a manual review queue — a mitigation, not a fix, and it does not +close the race. + +### D3 — Chains are resumable, not restartable + +Refactor both service functions from straight-line `await` sequences into ordered step +lists whose outputs are recorded in `progress` as each step commits. A drain that dies +after creating the well resumes at screens; it does not create a second well. + +This is the single largest code change in the ADR and it improves the online path too — +today, an inventory submission that fails at contact 3 of 5 silently leaves a well and +two contacts behind with no way to finish. + +### D4 — Asset bytes are held locally and uploaded at drain + +Do the existing `@TODO` first: move `POST asset/upload` from file-selection to submit. +Offline, the `File` is written to IndexedDB and uploaded at drain time, before the asset +record that references its `storage_path`. + +Cap per-submission attachment bytes (proposed: 50 MB) and warn when a photo set +approaches it. Field photos from a modern phone are 3–5 MB each, so this is roughly a +dozen photos per well — generous for inventory, and bounded. + +### D5 — Offline well selection is restricted to the offline store + +The GWL form's well picker reads from the ADR 0003 bundle store when offline. **You can +only record a groundwater level offline against a well that was pinned or previously +viewed.** This is a real workflow constraint and must be taught: pin your route before +you leave. + +The inventory form is unaffected when `locationMode === 'new'` (it creates everything). +When offline, the "use existing location" branch is disabled, since it needs a location +lookup the device cannot perform. + +### D6 — Queued is not submitted, and the UI never pretends otherwise + +The success screen after an offline submit says *saved on this device, not yet sent*, +with the pending count and a link to a queue view. `setCreatedWellId` and the +navigate-to-well affordance are suppressed for queued inventory submissions — there is +no well ID yet. + +A persistent badge shows the pending count. The queue view lists each submission with +its captured time, status, and last error, and offers retry-now, edit, and discard. + +### D7 — Drain policy: automatic, foregrounded, visible + +Drain on reconnect while the app is foregrounded. Unlike ADR 0003's *download* of +pinned bundles, this is small outbound data the user actively wants delivered, so +cellular is fine by default — except that submissions carrying more than ~10 MB of +attachments wait for Wi-Fi unless the user taps send-now. + +Retry policy: + +- **Network error / 5xx / timeout** — exponential backoff (30 s, 2 m, 10 m, 1 h), + capped at 6 attempts, then dead-letter. +- **401** — refresh the token once and retry; if refresh fails, hold the queue and + prompt for login rather than dead-lettering. Field data must never be lost to an + expired session. +- **4xx other than 401/429** — dead-letter immediately. A 422 will not become valid by + being retried. + +Dead-lettered submissions are never silently dropped and never retried forever. They +surface a notification and land in the queue view for a human to fix and requeue or +discard. + +### D8 — Pending submissions block logout and survive everything else + +ADR 0003 wipes offline stores on logout, user switch, and schema mismatch. The outbox is +the exception: + +- Logout with a non-empty queue requires an explicit confirmation naming the count and + offering to drain first. Prefer blocking over losing. +- The outbox is **never** evicted for storage pressure — it takes priority over cached + well bundles, which are re-downloadable. Field data is not. +- A schema-version bump must migrate the outbox, not clear it. If a payload genuinely + cannot be migrated, export it as JSON to the user rather than deleting it. + +### D9 — Provenance is recorded server-side + +Queued records carry `captured_at` (when the technician submitted in the field) distinct +from server `created_at` (when it arrived, possibly days later), plus a flag marking +offline capture. The scientific timestamps the user typed — +`observation_datetime`, `sample_date` — are already user-entered and unaffected, but QA +needs to be able to tell a record that sat in a truck for a week from a live one. + +### D10 — Offline well inventory consumes pre-minted well IDs + +[ADR 0002 — Well ID Minting Service](0002-well-id-minting-service.md) exists precisely +because technicians type well identifiers by hand, offline, with no uniqueness +enforcement — two people can independently pick `WL-0047`. That is the same trip, the +same technician, and the same lack of connectivity this ADR is about, so the two +features are not independent: + +- When the inventory form is filled offline, its `well.name` field should draw from the + batch of IDs minted for the trip on the Field Planning page, not from free text. + Minting happens *online, before departure* — the same pre-trip step as pinning wells + under [ADR 0003](0003-offline-well-viewing.md), and it should be presented as one + workflow rather than two unrelated chores. +- Minted IDs must therefore be cached on-device alongside pinned bundles, with local + bookkeeping of which have been consumed by queued submissions so the form does not + hand the same ID to two wells inventoried on the same day. +- If ADR 0002 does not ship, offline inventory still works — it just inherits the + existing collision risk, now with a longer window between capture and the server + seeing the name. Worth saying plainly: **offline capture makes the collision problem + ADR 0002 describes strictly worse**, because the duplicate is not discovered until + drain, potentially days later, when the technician has left the site. + +Neither ADR blocks the other. But sequencing 0002 first means the offline inventory form +ships with the collision already solved rather than deferred. + +### Phasing + +1. **Prerequisites (no offline behavior yet).** Move asset upload to submit time + (D4's `@TODO`); refactor both services into resumable step lists (D3); land the + idempotency contract with the API team (D2). +2. **GWL outbox.** Two steps, no blobs, highest field frequency. Ship it behind a flag + and field-test it before touching inventory. +3. **Inventory outbox.** N-step chains plus attachment blobs; queue view; logout guard. +4. **Polish.** Background Sync as a progressive enhancement on Chromium; queue export; + dead-letter review tooling. + +## Alternatives Considered + +**A. Service Worker Background Sync API as the primary mechanism.** +Rejected as primary, kept as a Phase 4 enhancement. Safari/iOS does not implement it, and +field devices include iPads. It also drains outside the app's React context, which fits +single fire-and-forget requests but fits multi-step chains with progress UI and +dead-letter handling poorly. + +**B. Per-request queue with client-generated UUID primary keys and ID remapping.** +Rejected. Cleanest in theory — every create becomes independently replayable — but it +requires the API to accept client-supplied IDs across five resources. That is a bigger +ask than idempotency keys and changes the server's key strategy permanently. + +**C. Local replica database with a sync engine (RxDB / PowerSync / ElectricSQL).** +Rejected for v1 scope, and named as the correct endgame *if* offline editing is ever +required. It solves conflicts properly, but it needs a sync protocol the API does not +have, and the create-only restriction (see non-goals) makes it heavy for what is +actually needed. + +**D. "Save draft locally, submit manually later."** +Rejected as the primary design — it is the outbox with the automation removed, and it +puts the burden of remembering on the person least able to carry it. Retained as a +*component*: the queue view's manual retry is exactly this, for the dead-letter case. + +**E. Do nothing; keep using paper and transcribe later.** +The status quo baseline. It loses data, delays entry by days, and introduces +transcription errors — but it is honestly cheaper than everything above, and it is the +right answer if the API cannot supply idempotency (D2). Say so plainly rather than +shipping a queue that silently duplicates observations. + +## Consequences + +### Positive + +- Field work stops depending on signal; data is captured once, at the well, in the app. +- Resumable chains (D3) fix a real existing online defect — partial submissions that + currently strand orphaned records with no recovery path. +- Deferring asset upload to submit (D4) fixes another: today, abandoning the inventory + form leaves uploaded files in storage with no record referencing them. +- `captured_at` gives QA a provenance signal it does not have today. + +### Negative / costs + +- **Hard external dependency.** Without server-side idempotency this design can + duplicate scientific observations. That is worse than the paper status quo, and it is + a reason to delay, not to ship with a mitigation. +- Refactoring two working service functions into resumable step machines is invasive and + carries regression risk on the online path, which is the path everyone uses today. +- Partial server state is still reachable: a permanent 422 halfway through an inventory + chain leaves a well with some of its children. The API likely needs an "incomplete + submission" state or an admin cleanup tool; the client cannot unwind it, because + `deleteOne` is not implemented. +- Unsent field data now lives on a device, extending ADR 0003's at-rest exposure to + landowner contacts and site photos — and the logout guard (D8) means the app will + sometimes refuse to do what a user asks. +- The offline-pinning prerequisite (D5) is a workflow burden: a technician who forgets + to pin a route cannot record levels for it, and will discover that at the well. +- Test surface grows sharply: every chain step gains a failure mode, and duplicate + detection is only observable across a reconnect. + +## Validation + +- **Unit (Vitest):** outbox state machine (each status transition, backoff schedule, + attempt cap); retry classification by HTTP status; resume-from-`progress` for both + chains; schema migration of a queued payload; blob round-trip. +- **E2E (Cypress):** submit a GWL reading offline via CDP + `Network.emulateNetworkConditions` → assert the queued-not-sent UI → reconnect → + assert exactly one sample and one observation exist. Repeat with the response to + step 1 dropped after commit, and assert idempotency prevents a second sample. +- **E2E:** kill the drain mid-inventory-chain, reconnect, assert one well — not two — + and that screens/contacts complete. +- **Manual field test:** a full airplane-mode inventory with photos on a real iPad, + then reconnect on Wi-Fi. Do this before any general rollout; the iOS storage and + lifecycle behavior is the part least likely to be caught in CI. +- **Telemetry:** PostHog events on queue, drain success, drain failure by class, + dead-letter, and discard — the dead-letter rate is the metric that says whether this + is trustworthy. + +## Open Questions + +1. Which idempotency mechanism will the API support — a header, a `client_submission_id` + column, or natural-key deduplication? This gates Phase 1. +2. Does the API want a first-class "incomplete submission" state, or is a partial chain + an admin cleanup problem? +3. Maximum queue age before a submission is considered suspect — a two-week-old + depth-to-water reading is still valid data, but is it still *expected* data? +4. Who reviews dead-lettered submissions — the technician who captured it, or a data + steward? +5. Should offline GWL capture be allowed against a well the device has not cached, by + entering an identifier manually? It removes the D5 workflow trap but permits typos + into a `thing_id` that may not exist. diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 00000000..e20be857 --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,16 @@ +# Architecture Decision Records + +## Convention + +- **Filename:** `docs/adr/NNNN-kebab-title.md` — four-digit sequence, **never renumbered**. +- **Status:** one of `Draft`, `Proposed`, `Accepted`, `Rejected`, or `Superseded by NNNN`. +- **Superseding:** a superseding ADR links back to the one it replaces. Superseded ADRs are **kept, not deleted** — the record of why a decision was made and later reversed is the point. + +## Index + +| # | Title | Status | +|---|-------|--------| +| [0001](0001-contextual-media-mapping.md) | Contextual Media Mapping | Draft | +| [0002](0002-well-id-minting-service.md) | Well ID Minting Service | Draft | +| [0003](0003-offline-well-viewing.md) | Offline Well Viewing | Draft | +| [0004](0004-offline-field-data-capture.md) | Offline Field Data Capture | Draft | diff --git a/docs/geothermal-gap-analysis.md b/docs/geothermal-gap-analysis.md new file mode 100644 index 00000000..c2dfc06f --- /dev/null +++ b/docs/geothermal-gap-analysis.md @@ -0,0 +1,142 @@ +# Geothermal — Gap Analysis vs the Legacy System + +**Status:** Findings / for review with the data owner + backend +**Ticket:** BDMS-878 +**Source:** Screenshots of the legacy Access DB **NM_Wells Geothermal (SQL2019)** +(`NM_Wells_FE_GeoThermal_ver03`) + a sample temperature-depth export +(`Borderplex_2026_03_31Td.csv`), provided by the geothermal data owner. + +The legacy Access app is the **authoritative data model** the new Ocotillo +geothermal UI/API is meant to replace. This doc maps what it holds against what +we've built so far (inventory grid + records grid) and lists the gaps. + +> Note: screenshot 3 shows a VBA "must be updated for use on 64-bit systems" +> compile error — the Access front end is breaking on 64-bit Office. That's +> context for *why* they're migrating off Access, not a data-model gap. + +--- + +## Current Ocotillo model (what we have) + +- **Well** (`IWell`, inventory grid): `well_data_id`, `api`, `name`, + `well_number`, `well_class`, `well_type`, `status`, `operator`, `owner`, + `total_depth`, `completion_date`, `has_geothermal_data`, `county`, `state`, + `latitude`, `longitude`. +- **Record** (`IWellRecord`, records grid): `OBJECTID`, `WellDataID`, + `WellName`, `WellNumber`, `API_suffix`, `ActionDate`, `EntryDate`, + `EnteredBy`, `RecrdSetID`, `SourceID`, `Comments`. + +This covers the **well identity / header** layer at a basic level. The gaps +below are what's missing. + +--- + +## Gaps (priority-ordered) + +> **Update:** G1 frontend built — a per-well **temp-depth log grid** +> (`temp-depth-grid.tsx`) with a CSV importer that handles the legacy export +> shape (the Borderplex file), a template, editable grid, and a batch "Save +> log" write. `ITempDepthPoint` models depth/temp/resistance/gradient. Still +> needs the backend temp-depth endpoint (GET + batch POST) to persist. G2/G3/G4 +> frontend also built (see commits). + +### 🔴 G1 — The core geothermal measurements are not modeled + +The whole purpose of the system is thermal data: +**temperature-depth logs → thermal gradient → heat flow**. The legacy DB has +dedicated subforms for exactly this (visible in the Forms list): +`GTTempDepth_Subform_new`, `GT_HeatFlowDataSbfrm`, `GTSumHeatFlow_Subform`, +`GT_Heatflow_sbfrm`. + +The sample CSV is one such log: + +| Column | Meaning | +|--------|---------| +| `Depth_m`, `Depth_ft` | measurement depth (both units) | +| `Resistance` | probe resistance | +| `Temp_F`, `Temp_C` | temperature (both units) | +| `Gradient_C_km` | thermal gradient (°C/km) | +| free-text notes | formation ("Camp Rice Formation"), "fine slots in screen" | + +Our `IWellRecord` (WellName / Comments / dates) has **no fields for +depth/temperature/gradient/resistance/heat-flow**. The CSV import we built +ingests *well* rows, not temp-depth logs — so **this file has nowhere to land**. + +**Needed:** a measurement model (temp-depth points + derived gradient / thermal +conductivity / heat-flow summary), plus a log importer that matches this CSV +shape. Backend endpoint required. + +### 🔴 G2 — Location is drastically oversimplified + +We store a single `latitude`/`longitude`. The legacy Well_Location record has: + +- **Two datums**: `Lat_dd27`/`Long_dd27` (NAD27) **and** `Lat_dd83`/`Long_dd83` + (NAD83), plus DMS (D/M/S), with `SourceUnits` + `SourceDatum`. +- **PLSS**: `UnitLetter`, `Sectn`, `Township` + `NorS_TDir`, `Range` + + `EorW_RDir`, `SectnPart`, `Footage_NS`/`Footage_EW` + `NorS_FDir`/`EorW_FDir`, + `UTM_zone`. +- **Basin** (e.g. San Juan) — missing entirely. +- **Location accuracy**: `LocAccType`, `LocAccMeas`, `LocAccVal`. +- **Multiple locations per well** ("Add New Location") with `Duplicated` / + `Exclude` flags and a per-location `SourceID` (provenance). + +Our county/state server-derivation plan is compatible, but the datum + PLSS + +accuracy + multi-location provenance model is absent. + +### 🟠 G3 — Well header is missing many fields + +| Legacy field | Ours | +|--------------|------| +| `Well_TVD` (true vertical depth) | only `total_depth` (measured) | +| `SpudDate`, `ComplDate`, `PlugDate`, `PlugBack` | only `completion_date` | +| `Fm_TD`, `Age_TD` (formation / age at TD) | — | +| `WellOrient` (vertical/deviated) | — | +| `CurOperatr`, `CurStatus`, `CurWellNam`, `CurWellNum`, `CurOwner` | flat operator/status/name/owner (no current-vs-historical split) | +| `PrdPoolCount` (producing pool count) | — | +| `Import_ID`, `Import_DB`, GUID | — (import provenance) | +| Data-existence flags: `ScoutTickt`, `DwnHoleSur`, `GeoLog`, `Geophyslog`, **`GthrmExist`**, `PetroData`, `CoreExists`, `Cuttings`, `SampleDat` | only `has_geothermal_data` (= `GthrmExist`) | + +`has_geothermal_data` is **one flag in a family of ~9** yes/no data-presence +flags. + +### 🟠 G4 — API is structured, not free text + +Legacy: `API = 30-039-05212` (state `30`=NM · county `039` · well `05212`), +`Well_ID = 3003905212` (concatenated), plus a separate `API_suffix`. Ours is a +plain string with no structure or validation. Worth a parsed/validated API. + +### 🟡 G5 — Missing supporting entities + +- **Sources** — `SourceID` is a foreign key to a bibliography record + (e.g. "Engler, Brister, Chen, Teufel, 2001"), not free text. +- **Records provenance/content** — `RecrdSetID`, `RecrdClass`, `EnteredBy`, + `EntryDate`, Sample Sets, and **Lithology** (color / grain size / texture), + `LithStrat`, `LithLog`. +- **Perf intervals**, **Production** (legacy `PerfIntrval_sbfrm`, `Prdctn_sc`). +- **Audit trails** — "Audits: Well Header / Locations / LithStrat / LithLog". + +--- + +## Summary + +Our inventory + records grids model the **well identity/header** layer at a +basic level. The three biggest gaps: + +1. **Geothermal measurements (temp-depth, gradient, heat flow) are unmodeled** — + the reason the system exists. The provided CSV can't be imported anywhere. +2. **Location** is ~10× simpler than the source (datums, PLSS, accuracy, + multi-location provenance). +3. **Records** is a stub vs the real provenance / lithology / sample-set model. + +## Recommended sequencing (proposal) + +1. Confirm scope with the data owner: is Ocotillo replacing the *full* NM_Wells + Geothermal DB, or just the well-inventory + thermal-log capture? +2. Backend: define the **temperature-depth / heat-flow** entities + endpoints + (blocks G1) and the **richer location** entity (G2). +3. Frontend: extend the well header (G3), add a **temp-depth log importer** + matching the CSV shape, and the location detail model (G2). +4. Later: Sources, Sample Sets, Lithology, Perf/Production, Audits (G5). + +Nothing here is implemented yet — this is a findings doc to align on scope +before building. diff --git a/docs/geothermal-well-inventory.md b/docs/geothermal-well-inventory.md new file mode 100644 index 00000000..85e4bf59 --- /dev/null +++ b/docs/geothermal-well-inventory.md @@ -0,0 +1,198 @@ +# Geothermal Well Inventory — Feature Spec + +**Status:** Draft / for review +**Ticket:** BDMS-878 (geothermal grid) — inventory sub-feature +**Author:** (draft) + +--- + +## 1. Goal + +Let an authorized user **inventory new geothermal wells** — create many well +records at once — through one of two entry paths that feed the same editable +grid: + +1. **Load a CSV** — upload a spreadsheet export; rows populate the grid. +2. **Enter directly** — type/paste into a blank Glide Data Grid, spreadsheet-style. + +Both paths converge on one reviewable grid; the user edits/corrects, then a +single **Save** creates the wells via the geothermal API. No per-cell autosave. + +This is distinct from the existing **records** grid (edit records under an +existing well). Inventory is about *creating wells*. + +--- + +## 2. Entity — geothermal well (real API contract) + +Source: `GET /thing/geothermal-well` (verified live). Fields are snake_case. + +| Field | Type | Notes | +|-------|------|-------| +| `well_data_id` | string (UUID) | **Server-assigned** on create — not entered | +| `thing_id` | number \| null | Currently null; not entered | +| `api` | string | API well number, e.g. `30-104-33218` | +| `name` | string | e.g. `GEOTHERMAL-0001` | +| `well_number` | string | e.g. `4` | +| `well_class` | string | e.g. `Oil & Gas` | +| `well_type` | string | e.g. `Wildcat`, `Production`, `Exploration` | +| `status` | string | e.g. `Abandoned`, `Active` | +| `operator` | string | | +| `owner` | string | | +| `total_depth` | number | feet | +| `completion_date` | string (ISO datetime) | | +| `has_geothermal_data` | boolean | | +| `county` | string | | +| `state` | string | default `NM` | +| `latitude` | number | | +| `longitude` | number | | + +Create payload = these fields minus `well_data_id`/`thing_id` (server-owned). + +--- + +## 3. User flows + +### 3a. CSV load +1. User clicks **Upload CSV** (or drags a file). +2. Client parses the CSV (headers → well fields). +3. Parsed rows load into the grid as new (unsaved) rows. +4. Unmapped/blank cells are empty; parse errors are surfaced per row. +5. User reviews/edits in the grid, then **Save**. + +- **Template:** a **Download template** button emits a CSV with the canonical + header row (the field names in §2) so users start from the right shape. +- **Column mapping:** v1 matches CSV headers to field names **exactly** + (case-insensitive, trimmed). Unknown headers are ignored (reported). A + mapping UI (drag headers → fields) is a later enhancement. + +### 3b. Direct grid entry +1. User clicks **Add rows** → N blank rows appended. +2. User types or pastes (Glide handles paste-from-Excel across a range). +3. User **Save**. + +Both paths share the same grid, dirty tracking, and save. + +--- + +## 4. Grid + +Reuse **`EditableDataGrid`** (`src/components/grid`) — the entity-agnostic +component already extracted in Phase 1. + +- Columns = the editable fields in §2 (all editable; `well_data_id` shown + read-only, blank until saved). +- Cell kinds: text for strings, number for `total_depth`/`latitude`/`longitude`, + a boolean/checkbox for `has_geothermal_data`, date for `completion_date`, and + **dropdown (single-select)** for the enum fields `well_type`, `well_class`, + `status`. (Number/text exist today; **boolean, date, and dropdown cell kinds + are new** — additions to `EditableDataGrid`.) +- Dropdown values: fixed allowed-value lists per enum field (source TBD — hard- + coded constants vs lexicon-backed). CSV/paste values outside the list are + flagged as invalid cells. +- Keyboard nav + range paste come free from Glide. + +--- + +## 5. Save — batch create + +Reuse the batch-save pattern already built in `records-grid.tsx` +(`computePendingOps` + `Promise.allSettled`), specialized to create-only: + +- Every non-blank row → `POST /thing/geothermal-well` via the geothermal + provider `create`. +- Per-row tracking: created rows adopt the server response (real + `well_data_id`) and clear dirty; failed rows stay for retry with rejected + cells tinted from Pydantic `fieldErrors` (provider already maps 422/409). +- Toolbar shows `n created, m failed`. +- No server bulk endpoint assumed → one request per row. + +--- + +## 6. Validation + +- **Client (pre-save):** required fields must be non-empty; numeric/date/bool + fields must parse. Invalid cells are tinted and block that row's save. +- **Server:** 422/409 `fieldErrors` surface inline (existing mapping). +- **Required fields:** `name`, `api`, `well_type`, `latitude`, `longitude`. + `county`/`state` are **not** required — see §6a. **TBD** — the API's create + schema isn't in the OpenAPI (stripped), so the real required set must be + confirmed against the backend. + +### 6a. county / state — server-derived from lat/lon + +Decision: `county` and `state` are reverse-geocoded from `latitude`/`longitude` +**by the backend**, **auto-filling only when left blank** (a user-entered value +is kept). Rationale: reverse-geocoding is authoritative and avoids bundling a +county-boundaries dataset in the client. + +- Frontend: `latitude`/`longitude` are the required location inputs; `county`/ + `state` are optional (the user may still type them, e.g. from CSV). No + client-side geocoding. +- Backend (to implement): on create, if `county`/`state` are absent, derive + them from the coordinates (NM county point-in-polygon; `state` defaults to + `NM`) before persisting. + +--- + +## 7. Access control + +Admin-gated via `canEnterGeothermalData(canManageGeothermal)` — the same helper +as the records grid (bypassed in local dev, enforced in prod). + +--- + +## 8. Navigation & route + +- Route: `/geothermal/wells/inventory`. +- Nav: none yet. The page is reachable by URL while the work is in progress; + it gets a nav entry when a geothermal nav group lands. + +--- + +## 9. Reuse vs new work + +**Reuse (exists):** +- `EditableDataGrid` + theme/sizing hooks (Phase 1). +- Geothermal provider `create` + Pydantic `fieldErrors` mapping (Phase 3). +- `computePendingOps` / batch-save + dirty tracking + inline cell errors. +- `IWell` real field interface. + +**New:** +- CSV parse + template download via **`papaparse`** (new dependency — decided). +- Boolean, date, and **dropdown** cell kinds in `EditableDataGrid`, plus + allowed-value lists for the enum fields. +- Inventory page (grid + toolbar: Add rows, Upload CSV, Download template, Save). +- Route (no nav entry yet). +- Create-only save wrapper (adapt records-grid save to POST-only). + +--- + +## 10. Decisions + +- **CSV parser:** `papaparse` (new dependency). ✔ +- **Duplicate handling:** treat every row as create; let the server 409 and + surface the conflict inline. No client-side dedupe/upsert in v1. ✔ +- **Enum fields** (`well_type`, `well_class`, `status`): **dropdowns** from + fixed allowed-value lists (new select cell kind). ✔ +- **Nav placement:** none yet — URL-only until a geothermal nav group lands. ✔ +- **Records vs inventory:** inventory is create-wells only; the existing records + grid is untouched. ✔ + +### Still open +1. **Required fields** — the real create-required set (§6) isn't in the OpenAPI + (stripped); confirm against the backend before finalizing client validation. +2. **Dropdown value source** — hard-coded constants vs lexicon-backed lists for + the enum fields, and the allowed values themselves. + +--- + +## 11. Phased plan + +- **P1 — Inventory page + direct entry:** new route/page, `EditableDataGrid` + over blank rows, Add rows, admin gate. (No CSV yet.) +- **P2 — Batch create save:** POST-only save wrapper, per-row status, inline + field errors. +- **P3 — CSV load:** parser + Upload + Download template + parse-error surfacing. +- **P4 — Cell kinds + polish:** boolean/date editors, required-field validation, + duplicate handling, dropdowns for enum fields. diff --git a/docs/geothermal-well-search-contract.md b/docs/geothermal-well-search-contract.md new file mode 100644 index 00000000..0fb8c507 --- /dev/null +++ b/docs/geothermal-well-search-contract.md @@ -0,0 +1,114 @@ +# Geothermal Well Search — API Contract (Proposal) + +Status: **draft / proposal** — the parameter this describes is not implemented, +and at the time of writing no reachable backend serves the geothermal well +endpoint at all. + +## Background + +The geothermal grid pages (Records Grid, Temp-Depth log) start with a well +picker. It used to be a single dropdown populated by one request: + +``` +GET thing/geothermal-well?page=1&size=500 +``` + +Two problems, both consequences of doing selection without search: + +1. **Silent truncation.** The catalogue runs to thousands of wells. Everything + past the 500th was absent from the dropdown with nothing to say so, and a + well that existed but was not listed was indistinguishable from a well that + did not exist. +2. **Scrolling as a workflow.** Even within those 500, the only way to reach a + well was to scroll a flat list ordered by whatever the server returned. + +The picker is now a search box. It sends the user's term to the server and +reports how much of the match set it is showing. + +## Proposed parameter + +``` +GET thing/geothermal-well?q=&page=1&size=50 +``` + +`q` is a free-text term. When absent or empty the endpoint behaves exactly as +it does today — the first page of the unfiltered list — so a picker with an +empty box still shows something. + +### Matching + +The term should match, case-insensitively, as a substring against at least: + +| Field | Why | +| --- | --- | +| `name` | The label the picker shows, and what a user knows the well by | +| `api` | State-county-well identifier; how wells are cross-referenced | +| `well_number` | Operator's own numbering | +| `operator` | "show me everything Chevron drilled" | +| `county` | Coarse geographic narrowing | + +A term with several whitespace-separated words should require every word to +match somewhere (AND), not any of them — `jemez 1` should narrow the results +that `jemez` returns, not widen them. + +### Response + +Unchanged from the existing list shape. The client relies on the paginated +envelope, and specifically on `total`: + +```jsonc +{ + "items": [ /* IWell */ ], + "total": 1284, // matches for this q, NOT the size of the whole catalogue + "page": 1, + "size": 50, + "pages": 26 +} +``` + +`total` must be the count of records matching `q`. The picker shows +"Showing 50 of 1284 wells matching your search" from it, and decides whether to +offer "Show more". If the server returns the unfiltered count instead, that +line misreports and the button appears when there is nothing more to fetch. + +The provider also accepts a bare array response, but then `total` is just the +array length and the picker cannot report the match count. The envelope is +preferred. + +## Client behaviour + +- Term is debounced 300 ms, so typing a well name is one request rather than + one per keystroke. +- Page size starts at 50 and grows by 50 via "Show more", capped at 500 loaded + at once. On reaching the cap the picker says so and asks the user to narrow + the search, rather than truncating quietly the way the old dropdown did. +- A new term resets the page size, so a narrow search does not inherit a broad + one's over-fetch. +- The term travels as `meta.params` through `geothermalDataProvider.getList`, + which passes any `meta.params` entry through as a query parameter and skips + null/undefined/empty values. + +## If `q` is not implemented + +The parameter is ignored and the endpoint returns the unfiltered first page. +The picker still works — it lists wells and the user can page through them — +but typing narrows nothing, and the "Showing N of M" line reveals the problem +immediately rather than hiding it: the total will not move as the term changes. + +That is a deliberate property. The previous design failed silently; this one +fails visibly. + +## Open questions + +1. **Ordering.** Unspecified today. Relevance ordering (exact `name` match + first, then prefix, then substring) would make the first result usually the + right one. Absent that, a stable `name` sort is better than arbitrary order. +2. **Fuzzy matching.** Substring only, for now. Whether typo tolerance is worth + it depends on how users actually refer to these wells. +3. **Where the endpoint lives.** `thing/geothermal-well` is absent from the + bundled OpenAPI spec, returns 404 on the dev API, and collides with + `thing/{thing_id}` on the local Ocotillo backend. + `VITE_NMBGMR_GEOTHERMAL_API_URL` is unset in every `.env` example and + `settings.tsx` falls back to the Ocotillo URL, so as configured the + geothermal provider currently points somewhere that cannot serve it. This + needs resolving before any of the above can be verified end to end. diff --git a/docs/gis-artifact-downloads-contract.md b/docs/gis-artifact-downloads-contract.md new file mode 100644 index 00000000..f160d92a --- /dev/null +++ b/docs/gis-artifact-downloads-contract.md @@ -0,0 +1,303 @@ +--- +generated-by: claude-opus-5 +generated-on: 2026-08-23 +prompted-by: jakeross +--- + +# Implementing the GIS artifact downloads + +Task brief for an agent working in **OcotilloUI**. Adds a surface that lets a +user download ready-made QGIS and ArcGIS Pro files for our OGC API layers, so +they can open our data in a desktop GIS without configuring a connection by +hand. + +The API side is built and tested; nothing here needs backend work. Read +`AGENTS.md` and `FRONTEND.md` first — this document assumes their conventions +and does not repeat them. + +## Before you can start: the spec is not in this repo yet + +`src/generated/` is emitted from the committed `openapi-auth.json`, and **that +snapshot has no `/gis` paths** — the endpoints exist on an unmerged API branch +(`feat/ogc-desktop-gis-artifacts`) and are not deployed. Confirm before +starting: + +```bash +python3 -c "import json;print([p for p in json.load(open('openapi-auth.json'))['paths'] if p.startswith('/gis')])" +``` + +Empty list means you are blocked on one of: + +1. the API branch merging and deploying, then refresh the spec from the + deployed `/openapi-auth.json`; or +2. running that API branch locally and pulling the spec from it. + +Then regenerate and commit the output alongside the spec, per `AGENTS.md`: + +```bash +npm run openapi:generate +``` + +Do not hand-write the types in `src/generated/`. Do not proceed by typing the +responses by hand in application code either — the generated zod schemas are +how every other surface in this repo validates responses, and diverging here +means the next spec change breaks silently instead of at build time. + +Use `/openapi-auth.json`, **not** `/openapi.json`. The latter is the anonymous +schema and omits the authenticated internal-connections route. + +## What the API offers + +Base URL is whatever `VITE_API_URL` (or the existing API config) already +resolves to. All routes are `GET`, no request body. + +| Route | Returns | Auth | +|---|---|---| +| `/gis?f=json` | catalogue, JSON | none | +| `/gis/qgis/connections.xml` | QGIS connections file | none | +| `/gis/qgis/layers/{layer_id}.qlr` | styled QGIS layer | none | +| `/gis/arcgis/layers/{layer_id}.lyrx` | styled ArcGIS Pro layer | none | +| `/gis/qgis/connections-internal.xml` | connections incl. internal mount | **viewer role** | + +### The catalogue drives everything + +``` +GET /gis?f=json +``` + +`/gis` is content-negotiated: HTML by default (it is also a human-facing +landing page), JSON on `?f=json` or `Accept: application/json`. Always pass +`?f=json` explicitly rather than relying on the Accept header — axios defaults +vary and an HTML response into a JSON parser is a confusing failure. + +```jsonc +{ + "service_url": "https://ocotillo-api.../ogcapi", + "connections": [ + { + "client": "qgis", + "href": "https://ocotillo-api.../gis/qgis/connections.xml", + "media_type": "text/xml", + "filename": "ocotillo-ogcapi-connections.xml" + } + ], + "layers": [ + { + "id": "water-level-trend", + "title": "Water-Level Trend", + "abstract": "Direction of the fitted depth-to-water trend at each well…", + "collection": "depth_to_water_trend_wells", + "collection_url": "https://ocotillo-api.../ogcapi/collections/depth_to_water_trend_wells", + "geometry": "Point", + "renderer": "categorized", + "downloads": [ + { + "client": "qgis", + "href": "https://ocotillo-api.../gis/qgis/layers/water-level-trend.qlr", + "media_type": "text/xml", + "filename": "water-level-trend.qlr" + }, + { + "client": "arcgis", + "href": "https://ocotillo-api.../gis/arcgis/layers/water-level-trend.lyrx", + "media_type": "application/json", + "filename": "water-level-trend.lyrx" + } + ] + } + ] +} +``` + +`renderer` is one of `single | graduated | categorized`. It describes how the +layer is symbolised and is there if you want an icon or a caption; it is not +required to build the download. + +**Do not hardcode the layer ids.** They come from a YAML config on the API side +and are expected to change — layers get added, and one has already been renamed +during development. There are six today. Render whatever the catalogue returns. + +**Do not construct download URLs yourself.** Use `href` verbatim. It is +absolute and built from the API's configured base URL, so it is already correct +for staging, production and a preview environment pointed at an ephemeral API. +Building `${base}/gis/qgis/layers/${id}.qlr` in the frontend duplicates a rule +that lives on the server and will drift. + +## The one real gotcha: you cannot read the filename from the response + +The API sends `Content-Disposition: attachment; filename="…"`, but CORS on this +API is configured with `allow_origins=['*']` and **no** `expose_headers`. +Verified against a running instance with an `Origin` header set: + +``` +access-control-allow-origin: https://ocotillo.newmexicowaterdata.org +access-control-expose-headers: +content-disposition: attachment; filename="water-wells.qlr" +``` + +The header is on the wire, but the browser will not let JS read it +cross-origin. `response.headers.get('content-disposition')` returns `null` in +the app even though curl shows it. + +**This is already solved: use the `filename` from the catalogue.** That field +exists for exactly this reason. Do not ask for a backend CORS change, and do +not parse `Content-Disposition`. + +### Anonymous downloads — plain anchor, no JS + +The four anonymous routes are attachments. A plain link is the whole +implementation, and the browser handles the save dialog, progress and errors: + +```tsx + +``` + +Do not `fetch()` these into a blob. It buys nothing, costs you the browser's +native download handling, and puts a few KB through the JS heap for no reason. + +### The internal connections file — fetch, because it needs a bearer token + +An anchor cannot send an `Authorization` header, so +`/gis/qgis/connections-internal.xml` is the one case that needs a blob: + +```ts +const response = await axiosInstance.get(href, { responseType: 'blob' }) +const url = URL.createObjectURL(response.data) +const anchor = document.createElement('a') +anchor.href = url +anchor.download = filename // from the catalogue, not the response headers +anchor.click() +URL.revokeObjectURL(url) +``` + +Use the repo's existing authenticated axios instance so the token refresh +interceptor applies. Gate the control on the viewer role through the existing +`accessControl` provider rather than catching a 403 after the fact. + +## Things that will bite you + +**Do not let a JSON interceptor touch the `.lyrx`.** It is served as +`application/json` because that is what it is, but it is a *file*. If axios +parses it and something later re-serialises it, key order and formatting change +and you may hand ArcGIS Pro a subtly different document. Always request it with +`responseType: 'blob'` if you fetch it at all — or better, use an anchor and +never touch it. + +**Compare media types after stripping parameters.** The server sends +`text/xml; charset=utf-8`, while the catalogue says `text/xml`. If you assert +equality anywhere, split on `;` first. + +**Responses are generated per request** from live config plus a database +lookup. They are small, but do not build an aggressive client-side cache that +would serve a stale URL after a deploy moves environments. TanStack Query's +defaults are fine; the catalogue is a good candidate for a normal `useQuery` +with the default `staleTime`. + +**`404` is the error you will actually hit** — a `layer_id` that no longer +exists, which is the failure mode of hardcoding ids. Body is +`{ "detail": "No curated layer 'x'." }`. `422` only fires on path-param +validation. + +## What to build + +Minimum useful surface, in repo layout terms: + +- `src/hooks/` — a `useGisArtifacts()` hook wrapping `GET /gis?f=json` with + TanStack Query, returning the parsed catalogue typed from `src/generated`. +- `src/components/` — a presentational component listing the layers: title, + abstract, and one download control per `client`. Plus a prominent + "connect to everything" control for the connections file, since that is the + better path for most users and the per-layer files are the narrow case. +- A route/page wiring the two together, registered the way `AGENTS.md` + describes (`src/routes/`, `src/config/navigation.ts`, access-control aware). +- `src/test/` — Vitest specs mirroring the tree. + +Copy worth reusing, because it is the part users get wrong: the connections +file is imported in QGIS through **Browser panel → right-click "WFS / OGC API - +Features" → Load Connections**. ArcGIS Pro has no importable connection file +from us — the user adds the connection once via *Insert → Connections → Server +→ New OGC API Server* and pastes `service_url`. Surface `service_url` as +copyable text for that reason. + +## Acceptance criteria + +- [ ] `openapi-auth.json` refreshed and `src/generated/` regenerated, both + committed together; no hand-edits under `src/generated/`. +- [ ] Layer list is rendered from the catalogue response. Grepping the source + for `water-level-trend` or any other layer id returns nothing. +- [ ] Download URLs come from `href`; no string concatenation of API paths. +- [ ] Saved filenames come from the catalogue's `filename`; nothing reads + `Content-Disposition`. +- [ ] Anonymous downloads use an anchor, not a blob round-trip. +- [ ] The internal-connections control is role-gated, uses the authenticated + axios instance, and is absent for a user without the viewer role. +- [ ] Vitest covers: catalogue renders N layers from a fixture, a download + control carries the exact `href` and `filename` from the fixture, and the + internal control is hidden without the role. +- [ ] `npm run lint`, `npm run typecheck`, `npm run test:run` all clean. +- [ ] Branched from `origin/staging` and targeting `staging`. + +## Verifying against a real API + +Prism mocks `openapi-auth.json`, so once the spec is refreshed +`npm run mock:server:cypress` serves these routes without the backend. The mock +returns schema-shaped data, not real files — good enough for component tests, +not for confirming a file opens in QGIS. + +To check a real artifact end to end, fetch one and open it: + +```bash +curl -sOJ https://ocotillo-api.newmexicowaterdata.org/gis/qgis/layers/water-level-trend.qlr +``` + +Dragging that onto a QGIS canvas should give a styled point layer with a +red/blue/grey trend legend and roughly 2,450 features. That path is already +verified on the API side against QGIS 4.0.1; if it fails, the problem is the +environment, not your frontend code. + +--- + +## How this repo consumes it today + +Added on the branch that wired the catalogue into the datasets page +(`/ocotillo/collections`). Two deviations from the plan above, both deliberate. + +**Types are hand-written zod, not generated.** `openapi-auth.json` still has no +`/gis` paths, and the only available source is the unmerged API branch. A spec +dumped from that branch is a superset: five `/gis` paths, but also two unrelated +unreleased endpoints and eight changed schemas — `WellResponse`, `ThingResponse` +and `SpringResponse` among them — which the rest of the app validates against. +Refreshing the whole snapshot to reach `/gis` would have re-generated all of +that on a feature branch. + +So `src/utils/gisArtifacts.ts` carries hand-written zod for the catalogue only, +scoped to the GIS surface and marked for replacement. **When the API branch +merges and deploys: refresh the spec, run `npm run openapi:generate`, and delete +those schemas in favour of the generated ones.** Nothing else in the app depends +on them. + +**The surface is the existing datasets page, not a new route.** Per-layer +downloads render on the collection row they belong to, matched on the +catalogue's `collection` field against the collection id the page already +resolves. The connections file, the QGIS import instructions and the copyable +`service_url` sit in one panel above the groups. + +Everything else follows the contract: hrefs and filenames verbatim from the +catalogue, anonymous downloads as plain anchors, no layer ids in source, and the +internal connections file fetched as a blob through the authenticated axios +instance behind an `AMP.Viewer` check. + +### One gap on the API side + +`/gis?f=json` lists only anonymous artifacts, so +`/gis/qgis/connections-internal.xml` appears nowhere in the catalogue. The +frontend currently derives it from the public entry's `href` by swapping +`connections.xml` for `connections-internal.xml` — string surgery of exactly the +kind this contract says to avoid, and the only place the rule is broken. + +The clean fix is on the API: list the internal connection in the catalogue (for +authenticated callers, or unconditionally, since the route enforces its own +auth). `deriveInternalGisConnection` in `src/utils/gisArtifacts.ts` should be +deleted when that lands. diff --git a/docs/hydrograph-correction-gap-analysis.md b/docs/hydrograph-correction-gap-analysis.md new file mode 100644 index 00000000..c8286502 --- /dev/null +++ b/docs/hydrograph-correction-gap-analysis.md @@ -0,0 +1,169 @@ +# Hydrograph Correction — Gap Analysis Against Real Artifacts + +Date: 2026-07-29 +Inputs: real wellpy artifacts (`sa-0231_DK744_compensated.CSV`, +`EB-165.wcsv`, `2025-11-25_MG009.txt`) and the NMBGMR *Procedure for +Collecting and Processing Continuous Depth-to-Water Data* (Dec 2022). +Every finding below was verified empirically by running the artifact +through the current parser/filters. + +## Verdict summary + +| Artifact | Result today | Severity | +|---|---|---| +| `sa-0231_DK744_compensated.CSV` (real Diver Office export) | **Fails to parse** — "No hydrograph rows could be parsed" | Critical | +| `2025-11-25_MG009.txt` (field data logger telemetry) | **Fails to parse** — unsupported format | Critical | +| `EB-165.wcsv` (real Wellntel export) | Parses, but reflection filter leaves **131 of ~299 spurious readings** | High | +| Methodology PDF | Conversion math matches eq. (2)/(3), but single-anchor flow and QC diagnostics missing | Medium | + +## 1. Real Diver Office CSV fails to parse (critical) + +The real export differs from the synthetic demo file in ways that break +two assumptions: + +- **Delimiter sniffing picks `|`.** The first 20 lines are the metadata + block, which contains no commas but two pipes (in + `COMP.STATUS: ... (Barometer: ... | Serial number: DL572 | ...)`). + `detectDelimiter` samples only the first 20 lines, scores `|` highest, + and every data row then becomes a single cell. The header row is still + "found" (the whole string matches both time and value patterns), but + the datetime and value columns collapse to the same cell and zero rows + parse. Fix: sniff the delimiter from the rows at/after the detected + header row, or re-sniff when the first pass yields no rows. +- **`Location` is not a recognized point-id pattern.** The real file + identifies the well only via `Location =sa-0231`. + `POINT_ID_PATTERNS` knows `thing.name`, `point id`, `well name`, and + `site id` — not `Location` — so even after a delimiter fix the well + cannot auto-resolve. (`parseDiverOfficeUpload` knows `Location=`, but + this file never reaches that code path because it *has* a proper + header row, `Date/time,Water head[ft],Temperature[°C]`, so the generic + parser handles it.) +- Also worth noting from the real file: `°C` arrives as a non-UTF8 byte + (Diver Office writes Windows-1252 — parsing must not choke on it), the + header uses `Water head[ft]` with no space before the bracket, dates + are `2024/02/20 12:00:00`, and the terminator line is + `END OF DATA FILE OF DATALOGGER FOR WINDOWS` (our Diver detector + matches `END OF DATA` as a prefix, so that part is fine). +- The metadata block carries values we currently ignore that the + methodology makes meaningful: `Sample period =H12` (12-hour cadence), + `Reference level`/`Range` per channel (overpressurization bounds), and + the barometer used for compensation (provenance). + +## 2. Field data logger telemetry format unsupported (critical) + +`2025-11-25_MG009.txt` is the "Field Data Logger Methodology" format — +telemetered readings, one line per record, space-delimited tokens: + +``` +2024/11/19 18:54:05 ID 009 D 151.02 T 51.2 B 13.9 G 218 R 0001 +``` + +`D` is already depth to water (ft), `T` temperature (°F), `B` battery +voltage, `G` signal, `R` restart flag. No header, no commas — the +current parser errors immediately ("Unable to detect a header row"). +Needs a dedicated line-format parser (regex per row), `valueKind: +'depth_to_water'`, station id from the `ID` token or filename +(`MG009` → MG-009). Battery voltage is worth surfacing: the methodology +replaces loggers below 75% battery, so a declining `B` column is a QC +signal at ingest time. + +## 3. Reflection filter defeated by dense clusters (high) + +EB-165 is far harsher than the synthetic demo. Real characteristics: + +- Spurious readings are **systematically positive** and **multi-modal**: + baseline ~478.5 ft with populations near +3.3, +7, +11, +15 ft — + consistent with n-bounce echo multiples, exactly the 1x/2x behavior + described earlier but at several multiples. +- They are **dense and clustered**: in some stretches (e.g. mid-May and + May 18–20, 2023) a third to all of consecutive readings are spurious. + Six consecutive readings at ~481.9 look exactly like a sustained step. +- They are **temperature-correlated**: the spurious population is + overwhelmingly the warm evening (~20:00) readings — the parsed-away + `temperature_C` column carries real discriminating signal. + +Measured performance of the current median-window filter on the real +file (405 rows, ~299 readings above the 478–479.5 ft trend band): +removes 168 at the 0.25 ft threshold, **131 spurious readings survive** +— adjacent spurious readings rescue each other via the +neighbor-agreement rule, and the 7-sample median itself is contaminated +when half the window is spurious. + +What would close the gap: + +- **Running-baseline filter** (wellpy's `remove_up_spikes` normal mode, + which this well's data clearly motivated): track the last accepted + clean value; reject any reading more than the threshold *above* it + (reflections here are one-sided). Handles arbitrarily long spurious + runs. +- Optionally **mode-based baseline**: the true trace is the lowest + density mode; take a rolling lower quantile (e.g. 20th percentile over + a 2-day window) as baseline and drop readings > threshold above it. +- Optionally **temperature-aware assist**: flag readings whose + temperature is far above the daily median as reflection-suspect. + Requires keeping the temperature column through parsing (currently + discarded for `.wcsv`). + +## 4. Methodology alignment (medium) + +The PDF confirms several design decisions and exposes a few gaps: + +Confirmed correct: +- DTW conversion math matches eq. (2)/(3): calculated hanging point = + manual DTW + head, series DTW = hanging point − head. Our + anchor-pair conversion generalizes eq. (3) between bounding manuals. +- `release_status: provisional` on publish, independent review to + approved, then public release — matches the QC process section and + the existing block `review_status` model. +- Zero/near-zero head handling ("water column above Diver dropped to + zero") and cable-slip re-leveling match documented failure modes. + +Gaps: +- **Single-anchor conversion.** The documented workflow anchors the + whole series on *one* selected manual measurement (Snap to Selected → + eq. 2 → eq. 3). Our converter requires ≥ 2 overlapping manual + observations and throws otherwise. A series with only the one manual + taken at download day — the common case per the methodology (annual + visits) — cannot be converted today. Support a single-anchor mode: + constant hanging point from the chosen manual. +- **Drift diagnostic.** The methodology's key QC test: does the + converted series pass through *both* bounding manual measurements + (each repeatable within 0.02 ft)? If not, the logger is drifting and + the data must **not** be uploaded. We offer drift *correction* but no + drift *detection* — the workbench should report the misfit (ft) at + each bounding manual and warn when it exceeds a tolerance, before + Publish. +- **Manual-measurement quality.** The methodology classifies manuals by + repeatability (±0.02 ft) and distrusts low-quality ones as anchors. + Ties into the planned omission feature (session task #1): omission + should be informed by the measurement's stored quality flag, not only + hand-picked. +- **Overpressurization clipping.** A Diver pushed past its range records + its maximum pressure — a flat-topped plateau. Not detected today; a + "flatline at series max" check would catch it (the `Range` metadata in + the Diver header gives the exact ceiling). + +## Recommended order of work + +1. ✅ Fix Diver Office CSV parsing (500-line delimiter sample + `Location` + point-id pattern with compact-id normalization). Real export is a + committed regression fixture. +2. ✅ Field-data-logger `.txt` parser with low-battery warning surfaced on + the page. Real telemetry file is a committed fixture. +3. ✅ Dense-cluster reflection mode: 'baseline' detection flags readings + above the trailing lower quantile of a 15-sample window — clears + EB-165's clusters (nothing above 486 survives) while following the + genuine ~3.5 ft July rise. Selectable in the Clean panel. + *Deferred*: temperature-aware assist (temperature is still dropped at + parse time). +4. ✅ Single-anchor head→DTW conversion (constant calculated hanging point + per methodology eq. 2/3). +5. ✅ Drift diagnostic: converted water-head series is checked against + every in-coverage manual; misfits > 0.1 ft raise a workbench warning + citing the methodology's do-not-publish guidance. +6. ✅ Overpressurization detection: a plateau of ≥ 6 readings at the raw + head's maximum raises a clipping warning. + +Remaining from the analysis: manual-measurement quality flags feeding +anchor selection/omission (session task #1), and the temperature-aware +reflection assist. diff --git a/docs/hydrograph-correction-upload-contract.md b/docs/hydrograph-correction-upload-contract.md new file mode 100644 index 00000000..dc2aef4e --- /dev/null +++ b/docs/hydrograph-correction-upload-contract.md @@ -0,0 +1,334 @@ +# Hydrograph Correction — Upload API Contract (Proposal) + +Status: **draft / proposal** — no upload endpoint exists in the Ocotillo API yet. +This document specifies the contract the Hydrograph Correction workbench will +use to publish corrected transducer measurements to the Ocotillo database. + +## Background + +The workbench (`/ocotillo/hydrograph-correction`) ingests a raw logger file +(Diver Office water head, Wellntel acoustic, wellpy workbook, or generic CSV), +converts water head to depth to water below ground surface (ft bgs) using +manual observations as sensor-depth anchors, and lets the user apply +corrections (offset/zero cleanup, shifts, snaps, drift correction). The output +is a corrected time series that today can only be downloaded as CSV. + +The Ocotillo API already models stored transducer data as **observation +blocks**: + +- `GET /observation/transducer-groundwater-level` returns + `TransducerObservationWithBlockResponse` items — an `observation` + (`value`, `observation_datetime`, `parameter_id`, `deployment_id`, + `release_status`) paired with its `block` + (`start_datetime`, `end_datetime`, `parameter_id`, `release_status`, + `review_status`). +- There is no `POST` for transducer observations. The closest precedent is + `POST /observation/groundwater-level/bulk-upload` (multipart file) and + `POST /observation/groundwater-level` (single JSON observation). + +A corrected upload is a batch with shared provenance and review lifecycle, so +the natural unit of upload is **one block per corrected file**. + +## Proposed endpoint + +``` +POST /observation/transducer-groundwater-level/block +Content-Type: application/json +Authorization: Bearer (same OAuth2AuthorizationCodeBearer as the rest of the API) +``` + +### Request body + +```jsonc +{ + // Target well. Required. + "thing_id": 1234, + + // Deployment the data came from. Optional: when omitted, the server + // resolves the deployment for thing_id whose installation/removal dates + // cover the block's time span; 422 if none or more than one matches. + "deployment_id": 88, + + // Lexicon id for the observed parameter (depth to water bgs, ft). + // Required — the client sends it explicitly rather than assuming a + // server-side default, mirroring CreateGroundwaterLevelObservation. + "parameter_id": 7, + + // Block lifecycle. Both optional with safe defaults. + "release_status": "provisional", // default "draft"; enum release_status + "review_status": "not reviewed", // default "not reviewed"; enum review_status + + // Provenance for auditability. source_file is required; the rest optional. + "provenance": { + "source_file": "SO-0167_20250115.csv", + "source_kind": "water_head", // "water_head" | "depth_to_water" + // Free-form audit trail, in applied order. Snap entries name the manual + // anchor's collector when the field event records one — the alignment is + // only as good as the hand measurement it was pinned to — and say how the + // anchor value was obtained: "interpolated at the measurement time" when + // the manual falls inside the trace, "clamped to the nearest end of the + // trace" when it does not and the line cannot pass through it. + "corrections": [ + "convert_water_head (drift corrected)", + "remove_offsets_zeros (threshold 0.25)", + "shift (-1.25 ft, 2025-03-16T00:00:00Z to 2025-04-15T00:00:00Z)", + "snap_to_manual (+0.42 ft to 2025-02-11T17:00:00Z, interpolated at the measurement time, collected by Joseph Beman (NMBGMR))" + ], + "notes": "Snapped to 2025-04-13 manual measurement." + }, + + // The corrected series. Values are depth to water below ground surface in + // feet. Timestamps are ISO 8601; naive timestamps are rejected — the + // client must send an explicit offset (the workbench sends UTC). + // + // `note` is an optional per-observation correction annotation, present + // on every observation whose value was modified by a correction: shifts, + // snaps, level-offset removal, and estimates (e.g. a spurious acoustic + // reflection interpolated from its neighbors). Multiple corrections + // accumulate as semicolon-separated clauses. The server should persist + // it with the observation so downstream review can distinguish measured + // from corrected/estimated values. + + "measurements": [ + { "observation_datetime": "2025-01-15T00:00:00Z", "value": 42.51 }, + { + "observation_datetime": "2025-01-15T06:00:00Z", + "value": 42.55, + "note": "spurious reflection removed; value interpolated from neighbors (was 84.53)" + } + ] +} +``` + +### Server-side semantics + +- Exactly **one block** is created per request. `start_datetime` / + `end_datetime` are derived server-side from the min/max measurement + timestamps — the client does not send them. +- All measurements are created atomically with the block: either the whole + request commits or nothing does. +- **Overlap policy**: if an existing block for the same `thing_id` + + `parameter_id` overlaps the new block's time span, the request is rejected + with `409 Conflict` listing the overlapping block ids. A + `?replace_overlapping=true` query parameter deletes/supersedes the listed + blocks in the same transaction (requires the same permission as block + deletion). The UI always makes the first request without the flag and + surfaces the conflict to the user before retrying. + +### Validation rules (422 on violation) + +| Rule | Detail | +|---|---| +| Non-empty | `measurements` must contain at least 1 row | +| Batch cap | ≤ 100,000 rows per request (one request per logger file is expected; a 90-day 6-hour file is 360 rows) | +| Timestamps | ISO 8601 with explicit offset; strictly increasing (no duplicates) | +| Values | finite numbers; ft bgs; server may enforce a plausibility range per well (e.g. non-negative, less than well depth when known) | +| Enums | `release_status`, `review_status` must be valid enum members | +| Error shape | standard `HTTPValidationError`, with row indices in `loc` (e.g. `["body","measurements",41,"value"]`) so the UI can highlight offending rows | + +### Response — `201 Created` + +Mirrors the existing read shape so the UI can merge it straight into the +`GET /observation/transducer-groundwater-level` result set: + +```jsonc +{ + "block": { + "id": 512, + "created_at": "2026-07-28T17:04:11Z", + "release_status": "provisional", + "review_status": "not reviewed", + "start_datetime": "2025-01-15T00:00:00Z", + "end_datetime": "2025-04-14T18:00:00Z", + "parameter_id": 7 + }, + "observation_count": 356, + "thing_id": 1234, + "deployment_id": 88 +} +``` + +Individual observations are not echoed back (the client already has them); +`observation_count` confirms how many rows were written. + +### Errors + +| Status | Meaning | +|---|---| +| 401 | missing/expired token | +| 403 | authenticated but lacking write permission on the resource | +| 404 | `thing_id` or `deployment_id` not found | +| 409 | time-span overlap with existing block(s); body lists block ids | +| 422 | validation failure (see rules above) | + +## UI integration plan + +- The workbench gains a **Publish to Ocotillo** action (next to Download CSV) + that maps `correctedMeasurements` to `measurements`, fills `provenance` + from the session (file name, value kind, applied operations), and posts + via `ocotilloDataProvider`. +- Publishing is disabled in demo mode and until a well is resolved + (`thing_id` required). +- On 409, the UI shows the overlapping block spans and offers an explicit + "Replace existing block(s)" confirmation before retrying with + `replace_overlapping=true`. +- On success, the stored-transducer series is refetched so the new block + appears on the chart and in the data table. + +## Proposed endpoint — delete stored observations by time range + +The workbench's **Delete Stored Data** pane removes stored transducer +observations for a well over an explicit time range (re-ingesting a span that +was published from a bad file, clearing readings recorded while the sensor was +out of the water, and so on). No delete endpoint exists yet. + +``` +DELETE /observation/transducer-groundwater-level?thing_id=1234&start_time=2025-03-01T00:00:00Z&end_time=2025-03-15T00:00:00Z +Authorization: Bearer +``` + +Query parameters mirror the existing `GET` on the same path, so the set the +client previews with `GET` is exactly the set `DELETE` removes. + +| Parameter | Required | Detail | +|---|---|---| +| `thing_id` | yes | target well; deleting without it is rejected | +| `start_time` | yes | ISO 8601 with explicit offset; inclusive | +| `end_time` | yes | ISO 8601 with explicit offset; inclusive; must be after `start_time` | + +### Server-side semantics + +- Deletes every transducer observation for `thing_id` whose + `observation_datetime` falls within `[start_time, end_time]`, in one + transaction. +- Blocks are reconciled: a block fully covered by the range is deleted; a + block partially covered keeps its surviving observations and has its + `start_datetime` / `end_datetime` narrowed to them. +- Omitting `thing_id`, `start_time`, or `end_time` is a `422` — there is + deliberately no "delete everything" form of the request. + +### Response — `200 OK` + +```jsonc +{ + "deleted_observation_count": 1204, + "deleted_block_ids": [512], + "updated_block_ids": [498], + "thing_id": 1234 +} +``` + +### Errors + +| Status | Meaning | +|---|---| +| 401 | missing/expired token | +| 403 | authenticated but lacking delete permission | +| 404 | `thing_id` not found | +| 422 | missing bound, unparsable timestamp, or `end_time` <= `start_time` | + +### UI safeguards + +Deletion is irreversible, so the workbench gates it at several points: + +- The pane only renders when a real Ocotillo well is bound (never in demo + mode) **and** the signed-in user passes + `can({ resource: 'ocotillo.hydrograph-correction', action: 'delete' })` — + admin-only, one step above the editor access the rest of the page needs. + The gate is a UX affordance, not a security control; the server must still + enforce `403`. +- Both bounds must be entered explicitly. The brushed chart selection can be + copied into them with a button, but brushing alone never arms a deletion — + the brush scopes correction edits and reusing it here would let a stray drag + set up a delete. +- An inverted range resolves to "no range" rather than being silently + reordered, and reports the problem inline. +- The pending range is shaded on the chart over the stored series, so the + span being confirmed is visible against the data. +- The pane counts the stored observations inside the range before anything is + sent, and the delete button stays disabled while that count is zero. +- Confirmation is a modal that restates well, bounds, and the affected count + as a fraction of all stored observations, warns separately when the range + covers every stored observation, and requires the user to type the well + name — a per-well phrase, so the confirmation cannot be muscle-memoried. +- The dialog cannot be dismissed while the request is in flight, and the + stored series is refetched on success so the chart shows what remains. + +## Supporting endpoints for Wellntel ingestion + +The Wellntel ingest dialog (Ingest Wellntel on the Hydrograph Correction +page) needs two additional endpoints. The UI already calls both and falls +back to demo data when they are unavailable. + +### 1. Sensor-type filter on the thing list + +``` +GET /thing?sensor_type=Acoustic%20Sounder +``` + +Returns only things that have a deployment whose sensor is of the given +`sensor_type` (existing `sensor_type` enum; Wellntel units are +`"Acoustic Sounder"`). Used to restrict the dialog's well picker to wells +with a Wellntel sensor installed. Today this linkage is only walkable in +the other direction (`GET /sensor?thing_id=...`), which would force an +N+1 scan over every well. + +Alternative shape if filtering `/thing` is awkward server-side: a +dedicated `GET /deployment?sensor_type=...` list endpoint returning +`thing_id`s; the UI would then hydrate names with one thing query. + +### 2. Wellntel readings proxy + +``` +GET /wellntel/readings?thing_id=1234&start_time=...&end_time=... +``` + +Server-side proxy for the Wellntel analytics API +(`https://connect.wellntel.com/analytics-api/readings`). Rationale: + +- The Wellntel API key stays server-side. (wellpy currently stores the + key in client preferences — this is the chance to fix that.) +- The server owns the wellname→PointID mapping (wellpy hardcodes + `POINTID_MAP`; it should live in the database, e.g. on the deployment + or a thing-id-link). +- The Wellntel API pages at 1000 readings per request with cursor-style + `start` advancement; the proxy hides that pagination and returns the + full range. + +Response rows mirror wellpy's `.wcsv` export shape: + +```jsonc +{ + "items": [ + { "timestamp": "2025-01-15T06:00:00Z", "depth": 42.01, "temperature_C": 18.2 } + ], + "total": 356, + "page": 1, + "size": 10000, + "pages": 1 +} +``` + +`depth` is already depth to water bgs in feet — no head conversion. The +dialog defaults the requested `start_time` to the timestamp of the latest +stored transducer observation for the well (queried via +`GET /observation/transducer-groundwater-level?thing_id=...` sorted +descending, size 1 — requires that endpoint to honor `sort`/`order` +parameters), so recurring ingests continue where the last one ended. + +## Open questions + +1. **Parameter id source** — hardcode the DTW-bgs lexicon id in UI config, or + resolve it by name via the lexicon endpoint at runtime? +2. **Raw head retention** — should the request optionally carry the raw + water-head series (second parameter block) so the uncorrected signal is + preserved server-side, or is file/asset attachment the right home for it? +3. **Review workflow** — does publishing as `provisional` trigger any + existing review queue, or does review tooling need to grow a view for + transducer blocks? +4. **Wellntel cadence** — acoustic data may arrive via recurring API pulls + rather than file uploads; same endpoint, or a separate ingest path with + dedupe-by-timestamp instead of block overlap rejection? +5. **Wellntel identity mapping** — where should the wellname→PointID map + live (deployment metadata, thing-id-link, or a wellntel-specific + table), and who maintains it when new sensors are installed? diff --git a/docs/preview-deployments.md b/docs/preview-deployments.md new file mode 100644 index 00000000..338dee62 --- /dev/null +++ b/docs/preview-deployments.md @@ -0,0 +1,148 @@ +# Preview deployments + +A preview is a throwaway copy of the Ocotillo frontend on Cloud Run, reachable at +a public URL, built from any branch. Use one to put a work-in-progress in front +of a reviewer or a user without touching staging or production. + +Previews come in two flavours, chosen per deploy: + +| Backend | What the frontend talks to | Data | Use it when | +|---|---|---|---| +| `staging` (default) | `https://ocotillo-api-staging.newmexicowaterdata.org` | Real staging data, shared | The branch only changes the frontend | +| `ephemeral` | A per-branch API + postgis spun up for this preview alone | Fake seed data, disposable | The branch needs unreleased OcotilloAPI changes, or you want a sandbox nobody else can disturb | + +## Deploying + +### From a pull request + +Automatic. Opening, updating, or reopening a PR deploys a preview and comments +the URL on the PR. Closing the PR tears it down. + +To give a PR an ephemeral backend, add the **`preview-backend`** label to the PR +and push a commit (or re-run the workflow). Without the label a PR preview uses +staging. + +### From any branch, on demand + +```bash +gh workflow run CD_preview_ondemand.yml --ref my-branch +``` + +With an ephemeral backend built from a matching API branch: + +```bash +gh workflow run CD_preview_ondemand.yml \ + --ref my-branch \ + -f backend=ephemeral \ + -f backend_ref=BDMS-1234-new-endpoint \ + -f ttl_hours=24 +``` + +Or from the Actions tab: **Preview deploy (on demand)** → *Run workflow* → pick +the branch and the inputs. The preview URL lands in the run's job summary. + +`workflow_dispatch` reads the workflow *definition* from the default branch +(`staging`), but `--ref` selects the branch that actually gets built. + +Inputs: + +| Input | Default | Notes | +|---|---|---| +| `backend` | `staging` | `staging` or `ephemeral` | +| `backend_ref` | `staging` | Which `DataIntegrationGroup/OcotilloAPI` ref to build (ephemeral only) | +| `backend_auth` | `disabled` | See the warning below | +| `seed` | `true` | Runs `transfers.seed` against the ephemeral database | +| `ttl_hours` | `48` | Hours before the nightly sweep may delete the preview. `0` disables the TTL | + +## Tearing down + +Three automatic paths and one manual one: + +- **PR close** — `CD_preview.yml` tears the preview down. +- **Branch deletion** — `CD_preview_teardown.yml` fires on the `delete` event. +- **Nightly sweep** — 09:00 UTC, removes previews past their TTL and previews + whose branch no longer exists. +- **Manual**: + + ```bash + gh workflow run CD_preview_teardown.yml -f branch=my-branch + ``` + +Teardown deletes the frontend service, the ephemeral backend service if there is +one, both Artifact Registry image sets, and the authentik redirect URI. It is +idempotent — running it against a branch that has no preview succeeds and does +nothing. + +The `delete` and `schedule` triggers only fire from the repository's default +branch, which is **`staging`**. Until `CD_preview_teardown.yml` is merged there, +branch-deletion cleanup and the nightly sweep do not run. The same applies to +`CD_preview_ondemand.yml`: `workflow_dispatch` only appears once the file is on +`staging`. + +## How the ephemeral backend works + +It is the Cypress job's docker-compose stack, re-expressed as one multi-container +Cloud Run service. `CI_cypress.yml` runs OcotilloAPI's `docker/app/Dockerfile` +alongside `postgis/postgis:17-3.5` under `docker compose`; the preview builds the +same two images and deploys them as Cloud Run sidecars +([`.github/preview/api-service.tmpl.yaml`](../.github/preview/api-service.tmpl.yaml)). + +Sidecars share a network namespace, so the compose service name `db` simply +becomes `127.0.0.1:5432`. Everything else carries over: the same `alembic upgrade +head`, the same `python -m transfers.seed`. + +Two things the compose setup does not have to worry about: + +- **Cloud Run has no `exec`**, so the seed cannot be run as a follow-up command + the way `CI_cypress.yml` does it. The API container's `command` is overridden + with a small script that waits for postgres, migrates, seeds, then execs + uvicorn. +- **The instance is the database.** Postgres data lives on an in-memory `tmpfs` + volume, so the service is pinned to `minScale: 1, maxScale: 1` with CPU always + allocated. A revision restart wipes the data and re-seeds. + +### Limits and cautions + +> **The ephemeral API is publicly reachable and, by default, runs with +> `AUTHENTIK_DISABLE_AUTHENTICATION=1`.** The default exists because +> `transfers.seed` creates no users and no permission rows — with authentik +> enforcement on, a seeded preview locks every user out. Treat these previews as +> public sandboxes: never load real or sensitive data into one. Pass +> `-f backend_auth=enabled` if the branch is specifically exercising auth and you +> have another way to populate permissions. + +- Data does not survive a restart. Fine for a demo; not a place to park work. +- No GCS credentials are wired in, so file upload/download features will fail. +- `minScale: 1` means an ephemeral preview bills continuously until torn down — + the instance is the database, so it cannot scale to zero. At the current + sizing (2 vCPU and ~3.5 GiB across both containers) that is roughly $3–4 a + day. Keep the TTL short and tear previews down when you are finished. +- Switching a preview back to the staging API deletes its ephemeral backend on + the next deploy, so dropping the `preview-backend` label and pushing is enough + to stop the bill. + +## Layout + +| File | Role | +|---|---| +| `.github/workflows/_preview_deploy.yml` | Reusable: build + deploy frontend, optional backend, authentik registration | +| `.github/workflows/_preview_teardown.yml` | Reusable: delete services, images, authentik entry | +| `.github/workflows/CD_preview.yml` | PR trigger — calls both reusables, comments the URL | +| `.github/workflows/CD_preview_ondemand.yml` | `workflow_dispatch` trigger for any branch | +| `.github/workflows/CD_preview_teardown.yml` | Manual, branch-delete, and nightly-sweep triggers | +| `.github/preview/api-service.tmpl.yaml` | Cloud Run spec for the ephemeral backend | + +Both reusables key every resource off the same sanitized branch name. That +sanitizer is duplicated in the two files — **if you change one, change the +other**, or teardown will compute a different service name than deploy did and +silently leak resources. + +Cloud Run services are labelled `preview=true`, `preview-branch=`, +`preview-role=frontend|api`, and `preview-expires=` (`0` = no TTL), which +is how the nightly sweep finds them: + +```bash +gcloud run services list --region us-central1 \ + --filter 'metadata.labels.preview=true' \ + --format 'table(metadata.name, metadata.labels.preview-branch, metadata.labels.preview-expires, status.url)' +``` diff --git a/docs/public-page-content-audit.md b/docs/public-page-content-audit.md new file mode 100644 index 00000000..183c5231 --- /dev/null +++ b/docs/public-page-content-audit.md @@ -0,0 +1,58 @@ +# Public Page Content Audit + +## Summary + +Define what unauthenticated users should see on Ocotillo before public-page design begins. + +The public page should explain what Ocotillo is, how to access it, and who it is for without exposing internal data, workflows, or infrastructure. + +## Recommended Public Content + +The public page should include: + +- A sign-in button. +- Instructions for requesting access. +- A support contact method. +- A note that access is restricted to authorized users. + +Avoid assuming visitors already understand "Ocotillo" or internal acronyms. + +## Security + +Public pages should not expose: + +- API endpoints, infrastructure, environments, or bucket names +- Authentication or authorization implementation details +- Internal documentation, admin tools, dashboards, or logs +- Unpublished datasets, record identifiers, or data schemas +- Internal workflows +- Staff-only (nonpublic) contact information + +Public content should generally be limited to application information, authentication, support, and explicitly approved public services. + +## Public Routes + +Recommended public routes: + +- `/login` . +- `/callback` — Required for the authentication redirect flow +- `/about` +- `/analytics-disclosure` +- `/report-a-bug` +- `/ogcapi` — How to connect to ArcGIS manual + +Basically, the existing markdown files in `public/content/` for `about`, `analytics-disclosure`, `ogcapi`, and `report-a-bug` are good candidates for these public informational pages. + +## Protected Routes + +Keep application and data-management functionality authenticated, including: + +- `/home` in its current form. +- `/ocotillo/*` +- `/geothermal/*` +- `/st2/*` +- `/geochronology/*` +- `/example/*` +- Create, edit, import, correction, export, inventory, record-detail, and form routes. + +Error pages **should** be accessible without authentication but should **not** expose protected navigation or debugging information. diff --git a/docs/tests/ci-test-suite.md b/docs/tests/ci-test-suite.md new file mode 100644 index 00000000..6858f651 --- /dev/null +++ b/docs/tests/ci-test-suite.md @@ -0,0 +1,233 @@ +# CI Test Suite + +OcotilloUI runs several automated checks on every pull request. These checks are split into separate GitHub Actions workflows so linting, type checking, unit/integration tests, browser tests, and production build validation fail independently. + +## Workflow Summary + +| Workflow | Purpose | Main commands | +| ------------------------------------------- | --------------------------------------------- | --------------------------------------------------------------------------- | +| `.github/workflows/CI_lint.yml` | Biome linting and TypeScript type checking | `npm run lint`, `npm run typecheck` | +| `.github/workflows/CI_vitest.yml` | Generated API drift check and Vitest suite | `npx @hey-api/openapi-ts`, `npm run mock:server:vitest`, `npm run test:run` | +| `.github/workflows/CI_cypress.yml` | End-to-end browser tests against a seeded API | `npm run build:fast`, Cypress GitHub Action | +| `.github/workflows/CI_production_build.yml` | Production build validation | `npm run build:ci` | + +## Local Commands + +Use these commands before opening or updating a pull request: + +```bash +npm ci +npm run lint +npm run typecheck +npm run test:run +npm run build:ci +``` + +For coverage reports: + +```bash +npm run test:coverage +``` + +For interactive Vitest work: + +```bash +npm run test +npm run test:ui +``` + +## Lint and Typecheck + +The lint workflow runs on every pull request: + +```bash +npm run lint +npm run typecheck +``` + +`npm run lint` runs Biome against the repo. The Biome configuration lives in `biome.json`; more linting detail is documented in `LINTING.md`. + +`npm run typecheck` runs `tsc` using the repository TypeScript configuration. This catches type errors that may not appear while running Vite locally. + +Common failures: + +- Biome rule violations in changed TypeScript, React, JSON, or Markdown files +- Unused imports or variables that have been promoted to errors +- Type errors caused by changed interfaces, generated API types, props, or provider contracts +- Missing type declarations for new dependencies or imported modules + +## Generated API Drift Check + +The Vitest workflow first regenerates API types and Zod schemas: + +```bash +npx @hey-api/openapi-ts +``` + +It then stages `src/generated/` and checks whether regeneration changed any generated files. If Git sees generated changes, CI fails with instructions to run: + +```bash +npm run openapi:generate +``` + +This prevents pull requests from changing the OpenAPI spec or generator inputs without committing the resulting generated TypeScript and schema files. + +Common failures: + +- `openapi-auth.json` changed but `src/generated/` was not regenerated +- Generator configuration changed in `openapi-ts.config.ts` +- Generated files were edited manually and no longer match the spec output + +## Vitest + +Vitest tests are configured in `vite.config.ts` under the `test` key. The suite uses: + +- `globals: true` +- Node test environment +- `src/test/setup.ts` as the setup file +- V8 coverage through `@vitest/coverage-v8` + +The workflow starts a Prism mock server before running tests: + +```bash +npm run mock:server:vitest +npm run test:run +``` + +`npm run mock:server:vitest` runs: + +```bash +prism mock openapi-auth.json --dynamic=false --port 4010 +``` + +The workflow waits for `http://127.0.0.1:4010` to respond before starting Vitest. A response from `/` may be a 404 because Prism has no matching route for that path; CI treats any response as proof that the mock server is accepting connections. + +Test files are mostly under `src/test/`, with some colocated `*.test.ts` files in source folders. Current coverage includes utilities, components, hooks, providers, config, pages, hydrograph logic, and Ocotillo API contract tests. + +Common failures: + +- A component test needs missing setup in `src/test/setup.ts` +- API contract tests no longer match `openapi-auth.json` +- A provider changed request paths, response parsing, or error handling +- A test assumes browser APIs that are unavailable in the Node test environment +- Prism did not start on port `4010` + +## Cypress End-To-End Tests + +Cypress E2E tests run in `.github/workflows/CI_cypress.yml` on every pull request. These tests exercise the built frontend in Chrome against a real FastAPI backend and database. + +The workflow: + +1. Checks out this frontend repository. +2. Checks out `DataIntegrationGroup/OcotilloAPI` at the `staging` branch into `api-repo`. +3. Starts the backend with Docker Compose. +4. Waits for `http://localhost:8000/docs`. +5. Seeds the database with `python -m transfers.seed`. +6. Builds the frontend with `npm run build:fast`. +7. Runs Cypress against the Vite preview server. + +The Cypress config lives in `cypress.config.ts`. In CI, E2E tests use: + +```text +baseUrl: http://localhost:4173 +``` + +The workflow runs these specs: + +```text +cypress/e2e/test-api-connectivity.cy.ts +cypress/e2e/ocotillo/**/*.cy.ts +``` + +Authentication is disabled for CI by setting test-oriented environment variables, including: + +```text +NODE_ENV=test +MODE=development +OCOTILLO_API_URL=http://localhost:8000 +VITE_OCOTILLO_API_URL=http://localhost:8000 +VITE_TEST_AUTH=true +AUTHENTIK_DISABLE_AUTHENTICATION=${{ secrets.AUTHENTIK_TEST }} +``` + +Common failures: + +- Docker Compose cannot build or start the backend +- PostgreSQL is not ready before the app tries to connect +- The backend readiness probe at `/docs` times out +- Database seed data changed and the Cypress assertions need updating +- Frontend routes or labels changed without updating Cypress specs +- A Cypress test depends on record ordering that is not stable + +### Backend Changes That Can Break Cypress + +The Cypress workflow depends on the backend repository being able to build, migrate a fresh PostGIS database, start FastAPI, and load deterministic seed data. A backend-only change can therefore fail the frontend Cypress job even when no frontend code changed. + +The backend Docker entrypoint runs: + +```bash +alembic upgrade head +``` + +before starting Uvicorn. If the Alembic migration graph has more than one head, has a detached revision, has a missing `down_revision`, or contains a migration that cannot run from an empty database, the app container exits before FastAPI serves `/docs`. In the frontend Cypress workflow this usually appears as a timeout in the "Wait for FastAPI to be ready" step, not as a browser test failure. + +Backend-side causes to check first: + +- Multiple Alembic heads from parallel migrations that were not merged with `alembic merge heads` +- A migration file whose `down_revision` points at the wrong revision, a deleted revision, or `None` when it is not the base migration +- A migration that depends on local state, existing data, unavailable extensions, or a table/view not created earlier in the migration chain +- Model and migration drift where the app starts but seeded records fail because expected tables, columns, constraints, triggers, or materialized views are missing +- Startup environment changes such as renamed `POSTGRES_*`, `PYGEOAPI_POSTGRES_*`, `MODE`, `SESSION_SECRET_KEY`, or `AUTHENTIK_DISABLE_AUTHENTICATION` variables +- Docker Compose changes that rename the `app` or `db` service, change port `8000`, remove PostGIS initialization, or alter database names used by CI +- Changes to `transfers.seed` that make seed data nondeterministic, remove records Cypress asserts against, or require credentials/files unavailable in CI +- API route, response shape, status code, auth, CORS, pagination, filtering, sorting, or OpenAPI changes that the generated frontend client or Cypress specs still expect + +Useful backend checks before updating the frontend Cypress workflow dependency: + +```bash +uv run alembic heads +uv run pytest tests/integration/test_alembic_migrations.py tests/test_migrations.py +docker compose up --build +docker compose exec -T app python -m transfers.seed +curl -sf http://localhost:8000/docs +``` + +The backend repo already includes Alembic-focused tests that assert there is a single migration head and that migrations can upgrade a fresh database to `head`. Those checks should run in backend CI before backend changes are merged into the branch consumed by `.github/workflows/CI_cypress.yml`. + +## Production Build Validation + +The production build workflow runs on every pull request: + +```bash +npm run build:ci +``` + +`build:ci` runs: + +```bash +VITE_DISABLE_SOURCEMAP=true VITE_SENTRY_TELEMETRY_DISABLED=true NODE_OPTIONS="--max-old-space-size=4096" tsc && vite build +``` + +This check verifies that the app typechecks and can produce a production Vite build with source maps and Sentry telemetry disabled for CI stability. + +Common failures: + +- TypeScript errors that block `tsc` +- Vite build errors from invalid imports, missing assets, or environment assumptions +- Bundling issues caused by dependency changes +- Memory-sensitive build failures + +## Debugging Failed CI + +Start with the failing workflow name, then run the closest local command: + +| Failed workflow | First local command | +| ---------------------- | ---------------------------------------------------------------------------------- | +| Lint | `npm run lint` | +| Lint typecheck step | `npm run typecheck` | +| Vitest generated check | `npm run openapi:generate` | +| Vitest test step | `npm run mock:server:vitest` in one shell, then `npm run test:run` in another | +| Cypress | Reproduce with the backend, seeded database, and Cypress spec named in the CI logs | +| PR Build Test | `npm run build:ci` | + +When fixing tests, prefer updating the behavior or fixture data that changed rather than weakening assertions. If generated files changed, review the generated diff before committing it so API contract changes are intentional. diff --git a/eslint.config.ts b/eslint.config.ts deleted file mode 100644 index 65713a04..00000000 --- a/eslint.config.ts +++ /dev/null @@ -1,93 +0,0 @@ -import js from '@eslint/js' -import globals from 'globals' -import tseslint from 'typescript-eslint' -import reactPlugin from 'eslint-plugin-react' -import reactHooks from 'eslint-plugin-react-hooks' -import reactRefresh from 'eslint-plugin-react-refresh' -import prettierConfig from 'eslint-config-prettier' - -export default tseslint.config( - // Files and directories to ignore entirely - { - ignores: [ - 'dist/**', - 'node_modules/**', - 'src/generated/**', - 'coverage/**', - 'cypress/**', - ], - }, - - // Base JS recommended rules - js.configs.recommended, - - // React flat/recommended (ESLint 10 compatible) - // @ts-expect-error — flat config types not fully typed in this plugin version - reactPlugin.configs.flat.recommended, - // @ts-expect-error — flat config types not fully typed in this plugin version - reactPlugin.configs.flat['jsx-runtime'], - - // TypeScript + React-specific rules - { - files: ['**/*.{ts,tsx}'], - extends: [...tseslint.configs.recommended], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - parserOptions: { - ecmaFeatures: { jsx: true }, - }, - }, - plugins: { - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, - }, - rules: { - // Hooks - ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - - // TypeScript covers prop validation — disable the React-only version - 'react/prop-types': 'off', - 'react/display-name': 'off', - - // React Compiler rules — demote from error to warn until violations are - // fixed and the compiler is actually enabled. - // See docs/product/decisions/react-compiler.md in the-brain repo. - 'react-hooks/static-components': 'warn', - 'react-hooks/use-memo': 'warn', - 'react-hooks/preserve-manual-memoization': 'warn', - 'react-hooks/immutability': 'warn', - 'react-hooks/globals': 'warn', - 'react-hooks/refs': 'warn', - 'react-hooks/set-state-in-effect': 'warn', - 'react-hooks/error-boundaries': 'warn', - 'react-hooks/purity': 'warn', - 'react-hooks/set-state-in-render': 'warn', - 'react-hooks/config': 'warn', - 'react-hooks/gating': 'warn', - - // TypeScript — warn rather than error for a realistic first-run baseline - '@typescript-eslint/no-explicit-any': 'warn', - '@typescript-eslint/no-unused-vars': [ - 'warn', - { - argsIgnorePattern: '^_', - varsIgnorePattern: '^_', - ignoreRestSiblings: true, - }, - ], - '@typescript-eslint/no-empty-object-type': 'warn', - '@typescript-eslint/no-require-imports': 'warn', - }, - settings: { - react: { version: 'detect' }, - }, - }, - - // Disable all rules that conflict with Prettier formatting - prettierConfig, -) diff --git a/openapi-auth.json b/openapi-auth.json index 38407bf1..5061cfab 100644 --- a/openapi-auth.json +++ b/openapi-auth.json @@ -1 +1 @@ -{"openapi":"3.1.0","info":{"title":"Ocotillo API (Full)","description":"Full API schema (authorized users)","version":"0.0.1"},"paths":{"/asset/upload":{"post":{"tags":["asset"],"summary":"Upload Asset","operationId":"upload_asset_asset_upload_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_asset_asset_upload_post"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Upload Asset Asset Upload Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2AuthorizationCodeBearer":[]}]}},"/asset":{"post":{"tags":["asset"],"summary":"Add Asset","operationId":"add_asset_asset_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAsset"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["asset"],"summary":"List Assets","description":"List all assets or assets associated with a specific thing.","operationId":"list_assets_asset_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"query","required":false,"schema":{"type":"integer","title":"Thing Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_AssetResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/asset/{asset_id}":{"get":{"tags":["asset"],"summary":"Get Asset","description":"Retrieve an asset by its ID.","operationId":"get_asset_asset__asset_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"asset_id","in":"path","required":true,"schema":{"type":"integer","title":"Asset Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["asset"],"summary":"Update Asset","description":"Update an existing asset.","operationId":"update_asset_asset__asset_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"asset_id","in":"path","required":true,"schema":{"type":"integer","title":"Asset Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAsset"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["asset"],"summary":"Delete Asset","operationId":"delete_asset_asset__asset_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"asset_id","in":"path","required":true,"schema":{"type":"integer","title":"Asset Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/asset/{asset_id}/remove":{"delete":{"tags":["asset"],"summary":"Remove Asset","operationId":"remove_asset_asset__asset_id__remove_delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"asset_id","in":"path","required":true,"schema":{"type":"integer","title":"Asset Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/author/{author_id}/publications":{"get":{"tags":["author"],"summary":"Get Author Publications","description":"Retrieve all publications for a specific author.","operationId":"get_author_publications_author__author_id__publications_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"author_id","in":"path","required":true,"schema":{"type":"integer","title":"Author Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicationResponse"},"title":"Response Get Author Publications Author Author Id Publications Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact":{"post":{"tags":["contact"],"summary":"Create a new contact","operationId":"create_contact_contact_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateContact"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["contact"],"summary":"Get contacts","description":"Retrieve all contacts from the database.\n:param session:\n:return:","operationId":"get_contacts_contact_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"sort","in":"query","required":false,"schema":{"type":"string","title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"thing_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Thing Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_ContactResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact/address":{"post":{"tags":["contact"],"summary":"Add an address to a contact","description":"Add a new address to an existing contact in the database.\n:param contact_id: ID of the contact to add the address to\n:param address_data: Data for the new address\n:param session: Database session\n:return: Response containing the added address","operationId":"create_address_contact_address_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAddress"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddressResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["contact"],"summary":"Get all addresses","description":"Retrieve all addresses from the database.\n:param session:\n:return:","operationId":"get_addresses_contact_address_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_AddressResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact/email":{"post":{"tags":["contact"],"summary":"Add an email to a contact","operationId":"create_email_contact_email_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEmail"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["contact"],"summary":"Get all emails","description":"Retrieve all emails from the database.\n:param session:\n:return:","operationId":"get_emails_contact_email_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_EmailResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact/phone":{"post":{"tags":["contact"],"summary":"Add a phone number to a contact","operationId":"create_phone_contact_phone_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePhone"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhoneResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["contact"],"summary":"Get all phones","description":"Retrieve all phone numbers from the database.\n:param session:\n:return:","operationId":"get_phones_contact_phone_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_PhoneResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact/email/{email_id}":{"patch":{"tags":["contact"],"summary":"Update Contact Email","description":"Update an existing contact's email in the database.","operationId":"update_contact_email_contact_email__email_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"email_id","in":"path","required":true,"schema":{"type":"integer","title":"Email Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEmail"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["contact"],"summary":"Get email by ID","description":"Retrieve an email by ID from the database.","operationId":"get_email_by_id_contact_email__email_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"email_id","in":"path","required":true,"schema":{"type":"integer","title":"Email Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["contact"],"summary":"Delete contact email","description":"Delete a contact email by ID from the database.","operationId":"delete_contact_email_contact_email__email_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"email_id","in":"path","required":true,"schema":{"type":"integer","title":"Email Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact/phone/{phone_id}":{"patch":{"tags":["contact"],"summary":"Update Contact Phone","description":"Update an existing contact's phone number in the database.\n:param contact_id: ID of the contact to update\n:param phone_type: Type of the phone to update\n:param phone_number: New phone number\n:param session: Database session\n:return: Updated contact response","operationId":"update_contact_phone_contact_phone__phone_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"phone_id","in":"path","required":true,"schema":{"type":"integer","title":"Phone Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePhone"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhoneResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["contact"],"summary":"Get phone by ID","description":"Retrieve a phone by ID from the database.","operationId":"get_phone_by_id_contact_phone__phone_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"phone_id","in":"path","required":true,"schema":{"type":"integer","title":"Phone Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhoneResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["contact"],"summary":"Delete contact phone","description":"Delete a contact phone by ID from the database.","operationId":"delete_contact_phone_contact_phone__phone_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"phone_id","in":"path","required":true,"schema":{"type":"integer","title":"Phone Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact/address/{address_id}":{"patch":{"tags":["contact"],"summary":"Update Contact Address","description":"Update an existing contact's address in the database.\n\n:param address_id:\n:param address_data:\n:param session:\n:return:","operationId":"update_contact_address_contact_address__address_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"address_id","in":"path","required":true,"schema":{"type":"integer","title":"Address Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAddress"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddressResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["contact"],"summary":"Get address by ID","description":"Retrieve an address by ID from the database.","operationId":"get_address_by_id_contact_address__address_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"address_id","in":"path","required":true,"schema":{"type":"integer","title":"Address Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddressResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["contact"],"summary":"Delete contact address","description":"Delete a contact address by ID from the database.","operationId":"delete_contact_address_contact_address__address_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"address_id","in":"path","required":true,"schema":{"type":"integer","title":"Address Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact/{contact_id}":{"patch":{"tags":["contact"],"summary":"Update contact","description":"Update an existing contact in the database.\n:param contact_id: ID of the contact to update\n:param contact_data: Data to update the contact with\n:param session: Database session\n:return: Updated contact response","operationId":"update_contact_contact__contact_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"integer","title":"Contact Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateContact"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["contact"],"summary":"Get contact by ID","description":"Retrieve a contact by ID from the database.","operationId":"get_contact_by_id_contact__contact_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"integer","title":"Contact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["contact"],"summary":"Delete contact","description":"Delete a contact by ID from the database.","operationId":"delete_contact_contact__contact_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"integer","title":"Contact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact/{contact_id}/email":{"get":{"tags":["contact"],"summary":"Get contact emails","description":"Retrieve all emails associated with a contact.","operationId":"get_contact_emails_contact__contact_id__email_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"integer","title":"Contact Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_EmailResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact/{contact_id}/phone":{"get":{"tags":["contact"],"summary":"Get contact phones","description":"Retrieve all phone numbers associated with a contact.","operationId":"get_contact_phones_contact__contact_id__phone_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"integer","title":"Contact Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_PhoneResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact/{contact_id}/address":{"get":{"tags":["contact"],"summary":"Get contact addresses","description":"Retrieve all addresses associated with a contact.","operationId":"get_contact_addresses_contact__contact_id__address_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"integer","title":"Contact Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_AddressResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/geospatial":{"get":{"tags":["geospatial"],"summary":"Get Geospatial","description":"Endpoint to retrieve a GeoJSON FeatureCollection or a shapefile.\nIf the request is for a shapefile, it will return a zip file containing the shapefile.\nOtherwise, it returns a GeoJSON FeatureCollection.","operationId":"get_geospatial_geospatial_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_type","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"title":"thing_type"}},{"name":"group","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"integer"}],"title":"group"}},{"name":"format","in":"query","required":false,"schema":{"type":"string","pattern":"^(geojson|shapefile)$","title":"format","description":"Format of the response. 'geojson' for GeoJSON FeatureCollection, 'shapefile' for a shapefile.","default":"geojson"},"description":"Format of the response. 'geojson' for GeoJSON FeatureCollection, 'shapefile' for a shapefile."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/geospatial/project-area/{group_id}":{"get":{"tags":["geospatial"],"summary":"Get project area for group","operationId":"get_project_area_geospatial_project_area__group_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"group_id","in":"path","required":true,"schema":{"type":"integer","title":"Group Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureCollectionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/group":{"post":{"tags":["group"],"summary":"Create a new group","description":"Create a new group in the database.","operationId":"create_group_group_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGroup"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["group"],"summary":"Get groups","description":"Retrieve all groups from the database.","operationId":"get_groups_group_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_GroupResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/group/{group_id}":{"get":{"tags":["group"],"summary":"Get group by ID","description":"Retrieve a group by ID from the database.","operationId":"get_group_by_id_group__group_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"group_id","in":"path","required":true,"schema":{"type":"integer","title":"Group Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["group"],"summary":"Update a group by ID","description":"Update a group by ID in the database.","operationId":"update_group_group__group_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"group_id","in":"path","required":true,"schema":{"type":"integer","title":"Group Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateGroup"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["group"],"summary":"Delete a group by ID","operationId":"delete_group_group__group_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"group_id","in":"path","required":true,"schema":{"type":"integer","title":"Group Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/lexicon/category":{"post":{"tags":["lexicon"],"summary":"Add Category","description":"Endpoint to add a category to the lexicon.","operationId":"add_category_lexicon_category_post","deprecated":true,"security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLexiconCategory"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LexiconCategoryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["lexicon"],"summary":"Get Lexicon Categories","description":"Endpoint to retrieve lexicon categories.","operationId":"get_lexicon_categories_lexicon_category_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"name","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","default":"name","title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","default":"asc","title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_LexiconCategoryResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/lexicon/term":{"post":{"tags":["lexicon"],"summary":"Add term","description":"Endpoint to add a term to the lexicon.","operationId":"add_term_lexicon_term_post","deprecated":true,"security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLexiconTerm"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LexiconTermResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["lexicon"],"summary":"Get lexicon terms","description":"Endpoint to retrieve lexicon terms.","operationId":"get_lexicon_terms_lexicon_term_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"}},{"name":"term","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Term"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_LexiconTermResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/lexicon/triple":{"post":{"tags":["lexicon"],"summary":"Add triple","operationId":"add_triple_lexicon_triple_post","deprecated":true,"security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLexiconTriple"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LexiconTripleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["lexicon"],"summary":"Get lexicon triples","description":"Endpoint to retrieve lexicon triples.","operationId":"get_lexicon_triples_lexicon_triple_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"sort","in":"query","required":false,"schema":{"type":"string","default":"subject","title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","default":"asc","title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_LexiconTripleResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/lexicon/term/{term_id}":{"patch":{"tags":["lexicon"],"summary":"Update Lexicon Term","operationId":"update_lexicon_term_lexicon_term__term_id__patch","deprecated":true,"security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"term_id","in":"path","required":true,"schema":{"type":"integer","title":"Term Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLexiconTerm"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LexiconTermResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["lexicon"],"summary":"Get Lexicon Term","operationId":"get_lexicon_term_lexicon_term__term_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"term_id","in":"path","required":true,"schema":{"type":"integer","title":"Term Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LexiconTermResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["lexicon"],"summary":"Delete a lexicon term by ID","operationId":"delete_lexicon_term_lexicon_term__term_id__delete","deprecated":true,"security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"term_id","in":"path","required":true,"schema":{"type":"integer","title":"Term Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/lexicon/category/{category_id}":{"patch":{"tags":["lexicon"],"summary":"Update Lexicon Category","operationId":"update_lexicon_category_lexicon_category__category_id__patch","deprecated":true,"security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"category_id","in":"path","required":true,"schema":{"type":"integer","title":"Category Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLexiconCategory"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LexiconCategoryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["lexicon"],"summary":"Get Lexicon Category","operationId":"get_lexicon_category_lexicon_category__category_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"category_id","in":"path","required":true,"schema":{"type":"integer","title":"Category Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LexiconCategoryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["lexicon"],"summary":"Delete a lexicon category by ID","operationId":"delete_lexicon_category_lexicon_category__category_id__delete","deprecated":true,"security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"category_id","in":"path","required":true,"schema":{"type":"integer","title":"Category Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/lexicon/triple/{triple_id}":{"patch":{"tags":["lexicon"],"summary":"Update Lexicon Triple","operationId":"update_lexicon_triple_lexicon_triple__triple_id__patch","deprecated":true,"security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"triple_id","in":"path","required":true,"schema":{"type":"integer","title":"Triple Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLexiconTriple"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LexiconTripleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["lexicon"],"summary":"Get Lexicon Triple","operationId":"get_lexicon_triple_lexicon_triple__triple_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"triple_id","in":"path","required":true,"schema":{"type":"integer","title":"Triple Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LexiconTripleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["lexicon"],"summary":"Delete a lexicon triple by ID","operationId":"delete_lexicon_triple_lexicon_triple__triple_id__delete","deprecated":true,"security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"triple_id","in":"path","required":true,"schema":{"type":"integer","title":"Triple Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/location":{"post":{"tags":["location"],"summary":"Create a new sample location","description":"Create a new sample location in the database.","operationId":"create_location_location_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLocation"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LocationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["location"],"summary":"Get all locations","description":"Retrieve all wells from the database.","operationId":"get_location_location_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"nearby_point","in":"query","required":false,"schema":{"type":"string","title":"Nearby Point"}},{"name":"nearby_distance_km","in":"query","required":false,"schema":{"type":"number","default":1,"title":"Nearby Distance Km"}},{"name":"within","in":"query","required":false,"schema":{"type":"string","title":"Within"}},{"name":"query","in":"query","required":false,"schema":{"type":"string","title":"Query"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_LocationResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/location/{location_id}":{"patch":{"tags":["location"],"summary":"Update a location","description":"Update a sample location in the database.","operationId":"update_location_location__location_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"location_id","in":"path","required":true,"schema":{"type":"integer","title":"Location Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLocation"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LocationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["location"],"summary":"Get location by ID","description":"Retrieve a sample location by ID from the database.","operationId":"get_location_by_id_location__location_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"location_id","in":"path","required":true,"schema":{"type":"integer","title":"Location Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LocationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["location"],"summary":"Delete location by ID","description":"Delete a sample location by ID from the database.","operationId":"delete_location_location__location_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"location_id","in":"path","required":true,"schema":{"type":"integer","title":"Location Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/observation/groundwater-level":{"post":{"tags":["observation"],"summary":"Add Groundwater Level Observation","description":"Add a new groundwater observation to the database.","operationId":"add_groundwater_level_observation_observation_groundwater_level_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGroundwaterLevelObservation"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroundwaterLevelObservationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["observation"],"summary":"Get groundwater level observations","description":"Retrieve all groundwater level observations from the database.","operationId":"get_groundwater_level_observations_observation_groundwater_level_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Thing Id"}},{"name":"sensor_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sensor Id"}},{"name":"sample_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sample Id"}},{"name":"start_time","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Start Time"}},{"name":"end_time","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"End Time"}},{"name":"sort","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_GroundwaterLevelObservationResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/observation/water-chemistry":{"post":{"tags":["observation"],"summary":"Add Water Chemistry Observation","description":"Add a new water chemistry observation to the database.\nThis endpoint is currently a placeholder and does not implement any functionality.","operationId":"add_water_chemistry_observation_observation_water_chemistry_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWaterChemistryObservation"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaterChemistryObservationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["observation"],"summary":"Get water chemistry observations","description":"Retrieve all water chemistry observations from the database.","operationId":"get_water_chemistry_observations_observation_water_chemistry_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Thing Id"}},{"name":"sensor_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sensor Id"}},{"name":"sample_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sample Id"}},{"name":"start_time","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Start Time"}},{"name":"end_time","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"End Time"}},{"name":"sort","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_WaterChemistryObservationResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/observation/groundwater-level/bulk-upload":{"post":{"tags":["observation"],"summary":"Bulk Upload Groundwater Levels","operationId":"bulk_upload_groundwater_levels_observation_groundwater_level_bulk_upload_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_bulk_upload_groundwater_levels_observation_groundwater_level_bulk_upload_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaterLevelBulkUploadResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2AuthorizationCodeBearer":[]}]}},"/observation/groundwater-level/{observation_id}":{"patch":{"tags":["observation"],"summary":"Update Groundwater Level Observation","description":"Update an existing groundwater level observation in the database.","operationId":"update_groundwater_level_observation_observation_groundwater_level__observation_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"observation_id","in":"path","required":true,"schema":{"type":"integer","title":"Observation Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateGroundwaterLevelObservation"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroundwaterLevelObservationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["observation"],"summary":"Get groundwater level observation by ID","operationId":"get_groundwater_level_observation_by_id_observation_groundwater_level__observation_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"observation_id","in":"path","required":true,"schema":{"type":"integer","title":"Observation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroundwaterLevelObservationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/observation/water-chemistry/{observation_id}":{"patch":{"tags":["observation"],"summary":"Update Water Chemistry Observation","description":"Update an existing water chemistry observation in the database.","operationId":"update_water_chemistry_observation_observation_water_chemistry__observation_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"observation_id","in":"path","required":true,"schema":{"type":"integer","title":"Observation Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWaterChemistryObservation"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaterChemistryObservationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["observation"],"summary":"Get water chemistry observation by ID","operationId":"get_water_chemistry_observation_by_id_observation_water_chemistry__observation_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"observation_id","in":"path","required":true,"schema":{"type":"integer","title":"Observation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaterChemistryObservationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/observation/transducer-groundwater-level":{"get":{"tags":["observation"],"summary":"Get transducer groundwater level observations","operationId":"get_transducer_groundwater_level_observations_observation_transducer_groundwater_level_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Thing Id"}},{"name":"start_time","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Start Time"}},{"name":"end_time","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"End Time"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_TransducerObservationWithBlockResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/observation":{"get":{"tags":["observation"],"summary":"Get all observations","operationId":"get_all_observations_observation_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Thing Id"}},{"name":"sensor_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sensor Id"}},{"name":"sample_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sample Id"}},{"name":"start_time","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Start Time"}},{"name":"end_time","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"End Time"}},{"name":"sort","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_ObservationResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/observation/{observation_id}":{"get":{"tags":["observation"],"summary":"Get an observation by its ID","operationId":"get_observation_by_id_observation__observation_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"observation_id","in":"path","required":true,"schema":{"type":"integer","title":"Observation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ObservationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["observation"],"summary":"Delete an observation","operationId":"delete_observation_observation__observation_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"observation_id","in":"path","required":true,"schema":{"type":"integer","title":"Observation Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/publication/add":{"post":{"tags":["publication"],"summary":"Post Publication","description":"Add a new publication.","operationId":"post_publication_publication_add_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePublication"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2AuthorizationCodeBearer":[]}]}},"/sample":{"post":{"tags":["sample"],"summary":"Add Sample","description":"Endpoint to add a sample.","operationId":"add_sample_sample_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSample"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SampleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["sample"],"summary":"Get Samples","description":"Endpoint to retrieve samples.","operationId":"get_samples_sample_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Thing Id"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_SampleResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sample/{sample_id}":{"patch":{"tags":["sample"],"summary":"Update Sample","description":"Endpoint to update a sample.","operationId":"update_sample_sample__sample_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"sample_id","in":"path","required":true,"schema":{"type":"integer","title":"Sample Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSample"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/SampleResponse"},{"$ref":"#/components/schemas/ResourceNotFoundResponse"}],"title":"Response Update Sample Sample Sample Id Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["sample"],"summary":"Get Sample by ID","description":"Endpoint to retrieve a sample by its ID.","operationId":"get_sample_by_id_sample__sample_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"sample_id","in":"path","required":true,"schema":{"type":"integer","title":"Sample Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/SampleResponse"},{"$ref":"#/components/schemas/ResourceNotFoundResponse"}],"title":"Response Get Sample By Id Sample Sample Id Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["sample"],"summary":"Delete Sample by ID","operationId":"delete_sample_by_id_sample__sample_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"sample_id","in":"path","required":true,"schema":{"type":"integer","title":"Sample Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sensor":{"post":{"tags":["sensor"],"summary":"Add Sensor","description":"Add a sensor to the system.","operationId":"add_sensor_sensor_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSensor"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SensorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["sensor"],"summary":"Get Sensors","description":"Retrieve all sensors from the system.\nThis endpoint is a placeholder and should be implemented with actual logic.","operationId":"get_sensors_sensor_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"query","required":false,"schema":{"type":"integer","title":"Thing Id"}},{"name":"parameter_id","in":"query","required":false,"schema":{"type":"integer","title":"Parameter Id"}},{"name":"sort","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_SensorResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sensor/{sensor_id}":{"patch":{"tags":["sensor"],"summary":"Update Sensor","description":"Update a sensor in the system.","operationId":"update_sensor_sensor__sensor_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"sensor_id","in":"path","required":true,"schema":{"type":"integer","title":"Sensor Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSensor"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SensorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["sensor"],"summary":"Delete Sensor","description":"Delete a sensor in the system","operationId":"delete_sensor_sensor__sensor_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"sensor_id","in":"path","required":true,"schema":{"type":"integer","title":"Sensor Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["sensor"],"summary":"Get Sensor","description":"Retrieve a sensor by its ID.","operationId":"get_sensor_sensor__sensor_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"sensor_id","in":"path","required":true,"schema":{"type":"integer","title":"Sensor Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SensorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/search":{"get":{"tags":["search"],"summary":"Search Api","description":"Search endpoint for the collaborative network.","operationId":"search_api_search_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","title":"Q"}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":25,"title":"Limit"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_dict_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/water-well":{"get":{"tags":["thing"],"summary":"Get all water wells","description":"Retrieve all wells from the database.","operationId":"get_water_wells_thing_water_well_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"sort","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"query","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Query"}},{"name":"name","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"}},{"name":"include_contacts","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Contacts"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_WellResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["thing"],"summary":"Create a water well","description":"Create a new water well in the database.","operationId":"create_well_thing_water_well_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWell"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WellResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/water-well/{thing_id}":{"get":{"tags":["thing"],"summary":"Get water well by ID","description":"Retrieve a water well by ID from the database.","operationId":"get_well_by_id_thing_water_well__thing_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WellResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["thing"],"summary":"Update well by parent thing ID","description":"Update an existing well by ID.","operationId":"update_water_well_thing_water_well__thing_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWell"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WellResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/water-well/{thing_id}/details":{"get":{"tags":["thing"],"summary":"Get water well details payload","description":"Retrieve the consolidated payload needed to render the well details page.\nHydrograph series and map layer loading are intentionally handled separately.","operationId":"get_well_details_thing_water_well__thing_id__details_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}},{"name":"field_event_limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Field Event Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WellDetailsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/water-well/{thing_id}/export":{"get":{"tags":["thing"],"summary":"Get water well export payload","description":"Retrieve the minimal payload needed for field sheet export generation.","operationId":"get_well_export_thing_water_well__thing_id__export_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WellExportResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/water-well/{thing_id}/well-screen":{"get":{"tags":["thing"],"summary":"Get well screens by water well ID","description":"Retrieve all well screens for a specific water well by its ID.","operationId":"get_well_screens_by_well_id_thing_water_well__thing_id__well_screen_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_WellScreenResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/well-screen":{"get":{"tags":["thing"],"summary":"Get well screens","description":"Retrieve all well screens from the database.","operationId":"get_well_screens_thing_well_screen_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"query","required":false,"schema":{"type":"integer","title":"Thing Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_WellScreenResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["thing"],"summary":"Create a new well screen","description":"Create a new well screen in the database.","operationId":"create_wellscreen_thing_well_screen_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWellScreen"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WellScreenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/well-screen/{wellscreen_id}":{"get":{"tags":["thing"],"summary":"Get well screen by ID","description":"Retrieve a well screen by ID from the database.","operationId":"get_well_screen_by_id_thing_well_screen__wellscreen_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"wellscreen_id","in":"path","required":true,"schema":{"type":"integer","title":"Wellscreen Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WellScreenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/spring":{"get":{"tags":["thing"],"summary":"Get all springs","description":"Retrieve all springs from the database.","operationId":"get_springs_thing_spring_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"sort","in":"query","required":false,"schema":{"type":"string","title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"query","in":"query","required":false,"schema":{"type":"string","title":"Query"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_SpringResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["thing"],"summary":"Create a new spring","description":"Create a new well in the database.","operationId":"create_spring_thing_spring_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSpring"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpringResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/spring/{thing_id}":{"get":{"tags":["thing"],"summary":"Get spring by ID","description":"Retrieve a spring by ID from the database.","operationId":"get_spring_by_id_thing_spring__thing_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpringResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["thing"],"summary":"Update spring by parent thing ID","description":"Update an existing spring by ID.","operationId":"update_spring_thing_spring__thing_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSpring"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpringResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/id-link":{"get":{"tags":["thing"],"summary":"Get all thing links","description":"Retrieve all thing links, optionally filtered and sorted.","operationId":"get_thing_id_links_thing_id_link_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","title":"Order"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_ThingIdLinkResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["thing"],"summary":"Create a new thing link","description":"Create a new link between a thing and an alternate ID.","operationId":"create_thing_id_link_thing_id_link_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateThingIdLink"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThingIdLinkResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/id-link/{link_id}":{"get":{"tags":["thing"],"summary":"Get thing links by link ID","description":"Retrieve all links for a specific thing by its ID.","operationId":"get_thing_id_links_thing_id_link__link_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"link_id","in":"path","required":true,"schema":{"type":"integer","title":"Link Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThingIdLinkResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["thing"],"summary":"Update thing link by ID","operationId":"update_thing_id_link_thing_id_link__link_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"link_id","in":"path","required":true,"schema":{"type":"integer","title":"Link Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateThingIdLink"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThingIdLinkResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["thing"],"summary":"Delete thing link by ID","description":"Delete a thing link by ID.","operationId":"delete_thing_id_link_thing_id_link__link_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"link_id","in":"path","required":true,"schema":{"type":"integer","title":"Link Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing":{"get":{"tags":["thing"],"summary":"Get all things","description":"Retrieve all things or filter by type.","operationId":"get_things_thing_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"within","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Within"}},{"name":"query","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Query"}},{"name":"sort","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Order"}},{"name":"include_contacts","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Contacts"}},{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_ThingResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/{thing_id}":{"get":{"tags":["thing"],"summary":"Get thing by ID","description":"Retrieve a thing by ID from the database.","operationId":"get_thing_by_id_thing__thing_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThingResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["thing"],"summary":"Delete thing by ID","description":"Delete a thing by ID.","operationId":"delete_thing_thing__thing_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/{thing_id}/id-link":{"get":{"tags":["thing"],"summary":"Get thing links by thing ID","description":"Retrieve all links for a specific thing by its ID.","operationId":"get_thing_id_links_thing__thing_id__id_link_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_ThingIdLinkResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/{thing_id}/deployment":{"get":{"tags":["thing"],"summary":"Get deployments by thing ID","description":"Retrieve all deployments for a specific thing by its ID.","operationId":"get_thing_deployments_thing__thing_id__deployment_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_DeploymentResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/well-screen/{well_screen_id}":{"patch":{"tags":["thing"],"summary":"Update Well Screen by ID","operationId":"update_well_screen_thing_well_screen__well_screen_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"well_screen_id","in":"path","required":true,"schema":{"type":"integer","title":"Well Screen Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWellScreen"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WellScreenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["thing"],"summary":"Delete well screen by ID","description":"Delete a well screen by ID.","operationId":"delete_well_screen_thing_well_screen__well_screen_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"well_screen_id","in":"path","required":true,"schema":{"type":"integer","title":"Well Screen Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ngwmn/waterlevels/{pointid}":{"get":{"tags":["NGWMN"],"summary":"Get waterlevels for a given pointid in the NGWMN format","operationId":"read_ngwmn_waterlevels_ngwmn_waterlevels__pointid__get","parameters":[{"name":"pointid","in":"path","required":true,"schema":{"type":"string","title":"Pointid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ngwmn/wellconstruction/{pointid}":{"get":{"tags":["NGWMN"],"summary":"Get wellconstruction for a given pointid in the NGWMN format","operationId":"read_ngwmn_wellconstruction_ngwmn_wellconstruction__pointid__get","parameters":[{"name":"pointid","in":"path","required":true,"schema":{"type":"string","title":"Pointid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ngwmn/lithology/{pointid}":{"get":{"tags":["NGWMN"],"summary":"Get lithology for a given pointid in the NGWMN format","operationId":"read_ngwmn_lithology_ngwmn_lithology__pointid__get","parameters":[{"name":"pointid","in":"path","required":true,"schema":{"type":"string","title":"Pointid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AddressResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"contact_id":{"type":"integer","title":"Contact Id"},"address_line_1":{"type":"string","title":"Address Line 1"},"address_line_2":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address Line 2"},"city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"postal_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Postal Code"},"country":{"type":"string","title":"Country"},"address_type":{"$ref":"#/components/schemas/address_type"}},"type":"object","required":["id","created_at","release_status","contact_id","address_line_1","country","address_type"],"title":"AddressResponse","description":"Response schema for address details."},"AssetResponse":{"properties":{"name":{"type":"string","title":"Name"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"},"storage_path":{"type":"string","title":"Storage Path"},"mime_type":{"type":"string","title":"Mime Type"},"size":{"type":"integer","title":"Size"},"uri":{"type":"string","title":"Uri"},"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"storage_service":{"type":"string","title":"Storage Service"},"signed_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Signed Url"}},"type":"object","required":["name","storage_path","mime_type","size","uri","id","created_at","release_status","storage_service"],"title":"AssetResponse"},"AuthorResponse":{"properties":{"id":{"type":"integer","title":"Id"},"name":{"type":"string","title":"Name"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"affiliation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Affiliation"}},"type":"object","required":["id","name"],"title":"AuthorResponse","description":"Schema for the response of an author."},"Body_bulk_upload_groundwater_levels_observation_groundwater_level_bulk_upload_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_bulk_upload_groundwater_levels_observation_groundwater_level_bulk_upload_post"},"Body_upload_asset_asset_upload_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_upload_asset_asset_upload_post"},"ContactResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"organization":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Organization"},"role":{"$ref":"#/components/schemas/role"},"contact_type":{"$ref":"#/components/schemas/contact_type"},"incomplete_nma_phones":{"items":{"type":"string"},"type":"array","title":"Incomplete Nma Phones","default":[]},"emails":{"items":{"$ref":"#/components/schemas/EmailResponse"},"type":"array","title":"Emails","default":[]},"phones":{"items":{"$ref":"#/components/schemas/PhoneResponse"},"type":"array","title":"Phones","default":[]},"addresses":{"items":{"$ref":"#/components/schemas/AddressResponse"},"type":"array","title":"Addresses","default":[]},"things":{"items":{"$ref":"#/components/schemas/ThingResponseForContact"},"type":"array","title":"Things","default":[]},"communication_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Communication Notes","default":[]},"general_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"General Notes","default":[]}},"type":"object","required":["id","created_at","release_status","name","organization","role","contact_type"],"title":"ContactResponse","description":"Response schema for contact details."},"CreateAddress":{"properties":{"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"contact_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Contact Id"},"address_line_1":{"type":"string","title":"Address Line 1"},"address_line_2":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address Line 2"},"city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State","default":"NM"},"postal_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Postal Code"},"country":{"type":"string","title":"Country","default":"United States"},"address_type":{"$ref":"#/components/schemas/address_type","default":"Primary"}},"type":"object","required":["address_line_1"],"title":"CreateAddress","description":"Schema for creating an address."},"CreateAsset":{"properties":{"name":{"type":"string","title":"Name"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"},"storage_path":{"type":"string","title":"Storage Path"},"mime_type":{"type":"string","title":"Mime Type"},"size":{"type":"integer","title":"Size"},"uri":{"type":"string","title":"Uri"},"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"thing_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Thing Id"}},"type":"object","required":["name","storage_path","mime_type","size","uri"],"title":"CreateAsset"},"CreateContact":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"organization":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Organization"},"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"thing_id":{"type":"integer","title":"Thing Id"},"role":{"$ref":"#/components/schemas/role"},"contact_type":{"$ref":"#/components/schemas/contact_type"},"nma_pk_owners":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Pk Owners"},"emails":{"anyOf":[{"items":{"$ref":"#/components/schemas/CreateEmail"},"type":"array"},{"type":"null"}],"title":"Emails"},"phones":{"anyOf":[{"items":{"$ref":"#/components/schemas/CreatePhone"},"type":"array"},{"type":"null"}],"title":"Phones"},"addresses":{"anyOf":[{"items":{"$ref":"#/components/schemas/CreateAddress"},"type":"array"},{"type":"null"}],"title":"Addresses"},"notes":{"anyOf":[{"items":{"$ref":"#/components/schemas/CreateNote"},"type":"array"},{"type":"null"}],"title":"Notes"}},"type":"object","required":["thing_id","role","contact_type"],"title":"CreateContact","description":"Schema for creating a contact."},"CreateEmail":{"properties":{"email":{"type":"string","title":"Email"},"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"contact_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Contact Id"},"email_type":{"$ref":"#/components/schemas/email_type","default":"Primary"}},"type":"object","required":["email"],"title":"CreateEmail","description":"Schema for creating an email."},"CreateGroundwaterLevelObservation":{"properties":{"parameter_id":{"type":"integer","title":"Parameter Id"},"observation_datetime":{"type":"string","format":"date-time","title":"Observation Datetime"},"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"sample_id":{"type":"integer","title":"Sample Id"},"sensor_id":{"type":"integer","title":"Sensor Id"},"value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Value"},"unit":{"anyOf":[{"$ref":"#/components/schemas/unit"},{"type":"null"}]},"measuring_point_height":{"type":"number","title":"Measuring Point Height"},"groundwater_level_reason":{"type":"string","title":"Groundwater Level Reason"}},"type":"object","required":["parameter_id","observation_datetime","sample_id","sensor_id","value","unit","measuring_point_height","groundwater_level_reason"],"title":"CreateGroundwaterLevelObservation"},"CreateGroup":{"properties":{"project_area":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Project Area"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"parent_group_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Parent Group Id"},"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"name":{"type":"string","title":"Name"}},"type":"object","required":["name"],"title":"CreateGroup","description":"Schema for creating a group."},"CreateLexiconCategory":{"properties":{"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"}},"type":"object","required":["name"],"title":"CreateLexiconCategory","description":"Pydantic model for creating a lexicon category.\nThis model can be extended to include additional fields as needed."},"CreateLexiconTerm":{"properties":{"term":{"type":"string","title":"Term"},"definition":{"type":"string","title":"Definition"},"categories":{"items":{"type":"string"},"type":"array","title":"Categories"}},"type":"object","required":["term","definition","categories"],"title":"CreateLexiconTerm","description":"Pydantic model for creating a lexicon term.\nThis model can be extended to include additional fields as needed."},"CreateLexiconTriple":{"properties":{"subject":{"$ref":"#/components/schemas/CreateLexiconTerm"},"predicate":{"type":"string","title":"Predicate"},"object_":{"$ref":"#/components/schemas/CreateLexiconTerm"}},"type":"object","required":["subject","predicate","object_"],"title":"CreateLexiconTriple","description":"Pydantic model for creating a triple.\nThis model can be extended to include additional fields as needed."},"CreateLocation":{"properties":{"point":{"type":"string","title":"Point"},"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"notes":{"items":{"$ref":"#/components/schemas/CreateNote"},"type":"array","title":"Notes","default":[]},"elevation":{"type":"number","title":"Elevation"}},"type":"object","required":["point","elevation"],"title":"CreateLocation","description":"Schema for creating a sample location."},"CreateMonitoringFrequency":{"properties":{"monitoring_frequency":{"$ref":"#/components/schemas/monitoring_frequency"},"start_date":{"type":"string","format":"date","title":"Start Date"},"end_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"End Date"}},"type":"object","required":["monitoring_frequency","start_date"],"title":"CreateMonitoringFrequency"},"CreateNote":{"properties":{"note_type":{"$ref":"#/components/schemas/note_type"},"content":{"type":"string","title":"Content"},"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"}},"type":"object","required":["note_type","content"],"title":"CreateNote","description":"Schema for creating a new Note. The parent object's ID and type will be\ntaken from the URL path, not the request body."},"CreatePhone":{"properties":{"phone_number":{"type":"string","title":"Phone Number"},"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"contact_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Contact Id"},"phone_type":{"$ref":"#/components/schemas/phone_type","default":"Primary"}},"type":"object","required":["phone_number"],"title":"CreatePhone","description":"Schema for creating a phone number."},"CreatePublication":{"properties":{"title":{"type":"string","title":"Title"},"authors":{"items":{"type":"string"},"type":"array","title":"Authors"},"year":{"type":"integer","title":"Year"},"doi":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Doi"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"},"publication_type":{"$ref":"#/components/schemas/publication_type"}},"type":"object","required":["title","authors","year","publication_type"],"title":"CreatePublication","description":"Schema for creating a new publication."},"CreateSample":{"properties":{"sample_date":{"type":"string","format":"date-time","title":"Sample Date"},"depth_top":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Depth Top"},"depth_bottom":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Depth Bottom"},"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"field_activity_id":{"type":"integer","title":"Field Activity Id"},"field_event_participant_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Field Event Participant Id"},"sample_name":{"type":"string","title":"Sample Name"},"sample_matrix":{"$ref":"#/components/schemas/sample_matrix"},"sample_method":{"$ref":"#/components/schemas/sample_method"},"qc_type":{"$ref":"#/components/schemas/qc_type"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","required":["sample_date","field_activity_id","sample_name","sample_matrix","sample_method","qc_type"],"title":"CreateSample"},"CreateSensor":{"properties":{"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"name":{"type":"string","title":"Name"},"sensor_type":{"$ref":"#/components/schemas/sensor_type"},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model"},"serial_no":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Serial No"},"pcn_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pcn Number"},"owner_agency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner Agency"},"sensor_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sensor Status"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","required":["name","sensor_type"],"title":"CreateSensor","description":"Schema for creating a new sensor."},"CreateSpring":{"properties":{"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"location_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Location Id"},"group_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Group Id"},"name":{"type":"string","title":"Name"},"first_visit_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"First Visit Date"},"notes":{"anyOf":[{"items":{"$ref":"#/components/schemas/CreateNote"},"type":"array"},{"type":"null"}],"title":"Notes"},"alternate_ids":{"anyOf":[{"items":{"$ref":"#/components/schemas/CreateThingIdLink"},"type":"array"},{"type":"null"}],"title":"Alternate Ids"},"monitoring_frequencies":{"anyOf":[{"items":{"$ref":"#/components/schemas/CreateMonitoringFrequency"},"type":"array"},{"type":"null"}],"title":"Monitoring Frequencies"},"spring_type":{"anyOf":[{"$ref":"#/components/schemas/spring_type"},{"type":"null"}]}},"type":"object","required":["name"],"title":"CreateSpring","description":"Schema for creating a spring."},"CreateThingIdLink":{"properties":{"thing_id":{"type":"integer","title":"Thing Id"},"relation":{"type":"string","title":"Relation"},"alternate_id":{"type":"string","title":"Alternate Id"},"alternate_organization":{"type":"string","title":"Alternate Organization"}},"type":"object","required":["thing_id","relation","alternate_id","alternate_organization"],"title":"CreateThingIdLink","description":"Schema for creating a link between a thing and its ID."},"CreateWaterChemistryObservation":{"properties":{"parameter_id":{"type":"integer","title":"Parameter Id"},"observation_datetime":{"type":"string","format":"date-time","title":"Observation Datetime"},"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"sample_id":{"type":"integer","title":"Sample Id"},"sensor_id":{"type":"integer","title":"Sensor Id"},"value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Value"},"unit":{"anyOf":[{"$ref":"#/components/schemas/unit"},{"type":"null"}]}},"type":"object","required":["parameter_id","observation_datetime","sample_id","sensor_id","value","unit"],"title":"CreateWaterChemistryObservation"},"CreateWell":{"properties":{"well_depth":{"anyOf":[{"type":"number","exclusiveMinimum":0.0},{"type":"null"}],"title":"Well Depth","description":"Well depth in feet"},"hole_depth":{"anyOf":[{"type":"number","exclusiveMinimum":0.0},{"type":"null"}],"title":"Hole Depth","description":"Hole depth in feet"},"well_casing_depth":{"anyOf":[{"type":"number","exclusiveMinimum":0.0},{"type":"null"}],"title":"Well Casing Depth","description":"Well casing depth in feet"},"measuring_point_height":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Measuring Point Height","description":"Measuring point height in feet"},"well_pump_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Pump Depth"},"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"location_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Location Id"},"group_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Group Id"},"name":{"type":"string","title":"Name"},"first_visit_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"First Visit Date"},"notes":{"anyOf":[{"items":{"$ref":"#/components/schemas/CreateNote"},"type":"array"},{"type":"null"}],"title":"Notes"},"alternate_ids":{"anyOf":[{"items":{"$ref":"#/components/schemas/CreateThingIdLink"},"type":"array"},{"type":"null"}],"title":"Alternate Ids"},"monitoring_frequencies":{"anyOf":[{"items":{"$ref":"#/components/schemas/CreateMonitoringFrequency"},"type":"array"},{"type":"null"}],"title":"Monitoring Frequencies"},"well_purposes":{"anyOf":[{"items":{"$ref":"#/components/schemas/well_purpose"},"type":"array"},{"type":"null"}],"title":"Well Purposes"},"well_depth_source":{"anyOf":[{"$ref":"#/components/schemas/origin_type"},{"type":"null"}]},"well_casing_diameter":{"anyOf":[{"type":"number","exclusiveMinimum":0.0},{"type":"null"}],"title":"Well Casing Diameter","description":"Well casing diameter in inches"},"well_casing_materials":{"anyOf":[{"items":{"$ref":"#/components/schemas/casing_material"},"type":"array"},{"type":"null"}],"title":"Well Casing Materials"},"measuring_point_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Measuring Point Description"},"well_completion_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Well Completion Date"},"well_completion_date_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Completion Date Source"},"well_driller_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Driller Name"},"well_construction_method":{"anyOf":[{"$ref":"#/components/schemas/well_construction_method"},{"type":"null"}]},"well_construction_method_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Construction Method Source"},"well_pump_type":{"anyOf":[{"$ref":"#/components/schemas/well_pump_type"},{"type":"null"}]},"is_suitable_for_datalogger":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Suitable For Datalogger"},"is_open":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Open"},"well_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Status"},"monitoring_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Monitoring Status"},"formation_completion_code":{"anyOf":[{"$ref":"#/components/schemas/formation_code"},{"type":"null"}]},"nma_formation_zone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Formation Zone"}},"type":"object","required":["name"],"title":"CreateWell","description":"Schema for creating a well."},"CreateWellScreen":{"properties":{"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"thing_id":{"type":"integer","title":"Thing Id"},"aquifer_system_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Aquifer System Id"},"geologic_formation_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Geologic Formation Id"},"screen_depth_bottom":{"anyOf":[{"type":"number","minimum":0.0},{"type":"null"}],"title":"Screen Depth Bottom","description":"Screen depth bottom in feet"},"screen_depth_top":{"anyOf":[{"type":"number","minimum":0.0},{"type":"null"}],"title":"Screen Depth Top","description":"Screen depth top in feet"},"screen_type":{"anyOf":[{"$ref":"#/components/schemas/screen_type"},{"type":"null"}]},"screen_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Screen Description"}},"type":"object","required":["thing_id"],"title":"CreateWellScreen","description":"Schema for creating a well screen."},"DeploymentResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"thing_id":{"type":"integer","title":"Thing Id"},"sensor":{"$ref":"#/components/schemas/SensorResponse"},"installation_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Installation Date"},"removal_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Removal Date"},"recording_interval":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Recording Interval"},"recording_interval_units":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Recording Interval Units"},"hanging_cable_length":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Hanging Cable Length"},"hanging_point_height":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Hanging Point Height"},"hanging_point_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Hanging Point Description"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","required":["id","created_at","release_status","thing_id","sensor","installation_date","removal_date","recording_interval","recording_interval_units","hanging_cable_length","hanging_point_height","hanging_point_description","notes"],"title":"DeploymentResponse"},"EmailResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"contact_id":{"type":"integer","title":"Contact Id"},"email":{"type":"string","title":"Email"},"email_type":{"$ref":"#/components/schemas/email_type"}},"type":"object","required":["id","created_at","release_status","contact_id","email","email_type"],"title":"EmailResponse","description":"Response schema for email details."},"Feature":{"properties":{"type":{"type":"string","title":"Type","default":"Feature"},"geometry":{"$ref":"#/components/schemas/schemas__thing__GeoJSONGeometry"},"properties":{"additionalProperties":true,"type":"object","title":"Properties","default":{}}},"type":"object","required":["geometry"],"title":"Feature","description":"Feature schema for GeoJSON response."},"FeatureCollectionResponse":{"properties":{"type":{"type":"string","title":"Type","default":"FeatureCollection"},"features":{"items":{"$ref":"#/components/schemas/Feature"},"type":"array","title":"Features","default":[]}},"type":"object","title":"FeatureCollectionResponse","description":"Response schema for GeoJSON FeatureCollection."},"FieldActivityResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"field_event_id":{"type":"integer","title":"Field Event Id"},"activity_type":{"$ref":"#/components/schemas/activity_type"}},"type":"object","required":["id","created_at","release_status","field_event_id","activity_type"],"title":"FieldActivityResponse"},"FieldEventParticipantResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"field_event_id":{"type":"integer","title":"Field Event Id"},"contact_id":{"type":"integer","title":"Contact Id"},"participant_role":{"type":"string","title":"Participant Role"},"participant":{"$ref":"#/components/schemas/ContactResponse"}},"type":"object","required":["id","created_at","release_status","field_event_id","contact_id","participant_role","participant"],"title":"FieldEventParticipantResponse"},"FieldEventResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"thing_id":{"type":"integer","title":"Thing Id"},"event_date":{"type":"string","format":"date-time","title":"Event Date"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","required":["id","created_at","release_status","thing_id","event_date","notes"],"title":"FieldEventResponse"},"GeoJSONProperties":{"properties":{"elevation":{"type":"number","title":"Elevation"},"elevation_unit":{"type":"string","title":"Elevation Unit","default":"ft"},"vertical_datum":{"type":"string","title":"Vertical Datum","default":"NAVD88"},"elevation_method":{"anyOf":[{"$ref":"#/components/schemas/elevation_method"},{"type":"null"}]},"county":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"County"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"quad_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Quad Name"},"utm_coordinates":{"$ref":"#/components/schemas/GeoJSONUTMCoordinates"},"notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Notes","default":[]},"nma_location_notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Location Notes"},"nma_data_reliability":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Data Reliability"},"nma_date_created":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Nma Date Created"},"nma_site_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Nma Site Date"}},"type":"object","required":["elevation","elevation_method"],"title":"GeoJSONProperties"},"GeoJSONUTMCoordinates":{"properties":{"easting":{"type":"number","title":"Easting"},"northing":{"type":"number","title":"Northing"},"utm_zone":{"type":"string","title":"Utm Zone","default":"13N"},"horizontal_datum":{"type":"string","title":"Horizontal Datum","default":"NAD83"}},"type":"object","required":["easting","northing"],"title":"GeoJSONUTMCoordinates"},"GroundwaterLevelObservationResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"sample_id":{"type":"integer","title":"Sample Id"},"sensor_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sensor Id"},"observation_datetime":{"type":"string","title":"Observation Datetime"},"parameter":{"$ref":"#/components/schemas/ParameterResponse"},"value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Value"},"unit":{"$ref":"#/components/schemas/unit"},"nma_data_quality":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Data Quality"},"depth_to_water_bgs":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Depth To Water Bgs"},"measuring_point_height":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Measuring Point Height"},"groundwater_level_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Groundwater Level Reason"}},"type":"object","required":["id","created_at","release_status","sample_id","sensor_id","observation_datetime","parameter","value","unit","depth_to_water_bgs","measuring_point_height","groundwater_level_reason"],"title":"GroundwaterLevelObservationResponse"},"GroupResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"project_area":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Project Area"},"group_type":{"anyOf":[{"$ref":"#/components/schemas/group_type"},{"type":"null"}]},"parent_group_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Parent Group Id"}},"type":"object","required":["id","created_at","release_status","name","description","project_area","group_type","parent_group_id"],"title":"GroupResponse","description":"Pydantic model for the response of a group.\nThis model can be extended to include additional fields as needed."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"LexiconCategoryResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"}},"type":"object","required":["id","created_at","name"],"title":"LexiconCategoryResponse","description":"Pydantic model for the response of a lexicon category.\nThis model can be extended to include additional fields as needed."},"LexiconTermResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"term":{"type":"string","title":"Term"},"definition":{"type":"string","title":"Definition"},"categories":{"items":{"$ref":"#/components/schemas/LexiconCategoryResponse"},"type":"array","title":"Categories","default":[]}},"type":"object","required":["id","created_at","term","definition"],"title":"LexiconTermResponse","description":"Pydantic model for the response of a lexicon term.\nThis model can be extended to include additional fields as needed."},"LexiconTripleResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"subject":{"type":"string","title":"Subject"},"predicate":{"type":"string","title":"Predicate"},"object_":{"type":"string","title":"Object"}},"type":"object","required":["id","created_at","subject","predicate","object_"],"title":"LexiconTripleResponse"},"LocationGeoJSONResponse":{"properties":{"type":{"type":"string","title":"Type","default":"Feature"},"release_status":{"$ref":"#/components/schemas/release_status"},"geometry":{"$ref":"#/components/schemas/schemas__location__GeoJSONGeometry"},"properties":{"$ref":"#/components/schemas/GeoJSONProperties"}},"type":"object","required":["release_status","geometry","properties"],"title":"LocationGeoJSONResponse"},"LocationResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Notes","default":[]},"point":{"type":"string","title":"Point"},"elevation":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Elevation"},"horizontal_datum":{"type":"string","title":"Horizontal Datum","default":"WGS84"},"vertical_datum":{"type":"string","title":"Vertical Datum","default":"NAVD88"},"elevation_method":{"anyOf":[{"$ref":"#/components/schemas/elevation_method"},{"type":"null"}]},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"county":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"County"},"quad_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Quad Name"},"nma_location_notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Location Notes"},"nma_data_reliability":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Data Reliability"},"nma_date_created":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Nma Date Created"},"nma_site_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Nma Site Date"}},"type":"object","required":["id","created_at","release_status","point","elevation","elevation_method","state","county","quad_name"],"title":"LocationResponse","description":"Response schema for sample location details."},"MonitoringFrequencyResponse":{"properties":{"monitoring_frequency":{"$ref":"#/components/schemas/monitoring_frequency"},"start_date":{"type":"string","format":"date","title":"Start Date"},"end_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"End Date"}},"type":"object","required":["monitoring_frequency","start_date","end_date"],"title":"MonitoringFrequencyResponse"},"NoteResponse":{"properties":{"note_type":{"$ref":"#/components/schemas/note_type"},"content":{"type":"string","title":"Content"},"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"target_id":{"type":"integer","title":"Target Id"},"target_table":{"type":"string","title":"Target Table"}},"type":"object","required":["note_type","content","id","created_at","release_status","target_id","target_table"],"title":"NoteResponse","description":"Response schema for Note details."},"ObservationResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"sample_id":{"type":"integer","title":"Sample Id"},"sensor_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sensor Id"},"observation_datetime":{"type":"string","title":"Observation Datetime"},"parameter":{"$ref":"#/components/schemas/ParameterResponse"},"value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Value"},"unit":{"$ref":"#/components/schemas/unit"},"nma_data_quality":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Data Quality"},"depth_to_water_bgs":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Depth To Water Bgs"},"measuring_point_height":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Measuring Point Height"},"groundwater_level_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Groundwater Level Reason"}},"type":"object","required":["id","created_at","release_status","sample_id","sensor_id","observation_datetime","parameter","value","unit","depth_to_water_bgs","measuring_point_height","groundwater_level_reason"],"title":"ObservationResponse","description":"Response model for observations.\nCombines groundwater level and geothermal observation responses."},"Page_AddressResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/AddressResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[AddressResponse]"},"Page_AssetResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/AssetResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[AssetResponse]"},"Page_ContactResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ContactResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[ContactResponse]"},"Page_DeploymentResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/DeploymentResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[DeploymentResponse]"},"Page_EmailResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/EmailResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[EmailResponse]"},"Page_GroundwaterLevelObservationResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/GroundwaterLevelObservationResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[GroundwaterLevelObservationResponse]"},"Page_GroupResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/GroupResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[GroupResponse]"},"Page_LexiconCategoryResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/LexiconCategoryResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[LexiconCategoryResponse]"},"Page_LexiconTermResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/LexiconTermResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[LexiconTermResponse]"},"Page_LexiconTripleResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/LexiconTripleResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[LexiconTripleResponse]"},"Page_LocationResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/LocationResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[LocationResponse]"},"Page_ObservationResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ObservationResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[ObservationResponse]"},"Page_PhoneResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/PhoneResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[PhoneResponse]"},"Page_SampleResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/SampleResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[SampleResponse]"},"Page_SensorResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/SensorResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[SensorResponse]"},"Page_SpringResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/SpringResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[SpringResponse]"},"Page_ThingIdLinkResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ThingIdLinkResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[ThingIdLinkResponse]"},"Page_ThingResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ThingResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[ThingResponse]"},"Page_TransducerObservationWithBlockResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/TransducerObservationWithBlockResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[TransducerObservationWithBlockResponse]"},"Page_WaterChemistryObservationResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/WaterChemistryObservationResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[WaterChemistryObservationResponse]"},"Page_WellResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/WellResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[WellResponse]"},"Page_WellScreenResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/WellScreenResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[WellScreenResponse]"},"Page_dict_":{"properties":{"items":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[dict]"},"ParameterResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"parameter_name":{"$ref":"#/components/schemas/parameter_name"},"matrix":{"type":"string","title":"Matrix"},"parameter_type":{"anyOf":[{"$ref":"#/components/schemas/parameter_type"},{"type":"null"}]},"cas_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cas Number"},"default_unit":{"anyOf":[{"$ref":"#/components/schemas/unit"},{"type":"null"}]}},"type":"object","required":["id","created_at","release_status","parameter_name","matrix","parameter_type","cas_number","default_unit"],"title":"ParameterResponse","description":"Pydantic model for the response of a parameter.\nThis model can be extended to include additional fields as needed."},"PermissionHistoryResponse":{"properties":{"permission_type":{"$ref":"#/components/schemas/permission_type"},"permission_allowed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Permission Allowed"},"start_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Start Date"},"end_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"End Date"}},"type":"object","required":["permission_type","permission_allowed","start_date","end_date"],"title":"PermissionHistoryResponse","description":"Even though permission_allowed and start_date are not-nullable in the\ndatabase, they are nullable here to accommodate cases where no permission\nrecord exists for a given permission type."},"PhoneResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"contact_id":{"type":"integer","title":"Contact Id"},"phone_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number"},"phone_type":{"type":"string","title":"Phone Type"}},"type":"object","required":["id","created_at","release_status","contact_id","phone_type"],"title":"PhoneResponse","description":"Response schema for phone details."},"PublicationResponse":{"properties":{"id":{"type":"integer","title":"Id"},"title":{"type":"string","title":"Title"},"authors":{"items":{"$ref":"#/components/schemas/AuthorResponse"},"type":"array","title":"Authors"},"year":{"type":"integer","title":"Year"},"doi":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Doi"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"},"publication_type":{"$ref":"#/components/schemas/publication_type"}},"type":"object","required":["id","title","authors","year","publication_type"],"title":"PublicationResponse","description":"Schema for the response of a publication."},"ResourceNotFoundResponse":{"properties":{"detail":{"type":"string","title":"Detail"}},"type":"object","required":["detail"],"title":"ResourceNotFoundResponse"},"SampleResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"thing":{"$ref":"#/components/schemas/ThingResponse"},"field_event":{"$ref":"#/components/schemas/FieldEventResponse"},"field_activity":{"$ref":"#/components/schemas/FieldActivityResponse"},"contact":{"anyOf":[{"$ref":"#/components/schemas/ContactResponse"},{"type":"null"}]},"sample_date":{"type":"string","title":"Sample Date"},"sample_name":{"type":"string","title":"Sample Name"},"sample_matrix":{"$ref":"#/components/schemas/sample_matrix"},"sample_method":{"$ref":"#/components/schemas/sample_method"},"qc_type":{"$ref":"#/components/schemas/qc_type"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"depth_top":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Depth Top"},"depth_bottom":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Depth Bottom"}},"type":"object","required":["id","created_at","release_status","thing","field_event","field_activity","contact","sample_date","sample_name","sample_matrix","sample_method","qc_type","notes","depth_top","depth_bottom"],"title":"SampleResponse","description":"Developer's note\n\nThe frontend uses multiple fields for a thing, field_even, and field_activity,\nwhich is why full ThingResponse, FieldEventResponse, and FieldActivityResponse\nare returned. If the response becomes too large and slow, we can use\n.model_dump() and exlude fields to reduce the size."},"SensorResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"name":{"type":"string","title":"Name"},"sensor_type":{"$ref":"#/components/schemas/sensor_type"},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model"},"serial_no":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Serial No"},"pcn_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pcn Number"},"owner_agency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner Agency"},"sensor_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sensor Status"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","required":["id","created_at","release_status","name","sensor_type","model","serial_no","pcn_number","owner_agency","sensor_status","notes"],"title":"SensorResponse"},"SpringResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"name":{"type":"string","title":"Name"},"site_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Site Name"},"thing_type":{"type":"string","title":"Thing Type"},"current_location":{"$ref":"#/components/schemas/LocationGeoJSONResponse"},"first_visit_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"First Visit Date"},"groups":{"items":{"$ref":"#/components/schemas/GroupResponse"},"type":"array","title":"Groups","default":[]},"monitoring_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Monitoring Status"},"alternate_ids":{"items":{"$ref":"#/components/schemas/ThingIdLinkResponse"},"type":"array","title":"Alternate Ids","default":[]},"monitoring_frequencies":{"items":{"$ref":"#/components/schemas/MonitoringFrequencyResponse"},"type":"array","title":"Monitoring Frequencies","default":[]},"general_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"General Notes","default":[]},"sampling_procedure_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Sampling Procedure Notes","default":[]},"site_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Site Notes","default":[]},"spring_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Spring Type"}},"type":"object","required":["id","created_at","release_status","name","thing_type","current_location","first_visit_date","monitoring_status"],"title":"SpringResponse","description":"Response schema for spring details."},"ThingIdLinkResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"thing_id":{"type":"integer","title":"Thing Id"},"relation":{"type":"string","title":"Relation"},"alternate_id":{"type":"string","title":"Alternate Id"},"alternate_organization":{"$ref":"#/components/schemas/organization"}},"type":"object","required":["id","created_at","release_status","thing_id","relation","alternate_id","alternate_organization"],"title":"ThingIdLinkResponse"},"ThingResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"name":{"type":"string","title":"Name"},"site_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Site Name"},"thing_type":{"type":"string","title":"Thing Type"},"current_location":{"$ref":"#/components/schemas/LocationGeoJSONResponse"},"first_visit_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"First Visit Date"},"groups":{"items":{"$ref":"#/components/schemas/GroupResponse"},"type":"array","title":"Groups","default":[]},"monitoring_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Monitoring Status"},"alternate_ids":{"items":{"$ref":"#/components/schemas/ThingIdLinkResponse"},"type":"array","title":"Alternate Ids","default":[]},"monitoring_frequencies":{"items":{"$ref":"#/components/schemas/MonitoringFrequencyResponse"},"type":"array","title":"Monitoring Frequencies","default":[]},"general_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"General Notes","default":[]},"sampling_procedure_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Sampling Procedure Notes","default":[]},"site_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Site Notes","default":[]},"spring_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Spring Type"},"well_purposes":{"items":{"$ref":"#/components/schemas/well_purpose"},"type":"array","title":"Well Purposes","default":[]},"well_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Depth"},"well_depth_unit":{"type":"string","title":"Well Depth Unit","default":"ft"},"well_depth_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Depth Source"},"historic_depth_to_water":{"items":{"type":"string"},"type":"array","title":"Historic Depth To Water","default":[]},"hole_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Hole Depth"},"hole_depth_unit":{"type":"string","title":"Hole Depth Unit","default":"ft"},"well_casing_diameter":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Casing Diameter"},"well_casing_diameter_unit":{"type":"string","title":"Well Casing Diameter Unit","default":"in"},"well_casing_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Casing Depth"},"well_casing_depth_unit":{"type":"string","title":"Well Casing Depth Unit","default":"ft"},"well_casing_materials":{"items":{"$ref":"#/components/schemas/casing_material"},"type":"array","title":"Well Casing Materials","default":[]},"well_completion_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Well Completion Date"},"well_completion_date_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Completion Date Source"},"well_driller_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Driller Name"},"well_construction_method":{"anyOf":[{"$ref":"#/components/schemas/well_construction_method"},{"type":"null"}]},"well_construction_method_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Construction Method Source"},"well_pump_type":{"anyOf":[{"$ref":"#/components/schemas/well_pump_type"},{"type":"null"}]},"well_pump_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Pump Depth"},"well_pump_depth_unit":{"type":"string","title":"Well Pump Depth Unit","default":"ft"},"well_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Status"},"open_status":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Open Status"},"datalogger_suitability_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Datalogger Suitability Status"},"measuring_point_height":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Measuring Point Height"},"measuring_point_height_unit":{"type":"string","title":"Measuring Point Height Unit","default":"ft"},"measuring_point_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Measuring Point Description"},"aquifers":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Aquifers","default":[]},"water_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Water Notes","default":[]},"construction_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Construction Notes","default":[]},"contacts":{"items":{"$ref":"#/components/schemas/WellContactSummaryResponse"},"type":"array","title":"Contacts","default":[]},"permissions":{"items":{"$ref":"#/components/schemas/PermissionHistoryResponse"},"type":"array","title":"Permissions"},"formation_completion_code":{"anyOf":[{"$ref":"#/components/schemas/formation_code"},{"type":"null"}]},"nma_formation_zone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Formation Zone"},"well_location_note":{"items":{"type":"string"},"type":"array","title":"Well Location Note","default":[]}},"type":"object","required":["id","created_at","release_status","name","thing_type","current_location","first_visit_date","monitoring_status","well_depth_source","well_completion_date","well_completion_date_source","well_driller_name","well_construction_method","well_construction_method_source","well_pump_type","well_pump_depth","well_status","open_status","datalogger_suitability_status","measuring_point_height","measuring_point_description","permissions","formation_completion_code","nma_formation_zone"],"title":"ThingResponse"},"ThingResponseForContact":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"name":{"type":"string","title":"Name"}},"type":"object","required":["id","created_at","release_status","name"],"title":"ThingResponseForContact","description":"Response schema for thing details related to a contact. All that is needed\nare the id and name"},"TransducerObservationBlockResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"review_status":{"$ref":"#/components/schemas/review_status"},"start_datetime":{"type":"string","format":"date-time","title":"Start Datetime"},"end_datetime":{"type":"string","format":"date-time","title":"End Datetime"},"parameter_id":{"type":"integer","title":"Parameter Id"}},"type":"object","required":["id","created_at","release_status","review_status","start_datetime","end_datetime","parameter_id"],"title":"TransducerObservationBlockResponse"},"TransducerObservationResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"value":{"type":"number","title":"Value"},"observation_datetime":{"type":"string","format":"date-time","title":"Observation Datetime"},"parameter_id":{"type":"integer","title":"Parameter Id"},"deployment_id":{"type":"integer","title":"Deployment Id"}},"type":"object","required":["id","created_at","release_status","value","observation_datetime","parameter_id","deployment_id"],"title":"TransducerObservationResponse"},"TransducerObservationWithBlockResponse":{"properties":{"observation":{"$ref":"#/components/schemas/TransducerObservationResponse"},"block":{"$ref":"#/components/schemas/TransducerObservationBlockResponse"}},"type":"object","required":["observation","block"],"title":"TransducerObservationWithBlockResponse"},"UpdateAddress":{"properties":{"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"contact_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Contact Id"},"address_line_1":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address Line 1"},"address_line_2":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address Line 2"},"city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"postal_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Postal Code"},"country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country"},"address_type":{"anyOf":[{"$ref":"#/components/schemas/address_type"},{"type":"null"}]}},"type":"object","title":"UpdateAddress","description":"Schema for updating address information."},"UpdateAsset":{"properties":{"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"}},"type":"object","title":"UpdateAsset"},"UpdateContact":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"organization":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Organization"},"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"role":{"anyOf":[{"$ref":"#/components/schemas/role"},{"type":"null"}]},"contact_type":{"anyOf":[{"$ref":"#/components/schemas/contact_type"},{"type":"null"}]},"thing_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Thing Id"}},"type":"object","title":"UpdateContact","description":"Schema for updating contact information."},"UpdateEmail":{"properties":{"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"contact_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Contact Id"},"email_type":{"anyOf":[{"$ref":"#/components/schemas/email_type"},{"type":"null"}]}},"type":"object","title":"UpdateEmail","description":"Schema for updating email information."},"UpdateGroundwaterLevelObservation":{"properties":{"parameter_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Parameter Id"},"observation_datetime":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Observation Datetime"},"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"sample_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sample Id"},"sensor_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sensor Id"},"value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Value"},"unit":{"anyOf":[{"$ref":"#/components/schemas/unit"},{"type":"null"}]},"measuring_point_height":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Measuring Point Height"},"groundwater_level_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Groundwater Level Reason"}},"type":"object","title":"UpdateGroundwaterLevelObservation"},"UpdateGroup":{"properties":{"project_area":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Project Area"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"parent_group_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Parent Group Id"},"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"}},"type":"object","title":"UpdateGroup","description":"Pydantic model for updating a group.\nThis model can be extended to include additional fields as needed."},"UpdateLexiconCategory":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"}},"type":"object","title":"UpdateLexiconCategory"},"UpdateLexiconTerm":{"properties":{"term":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Term"},"definition":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Definition"}},"type":"object","title":"UpdateLexiconTerm"},"UpdateLexiconTriple":{"properties":{"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"predicate":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Predicate"},"object_":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Object"}},"type":"object","title":"UpdateLexiconTriple"},"UpdateLocation":{"properties":{"point":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Point"},"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"notes":{"items":{"$ref":"#/components/schemas/UpdateNote"},"type":"array","title":"Notes","default":[]},"elevation":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Elevation"},"elevation_accuracy":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Elevation Accuracy"},"elevation_method":{"anyOf":[{"$ref":"#/components/schemas/elevation_method"},{"type":"null"}]},"coordinate_accuracy":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Coordinate Accuracy"},"coordinate_method":{"anyOf":[{"$ref":"#/components/schemas/coordinate_method"},{"type":"null"}]}},"type":"object","title":"UpdateLocation","description":"Schema for updating a location. Notes are managed via the polymorphic Notes table."},"UpdateNote":{"properties":{"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"note_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note Type"},"content":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content"}},"type":"object","title":"UpdateNote","description":"Schema for updating an existing Note. All fields are optional"},"UpdatePhone":{"properties":{"phone_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number"},"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"contact_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Contact Id"},"phone_type":{"anyOf":[{"$ref":"#/components/schemas/phone_type"},{"type":"null"}]}},"type":"object","title":"UpdatePhone","description":"Schema for updating phone information."},"UpdateSample":{"properties":{"sample_date":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Sample Date"},"depth_top":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Depth Top"},"depth_bottom":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Depth Bottom"},"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"field_activity_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Field Activity Id"},"field_event_participant_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Field Event Participant Id"},"sample_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sample Name"},"sample_matrix":{"anyOf":[{"$ref":"#/components/schemas/sample_matrix"},{"type":"null"}]},"sample_method":{"anyOf":[{"$ref":"#/components/schemas/sample_method"},{"type":"null"}]},"qc_type":{"anyOf":[{"$ref":"#/components/schemas/qc_type"},{"type":"null"}]},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","title":"UpdateSample"},"UpdateSensor":{"properties":{"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"sensor_type":{"anyOf":[{"$ref":"#/components/schemas/sensor_type"},{"type":"null"}]},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model"},"serial_no":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Serial No"},"pcn_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pcn Number"},"owner_agency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner Agency"},"sensor_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sensor Status"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","title":"UpdateSensor"},"UpdateSpring":{"properties":{"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"first_visit_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"First Visit Date"},"spring_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Spring Type"}},"type":"object","title":"UpdateSpring"},"UpdateThingIdLink":{"properties":{"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"alternate_organization":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Alternate Organization"},"alternate_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Alternate Id"},"relation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relation"}},"type":"object","title":"UpdateThingIdLink"},"UpdateWaterChemistryObservation":{"properties":{"parameter_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Parameter Id"},"observation_datetime":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Observation Datetime"},"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"sample_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sample Id"},"sensor_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sensor Id"},"value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Value"},"unit":{"anyOf":[{"$ref":"#/components/schemas/unit"},{"type":"null"}]}},"type":"object","title":"UpdateWaterChemistryObservation"},"UpdateWell":{"properties":{"well_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Depth"},"hole_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Hole Depth"},"well_casing_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Casing Depth"},"measuring_point_height":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Measuring Point Height"},"well_pump_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Pump Depth"},"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"first_visit_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"First Visit Date"},"well_purposes":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Well Purposes"},"well_construction_notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Construction Notes"},"well_casing_diameter":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Casing Diameter"},"well_casing_materials":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Well Casing Materials"},"nma_formation_zone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Formation Zone"}},"type":"object","title":"UpdateWell"},"UpdateWellScreen":{"properties":{"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"aquifer_system_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Aquifer System Id"},"geologic_formation_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Geologic Formation Id"},"screen_depth_bottom":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Screen Depth Bottom"},"screen_depth_top":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Screen Depth Top"},"screen_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Screen Description"},"screen_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Screen Type"}},"type":"object","title":"UpdateWellScreen"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WaterChemistryObservationResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"sample_id":{"type":"integer","title":"Sample Id"},"sensor_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sensor Id"},"observation_datetime":{"type":"string","title":"Observation Datetime"},"parameter":{"$ref":"#/components/schemas/ParameterResponse"},"value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Value"},"unit":{"$ref":"#/components/schemas/unit"},"nma_data_quality":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Data Quality"}},"type":"object","required":["id","created_at","release_status","sample_id","sensor_id","observation_datetime","parameter","value","unit"],"title":"WaterChemistryObservationResponse"},"WaterLevelBulkUploadResponse":{"properties":{"summary":{"$ref":"#/components/schemas/WaterLevelBulkUploadSummary"},"water_levels":{"items":{"$ref":"#/components/schemas/WaterLevelBulkUploadRow"},"type":"array","title":"Water Levels"},"validation_errors":{"items":{"type":"string"},"type":"array","title":"Validation Errors"}},"type":"object","required":["summary","water_levels","validation_errors"],"title":"WaterLevelBulkUploadResponse"},"WaterLevelBulkUploadRow":{"properties":{"well_name_point_id":{"type":"string","title":"Well Name Point Id"},"field_event_id":{"type":"integer","title":"Field Event Id"},"field_activity_id":{"type":"integer","title":"Field Activity Id"},"sample_id":{"type":"integer","title":"Sample Id"},"observation_id":{"type":"integer","title":"Observation Id"},"measurement_date_time":{"type":"string","title":"Measurement Date Time"},"level_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Level Status"},"data_quality":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Data Quality"}},"type":"object","required":["well_name_point_id","field_event_id","field_activity_id","sample_id","observation_id","measurement_date_time","level_status","data_quality"],"title":"WaterLevelBulkUploadRow"},"WaterLevelBulkUploadSummary":{"properties":{"total_rows_processed":{"type":"integer","title":"Total Rows Processed"},"total_rows_imported":{"type":"integer","title":"Total Rows Imported"},"validation_errors_or_warnings":{"type":"integer","title":"Validation Errors Or Warnings"}},"type":"object","required":["total_rows_processed","total_rows_imported","validation_errors_or_warnings"],"title":"WaterLevelBulkUploadSummary"},"WellContactSummaryResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"organization":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Organization"},"role":{"$ref":"#/components/schemas/role"},"contact_type":{"$ref":"#/components/schemas/contact_type"}},"type":"object","required":["id","created_at","release_status","role","contact_type"],"title":"WellContactSummaryResponse"},"WellDetailsFieldActivityResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"field_event_id":{"type":"integer","title":"Field Event Id"},"activity_type":{"$ref":"#/components/schemas/activity_type"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"samples":{"items":{"$ref":"#/components/schemas/WellDetailsFieldEventSampleResponse"},"type":"array","title":"Samples"}},"type":"object","required":["id","created_at","release_status","field_event_id","activity_type"],"title":"WellDetailsFieldActivityResponse"},"WellDetailsFieldEventResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"thing_id":{"type":"integer","title":"Thing Id"},"event_date":{"type":"string","title":"Event Date"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"field_event_participants":{"items":{"$ref":"#/components/schemas/FieldEventParticipantResponse"},"type":"array","title":"Field Event Participants"},"field_activities":{"items":{"$ref":"#/components/schemas/WellDetailsFieldActivityResponse"},"type":"array","title":"Field Activities"}},"type":"object","required":["id","created_at","release_status","thing_id","event_date"],"title":"WellDetailsFieldEventResponse"},"WellDetailsFieldEventSampleResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"contact":{"anyOf":[{"$ref":"#/components/schemas/ContactResponse"},{"type":"null"}]},"sample_date":{"type":"string","title":"Sample Date"},"sample_name":{"type":"string","title":"Sample Name"},"sample_matrix":{"$ref":"#/components/schemas/sample_matrix"},"sample_method":{"$ref":"#/components/schemas/sample_method"},"qc_type":{"$ref":"#/components/schemas/qc_type"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"depth_top":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Depth Top"},"depth_bottom":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Depth Bottom"},"observations":{"items":{"$ref":"#/components/schemas/ObservationResponse"},"type":"array","title":"Observations"}},"type":"object","required":["id","created_at","release_status","sample_date","sample_name","sample_matrix","sample_method","qc_type"],"title":"WellDetailsFieldEventSampleResponse"},"WellDetailsResponse":{"properties":{"well":{"$ref":"#/components/schemas/WellResponse"},"contacts":{"items":{"$ref":"#/components/schemas/ContactResponse"},"type":"array","title":"Contacts"},"sensors":{"items":{"$ref":"#/components/schemas/SensorResponse"},"type":"array","title":"Sensors"},"deployments":{"items":{"$ref":"#/components/schemas/DeploymentResponse"},"type":"array","title":"Deployments"},"well_screens":{"items":{"$ref":"#/components/schemas/WellScreenBaseResponse"},"type":"array","title":"Well Screens"},"field_events":{"items":{"$ref":"#/components/schemas/WellDetailsFieldEventResponse"},"type":"array","title":"Field Events"},"first_field_event":{"anyOf":[{"$ref":"#/components/schemas/WellDetailsFieldEventResponse"},{"type":"null"}]}},"type":"object","required":["well"],"title":"WellDetailsResponse"},"WellExportResponse":{"properties":{"well":{"$ref":"#/components/schemas/WellResponse"},"contacts":{"items":{"$ref":"#/components/schemas/ContactResponse"},"type":"array","title":"Contacts"},"sensors":{"items":{"$ref":"#/components/schemas/SensorResponse"},"type":"array","title":"Sensors"},"deployments":{"items":{"$ref":"#/components/schemas/DeploymentResponse"},"type":"array","title":"Deployments"}},"type":"object","required":["well"],"title":"WellExportResponse"},"WellResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"name":{"type":"string","title":"Name"},"site_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Site Name"},"thing_type":{"type":"string","title":"Thing Type"},"current_location":{"$ref":"#/components/schemas/LocationGeoJSONResponse"},"first_visit_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"First Visit Date"},"groups":{"items":{"$ref":"#/components/schemas/GroupResponse"},"type":"array","title":"Groups","default":[]},"monitoring_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Monitoring Status"},"alternate_ids":{"items":{"$ref":"#/components/schemas/ThingIdLinkResponse"},"type":"array","title":"Alternate Ids","default":[]},"monitoring_frequencies":{"items":{"$ref":"#/components/schemas/MonitoringFrequencyResponse"},"type":"array","title":"Monitoring Frequencies","default":[]},"general_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"General Notes","default":[]},"sampling_procedure_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Sampling Procedure Notes","default":[]},"site_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Site Notes","default":[]},"well_purposes":{"items":{"$ref":"#/components/schemas/well_purpose"},"type":"array","title":"Well Purposes","default":[]},"well_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Depth"},"well_depth_unit":{"type":"string","title":"Well Depth Unit","default":"ft"},"well_depth_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Depth Source"},"historic_depth_to_water":{"items":{"type":"string"},"type":"array","title":"Historic Depth To Water","default":[]},"hole_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Hole Depth"},"hole_depth_unit":{"type":"string","title":"Hole Depth Unit","default":"ft"},"well_casing_diameter":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Casing Diameter"},"well_casing_diameter_unit":{"type":"string","title":"Well Casing Diameter Unit","default":"in"},"well_casing_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Casing Depth"},"well_casing_depth_unit":{"type":"string","title":"Well Casing Depth Unit","default":"ft"},"well_casing_materials":{"items":{"$ref":"#/components/schemas/casing_material"},"type":"array","title":"Well Casing Materials","default":[]},"well_completion_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Well Completion Date"},"well_completion_date_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Completion Date Source"},"well_driller_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Driller Name"},"well_construction_method":{"anyOf":[{"$ref":"#/components/schemas/well_construction_method"},{"type":"null"}]},"well_construction_method_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Construction Method Source"},"well_pump_type":{"anyOf":[{"$ref":"#/components/schemas/well_pump_type"},{"type":"null"}]},"well_pump_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Pump Depth"},"well_pump_depth_unit":{"type":"string","title":"Well Pump Depth Unit","default":"ft"},"well_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Status"},"open_status":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Open Status"},"datalogger_suitability_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Datalogger Suitability Status"},"measuring_point_height":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Measuring Point Height"},"measuring_point_height_unit":{"type":"string","title":"Measuring Point Height Unit","default":"ft"},"measuring_point_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Measuring Point Description"},"aquifers":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Aquifers","default":[]},"water_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Water Notes","default":[]},"construction_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Construction Notes","default":[]},"contacts":{"items":{"$ref":"#/components/schemas/WellContactSummaryResponse"},"type":"array","title":"Contacts","default":[]},"permissions":{"items":{"$ref":"#/components/schemas/PermissionHistoryResponse"},"type":"array","title":"Permissions"},"formation_completion_code":{"anyOf":[{"$ref":"#/components/schemas/formation_code"},{"type":"null"}]},"nma_formation_zone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Formation Zone"},"well_location_note":{"items":{"type":"string"},"type":"array","title":"Well Location Note","default":[]}},"type":"object","required":["id","created_at","release_status","name","thing_type","current_location","first_visit_date","monitoring_status","well_depth_source","well_completion_date","well_completion_date_source","well_driller_name","well_construction_method","well_construction_method_source","well_pump_type","well_pump_depth","well_status","open_status","datalogger_suitability_status","measuring_point_height","measuring_point_description","permissions","formation_completion_code","nma_formation_zone"],"title":"WellResponse","description":"Response schema for well details."},"WellScreenBaseResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"thing_id":{"type":"integer","title":"Thing Id"},"aquifer_system_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Aquifer System Id"},"aquifer_system":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Aquifer System"},"aquifer_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Aquifer Type"},"geologic_formation_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Geologic Formation Id"},"geologic_formation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Geologic Formation"},"screen_depth_bottom":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Screen Depth Bottom"},"screen_depth_bottom_unit":{"type":"string","title":"Screen Depth Bottom Unit","default":"ft"},"screen_depth_top":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Screen Depth Top"},"screen_depth_top_unit":{"type":"string","title":"Screen Depth Top Unit","default":"ft"},"screen_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Screen Type"},"screen_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Screen Description"}},"type":"object","required":["id","created_at","release_status","thing_id"],"title":"WellScreenBaseResponse"},"WellScreenResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"thing_id":{"type":"integer","title":"Thing Id"},"aquifer_system_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Aquifer System Id"},"aquifer_system":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Aquifer System"},"aquifer_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Aquifer Type"},"geologic_formation_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Geologic Formation Id"},"geologic_formation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Geologic Formation"},"screen_depth_bottom":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Screen Depth Bottom"},"screen_depth_bottom_unit":{"type":"string","title":"Screen Depth Bottom Unit","default":"ft"},"screen_depth_top":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Screen Depth Top"},"screen_depth_top_unit":{"type":"string","title":"Screen Depth Top Unit","default":"ft"},"screen_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Screen Type"},"screen_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Screen Description"},"thing":{"$ref":"#/components/schemas/WellResponse"}},"type":"object","required":["id","created_at","release_status","thing_id","thing"],"title":"WellScreenResponse","description":"Response schema for well screen details."},"activity_type":{"type":"string","enum":["well inventory","groundwater level","water chemistry"],"title":"activity_type"},"address_type":{"type":"string","enum":["Primary","Work","Personal","Mailing","Physical"],"title":"address_type"},"casing_material":{"type":"string","enum":["PVC","Steel","Concrete"],"title":"casing_material"},"contact_type":{"type":"string","enum":["Primary","Secondary","Field Event Participant"],"title":"contact_type"},"coordinate_method":{"type":"string","enum":["Unknown","Differentially corrected GPS","Survey-grade global positioning system (SGPS)","GPS, uncorrected","Interpolated from map","Interpolated from DEM","Reported","Transit, theodolite, or other survey method"],"title":"coordinate_method"},"elevation_method":{"type":"string","enum":["Altimeter","Differentially corrected GPS","Survey-grade GPS","Global positioning system (GPS)","LiDAR DEM","Level or other survey method","Interpolated from topographic map","Interpolated from digital elevation model (DEM)","Reported","Survey-grade Global Navigation Satellite Sys, Lvl1","USGS National Elevation Dataset (NED)","Unknown"],"title":"elevation_method"},"email_type":{"type":"string","enum":["Primary","Work","Personal"],"title":"email_type"},"formation_code":{"type":"string","enum":["000EXRV","000IRSV","050QUAL","100QBAS","110ALVM","110AVMB","110BLSN","110NTGU","110PTODC","111MCCR","112ANCH","112CURB","112LAMA","112LAMAb","112LGUN","112QTBF","112QTBFlac","112QTBFpd","112QTBFppm","112SNTF","112SNTFA","112SNTFOB","112SNTFP","112TRTO","120DTIL","120ELRT","120IRSV","120SBLC","120SRVB","120SRVBf","120TSBV_Lower","120TSBV_Upper","121CHMT","121CHMTv","121CHMTvs","121OGLL","121PUYEF","121TSUQ","121TSUQa","121TSUQacu","121TSUQacuf","121TSUQaml","121TSUQb","121TSUQbfl","121TSUQbfm","121TSUQbp","121TSUQce","121TSUQe","121TSUQs","121TSUQsa","121TSUQsc","121TSUQsf","122CHOC","122CRTO","122OJOC","122PICR","122PPTS","122SNTFP","123DTILSPRS","123DTMGandbas","123DTMGign","123DTMGrhydac","123ESPN","123GLST","123PICS","123PICSc","123PICSl","123SPRSDTMGlava","123SPRSlower","123SPRSmid_uppe","124BACA","124CBMN","124LLVS","124PSCN","124RGIN","124SNJS","124TPCS","125NCMN","125NCMNS","125RTON","130CALDFLOOR","180TKSCC_Upper","180TKTR","210CRCS","210GLUPC_Lower","210HOSTD","210MCDK","210MNCS","210MNCSL","210MNCSU","211CLFHV","211CRLL","211CRVC","211DKOT","211DLCO","211DLTN","211FRHS","211FRLD","211FRMG","211GBSNC","211GLLG","211GLLP","211GRRG","211GRRS","211HOST","211KRLD","211LWIS","211MENF","211MENFU","211MVRD","211OJAM","211PCCF","211PIRR","211PNLK","211SMKH","211TLLS","212KTRP","217PRGR","220ENRD","220JURC","220NAVJ","221BLFF","221CSPG","221ERADU","221MRSN","221MRSN/BBSN","221MRSN/JCKP","221MRSN/RCAP","221MRSN/WWCN","221SLWS","221SMVL","221TDLT","221WSRC","221ZUNIS","231AGZC","231AGZCU","231CHNL","231CORR","231DCKM","231PFDF","231PFDFL","231PFDFM","231PFDFU","231RCKP","231SNRS","231SNSL","231SRMP","231WNGT","260SNAN","260SNAN_lower","261SNGL","300YESO","300YESO_lower","300YESO_upper","310ABO","310DCLL","310GLOR","310MBLC","310TRRS","310YESO","310YESOG","312CSTL","312RSLR","313ARTS","313BLCN","313BRUC","313CKBF","313CLBD","313CPTN","313GDLP","313GOSP","313SADG","313SADR","313TNSL","313YATS","315LABR","315YESOABO","318ABO","318BSPG","318JOYT","318YESO","319BRSM","320HLDR","320PENN","320SNDI","321SGDC","322BEMN","325GBLR","325MDER","325MDERL","325MDERU","325SAND","326MGDL","340EPRS","350PZBA","350PZBB","400EMBD","400PCMB","400PREC","400PRECintr","400PRST","400TUSS","410PRCG","410PRCGf","410PRCQ","410PRCQf","121GILA","312DYLK","120WMVL","313GRBG","318ABOL","318ABOU","112SNTFU","310FRNR","312OCHO","313AZOT","313QUEN","319HUCO","313SVRV","313CABD","320GRMS","211CLRDH","120BRLM","122RUBO","313SADRL","313SADRU","313BRNL","318CPDR","121BDHC","313SADY","221SRFLL","221BLUF","221COSP","317ABYS","221BRSB","310SYDR","400SDVL","221SRFL","310SGRC","231TCVS","211DCRS","211ALSN","211LVNN","211MORD","210PRMD","124ANMS","211NBRR","111ALVM","122SNTFL","111CPLN","120CRSN","111CRMS","111CRMSA","111SPOL","110TURT","221RCPR","320BLNG","112ANCHsr","121TSUQae","230TRSC","122TSUQdx","123PICSu","123PICSm","123PICSmc","120VBVC","120VCSS","124DMDT","325ALMT","400SAND","318VCPK","318BSVP","100ALVM","310PRMN","110AVPS","313CRCX","112SLBL","112SBCRC","313CRDM","112SBDM","120BLSN","112SBCR","112HCBL","120IVIG","112RLBL","112EFBL","112GRBL","123SAND","210MRNH","320ALMT","313DLRM","300PLZC","122SPRS","110AVTV","313DMBS","120ERSV"],"title":"formation_code"},"group_type":{"type":"string","enum":["Monitoring Plan","Geographic Area","Historical"],"title":"group_type"},"monitoring_frequency":{"type":"string","enum":["Monthly","Bimonthly","Bimonthly reported","Quarterly","Biannual","Annual","Decadal","Event-based"],"title":"monitoring_frequency"},"note_type":{"type":"string","enum":["Access","Directions","Communication","Construction","Maintenance","Historical","General","Water","Water Quality","Sampling Procedure","Coordinate","OwnerComment","Site Notes (legacy)"],"title":"note_type"},"organization":{"type":"string","enum":["Unknown","City of Aztec","Daybreak Investments","Vallecitos HOA","SFC, Santa Fe Animal Shelter","El Guicu Ditch Association","Santa Fe Municipal Airport","Uluru Development","AllSup's Convenience Stores","Santa Fe Downs Resort","City of Truth or Consequences, WWTP","Riverbend Hotsprings","Armendaris Ranch","El Paso Water","BLM, Socorro Field Office","USFWS","Sile MDWCA","Pena Blanca Water & Sanitation District","Town of Questa","Town of Cerro","Farr Cattle Company","Carrizozo Orchard","USFS, Kiowa Grasslands","Cloud Country West Subdivision","Chama West WUA","El Rito Regional Water and Waste Water Association","West Rim MDWUA","Village of Willard","Quemado Municipal Water & SWA","Coyote Creek MDWUA","Lamy MDWCA","La Joya CWDA","NM Firefighters Training Academy","Cebolleta Land Grant","Madrid Water Co-op","Sun Valley Water and Sanitation","Bluewater Lake MDWCA","Bluewater Acres Domestic WUA","Lybrook MDWCA","New Mexico Museum of Natural History","Hillsboro MDWCA","Tyrone MDWCA","Santa Clara Water System","Casas Adobes MDWCA","Lake Roberts WUA","El Creston MDWCA","Reserve Municipality Water Works","Town of Estancia","Pie Town MDWCA","Roosevelt SWCD","Otis MDWCA","White Cliffs MDWUA","Vista Linda Water Co-op","Anasazi Trails Water Co-op","Canon MDWCA","Placitas Trails Water Co-op","BLM, Roswell Office","Forked Lightning Ranch","Cottonwood RWA","Pinon Ridge WUA","McSherry Farms","Agua Sana WUA","Chamita MDWCA","W Spear-bar Ranch","Village of Capitan","Brazos MDWCA","Alto Alps HOA","Chiricahua Desert Museum","Bike Ranch","Hachita MDWCA","Carrizozo Municipal Water","Dunhill Ranch","Santa Fe Conservation Trust","NMSU","USGS","TWDB","NMED","NMOSE","NMBGMR","Bernalillo County","BLM","BLM Taos Office","SFC","SFC, Fire Facilities","SFC, Utilities Dept.","SFC, Valle Vista Water Utility, Inc.","City of Santa Fe","City of Santa Fe WWTP","City of Santa Fe, Municipal Recreation Complex","City of Santa Fe, Sangre de Cristo Water Co.","NMISC","PVACD","Bayard","SNL","USFS","NMT","NPS","NMRWA","NMDOT","Taos SWCD","Otero SWCD","Northeastern SWCD","CDWR","Pendaries Village","A&T Pump & Well Service, LLC","A. G. Wassenaar, Inc","AMEC","Balleau Groundwater, Inc","CDM Smith","CH2M Hill","Corbin Consulting, Inc","Chevron","Daniel B. Stephens & Associates, Inc","EnecoTech","Faith Engineering, Inc","Foster Well Service, Inc","Glorieta Geoscience, Inc","Golder Associates, Inc","Hathorn's Well Service, Inc","Hydroscience Associates, Inc","IC Tech, Inc","John Shomaker & Associates, Inc","Kuckleman Pump Service","Los Golondrinas","Minton Engineers","MJDarrconsult, Inc","Puerta del Canon Ranch","Rodgers & Company, Inc","San Pedro Creek Estates HOA","Statewide Drilling, Inc","Tec Drilling Limited","Tetra Tech, Inc","Thompson Drilling, Inc","Witcher & Associates","Zeigler Geologic Consulting, LLC","Sandia Well Service, Inc","San Marcos Association","URS","Vista del Oro","Abeyta Engineering, Inc","Adobe Ranch","Agua Fria Community Water Association","Apache Gap Ranch","Aspendale Mountain Retreat","Augustin Plains Ranch LLC","B & B Cattle Co","Berridge Distributing Company","Bishop's Lodge","Bonanza Creek Ranch","Bug Scuffle Water Association","Wehinahpay Mountain Camp","Campbell Ranch","Capitol Ford Santa Fe","Cemex, Inc","Cerro Community Center","Santa Fe Jewish Center","Chupadero MDWCA","Cielo Lumbre HOA","Circle Cross Ranch","City of Alamogordo","City of Portales, Public Works Dept.","City of Socorro","Commonwealth Conservancy","Costilla MDWCA","Country Club Garden Mobile Home Park","Crossroads Cattle Co., Ltd","Double H Ranch","E.A. Meadows East","El Camino Realty, Inc","Eldorado Area Water & Sanitation District","Bourbon Grill at El Gancho","El Prado HOA","El Rancho de las Golondrinas","El Rito Canyon MDWCA","Encantado Enterprises","Estrella Concepts LLC","Sixteen Springs Fire Department","Fire Water Lodge","Ford County Land & Cattle Company, Inc","Friendly Construction, Inc","Hacienda Del Cerezo","Hefker Vega Ranch","High Nogal Ranch","Holloman Air Force Base","Hyde Park Estates MDWCA","Desert Village RV & Mobile Home Park","K. Schmitt Trust","La Cienega MDWCA","La Vista HOA","Land Ventures LLC","Las Lagunitas","Las Lagunitas HOA","Living World Ministries","Los Atrevidos, Inc","Los Prados HOA","Malaga MDWCA & SWA","Mangas Outfitters","Medina Gravel Pit","Mendenhall Trading Co","Mesa Verde Ranch","NMDGF","NMSU College of Agriculture","Naiche Development","NRAO","NMSA","Nogal MDWCA","O Bar O Ranch","OMI Wastewater Treatment Plant","Old Road Ranch Pardners Ltd","PNM Service Center","Peace Tabernacle Church","Pecos Trail Inn","Pelican Spa","Pistachio Tree Ranch","Rancho Encantado","Rancho San Lucas","Rancho San Marcos","Rancho Viejo Partnership","Ranney Ranch","Rio En Medio MDWCA","San Acacia MDWCA","San Juan Residences","Sangre de Cristo Estates","Santa Fe Community College","Sangre de Cristo Center","Santa Fe Horse Park","Santa Fe Opera","Santa Fe Waldorf School","Shidoni Foundry and Gallery","Sierra Grande Lodge","Sierra Vista Retirement Community","Slash Triangle Ranch","Stagecoach Motel","State of New Mexico","Stephenson Ranch","Sun Broadcasting Network","Tano Rd LLC","UNM-Taos","Tee Pee Ranch/Tee Pee Subdivision","Tent Rock, Inc","Tesuque MDWCA","The Great Cloud Zen Center","Three Rivers Ranch","Timberon Water and Sanitation District","Town of Magdalena","Town of Taos","Town of Taos, National Guard Armory","Trinity Ranch","Tularosa Basin National Desalination Research Facility","Turquoise Trail Charter School","US Bureau of Indian Affairs, Santa Fe Indian School","USFS, Carson NF, Taos Office","USFS, Cibola NF, Magdalena Ranger District","USFS, Santa Fe NF, Espanola Ranger District","Ute Mountain Farms","VA Hospital","Velte","Vereda Serena Property","Village of Corona","Village of Floyd","Village of Melrose","Village of Vaughn","Vista Land Company","Vista Redonda MDWCA","Vista de Oro de Placitas Water Users Coop","Walker Ranch","Wild & Woolley Trailer Ranch","Winter Brothers","Yates Petroleum Corporation","Zamora Accounting Services","Agua Sana MWCD","Canada Los Alamos MDWCA","Canjilon Mutual Domestic Water System","Cebolla Mutual Domestic","Chihuahuan Desert Rangeland Research Center (CDRRC)","East Rio Arriba SWCD","El Prado Municipal Water","Hachita Mutual Domestic","Jornada Experimental Range (JER)","La Canada Way HOA","Los Ojos Mutual Domestic","The Nature Conservancy (TNC)","Smith Ranch LLC","Zia Pueblo","Our Lady of Guadalupe (OLG)","PLSS"],"title":"organization"},"origin_type":{"type":"string","enum":["Reported by another agency","From driller's log or well report","Private geologist, consultant or univ associate","Interpreted fr geophys logs by source agency","Memory of owner, operator, driller","Measured by source agency","Reported by owner of well","Reported by person other than driller owner agency","Measured by NMBGMR staff","Other","Data Portal"],"title":"origin_type"},"parameter_name":{"type":"string","enum":["groundwater level","temperature","pH","Alkalinity, Total","Alkalinity as CaCO3","Alkalinity as OH-","Calcium","Calcium, total, unfiltered","Chloride","Carbonate","Conductivity, laboratory","Bicarbonate","Hardness (CaCO3)","Ion Balance","Potassium","Potassium, total, unfiltered","Magnesium","Magnesium, total, unfiltered","Sodium","Sodium, total, unfiltered","Sodium and Potassium combined","Sulfate","Total Anions","Total Cations","Total Dissolved Solids","Tritium","Age of Water using dissolved gases","Silver","Silver, total, unfiltered","Aluminum","Aluminum, total, unfiltered","Arsenic","Arsenic, total, unfiltered","Boron","Boron, total, unfiltered","Barium","Barium, total, unfiltered","Beryllium","Beryllium, total, unfiltered","Bromide","13C:12C ratio","14C content, pmc","Uncorrected C14 age","Cadmium","Cadmium, total, unfiltered","Chlorofluorocarbon-11 avg age","Chlorofluorocarbon-113 avg age","Chlorofluorocarbon-113/12 avg RATIO age","Chlorofluorocarbon-12 avg age","Cobalt","Cobalt, total, unfiltered","Chromium","Chromium, total, unfiltered","Copper","Copper, total, unfiltered","delta O18 sulfate","Sulfate 34 isotope ratio","Fluoride","Iron","Iron, total, unfiltered","Deuterium:Hydrogen ratio","Mercury","Mercury, total, unfiltered","Lithium","Lithium, total, unfiltered","Manganese","Manganese, total, unfiltered","Molybdenum","Molybdenum, total, unfiltered","Nickel","Nickel, total, unfiltered","Nitrite (as NO2)","Nitrite (as N)","Nitrate (as NO3)","Nitrate (as N)","18O:16O ratio","Lead","Lead, total, unfiltered","Phosphate","Antimony","Antimony, total, unfiltered","Selenium","Selenium, total, unfiltered","Sulfur hexafluoride","Silicon","Silicon, total, unfiltered","Silica","Tin","Tin, total, unfiltered","Strontium","Strontium, total, unfiltered","Strontium 87:86 ratio","Thorium","Thorium, total, unfiltered","Titanium","Titanium, total, unfiltered","Thallium","Thallium, total, unfiltered","Uranium (total, by ICP-MS)","Uranium, total, unfiltered","Vanadium","Vanadium, total, unfiltered","Zinc","Zinc, total, unfiltered","Corrected C14 in years","Arsenite (arsenic species)","Arsenate (arsenic species)","Cyanide","Estimated recharge temperature","Hydrogen sulfide","Ammonia","Ammonium","Total nitrogen","Total Kjeldahl nitrogen","Dissolved organic carbon","Total organic carbon","delta C13 of dissolved inorganic carbon"],"title":"parameter_name"},"parameter_type":{"type":"string","enum":["Field Parameter","Metal","Radionuclide","Major Element","Minor Element","Physical property"],"title":"parameter_type"},"permission_type":{"type":"string","enum":["Water Level Sample","Water Chemistry Sample","Datalogger Installation"],"title":"permission_type"},"phone_type":{"type":"string","enum":["Primary","Work","Home","Mobile"],"title":"phone_type"},"publication_type":{"type":"string","enum":["Map","Report","Dataset","Model","Software","Paper","Thesis","Book","Conference","Webpage"],"title":"publication_type"},"qc_type":{"type":"string","enum":["Normal","Duplicate","Split","Field Blank","Trip Blank","Equipment Blank"],"title":"qc_type"},"release_status":{"type":"string","enum":["draft","provisional","final","published","archived","public","private"],"title":"release_status"},"review_status":{"type":"string","enum":["approved","not reviewed"],"title":"review_status"},"role":{"type":"string","enum":["Unknown","Principal Investigator","Owner","Manager","Operator","Driller","Geologist","Hydrologist","Hydrogeologist","Engineer","Organization","Specialist","Technician","Research Assistant","Research Scientist","Graduate Student","Biologist","Lab Manager","Publications Manager","Software Developer"],"title":"role"},"sample_matrix":{"type":"string","enum":["water","groundwater","soil"],"title":"sample_matrix"},"sample_method":{"type":"string","enum":["Unknown","Airline measurement","Analog or graphic recorder","Calibrated airline measurement","Differential GPS; especially applicable to surface expression of ground water","Estimated","Transducer","Pressure-gage measurement","Calibrated pressure-gage measurement","Interpreted from geophysical logs","Manometer","Non-recording gage","Observed (required for F, N, and W water level status)","Sonic water level meter (acoustic pulse)","Reported, method not known","Steel-tape measurement","Electric tape measurement (E-probe)","Unknown (for legacy data only; not for new data entry)","Calibrated electric tape; accuracy of equipment has been checked","Calibrated electric cable","Uncalibrated electric cable","Continuous acoustic sounder","Measurement not attempted","null placeholder","bailer","faucet at well head","faucet or outlet at house","grab sample","pump","thief sampler"],"title":"sample_method"},"schemas__location__GeoJSONGeometry":{"properties":{"type":{"type":"string","title":"Type","default":"Point"},"coordinates":{"items":{},"type":"array","maxItems":3,"minItems":3,"title":"Coordinates","description":"Coordinates in [longitude, latitude, elevation] format"}},"type":"object","required":["coordinates"],"title":"GeoJSONGeometry"},"schemas__thing__GeoJSONGeometry":{"properties":{"type":{"type":"string","title":"Type"},"coordinates":{"anyOf":[{"items":{"type":"number"},"type":"array"},{"items":{"items":{"type":"number"},"type":"array"},"type":"array"},{"items":{"items":{"items":{"type":"number"},"type":"array"},"type":"array"},"type":"array"},{"items":{"items":{"items":{"items":{"type":"number"},"type":"array"},"type":"array"},"type":"array"},"type":"array"}],"title":"Coordinates"}},"type":"object","required":["type","coordinates"],"title":"GeoJSONGeometry","description":"Geometry schema for GeoJSON response."},"screen_type":{"type":"string","enum":["PVC","Steel","Concrete"],"title":"screen_type"},"sensor_type":{"type":"string","enum":["DiverLink","Diver Cable","Pressure Transducer","Data Logger","Barometer","Acoustic Sounder","Precip Collector","Camera","Soil Moisture Sensor","Tipping Bucket","Weather Station","Weir","Snow Lysimeter","Lysimeter"],"title":"sensor_type"},"spring_type":{"type":"string","enum":["Artesian","Ephemeral","Perennial","Thermal","Mineral"],"title":"spring_type"},"unit":{"type":"string","enum":["dimensionless","ft","ftbgs","F","mg/L","mW/m²","W/m²","W/m·K","m²/s","deg C","deg second","deg minute","second","minute","hour","m"],"title":"unit"},"well_construction_method":{"type":"string","enum":["Unknown","Air-Rotary","Bored or augered","Cable-tool","Hydraulic rotary (mud or water)","Air percussion","Reverse rotary","Driven","Other (explain in notes)"],"title":"well_construction_method"},"well_pump_type":{"type":"string","enum":["Submersible","Jet","Line Shaft","Hand","Windmill"],"title":"well_pump_type"},"well_purpose":{"type":"string","enum":["Unknown","Open, unequipped well","Commercial","Domestic","Power generation","Irrigation","Livestock","Mining","Industrial","Observation","Public supply","Shared domestic","Institutional","Unused","Exploration","Monitoring","Production","Injection"],"title":"well_purpose"}},"securitySchemes":{"OAuth2AuthorizationCodeBearer":{"type":"oauth2","flows":{"authorizationCode":{"scopes":{"openid":"openid","offline_access":"offline_access"},"authorizationUrl":"https://authentik.newmexicowaterdata.org/application/o/authorize/","tokenUrl":"https://authentik.newmexicowaterdata.org/application/o/token/"}}}}}} \ No newline at end of file +{"openapi":"3.1.0","info":{"title":"Ocotillo API (Full)","description":"Full API schema (authorized users)","version":"1.2.0"},"paths":{"/health":{"get":{"tags":["meta"],"summary":"Health","operationId":"health_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/asset/upload":{"post":{"tags":["asset"],"summary":"Upload Asset","operationId":"upload_asset_asset_upload_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_asset_asset_upload_post"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Upload Asset Asset Upload Post"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2AuthorizationCodeBearer":[]}]}},"/asset/upload-and-record":{"post":{"tags":["asset"],"summary":"Upload And Record Asset","description":"Upload a digital asset to GCS and record it in the database in one step.\n\nAccepts a multipart/form-data request containing the file and optional\nmetadata. Validates the file type and size before uploading. If the same\nfile has already been uploaded for the same Thing, the existing record is\nreturned instead of creating a duplicate.\n\nArgs:\n user: Authenticated admin user performing the upload.\n session: Active database session.\n bucket: GCS storage bucket resolved via dependency injection.\n file: The file to upload. Accepted MIME types: JPEG, PNG, GIF, WebP,\n TIFF (images); PDF (documents); plain text. Max size: 250 MB.\n thing_id: ID of the Thing (e.g. a well) this asset belongs to.\n label: Optional human-readable label for the asset.\n name: Optional asset name. Defaults to the uploaded filename.\n\nReturns:\n AssetResponse: The newly created (or pre-existing duplicate) asset\n record, including its database ID, GCS URI, and storage path.\n\nRaises:\n 400 Bad Request: File MIME type is not in the allowed set, or the\n file size exceeds 250 MB.\n 409 Conflict: No Thing with the given thing_id exists.","operationId":"upload_and_record_asset_asset_upload_and_record_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_upload_and_record_asset_asset_upload_and_record_post"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2AuthorizationCodeBearer":[]}]}},"/asset":{"post":{"tags":["asset"],"summary":"Add Asset","operationId":"add_asset_asset_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAsset"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["asset"],"summary":"List Assets","description":"List all assets or assets associated with a specific thing.","operationId":"list_assets_asset_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"query","required":false,"schema":{"type":"integer","title":"Thing Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_AssetResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/asset/unassociated":{"get":{"tags":["asset"],"summary":"List Unassociated Assets","description":"List assets that are not associated with any Thing.","operationId":"list_unassociated_assets_asset_unassociated_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_AssetResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/asset/{asset_id}":{"get":{"tags":["asset"],"summary":"Get Asset","description":"Retrieve an asset by its ID.","operationId":"get_asset_asset__asset_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"asset_id","in":"path","required":true,"schema":{"type":"integer","title":"Asset Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["asset"],"summary":"Update Asset","description":"Update an existing asset.","operationId":"update_asset_asset__asset_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"asset_id","in":"path","required":true,"schema":{"type":"integer","title":"Asset Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAsset"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["asset"],"summary":"Delete Asset","operationId":"delete_asset_asset__asset_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"asset_id","in":"path","required":true,"schema":{"type":"integer","title":"Asset Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/asset/{asset_id}/association":{"patch":{"tags":["asset"],"summary":"Update Asset Thing Association","description":"Move an asset to another Thing or remove its Thing association.\n\nPassing a `thing_id` replaces any existing Thing links for the asset with\nthat one Thing. Passing `thing_id: null` leaves the Asset record in place\nand removes all Thing links.","operationId":"update_asset_thing_association_asset__asset_id__association_patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"asset_id","in":"path","required":true,"schema":{"type":"integer","title":"Asset Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetAssociationUpdate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetAssociationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/asset/{asset_id}/remove":{"delete":{"tags":["asset"],"summary":"Remove Asset","operationId":"remove_asset_asset__asset_id__remove_delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"asset_id","in":"path","required":true,"schema":{"type":"integer","title":"Asset Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/author/{author_id}/publications":{"get":{"tags":["author"],"summary":"Get Author Publications","description":"Retrieve all publications for a specific author.","operationId":"get_author_publications_author__author_id__publications_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"author_id","in":"path","required":true,"schema":{"type":"integer","title":"Author Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PublicationResponse"},"title":"Response Get Author Publications Author Author Id Publications Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact":{"post":{"tags":["contact"],"summary":"Create a new contact","operationId":"create_contact_contact_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateContact"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["contact"],"summary":"Get contacts","description":"Paginated contacts.\n\n**Filtering.** ``filter_params`` collects every ``filter=`` query parameter.\nRefine sends one JSON object per active DataGrid column (AND semantics).\nVirtual field ``things`` filters by linked monitoring site ``Thing.name``.\nSee docs/refine-json-filters-and-virtual-fields.md and ``get_db_contacts``.","operationId":"get_contacts_contact_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"sort","in":"query","required":false,"schema":{"type":"string","title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"title":"Filter"}},{"name":"thing_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Thing Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_ContactResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact/address":{"post":{"tags":["contact"],"summary":"Add an address to a contact","description":"Add a new address to an existing contact in the database.\n:param contact_id: ID of the contact to add the address to\n:param address_data: Data for the new address\n:param session: Database session\n:return: Response containing the added address","operationId":"create_address_contact_address_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAddress"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddressResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["contact"],"summary":"Get all addresses","description":"Retrieve all addresses from the database.\n:param session:\n:return:","operationId":"get_addresses_contact_address_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_AddressResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact/email":{"post":{"tags":["contact"],"summary":"Add an email to a contact","operationId":"create_email_contact_email_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateEmail"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["contact"],"summary":"Get all emails","description":"Retrieve all emails from the database.\n:param session:\n:return:","operationId":"get_emails_contact_email_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_EmailResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact/phone":{"post":{"tags":["contact"],"summary":"Add a phone number to a contact","operationId":"create_phone_contact_phone_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePhone"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhoneResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["contact"],"summary":"Get all phones","description":"Retrieve all phone numbers from the database.\n:param session:\n:return:","operationId":"get_phones_contact_phone_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_PhoneResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact/email/{email_id}":{"patch":{"tags":["contact"],"summary":"Update Contact Email","description":"Update an existing contact's email in the database.","operationId":"update_contact_email_contact_email__email_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"email_id","in":"path","required":true,"schema":{"type":"integer","title":"Email Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEmail"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["contact"],"summary":"Get email by ID","description":"Retrieve an email by ID from the database.","operationId":"get_email_by_id_contact_email__email_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"email_id","in":"path","required":true,"schema":{"type":"integer","title":"Email Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EmailResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["contact"],"summary":"Delete contact email","description":"Delete a contact email by ID from the database.","operationId":"delete_contact_email_contact_email__email_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"email_id","in":"path","required":true,"schema":{"type":"integer","title":"Email Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact/phone/{phone_id}":{"patch":{"tags":["contact"],"summary":"Update Contact Phone","description":"Update an existing contact's phone number in the database.\n:param contact_id: ID of the contact to update\n:param phone_type: Type of the phone to update\n:param phone_number: New phone number\n:param session: Database session\n:return: Updated contact response","operationId":"update_contact_phone_contact_phone__phone_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"phone_id","in":"path","required":true,"schema":{"type":"integer","title":"Phone Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePhone"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhoneResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["contact"],"summary":"Get phone by ID","description":"Retrieve a phone by ID from the database.","operationId":"get_phone_by_id_contact_phone__phone_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"phone_id","in":"path","required":true,"schema":{"type":"integer","title":"Phone Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PhoneResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["contact"],"summary":"Delete contact phone","description":"Delete a contact phone by ID from the database.","operationId":"delete_contact_phone_contact_phone__phone_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"phone_id","in":"path","required":true,"schema":{"type":"integer","title":"Phone Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact/address/{address_id}":{"patch":{"tags":["contact"],"summary":"Update Contact Address","description":"Update an existing contact's address in the database.\n\n:param address_id:\n:param address_data:\n:param session:\n:return:","operationId":"update_contact_address_contact_address__address_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"address_id","in":"path","required":true,"schema":{"type":"integer","title":"Address Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAddress"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddressResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["contact"],"summary":"Get address by ID","description":"Retrieve an address by ID from the database.","operationId":"get_address_by_id_contact_address__address_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"address_id","in":"path","required":true,"schema":{"type":"integer","title":"Address Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddressResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["contact"],"summary":"Delete contact address","description":"Delete a contact address by ID from the database.","operationId":"delete_contact_address_contact_address__address_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"address_id","in":"path","required":true,"schema":{"type":"integer","title":"Address Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact/{contact_id}":{"patch":{"tags":["contact"],"summary":"Update contact","description":"Update an existing contact in the database.\n:param contact_id: ID of the contact to update\n:param contact_data: Data to update the contact with\n:param session: Database session\n:return: Updated contact response","operationId":"update_contact_contact__contact_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"integer","title":"Contact Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateContact"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["contact"],"summary":"Get contact by ID","description":"Retrieve a contact by ID from the database.","operationId":"get_contact_by_id_contact__contact_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"integer","title":"Contact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContactResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["contact"],"summary":"Delete contact","description":"Delete a contact by ID from the database.","operationId":"delete_contact_contact__contact_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"integer","title":"Contact Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact/{contact_id}/email":{"get":{"tags":["contact"],"summary":"Get contact emails","description":"Retrieve all emails associated with a contact.","operationId":"get_contact_emails_contact__contact_id__email_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"integer","title":"Contact Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_EmailResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact/{contact_id}/phone":{"get":{"tags":["contact"],"summary":"Get contact phones","description":"Retrieve all phone numbers associated with a contact.","operationId":"get_contact_phones_contact__contact_id__phone_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"integer","title":"Contact Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_PhoneResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/contact/{contact_id}/address":{"get":{"tags":["contact"],"summary":"Get contact addresses","description":"Retrieve all addresses associated with a contact.","operationId":"get_contact_addresses_contact__contact_id__address_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"contact_id","in":"path","required":true,"schema":{"type":"integer","title":"Contact Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_AddressResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/disclaimer":{"get":{"tags":["disclaimer"],"summary":"Data disclaimer and terms of service","operationId":"get_disclaimer_disclaimer_get","parameters":[{"name":"f","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Response format. Use 'json' for the text as data.","title":"F"},"description":"Response format. Use 'json' for the text as data."}],"responses":{"200":{"description":"The disclaimer as HTML (default) or JSON (?f=json).","content":{"text/html":{"schema":{"type":"string"}},"application/json":{}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/geospatial":{"get":{"tags":["geospatial"],"summary":"Get Geospatial","description":"Endpoint to retrieve a GeoJSON FeatureCollection or a shapefile.\nIf the request is for a shapefile, it will return a zip file containing the shapefile.\nOtherwise, it returns a GeoJSON FeatureCollection.","operationId":"get_geospatial_geospatial_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_type","in":"query","required":false,"schema":{"type":"array","items":{"type":"string"},"title":"thing_type"}},{"name":"group","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"integer"}],"title":"group"}},{"name":"format","in":"query","required":false,"schema":{"type":"string","pattern":"^(geojson|shapefile)$","title":"format","description":"Format of the response. 'geojson' for GeoJSON FeatureCollection, 'shapefile' for a shapefile.","default":"geojson"},"description":"Format of the response. 'geojson' for GeoJSON FeatureCollection, 'shapefile' for a shapefile."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/geospatial/project-area/{group_id}":{"get":{"tags":["geospatial"],"summary":"Get project area for group","operationId":"get_project_area_geospatial_project_area__group_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"group_id","in":"path","required":true,"schema":{"type":"integer","title":"Group Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeatureCollectionResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/group":{"post":{"tags":["group"],"summary":"Create a new group","description":"Create a new group in the database.","operationId":"create_group_group_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGroup"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["group"],"summary":"Get groups","description":"Retrieve all groups from the database.","operationId":"get_groups_group_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_GroupResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/group/{group_id}/things/{thing_id}":{"post":{"tags":["group"],"summary":"Add a thing to a group","description":"Associate a thing (e.g. a water well) with a group (project).\nReturns 409 if the association already exists.","operationId":"add_thing_to_group_route_group__group_id__things__thing_id__post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"group_id","in":"path","required":true,"schema":{"type":"integer","title":"Group Id"}},{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}}],"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["group"],"summary":"Remove a thing from a group","description":"Remove the association between a thing and a group.\nReturns 404 if the association does not exist.","operationId":"remove_thing_from_group_route_group__group_id__things__thing_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"group_id","in":"path","required":true,"schema":{"type":"integer","title":"Group Id"}},{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/group/{group_id}":{"get":{"tags":["group"],"summary":"Get group by ID","description":"Retrieve a group by ID from the database.","operationId":"get_group_by_id_group__group_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"group_id","in":"path","required":true,"schema":{"type":"integer","title":"Group Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["group"],"summary":"Update a group by ID","description":"Update a group by ID in the database.","operationId":"update_group_group__group_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"group_id","in":"path","required":true,"schema":{"type":"integer","title":"Group Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateGroup"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroupResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["group"],"summary":"Delete a group by ID","operationId":"delete_group_group__group_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"group_id","in":"path","required":true,"schema":{"type":"integer","title":"Group Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/lexicon/category":{"post":{"tags":["lexicon"],"summary":"Add Category","description":"Endpoint to add a category to the lexicon.","operationId":"add_category_lexicon_category_post","deprecated":true,"security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLexiconCategory"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LexiconCategoryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["lexicon"],"summary":"Get Lexicon Categories","description":"Endpoint to retrieve lexicon categories.","operationId":"get_lexicon_categories_lexicon_category_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"name","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","default":"name","title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","default":"asc","title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_LexiconCategoryResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/lexicon/term":{"post":{"tags":["lexicon"],"summary":"Add term","description":"Endpoint to add a term to the lexicon.","operationId":"add_term_lexicon_term_post","deprecated":true,"security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLexiconTerm"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LexiconTermResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["lexicon"],"summary":"Get lexicon terms","description":"Endpoint to retrieve lexicon terms.","operationId":"get_lexicon_terms_lexicon_term_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"category","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"}},{"name":"term","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Term"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_LexiconTermResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/lexicon/triple":{"post":{"tags":["lexicon"],"summary":"Add triple","operationId":"add_triple_lexicon_triple_post","deprecated":true,"security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLexiconTriple"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LexiconTripleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["lexicon"],"summary":"Get lexicon triples","description":"Endpoint to retrieve lexicon triples.","operationId":"get_lexicon_triples_lexicon_triple_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"sort","in":"query","required":false,"schema":{"type":"string","default":"subject","title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","default":"asc","title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_LexiconTripleResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/lexicon/term/{term_id}":{"patch":{"tags":["lexicon"],"summary":"Update Lexicon Term","operationId":"update_lexicon_term_lexicon_term__term_id__patch","deprecated":true,"security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"term_id","in":"path","required":true,"schema":{"type":"integer","title":"Term Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLexiconTerm"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LexiconTermResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["lexicon"],"summary":"Get Lexicon Term","operationId":"get_lexicon_term_lexicon_term__term_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"term_id","in":"path","required":true,"schema":{"type":"integer","title":"Term Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LexiconTermResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["lexicon"],"summary":"Delete a lexicon term by ID","operationId":"delete_lexicon_term_lexicon_term__term_id__delete","deprecated":true,"security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"term_id","in":"path","required":true,"schema":{"type":"integer","title":"Term Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/lexicon/category/{category_id}":{"patch":{"tags":["lexicon"],"summary":"Update Lexicon Category","operationId":"update_lexicon_category_lexicon_category__category_id__patch","deprecated":true,"security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"category_id","in":"path","required":true,"schema":{"type":"integer","title":"Category Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLexiconCategory"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LexiconCategoryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["lexicon"],"summary":"Get Lexicon Category","operationId":"get_lexicon_category_lexicon_category__category_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"category_id","in":"path","required":true,"schema":{"type":"integer","title":"Category Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LexiconCategoryResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["lexicon"],"summary":"Delete a lexicon category by ID","operationId":"delete_lexicon_category_lexicon_category__category_id__delete","deprecated":true,"security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"category_id","in":"path","required":true,"schema":{"type":"integer","title":"Category Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/lexicon/triple/{triple_id}":{"patch":{"tags":["lexicon"],"summary":"Update Lexicon Triple","operationId":"update_lexicon_triple_lexicon_triple__triple_id__patch","deprecated":true,"security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"triple_id","in":"path","required":true,"schema":{"type":"integer","title":"Triple Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLexiconTriple"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LexiconTripleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["lexicon"],"summary":"Get Lexicon Triple","operationId":"get_lexicon_triple_lexicon_triple__triple_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"triple_id","in":"path","required":true,"schema":{"type":"integer","title":"Triple Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LexiconTripleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["lexicon"],"summary":"Delete a lexicon triple by ID","operationId":"delete_lexicon_triple_lexicon_triple__triple_id__delete","deprecated":true,"security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"triple_id","in":"path","required":true,"schema":{"type":"integer","title":"Triple Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/location":{"post":{"tags":["location"],"summary":"Create a new sample location","description":"Create a new sample location in the database.","operationId":"create_location_location_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateLocation"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LocationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["location"],"summary":"Get all locations","description":"Retrieve all wells from the database.","operationId":"get_location_location_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"nearby_point","in":"query","required":false,"schema":{"type":"string","title":"Nearby Point"}},{"name":"nearby_distance_km","in":"query","required":false,"schema":{"type":"number","default":1,"title":"Nearby Distance Km"}},{"name":"within","in":"query","required":false,"schema":{"type":"string","title":"Within"}},{"name":"query","in":"query","required":false,"schema":{"type":"string","title":"Query"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_LocationResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/location/{location_id}":{"patch":{"tags":["location"],"summary":"Update a location","description":"Update a sample location in the database.","operationId":"update_location_location__location_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"location_id","in":"path","required":true,"schema":{"type":"integer","title":"Location Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateLocation"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LocationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["location"],"summary":"Get location by ID","description":"Retrieve a sample location by ID from the database.","operationId":"get_location_by_id_location__location_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"location_id","in":"path","required":true,"schema":{"type":"integer","title":"Location Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LocationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["location"],"summary":"Delete location by ID","description":"Delete a sample location by ID from the database.","operationId":"delete_location_location__location_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"location_id","in":"path","required":true,"schema":{"type":"integer","title":"Location Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/observation/groundwater-level":{"post":{"tags":["observation"],"summary":"Add Groundwater Level Observation","description":"Add a new groundwater observation to the database.","operationId":"add_groundwater_level_observation_observation_groundwater_level_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGroundwaterLevelObservation"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroundwaterLevelObservationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["observation"],"summary":"Get groundwater level observations","description":"Retrieve all groundwater level observations from the database.","operationId":"get_groundwater_level_observations_observation_groundwater_level_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Thing Id"}},{"name":"sensor_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sensor Id"}},{"name":"sample_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sample Id"}},{"name":"start_time","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Start Time"}},{"name":"end_time","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"End Time"}},{"name":"sort","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_GroundwaterLevelObservationResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/observation/water-chemistry":{"post":{"tags":["observation"],"summary":"Add Water Chemistry Observation","description":"Add a new water chemistry observation to the database.\nThis endpoint is currently a placeholder and does not implement any functionality.","operationId":"add_water_chemistry_observation_observation_water_chemistry_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWaterChemistryObservation"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaterChemistryObservationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["observation"],"summary":"Get water chemistry observations","description":"Retrieve all water chemistry observations from the database.","operationId":"get_water_chemistry_observations_observation_water_chemistry_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Thing Id"}},{"name":"sensor_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sensor Id"}},{"name":"sample_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sample Id"}},{"name":"start_time","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Start Time"}},{"name":"end_time","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"End Time"}},{"name":"sort","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_WaterChemistryObservationResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/observation/groundwater-level/bulk-upload":{"post":{"tags":["observation"],"summary":"Bulk Upload Groundwater Levels","operationId":"bulk_upload_groundwater_levels_observation_groundwater_level_bulk_upload_post","requestBody":{"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_bulk_upload_groundwater_levels_observation_groundwater_level_bulk_upload_post"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaterLevelBulkUploadResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2AuthorizationCodeBearer":[]}]}},"/observation/groundwater-level/{observation_id}":{"patch":{"tags":["observation"],"summary":"Update Groundwater Level Observation","description":"Update an existing groundwater level observation in the database.","operationId":"update_groundwater_level_observation_observation_groundwater_level__observation_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"observation_id","in":"path","required":true,"schema":{"type":"integer","title":"Observation Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateGroundwaterLevelObservation"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroundwaterLevelObservationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["observation"],"summary":"Get groundwater level observation by ID","operationId":"get_groundwater_level_observation_by_id_observation_groundwater_level__observation_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"observation_id","in":"path","required":true,"schema":{"type":"integer","title":"Observation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GroundwaterLevelObservationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/observation/water-chemistry/{observation_id}":{"patch":{"tags":["observation"],"summary":"Update Water Chemistry Observation","description":"Update an existing water chemistry observation in the database.","operationId":"update_water_chemistry_observation_observation_water_chemistry__observation_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"observation_id","in":"path","required":true,"schema":{"type":"integer","title":"Observation Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWaterChemistryObservation"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaterChemistryObservationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["observation"],"summary":"Get water chemistry observation by ID","operationId":"get_water_chemistry_observation_by_id_observation_water_chemistry__observation_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"observation_id","in":"path","required":true,"schema":{"type":"integer","title":"Observation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WaterChemistryObservationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/observation/transducer-groundwater-level":{"get":{"tags":["observation"],"summary":"Get transducer groundwater level observations","operationId":"get_transducer_groundwater_level_observations_observation_transducer_groundwater_level_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Thing Id"}},{"name":"start_time","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Start Time"}},{"name":"end_time","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"End Time"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_TransducerObservationWithBlockResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/observation":{"get":{"tags":["observation"],"summary":"Get all observations","operationId":"get_all_observations_observation_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Thing Id"}},{"name":"sensor_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sensor Id"}},{"name":"sample_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sample Id"}},{"name":"start_time","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Start Time"}},{"name":"end_time","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"End Time"}},{"name":"sort","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_ObservationResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/observation/{observation_id}":{"get":{"tags":["observation"],"summary":"Get an observation by its ID","operationId":"get_observation_by_id_observation__observation_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"observation_id","in":"path","required":true,"schema":{"type":"integer","title":"Observation Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ObservationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["observation"],"summary":"Delete an observation","operationId":"delete_observation_observation__observation_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"observation_id","in":"path","required":true,"schema":{"type":"integer","title":"Observation Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/publication/add":{"post":{"tags":["publication"],"summary":"Post Publication","description":"Add a new publication.","operationId":"post_publication_publication_add_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePublication"}}},"required":true},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicationResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}},"security":[{"OAuth2AuthorizationCodeBearer":[]}]}},"/sample":{"post":{"tags":["sample"],"summary":"Add Sample","description":"Endpoint to add a sample.","operationId":"add_sample_sample_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSample"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SampleResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["sample"],"summary":"Get Samples","description":"Endpoint to retrieve samples.","operationId":"get_samples_sample_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Thing Id"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_SampleResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sample/{sample_id}":{"patch":{"tags":["sample"],"summary":"Update Sample","description":"Endpoint to update a sample.","operationId":"update_sample_sample__sample_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"sample_id","in":"path","required":true,"schema":{"type":"integer","title":"Sample Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSample"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/SampleResponse"},{"$ref":"#/components/schemas/ResourceNotFoundResponse"}],"title":"Response Update Sample Sample Sample Id Patch"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["sample"],"summary":"Get Sample by ID","description":"Endpoint to retrieve a sample by its ID.","operationId":"get_sample_by_id_sample__sample_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"sample_id","in":"path","required":true,"schema":{"type":"integer","title":"Sample Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/SampleResponse"},{"$ref":"#/components/schemas/ResourceNotFoundResponse"}],"title":"Response Get Sample By Id Sample Sample Id Get"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["sample"],"summary":"Delete Sample by ID","operationId":"delete_sample_by_id_sample__sample_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"sample_id","in":"path","required":true,"schema":{"type":"integer","title":"Sample Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sensor":{"post":{"tags":["sensor"],"summary":"Add Sensor","description":"Add a sensor to the system.","operationId":"add_sensor_sensor_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSensor"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SensorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["sensor"],"summary":"Get Sensors","description":"Retrieve all sensors from the system.\nThis endpoint is a placeholder and should be implemented with actual logic.","operationId":"get_sensors_sensor_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"query","required":false,"schema":{"type":"integer","title":"Thing Id"}},{"name":"parameter_id","in":"query","required":false,"schema":{"type":"integer","title":"Parameter Id"}},{"name":"sort","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_SensorResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sensor/{sensor_id}":{"patch":{"tags":["sensor"],"summary":"Update Sensor","description":"Update a sensor in the system.","operationId":"update_sensor_sensor__sensor_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"sensor_id","in":"path","required":true,"schema":{"type":"integer","title":"Sensor Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSensor"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SensorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["sensor"],"summary":"Delete Sensor","description":"Delete a sensor in the system","operationId":"delete_sensor_sensor__sensor_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"sensor_id","in":"path","required":true,"schema":{"type":"integer","title":"Sensor Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"tags":["sensor"],"summary":"Get Sensor","description":"Retrieve a sensor by its ID.","operationId":"get_sensor_sensor__sensor_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"sensor_id","in":"path","required":true,"schema":{"type":"integer","title":"Sensor Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SensorResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/search":{"get":{"tags":["search"],"summary":"Search Api","description":"Search endpoint for the collaborative network.","operationId":"search_api_search_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","title":"Q"}},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":25,"title":"Limit"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_dict_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/geothermal-well":{"get":{"tags":["geothermal"],"summary":"Get all geothermal wells","description":"List geothermal wells.\n\nNOTE: sourced from the legacy NM_Wells mirror (NMW_WellHeaders where\nGthrmExist is set). Will be re-pointed at the thing table post-transform.\n\n``q`` is what the UI well picker uses: the catalogue is far too large to\nchoose from by scrolling, so the term is matched server-side and the total\nreported by the pagination envelope is the size of the match set.","operationId":"get_geothermal_wells_thing_geothermal_well_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"county","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"County"}},{"name":"name_contains","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name Contains"}},{"name":"q","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"Free-text search across well name, API, well number, operator and county. Whitespace-separated words are ANDed, so each word added narrows the result. Case-insensitive substring match.","title":"Q"},"description":"Free-text search across well name, API, well number, operator and county. Whitespace-separated words are ANDed, so each word added narrows the result. Case-insensitive substring match."},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_GeothermalWellResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/geothermal-well/{well_data_id}":{"get":{"tags":["geothermal"],"summary":"Get geothermal well by legacy WellDataID","description":"Get a single geothermal well by its legacy NMW WellDataID (GUID).\n\nNOTE: keyed by the legacy GUID because these rows are not yet in the thing\ntable. Post-transform this becomes an integer thing_id lookup.","operationId":"get_geothermal_well_thing_geothermal_well__well_data_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"well_data_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Well Data Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GeothermalWellResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/water-well":{"get":{"tags":["thing"],"summary":"Get all water wells","description":"Retrieve all wells from the database.","operationId":"get_water_wells_thing_water_well_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"sort","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"title":"Filter"}},{"name":"query","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Query"}},{"name":"name","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"}},{"name":"name_contains","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name Contains"}},{"name":"include_contacts","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Contacts"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_WellResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["thing"],"summary":"Create a water well","description":"Create a new water well in the database.","operationId":"create_well_thing_water_well_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWell"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WellResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/water-well/{thing_id}":{"get":{"tags":["thing"],"summary":"Get water well by ID","description":"Retrieve a water well by ID from the database.","operationId":"get_well_by_id_thing_water_well__thing_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WellResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["thing"],"summary":"Update well by parent thing ID","description":"Update an existing well by ID.","operationId":"update_water_well_thing_water_well__thing_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWell"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WellResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/water-well/{thing_id}/details":{"get":{"tags":["thing"],"summary":"Get water well details payload","description":"Retrieve the consolidated payload needed to render the well details page.\nHydrograph series and map layer loading are intentionally handled separately.","operationId":"get_well_details_thing_water_well__thing_id__details_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}},{"name":"field_event_limit","in":"query","required":false,"schema":{"type":"integer","maximum":100,"minimum":1,"default":25,"title":"Field Event Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WellDetailsResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/water-well/{thing_id}/export":{"get":{"tags":["thing"],"summary":"Get water well export payload","description":"Retrieve the minimal payload needed for field sheet export generation.","operationId":"get_well_export_thing_water_well__thing_id__export_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WellExportResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/water-well/{thing_id}/well-screen":{"get":{"tags":["thing"],"summary":"Get well screens by water well ID","description":"Retrieve all well screens for a specific water well by its ID.","operationId":"get_well_screens_by_well_id_thing_water_well__thing_id__well_screen_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_WellScreenResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/well-screen":{"get":{"tags":["thing"],"summary":"Get well screens","description":"Retrieve all well screens from the database.","operationId":"get_well_screens_thing_well_screen_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"query","required":false,"schema":{"type":"integer","title":"Thing Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_WellScreenResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["thing"],"summary":"Create a new well screen","description":"Create a new well screen in the database.","operationId":"create_wellscreen_thing_well_screen_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWellScreen"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WellScreenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/well-screen/{wellscreen_id}":{"get":{"tags":["thing"],"summary":"Get well screen by ID","description":"Retrieve a well screen by ID from the database.","operationId":"get_well_screen_by_id_thing_well_screen__wellscreen_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"wellscreen_id","in":"path","required":true,"schema":{"type":"integer","title":"Wellscreen Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WellScreenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/spring":{"get":{"tags":["thing"],"summary":"Get all springs","description":"Retrieve all springs from the database.","operationId":"get_springs_thing_spring_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"sort","in":"query","required":false,"schema":{"type":"string","title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","title":"Order"}},{"name":"filter","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"title":"Filter"}},{"name":"query","in":"query","required":false,"schema":{"type":"string","title":"Query"}},{"name":"name_contains","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name Contains"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_SpringResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["thing"],"summary":"Create a new spring","description":"Create a new well in the database.","operationId":"create_spring_thing_spring_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSpring"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpringResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/spring/{thing_id}":{"get":{"tags":["thing"],"summary":"Get spring by ID","description":"Retrieve a spring by ID from the database.","operationId":"get_spring_by_id_thing_spring__thing_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpringResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["thing"],"summary":"Update spring by parent thing ID","description":"Update an existing spring by ID.","operationId":"update_spring_thing_spring__thing_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSpring"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SpringResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/id-link":{"get":{"tags":["thing"],"summary":"Get all thing links","description":"Retrieve all thing links, optionally filtered and sorted.","operationId":"get_thing_id_links_thing_id_link_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"filter","in":"query","required":false,"schema":{"type":"string","title":"Filter"}},{"name":"sort","in":"query","required":false,"schema":{"type":"string","title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","title":"Order"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_ThingIdLinkResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"tags":["thing"],"summary":"Create a new thing link","description":"Create a new link between a thing and an alternate ID.","operationId":"create_thing_id_link_thing_id_link_post","security":[{"OAuth2AuthorizationCodeBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateThingIdLink"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThingIdLinkResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/id-link/{link_id}":{"get":{"tags":["thing"],"summary":"Get thing links by link ID","description":"Retrieve all links for a specific thing by its ID.","operationId":"get_thing_id_links_thing_id_link__link_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"link_id","in":"path","required":true,"schema":{"type":"integer","title":"Link Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThingIdLinkResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"patch":{"tags":["thing"],"summary":"Update thing link by ID","operationId":"update_thing_id_link_thing_id_link__link_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"link_id","in":"path","required":true,"schema":{"type":"integer","title":"Link Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateThingIdLink"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThingIdLinkResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["thing"],"summary":"Delete thing link by ID","description":"Delete a thing link by ID.","operationId":"delete_thing_id_link_thing_id_link__link_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"link_id","in":"path","required":true,"schema":{"type":"integer","title":"Link Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing":{"get":{"tags":["thing"],"summary":"Get all things","description":"Retrieve all things or filter by type.","operationId":"get_things_thing_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"within","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Within"}},{"name":"query","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Query"}},{"name":"sort","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sort"}},{"name":"order","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Order"}},{"name":"include_contacts","in":"query","required":false,"schema":{"type":"boolean","default":false,"title":"Include Contacts"}},{"name":"filter","in":"query","required":false,"schema":{"anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}],"title":"Filter"}},{"name":"name_contains","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name Contains"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_ThingResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/{thing_id}":{"get":{"tags":["thing"],"summary":"Get thing by ID","description":"Retrieve a thing by ID from the database.","operationId":"get_thing_by_id_thing__thing_id__get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ThingResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["thing"],"summary":"Delete thing by ID","description":"Delete a thing by ID.","operationId":"delete_thing_thing__thing_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/{thing_id}/id-link":{"get":{"tags":["thing"],"summary":"Get thing links by thing ID","description":"Retrieve all links for a specific thing by its ID.","operationId":"get_thing_id_links_thing__thing_id__id_link_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_ThingIdLinkResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/{thing_id}/deployment":{"get":{"tags":["thing"],"summary":"Get deployments by thing ID","description":"Retrieve all deployments for a specific thing by its ID.","operationId":"get_thing_deployments_thing__thing_id__deployment_get","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"thing_id","in":"path","required":true,"schema":{"type":"integer","title":"Thing Id"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","minimum":1,"description":"Page number","default":1,"title":"Page"},"description":"Page number"},{"name":"size","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":1,"default":25,"title":"Size"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page_DeploymentResponse_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/thing/well-screen/{well_screen_id}":{"patch":{"tags":["thing"],"summary":"Update Well Screen by ID","operationId":"update_well_screen_thing_well_screen__well_screen_id__patch","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"well_screen_id","in":"path","required":true,"schema":{"type":"integer","title":"Well Screen Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateWellScreen"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WellScreenResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"tags":["thing"],"summary":"Delete well screen by ID","description":"Delete a well screen by ID.","operationId":"delete_well_screen_thing_well_screen__well_screen_id__delete","security":[{"OAuth2AuthorizationCodeBearer":[]}],"parameters":[{"name":"well_screen_id","in":"path","required":true,"schema":{"type":"integer","title":"Well Screen Id"}}],"responses":{"204":{"description":"Successful Response"},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ngwmn/waterlevels/{pointid}":{"get":{"tags":["NGWMN"],"summary":"Get waterlevels for a given pointid in the NGWMN format","operationId":"read_ngwmn_waterlevels_ngwmn_waterlevels__pointid__get","parameters":[{"name":"pointid","in":"path","required":true,"schema":{"type":"string","title":"Pointid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ngwmn/wellconstruction/{pointid}":{"get":{"tags":["NGWMN"],"summary":"Get wellconstruction for a given pointid in the NGWMN format","operationId":"read_ngwmn_wellconstruction_ngwmn_wellconstruction__pointid__get","parameters":[{"name":"pointid","in":"path","required":true,"schema":{"type":"string","title":"Pointid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ngwmn/lithology/{pointid}":{"get":{"tags":["NGWMN"],"summary":"Get lithology for a given pointid in the NGWMN format","operationId":"read_ngwmn_lithology_ngwmn_lithology__pointid__get","parameters":[{"name":"pointid","in":"path","required":true,"schema":{"type":"string","title":"Pointid"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/feedback":{"post":{"tags":["feedback"],"summary":"Create Feedback","operationId":"create_feedback_feedback_post","parameters":[{"name":"_user","in":"query","required":false,"schema":{"title":" User"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackCreate"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FeedbackResponse"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"AddressResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"contact_id":{"type":"integer","title":"Contact Id"},"address_line_1":{"type":"string","title":"Address Line 1"},"address_line_2":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address Line 2"},"city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"postal_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Postal Code"},"country":{"type":"string","title":"Country"},"address_type":{"$ref":"#/components/schemas/address_type"}},"type":"object","required":["id","created_at","release_status","contact_id","address_line_1","country","address_type"],"title":"AddressResponse","description":"Response schema for address details."},"AssetAssociationResponse":{"properties":{"asset_id":{"type":"integer","title":"Asset Id"},"thing_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Thing Id"}},"type":"object","required":["asset_id"],"title":"AssetAssociationResponse"},"AssetAssociationUpdate":{"properties":{"thing_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Thing Id"}},"type":"object","title":"AssetAssociationUpdate"},"AssetResponse":{"properties":{"name":{"type":"string","title":"Name"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"},"storage_path":{"type":"string","title":"Storage Path"},"mime_type":{"type":"string","title":"Mime Type"},"size":{"type":"integer","title":"Size"},"uri":{"type":"string","title":"Uri"},"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"storage_service":{"type":"string","title":"Storage Service"},"signed_url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Signed Url"}},"type":"object","required":["name","storage_path","mime_type","size","uri","id","created_at","release_status","storage_service"],"title":"AssetResponse"},"AuthorResponse":{"properties":{"id":{"type":"integer","title":"Id"},"name":{"type":"string","title":"Name"},"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"affiliation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Affiliation"}},"type":"object","required":["id","name"],"title":"AuthorResponse","description":"Schema for the response of an author."},"Body_bulk_upload_groundwater_levels_observation_groundwater_level_bulk_upload_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_bulk_upload_groundwater_levels_observation_groundwater_level_bulk_upload_post"},"Body_upload_and_record_asset_asset_upload_and_record_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"},"thing_id":{"type":"integer","title":"Thing Id"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"}},"type":"object","required":["file","thing_id"],"title":"Body_upload_and_record_asset_asset_upload_and_record_post"},"Body_upload_asset_asset_upload_post":{"properties":{"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["file"],"title":"Body_upload_asset_asset_upload_post"},"ContactResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"organization":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Organization"},"role":{"$ref":"#/components/schemas/role"},"contact_type":{"$ref":"#/components/schemas/contact_type"},"incomplete_nma_phones":{"items":{"type":"string"},"type":"array","title":"Incomplete Nma Phones","default":[]},"emails":{"items":{"$ref":"#/components/schemas/EmailResponse"},"type":"array","title":"Emails","default":[]},"phones":{"items":{"$ref":"#/components/schemas/PhoneResponse"},"type":"array","title":"Phones","default":[]},"addresses":{"items":{"$ref":"#/components/schemas/AddressResponse"},"type":"array","title":"Addresses","default":[]},"things":{"items":{"$ref":"#/components/schemas/ThingResponseForContact"},"type":"array","title":"Things","default":[]},"communication_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Communication Notes","default":[]},"general_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"General Notes","default":[]}},"type":"object","required":["id","created_at","release_status","name","organization","role","contact_type"],"title":"ContactResponse","description":"Response schema for contact details."},"CreateAddress":{"properties":{"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"contact_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Contact Id"},"address_line_1":{"type":"string","title":"Address Line 1"},"address_line_2":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address Line 2"},"city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State","default":"NM"},"postal_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Postal Code"},"country":{"type":"string","title":"Country","default":"United States"},"address_type":{"$ref":"#/components/schemas/address_type","default":"Primary"}},"type":"object","required":["address_line_1"],"title":"CreateAddress","description":"Schema for creating an address."},"CreateAsset":{"properties":{"name":{"type":"string","title":"Name"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"},"storage_path":{"type":"string","title":"Storage Path"},"mime_type":{"type":"string","title":"Mime Type"},"size":{"type":"integer","title":"Size"},"uri":{"type":"string","title":"Uri"},"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"thing_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Thing Id"}},"type":"object","required":["name","storage_path","mime_type","size","uri"],"title":"CreateAsset"},"CreateContact":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"organization":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Organization"},"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"thing_id":{"type":"integer","title":"Thing Id"},"role":{"$ref":"#/components/schemas/role"},"contact_type":{"$ref":"#/components/schemas/contact_type"},"nma_pk_owners":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Pk Owners"},"emails":{"anyOf":[{"items":{"$ref":"#/components/schemas/CreateEmail"},"type":"array"},{"type":"null"}],"title":"Emails"},"phones":{"anyOf":[{"items":{"$ref":"#/components/schemas/CreatePhone"},"type":"array"},{"type":"null"}],"title":"Phones"},"addresses":{"anyOf":[{"items":{"$ref":"#/components/schemas/CreateAddress"},"type":"array"},{"type":"null"}],"title":"Addresses"},"notes":{"anyOf":[{"items":{"$ref":"#/components/schemas/CreateNote"},"type":"array"},{"type":"null"}],"title":"Notes"}},"type":"object","required":["thing_id","role","contact_type"],"title":"CreateContact","description":"Schema for creating a contact."},"CreateEmail":{"properties":{"email":{"type":"string","title":"Email"},"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"contact_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Contact Id"},"email_type":{"$ref":"#/components/schemas/email_type","default":"Primary"}},"type":"object","required":["email"],"title":"CreateEmail","description":"Schema for creating an email."},"CreateGroundwaterLevelObservation":{"properties":{"parameter_id":{"type":"integer","title":"Parameter Id"},"observation_datetime":{"type":"string","format":"date-time","title":"Observation Datetime"},"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"sample_id":{"type":"integer","title":"Sample Id"},"sensor_id":{"type":"integer","title":"Sensor Id"},"value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Value"},"unit":{"anyOf":[{"$ref":"#/components/schemas/unit"},{"type":"null"}]},"measuring_point_height":{"type":"number","title":"Measuring Point Height"},"groundwater_level_reason":{"type":"string","title":"Groundwater Level Reason"}},"type":"object","required":["parameter_id","observation_datetime","sample_id","sensor_id","value","unit","measuring_point_height","groundwater_level_reason"],"title":"CreateGroundwaterLevelObservation"},"CreateGroup":{"properties":{"project_area":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Project Area"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"parent_group_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Parent Group Id"},"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"name":{"type":"string","title":"Name"}},"type":"object","required":["name"],"title":"CreateGroup","description":"Schema for creating a group."},"CreateLexiconCategory":{"properties":{"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"}},"type":"object","required":["name"],"title":"CreateLexiconCategory","description":"Pydantic model for creating a lexicon category.\nThis model can be extended to include additional fields as needed."},"CreateLexiconTerm":{"properties":{"term":{"type":"string","title":"Term"},"definition":{"type":"string","title":"Definition"},"categories":{"items":{"type":"string"},"type":"array","title":"Categories"}},"type":"object","required":["term","definition","categories"],"title":"CreateLexiconTerm","description":"Pydantic model for creating a lexicon term.\nThis model can be extended to include additional fields as needed."},"CreateLexiconTriple":{"properties":{"subject":{"$ref":"#/components/schemas/CreateLexiconTerm"},"predicate":{"type":"string","title":"Predicate"},"object_":{"$ref":"#/components/schemas/CreateLexiconTerm"}},"type":"object","required":["subject","predicate","object_"],"title":"CreateLexiconTriple","description":"Pydantic model for creating a triple.\nThis model can be extended to include additional fields as needed."},"CreateLocation":{"properties":{"point":{"type":"string","title":"Point"},"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"notes":{"items":{"$ref":"#/components/schemas/CreateNote"},"type":"array","title":"Notes","default":[]},"elevation":{"type":"number","title":"Elevation"}},"type":"object","required":["point","elevation"],"title":"CreateLocation","description":"Schema for creating a sample location."},"CreateMonitoringFrequency":{"properties":{"monitoring_frequency":{"$ref":"#/components/schemas/monitoring_frequency"},"start_date":{"type":"string","format":"date","title":"Start Date"},"end_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"End Date"}},"type":"object","required":["monitoring_frequency","start_date"],"title":"CreateMonitoringFrequency"},"CreateNote":{"properties":{"note_type":{"$ref":"#/components/schemas/note_type"},"content":{"type":"string","title":"Content"},"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"}},"type":"object","required":["note_type","content"],"title":"CreateNote","description":"Schema for creating a new Note. The parent object's ID and type will be\ntaken from the URL path, not the request body."},"CreatePhone":{"properties":{"phone_number":{"type":"string","title":"Phone Number"},"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"contact_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Contact Id"},"phone_type":{"$ref":"#/components/schemas/phone_type","default":"Primary"}},"type":"object","required":["phone_number"],"title":"CreatePhone","description":"Schema for creating a phone number."},"CreatePublication":{"properties":{"title":{"type":"string","title":"Title"},"authors":{"items":{"type":"string"},"type":"array","title":"Authors"},"year":{"type":"integer","title":"Year"},"doi":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Doi"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"},"publication_type":{"$ref":"#/components/schemas/publication_type"}},"type":"object","required":["title","authors","year","publication_type"],"title":"CreatePublication","description":"Schema for creating a new publication."},"CreateSample":{"properties":{"sample_date":{"type":"string","format":"date-time","title":"Sample Date"},"depth_top":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Depth Top"},"depth_bottom":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Depth Bottom"},"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"field_activity_id":{"type":"integer","title":"Field Activity Id"},"field_event_participant_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Field Event Participant Id"},"sample_name":{"type":"string","title":"Sample Name"},"sample_matrix":{"$ref":"#/components/schemas/sample_matrix"},"sample_method":{"$ref":"#/components/schemas/sample_method"},"qc_type":{"$ref":"#/components/schemas/qc_type"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","required":["sample_date","field_activity_id","sample_name","sample_matrix","sample_method","qc_type"],"title":"CreateSample"},"CreateSensor":{"properties":{"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"name":{"type":"string","title":"Name"},"sensor_type":{"$ref":"#/components/schemas/sensor_type"},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model"},"serial_no":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Serial No"},"pcn_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pcn Number"},"owner_agency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner Agency"},"sensor_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sensor Status"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","required":["name","sensor_type"],"title":"CreateSensor","description":"Schema for creating a new sensor."},"CreateSpring":{"properties":{"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"location_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Location Id"},"group_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Group Id"},"name":{"type":"string","title":"Name"},"first_visit_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"First Visit Date"},"notes":{"anyOf":[{"items":{"$ref":"#/components/schemas/CreateNote"},"type":"array"},{"type":"null"}],"title":"Notes"},"alternate_ids":{"anyOf":[{"items":{"$ref":"#/components/schemas/CreateThingIdLink"},"type":"array"},{"type":"null"}],"title":"Alternate Ids"},"monitoring_frequencies":{"anyOf":[{"items":{"$ref":"#/components/schemas/CreateMonitoringFrequency"},"type":"array"},{"type":"null"}],"title":"Monitoring Frequencies"},"spring_type":{"anyOf":[{"$ref":"#/components/schemas/spring_type"},{"type":"null"}]}},"type":"object","required":["name"],"title":"CreateSpring","description":"Schema for creating a spring."},"CreateThingIdLink":{"properties":{"thing_id":{"type":"integer","title":"Thing Id"},"relation":{"type":"string","title":"Relation"},"alternate_id":{"type":"string","title":"Alternate Id"},"alternate_organization":{"type":"string","title":"Alternate Organization"}},"type":"object","required":["thing_id","relation","alternate_id","alternate_organization"],"title":"CreateThingIdLink","description":"Schema for creating a link between a thing and its ID."},"CreateWaterChemistryObservation":{"properties":{"parameter_id":{"type":"integer","title":"Parameter Id"},"observation_datetime":{"type":"string","format":"date-time","title":"Observation Datetime"},"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"sample_id":{"type":"integer","title":"Sample Id"},"sensor_id":{"type":"integer","title":"Sensor Id"},"value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Value"},"unit":{"anyOf":[{"$ref":"#/components/schemas/unit"},{"type":"null"}]}},"type":"object","required":["parameter_id","observation_datetime","sample_id","sensor_id","value","unit"],"title":"CreateWaterChemistryObservation"},"CreateWell":{"properties":{"well_depth":{"anyOf":[{"type":"number","exclusiveMinimum":0.0},{"type":"null"}],"title":"Well Depth","description":"Well depth in feet"},"hole_depth":{"anyOf":[{"type":"number","exclusiveMinimum":0.0},{"type":"null"}],"title":"Hole Depth","description":"Hole depth in feet"},"well_casing_depth":{"anyOf":[{"type":"number","exclusiveMinimum":0.0},{"type":"null"}],"title":"Well Casing Depth","description":"Well casing depth in feet"},"measuring_point_height":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Measuring Point Height","description":"Measuring point height in feet"},"well_pump_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Pump Depth"},"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"location_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Location Id"},"group_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Group Id"},"name":{"type":"string","title":"Name"},"first_visit_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"First Visit Date"},"notes":{"anyOf":[{"items":{"$ref":"#/components/schemas/CreateNote"},"type":"array"},{"type":"null"}],"title":"Notes"},"alternate_ids":{"anyOf":[{"items":{"$ref":"#/components/schemas/CreateThingIdLink"},"type":"array"},{"type":"null"}],"title":"Alternate Ids"},"monitoring_frequencies":{"anyOf":[{"items":{"$ref":"#/components/schemas/CreateMonitoringFrequency"},"type":"array"},{"type":"null"}],"title":"Monitoring Frequencies"},"well_purposes":{"anyOf":[{"items":{"$ref":"#/components/schemas/well_purpose"},"type":"array"},{"type":"null"}],"title":"Well Purposes"},"well_depth_source":{"anyOf":[{"$ref":"#/components/schemas/origin_type"},{"type":"null"}]},"well_casing_diameter":{"anyOf":[{"type":"number","exclusiveMinimum":0.0},{"type":"null"}],"title":"Well Casing Diameter","description":"Well casing diameter in inches"},"well_casing_materials":{"anyOf":[{"items":{"$ref":"#/components/schemas/casing_material"},"type":"array"},{"type":"null"}],"title":"Well Casing Materials"},"measuring_point_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Measuring Point Description"},"well_completion_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Well Completion Date"},"well_completion_date_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Completion Date Source"},"well_driller_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Driller Name"},"well_construction_method":{"anyOf":[{"$ref":"#/components/schemas/well_construction_method"},{"type":"null"}]},"well_construction_method_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Construction Method Source"},"well_pump_type":{"anyOf":[{"$ref":"#/components/schemas/well_pump_type"},{"type":"null"}]},"is_suitable_for_datalogger":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Suitable For Datalogger"},"is_open":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Is Open"},"well_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Status"},"monitoring_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Monitoring Status"},"formation_completion_code":{"anyOf":[{"$ref":"#/components/schemas/formation_code"},{"type":"null"}]},"nma_formation_zone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Formation Zone"}},"type":"object","required":["name"],"title":"CreateWell","description":"Schema for creating a well."},"CreateWellScreen":{"properties":{"release_status":{"$ref":"#/components/schemas/release_status","default":"draft"},"thing_id":{"type":"integer","title":"Thing Id"},"aquifer_system_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Aquifer System Id"},"geologic_formation_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Geologic Formation Id"},"screen_depth_bottom":{"anyOf":[{"type":"number","minimum":0.0},{"type":"null"}],"title":"Screen Depth Bottom","description":"Screen depth bottom in feet"},"screen_depth_top":{"anyOf":[{"type":"number","minimum":0.0},{"type":"null"}],"title":"Screen Depth Top","description":"Screen depth top in feet"},"screen_type":{"anyOf":[{"$ref":"#/components/schemas/screen_type"},{"type":"null"}]},"screen_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Screen Description"}},"type":"object","required":["thing_id"],"title":"CreateWellScreen","description":"Schema for creating a well screen."},"DeploymentResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"thing_id":{"type":"integer","title":"Thing Id"},"sensor":{"$ref":"#/components/schemas/SensorResponse"},"installation_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Installation Date"},"removal_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Removal Date"},"recording_interval":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Recording Interval"},"recording_interval_units":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Recording Interval Units"},"hanging_cable_length":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Hanging Cable Length"},"hanging_point_height":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Hanging Point Height"},"hanging_point_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Hanging Point Description"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","required":["id","created_at","release_status","thing_id","sensor","installation_date","removal_date","recording_interval","recording_interval_units","hanging_cable_length","hanging_point_height","hanging_point_description","notes"],"title":"DeploymentResponse"},"EmailResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"contact_id":{"type":"integer","title":"Contact Id"},"email":{"type":"string","title":"Email"},"email_type":{"$ref":"#/components/schemas/email_type"}},"type":"object","required":["id","created_at","release_status","contact_id","email","email_type"],"title":"EmailResponse","description":"Response schema for email details."},"Feature":{"properties":{"type":{"type":"string","title":"Type","default":"Feature"},"geometry":{"$ref":"#/components/schemas/schemas__thing__GeoJSONGeometry"},"properties":{"additionalProperties":true,"type":"object","title":"Properties","default":{}}},"type":"object","required":["geometry"],"title":"Feature","description":"Feature schema for GeoJSON response."},"FeatureCollectionResponse":{"properties":{"type":{"type":"string","title":"Type","default":"FeatureCollection"},"features":{"items":{"$ref":"#/components/schemas/Feature"},"type":"array","title":"Features","default":[]}},"type":"object","title":"FeatureCollectionResponse","description":"Response schema for GeoJSON FeatureCollection."},"FeedbackCreate":{"properties":{"type":{"type":"string","enum":["bug","feature"],"title":"Type"},"page_url":{"type":"string","title":"Page Url"},"reporter_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reporter Name"},"reporter_email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reporter Email"},"browser":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Browser"},"submitted_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Submitted At"},"what_happened":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"What Happened"},"severity":{"type":"string","title":"Severity","default":"Low"},"problem":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Problem"},"who_would_use":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Who Would Use"},"what_it_should_do":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"What It Should Do"}},"type":"object","required":["type","page_url"],"title":"FeedbackCreate"},"FeedbackResponse":{"properties":{"jira_key":{"type":"string","title":"Jira Key"},"jira_url":{"type":"string","title":"Jira Url"}},"type":"object","required":["jira_key","jira_url"],"title":"FeedbackResponse"},"FieldActivityResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"field_event_id":{"type":"integer","title":"Field Event Id"},"activity_type":{"$ref":"#/components/schemas/activity_type"}},"type":"object","required":["id","created_at","release_status","field_event_id","activity_type"],"title":"FieldActivityResponse"},"FieldEventParticipantResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"field_event_id":{"type":"integer","title":"Field Event Id"},"contact_id":{"type":"integer","title":"Contact Id"},"participant_role":{"type":"string","title":"Participant Role"},"participant":{"$ref":"#/components/schemas/ContactResponse"}},"type":"object","required":["id","created_at","release_status","field_event_id","contact_id","participant_role","participant"],"title":"FieldEventParticipantResponse"},"FieldEventResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"thing_id":{"type":"integer","title":"Thing Id"},"event_date":{"type":"string","format":"date-time","title":"Event Date"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","required":["id","created_at","release_status","thing_id","event_date","notes"],"title":"FieldEventResponse"},"GeoJSONProperties":{"properties":{"elevation":{"type":"number","title":"Elevation"},"elevation_unit":{"type":"string","title":"Elevation Unit","default":"ft"},"vertical_datum":{"type":"string","title":"Vertical Datum","default":"NAVD88"},"elevation_method":{"anyOf":[{"$ref":"#/components/schemas/elevation_method"},{"type":"null"}]},"county":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"County"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"quad_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Quad Name"},"utm_coordinates":{"$ref":"#/components/schemas/GeoJSONUTMCoordinates"},"notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Notes","default":[]},"nma_location_notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Location Notes"},"nma_data_reliability":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Data Reliability"},"nma_date_created":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Nma Date Created"},"nma_site_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Nma Site Date"}},"type":"object","required":["elevation","elevation_method"],"title":"GeoJSONProperties"},"GeoJSONUTMCoordinates":{"properties":{"easting":{"type":"number","title":"Easting"},"northing":{"type":"number","title":"Northing"},"utm_zone":{"type":"string","title":"Utm Zone","default":"13N"},"horizontal_datum":{"type":"string","title":"Horizontal Datum","default":"NAD83"}},"type":"object","required":["easting","northing"],"title":"GeoJSONUTMCoordinates"},"GeothermalWellResponse":{"properties":{"well_data_id":{"type":"string","format":"uuid","title":"Well Data Id"},"thing_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Thing Id"},"api":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Api"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"well_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Number"},"well_class":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Class"},"well_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Type"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"operator":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Operator"},"owner":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner"},"total_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Total Depth"},"completion_date":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Completion Date"},"has_geothermal_data":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Has Geothermal Data"},"county":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"County"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"latitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Latitude"},"longitude":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Longitude"}},"type":"object","required":["well_data_id"],"title":"GeothermalWellResponse","description":"Read model for a geothermal well sourced from the legacy NM_Wells mirror.\n\nNOTE: This currently reads directly from the ``NMW_WellHeaders`` /\n``NMW_WellLocations`` staging tables (see ``db/nmw_legacy.py``). Once the\nNM_Wells -> Ocotillo transform lands, these rows will be backed by the\n``thing`` table and ``thing_id`` will be populated. Until then ``thing_id``\nis always ``None`` and ``well_data_id`` (legacy GUID) is the identifier."},"GroundwaterLevelObservationResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"sample_id":{"type":"integer","title":"Sample Id"},"sensor_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sensor Id"},"observation_datetime":{"type":"string","title":"Observation Datetime"},"parameter":{"$ref":"#/components/schemas/ParameterResponse"},"value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Value"},"unit":{"$ref":"#/components/schemas/unit"},"nma_data_quality":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Data Quality"},"depth_to_water_bgs":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Depth To Water Bgs"},"measuring_point_height":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Measuring Point Height"},"groundwater_level_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Groundwater Level Reason"}},"type":"object","required":["id","created_at","release_status","sample_id","sensor_id","observation_datetime","parameter","value","unit","depth_to_water_bgs","measuring_point_height","groundwater_level_reason"],"title":"GroundwaterLevelObservationResponse"},"GroupResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"project_area":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Project Area"},"group_type":{"anyOf":[{"$ref":"#/components/schemas/group_type"},{"type":"null"}]},"parent_group_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Parent Group Id"},"well_count":{"type":"integer","title":"Well Count","default":0}},"type":"object","required":["id","created_at","release_status","name","description","project_area","group_type","parent_group_id"],"title":"GroupResponse","description":"Pydantic model for the response of a group.\nThis model can be extended to include additional fields as needed."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"LexiconCategoryResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"name":{"type":"string","title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"}},"type":"object","required":["id","created_at","name"],"title":"LexiconCategoryResponse","description":"Pydantic model for the response of a lexicon category.\nThis model can be extended to include additional fields as needed."},"LexiconTermResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"term":{"type":"string","title":"Term"},"definition":{"type":"string","title":"Definition"},"categories":{"items":{"$ref":"#/components/schemas/LexiconCategoryResponse"},"type":"array","title":"Categories","default":[]}},"type":"object","required":["id","created_at","term","definition"],"title":"LexiconTermResponse","description":"Pydantic model for the response of a lexicon term.\nThis model can be extended to include additional fields as needed."},"LexiconTripleResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"subject":{"type":"string","title":"Subject"},"predicate":{"type":"string","title":"Predicate"},"object_":{"type":"string","title":"Object"}},"type":"object","required":["id","created_at","subject","predicate","object_"],"title":"LexiconTripleResponse"},"LocationGeoJSONResponse":{"properties":{"type":{"type":"string","title":"Type","default":"Feature"},"release_status":{"$ref":"#/components/schemas/release_status"},"geometry":{"$ref":"#/components/schemas/schemas__location__GeoJSONGeometry"},"properties":{"$ref":"#/components/schemas/GeoJSONProperties"}},"type":"object","required":["release_status","geometry","properties"],"title":"LocationGeoJSONResponse"},"LocationResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Notes","default":[]},"point":{"type":"string","title":"Point"},"elevation":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Elevation"},"horizontal_datum":{"type":"string","title":"Horizontal Datum","default":"WGS84"},"vertical_datum":{"type":"string","title":"Vertical Datum","default":"NAVD88"},"elevation_method":{"anyOf":[{"$ref":"#/components/schemas/elevation_method"},{"type":"null"}]},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"county":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"County"},"quad_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Quad Name"},"nma_location_notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Location Notes"},"nma_data_reliability":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Data Reliability"},"nma_date_created":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Nma Date Created"},"nma_site_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Nma Site Date"}},"type":"object","required":["id","created_at","release_status","point","elevation","elevation_method","state","county","quad_name"],"title":"LocationResponse","description":"Response schema for sample location details."},"MonitoringFrequencyResponse":{"properties":{"monitoring_frequency":{"$ref":"#/components/schemas/monitoring_frequency"},"start_date":{"type":"string","format":"date","title":"Start Date"},"end_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"End Date"}},"type":"object","required":["monitoring_frequency","start_date","end_date"],"title":"MonitoringFrequencyResponse"},"NoteResponse":{"properties":{"note_type":{"$ref":"#/components/schemas/note_type"},"content":{"type":"string","title":"Content"},"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"target_id":{"type":"integer","title":"Target Id"},"target_table":{"type":"string","title":"Target Table"}},"type":"object","required":["note_type","content","id","created_at","release_status","target_id","target_table"],"title":"NoteResponse","description":"Response schema for Note details."},"ObservationResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"sample_id":{"type":"integer","title":"Sample Id"},"sensor_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sensor Id"},"observation_datetime":{"type":"string","title":"Observation Datetime"},"parameter":{"$ref":"#/components/schemas/ParameterResponse"},"value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Value"},"unit":{"$ref":"#/components/schemas/unit"},"nma_data_quality":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Data Quality"},"depth_to_water_bgs":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Depth To Water Bgs"},"measuring_point_height":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Measuring Point Height"},"groundwater_level_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Groundwater Level Reason"}},"type":"object","required":["id","created_at","release_status","sample_id","sensor_id","observation_datetime","parameter","value","unit","depth_to_water_bgs","measuring_point_height","groundwater_level_reason"],"title":"ObservationResponse","description":"Response model for observations.\nCombines groundwater level and geothermal observation responses."},"Page_AddressResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/AddressResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[AddressResponse]"},"Page_AssetResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/AssetResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[AssetResponse]"},"Page_ContactResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ContactResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[ContactResponse]"},"Page_DeploymentResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/DeploymentResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[DeploymentResponse]"},"Page_EmailResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/EmailResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[EmailResponse]"},"Page_GeothermalWellResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/GeothermalWellResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[GeothermalWellResponse]"},"Page_GroundwaterLevelObservationResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/GroundwaterLevelObservationResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[GroundwaterLevelObservationResponse]"},"Page_GroupResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/GroupResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[GroupResponse]"},"Page_LexiconCategoryResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/LexiconCategoryResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[LexiconCategoryResponse]"},"Page_LexiconTermResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/LexiconTermResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[LexiconTermResponse]"},"Page_LexiconTripleResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/LexiconTripleResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[LexiconTripleResponse]"},"Page_LocationResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/LocationResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[LocationResponse]"},"Page_ObservationResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ObservationResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[ObservationResponse]"},"Page_PhoneResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/PhoneResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[PhoneResponse]"},"Page_SampleResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/SampleResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[SampleResponse]"},"Page_SensorResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/SensorResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[SensorResponse]"},"Page_SpringResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/SpringResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[SpringResponse]"},"Page_ThingIdLinkResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ThingIdLinkResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[ThingIdLinkResponse]"},"Page_ThingResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ThingResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[ThingResponse]"},"Page_TransducerObservationWithBlockResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/TransducerObservationWithBlockResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[TransducerObservationWithBlockResponse]"},"Page_WaterChemistryObservationResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/WaterChemistryObservationResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[WaterChemistryObservationResponse]"},"Page_WellResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/WellResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[WellResponse]"},"Page_WellScreenResponse_":{"properties":{"items":{"items":{"$ref":"#/components/schemas/WellScreenResponse"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[WellScreenResponse]"},"Page_dict_":{"properties":{"items":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Items"},"total":{"type":"integer","minimum":0.0,"title":"Total"},"page":{"type":"integer","minimum":1.0,"title":"Page"},"size":{"type":"integer","minimum":1.0,"title":"Size"},"pages":{"type":"integer","minimum":0.0,"title":"Pages"}},"type":"object","required":["items","total","page","size","pages"],"title":"Page[dict]"},"ParameterResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"parameter_name":{"$ref":"#/components/schemas/parameter_name"},"matrix":{"type":"string","title":"Matrix"},"parameter_type":{"anyOf":[{"$ref":"#/components/schemas/parameter_type"},{"type":"null"}]},"cas_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cas Number"},"default_unit":{"anyOf":[{"$ref":"#/components/schemas/unit"},{"type":"null"}]}},"type":"object","required":["id","created_at","release_status","parameter_name","matrix","parameter_type","cas_number","default_unit"],"title":"ParameterResponse","description":"Pydantic model for the response of a parameter.\nThis model can be extended to include additional fields as needed."},"PermissionHistoryResponse":{"properties":{"permission_type":{"$ref":"#/components/schemas/permission_type"},"permission_allowed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Permission Allowed"},"start_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Start Date"},"end_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"End Date"}},"type":"object","required":["permission_type","permission_allowed","start_date","end_date"],"title":"PermissionHistoryResponse","description":"Even though permission_allowed and start_date are not-nullable in the\ndatabase, they are nullable here to accommodate cases where no permission\nrecord exists for a given permission type."},"PhoneResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"contact_id":{"type":"integer","title":"Contact Id"},"phone_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number"},"phone_type":{"type":"string","title":"Phone Type"}},"type":"object","required":["id","created_at","release_status","contact_id","phone_type"],"title":"PhoneResponse","description":"Response schema for phone details."},"PublicationResponse":{"properties":{"id":{"type":"integer","title":"Id"},"title":{"type":"string","title":"Title"},"authors":{"items":{"$ref":"#/components/schemas/AuthorResponse"},"type":"array","title":"Authors"},"year":{"type":"integer","title":"Year"},"doi":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Doi"},"url":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Url"},"publication_type":{"$ref":"#/components/schemas/publication_type"}},"type":"object","required":["id","title","authors","year","publication_type"],"title":"PublicationResponse","description":"Schema for the response of a publication."},"ResourceNotFoundResponse":{"properties":{"detail":{"type":"string","title":"Detail"}},"type":"object","required":["detail"],"title":"ResourceNotFoundResponse"},"SampleResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"thing":{"$ref":"#/components/schemas/ThingResponse"},"field_event":{"$ref":"#/components/schemas/FieldEventResponse"},"field_activity":{"$ref":"#/components/schemas/FieldActivityResponse"},"contact":{"anyOf":[{"$ref":"#/components/schemas/ContactResponse"},{"type":"null"}]},"sample_date":{"type":"string","title":"Sample Date"},"sample_name":{"type":"string","title":"Sample Name"},"sample_matrix":{"$ref":"#/components/schemas/sample_matrix"},"sample_method":{"$ref":"#/components/schemas/sample_method"},"qc_type":{"$ref":"#/components/schemas/qc_type"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"depth_top":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Depth Top"},"depth_bottom":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Depth Bottom"}},"type":"object","required":["id","created_at","release_status","thing","field_event","field_activity","contact","sample_date","sample_name","sample_matrix","sample_method","qc_type","notes","depth_top","depth_bottom"],"title":"SampleResponse","description":"Developer's note\n\nThe frontend uses multiple fields for a thing, field_even, and field_activity,\nwhich is why full ThingResponse, FieldEventResponse, and FieldActivityResponse\nare returned. If the response becomes too large and slow, we can use\n.model_dump() and exlude fields to reduce the size."},"SensorResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"name":{"type":"string","title":"Name"},"sensor_type":{"$ref":"#/components/schemas/sensor_type"},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model"},"serial_no":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Serial No"},"pcn_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pcn Number"},"owner_agency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner Agency"},"sensor_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sensor Status"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","required":["id","created_at","release_status","name","sensor_type","model","serial_no","pcn_number","owner_agency","sensor_status","notes"],"title":"SensorResponse"},"SpringResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"name":{"type":"string","title":"Name"},"site_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Site Name"},"thing_type":{"type":"string","title":"Thing Type"},"current_location":{"$ref":"#/components/schemas/LocationGeoJSONResponse"},"first_visit_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"First Visit Date"},"groups":{"items":{"$ref":"#/components/schemas/GroupResponse"},"type":"array","title":"Groups","default":[]},"monitoring_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Monitoring Status"},"alternate_ids":{"items":{"$ref":"#/components/schemas/ThingIdLinkResponse"},"type":"array","title":"Alternate Ids","default":[]},"monitoring_frequencies":{"items":{"$ref":"#/components/schemas/MonitoringFrequencyResponse"},"type":"array","title":"Monitoring Frequencies","default":[]},"general_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"General Notes","default":[]},"sampling_procedure_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Sampling Procedure Notes","default":[]},"site_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Site Notes","default":[]},"spring_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Spring Type"}},"type":"object","required":["id","created_at","release_status","name","thing_type","current_location","first_visit_date","monitoring_status"],"title":"SpringResponse","description":"Response schema for spring details."},"ThingIdLinkResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"thing_id":{"type":"integer","title":"Thing Id"},"relation":{"type":"string","title":"Relation"},"alternate_id":{"type":"string","title":"Alternate Id"},"alternate_organization":{"$ref":"#/components/schemas/organization"}},"type":"object","required":["id","created_at","release_status","thing_id","relation","alternate_id","alternate_organization"],"title":"ThingIdLinkResponse"},"ThingResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"name":{"type":"string","title":"Name"},"site_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Site Name"},"thing_type":{"type":"string","title":"Thing Type"},"current_location":{"$ref":"#/components/schemas/LocationGeoJSONResponse"},"first_visit_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"First Visit Date"},"groups":{"items":{"$ref":"#/components/schemas/GroupResponse"},"type":"array","title":"Groups","default":[]},"monitoring_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Monitoring Status"},"alternate_ids":{"items":{"$ref":"#/components/schemas/ThingIdLinkResponse"},"type":"array","title":"Alternate Ids","default":[]},"monitoring_frequencies":{"items":{"$ref":"#/components/schemas/MonitoringFrequencyResponse"},"type":"array","title":"Monitoring Frequencies","default":[]},"general_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"General Notes","default":[]},"sampling_procedure_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Sampling Procedure Notes","default":[]},"site_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Site Notes","default":[]},"spring_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Spring Type"},"well_purposes":{"items":{"$ref":"#/components/schemas/well_purpose"},"type":"array","title":"Well Purposes","default":[]},"well_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Depth"},"well_depth_unit":{"type":"string","title":"Well Depth Unit","default":"ft"},"well_depth_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Depth Source"},"historic_depth_to_water":{"items":{"type":"string"},"type":"array","title":"Historic Depth To Water","default":[]},"hole_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Hole Depth"},"hole_depth_unit":{"type":"string","title":"Hole Depth Unit","default":"ft"},"well_casing_diameter":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Casing Diameter"},"well_casing_diameter_unit":{"type":"string","title":"Well Casing Diameter Unit","default":"in"},"well_casing_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Casing Depth"},"well_casing_depth_unit":{"type":"string","title":"Well Casing Depth Unit","default":"ft"},"well_casing_materials":{"items":{"$ref":"#/components/schemas/casing_material"},"type":"array","title":"Well Casing Materials","default":[]},"well_completion_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Well Completion Date"},"well_completion_date_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Completion Date Source"},"well_driller_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Driller Name"},"well_construction_method":{"anyOf":[{"$ref":"#/components/schemas/well_construction_method"},{"type":"null"}]},"well_construction_method_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Construction Method Source"},"well_pump_type":{"anyOf":[{"$ref":"#/components/schemas/well_pump_type"},{"type":"null"}]},"well_pump_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Pump Depth"},"well_pump_depth_unit":{"type":"string","title":"Well Pump Depth Unit","default":"ft"},"well_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Status"},"open_status":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Open Status"},"datalogger_suitability_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Datalogger Suitability Status"},"measuring_point_height":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Measuring Point Height"},"measuring_point_height_unit":{"type":"string","title":"Measuring Point Height Unit","default":"ft"},"measuring_point_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Measuring Point Description"},"aquifers":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Aquifers","default":[]},"water_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Water Notes","default":[]},"construction_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Construction Notes","default":[]},"contacts":{"items":{"$ref":"#/components/schemas/WellContactSummaryResponse"},"type":"array","title":"Contacts","default":[]},"permissions":{"items":{"$ref":"#/components/schemas/PermissionHistoryResponse"},"type":"array","title":"Permissions"},"formation_completion_code":{"anyOf":[{"$ref":"#/components/schemas/formation_code"},{"type":"null"}]},"nma_formation_zone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Formation Zone"},"well_location_note":{"items":{"type":"string"},"type":"array","title":"Well Location Note","default":[]}},"type":"object","required":["id","created_at","release_status","name","thing_type","current_location","first_visit_date","monitoring_status","well_depth_source","well_completion_date","well_completion_date_source","well_driller_name","well_construction_method","well_construction_method_source","well_pump_type","well_pump_depth","well_status","open_status","datalogger_suitability_status","measuring_point_height","measuring_point_description","permissions","formation_completion_code","nma_formation_zone"],"title":"ThingResponse"},"ThingResponseForContact":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"name":{"type":"string","title":"Name"}},"type":"object","required":["id","created_at","release_status","name"],"title":"ThingResponseForContact","description":"Response schema for thing details related to a contact. All that is needed\nare the id and name"},"TransducerObservationBlockResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"review_status":{"$ref":"#/components/schemas/review_status"},"start_datetime":{"type":"string","format":"date-time","title":"Start Datetime"},"end_datetime":{"type":"string","format":"date-time","title":"End Datetime"},"parameter_id":{"type":"integer","title":"Parameter Id"}},"type":"object","required":["id","created_at","release_status","review_status","start_datetime","end_datetime","parameter_id"],"title":"TransducerObservationBlockResponse"},"TransducerObservationResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"value":{"type":"number","title":"Value"},"observation_datetime":{"type":"string","format":"date-time","title":"Observation Datetime"},"parameter_id":{"type":"integer","title":"Parameter Id"},"deployment_id":{"type":"integer","title":"Deployment Id"}},"type":"object","required":["id","created_at","release_status","value","observation_datetime","parameter_id","deployment_id"],"title":"TransducerObservationResponse"},"TransducerObservationWithBlockResponse":{"properties":{"observation":{"$ref":"#/components/schemas/TransducerObservationResponse"},"block":{"$ref":"#/components/schemas/TransducerObservationBlockResponse"}},"type":"object","required":["observation","block"],"title":"TransducerObservationWithBlockResponse"},"UpdateAddress":{"properties":{"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"contact_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Contact Id"},"address_line_1":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address Line 1"},"address_line_2":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Address Line 2"},"city":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"City"},"state":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"State"},"postal_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Postal Code"},"country":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Country"},"address_type":{"anyOf":[{"$ref":"#/components/schemas/address_type"},{"type":"null"}]}},"type":"object","title":"UpdateAddress","description":"Schema for updating address information."},"UpdateAsset":{"properties":{"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"label":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Label"}},"type":"object","title":"UpdateAsset"},"UpdateContact":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"organization":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Organization"},"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"role":{"anyOf":[{"$ref":"#/components/schemas/role"},{"type":"null"}]},"contact_type":{"anyOf":[{"$ref":"#/components/schemas/contact_type"},{"type":"null"}]},"thing_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Thing Id"}},"type":"object","title":"UpdateContact","description":"Schema for updating contact information."},"UpdateEmail":{"properties":{"email":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"},"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"contact_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Contact Id"},"email_type":{"anyOf":[{"$ref":"#/components/schemas/email_type"},{"type":"null"}]}},"type":"object","title":"UpdateEmail","description":"Schema for updating email information."},"UpdateGroundwaterLevelObservation":{"properties":{"parameter_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Parameter Id"},"observation_datetime":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Observation Datetime"},"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"sample_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sample Id"},"sensor_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sensor Id"},"value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Value"},"unit":{"anyOf":[{"$ref":"#/components/schemas/unit"},{"type":"null"}]},"measuring_point_height":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Measuring Point Height"},"groundwater_level_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Groundwater Level Reason"}},"type":"object","title":"UpdateGroundwaterLevelObservation"},"UpdateGroup":{"properties":{"project_area":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Project Area"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"parent_group_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Parent Group Id"},"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"}},"type":"object","title":"UpdateGroup","description":"Pydantic model for updating a group.\nThis model can be extended to include additional fields as needed."},"UpdateLexiconCategory":{"properties":{"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"}},"type":"object","title":"UpdateLexiconCategory"},"UpdateLexiconTerm":{"properties":{"term":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Term"},"definition":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Definition"}},"type":"object","title":"UpdateLexiconTerm"},"UpdateLexiconTriple":{"properties":{"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"predicate":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Predicate"},"object_":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Object"}},"type":"object","title":"UpdateLexiconTriple"},"UpdateLocation":{"properties":{"point":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Point"},"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"notes":{"items":{"$ref":"#/components/schemas/UpdateNote"},"type":"array","title":"Notes","default":[]},"elevation":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Elevation"},"elevation_accuracy":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Elevation Accuracy"},"elevation_method":{"anyOf":[{"$ref":"#/components/schemas/elevation_method"},{"type":"null"}]},"coordinate_accuracy":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Coordinate Accuracy"},"coordinate_method":{"anyOf":[{"$ref":"#/components/schemas/coordinate_method"},{"type":"null"}]}},"type":"object","title":"UpdateLocation","description":"Schema for updating a location. Notes are managed via the polymorphic Notes table."},"UpdateNote":{"properties":{"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"note_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note Type"},"content":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Content"}},"type":"object","title":"UpdateNote","description":"Schema for updating an existing Note. All fields are optional"},"UpdatePhone":{"properties":{"phone_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Phone Number"},"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"contact_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Contact Id"},"phone_type":{"anyOf":[{"$ref":"#/components/schemas/phone_type"},{"type":"null"}]}},"type":"object","title":"UpdatePhone","description":"Schema for updating phone information."},"UpdateSample":{"properties":{"sample_date":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Sample Date"},"depth_top":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Depth Top"},"depth_bottom":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Depth Bottom"},"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"field_activity_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Field Activity Id"},"field_event_participant_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Field Event Participant Id"},"sample_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sample Name"},"sample_matrix":{"anyOf":[{"$ref":"#/components/schemas/sample_matrix"},{"type":"null"}]},"sample_method":{"anyOf":[{"$ref":"#/components/schemas/sample_method"},{"type":"null"}]},"qc_type":{"anyOf":[{"$ref":"#/components/schemas/qc_type"},{"type":"null"}]},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","title":"UpdateSample"},"UpdateSensor":{"properties":{"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"sensor_type":{"anyOf":[{"$ref":"#/components/schemas/sensor_type"},{"type":"null"}]},"model":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Model"},"serial_no":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Serial No"},"pcn_number":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Pcn Number"},"owner_agency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Owner Agency"},"sensor_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sensor Status"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"}},"type":"object","title":"UpdateSensor"},"UpdateSpring":{"properties":{"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"first_visit_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"First Visit Date"},"spring_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Spring Type"}},"type":"object","title":"UpdateSpring"},"UpdateThingIdLink":{"properties":{"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"alternate_organization":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Alternate Organization"},"alternate_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Alternate Id"},"relation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Relation"}},"type":"object","title":"UpdateThingIdLink"},"UpdateWaterChemistryObservation":{"properties":{"parameter_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Parameter Id"},"observation_datetime":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Observation Datetime"},"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"sample_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sample Id"},"sensor_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sensor Id"},"value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Value"},"unit":{"anyOf":[{"$ref":"#/components/schemas/unit"},{"type":"null"}]}},"type":"object","title":"UpdateWaterChemistryObservation"},"UpdateWell":{"properties":{"well_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Depth"},"hole_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Hole Depth"},"well_casing_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Casing Depth"},"measuring_point_height":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Measuring Point Height"},"well_pump_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Pump Depth"},"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"first_visit_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"First Visit Date"},"well_purposes":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Well Purposes"},"well_construction_notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Construction Notes"},"well_casing_diameter":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Casing Diameter"},"well_casing_materials":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Well Casing Materials"},"nma_formation_zone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Formation Zone"}},"type":"object","title":"UpdateWell"},"UpdateWellScreen":{"properties":{"release_status":{"anyOf":[{"$ref":"#/components/schemas/release_status"},{"type":"null"}]},"aquifer_system_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Aquifer System Id"},"geologic_formation_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Geologic Formation Id"},"screen_depth_bottom":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Screen Depth Bottom"},"screen_depth_top":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Screen Depth Top"},"screen_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Screen Description"},"screen_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Screen Type"}},"type":"object","title":"UpdateWellScreen"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WaterChemistryObservationResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"sample_id":{"type":"integer","title":"Sample Id"},"sensor_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Sensor Id"},"observation_datetime":{"type":"string","title":"Observation Datetime"},"parameter":{"$ref":"#/components/schemas/ParameterResponse"},"value":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Value"},"unit":{"$ref":"#/components/schemas/unit"},"nma_data_quality":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Data Quality"}},"type":"object","required":["id","created_at","release_status","sample_id","sensor_id","observation_datetime","parameter","value","unit"],"title":"WaterChemistryObservationResponse"},"WaterLevelBulkUploadResponse":{"properties":{"summary":{"$ref":"#/components/schemas/WaterLevelBulkUploadSummary"},"water_levels":{"items":{"$ref":"#/components/schemas/WaterLevelBulkUploadRow"},"type":"array","title":"Water Levels"},"validation_errors":{"items":{"type":"string"},"type":"array","title":"Validation Errors"}},"type":"object","required":["summary","water_levels","validation_errors"],"title":"WaterLevelBulkUploadResponse"},"WaterLevelBulkUploadRow":{"properties":{"well_name_point_id":{"type":"string","title":"Well Name Point Id"},"field_event_id":{"type":"integer","title":"Field Event Id"},"field_activity_id":{"type":"integer","title":"Field Activity Id"},"sample_id":{"type":"integer","title":"Sample Id"},"observation_id":{"type":"integer","title":"Observation Id"},"measurement_date_time":{"type":"string","title":"Measurement Date Time"},"level_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Level Status"},"data_quality":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Data Quality"}},"type":"object","required":["well_name_point_id","field_event_id","field_activity_id","sample_id","observation_id","measurement_date_time","level_status","data_quality"],"title":"WaterLevelBulkUploadRow"},"WaterLevelBulkUploadSummary":{"properties":{"total_rows_processed":{"type":"integer","title":"Total Rows Processed"},"total_rows_imported":{"type":"integer","title":"Total Rows Imported"},"validation_errors_or_warnings":{"type":"integer","title":"Validation Errors Or Warnings"}},"type":"object","required":["total_rows_processed","total_rows_imported","validation_errors_or_warnings"],"title":"WaterLevelBulkUploadSummary"},"WellContactSummaryResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Name"},"organization":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Organization"},"role":{"$ref":"#/components/schemas/role"},"contact_type":{"$ref":"#/components/schemas/contact_type"}},"type":"object","required":["id","created_at","release_status","role","contact_type"],"title":"WellContactSummaryResponse"},"WellDetailsFieldActivityResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"field_event_id":{"type":"integer","title":"Field Event Id"},"activity_type":{"$ref":"#/components/schemas/activity_type"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"samples":{"items":{"$ref":"#/components/schemas/WellDetailsFieldEventSampleResponse"},"type":"array","title":"Samples"}},"type":"object","required":["id","created_at","release_status","field_event_id","activity_type"],"title":"WellDetailsFieldActivityResponse"},"WellDetailsFieldEventResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"thing_id":{"type":"integer","title":"Thing Id"},"event_date":{"type":"string","title":"Event Date"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"field_event_participants":{"items":{"$ref":"#/components/schemas/FieldEventParticipantResponse"},"type":"array","title":"Field Event Participants"},"field_activities":{"items":{"$ref":"#/components/schemas/WellDetailsFieldActivityResponse"},"type":"array","title":"Field Activities"}},"type":"object","required":["id","created_at","release_status","thing_id","event_date"],"title":"WellDetailsFieldEventResponse"},"WellDetailsFieldEventSampleResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"contact":{"anyOf":[{"$ref":"#/components/schemas/ContactResponse"},{"type":"null"}]},"sample_date":{"type":"string","title":"Sample Date"},"sample_name":{"type":"string","title":"Sample Name"},"sample_matrix":{"$ref":"#/components/schemas/sample_matrix"},"sample_method":{"$ref":"#/components/schemas/sample_method"},"qc_type":{"$ref":"#/components/schemas/qc_type"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"depth_top":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Depth Top"},"depth_bottom":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Depth Bottom"},"observations":{"items":{"$ref":"#/components/schemas/ObservationResponse"},"type":"array","title":"Observations"}},"type":"object","required":["id","created_at","release_status","sample_date","sample_name","sample_matrix","sample_method","qc_type"],"title":"WellDetailsFieldEventSampleResponse"},"WellDetailsResponse":{"properties":{"well":{"$ref":"#/components/schemas/WellResponse"},"contacts":{"items":{"$ref":"#/components/schemas/ContactResponse"},"type":"array","title":"Contacts"},"sensors":{"items":{"$ref":"#/components/schemas/SensorResponse"},"type":"array","title":"Sensors"},"deployments":{"items":{"$ref":"#/components/schemas/DeploymentResponse"},"type":"array","title":"Deployments"},"well_screens":{"items":{"$ref":"#/components/schemas/WellScreenBaseResponse"},"type":"array","title":"Well Screens"},"field_events":{"items":{"$ref":"#/components/schemas/WellDetailsFieldEventResponse"},"type":"array","title":"Field Events"},"first_field_event":{"anyOf":[{"$ref":"#/components/schemas/WellDetailsFieldEventResponse"},{"type":"null"}]}},"type":"object","required":["well"],"title":"WellDetailsResponse"},"WellExportResponse":{"properties":{"well":{"$ref":"#/components/schemas/WellResponse"},"contacts":{"items":{"$ref":"#/components/schemas/ContactResponse"},"type":"array","title":"Contacts"},"sensors":{"items":{"$ref":"#/components/schemas/SensorResponse"},"type":"array","title":"Sensors"},"deployments":{"items":{"$ref":"#/components/schemas/DeploymentResponse"},"type":"array","title":"Deployments"}},"type":"object","required":["well"],"title":"WellExportResponse"},"WellResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"name":{"type":"string","title":"Name"},"site_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Site Name"},"thing_type":{"type":"string","title":"Thing Type"},"current_location":{"$ref":"#/components/schemas/LocationGeoJSONResponse"},"first_visit_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"First Visit Date"},"groups":{"items":{"$ref":"#/components/schemas/GroupResponse"},"type":"array","title":"Groups","default":[]},"monitoring_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Monitoring Status"},"alternate_ids":{"items":{"$ref":"#/components/schemas/ThingIdLinkResponse"},"type":"array","title":"Alternate Ids","default":[]},"monitoring_frequencies":{"items":{"$ref":"#/components/schemas/MonitoringFrequencyResponse"},"type":"array","title":"Monitoring Frequencies","default":[]},"general_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"General Notes","default":[]},"sampling_procedure_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Sampling Procedure Notes","default":[]},"site_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Site Notes","default":[]},"well_purposes":{"items":{"$ref":"#/components/schemas/well_purpose"},"type":"array","title":"Well Purposes","default":[]},"well_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Depth"},"well_depth_unit":{"type":"string","title":"Well Depth Unit","default":"ft"},"well_depth_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Depth Source"},"historic_depth_to_water":{"items":{"type":"string"},"type":"array","title":"Historic Depth To Water","default":[]},"hole_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Hole Depth"},"hole_depth_unit":{"type":"string","title":"Hole Depth Unit","default":"ft"},"well_casing_diameter":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Casing Diameter"},"well_casing_diameter_unit":{"type":"string","title":"Well Casing Diameter Unit","default":"in"},"well_casing_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Casing Depth"},"well_casing_depth_unit":{"type":"string","title":"Well Casing Depth Unit","default":"ft"},"well_casing_materials":{"items":{"$ref":"#/components/schemas/casing_material"},"type":"array","title":"Well Casing Materials","default":[]},"well_completion_date":{"anyOf":[{"type":"string","format":"date"},{"type":"null"}],"title":"Well Completion Date"},"well_completion_date_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Completion Date Source"},"well_driller_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Driller Name"},"well_construction_method":{"anyOf":[{"$ref":"#/components/schemas/well_construction_method"},{"type":"null"}]},"well_construction_method_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Construction Method Source"},"well_pump_type":{"anyOf":[{"$ref":"#/components/schemas/well_pump_type"},{"type":"null"}]},"well_pump_depth":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Well Pump Depth"},"well_pump_depth_unit":{"type":"string","title":"Well Pump Depth Unit","default":"ft"},"well_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Well Status"},"open_status":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Open Status"},"datalogger_suitability_status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Datalogger Suitability Status"},"measuring_point_height":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Measuring Point Height"},"measuring_point_height_unit":{"type":"string","title":"Measuring Point Height Unit","default":"ft"},"measuring_point_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Measuring Point Description"},"aquifers":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Aquifers","default":[]},"water_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Water Notes","default":[]},"construction_notes":{"items":{"$ref":"#/components/schemas/NoteResponse"},"type":"array","title":"Construction Notes","default":[]},"contacts":{"items":{"$ref":"#/components/schemas/WellContactSummaryResponse"},"type":"array","title":"Contacts","default":[]},"permissions":{"items":{"$ref":"#/components/schemas/PermissionHistoryResponse"},"type":"array","title":"Permissions"},"formation_completion_code":{"anyOf":[{"$ref":"#/components/schemas/formation_code"},{"type":"null"}]},"nma_formation_zone":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Nma Formation Zone"},"well_location_note":{"items":{"type":"string"},"type":"array","title":"Well Location Note","default":[]}},"type":"object","required":["id","created_at","release_status","name","thing_type","current_location","first_visit_date","monitoring_status","well_depth_source","well_completion_date","well_completion_date_source","well_driller_name","well_construction_method","well_construction_method_source","well_pump_type","well_pump_depth","well_status","open_status","datalogger_suitability_status","measuring_point_height","measuring_point_description","permissions","formation_completion_code","nma_formation_zone"],"title":"WellResponse","description":"Response schema for well details."},"WellScreenBaseResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"thing_id":{"type":"integer","title":"Thing Id"},"aquifer_system_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Aquifer System Id"},"aquifer_system":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Aquifer System"},"aquifer_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Aquifer Type"},"geologic_formation_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Geologic Formation Id"},"geologic_formation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Geologic Formation"},"screen_depth_bottom":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Screen Depth Bottom"},"screen_depth_bottom_unit":{"type":"string","title":"Screen Depth Bottom Unit","default":"ft"},"screen_depth_top":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Screen Depth Top"},"screen_depth_top_unit":{"type":"string","title":"Screen Depth Top Unit","default":"ft"},"screen_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Screen Type"},"screen_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Screen Description"}},"type":"object","required":["id","created_at","release_status","thing_id"],"title":"WellScreenBaseResponse"},"WellScreenResponse":{"properties":{"id":{"type":"integer","title":"Id"},"created_at":{"type":"string","title":"Created At"},"release_status":{"$ref":"#/components/schemas/release_status"},"thing_id":{"type":"integer","title":"Thing Id"},"aquifer_system_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Aquifer System Id"},"aquifer_system":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Aquifer System"},"aquifer_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Aquifer Type"},"geologic_formation_id":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Geologic Formation Id"},"geologic_formation":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Geologic Formation"},"screen_depth_bottom":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Screen Depth Bottom"},"screen_depth_bottom_unit":{"type":"string","title":"Screen Depth Bottom Unit","default":"ft"},"screen_depth_top":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Screen Depth Top"},"screen_depth_top_unit":{"type":"string","title":"Screen Depth Top Unit","default":"ft"},"screen_type":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Screen Type"},"screen_description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Screen Description"},"thing":{"$ref":"#/components/schemas/WellResponse"}},"type":"object","required":["id","created_at","release_status","thing_id","thing"],"title":"WellScreenResponse","description":"Response schema for well screen details."},"activity_type":{"type":"string","enum":["well inventory","groundwater level","water chemistry"],"title":"activity_type"},"address_type":{"type":"string","enum":["Primary","Work","Personal","Mailing","Physical"],"title":"address_type"},"casing_material":{"type":"string","enum":["PVC","Steel","Concrete"],"title":"casing_material"},"contact_type":{"type":"string","enum":["Primary","Secondary","Field Event Participant"],"title":"contact_type"},"coordinate_method":{"type":"string","enum":["Unknown","Differentially corrected GPS","Survey-grade global positioning system (SGPS)","GPS, uncorrected","Interpolated from map","Interpolated from DEM","Reported","Transit, theodolite, or other survey method"],"title":"coordinate_method"},"elevation_method":{"type":"string","enum":["Altimeter","Differentially corrected GPS","Survey-grade GPS","Global positioning system (GPS)","LiDAR DEM","Level or other survey method","Interpolated from topographic map","Interpolated from digital elevation model (DEM)","Reported","Survey-grade Global Navigation Satellite Sys, Lvl1","USGS National Elevation Dataset (NED)","Unknown"],"title":"elevation_method"},"email_type":{"type":"string","enum":["Primary","Work","Personal"],"title":"email_type"},"formation_code":{"type":"string","enum":["000EXRV","000IRSV","050QUAL","100QBAS","110ALVM","110AVMB","110BLSN","110NTGU","110PTODC","111MCCR","112ANCH","112CURB","112LAMA","112LAMAb","112LGUN","112QTBF","112QTBFlac","112QTBFpd","112QTBFppm","112SNTF","112SNTFA","112SNTFOB","112SNTFP","112TRTO","120DTIL","120ELRT","120IRSV","120SBLC","120SRVB","120SRVBf","120TSBV_Lower","120TSBV_Upper","121CHMT","121CHMTv","121CHMTvs","121OGLL","121PUYEF","121TSUQ","121TSUQa","121TSUQacu","121TSUQacuf","121TSUQaml","121TSUQb","121TSUQbfl","121TSUQbfm","121TSUQbp","121TSUQce","121TSUQe","121TSUQs","121TSUQsa","121TSUQsc","121TSUQsf","122CHOC","122CRTO","122OJOC","122PICR","122PPTS","122SNTFP","123DTILSPRS","123DTMGandbas","123DTMGign","123DTMGrhydac","123ESPN","123GLST","123PICS","123PICSc","123PICSl","123SPRSDTMGlava","123SPRSlower","123SPRSmid_uppe","124BACA","124CBMN","124LLVS","124PSCN","124RGIN","124SNJS","124TPCS","125NCMN","125NCMNS","125RTON","130CALDFLOOR","180TKSCC_Upper","180TKTR","210CRCS","210GLUPC_Lower","210HOSTD","210MCDK","210MNCS","210MNCSL","210MNCSU","211CLFHV","211CRLL","211CRVC","211DKOT","211DLCO","211DLTN","211FRHS","211FRLD","211FRMG","211GBSNC","211GLLG","211GLLP","211GRRG","211GRRS","211HOST","211KRLD","211LWIS","211MENF","211MENFU","211MVRD","211OJAM","211PCCF","211PIRR","211PNLK","211SMKH","211TLLS","212KTRP","217PRGR","220ENRD","220JURC","220NAVJ","221BLFF","221CSPG","221ERADU","221MRSN","221MRSN/BBSN","221MRSN/JCKP","221MRSN/RCAP","221MRSN/WWCN","221SLWS","221SMVL","221TDLT","221WSRC","221ZUNIS","231AGZC","231AGZCU","231CHNL","231CORR","231DCKM","231PFDF","231PFDFL","231PFDFM","231PFDFU","231RCKP","231SNRS","231SNSL","231SRMP","231WNGT","260SNAN","260SNAN_lower","261SNGL","300YESO","300YESO_lower","300YESO_upper","310ABO","310DCLL","310GLOR","310MBLC","310TRRS","310YESO","310YESOG","312CSTL","312RSLR","313ARTS","313BLCN","313BRUC","313CKBF","313CLBD","313CPTN","313GDLP","313GOSP","313SADG","313SADR","313TNSL","313YATS","315LABR","315YESOABO","318ABO","318BSPG","318JOYT","318YESO","319BRSM","320HLDR","320PENN","320SNDI","321SGDC","322BEMN","325GBLR","325MDER","325MDERL","325MDERU","325SAND","326MGDL","340EPRS","350PZBA","350PZBB","400EMBD","400PCMB","400PREC","400PRECintr","400PRST","400TUSS","410PRCG","410PRCGf","410PRCQ","410PRCQf","121GILA","312DYLK","120WMVL","313GRBG","318ABOL","318ABOU","112SNTFU","310FRNR","312OCHO","313AZOT","313QUEN","319HUCO","313SVRV","313CABD","320GRMS","211CLRDH","120BRLM","122RUBO","313SADRL","313SADRU","313BRNL","318CPDR","121BDHC","313SADY","221SRFLL","221BLUF","221COSP","317ABYS","221BRSB","310SYDR","400SDVL","221SRFL","310SGRC","231TCVS","211DCRS","211ALSN","211LVNN","211MORD","210PRMD","124ANMS","211NBRR","111ALVM","122SNTFL","111CPLN","120CRSN","111CRMS","111CRMSA","111SPOL","110TURT","221RCPR","320BLNG","112ANCHsr","121TSUQae","230TRSC","122TSUQdx","123PICSu","123PICSm","123PICSmc","120VBVC","120VCSS","124DMDT","325ALMT","400SAND","318VCPK","318BSVP","100ALVM","310PRMN","110AVPS","313CRCX","112SLBL","112SBCRC","313CRDM","112SBDM","120BLSN","112SBCR","112HCBL","120IVIG","112RLBL","112EFBL","112GRBL","123SAND","210MRNH","320ALMT","313DLRM","300PLZC","122SPRS","110AVTV","313DMBS","120ERSV"],"title":"formation_code"},"group_type":{"type":"string","enum":["Monitoring Plan","Geographic Area","Historical"],"title":"group_type"},"monitoring_frequency":{"type":"string","enum":["Monthly","Bimonthly","Bimonthly reported","Quarterly","Biannual","Annual","Decadal","Event-based"],"title":"monitoring_frequency"},"note_type":{"type":"string","enum":["Access","Directions","Communication","Construction","Maintenance","Historical","General","Water","Water Quality","Sampling Procedure","Coordinate","OwnerComment","Site Notes (legacy)"],"title":"note_type"},"organization":{"type":"string","enum":["Unknown","City of Aztec","Daybreak Investments","Vallecitos HOA","SFC, Santa Fe Animal Shelter","El Guicu Ditch Association","Santa Fe Municipal Airport","Uluru Development","AllSup's Convenience Stores","Santa Fe Downs Resort","City of Truth or Consequences, WWTP","Riverbend Hotsprings","Armendaris Ranch","El Paso Water","BLM, Socorro Field Office","USFWS","Sile MDWCA","Pena Blanca Water & Sanitation District","Town of Questa","Town of Cerro","Cerro MDWCA","Farr Cattle Company","Carrizozo Orchard","White Oaks Pottery","USFS, Kiowa Grasslands","Cloud Country West Subdivision","Chama West WUA","El Rito Regional Water and Waste Water Association","El Rito MDWCA","West Rim MDWUA","Village of Willard","Quemado Municipal Water & SWA","Coyote Creek MDWUA","Lamy MDWCA","La Joya CWDA","NM Firefighters Training Academy","Cebolleta Land Grant","Madrid Water Co-op","Sun Valley Water and Sanitation","Bluewater Lake MDWCA","Bluewater Acres Domestic WUA","Lybrook MDWCA","New Mexico Museum of Natural History","Hillsboro MDWCA","Tyrone MDWCA","Santa Clara Water System","Casas Adobes MDWCA","Lake Roberts WUA","El Creston MDWCA","Reserve Municipality Water Works","Town of Estancia","Pie Town MDWCA","Roosevelt SWCD","Otis MDWCA","White Cliffs MDWUA","Vista Linda Water Co-op","Anasazi Trails Water Co-op","Canon MDWCA","Placitas Trails Water Co-op","BLM, Roswell Office","Forked Lightning Ranch","Cottonwood RWA","Pinon Ridge WUA","McSherry Farms","Agua Sana WUA","Chamita MDWCA","W Spear-bar Ranch","Village of Capitan","Brazos MDWCA","Alto Alps HOA","Chiricahua Desert Museum","Bike Ranch","Hachita MDWCA","Carrizozo Municipal Water","Dunhill Ranch","Santa Fe Conservation Trust","NMSU","USGS","TWDB","NMED","NMOSE","NMBGMR","Bernalillo County","BLM","BLM Taos Office","SFC","SFC, Fire Facilities","SFC, Utilities Dept.","SFC, Valle Vista Water Utility, Inc.","City of Santa Fe","City of Santa Fe WWTP","City of Santa Fe, Municipal Recreation Complex","City of Santa Fe, Sangre de Cristo Water Co.","NMISC","PVACD","Bayard","SNL","USFS","NMT","NPS","NMRWA","NMDOT","Taos SWCD","Otero SWCD","Northeastern SWCD","CDWR","Pendaries Village","A&T Pump & Well Service, LLC","A. G. Wassenaar, Inc","AMEC","Balleau Groundwater, Inc","CDM Smith","CH2M Hill","Corbin Consulting, Inc","Chevron","Daniel B. Stephens & Associates, Inc","EnecoTech","Faith Engineering, Inc","Foster Well Service, Inc","Glorieta Geoscience, Inc","Golder Associates, Inc","Hathorn's Well Service, Inc","Hydroscience Associates, Inc","IC Tech, Inc","John Shomaker & Associates, Inc","Kuckleman Pump Service","Los Golondrinas","Minton Engineers","MJDarrconsult, Inc","Puerta del Canon Ranch","Rodgers & Company, Inc","San Pedro Creek Estates HOA","Statewide Drilling, Inc","Tec Drilling Limited","Tetra Tech, Inc","Thompson Drilling, Inc","Witcher & Associates","Zeigler Geologic Consulting, LLC","Sandia Well Service, Inc","San Marcos Association","URS","Vista del Oro","Abeyta Engineering, Inc","Adobe Ranch","Agua Fria Community Water Association","Apache Gap Ranch","Aspendale Mountain Retreat","Augustin Plains Ranch LLC","B & B Cattle Co","Berridge Distributing Company","Bishop's Lodge","Bonanza Creek Ranch","Bug Scuffle Water Association","Wehinahpay Mountain Camp","Campbell Ranch","Capitol Ford Santa Fe","Cemex, Inc","Cerro Community Center","Santa Fe Jewish Center","Chupadero MDWCA","Cielo Lumbre HOA","Circle Cross Ranch","City of Alamogordo","City of Portales, Public Works Dept.","City of Socorro","Commonwealth Conservancy","Costilla MDWCA","Country Club Garden Mobile Home Park","Crossroads Cattle Co., Ltd","Double H Ranch","E.A. Meadows East","El Camino Realty, Inc","Eldorado Area Water & Sanitation District","Bourbon Grill at El Gancho","El Prado HOA","El Rancho de las Golondrinas","El Rito Canyon MDWCA","Encantado Enterprises","Estrella Concepts LLC","Sixteen Springs Fire Department","Fire Water Lodge","Ford County Land & Cattle Company, Inc","Friendly Construction, Inc","Hacienda Del Cerezo","Hefker Vega Ranch","High Nogal Ranch","Holloman Air Force Base","Hyde Park Estates MDWCA","Desert Village RV & Mobile Home Park","K. Schmitt Trust","La Cienega MDWCA","La Vista HOA","Land Ventures LLC","Las Lagunitas","Las Lagunitas HOA","Lightning Dock Zanskar","Living World Ministries","Los Atrevidos, Inc","Los Prados HOA","Malaga MDWCA & SWA","Mangas Outfitters","Medina Gravel Pit","Mendenhall Trading Co","Mesa Verde Ranch","NMDGF","NMSU College of Agriculture","Naiche Development","NRAO","NMSA","Nogal MDWCA","O Bar O Ranch","OMI Wastewater Treatment Plant","Old Road Ranch Pardners Ltd","PNM Service Center","Peace Tabernacle Church","Pecos Trail Inn","Pelican Spa","Pistachio Tree Ranch","Rancho Encantado","Rancho San Lucas","Rancho San Marcos","Rancho Viejo Partnership","Ranney Ranch","Rio En Medio MDWCA","San Acacia MDWCA","San Juan Residences","Sangre de Cristo Estates","Santa Fe Community College","Sangre de Cristo Center","Santa Fe Horse Park","Santa Fe Opera","Santa Fe Waldorf School","Shidoni Foundry and Gallery","Sierra Grande Lodge","Sierra Vista Retirement Community","Slash Triangle Ranch","Spanish Stirrup Rockshop","Sparrowhawk Farm","Stagecoach Motel","State of New Mexico","Stephenson Ranch","Sun Broadcasting Network","Tano Rd LLC","UNM-Taos","Tee Pee Ranch/Tee Pee Subdivision","Tent Rock, Inc","Tesuque MDWCA","The Great Cloud Zen Center","Three Rivers Ranch","Timberon Water and Sanitation District","Town of Magdalena","Town of Taos","Town of Taos, National Guard Armory","Trinity Ranch","Tularosa Basin National Desalination Research Facility","Turquoise Trail Charter School","US Bureau of Indian Affairs, Santa Fe Indian School","USFS, Carson NF, Taos Office","USFS, Cibola NF, Magdalena Ranger District","USFS, Cibola NF, Supervisor's Office","USFS, Santa Fe NF, Espanola Ranger District","Ute Mountain Farms","VA Hospital","Velte","Vereda Serena Property","Village of Corona","Village of Floyd","Village of Melrose","Village of Vaughn","Vista Land Company","Vista Redonda MDWCA","Vista de Oro de Placitas Water Users Coop","Walker Ranch","Wild & Woolley Trailer Ranch","Winter Brothers","Yates Petroleum Corporation","Zamora Accounting Services","Agua Sana MWCD","Canada Los Alamos MDWCA","Canjilon Mutual Domestic Water System","Cebolla Mutual Domestic","Chihuahuan Desert Rangeland Research Center (CDRRC)","East Rio Arriba SWCD","El Prado Municipal Water","Hachita Mutual Domestic","Jornada Experimental Range (JER)","La Canada Way HOA","Los Ojos Mutual Domestic","The Nature Conservancy (TNC)","Smith Ranch LLC","Santa Ana Pueblo Department of Natural Resources","Village of Hope","WSP","Zia Pueblo","Our Lady of Guadalupe (OLG)","PLSS"],"title":"organization"},"origin_type":{"type":"string","enum":["Reported by another agency","From driller's log or well report","Private geologist, consultant or univ associate","Interpreted fr geophys logs by source agency","Memory of owner, operator, driller","Measured by source agency","Reported by owner of well","Reported by person other than driller owner agency","Measured by NMBGMR staff","Other","Data Portal"],"title":"origin_type"},"parameter_name":{"type":"string","enum":["groundwater level","temperature","pH","Alkalinity, Total","Alkalinity as CaCO3","Alkalinity as OH-","Calcium","Calcium, total, unfiltered","Chloride","Carbonate","Conductivity, laboratory","Bicarbonate","Hardness (CaCO3)","Ion Balance","Potassium","Potassium, total, unfiltered","Magnesium","Magnesium, total, unfiltered","Sodium","Sodium, total, unfiltered","Sodium and Potassium combined","Sulfate","Total Anions","Total Cations","Total Dissolved Solids","Tritium","Age of Water using dissolved gases","Silver","Silver, total, unfiltered","Aluminum","Aluminum, total, unfiltered","Arsenic","Arsenic, total, unfiltered","Boron","Boron, total, unfiltered","Barium","Barium, total, unfiltered","Beryllium","Beryllium, total, unfiltered","Bromide","13C:12C ratio","14C content, pmc","Uncorrected C14 age","Cadmium","Cadmium, total, unfiltered","Chlorofluorocarbon-11 avg age","Chlorofluorocarbon-113 avg age","Chlorofluorocarbon-113/12 avg RATIO age","Chlorofluorocarbon-12 avg age","Cobalt","Cobalt, total, unfiltered","Chromium","Chromium, total, unfiltered","Copper","Copper, total, unfiltered","delta O18 sulfate","Sulfate 34 isotope ratio","Fluoride","Iron","Iron, total, unfiltered","Deuterium:Hydrogen ratio","Mercury","Mercury, total, unfiltered","Lithium","Lithium, total, unfiltered","Manganese","Manganese, total, unfiltered","Molybdenum","Molybdenum, total, unfiltered","Nickel","Nickel, total, unfiltered","Nitrite (as NO2)","Nitrite (as N)","Nitrate (as NO3)","Nitrate (as N)","18O:16O ratio","Lead","Lead, total, unfiltered","Phosphate","Antimony","Antimony, total, unfiltered","Selenium","Selenium, total, unfiltered","Sulfur hexafluoride","Silicon","Silicon, total, unfiltered","Silica","Tin","Tin, total, unfiltered","Strontium","Strontium, total, unfiltered","Strontium 87:86 ratio","Thorium","Thorium, total, unfiltered","Titanium","Titanium, total, unfiltered","Thallium","Thallium, total, unfiltered","Uranium (total, by ICP-MS)","Uranium, total, unfiltered","Vanadium","Vanadium, total, unfiltered","Zinc","Zinc, total, unfiltered","Corrected C14 in years","Arsenite (arsenic species)","Arsenate (arsenic species)","Cyanide","Estimated recharge temperature","Hydrogen sulfide","Ammonia","Ammonium","Total nitrogen","Total Kjeldahl nitrogen","Dissolved organic carbon","Total organic carbon","delta C13 of dissolved inorganic carbon"],"title":"parameter_name"},"parameter_type":{"type":"string","enum":["Field Parameter","Metal","Radionuclide","Major Element","Minor Element","Physical property"],"title":"parameter_type"},"permission_type":{"type":"string","enum":["Water Level Sample","Water Chemistry Sample","Datalogger Installation"],"title":"permission_type"},"phone_type":{"type":"string","enum":["Primary","Work","Home","Mobile"],"title":"phone_type"},"publication_type":{"type":"string","enum":["Map","Report","Dataset","Model","Software","Paper","Thesis","Book","Conference","Webpage"],"title":"publication_type"},"qc_type":{"type":"string","enum":["Normal","Duplicate","Split","Field Blank","Trip Blank","Equipment Blank"],"title":"qc_type"},"release_status":{"type":"string","enum":["draft","provisional","final","published","archived","public","private"],"title":"release_status"},"review_status":{"type":"string","enum":["approved","not reviewed"],"title":"review_status"},"role":{"type":"string","enum":["Unknown","Principal Investigator","Owner","Manager","Operator","Driller","Geologist","Hydrologist","Hydrogeologist","Engineer","Organization","Specialist","Technician","Research Assistant","Research Scientist","Graduate Student","Biologist","Lab Manager","Publications Manager","Software Developer"],"title":"role"},"sample_matrix":{"type":"string","enum":["water","groundwater","soil"],"title":"sample_matrix"},"sample_method":{"type":"string","enum":["Unknown","Airline measurement","Analog or graphic recorder","Calibrated airline measurement","Differential GPS; especially applicable to surface expression of ground water","Estimated","Transducer","Pressure-gage measurement","Calibrated pressure-gage measurement","Interpreted from geophysical logs","Manometer","Non-recording gage","Observed (required for F, N, and W water level status)","Sonic water level meter (acoustic pulse)","Reported, method not known","Steel-tape measurement","Electric tape measurement (E-probe)","Unknown (for legacy data only; not for new data entry)","Calibrated electric tape; accuracy of equipment has been checked","Calibrated electric cable","Uncalibrated electric cable","Continuous acoustic sounder","Measurement not attempted","null placeholder","bailer","faucet at well head","faucet or outlet at house","grab sample","pump","thief sampler"],"title":"sample_method"},"schemas__location__GeoJSONGeometry":{"properties":{"type":{"type":"string","title":"Type","default":"Point"},"coordinates":{"items":{},"type":"array","maxItems":3,"minItems":3,"title":"Coordinates","description":"Coordinates in [longitude, latitude, elevation] format"}},"type":"object","required":["coordinates"],"title":"GeoJSONGeometry"},"schemas__thing__GeoJSONGeometry":{"properties":{"type":{"type":"string","title":"Type"},"coordinates":{"anyOf":[{"items":{"type":"number"},"type":"array"},{"items":{"items":{"type":"number"},"type":"array"},"type":"array"},{"items":{"items":{"items":{"type":"number"},"type":"array"},"type":"array"},"type":"array"},{"items":{"items":{"items":{"items":{"type":"number"},"type":"array"},"type":"array"},"type":"array"},"type":"array"}],"title":"Coordinates"}},"type":"object","required":["type","coordinates"],"title":"GeoJSONGeometry","description":"Geometry schema for GeoJSON response."},"screen_type":{"type":"string","enum":["PVC","Steel","Concrete"],"title":"screen_type"},"sensor_type":{"type":"string","enum":["DiverLink","Diver Cable","Pressure Transducer","Data Logger","Barometer","Acoustic Sounder","Precip Collector","Camera","Soil Moisture Sensor","Tipping Bucket","Weather Station","Weir","Snow Lysimeter","Lysimeter"],"title":"sensor_type"},"spring_type":{"type":"string","enum":["Artesian","Ephemeral","Perennial","Thermal","Mineral"],"title":"spring_type"},"unit":{"type":"string","enum":["dimensionless","ft","ftbgs","F","mg/L","mW/m\u00b2","W/m\u00b2","W/m\u00b7K","m\u00b2/s","deg C","deg second","deg minute","second","minute","hour","m"],"title":"unit"},"well_construction_method":{"type":"string","enum":["Unknown","Air-Rotary","Bored or augered","Cable-tool","Hydraulic rotary (mud or water)","Air percussion","Reverse rotary","Driven","Other (explain in notes)"],"title":"well_construction_method"},"well_pump_type":{"type":"string","enum":["Submersible","Jet","Line Shaft","Hand","Windmill"],"title":"well_pump_type"},"well_purpose":{"type":"string","enum":["Unknown","Open, unequipped well","Commercial","Domestic","Power generation","Irrigation","Livestock","Mining","Industrial","Observation","Public supply","Shared domestic","Institutional","Unused","Exploration","Monitoring","Production","Injection"],"title":"well_purpose"}},"securitySchemes":{"OAuth2AuthorizationCodeBearer":{"type":"oauth2","flows":{"authorizationCode":{"scopes":{"openid":"openid","offline_access":"offline_access"},"authorizationUrl":"https://authentik.newmexicowaterdata.org/application/o/authorize/","tokenUrl":"https://authentik.newmexicowaterdata.org/application/o/token/"}}}}}} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b6cf175f..de8833d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ocotillo-ui", - "version": "1.0.1", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ocotillo-ui", - "version": "1.0.1", + "version": "1.1.0", "dependencies": { "@base-ui-components/react": "^1.0.0-alpha.6", "@casl/ability": "^6.7.3", @@ -16,9 +16,9 @@ "@fontsource-variable/outfit": "^5.2.8", "@fontsource-variable/public-sans": "^5.2.7", "@glideapps/glide-data-grid": "^6.0.3", + "@glideapps/glide-data-grid-cells": "^6.0.3", "@hookform/resolvers": "^5.2.2", "@mapbox/mapbox-gl-draw": "^1.4.3", - "@mapbox/mapbox-gl-geocoder": "^5.0.3", "@mui/icons-material": "^6.4.7", "@mui/lab": "^6.0.0-beta.14", "@mui/material": "^6.4.6", @@ -41,6 +41,7 @@ "@tiptap/react": "^2.9.1", "@tiptap/starter-kit": "^2.9.1", "@turf/turf": "^7.2.0", + "@types/papaparse": "^5.5.2", "axios-auth-refresh": "^3.3.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -53,10 +54,10 @@ "jwt-decode": "^4.0.0", "lodash": "^4.18.1", "lucide-react": "^1.17.0", - "mapbox-gl": "^3.0.0", - "mapbox-gl-style-switcher": "^1.0.11", + "maplibre-gl": "^4.7.1", "marked": "^4.3.0", "pako": "^2.1.0", + "papaparse": "^5.5.4", "posthog-js": "^1.402.2", "proj4": "^2.15.0", "radix-ui": "^1.4.3", @@ -78,7 +79,7 @@ "zod": "^4.1.8" }, "devDependencies": { - "@eslint/js": "^9.39.4", + "@biomejs/biome": "^2.5.5", "@hey-api/openapi-ts": "^0.85.2", "@sentry/vite-plugin": "^3.4.0", "@stoplight/prism-cli": "^5.14.2", @@ -100,20 +101,11 @@ "@vitest/ui": "^3.2.4", "autoprefixer": "^10.5.0", "cypress": "^15.0.0", - "eslint": "^9.39.4", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^7.1.1", - "eslint-plugin-react-refresh": "^0.5.2", - "globals": "^17.6.0", - "jiti": "^2.7.0", "jsdom": "^26.1.0", "pdfjs-dist": "^5.7.284", "postcss": "^8.5.15", - "prettier": "3.5.3", "tailwindcss": "^4.3.0", "typescript": "^5.4.2", - "typescript-eslint": "^8.61.0", "vite": "^6.2.2", "vite-tsconfig-paths": "^5.0.1", "vitest": "^3.2.4" @@ -920,6 +912,169 @@ "node": ">=18" } }, + "node_modules/@biomejs/biome": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.5.tgz", + "integrity": "sha512-r1S8nFsAG1MY+vJFZALzIvwXAJv6ejDQ0mxP21Tgr9YK3ZFtjrvbBwDdNhx1rUqvccEIeNg20cYCNzl6Cr69pQ==", + "dev": true, + "license": "MIT OR Apache-2.0", + "bin": { + "biome": "bin/biome" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/biome" + }, + "optionalDependencies": { + "@biomejs/cli-darwin-arm64": "2.5.5", + "@biomejs/cli-darwin-x64": "2.5.5", + "@biomejs/cli-linux-arm64": "2.5.5", + "@biomejs/cli-linux-arm64-musl": "2.5.5", + "@biomejs/cli-linux-x64": "2.5.5", + "@biomejs/cli-linux-x64-musl": "2.5.5", + "@biomejs/cli-win32-arm64": "2.5.5", + "@biomejs/cli-win32-x64": "2.5.5" + } + }, + "node_modules/@biomejs/cli-darwin-arm64": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.5.tgz", + "integrity": "sha512-kUrAhXVWUrwmAUnV2iXSK7umxKFysTwvqK+Ty6ptUcLY/7T3SnCAjUowE4uvwaEej6nXZ7hu/dTtbokKdsPeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-darwin-x64": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.5.tgz", + "integrity": "sha512-DamiYc5bUYZ2uxlfc+RLEPtz1Abb6PO5eTbOkufLpSGwd/7AMQAdxhFYiXmwwkJL8IsT8S7GvdgwDHqaMFAvKw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.5.tgz", + "integrity": "sha512-lRKF/pH/1RiYiBKExi3TCZVAtvzEm77aifrvcNiDFrR9WxeAnDUjDnseb6y2XV85mjitLs6SILGm2XG77cHtSQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-arm64-musl": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.5.tgz", + "integrity": "sha512-U4WMl/sy/E/Q73vf15VspakLRRs2LDFcCeBxJnQfXzssb88zpV6PJPaQ3ezhQ7H6Ht2/8bvuZeHgJWzmoxllZg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.5.tgz", + "integrity": "sha512-H/O39nJEw/2Zm/fm7hrmxxoF8kK/aU1uCoPp70ruXVbomaAdLpJJnCmL11Q2JotT8QVHH06So04Oq53lCSwSwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-linux-x64-musl": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.5.tgz", + "integrity": "sha512-m7wC7tjX5Lrmo69dc4md8FeKpPU1NTCY1v7xUoQQ2vadWwNnBS0KZOG8471otFPHrTHihQJAjQPgMObpLvDe6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-arm64": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.5.tgz", + "integrity": "sha512-7BryINPuYypLUAH3o/o5ZdgomJ4zn3EDR0ChZJst7n32S6ZhKbgHXuYydLu+YAnx59ehGFR0z/MG6qnzQi3Yyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, + "node_modules/@biomejs/cli-win32-x64": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.5.tgz", + "integrity": "sha512-bIBFo+n6MIxdNcVFy5CrurbKiZQiUciK3bt8+O9I4wjFZNTfXLpi+giq47522eXqW5NBc9ulx7dR1SlZKi2J5g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT OR Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=14.21.3" + } + }, "node_modules/@casl/ability": { "version": "6.8.0", "resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.8.0.tgz", @@ -1775,235 +1930,6 @@ "node": ">=18" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.21.2", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", - "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^2.1.7", - "debug": "^4.3.1", - "minimatch": "^3.1.5" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/config-array/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", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/config-array/node_modules/minimatch": { - "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" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", - "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.14.0", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.5", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/@eslint/eslintrc/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", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "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" - }, - "engines": { - "node": "*" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", - "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - } - }, - "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^0.17.0", - "levn": "^0.4.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, "node_modules/@faker-js/faker": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-6.3.1.tgz", @@ -2100,23 +2026,83 @@ "react-responsive-carousel": "^3.2.7" } }, - "node_modules/@hey-api/codegen-core": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.2.0.tgz", - "integrity": "sha512-c7VjBy/8ed0EVLNgaeS9Xxams1Tuv/WK/b4xXH3Qr4wjzYeJUtxOcoP8YdwNLavqKP8pGiuctjX2Z1Pwc4jMgQ==", - "dev": true, + "node_modules/@glideapps/glide-data-grid-cells": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@glideapps/glide-data-grid-cells/-/glide-data-grid-cells-6.0.3.tgz", + "integrity": "sha512-SOZ+zlXAqSEACyZ26w41O2y0tr5qhJnxYOu9W327L23+Z2Sz0xpyiP8FQOZx1mzdyXLO9eBqQIv9ByDTftLIIA==", "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=22.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/hey-api" - }, - "peerDependencies": { - "typescript": ">=5.5.3" + "dependencies": { + "@glideapps/glide-data-grid": "6.0.3", + "@linaria/react": "^4.5.3", + "@toast-ui/editor": "3.1.10", + "@toast-ui/react-editor": "3.1.10", + "react-select": "^5.8.0" } }, - "node_modules/@hey-api/json-schema-ref-parser": { + "node_modules/@glideapps/glide-data-grid-cells/node_modules/@toast-ui/react-editor": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/@toast-ui/react-editor/-/react-editor-3.1.10.tgz", + "integrity": "sha512-IY9uEIVKsOcWHuGf4kn08xILudhhNBbp3tGEvhpxpmhWiuFDKuEVBhTk/sUmFR4pytNefcvNi9wjzWU2BcYD+Q==", + "license": "MIT", + "dependencies": { + "@toast-ui/editor": "^3.1.10" + }, + "peerDependencies": { + "react": "^17.0.1" + } + }, + "node_modules/@glideapps/glide-data-grid-cells/node_modules/react": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@glideapps/glide-data-grid-cells/node_modules/react-select": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.2.tgz", + "integrity": "sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@hey-api/codegen-core": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.2.0.tgz", + "integrity": "sha512-c7VjBy/8ed0EVLNgaeS9Xxams1Tuv/WK/b4xXH3Qr4wjzYeJUtxOcoP8YdwNLavqKP8pGiuctjX2Z1Pwc4jMgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=22.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/hey-api" + }, + "peerDependencies": { + "typescript": ">=5.5.3" + } + }, + "node_modules/@hey-api/json-schema-ref-parser": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@hey-api/json-schema-ref-parser/-/json-schema-ref-parser-1.2.0.tgz", "integrity": "sha512-BMnIuhVgNmSudadw1GcTsP18Yk5l8FrYrg/OSYNxz0D2E0vf4D5e4j5nUbuY8MU6p1vp7ev0xrfP6A/NWazkzQ==", @@ -2189,72 +2175,6 @@ "react-hook-form": "^7.55.0" } }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/types": "^0.15.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@inquirer/ansi": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.7.tgz", @@ -2673,16 +2593,6 @@ "node": "^12.16.0 || >=13.7.0" } }, - "node_modules/@mapbox/fusspot": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@mapbox/fusspot/-/fusspot-0.4.0.tgz", - "integrity": "sha512-6sys1vUlhNCqMvJOqPEPSi0jc9tg7aJ//oG1A16H3PXoIt9whtNngD7UzBHUVTH15zunR/vRvMtGNVsogm1KzA==", - "license": "BSD 2-Clause", - "dependencies": { - "is-plain-obj": "^1.1.0", - "xtend": "^4.0.1" - } - }, "node_modules/@mapbox/geojson-area": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/@mapbox/geojson-area/-/geojson-area-0.2.2.tgz", @@ -2714,12 +2624,6 @@ "geojson-rewind": "geojson-rewind" } }, - "node_modules/@mapbox/geojson-types": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@mapbox/geojson-types/-/geojson-types-1.0.2.tgz", - "integrity": "sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw==", - "license": "ISC" - }, "node_modules/@mapbox/jsonlint-lines-primitives": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", @@ -2745,97 +2649,23 @@ "node": "^18.0.0 || >=20.0.0" } }, - "node_modules/@mapbox/mapbox-gl-geocoder": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-geocoder/-/mapbox-gl-geocoder-5.1.2.tgz", - "integrity": "sha512-UjtGKL/bfaUTf4NfDaKCeYIvtMIJi9nr94QQB13p8uPXhSfjo931zhWNAib3YY7xkNqzVEBWzFGuB1IT5w7lGA==", - "license": "ISC", - "dependencies": { - "@mapbox/mapbox-sdk": "^0.16.1", - "events": "^3.3.0", - "lodash.debounce": "^4.0.6", - "nanoid": "^3.1.31", - "subtag": "^0.5.0", - "suggestions": "^1.6.0", - "xtend": "^4.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@mapbox/mapbox-gl-geocoder/node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, "node_modules/@mapbox/mapbox-gl-supported": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-3.0.0.tgz", "integrity": "sha512-2XghOwu16ZwPJLOFVuIOaLbN0iKMn867evzXFyf0P22dqugezfJwLmdanAgU25ITvz1TvOfVP4jsDImlDJzcWg==", + "devOptional": true, "license": "BSD-3-Clause" }, - "node_modules/@mapbox/mapbox-sdk": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@mapbox/mapbox-sdk/-/mapbox-sdk-0.16.2.tgz", - "integrity": "sha512-II8KrqOD+neL94bCakBQfYmNSD8A3MQHyxxtZ9Hy5nZQWFacocpj2PjPvQqgctmddWZQ9GS+WBgHPVamhuM9xA==", - "license": "BSD-2-Clause", - "dependencies": { - "@mapbox/fusspot": "^0.4.0", - "@mapbox/parse-mapbox-token": "^0.2.0", - "@mapbox/polyline": "^1.0.0", - "eventemitter3": "^3.1.0", - "form-data": "^3.0.4", - "got": "^11.8.5", - "is-plain-obj": "^1.1.0", - "xtend": "^4.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@mapbox/parse-mapbox-token": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@mapbox/parse-mapbox-token/-/parse-mapbox-token-0.2.0.tgz", - "integrity": "sha512-BjeuG4sodYaoTygwXIuAWlZV6zUv4ZriYAQhXikzx+7DChycMUQ9g85E79Htat+AsBg+nStFALehlOhClYm5cQ==", - "license": "BSD-2-Clause", - "dependencies": { - "base-64": "^0.1.0" - } - }, "node_modules/@mapbox/point-geometry": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-1.1.0.tgz", "integrity": "sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==", "license": "ISC" }, - "node_modules/@mapbox/polyline": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@mapbox/polyline/-/polyline-1.2.1.tgz", - "integrity": "sha512-sn0V18O3OzW4RCcPoUIVDWvEGQaBNH9a0y5lgqrf5hUycyw1CzrhEoxV5irzrMNXKCkw1xRsZXcaVbsVZggHXA==", - "dependencies": { - "meow": "^9.0.0" - }, - "bin": { - "polyline": "bin/polyline.bin.js" - } - }, "node_modules/@mapbox/tiny-sdf": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.7.tgz", - "integrity": "sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.2.0.tgz", + "integrity": "sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg==", "license": "BSD-2-Clause" }, "node_modules/@mapbox/unitbezier": { @@ -2848,6 +2678,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-2.0.4.tgz", "integrity": "sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==", + "devOptional": true, "license": "BSD-3-Clause", "dependencies": { "@mapbox/point-geometry": "~1.1.0", @@ -7885,18 +7716,6 @@ "tslib": "^2.8.0" } }, - "node_modules/@szmarczak/http-timer": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", - "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", - "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@tailwindcss/node": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", @@ -8690,6 +8509,116 @@ "url": "https://github.com/sponsors/ueberdosis" } }, + "node_modules/@toast-ui/editor": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/@toast-ui/editor/-/editor-3.1.10.tgz", + "integrity": "sha512-lzJxNM9lEbAxEqVAnLGqARLFqcVAPW3gwVOU0qKHf/IIwdhjZPjo8VDNQ4sPqOsC7vKCG35HVX8bNC+ab+Gzlg==", + "license": "MIT", + "dependencies": { + "dompurify": "^2.3.3", + "prosemirror-commands": "~1.1.9", + "prosemirror-history": "~1.1.3", + "prosemirror-inputrules": "~1.1.3", + "prosemirror-keymap": "~1.1.4", + "prosemirror-model": "~1.14.1", + "prosemirror-state": "~1.3.4", + "prosemirror-transform": "~1.3.0", + "prosemirror-view": "~1.18.7" + } + }, + "node_modules/@toast-ui/editor/node_modules/dompurify": { + "version": "2.5.9", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.9.tgz", + "integrity": "sha512-i6mvVmWN4xo9LrhCOZrDgSs9noW6nOahbrmzjRbPF36YPyj5Ue5lgok0MHDWkG7xzpWFO2OYttXdzM7rJxHvNA==", + "license": "(MPL-2.0 OR Apache-2.0)" + }, + "node_modules/@toast-ui/editor/node_modules/orderedmap": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-1.1.8.tgz", + "integrity": "sha512-eWEYOAggZZpZbJ9CTsqAKOTxlbBHdHZ8pzcfEvNTxGrjQ/m+Q25nSWUiMlT9MTbgpB6FOiBDKqsgJ2FlLDVNaw==", + "license": "MIT" + }, + "node_modules/@toast-ui/editor/node_modules/prosemirror-commands": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.1.12.tgz", + "integrity": "sha512-+CrMs3w/ZVPSkR+REg8KL/clyFLv/1+SgY/OMN+CB22Z24j9TZDje72vL36lOZ/E4NeRXuiCcmENcW/vAcG67A==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/@toast-ui/editor/node_modules/prosemirror-history": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.1.3.tgz", + "integrity": "sha512-zGDotijea+vnfnyyUGyiy1wfOQhf0B/b6zYcCouBV8yo6JmrE9X23M5q7Nf/nATywEZbgRLG70R4DmfSTC+gfg==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/@toast-ui/editor/node_modules/prosemirror-inputrules": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.1.3.tgz", + "integrity": "sha512-ZaHCLyBtvbyIHv0f5p6boQTIJjlD6o2NPZiEaZWT2DA+j591zS29QQEMT4lBqwcLW3qRSf7ZvoKNbf05YrsStw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/@toast-ui/editor/node_modules/prosemirror-keymap": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.1.5.tgz", + "integrity": "sha512-8SZgPH3K+GLsHL2wKuwBD9rxhsbnVBTwpHCO4VUO5GmqUQlxd/2GtBVWTsyLq4Dp3N9nGgPd3+lZFKUDuVp+Vw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/@toast-ui/editor/node_modules/prosemirror-model": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.14.3.tgz", + "integrity": "sha512-yzZlBaSxfUPIIP6U5Edh5zKxJPZ5f7bwZRhiCuH3UYkWhj+P3d8swHsbuAMOu/iDatDc5J/Qs5Mb3++mZf+CvQ==", + "license": "MIT", + "dependencies": { + "orderedmap": "^1.1.0" + } + }, + "node_modules/@toast-ui/editor/node_modules/prosemirror-state": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.3.4.tgz", + "integrity": "sha512-Xkkrpd1y/TQ6HKzN3agsQIGRcLckUMA9u3j207L04mt8ToRgpGeyhbVv0HI7omDORIBHjR29b7AwlATFFf2GLA==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/@toast-ui/editor/node_modules/prosemirror-transform": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.3.4.tgz", + "integrity": "sha512-gTsg3UIeaFuEY6+YmNPMgTpEkCKPedkFIUnsPpOMbclU701fEVI/e4VOXACXh3BO5rZJaBbEBwrnzB0mLp6eBA==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0" + } + }, + "node_modules/@toast-ui/editor/node_modules/prosemirror-view": { + "version": "1.18.11", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.18.11.tgz", + "integrity": "sha512-KXUM8UEV+IK4JYWHNyxkPGDGbxeTEUHQv3POApfyTRN5eMcPFbY4cB0mDJr0LPelVvYPghmZDOCqfCIm9mYHtQ==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.14.3", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, "node_modules/@tootallnate/once": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.1.tgz", @@ -10841,18 +10770,6 @@ "@babel/types": "^7.28.2" } }, - "node_modules/@types/cacheable-request": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", - "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "*", - "@types/keyv": "^3.1.4", - "@types/node": "*", - "@types/responselike": "^1.0.0" - } - }, "node_modules/@types/chai": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", @@ -11224,12 +11141,6 @@ "@types/unist": "*" } }, - "node_modules/@types/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==", - "license": "MIT" - }, "node_modules/@types/http-proxy": { "version": "1.17.17", "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", @@ -11252,15 +11163,6 @@ "integrity": "sha512-tdJz7jaWFu4nR+8b2B+CdPZ6811ighYylWsu2hpsivapzW058yP0KdfZuNY89IiRe5jbKvBGXN3LQdN2KPXVdQ==", "license": "MIT" }, - "node_modules/@types/keyv": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", - "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", @@ -11295,6 +11197,23 @@ "mapbox-gl": "*" } }, + "node_modules/@types/mapbox__point-geometry": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", + "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==", + "license": "MIT" + }, + "node_modules/@types/mapbox__vector-tile": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz", + "integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*", + "@types/mapbox__point-geometry": "*", + "@types/pbf": "*" + } + }, "node_modules/@types/mapbox-gl": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-3.4.1.tgz", @@ -11329,12 +11248,6 @@ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", "license": "MIT" }, - "node_modules/@types/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==", - "license": "MIT" - }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -11350,12 +11263,6 @@ "undici-types": "~5.26.4" } }, - "node_modules/@types/normalize-package-data": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", - "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "license": "MIT" - }, "node_modules/@types/pako": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", @@ -11363,6 +11270,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/papaparse": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.5.2.tgz", + "integrity": "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -11419,15 +11335,6 @@ "@types/react": "*" } }, - "node_modules/@types/responselike": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", - "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/set-cookie-parser": { "version": "2.4.10", "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.10.tgz", @@ -11522,293 +11429,11 @@ "dev": true, "license": "MIT" }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", - "integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.61.0", - "@typescript-eslint/type-utils": "8.61.0", - "@typescript-eslint/utils": "8.61.0", - "@typescript-eslint/visitor-keys": "8.61.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.61.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz", - "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.61.0", - "@typescript-eslint/types": "8.61.0", - "@typescript-eslint/typescript-estree": "8.61.0", - "@typescript-eslint/visitor-keys": "8.61.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz", - "integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.61.0", - "@typescript-eslint/types": "^8.61.0", - "debug": "^4.4.3" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz", - "integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.61.0", - "@typescript-eslint/visitor-keys": "8.61.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz", - "integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz", - "integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.61.0", - "@typescript-eslint/typescript-estree": "8.61.0", - "@typescript-eslint/utils": "8.61.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz", - "integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz", - "integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.61.0", - "@typescript-eslint/tsconfig-utils": "8.61.0", - "@typescript-eslint/types": "8.61.0", - "@typescript-eslint/visitor-keys": "8.61.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "brace-expansion": "^5.0.5" - }, - "engines": { - "node": "18 || 20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", - "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz", - "integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.61.0", - "@typescript-eslint/types": "8.61.0", - "@typescript-eslint/typescript-estree": "8.61.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz", - "integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.61.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@ucast/core": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz", - "integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==", - "license": "Apache-2.0" + "node_modules/@ucast/core": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz", + "integrity": "sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g==", + "license": "Apache-2.0" }, "node_modules/@ucast/js": { "version": "3.1.0", @@ -12069,16 +11694,6 @@ "node": ">=0.4.0" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -12293,52 +11908,12 @@ "node": ">=0.10.0" } }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", - "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "is-array-buffer": "^3.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "license": "MIT" }, - "node_modules/array-includes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", - "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.0", - "es-object-atoms": "^1.1.1", - "get-intrinsic": "^1.3.0", - "is-string": "^1.1.1", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -12348,113 +11923,6 @@ "node": ">=8" } }, - "node_modules/array.prototype.findlast": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", - "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flat": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", - "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.flatmap": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", - "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array.prototype.tosorted": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", - "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.3", - "es-errors": "^1.3.0", - "es-shim-unscopables": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", - "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "is-array-buffer": "^3.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/asn1": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", @@ -12525,16 +11993,6 @@ "dev": true, "license": "MIT" }, - "node_modules/async-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", - "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -12607,22 +12065,6 @@ "postcss": "^8.1.0" } }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -12740,11 +12182,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, - "node_modules/base-64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", - "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -13137,48 +12574,6 @@ "node": ">=8" } }, - "node_modules/cacheable-lookup": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", - "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", - "license": "MIT", - "engines": { - "node": ">=10.6.0" - } - }, - "node_modules/cacheable-request": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", - "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", - "license": "MIT", - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^4.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^6.0.1", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cacheable-request/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "license": "MIT", - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cachedir": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/cachedir/-/cachedir-2.4.0.tgz", @@ -13189,25 +12584,6 @@ "node": ">=6" } }, - "node_modules/call-bind": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", - "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "get-intrinsic": "^1.3.0", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -13265,41 +12641,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "license": "MIT", - "dependencies": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/camelcase-keys/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camelcase-keys/node_modules/quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001793", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", @@ -13471,6 +12812,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/cheap-ruler/-/cheap-ruler-4.0.0.tgz", "integrity": "sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw==", + "devOptional": true, "license": "ISC" }, "node_modules/check-error": { @@ -13731,18 +13073,6 @@ "node": ">=0.10.0" } }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "license": "MIT", - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -14141,6 +13471,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==", + "devOptional": true, "license": "MIT" }, "node_modules/cssesc": { @@ -14863,60 +14194,6 @@ "node": ">=18" } }, - "node_modules/data-view-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", - "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/data-view-byte-length": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", - "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/inspect-js" - } - }, - "node_modules/data-view-byte-offset": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", - "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-data-view": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/dayjs": { "version": "1.11.20", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", @@ -14967,40 +14244,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/decamelize-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", - "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", - "license": "MIT", - "dependencies": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decamelize-keys/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decamelize-keys/node_modules/map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/decimal.js": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", @@ -15030,33 +14273,6 @@ "node": ">=0.10" } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -15073,13 +14289,6 @@ "node": ">=6" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", @@ -15138,33 +14347,6 @@ "node": ">=0.8" } }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/define-lazy-prop": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", @@ -15177,24 +14359,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/defu": { "version": "6.1.7", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", @@ -15316,19 +14480,6 @@ "node": ">=8" } }, - "node_modules/doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -15610,6 +14761,7 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -15712,75 +14864,6 @@ "stackframe": "^1.3.4" } }, - "node_modules/es-abstract": { - "version": "1.24.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", - "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.2", - "arraybuffer.prototype.slice": "^1.0.4", - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "data-view-buffer": "^1.0.2", - "data-view-byte-length": "^1.0.2", - "data-view-byte-offset": "^1.0.1", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "es-set-tostringtag": "^2.1.0", - "es-to-primitive": "^1.3.0", - "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.3.0", - "get-proto": "^1.0.1", - "get-symbol-description": "^1.1.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "internal-slot": "^1.1.0", - "is-array-buffer": "^3.0.5", - "is-callable": "^1.2.7", - "is-data-view": "^1.0.2", - "is-negative-zero": "^2.0.3", - "is-regex": "^1.2.1", - "is-set": "^2.0.3", - "is-shared-array-buffer": "^1.0.4", - "is-string": "^1.1.1", - "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.1", - "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.7", - "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.4", - "safe-array-concat": "^1.1.3", - "safe-push-apply": "^1.0.0", - "safe-regex-test": "^1.1.0", - "set-proto": "^1.0.0", - "stop-iteration-iterator": "^1.1.0", - "string.prototype.trim": "^1.2.10", - "string.prototype.trimend": "^1.0.9", - "string.prototype.trimstart": "^1.0.8", - "typed-array-buffer": "^1.0.3", - "typed-array-byte-length": "^1.0.3", - "typed-array-byte-offset": "^1.0.4", - "typed-array-length": "^1.0.7", - "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.19" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -15799,34 +14882,6 @@ "node": ">= 0.4" } }, - "node_modules/es-iterator-helpers": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.3.tgz", - "integrity": "sha512-0PuBxFi+4uPanB97iDxCLWuHeYud2FALrw5HFZGtAF38UpJDbDC8frwp2cnDyae692CQ0dou60UwWfhgsa4U/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.2", - "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.1.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.3.0", - "globalthis": "^1.0.4", - "gopd": "^1.2.0", - "has-property-descriptors": "^1.0.2", - "has-proto": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.5", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/es-module-lexer": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", @@ -15861,37 +14916,6 @@ "node": ">= 0.4" } }, - "node_modules/es-shim-unscopables": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", - "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -15960,338 +14984,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint": { - "version": "9.39.4", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", - "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.2", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.5", - "@eslint/js": "9.39.4", - "@eslint/plugin-kit": "^0.4.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-config-prettier": { - "version": "10.1.8", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", - "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", - "dev": true, - "license": "MIT", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "funding": { - "url": "https://opencollective.com/eslint-config-prettier" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, - "node_modules/eslint-plugin-react": { - "version": "7.37.5", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", - "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.8", - "array.prototype.findlast": "^1.2.5", - "array.prototype.flatmap": "^1.3.3", - "array.prototype.tosorted": "^1.1.4", - "doctrine": "^2.1.0", - "es-iterator-helpers": "^1.2.1", - "estraverse": "^5.3.0", - "hasown": "^2.0.2", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.1.2", - "object.entries": "^1.1.9", - "object.fromentries": "^2.0.8", - "object.values": "^1.2.1", - "prop-types": "^15.8.1", - "resolve": "^2.0.0-next.5", - "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.12", - "string.prototype.repeat": "^1.0.0" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" - } - }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", - "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" - } - }, - "node_modules/eslint-plugin-react-refresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", - "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "eslint": "^9 || ^10" - } - }, - "node_modules/eslint-plugin-react/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", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-react/node_modules/minimatch": { - "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" - }, - "engines": { - "node": "*" - } - }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", - "integrity": "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "is-core-module": "^2.16.2", - "node-exports-info": "^1.6.0", - "object-keys": "^1.1.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/eslint/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", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/eslint/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/eslint/node_modules/minimatch": { - "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" - }, - "engines": { - "node": "*" - } - }, - "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", @@ -16305,42 +14997,6 @@ "node": ">=4" } }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, "node_modules/estree-util-is-identifier-name": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", @@ -16361,16 +15017,6 @@ "@types/estree": "^1.0.0" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", @@ -16387,12 +15033,6 @@ "dev": true, "license": "MIT" }, - "node_modules/eventemitter3": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.2.tgz", - "integrity": "sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==", - "license": "MIT" - }, "node_modules/events": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", @@ -16611,20 +15251,6 @@ "node": ">=8.6.0" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-redact": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", @@ -16795,19 +15421,6 @@ "node": ">=0.8.0" } }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, "node_modules/file-type": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", @@ -17013,20 +15626,6 @@ "node": ">=8" } }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/flatstr": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/flatstr/-/flatstr-1.0.12.tgz", @@ -17094,22 +15693,6 @@ "unicode-trie": "^2.0.0" } }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/foreach": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", @@ -17155,22 +15738,6 @@ "node": "*" } }, - "node_modules/form-data": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.5.tgz", - "integrity": "sha512-j23EibVLnp4zNXGW7LjryXYa2X6U/M96yoOX+ybZxwkYajdxRNEqYY3zhh7y0i6kfISKS2jr+EJq1YTUDEv5+w==", - "license": "MIT", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.4", - "mime-types": "^2.1.35" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/format-util": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/format-util/-/format-util-1.0.5.tgz", @@ -17272,61 +15839,12 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/fuzzy": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz", - "integrity": "sha512-/gZffu4ykarLrCiP3Ygsa86UAo1E5vEVlvTrpkKywXSbP9Xhln3oSp9QSV57gEq3JFFpGJ4GZ+5zdEp3FcUh4w==", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/fuzzysort": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz", "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==", "license": "MIT" }, - "node_modules/generator-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", - "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -17481,24 +15999,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-symbol-description": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", - "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", @@ -17601,34 +16101,27 @@ "node": ">=10" } }, - "node_modules/globals": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", - "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", - "dev": true, + "node_modules/global-prefix": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz", + "integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==", "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "ini": "^4.1.3", + "kind-of": "^6.0.3", + "which": "^4.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=16" } }, - "node_modules/globalthis": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", - "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", - "dev": true, + "node_modules/global-prefix/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "gopd": "^1.0.1" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=0.10.0" } }, "node_modules/globby": { @@ -17670,31 +16163,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/got": { - "version": "11.8.6", - "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", - "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.0.0", - "@szmarczak/http-timer": "^4.0.5", - "@types/cacheable-request": "^6.0.1", - "@types/responselike": "^1.0.0", - "cacheable-lookup": "^5.0.3", - "cacheable-request": "^7.0.2", - "decompress-response": "^6.0.0", - "http2-wrapper": "^1.0.0-beta.5.2", - "lowercase-keys": "^2.0.0", - "p-cancelable": "^2.0.0", - "responselike": "^2.0.0" - }, - "engines": { - "node": ">=10.19.0" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -17760,6 +16228,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==", + "devOptional": true, "license": "ISC" }, "node_modules/handlebars": { @@ -17799,28 +16268,6 @@ "dev": true, "license": "MIT" }, - "node_modules/hard-rejection": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/has-bigints": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", - "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -17830,35 +16277,6 @@ "node": ">=8" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", - "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/has-symbols": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", @@ -17981,23 +16399,6 @@ "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", "license": "MIT" }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", - "dev": true, - "license": "MIT" - }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -18119,12 +16520,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "license": "BSD-2-Clause" - }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -18219,19 +16614,6 @@ "node": ">=0.10" } }, - "node_modules/http2-wrapper": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", - "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.0.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -18330,6 +16712,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -18426,21 +16809,6 @@ "node": ">=8" } }, - "node_modules/internal-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", - "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "hasown": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -18511,66 +16879,12 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-array-buffer": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", - "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "license": "MIT" }, - "node_modules/is-async-function": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", - "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "async-function": "^1.0.0", - "call-bound": "^1.0.3", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-bigint": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", - "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -18584,23 +16898,6 @@ "node": ">=8" } }, - "node_modules/is-boolean-object": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", - "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-buffer": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", @@ -18624,19 +16921,6 @@ "node": ">=4" } }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-core-module": { "version": "2.16.2", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", @@ -18652,41 +16936,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-data-view": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", - "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "get-intrinsic": "^1.2.6", - "is-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-date-object": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", - "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-decimal": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", @@ -18730,22 +16979,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-finalizationregistry": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", - "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -18755,26 +16988,6 @@ "node": ">=8" } }, - "node_modules/is-generator-function": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", - "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.4", - "generator-function": "^2.0.0", - "get-proto": "^1.0.1", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -18853,32 +17066,6 @@ "node": ">=8" } }, - "node_modules/is-map": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", - "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", - "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-node-process": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", @@ -18894,23 +17081,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-number-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", - "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-obj": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", @@ -18930,15 +17100,6 @@ "node": ">=8" } }, - "node_modules/is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -18961,25 +17122,6 @@ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", "license": "MIT" }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-regexp": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-3.1.0.tgz", @@ -18992,35 +17134,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-set": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", - "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", - "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -19033,57 +17146,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-string": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", - "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", - "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-symbols": "^1.1.0", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -19109,52 +17171,6 @@ "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", "license": "MIT" }, - "node_modules/is-weakmap": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", - "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", - "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakset": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", - "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-intrinsic": "^1.2.6" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-wsl": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", @@ -19255,33 +17271,15 @@ "node_modules/istanbul-reports": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/iterator.prototype": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", - "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, - "license": "MIT", + "license": "BSD-3-Clause", "dependencies": { - "define-data-property": "^1.1.4", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "get-proto": "^1.0.0", - "has-symbols": "^1.1.0", - "set-function-name": "^2.0.2" + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=8" } }, "node_modules/jackspeak": { @@ -19523,12 +17521,6 @@ "node": ">=6" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "license": "MIT" - }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -19645,13 +17637,6 @@ "integrity": "sha512-7DE8mpG+/fVw+dTpjbxnx47TaMnDfOI1jwft9g1VybltZCduyRQPJPvc+zzKY9WPHxhPWczyFuYa6I8Mw4iU5A==", "license": "BSD-2-Clause" }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/json-stringify-pretty-compact": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-3.0.0.tgz", @@ -19750,22 +17735,6 @@ "node": ">= 12" } }, - "node_modules/jsx-ast-utils": { - "version": "3.3.5", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", - "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "array-includes": "^3.1.6", - "array.prototype.flat": "^1.3.1", - "object.assign": "^4.1.4", - "object.values": "^1.1.6" - }, - "engines": { - "node": ">=4.0" - } - }, "node_modules/jwt-decode": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", @@ -19776,20 +17745,11 @@ } }, "node_modules/kdbush": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", - "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.1.0.tgz", + "integrity": "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==", "license": "ISC" }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, "node_modules/kind-of": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", @@ -19817,20 +17777,6 @@ "dayjs": "^1.11.7" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/lightningcss": { "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", @@ -20364,19 +18310,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "license": "MIT" - }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -20654,15 +18587,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", @@ -20730,22 +18654,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/map-obj": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", - "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/mapbox-gl": { "version": "3.21.0", "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-3.21.0.tgz", "integrity": "sha512-0mv/LHDoW6QmxLEoNqCPuby428WTekfs38TtNXd/cxDOLdY6kFd/ztaSkcBeqNksbYSz2lXnPfBm8nN5+hxA0w==", + "devOptional": true, "license": "SEE LICENSE IN LICENSE.txt", "workspaces": [ "src/style-spec", @@ -20781,43 +18694,68 @@ "tinyqueue": "^3.0.0" } }, - "node_modules/mapbox-gl-style-switcher": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/mapbox-gl-style-switcher/-/mapbox-gl-style-switcher-1.0.11.tgz", - "integrity": "sha512-8TDv7ODYqDSNKPILTiz8NdoCRe9WGGq+Tcf+Jxn4DAZJht5FtiNtaQtkzyU1gn4W9dEh6a9zKhzfXvpwApf0hA==", - "license": "GPL-3.0", - "dependencies": { - "mapbox-gl": "^1.11.1" - } + "node_modules/mapbox-gl/node_modules/earcut": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", + "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", + "devOptional": true, + "license": "ISC" }, - "node_modules/mapbox-gl-style-switcher/node_modules/@mapbox/mapbox-gl-supported": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.5.0.tgz", - "integrity": "sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==", + "node_modules/mapbox-gl/node_modules/tinyqueue": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "devOptional": true, + "license": "ISC" + }, + "node_modules/maplibre-gl": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.7.1.tgz", + "integrity": "sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==", "license": "BSD-3-Clause", - "peerDependencies": { - "mapbox-gl": ">=0.32.1 <2.0.0" + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^2.0.6", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "@maplibre/maplibre-gl-style-spec": "^20.3.1", + "@types/geojson": "^7946.0.14", + "@types/geojson-vt": "3.2.5", + "@types/mapbox__point-geometry": "^0.1.4", + "@types/mapbox__vector-tile": "^1.3.4", + "@types/pbf": "^3.0.5", + "@types/supercluster": "^7.1.3", + "earcut": "^3.0.0", + "geojson-vt": "^4.0.2", + "gl-matrix": "^3.4.3", + "global-prefix": "^4.0.0", + "kdbush": "^4.0.2", + "murmurhash-js": "^1.0.0", + "pbf": "^3.3.0", + "potpack": "^2.0.0", + "quickselect": "^3.0.0", + "supercluster": "^8.0.1", + "tinyqueue": "^3.0.0", + "vt-pbf": "^3.1.3" + }, + "engines": { + "node": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" } }, - "node_modules/mapbox-gl-style-switcher/node_modules/@mapbox/point-geometry": { + "node_modules/maplibre-gl/node_modules/@mapbox/point-geometry": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==", "license": "ISC" }, - "node_modules/mapbox-gl-style-switcher/node_modules/@mapbox/tiny-sdf": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-1.2.5.tgz", - "integrity": "sha512-cD8A/zJlm6fdJOk6DqPUV8mcpyJkRz2x2R+/fYcWDYG3oWbG7/L7Yl/WqQ1VZCjnL9OTIMAn6c+BC5Eru4sQEw==", - "license": "BSD-2-Clause" - }, - "node_modules/mapbox-gl-style-switcher/node_modules/@mapbox/unitbezier": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz", - "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==", - "license": "BSD-2-Clause" - }, - "node_modules/mapbox-gl-style-switcher/node_modules/@mapbox/vector-tile": { + "node_modules/maplibre-gl/node_modules/@mapbox/vector-tile": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", @@ -20826,52 +18764,54 @@ "@mapbox/point-geometry": "~0.1.0" } }, - "node_modules/mapbox-gl-style-switcher/node_modules/geojson-vt": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", - "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==", - "license": "ISC" - }, - "node_modules/mapbox-gl-style-switcher/node_modules/kdbush": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", - "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==", - "license": "ISC" - }, - "node_modules/mapbox-gl-style-switcher/node_modules/mapbox-gl": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.13.3.tgz", - "integrity": "sha512-p8lJFEiqmEQlyv+DQxFAOG/XPWN0Wp7j/Psq93Zywz7qt9CcUKFYDBOoOEKzqe6gudHVJY8/Bhqw6VDpX2lSBg==", - "license": "SEE LICENSE IN LICENSE.txt", + "node_modules/maplibre-gl/node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "20.4.0", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.4.0.tgz", + "integrity": "sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==", + "license": "ISC", "dependencies": { - "@mapbox/geojson-rewind": "^0.5.2", - "@mapbox/geojson-types": "^1.0.2", - "@mapbox/jsonlint-lines-primitives": "^2.0.2", - "@mapbox/mapbox-gl-supported": "^1.5.0", - "@mapbox/point-geometry": "^0.1.0", - "@mapbox/tiny-sdf": "^1.1.1", - "@mapbox/unitbezier": "^0.0.0", - "@mapbox/vector-tile": "^1.3.1", - "@mapbox/whoots-js": "^3.1.0", - "csscolorparser": "~1.0.3", - "earcut": "^2.2.2", - "geojson-vt": "^3.2.1", - "gl-matrix": "^3.2.1", - "grid-index": "^1.1.0", - "murmurhash-js": "^1.0.0", - "pbf": "^3.2.1", - "potpack": "^1.0.1", + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/unitbezier": "^0.0.1", + "json-stringify-pretty-compact": "^4.0.0", + "minimist": "^1.2.8", "quickselect": "^2.0.0", "rw": "^1.3.3", - "supercluster": "^7.1.0", - "tinyqueue": "^2.0.3", - "vt-pbf": "^3.1.1" + "tinyqueue": "^3.0.0" }, - "engines": { - "node": ">=6.4.0" + "bin": { + "gl-style-format": "dist/gl-style-format.mjs", + "gl-style-migrate": "dist/gl-style-migrate.mjs", + "gl-style-validate": "dist/gl-style-validate.mjs" + } + }, + "node_modules/maplibre-gl/node_modules/@maplibre/maplibre-gl-style-spec/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/maplibre-gl/node_modules/earcut": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.2.3.tgz", + "integrity": "sha512-vnS4AVwp1KHAF13i1vp1/2D5evWy3k5u/iW/B81QVsUZtV8cv2tU0b2VNFlqvh4kYwrFMDdjPCfAmfyJW9y14Q==", + "license": "ISC" + }, + "node_modules/maplibre-gl/node_modules/json-stringify-pretty-compact": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" + }, + "node_modules/maplibre-gl/node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mapbox-gl-style-switcher/node_modules/pbf": { + "node_modules/maplibre-gl/node_modules/pbf": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", @@ -20884,34 +18824,7 @@ "pbf": "bin/pbf" } }, - "node_modules/mapbox-gl-style-switcher/node_modules/potpack": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", - "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", - "license": "ISC" - }, - "node_modules/mapbox-gl-style-switcher/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/mapbox-gl-style-switcher/node_modules/supercluster": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz", - "integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==", - "license": "ISC", - "dependencies": { - "kdbush": "^3.0.0" - } - }, - "node_modules/mapbox-gl/node_modules/earcut": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.2.tgz", - "integrity": "sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==", - "license": "ISC" - }, - "node_modules/mapbox-gl/node_modules/tinyqueue": { + "node_modules/maplibre-gl/node_modules/tinyqueue": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", @@ -21029,6 +18942,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/martinez-polygon-clipping/-/martinez-polygon-clipping-0.8.1.tgz", "integrity": "sha512-9PLLMzMPI6ihHox4Ns6LpVBLpRc7sbhULybZ/wyaY8sY3ECNe2+hxm1hA2/9bEEpRrdpjoeduBuZLg2aq1cSIQ==", + "devOptional": true, "license": "MIT", "dependencies": { "robust-predicates": "^2.0.4", @@ -21040,6 +18954,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "devOptional": true, "license": "ISC" }, "node_modules/math-intrinsics": { @@ -22072,97 +19987,11 @@ "node": ">= 0.6" } }, - "node_modules/meow": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", - "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", - "license": "MIT", - "dependencies": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize": "^1.2.0", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.18.0", - "yargs-parser": "^20.2.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/meow/node_modules/hosted-git-info": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", - "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/meow/node_modules/lru-cache": { + "node_modules/memoize-one": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/meow/node_modules/normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/meow/node_modules/type-fest": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", - "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/meow/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "license": "ISC" + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" }, "node_modules/merge-descriptors": { "version": "1.0.3", @@ -23375,19 +21204,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -23408,35 +21229,12 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/minimist": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", - "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", - "license": "MIT" - }, - "node_modules/minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", - "license": "MIT", - "dependencies": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/minimist-options/node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "license": "MIT" + }, "node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", @@ -23647,13 +21445,6 @@ "node": "^18 || >=20" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -23720,35 +21511,6 @@ "node": ">=6" } }, - "node_modules/node-exports-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", - "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "array.prototype.flatmap": "^1.3.3", - "es-errors": "^1.3.0", - "object.entries": "^1.1.9", - "semver": "^6.3.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/node-exports-info/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -23837,18 +21599,6 @@ "svg-arc-to-cubic-bezier": "^3.0.0" } }, - "node_modules/normalize-url": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", - "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/notistack": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/notistack/-/notistack-2.0.8.tgz", @@ -24003,16 +21753,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/object-treeify": { "version": "1.1.33", "resolved": "https://registry.npmjs.org/object-treeify/-/object-treeify-1.1.33.tgz", @@ -24022,62 +21762,6 @@ "node": ">= 10" } }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.entries": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", - "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.fromentries": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", - "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object.omit": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-3.0.0.tgz", @@ -24114,25 +21798,6 @@ "node": ">=0.10.0" } }, - "node_modules/object.values": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", - "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/ohash": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", @@ -24234,24 +21899,6 @@ "node": ">= 6" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -24294,33 +21941,6 @@ "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", "license": "MIT" }, - "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.6", - "object-keys": "^1.1.1", - "safe-push-apply": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/p-cancelable": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", - "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -24379,9 +21999,9 @@ "license": "(MIT AND Zlib)" }, "node_modules/papaparse": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", - "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.4.tgz", + "integrity": "sha512-SwzWD9gl/ElwYLCI0nUja1mFJzjq2D8ziShfNBa7zCHzkOozeOGDwHWQ+tvCzEZcewecWZ5U7kUopDnG+DFYEQ==", "license": "MIT" }, "node_modules/parent-module": { @@ -24599,6 +22219,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/pbf/-/pbf-4.0.1.tgz", "integrity": "sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==", + "devOptional": true, "license": "BSD-3-Clause", "dependencies": { "resolve-protobuf-schema": "^2.1.0" @@ -24998,16 +22619,6 @@ "splaytree-ts": "^1.0.2" } }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/postcss": { "version": "8.5.15", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", @@ -25223,32 +22834,6 @@ "node": ">=10" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", - "dev": true, - "license": "MIT", - "bin": { - "prettier": "bin/prettier.cjs" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -25679,6 +23264,7 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", @@ -25780,18 +23366,6 @@ "dev": true, "license": "MIT" }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/quickselect": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", @@ -26224,174 +23798,45 @@ }, "funding": { "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-transition-group": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", - "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "license": "BSD-3-Clause", - "dependencies": { - "@babel/runtime": "^7.5.5", - "dom-helpers": "^5.0.1", - "loose-envify": "^1.4.0", - "prop-types": "^15.6.2" - }, - "peerDependencies": { - "react": ">=16.6.0", - "react-dom": ">=16.6.0" - } - }, - "node_modules/read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "license": "MIT", - "dependencies": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "license": "MIT", - "dependencies": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/read-pkg-up/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/express" } }, - "node_modules/read-pkg-up/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" }, "engines": { - "node": ">=8" - } - }, - "node_modules/read-pkg-up/node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=8" + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } } }, - "node_modules/read-pkg/node_modules/hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "license": "ISC" - }, - "node_modules/read-pkg/node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "license": "BSD-2-Clause", + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/read-pkg/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/read-pkg/node_modules/type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=8" + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" } }, "node_modules/readable-stream": { @@ -26450,6 +23895,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, "license": "MIT", "dependencies": { "indent-string": "^4.0.0", @@ -26468,50 +23914,6 @@ "esprima": "~4.0.0" } }, - "node_modules/reflect.getprototypeof": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", - "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.9", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.1", - "which-builtin-type": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", - "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "define-properties": "^1.2.1", - "es-errors": "^1.3.0", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "set-function-name": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/remark-gfm": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-1.0.0.tgz", @@ -26628,12 +24030,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "license": "MIT" - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -26652,18 +24048,6 @@ "protocol-buffers-schema": "^3.3.1" } }, - "node_modules/responselike": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", - "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", - "license": "MIT", - "dependencies": { - "lowercase-keys": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -26919,33 +24303,6 @@ "tslib": "^2.1.0" } }, - "node_modules/safe-array-concat": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", - "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "get-intrinsic": "^1.3.0", - "has-symbols": "^1.1.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-array-concat/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -26966,48 +24323,6 @@ ], "license": "MIT" }, - "node_modules/safe-push-apply": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", - "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "isarray": "^2.0.5" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safe-push-apply/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/safe-stable-stringify": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz", @@ -27180,55 +24495,6 @@ "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-function-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", - "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/set-proto": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", - "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/set-value": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", @@ -28263,6 +25529,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/splaytree/-/splaytree-0.1.4.tgz", "integrity": "sha512-D50hKrjZgBzqD3FT2Ek53f2dcDLAQT8SSGrzj3vidNH5ISRgceeGVJ2dQIthKOuayqFXfFjXheHNo4bbt9LhRQ==", + "devOptional": true, "license": "MIT" }, "node_modules/splaytree-ts": { @@ -28412,170 +25679,57 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/strict-event-emitter": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", - "license": "MIT" - }, - "node_modules/strict-uri-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", - "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string.prototype.matchall": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", - "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.6", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "internal-slot": "^1.1.0", - "regexp.prototype.flags": "^1.5.3", - "set-function-name": "^2.0.2", - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "license": "MIT" }, - "node_modules/string.prototype.repeat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", - "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", - "dev": true, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" + "engines": { + "node": ">=4" } }, - "node_modules/string.prototype.trim": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", - "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", - "dev": true, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "define-data-property": "^1.1.4", - "define-properties": "^1.2.1", - "es-abstract": "^1.24.2", - "es-object-atoms": "^1.1.2", - "has-property-descriptors": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "safe-buffer": "~5.2.0" } }, - "node_modules/string.prototype.trimend": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", - "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", - "dev": true, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.1.2" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", - "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", - "dev": true, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, "node_modules/stringify-entities": { @@ -28677,6 +25831,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, "license": "MIT", "dependencies": { "min-indent": "^1.0.0" @@ -28685,19 +25840,6 @@ "node": ">=8" } }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/strip-literal": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", @@ -28755,22 +25897,6 @@ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", "license": "MIT" }, - "node_modules/subtag": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/subtag/-/subtag-0.5.0.tgz", - "integrity": "sha512-CaIBcTSb/nyk4xiiSOtZYz1B+F12ZxW8NEp54CdT+84vmh/h4sUnHGC6+KQXUfED8u22PQjCYWfZny8d2ELXwg==", - "license": "ISC" - }, - "node_modules/suggestions": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/suggestions/-/suggestions-1.7.1.tgz", - "integrity": "sha512-gl5YPAhPYl07JZ5obiD9nTZsg4SyZswAQU/NNtnYiSnFkI3+ZHuXAiEsYm7AaZ71E0LXSFaGVaulGSWN3Gd71A==", - "license": "ISC", - "dependencies": { - "fuzzy": "^0.1.1", - "xtend": "^4.0.0" - } - }, "node_modules/supercluster": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", @@ -29274,15 +26400,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/trough": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", @@ -29293,19 +26410,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, "node_modules/ts-invariant": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz", @@ -29398,19 +26502,6 @@ "dev": true, "license": "Unlicense" }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -29436,84 +26527,6 @@ "node": ">= 0.6" } }, - "node_modules/typed-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", - "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "es-errors": "^1.3.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/typed-array-byte-length": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", - "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.14" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-byte-offset": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", - "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "for-each": "^0.3.3", - "gopd": "^1.2.0", - "has-proto": "^1.2.0", - "is-typed-array": "^1.1.15", - "reflect.getprototypeof": "^1.0.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typed-array-length": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", - "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.9", - "for-each": "^0.3.5", - "gopd": "^1.2.0", - "is-typed-array": "^1.1.15", - "possible-typed-array-names": "^1.1.0", - "reflect.getprototypeof": "^1.0.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/typedarray": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.7.tgz", @@ -29537,30 +26550,6 @@ "node": ">=14.17" } }, - "node_modules/typescript-eslint": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz", - "integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/eslint-plugin": "8.61.0", - "@typescript-eslint/parser": "8.61.0", - "@typescript-eslint/typescript-estree": "8.61.0", - "@typescript-eslint/utils": "8.61.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, "node_modules/typewise": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typewise/-/typewise-1.0.3.tgz", @@ -29595,25 +26584,6 @@ "node": ">=0.8.0" } }, - "node_modules/unbox-primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", - "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "has-bigints": "^1.0.2", - "has-symbols": "^1.1.0", - "which-boxed-primitive": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -29911,16 +26881,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/uri-template-lite": { "version": "22.9.0", "resolved": "https://registry.npmjs.org/uri-template-lite/-/uri-template-lite-22.9.0.tgz", @@ -29956,6 +26916,20 @@ } } }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sidecar": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", @@ -30637,80 +27611,6 @@ "node": "^16.13.0 || >=18.0.0" } }, - "node_modules/which-boxed-primitive": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", - "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.1.0", - "is-boolean-object": "^1.2.1", - "is-number-object": "^1.1.1", - "is-string": "^1.1.1", - "is-symbol": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", - "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "function.prototype.name": "^1.1.6", - "has-tostringtag": "^1.0.2", - "is-async-function": "^2.0.0", - "is-date-object": "^1.1.0", - "is-finalizationregistry": "^1.1.0", - "is-generator-function": "^1.0.10", - "is-regex": "^1.2.1", - "is-weakref": "^1.0.2", - "isarray": "^2.0.5", - "which-boxed-primitive": "^1.1.0", - "which-collection": "^1.0.2", - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-builtin-type/node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true, - "license": "MIT" - }, - "node_modules/which-collection": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", - "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-map": "^2.0.3", - "is-set": "^2.0.3", - "is-weakmap": "^2.0.2", - "is-weakset": "^2.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/which-pm": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/which-pm/-/which-pm-2.2.0.tgz", @@ -30724,28 +27624,6 @@ "node": ">=8.15" } }, - "node_modules/which-typed-array": { - "version": "1.1.22", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", - "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.9", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -30790,16 +27668,6 @@ "url": "https://github.com/sponsors/ahocevar" } }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", @@ -30973,6 +27841,7 @@ "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" @@ -31078,19 +27947,6 @@ "zod": "^3.25.28 || ^4" } }, - "node_modules/zod-validation-error": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", - "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - } - }, "node_modules/zrender": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz", diff --git a/package.json b/package.json index c992f5b5..f426e70a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ocotillo-ui", - "version": "1.1.1", + "version": "1.2.0", "private": true, "type": "module", "scripts": { @@ -14,8 +14,14 @@ "test:ui": "vitest --ui", "test:run": "vitest run", "test:coverage": "vitest run --coverage", - "lint": "eslint .", - "lint:fix": "eslint . --fix", + "lint": "biome lint .", + "lint:fix": "biome lint . --write", + "lint:fix:unsafe": "biome lint . --write --unsafe", + "format": "biome format . --write", + "format:check": "biome format .", + "check": "biome check .", + "check:fix": "biome check . --write", + "check:fix:unsafe": "biome check . --write --unsafe", "typecheck": "tsc", "mock:server:vitest": "prism mock openapi-auth.json --dynamic=false --port 4010", "mock:server:cypress": "prism mock openapi-auth.json --dynamic=true --port 4010 --seed 12345", @@ -42,9 +48,9 @@ "@fontsource-variable/outfit": "^5.2.8", "@fontsource-variable/public-sans": "^5.2.7", "@glideapps/glide-data-grid": "^6.0.3", + "@glideapps/glide-data-grid-cells": "^6.0.3", "@hookform/resolvers": "^5.2.2", "@mapbox/mapbox-gl-draw": "^1.4.3", - "@mapbox/mapbox-gl-geocoder": "^5.0.3", "@mui/icons-material": "^6.4.7", "@mui/lab": "^6.0.0-beta.14", "@mui/material": "^6.4.6", @@ -67,6 +73,7 @@ "@tiptap/react": "^2.9.1", "@tiptap/starter-kit": "^2.9.1", "@turf/turf": "^7.2.0", + "@types/papaparse": "^5.5.2", "axios-auth-refresh": "^3.3.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -79,10 +86,10 @@ "jwt-decode": "^4.0.0", "lodash": "^4.18.1", "lucide-react": "^1.17.0", - "mapbox-gl": "^3.0.0", - "mapbox-gl-style-switcher": "^1.0.11", + "maplibre-gl": "^4.7.1", "marked": "^4.3.0", "pako": "^2.1.0", + "papaparse": "^5.5.4", "posthog-js": "^1.402.2", "proj4": "^2.15.0", "radix-ui": "^1.4.3", @@ -104,7 +111,7 @@ "zod": "^4.1.8" }, "devDependencies": { - "@eslint/js": "^9.39.4", + "@biomejs/biome": "^2.5.5", "@hey-api/openapi-ts": "^0.85.2", "@sentry/vite-plugin": "^3.4.0", "@stoplight/prism-cli": "^5.14.2", @@ -126,20 +133,11 @@ "@vitest/ui": "^3.2.4", "autoprefixer": "^10.5.0", "cypress": "^15.0.0", - "eslint": "^9.39.4", - "eslint-config-prettier": "^10.1.8", - "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^7.1.1", - "eslint-plugin-react-refresh": "^0.5.2", - "globals": "^17.6.0", - "jiti": "^2.7.0", "jsdom": "^26.1.0", "pdfjs-dist": "^5.7.284", "postcss": "^8.5.15", - "prettier": "3.5.3", "tailwindcss": "^4.3.0", "typescript": "^5.4.2", - "typescript-eslint": "^8.61.0", "vite": "^6.2.2", "vite-tsconfig-paths": "^5.0.1", "vitest": "^3.2.4" diff --git a/public/2025-11-25_MG009.txt b/public/2025-11-25_MG009.txt new file mode 100644 index 00000000..d087ce6e --- /dev/null +++ b/public/2025-11-25_MG009.txt @@ -0,0 +1,1056 @@ +2024/11/19 18:54:05 ID 009 D 151.02 T 51.2 B 13.9 G 218 R 0001 +2024/11/20 02:54:03 ID 009 D 149.23 T 50.6 B 13.9 G 217 R 0000 +2024/11/20 10:54:03 ID 009 D 149.40 T 49.4 B 13.9 G 217 R 0000 +2024/11/20 18:54:01 ID 009 D 149.29 T 50.2 B 14.0 G 217 R 0000 +2024/11/21 02:54:01 ID 009 D 149.40 T 49.4 B 14.0 G 216 R 0000 +2024/11/21 10:54:01 ID 009 D 149.40 T 49.0 B 14.0 G 216 R 0000 +2024/11/21 18:54:01 ID 009 D 149.34 T 50.0 B 14.0 G 216 R 0000 +2024/11/22 02:54:01 ID 009 D 149.29 T 49.2 B 14.0 G 216 R 0000 +2024/11/22 10:54:01 ID 009 D 149.40 T 49.2 B 14.0 G 216 R 0000 +2024/11/22 18:54:01 ID 009 D 149.18 T 50.0 B 14.0 G 216 R 0000 +2024/11/23 02:54:01 ID 009 D 149.18 T 49.4 B 14.0 G 216 R 0000 +2024/11/23 10:54:01 ID 009 D 149.12 T 49.4 B 14.0 G 216 R 0000 +2024/11/23 18:54:01 ID 009 D 149.06 T 50.4 B 14.0 G 216 R 0000 +2024/11/24 02:54:01 ID 009 D 149.07 T 50.6 B 14.0 G 216 R 0000 +2024/11/24 10:54:01 ID 009 D 149.07 T 50.0 B 14.0 G 216 R 0000 +2024/11/24 18:54:01 ID 009 D 149.07 T 51.8 B 14.0 G 217 R 0000 +2024/11/25 02:54:01 ID 009 D 149.07 T 52.1 B 14.0 G 217 R 0000 +2024/11/25 10:54:01 ID 009 D 149.29 T 51.8 B 14.0 G 217 R 0000 +2024/11/25 18:54:01 ID 009 D 149.23 T 52.9 B 14.0 G 217 R 0000 +2024/11/26 02:54:01 ID 009 D 149.40 T 52.9 B 14.0 G 217 R 0000 +2024/11/26 10:54:01 ID 009 D 149.35 T 52.3 B 14.0 G 217 R 0000 +2024/11/26 18:54:02 ID 009 D 149.13 T 54.8 B 13.9 G 219 R 0001 +2024/11/27 02:54:01 ID 009 D 149.01 T 54.2 B 14.0 G 219 R 0000 +2024/11/27 10:54:01 ID 009 D 149.13 T 54.1 B 14.0 G 219 R 0000 +2024/11/27 18:54:01 ID 009 D 149.18 T 54.4 B 14.0 G 219 R 0000 +2024/11/28 02:54:01 ID 009 D 149.13 T 53.9 B 14.0 G 218 R 0000 +2024/11/28 10:54:01 ID 009 D 149.13 T 52.9 B 14.0 G 218 R 0000 +2024/11/28 18:54:01 ID 009 D 149.24 T 53.1 B 14.0 G 218 R 0000 +2024/11/29 02:54:01 ID 009 D 149.24 T 51.8 B 14.0 G 218 R 0000 +2024/11/29 10:54:01 ID 009 D 149.35 T 50.6 B 14.0 G 217 R 0000 +2024/11/29 18:54:01 ID 009 D 149.23 T 50.8 B 14.0 G 217 R 0000 +2024/11/30 02:54:01 ID 009 D 149.29 T 50.4 B 14.0 G 217 R 0000 +2024/11/30 10:54:01 ID 009 D 149.29 T 49.2 B 14.0 G 216 R 0000 +2024/11/30 18:54:01 ID 009 D 149.23 T 50.2 B 14.0 G 216 R 0000 +2024/12/01 02:54:01 ID 009 D 149.34 T 49.2 B 14.0 G 216 R 0000 +2024/12/01 10:54:01 ID 009 D 149.40 T 48.3 B 14.0 G 216 R 0000 +2024/12/01 18:54:01 ID 009 D 149.34 T 49.0 B 14.0 G 216 R 0000 +2024/12/02 02:54:01 ID 009 D 149.29 T 48.7 B 14.0 G 216 R 0000 +2024/12/02 10:54:01 ID 009 D 149.34 T 47.9 B 14.0 G 216 R 0000 +2024/12/02 18:54:01 ID 009 D 149.40 T 48.5 B 14.0 G 216 R 0000 +2024/12/03 02:54:01 ID 009 D 149.34 T 47.9 B 14.0 G 216 R 0000 +2024/12/03 10:54:01 ID 009 D 149.34 T 47.9 B 14.0 G 216 R 0000 +2024/12/03 18:54:01 ID 009 D 149.29 T 49.4 B 14.0 G 216 R 0000 +2024/12/04 02:54:01 ID 009 D 149.23 T 49.2 B 14.0 G 216 R 0000 +2024/12/04 10:54:01 ID 009 D 149.40 T 47.9 B 14.0 G 216 R 0000 +2024/12/04 18:54:01 ID 009 D 149.29 T 49.0 B 14.0 G 216 R 0000 +2024/12/05 02:54:01 ID 009 D 149.29 T 48.3 B 14.0 G 216 R 0000 +2024/12/05 10:54:01 ID 009 D 149.40 T 48.5 B 14.0 G 216 R 0000 +2024/12/05 18:54:01 ID 009 D 149.34 T 48.7 B 14.0 G 216 R 0000 +2024/12/06 02:54:01 ID 009 D 149.34 T 48.5 B 14.0 G 216 R 0000 +2024/12/06 10:54:01 ID 009 D 149.23 T 48.1 B 14.0 G 216 R 0000 +2024/12/06 18:54:01 ID 009 D 149.29 T 48.5 B 14.0 G 216 R 0000 +2024/12/07 02:54:01 ID 009 D 149.40 T 47.7 B 14.0 G 216 R 0000 +2024/12/07 10:54:01 ID 009 D 149.34 T 47.3 B 14.0 G 215 R 0000 +2024/12/07 18:54:01 ID 009 D 149.23 T 48.7 B 14.0 G 215 R 0000 +2024/12/08 02:54:01 ID 009 D 149.23 T 47.7 B 14.0 G 215 R 0000 +2024/12/08 10:54:01 ID 009 D 149.17 T 47.1 B 14.0 G 215 R 0000 +2024/12/08 18:54:01 ID 009 D 149.12 T 48.1 B 14.0 G 215 R 0000 +2024/12/09 02:54:01 ID 009 D 149.17 T 47.9 B 14.0 G 215 R 0000 +2024/12/09 10:54:01 ID 009 D 149.17 T 48.1 B 14.0 G 215 R 0000 +2024/12/09 18:54:01 ID 009 D 149.17 T 48.8 B 14.0 G 215 R 0000 +2024/12/10 02:54:02 ID 009 D 149.23 T 47.7 B 13.9 G 216 R 0001 +2024/12/10 10:54:03 ID 009 D 149.45 T 47.3 B 13.9 G 216 R 0000 +2024/12/10 18:54:02 ID 009 D 149.40 T 46.9 B 13.9 G 216 R 0001 +2024/12/11 02:54:03 ID 009 D 149.39 T 45.3 B 13.9 G 215 R 0000 +2024/12/11 10:54:01 ID 009 D 149.45 T 44.7 B 14.0 G 215 R 0000 +2024/12/11 18:54:01 ID 009 D 149.28 T 45.5 B 14.0 G 215 R 0000 +2024/12/12 02:54:01 ID 009 D 149.40 T 44.7 B 14.0 G 215 R 0000 +2024/12/12 10:54:01 ID 009 D 149.23 T 44.5 B 14.0 G 214 R 0000 +2024/12/12 18:54:01 ID 009 D 149.00 T 44.3 B 14.0 G 214 R 0000 +2024/12/13 02:54:01 ID 009 D 149.17 T 44.5 B 14.0 G 214 R 0000 +2024/12/13 10:54:01 ID 009 D 149.39 T 44.7 B 14.0 G 214 R 0000 +2024/12/13 18:54:01 ID 009 D 149.39 T 45.5 B 14.0 G 214 R 0000 +2024/12/14 02:54:01 ID 009 D 149.45 T 44.7 B 14.0 G 214 R 0000 +2024/12/14 10:54:01 ID 009 D 149.45 T 44.3 B 14.0 G 214 R 0000 +2024/12/14 18:54:01 ID 009 D 149.34 T 46.7 B 14.0 G 215 R 0000 +2024/12/15 02:54:01 ID 009 D 149.28 T 46.3 B 14.0 G 215 R 0000 +2024/12/15 10:54:01 ID 009 D 149.45 T 45.5 B 14.0 G 215 R 0000 +2024/12/15 18:54:01 ID 009 D 149.34 T 46.3 B 14.0 G 215 R 0000 +2024/12/16 02:54:01 ID 009 D 149.40 T 46.3 B 14.0 G 215 R 0000 +2024/12/16 10:54:01 ID 009 D 149.40 T 45.1 B 14.0 G 215 R 0000 +2024/12/16 18:54:01 ID 009 D 149.34 T 46.5 B 14.0 G 215 R 0000 +2024/12/17 02:54:01 ID 009 D 149.28 T 46.1 B 14.0 G 215 R 0000 +2024/12/17 10:54:01 ID 009 D 149.45 T 45.1 B 14.0 G 215 R 0000 +2024/12/17 18:54:01 ID 009 D 149.39 T 46.7 B 14.0 G 215 R 0000 +2024/12/18 02:54:01 ID 009 D 149.56 T 46.3 B 14.0 G 215 R 0000 +2024/12/18 10:54:01 ID 009 D 149.67 T 45.7 B 14.0 G 215 R 0000 +2024/12/18 18:54:01 ID 009 D 149.51 T 46.3 B 14.0 G 215 R 0000 +2024/12/19 02:54:01 ID 009 D 149.51 T 45.5 B 14.0 G 215 R 0000 +2024/12/19 10:54:01 ID 009 D 149.51 T 45.5 B 14.2 G 215 R 0000 +2024/12/19 18:54:01 ID 009 D 149.51 T 46.1 B 14.0 G 215 R 0000 +2024/12/20 02:54:01 ID 009 D 149.51 T 45.7 B 14.0 G 215 R 0000 +2024/12/20 10:54:01 ID 009 D 149.45 T 45.5 B 14.0 G 215 R 0000 +2024/12/20 18:54:01 ID 009 D 149.51 T 46.7 B 14.0 G 215 R 0000 +2024/12/21 02:54:01 ID 009 D 149.56 T 45.9 B 14.0 G 215 R 0000 +2024/12/21 10:54:02 ID 009 D 150.17 T 45.1 B 14.0 G 215 R 0001 +2024/12/21 18:54:01 ID 009 D 149.39 T 46.5 B 14.0 G 215 R 0000 +2024/12/22 02:54:01 ID 009 D 149.40 T 46.5 B 14.0 G 215 R 0000 +2024/12/22 10:54:01 ID 009 D 149.28 T 45.5 B 14.0 G 215 R 0000 +2024/12/22 18:54:01 ID 009 D 149.28 T 48.8 B 14.0 G 216 R 0000 +2024/12/23 02:54:01 ID 009 D 149.34 T 47.5 B 14.0 G 216 R 0000 +2024/12/23 10:54:01 ID 009 D 149.12 T 47.7 B 14.0 G 215 R 0000 +2024/12/23 18:54:01 ID 009 D 149.34 T 47.9 B 14.0 G 215 R 0000 +2024/12/24 02:54:01 ID 009 D 149.34 T 48.1 B 14.0 G 215 R 0000 +2024/12/24 10:54:01 ID 009 D 149.40 T 47.3 B 14.0 G 215 R 0000 +2024/12/24 18:54:01 ID 009 D 149.34 T 47.9 B 14.0 G 215 R 0000 +2024/12/25 02:54:01 ID 009 D 149.17 T 47.7 B 14.0 G 215 R 0000 +2024/12/25 10:54:01 ID 009 D 149.12 T 47.5 B 14.0 G 215 R 0000 +2024/12/25 18:54:01 ID 009 D 149.17 T 48.1 B 14.0 G 215 R 0000 +2024/12/26 02:54:01 ID 009 D 149.23 T 47.5 B 14.0 G 215 R 0000 +2024/12/26 10:54:01 ID 009 D 149.34 T 45.5 B 14.0 G 215 R 0000 +2024/12/26 18:54:01 ID 009 D 149.23 T 46.7 B 14.0 G 215 R 0000 +2024/12/27 02:54:01 ID 009 D 149.28 T 46.5 B 14.2 G 215 R 0000 +2024/12/27 10:54:01 ID 009 D 149.17 T 46.3 B 14.0 G 215 R 0000 +2024/12/27 18:54:01 ID 009 D 149.12 T 46.9 B 14.0 G 215 R 0000 +2024/12/28 03:02:55 ID 009 D 149.34 T 43.7 B 13.9 G 214 R 0001 +2024/12/28 11:02:01 ID 009 D 149.39 T 46.1 B 14.0 G 214 R 0000 +2024/12/28 19:02:01 ID 009 D 149.45 T 46.9 B 14.0 G 214 R 0000 +2024/12/29 03:02:01 ID 009 D 149.40 T 47.1 B 14.0 G 214 R 0000 +2024/12/29 11:02:01 ID 009 D 149.56 T 46.7 B 14.0 G 214 R 0000 +2024/12/29 19:02:01 ID 009 D 149.45 T 47.5 B 14.0 G 214 R 0000 +2024/12/30 03:02:01 ID 009 D 149.34 T 48.1 B 14.0 G 215 R 0000 +2024/12/30 11:02:01 ID 009 D 149.40 T 47.7 B 14.0 G 215 R 0000 +2024/12/30 19:02:01 ID 009 D 149.29 T 49.0 B 14.0 G 215 R 0000 +2024/12/31 03:02:01 ID 009 D 149.34 T 47.9 B 14.0 G 215 R 0000 +2024/12/31 11:02:01 ID 009 D 149.40 T 47.1 B 14.0 G 215 R 0000 +2024/12/31 19:02:01 ID 009 D 149.34 T 47.5 B 14.0 G 215 R 0000 +2025/01/01 03:02:01 ID 009 D 149.45 T 47.5 B 14.0 G 215 R 0000 +2025/01/01 11:02:01 ID 009 D 149.56 T 46.1 B 14.0 G 215 R 0000 +2025/01/01 19:02:01 ID 009 D 149.56 T 46.7 B 14.0 G 215 R 0000 +2025/01/02 03:02:01 ID 009 D 149.56 T 45.9 B 14.0 G 215 R 0000 +2025/01/02 11:02:01 ID 009 D 149.56 T 45.9 B 14.0 G 215 R 0000 +2025/01/02 19:02:01 ID 009 D 149.62 T 46.7 B 14.0 G 215 R 0000 +2025/01/03 03:02:01 ID 009 D 149.56 T 46.7 B 14.0 G 215 R 0000 +2025/01/03 11:02:01 ID 009 D 149.56 T 45.7 B 14.0 G 215 R 0000 +2025/01/03 19:02:01 ID 009 D 149.51 T 46.7 B 14.0 G 215 R 0000 +2025/01/04 03:02:01 ID 009 D 149.40 T 47.5 B 14.0 G 215 R 0000 +2025/01/04 11:02:01 ID 009 D 149.34 T 47.1 B 14.0 G 215 R 0000 +2025/01/04 19:02:01 ID 009 D 149.17 T 48.7 B 14.0 G 215 R 0000 +2025/01/05 03:02:02 ID 009 D 149.29 T 48.5 B 13.9 G 216 R 0001 +2025/01/05 11:02:02 ID 009 D 149.40 T 46.9 B 13.9 G 216 R 0001 +2025/01/05 19:02:03 ID 009 D 149.40 T 47.3 B 13.9 G 216 R 0000 +2025/01/06 03:02:03 ID 009 D 149.51 T 45.7 B 13.9 G 215 R 0000 +2025/01/06 11:02:01 ID 009 D 149.51 T 45.1 B 14.0 G 215 R 0000 +2025/01/06 19:02:01 ID 009 D 149.39 T 46.1 B 14.0 G 215 R 0000 +2025/01/07 03:02:01 ID 009 D 149.28 T 44.7 B 14.0 G 215 R 0000 +2025/01/07 11:02:01 ID 009 D 149.73 T 43.3 B 14.0 G 214 R 0000 +2025/01/07 19:02:01 ID 009 D 149.45 T 43.1 B 14.0 G 214 R 0000 +2025/01/08 03:02:01 ID 009 D 149.67 T 42.0 B 14.0 G 214 R 0000 +2025/01/08 11:02:01 ID 009 D 149.67 T 42.0 B 14.0 G 214 R 0000 +2025/01/08 19:02:01 ID 009 D 149.56 T 42.2 B 14.0 G 214 R 0000 +2025/01/09 03:02:01 ID 009 D 149.39 T 41.0 B 14.0 G 213 R 0000 +2025/01/09 11:02:01 ID 009 D 149.28 T 40.4 B 14.0 G 213 R 0000 +2025/01/09 19:02:01 ID 009 D 149.39 T 39.5 B 14.0 G 213 R 0000 +2025/01/10 03:02:03 ID 009 D 149.39 T 39.7 B 13.9 G 212 R 0000 +2025/01/10 11:02:03 ID 009 D 149.39 T 38.5 B 13.9 G 211 R 0000 +2025/01/10 19:02:03 ID 009 D 149.39 T 38.9 B 13.9 G 211 R 0000 +2025/01/11 03:02:03 ID 009 D 149.28 T 39.3 B 13.9 G 210 R 0000 +2025/01/11 11:02:03 ID 009 D 149.22 T 38.7 B 13.9 G 210 R 0000 +2025/01/11 19:02:03 ID 009 D 149.22 T 40.2 B 13.9 G 210 R 0000 +2025/01/12 03:02:03 ID 009 D 149.22 T 40.0 B 13.9 G 210 R 0000 +2025/01/12 11:02:03 ID 009 D 149.28 T 38.5 B 13.9 G 210 R 0000 +2025/01/12 19:02:03 ID 009 D 149.28 T 39.3 B 14.0 G 210 R 0000 +2025/01/13 03:02:03 ID 009 D 149.44 T 37.8 B 13.9 G 209 R 0000 +2025/01/13 11:02:03 ID 009 D 149.44 T 38.9 B 13.9 G 210 R 0000 +2025/01/13 19:02:03 ID 009 D 149.44 T 38.7 B 14.0 G 210 R 0000 +2025/01/14 03:02:03 ID 009 D 149.44 T 38.3 B 13.9 G 210 R 0000 +2025/01/14 11:02:03 ID 009 D 150.95 T 37.4 B 13.9 G 209 R 0000 +2025/01/14 19:02:03 ID 009 D 149.50 T 38.5 B 13.9 G 210 R 0000 +2025/01/15 03:02:03 ID 009 D 149.55 T 37.8 B 14.0 G 209 R 0000 +2025/01/15 11:02:03 ID 009 D 149.61 T 37.6 B 13.9 G 209 R 0000 +2025/01/15 19:02:03 ID 009 D 149.61 T 38.3 B 13.9 G 210 R 0000 +2025/01/16 03:02:03 ID 009 D 149.50 T 38.7 B 13.9 G 209 R 0000 +2025/01/16 11:02:03 ID 009 D 149.50 T 37.0 B 14.0 G 209 R 0000 +2025/01/16 19:02:03 ID 009 D 149.39 T 38.7 B 13.9 G 210 R 0000 +2025/01/17 03:02:03 ID 009 D 149.17 T 38.9 B 14.0 G 209 R 0000 +2025/01/17 13:25:35 ID 009 D 149.27 T 35.7 B 13.9 G 209 R 0001 +2025/01/17 21:25:03 ID 009 D 149.33 T 39.5 B 13.9 G 210 R 0000 +2025/01/18 05:25:03 ID 009 D 149.33 T 40.2 B 13.9 G 210 R 0000 +2025/01/18 13:25:03 ID 009 D 149.22 T 40.0 B 14.0 G 210 R 0000 +2025/01/18 21:25:03 ID 009 D 149.56 T 40.6 B 13.9 G 210 R 0000 +2025/01/19 05:25:03 ID 009 D 149.50 T 40.0 B 14.0 G 210 R 0000 +2025/01/19 13:25:03 ID 009 D 149.33 T 39.1 B 13.9 G 210 R 0000 +2025/01/19 21:25:03 ID 009 D 149.33 T 40.4 B 14.0 G 210 R 0000 +2025/01/20 05:25:03 ID 009 D 149.33 T 39.5 B 13.9 G 210 R 0000 +2025/01/20 13:25:03 ID 009 D 149.33 T 38.7 B 13.9 G 210 R 0000 +2025/01/20 21:25:03 ID 009 D 149.50 T 38.5 B 13.9 G 210 R 0000 +2025/01/21 05:25:03 ID 009 D 149.67 T 37.6 B 13.9 G 209 R 0000 +2025/01/21 13:25:03 ID 009 D 149.78 T 36.6 B 14.0 G 209 R 0000 +2025/01/21 21:25:03 ID 009 D 149.61 T 37.8 B 13.9 G 209 R 0000 +2025/01/22 05:25:03 ID 009 D 149.50 T 37.0 B 13.9 G 209 R 0000 +2025/01/22 13:25:03 ID 009 D 149.55 T 36.8 B 14.0 G 209 R 0000 +2025/01/22 21:25:03 ID 009 D 149.61 T 37.6 B 13.9 G 209 R 0000 +2025/01/23 05:25:03 ID 009 D 149.55 T 36.3 B 13.9 G 209 R 0000 +2025/01/23 13:25:03 ID 009 D 149.67 T 37.0 B 14.0 G 209 R 0000 +2025/01/23 21:25:03 ID 009 D 150.33 T 37.4 B 13.9 G 209 R 0000 +2025/01/24 05:25:03 ID 009 D 149.55 T 35.5 B 13.9 G 209 R 0000 +2025/01/24 13:25:03 ID 009 D 149.44 T 35.0 B 13.9 G 209 R 0000 +2025/01/24 21:25:03 ID 009 D 149.33 T 37.2 B 13.9 G 209 R 0000 +2025/01/25 05:25:03 ID 009 D 149.28 T 37.2 B 14.0 G 209 R 0000 +2025/01/25 13:25:03 ID 009 D 149.28 T 37.6 B 13.9 G 209 R 0000 +2025/01/25 21:25:02 ID 009 D 149.56 T 40.0 B 13.9 G 210 R 0000 +2025/01/26 05:25:03 ID 009 D 149.39 T 40.2 B 13.9 G 210 R 0000 +2025/01/26 13:25:03 ID 009 D 149.50 T 40.6 B 13.9 G 210 R 0000 +2025/01/26 21:25:03 ID 009 D 149.61 T 41.8 B 14.0 G 211 R 0000 +2025/01/27 05:25:03 ID 009 D 149.45 T 41.0 B 14.0 G 211 R 0000 +2025/01/27 13:25:03 ID 009 D 149.50 T 41.6 B 14.0 G 211 R 0000 +2025/01/27 21:25:03 ID 009 D 149.45 T 42.2 B 14.0 G 211 R 0000 +2025/01/28 05:25:03 ID 009 D 149.45 T 42.0 B 13.9 G 211 R 0000 +2025/01/28 13:25:03 ID 009 D 149.34 T 42.4 B 13.9 G 211 R 0000 +2025/01/28 21:25:03 ID 009 D 149.34 T 42.8 B 13.9 G 211 R 0000 +2025/01/29 05:25:03 ID 009 D 149.34 T 41.6 B 14.0 G 211 R 0000 +2025/01/29 13:25:03 ID 009 D 149.34 T 41.8 B 13.9 G 211 R 0000 +2025/01/29 21:25:03 ID 009 D 149.45 T 41.2 B 14.0 G 211 R 0000 +2025/01/30 05:25:03 ID 009 D 149.45 T 40.4 B 13.9 G 211 R 0000 +2025/01/30 13:25:03 ID 009 D 149.45 T 40.6 B 14.0 G 211 R 0000 +2025/01/30 21:25:03 ID 009 D 149.50 T 40.4 B 13.9 G 210 R 0000 +2025/01/31 05:25:03 ID 009 D 149.67 T 39.3 B 13.9 G 210 R 0000 +2025/01/31 17:34:56 ID 009 D 149.72 T 37.6 B 13.9 G 210 R 0001 +2025/02/01 01:34:03 ID 009 D 149.67 T 41.0 B 13.9 G 211 R 0000 +2025/02/01 09:34:03 ID 009 D 149.61 T 40.0 B 13.9 G 210 R 0000 +2025/02/01 17:34:03 ID 009 D 149.56 T 41.0 B 14.0 G 211 R 0000 +2025/02/02 01:34:03 ID 009 D 149.78 T 42.4 B 13.9 G 211 R 0000 +2025/02/02 09:34:03 ID 009 D 149.73 T 42.2 B 13.9 G 211 R 0000 +2025/02/02 17:34:01 ID 009 D 149.61 T 44.7 B 14.0 G 211 R 0000 +2025/02/03 01:34:01 ID 009 D 149.73 T 45.9 B 14.0 G 212 R 0000 +2025/02/03 09:34:01 ID 009 D 149.73 T 45.1 B 14.0 G 212 R 0000 +2025/02/03 17:34:01 ID 009 D 149.67 T 47.3 B 14.0 G 212 R 0000 +2025/02/04 01:34:01 ID 009 D 149.68 T 47.9 B 14.0 G 213 R 0000 +2025/02/04 09:34:01 ID 009 D 149.68 T 47.7 B 14.0 G 213 R 0000 +2025/02/04 17:34:01 ID 009 D 149.68 T 49.2 B 14.0 G 213 R 0000 +2025/02/05 01:34:01 ID 009 D 149.62 T 49.6 B 14.0 G 213 R 0000 +2025/02/05 09:34:01 ID 009 D 149.62 T 49.6 B 14.0 G 213 R 0000 +2025/02/05 17:34:01 ID 009 D 149.57 T 50.4 B 14.0 G 214 R 0000 +2025/02/06 01:34:01 ID 009 D 149.68 T 50.4 B 14.0 G 214 R 0000 +2025/02/06 09:34:01 ID 009 D 149.73 T 49.6 B 14.0 G 214 R 0000 +2025/02/06 17:34:01 ID 009 D 149.62 T 51.0 B 14.0 G 214 R 0000 +2025/02/07 01:34:01 ID 009 D 149.68 T 51.4 B 14.0 G 214 R 0000 +2025/02/07 09:34:01 ID 009 D 149.79 T 50.4 B 14.0 G 214 R 0000 +2025/02/07 17:34:01 ID 009 D 149.62 T 51.4 B 14.0 G 214 R 0000 +2025/02/08 01:34:01 ID 009 D 149.57 T 51.8 B 14.0 G 215 R 0000 +2025/02/08 09:34:01 ID 009 D 149.68 T 51.4 B 14.0 G 215 R 0000 +2025/02/08 17:34:01 ID 009 D 149.57 T 52.5 B 14.0 G 215 R 0000 +2025/02/09 01:34:01 ID 009 D 148.40 T 52.7 B 14.0 G 215 R 0000 +2025/02/09 09:34:01 ID 009 D 149.79 T 51.6 B 14.0 G 215 R 0000 +2025/02/09 17:34:01 ID 009 D 148.40 T 53.3 B 14.0 G 216 R 0000 +2025/02/10 01:34:01 ID 009 D 149.63 T 53.5 B 14.0 G 216 R 0000 +2025/02/10 09:34:01 ID 009 D 149.57 T 53.7 B 14.0 G 216 R 0000 +2025/02/10 17:34:01 ID 009 D 149.57 T 53.7 B 14.0 G 216 R 0000 +2025/02/11 01:34:01 ID 009 D 149.57 T 53.5 B 14.0 G 216 R 0000 +2025/02/11 09:34:01 ID 009 D 149.46 T 52.9 B 14.0 G 216 R 0000 +2025/02/11 17:34:01 ID 009 D 149.29 T 52.7 B 14.0 G 216 R 0000 +2025/02/12 01:34:01 ID 009 D 149.35 T 52.0 B 14.0 G 216 R 0000 +2025/02/12 09:34:01 ID 009 D 149.57 T 51.0 B 14.0 G 216 R 0000 +2025/02/12 17:34:01 ID 009 D 149.62 T 50.6 B 14.0 G 216 R 0000 +2025/02/13 01:34:01 ID 009 D 149.73 T 49.6 B 14.0 G 215 R 0000 +2025/02/13 09:34:01 ID 009 D 149.79 T 48.3 B 14.0 G 214 R 0000 +2025/02/13 17:34:01 ID 009 D 149.79 T 49.0 B 14.0 G 214 R 0000 +2025/02/14 01:34:01 ID 009 D 149.79 T 49.2 B 14.0 G 214 R 0000 +2025/02/14 09:34:01 ID 009 D 153.08 T 49.4 B 14.0 G 214 R 0000 +2025/02/14 17:34:01 ID 009 D 149.68 T 50.2 B 14.0 G 214 R 0000 +2025/02/15 01:34:01 ID 009 D 149.68 T 50.2 B 14.0 G 214 R 0000 +2025/02/15 09:34:01 ID 009 D 149.73 T 49.4 B 14.0 G 214 R 0000 +2025/02/15 17:34:01 ID 009 D 149.73 T 49.4 B 14.0 G 214 R 0000 +2025/02/16 01:34:01 ID 009 D 149.85 T 48.7 B 14.0 G 214 R 0000 +2025/02/16 09:34:01 ID 009 D 149.90 T 47.1 B 14.0 G 213 R 0000 +2025/02/16 17:34:01 ID 009 D 149.79 T 49.2 B 14.0 G 213 R 0000 +2025/02/17 01:34:01 ID 009 D 149.62 T 49.2 B 14.0 G 213 R 0000 +2025/02/17 09:34:01 ID 009 D 149.68 T 48.1 B 14.0 G 213 R 0000 +2025/02/17 17:34:01 ID 009 D 149.62 T 48.5 B 14.0 G 213 R 0000 +2025/02/18 01:34:01 ID 009 D 149.79 T 49.4 B 14.0 G 213 R 0000 +2025/02/18 09:34:01 ID 009 D 149.73 T 48.8 B 14.0 G 213 R 0000 +2025/02/18 17:34:01 ID 009 D 149.73 T 49.0 B 14.0 G 214 R 0000 +2025/02/19 01:34:01 ID 009 D 149.84 T 48.7 B 14.0 G 214 R 0000 +2025/02/19 09:34:01 ID 009 D 149.84 T 47.7 B 14.0 G 214 R 0000 +2025/02/19 17:34:01 ID 009 D 149.79 T 48.7 B 14.0 G 214 R 0000 +2025/02/20 01:34:01 ID 009 D 149.84 T 48.7 B 14.0 G 214 R 0000 +2025/02/20 09:34:01 ID 009 D 149.79 T 48.7 B 14.1 G 214 R 0000 +2025/02/20 17:34:01 ID 009 D 149.68 T 49.6 B 14.0 G 214 R 0000 +2025/02/21 01:34:01 ID 009 D 148.45 T 50.0 B 14.0 G 214 R 0000 +2025/02/21 09:34:01 ID 009 D 154.14 T 48.7 B 14.0 G 214 R 0000 +2025/02/21 17:34:01 ID 009 D 149.79 T 50.0 B 14.0 G 214 R 0000 +2025/02/22 01:34:01 ID 009 D 149.90 T 49.6 B 14.0 G 214 R 0000 +2025/02/22 09:34:01 ID 009 D 149.96 T 48.3 B 14.0 G 214 R 0000 +2025/02/22 17:34:01 ID 009 D 149.90 T 49.6 B 14.0 G 214 R 0000 +2025/02/23 01:34:01 ID 009 D 149.96 T 49.2 B 14.0 G 214 R 0000 +2025/02/23 09:34:01 ID 009 D 149.96 T 47.9 B 14.0 G 214 R 0000 +2025/02/23 17:34:01 ID 009 D 149.79 T 49.4 B 14.0 G 214 R 0000 +2025/02/24 01:34:01 ID 009 D 149.96 T 49.6 B 14.0 G 214 R 0000 +2025/02/24 09:34:01 ID 009 D 149.90 T 49.0 B 14.0 G 214 R 0000 +2025/02/24 17:34:01 ID 009 D 148.56 T 50.2 B 14.0 G 214 R 0000 +2025/02/25 01:34:01 ID 009 D 148.62 T 51.4 B 14.0 G 214 R 0000 +2025/02/25 09:34:01 ID 009 D 149.90 T 50.2 B 14.0 G 214 R 0000 +2025/02/25 17:34:01 ID 009 D 149.74 T 52.9 B 14.0 G 216 R 0000 +2025/02/26 01:34:01 ID 009 D 149.85 T 52.5 B 14.0 G 216 R 0000 +2025/02/26 09:34:01 ID 009 D 149.74 T 52.0 B 14.0 G 216 R 0000 +2025/02/26 17:34:01 ID 009 D 149.74 T 52.5 B 14.0 G 216 R 0000 +2025/02/27 02:24:31 ID 009 D 149.90 T 49.0 B 13.9 G 215 R 0001 +2025/02/27 10:24:01 ID 009 D 150.07 T 52.0 B 14.0 G 215 R 0000 +2025/02/27 18:24:01 ID 009 D 149.90 T 52.9 B 14.0 G 216 R 0000 +2025/02/28 02:24:01 ID 009 D 149.96 T 52.1 B 14.0 G 216 R 0000 +2025/02/28 10:24:01 ID 009 D 149.85 T 50.8 B 14.0 G 215 R 0000 +2025/02/28 18:24:02 ID 009 D 149.68 T 52.1 B 13.9 G 216 R 0001 +2025/03/01 02:24:01 ID 009 D 149.79 T 52.0 B 14.0 G 216 R 0000 +2025/03/01 10:24:01 ID 009 D 149.79 T 51.4 B 14.0 G 216 R 0000 +2025/03/01 18:24:01 ID 009 D 149.74 T 52.1 B 14.0 G 216 R 0000 +2025/03/02 02:24:01 ID 009 D 149.68 T 52.9 B 14.0 G 216 R 0000 +2025/03/02 10:24:02 ID 009 D 149.73 T 50.4 B 14.0 G 215 R 0001 +2025/03/02 18:24:01 ID 009 D 149.73 T 53.9 B 14.0 G 216 R 0000 +2025/03/03 02:24:01 ID 009 D 149.79 T 53.5 B 14.0 G 216 R 0000 +2025/03/03 10:24:01 ID 009 D 149.51 T 52.9 B 14.0 G 216 R 0000 +2025/03/03 18:24:02 ID 009 D 149.46 T 53.3 B 14.0 G 217 R 0001 +2025/03/04 10:15:43 ID 009 D 149.79 T 48.3 B 13.9 G 215 R 0001 +2025/03/04 18:15:02 ID 009 D 149.90 T 51.6 B 14.0 G 216 R 0001 +2025/03/05 02:15:01 ID 009 D 149.96 T 51.4 B 14.0 G 216 R 0000 +2025/03/05 10:15:01 ID 009 D 149.96 T 50.4 B 14.0 G 215 R 0000 +2025/03/05 18:15:01 ID 009 D 149.79 T 51.4 B 14.0 G 215 R 0000 +2025/03/06 02:15:01 ID 009 D 149.68 T 51.8 B 14.0 G 215 R 0000 +2025/03/06 10:15:01 ID 009 D 149.74 T 52.0 B 14.0 G 215 R 0000 +2025/03/06 18:15:02 ID 009 D 149.57 T 52.9 B 14.0 G 216 R 0001 +2025/03/07 02:15:03 ID 009 D 149.74 T 52.3 B 14.0 G 217 R 0000 +2025/03/07 10:15:03 ID 009 D 149.74 T 52.0 B 14.0 G 216 R 0000 +2025/03/07 18:15:02 ID 009 D 149.62 T 52.0 B 13.9 G 216 R 0001 +2025/03/08 02:15:01 ID 009 D 149.85 T 50.8 B 14.0 G 216 R 0000 +2025/03/08 10:15:01 ID 009 D 150.24 T 50.0 B 14.0 G 216 R 0000 +2025/03/08 18:15:01 ID 009 D 149.90 T 49.8 B 14.0 G 215 R 0000 +2025/03/09 02:15:01 ID 009 D 150.07 T 48.5 B 14.0 G 214 R 0000 +2025/03/09 14:03:00 ID 009 D 150.12 T 44.3 B 13.9 G 213 R 0001 +2025/03/09 22:02:01 ID 009 D 150.23 T 48.3 B 14.0 G 213 R 0000 +2025/03/10 06:02:01 ID 009 D 150.07 T 48.3 B 14.0 G 213 R 0000 +2025/03/10 14:02:01 ID 009 D 149.90 T 48.5 B 14.0 G 213 R 0000 +2025/03/10 22:02:01 ID 009 D 149.84 T 49.4 B 14.0 G 214 R 0000 +2025/03/11 06:02:01 ID 009 D 149.90 T 49.0 B 14.0 G 214 R 0000 +2025/03/11 14:02:01 ID 009 D 149.79 T 49.8 B 14.0 G 214 R 0000 +2025/03/11 22:02:01 ID 009 D 149.96 T 51.0 B 14.0 G 214 R 0000 +2025/03/12 06:02:01 ID 009 D 149.79 T 51.0 B 14.0 G 214 R 0000 +2025/03/12 14:02:01 ID 009 D 149.90 T 51.0 B 14.0 G 214 R 0000 +2025/03/12 22:02:01 ID 009 D 150.01 T 51.2 B 14.0 G 215 R 0000 +2025/03/13 06:02:01 ID 009 D 149.90 T 51.0 B 14.0 G 215 R 0000 +2025/03/13 14:02:01 ID 009 D 149.79 T 51.8 B 14.0 G 215 R 0000 +2025/03/13 22:02:01 ID 009 D 148.18 T 53.3 B 14.0 G 216 R 0000 +2025/03/14 06:02:01 ID 009 D 149.63 T 52.0 B 14.0 G 216 R 0000 +2025/03/14 14:02:02 ID 009 D 149.62 T 51.2 B 13.9 G 216 R 0001 +2025/03/14 22:02:01 ID 009 D 149.79 T 51.0 B 14.0 G 216 R 0000 +2025/03/15 06:02:01 ID 009 D 149.68 T 50.6 B 14.0 G 215 R 0000 +2025/03/15 14:02:01 ID 009 D 149.79 T 49.6 B 14.0 G 215 R 0000 +2025/03/15 22:02:01 ID 009 D 150.01 T 49.6 B 14.0 G 215 R 0000 +2025/03/16 06:02:01 ID 009 D 150.07 T 48.1 B 14.0 G 214 R 0000 +2025/03/16 14:02:01 ID 009 D 150.07 T 48.3 B 14.0 G 214 R 0000 +2025/03/16 22:02:01 ID 009 D 150.18 T 49.6 B 14.0 G 214 R 0000 +2025/03/17 06:02:01 ID 009 D 150.12 T 49.2 B 14.0 G 214 R 0000 +2025/03/17 14:02:01 ID 009 D 150.07 T 49.6 B 14.0 G 214 R 0000 +2025/03/17 22:02:01 ID 009 D 149.96 T 51.0 B 14.0 G 214 R 0000 +2025/03/18 06:02:01 ID 009 D 149.90 T 51.4 B 14.0 G 214 R 0000 +2025/03/18 16:01:24 ID 009 D 149.73 T 49.4 B 13.9 G 215 R 0001 +2025/03/19 00:01:02 ID 009 D 150.02 T 52.1 B 14.0 G 216 R 0001 +2025/03/19 08:01:01 ID 009 D 150.07 T 51.2 B 14.0 G 216 R 0000 +2025/03/19 16:01:01 ID 009 D 150.18 T 50.8 B 14.0 G 216 R 0000 +2025/03/20 00:01:01 ID 009 D 150.18 T 50.6 B 14.0 G 216 R 0000 +2025/03/20 08:01:03 ID 009 D 150.12 T 48.5 B 13.9 G 214 R 0000 +2025/03/20 16:01:01 ID 009 D 149.90 T 49.4 B 14.0 G 214 R 0000 +2025/03/21 00:01:01 ID 009 D 149.96 T 50.0 B 14.0 G 214 R 0000 +2025/03/21 08:01:01 ID 009 D 149.96 T 49.2 B 14.0 G 214 R 0000 +2025/03/21 16:01:03 ID 009 D 149.95 T 47.9 B 13.9 G 214 R 0001 +2025/03/22 00:01:01 ID 009 D 150.01 T 51.4 B 14.0 G 214 R 0000 +2025/03/22 08:01:01 ID 009 D 149.96 T 50.8 B 14.0 G 214 R 0000 +2025/03/22 16:01:01 ID 009 D 149.96 T 51.6 B 14.0 G 215 R 0000 +2025/03/23 00:01:01 ID 009 D 148.68 T 53.1 B 14.0 G 215 R 0000 +2025/03/23 08:01:01 ID 009 D 150.24 T 52.9 B 14.0 G 215 R 0000 +2025/03/23 16:01:01 ID 009 D 150.02 T 53.1 B 14.0 G 215 R 0000 +2025/03/24 00:01:01 ID 009 D 150.24 T 52.9 B 14.0 G 215 R 0000 +2025/03/24 08:01:01 ID 009 D 160.16 T 52.0 B 14.0 G 215 R 0000 +2025/03/24 16:01:01 ID 009 D 150.13 T 53.1 B 14.0 G 215 R 0000 +2025/03/25 00:01:01 ID 009 D 150.24 T 53.9 B 14.0 G 215 R 0000 +2025/03/25 08:01:01 ID 009 D 150.30 T 53.7 B 14.0 G 215 R 0000 +2025/03/25 16:01:01 ID 009 D 150.07 T 54.6 B 14.0 G 215 R 0000 +2025/03/26 00:01:01 ID 009 D 148.79 T 55.9 B 14.0 G 216 R 0000 +2025/03/26 08:01:01 ID 009 D 150.13 T 55.0 B 14.0 G 216 R 0000 +2025/03/26 16:01:01 ID 009 D 148.62 T 56.1 B 14.0 G 216 R 0000 +2025/03/27 00:01:01 ID 009 D 150.19 T 57.1 B 14.0 G 217 R 0000 +2025/03/27 08:01:01 ID 009 D 150.08 T 56.7 B 14.0 G 217 R 0000 +2025/03/27 16:01:01 ID 009 D 149.96 T 57.1 B 14.0 G 217 R 0000 +2025/03/28 00:01:01 ID 009 D 149.96 T 57.6 B 14.0 G 217 R 0000 +2025/03/28 08:01:01 ID 009 D 149.97 T 57.1 B 14.0 G 217 R 0000 +2025/03/28 16:01:01 ID 009 D 149.85 T 57.8 B 14.0 G 217 R 0000 +2025/03/29 00:01:01 ID 009 D 149.91 T 58.4 B 14.0 G 217 R 0000 +2025/03/29 08:01:01 ID 009 D 149.91 T 57.8 B 14.0 G 217 R 0000 +2025/03/29 16:01:01 ID 009 D 149.85 T 57.8 B 14.0 G 217 R 0000 +2025/03/30 00:01:01 ID 009 D 150.02 T 57.6 B 14.0 G 217 R 0000 +2025/03/30 08:01:01 ID 009 D 150.41 T 56.5 B 14.0 G 217 R 0000 +2025/03/30 16:01:02 ID 009 D 151.47 T 56.9 B 13.9 G 213 R 0000 +2025/03/31 00:01:01 ID 009 D 150.08 T 57.4 B 14.0 G 213 R 0000 +2025/03/31 08:01:01 ID 009 D 150.13 T 56.3 B 14.0 G 213 R 0000 +2025/03/31 16:01:01 ID 009 D 150.08 T 56.7 B 14.0 G 213 R 0000 +2025/04/01 00:01:01 ID 009 D 149.96 T 57.4 B 14.0 G 213 R 0000 +2025/04/01 08:01:01 ID 009 D 149.80 T 57.3 B 14.0 G 213 R 0000 +2025/04/01 16:01:01 ID 009 D 149.63 T 57.4 B 14.0 G 213 R 0000 +2025/04/02 00:01:01 ID 009 D 149.91 T 57.1 B 14.0 G 213 R 0000 +2025/04/02 08:01:01 ID 009 D 150.24 T 56.1 B 14.1 G 213 R 0000 +2025/04/02 16:01:01 ID 009 D 149.80 T 55.9 B 14.0 G 213 R 0000 +2025/04/03 00:01:01 ID 009 D 150.02 T 55.6 B 14.0 G 213 R 0000 +2025/04/03 08:01:01 ID 009 D 150.02 T 54.2 B 14.0 G 212 R 0000 +2025/04/03 16:01:01 ID 009 D 149.91 T 54.8 B 14.0 G 212 R 0000 +2025/04/04 00:01:01 ID 009 D 150.13 T 54.8 B 14.0 G 212 R 0000 +2025/04/04 08:01:01 ID 009 D 150.02 T 53.1 B 14.0 G 211 R 0000 +2025/04/04 16:01:01 ID 009 D 150.13 T 53.5 B 14.0 G 211 R 0000 +2025/04/05 00:01:01 ID 009 D 150.18 T 53.9 B 14.0 G 211 R 0000 +2025/04/05 08:01:01 ID 009 D 151.41 T 53.3 B 14.0 G 211 R 0000 +2025/04/05 16:01:01 ID 009 D 150.29 T 52.5 B 14.0 G 211 R 0000 +2025/04/06 00:01:01 ID 009 D 150.29 T 52.0 B 14.0 G 211 R 0000 +2025/04/06 08:01:01 ID 009 D 150.29 T 50.8 B 14.0 G 210 R 0000 +2025/04/06 16:01:01 ID 009 D 150.24 T 51.4 B 14.0 G 210 R 0000 +2025/04/07 00:01:01 ID 009 D 150.24 T 52.0 B 14.0 G 210 R 0000 +2025/04/07 08:01:01 ID 009 D 150.29 T 50.6 B 14.0 G 210 R 0000 +2025/04/07 16:01:01 ID 009 D 150.24 T 51.8 B 14.0 G 210 R 0000 +2025/04/08 00:01:01 ID 009 D 148.90 T 52.9 B 14.0 G 210 R 0000 +2025/04/08 08:01:01 ID 009 D 150.29 T 52.0 B 14.0 G 210 R 0000 +2025/04/08 16:01:01 ID 009 D 148.79 T 53.7 B 14.0 G 210 R 0000 +2025/04/09 00:01:01 ID 009 D 150.18 T 54.8 B 14.0 G 211 R 0000 +2025/04/09 08:01:01 ID 009 D 150.35 T 54.1 B 14.0 G 211 R 0000 +2025/04/09 16:01:01 ID 009 D 150.24 T 55.4 B 14.0 G 211 R 0000 +2025/04/10 00:01:01 ID 009 D 150.41 T 56.5 B 14.0 G 212 R 0000 +2025/04/10 08:01:01 ID 009 D 150.30 T 56.1 B 14.0 G 212 R 0000 +2025/04/10 16:01:01 ID 009 D 150.30 T 57.3 B 14.0 G 213 R 0000 +2025/04/11 00:01:01 ID 009 D 150.30 T 58.6 B 14.0 G 213 R 0000 +2025/04/11 08:01:01 ID 009 D 150.24 T 58.4 B 14.0 G 213 R 0000 +2025/04/11 16:01:02 ID 009 D 150.30 T 59.5 B 14.0 G 214 R 0001 +2025/04/12 00:01:01 ID 009 D 150.19 T 60.8 B 14.0 G 214 R 0000 +2025/04/12 08:01:01 ID 009 D 150.14 T 59.5 B 14.0 G 214 R 0000 +2025/04/12 16:01:01 ID 009 D 150.02 T 60.8 B 14.0 G 215 R 0000 +2025/04/13 00:01:01 ID 009 D 150.08 T 62.1 B 14.0 G 215 R 0000 +2025/04/13 08:01:01 ID 009 D 150.19 T 61.7 B 14.0 G 215 R 0000 +2025/04/13 16:01:01 ID 009 D 149.97 T 62.1 B 14.0 G 215 R 0000 +2025/04/14 00:01:01 ID 009 D 150.25 T 62.4 B 14.0 G 215 R 0000 +2025/04/14 08:01:01 ID 009 D 151.86 T 61.9 B 14.0 G 215 R 0000 +2025/04/14 16:01:01 ID 009 D 150.19 T 61.9 B 14.0 G 215 R 0000 +2025/04/15 00:01:01 ID 009 D 150.30 T 62.6 B 14.0 G 215 R 0000 +2025/04/15 08:01:01 ID 009 D 150.30 T 61.7 B 14.0 G 215 R 0000 +2025/04/15 16:01:01 ID 009 D 150.19 T 63.2 B 14.0 G 216 R 0000 +2025/04/16 00:01:01 ID 009 D 150.19 T 63.2 B 14.0 G 216 R 0000 +2025/04/16 08:01:01 ID 009 D 150.08 T 62.8 B 14.0 G 216 R 0000 +2025/04/16 16:01:01 ID 009 D 149.97 T 63.3 B 14.0 G 216 R 0000 +2025/04/17 00:01:01 ID 009 D 150.03 T 64.1 B 14.0 G 216 R 0000 +2025/04/17 08:01:01 ID 009 D 150.03 T 63.5 B 14.0 G 216 R 0000 +2025/04/17 16:01:01 ID 009 D 149.92 T 64.3 B 14.0 G 216 R 0000 +2025/04/18 00:01:01 ID 009 D 149.97 T 64.1 B 14.0 G 216 R 0000 +2025/04/18 08:01:01 ID 009 D 149.97 T 63.0 B 14.0 G 216 R 0000 +2025/04/18 16:01:01 ID 009 D 149.91 T 63.3 B 14.0 G 216 R 0000 +2025/04/19 00:01:01 ID 009 D 150.03 T 62.8 B 14.0 G 216 R 0000 +2025/04/19 08:01:01 ID 009 D 150.14 T 62.1 B 14.0 G 216 R 0000 +2025/04/19 16:01:01 ID 009 D 151.53 T 60.8 B 14.0 G 216 R 0000 +2025/04/20 00:01:01 ID 009 D 150.25 T 59.9 B 14.0 G 215 R 0000 +2025/04/20 08:01:01 ID 009 D 157.44 T 57.6 B 14.0 G 214 R 0000 +2025/04/20 16:01:01 ID 009 D 150.19 T 58.2 B 14.0 G 214 R 0000 +2025/04/21 00:01:01 ID 009 D 150.24 T 58.7 B 14.0 G 214 R 0000 +2025/04/21 08:01:01 ID 009 D 150.30 T 57.6 B 14.0 G 214 R 0000 +2025/04/21 16:01:01 ID 009 D 150.19 T 58.6 B 14.0 G 214 R 0000 +2025/04/22 00:01:01 ID 009 D 150.24 T 59.3 B 14.0 G 214 R 0000 +2025/04/22 08:01:01 ID 009 D 150.19 T 58.4 B 14.0 G 214 R 0000 +2025/04/22 16:01:01 ID 009 D 150.08 T 59.5 B 14.0 G 214 R 0000 +2025/04/23 00:01:01 ID 009 D 150.19 T 60.6 B 14.0 G 215 R 0000 +2025/04/23 08:01:01 ID 009 D 150.25 T 59.9 B 14.0 G 215 R 0000 +2025/04/23 16:01:01 ID 009 D 150.13 T 60.8 B 14.0 G 215 R 0000 +2025/04/24 00:01:01 ID 009 D 148.74 T 61.7 B 14.0 G 215 R 0000 +2025/04/24 08:01:01 ID 009 D 150.30 T 60.6 B 14.0 G 215 R 0000 +2025/04/24 16:01:01 ID 009 D 150.14 T 61.5 B 14.0 G 215 R 0000 +2025/04/25 00:01:01 ID 009 D 150.25 T 62.2 B 14.0 G 216 R 0000 +2025/04/25 08:01:01 ID 009 D 150.30 T 60.8 B 14.0 G 216 R 0000 +2025/04/25 16:01:01 ID 009 D 150.14 T 61.9 B 14.0 G 216 R 0000 +2025/04/26 00:01:01 ID 009 D 150.25 T 62.6 B 14.0 G 216 R 0000 +2025/04/26 08:01:01 ID 009 D 150.30 T 61.7 B 14.0 G 216 R 0000 +2025/04/26 16:01:01 ID 009 D 148.74 T 64.8 B 14.0 G 217 R 0000 +2025/04/27 00:01:01 ID 009 D 150.14 T 64.1 B 14.0 G 217 R 0000 +2025/04/27 08:01:01 ID 009 D 150.14 T 63.3 B 14.0 G 217 R 0000 +2025/04/27 16:01:01 ID 009 D 150.08 T 63.7 B 14.0 G 217 R 0000 +2025/04/28 00:01:01 ID 009 D 150.19 T 64.1 B 14.0 G 217 R 0000 +2025/04/28 08:01:01 ID 009 D 150.31 T 62.4 B 14.0 G 217 R 0000 +2025/04/28 16:42:42 ID 009 D 150.19 T 60.2 B 13.9 G 216 R 0001 +2025/04/29 00:42:01 ID 009 D 150.30 T 62.8 B 14.0 G 216 R 0000 +2025/04/29 08:42:01 ID 009 D 150.30 T 61.5 B 14.0 G 216 R 0000 +2025/04/29 16:42:01 ID 009 D 150.14 T 62.4 B 14.0 G 216 R 0000 +2025/04/30 00:42:01 ID 009 D 150.30 T 62.1 B 14.0 G 216 R 0000 +2025/04/30 08:42:01 ID 009 D 150.36 T 61.1 B 14.0 G 216 R 0000 +2025/04/30 16:42:01 ID 009 D 150.19 T 61.7 B 14.0 G 216 R 0000 +2025/05/01 00:42:01 ID 009 D 150.30 T 62.1 B 14.0 G 216 R 0000 +2025/05/01 08:42:01 ID 009 D 152.42 T 61.1 B 14.0 G 216 R 0000 +2025/05/01 16:42:01 ID 009 D 150.14 T 61.9 B 14.0 G 216 R 0000 +2025/05/02 00:42:01 ID 009 D 150.30 T 63.0 B 14.0 G 216 R 0000 +2025/05/02 08:42:01 ID 009 D 150.30 T 62.4 B 14.0 G 216 R 0000 +2025/05/02 16:42:01 ID 009 D 150.36 T 62.8 B 14.0 G 216 R 0000 +2025/05/03 00:42:01 ID 009 D 150.36 T 62.4 B 14.0 G 216 R 0000 +2025/05/03 08:42:01 ID 009 D 150.36 T 61.3 B 14.0 G 216 R 0000 +2025/05/03 16:42:01 ID 009 D 150.19 T 61.9 B 14.0 G 216 R 0000 +2025/05/04 00:42:01 ID 009 D 150.19 T 62.4 B 14.0 G 216 R 0000 +2025/05/04 08:42:01 ID 009 D 150.14 T 61.7 B 14.0 G 216 R 0000 +2025/05/04 16:42:01 ID 009 D 149.91 T 61.9 B 14.0 G 216 R 0000 +2025/05/05 00:42:01 ID 009 D 150.03 T 61.9 B 14.0 G 216 R 0000 +2025/05/05 08:42:01 ID 009 D 150.08 T 61.0 B 14.0 G 216 R 0000 +2025/05/05 16:42:01 ID 009 D 150.08 T 61.1 B 14.0 G 216 R 0000 +2025/05/06 00:42:01 ID 009 D 150.25 T 61.0 B 14.0 G 216 R 0000 +2025/05/06 08:42:01 ID 009 D 150.19 T 59.5 B 14.0 G 216 R 0000 +2025/05/06 16:42:01 ID 009 D 150.13 T 59.5 B 14.0 G 215 R 0000 +2025/05/07 00:42:01 ID 009 D 150.36 T 59.1 B 14.0 G 215 R 0000 +2025/05/07 08:42:01 ID 009 D 152.20 T 58.0 B 14.0 G 215 R 0000 +2025/05/07 16:42:01 ID 009 D 150.36 T 58.4 B 14.0 G 215 R 0000 +2025/05/08 00:42:01 ID 009 D 150.41 T 59.1 B 14.0 G 215 R 0000 +2025/05/08 08:42:01 ID 009 D 150.52 T 58.4 B 14.0 G 214 R 0000 +2025/05/08 16:42:01 ID 009 D 150.47 T 58.9 B 14.0 G 214 R 0000 +2025/05/09 00:42:01 ID 009 D 150.52 T 59.7 B 14.0 G 214 R 0000 +2025/05/09 08:42:01 ID 009 D 150.52 T 59.1 B 14.0 G 214 R 0000 +2025/05/09 16:42:01 ID 009 D 150.52 T 60.0 B 14.0 G 214 R 0000 +2025/05/10 00:42:01 ID 009 D 150.47 T 60.8 B 14.0 G 214 R 0000 +2025/05/10 08:42:02 ID 009 D 150.64 T 60.6 B 14.0 G 215 R 0001 +2025/05/10 16:42:01 ID 009 D 150.41 T 61.9 B 14.0 G 215 R 0000 +2025/05/11 00:42:01 ID 009 D 150.53 T 62.1 B 14.0 G 215 R 0000 +2025/05/11 08:42:01 ID 009 D 150.53 T 61.5 B 14.0 G 215 R 0000 +2025/05/11 16:42:01 ID 009 D 150.30 T 62.2 B 14.0 G 215 R 0000 +2025/05/12 00:42:01 ID 009 D 150.36 T 63.3 B 14.0 G 216 R 0000 +2025/05/12 08:42:01 ID 009 D 152.98 T 63.3 B 14.0 G 216 R 0000 +2025/05/12 16:42:01 ID 009 D 150.19 T 64.4 B 14.0 G 216 R 0000 +2025/05/13 00:42:01 ID 009 D 150.25 T 65.3 B 14.0 G 217 R 0000 +2025/05/13 08:42:01 ID 009 D 150.20 T 64.6 B 14.0 G 217 R 0000 +2025/05/13 16:42:01 ID 009 D 148.75 T 65.7 B 14.0 G 217 R 0000 +2025/05/14 00:42:01 ID 009 D 150.20 T 66.2 B 14.0 G 218 R 0000 +2025/05/14 08:42:01 ID 009 D 150.09 T 65.3 B 14.0 G 218 R 0000 +2025/05/14 16:42:01 ID 009 D 150.14 T 66.1 B 14.0 G 218 R 0000 +2025/05/15 00:42:01 ID 009 D 150.31 T 65.7 B 14.0 G 218 R 0000 +2025/05/15 08:42:01 ID 009 D 150.25 T 64.3 B 14.0 G 218 R 0000 +2025/05/15 16:42:01 ID 009 D 150.25 T 65.0 B 14.0 G 218 R 0000 +2025/05/16 00:42:01 ID 009 D 150.31 T 65.2 B 14.0 G 218 R 0000 +2025/05/16 08:42:01 ID 009 D 150.42 T 63.9 B 14.0 G 218 R 0000 +2025/05/16 16:42:01 ID 009 D 150.36 T 64.8 B 14.0 G 218 R 0000 +2025/05/17 00:42:01 ID 009 D 150.42 T 65.2 B 14.0 G 218 R 0000 +2025/05/17 08:42:01 ID 009 D 150.42 T 64.3 B 14.0 G 218 R 0000 +2025/05/17 16:42:01 ID 009 D 150.25 T 65.0 B 14.0 G 218 R 0000 +2025/05/18 00:42:01 ID 009 D 150.31 T 65.0 B 14.0 G 218 R 0000 +2025/05/18 08:42:01 ID 009 D 150.20 T 64.4 B 14.0 G 218 R 0000 +2025/05/18 16:42:01 ID 009 D 150.25 T 64.8 B 14.0 G 218 R 0000 +2025/05/19 00:42:01 ID 009 D 150.31 T 65.2 B 14.0 G 218 R 0000 +2025/05/19 08:42:01 ID 009 D 150.36 T 64.3 B 14.0 G 218 R 0000 +2025/05/19 16:42:01 ID 009 D 150.14 T 64.4 B 14.0 G 218 R 0000 +2025/05/20 00:42:01 ID 009 D 150.36 T 64.4 B 14.0 G 218 R 0000 +2025/05/20 08:42:01 ID 009 D 150.53 T 62.8 B 14.0 G 217 R 0000 +2025/05/20 16:42:01 ID 009 D 150.47 T 63.7 B 14.0 G 217 R 0000 +2025/05/21 00:42:01 ID 009 D 150.53 T 63.7 B 14.0 G 217 R 0000 +2025/05/21 08:42:01 ID 009 D 150.58 T 62.8 B 14.0 G 217 R 0000 +2025/05/21 16:42:01 ID 009 D 150.47 T 64.1 B 14.0 G 217 R 0000 +2025/05/22 00:42:01 ID 009 D 150.42 T 64.8 B 14.0 G 217 R 0000 +2025/05/22 08:42:01 ID 009 D 150.59 T 64.6 B 14.0 G 217 R 0000 +2025/05/22 16:42:02 ID 009 D 150.59 T 65.9 B 13.9 G 221 R 0001 +2025/05/23 00:42:01 ID 009 D 150.42 T 66.8 B 14.0 G 221 R 0000 +2025/05/23 08:42:01 ID 009 D 150.53 T 66.2 B 14.0 G 221 R 0000 +2025/05/23 16:42:01 ID 009 D 150.48 T 67.2 B 14.0 G 221 R 0000 +2025/05/24 00:42:01 ID 009 D 150.53 T 67.7 B 14.0 G 221 R 0000 +2025/05/24 08:42:01 ID 009 D 150.64 T 67.0 B 14.0 G 221 R 0000 +2025/05/24 16:42:01 ID 009 D 153.82 T 70.6 B 14.0 G 224 R 0000 +2025/05/25 00:42:01 ID 009 D 150.48 T 69.0 B 14.0 G 224 R 0000 +2025/05/25 08:42:01 ID 009 D 150.53 T 68.1 B 14.0 G 223 R 0000 +2025/05/25 16:42:01 ID 009 D 150.37 T 68.2 B 14.0 G 223 R 0000 +2025/05/26 00:42:01 ID 009 D 150.48 T 68.8 B 14.0 G 223 R 0000 +2025/05/26 08:42:01 ID 009 D 150.42 T 67.2 B 14.0 G 223 R 0000 +2025/05/26 16:42:01 ID 009 D 150.42 T 68.1 B 14.0 G 223 R 0000 +2025/05/27 00:42:01 ID 009 D 150.53 T 68.8 B 14.0 G 223 R 0000 +2025/05/27 08:42:01 ID 009 D 151.37 T 67.9 B 14.0 G 223 R 0000 +2025/05/27 16:42:01 ID 009 D 150.48 T 68.4 B 14.0 G 223 R 0000 +2025/05/28 00:42:01 ID 009 D 150.53 T 69.0 B 14.0 G 223 R 0000 +2025/05/28 08:42:01 ID 009 D 150.59 T 67.5 B 14.0 G 223 R 0000 +2025/05/28 16:42:01 ID 009 D 150.48 T 69.1 B 14.0 G 223 R 0000 +2025/05/29 00:42:01 ID 009 D 150.48 T 69.9 B 14.0 G 223 R 0000 +2025/05/29 08:42:01 ID 009 D 152.65 T 68.6 B 14.0 G 223 R 0000 +2025/05/29 16:42:01 ID 009 D 150.53 T 71.1 B 14.0 G 224 R 0000 +2025/05/30 00:42:01 ID 009 D 150.59 T 71.3 B 14.0 G 224 R 0000 +2025/05/30 08:42:01 ID 009 D 150.54 T 69.9 B 14.0 G 224 R 0000 +2025/05/30 16:42:01 ID 009 D 150.59 T 71.1 B 14.0 G 224 R 0000 +2025/05/31 00:42:01 ID 009 D 150.59 T 70.7 B 14.0 G 224 R 0000 +2025/05/31 08:42:01 ID 009 D 150.65 T 70.6 B 14.0 G 224 R 0000 +2025/05/31 16:42:01 ID 009 D 150.42 T 70.9 B 14.0 G 224 R 0000 +2025/06/01 00:42:01 ID 009 D 150.42 T 72.2 B 14.0 G 224 R 0000 +2025/06/01 08:42:01 ID 009 D 150.43 T 71.1 B 14.0 G 224 R 0000 +2025/06/01 16:42:01 ID 009 D 150.37 T 71.8 B 14.0 G 224 R 0000 +2025/06/02 00:42:01 ID 009 D 150.37 T 72.5 B 14.0 G 224 R 0000 +2025/06/02 08:42:01 ID 009 D 150.43 T 71.6 B 14.0 G 224 R 0000 +2025/06/02 16:42:01 ID 009 D 150.43 T 72.0 B 14.0 G 224 R 0000 +2025/06/03 00:42:01 ID 009 D 150.43 T 71.3 B 14.0 G 224 R 0000 +2025/06/03 08:42:01 ID 009 D 150.54 T 70.7 B 14.0 G 224 R 0000 +2025/06/03 16:42:01 ID 009 D 150.42 T 70.7 B 14.0 G 224 R 0000 +2025/06/04 00:42:01 ID 009 D 150.48 T 71.3 B 14.0 G 224 R 0000 +2025/06/04 08:42:01 ID 009 D 150.37 T 70.0 B 14.0 G 224 R 0000 +2025/06/04 16:42:01 ID 009 D 150.31 T 70.7 B 14.0 G 224 R 0000 +2025/06/05 00:42:01 ID 009 D 150.48 T 70.7 B 14.0 G 224 R 0000 +2025/06/05 08:42:01 ID 009 D 150.59 T 69.9 B 14.0 G 224 R 0000 +2025/06/05 16:42:01 ID 009 D 150.42 T 71.5 B 14.0 G 224 R 0000 +2025/06/06 00:42:01 ID 009 D 150.43 T 71.3 B 14.2 G 224 R 0000 +2025/06/06 08:42:01 ID 009 D 150.59 T 70.4 B 14.0 G 224 R 0000 +2025/06/06 16:42:01 ID 009 D 150.54 T 70.7 B 14.0 G 224 R 0000 +2025/06/07 00:42:01 ID 009 D 150.48 T 71.5 B 14.0 G 224 R 0000 +2025/06/07 08:42:01 ID 009 D 150.70 T 70.7 B 14.0 G 224 R 0000 +2025/06/07 16:42:01 ID 009 D 150.54 T 72.0 B 14.0 G 224 R 0000 +2025/06/08 00:42:01 ID 009 D 150.48 T 72.4 B 14.0 G 224 R 0000 +2025/06/08 08:42:01 ID 009 D 150.59 T 71.8 B 14.0 G 224 R 0000 +2025/06/08 16:42:01 ID 009 D 150.43 T 72.4 B 14.0 G 224 R 0000 +2025/06/09 00:42:01 ID 009 D 150.48 T 72.5 B 14.0 G 224 R 0000 +2025/06/09 08:42:01 ID 009 D 150.65 T 72.5 B 14.0 G 224 R 0000 +2025/06/09 16:42:01 ID 009 D 150.48 T 72.7 B 14.0 G 224 R 0000 +2025/06/10 00:42:01 ID 009 D 150.59 T 72.4 B 14.0 G 224 R 0000 +2025/06/10 08:42:01 ID 009 D 150.65 T 70.7 B 14.0 G 224 R 0000 +2025/06/10 16:42:01 ID 009 D 150.59 T 72.2 B 14.0 G 224 R 0000 +2025/06/11 00:42:01 ID 009 D 150.65 T 71.5 B 14.0 G 224 R 0000 +2025/06/11 08:42:01 ID 009 D 150.59 T 70.7 B 14.0 G 224 R 0000 +2025/06/11 16:42:01 ID 009 D 150.48 T 71.3 B 14.0 G 224 R 0000 +2025/06/12 00:42:01 ID 009 D 150.48 T 71.5 B 14.0 G 224 R 0000 +2025/06/12 08:42:01 ID 009 D 150.54 T 71.1 B 14.0 G 224 R 0000 +2025/06/12 16:42:01 ID 009 D 150.37 T 72.2 B 14.0 G 224 R 0000 +2025/06/13 00:42:01 ID 009 D 150.43 T 72.9 B 14.0 G 224 R 0000 +2025/06/13 08:42:01 ID 009 D 150.48 T 71.6 B 14.0 G 224 R 0000 +2025/06/13 16:42:01 ID 009 D 150.54 T 72.9 B 14.0 G 224 R 0000 +2025/06/14 00:42:01 ID 009 D 150.48 T 73.6 B 14.0 G 225 R 0000 +2025/06/14 08:42:01 ID 009 D 150.59 T 72.0 B 14.0 G 225 R 0000 +2025/06/14 16:42:01 ID 009 D 149.26 T 74.7 B 14.0 G 225 R 0000 +2025/06/15 00:42:01 ID 009 D 150.60 T 74.1 B 14.0 G 225 R 0000 +2025/06/15 08:42:01 ID 009 D 150.65 T 73.4 B 14.0 G 225 R 0000 +2025/06/15 16:42:01 ID 009 D 150.48 T 75.6 B 14.0 G 226 R 0000 +2025/06/16 00:42:01 ID 009 D 150.60 T 75.4 B 14.0 G 226 R 0000 +2025/06/16 08:42:01 ID 009 D 153.05 T 75.2 B 14.0 G 226 R 0000 +2025/06/16 16:42:01 ID 009 D 150.60 T 75.6 B 14.0 G 226 R 0000 +2025/06/17 00:42:01 ID 009 D 150.60 T 76.6 B 14.0 G 227 R 0000 +2025/06/17 08:42:01 ID 009 D 150.60 T 75.0 B 14.0 G 227 R 0000 +2025/06/17 16:42:01 ID 009 D 149.09 T 78.2 B 14.0 G 228 R 0000 +2025/06/18 00:42:01 ID 009 D 150.43 T 76.6 B 14.0 G 228 R 0000 +2025/06/18 08:42:01 ID 009 D 150.60 T 75.2 B 14.0 G 227 R 0000 +2025/06/18 16:42:01 ID 009 D 150.54 T 76.6 B 14.0 G 227 R 0000 +2025/06/19 00:42:01 ID 009 D 150.54 T 77.2 B 14.0 G 227 R 0000 +2025/06/19 08:42:01 ID 009 D 150.65 T 75.8 B 14.0 G 227 R 0000 +2025/06/19 16:42:01 ID 009 D 150.65 T 77.2 B 14.0 G 227 R 0000 +2025/06/20 00:42:01 ID 009 D 150.49 T 77.2 B 14.0 G 227 R 0000 +2025/06/20 08:42:01 ID 009 D 150.60 T 77.4 B 14.0 G 227 R 0000 +2025/06/20 16:42:01 ID 009 D 150.43 T 77.4 B 14.0 G 227 R 0000 +2025/06/21 00:42:01 ID 009 D 150.49 T 78.2 B 14.0 G 228 R 0000 +2025/06/21 08:42:01 ID 009 D 150.60 T 74.7 B 14.0 G 227 R 0000 +2025/06/21 16:42:01 ID 009 D 150.43 T 77.4 B 14.0 G 227 R 0000 +2025/06/22 00:42:01 ID 009 D 150.49 T 77.9 B 14.0 G 228 R 0000 +2025/06/22 08:42:01 ID 009 D 150.60 T 76.6 B 14.0 G 228 R 0000 +2025/06/22 16:42:01 ID 009 D 150.43 T 77.7 B 14.0 G 228 R 0000 +2025/06/23 00:42:01 ID 009 D 150.60 T 77.2 B 14.0 G 228 R 0000 +2025/06/23 08:42:01 ID 009 D 150.71 T 76.1 B 14.0 G 228 R 0000 +2025/06/23 16:42:01 ID 009 D 150.60 T 77.4 B 14.0 G 228 R 0000 +2025/06/24 00:42:01 ID 009 D 150.54 T 76.8 B 14.0 G 228 R 0000 +2025/06/24 08:42:01 ID 009 D 150.71 T 77.0 B 13.9 G 228 R 0000 +2025/06/24 16:42:01 ID 009 D 150.65 T 76.8 B 14.0 G 228 R 0000 +2025/06/25 00:42:01 ID 009 D 150.71 T 77.0 B 14.0 G 228 R 0000 +2025/06/25 08:42:01 ID 009 D 150.54 T 74.7 B 14.0 G 227 R 0000 +2025/06/25 16:42:01 ID 009 D 150.60 T 74.9 B 14.0 G 227 R 0000 +2025/06/26 00:42:01 ID 009 D 150.60 T 75.4 B 14.0 G 227 R 0000 +2025/06/26 08:42:01 ID 009 D 151.15 T 74.1 B 14.0 G 226 R 0000 +2025/06/26 16:42:01 ID 009 D 150.54 T 74.9 B 14.0 G 226 R 0000 +2025/06/27 00:42:01 ID 009 D 150.60 T 74.7 B 14.0 G 226 R 0000 +2025/06/27 08:42:01 ID 009 D 150.71 T 74.3 B 14.0 G 226 R 0000 +2025/06/27 16:42:01 ID 009 D 150.65 T 74.9 B 14.0 G 226 R 0000 +2025/06/28 00:42:01 ID 009 D 150.60 T 75.8 B 14.0 G 226 R 0000 +2025/06/28 08:42:01 ID 009 D 150.65 T 74.1 B 14.1 G 226 R 0000 +2025/06/28 16:42:01 ID 009 D 150.65 T 75.4 B 14.0 G 226 R 0000 +2025/06/29 00:42:01 ID 009 D 150.60 T 75.2 B 14.0 G 226 R 0000 +2025/06/29 08:42:01 ID 009 D 150.65 T 75.2 B 14.0 G 226 R 0000 +2025/06/29 16:42:01 ID 009 D 150.65 T 75.6 B 14.0 G 226 R 0000 +2025/06/30 00:42:01 ID 009 D 150.54 T 76.8 B 14.0 G 227 R 0000 +2025/06/30 08:42:01 ID 009 D 150.65 T 75.0 B 14.0 G 227 R 0000 +2025/06/30 16:42:01 ID 009 D 150.65 T 76.5 B 14.0 G 227 R 0000 +2025/07/01 00:42:01 ID 009 D 150.65 T 76.1 B 14.0 G 227 R 0000 +2025/07/01 08:42:01 ID 009 D 150.65 T 74.0 B 14.0 G 226 R 0000 +2025/07/01 16:42:01 ID 009 D 150.65 T 74.9 B 14.0 G 226 R 0000 +2025/07/02 00:42:01 ID 009 D 150.54 T 74.0 B 14.0 G 226 R 0000 +2025/07/02 08:42:01 ID 009 D 150.65 T 72.9 B 14.0 G 225 R 0000 +2025/07/02 16:42:01 ID 009 D 150.65 T 73.4 B 14.0 G 225 R 0000 +2025/07/03 00:42:01 ID 009 D 150.54 T 74.0 B 14.0 G 225 R 0000 +2025/07/03 08:42:01 ID 009 D 150.54 T 73.8 B 14.0 G 225 R 0000 +2025/07/03 16:42:01 ID 009 D 150.65 T 73.8 B 14.0 G 225 R 0000 +2025/07/04 00:42:01 ID 009 D 150.65 T 74.0 B 14.0 G 225 R 0000 +2025/07/04 08:42:01 ID 009 D 150.71 T 73.3 B 14.0 G 225 R 0000 +2025/07/04 16:42:01 ID 009 D 150.59 T 73.3 B 14.0 G 225 R 0000 +2025/07/05 00:42:01 ID 009 D 150.71 T 73.8 B 14.0 G 225 R 0000 +2025/07/05 08:42:01 ID 009 D 150.87 T 72.9 B 14.0 G 225 R 0000 +2025/07/05 16:42:01 ID 009 D 150.71 T 73.6 B 14.0 G 225 R 0000 +2025/07/06 00:42:01 ID 009 D 150.71 T 74.3 B 14.0 G 225 R 0000 +2025/07/06 08:42:01 ID 009 D 150.71 T 74.1 B 14.0 G 225 R 0000 +2025/07/06 16:42:01 ID 009 D 150.60 T 74.5 B 14.0 G 225 R 0000 +2025/07/07 00:42:01 ID 009 D 150.65 T 75.6 B 14.0 G 226 R 0000 +2025/07/07 08:42:01 ID 009 D 150.76 T 75.4 B 14.0 G 226 R 0000 +2025/07/07 16:42:01 ID 009 D 150.65 T 74.7 B 14.0 G 226 R 0000 +2025/07/08 00:42:01 ID 009 D 150.71 T 75.9 B 14.0 G 226 R 0000 +2025/07/08 08:42:01 ID 009 D 150.82 T 74.1 B 14.0 G 226 R 0000 +2025/07/08 16:42:01 ID 009 D 150.65 T 74.9 B 14.0 G 226 R 0000 +2025/07/09 00:42:01 ID 009 D 150.71 T 75.9 B 14.0 G 226 R 0000 +2025/07/09 08:42:01 ID 009 D 150.88 T 75.6 B 14.0 G 226 R 0000 +2025/07/09 16:42:01 ID 009 D 150.71 T 76.6 B 14.0 G 226 R 0000 +2025/07/10 00:42:01 ID 009 D 149.32 T 77.4 B 14.0 G 227 R 0000 +2025/07/10 08:42:01 ID 009 D 150.71 T 75.9 B 14.0 G 227 R 0000 +2025/07/10 16:42:01 ID 009 D 150.49 T 77.4 B 14.0 G 227 R 0000 +2025/07/11 00:42:01 ID 009 D 150.54 T 77.7 B 14.1 G 227 R 0000 +2025/07/11 08:42:01 ID 009 D 150.65 T 76.1 B 14.0 G 227 R 0000 +2025/07/11 16:42:01 ID 009 D 150.60 T 77.5 B 14.0 G 227 R 0000 +2025/07/12 00:42:01 ID 009 D 150.65 T 77.0 B 14.0 G 227 R 0000 +2025/07/12 08:42:01 ID 009 D 150.77 T 75.8 B 14.0 G 227 R 0000 +2025/07/12 16:42:01 ID 009 D 150.71 T 77.0 B 14.0 G 227 R 0000 +2025/07/13 00:42:01 ID 009 D 150.82 T 77.4 B 14.0 G 227 R 0000 +2025/07/13 08:42:01 ID 009 D 150.77 T 76.8 B 14.0 G 227 R 0000 +2025/07/13 16:42:01 ID 009 D 150.65 T 76.8 B 14.0 G 227 R 0000 +2025/07/14 00:42:01 ID 009 D 150.82 T 77.4 B 14.0 G 227 R 0000 +2025/07/14 08:42:01 ID 009 D 150.71 T 75.6 B 14.0 G 227 R 0000 +2025/07/14 16:42:01 ID 009 D 150.71 T 76.6 B 14.0 G 227 R 0000 +2025/07/15 00:42:01 ID 009 D 150.77 T 76.8 B 14.0 G 227 R 0000 +2025/07/15 08:42:01 ID 009 D 155.89 T 74.3 B 14.0 G 227 R 0000 +2025/07/15 16:42:01 ID 009 D 150.71 T 75.9 B 14.0 G 227 R 0000 +2025/07/16 00:42:01 ID 009 D 150.65 T 75.8 B 14.0 G 227 R 0000 +2025/07/16 08:42:01 ID 009 D 150.71 T 75.4 B 14.0 G 227 R 0000 +2025/07/16 16:42:01 ID 009 D 150.65 T 76.1 B 14.0 G 227 R 0000 +2025/07/17 00:42:01 ID 009 D 150.71 T 75.8 B 14.0 G 227 R 0000 +2025/07/17 08:42:01 ID 009 D 150.76 T 75.4 B 14.2 G 227 R 0000 +2025/07/17 16:42:01 ID 009 D 150.65 T 76.3 B 14.0 G 227 R 0000 +2025/07/18 00:42:01 ID 009 D 150.76 T 76.8 B 14.0 G 227 R 0000 +2025/07/18 08:42:01 ID 009 D 150.82 T 75.2 B 14.0 G 227 R 0000 +2025/07/18 16:42:01 ID 009 D 150.76 T 75.8 B 14.0 G 227 R 0000 +2025/07/19 00:42:01 ID 009 D 150.71 T 75.9 B 14.0 G 227 R 0000 +2025/07/19 08:42:01 ID 009 D 150.76 T 74.5 B 14.0 G 226 R 0000 +2025/07/19 16:42:01 ID 009 D 150.65 T 74.9 B 14.0 G 226 R 0000 +2025/07/20 00:42:01 ID 009 D 150.71 T 75.6 B 14.0 G 226 R 0000 +2025/07/20 08:42:01 ID 009 D 150.76 T 74.7 B 14.1 G 226 R 0000 +2025/07/20 16:42:01 ID 009 D 150.76 T 74.7 B 14.0 G 226 R 0000 +2025/07/21 00:42:01 ID 009 D 150.65 T 74.1 B 14.0 G 226 R 0000 +2025/07/21 08:42:01 ID 009 D 150.82 T 74.0 B 14.0 G 226 R 0000 +2025/07/21 16:42:01 ID 009 D 150.60 T 74.0 B 14.0 G 226 R 0000 +2025/07/22 00:42:01 ID 009 D 150.65 T 74.3 B 14.0 G 226 R 0000 +2025/07/22 08:42:01 ID 009 D 150.71 T 72.9 B 14.0 G 225 R 0000 +2025/07/22 16:42:01 ID 009 D 150.71 T 73.4 B 14.0 G 225 R 0000 +2025/07/23 00:42:01 ID 009 D 150.76 T 73.8 B 14.0 G 225 R 0000 +2025/07/23 08:42:01 ID 009 D 150.82 T 72.9 B 14.0 G 225 R 0000 +2025/07/23 16:42:01 ID 009 D 150.76 T 73.1 B 14.0 G 225 R 0000 +2025/07/24 00:42:01 ID 009 D 150.82 T 72.7 B 14.0 G 225 R 0000 +2025/07/24 08:42:01 ID 009 D 150.87 T 72.5 B 14.0 G 225 R 0000 +2025/07/24 16:42:01 ID 009 D 150.82 T 72.2 B 14.0 G 225 R 0000 +2025/07/25 00:42:01 ID 009 D 150.82 T 72.7 B 14.0 G 225 R 0000 +2025/07/25 08:42:01 ID 009 D 150.76 T 72.4 B 14.0 G 225 R 0000 +2025/07/25 16:42:01 ID 009 D 150.82 T 74.5 B 14.0 G 225 R 0000 +2025/07/26 00:42:01 ID 009 D 150.76 T 74.7 B 14.0 G 225 R 0000 +2025/07/26 08:42:01 ID 009 D 150.76 T 73.8 B 14.0 G 225 R 0000 +2025/07/26 16:42:01 ID 009 D 150.76 T 74.1 B 14.0 G 225 R 0000 +2025/07/27 00:42:01 ID 009 D 150.87 T 74.9 B 14.0 G 225 R 0000 +2025/07/27 08:42:01 ID 009 D 150.93 T 74.3 B 14.0 G 225 R 0000 +2025/07/27 16:42:01 ID 009 D 150.71 T 74.7 B 14.0 G 225 R 0000 +2025/07/28 00:42:01 ID 009 D 150.93 T 76.1 B 14.0 G 226 R 0000 +2025/07/28 08:42:01 ID 009 D 150.82 T 76.1 B 14.0 G 226 R 0000 +2025/07/28 16:42:01 ID 009 D 150.88 T 76.3 B 14.0 G 226 R 0000 +2025/07/29 00:42:01 ID 009 D 150.88 T 75.8 B 14.0 G 226 R 0000 +2025/07/29 08:42:01 ID 009 D 152.32 T 75.0 B 14.0 G 226 R 0000 +2025/07/29 16:42:01 ID 009 D 150.87 T 74.9 B 14.0 G 226 R 0000 +2025/07/30 00:42:01 ID 009 D 150.87 T 75.2 B 14.0 G 226 R 0000 +2025/07/30 08:42:01 ID 009 D 150.88 T 73.3 B 14.0 G 226 R 0000 +2025/07/30 16:42:01 ID 009 D 150.76 T 74.5 B 14.0 G 226 R 0000 +2025/07/31 00:42:01 ID 009 D 150.93 T 74.9 B 14.0 G 226 R 0000 +2025/07/31 08:42:01 ID 009 D 150.82 T 73.1 B 14.0 G 225 R 0000 +2025/07/31 16:42:01 ID 009 D 150.82 T 74.0 B 14.0 G 225 R 0000 +2025/08/01 00:42:01 ID 009 D 150.93 T 73.6 B 14.0 G 225 R 0000 +2025/08/01 08:42:01 ID 009 D 150.98 T 73.1 B 14.0 G 225 R 0000 +2025/08/01 16:42:01 ID 009 D 150.82 T 73.1 B 14.0 G 225 R 0000 +2025/08/02 00:42:01 ID 009 D 150.93 T 73.3 B 14.0 G 225 R 0000 +2025/08/02 08:42:01 ID 009 D 150.98 T 73.3 B 14.0 G 225 R 0000 +2025/08/02 16:42:01 ID 009 D 150.87 T 73.6 B 14.0 G 225 R 0000 +2025/08/03 00:42:01 ID 009 D 150.82 T 74.1 B 14.0 G 225 R 0000 +2025/08/03 08:42:01 ID 009 D 150.76 T 72.9 B 14.0 G 225 R 0000 +2025/08/03 16:42:01 ID 009 D 150.76 T 73.8 B 14.0 G 225 R 0000 +2025/08/04 00:42:01 ID 009 D 150.76 T 74.5 B 14.0 G 225 R 0000 +2025/08/04 08:42:01 ID 009 D 150.87 T 72.9 B 14.0 G 225 R 0000 +2025/08/04 16:42:01 ID 009 D 150.71 T 73.6 B 14.0 G 225 R 0000 +2025/08/05 00:42:01 ID 009 D 150.82 T 75.2 B 14.0 G 225 R 0000 +2025/08/05 08:42:01 ID 009 D 150.93 T 73.6 B 14.0 G 225 R 0000 +2025/08/05 16:42:01 ID 009 D 150.87 T 75.0 B 14.0 G 225 R 0000 +2025/08/06 00:42:01 ID 009 D 150.93 T 76.1 B 14.0 G 226 R 0000 +2025/08/06 08:42:01 ID 009 D 150.99 T 74.9 B 14.0 G 226 R 0000 +2025/08/06 16:42:01 ID 009 D 150.93 T 76.5 B 14.0 G 226 R 0000 +2025/08/07 00:42:01 ID 009 D 149.65 T 77.0 B 14.0 G 227 R 0000 +2025/08/07 08:42:01 ID 009 D 150.99 T 75.0 B 14.0 G 227 R 0000 +2025/08/07 16:42:01 ID 009 D 150.87 T 76.6 B 14.0 G 227 R 0000 +2025/08/08 00:42:01 ID 009 D 150.88 T 77.0 B 14.0 G 227 R 0000 +2025/08/08 08:42:01 ID 009 D 150.77 T 75.4 B 14.0 G 227 R 0000 +2025/08/08 16:42:01 ID 009 D 150.76 T 75.9 B 14.0 G 227 R 0000 +2025/08/09 00:42:01 ID 009 D 150.82 T 77.2 B 14.0 G 227 R 0000 +2025/08/09 08:42:01 ID 009 D 150.77 T 76.8 B 14.0 G 227 R 0000 +2025/08/09 16:42:01 ID 009 D 150.77 T 78.8 B 14.0 G 228 R 0000 +2025/08/10 00:42:01 ID 009 D 150.82 T 77.4 B 14.0 G 228 R 0000 +2025/08/10 08:42:01 ID 009 D 150.77 T 76.1 B 14.0 G 227 R 0000 +2025/08/10 16:42:01 ID 009 D 150.76 T 77.2 B 14.0 G 227 R 0000 +2025/08/11 00:42:01 ID 009 D 150.77 T 77.2 B 14.0 G 227 R 0000 +2025/08/11 08:42:01 ID 009 D 150.82 T 75.2 B 14.0 G 227 R 0000 +2025/08/11 16:42:01 ID 009 D 150.76 T 75.6 B 14.0 G 227 R 0000 +2025/08/12 00:42:01 ID 009 D 150.82 T 75.8 B 14.0 G 227 R 0000 +2025/08/12 08:42:01 ID 009 D 151.66 T 75.4 B 14.0 G 227 R 0000 +2025/08/12 16:42:01 ID 009 D 150.82 T 75.4 B 14.0 G 227 R 0000 +2025/08/13 00:42:01 ID 009 D 150.82 T 75.4 B 14.0 G 227 R 0000 +2025/08/13 08:42:01 ID 009 D 150.99 T 74.9 B 14.0 G 226 R 0000 +2025/08/13 16:42:01 ID 009 D 150.82 T 75.8 B 14.0 G 226 R 0000 +2025/08/14 00:42:01 ID 009 D 150.88 T 76.3 B 14.0 G 226 R 0000 +2025/08/14 08:42:01 ID 009 D 150.88 T 74.9 B 14.0 G 226 R 0000 +2025/08/14 16:42:01 ID 009 D 155.78 T 75.8 B 14.0 G 226 R 0000 +2025/08/15 00:42:01 ID 009 D 150.82 T 75.6 B 14.0 G 226 R 0000 +2025/08/15 08:42:01 ID 009 D 150.93 T 75.2 B 14.0 G 226 R 0000 +2025/08/15 16:42:01 ID 009 D 154.05 T 74.7 B 14.0 G 226 R 0000 +2025/08/16 00:42:01 ID 009 D 150.93 T 74.3 B 14.0 G 226 R 0000 +2025/08/16 08:42:01 ID 009 D 150.87 T 73.1 B 14.0 G 225 R 0000 +2025/08/16 16:42:01 ID 009 D 150.82 T 74.3 B 14.0 G 225 R 0000 +2025/08/17 00:42:01 ID 009 D 150.93 T 74.5 B 14.0 G 225 R 0000 +2025/08/17 08:42:01 ID 009 D 150.87 T 74.5 B 14.0 G 225 R 0000 +2025/08/17 16:42:01 ID 009 D 150.87 T 74.3 B 14.0 G 225 R 0000 +2025/08/18 00:42:01 ID 009 D 150.87 T 74.7 B 14.0 G 225 R 0000 +2025/08/18 08:42:01 ID 009 D 150.87 T 74.9 B 14.0 G 225 R 0000 +2025/08/18 16:42:01 ID 009 D 150.93 T 75.8 B 14.0 G 226 R 0000 +2025/08/19 00:42:01 ID 009 D 150.99 T 75.0 B 14.0 G 226 R 0000 +2025/08/19 08:42:01 ID 009 D 151.04 T 75.0 B 14.0 G 226 R 0000 +2025/08/19 16:42:01 ID 009 D 150.93 T 75.8 B 14.0 G 226 R 0000 +2025/08/20 00:42:01 ID 009 D 151.04 T 76.3 B 14.0 G 226 R 0000 +2025/08/20 08:42:01 ID 009 D 151.04 T 75.4 B 14.0 G 226 R 0000 +2025/08/20 16:42:01 ID 009 D 150.93 T 75.9 B 14.0 G 226 R 0000 +2025/08/21 00:42:01 ID 009 D 150.99 T 76.5 B 14.0 G 226 R 0000 +2025/08/21 08:42:01 ID 009 D 150.93 T 75.8 B 14.0 G 226 R 0000 +2025/08/21 16:42:01 ID 009 D 150.88 T 76.5 B 14.0 G 226 R 0000 +2025/08/22 00:42:01 ID 009 D 150.99 T 75.9 B 14.0 G 226 R 0000 +2025/08/22 08:42:01 ID 009 D 150.99 T 75.6 B 14.0 G 226 R 0000 +2025/08/22 16:42:01 ID 009 D 150.82 T 75.6 B 14.0 G 226 R 0000 +2025/08/23 00:42:01 ID 009 D 150.88 T 76.3 B 14.0 G 226 R 0000 +2025/08/23 08:42:01 ID 009 D 150.99 T 74.7 B 14.0 G 226 R 0000 +2025/08/23 16:42:01 ID 009 D 150.93 T 75.4 B 14.0 G 226 R 0000 +2025/08/24 00:42:01 ID 009 D 150.99 T 76.1 B 14.0 G 226 R 0000 +2025/08/24 08:42:01 ID 009 D 150.99 T 74.9 B 14.0 G 226 R 0000 +2025/08/24 16:42:01 ID 009 D 150.82 T 75.8 B 14.0 G 226 R 0000 +2025/08/25 00:42:01 ID 009 D 150.93 T 75.9 B 14.0 G 226 R 0000 +2025/08/25 08:42:01 ID 009 D 150.88 T 75.6 B 14.0 G 226 R 0000 +2025/08/25 16:42:01 ID 009 D 150.76 T 75.2 B 14.0 G 226 R 0000 +2025/08/26 00:42:01 ID 009 D 151.04 T 75.8 B 14.0 G 226 R 0000 +2025/08/26 08:42:01 ID 009 D 151.10 T 74.9 B 14.0 G 226 R 0000 +2025/08/26 16:42:01 ID 009 D 150.93 T 75.2 B 14.0 G 226 R 0000 +2025/08/27 00:42:01 ID 009 D 150.93 T 75.9 B 14.0 G 226 R 0000 +2025/08/27 08:42:01 ID 009 D 157.95 T 75.8 B 14.0 G 226 R 0000 +2025/08/27 16:42:01 ID 009 D 150.93 T 75.6 B 14.0 G 226 R 0000 +2025/08/28 00:42:01 ID 009 D 150.99 T 75.6 B 14.0 G 226 R 0000 +2025/08/28 08:42:01 ID 009 D 150.93 T 75.4 B 14.0 G 226 R 0000 +2025/08/28 16:42:01 ID 009 D 150.93 T 75.8 B 14.0 G 226 R 0000 +2025/08/29 00:42:01 ID 009 D 150.99 T 75.2 B 14.0 G 226 R 0000 +2025/08/29 08:42:01 ID 009 D 150.93 T 75.2 B 14.0 G 226 R 0000 +2025/08/29 16:42:01 ID 009 D 150.88 T 74.9 B 14.0 G 226 R 0000 +2025/08/30 00:42:01 ID 009 D 150.87 T 74.9 B 14.0 G 226 R 0000 +2025/08/30 08:42:01 ID 009 D 150.99 T 74.0 B 14.0 G 226 R 0000 +2025/08/30 16:42:01 ID 009 D 150.93 T 74.0 B 14.0 G 226 R 0000 +2025/08/31 00:42:01 ID 009 D 150.99 T 73.8 B 14.0 G 226 R 0000 +2025/08/31 08:42:01 ID 009 D 151.04 T 72.7 B 14.0 G 225 R 0000 +2025/08/31 16:42:01 ID 009 D 150.93 T 73.4 B 14.0 G 225 R 0000 +2025/09/01 00:42:01 ID 009 D 150.98 T 73.6 B 14.0 G 225 R 0000 +2025/09/01 08:42:01 ID 009 D 151.10 T 72.2 B 14.0 G 225 R 0000 +2025/09/01 16:42:01 ID 009 D 150.93 T 73.1 B 14.0 G 225 R 0000 +2025/09/02 00:42:01 ID 009 D 150.98 T 73.6 B 14.0 G 225 R 0000 +2025/09/02 08:42:01 ID 009 D 151.10 T 72.2 B 14.0 G 224 R 0000 +2025/09/02 16:42:01 ID 009 D 150.93 T 72.9 B 14.0 G 224 R 0000 +2025/09/03 00:42:01 ID 009 D 151.04 T 73.1 B 14.0 G 224 R 0000 +2025/09/03 08:42:01 ID 009 D 159.40 T 72.9 B 14.0 G 224 R 0000 +2025/09/03 16:42:01 ID 009 D 150.87 T 72.2 B 14.0 G 224 R 0000 +2025/09/04 00:42:01 ID 009 D 150.93 T 73.1 B 14.0 G 224 R 0000 +2025/09/04 08:42:01 ID 009 D 150.87 T 72.0 B 14.0 G 224 R 0000 +2025/09/04 16:42:01 ID 009 D 150.82 T 72.0 B 14.0 G 224 R 0000 +2025/09/05 00:42:01 ID 009 D 150.93 T 72.5 B 14.0 G 224 R 0000 +2025/09/05 08:42:01 ID 009 D 150.98 T 71.8 B 14.0 G 224 R 0000 +2025/09/05 16:42:01 ID 009 D 150.87 T 72.5 B 14.0 G 224 R 0000 +2025/09/06 00:42:01 ID 009 D 150.98 T 72.0 B 14.0 G 224 R 0000 +2025/09/06 08:42:01 ID 009 D 151.04 T 71.6 B 14.0 G 224 R 0000 +2025/09/06 16:42:01 ID 009 D 150.93 T 71.3 B 14.0 G 224 R 0000 +2025/09/07 00:42:01 ID 009 D 151.09 T 71.5 B 14.0 G 224 R 0000 +2025/09/07 08:42:01 ID 009 D 151.04 T 70.0 B 14.0 G 224 R 0000 +2025/09/07 16:42:01 ID 009 D 151.04 T 70.7 B 14.0 G 224 R 0000 +2025/09/08 00:42:01 ID 009 D 151.09 T 70.9 B 14.0 G 224 R 0000 +2025/09/08 08:42:01 ID 009 D 151.09 T 70.7 B 14.0 G 224 R 0000 +2025/09/08 16:42:01 ID 009 D 150.93 T 71.6 B 14.0 G 224 R 0000 +2025/09/09 00:42:01 ID 009 D 150.93 T 71.8 B 14.0 G 224 R 0000 +2025/09/09 08:42:01 ID 009 D 150.93 T 71.5 B 14.0 G 224 R 0000 +2025/09/09 16:42:01 ID 009 D 150.87 T 72.4 B 14.0 G 224 R 0000 +2025/09/10 00:42:01 ID 009 D 150.98 T 72.4 B 14.0 G 224 R 0000 +2025/09/10 08:42:01 ID 009 D 151.04 T 71.8 B 14.0 G 224 R 0000 +2025/09/10 16:42:01 ID 009 D 150.98 T 72.4 B 14.0 G 224 R 0000 +2025/09/11 00:42:01 ID 009 D 151.04 T 73.1 B 14.0 G 224 R 0000 +2025/09/11 08:42:01 ID 009 D 151.15 T 72.9 B 14.0 G 224 R 0000 +2025/09/11 16:42:01 ID 009 D 151.04 T 73.1 B 14.0 G 224 R 0000 +2025/09/12 00:42:01 ID 009 D 151.10 T 73.3 B 14.0 G 225 R 0000 +2025/09/12 08:42:01 ID 009 D 151.04 T 73.1 B 14.0 G 225 R 0000 +2025/09/12 16:42:01 ID 009 D 151.04 T 73.6 B 14.0 G 225 R 0000 +2025/09/13 00:42:01 ID 009 D 151.04 T 73.6 B 14.0 G 225 R 0000 +2025/09/13 08:42:01 ID 009 D 151.04 T 72.2 B 14.0 G 225 R 0000 +2025/09/13 16:42:01 ID 009 D 150.93 T 71.5 B 14.0 G 224 R 0000 +2025/09/14 00:42:01 ID 009 D 151.09 T 71.1 B 14.0 G 224 R 0000 +2025/09/14 08:42:01 ID 009 D 151.09 T 69.9 B 14.0 G 224 R 0000 +2025/09/14 16:42:01 ID 009 D 151.04 T 69.7 B 14.0 G 223 R 0000 +2025/09/15 00:42:01 ID 009 D 151.09 T 70.2 B 14.0 G 223 R 0000 +2025/09/15 08:42:01 ID 009 D 151.20 T 69.7 B 14.0 G 223 R 0000 +2025/09/15 16:42:01 ID 009 D 151.54 T 69.9 B 14.0 G 223 R 0000 +2025/09/16 00:42:01 ID 009 D 151.15 T 70.4 B 14.0 G 223 R 0000 +2025/09/16 08:42:01 ID 009 D 151.15 T 70.4 B 14.0 G 223 R 0000 +2025/09/16 16:42:01 ID 009 D 151.15 T 70.2 B 14.0 G 223 R 0000 +2025/09/17 00:42:01 ID 009 D 151.15 T 70.9 B 14.0 G 223 R 0000 +2025/09/17 08:42:01 ID 009 D 151.15 T 69.7 B 14.0 G 223 R 0000 +2025/09/17 16:42:01 ID 009 D 151.04 T 70.2 B 14.0 G 223 R 0000 +2025/09/18 00:42:01 ID 009 D 150.98 T 70.7 B 14.0 G 223 R 0000 +2025/09/18 08:42:01 ID 009 D 151.09 T 69.3 B 14.0 G 223 R 0000 +2025/09/18 16:42:01 ID 009 D 151.09 T 70.2 B 14.0 G 223 R 0000 +2025/09/19 00:42:01 ID 009 D 151.09 T 70.6 B 14.0 G 223 R 0000 +2025/09/19 08:42:01 ID 009 D 151.20 T 69.7 B 14.0 G 223 R 0000 +2025/09/19 16:42:01 ID 009 D 151.09 T 70.2 B 14.0 G 223 R 0000 +2025/09/20 00:42:01 ID 009 D 151.15 T 70.0 B 14.0 G 223 R 0000 +2025/09/20 08:42:01 ID 009 D 151.20 T 68.4 B 14.0 G 223 R 0000 +2025/09/20 16:42:01 ID 009 D 151.09 T 69.9 B 14.0 G 223 R 0000 +2025/09/21 00:42:01 ID 009 D 151.04 T 69.3 B 14.0 G 223 R 0000 +2025/09/21 08:42:01 ID 009 D 151.15 T 69.0 B 14.0 G 223 R 0000 +2025/09/21 16:42:01 ID 009 D 151.04 T 69.9 B 14.0 G 223 R 0000 +2025/09/22 00:42:01 ID 009 D 151.09 T 70.6 B 14.0 G 223 R 0000 +2025/09/22 08:42:01 ID 009 D 150.98 T 69.5 B 14.0 G 223 R 0000 +2025/09/22 16:42:01 ID 009 D 151.04 T 70.4 B 14.0 G 223 R 0000 +2025/09/23 00:42:01 ID 009 D 151.09 T 70.2 B 14.0 G 223 R 0000 +2025/09/23 08:42:01 ID 009 D 151.09 T 68.8 B 14.0 G 223 R 0000 +2025/09/23 16:42:01 ID 009 D 151.09 T 69.5 B 14.0 G 223 R 0000 +2025/09/24 00:42:01 ID 009 D 151.15 T 69.7 B 14.0 G 223 R 0000 +2025/09/24 08:42:01 ID 009 D 151.32 T 68.6 B 14.0 G 222 R 0000 +2025/09/24 16:42:01 ID 009 D 151.26 T 68.6 B 14.0 G 222 R 0000 +2025/09/25 00:42:01 ID 009 D 151.26 T 69.0 B 14.0 G 222 R 0000 +2025/09/25 08:42:01 ID 009 D 152.65 T 68.1 B 14.0 G 222 R 0000 +2025/09/25 16:42:01 ID 009 D 151.15 T 68.2 B 14.0 G 222 R 0000 +2025/09/26 00:42:01 ID 009 D 151.09 T 68.6 B 14.0 G 222 R 0000 +2025/09/26 08:42:01 ID 009 D 151.15 T 68.6 B 14.0 G 222 R 0000 +2025/09/26 16:42:01 ID 009 D 151.15 T 68.6 B 14.0 G 222 R 0000 +2025/09/27 00:42:01 ID 009 D 151.09 T 68.6 B 14.0 G 222 R 0000 +2025/09/27 08:42:01 ID 009 D 151.15 T 68.4 B 14.0 G 221 R 0000 +2025/09/27 16:42:01 ID 009 D 151.04 T 68.2 B 14.0 G 221 R 0000 +2025/09/28 00:42:01 ID 009 D 151.20 T 68.8 B 14.0 G 221 R 0000 +2025/09/28 08:42:01 ID 009 D 154.66 T 68.2 B 14.0 G 221 R 0000 +2025/09/28 16:42:01 ID 009 D 151.09 T 68.2 B 14.0 G 221 R 0000 +2025/09/29 00:42:01 ID 009 D 151.15 T 68.2 B 14.0 G 221 R 0000 +2025/09/29 08:42:01 ID 009 D 151.15 T 66.8 B 14.0 G 220 R 0000 +2025/09/29 16:42:01 ID 009 D 151.20 T 67.5 B 14.0 G 220 R 0000 +2025/09/30 00:42:01 ID 009 D 151.15 T 67.9 B 14.0 G 220 R 0000 +2025/09/30 08:42:01 ID 009 D 151.31 T 67.2 B 14.0 G 220 R 0000 +2025/09/30 16:42:01 ID 009 D 153.76 T 68.1 B 14.0 G 220 R 0000 +2025/10/01 00:42:01 ID 009 D 151.15 T 68.4 B 14.0 G 221 R 0000 +2025/10/01 08:42:01 ID 009 D 151.26 T 67.7 B 14.0 G 221 R 0000 +2025/10/01 16:42:01 ID 009 D 151.26 T 67.9 B 14.0 G 221 R 0000 +2025/10/02 00:42:01 ID 009 D 151.26 T 68.6 B 14.0 G 221 R 0000 +2025/10/02 08:42:01 ID 009 D 151.26 T 68.1 B 14.0 G 221 R 0000 +2025/10/02 16:42:01 ID 009 D 153.77 T 68.4 B 14.0 G 221 R 0000 +2025/10/03 00:42:01 ID 009 D 151.31 T 69.0 B 14.0 G 221 R 0000 +2025/10/03 08:42:01 ID 009 D 151.31 T 69.0 B 14.0 G 221 R 0000 +2025/10/03 16:42:01 ID 009 D 151.26 T 69.7 B 14.0 G 221 R 0000 +2025/10/04 00:42:01 ID 009 D 151.09 T 70.0 B 14.0 G 222 R 0000 +2025/10/04 08:42:01 ID 009 D 151.09 T 69.0 B 14.0 G 222 R 0000 +2025/10/04 16:42:01 ID 009 D 150.92 T 71.1 B 14.0 G 222 R 0000 +2025/10/05 00:42:01 ID 009 D 151.15 T 70.6 B 14.0 G 222 R 0000 +2025/10/05 08:42:01 ID 009 D 151.15 T 68.8 B 14.0 G 222 R 0000 +2025/10/05 16:42:01 ID 009 D 151.04 T 69.3 B 14.0 G 222 R 0000 +2025/10/06 00:42:01 ID 009 D 151.04 T 69.5 B 14.0 G 222 R 0000 +2025/10/06 08:42:01 ID 009 D 151.26 T 68.1 B 14.0 G 222 R 0000 +2025/10/06 16:42:01 ID 009 D 151.20 T 68.6 B 14.0 G 222 R 0000 +2025/10/07 00:42:01 ID 009 D 151.20 T 68.8 B 14.0 G 222 R 0000 +2025/10/07 08:42:01 ID 009 D 151.31 T 67.7 B 14.0 G 222 R 0000 +2025/10/07 16:42:01 ID 009 D 151.26 T 68.4 B 14.0 G 222 R 0000 +2025/10/08 00:42:01 ID 009 D 151.43 T 69.1 B 14.0 G 222 R 0000 +2025/10/08 08:42:01 ID 009 D 151.43 T 68.2 B 14.0 G 222 R 0000 +2025/10/08 16:42:01 ID 009 D 151.43 T 69.0 B 14.0 G 222 R 0000 +2025/10/09 00:42:01 ID 009 D 151.43 T 68.8 B 14.0 G 222 R 0000 +2025/10/09 08:42:01 ID 009 D 151.43 T 68.4 B 14.0 G 222 R 0000 +2025/10/09 16:42:01 ID 009 D 151.37 T 68.4 B 14.0 G 222 R 0000 +2025/10/10 00:42:01 ID 009 D 151.43 T 68.2 B 14.0 G 222 R 0000 +2025/10/10 08:42:01 ID 009 D 151.43 T 67.7 B 14.0 G 222 R 0000 +2025/10/10 16:42:01 ID 009 D 151.31 T 68.8 B 14.0 G 222 R 0000 +2025/10/11 00:42:01 ID 009 D 151.26 T 69.0 B 14.0 G 222 R 0000 +2025/10/11 08:42:01 ID 009 D 151.15 T 68.1 B 14.0 G 222 R 0000 +2025/10/11 16:42:01 ID 009 D 151.15 T 68.6 B 14.0 G 222 R 0000 +2025/10/12 00:42:01 ID 009 D 151.04 T 68.8 B 14.0 G 222 R 0000 +2025/10/12 08:42:01 ID 009 D 151.15 T 67.7 B 14.0 G 222 R 0000 +2025/10/12 16:42:01 ID 009 D 151.15 T 67.2 B 14.0 G 222 R 0000 +2025/10/13 00:42:01 ID 009 D 151.15 T 67.3 B 14.0 G 222 R 0000 +2025/10/13 08:42:01 ID 009 D 151.20 T 66.6 B 14.0 G 222 R 0000 +2025/10/13 16:42:01 ID 009 D 151.20 T 66.6 B 14.0 G 221 R 0000 +2025/10/14 00:42:01 ID 009 D 151.31 T 66.6 B 14.0 G 221 R 0000 +2025/10/14 08:42:01 ID 009 D 151.31 T 66.2 B 14.0 G 221 R 0000 +2025/10/14 16:42:01 ID 009 D 151.20 T 66.2 B 14.2 G 221 R 0000 +2025/10/15 00:42:01 ID 009 D 151.37 T 67.0 B 14.0 G 221 R 0000 +2025/10/15 08:42:01 ID 009 D 151.26 T 66.6 B 14.0 G 221 R 0000 +2025/10/15 16:42:01 ID 009 D 151.14 T 67.2 B 14.0 G 221 R 0000 +2025/10/16 00:42:01 ID 009 D 151.20 T 67.5 B 14.0 G 221 R 0000 +2025/10/16 08:42:01 ID 009 D 152.15 T 66.6 B 14.0 G 221 R 0000 +2025/10/16 16:42:01 ID 009 D 151.03 T 66.8 B 14.0 G 221 R 0000 +2025/10/17 00:42:01 ID 009 D 151.20 T 66.8 B 14.0 G 221 R 0000 +2025/10/17 08:42:01 ID 009 D 151.14 T 65.2 B 14.0 G 221 R 0000 +2025/10/17 16:42:01 ID 009 D 151.20 T 65.3 B 14.0 G 221 R 0000 +2025/10/18 00:42:01 ID 009 D 151.25 T 65.5 B 14.0 G 221 R 0000 +2025/10/18 08:42:01 ID 009 D 151.42 T 63.9 B 14.0 G 221 R 0000 +2025/10/18 16:42:01 ID 009 D 151.42 T 64.3 B 14.0 G 220 R 0000 +2025/10/19 00:42:01 ID 009 D 151.53 T 64.3 B 14.0 G 220 R 0000 +2025/10/19 08:42:01 ID 009 D 151.48 T 62.8 B 14.0 G 220 R 0000 +2025/10/19 16:42:01 ID 009 D 151.48 T 63.3 B 14.0 G 220 R 0000 +2025/10/20 00:42:01 ID 009 D 151.36 T 64.1 B 14.0 G 220 R 0000 +2025/10/20 08:42:01 ID 009 D 151.36 T 63.0 B 14.0 G 220 R 0000 +2025/10/20 16:42:01 ID 009 D 151.36 T 63.7 B 14.0 G 220 R 0000 +2025/10/21 00:42:01 ID 009 D 151.36 T 63.9 B 14.0 G 220 R 0000 +2025/10/21 08:42:01 ID 009 D 151.53 T 62.6 B 14.0 G 220 R 0000 +2025/10/21 16:42:01 ID 009 D 151.47 T 63.5 B 14.0 G 220 R 0000 +2025/10/22 00:42:01 ID 009 D 151.48 T 63.5 B 14.0 G 220 R 0000 +2025/10/22 08:42:01 ID 009 D 151.48 T 62.6 B 14.0 G 220 R 0000 +2025/10/22 16:42:01 ID 009 D 151.25 T 63.0 B 14.0 G 220 R 0000 +2025/10/23 00:42:01 ID 009 D 151.31 T 63.3 B 14.0 G 220 R 0000 +2025/10/23 08:42:01 ID 009 D 152.59 T 63.0 B 14.0 G 220 R 0000 +2025/10/23 16:42:01 ID 009 D 151.25 T 63.5 B 14.0 G 220 R 0000 +2025/10/24 00:42:01 ID 009 D 151.31 T 63.5 B 14.0 G 220 R 0000 +2025/10/24 08:42:01 ID 009 D 151.25 T 61.9 B 14.0 G 219 R 0000 +2025/10/24 16:42:01 ID 009 D 151.31 T 62.6 B 14.0 G 219 R 0000 +2025/10/25 00:42:01 ID 009 D 151.36 T 62.8 B 14.0 G 219 R 0000 +2025/10/25 08:42:01 ID 009 D 151.31 T 61.3 B 14.0 G 219 R 0000 +2025/10/25 16:42:01 ID 009 D 151.36 T 61.9 B 14.0 G 219 R 0000 +2025/10/26 00:42:01 ID 009 D 151.36 T 61.9 B 14.0 G 219 R 0000 +2025/10/26 08:42:01 ID 009 D 151.25 T 60.6 B 14.0 G 219 R 0000 +2025/10/26 16:42:01 ID 009 D 151.25 T 61.3 B 14.0 G 219 R 0000 +2025/10/27 00:42:01 ID 009 D 151.36 T 61.5 B 14.0 G 219 R 0000 +2025/10/27 08:42:01 ID 009 D 151.36 T 60.6 B 14.0 G 219 R 0000 +2025/10/27 16:42:01 ID 009 D 151.31 T 61.3 B 14.0 G 219 R 0000 +2025/10/28 00:42:01 ID 009 D 151.42 T 61.5 B 14.0 G 219 R 0000 +2025/10/28 08:42:01 ID 009 D 151.47 T 60.6 B 14.0 G 219 R 0000 +2025/10/28 16:42:01 ID 009 D 151.53 T 61.1 B 14.0 G 219 R 0000 +2025/10/29 00:42:01 ID 009 D 151.58 T 60.8 B 14.0 G 219 R 0000 +2025/10/29 08:42:01 ID 009 D 151.58 T 58.9 B 14.0 G 218 R 0000 +2025/10/29 16:42:01 ID 009 D 151.42 T 59.7 B 14.0 G 218 R 0000 +2025/10/30 00:42:01 ID 009 D 151.36 T 59.3 B 14.0 G 218 R 0000 +2025/10/30 08:42:01 ID 009 D 151.42 T 58.0 B 14.0 G 218 R 0000 +2025/10/30 16:42:01 ID 009 D 151.25 T 58.7 B 14.0 G 218 R 0000 +2025/10/31 00:42:01 ID 009 D 151.30 T 58.9 B 14.0 G 218 R 0000 +2025/10/31 08:42:01 ID 009 D 151.25 T 58.0 B 14.0 G 218 R 0000 +2025/10/31 16:42:01 ID 009 D 151.25 T 59.1 B 14.0 G 218 R 0000 +2025/11/01 00:42:01 ID 009 D 151.30 T 59.1 B 14.0 G 218 R 0000 +2025/11/01 08:42:01 ID 009 D 151.42 T 58.4 B 14.0 G 218 R 0000 +2025/11/01 16:42:01 ID 009 D 151.41 T 59.1 B 14.0 G 218 R 0000 +2025/11/02 00:42:01 ID 009 D 151.53 T 59.1 B 14.0 G 218 R 0000 +2025/11/02 08:42:01 ID 009 D 151.42 T 58.0 B 14.0 G 218 R 0000 +2025/11/02 16:42:01 ID 009 D 151.36 T 59.5 B 14.0 G 218 R 0000 +2025/11/03 00:42:01 ID 009 D 151.47 T 59.7 B 14.0 G 218 R 0000 +2025/11/03 08:42:07 ID 009 D 165.63 T 59.1 B 13.9 G 219 R 0000 +2025/11/03 16:42:07 ID 009 D 151.31 T 60.2 B 13.9 G 219 R 0000 +2025/11/04 00:42:01 ID 009 D 151.42 T 60.8 B 14.0 G 219 R 0000 +2025/11/04 08:42:01 ID 009 D 151.47 T 60.0 B 14.0 G 219 R 0000 +2025/11/04 16:42:01 ID 009 D 151.42 T 61.0 B 14.0 G 219 R 0000 +2025/11/05 00:42:01 ID 009 D 151.42 T 61.0 B 14.0 G 219 R 0000 +2025/11/05 08:42:01 ID 009 D 151.53 T 59.5 B 14.0 G 218 R 0000 +2025/11/05 16:42:01 ID 009 D 151.47 T 60.2 B 14.0 G 218 R 0000 +2025/11/06 00:42:01 ID 009 D 151.42 T 60.2 B 14.0 G 218 R 0000 +2025/11/06 08:42:01 ID 009 D 151.42 T 59.1 B 14.0 G 218 R 0000 +2025/11/06 16:42:01 ID 009 D 151.19 T 60.0 B 14.0 G 218 R 0000 +2025/11/07 00:42:01 ID 009 D 151.36 T 60.0 B 14.0 G 218 R 0000 +2025/11/07 08:42:01 ID 009 D 154.31 T 58.7 B 14.0 G 218 R 0000 diff --git a/public/EB-165.wcsv b/public/EB-165.wcsv new file mode 100644 index 00000000..50c27e8b --- /dev/null +++ b/public/EB-165.wcsv @@ -0,0 +1,406 @@ +timestamp,temperature_C,temperature_raw,depth +2023-03-01 13:06:24,1.55555555556,348,478.09 +2023-03-01 21:06:03,7.16666666667,449,478.26 +2023-03-01 21:07:06,7.5,455,478.44 +2023-03-03 21:04:59,7.88888888889,462,478.23 +2023-03-04 05:04:38,2.11111111111,358,478.2 +2023-03-04 21:03:55,10.1111111111,502,481.71 +2023-03-05 05:03:34,3.88888888889,390,478.3 +2023-03-05 13:03:12,1.72222222222,351,478.37 +2023-03-05 21:02:50,11.6111111111,529,481.66 +2023-03-06 13:02:07,2.61111111111,367,478.53 +2023-03-06 21:01:45,12.5,545,481.78 +2023-03-07 05:01:24,5.77777777778,424,478.69 +2023-03-07 13:01:02,4.55555555556,402,478.45 +2023-03-07 21:00:40,12.8888888889,552,481.86 +2023-03-08 05:00:19,5.16666666667,413,478.61 +2023-03-08 12:59:46,2.66666666667,368,478.6 +2023-03-08 20:59:24,14.2777777778,577,481.92 +2023-03-09 04:59:03,8.77777777778,478,478.63 +2023-03-09 12:58:51,2.55555555556,366,478.52 +2023-03-09 20:58:29,14.3888888889,579,481.85 +2023-03-10 04:58:08,4.72222222222,405,478.63 +2023-03-10 12:57:46,2.88888888889,372,478.67 +2023-03-10 20:57:24,15.1666666667,593,481.97 +2023-03-11 04:57:03,8.38888888889,471,478.56 +2023-03-11 12:56:42,6.27777777778,433,478.59 +2023-03-11 20:56:20,14.2777777778,577,478.8 +2023-03-12 04:55:59,5.61111111111,421,478.61 +2023-03-12 12:55:37,4.05555555556,393,478.55 +2023-03-12 20:55:15,12.3888888889,543,481.82 +2023-03-13 04:54:54,4.94444444444,409,478.53 +2023-03-13 12:54:31,3.38888888889,381,478.64 +2023-03-13 20:54:09,9.83333333333,497,478.62 +2023-03-14 04:53:48,4.77777777778,406,478.39 +2023-03-14 12:53:27,1.27777777778,343,478.54 +2023-03-14 20:53:05,13.6111111111,565,481.73 +2023-03-15 04:52:44,6.77777777778,442,478.53 +2023-03-15 12:52:22,5.66666666667,422,478.65 +2023-03-15 20:52:00,14.1111111111,574,481.64 +2023-03-16 04:51:39,6.77777777778,442,478.32 +2023-03-16 12:51:17,4.66666666667,404,478.46 +2023-03-16 20:50:55,8.77777777778,478,478.35 +2023-03-17 04:50:34,3.94444444444,391,478.41 +2023-03-17 20:49:51,5.16666666667,413,478.57 +2023-03-18 04:49:30,2.11111111111,358,478.52 +2023-03-18 20:48:47,8.22222222222,468,478.76 +2023-03-19 20:47:44,10.1666666667,503,478.87 +2023-03-20 04:47:23,2.72222222222,369,478.56 +2023-03-20 12:47:02,2.55555555556,366,478.52 +2023-03-20 20:46:40,8.38888888889,471,478.87 +2023-03-21 04:46:19,3.27777777778,379,478.47 +2023-03-21 12:45:58,2.55555555556,366,478.49 +2023-03-21 20:45:36,6.83333333333,443,478.42 +2023-03-22 04:45:15,4.27777777778,397,478.41 +2023-03-22 12:44:54,4.38888888889,399,478.52 +2023-03-22 20:44:32,12.6111111111,547,481.61 +2023-03-23 04:44:11,6.38888888889,435,478.56 +2023-03-23 12:43:47,3.72222222222,387,478.47 +2023-03-23 20:43:25,12.5,545,481.61 +2023-03-24 04:43:04,3.88888888889,390,478.3 +2023-03-24 20:42:21,7.44444444444,454,478.47 +2023-03-25 04:42:00,2.33333333333,362,478.45 +2023-03-25 20:41:18,9.33333333333,488,478.61 +2023-03-26 20:40:14,8.05555555556,465,478.59 +2023-03-27 20:39:09,9.44444444444,490,478.58 +2023-03-28 20:38:06,12.8333333333,551,481.89 +2023-03-29 04:37:45,3.83333333333,389,478.51 +2023-03-29 12:37:24,1.5,347,478.58 +2023-03-29 20:37:02,16.0555555556,609,481.71 +2023-03-30 04:36:41,7.66666666667,458,478.47 +2023-03-30 12:36:19,4.66666666667,404,478.49 +2023-03-30 20:35:57,17.2777777778,631,481.9 +2023-03-31 04:35:36,5.5,419,478.37 +2023-03-31 20:34:53,11.6111111111,529,478.51 +2023-04-01 20:33:46,16.7222222222,621,482.03 +2023-04-02 04:33:25,6.88888888889,444,478.6 +2023-04-02 12:33:05,3.5,383,478.54 +2023-04-02 20:32:43,17.7777777778,640,482.33 +2023-04-03 04:32:22,7.94444444444,463,479.18 +2023-04-03 12:31:59,4.66666666667,404,478.49 +2023-04-03 20:31:37,18.5555555556,654,481.82 +2023-04-04 04:31:16,10.1111111111,502,479.25 +2023-04-04 20:30:33,9.77777777778,496,478.72 +2023-04-05 20:29:27,12.5,545,479.86 +2023-04-06 20:28:23,16.8888888889,624,483.12 +2023-04-07 04:28:02,7.33333333333,452,478.64 +2023-04-07 12:27:42,3.83333333333,389,478.44 +2023-04-07 20:27:20,18.6111111111,655,488.3 +2023-04-08 04:26:59,8.22222222222,468,478.48 +2023-04-08 12:26:37,4.94444444444,409,478.95 +2023-04-08 20:26:15,19.7222222222,675,481.84 +2023-04-09 04:25:54,10.0,500,478.48 +2023-04-09 12:25:31,5.27777777778,415,478.75 +2023-04-09 20:25:10,19.9444444444,679,482.3 +2023-04-10 04:24:49,9.94444444444,499,478.72 +2023-04-10 12:24:26,5.66666666667,422,478.65 +2023-04-10 20:24:04,22.7777777778,730,485.61 +2023-04-11 04:23:43,11.8333333333,533,478.83 +2023-04-11 12:23:21,7.27777777778,451,478.54 +2023-04-11 20:22:59,24.1111111111,754,482.62 +2023-04-12 04:22:38,13.0,554,478.6 +2023-04-12 12:22:16,9.0,482,478.71 +2023-04-12 20:21:54,24.7777777778,766,482.04 +2023-04-13 04:21:33,13.5555555556,564,478.47 +2023-04-13 12:21:10,11.2777777778,523,478.43 +2023-04-13 20:20:48,20.9444444444,697,484.18 +2023-04-14 04:20:27,12.5555555556,546,478.84 +2023-04-14 12:20:04,8.0,464,478.16 +2023-04-14 20:19:43,18.6111111111,655,481.54 +2023-04-15 04:19:22,8.38888888889,471,479.15 +2023-04-15 20:18:37,18.1666666667,647,481.88 +2023-04-16 04:18:16,8.5,473,484.69 +2023-04-16 20:17:31,20.2777777778,685,482.62 +2023-04-17 04:17:10,10.8333333333,515,478.81 +2023-04-17 12:16:50,6.72222222222,441,478.77 +2023-04-17 20:16:28,22.8333333333,731,487.82 +2023-04-18 04:16:07,11.7777777778,532,478.97 +2023-04-18 12:15:44,8.38888888889,471,478.38 +2023-04-18 20:15:22,21.7777777778,712,482.26 +2023-04-19 04:15:01,12.6111111111,547,478.49 +2023-04-19 20:14:16,20.5555555556,690,483.33 +2023-04-20 04:13:55,10.4444444444,508,478.38 +2023-04-20 12:13:32,5.55555555556,420,478.19 +2023-04-20 20:13:10,21.2777777778,703,492.98 +2023-04-21 04:12:49,8.16666666667,467,478.59 +2023-04-21 20:12:06,18.6111111111,655,481.82 +2023-04-22 04:11:45,11.2777777778,523,478.64 +2023-04-22 12:11:24,8.88888888889,480,478.53 +2023-04-22 20:11:02,19.2777777778,667,481.69 +2023-04-23 04:10:41,10.2222222222,504,478.62 +2023-04-23 12:10:18,4.44444444444,400,478.42 +2023-04-23 20:09:56,20.0555555556,681,481.74 +2023-04-24 04:09:35,11.0,518,478.43 +2023-04-24 12:09:13,7.77777777778,460,478.51 +2023-04-24 20:08:51,18.2777777778,649,481.64 +2023-04-25 04:08:30,10.9444444444,517,478.46 +2023-04-25 12:08:08,7.0,446,478.36 +2023-04-25 20:07:46,18.3333333333,650,481.68 +2023-04-26 04:07:25,8.11111111111,466,478.38 +2023-04-26 12:07:02,5.38888888889,417,478.43 +2023-04-26 20:06:40,16.8333333333,623,481.58 +2023-04-27 04:06:19,9.0,482,478.43 +2023-04-27 12:05:58,5.55555555556,420,478.58 +2023-04-27 20:05:36,22.8333333333,731,493.28 +2023-04-28 04:05:15,13.3333333333,560,481.94 +2023-04-28 12:04:52,7.44444444444,454,478.47 +2023-04-28 20:04:31,13.7777777778,568,481.74 +2023-04-29 04:04:10,8.44444444444,472,478.66 +2023-04-29 12:03:47,5.0,410,479.27 +2023-04-29 20:03:25,21.2777777778,703,481.87 +2023-04-30 04:03:04,13.2222222222,558,481.76 +2023-04-30 12:02:42,8.5,473,478.63 +2023-04-30 20:02:21,26.1666666667,791,482.2 +2023-05-01 04:00:54,15.2777777778,595,481.69 +2023-05-01 04:02:00,15.7777777778,604,481.77 +2023-05-01 12:00:33,12.1666666667,539,478.55 +2023-05-01 12:01:36,8.88888888889,480,478.64 +2023-05-01 20:00:12,24.8333333333,767,482.21 +2023-05-01 20:01:15,26.2777777778,793,489.7 +2023-05-02 03:59:51,13.6111111111,565,481.66 +2023-05-03 11:59:26,9.66666666667,494,478.61 +2023-05-03 19:59:04,25.3333333333,776,481.84 +2023-05-04 03:58:43,15.8333333333,605,481.63 +2023-05-04 11:58:20,12.2777777778,541,478.63 +2023-05-04 19:57:58,25.0,770,494.65 +2023-05-05 03:57:37,16.1111111111,610,487.31 +2023-05-05 11:57:13,10.8333333333,515,488.97 +2023-05-05 19:56:52,23.7777777778,748,497.79 +2023-05-06 03:56:31,16.0555555556,609,484.65 +2023-05-06 11:56:07,10.6666666667,512,497.24 +2023-05-06 19:55:46,25.0,770,491.82 +2023-05-07 03:55:25,16.1666666667,611,486.16 +2023-05-07 11:55:02,10.7222222222,513,496.4 +2023-05-07 19:54:41,25.8888888889,786,490.04 +2023-05-08 03:54:20,16.8333333333,623,497.42 +2023-05-08 11:53:56,11.7222222222,531,491.2 +2023-05-08 19:53:35,27.1111111111,808,488.38 +2023-05-09 03:53:14,17.2777777778,631,485.2 +2023-05-09 11:52:51,12.1666666667,539,493.2 +2023-05-09 19:52:30,27.0,806,482.74 +2023-05-10 03:52:09,16.2777777778,613,483.5 +2023-05-10 11:51:46,11.9444444444,535,479.81 +2023-05-10 19:51:25,26.8333333333,803,482.42 +2023-05-11 03:51:04,14.8888888889,588,478.57 +2023-05-11 11:50:39,10.2777777778,505,478.91 +2023-05-11 19:50:18,21.8888888889,714,491.62 +2023-05-12 03:49:57,15.2222222222,594,481.76 +2023-05-12 11:49:34,10.0,500,478.59 +2023-05-12 19:49:13,26.0555555556,789,482.16 +2023-05-13 03:48:52,16.6111111111,619,478.7 +2023-05-13 11:48:28,13.7777777778,568,478.83 +2023-05-13 19:48:07,23.7222222222,747,481.98 +2023-05-14 03:47:46,15.7777777778,604,490.08 +2023-05-14 11:47:22,10.5555555556,510,478.84 +2023-05-14 19:47:01,22.2777777778,721,481.99 +2023-05-15 03:46:40,15.1666666667,593,481.94 +2023-05-15 11:46:17,11.3333333333,524,478.92 +2023-05-15 19:45:56,24.0,752,482.13 +2023-05-16 03:45:35,17.1666666667,629,482.18 +2023-05-16 11:45:11,12.2777777778,541,478.98 +2023-05-16 19:44:50,25.9444444444,787,486.51 +2023-05-17 03:44:29,18.7777777778,658,493.84 +2023-05-17 11:44:07,13.4444444444,562,481.87 +2023-05-17 19:43:45,29.0,842,482.99 +2023-05-18 03:43:24,13.9444444444,571,481.84 +2023-05-18 11:43:01,12.2222222222,540,478.77 +2023-05-18 19:42:39,20.7777777778,694,482.63 +2023-05-19 03:42:18,13.7777777778,568,481.7 +2023-05-19 11:41:55,12.7222222222,549,481.96 +2023-05-19 19:41:34,13.4444444444,562,481.91 +2023-05-20 03:41:13,13.7777777778,568,481.91 +2023-05-20 11:40:49,11.2777777778,523,481.9 +2023-05-20 19:40:27,22.2777777778,721,482.03 +2023-05-21 03:40:06,12.3888888889,543,478.63 +2023-05-21 11:39:44,10.7777777778,514,478.67 +2023-05-21 19:39:22,22.7222222222,729,482 +2023-05-22 03:39:01,12.8888888889,552,478.64 +2023-05-22 11:38:40,10.0,500,478.62 +2023-05-22 19:38:17,22.7222222222,729,481.96 +2023-05-23 03:37:56,14.3888888889,579,482.27 +2023-05-23 11:37:32,10.7222222222,513,478.95 +2023-05-23 19:37:11,21.2222222222,702,481.9 +2023-05-24 03:36:49,15.5,599,481.73 +2023-05-24 11:36:27,11.3888888889,525,479.07 +2023-05-24 19:36:05,26.2222222222,792,481.96 +2023-05-25 03:35:44,18.3333333333,650,481.82 +2023-05-25 11:35:21,13.3333333333,560,478.82 +2023-05-25 19:34:59,27.9444444444,823,496.21 +2023-05-26 03:34:38,19.2777777778,667,481.97 +2023-05-26 11:34:16,14.7222222222,585,481.93 +2023-05-26 19:33:54,27.5,815,481.98 +2023-05-27 03:33:33,18.8888888889,660,481.9 +2023-05-27 11:33:10,14.8888888889,588,482.21 +2023-05-27 19:32:48,26.7777777778,802,482.07 +2023-05-28 03:32:27,21.2222222222,702,486.91 +2023-05-28 11:32:04,13.2777777778,559,478.82 +2023-05-28 19:31:42,26.7777777778,802,482.04 +2023-05-29 03:31:21,19.6666666667,674,481.81 +2023-05-29 11:30:58,13.2777777778,559,478.71 +2023-05-29 19:30:36,28.4444444444,832,482.14 +2023-05-30 03:30:15,20.4444444444,688,482.38 +2023-05-30 11:29:53,14.4444444444,580,479.01 +2023-05-30 19:29:31,28.6666666667,836,482.49 +2023-05-31 03:29:10,21.8888888889,714,488.26 +2023-05-31 11:28:46,15.4444444444,598,478.79 +2023-05-31 19:28:24,28.0,824,489.7 +2023-06-01 03:28:03,17.2222222222,630,481.76 +2023-06-01 11:27:41,13.1111111111,556,479.2 +2023-06-01 19:27:19,15.8888888889,606,478.83 +2023-06-02 03:26:58,15.7777777778,604,478.65 +2023-06-02 11:26:35,12.0555555556,537,478.83 +2023-06-02 19:26:13,19.2777777778,667,481.9 +2023-06-03 03:25:52,15.5555555556,600,478.75 +2023-06-03 11:25:29,11.1111111111,520,479.59 +2023-06-03 19:25:07,23.8333333333,749,481.98 +2023-06-04 03:24:46,18.7222222222,657,489.29 +2023-06-04 11:24:23,12.1666666667,539,481.5 +2023-06-04 19:24:01,26.3888888889,795,481.99 +2023-06-05 03:23:40,16.6111111111,619,481.93 +2023-06-05 11:23:18,12.5,545,478.95 +2023-06-05 19:22:56,25.3888888889,777,482.08 +2023-06-06 03:22:35,18.6666666667,656,481.93 +2023-06-06 11:22:12,13.1111111111,556,478.99 +2023-06-06 19:21:50,26.9444444444,805,482.11 +2023-06-07 03:21:29,16.8333333333,623,492.79 +2023-06-07 11:21:07,13.0555555556,555,481.93 +2023-06-07 19:20:45,24.3333333333,758,481.92 +2023-06-08 03:20:24,17.7777777778,640,481.84 +2023-06-08 11:20:01,12.8333333333,551,479.16 +2023-06-08 19:19:39,26.6666666667,800,482.03 +2023-06-09 03:19:18,20.3888888889,687,482.03 +2023-06-09 11:18:56,13.6111111111,565,478.72 +2023-06-09 19:18:34,26.5,797,482 +2023-06-10 03:18:13,19.1111111111,664,481.97 +2023-06-10 11:17:50,13.5,563,488.67 +2023-06-10 19:17:28,27.8888888889,822,482.06 +2023-06-11 03:17:07,21.3888888889,705,489.26 +2023-06-11 11:16:44,13.7222222222,567,478.69 +2023-06-11 19:16:22,27.2777777778,811,482.15 +2023-06-12 03:16:01,20.2777777778,685,481.89 +2023-06-12 11:15:38,15.6666666667,602,481.87 +2023-06-12 19:15:16,27.0,806,482.11 +2023-06-13 03:14:55,20.7777777778,694,481.86 +2023-06-13 11:14:32,13.8333333333,569,483.87 +2023-06-13 19:14:10,25.4444444444,778,482.22 +2023-06-14 03:13:49,19.8888888889,678,487.98 +2023-06-14 11:13:26,12.8888888889,552,478.6 +2023-06-14 19:13:04,28.7777777778,838,496.86 +2023-06-15 03:12:43,20.8888888889,696,482 +2023-06-15 11:12:20,15.1111111111,592,479.06 +2023-06-15 19:11:58,25.5555555556,780,481.98 +2023-06-16 03:11:37,21.8888888889,714,481.77 +2023-06-16 11:11:15,15.7777777778,604,481.98 +2023-06-16 19:10:53,27.7777777778,820,482.23 +2023-06-17 03:10:32,21.2777777778,703,481.94 +2023-06-17 11:10:08,14.4444444444,580,479.01 +2023-06-17 19:09:46,27.0,806,482 +2023-06-18 03:09:25,21.6666666667,710,487.52 +2023-06-18 11:09:03,14.0555555556,573,478.69 +2023-06-18 19:08:41,28.0555555556,825,482.13 +2023-06-19 03:08:20,23.3333333333,740,481.83 +2023-06-19 11:07:57,16.2777777778,613,482.03 +2023-06-19 19:07:35,29.0555555556,843,482.25 +2023-06-20 03:07:14,25.1111111111,772,481.97 +2023-06-20 11:06:51,17.7777777778,640,482.23 +2023-06-20 19:06:29,29.1666666667,845,482.25 +2023-06-21 03:06:08,25.4444444444,778,487.06 +2023-06-21 11:05:46,15.7777777778,604,478.93 +2023-06-21 19:05:24,31.2777777778,883,489.82 +2023-06-22 03:05:03,24.8888888889,768,481.97 +2023-06-22 11:04:40,18.0555555556,645,482.51 +2023-06-22 19:04:18,31.6111111111,889,486.22 +2023-06-23 03:03:57,25.6666666667,782,487.52 +2023-06-23 11:03:34,19.1666666667,665,482.25 +2023-06-23 19:03:12,30.1666666667,863,482.45 +2023-06-24 03:02:51,24.8888888889,768,481.93 +2023-06-24 11:02:28,15.6666666667,602,479.39 +2023-06-24 19:02:06,30.0,860,482.2 +2023-06-25 03:01:45,25.9444444444,787,492.11 +2023-06-25 11:01:22,15.2777777778,595,478.89 +2023-06-25 19:01:00,31.8333333333,893,482.41 +2023-06-26 03:00:39,24.7777777778,766,482.04 +2023-06-26 11:00:17,18.8333333333,659,482.32 +2023-06-26 18:59:55,31.8888888889,894,482.27 +2023-06-27 02:59:34,25.1111111111,772,482.04 +2023-06-27 10:59:10,21.7777777778,712,482.47 +2023-06-27 18:58:48,30.2777777778,865,482.66 +2023-06-28 02:58:27,24.7777777778,766,489.18 +2023-06-28 10:58:04,18.2222222222,648,482.06 +2023-06-28 18:57:42,31.5555555556,888,482.19 +2023-06-29 02:57:21,25.3888888889,777,483.97 +2023-06-29 10:56:58,19.6666666667,674,483.56 +2023-06-29 18:56:36,32.0555555556,897,492.57 +2023-06-30 02:56:15,22.3888888889,723,482.27 +2023-06-30 10:55:52,16.6111111111,619,482.38 +2023-06-30 18:55:30,29.6666666667,854,482.47 +2023-07-01 02:54:03,25.3888888889,777,489.12 +2023-07-01 02:55:09,25.6666666667,782,482.02 +2023-07-01 10:53:42,19.0555555556,663,488.52 +2023-07-01 10:54:46,17.1666666667,629,479.8 +2023-07-01 18:53:21,28.2777777778,829,482.24 +2023-07-01 18:54:24,28.8333333333,839,482.28 +2023-07-02 02:53:00,23.3333333333,740,482.04 +2023-07-03 10:52:35,17.8333333333,641,482.33 +2023-07-03 18:52:13,30.4444444444,868,482.42 +2023-07-04 02:51:52,27.7777777778,820,482.75 +2023-07-04 10:51:27,19.9444444444,679,484.05 +2023-07-04 18:51:06,33.2222222222,918,486.11 +2023-07-05 02:50:45,28.7222222222,837,491.74 +2023-07-05 10:50:22,19.6111111111,673,482.15 +2023-07-05 18:50:00,33.1111111111,916,482.67 +2023-07-06 02:49:39,28.2777777778,829,482.06 +2023-07-06 10:49:16,19.5,671,482.4 +2023-07-06 18:48:54,33.0,914,496.09 +2023-07-07 02:48:33,28.2777777778,829,482.13 +2023-07-07 10:48:10,21.0,698,482.49 +2023-07-07 18:47:48,32.6666666667,908,482.14 +2023-07-08 02:47:27,27.5,815,481.94 +2023-07-08 10:47:03,20.2222222222,684,482.62 +2023-07-08 18:46:41,31.6111111111,889,482.09 +2023-07-09 02:46:20,27.6666666667,818,489.66 +2023-07-09 10:45:58,19.2777777778,667,482.08 +2023-07-09 18:45:36,32.0,896,482.16 +2023-07-10 02:45:15,29.3333333333,848,481.98 +2023-07-10 10:44:52,21.8888888889,714,482.58 +2023-07-10 18:44:30,31.6666666667,890,486.89 +2023-07-11 02:44:09,29.0555555556,843,482.22 +2023-07-11 10:43:45,20.7777777778,694,482.1 +2023-07-11 18:43:23,33.4444444444,922,491.54 +2023-07-12 10:42:39,22.2777777778,721,482.06 +2023-07-12 18:42:17,33.7777777778,928,483.91 +2023-07-13 02:41:56,30.5555555556,870,482.17 +2023-07-13 10:41:33,21.3888888889,705,484.29 +2023-07-13 18:41:11,32.7777777778,910,499.21 +2023-07-14 02:40:50,28.5555555556,834,482.24 +2023-07-14 10:40:27,20.3888888889,687,482.8 +2023-07-14 18:40:05,31.7222222222,891,482.37 +2023-07-15 02:39:44,28.7222222222,837,482.04 +2023-07-15 10:39:21,20.5,689,482.03 +2023-07-15 18:38:59,32.1666666667,899,482.24 +2023-07-16 02:38:38,29.2777777778,847,487.76 +2023-07-16 10:38:15,20.7777777778,694,482.35 +2023-07-16 18:37:53,31.0,878,482.95 +2023-07-17 02:37:32,30.5,869,482.07 +2023-07-17 10:37:09,21.1666666667,701,482.92 +2023-07-17 18:36:47,33.6111111111,925,487.97 +2023-07-18 02:36:26,31.7222222222,891,482.4 +2023-07-18 10:36:03,21.6666666667,710,482.23 +2023-07-18 18:35:41,34.3333333333,938,485.46 +2023-07-19 02:35:20,30.1666666667,863,486.48 +2023-07-19 10:34:57,21.7222222222,711,482.26 +2023-07-19 18:34:35,34.1111111111,934,485.04 +2023-07-20 02:34:14,31.5,887,482.29 +2023-07-20 10:33:50,22.8888888889,732,482.32 +2023-07-20 18:33:29,34.8333333333,947,496.83 +2023-07-21 02:33:08,26.5555555556,798,482.6 +2023-07-21 10:32:45,21.0555555556,699,483.17 +2023-07-21 18:32:23,31.1666666667,881,482.44 +2023-07-22 02:32:02,27.2777777778,811,482.2 +2023-07-22 10:31:38,20.4444444444,688,482.32 +2023-07-22 18:31:16,29.7777777778,856,482.35 +2023-07-23 02:30:55,25.9444444444,787,486.66 +2023-07-23 10:30:32,19.1111111111,664,482.4 +2023-07-23 18:30:11,29.9444444444,859,482.52 +2023-07-24 02:29:50,30.5555555556,870,482.36 +2023-07-24 10:29:27,21.2222222222,702,485.35 diff --git a/public/content/analytics-disclosure.md b/public/content/analytics-disclosure.md new file mode 100644 index 00000000..396a837a --- /dev/null +++ b/public/content/analytics-disclosure.md @@ -0,0 +1,28 @@ +--- +title: Analytics Disclosure +deck: Why Ocotillo records production sessions and uses site analytics. +date: 2026-08-14 +--- + +## What is recorded? + +Ocotillo uses PostHog to collect site analytics and, on the production site, session recordings for signed-in users. A session recording captures page views, clicks, navigation, and general interface behavior so the development team can understand where the application is confusing, slow, or broken. + +Form inputs are masked by default in session recordings. Ocotillo does not use session recordings to collect passwords or intentionally capture sensitive field values. + +## Why we need it + +Ocotillo supports Bureau staff working with geologic data across many workflows. Session analytics help the team: + +- Find broken or confusing screens +- Understand which workflows need performance or usability improvements +- Troubleshoot support requests with the page and browser context needed to reproduce problems +- Prioritize fixes based on real usage instead of guesses + +## When it starts + +Session recording starts after sign-in on the production Ocotillo site. Analytics events may also record basic application activity, such as page views and feature usage. + +## Questions + +Contact the Data Services team at [ocotillo-nmbg@nmt.edu](mailto:ocotillo-nmbg@nmt.edu) with questions about Ocotillo analytics or session recording. diff --git a/public/content/ogcapi.md b/public/content/ogcapi.md index 5d14294b..b3debed9 100644 --- a/public/content/ogcapi.md +++ b/public/content/ogcapi.md @@ -63,8 +63,7 @@ Review available collections before connecting from desktop GIS. - [!CHIPS] - Water Wells - Springs -- Latest Depth to Water -- Average TDS - Latest TDS +- Water Well Summary Collection names can change by deployment. If you do not see one of these, open the [collections endpoint]({{ ocotillo_api_url }}/ogcapi/collections) and use the names published there. diff --git a/public/example_diver_office.csv b/public/example_diver_office.csv new file mode 100644 index 00000000..48046e11 --- /dev/null +++ b/public/example_diver_office.csv @@ -0,0 +1,366 @@ +Data file for DataLogger. +============================================================= +Serial number=V9917 2201 +Location=DM-0107 +============================================================= +2025/01/15 00:00:00,13.495,13.8 +2025/01/15 06:00:00,13.509,13.7 +2025/01/15 12:00:00,13.509,13.9 +2025/01/15 18:00:00,13.499,14.0 +2025/01/16 00:00:00,13.501,14.0 +2025/01/16 06:00:00,13.504,13.8 +2025/01/16 12:00:00,13.516,14.2 +2025/01/16 18:00:00,13.507,13.8 +2025/01/17 00:00:00,13.520,14.3 +2025/01/17 06:00:00,13.516,13.9 +2025/01/17 12:00:00,13.524,13.7 +2025/01/17 18:00:00,13.515,13.9 +2025/01/18 00:00:00,13.487,13.8 +2025/01/18 06:00:00,13.484,14.2 +2025/01/18 12:00:00,13.471,14.0 +2025/01/18 18:00:00,13.474,13.9 +2025/01/19 00:00:00,13.460,13.7 +2025/01/19 06:00:00,13.434,13.8 +2025/01/19 12:00:00,13.440,14.0 +2025/01/19 18:00:00,13.417,14.1 +2025/01/20 00:00:00,13.409,13.9 +2025/01/20 06:00:00,13.407,14.1 +2025/01/20 12:00:00,13.379,14.0 +2025/01/20 18:00:00,13.377,14.2 +2025/01/21 00:00:00,13.373,13.9 +2025/01/21 06:00:00,13.372,13.8 +2025/01/21 12:00:00,13.347,14.2 +2025/01/21 18:00:00,13.333,14.0 +2025/01/22 00:00:00,13.324,14.1 +2025/01/22 06:00:00,13.342,14.0 +2025/01/22 12:00:00,13.343,13.9 +2025/01/22 18:00:00,13.337,14.1 +2025/01/23 00:00:00,13.334,14.0 +2025/01/23 06:00:00,13.343,14.3 +2025/01/23 12:00:00,13.334,14.1 +2025/01/23 18:00:00,13.325,14.1 +2025/01/24 00:00:00,13.346,14.3 +2025/01/24 06:00:00,13.356,13.9 +2025/01/24 12:00:00,13.347,14.1 +2025/01/24 18:00:00,13.340,14.0 +2025/01/25 00:00:00,13.349,13.8 +2025/01/25 06:00:00,13.349,14.2 +2025/01/25 12:00:00,13.354,13.8 +2025/01/25 18:00:00,13.363,14.2 +2025/01/26 00:00:00,13.355,14.0 +2025/01/26 06:00:00,13.368,14.2 +2025/01/26 12:00:00,13.375,14.2 +2025/01/26 18:00:00,13.355,13.9 +2025/01/27 00:00:00,13.353,14.2 +2025/01/27 06:00:00,13.365,13.8 +2025/01/27 12:00:00,13.335,13.8 +2025/01/27 18:00:00,13.328,14.0 +2025/01/28 00:00:00,13.329,13.9 +2025/01/28 06:00:00,13.301,14.0 +2025/01/28 12:00:00,13.301,14.0 +2025/01/28 18:00:00,13.306,14.1 +2025/01/29 00:00:00,13.281,14.1 +2025/01/29 06:00:00,13.274,13.7 +2025/01/29 12:00:00,13.268,14.2 +2025/01/29 18:00:00,13.255,14.2 +2025/01/30 00:00:00,13.230,13.9 +2025/01/30 06:00:00,13.210,14.1 +2025/01/30 12:00:00,13.199,13.7 +2025/01/30 18:00:00,13.195,13.8 +2025/01/31 00:00:00,13.192,13.7 +2025/01/31 06:00:00,13.176,13.8 +2025/01/31 12:00:00,13.174,13.9 +2025/01/31 18:00:00,13.168,14.2 +2025/02/01 00:00:00,13.184,13.8 +2025/02/01 06:00:00,13.173,13.9 +2025/02/01 12:00:00,13.177,13.8 +2025/02/01 18:00:00,13.193,14.3 +2025/02/02 00:00:00,13.184,14.0 +2025/02/02 06:00:00,13.176,13.8 +2025/02/02 12:00:00,13.188,13.9 +2025/02/02 18:00:00,13.207,13.8 +2025/02/03 00:00:00,13.187,14.3 +2025/02/03 06:00:00,13.206,13.8 +2025/02/03 12:00:00,13.210,13.7 +2025/02/03 18:00:00,13.213,14.3 +2025/02/04 00:00:00,13.226,14.1 +2025/02/04 06:00:00,13.209,13.9 +2025/02/04 12:00:00,13.207,14.2 +2025/02/04 18:00:00,13.217,14.2 +2025/02/05 00:00:00,13.208,13.8 +2025/02/05 06:00:00,13.219,14.3 +2025/02/05 12:00:00,13.216,14.2 +2025/02/05 18:00:00,13.208,14.1 +2025/02/06 00:00:00,13.183,14.0 +2025/02/06 06:00:00,13.178,13.7 +2025/02/06 12:00:00,13.158,13.9 +2025/02/06 18:00:00,13.155,14.1 +2025/02/07 00:00:00,13.164,14.0 +2025/02/07 06:00:00,13.151,14.3 +2025/02/07 12:00:00,13.140,13.9 +2025/02/07 18:00:00,13.105,13.8 +2025/02/08 00:00:00,13.093,13.8 +2025/02/08 06:00:00,13.094,14.2 +2025/02/08 12:00:00,13.089,14.0 +2025/02/08 18:00:00,13.073,14.2 +2025/02/09 00:00:00,13.047,14.1 +2025/02/09 06:00:00,13.063,14.2 +2025/02/09 12:00:00,13.051,14.0 +2025/02/09 18:00:00,13.029,14.2 +2025/02/10 00:00:00,13.029,14.2 +2025/02/10 06:00:00,13.045,13.9 +2025/02/10 12:00:00,13.027,14.3 +2025/02/10 18:00:00,13.036,13.8 +2025/02/11 00:00:00,13.019,13.8 +2025/02/11 06:00:00,13.045,14.2 +2025/02/11 12:00:00,13.025,14.2 +2025/02/11 18:00:00,13.053,14.1 +2025/02/12 00:00:00,13.038,14.0 +2025/02/12 06:00:00,13.036,13.7 +2025/02/12 12:00:00,13.066,14.1 +2025/02/12 18:00:00,13.056,14.3 +2025/02/13 00:00:00,13.057,14.2 +2025/02/13 06:00:00,13.072,13.8 +2025/02/13 12:00:00,13.057,13.9 +2025/02/13 18:00:00,13.058,14.1 +2025/02/14 00:00:00,0.000,14.0 +2025/02/14 06:00:00,0.000,14.2 +2025/02/14 12:00:00,0.000,14.0 +2025/02/14 18:00:00,0.000,14.2 +2025/02/15 00:00:00,13.050,14.3 +2025/02/15 06:00:00,13.046,14.0 +2025/02/15 12:00:00,13.039,13.7 +2025/02/15 18:00:00,13.027,13.8 +2025/02/16 00:00:00,13.004,14.2 +2025/02/16 06:00:00,12.998,14.0 +2025/02/16 12:00:00,13.003,14.0 +2025/02/16 18:00:00,12.979,14.0 +2025/02/17 00:00:00,12.973,14.2 +2025/02/17 06:00:00,12.947,14.0 +2025/02/17 12:00:00,12.940,13.9 +2025/02/17 18:00:00,12.944,14.0 +2025/02/18 00:00:00,12.927,14.2 +2025/02/18 06:00:00,12.927,14.0 +2025/02/18 12:00:00,12.909,14.0 +2025/02/18 18:00:00,12.898,14.1 +2025/02/19 00:00:00,12.890,14.0 +2025/02/19 06:00:00,12.885,14.3 +2025/02/19 12:00:00,12.888,14.2 +2025/02/19 18:00:00,12.893,13.9 +2025/02/20 00:00:00,12.881,14.3 +2025/02/20 06:00:00,12.889,13.8 +2025/02/20 12:00:00,12.869,14.0 +2025/02/20 18:00:00,12.870,13.8 +2025/02/21 00:00:00,12.873,14.1 +2025/02/21 06:00:00,12.898,14.2 +2025/02/21 12:00:00,12.883,14.1 +2025/02/21 18:00:00,12.903,13.8 +2025/02/22 00:00:00,12.913,14.3 +2025/02/22 06:00:00,12.898,14.3 +2025/02/22 12:00:00,12.906,14.0 +2025/02/22 18:00:00,12.927,14.2 +2025/02/23 00:00:00,12.904,14.0 +2025/02/23 06:00:00,12.915,13.9 +2025/02/23 12:00:00,12.905,13.9 +2025/02/23 18:00:00,12.920,13.7 +2025/02/24 00:00:00,12.912,14.0 +2025/02/24 06:00:00,12.891,13.9 +2025/02/24 12:00:00,12.904,14.0 +2025/02/24 18:00:00,12.880,14.3 +2025/02/25 00:00:00,12.893,14.3 +2025/02/25 06:00:00,12.863,13.9 +2025/02/25 12:00:00,12.851,14.2 +2025/02/25 18:00:00,12.846,13.8 +2025/02/26 00:00:00,12.839,14.2 +2025/02/26 06:00:00,12.839,13.9 +2025/02/26 12:00:00,12.807,14.3 +2025/02/26 18:00:00,12.807,14.1 +2025/02/27 00:00:00,12.781,13.7 +2025/02/27 06:00:00,12.787,14.0 +2025/02/27 12:00:00,12.758,14.3 +2025/02/27 18:00:00,12.765,14.2 +2025/02/28 00:00:00,12.740,14.2 +2025/02/28 06:00:00,12.732,14.2 +2025/02/28 12:00:00,12.737,13.9 +2025/02/28 18:00:00,12.736,14.3 +2025/03/01 00:00:00,12.724,13.8 +2025/03/01 06:00:00,12.729,13.8 +2025/03/01 12:00:00,12.716,13.8 +2025/03/01 18:00:00,12.715,13.8 +2025/03/02 00:00:00,12.724,13.9 +2025/03/02 06:00:00,12.740,13.9 +2025/03/02 12:00:00,12.736,13.8 +2025/03/02 18:00:00,12.735,13.7 +2025/03/03 00:00:00,12.736,13.7 +2025/03/03 06:00:00,12.755,14.0 +2025/03/03 12:00:00,12.743,14.0 +2025/03/03 18:00:00,12.769,13.8 +2025/03/04 00:00:00,12.769,14.0 +2025/03/04 06:00:00,12.762,14.2 +2025/03/04 12:00:00,12.760,14.0 +2025/03/04 18:00:00,12.770,14.3 +2025/03/05 00:00:00,12.759,14.2 +2025/03/05 06:00:00,12.768,14.1 +2025/03/05 12:00:00,12.755,13.9 +2025/03/05 18:00:00,12.740,13.8 +2025/03/06 00:00:00,12.734,14.1 +2025/03/06 06:00:00,12.732,13.8 +2025/03/06 12:00:00,12.718,14.2 +2025/03/06 18:00:00,12.732,14.1 +2025/03/07 00:00:00,12.704,13.8 +2025/03/07 06:00:00,12.693,14.0 +2025/03/07 12:00:00,12.677,14.0 +2025/03/07 18:00:00,12.668,14.3 +2025/03/08 00:00:00,12.677,14.0 +2025/03/08 06:00:00,12.643,14.3 +2025/03/08 12:00:00,12.633,13.9 +2025/03/08 18:00:00,12.612,13.9 +2025/03/09 00:00:00,12.616,14.0 +2025/03/09 06:00:00,12.599,14.0 +2025/03/09 12:00:00,12.584,13.9 +2025/03/09 18:00:00,12.580,13.9 +2025/03/10 00:00:00,12.573,13.7 +2025/03/10 06:00:00,12.576,13.8 +2025/03/10 12:00:00,12.582,14.0 +2025/03/10 18:00:00,12.585,14.1 +2025/03/11 00:00:00,13.484,14.2 +2025/03/11 06:00:00,13.475,13.9 +2025/03/11 12:00:00,13.494,13.8 +2025/03/11 18:00:00,13.489,14.1 +2025/03/12 00:00:00,13.472,14.2 +2025/03/12 06:00:00,13.502,14.1 +2025/03/12 12:00:00,13.501,14.2 +2025/03/12 18:00:00,13.488,14.0 +2025/03/13 00:00:00,13.503,14.2 +2025/03/13 06:00:00,13.516,14.2 +2025/03/13 12:00:00,13.512,14.2 +2025/03/13 18:00:00,13.517,14.1 +2025/03/14 00:00:00,13.505,13.7 +2025/03/14 06:00:00,13.502,13.9 +2025/03/14 12:00:00,13.500,14.2 +2025/03/14 18:00:00,13.511,14.1 +2025/03/15 00:00:00,13.510,14.1 +2025/03/15 06:00:00,13.500,13.7 +2025/03/15 12:00:00,13.503,14.1 +2025/03/15 18:00:00,13.486,14.0 +2025/03/16 00:00:00,13.482,13.7 +2025/03/16 06:00:00,13.474,13.9 +2025/03/16 12:00:00,13.444,13.9 +2025/03/16 18:00:00,13.452,13.8 +2025/03/17 00:00:00,13.440,14.3 +2025/03/17 06:00:00,13.420,13.9 +2025/03/17 12:00:00,13.407,14.1 +2025/03/17 18:00:00,13.404,14.1 +2025/03/18 00:00:00,13.389,13.7 +2025/03/18 06:00:00,13.363,13.9 +2025/03/18 12:00:00,13.371,13.9 +2025/03/18 18:00:00,13.356,13.7 +2025/03/19 00:00:00,13.333,13.9 +2025/03/19 06:00:00,13.345,14.1 +2025/03/19 12:00:00,13.339,13.9 +2025/03/19 18:00:00,13.331,14.0 +2025/03/20 00:00:00,13.327,13.8 +2025/03/20 06:00:00,13.338,13.8 +2025/03/20 12:00:00,13.341,14.3 +2025/03/20 18:00:00,13.313,14.0 +2025/03/21 00:00:00,13.339,14.3 +2025/03/21 06:00:00,13.331,13.9 +2025/03/21 12:00:00,13.328,14.3 +2025/03/21 18:00:00,13.332,14.0 +2025/03/22 00:00:00,13.334,14.0 +2025/03/22 06:00:00,13.363,13.8 +2025/03/22 12:00:00,13.363,14.0 +2025/03/22 18:00:00,13.368,14.1 +2025/03/23 00:00:00,13.352,14.2 +2025/03/23 06:00:00,13.361,13.7 +2025/03/23 12:00:00,13.348,14.0 +2025/03/23 18:00:00,13.361,13.9 +2025/03/24 00:00:00,13.350,13.9 +2025/03/24 06:00:00,13.352,14.2 +2025/03/24 12:00:00,13.339,14.2 +2025/03/24 18:00:00,13.358,13.8 +2025/03/25 00:00:00,13.354,14.1 +2025/03/25 06:00:00,13.345,13.9 +2025/03/25 12:00:00,13.320,13.9 +2025/03/25 18:00:00,13.328,14.1 +2025/03/26 00:00:00,13.298,14.0 +2025/03/26 06:00:00,13.284,13.7 +2025/03/26 12:00:00,13.266,14.2 +2025/03/26 18:00:00,13.259,14.3 +2025/03/27 00:00:00,13.246,13.9 +2025/03/27 06:00:00,13.242,13.8 +2025/03/27 12:00:00,13.226,14.3 +2025/03/27 18:00:00,13.231,14.2 +2025/03/28 00:00:00,13.214,14.2 +2025/03/28 06:00:00,13.214,14.0 +2025/03/28 12:00:00,13.200,13.7 +2025/03/28 18:00:00,13.194,14.0 +2025/03/29 00:00:00,13.190,14.1 +2025/03/29 06:00:00,13.172,13.7 +2025/03/29 12:00:00,13.189,13.8 +2025/03/29 18:00:00,13.175,13.9 +2025/03/30 00:00:00,13.170,14.1 +2025/03/30 06:00:00,13.192,13.9 +2025/03/30 12:00:00,13.184,13.9 +2025/03/30 18:00:00,13.185,13.9 +2025/03/31 00:00:00,13.177,13.8 +2025/03/31 06:00:00,13.182,14.2 +2025/03/31 12:00:00,13.195,13.8 +2025/03/31 18:00:00,13.212,14.3 +2025/04/01 00:00:00,13.202,13.8 +2025/04/01 06:00:00,13.198,13.8 +2025/04/01 12:00:00,13.205,13.8 +2025/04/01 18:00:00,13.203,13.9 +2025/04/02 00:00:00,13.214,14.2 +2025/04/02 06:00:00,13.219,13.9 +2025/04/02 12:00:00,13.207,14.0 +2025/04/02 18:00:00,13.202,13.9 +2025/04/03 00:00:00,13.188,13.9 +2025/04/03 06:00:00,13.209,13.8 +2025/04/03 12:00:00,13.188,14.1 +2025/04/03 18:00:00,13.190,13.8 +2025/04/04 00:00:00,13.163,13.8 +2025/04/04 06:00:00,13.156,14.0 +2025/04/04 12:00:00,13.161,14.2 +2025/04/04 18:00:00,13.147,13.7 +2025/04/05 00:00:00,13.110,14.1 +2025/04/05 06:00:00,13.123,14.0 +2025/04/05 12:00:00,13.102,13.7 +2025/04/05 18:00:00,13.084,14.3 +2025/04/06 00:00:00,13.086,14.2 +2025/04/06 06:00:00,13.080,13.8 +2025/04/06 12:00:00,13.044,13.8 +2025/04/06 18:00:00,13.048,14.1 +2025/04/07 00:00:00,13.054,14.1 +2025/04/07 06:00:00,13.039,14.2 +2025/04/07 12:00:00,13.029,14.0 +2025/04/07 18:00:00,13.013,14.2 +2025/04/08 00:00:00,13.017,14.3 +2025/04/08 06:00:00,13.029,13.9 +2025/04/08 12:00:00,13.014,13.9 +2025/04/08 18:00:00,13.031,14.1 +2025/04/09 00:00:00,13.018,13.7 +2025/04/09 06:00:00,13.034,14.0 +2025/04/09 12:00:00,13.034,13.8 +2025/04/09 18:00:00,13.045,13.7 +2025/04/10 00:00:00,13.040,14.0 +2025/04/10 06:00:00,13.064,14.1 +2025/04/10 12:00:00,13.065,14.0 +2025/04/10 18:00:00,13.049,13.8 +2025/04/11 00:00:00,13.073,14.1 +2025/04/11 06:00:00,13.055,13.7 +2025/04/11 12:00:00,13.061,14.1 +2025/04/11 18:00:00,13.058,13.9 +2025/04/12 00:00:00,13.063,14.3 +2025/04/12 06:00:00,13.046,13.7 +2025/04/12 12:00:00,13.044,14.0 +2025/04/12 18:00:00,13.048,13.8 +2025/04/13 00:00:00,13.044,14.1 +2025/04/13 06:00:00,13.026,13.8 +2025/04/13 12:00:00,13.030,13.9 +2025/04/13 18:00:00,13.015,13.8 +2025/04/14 00:00:00,12.985,14.2 +2025/04/14 06:00:00,12.975,14.3 +2025/04/14 12:00:00,12.969,13.8 +2025/04/14 18:00:00,12.949,14.0 +END OF DATA diff --git a/public/example_transducer.csv b/public/example_transducer.csv new file mode 100644 index 00000000..436fe253 --- /dev/null +++ b/public/example_transducer.csv @@ -0,0 +1,363 @@ +# Example transducer export for demo purposes +# thing.name: AR-0209 +Date Time,Depth To Water (ft bgs) +2025-01-15 00:00:00,42.506 +2025-01-15 06:00:00,42.507 +2025-01-15 12:00:00,42.540 +2025-01-15 18:00:00,42.555 +2025-01-16 00:00:00,42.583 +2025-01-16 06:00:00,42.580 +2025-01-16 12:00:00,42.579 +2025-01-16 18:00:00,42.531 +2025-01-17 00:00:00,42.525 +2025-01-17 06:00:00,42.490 +2025-01-17 12:00:00,42.481 +2025-01-17 18:00:00,42.483 +2025-01-18 00:00:00,42.463 +2025-01-18 06:00:00,42.478 +2025-01-18 12:00:00,42.513 +2025-01-18 18:00:00,42.531 +2025-01-19 00:00:00,42.545 +2025-01-19 06:00:00,42.586 +2025-01-19 12:00:00,42.618 +2025-01-19 18:00:00,42.602 +2025-01-20 00:00:00,42.642 +2025-01-20 06:00:00,42.637 +2025-01-20 12:00:00,42.613 +2025-01-20 18:00:00,42.590 +2025-01-21 00:00:00,42.602 +2025-01-21 06:00:00,42.558 +2025-01-21 12:00:00,42.532 +2025-01-21 18:00:00,42.523 +2025-01-22 00:00:00,42.552 +2025-01-22 06:00:00,42.550 +2025-01-22 12:00:00,42.575 +2025-01-22 18:00:00,42.595 +2025-01-23 00:00:00,42.613 +2025-01-23 06:00:00,42.657 +2025-01-23 12:00:00,42.657 +2025-01-23 18:00:00,42.680 +2025-01-24 00:00:00,42.699 +2025-01-24 06:00:00,42.690 +2025-01-24 12:00:00,42.690 +2025-01-24 18:00:00,42.663 +2025-01-25 00:00:00,42.648 +2025-01-25 06:00:00,42.602 +2025-01-25 12:00:00,42.594 +2025-01-25 18:00:00,42.587 +2025-01-26 00:00:00,42.577 +2025-01-26 06:00:00,42.591 +2025-01-26 12:00:00,42.603 +2025-01-26 18:00:00,42.633 +2025-01-27 00:00:00,42.673 +2025-01-27 06:00:00,42.689 +2025-01-27 12:00:00,42.712 +2025-01-27 18:00:00,42.722 +2025-01-28 00:00:00,42.733 +2025-01-28 06:00:00,42.758 +2025-01-28 12:00:00,42.737 +2025-01-28 18:00:00,42.720 +2025-01-29 00:00:00,42.683 +2025-01-29 06:00:00,42.686 +2025-01-29 12:00:00,42.647 +2025-01-29 18:00:00,42.646 +2025-01-30 00:00:00,42.670 +2025-01-30 06:00:00,42.664 +2025-01-30 12:00:00,42.677 +2025-01-30 18:00:00,42.705 +2025-01-31 00:00:00,42.738 +2025-01-31 06:00:00,42.762 +2025-01-31 12:00:00,42.763 +2025-01-31 18:00:00,42.771 +2025-02-01 00:00:00,42.791 +2025-02-01 06:00:00,42.788 +2025-02-01 12:00:00,42.776 +2025-02-01 18:00:00,42.789 +2025-02-02 00:00:00,42.767 +2025-02-02 06:00:00,42.725 +2025-02-02 12:00:00,42.723 +2025-02-02 18:00:00,42.703 +2025-02-03 00:00:00,42.723 +2025-02-03 06:00:00,42.712 +2025-02-03 12:00:00,42.721 +2025-02-03 18:00:00,42.743 +2025-02-04 00:00:00,42.782 +2025-02-04 06:00:00,42.797 +2025-02-04 12:00:00,42.833 +2025-02-04 18:00:00,42.862 +2025-02-05 00:00:00,42.850 +2025-02-05 06:00:00,42.842 +2025-02-05 12:00:00,42.863 +2025-02-05 18:00:00,42.828 +2025-02-06 00:00:00,42.792 +2025-02-06 06:00:00,42.770 +2025-02-06 12:00:00,42.757 +2025-02-06 18:00:00,42.768 +2025-02-07 00:00:00,42.774 +2025-02-07 06:00:00,42.767 +2025-02-07 12:00:00,42.769 +2025-02-07 18:00:00,42.805 +2025-02-08 00:00:00,42.856 +2025-02-08 06:00:00,42.864 +2025-02-08 12:00:00,42.904 +2025-02-08 18:00:00,42.916 +2025-02-09 00:00:00,42.890 +2025-02-09 06:00:00,42.918 +2025-02-09 12:00:00,42.907 +2025-02-09 18:00:00,42.885 +2025-02-10 00:00:00,42.855 +2025-02-10 06:00:00,42.850 +2025-02-10 12:00:00,42.813 +2025-02-10 18:00:00,42.816 +2025-02-11 00:00:00,42.816 +2025-02-11 06:00:00,42.844 +2025-02-11 12:00:00,42.858 +2025-02-11 18:00:00,42.856 +2025-02-12 00:00:00,42.892 +2025-02-12 06:00:00,42.906 +2025-02-12 12:00:00,42.958 +2025-02-12 18:00:00,42.973 +2025-02-13 00:00:00,42.958 +2025-02-13 06:00:00,42.970 +2025-02-13 12:00:00,42.960 +2025-02-13 18:00:00,42.926 +2025-02-14 00:00:00,42.931 +2025-02-14 06:00:00,42.902 +2025-02-14 12:00:00,42.896 +2025-02-14 18:00:00,42.876 +2025-02-15 00:00:00,42.854 +2025-02-15 06:00:00,42.875 +2025-02-15 12:00:00,42.879 +2025-02-15 18:00:00,42.939 +2025-02-16 00:00:00,42.963 +2025-02-16 06:00:00,42.988 +2025-02-16 12:00:00,42.990 +2025-02-16 18:00:00,42.996 +2025-02-17 00:00:00,43.037 +2025-02-17 06:00:00,43.039 +2025-02-17 12:00:00,42.995 +2025-02-17 18:00:00,42.995 +2025-02-18 00:00:00,42.959 +2025-02-18 06:00:00,42.967 +2025-02-18 12:00:00,42.951 +2025-02-18 18:00:00,42.916 +2025-02-19 00:00:00,42.929 +2025-02-19 06:00:00,42.940 +2025-02-19 12:00:00,42.945 +2025-02-19 18:00:00,42.992 +2025-02-20 00:00:00,43.001 +2025-02-20 06:00:00,43.019 +2025-02-20 12:00:00,43.055 +2025-02-20 18:00:00,43.079 +2025-02-21 00:00:00,43.066 +2025-02-21 06:00:00,43.069 +2025-02-21 12:00:00,43.087 +2025-02-21 18:00:00,43.057 +2025-02-22 00:00:00,43.030 +2025-02-22 06:00:00,43.013 +2025-02-22 12:00:00,42.981 +2025-02-22 18:00:00,42.976 +2025-02-23 00:00:00,42.980 +2025-02-23 06:00:00,42.998 +2025-02-23 12:00:00,43.000 +2025-02-23 18:00:00,43.022 +2025-02-24 00:00:00,43.043 +2025-02-24 06:00:00,43.092 +2025-02-24 12:00:00,43.099 +2025-02-24 18:00:00,43.142 +2025-02-25 00:00:00,43.148 +2025-02-25 06:00:00,43.116 +2025-02-25 12:00:00,43.113 +2025-02-25 18:00:00,43.114 +2025-02-26 00:00:00,43.077 +2025-02-26 06:00:00,43.054 +2025-02-26 12:00:00,43.070 +2025-02-26 18:00:00,43.046 +2025-02-27 00:00:00,43.041 +2025-02-27 06:00:00,43.061 +2025-02-27 12:00:00,43.079 +2025-02-27 18:00:00,43.077 +2025-02-28 00:00:00,43.100 +2025-02-28 06:00:00,43.140 +2025-02-28 12:00:00,43.162 +2025-02-28 18:00:00,43.181 +2025-03-01 00:00:00,43.199 +2025-03-01 06:00:00,43.196 +2025-03-01 12:00:00,43.199 +2025-03-01 18:00:00,43.147 +2025-03-02 00:00:00,43.140 +2025-03-02 06:00:00,43.118 +2025-03-02 12:00:00,43.123 +2025-03-02 18:00:00,43.089 +2025-03-03 00:00:00,43.086 +2025-03-03 06:00:00,43.104 +2025-03-03 12:00:00,43.119 +2025-03-03 18:00:00,43.137 +2025-03-04 00:00:00,43.162 +2025-03-04 06:00:00,43.215 +2025-03-04 12:00:00,43.219 +2025-03-04 18:00:00,43.252 +2025-03-05 00:00:00,43.248 +2025-03-05 06:00:00,43.227 +2025-03-05 12:00:00,43.255 +2025-03-05 18:00:00,43.233 +2025-03-06 00:00:00,43.219 +2025-03-06 06:00:00,43.198 +2025-03-06 12:00:00,43.179 +2025-03-06 18:00:00,43.142 +2025-03-07 00:00:00,43.153 +2025-03-07 06:00:00,43.151 +2025-03-07 12:00:00,43.175 +2025-03-07 18:00:00,43.184 +2025-03-08 00:00:00,43.223 +2025-03-08 06:00:00,43.274 +2025-03-08 12:00:00,43.268 +2025-03-08 18:00:00,43.305 +2025-03-09 00:00:00,43.300 +2025-03-09 06:00:00,43.298 +2025-03-09 12:00:00,43.310 +2025-03-09 18:00:00,43.295 +2025-03-10 00:00:00,43.258 +2025-03-10 06:00:00,43.245 +2025-03-10 12:00:00,43.207 +2025-03-10 18:00:00,43.203 +2025-03-11 00:00:00,43.229 +2025-03-11 06:00:00,43.221 +2025-03-11 12:00:00,43.236 +2025-03-11 18:00:00,43.267 +2025-03-12 00:00:00,43.266 +2025-03-12 06:00:00,43.314 +2025-03-12 12:00:00,43.334 +2025-03-12 18:00:00,43.364 +2025-03-13 00:00:00,43.344 +2025-03-13 06:00:00,43.375 +2025-03-13 12:00:00,43.331 +2025-03-13 18:00:00,43.319 +2025-03-14 00:00:00,43.316 +2025-03-14 06:00:00,43.300 +2025-03-14 12:00:00,43.266 +2025-03-14 18:00:00,43.252 +2025-03-15 00:00:00,43.282 +2025-03-15 06:00:00,43.264 +2025-03-15 12:00:00,43.294 +2025-03-15 18:00:00,43.318 +2025-03-16 00:00:00,44.587 +2025-03-16 06:00:00,44.620 +2025-03-16 12:00:00,44.640 +2025-03-16 18:00:00,44.673 +2025-03-17 00:00:00,44.652 +2025-03-17 06:00:00,44.672 +2025-03-17 12:00:00,44.643 +2025-03-17 18:00:00,44.633 +2025-03-18 00:00:00,44.625 +2025-03-18 06:00:00,44.591 +2025-03-18 12:00:00,44.575 +2025-03-18 18:00:00,44.583 +2025-03-19 00:00:00,44.555 +2025-03-19 06:00:00,44.578 +2025-03-19 12:00:00,44.617 +2025-03-19 18:00:00,44.639 +2025-03-20 00:00:00,44.629 +2025-03-20 06:00:00,44.661 +2025-03-20 12:00:00,44.686 +2025-03-20 18:00:00,44.729 +2025-03-21 00:00:00,44.735 +2025-03-21 06:00:00,44.734 +2025-03-21 12:00:00,44.704 +2025-03-21 18:00:00,44.680 +2025-03-22 00:00:00,44.687 +2025-03-22 06:00:00,44.663 +2025-03-22 12:00:00,44.643 +2025-03-22 18:00:00,44.649 +2025-03-23 00:00:00,44.634 +2025-03-23 06:00:00,44.616 +2025-03-23 12:00:00,44.665 +2025-03-23 18:00:00,44.668 +2025-03-24 00:00:00,44.709 +2025-03-24 06:00:00,44.746 +2025-03-24 12:00:00,44.737 +2025-03-24 18:00:00,44.753 +2025-03-25 00:00:00,44.760 +2025-03-25 06:00:00,44.777 +2025-03-25 12:00:00,44.756 +2025-03-25 18:00:00,44.754 +2025-03-26 00:00:00,44.739 +2025-03-26 06:00:00,44.699 +2025-03-26 12:00:00,44.700 +2025-03-26 18:00:00,44.676 +2025-03-27 00:00:00,44.684 +2025-03-27 06:00:00,44.708 +2025-03-27 12:00:00,44.722 +2025-03-27 18:00:00,44.715 +2025-03-28 00:00:00,44.755 +2025-03-28 06:00:00,44.776 +2025-03-28 12:00:00,44.788 +2025-03-28 18:00:00,44.835 +2025-03-29 00:00:00,44.837 +2025-03-29 06:00:00,44.821 +2025-03-29 12:00:00,44.831 +2025-03-29 18:00:00,44.808 +2025-03-30 00:00:00,44.783 +2025-03-30 06:00:00,44.747 +2025-03-30 12:00:00,44.734 +2025-03-30 18:00:00,44.756 +2025-03-31 00:00:00,44.756 +2025-03-31 06:00:00,44.750 +2025-03-31 12:00:00,44.778 +2025-03-31 18:00:00,44.791 +2025-04-01 00:00:00,44.800 +2025-04-01 06:00:00,44.826 +2025-04-01 12:00:00,44.856 +2025-04-01 18:00:00,44.896 +2025-04-02 00:00:00,44.900 +2025-04-02 06:00:00,44.901 +2025-04-02 12:00:00,44.893 +2025-04-02 18:00:00,44.850 +2025-04-03 00:00:00,44.832 +2025-04-03 06:00:00,44.807 +2025-04-03 12:00:00,44.818 +2025-04-03 18:00:00,44.812 +2025-04-04 00:00:00,44.792 +2025-04-04 06:00:00,44.809 +2025-04-04 12:00:00,44.807 +2025-04-04 18:00:00,44.861 +2025-04-05 00:00:00,44.885 +2025-04-05 06:00:00,44.916 +2025-04-05 12:00:00,44.932 +2025-04-05 18:00:00,44.951 +2025-04-06 00:00:00,44.925 +2025-04-06 06:00:00,44.952 +2025-04-06 12:00:00,44.927 +2025-04-06 18:00:00,44.935 +2025-04-07 00:00:00,44.910 +2025-04-07 06:00:00,44.893 +2025-04-07 12:00:00,44.875 +2025-04-07 18:00:00,44.844 +2025-04-08 00:00:00,44.863 +2025-04-08 06:00:00,44.844 +2025-04-08 12:00:00,44.891 +2025-04-08 18:00:00,44.914 +2025-04-09 00:00:00,44.915 +2025-04-09 06:00:00,44.965 +2025-04-09 12:00:00,44.974 +2025-04-09 18:00:00,44.984 +2025-04-10 00:00:00,45.012 +2025-04-10 06:00:00,44.988 +2025-04-10 12:00:00,44.970 +2025-04-10 18:00:00,44.961 +2025-04-11 00:00:00,44.947 +2025-04-11 06:00:00,44.949 +2025-04-11 12:00:00,44.937 +2025-04-11 18:00:00,44.900 +2025-04-12 00:00:00,44.914 +2025-04-12 06:00:00,44.912 +2025-04-12 12:00:00,44.952 +2025-04-12 18:00:00,44.957 +2025-04-13 00:00:00,45.000 +2025-04-13 06:00:00,44.993 +2025-04-13 12:00:00,45.050 +2025-04-13 18:00:00,45.035 +2025-04-14 00:00:00,45.075 +2025-04-14 06:00:00,45.046 +2025-04-14 12:00:00,45.030 +2025-04-14 18:00:00,45.027 diff --git a/public/example_wellntel.wcsv b/public/example_wellntel.wcsv new file mode 100644 index 00000000..095faef8 --- /dev/null +++ b/public/example_wellntel.wcsv @@ -0,0 +1,361 @@ +timestamp,temperature_C,temperature_raw,depth +2025-01-15 00:00:00,16.8,655,41.997 +2025-01-15 06:00:00,17.8,656,42.016 +2025-01-15 12:00:00,18.4,653,45.151 +2025-01-15 18:00:00,16.7,641,84.056 +2025-01-16 00:00:00,18.3,645,42.049 +2025-01-16 06:00:00,18.4,651,42.063 +2025-01-16 12:00:00,18.2,651,42.048 +2025-01-16 18:00:00,19.4,649,42.074 +2025-01-17 00:00:00,18.5,650,42.087 +2025-01-17 06:00:00,18.2,655,42.101 +2025-01-17 12:00:00,17.0,655,45.161 +2025-01-17 18:00:00,18.3,654,42.074 +2025-01-18 00:00:00,18.5,645,42.061 +2025-01-18 06:00:00,17.0,645,42.102 +2025-01-18 12:00:00,18.5,656,84.186 +2025-01-18 18:00:00,17.1,643,42.048 +2025-01-19 00:00:00,16.7,653,42.100 +2025-01-19 06:00:00,16.6,648,42.093 +2025-01-19 12:00:00,19.3,648,42.069 +2025-01-19 18:00:00,19.0,650,42.062 +2025-01-20 00:00:00,16.8,649,42.030 +2025-01-20 06:00:00,18.1,654,42.017 +2025-01-20 12:00:00,18.1,648,42.045 +2025-01-20 18:00:00,16.9,659,42.016 +2025-01-21 00:00:00,18.9,655,42.023 +2025-01-21 06:00:00,17.2,657,42.028 +2025-01-21 12:00:00,16.9,658,42.018 +2025-01-21 18:00:00,16.8,652,42.049 +2025-01-22 00:00:00,17.0,641,42.054 +2025-01-22 06:00:00,16.7,653,42.085 +2025-01-22 12:00:00,17.7,645,42.075 +2025-01-22 18:00:00,18.9,640,42.093 +2025-01-23 00:00:00,19.4,641,42.096 +2025-01-23 06:00:00,18.1,654,42.118 +2025-01-23 12:00:00,18.7,651,42.118 +2025-01-23 18:00:00,16.5,652,42.120 +2025-01-24 00:00:00,18.0,641,42.176 +2025-01-24 06:00:00,16.7,654,42.188 +2025-01-24 12:00:00,18.1,654,42.190 +2025-01-24 18:00:00,19.4,646,42.193 +2025-01-25 00:00:00,16.8,657,42.179 +2025-01-25 06:00:00,18.1,645,42.182 +2025-01-25 12:00:00,18.6,657,42.192 +2025-01-25 18:00:00,18.9,648,42.183 +2025-01-26 00:00:00,16.8,645,45.328 +2025-01-26 06:00:00,19.2,654,42.208 +2025-01-26 12:00:00,18.8,653,42.235 +2025-01-26 18:00:00,18.9,659,42.206 +2025-01-27 00:00:00,18.6,653,42.237 +2025-01-27 06:00:00,16.7,660,42.227 +2025-01-27 12:00:00,18.7,657,42.171 +2025-01-27 18:00:00,17.4,653,42.179 +2025-01-28 00:00:00,17.4,657,42.216 +2025-01-28 06:00:00,18.3,641,42.184 +2025-01-28 12:00:00,19.0,643,42.209 +2025-01-28 18:00:00,16.6,644,42.170 +2025-01-29 00:00:00,17.5,657,42.147 +2025-01-29 06:00:00,18.7,653,42.178 +2025-01-29 12:00:00,17.2,642,42.186 +2025-01-29 18:00:00,17.5,657,42.156 +2025-01-30 00:00:00,16.8,657,42.147 +2025-01-30 06:00:00,19.4,654,42.166 +2025-01-30 12:00:00,19.5,648,42.209 +2025-01-30 18:00:00,16.5,652,42.204 +2025-01-31 00:00:00,16.6,644,42.174 +2025-01-31 06:00:00,18.7,652,42.191 +2025-01-31 12:00:00,17.1,640,42.193 +2025-01-31 18:00:00,17.0,654,42.203 +2025-02-01 00:00:00,18.7,642,39.859 +2025-02-01 06:00:00,17.8,648,42.235 +2025-02-01 12:00:00,17.9,657,42.271 +2025-02-01 18:00:00,18.7,645,42.276 +2025-02-02 00:00:00,18.1,649,42.273 +2025-02-02 06:00:00,17.3,642,42.316 +2025-02-02 12:00:00,17.2,641,42.290 +2025-02-02 18:00:00,19.1,655,42.330 +2025-02-03 00:00:00,17.6,640,42.306 +2025-02-03 06:00:00,18.5,659,42.330 +2025-02-03 12:00:00,17.7,650,42.321 +2025-02-03 18:00:00,17.7,649,42.327 +2025-02-04 00:00:00,17.3,650,42.370 +2025-02-04 06:00:00,17.0,641,42.359 +2025-02-04 12:00:00,17.8,655,42.327 +2025-02-04 18:00:00,18.8,648,42.359 +2025-02-05 00:00:00,18.1,642,42.315 +2025-02-05 06:00:00,17.7,648,42.321 +2025-02-05 12:00:00,19.5,644,42.343 +2025-02-05 18:00:00,18.0,643,42.325 +2025-02-06 00:00:00,18.4,643,42.287 +2025-02-06 06:00:00,17.3,654,42.336 +2025-02-06 12:00:00,16.5,641,42.285 +2025-02-06 18:00:00,17.3,643,42.285 +2025-02-07 00:00:00,17.2,658,42.314 +2025-02-07 06:00:00,18.4,641,42.287 +2025-02-07 12:00:00,17.7,642,42.286 +2025-02-07 18:00:00,17.3,651,42.276 +2025-02-08 00:00:00,18.2,659,42.281 +2025-02-08 06:00:00,19.0,642,42.317 +2025-02-08 12:00:00,17.0,656,42.304 +2025-02-08 18:00:00,17.7,651,42.335 +2025-02-09 00:00:00,17.4,642,39.931 +2025-02-09 06:00:00,18.4,660,42.348 +2025-02-09 12:00:00,18.3,641,42.390 +2025-02-09 18:00:00,19.4,645,42.374 +2025-02-10 00:00:00,19.5,644,42.392 +2025-02-10 06:00:00,18.3,644,42.422 +2025-02-10 12:00:00,19.3,649,42.394 +2025-02-10 18:00:00,17.9,649,42.404 +2025-02-11 00:00:00,18.2,641,42.460 +2025-02-11 06:00:00,19.0,659,42.464 +2025-02-11 12:00:00,18.8,643,42.438 +2025-02-11 18:00:00,17.5,644,42.441 +2025-02-12 00:00:00,17.6,645,42.479 +2025-02-12 06:00:00,19.1,646,42.494 +2025-02-12 12:00:00,19.4,650,42.451 +2025-02-12 18:00:00,17.2,658,85.019 +2025-02-13 00:00:00,19.1,653,42.491 +2025-02-13 06:00:00,18.5,657,42.472 +2025-02-13 12:00:00,18.5,657,42.457 +2025-02-13 18:00:00,19.3,649,42.434 +2025-02-14 00:00:00,17.9,660,42.425 +2025-02-14 06:00:00,16.9,652,42.475 +2025-02-14 12:00:00,19.4,649,42.426 +2025-02-14 18:00:00,19.0,641,42.449 +2025-02-15 00:00:00,18.0,651,42.418 +2025-02-15 06:00:00,18.7,660,42.448 +2025-02-15 12:00:00,18.1,646,42.432 +2025-02-15 18:00:00,17.4,653,42.413 +2025-02-16 00:00:00,16.7,650,42.406 +2025-02-16 06:00:00,18.7,655,42.441 +2025-02-16 12:00:00,19.0,658,42.405 +2025-02-16 18:00:00,19.3,651,42.420 +2025-02-17 00:00:00,17.2,650,42.447 +2025-02-17 06:00:00,16.6,649,42.473 +2025-02-17 12:00:00,17.8,645,42.468 +2025-02-17 18:00:00,17.7,651,42.459 +2025-02-18 00:00:00,18.2,640,42.509 +2025-02-18 06:00:00,18.1,659,42.478 +2025-02-18 12:00:00,18.4,659,40.080 +2025-02-18 18:00:00,17.0,654,42.507 +2025-02-19 00:00:00,19.4,653,42.531 +2025-02-19 06:00:00,19.4,654,42.519 +2025-02-19 12:00:00,19.0,652,42.538 +2025-02-19 18:00:00,18.4,648,42.559 +2025-02-20 00:00:00,16.7,641,42.580 +2025-02-20 06:00:00,17.8,651,42.562 +2025-02-20 12:00:00,19.4,648,42.581 +2025-02-20 18:00:00,18.7,644,42.617 +2025-02-21 00:00:00,18.3,642,42.625 +2025-02-21 06:00:00,18.8,656,42.607 +2025-02-21 12:00:00,17.9,643,45.709 +2025-02-21 18:00:00,19.3,653,42.600 +2025-02-22 00:00:00,17.9,644,42.571 +2025-02-22 06:00:00,19.4,649,42.560 +2025-02-22 12:00:00,16.9,640,42.554 +2025-02-22 18:00:00,18.8,659,42.603 +2025-02-23 00:00:00,17.1,660,42.585 +2025-02-23 06:00:00,17.1,651,42.595 +2025-02-23 12:00:00,18.8,649,42.534 +2025-02-23 18:00:00,17.3,649,45.667 +2025-02-24 00:00:00,17.8,657,42.554 +2025-02-24 06:00:00,19.3,657,42.567 +2025-02-24 12:00:00,17.4,640,42.542 +2025-02-24 18:00:00,17.2,658,42.584 +2025-02-25 00:00:00,18.9,657,42.531 +2025-02-25 06:00:00,17.0,644,42.564 +2025-02-25 12:00:00,18.5,643,42.581 +2025-02-25 18:00:00,18.6,649,42.598 +2025-02-26 00:00:00,19.4,643,42.598 +2025-02-26 06:00:00,19.4,658,42.606 +2025-02-26 12:00:00,16.7,642,42.622 +2025-02-26 18:00:00,19.4,641,42.642 +2025-02-27 00:00:00,18.7,660,42.620 +2025-02-27 06:00:00,16.6,648,42.653 +2025-02-27 12:00:00,17.3,658,42.679 +2025-02-27 18:00:00,17.8,660,42.689 +2025-02-28 00:00:00,18.4,646,42.674 +2025-02-28 06:00:00,19.3,642,42.707 +2025-02-28 12:00:00,18.5,644,42.721 +2025-02-28 18:00:00,17.0,646,42.712 +2025-03-01 00:00:00,18.0,644,42.722 +2025-03-01 06:00:00,17.3,640,42.712 +2025-03-01 12:00:00,18.3,652,42.728 +2025-03-01 18:00:00,18.8,658,42.732 +2025-03-02 00:00:00,18.8,653,42.700 +2025-03-02 06:00:00,18.9,660,40.333 +2025-03-02 12:00:00,16.6,644,42.751 +2025-03-02 18:00:00,18.9,651,42.741 +2025-03-03 00:00:00,18.4,656,42.728 +2025-03-03 06:00:00,18.2,653,42.702 +2025-03-03 12:00:00,16.5,655,42.718 +2025-03-03 18:00:00,16.7,650,42.703 +2025-03-04 00:00:00,17.4,659,42.690 +2025-03-04 06:00:00,16.6,645,42.709 +2025-03-04 12:00:00,17.1,643,42.660 +2025-03-04 18:00:00,16.9,644,42.697 +2025-03-05 00:00:00,19.0,643,45.753 +2025-03-05 06:00:00,18.3,653,42.687 +2025-03-05 12:00:00,18.8,646,42.682 +2025-03-05 18:00:00,18.9,656,42.670 +2025-03-06 00:00:00,18.5,660,42.703 +2025-03-06 06:00:00,18.3,646,42.698 +2025-03-06 12:00:00,19.0,648,42.713 +2025-03-06 18:00:00,18.5,643,42.741 +2025-03-07 00:00:00,19.0,644,42.712 +2025-03-07 06:00:00,16.8,653,42.708 +2025-03-07 12:00:00,17.6,646,42.738 +2025-03-07 18:00:00,18.5,652,42.773 +2025-03-08 00:00:00,19.5,643,42.758 +2025-03-08 06:00:00,18.6,644,42.768 +2025-03-08 12:00:00,17.6,653,42.825 +2025-03-08 18:00:00,17.9,645,42.821 +2025-03-09 00:00:00,18.2,647,42.818 +2025-03-09 06:00:00,17.5,658,42.854 +2025-03-09 12:00:00,18.3,643,42.828 +2025-03-09 18:00:00,18.8,644,42.855 +2025-03-10 00:00:00,16.7,651,42.831 +2025-03-10 06:00:00,18.4,641,42.848 +2025-03-10 12:00:00,17.8,641,45.972 +2025-03-10 18:00:00,17.0,648,42.878 +2025-03-11 00:00:00,17.5,646,42.874 +2025-03-11 06:00:00,17.9,650,42.841 +2025-03-11 12:00:00,17.4,653,42.849 +2025-03-11 18:00:00,19.2,643,42.829 +2025-03-12 00:00:00,17.7,652,42.813 +2025-03-12 06:00:00,17.6,640,42.829 +2025-03-12 12:00:00,17.0,660,42.844 +2025-03-12 18:00:00,19.3,640,42.840 +2025-03-13 00:00:00,16.6,650,42.827 +2025-03-13 06:00:00,16.8,660,85.696 +2025-03-13 12:00:00,18.4,650,42.796 +2025-03-13 18:00:00,19.3,660,42.789 +2025-03-14 00:00:00,19.3,644,42.807 +2025-03-14 06:00:00,17.7,644,42.799 +2025-03-14 12:00:00,18.1,659,85.601 +2025-03-14 18:00:00,18.4,659,42.817 +2025-03-15 00:00:00,18.2,660,42.837 +2025-03-15 06:00:00,19.3,652,42.838 +2025-03-15 12:00:00,18.5,645,42.837 +2025-03-15 18:00:00,18.3,652,42.879 +2025-03-16 00:00:00,18.7,644,42.900 +2025-03-16 06:00:00,18.9,650,42.881 +2025-03-16 12:00:00,18.9,657,42.871 +2025-03-16 18:00:00,16.7,655,42.881 +2025-03-17 00:00:00,17.9,641,42.944 +2025-03-17 06:00:00,18.8,642,42.906 +2025-03-17 12:00:00,17.3,643,42.957 +2025-03-17 18:00:00,16.6,659,42.958 +2025-03-18 00:00:00,17.2,651,42.951 +2025-03-18 06:00:00,17.8,647,42.987 +2025-03-18 12:00:00,18.3,660,42.946 +2025-03-18 18:00:00,18.1,648,42.957 +2025-03-19 00:00:00,17.3,646,42.978 +2025-03-19 06:00:00,19.4,645,42.953 +2025-03-19 12:00:00,19.4,644,43.000 +2025-03-19 18:00:00,18.0,650,42.962 +2025-03-20 00:00:00,18.7,653,42.953 +2025-03-20 06:00:00,17.5,657,42.943 +2025-03-20 12:00:00,17.2,653,42.973 +2025-03-20 18:00:00,19.0,641,42.957 +2025-03-21 00:00:00,18.7,650,42.962 +2025-03-21 06:00:00,18.3,643,42.959 +2025-03-21 12:00:00,16.7,644,42.963 +2025-03-21 18:00:00,16.8,643,42.969 +2025-03-22 00:00:00,19.1,649,42.949 +2025-03-22 06:00:00,19.1,651,42.919 +2025-03-22 12:00:00,17.0,651,42.935 +2025-03-22 18:00:00,17.5,658,42.919 +2025-03-23 00:00:00,19.1,654,42.912 +2025-03-23 06:00:00,19.3,659,42.945 +2025-03-23 12:00:00,17.1,658,42.966 +2025-03-23 18:00:00,18.8,659,42.942 +2025-03-24 00:00:00,18.7,649,42.956 +2025-03-24 06:00:00,19.5,641,46.071 +2025-03-24 12:00:00,18.8,645,43.004 +2025-03-24 18:00:00,18.7,641,43.006 +2025-03-25 00:00:00,19.0,657,43.024 +2025-03-25 06:00:00,17.7,660,43.045 +2025-03-25 12:00:00,16.6,655,43.036 +2025-03-25 18:00:00,16.5,651,43.072 +2025-03-26 00:00:00,19.2,660,43.090 +2025-03-26 06:00:00,17.8,647,43.052 +2025-03-26 12:00:00,18.1,644,43.112 +2025-03-26 18:00:00,18.5,646,43.107 +2025-03-27 00:00:00,18.6,648,43.078 +2025-03-27 06:00:00,19.1,658,43.102 +2025-03-27 12:00:00,17.1,649,43.115 +2025-03-27 18:00:00,19.5,658,43.134 +2025-03-28 00:00:00,19.2,640,43.101 +2025-03-28 06:00:00,18.9,648,43.111 +2025-03-28 12:00:00,18.0,655,43.127 +2025-03-28 18:00:00,17.0,659,43.118 +2025-03-29 00:00:00,19.5,653,43.123 +2025-03-29 06:00:00,18.9,643,43.089 +2025-03-29 12:00:00,18.6,652,43.095 +2025-03-29 18:00:00,19.4,653,43.062 +2025-03-30 00:00:00,16.9,648,43.089 +2025-03-30 06:00:00,17.0,647,43.090 +2025-03-30 12:00:00,17.2,640,43.076 +2025-03-30 18:00:00,18.4,649,43.078 +2025-03-31 00:00:00,18.0,653,43.047 +2025-03-31 06:00:00,16.8,645,43.089 +2025-03-31 12:00:00,17.7,646,43.096 +2025-03-31 18:00:00,19.2,641,43.100 +2025-04-01 00:00:00,18.9,646,43.079 +2025-04-01 06:00:00,16.7,640,46.202 +2025-04-01 12:00:00,16.5,651,43.082 +2025-04-01 18:00:00,16.6,658,43.127 +2025-04-02 00:00:00,16.6,653,43.135 +2025-04-02 06:00:00,18.6,658,43.116 +2025-04-02 12:00:00,18.0,660,43.113 +2025-04-02 18:00:00,18.8,647,43.147 +2025-04-03 00:00:00,17.3,644,43.166 +2025-04-03 06:00:00,16.5,641,43.192 +2025-04-03 12:00:00,17.1,642,43.187 +2025-04-03 18:00:00,19.5,644,43.170 +2025-04-04 00:00:00,19.0,654,43.227 +2025-04-04 06:00:00,18.1,655,43.191 +2025-04-04 12:00:00,18.0,641,43.242 +2025-04-04 18:00:00,18.2,645,86.458 +2025-04-05 00:00:00,16.5,656,43.224 +2025-04-05 06:00:00,19.0,655,43.253 +2025-04-05 12:00:00,19.0,640,43.215 +2025-04-05 18:00:00,18.2,647,43.215 +2025-04-06 00:00:00,17.4,658,43.235 +2025-04-06 06:00:00,18.5,650,43.244 +2025-04-06 12:00:00,18.8,656,43.247 +2025-04-06 18:00:00,18.1,656,43.233 +2025-04-07 00:00:00,19.0,645,43.243 +2025-04-07 06:00:00,18.0,642,43.210 +2025-04-07 12:00:00,17.1,649,43.233 +2025-04-07 18:00:00,19.0,650,43.176 +2025-04-08 00:00:00,18.4,659,43.181 +2025-04-08 06:00:00,18.7,649,43.196 +2025-04-08 12:00:00,18.9,647,43.180 +2025-04-08 18:00:00,17.8,660,43.206 +2025-04-09 00:00:00,19.2,641,43.202 +2025-04-09 06:00:00,16.7,650,43.198 +2025-04-09 12:00:00,17.7,658,43.222 +2025-04-09 18:00:00,17.6,653,43.211 +2025-04-10 00:00:00,19.2,647,43.204 +2025-04-10 06:00:00,18.2,656,43.218 +2025-04-10 12:00:00,17.0,646,43.256 +2025-04-10 18:00:00,17.8,643,43.271 +2025-04-11 00:00:00,18.1,654,43.241 +2025-04-11 06:00:00,18.2,641,43.292 +2025-04-11 12:00:00,17.9,642,43.312 +2025-04-11 18:00:00,17.7,643,43.298 +2025-04-12 00:00:00,18.1,647,43.313 +2025-04-12 06:00:00,16.6,645,43.303 +2025-04-12 12:00:00,18.1,652,43.333 +2025-04-12 18:00:00,16.7,656,43.341 +2025-04-13 00:00:00,17.3,645,43.355 +2025-04-13 06:00:00,17.1,644,43.327 +2025-04-13 12:00:00,19.1,657,43.388 +2025-04-13 18:00:00,17.0,651,43.364 +2025-04-14 00:00:00,18.7,655,43.359 +2025-04-14 06:00:00,17.0,652,43.383 +2025-04-14 12:00:00,16.6,646,43.367 +2025-04-14 18:00:00,19.2,653,43.360 diff --git a/public/sa-0231_DK744_compensated.CSV b/public/sa-0231_DK744_compensated.CSV new file mode 100644 index 00000000..49311abc --- /dev/null +++ b/public/sa-0231_DK744_compensated.CSV @@ -0,0 +1,767 @@ +Data file for DataLogger. +============================================================================== +COMPANY : +COMP.STATUS: Done (Barometer: SA-0231 Baro | Serial number: DL572 | Sample Interval: 12 Hour) +DATE : 29/07/2026 +TIME : 09:48:59 +FILENAME : \\agustin\homes\emamer\Desktop\CSV\sa-0231_DK744_compensated.CSV +CREATED BY : Diver-Office 13.0.0.1 +========================== BEGINNING OF DATA ========================== +[Logger settings] + Instrument type =TD-Diver=19 + Status =Started =0 + Serial number =..06-DK744 219. + Instrument number =UTC-7 + =0 + Location =sa-0231 + Sample period =H12 + Sample method =T + Number of channels =2 +[Channel 1] + Identification =WATER HEAD (WC) + Reference level =13.12336 ft + Range =57.41470 ft + Master level =400 CMH2O + Altitude =0 ft +[Channel 2] + Identification =TEMPERATURE + Reference level =-20.000 C + Range =100.000 C + + +[Series settings] + Serial number =..06-DK744 219. + Instrument number =UTC-7 + Location =sa-0231 + Sample period =00 12:00:00 0 + Sample method =T + Start date / time =00:00:12 20/02/24 + End date / time =00:00:00 11/02/25 +[Channel 1 from data header] + Identification =WATER HEAD (WC) + Reference level =13.12336 ft + Range =57.41470 ft + Master level =400 CMH2O + Altitude =0 ft +[Channel 2 from data header] + Identification =TEMPERATURE + Reference level =-20.000 C + Range =100.000 C + + +Date/time,Water head[ft],Temperature[C] +2024/02/20 12:00:00,23.09766,13.800 +2024/02/21 00:00:00,23.06595,13.787 +2024/02/21 12:00:00,23.16519,13.787 +2024/02/22 00:00:00,23.09492,13.787 +2024/02/22 12:00:00,23.11898,13.777 +2024/02/23 00:00:00,23.07305,13.793 +2024/02/23 12:00:00,23.09247,13.793 +2024/02/24 00:00:00,23.05583,13.793 +2024/02/24 12:00:00,23.07141,13.800 +2024/02/25 00:00:00,23.06540,13.800 +2024/02/25 12:00:00,23.06075,13.787 +2024/02/26 00:00:00,23.06075,13.787 +2024/02/26 12:00:00,23.08892,13.800 +2024/02/27 00:00:00,23.15781,13.787 +2024/02/27 12:00:00,23.16081,13.793 +2024/02/28 00:00:00,23.14960,13.800 +2024/02/28 12:00:00,23.06512,13.787 +2024/02/29 00:00:00,23.09411,13.787 +2024/02/29 12:00:00,23.08345,13.800 +2024/03/01 00:00:00,23.07879,13.787 +2024/03/01 12:00:00,23.05282,13.787 +2024/03/02 00:00:00,23.08125,13.787 +2024/03/02 12:00:00,23.09875,13.800 +2024/03/03 00:00:00,23.13074,13.777 +2024/03/03 12:00:00,23.14085,13.787 +2024/03/04 00:00:00,23.11871,13.800 +2024/03/04 12:00:00,23.10285,13.800 +2024/03/05 00:00:00,23.05364,13.787 +2024/03/05 12:00:00,23.08371,13.793 +2024/03/06 00:00:00,23.01345,13.793 +2024/03/06 12:00:00,23.08727,13.787 +2024/03/07 00:00:00,23.06704,13.793 +2024/03/07 12:00:00,23.12199,13.793 +2024/03/08 00:00:00,23.04407,13.800 +2024/03/08 12:00:00,23.04735,13.800 +2024/03/09 00:00:00,22.93826,13.793 +2024/03/09 12:00:00,22.91666,13.793 +2024/03/10 00:00:00,22.89233,13.787 +2024/03/10 12:00:00,22.89561,13.777 +2024/03/11 00:00:00,22.89699,13.777 +2024/03/11 12:00:00,22.93744,13.800 +2024/03/12 00:00:00,22.95686,13.793 +2024/03/12 12:00:00,22.94620,13.793 +2024/03/13 00:00:00,23.01263,13.793 +2024/03/13 12:00:00,23.02192,13.793 +2024/03/14 00:00:00,23.05747,13.787 +2024/03/14 12:00:00,23.02001,13.787 +2024/03/15 00:00:00,23.02849,13.800 +2024/03/15 12:00:00,22.95822,13.777 +2024/03/16 00:00:00,22.97955,13.770 +2024/03/16 12:00:00,22.96369,13.793 +2024/03/17 00:00:00,22.95331,13.777 +2024/03/17 12:00:00,22.91421,13.793 +2024/03/18 00:00:00,22.88276,13.800 +2024/03/18 12:00:00,22.83574,13.793 +2024/03/19 00:00:00,22.81989,13.800 +2024/03/19 12:00:00,22.86117,13.777 +2024/03/20 00:00:00,22.83328,13.787 +2024/03/20 12:00:00,22.88167,13.800 +2024/03/21 00:00:00,22.83464,13.787 +2024/03/21 12:00:00,22.84941,13.787 +2024/03/22 00:00:00,22.77258,13.793 +2024/03/22 12:00:00,22.76821,13.800 +2024/03/23 00:00:00,22.77013,13.800 +2024/03/23 12:00:00,22.83164,13.793 +2024/03/24 00:00:00,22.92023,13.800 +2024/03/24 12:00:00,23.00197,13.800 +2024/03/25 00:00:00,22.99787,13.787 +2024/03/25 12:00:00,22.95084,13.777 +2024/03/26 00:00:00,22.91393,13.800 +2024/03/26 12:00:00,22.84558,13.800 +2024/03/27 00:00:00,22.81004,13.787 +2024/03/27 12:00:00,22.72364,13.793 +2024/03/28 00:00:00,22.72911,13.787 +2024/03/28 12:00:00,22.69631,13.800 +2024/03/29 00:00:00,22.80156,13.793 +2024/03/29 12:00:00,22.74688,13.800 +2024/03/30 00:00:00,22.80840,13.800 +2024/03/30 12:00:00,22.76575,13.793 +2024/03/31 00:00:00,22.82589,13.787 +2024/03/31 12:00:00,22.83738,13.777 +2024/04/01 00:00:00,22.83164,13.800 +2024/04/01 12:00:00,22.83546,13.800 +2024/04/02 00:00:00,22.76438,13.787 +2024/04/02 12:00:00,22.68673,13.800 +2024/04/03 00:00:00,22.63205,13.793 +2024/04/03 12:00:00,22.64682,13.787 +2024/04/04 00:00:00,22.63287,13.777 +2024/04/04 12:00:00,22.65611,13.787 +2024/04/05 00:00:00,22.64901,13.793 +2024/04/05 12:00:00,22.68947,13.800 +2024/04/06 00:00:00,22.69712,13.800 +2024/04/06 12:00:00,22.65912,13.793 +2024/04/07 00:00:00,22.62522,13.800 +2024/04/07 12:00:00,22.58831,13.787 +2024/04/08 00:00:00,22.61319,13.777 +2024/04/08 12:00:00,22.63725,13.800 +2024/04/09 00:00:00,22.66431,13.800 +2024/04/09 12:00:00,22.56535,13.787 +2024/04/10 00:00:00,22.58257,13.787 +2024/04/10 12:00:00,22.49371,13.810 +2024/04/11 00:00:00,22.55195,13.793 +2024/04/11 12:00:00,22.49917,13.793 +2024/04/12 00:00:00,22.55796,13.793 +2024/04/12 12:00:00,22.48907,13.793 +2024/04/13 00:00:00,22.55058,13.787 +2024/04/13 12:00:00,22.49316,13.793 +2024/04/14 00:00:00,22.53281,13.810 +2024/04/14 12:00:00,22.49836,13.793 +2024/04/15 00:00:00,22.56999,13.793 +2024/04/15 12:00:00,22.60963,13.793 +2024/04/16 00:00:00,22.55386,13.793 +2024/04/16 12:00:00,22.51039,13.800 +2024/04/17 00:00:00,22.46801,13.800 +2024/04/17 12:00:00,22.46993,13.787 +2024/04/18 00:00:00,22.44614,13.787 +2024/04/18 12:00:00,22.42974,13.800 +2024/04/19 00:00:00,22.42837,13.777 +2024/04/19 12:00:00,22.43439,13.793 +2024/04/20 00:00:00,22.45133,13.800 +2024/04/20 12:00:00,22.40595,13.800 +2024/04/21 00:00:00,22.38353,13.793 +2024/04/21 12:00:00,22.31655,13.787 +2024/04/22 00:00:00,22.35947,13.793 +2024/04/22 12:00:00,22.35236,13.777 +2024/04/23 00:00:00,22.38162,13.787 +2024/04/23 12:00:00,22.35510,13.787 +2024/04/24 00:00:00,22.37259,13.787 +2024/04/24 12:00:00,22.29796,13.800 +2024/04/25 00:00:00,22.39064,13.800 +2024/04/25 12:00:00,22.37286,13.793 +2024/04/26 00:00:00,22.43684,13.777 +2024/04/26 12:00:00,22.40240,13.810 +2024/04/27 00:00:00,22.50328,13.800 +2024/04/27 12:00:00,22.44560,13.800 +2024/04/28 00:00:00,22.43356,13.777 +2024/04/28 12:00:00,22.36794,13.793 +2024/04/29 00:00:00,22.36849,13.793 +2024/04/29 12:00:00,22.33569,13.800 +2024/04/30 00:00:00,22.34881,13.770 +2024/04/30 12:00:00,22.34471,13.787 +2024/05/01 00:00:00,22.36630,13.793 +2024/05/01 12:00:00,22.37150,13.793 +2024/05/02 00:00:00,22.35045,13.787 +2024/05/02 12:00:00,22.33377,13.800 +2024/05/03 00:00:00,22.30861,13.793 +2024/05/03 12:00:00,22.26487,13.800 +2024/05/04 00:00:00,22.26269,13.787 +2024/05/04 12:00:00,22.22304,13.793 +2024/05/05 00:00:00,22.26242,13.787 +2024/05/05 12:00:00,22.21538,13.800 +2024/05/06 00:00:00,22.27143,13.800 +2024/05/06 12:00:00,22.21484,13.793 +2024/05/07 00:00:00,22.24355,13.793 +2024/05/07 12:00:00,22.19434,13.793 +2024/05/08 00:00:00,22.28428,13.800 +2024/05/08 12:00:00,22.18176,13.787 +2024/05/09 00:00:00,22.25476,13.793 +2024/05/09 12:00:00,22.14786,13.793 +2024/05/10 00:00:00,22.17711,13.800 +2024/05/10 12:00:00,22.06966,13.793 +2024/05/11 00:00:00,22.18805,13.793 +2024/05/11 12:00:00,22.14841,13.800 +2024/05/12 00:00:00,22.20691,13.800 +2024/05/12 12:00:00,22.17273,13.800 +2024/05/13 00:00:00,22.17438,13.793 +2024/05/13 12:00:00,22.11833,13.787 +2024/05/14 00:00:00,22.14047,13.793 +2024/05/14 12:00:00,22.13637,13.800 +2024/05/15 00:00:00,22.15551,13.800 +2024/05/15 12:00:00,22.14020,13.800 +2024/05/16 00:00:00,22.13035,13.793 +2024/05/16 12:00:00,22.11587,13.800 +2024/05/17 00:00:00,22.09645,13.800 +2024/05/17 12:00:00,22.08142,13.800 +2024/05/18 00:00:00,22.07103,13.793 +2024/05/18 12:00:00,21.99886,13.810 +2024/05/19 00:00:00,22.01060,13.800 +2024/05/19 12:00:00,21.98546,13.793 +2024/05/20 00:00:00,22.01799,13.787 +2024/05/20 12:00:00,21.97151,13.793 +2024/05/21 00:00:00,22.01307,13.800 +2024/05/21 12:00:00,21.96987,13.800 +2024/05/22 00:00:00,21.98573,13.787 +2024/05/22 12:00:00,21.93022,13.777 +2024/05/23 00:00:00,22.00623,13.777 +2024/05/23 12:00:00,21.94117,13.777 +2024/05/24 00:00:00,21.99967,13.787 +2024/05/24 12:00:00,21.90124,13.800 +2024/05/25 00:00:00,21.99830,13.800 +2024/05/25 12:00:00,21.95702,13.793 +2024/05/26 00:00:00,21.98491,13.793 +2024/05/26 12:00:00,21.87910,13.793 +2024/05/27 00:00:00,21.91902,13.787 +2024/05/27 12:00:00,21.84766,13.800 +2024/05/28 00:00:00,21.89879,13.787 +2024/05/28 12:00:00,21.85012,13.800 +2024/05/29 00:00:00,21.89250,13.793 +2024/05/29 12:00:00,21.86379,13.777 +2024/05/30 00:00:00,21.90316,13.793 +2024/05/30 12:00:00,21.89414,13.800 +2024/05/31 00:00:00,21.87828,13.800 +2024/05/31 12:00:00,21.87007,13.793 +2024/06/01 00:00:00,21.87445,13.810 +2024/06/01 12:00:00,21.82688,13.800 +2024/06/02 00:00:00,21.85423,13.793 +2024/06/02 12:00:00,21.81294,13.800 +2024/06/03 00:00:00,21.83563,13.800 +2024/06/03 12:00:00,21.77575,13.793 +2024/06/04 00:00:00,21.83563,13.817 +2024/06/04 12:00:00,21.74431,13.800 +2024/06/05 00:00:00,21.79244,13.787 +2024/06/05 12:00:00,21.67951,13.793 +2024/06/06 00:00:00,21.74760,13.793 +2024/06/06 12:00:00,21.63796,13.793 +2024/06/07 00:00:00,21.74842,13.800 +2024/06/07 12:00:00,21.65463,13.787 +2024/06/08 00:00:00,21.73721,13.787 +2024/06/08 12:00:00,21.66776,13.800 +2024/06/09 00:00:00,21.74076,13.800 +2024/06/09 12:00:00,21.65956,13.800 +2024/06/10 00:00:00,21.69975,13.777 +2024/06/10 12:00:00,21.64151,13.793 +2024/06/11 00:00:00,21.68116,13.793 +2024/06/11 12:00:00,21.63167,13.777 +2024/06/12 00:00:00,21.68116,13.793 +2024/06/12 12:00:00,21.61855,13.800 +2024/06/13 00:00:00,21.64507,13.800 +2024/06/13 12:00:00,21.63659,13.800 +2024/06/14 00:00:00,21.62401,13.787 +2024/06/14 12:00:00,21.59148,13.793 +2024/06/15 00:00:00,21.61663,13.800 +2024/06/15 12:00:00,21.63358,13.787 +2024/06/16 00:00:00,21.66858,13.800 +2024/06/16 12:00:00,21.63112,13.800 +2024/06/17 00:00:00,21.67569,13.787 +2024/06/17 12:00:00,21.64644,13.793 +2024/06/18 00:00:00,21.69209,13.787 +2024/06/18 12:00:00,21.60297,13.800 +2024/06/19 00:00:00,21.61226,13.793 +2024/06/19 12:00:00,21.47911,13.787 +2024/06/20 00:00:00,21.53926,13.793 +2024/06/20 12:00:00,21.40502,13.793 +2024/06/21 00:00:00,21.47529,13.793 +2024/06/21 12:00:00,21.42498,13.793 +2024/06/22 00:00:00,21.47064,13.793 +2024/06/22 12:00:00,21.38014,13.800 +2024/06/23 00:00:00,21.46353,13.800 +2024/06/23 12:00:00,21.37412,13.800 +2024/06/24 00:00:00,21.45533,13.787 +2024/06/24 12:00:00,21.41732,13.793 +2024/06/25 00:00:00,21.46872,13.800 +2024/06/25 12:00:00,21.42853,13.777 +2024/06/26 00:00:00,21.44275,13.793 +2024/06/26 12:00:00,21.43536,13.800 +2024/06/27 00:00:00,21.45888,13.793 +2024/06/27 12:00:00,21.44439,13.800 +2024/06/28 00:00:00,21.46544,13.793 +2024/06/28 12:00:00,21.43865,13.800 +2024/06/29 00:00:00,21.42580,13.793 +2024/06/29 12:00:00,21.37303,13.787 +2024/06/30 00:00:00,21.34405,13.800 +2024/06/30 12:00:00,21.29429,13.793 +2024/07/01 00:00:00,21.32300,13.787 +2024/07/01 12:00:00,21.28801,13.793 +2024/07/02 00:00:00,21.36401,13.787 +2024/07/02 12:00:00,21.29019,13.800 +2024/07/03 00:00:00,21.35034,13.800 +2024/07/03 12:00:00,21.25273,13.787 +2024/07/04 00:00:00,21.32492,13.800 +2024/07/04 12:00:00,21.20379,13.793 +2024/07/05 00:00:00,21.29183,13.793 +2024/07/05 12:00:00,21.18903,13.793 +2024/07/06 00:00:00,21.27324,13.787 +2024/07/06 12:00:00,21.19914,13.800 +2024/07/07 00:00:00,21.28745,13.793 +2024/07/07 12:00:00,21.21965,13.793 +2024/07/08 00:00:00,21.29866,13.793 +2024/07/08 12:00:00,21.23824,13.777 +2024/07/09 00:00:00,21.26121,13.793 +2024/07/09 12:00:00,21.20953,13.800 +2024/07/10 00:00:00,21.22676,13.787 +2024/07/10 12:00:00,21.17426,13.793 +2024/07/11 00:00:00,21.19477,13.810 +2024/07/11 12:00:00,21.16743,13.800 +2024/07/12 00:00:00,21.17782,13.800 +2024/07/12 12:00:00,21.14857,13.800 +2024/07/13 00:00:00,21.15677,13.793 +2024/07/13 12:00:00,21.10291,13.800 +2024/07/14 00:00:00,21.12560,13.793 +2024/07/14 12:00:00,21.10236,13.787 +2024/07/15 00:00:00,21.11384,13.787 +2024/07/15 12:00:00,21.08377,13.793 +2024/07/16 00:00:00,21.13736,13.800 +2024/07/16 12:00:00,21.07612,13.787 +2024/07/17 00:00:00,21.10291,13.787 +2024/07/17 12:00:00,21.03347,13.787 +2024/07/18 00:00:00,21.04577,13.793 +2024/07/18 12:00:00,20.96840,13.800 +2024/07/19 00:00:00,21.01049,13.800 +2024/07/19 12:00:00,20.92820,13.800 +2024/07/20 00:00:00,21.04057,13.787 +2024/07/20 12:00:00,20.95363,13.793 +2024/07/21 00:00:00,21.01323,13.787 +2024/07/21 12:00:00,20.93968,13.800 +2024/07/22 00:00:00,21.02144,13.800 +2024/07/22 12:00:00,20.96593,13.800 +2024/07/23 00:00:00,21.02089,13.793 +2024/07/23 12:00:00,20.97824,13.793 +2024/07/24 00:00:00,20.97250,13.800 +2024/07/24 12:00:00,20.93723,13.800 +2024/07/25 00:00:00,20.94188,13.800 +2024/07/25 12:00:00,20.93723,13.800 +2024/07/26 00:00:00,20.96320,13.793 +2024/07/26 12:00:00,20.98944,13.777 +2024/07/27 00:00:00,21.01487,13.787 +2024/07/27 12:00:00,21.00722,13.793 +2024/07/28 00:00:00,21.02280,13.800 +2024/07/28 12:00:00,20.96730,13.800 +2024/07/29 00:00:00,20.98672,13.810 +2024/07/29 12:00:00,20.90989,13.793 +2024/07/30 00:00:00,20.91919,13.793 +2024/07/30 12:00:00,20.87845,13.793 +2024/07/31 00:00:00,20.92055,13.800 +2024/07/31 12:00:00,20.82923,13.793 +2024/08/01 00:00:00,20.87708,13.800 +2024/08/01 12:00:00,20.76580,13.793 +2024/08/02 00:00:00,20.79752,13.793 +2024/08/02 12:00:00,20.70511,13.787 +2024/08/03 00:00:00,20.79040,13.793 +2024/08/03 12:00:00,20.72534,13.810 +2024/08/04 00:00:00,20.78986,13.793 +2024/08/04 12:00:00,20.74530,13.800 +2024/08/05 00:00:00,20.80517,13.793 +2024/08/05 12:00:00,20.73682,13.793 +2024/08/06 00:00:00,20.76690,13.793 +2024/08/06 12:00:00,20.71085,13.800 +2024/08/07 00:00:00,20.73573,13.800 +2024/08/07 12:00:00,20.73573,13.800 +2024/08/08 00:00:00,20.76306,13.787 +2024/08/08 12:00:00,20.75460,13.800 +2024/08/09 00:00:00,20.74639,13.800 +2024/08/09 12:00:00,20.73108,13.800 +2024/08/10 00:00:00,20.71631,13.800 +2024/08/10 12:00:00,20.69909,13.800 +2024/08/11 00:00:00,20.71085,13.800 +2024/08/11 12:00:00,20.70948,13.800 +2024/08/12 00:00:00,20.70620,13.800 +2024/08/12 12:00:00,20.68761,13.800 +2024/08/13 00:00:00,20.69717,13.800 +2024/08/13 12:00:00,20.67449,13.800 +2024/08/14 00:00:00,20.69746,13.800 +2024/08/14 12:00:00,20.62281,13.770 +2024/08/15 00:00:00,21.00530,13.800 +2024/08/15 12:00:00,21.56742,13.793 +2024/08/16 00:00:00,22.29604,13.800 +2024/08/16 12:00:00,22.89315,13.787 +2024/08/17 00:00:00,23.61603,13.793 +2024/08/17 12:00:00,23.89710,13.793 +2024/08/18 00:00:00,24.05403,13.800 +2024/08/18 12:00:00,24.00016,13.800 +2024/08/19 00:00:00,24.33727,13.810 +2024/08/19 12:00:00,24.49311,13.800 +2024/08/20 00:00:00,24.84772,13.793 +2024/08/20 12:00:00,25.10116,13.793 +2024/08/21 00:00:00,25.34914,13.777 +2024/08/21 12:00:00,25.51892,13.770 +2024/08/22 00:00:00,25.72233,13.777 +2024/08/22 12:00:00,25.91699,13.793 +2024/08/23 00:00:00,26.08021,13.800 +2024/08/23 12:00:00,26.26258,13.800 +2024/08/24 00:00:00,26.41022,13.800 +2024/08/24 12:00:00,26.56769,13.800 +2024/08/25 00:00:00,26.72955,13.800 +2024/08/25 12:00:00,26.82688,13.800 +2024/08/26 00:00:00,27.01115,13.800 +2024/08/26 12:00:00,27.12516,13.787 +2024/08/27 00:00:00,27.29386,13.800 +2024/08/27 12:00:00,27.47403,13.810 +2024/08/28 00:00:00,27.66350,13.810 +2024/08/28 12:00:00,27.78379,13.800 +2024/08/29 00:00:00,28.00552,13.800 +2024/08/29 12:00:00,28.08263,13.823 +2024/08/30 00:00:00,28.25815,13.823 +2024/08/30 12:00:00,28.27674,13.833 +2024/08/31 00:00:00,28.40059,13.793 +2024/08/31 12:00:00,28.46757,13.800 +2024/09/01 00:00:00,28.64282,13.817 +2024/09/01 12:00:00,28.67017,13.800 +2024/09/02 00:00:00,28.80195,13.793 +2024/09/02 12:00:00,28.85472,13.800 +2024/09/03 00:00:00,28.97665,13.817 +2024/09/03 12:00:00,29.03571,13.793 +2024/09/04 00:00:00,29.00262,13.793 +2024/09/04 12:00:00,28.97719,13.810 +2024/09/05 00:00:00,28.96380,13.833 +2024/09/05 12:00:00,28.98540,13.817 +2024/09/06 00:00:00,28.96408,13.833 +2024/09/06 12:00:00,28.98293,13.817 +2024/09/07 00:00:00,28.96626,13.823 +2024/09/07 12:00:00,28.98403,13.817 +2024/09/08 00:00:00,28.98977,13.823 +2024/09/08 12:00:00,29.02395,13.810 +2024/09/09 00:00:00,28.98376,13.817 +2024/09/09 12:00:00,29.02395,13.833 +2024/09/10 00:00:00,29.00208,13.823 +2024/09/10 12:00:00,29.02505,13.833 +2024/09/11 00:00:00,29.04008,13.833 +2024/09/11 12:00:00,29.04801,13.817 +2024/09/12 00:00:00,29.06496,13.800 +2024/09/12 12:00:00,29.05293,13.817 +2024/09/13 00:00:00,29.07863,13.823 +2024/09/13 12:00:00,28.99743,13.823 +2024/09/14 00:00:00,29.00754,13.833 +2024/09/14 12:00:00,28.91405,13.817 +2024/09/15 00:00:00,28.97638,13.823 +2024/09/15 12:00:00,28.86374,13.823 +2024/09/16 00:00:00,28.92306,13.823 +2024/09/16 12:00:00,28.87632,13.823 +2024/09/17 00:00:00,28.88698,13.817 +2024/09/17 12:00:00,28.89108,13.817 +2024/09/18 00:00:00,28.87959,13.810 +2024/09/18 12:00:00,28.86100,13.817 +2024/09/19 00:00:00,28.84131,13.817 +2024/09/19 12:00:00,28.85526,13.817 +2024/09/20 00:00:00,28.80878,13.833 +2024/09/20 12:00:00,28.83393,13.833 +2024/09/21 00:00:00,28.79566,13.833 +2024/09/21 12:00:00,28.86920,13.810 +2024/09/22 00:00:00,28.75821,13.810 +2024/09/22 12:00:00,28.78773,13.810 +2024/09/23 00:00:00,28.73605,13.800 +2024/09/23 12:00:00,28.74589,13.800 +2024/09/24 00:00:00,28.73087,13.817 +2024/09/24 12:00:00,28.70598,13.800 +2024/09/25 00:00:00,28.69750,13.800 +2024/09/25 12:00:00,28.65813,13.823 +2024/09/26 00:00:00,28.68793,13.793 +2024/09/26 12:00:00,28.68055,13.793 +2024/09/27 00:00:00,28.70517,13.793 +2024/09/27 12:00:00,28.67809,13.823 +2024/09/28 00:00:00,28.70024,13.800 +2024/09/28 12:00:00,28.64036,13.800 +2024/09/29 00:00:00,28.67618,13.800 +2024/09/29 12:00:00,28.68110,13.817 +2024/09/30 00:00:00,28.73578,13.793 +2024/09/30 12:00:00,28.73660,13.787 +2024/10/01 00:00:00,28.73852,13.810 +2024/10/01 12:00:00,28.69504,13.793 +2024/10/02 00:00:00,28.72731,13.793 +2024/10/02 12:00:00,28.77023,13.800 +2024/10/03 00:00:00,28.79894,13.810 +2024/10/03 12:00:00,28.84159,13.800 +2024/10/04 00:00:00,28.79402,13.793 +2024/10/04 12:00:00,28.75902,13.793 +2024/10/05 00:00:00,28.73934,13.800 +2024/10/05 12:00:00,28.76859,13.777 +2024/10/06 00:00:00,28.72950,13.787 +2024/10/06 12:00:00,28.75820,13.800 +2024/10/07 00:00:00,28.69888,13.777 +2024/10/07 12:00:00,28.75629,13.787 +2024/10/08 00:00:00,28.71746,13.787 +2024/10/08 12:00:00,28.75766,13.793 +2024/10/09 00:00:00,28.71992,13.787 +2024/10/09 12:00:00,28.72703,13.800 +2024/10/10 00:00:00,28.67044,13.793 +2024/10/10 12:00:00,28.65704,13.800 +2024/10/11 00:00:00,28.64802,13.793 +2024/10/11 12:00:00,28.59443,13.793 +2024/10/12 00:00:00,28.59552,13.787 +2024/10/12 12:00:00,28.55507,13.793 +2024/10/13 00:00:00,28.58295,13.793 +2024/10/13 12:00:00,28.52663,13.793 +2024/10/14 00:00:00,28.52417,13.800 +2024/10/14 12:00:00,28.46566,13.800 +2024/10/15 00:00:00,28.48507,13.793 +2024/10/15 12:00:00,28.50230,13.787 +2024/10/16 00:00:00,28.42984,13.787 +2024/10/16 12:00:00,28.43750,13.777 +2024/10/17 00:00:00,28.44160,13.793 +2024/10/17 12:00:00,28.51979,13.793 +2024/10/18 00:00:00,28.49136,13.777 +2024/10/18 12:00:00,28.56272,13.787 +2024/10/19 00:00:00,28.46266,13.800 +2024/10/19 12:00:00,28.46456,13.800 +2024/10/20 00:00:00,28.38446,13.793 +2024/10/20 12:00:00,28.40825,13.777 +2024/10/21 00:00:00,28.32841,13.793 +2024/10/21 12:00:00,28.33470,13.793 +2024/10/22 00:00:00,28.28767,13.800 +2024/10/22 12:00:00,28.28521,13.777 +2024/10/23 00:00:00,28.24912,13.777 +2024/10/23 12:00:00,28.23765,13.793 +2024/10/24 00:00:00,28.24584,13.810 +2024/10/24 12:00:00,28.27236,13.800 +2024/10/25 00:00:00,28.23272,13.787 +2024/10/25 12:00:00,28.19062,13.787 +2024/10/26 00:00:00,28.16765,13.800 +2024/10/26 12:00:00,28.11872,13.787 +2024/10/27 00:00:00,28.12801,13.777 +2024/10/27 12:00:00,28.12172,13.777 +2024/10/28 00:00:00,28.16820,13.810 +2024/10/28 12:00:00,28.20511,13.800 +2024/10/29 00:00:00,28.24667,13.800 +2024/10/29 12:00:00,28.31666,13.787 +2024/10/30 00:00:00,28.26827,13.800 +2024/10/30 12:00:00,28.22178,13.777 +2024/10/31 00:00:00,28.13293,13.793 +2024/10/31 12:00:00,28.16136,13.800 +2024/11/01 00:00:00,28.13128,13.800 +2024/11/01 12:00:00,28.14769,13.793 +2024/11/02 00:00:00,28.08618,13.793 +2024/11/02 12:00:00,28.20456,13.793 +2024/11/03 00:00:00,28.18843,13.810 +2024/11/03 12:00:00,28.29970,13.787 +2024/11/04 00:00:00,28.28385,13.800 +2024/11/04 12:00:00,28.31146,13.793 +2024/11/05 00:00:00,28.16930,13.787 +2024/11/05 12:00:00,28.21659,13.793 +2024/11/06 00:00:00,28.21276,13.793 +2024/11/06 12:00:00,28.26498,13.800 +2024/11/07 00:00:00,28.16792,13.787 +2024/11/07 12:00:00,28.20320,13.793 +2024/11/08 00:00:00,28.14168,13.800 +2024/11/08 12:00:00,28.14878,13.800 +2024/11/09 00:00:00,28.12282,13.777 +2024/11/09 12:00:00,28.12692,13.800 +2024/11/10 00:00:00,28.08289,13.777 +2024/11/10 12:00:00,28.02930,13.800 +2024/11/11 00:00:00,28.00826,13.763 +2024/11/11 12:00:00,28.01810,13.787 +2024/11/12 00:00:00,28.02575,13.770 +2024/11/12 12:00:00,28.06978,13.787 +2024/11/13 00:00:00,27.98556,13.800 +2024/11/13 12:00:00,27.95549,13.817 +2024/11/14 00:00:00,27.89179,13.817 +2024/11/14 12:00:00,27.96478,13.800 +2024/11/15 00:00:00,27.91475,13.770 +2024/11/15 12:00:00,28.05774,13.793 +2024/11/16 00:00:00,28.03150,13.777 +2024/11/16 12:00:00,28.11105,13.800 +2024/11/17 00:00:00,28.03286,13.817 +2024/11/17 12:00:00,28.13430,13.823 +2024/11/18 00:00:00,28.10176,13.787 +2024/11/18 12:00:00,28.15125,13.787 +2024/11/19 00:00:00,28.07825,13.787 +2024/11/19 12:00:00,28.06758,13.800 +2024/11/20 00:00:00,27.94128,13.810 +2024/11/20 12:00:00,27.93416,13.823 +2024/11/21 00:00:00,27.89288,13.777 +2024/11/21 12:00:00,27.88823,13.770 +2024/11/22 00:00:00,27.86472,13.787 +2024/11/22 12:00:00,27.91339,13.800 +2024/11/23 00:00:00,27.92869,13.810 +2024/11/23 12:00:00,27.95467,13.817 +2024/11/24 00:00:00,27.97354,13.777 +2024/11/24 12:00:00,28.00279,13.777 +2024/11/25 00:00:00,27.98556,13.777 +2024/11/25 12:00:00,27.93745,13.793 +2024/11/26 00:00:00,27.92405,13.810 +2024/11/26 12:00:00,27.95795,13.817 +2024/11/27 00:00:00,27.98119,13.753 +2024/11/27 12:00:00,28.06020,13.770 +2024/11/28 00:00:00,27.98693,13.777 +2024/11/28 12:00:00,28.01044,13.770 +2024/11/29 00:00:00,27.95795,13.787 +2024/11/29 12:00:00,28.00635,13.777 +2024/11/30 00:00:00,27.95467,13.793 +2024/11/30 12:00:00,28.00442,13.777 +2024/12/01 00:00:00,27.93745,13.800 +2024/12/01 12:00:00,27.99623,13.787 +2024/12/02 00:00:00,27.91394,13.787 +2024/12/02 12:00:00,28.00005,13.787 +2024/12/03 00:00:00,27.90163,13.800 +2024/12/03 12:00:00,27.99294,13.770 +2024/12/04 00:00:00,27.96506,13.793 +2024/12/04 12:00:00,28.03149,13.787 +2024/12/05 00:00:00,27.98666,13.800 +2024/12/05 12:00:00,28.03341,13.777 +2024/12/06 00:00:00,28.01482,13.800 +2024/12/06 12:00:00,28.05829,13.793 +2024/12/07 00:00:00,28.02684,13.787 +2024/12/07 12:00:00,28.04900,13.777 +2024/12/08 00:00:00,28.08673,13.787 +2024/12/08 12:00:00,28.12062,13.800 +2024/12/09 00:00:00,28.11297,13.800 +2024/12/09 12:00:00,28.13047,13.800 +2024/12/10 00:00:00,28.07962,13.800 +2024/12/10 12:00:00,28.03778,13.800 +2024/12/11 00:00:00,27.95931,13.787 +2024/12/11 12:00:00,27.98228,13.787 +2024/12/12 00:00:00,27.95221,13.800 +2024/12/12 12:00:00,28.04981,13.787 +2024/12/13 00:00:00,27.98229,13.787 +2024/12/13 12:00:00,28.05364,13.787 +2024/12/14 00:00:00,27.90108,13.787 +2024/12/14 12:00:00,27.95604,13.787 +2024/12/15 00:00:00,27.90163,13.810 +2024/12/15 12:00:00,28.01072,13.793 +2024/12/16 00:00:00,27.87073,13.777 +2024/12/16 12:00:00,27.92268,13.800 +2024/12/17 00:00:00,27.84175,13.793 +2024/12/17 12:00:00,27.94209,13.787 +2024/12/18 00:00:00,27.83929,13.777 +2024/12/18 12:00:00,27.83902,13.777 +2024/12/19 00:00:00,27.81797,13.800 +2024/12/19 12:00:00,27.84121,13.800 +2024/12/20 00:00:00,27.79090,13.793 +2024/12/20 12:00:00,27.80293,13.787 +2024/12/21 00:00:00,27.79008,13.800 +2024/12/21 12:00:00,27.82754,13.800 +2024/12/22 00:00:00,27.80239,13.777 +2024/12/22 12:00:00,27.82918,13.800 +2024/12/23 00:00:00,27.83301,13.777 +2024/12/23 12:00:00,27.84695,13.800 +2024/12/24 00:00:00,27.77505,13.793 +2024/12/24 12:00:00,27.79910,13.800 +2024/12/25 00:00:00,27.80157,13.793 +2024/12/25 12:00:00,27.89069,13.800 +2024/12/26 00:00:00,27.82399,13.793 +2024/12/26 12:00:00,27.86363,13.777 +2024/12/27 00:00:00,27.78379,13.787 +2024/12/27 12:00:00,27.85023,13.800 +2024/12/28 00:00:00,27.76328,13.810 +2024/12/28 12:00:00,27.76794,13.800 +2024/12/29 00:00:00,27.64409,13.793 +2024/12/29 12:00:00,27.69631,13.793 +2024/12/30 00:00:00,27.65201,13.787 +2024/12/30 12:00:00,27.70587,13.787 +2024/12/31 00:00:00,27.60226,13.800 +2024/12/31 12:00:00,27.70368,13.793 +2025/01/01 00:00:00,27.59623,13.800 +2025/01/01 12:00:00,27.62795,13.770 +2025/01/02 00:00:00,27.53855,13.793 +2025/01/02 12:00:00,27.55687,13.787 +2025/01/03 00:00:00,27.47293,13.787 +2025/01/03 12:00:00,27.52816,13.787 +2025/01/04 00:00:00,27.54184,13.787 +2025/01/04 12:00:00,27.61291,13.787 +2025/01/05 00:00:00,27.59761,13.793 +2025/01/05 12:00:00,27.56070,13.793 +2025/01/06 00:00:00,27.50738,13.777 +2025/01/06 12:00:00,27.51504,13.793 +2025/01/07 00:00:00,27.51695,13.793 +2025/01/07 12:00:00,27.52406,13.800 +2025/01/08 00:00:00,27.42728,13.787 +2025/01/08 12:00:00,27.44013,13.787 +2025/01/09 00:00:00,27.45625,13.787 +2025/01/09 12:00:00,27.52624,13.800 +2025/01/10 00:00:00,27.37424,13.777 +2025/01/10 12:00:00,27.39419,13.800 +2025/01/11 00:00:00,27.32529,13.793 +2025/01/11 12:00:00,27.46638,13.770 +2025/01/12 00:00:00,27.37806,13.800 +2025/01/12 12:00:00,27.42290,13.787 +2025/01/13 00:00:00,27.28812,13.787 +2025/01/13 12:00:00,27.32229,13.800 +2025/01/14 00:00:00,27.23179,13.800 +2025/01/14 12:00:00,27.24874,13.787 +2025/01/15 00:00:00,27.17055,13.800 +2025/01/15 12:00:00,27.17410,13.793 +2025/01/16 00:00:00,27.10712,13.787 +2025/01/16 12:00:00,27.19816,13.793 +2025/01/17 00:00:00,27.23343,13.793 +2025/01/17 12:00:00,27.29522,13.777 +2025/01/18 00:00:00,27.26979,13.793 +2025/01/18 12:00:00,27.25885,13.793 +2025/01/19 00:00:00,27.19106,13.800 +2025/01/19 12:00:00,27.21019,13.800 +2025/01/20 00:00:00,27.23781,13.793 +2025/01/20 12:00:00,27.22167,13.793 +2025/01/21 00:00:00,27.12735,13.777 +2025/01/21 12:00:00,27.05107,13.793 +2025/01/22 00:00:00,27.04998,13.800 +2025/01/22 12:00:00,27.07404,13.800 +2025/01/23 00:00:00,27.01909,13.787 +2025/01/23 12:00:00,27.00049,13.800 +2025/01/24 00:00:00,26.94253,13.810 +2025/01/24 12:00:00,27.01662,13.793 +2025/01/25 00:00:00,27.02236,13.793 +2025/01/25 12:00:00,27.07595,13.800 +2025/01/26 00:00:00,26.99393,13.800 +2025/01/26 12:00:00,27.00706,13.800 +2025/01/27 00:00:00,26.90534,13.800 +2025/01/27 12:00:00,26.97670,13.800 +2025/01/28 00:00:00,26.92257,13.793 +2025/01/28 12:00:00,26.98710,13.787 +2025/01/29 00:00:00,26.93570,13.800 +2025/01/29 12:00:00,26.96987,13.793 +2025/01/30 00:00:00,26.92284,13.793 +2025/01/30 12:00:00,26.95292,13.793 +2025/01/31 00:00:00,26.84547,13.787 +2025/01/31 12:00:00,26.80391,13.793 +2025/02/01 00:00:00,26.76674,13.787 +2025/02/01 12:00:00,26.80118,13.793 +2025/02/02 00:00:00,26.80474,13.787 +2025/02/02 12:00:00,26.77794,13.800 +2025/02/03 00:00:00,26.77712,13.793 +2025/02/03 12:00:00,26.73229,13.810 +2025/02/04 00:00:00,26.74486,13.787 +2025/02/04 12:00:00,26.74349,13.810 +2025/02/05 00:00:00,26.72408,13.787 +2025/02/05 12:00:00,26.72135,13.793 +2025/02/06 00:00:00,26.69128,13.770 +2025/02/06 12:00:00,26.69866,13.793 +2025/02/07 00:00:00,26.65300,13.800 +2025/02/07 12:00:00,26.66229,13.800 +2025/02/08 00:00:00,26.60405,13.793 +2025/02/08 12:00:00,26.65518,13.787 +2025/02/09 00:00:00,26.56195,13.793 +2025/02/09 12:00:00,26.59832,13.793 +2025/02/10 00:00:00,26.53489,13.793 +2025/02/10 12:00:00,26.59968,13.793 +2025/02/11 00:00:00,26.57070,13.787 +END OF DATA FILE OF DATALOGGER FOR WINDOWS diff --git a/scripts/generate_ose_pod_dictionary.py b/scripts/generate_ose_pod_dictionary.py new file mode 100644 index 00000000..f015aba5 --- /dev/null +++ b/scripts/generate_ose_pod_dictionary.py @@ -0,0 +1,358 @@ +#!/usr/bin/env python3 +"""Generate src/constants/osePodDictionary.ts from the NM OSE WATERS PODs data dictionary. + +The OSE publishes the dictionary as a workbook with a "Data Dictionary" sheet +(one row per column of the WATERS_PODs table) and a "Code Tables" sheet (the +coded-value lookups those columns reference). The ArcGIS feature service we +query exposes the same columns, but truncated to 10 characters and with +reserved words suffixed with an underscore, so this script also resolves the +service field name for each dictionary column. + +Usage: + python3 scripts/generate_ose_pod_dictionary.py path/to/nmose_WATERS_PODs_data_dictionary_v8.xlsx + npx biome check --write src/constants/osePodDictionary.ts + +Requires openpyxl (pip install openpyxl). +""" + +from __future__ import annotations + +import json +import re +import sys +from pathlib import Path + +import openpyxl + +REPO_ROOT = Path(__file__).resolve().parent.parent +OUTPUT_PATH = REPO_ROOT / "src" / "constants" / "osePodDictionary.ts" + +# Feature service the app queries. Its field names are the join key, so they are +# listed here rather than fetched, keeping generation offline and deterministic. +SERVICE_FIELDS = """ +OBJECTID pod_basin pod_nbr pod_suffix ref pod_name tws rng sec qtr_4th qtr_16th +qtr_64th blk zone_ x y landgrant legal county license_nb start_date finish_dat +plug_date pcw_rcv_da elevation depth_well grnd_wtr_s percent_sh depth_wate +log_file_d sched_date use_of_wel pump_type pump_seria discharge aquifer sys_date +subdiv_nam subdiv_loc restrict_ lat_deg lat_min lat_sec lon_deg lon_min lon_sec +surface_co estimate_y pod_status casing_siz ditch_name utm_zone easting northing +datum utm_source utm_accura xy_source xy_accurac lat_lon_so lat_lon_ac tract_nbr +map_nbr surv_map other_loc pod_rec_nb cfs_start_ cfs_end_md cfs_cnv_fa cs_code +wrats_s_id utm_error pod_sub_ba well_tag static_lev pod_file sum_rec_nb basin nbr +suffix sub_basin status use_ total_div sub_file sf_header db_file own_lname +own_fname addr1 addr2 city state zip contact_ln contact_fn nmwrrs_wrs in_state +dump_date loc_error wr_count replaced metered +""".split() + +# ArcGIS renames columns that collide with reserved words, which truncation +# alone cannot recover. +FIELD_OVERRIDES = { + "landgrant": "GRANT", + "zone_": "ZONE", + "use_": "USE", + "restrict_": "RESTRICT", +} + +# Service fields with no counterpart in the dictionary. Listed explicitly so a +# future dictionary revision that adds them shows up as a diff instead of +# silently staying undocumented. +UNDOCUMENTED_FIELDS = {"license_nb", "metered", "dump_date"} + +# Labels the word-by-word expansion below cannot produce correctly, either +# because the column name is ambiguous (LAT_SEC is seconds, SEC is a PLSS +# section) or because it is abbreviated past recognition. +LABEL_OVERRIDES = { + "ADDR1": "Address Line 1", + "ADDR2": "Address Line 2", + "BLK": "Block", + "CFS_END_MDAY": "CFS End (Month/Day)", + "CFS_START_MDAY": "CFS Start (Month/Day)", + "CONTACT_FNAME": "Contact First Name", + "CONTACT_LNAME": "Contact Last Name", + "DB_FILE": "Water Right File", + "DEPTH_WATER": "Depth to Water", + "DEPTH_WELL": "Well Depth", + "DISCHARGE": "Discharge Pipe Size", + "ESTIMATE_YIELD": "Estimated Yield", + "GRANT": "Land Grant", + "GRND_WTR_SRC": "Groundwater Source Type", + "IN_STATE": "In-State Flag", + "LAT_DEG": "Latitude Degrees", + "LAT_MIN": "Latitude Minutes", + "LAT_SEC": "Latitude Seconds", + "LEGAL": "Legal Description", + "LOC_ERROR": "Location Error", + "LOG_FILE_DATE": "Well Record Filed Date", + "LON_DEG": "Longitude Degrees", + "LON_MIN": "Longitude Minutes", + "LON_SEC": "Longitude Seconds", + "NBR": "File Number", + "OTHER_LOC": "Other Location", + "OWN_FNAME": "Owner First Name", + "OWN_LNAME": "Owner Last Name", + "PCW_RCV_DATE": "Proof of Completion Received", + "POD_FILE": "POD File Number", + "QTR_16TH": "Quarter (1/16 Section)", + "QTR_4TH": "Quarter (1/4 Section)", + "QTR_64TH": "Quarter (1/64 Section)", + "REF": "Reference", + "RESTRICT": "Diversion Restriction", + "SCHED_DATE": "Well Schedule Date", + "SF_HEADER": "Adjudication Subfile Header", + "STATIC_LEVEL": "Static Water Level", + "SUB_FILE": "Adjudication Subfile", + "SUFFIX": "File Suffix", + "SUM_REC_NBR": "Water Right Record Number", + "SURFACE_CODE": "Surface Water Source", + "SURV_MAP": "Survey Map Name", + "TOTAL_DIV": "Total Diversion", + "UTM_ERROR": "UTM Conversion Error", + "WRATS_S_ID": "WRATS POD ID", + "WR_COUNT": "Water Right File Count", + "X": "X Coordinate", + "Y": "Y Coordinate", + "ZIP": "ZIP Code", + "ZONE": "State Plane Zone", +} + +# Applied word-by-word when turning a column name into a display label. +ABBREVIATIONS = { + "acc": "Accuracy", + "cfs": "CFS", + "cnv": "Conversion", + "cs": "Coordinate System", + "db": "DB", + "id": "ID", + "lat": "Latitude", + "lon": "Longitude", + "mday": "Month/Day", + "nbr": "Number", + "nmwrrs": "NMWRRS", + "objectid": "Object ID", + "plss": "PLSS", + "pod": "POD", + "qtr": "Quarter", + "rec": "Record", + "rng": "Range", + "sec": "Section", + "sf": "Subfile", + "src": "Source", + "srv": "Survey", + "sub": "Sub", + "subdiv": "Subdivision", + "sum": "Summary", + "surv": "Survey", + "sys": "System", + "tws": "Township", + "url": "URL", + "utm": "UTM", + "wr": "Water Right", + "wrats": "WRATS", + "wrsum": "Water Right Summary", + "xy": "XY", +} + + +def read_rows(sheet) -> list[list[str]]: + return [ + ["" if cell is None else str(cell).strip() for cell in row] + for row in sheet.iter_rows(values_only=True) + ] + + +def parse_data_dictionary(sheet) -> dict[str, dict[str, str]]: + rows = read_rows(sheet) + header_index = next(i for i, row in enumerate(rows) if row and row[0] == "Column Name") + header = rows[header_index] + + columns: dict[str, dict[str, str]] = {} + for row in rows[header_index + 1 :]: + if not row or not row[0]: + continue + record = {header[i]: row[i] for i in range(len(header))} + columns[record["Column Name"]] = record + + return columns + + +def parse_code_tables(sheet) -> dict[str, dict[str, object]]: + tables: dict[str, dict[str, object]] = {} + current: str | None = None + in_values = False + + for row in read_rows(sheet): + code = row[0] if row else "" + label = row[1] if len(row) > 1 else "" + + if code.lower().startswith("code table:"): + current = code.split(":", 1)[1].strip() + tables[current] = {"description": "", "values": {}} + in_values = False + continue + + if current is None: + continue + + if code == "Code Value": + in_values = True + continue + + if not in_values: + if code and not tables[current]["description"]: + tables[current]["description"] = code + continue + + if code or label: + tables[current]["values"][code] = label + + return tables + + +def resolve_column(service_field: str, columns: dict[str, dict[str, str]]) -> str | None: + """Map an ArcGIS service field back to its dictionary column name.""" + if service_field in UNDOCUMENTED_FIELDS: + return None + if service_field in FIELD_OVERRIDES: + return FIELD_OVERRIDES[service_field] + + upper = service_field.upper() + if upper in columns: + return upper + + # The service truncates long column names to 10 characters. + candidates = sorted( + (column for column in columns if column.startswith(upper)), key=len + ) + return candidates[0] if candidates else None + + +def to_label(column: str) -> str: + if column in LABEL_OVERRIDES: + return LABEL_OVERRIDES[column] + + words = [word for word in re.split(r"[_\s]+", column.lower()) if word] + return " ".join(ABBREVIATIONS.get(word, word.capitalize()) for word in words) + + +def ts_string(value: str) -> str: + return json.dumps(value, ensure_ascii=False) + + +def render(columns: dict[str, dict[str, str]], code_tables: dict, source_name: str) -> str: + field_entries = [] + missing = [] + + for service_field in SERVICE_FIELDS: + column = resolve_column(service_field, columns) + if column is None: + missing.append(service_field) + continue + + record = columns[column] + code_table = record.get("Valid Values / Code Table", "").strip().upper() + if code_table and code_table not in code_tables: + code_table = "" + + field_entries.append( + " {}: {{\n" + " column: {},\n" + " label: {},\n" + " description: {},\n" + " dataType: {},\n" + " codeTable: {},\n" + " }},".format( + json.dumps(service_field), + ts_string(column), + ts_string(to_label(column)), + ts_string(record.get("Brief Description", "").strip()), + ts_string(record.get("Data Type", "").strip()), + ts_string(code_table) if code_table else "null", + ) + ) + + table_entries = [] + for name, table in sorted(code_tables.items()): + values = "\n".join( + " {}: {},".format(json.dumps(code), ts_string(label)) + for code, label in table["values"].items() + ) + table_entries.append( + " {}: {{\n" + " description: {},\n" + " values: {{\n{}\n }},\n" + " }},".format(json.dumps(name), ts_string(table["description"]), values) + ) + + fields_block = "\n".join(field_entries) + tables_block = "\n".join(table_entries) + missing_note = ", ".join(sorted(missing)) or "none" + + return f"""// GENERATED FILE — do not edit by hand. +// Source: {source_name} (NM OSE WATERS PODs data dictionary). +// Regenerate: python3 scripts/generate_ose_pod_dictionary.py +// +// Keys are the field names returned by the OSE Points of Diversion feature +// service, which truncates the dictionary's column names to 10 characters. +// Service fields with no entry in this dictionary revision: {missing_note}. + +export type OSEPODCodeTable = {{ + description: string + values: Record +}} + +export type OSEPODFieldDefinition = {{ + /** Column name in the OSE WATERS_PODs table. */ + column: string + /** Short human-readable label for the field. */ + label: string + /** The dictionary's brief description of the field. */ + description: string + dataType: string + /** Key into OSE_POD_CODE_TABLES when the field holds a coded value. */ + codeTable: string | null +}} + +export const OSE_POD_CODE_TABLES: Record = {{ +{tables_block} +}} + +export const OSE_POD_FIELDS: Record = {{ +{fields_block} +}} + +/** Decodes a coded value using the field's code table, falling back to the raw value. */ +export const decodeOSEPODValue = ( + field: string, + value: unknown +): string | null => {{ + if (value == null || value === '') return null + + const definition = OSE_POD_FIELDS[field] + const table = definition?.codeTable + ? OSE_POD_CODE_TABLES[definition.codeTable] + : undefined + + return table?.values[String(value).trim()] ?? String(value) +}} +""" + + +def main() -> int: + if len(sys.argv) != 2: + print(__doc__) + return 2 + + source = Path(sys.argv[1]) + workbook = openpyxl.load_workbook(source, data_only=True) + columns = parse_data_dictionary(workbook["Data Dictionary"]) + code_tables = parse_code_tables(workbook["Code Tables"]) + + OUTPUT_PATH.write_text(render(columns, code_tables, source.name), encoding="utf-8") + print(f"Wrote {OUTPUT_PATH.relative_to(REPO_ROOT)}") + print(f" {len(columns)} dictionary columns, {len(code_tables)} code tables") + print(f" Now run: npx biome check --write {OUTPUT_PATH.relative_to(REPO_ROOT)}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/generate_usgs_site_dictionary.py b/scripts/generate_usgs_site_dictionary.py new file mode 100644 index 00000000..c075b098 --- /dev/null +++ b/scripts/generate_usgs_site_dictionary.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python3 +"""Generate src/constants/usgsSiteDictionary.ts from the USGS OGC API reference lists. + +The NWIS site service returns coded values (site_tp_cd=GW, topo_cd=V, ...) and +its RDB header supplies the column labels but not the meaning of the codes. +USGS publishes the code lists as reference collections on the Water Data OGC +API, so this script pulls them at build time and emits a lookup module. Nothing +is hand-written, and re-running the script picks up USGS revisions. + +Usage: + python3 scripts/generate_usgs_site_dictionary.py + npx biome check --write src/constants/usgsSiteDictionary.ts + +Requires network access to api.waterdata.usgs.gov. +""" + +from __future__ import annotations + +import json +import sys +import urllib.parse +import urllib.request +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parent.parent +OUTPUT_PATH = REPO_ROOT / "src" / "constants" / "usgsSiteDictionary.ts" + +API_ROOT = "https://api.waterdata.usgs.gov/ogcapi/v0/collections" +PAGE_SIZE = 5000 + +# Reference collections we pull, and how each one turns into { code: label }. +# `key` builds the lookup key from a feature's properties; `label` builds the +# decoded text. Collections with tens of thousands of entries (hydrologic units, +# local aquifer codes) are deliberately left out — those columns stay raw rather +# than adding megabytes to the bundle. +COLLECTIONS = { + "agency-codes": { + "key": lambda p: p["id"], + "label": lambda p: p.get("agency_name"), + }, + "site-types": { + "key": lambda p: p["id"], + "label": lambda p: p.get("site_type_name"), + "description": lambda p: p.get("site_type_description"), + }, + "coordinate-accuracy-codes": { + "key": lambda p: p["id"], + "label": lambda p: p.get("coordinate_accuracy_description"), + }, + "coordinate-datum-codes": { + "key": lambda p: p["id"], + "label": lambda p: p.get("coordinate_datum_description"), + }, + "coordinate-method-codes": { + "key": lambda p: p["id"], + "label": lambda p: p.get("coordinate_method_description"), + }, + "altitude-datums": { + "key": lambda p: p["id"], + "label": lambda p: p.get("altitude_datum_description"), + }, + "reliability-codes": { + "key": lambda p: p["id"], + "label": lambda p: p.get("reliability_description"), + }, + "topographic-codes": { + "key": lambda p: p["id"], + "label": lambda p: p.get("topography_name"), + "description": lambda p: p.get("short_topography_description"), + }, + "aquifer-types": { + "key": lambda p: p["id"], + "label": lambda p: p.get("aquifer_type_description"), + }, + "national-aquifer-codes": { + "key": lambda p: p["id"], + "label": lambda p: p.get("national_aquifer_name"), + }, + "time-zone-codes": { + "key": lambda p: p["id"], + "label": lambda p: p.get("time_zone_name"), + }, + "countries": { + "key": lambda p: p["id"], + "label": lambda p: p.get("country_name"), + }, + # State and county codes arrive as FIPS digits, so they are keyed by FIPS + # rather than by the collection's own id. Limited to US entries; the site + # service reports the country separately. + "states": { + "key": lambda p: p["state_fips_code"] if p.get("country_code") == "US" else None, + "label": lambda p: p.get("state_name"), + }, + "counties": { + "key": lambda p: ( + f"{p['state_fips_code']}-{p['county_fips_code']}" + if p.get("country_code") == "US" + else None + ), + "label": lambda p: p.get("county_name"), + }, +} + +# RDB column -> reference collection. Columns whose code list is not published +# (or is too large to bundle) are omitted and render as the raw code. +COLUMN_CODE_TABLES = { + "agency_cd": "agency-codes", + "site_tp_cd": "site-types", + "coord_meth_cd": "coordinate-method-codes", + "coord_acy_cd": "coordinate-accuracy-codes", + "coord_datum_cd": "coordinate-datum-codes", + "dec_coord_datum_cd": "coordinate-datum-codes", + "alt_datum_cd": "altitude-datums", + "reliability_cd": "reliability-codes", + "topo_cd": "topographic-codes", + "aqfr_type_cd": "aquifer-types", + "nat_aqfr_cd": "national-aquifer-codes", + "tz_cd": "time-zone-codes", + "country_cd": "countries", + "state_cd": "states", + "district_cd": "states", +} + + +def fetch_collection(collection: str) -> list[dict]: + features: list[dict] = [] + offset = 0 + + while True: + query = urllib.parse.urlencode( + {"f": "json", "limit": PAGE_SIZE, "offset": offset} + ) + url = f"{API_ROOT}/{collection}/items?{query}" + with urllib.request.urlopen(url, timeout=120) as response: + payload = json.load(response) + + page = payload.get("features", []) + features.extend(page) + + matched = payload.get("numberMatched") + offset += len(page) + if not page or matched is None or offset >= matched: + break + + return features + + +def build_tables() -> dict[str, dict[str, dict[str, str]]]: + tables: dict[str, dict[str, dict[str, str]]] = {} + + for collection, spec in COLLECTIONS.items(): + entries: dict[str, dict[str, str]] = {} + for feature in fetch_collection(collection): + properties = feature.get("properties") or {} + key = spec["key"](properties) + label = spec["label"](properties) + if not key or not label: + continue + + entry = {"label": label} + describe = spec.get("description") + description = describe(properties) if describe else None + if description: + entry["description"] = description + entries[key] = entry + + print(f" {collection}: {len(entries)} codes", file=sys.stderr) + tables[collection] = entries + + return tables + + +def ts_string(value: str) -> str: + return json.dumps(value, ensure_ascii=False) + + +def render(tables: dict[str, dict[str, dict[str, str]]]) -> str: + table_blocks = [] + for name, entries in tables.items(): + rows = [] + for code, entry in sorted(entries.items()): + if "description" in entry: + rows.append( + " {}: {{ label: {}, description: {} }},".format( + json.dumps(code), + ts_string(entry["label"]), + ts_string(entry["description"]), + ) + ) + else: + rows.append( + " {}: {{ label: {} }},".format( + json.dumps(code), ts_string(entry["label"]) + ) + ) + table_blocks.append( + " {}: {{\n{}\n }},".format(json.dumps(name), "\n".join(rows)) + ) + + column_rows = "\n".join( + " {}: {},".format(json.dumps(column), json.dumps(collection)) + for column, collection in sorted(COLUMN_CODE_TABLES.items()) + ) + + return f"""// GENERATED FILE — do not edit by hand. +// Source: USGS Water Data OGC API reference collections +// ({API_ROOT}). +// Regenerate: python3 scripts/generate_usgs_site_dictionary.py +// +// The NWIS site service returns coded values and its RDB header supplies the +// column labels, but the code meanings live in these reference lists. + +export type USGSCodeEntry = {{ + label: string + description?: string +}} + +export const USGS_CODE_TABLES: Record> = {{ +{chr(10).join(table_blocks)} +}} + +/** RDB column -> reference collection. Columns absent here render their raw code. */ +export const USGS_COLUMN_CODE_TABLES: Record = {{ +{column_rows} +}} + +/** + * Decodes a coded site-file value, e.g. site_tp_cd "GW" -> "Well". + * County codes need the state FIPS code, which the caller passes as `context`. + */ +export const decodeUSGSValue = ( + column: string, + value: unknown, + context?: {{ stateFips?: string }} +): USGSCodeEntry | null => {{ + const raw = value == null ? '' : String(value).trim() + if (!raw) return null + + if (column === 'county_cd') {{ + const stateFips = context?.stateFips?.trim() + if (!stateFips) return null + return USGS_CODE_TABLES['counties'][`${{stateFips}}-${{raw}}`] ?? null + }} + + const collection = USGS_COLUMN_CODE_TABLES[column] + if (!collection) return null + + return USGS_CODE_TABLES[collection][raw] ?? null +}} +""" + + +def main() -> int: + print("Fetching USGS reference collections...", file=sys.stderr) + tables = build_tables() + OUTPUT_PATH.write_text(render(tables), encoding="utf-8") + total = sum(len(entries) for entries in tables.values()) + print(f"Wrote {OUTPUT_PATH.relative_to(REPO_ROOT)} ({total} codes)") + print(f" Now run: npx biome check --write {OUTPUT_PATH.relative_to(REPO_ROOT)}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/App.tsx b/src/App.tsx index 07496244..b05236da 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,21 +1,35 @@ import { Authenticated } from '@refinedev/core' -import { AuthPage, ErrorComponent } from '@refinedev/mui' +import { ErrorComponent } from '@refinedev/mui' import { BrowserRouter, Navigate, Outlet, Route, Routes } from 'react-router' +import { AppProviders } from '@/AppProviders' import { AppShell } from '@/components/AppShell' -import { ThemedTitleV2 } from '@/components/layout/title' -import { Callback } from '@/components/Auth' -import { Home } from '@/pages/home' -import { TypographyPage } from '@/pages/example/TypographyPage' -import { DataGridPage } from '@/pages/example/DataGridPage' +import { Callback, Login } from '@/components/Auth' import { ContentPage } from '@/pages/content' -import { OcotilloRoutes, ST2Routes } from '@/routes' +import { TypographyPage } from '@/pages/example/TypographyPage' +import { Home } from '@/pages/home' +import { GeothermalRoutes, OcotilloRoutes, ST2Routes } from '@/routes' import { settings } from '@/settings' -import { AppProviders } from '@/AppProviders' const App: React.FC = () => ( + } + > + + + } + > + } + /> + }> @@ -24,23 +38,7 @@ const App: React.FC = () => ( } > } /> - } - hideForm={true} - type="login" - registerLink={false} - providers={[ - { - name: 'authentik', - label: 'Sign in with Authentik', - }, - ]} - /> - } - /> + } /> ( /> {/* TEMPORARY: example specimen pages */} } /> - } /> } /> + } /> } /> USGS The National Map' + +const openFreeMapStyle = (name: string) => `${OPENFREEMAP_HOST}/styles/${name}` + +const usgsTileUrl = (service: string) => + `${USGS_HOST}/${service}/MapServer/tile/{z}/{y}/{x}` + +/** + * A single representative tile used as the selector thumbnail. Zoom 6 over + * central New Mexico — the same framing the previous Mapbox static previews + * used. ArcGIS tile paths are {z}/{row}/{col}, so y precedes x. + */ +const usgsPreviewUrl = (service: string) => + `${USGS_HOST}/${service}/MapServer/tile/6/25/13` + +/** + * Wraps a USGS raster tile service in a minimal MapLibre style. `maxzoom` is + * the deepest level the service caches; MapLibre overzooms past it rather than + * requesting tiles that would 404. + */ +const usgsRasterStyle = ( + service: string, + maxzoom: number +): StyleSpecification => ({ + version: 8, + glyphs: GLYPHS_URL, + sources: { + [service]: { + type: 'raster', + tiles: [usgsTileUrl(service)], + tileSize: 256, + maxzoom, + attribution: USGS_ATTRIBUTION, + }, + }, + layers: [ + { + id: service, + type: 'raster', + source: service, + }, + ], +}) + +export interface BasemapDefinition { + /** Stable key persisted in component state and analytics. */ + id: string + title: string + /** A style URL for vector basemaps, or an inline style for raster ones. */ + style: string | StyleSpecification + /** + * Static thumbnail for the selector. Raster basemaps can point at a single + * tile; vector basemaps have no static endpoint and render a live preview. + */ + previewUrl?: string +} + +export const BASEMAPS: BasemapDefinition[] = [ + { id: 'light', title: 'Light', style: openFreeMapStyle('positron') }, + { id: 'dark', title: 'Dark', style: openFreeMapStyle('dark') }, + { id: 'streets', title: 'Streets', style: openFreeMapStyle('bright') }, + { id: 'detailed', title: 'Detailed', style: openFreeMapStyle('liberty') }, + { + id: 'satellite', + title: 'Satellite', + style: usgsRasterStyle('USGSImageryOnly', 16), + previewUrl: usgsPreviewUrl('USGSImageryOnly'), + }, + { + id: 'satellite-labels', + title: 'Satellite + Labels', + style: usgsRasterStyle('USGSImageryTopo', 16), + previewUrl: usgsPreviewUrl('USGSImageryTopo'), + }, + { + id: 'topo', + title: 'Topographic', + style: usgsRasterStyle('USGSTopo', 16), + previewUrl: usgsPreviewUrl('USGSTopo'), + }, + { + id: 'shaded-relief', + title: 'Shaded Relief', + style: usgsRasterStyle('USGSShadedReliefOnly', 15), + previewUrl: usgsPreviewUrl('USGSShadedReliefOnly'), + }, +] + +export const LIGHT_BASEMAP_ID = 'light' +export const DARK_BASEMAP_ID = 'dark' +export const DEFAULT_BASEMAP_ID = LIGHT_BASEMAP_ID + +/** The basemap that tracks the app's color mode, keyed by that mode. */ +export const THEMED_BASEMAP_IDS = { + light: LIGHT_BASEMAP_ID, + dark: DARK_BASEMAP_ID, +} as const + +const BASEMAPS_BY_ID = new Map(BASEMAPS.map((basemap) => [basemap.id, basemap])) + +export const getBasemap = (id: string): BasemapDefinition => + BASEMAPS_BY_ID.get(id) ?? BASEMAPS_BY_ID.get(DEFAULT_BASEMAP_ID)! + +export const getBasemapStyle = (id: string) => getBasemap(id).style diff --git a/src/components/AppShell.tsx b/src/components/AppShell.tsx index 2c84b52b..f3a4b7d6 100644 --- a/src/components/AppShell.tsx +++ b/src/components/AppShell.tsx @@ -1,7 +1,7 @@ import { useCallback, useContext, useEffect, useRef, useState } from 'react' import { cn } from '@/lib/utils' import { useIsMobile } from '@/hooks/use-mobile' -import { Outlet, Link, useLocation, useNavigate } from 'react-router' +import { Outlet, Link, useLocation } from 'react-router' import { CanAccess, useCustomMutation, @@ -50,7 +50,6 @@ import { Check, ChevronDown, ChevronRight, - FlaskConical, Lock, LogOut, Menu, @@ -63,7 +62,7 @@ import { import { ColorModeContext } from '@/contexts' import SearchBar from '@/components/SearchBar' import { ReportBugButton } from '@/components/Button' -import { AmpRole, PRIMARY_NAV, RESOURCE_NAV, SHOW_EXAMPLE_NAV, type NavItem } from '@/config/navigation' +import { AmpRole, PRIMARY_NAV, RESOURCE_NAV, type NavItem } from '@/config/navigation' import { useAccessCapabilities } from '@/hooks' import { useSearch } from '@/providers/search-provider' import { SupportPanelContext } from '@/components/SupportPanelContext' @@ -159,6 +158,7 @@ function ExpandButton() { const FOOTER_LINKS = [ { label: 'About', href: '/about' }, { label: 'Connect Desktop GIS', href: '/ogcapi' }, + { label: 'Analytics Disclosure', href: '/analytics-disclosure' }, { label: 'Report a Bug', href: '/report-a-bug' }, ] as const @@ -424,7 +424,7 @@ function AppSidebar() { - {/* Resource navigation + temporary Example section — all in one group */} + {/* Resource navigation */} @@ -436,8 +436,6 @@ function AppSidebar() { canSeeNavItem={canSeeNavItem} /> ))} - {/* Example demos — toggle SHOW_EXAMPLE_NAV in config/navigation.ts */} - {SHOW_EXAMPLE_NAV ? : null} @@ -468,55 +466,6 @@ function AppSidebar() { ) } -function ExampleNavItem() { - const location = useLocation() - const navigate = useNavigate() - const [open, setOpen] = useState(location.pathname.startsWith('/example')) - - useEffect(() => { - if (!location.pathname.startsWith('/example')) setOpen(false) - }, [location.pathname]) - - const handleClick = () => { - setOpen(true) - navigate('/example/typography') - } - - return ( - - - - - - Example - - - - - - - - Typography - - - - - Data Grid - - - - - - - ) -} - function SupportPanelTrigger({ collapsed }: { collapsed: boolean }) { const { isOpen, open, close } = useContext(SupportPanelContext) return ( @@ -775,7 +724,8 @@ function SupportPanel() { -
+ {/* Brand-blue gradient border: brand-300 → brand-500 → brand-700 */} +
+) + +/** + * Per-layer artifacts for one collection, rendered inline on the datasets page. + */ +export const GisLayerDownloads = ({ layer }: { layer: GisLayer }) => { + if (layer.downloads.length === 0) return null + + return ( + + {layer.downloads.map((download) => ( + + ))} + + ) +} + +const CopyableUrl = ({ url }: { url: string }) => { + const [copied, setCopied] = useState(false) + + const handleCopy = async () => { + await navigator.clipboard.writeText(url) + setCopied(true) + window.setTimeout(() => setCopied(false), 2000) + } + + return ( + + + {url} + + + + + + + + ) +} + +/** + * The internal connections file is the one artifact behind auth, and an anchor + * cannot send a bearer token — hence the blob round-trip through the + * authenticated axios instance, which also carries the refresh interceptor. + */ +const InternalConnectionsButton = ({ download }: { download: GisDownload }) => { + const [isDownloading, setIsDownloading] = useState(false) + const [error, setError] = useState(null) + + const handleDownload = async () => { + setIsDownloading(true) + setError(null) + let objectUrl: string | undefined + try { + const response = await axiosInstance.get(download.href, { + responseType: 'blob', + }) + objectUrl = URL.createObjectURL(response.data) + const anchor = document.createElement('a') + anchor.href = objectUrl + anchor.download = download.filename + anchor.click() + } catch (downloadError) { + setError( + downloadError instanceof Error + ? downloadError.message + : 'Download failed.' + ) + } finally { + if (objectUrl) URL.revokeObjectURL(objectUrl) + setIsDownloading(false) + } + } + + return ( + + + {error ? ( + + {error} + + ) : null} + + ) +} + +/** + * The "connect to everything" surface: one connections file for QGIS, and the + * service URL for ArcGIS Pro, which has no importable connection file from us. + */ +export const GisConnectionsPanel = ({ + catalog, + canViewInternal, +}: { + catalog: GisCatalog + canViewInternal: boolean +}) => { + const qgisConnections = findGisConnection(catalog, 'qgis') + const internalConnections = deriveInternalGisConnection(catalog) + + return ( + + + + + Open these datasets in desktop GIS + + + Load every published collection at once with the connections file, + or download a single styled layer from a dataset below. + + + + + QGIS + {qgisConnections ? ( + + ) : null} + + In QGIS: Browser panel → right-click{' '} + WFS / OGC API - Features →{' '} + Load Connections, then pick the downloaded file. + + {canViewInternal && internalConnections ? ( + + ) : null} + + + + ArcGIS Pro + + ArcGIS Pro has no importable connection file. Add the server once + via{' '} + Insert → Connections → Server → New OGC API Server{' '} + and paste this service URL: + + + + + + ) +} diff --git a/src/components/Hydrographs/HydrographUiModeToggle.tsx b/src/components/Hydrographs/HydrographUiModeToggle.tsx new file mode 100644 index 00000000..f6660ecc --- /dev/null +++ b/src/components/Hydrographs/HydrographUiModeToggle.tsx @@ -0,0 +1,56 @@ +import { + Stack, + ToggleButton, + ToggleButtonGroup, + Tooltip, + Typography, +} from '@mui/material' +import { + HYDROGRAPH_UI_MODES, + HYDROGRAPH_UI_MODE_DESCRIPTIONS, + HYDROGRAPH_UI_MODE_LABELS, + isHydrographUiModeEnabled, + type HydrographUiMode, +} from './hydrographUiMode' + +/** + * PrusaSlicer-style mode selector: a single segmented control that governs + * how much of the corrector is exposed. + */ +export const HydrographUiModeToggle = ({ + mode, + onChange, +}: { + mode: HydrographUiMode + onChange: (mode: HydrographUiMode) => void +}) => ( + + { + // Exclusive groups emit null when the active button is re-clicked; + // a mode must always be set, so ignore that. + if (value) onChange(value) + }} + > + {HYDROGRAPH_UI_MODES.map((value) => ( + + + {HYDROGRAPH_UI_MODE_LABELS[value]} + + + ))} + + + {HYDROGRAPH_UI_MODE_DESCRIPTIONS[mode]} + + +) diff --git a/src/components/Hydrographs/OcotilloHydrographCorrectionWorkbench.tsx b/src/components/Hydrographs/OcotilloHydrographCorrectionWorkbench.tsx index 50f2f72e..cc92e021 100644 --- a/src/components/Hydrographs/OcotilloHydrographCorrectionWorkbench.tsx +++ b/src/components/Hydrographs/OcotilloHydrographCorrectionWorkbench.tsx @@ -1,38 +1,86 @@ -import { useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import ReactECharts from 'echarts-for-react' import { + Accordion, + AccordionDetails, + AccordionSummary, Alert, - Autocomplete, Box, Button, - Card, - CardContent, - CardHeader, + Checkbox, Chip, + Dialog, + DialogActions, + DialogContent, + DialogTitle, Divider, + FormControlLabel, + IconButton, + MenuItem, Paper, Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, TextField, + Tooltip, Typography, useTheme, } from '@mui/material' -import Grid from '@mui/material/Grid2' -import { Build, Publish, Refresh, Straighten } from '@mui/icons-material' +import { DataGrid, type GridColDef } from '@mui/x-data-grid' +import { DateTimePicker } from '@mui/x-date-pickers' +import dayjs, { type Dayjs } from 'dayjs' +import { + ChevronLeft, + ChevronRight, + CleaningServices, + Clear, + CloudUpload, + DeleteForever, + ExpandMore, + OpenInNew, + Refresh, + Straighten, + TableRows, +} from '@mui/icons-material' import { applyOffsetToRange, + assessDriftAtManualObservations, buildCsvFromMeasurements, calculateSnapOffset, + convertWaterHeadToDepthToWater, + detectOverpressureClipping, + interpolateSpuriousReflections, normalizePointId, - parseHydrographUpload, - parseHydrographWorkbookUpload, + parseObservationTimestamp, + removeOffsetsAndZeros, + removeSpuriousReflections, + type ReflectionDetectionMethod, type HydrographPoint, type HydrographRange, type ParsedHydrographUpload, } from './hydrographCorrection' +import { + DEFAULT_HYDROGRAPH_UI_MODE, + isAtLeastMode, + type HydrographUiMode, +} from './hydrographUiMode' +import { + formatCollector, + type ManualObservationFieldMetadata, +} from '@/utils/manualObservationFieldMetadata' interface ManualHydrographObservation { observation_datetime: string | Date depth_to_water_bgs: number + /** + * Field-event provenance for this reading, joined on by the page. Optional + * because demo mode has no Ocotillo records to join against. + */ + fieldMetadata?: ManualObservationFieldMetadata | null } interface TransducerHydrographObservation { @@ -41,80 +89,722 @@ interface TransducerHydrographObservation { } interface ManualOption { + /** Position in the manual series — also the chart's `dataIndex`. */ + index: number label: string point: HydrographPoint + fieldMetadata: ManualObservationFieldMetadata | null +} + +/** Outcome of a stored-data deletion, surfaced back in the workbench. */ +export interface HydrographDeleteResult { + deletedCount: number +} + +// Everything the page needs to build the upload-contract payload +// (docs/hydrograph-correction-upload-contract.md): the corrected series +// plus provenance gathered from the session. +export interface HydrographPublishArgs { + measurements: HydrographPoint[] + corrections: string[] + sourceFileName: string | null + sourceKind: 'depth_to_water' | 'water_head' +} + +// Well and sensor context for the metadata pane. The page maps Ocotillo +// records onto this shape so the workbench stays decoupled from the API +// types and still renders in demo mode, where none of it exists. +export interface HydrographWellMetadata { + /** Link to the well details page; omitted when no well is bound. */ + href?: string | null + siteName?: string | null + wellStatus?: string | null + wellDepth?: number | null + wellDepthUnit?: string | null + casingDepth?: number | null + casingDepthUnit?: string | null + measuringPointHeight?: number | null + measuringPointHeightUnit?: string | null +} + +export interface HydrographSensorDeployment { + id: string | number + name?: string | null + model?: string | null + serialNo?: string | null + installedAt?: string | null + removedAt?: string | null + hangingCableLength?: number | null + recordingInterval?: string | null +} + +interface MetadataFact { + label: string + value: string +} + +const EMPTY_SENSOR_DEPLOYMENTS: readonly HydrographSensorDeployment[] = [] + +// Bounds for the draggable controls column. +const DEFAULT_CONTROLS_WIDTH = 340 +const MIN_CONTROLS_WIDTH = 240 +const MAX_CONTROLS_WIDTH = 640 +const MIN_CHART_WIDTH = 360 +const SPLIT_HANDLE_WIDTH = 28 + +// Stacked chart panels, in pixels. Every panel rides the same time axis, so +// the layout is computed here rather than left to per-panel percentages. +// The toolbox owns the top row and the legend a right gutter, so the two +// never compete for the same space. Grids stop short of the gutter. +const CHART_TOOLBAR_HEIGHT = 44 +const CHART_LEGEND_GUTTER = 210 +const CHART_AXIS_FOOTER_HEIGHT = 76 +const CHART_PANEL_GAP = 16 +const CHART_PANEL_HEIGHTS = { + head: 120, + dtw: 420, + residual: 150, +} as const + +type ChartPanel = keyof typeof CHART_PANEL_HEIGHTS + +// Every panel is always present in the option, at a fixed index; the ones the +// current upload does not use collapse to zero height and hide their axes. +// +// This is what lets the chart be updated by merge instead of `notMerge`. +// ECharts matches components and series across a `setOption` by array +// position, so an option whose arrays change length silently rebinds series to +// the wrong grid — which is why the chart used to be rebuilt from scratch on +// every edit, discarding the zoom window and the brushed selection with it. +// Fixed positions make merge safe, and merge preserves both natively. +const CHART_PANEL_ORDER = ['head', 'dtw', 'residual'] as const +const GRID_INDEX: Record = { head: 0, dtw: 1, residual: 2 } + +const RESIDUAL_STORED_SERIES = 'Residual: stored − manual' +const RESIDUAL_CORRECTED_SERIES = 'Residual: corrected − manual' +const RESIDUAL_SERIES_NAMES = [ + RESIDUAL_STORED_SERIES, + RESIDUAL_CORRECTED_SERIES, +] + +// Speech bubble, for the toolbox button that turns the hover popup on/off. +const TOOLTIP_TOGGLE_ICON = + 'path://M4,3 L28,3 Q30,3 30,5 L30,19 Q30,21 28,21 L14,21 L8,27 L8,21 L4,21 Q2,21 2,19 L2,5 Q2,3 4,3 Z' + +/** The subset of an ECharts tooltip callback param this chart formats. */ +interface TooltipParam { + seriesName?: string + marker?: string + axisValueLabel?: string + value?: [unknown, number | null | undefined] +} + +const MINUTE_MS = 60 * 1000 +const HOUR_MS = 60 * MINUTE_MS +const DAY_MS = 24 * HOUR_MS +const MONTH_MS = 30.4375 * DAY_MS +const YEAR_MS = 365.25 * DAY_MS + +// Candidate tick spacings, coarsest last. +const TIME_TICK_STEPS = [ + MINUTE_MS, + 5 * MINUTE_MS, + 15 * MINUTE_MS, + 30 * MINUTE_MS, + HOUR_MS, + 3 * HOUR_MS, + 6 * HOUR_MS, + 12 * HOUR_MS, + DAY_MS, + 2 * DAY_MS, + 7 * DAY_MS, + 14 * DAY_MS, + MONTH_MS, + 3 * MONTH_MS, + 6 * MONTH_MS, + YEAR_MS, + 2 * YEAR_MS, + 5 * YEAR_MS, + 10 * YEAR_MS, + 20 * YEAR_MS, + 50 * YEAR_MS, + 100 * YEAR_MS, +] + +/** + * Pick one tick spacing for every stacked panel. + * + * Left to itself ECharts derives an interval per axis, and the inputs differ + * between panels — only the bottom axis draws labels, and only the residual + * panel draws bars. That produced a different interval per panel, so the + * vertical grid lines did not line up. Deriving the interval once and setting + * it on every axis makes the panels agree by construction. + */ +const chooseSharedTimeTick = (spanMs: number, targetTicks = 6) => { + if (!Number.isFinite(spanMs) || spanMs <= 0) return null + const rough = spanMs / targetTicks + return ( + TIME_TICK_STEPS.find((step) => step >= rough) ?? + TIME_TICK_STEPS[TIME_TICK_STEPS.length - 1] + ) +} + +/** + * Residual bars, drawn as zero-anchored line segments rather than a bar + * series. A real bar series pads its own axis by half a bar band at each end + * — even with an explicit extent and `boundaryGap` — which stretched the + * residual panel's time range relative to the panels above and pushed its + * grid lines out of line. Line segments leave the axis untouched. + */ +const residualBarSeries = ( + entries: readonly { anchorTime: Date; misfit: number }[], + color: string, + gridIndex: number +) => ({ + type: 'line', + showSymbol: false, + connectNulls: false, + xAxisIndex: gridIndex, + yAxisIndex: gridIndex, + lineStyle: { color, width: 5 }, + itemStyle: { color }, + data: entries.flatMap((entry) => [ + [entry.anchorTime, 0], + [entry.anchorTime, entry.misfit], + [entry.anchorTime, null], + ]), +}) + +/** Snap the axis start onto a round boundary so labels stay readable. */ +const snapTimeDomainStart = (startMs: number, intervalMs: number) => { + const start = new Date(startMs) + + if (intervalMs >= YEAR_MS) { + const years = Math.max(1, Math.round(intervalMs / YEAR_MS)) + const year = Math.floor(start.getFullYear() / years) * years + return new Date(year, 0, 1).getTime() + } + + if (intervalMs >= MONTH_MS) { + const months = Math.max(1, Math.round(intervalMs / MONTH_MS)) + const month = Math.floor(start.getMonth() / months) * months + return new Date(start.getFullYear(), month, 1).getTime() + } + + return Math.floor(startMs / intervalMs) * intervalMs +} + +const buildWellFacts = ( + metadata: HydrographWellMetadata | null | undefined +): MetadataFact[] => { + if (!metadata) return [] + + return ( + [ + ['Well depth', formatMeasure(metadata.wellDepth, metadata.wellDepthUnit)], + [ + 'Casing depth', + formatMeasure(metadata.casingDepth, metadata.casingDepthUnit), + ], + [ + 'Measuring point height', + formatMeasure( + metadata.measuringPointHeight, + metadata.measuringPointHeightUnit + ), + ], + ] as const + ) + .filter(([, value]) => value !== null) + .map(([label, value]) => ({ label, value: value as string })) +} + +// The pane lives in the narrow control column, so deployments are packed +// into a compact table rather than a label/value block per deployment. +const SensorDeploymentTable = ({ + deployments, +}: { + deployments: readonly HydrographSensorDeployment[] +}) => ( + + + + + Sensor + S/N + Installed + Removed + Cable + Interval + + + + {deployments.map((deployment) => ( + + + {[deployment.name || 'Unnamed', deployment.model] + .filter(Boolean) + .join(' · ')} + + {deployment.serialNo || '—'} + {formatDate(deployment.installedAt) ?? '—'} + + {formatDate(deployment.removedAt) ?? 'Deployed'} + + + {formatMeasure(deployment.hangingCableLength, 'ft') ?? '—'} + + {deployment.recordingInterval || '—'} + + ))} + +
+
+) + +const formatMeasure = ( + value: number | null | undefined, + unit: string | null | undefined +) => + value === null || value === undefined || !Number.isFinite(value) + ? null + : `${value}${unit ? ` ${unit}` : ''}` + +const formatDate = (value: string | null | undefined) => { + if (!value) return null + const parsed = new Date(value) + return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleDateString() +} + +const MetadataFacts = ({ facts }: { facts: readonly MetadataFact[] }) => ( + + {facts.map((fact) => ( + + + {fact.label} + + {fact.value} + + ))} + +) + +/** Field-event detail for the row tooltip; null when nothing is recorded. */ +const fieldEventTooltip = ( + metadata: ManualObservationFieldMetadata | null | undefined +) => { + if (!metadata) return null + + const lines = [ + ['Collected by', formatCollector(metadata)], + ['Method', metadata.measurementMethod], + ['Field event', formatDate(metadata.fieldEventDate)], + ['Notes', metadata.notes], + ].filter(([, value]) => value) as [string, string][] + + if (lines.length === 0) return null + + return ( + + {lines.map(([label, value]) => ( + + {label}: {value} + + ))} + + ) +} + +/** + * Manual observations as a selectable table, replacing the dropdown that + * used to feed the snap target. The dropdown collapsed every measurement to + * one line of text and hid the rest, so comparing candidate anchors meant + * reopening it repeatedly; the table keeps the whole series visible and the + * selected anchor marked while the snap is applied. + */ +const ManualMeasurementTable = ({ + options, + selected, + onSelect, +}: { + options: readonly ManualOption[] + selected: ManualOption | null + onSelect: (option: ManualOption | null) => void +}) => { + const selectedRowRef = useRef(null) + const selectedIndex = selected?.index + + // Newest first: the most recent hand measurement is the usual snap anchor, + // so it should not be at the bottom of a decade-long scroll. Only the + // display order flips — `options` stays in chart order, since each entry's + // `index` is the series `dataIndex` the chart selects by. + const rowsNewestFirst = useMemo(() => [...options].reverse(), [options]) + + // Clicking a manual point on the chart selects a row that may sit outside + // the table's scroll viewport, so the selection pulls itself into view. + useEffect(() => { + if (selectedIndex === undefined) return + selectedRowRef.current?.scrollIntoView({ block: 'nearest' }) + }, [selectedIndex]) + + if (options.length === 0) { + return ( + + No manual observations are available for this well. + + ) + } + + return ( + + + + + Measured + DTW (ft bgs) + Collected by + + + + {rowsNewestFirst.map((option) => { + const isSelected = selected?.index === option.index + // Re-clicking the snap target clears it, so the table is also the + // way to undo a selection made by clicking the chart. + const toggle = () => onSelect(isSelected ? null : option) + + const row = ( + { + if (event.key !== 'Enter' && event.key !== ' ') return + event.preventDefault() + toggle() + }} + sx={{ cursor: 'pointer' }} + > + {option.point.time.toLocaleString()} + + {option.point.value.toFixed(2)} + + {/* Name only — the control column is narrow, and the + organization would push it into a horizontal scroll. The + row tooltip carries the qualified form. */} + {option.fieldMetadata?.collectedBy ?? '—'} + + ) + + // The rest of the field event — method, visit date, notes — is + // what decides whether an anchor is trustworthy, but it does not + // fit the control column, so it hangs off the row. + const details = fieldEventTooltip(option.fieldMetadata) + return details ? ( + + {row} + + ) : ( + row + ) + })} + +
+
+ ) } +const WorkbenchSection = ({ + title, + defaultExpanded = false, + children, +}: { + title: string + defaultExpanded?: boolean + children: React.ReactNode +}) => ( + + } sx={{ px: 1.5 }}> + + {title} + + + + {children} + + +) + export const OcotilloHydrographCorrectionWorkbench = ({ thingName, manualObservations, transducerObservations, initialUpload, initialFileName, - onUploadParsed, + onPublish, + onDeleteStoredRange, + mode = DEFAULT_HYDROGRAPH_UI_MODE, + wellMetadata, + sensorDeployments = EMPTY_SENSOR_DEPLOYMENTS, }: { thingName: string manualObservations: readonly ManualHydrographObservation[] transducerObservations: readonly TransducerHydrographObservation[] initialUpload?: ParsedHydrographUpload | null initialFileName?: string | null - onUploadParsed?: ( - parsed: ParsedHydrographUpload, - fileName: string | null - ) => void + onPublish?: (args: HydrographPublishArgs) => Promise + /** + * Permanently deletes the stored transducer observations inside the range. + * Omitted when the session has no bound well, or when the signed-in user + * lacks delete permission — the destructive pane is hidden in both cases. + */ + onDeleteStoredRange?: ( + range: HydrographRange + ) => Promise + mode?: HydrographUiMode + wellMetadata?: HydrographWellMetadata | null + sensorDeployments?: readonly HydrographSensorDeployment[] }) => { const theme = useTheme() const chartRef = useRef(null) - const fileInputRef = useRef(null) + const chartContainerRef = useRef(null) + const splitRef = useRef(null) + const resizeStateRef = useRef<{ startX: number; startWidth: number } | null>( + null + ) + const [controlsWidth, setControlsWidth] = useState(DEFAULT_CONTROLS_WIDTH) + const [controlsCollapsed, setControlsCollapsed] = useState(false) + const [showTooltip, setShowTooltip] = useState(true) const [uploaded, setUploaded] = useState( initialUpload ?? null ) + // Populated by the effect below once the upload is derived (water-head + // uploads are converted first, so raw parsed values are never charted). const [rawUploadedMeasurements, setRawUploadedMeasurements] = useState< HydrographPoint[] - >(initialUpload?.measurements ?? []) + >([]) const [correctedMeasurements, setCorrectedMeasurements] = useState< HydrographPoint[] - >(initialUpload?.measurements ?? []) + >([]) const [selectedRange, setSelectedRange] = useState( null ) + // Mirror of the brushed selection, so the chart event handlers can compare + // against it without closing over changing state. + const selectedRangeRef = useRef(null) const [selectedManualOption, setSelectedManualOption] = useState(null) const [shiftAmount, setShiftAmount] = useState(0.1) + const [cleanThreshold, setCleanThreshold] = useState(0.25) + const [reflectionThreshold, setReflectionThreshold] = useState(0.25) + const [interpolateReflections, setInterpolateReflections] = useState(false) + const [reflectionMethod, setReflectionMethod] = + useState('median') + const [useTemperatureAssist, setUseTemperatureAssist] = useState(false) + // Audit trail of applied operations, in order — becomes the provenance + // corrections list in the upload-contract payload. + const [correctionLog, setCorrectionLog] = useState([]) + const [isPublishing, setIsPublishing] = useState(false) + const [correctDrift, setCorrectDrift] = useState(false) const [error, setError] = useState(null) + const [qualityWarnings, setQualityWarnings] = useState([]) const [fileName, setFileName] = useState(initialFileName ?? null) + // Stored-data deletion. The bounds are held separately from the brushed + // chart selection: the brush scopes correction edits, and reusing it for a + // destructive action would let an accidental drag arm a delete. + const [deleteStart, setDeleteStart] = useState(null) + const [deleteEnd, setDeleteEnd] = useState(null) + const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false) + const [deleteConfirmText, setDeleteConfirmText] = useState('') + const [isDeleting, setIsDeleting] = useState(false) + const [deleteError, setDeleteError] = useState(null) + const [deleteSuccess, setDeleteSuccess] = useState(null) + + // Progressive disclosure. Simple mode is the pressure-transducer workflow + // only, so the acoustic-logger tooling (reflections) and its tuning knobs + // are gated behind the higher modes. + const showReflectionTools = isAtLeastMode(mode, 'intermediate') + const showThresholdFields = isAtLeastMode(mode, 'intermediate') + const showDataTable = isAtLeastMode(mode, 'intermediate') + const showReflectionTuning = isAtLeastMode(mode, 'advanced') + + const wellFacts = useMemo(() => buildWellFacts(wellMetadata), [wellMetadata]) + + const clampControlsWidth = useCallback((width: number) => { + // Never let the controls squeeze the chart below a usable width. + const available = splitRef.current?.clientWidth + const max = available + ? Math.max( + MIN_CONTROLS_WIDTH, + available - MIN_CHART_WIDTH - SPLIT_HANDLE_WIDTH + ) + : MAX_CONTROLS_WIDTH + return Math.min(Math.max(width, MIN_CONTROLS_WIDTH), max) + }, []) + + const startControlsResize = (event: React.MouseEvent) => { + if (controlsCollapsed) return + event.preventDefault() + resizeStateRef.current = { + startX: event.clientX, + startWidth: controlsWidth, + } + // Dragging over the chart would otherwise select its labels. + document.body.style.userSelect = 'none' + } + + const handleSeparatorKeyDown = (event: React.KeyboardEvent) => { + if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return + event.preventDefault() + const step = event.key === 'ArrowLeft' ? -24 : 24 + setControlsWidth((current) => clampControlsWidth(current + step)) + } + useEffect(() => { - setUploaded(initialUpload ?? null) - setRawUploadedMeasurements(initialUpload?.measurements ?? []) - setCorrectedMeasurements(initialUpload?.measurements ?? []) - setFileName(initialFileName ?? null) - setSelectedRange(null) - setSelectedManualOption(null) - setError(null) - }, [initialFileName, initialUpload]) + const handleMove = (event: MouseEvent) => { + const drag = resizeStateRef.current + if (!drag) return + setControlsWidth( + clampControlsWidth(drag.startWidth + event.clientX - drag.startX) + ) + } + const stopDrag = () => { + if (!resizeStateRef.current) return + resizeStateRef.current = null + document.body.style.userSelect = '' + } - const manualPoints = useMemo( + window.addEventListener('mousemove', handleMove) + window.addEventListener('mouseup', stopDrag) + return () => { + window.removeEventListener('mousemove', handleMove) + window.removeEventListener('mouseup', stopDrag) + stopDrag() + } + }, [clampControlsWidth]) + + // The instance has to be told when the split changes its container width. + // echarts-for-react's own auto-resize deliberately swallows the first + // resize it observes, which is exactly the 0 -> real width transition when + // the chart mounts before the flex row has been measured — that left the + // canvas stuck at zero width. Resizing on a frame after mount covers it. + useEffect(() => { + const element = chartContainerRef.current + if (!element) return + + // echarts-for-react initialises the instance with explicit pixel + // dimensions, so a bare resize() would re-apply the size captured at + // mount. 'auto' makes it re-measure the container. + const resizeChart = () => + chartRef.current + ?.getEchartsInstance() + ?.resize({ width: 'auto', height: 'auto' }) + const frame = requestAnimationFrame(resizeChart) + + if (typeof ResizeObserver === 'undefined') { + return () => cancelAnimationFrame(frame) + } + + // Panel changes resize the container too, so the observer covers those. + const observer = new ResizeObserver(resizeChart) + observer.observe(element) + return () => { + cancelAnimationFrame(frame) + observer.disconnect() + } + }, []) + + // Dropping to a mode that hides a toggle must also disable it, otherwise a + // hidden option would keep silently changing what the buttons do. + useEffect(() => { + if (!showReflectionTuning) { + setReflectionMethod('median') + setUseTemperatureAssist(false) + setInterpolateReflections(false) + } + }, [showReflectionTuning]) + + // Points and their field-event provenance are kept together through the + // parse/filter/sort so the metadata cannot drift out of step with the + // series the chart and the snap table index into. + const manualEntries = useMemo( () => manualObservations .map((observation) => ({ - time: new Date(observation.observation_datetime), - value: Number(observation.depth_to_water_bgs), + point: { + time: parseObservationTimestamp(observation.observation_datetime), + value: Number(observation.depth_to_water_bgs), + }, + fieldMetadata: observation.fieldMetadata ?? null, })) .filter( - (point) => - !Number.isNaN(point.time.getTime()) && Number.isFinite(point.value) + (entry) => + !Number.isNaN(entry.point.time.getTime()) && + Number.isFinite(entry.point.value) ) - .sort((a, b) => a.time.getTime() - b.time.getTime()), + .sort((a, b) => a.point.time.getTime() - b.point.time.getTime()), [manualObservations] ) + const manualPoints = useMemo( + () => manualEntries.map((entry) => entry.point), + [manualEntries] + ) + const storedTransducerPoints = useMemo( () => transducerObservations .map((observation) => ({ - time: new Date(observation.observation_datetime), + time: parseObservationTimestamp(observation.observation_datetime), value: Number(observation.value), })) .filter( @@ -127,84 +817,465 @@ export const OcotilloHydrographCorrectionWorkbench = ({ const manualOptions = useMemo( () => - manualPoints.map((point, index) => ({ + manualEntries.map(({ point, fieldMetadata }, index) => ({ + index, label: `${point.time.toLocaleString()} · ${point.value.toFixed(2)} ft`, point, + fieldMetadata, })), - [manualPoints] + [manualEntries] + ) + + // Both bounds must be set explicitly — there is deliberately no "delete + // everything" default, and an inverted or unparsable range resolves to null + // rather than being silently reordered. + const deleteRange = useMemo(() => { + if (!deleteStart?.isValid() || !deleteEnd?.isValid()) return null + const startTime = deleteStart.toDate() + const endTime = deleteEnd.toDate() + return endTime.getTime() > startTime.getTime() + ? { startTime, endTime } + : null + }, [deleteEnd, deleteStart]) + + const deleteRangeIsInverted = + Boolean(deleteStart?.isValid() && deleteEnd?.isValid()) && !deleteRange + + // Exactly what the request would remove, computed from the same stored + // series the chart draws, so the confirmation count is verifiable on screen. + const doomedStoredPoints = useMemo(() => { + if (!deleteRange) return [] + const start = deleteRange.startTime.getTime() + const end = deleteRange.endTime.getTime() + return storedTransducerPoints.filter((point) => { + const time = point.time.getTime() + return time >= start && time <= end + }) + }, [deleteRange, storedTransducerPoints]) + + const deletesEveryStoredPoint = + storedTransducerPoints.length > 0 && + doomedStoredPoints.length === storedTransducerPoints.length + + // Typed confirmation. The well name is used rather than a generic word so + // the phrase cannot be muscle-memoried across wells. + const deleteConfirmPhrase = thingName || 'DELETE' + const deleteConfirmMatches = + deleteConfirmText.trim() === deleteConfirmPhrase.trim() + + // Residuals against the manual measurements: at each manual observation, + // the nearest transducer reading minus the manual value. Positive means the + // transducer reads deeper than the hand measurement. This is the misfit the + // shift and snap tools drive toward zero. + const residuals = useMemo(() => { + const corrected = assessDriftAtManualObservations( + correctedMeasurements, + manualPoints + ) + const stored = assessDriftAtManualObservations( + storedTransducerPoints, + manualPoints + ) + return { corrected, stored } + }, [correctedMeasurements, manualPoints, storedTransducerPoints]) + + const hasResiduals = + residuals.corrected.length > 0 || residuals.stored.length > 0 + + // Water-head uploads (Diver Office pressure transducers) are converted to + // depth to water using the manual observations as sensor-depth anchors, + // mirroring wellpy. Depth-to-water uploads pass through unchanged. + const deriveWorkingMeasurements = useCallback( + (upload: ParsedHydrographUpload) => + upload.valueKind === 'water_head' + ? convertWaterHeadToDepthToWater({ + measurements: upload.measurements, + manualPoints, + correctDrift, + }) + : upload.measurements, + [correctDrift, manualPoints] ) + const baselineCorrectionLog = useCallback( + (upload: ParsedHydrographUpload | null) => + upload?.valueKind === 'water_head' + ? [`convert_water_head${correctDrift ? ' (drift corrected)' : ''}`] + : [], + [correctDrift] + ) + + const selectedRangeSuffix = () => + selectedRange + ? `, ${selectedRange.startTime.toISOString()} to ${selectedRange.endTime.toISOString()}` + : '' + + useEffect(() => { + setUploaded(initialUpload ?? null) + setFileName(initialFileName ?? null) + // Written inline rather than through applySelectedRange so this effect + // does not take a dependency that changes every render. + selectedRangeRef.current = null + setSelectedRange(null) + setSelectedManualOption(null) + // Merge keeps the zoom window across every other option change, which is + // the point of it. A new upload is the one case that has to opt out: it + // spans a different period, so the previous window would drop the user + // somewhere arbitrary in the new trace. + chartRef.current + ?.getEchartsInstance() + ?.dispatchAction({ type: 'dataZoom', start: 0, end: 100 }) + + if (!initialUpload) { + setRawUploadedMeasurements([]) + setCorrectedMeasurements([]) + setQualityWarnings([]) + setError(null) + return + } + + try { + const working = deriveWorkingMeasurements(initialUpload) + setRawUploadedMeasurements(working) + setCorrectedMeasurements(working) + setCorrectionLog(baselineCorrectionLog(initialUpload)) + setError(null) + + // Methodology QC checks on water-head uploads: drift misfit at the + // manual measurements, and overpressurization clipping in raw head. + const warnings: string[] = [] + if (initialUpload.valueKind === 'water_head') { + assessDriftAtManualObservations(working, manualPoints) + .filter((assessment) => Math.abs(assessment.misfit) > 0.1) + .forEach((assessment) => { + warnings.push( + `Drift check: the converted series misses the manual measurement on ${assessment.anchorTime.toLocaleString()} by ${assessment.misfit > 0 ? '+' : ''}${assessment.misfit.toFixed(2)} ft — possible logger drift; per the methodology, review before publishing.` + ) + }) + + const clipping = detectOverpressureClipping(initialUpload.measurements) + if (clipping) { + warnings.push( + `Water head plateaus at its maximum (${clipping.value.toFixed(2)} ft) for ${clipping.count} consecutive readings (${clipping.start.toLocaleString()} to ${clipping.end.toLocaleString()}) — the Diver may have been overpressurized past its range; readings in that span are clipped.` + ) + } + } + setQualityWarnings(warnings) + } catch (deriveError) { + setRawUploadedMeasurements([]) + setCorrectedMeasurements([]) + setCorrectionLog([]) + setQualityWarnings([]) + setError( + deriveError instanceof Error + ? deriveError.message + : 'Unable to prepare the uploaded data.' + ) + } + }, [ + baselineCorrectionLog, + deriveWorkingMeasurements, + initialFileName, + initialUpload, + ]) + const parsedPointId = uploaded?.pointId ?? null const highlightedManualPoint = selectedManualOption?.point ?? null + // Charts pull series colors and text styles from the MUI theme so they + // match the rest of the app and adapt to dark mode. + const chartTextStyles = useMemo( + () => ({ + legend: { + // Stacked down the right gutter, clear of the toolbox row above. + // Scrolling caps it at the gutter height rather than letting a long + // series list push into the panels. + type: 'scroll', + orient: 'vertical', + top: CHART_TOOLBAR_HEIGHT, + right: 8, + width: CHART_LEGEND_GUTTER - 24, + textStyle: { color: theme.palette.text.primary }, + pageTextStyle: { color: theme.palette.text.secondary }, + pageIconColor: theme.palette.text.secondary, + pageIconInactiveColor: theme.palette.action.disabled, + }, + xAxis: { + axisLabel: { color: theme.palette.text.secondary }, + splitLine: { show: true, lineStyle: { color: theme.palette.divider } }, + }, + yAxis: { + axisLabel: { color: theme.palette.text.secondary }, + nameTextStyle: { + color: theme.palette.text.secondary, + padding: [0, 0, 0, 6], + }, + splitLine: { lineStyle: { color: theme.palette.divider } }, + }, + tooltip: { + backgroundColor: theme.palette.background.paper, + textStyle: { color: theme.palette.text.primary }, + }, + }), + [theme] + ) + + // Diver uploads get a raw water-head panel (mirroring wellpy's stacked head + // plot) since head and DTW share neither units nor axis orientation. It is + // stacked into the same chart instance as a second grid so both panels ride + // one shared time axis — zoom, pan, brush and the tooltip crosshair stay in + // lockstep across them. + const headPoints = uploaded?.valueKind === 'water_head' ? uploaded.measurements : null + + // Which of the three panels this upload actually uses. They keep their + // fixed positions either way; the unused ones just collapse. + const visiblePanels = useMemo>( + () => ({ head: Boolean(headPoints), dtw: true, residual: hasResiduals }), + [hasResiduals, headPoints] + ) + + const shownPanels = useMemo( + () => CHART_PANEL_ORDER.filter((panel) => visiblePanels[panel]), + [visiblePanels] + ) + + const chartHeight = useMemo( + () => + CHART_TOOLBAR_HEIGHT + + shownPanels.reduce((total, panel) => total + CHART_PANEL_HEIGHTS[panel], 0) + + CHART_PANEL_GAP * Math.max(0, shownPanels.length - 1) + + CHART_AXIS_FOOTER_HEIGHT, + [shownPanels] + ) + const chartOption = useMemo(() => { + const lastShownIndex = CHART_PANEL_ORDER.reduce( + (last, panel, index) => (visiblePanels[panel] ? index : last), + 0 + ) + + // Fixed length, fixed order, fixed panel binding. A series with nothing to + // draw carries an empty `data` rather than dropping out of the array, so + // merge never rebinds one series' data onto another. + // + // Every series also states its own `itemStyle` colour. Legend markers + // otherwise fall back to the palette entry for the series' position, which + // ties the swatch to how many series happen to be present. const series = [ - manualPoints.length > 0 - ? { - name: 'Manual water levels', - type: 'scatter', - symbolSize: 10, - data: manualPoints.map((point) => [point.time, point.value]), - itemStyle: { color: '#1565C0' }, - } - : null, - highlightedManualPoint - ? { - name: 'Selected manual point', - type: 'scatter', - symbol: 'diamond', - symbolSize: 16, - z: 10, - data: [[highlightedManualPoint.time, highlightedManualPoint.value]], - itemStyle: { - color: '#D32F2F', - borderColor: '#FFFFFF', - borderWidth: 2, - }, - } - : null, - storedTransducerPoints.length > 0 - ? { - name: 'Stored transducer', - type: 'line', - showSymbol: false, - data: storedTransducerPoints.map((point) => [point.time, point.value]), - lineStyle: { color: '#6D4C41', width: 2 }, - } - : null, - rawUploadedMeasurements.length > 0 - ? { - name: 'Uploaded raw', - type: 'line', - showSymbol: false, - data: rawUploadedMeasurements.map((point) => [point.time, point.value]), - lineStyle: { color: '#78909C', width: 1, type: 'dashed' }, - } - : null, - correctedMeasurements.length > 0 - ? { - name: 'Uploaded corrected', - type: 'line', - showSymbol: false, - data: correctedMeasurements.map((point) => [point.time, point.value]), - lineStyle: { color: '#2E7D32', width: 3 }, - } - : null, - ].filter(Boolean) + { + name: 'Raw water head', + type: 'line', + showSymbol: false, + xAxisIndex: GRID_INDEX.head, + yAxisIndex: GRID_INDEX.head, + data: (headPoints ?? []).map((point) => [point.time, point.value]), + lineStyle: { color: theme.palette.info.main, width: 2 }, + itemStyle: { color: theme.palette.info.main }, + }, + { + name: RESIDUAL_STORED_SERIES, + ...residualBarSeries( + residuals.stored, + theme.palette.secondary.main, + GRID_INDEX.residual + ), + }, + { + name: RESIDUAL_CORRECTED_SERIES, + ...residualBarSeries( + residuals.corrected, + theme.palette.success.main, + GRID_INDEX.residual + ), + // Perfect agreement sits on this line. + markLine: { + silent: true, + symbol: 'none', + data: residuals.corrected.length > 0 ? [{ yAxis: 0 }] : [], + lineStyle: { color: theme.palette.text.secondary, type: 'dashed' }, + label: { show: false }, + }, + }, + { + name: 'Manual water levels', + type: 'scatter', + symbolSize: 10, + xAxisIndex: GRID_INDEX.dtw, + yAxisIndex: GRID_INDEX.dtw, + data: manualPoints.map((point) => [point.time, point.value]), + itemStyle: { color: theme.palette.primary.main }, + }, + { + name: 'Selected manual point', + type: 'scatter', + symbol: 'diamond', + symbolSize: 16, + z: 10, + xAxisIndex: GRID_INDEX.dtw, + yAxisIndex: GRID_INDEX.dtw, + data: highlightedManualPoint + ? [[highlightedManualPoint.time, highlightedManualPoint.value]] + : [], + itemStyle: { + color: theme.palette.error.main, + borderColor: theme.palette.background.paper, + borderWidth: 2, + }, + }, + { + name: 'Stored transducer', + type: 'line', + showSymbol: false, + xAxisIndex: GRID_INDEX.dtw, + yAxisIndex: GRID_INDEX.dtw, + data: storedTransducerPoints.map((point) => [point.time, point.value]), + lineStyle: { color: theme.palette.secondary.main, width: 2 }, + itemStyle: { color: theme.palette.secondary.main }, + // Shade the span a pending deletion would remove, so the range being + // confirmed is visible against the data itself rather than only as two + // timestamps in the form. + markArea: { + silent: true, + itemStyle: { color: theme.palette.error.main, opacity: 0.15 }, + label: { + show: true, + position: 'insideTop', + color: theme.palette.error.main, + formatter: 'Pending deletion', + }, + data: deleteRange + ? [ + [ + { xAxis: deleteRange.startTime }, + { xAxis: deleteRange.endTime }, + ], + ] + : [], + }, + }, + { + name: 'Uploaded raw', + type: 'line', + showSymbol: false, + xAxisIndex: GRID_INDEX.dtw, + yAxisIndex: GRID_INDEX.dtw, + data: rawUploadedMeasurements.map((point) => [point.time, point.value]), + lineStyle: { + color: theme.palette.text.secondary, + width: 1, + type: 'dashed', + }, + itemStyle: { color: theme.palette.text.secondary }, + }, + { + name: 'Uploaded corrected', + type: 'line', + showSymbol: false, + xAxisIndex: GRID_INDEX.dtw, + yAxisIndex: GRID_INDEX.dtw, + data: correctedMeasurements.map((point) => [point.time, point.value]), + lineStyle: { color: theme.palette.success.main, width: 3 }, + itemStyle: { color: theme.palette.success.main }, + }, + ] - return { - animation: false, - legend: { - top: 0, + // Empty series stay in the option but not in the legend, which would + // otherwise list traces the chart is not drawing. + const legendNames = series + .filter((entry) => (entry.data as unknown[]).length > 0) + .map((entry) => entry.name) + + // Every panel must span the same instants, not each series' own extent. + // The head record usually covers one deployment while the DTW panel also + // carries years of stored observations, so without a shared domain the + // head trace would stretch across the full width and imply coverage it + // does not have. + const sharedTimeDomain = + shownPanels.length > 1 + ? [ + headPoints ?? [], + manualPoints, + storedTransducerPoints, + rawUploadedMeasurements, + correctedMeasurements, + ] + .flat() + .reduce<{ min: number; max: number } | null>((domain, point) => { + const time = point.time.getTime() + if (!domain) return { min: time, max: time } + return { + min: Math.min(domain.min, time), + max: Math.max(domain.max, time), + } + }, null) + : null + + const sharedTick = sharedTimeDomain + ? chooseSharedTimeTick(sharedTimeDomain.max - sharedTimeDomain.min) + : null + + const sharedExtent = sharedTimeDomain + ? { + min: sharedTick + ? snapTimeDomainStart(sharedTimeDomain.min, sharedTick) + : sharedTimeDomain.min, + max: sharedTimeDomain.max, + ...(sharedTick ? { interval: sharedTick } : {}), + } + : {} + + const panelYAxis: Record> = { + head: { + scale: true, + name: 'Water Head (ft)', }, - grid: { - left: 100, - right: 32, - top: 56, - bottom: 84, + dtw: { + inverse: true, + scale: true, + name: 'Depth To Water Below Ground Surface (ft)', + }, + residual: { + scale: true, + name: 'Residual (ft)', }, + } + + // Stack the panels top to bottom, each one a fixed height with a small + // gap, so the whole column lines up on the shared axis at the bottom. A + // panel this upload does not use keeps its slot at zero height, taking no + // space and no gap. + let panelTop = CHART_TOOLBAR_HEIGHT + const grids = CHART_PANEL_ORDER.map((panel) => { + const height = visiblePanels[panel] ? CHART_PANEL_HEIGHTS[panel] : 0 + const grid = { + left: 100, + right: CHART_LEGEND_GUTTER, + top: panelTop, + height, + } + if (height > 0) panelTop += height + CHART_PANEL_GAP + return grid + }) + + return { + animation: false, + legend: { ...chartTextStyles.legend, data: legendNames }, + grid: grids, toolbox: { + top: 0, + right: 8, feature: { + myToggleTooltip: { + show: true, + title: showTooltip ? 'Hide hover popup' : 'Show hover popup', + icon: TOOLTIP_TOGGLE_ICON, + iconStyle: { + borderColor: showTooltip + ? theme.palette.primary.main + : theme.palette.text.secondary, + }, + onclick: () => setShowTooltip((current) => !current), + }, dataZoom: [{ show: true }, { type: 'inside' }], restore: {}, brush: { type: ['lineX', 'clear'] }, @@ -212,70 +1283,199 @@ export const OcotilloHydrographCorrectionWorkbench = ({ }, }, tooltip: { + show: showTooltip, trigger: 'axis', axisPointer: { type: 'cross' }, - backgroundColor: theme.palette.background.paper, + // Residuals are read off their own panel; listing them here just + // padded every popup, two rows per series for the zero anchor and + // the value. + formatter: (params: TooltipParam | TooltipParam[]) => { + const entries = (Array.isArray(params) ? params : [params]).filter( + (entry) => !RESIDUAL_SERIES_NAMES.includes(entry.seriesName ?? '') + ) + + const rows = entries + .filter((entry) => entry.value?.[1] !== null && entry.value?.[1] !== undefined) + .map( + (entry) => + `${entry.marker ?? ''}${entry.seriesName}: ${Number( + entry.value?.[1] + ).toFixed(3)}` + ) + + if (rows.length === 0) return '' + return [entries[0]?.axisValueLabel ?? '', ...rows].join('
') + }, + ...chartTextStyles.tooltip, }, + // Crosshair follows the same instant in both panels. + axisPointer: { link: [{ xAxisIndex: 'all' }] }, brush: { xAxisIndex: 'all', brushMode: 'single', outOfBrush: { colorAlpha: 0.25 }, }, + // One zoom range drives every x axis, so the panels cannot drift apart. dataZoom: [ - { type: 'inside', realtime: true }, - { show: true, realtime: true }, + { type: 'inside', realtime: true, xAxisIndex: 'all' }, + { show: true, realtime: true, xAxisIndex: 'all' }, ], - xAxis: { + xAxis: CHART_PANEL_ORDER.map((panel, index) => ({ type: 'time', - splitLine: { show: true }, - }, - yAxis: { + gridIndex: index, + show: visiblePanels[panel], + ...sharedExtent, + ...chartTextStyles.xAxis, + // Only the bottom visible panel carries labels; the ones above would + // just repeat them. + ...(index === lastShownIndex ? {} : { axisLabel: { show: false } }), + })), + yAxis: CHART_PANEL_ORDER.map((panel, index) => ({ type: 'value', - inverse: true, - scale: true, - name: 'Depth To Water Below Ground Surface (ft)', + gridIndex: index, + show: visiblePanels[panel], nameLocation: 'center', nameGap: 74, - nameTextStyle: { - padding: [0, 0, 0, 6], - }, - }, + ...panelYAxis[panel], + ...chartTextStyles.yAxis, + })), series, } }, [ + chartTextStyles, correctedMeasurements, + deleteRange, + headPoints, highlightedManualPoint, manualPoints, rawUploadedMeasurements, + residuals, + shownPanels.length, + showTooltip, storedTransducerPoints, - theme.palette.background.paper, + theme, + visiblePanels, ]) - const handleUpload = async (file?: File) => { - if (!file) return + const tableRows = useMemo(() => { + interface CorrectionTableRow { + id: number + time: Date + waterHead: number | null + rawDtw: number | null + correctedDtw: number | null + manualDtw: number | null + storedTransducer: number | null + correctionNote: string | null + } - try { - const parsed = file.name.toLowerCase().endsWith('.xlsx') - ? parseHydrographWorkbookUpload(await file.arrayBuffer(), file.name) - : parseHydrographUpload(await file.text()) - setUploaded(parsed) - setRawUploadedMeasurements(parsed.measurements) - setCorrectedMeasurements(parsed.measurements) - setSelectedRange(null) - setError(null) - setFileName(file.name) - onUploadParsed?.(parsed, file.name) - } catch (uploadError) { - setUploaded(null) - setRawUploadedMeasurements([]) - setCorrectedMeasurements([]) - setSelectedRange(null) - setError( - uploadError instanceof Error - ? uploadError.message - : 'Unable to parse the uploaded file.' - ) + const rows = new Map() + const rowFor = (time: Date) => { + const key = time.getTime() + let row = rows.get(key) + if (!row) { + row = { + id: key, + time, + waterHead: null, + rawDtw: null, + correctedDtw: null, + manualDtw: null, + storedTransducer: null, + correctionNote: null, + } + rows.set(key, row) + } + return row + } + + if (uploaded?.valueKind === 'water_head') { + uploaded.measurements.forEach((point) => { + rowFor(point.time).waterHead = point.value + }) } + rawUploadedMeasurements.forEach((point) => { + rowFor(point.time).rawDtw = point.value + }) + correctedMeasurements.forEach((point) => { + const row = rowFor(point.time) + row.correctedDtw = point.value + row.correctionNote = point.correctionNote ?? null + }) + manualPoints.forEach((point) => { + rowFor(point.time).manualDtw = point.value + }) + storedTransducerPoints.forEach((point) => { + rowFor(point.time).storedTransducer = point.value + }) + + return [...rows.values()].sort((a, b) => a.id - b.id) + }, [ + correctedMeasurements, + manualPoints, + rawUploadedMeasurements, + storedTransducerPoints, + uploaded, + ]) + + const tableColumns = useMemo(() => { + const numberColumn = (field: string, headerName: string): GridColDef => ({ + field, + headerName, + width: 170, + type: 'number', + valueFormatter: (value: number | null) => + value == null ? '' : value.toFixed(3), + }) + + return [ + { + field: 'time', + headerName: 'Timestamp', + width: 190, + valueFormatter: (value: Date) => value.toLocaleString(), + }, + ...(uploaded?.valueKind === 'water_head' + ? [numberColumn('waterHead', 'Water Head (ft)')] + : []), + numberColumn('rawDtw', 'Raw DTW (ft bgs)'), + numberColumn('correctedDtw', 'Corrected DTW (ft bgs)'), + numberColumn('manualDtw', 'Manual DTW (ft bgs)'), + ...(storedTransducerPoints.length > 0 + ? [numberColumn('storedTransducer', 'Stored Transducer (ft bgs)')] + : []), + ...(correctedMeasurements.some((point) => point.correctionNote) + ? [ + { + field: 'correctionNote', + headerName: 'Correction', + flex: 1, + minWidth: 260, + } satisfies GridColDef, + ] + : []), + ] + }, [ + correctedMeasurements, + storedTransducerPoints.length, + uploaded?.valueKind, + ]) + + const applySelectedRange = (range: HydrographRange | null) => { + // Mirrored into a ref so the handlers below can compare against the + // current selection without being re-created on every change. + selectedRangeRef.current = range + setSelectedRange(range) + } + + // Clearing from outside the chart has to take the brush overlay with it, + // otherwise the shaded band survives on a chart that no longer has a + // selection. + const clearBrushSelection = () => { + applySelectedRange(null) + chartRef.current + ?.getEchartsInstance() + ?.dispatchAction({ type: 'brush', areas: [] }) } const handleBrushSelected = (params: { @@ -285,16 +1485,34 @@ export const OcotilloHydrographCorrectionWorkbench = ({ const coordRange = area?.coordRange if (!coordRange || coordRange.length !== 2) { - setSelectedRange(null) + if (selectedRangeRef.current !== null) applySelectedRange(null) return } - setSelectedRange({ - startTime: new Date(coordRange[0]), - endTime: new Date(coordRange[1]), - }) + const startTime = new Date(coordRange[0]) + const endTime = new Date(coordRange[1]) + const current = selectedRangeRef.current + + // Identical ranges are dropped: ECharts re-emits the selection whenever + // the brush layer redraws, and a fresh object each time would re-render + // the workbench for no change. + if ( + current && + current.startTime.getTime() === startTime.getTime() && + current.endTime.getTime() === endTime.getTime() + ) { + return + } + + applySelectedRange({ startTime, endTime }) + } + + // Restore drops the chart's own brush; the mirrored state has to follow. + const handleChartRestore = () => { + applySelectedRange(null) } + const handleChartClick = (params: { seriesName?: string dataIndex?: number @@ -310,25 +1528,82 @@ export const OcotilloHydrographCorrectionWorkbench = ({ const shiftSelection = (direction: 1 | -1) => { if (correctedMeasurements.length === 0) return + const offset = shiftAmount * direction + setCorrectedMeasurements((current) => + applyOffsetToRange( + current, + offset, + selectedRange, + `shifted ${offset > 0 ? '+' : ''}${offset} ft` + ) + ) + setCorrectionLog((log) => [ + ...log, + `shift (${offset > 0 ? '+' : ''}${offset} ft${selectedRangeSuffix()})`, + ]) + } + + const cleanOffsetsAndZeros = () => { + if (correctedMeasurements.length === 0) return + + setCorrectedMeasurements((current) => + removeOffsetsAndZeros(current, cleanThreshold, selectedRange) + ) + setCorrectionLog((log) => [ + ...log, + `remove_offsets_zeros (threshold ${cleanThreshold}${selectedRangeSuffix()})`, + ]) + } + + const cleanSpuriousReflections = () => { + if (correctedMeasurements.length === 0) return + + const treat = interpolateReflections + ? interpolateSpuriousReflections + : removeSpuriousReflections setCorrectedMeasurements((current) => - applyOffsetToRange(current, shiftAmount * direction, selectedRange) + treat(current, reflectionThreshold, selectedRange, reflectionMethod, { + useTemperature: useTemperatureAssist, + }) ) + setCorrectionLog((log) => [ + ...log, + `${interpolateReflections ? 'interpolate' : 'remove'}_reflections (${reflectionMethod}${useTemperatureAssist ? '+temp' : ''}, threshold ${reflectionThreshold}${selectedRangeSuffix()})`, + ]) } const snapToManual = () => { if (!selectedManualOption || correctedMeasurements.length === 0) return try { - const offset = calculateSnapOffset({ + const { offset, method } = calculateSnapOffset({ measurements: correctedMeasurements, target: selectedManualOption.point, range: selectedRange, }) setCorrectedMeasurements((current) => - applyOffsetToRange(current, offset, selectedRange) + applyOffsetToRange( + current, + offset, + selectedRange, + `snapped ${offset > 0 ? '+' : ''}${offset} ft to manual ${selectedManualOption.point.time.toISOString()}` + ) + ) + // The anchor's collector rides along in the audit trail: a snap is only + // as good as the manual reading it was aligned to. So does how the + // anchor value was obtained — a clamped snap does not put the line + // through the measurement's own time, and a reviewer needs to know. + const collector = formatCollector(selectedManualOption.fieldMetadata) + setCorrectionLog((log) => [ + ...log, + `snap_to_manual (${offset > 0 ? '+' : ''}${offset} ft to ${selectedManualOption.point.time.toISOString()}, ${method === 'interpolated' ? 'interpolated at the measurement time' : 'clamped to the nearest end of the trace'}${collector ? `, collected by ${collector}` : ''}${selectedRangeSuffix()})`, + ]) + setError( + method === 'clamped' + ? 'The manual measurement falls outside the trace being corrected, so the snap used the nearest end of it instead of the value at the measurement time. Widen the selection, or pick a manual inside the uploaded span, for an exact match.' + : null ) - setError(null) } catch (snapError) { setError( snapError instanceof Error @@ -338,12 +1613,92 @@ export const OcotilloHydrographCorrectionWorkbench = ({ } } + // Restore the originally loaded dataset, undoing every correction. For + // water-head uploads, turning drift correction off re-derives the + // baseline conversion through the effect above. const resetCorrections = () => { + setCorrectDrift(false) setCorrectedMeasurements(rawUploadedMeasurements) - setSelectedRange(null) + setCorrectionLog(baselineCorrectionLog(uploaded)) + clearBrushSelection() + setSelectedManualOption(null) setError(null) } + const publishToOcotillo = async () => { + if (!onPublish || correctedMeasurements.length === 0) return + + setIsPublishing(true) + try { + await onPublish({ + measurements: correctedMeasurements, + corrections: correctionLog, + sourceFileName: fileName, + sourceKind: uploaded?.valueKind ?? 'depth_to_water', + }) + } finally { + setIsPublishing(false) + } + } + + // Prefill the delete bounds from the brushed chart selection. Copying is + // explicit rather than automatic so brushing never arms a deletion on its + // own, and the copied bounds stay editable in the pickers. + const useSelectionAsDeleteRange = () => { + if (!selectedRange) return + setDeleteStart(dayjs(selectedRange.startTime)) + setDeleteEnd(dayjs(selectedRange.endTime)) + setDeleteError(null) + setDeleteSuccess(null) + } + + const clearDeleteRange = () => { + setDeleteStart(null) + setDeleteEnd(null) + setDeleteError(null) + setDeleteSuccess(null) + } + + const openDeleteDialog = () => { + if (!deleteRange || doomedStoredPoints.length === 0) return + setDeleteConfirmText('') + setDeleteError(null) + setDeleteSuccess(null) + setIsDeleteDialogOpen(true) + } + + const closeDeleteDialog = () => { + // Never abandon the dialog mid-request — the caller is still writing. + if (isDeleting) return + setIsDeleteDialogOpen(false) + setDeleteConfirmText('') + } + + const confirmDeleteStoredRange = async () => { + if (!onDeleteStoredRange || !deleteRange || !deleteConfirmMatches) return + + setIsDeleting(true) + setDeleteError(null) + try { + const { deletedCount } = await onDeleteStoredRange(deleteRange) + setDeleteSuccess( + `Deleted ${deletedCount} stored transducer observation${deletedCount === 1 ? '' : 's'} between ${deleteRange.startTime.toLocaleString()} and ${deleteRange.endTime.toLocaleString()}.` + ) + setIsDeleteDialogOpen(false) + setDeleteConfirmText('') + setDeleteStart(null) + setDeleteEnd(null) + } catch (error) { + setDeleteError( + error instanceof Error + ? error.message + : 'Deleting the stored transducer data failed.' + ) + } finally { + setIsDeleting(false) + } + } + const downloadCorrectedCsv = () => { if (correctedMeasurements.length === 0) return @@ -358,81 +1713,279 @@ export const OcotilloHydrographCorrectionWorkbench = ({ } return ( - - } - title="Hydrograph Correction Workspace" - subheader="Upload a logger text file, compare it with Ocotillo measurements, and apply local alignment edits." - /> - + + + + + Hydrograph Correction Workspace + + + Upload a logger file, compare it with Ocotillo measurements, and + apply local alignment edits. + + + {/* The brushed range scopes every edit, so it stays in the header + rather than moving into the collapsible detail panes. */} + + {selectedRange ? ( + + ) : ( + + )} + + + + + + + + - handleUpload(event.target.files?.[0])} - /> - {error ? {error} : null} + {qualityWarnings.map((warning) => ( + + {warning} + + ))} - - - {fileName ? : null} - {uploaded ? ( - - ) : null} - {uploaded?.detectedTimeColumn ? ( - - ) : null} - {uploaded?.detectedValueColumn ? ( - - ) : null} - {selectedRange ? ( - setSelectedRange(null)} - /> - ) : ( - - )} - - - - + + - - - - Upload - + + + , + } + : {})} + /> + {wellMetadata?.siteName ? ( + + ) : null} + {wellMetadata?.wellStatus ? ( + + ) : null} + + {wellFacts.length > 0 ? ( + + ) : null} + {sensorDeployments.length > 0 ? ( + <> + + + + ) : ( + + {wellMetadata + ? 'No sensor deployments are recorded for this well.' + : 'No Ocotillo well is bound — well and sensor metadata are unavailable in demo mode.'} + + )} + + + + {uploaded?.valueKind === 'water_head' ? ( + <> + + setCorrectDrift(event.target.checked) + } + /> + } + label="Correct drift" + /> + + Water head is converted to depth to water using + manual observations as sensor-depth anchors. Drift + correction interpolates the sensor depth between + anchors. Recomputing discards manual edits. + + + ) : null} + {showThresholdFields ? ( + + setCleanThreshold(Number(event.target.value)) + } + /> + ) : null} - Supports `.txt`, `.csv`, `.dat`, and the `wellpy` workbook - export format `.xlsx`. + Offsets are sustained level shifts (sensor repositioning + or cable slip): each step is detected from the medians + around it and the trace after it is re-leveled. Zero + readings (sensor out of water) are dropped. - - + {showReflectionTools ? ( + <> + + setReflectionThreshold(Number(event.target.value)) + } + /> + {showReflectionTuning ? ( + <> + + setReflectionMethod( + event.target + .value as ReflectionDetectionMethod + ) + } + > + + Isolated echoes (median window) + + + Dense one-sided (running baseline) + + + + setUseTemperatureAssist( + event.target.checked + ) + } + disabled={ + !correctedMeasurements.some( + (point) => point.temperature !== undefined + ) + } + /> + } + label="Temperature assist" + /> + + setInterpolateReflections( + event.target.checked + ) + } + /> + } + label="Interpolate across removals" + /> + + ) : null} + + + Reflections are acoustic-echo readings offset from + the local trend — positive or negative, near the true + depth (1x) or near twice it (2x). + {showReflectionTuning + ? ' The median method targets isolated echoes and keeps sustained steps; the running baseline method rejects dense one-sided clusters (echoes reading deeper), but treats genuine upward steps as spurious — brush the affected span when the trace has real steps. Temperature assist additionally flags readings that sit marginally above the local trend while their sensor temperature is well above the local norm — echoes correlate with warm readings (requires a temperature column in the upload). Interpolating keeps the sampling cadence, replacing each removed reading with a linear fit between its surviving neighbors.' + : ' Readings offset from the local trend by more than the threshold are dropped. Switch to Advanced for detection methods, temperature assist, and interpolation.'} + + + ) : null} + - - - - Shift - + - - + - - - - Snap + + + Select the manual measurement to snap to. Clicking a + manual point on the chart selects its row; clicking the + selected row again clears it. - - setSelectedManualOption(value) - } - renderInput={(params) => ( - - )} - noOptionsText="No manual observations available for this well" + selected={selectedManualOption} + onSelect={setSelectedManualOption} /> - - + - - - - Output + {/* Only rendered when the caller supplies a delete handler, + i.e. a real well is bound and the user may delete. */} + {onDeleteStoredRange ? ( + + + Permanently deletes stored transducer observations in + Ocotillo. This cannot be undone. + + + Stored transducer observations for this well:{' '} + {storedTransducerPoints.length} + { + setDeleteStart(value) + setDeleteSuccess(null) + }} + disabled={isDeleting} + slotProps={{ textField: { size: 'small' } }} + /> + { + setDeleteEnd(value) + setDeleteSuccess(null) + }} + disabled={isDeleting} + slotProps={{ textField: { size: 'small' } }} + /> + {deleteRangeIsInverted ? ( + + The end of the range must be after its start. + + ) : null} + {deleteRange ? ( + 0 + ? 'text.primary' + : 'text.secondary' + } + > + {doomedStoredPoints.length} stored observation + {doomedStoredPoints.length === 1 ? '' : 's'} fall inside + this range + {deletesEveryStoredPoint + ? ' — that is every stored observation for this well.' + : '.'} + + ) : null} + + {deleteSuccess ? ( + setDeleteSuccess(null)} + > + {deleteSuccess} + + ) : null} + {deleteError && !isDeleteDialogOpen ? ( + setDeleteError(null)}> + {deleteError} + + ) : null} + + Deletion applies to data already stored in Ocotillo, not + to the uploaded file or the corrections in this session. + + + ) : null} + + + {/* Reset to Original lives in the header, next to the + selection chip — a destructive discard does not belong + beside the publish action. */} + + + {!onPublish ? ( + + Publishing requires a resolved Ocotillo well and is + disabled in demo mode. + + ) : null} Brush the chart to scope edits. Without a selection, actions apply to the full uploaded trace. - - + - + - - + {/* Anchored to the top so it stays reachable however tall the + workspace grows. */} + - - setControlsCollapsed((current) => !current)} + > + {controlsCollapsed ? ( + + ) : ( + + )} + + + + + + + + + + + + + + + + + {showDataTable ? ( + + + + + + Data Table + + + + + - - - + + + ) : null} - Click a manual point on the chart to prefill the snap target. The - uploaded raw trace stays visible as a dashed reference while edits - are applied to the corrected trace. + Click a manual point on the chart to select it in the Snap table. + The uploaded raw trace stays visible as a dashed reference while + edits are applied to the corrected trace. - - + + + + + Delete stored transducer data? + + + + + This permanently deletes {doomedStoredPoints.length} stored + observation{doomedStoredPoints.length === 1 ? '' : 's'} from + Ocotillo. It cannot be undone. + + {deletesEveryStoredPoint ? ( + + This range covers every stored transducer observation for this + well — nothing will remain. + + ) : null} + + + + Well + + {thingName || 'Unknown'} + + + + From + + {deleteRange?.startTime.toLocaleString() ?? '—'} + + + + To + + {deleteRange?.endTime.toLocaleString() ?? '—'} + + + + Observations + + {doomedStoredPoints.length} of{' '} + {storedTransducerPoints.length} + + + +
+ {/* Typed confirmation: deleting requires reading and reproducing + the well name, so the dialog cannot be cleared by reflex. */} + + Type {deleteConfirmPhrase} to confirm. + + setDeleteConfirmText(event.target.value)} + disabled={isDeleting} + error={ + deleteConfirmText.length > 0 && !deleteConfirmMatches + } + /> + {deleteError ? {deleteError} : null} +
+
+ + + + +
+ ) } diff --git a/src/components/Hydrographs/hydrographCorrection.test.ts b/src/components/Hydrographs/hydrographCorrection.test.ts index 40fb3bd4..c1456754 100644 --- a/src/components/Hydrographs/hydrographCorrection.test.ts +++ b/src/components/Hydrographs/hydrographCorrection.test.ts @@ -3,11 +3,18 @@ import { resolve } from 'node:path' import { describe, expect, it } from 'vitest' import { applyOffsetToRange, + assessDriftAtManualObservations, calculateSnapOffset, + convertWaterHeadToDepthToWater, + detectOverpressureClipping, extractPointIdFromText, + interpolateSeriesValueAt, + interpolateSpuriousReflections, normalizePointId, parseHydrographUpload, parseHydrographWorkbookUpload, + removeOffsetsAndZeros, + removeSpuriousReflections, } from './hydrographCorrection' describe('hydrograph correction utilities', () => { @@ -36,8 +43,26 @@ SO-0200,2025-02-01,13:00:00,44.2`) expect(parsed.pointId).toBe('SO-0200') expect(parsed.detectedTimeColumn).toBe('Date + Time') - expect(parsed.measurements[1].time.getTime()).toBe( - new Date('2025-02-01 13:00:00').getTime() + // Logger timestamps carry no timezone and Ocotillo ingests them as UTC, + // so they must be read as UTC rather than in the viewer's timezone. + expect(parsed.measurements[1].time.toISOString()).toBe( + '2025-02-01T13:00:00.000Z' + ) + }) + + it('reads naive upload timestamps as UTC and honours declared zones', () => { + const parsed = parseHydrographUpload(`PointID: SO-0167 +Date Time,Depth To Water +2025/01/01 08:00:00,12.5 +2025-01-01T09:00:00Z,12.6 +2025-01-01T10:00:00-07:00,12.7`) + + expect(parsed.measurements.map((point) => point.time.toISOString())).toEqual( + [ + '2025-01-01T08:00:00.000Z', + '2025-01-01T09:00:00.000Z', + '2025-01-01T17:00:00.000Z', + ] ) }) @@ -48,27 +73,803 @@ SO-0200,2025-02-01,13:00:00,44.2`) { time: new Date('2025-01-03T00:00:00Z'), value: 12 }, ] - const shifted = applyOffsetToRange(measurements, 1.5, { - startTime: new Date('2025-01-02T00:00:00Z'), - endTime: new Date('2025-01-03T00:00:00Z'), - }) + const shifted = applyOffsetToRange( + measurements, + 1.5, + { + startTime: new Date('2025-01-02T00:00:00Z'), + endTime: new Date('2025-01-03T00:00:00Z'), + }, + 'shifted +1.5 ft' + ) expect(shifted.map((point) => point.value)).toEqual([10, 12.5, 13.5]) + // only the modified points carry the correction note + expect(shifted.map((point) => point.correctionNote ?? null)).toEqual([ + null, + 'shifted +1.5 ft', + 'shifted +1.5 ft', + ]) + + // a second correction appends to the existing note + const snapped = applyOffsetToRange(shifted, -0.5, null, 'snapped -0.5 ft') + expect(snapped[0].correctionNote).toBe('snapped -0.5 ft') + expect(snapped[1].correctionNote).toBe('shifted +1.5 ft; snapped -0.5 ft') + }) + + it('snaps to the trace value at the manual measurement time, not the nearest reading', () => { + // Logger rises 2 ft/day. The manual was taken at 18:00, three quarters of + // the way through the interval, where the trace reads 11.5. The nearest + // reading is the 12.0 at midnight, six hours away. + const measurements = [ + { time: new Date('2025-01-01T00:00:00Z'), value: 10 }, + { time: new Date('2025-01-02T00:00:00Z'), value: 12 }, + ] + const target = { time: new Date('2025-01-01T18:00:00Z'), value: 11.25 } + + const { offset, method, anchorValue } = calculateSnapOffset({ + measurements, + target, + }) + + expect(method).toBe('interpolated') + expect(anchorValue).toBeCloseTo(11.5, 4) + // -0.25 against the measurement time; snapping to the nearest reading + // would have given -0.75 and left the line 0.5 ft off at 18:00. + expect(offset).toBeCloseTo(-0.25, 4) + + // The acceptance criterion itself: after the offset, the corrected line + // passes through the manual value at the manual's own instant. + const corrected = applyOffsetToRange(measurements, offset, null, 'snapped') + expect(interpolateSeriesValueAt(corrected, target.time)).toBeCloseTo( + target.value, + 4 + ) }) - it('calculates the offset needed to snap to the nearest manual point', () => { - const offset = calculateSnapOffset({ + it('clamps to the nearest end when the manual falls outside the trace', () => { + const { offset, method } = calculateSnapOffset({ measurements: [ { time: new Date('2025-01-01T00:00:00Z'), value: 10 }, { time: new Date('2025-01-02T00:00:00Z'), value: 12 }, ], - target: { - time: new Date('2025-01-02T06:00:00Z'), - value: 11.25, - }, + // Six hours after the last reading: there is no line at this instant. + target: { time: new Date('2025-01-02T06:00:00Z'), value: 11.25 }, + }) + + expect(method).toBe('clamped') + expect(offset).toBeCloseTo(-0.75, 4) + }) + + it('interpolates the series value at an arbitrary instant', () => { + const series = [ + { time: new Date('2025-01-01T00:00:00Z'), value: 10 }, + { time: new Date('2025-01-02T00:00:00Z'), value: 12 }, + { time: new Date('2025-01-03T00:00:00Z'), value: 11 }, + ] + + // Endpoints, an exact sample, and both interpolated halves. + expect(interpolateSeriesValueAt(series, new Date('2025-01-01T00:00:00Z'))).toBe(10) + expect(interpolateSeriesValueAt(series, new Date('2025-01-02T00:00:00Z'))).toBe(12) + expect(interpolateSeriesValueAt(series, new Date('2025-01-03T00:00:00Z'))).toBe(11) + expect( + interpolateSeriesValueAt(series, new Date('2025-01-01T06:00:00Z')) + ).toBeCloseTo(10.5, 4) + expect( + interpolateSeriesValueAt(series, new Date('2025-01-02T12:00:00Z')) + ).toBeCloseTo(11.5, 4) + + // Outside the series, and an empty series: nothing to read. + expect( + interpolateSeriesValueAt(series, new Date('2024-12-31T23:00:00Z')) + ).toBeNull() + expect( + interpolateSeriesValueAt(series, new Date('2025-01-03T01:00:00Z')) + ).toBeNull() + expect(interpolateSeriesValueAt([], new Date('2025-01-01T00:00:00Z'))).toBeNull() + }) + + it('parses a Diver Office pressure-transducer export as water head', () => { + const parsed = parseHydrographUpload(`Data file for DataLogger. +Serial number=V5806 1250 +Location=SO-0167 +2025-01-01 00:00:00,10.000,14.1 +2025-01-01 06:00:00,10.500,14.2 +2025-01-01 12:00:00,9.800,14.1,412.0 +END OF DATA`) + + expect(parsed.valueKind).toBe('water_head') + expect(parsed.pointId).toBe('SO-0167') + expect(parsed.measurements).toHaveLength(3) + expect(parsed.measurements[1].value).toBe(10.5) + expect(parsed.detectedValueColumn).toBe('Water head (ft)') + }) + + it('parses a Wellntel acoustic wcsv export as depth to water', () => { + const parsed = parseHydrographUpload(`timestamp,temperature_C,temperature_raw,depth +2025-01-01 00:00:00,21.5,708,42.1 +2025-01-01 06:00:00,21.4,707,42.2`) + + expect(parsed.valueKind).toBe('depth_to_water') + expect(parsed.detectedTimeColumn).toBe('timestamp') + expect(parsed.detectedValueColumn).toBe('depth') + expect(parsed.measurements.map((point) => point.value)).toEqual([ + 42.1, 42.2, + ]) + }) + + it('converts water head to depth to water anchored on manual observations', () => { + const measurements = [ + { time: new Date('2025-01-01T00:00:00Z'), value: 10 }, + { time: new Date('2025-01-02T00:00:00Z'), value: 10.5 }, + { time: new Date('2025-01-03T00:00:00Z'), value: 9.8 }, + { time: new Date('2025-01-04T00:00:00Z'), value: 9.6 }, + ] + const manualPoints = [ + { time: new Date('2025-01-01T00:00:00Z'), value: 50 }, + { time: new Date('2025-01-04T00:00:00Z'), value: 52 }, + ] + + // L1 is anchored on the head at the second manual's own time, which here + // falls exactly on the 01-04 reading: L1 = 52 + 9.6 = 61.6. Anchoring on + // the bin's last reading instead (9.8, a day early) would put the trace + // 0.2 ft off at the measurement. + const converted = convertWaterHeadToDepthToWater({ + measurements, + manualPoints, + }) + + expect(converted.map((point) => point.value)).toEqual([ + 51.6, 51.1, 51.8, 52, + ]) + + // The converted trace passes through the manual at its own time. + expect( + interpolateSeriesValueAt(converted, manualPoints[1].time) + ).toBeCloseTo(52, 4) + }) + + it('drops zero-head readings before converting', () => { + const converted = convertWaterHeadToDepthToWater({ + measurements: [ + { time: new Date('2025-01-01T00:00:00Z'), value: 10 }, + { time: new Date('2025-01-02T00:00:00Z'), value: 0 }, + { time: new Date('2025-01-03T00:00:00Z'), value: 9.8 }, + ], + manualPoints: [ + { time: new Date('2025-01-01T00:00:00Z'), value: 50 }, + { time: new Date('2025-01-03T00:00:00Z'), value: 52 }, + ], + }) + + expect(converted).toHaveLength(2) + expect(converted.map((point) => point.value)).toEqual([51.8, 52]) + }) + + it('interpolates the sensor depth when drift correction is enabled', () => { + const measurements = [ + { time: new Date('2025-01-01T00:00:00Z'), value: 10 }, + { time: new Date('2025-01-02T00:00:00Z'), value: 10.5 }, + { time: new Date('2025-01-03T00:00:00Z'), value: 9.8 }, + ] + const manualPoints = [ + { time: new Date('2025-01-01T00:00:00Z'), value: 50 }, + { time: new Date('2025-01-03T00:00:00Z'), value: 52 }, + ] + + // L0 = 50 + 10 = 60 at the first manual, L1 = 52 + 9.8 = 61.8 at the + // second, both read at the manuals' own timestamps, and the ramp spans + // the manual timestamps rather than the first and last readings. + const converted = convertWaterHeadToDepthToWater({ + measurements, + manualPoints, + correctDrift: true, + }) + + expect(converted.map((point) => point.value)).toEqual([50, 50.4, 52]) + + // Both manuals are inside the record, so the trace passes through each. + for (const manual of manualPoints) { + expect(interpolateSeriesValueAt(converted, manual.time)).toBeCloseTo( + manual.value, + 4 + ) + } + }) + + it('converts with a single manual anchor as a constant hanging point', () => { + const measurements = [ + { time: new Date('2025-01-01T00:00:00Z'), value: 10 }, + { time: new Date('2025-01-02T00:00:00Z'), value: 10.5 }, + { time: new Date('2025-01-03T00:00:00Z'), value: 9.8 }, + ] + + // Hanging point = 50 + head at the manual's own time. The manual sits + // three hours into the 01-02 -> 01-03 interval, where the head reads + // 10.4125, not the 10.5 of the nearest reading. + const manual = { time: new Date('2025-01-02T03:00:00Z'), value: 50 } + const converted = convertWaterHeadToDepthToWater({ + measurements, + manualPoints: [manual], + }) + + expect(converted.map((point) => point.value)).toEqual([ + 50.4125, 49.9125, 50.6125, + ]) + + expect(interpolateSeriesValueAt(converted, manual.time)).toBeCloseTo(50, 4) + }) + + it('rejects conversion without manual observations or overlap', () => { + const measurements = [ + { time: new Date('2025-01-01T00:00:00Z'), value: 10 }, + ] + + expect(() => + convertWaterHeadToDepthToWater({ measurements, manualPoints: [] }) + ).toThrow('At least one manual observation') + + expect(() => + convertWaterHeadToDepthToWater({ + measurements, + manualPoints: [ + { time: new Date('2026-01-01T00:00:00Z'), value: 50 }, + { time: new Date('2026-02-01T00:00:00Z'), value: 51 }, + ], + }) + ).toThrow('do not overlap') + }) + + it('removes zeros and cancels offset jumps beyond the threshold', () => { + const measurements = [ + { time: new Date('2025-01-01T00:00:00Z'), value: 10 }, + { time: new Date('2025-01-02T00:00:00Z'), value: 10.1 }, + { time: new Date('2025-01-03T00:00:00Z'), value: 0 }, + { time: new Date('2025-01-04T00:00:00Z'), value: 12.1 }, + { time: new Date('2025-01-05T00:00:00Z'), value: 12.2 }, + { time: new Date('2025-01-06T00:00:00Z'), value: 12.3 }, + ] + + const cleaned = removeOffsetsAndZeros(measurements, 0.25) + + // step size estimated from window medians (2.1 here), zeros dropped + expect(cleaned.map((point) => point.value)).toEqual([ + 10, 10.1, 10, 10.1, 10.2, + ]) + }) + + it('does not mistake an isolated spike for an offset', () => { + const day = (n: number, value: number) => ({ + time: new Date(Date.UTC(2025, 0, n)), + value, + }) + const measurements = [ + day(1, 42), + day(2, 42.05), + day(3, 45.2), // reflection-style spike: the reflection tool's job + day(4, 42.1), + day(5, 42.15), + day(6, 42.2), + day(7, 42.25), + ] + + const cleaned = removeOffsetsAndZeros(measurements, 0.25) + + expect(cleaned.map((point) => point.value)).toEqual( + measurements.map((point) => point.value) + ) + }) + + it('localizes a step boundary exactly, even with a spike nearby', () => { + const day = (n: number, value: number) => ({ + time: new Date(Date.UTC(2025, 0, n)), + value, + }) + const measurements = [ + day(1, 42.0), + day(2, 42.01), + day(3, 45.0), // spike inside the pre-step window + day(4, 42.03), + day(5, 42.04), + day(6, 42.05), + day(7, 44.05), // sustained +2 step starts here + day(8, 44.06), + day(9, 44.07), + day(10, 44.08), + day(11, 44.09), + day(12, 44.1), + ] + + const cleaned = removeOffsetsAndZeros(measurements, 0.25) + + // pre-step samples (including the spike) untouched; the step segment is + // re-leveled by the median-estimated 2.03 starting at the true boundary + expect(cleaned.map((point) => point.value)).toEqual([ + 42.0, 42.01, 45.0, 42.03, 42.04, 42.05, 42.02, 42.03, 42.04, 42.05, + 42.06, 42.07, + ]) + + // only the re-leveled points carry the correction note + expect(cleaned.slice(0, 6).every((point) => !point.correctionNote)).toBe( + true + ) + expect( + cleaned + .slice(6) + .every((point) => + point.correctionNote?.includes('level offset removed (-2.0300 ft)') + ) + ).toBe(true) + }) + + it('removes isolated spurious reflections in either direction', () => { + const day = (n: number, value: number) => ({ + time: new Date(Date.UTC(2025, 0, n)), + value, + }) + const measurements = [ + day(1, 42.0), + day(2, 42.05), + day(3, 45.15), // spurious positive 1x echo + day(4, 42.1), + day(5, 39.9), // spurious negative 1x echo + day(6, 42.15), + day(7, 42.2), + ] + + const cleaned = removeSpuriousReflections(measurements, 0.25) + + expect(cleaned.map((point) => point.value)).toEqual([ + 42.0, 42.05, 42.1, 42.15, 42.2, + ]) + }) + + it('removes 2x double-bounce reflections and adjacent reflection pairs', () => { + const day = (n: number, value: number) => ({ + time: new Date(Date.UTC(2025, 0, n)), + value, }) + const measurements = [ + day(1, 42.0), + day(2, 42.05), + day(3, 84.2), // 2x double bounce (~twice true depth) + day(4, 42.1), + day(5, 45.2), // adjacent pair: positive 1x... + day(6, 84.3), // ...next to a 2x — both must go + day(7, 42.15), + day(8, 42.2), + day(9, 42.25), + ] + + const cleaned = removeSpuriousReflections(measurements, 0.25) + + expect(cleaned.map((point) => point.value)).toEqual([ + 42.0, 42.05, 42.1, 42.15, 42.2, 42.25, + ]) + }) + + it('interpolates across removed reflections instead of deleting them', () => { + const day = (n: number, value: number) => ({ + time: new Date(Date.UTC(2025, 0, n)), + value, + }) + const measurements = [ + day(1, 42.0), + day(2, 84.2), // spurious 2x + day(3, 42.1), + day(4, 45.3), // adjacent pair: 1x... + day(5, 84.5), // ...and 2x + day(6, 42.2), + day(7, 42.25), + ] + + const interpolated = interpolateSpuriousReflections(measurements, 0.25) + + // cadence preserved: same length, same timestamps + expect(interpolated).toHaveLength(measurements.length) + expect(interpolated.map((point) => point.time)).toEqual( + measurements.map((point) => point.time) + ) + // spurious values replaced with linear fits between survivors + expect(interpolated.map((point) => point.value)).toEqual([ + 42.0, + 42.05, // midpoint of day 1 (42.0) and day 3 (42.1) + 42.1, + 42.1333, // one third of day 3 (42.1) -> day 6 (42.2) + 42.1667, // two thirds + 42.2, + 42.25, + ]) + + // each replaced observation is flagged with what happened to it + expect(interpolated.map((point) => point.correctionNote ?? null)).toEqual([ + null, + 'spurious reflection removed; value interpolated from neighbors (was 84.2)', + null, + 'spurious reflection removed; value interpolated from neighbors (was 45.3)', + 'spurious reflection removed; value interpolated from neighbors (was 84.5)', + null, + null, + ]) + + // notes survive later whole-trace edits + const shifted = applyOffsetToRange(interpolated, 0.5) + expect(shifted[1].correctionNote).toContain('spurious reflection removed') + }) + + it('keeps genuine steps and points outside the selected range', () => { + const day = (n: number, value: number) => ({ + time: new Date(Date.UTC(2025, 0, n)), + value, + }) + + // A sustained two-sample offset is a real step, not a reflection. + const step = [day(1, 42), day(2, 45), day(3, 45.05), day(4, 42.1)] + expect(removeSpuriousReflections(step, 0.25)).toHaveLength(4) + + // A reflection outside the brushed range is untouched. + const spike = [day(1, 42), day(2, 45.1), day(3, 42.05), day(4, 42.1)] + const cleaned = removeSpuriousReflections(spike, 0.25, { + startTime: new Date(Date.UTC(2025, 0, 3)), + endTime: new Date(Date.UTC(2025, 0, 4)), + }) + expect(cleaned).toHaveLength(4) + }) + + it('only cancels jumps inside the selected range', () => { + const measurements = [ + { time: new Date('2025-01-01T00:00:00Z'), value: 10 }, + { time: new Date('2025-01-02T00:00:00Z'), value: 12 }, + { time: new Date('2025-01-03T00:00:00Z'), value: 12.1 }, + ] + + const cleaned = removeOffsetsAndZeros(measurements, 0.25, { + startTime: new Date('2025-01-02T12:00:00Z'), + endTime: new Date('2025-01-03T12:00:00Z'), + }) + + expect(cleaned.map((point) => point.value)).toEqual([10, 12, 12.1]) + }) + + it('baseline detection clears dense reflection clusters the median method cannot', () => { + const day = (n: number, value: number) => ({ + time: new Date(Date.UTC(2025, 0, n)), + value, + }) + const measurements = [ + day(1, 42.0), + day(2, 42.02), + day(3, 42.04), + day(4, 42.06), + day(5, 42.08), + day(6, 45.3), // dense adjacent cluster: readings rescue each other... + day(7, 45.31), + day(8, 45.32), + day(9, 45.3), + day(10, 42.1), + day(11, 42.12), + day(12, 42.14), + day(13, 42.16), + ] + + // median method: the cluster members agree with their neighbors and survive + const medianKept = removeSpuriousReflections(measurements, 0.25) + expect(medianKept.some((point) => point.value > 45)).toBe(true) + + // baseline method: trailing lower quantile flags the whole cluster + const baselineKept = removeSpuriousReflections( + measurements, + 0.25, + null, + 'baseline' + ) + expect(baselineKept.every((point) => point.value < 43)).toBe(true) + expect(baselineKept.length).toBe(9) + }) + + it('baseline detection follows a genuine sustained rise', () => { + const points = [ + ...Array.from({ length: 10 }, (_value, i) => ({ + time: new Date(Date.UTC(2025, 0, 1 + i)), + value: 42 + i * 0.01, + })), + ...Array.from({ length: 20 }, (_value, i) => ({ + time: new Date(Date.UTC(2025, 0, 11 + i)), + value: 44 + i * 0.01, + })), + ] + + const kept = removeSpuriousReflections(points, 0.25, null, 'baseline') + + // the transition is flagged for up to ~a window, but once the new level + // fills the trailing window it is accepted — the rise is not erased + expect(kept.filter((point) => point.value >= 44).length).toBeGreaterThan(5) + expect(kept[kept.length - 1].value).toBeCloseTo(44.19, 4) + }) + + it('temperature assist flags marginal warm echoes the value test misses', () => { + const day = (n: number, value: number, temperature: number) => ({ + time: new Date(Date.UTC(2025, 0, n)), + value, + temperature, + }) + const measurements = [ + day(1, 42.0, 8), + day(2, 42.01, 9), + day(3, 42.02, 8), + day(4, 42.03, 9), + day(5, 42.2, 22), // marginal (+0.17 < threshold) but hot: echo + day(6, 42.05, 8), + day(7, 42.18, 9), // equally marginal but cool: genuine wiggle + day(8, 42.07, 9), + ] + + const withoutAssist = removeSpuriousReflections(measurements, 0.25) + expect(withoutAssist).toHaveLength(8) + + const withAssist = removeSpuriousReflections( + measurements, + 0.25, + null, + 'median', + { useTemperature: true } + ) + expect(withAssist).toHaveLength(7) + expect(withAssist.some((point) => point.value === 42.2)).toBe(false) + expect(withAssist.some((point) => point.value === 42.18)).toBe(true) + }) + + it('parses temperature columns and keeps them through interpolation', () => { + const parsed = parseHydrographUpload(`timestamp,temperature_C,temperature_raw,depth +2025-01-01 00:00:00,21.5,708,42.1 +2025-01-01 06:00:00,21.4,707,42.2`) + + expect(parsed.measurements.map((point) => point.temperature)).toEqual([ + 21.5, 21.4, + ]) + + const interpolated = interpolateSpuriousReflections( + [ + { time: new Date('2025-01-01T00:00:00Z'), value: 42, temperature: 8 }, + { time: new Date('2025-01-02T00:00:00Z'), value: 84, temperature: 22 }, + { time: new Date('2025-01-03T00:00:00Z'), value: 42.1, temperature: 9 }, + ], + 0.25 + ) + expect(interpolated[1].temperature).toBe(22) + expect(interpolated[1].correctionNote).toContain('interpolated') + }) - expect(offset).toBe(-0.75) + it('temperature assist improves the median method on real EB-165 data', () => { + const parsed = parseHydrographUpload( + readFileSync(resolve(process.cwd(), 'tmp/wellpy-samples/EB-165.wcsv'), 'utf-8') + ) + expect(parsed.measurements[0].temperature).toBeCloseTo(1.5556, 3) + + const withoutAssist = removeSpuriousReflections( + parsed.measurements, + 0.5 + ).length + const withAssist = removeSpuriousReflections( + parsed.measurements, + 0.5, + null, + 'median', + { useTemperature: true } + ).length + + // assist removes at least 30 additional warm echoes + expect(withoutAssist - withAssist).toBeGreaterThanOrEqual(30) + }) + + it('baseline detection cleans the real EB-165 Wellntel export', () => { + const parsed = parseHydrographUpload( + readFileSync(resolve(process.cwd(), 'tmp/wellpy-samples/EB-165.wcsv'), 'utf-8') + ) + + const kept = removeSpuriousReflections( + parsed.measurements, + 0.5, + null, + 'baseline' + ) + const values = kept.map((point) => point.value) + const july = kept.filter((point) => point.time.getMonth() === 6) + + // ~170 of 405 readings are spurious echoes; none of the deep multiples + // survive, while the genuine ~3.5 ft seasonal rise into July is kept + expect(parsed.measurements.length - kept.length).toBeGreaterThanOrEqual(165) + expect(values.filter((value) => value > 486)).toHaveLength(0) + expect(july.length).toBeGreaterThanOrEqual(40) + }) + + it('assesses drift misfit at manual observations', () => { + const converted = [ + { time: new Date('2025-01-01T00:00:00Z'), value: 42.0 }, + { time: new Date('2025-02-01T00:00:00Z'), value: 42.5 }, + ] + const manualPoints = [ + { time: new Date('2025-01-01T02:00:00Z'), value: 42.31 }, // misses by -0.3087 + { time: new Date('2025-02-01T01:00:00Z'), value: 42.49 }, // fits + { time: new Date('2025-06-01T00:00:00Z'), value: 44 }, // outside coverage + ] + + const assessments = assessDriftAtManualObservations(converted, manualPoints) + + expect(assessments).toHaveLength(2) + // Read at the manual's own time: two hours into a month-long interval + // that rises 0.5 ft, so the series is at 42.0013 there, not the 42.0 of + // the reading two hours earlier. + expect(assessments[0].misfit).toBeCloseTo(-0.3087, 4) + // An hour past the last reading, so there is nothing to interpolate + // between and the nearest reading stands in — the download-day manual, + // which still has to be reported. + expect(assessments[1].misfit).toBeCloseTo(0.01, 4) + }) + + it('detects overpressurization clipping as a plateau at the series max', () => { + const day = (n: number, value: number) => ({ + time: new Date(Date.UTC(2025, 0, n)), + value, + }) + const clipped = [ + day(1, 30), + day(2, 31), + day(3, 32.8084), // hits the Diver's range ceiling... + day(4, 32.8084), + day(5, 32.8084), + day(6, 32.8084), + day(7, 32.8084), + day(8, 32.8084), + day(9, 31.5), + ] + + const clipping = detectOverpressureClipping(clipped) + expect(clipping).not.toBeNull() + expect(clipping?.count).toBe(6) + expect(clipping?.value).toBeCloseTo(32.8084, 4) + + // a normal trace whose max occurs once is not clipping + expect( + detectOverpressureClipping([day(1, 30), day(2, 31), day(3, 30.5)]) + ).toBeNull() + }) + + it('parses a real Diver Office compensated export', () => { + const text = readFileSync( + resolve(process.cwd(), 'tmp/wellpy-samples/sa-0231_DK744_compensated.CSV'), + 'latin1' + ) + + const parsed = parseHydrographUpload(text) + + expect(parsed.valueKind).toBe('water_head') + expect(parsed.pointId).toBe('SA-0231') + expect(parsed.detectedDelimiter).toBe(',') + expect(parsed.detectedTimeColumn).toBe('Date/time') + expect(parsed.detectedValueColumn).toBe('Water head[ft]') + expect(parsed.measurements.length).toBeGreaterThan(700) + expect(parsed.measurements[0].value).toBeCloseTo(23.09766, 4) + expect(parsed.measurements[0].time.getFullYear()).toBe(2024) + }) + + it('anchors a real export on the manual inside the logged period', () => { + // The real Diver Office artifact against the real manual observations + // Ocotillo holds for SA-0231. Only one of them, 2024-02-20 17:38, falls + // inside the logged period, and it is five and a half hours off the + // logger's 12-hour cadence. The next one is seventeen hours past the last + // reading, so it has no head to anchor on — deriving one from the closest + // reading used to ride the whole trace on an invented sensor depth and + // left it 0.006 ft off the manual that is actually in range. + const text = readFileSync( + resolve(process.cwd(), 'tmp/wellpy-samples/sa-0231_DK744_compensated.CSV'), + 'latin1' + ) + const readings = parseHydrographUpload(text).measurements + const manualPoints = [ + { time: new Date('2023-02-20T19:54:00Z'), value: 90.76 }, + { time: new Date('2024-02-20T17:38:00Z'), value: 88.85 }, + { time: new Date('2025-02-11T17:00:00Z'), value: 85.35 }, + ] + const anchor = manualPoints[1] + + for (const correctDrift of [false, true]) { + const converted = convertWaterHeadToDepthToWater({ + measurements: readings, + manualPoints, + correctDrift, + }) + + // Every reading converted, nothing added: the series is shifted onto + // the anchor, not given an extra vertex at it. + expect(converted).toHaveLength(readings.length) + expect(converted.every((point) => !point.correctionNote)).toBe(true) + + // The line passes through the manual at its own instant, to within the + // four decimals the converted values are rounded to. + expect(interpolateSeriesValueAt(converted, anchor.time)).toBeCloseTo( + anchor.value, + 3 + ) + + // Which is what the residual panel shows the user. + const [assessment] = assessDriftAtManualObservations(converted, [anchor]) + expect(assessment.misfit).toBeCloseTo(0, 3) + + // And a snap onto it is a no-op rather than a correction that + // reintroduces the offset. + const { offset, method } = calculateSnapOffset({ + measurements: converted, + target: anchor, + }) + expect(method).toBe('interpolated') + expect(offset).toBeCloseTo(0, 3) + } + }) + + it('snaps a converted trace onto a manual it was not anchored to', () => { + const text = readFileSync( + resolve(process.cwd(), 'tmp/wellpy-samples/sa-0231_DK744_compensated.CSV'), + 'latin1' + ) + const readings = parseHydrographUpload(text).measurements + const converted = convertWaterHeadToDepthToWater({ + measurements: readings, + manualPoints: [ + { time: new Date('2023-02-20T19:54:00Z'), value: 90.76 }, + { time: new Date('2024-02-20T17:38:00Z'), value: 88.85 }, + { time: new Date('2025-02-11T17:00:00Z'), value: 85.35 }, + ], + }) + + // Deliberately off-cadence: three hours into a 12-hour interval. + const target = { + time: new Date(readings[200].time.getTime() + 3 * 60 * 60 * 1000), + value: 97.5, + } + + const { offset, method } = calculateSnapOffset({ + measurements: converted, + target, + }) + expect(method).toBe('interpolated') + + // Offsets are rounded to four decimals before being applied, so the + // landing is exact to within that rounding. + const snapped = applyOffsetToRange(converted, offset, null, 'snapped') + expect(interpolateSeriesValueAt(snapped, target.time)).toBeCloseTo( + target.value, + 3 + ) + }) + + it('parses a real field data logger telemetry file', () => { + const text = readFileSync( + resolve(process.cwd(), 'tmp/wellpy-samples/2025-11-25_MG009.txt'), + 'utf-8' + ) + + const parsed = parseHydrographUpload(text, '2025-11-25_MG009.txt') + + expect(parsed.valueKind).toBe('depth_to_water') + expect(parsed.pointId).toBe('MG-009') + expect(parsed.detectedDelimiter).toBe('field-logger') + expect(parsed.measurements.length).toBeGreaterThan(1000) + expect(parsed.measurements[0].value).toBeCloseTo(151.02, 2) + // healthy ~14 V battery: no low-battery warning + expect(parsed.warnings).toBeUndefined() + }) + + it('warns on low field logger battery and reads the ID token', () => { + const parsed = parseHydrographUpload( + `2024/11/19 18:54:05 ID 009 D 151.02 T 51.2 B 11.4 G 218 R 0001 +2024/11/20 02:54:03 ID 009 D 149.23 T 50.6 B 11.2 G 217 R 0000` + ) + + expect(parsed.pointId).toBe('009') + expect(parsed.measurements).toHaveLength(2) + expect(parsed.warnings?.[0]).toContain('battery is low') + expect(parsed.warnings?.[0]).toContain('11.2 V') }) it('parses the sample wellpy workbook export', () => { diff --git a/src/components/Hydrographs/hydrographCorrection.ts b/src/components/Hydrographs/hydrographCorrection.ts index 1f6d3860..084af378 100644 --- a/src/components/Hydrographs/hydrographCorrection.ts +++ b/src/components/Hydrographs/hydrographCorrection.ts @@ -3,6 +3,15 @@ import { inflateRaw } from 'pako' export interface HydrographPoint { time: Date value: number + // Sensor temperature in the source's units (°C for Wellntel exports). + // Reflections correlate with high sensor temperature, so the reflection + // tools can use it as a supporting signal. + temperature?: number + // Per-observation audit note set when a correction replaces this + // reading's value (e.g. a spurious reflection interpolated away). Carried + // through later edits, shown in the data table, and uploaded with the + // observation. + correctionNote?: string } export interface HydrographRange { @@ -10,12 +19,23 @@ export interface HydrographRange { endTime: Date } +// 'water_head' measurements are the height of the water column above the +// sensor (Diver Office pressure-transducer exports) and must be converted to +// depth to water below ground surface with convertWaterHeadToDepthToWater +// before they are comparable to Ocotillo observations. 'depth_to_water' +// measurements (wellpy workbooks, Wellntel acoustic exports) are used as-is. +export type HydrographValueKind = 'depth_to_water' | 'water_head' + export interface ParsedHydrographUpload { pointId: string | null detectedDelimiter: string detectedValueColumn: string detectedTimeColumn: string + valueKind: HydrographValueKind measurements: HydrographPoint[] + // Non-fatal quality observations surfaced during parsing (e.g. a field + // logger reporting low battery voltage). + warnings?: string[] } const DELIMITER_CANDIDATES = ['\t', ',', ';', '|'] @@ -25,6 +45,10 @@ const POINT_ID_PATTERNS = [ /point(?:\s|_|-)?id\s*[:=]\s*([A-Za-z0-9._-]+)/i, /well(?:\s|_|-)?name\s*[:=]\s*([A-Za-z0-9._-]+)/i, /site(?:\s|_|-)?id\s*[:=]\s*([A-Za-z0-9._-]+)/i, + // Diver Office metadata identifies the well as `Location =sa-0231`. + // Restricted to point-id-shaped values so prose location names (e.g. + // wellpy workbook "Location=Aztec MW") fall through to other sources. + /^\s*location\s*[:=]\s*([A-Za-z]{1,4}[-_ ]?\d{3,6})\b/im, ] const TIME_COLUMN_PATTERNS = [ @@ -45,13 +69,68 @@ const VALUE_COLUMN_PATTERNS = [ /reading/i, /result/i, /value/i, + /^depth$/i, ] +const WATER_HEAD_COLUMN_PATTERN = /head/i + const DATE_ONLY_PATTERN = /date/i const TIME_ONLY_PATTERN = /^time$/i const toUnixTime = (value: Date) => value.getTime() +// "2024/02/20 12:00:00", "2024-02-20T12:00", with optional seconds and +// fractional seconds, and no timezone designator. +const NAIVE_TIMESTAMP_PATTERN = + /^(\d{4})[-/](\d{1,2})[-/](\d{1,2})[ T](\d{1,2}):(\d{2})(?::(\d{2}))?(?:\.(\d{1,3}))?$/ + +/** + * Parse a timestamp, reading one without a timezone as UTC. + * + * Logger exports carry naive wall-clock timestamps, and Ocotillo ingests them + * as UTC — the stored observations for a file come back as the same wall-clock + * time with a `Z`. `new Date()` instead reads a naive string in the browser's + * timezone, so an upload plotted against its own already-stored observations + * appeared shifted by the viewer's UTC offset (8 hours in US Pacific). + * Timestamps that do declare a zone are honoured as written. + */ +export const parseObservationTimestamp = (value: string | Date) => { + if (value instanceof Date) return value + + const candidate = String(value).trim() + const naive = NAIVE_TIMESTAMP_PATTERN.exec(candidate) + if (!naive) return new Date(candidate) + + const [, year, month, day, hour, minute, second, milli] = naive + return new Date( + Date.UTC( + Number(year), + Number(month) - 1, + Number(day), + Number(hour), + Number(minute), + Number(second ?? 0), + Number((milli ?? '').padEnd(3, '0') || 0) + ) + ) +} + +// Every operation that changes a point's value appends a clause to its +// correctionNote, so any corrected observation carries a full account of +// what happened to it. +const appendCorrectionNote = ( + point: HydrographPoint, + note?: string +): HydrographPoint => + note + ? { + ...point, + correctionNote: point.correctionNote + ? `${point.correctionNote}; ${note}` + : note, + } + : point + export const normalizePointId = (value?: string | null) => (value ?? '').trim().toUpperCase() @@ -82,19 +161,29 @@ export const extractPointIdFromFileName = (fileName: string) => { return normalizePointId(baseName) } +// Compact ids like "AR0209" normalize to the canonical dashed form +// ("AR-0209") used by Ocotillo well names. +const expandCompactPointId = (value: string) => { + const compact = value.match(/^([A-Za-z]{1,4})[-_ ]?(\d{3,6})$/) + return compact ? `${compact[1]}-${compact[2]}` : value +} + export const extractPointIdFromText = (text: string) => { for (const pattern of POINT_ID_PATTERNS) { const match = text.match(pattern) if (match?.[1]) { - return normalizePointId(match[1]) + return normalizePointId(expandCompactPointId(match[1])) } } return null } +// Sample well past any metadata preamble: real Diver Office exports open +// with ~50 lines of metadata containing stray pipes but no commas, which a +// 20-line sample mis-sniffed as pipe-delimited. const detectDelimiter = (lines: string[]) => { - const sample = lines.slice(0, 20).join('\n') + const sample = lines.slice(0, 500).join('\n') const ranked = DELIMITER_CANDIDATES.map((delimiter) => ({ delimiter, @@ -113,7 +202,7 @@ const maybeParseDate = (value: string) => { const candidate = value.trim() if (!candidate) return null - const parsed = new Date(candidate) + const parsed = parseObservationTimestamp(candidate) if (!Number.isNaN(parsed.getTime())) { return parsed } @@ -150,6 +239,7 @@ const pickPreferredValueColumnIndex = (headers: string[]) => { /reading/i, /result/i, /value/i, + /^depth$/i, ] for (const pattern of priorities) { @@ -190,6 +280,9 @@ const parseMeasurementRows = ({ const valueIndex = pickPreferredValueColumnIndex(headers) const dateIndex = pickColumnIndex(headers, [DATE_ONLY_PATTERN]) const timeIndex = pickColumnIndex(headers, [TIME_ONLY_PATTERN]) + const temperatureIndex = headers.findIndex( + (header, index) => index !== valueIndex && /temp/i.test(header.trim()) + ) if (valueIndex < 0) { throw new Error('Unable to find a water-level column in the uploaded file.') @@ -226,9 +319,17 @@ const parseMeasurementRows = ({ continue } + const parsedTemperature = + temperatureIndex >= 0 + ? Number.parseFloat(row[temperatureIndex] ?? '') + : Number.NaN + measurements.push({ time: parsedDate, value: parsedValue, + ...(Number.isFinite(parsedTemperature) + ? { temperature: parsedTemperature } + : {}), }) } @@ -257,7 +358,165 @@ const parseMeasurementRows = ({ } } -export const parseHydrographUpload = (text: string): ParsedHydrographUpload => { +// Diver Office pressure-transducer CSV exports have no header row: a block +// of metadata lines (including `Serial number=...` and `Location=...`), then +// bare data rows of `date,water head,temperature[,conductivity]`, terminated +// by an `END OF DATA` line. The head values are the water column above the +// sensor in feet, mirroring wellpy's `DataModel._load_csv`. +const DIVER_OFFICE_LOCATION_PATTERN = /^Location\s*[:=](.+)$/i +const DIVER_OFFICE_SERIAL_PATTERN = /^Serial number\s*[:=](.+)$/i + +const looksLikeDiverOfficeUpload = (text: string) => + text + .split(/\r?\n/) + .some((line) => DIVER_OFFICE_SERIAL_PATTERN.test(line.trim())) || + /^END OF DATA/im.test(text) + +export const parseDiverOfficeUpload = ( + text: string +): ParsedHydrographUpload => { + const lines = text + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + + let pointId: string | null = null + const measurements: HydrographPoint[] = [] + + for (const line of lines) { + if (/^END OF DATA/i.test(line)) break + + const locationMatch = line.match(DIVER_OFFICE_LOCATION_PATTERN) + if (locationMatch) { + pointId = normalizePointId(locationMatch[1]) + continue + } + + const cells = splitRow(line, ',') + if (cells.length !== 3 && cells.length !== 4) continue + + const parsedDate = maybeParseDate(cells[0]) + const parsedHead = Number.parseFloat(cells[1]) + if (!parsedDate || !Number.isFinite(parsedHead)) continue + + const parsedTemperature = Number.parseFloat(cells[2] ?? '') + measurements.push({ + time: parsedDate, + value: parsedHead, + ...(Number.isFinite(parsedTemperature) + ? { temperature: parsedTemperature } + : {}), + }) + } + + if (measurements.length === 0) { + throw new Error( + 'No data rows could be parsed from the Diver Office export.' + ) + } + + return { + pointId, + detectedDelimiter: ',', + detectedTimeColumn: 'Date/time', + detectedValueColumn: 'Water head (ft)', + valueKind: 'water_head', + measurements: measurements.sort( + (a, b) => toUnixTime(a.time) - toUnixTime(b.time) + ), + } +} + +// NMBGMR field data logger telemetry: one space-delimited record per line, +// no header, depth to water already computed. +// 2024/11/19 18:54:05 ID 009 D 151.02 T 51.2 B 13.9 G 218 R 0001 +// D = depth to water (ft bgs), T = temperature (F), B = battery voltage, +// G = signal, R = restart flag. +const FIELD_LOGGER_ROW_PATTERN = + /^(\d{4}\/\d{2}\/\d{2}\s+\d{2}:\d{2}:\d{2})\s+ID\s+(\S+)\s+D\s+(-?\d+(?:\.\d+)?)\s+T\s+(-?\d+(?:\.\d+)?)\s+B\s+(-?\d+(?:\.\d+)?)/ + +const FIELD_LOGGER_LOW_BATTERY_VOLTS = 12 + +export const parseFieldLoggerUpload = ( + text: string, + fileName?: string | null +): ParsedHydrographUpload => { + const lines = text + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + + const measurements: HydrographPoint[] = [] + let stationId: string | null = null + let lastBattery: number | null = null + let minBattery: number | null = null + + for (const line of lines) { + const match = line.match(FIELD_LOGGER_ROW_PATTERN) + if (!match) continue + + const parsedDate = maybeParseDate(match[1]) + const depth = Number.parseFloat(match[3]) + if (!parsedDate || !Number.isFinite(depth)) continue + + if (!stationId) stationId = match[2] + const battery = Number.parseFloat(match[5]) + if (Number.isFinite(battery)) { + lastBattery = battery + minBattery = minBattery === null ? battery : Math.min(minBattery, battery) + } + + const temperature = Number.parseFloat(match[4]) + measurements.push({ + time: parsedDate, + value: depth, + ...(Number.isFinite(temperature) ? { temperature } : {}), + }) + } + + if (measurements.length === 0) { + throw new Error( + 'No data rows could be parsed from the field data logger file.' + ) + } + + // The filename usually carries the well id ("2025-11-25_MG009.txt"); + // fall back to the numeric ID token from the records. + const fileToken = (fileName ?? '') + .replace(/\.[^.]+$/, '') + .split(/[_\-\s]+/) + .map((token) => token.match(/^([A-Za-z]{1,4})(\d{3,6})$/)) + .find(Boolean) + const pointId = fileToken + ? normalizePointId(`${fileToken[1]}-${fileToken[2]}`) + : stationId + ? normalizePointId(stationId) + : null + + const warnings: string[] = [] + if (lastBattery !== null && lastBattery < FIELD_LOGGER_LOW_BATTERY_VOLTS) { + warnings.push( + `Field logger battery is low: last reading ${lastBattery.toFixed(1)} V (minimum ${minBattery?.toFixed(1)} V). The methodology recommends replacing declining loggers.` + ) + } + + return { + pointId, + detectedDelimiter: 'field-logger', + detectedTimeColumn: 'Date/time', + detectedValueColumn: 'D (depth to water, ft)', + valueKind: 'depth_to_water', + measurements: measurements.sort( + (a, b) => toUnixTime(a.time) - toUnixTime(b.time) + ), + ...(warnings.length > 0 ? { warnings } : {}), + } +} + +export const parseHydrographUpload = ( + text: string, + fileName?: string | null +): ParsedHydrographUpload => { const lines = text .split(/\r?\n/) .map((line) => line.trim()) @@ -267,11 +526,19 @@ export const parseHydrographUpload = (text: string): ParsedHydrographUpload => { throw new Error('Uploaded file is empty.') } + if (FIELD_LOGGER_ROW_PATTERN.test(lines[0])) { + return parseFieldLoggerUpload(text, fileName) + } + const detectedDelimiter = detectDelimiter(lines) const rows = lines.map((line) => splitRow(line, detectedDelimiter)) const headerIndex = resolveHeaderRow(rows) if (headerIndex < 0) { + if (looksLikeDiverOfficeUpload(text)) { + return parseDiverOfficeUpload(text) + } + throw new Error( 'Unable to detect a header row with timestamp and water-level columns.' ) @@ -288,6 +555,9 @@ export const parseHydrographUpload = (text: string): ParsedHydrographUpload => { detectedDelimiter, detectedValueColumn: parsed.detectedValueColumn, detectedTimeColumn: parsed.detectedTimeColumn, + valueKind: WATER_HEAD_COLUMN_PATTERN.test(parsed.detectedValueColumn) + ? 'water_head' + : 'depth_to_water', measurements: parsed.measurements, } } @@ -474,6 +744,7 @@ const parseXlsxWorksheet = (rows: XlsxRow[], fileName: string): ParsedHydrograph detectedDelimiter: 'xlsx', detectedTimeColumn: headers[timeIndex], detectedValueColumn: headers[valueIndex], + valueKind: 'depth_to_water', measurements: measurements.sort( (a, b) => toUnixTime(a.time) - toUnixTime(b.time) ), @@ -516,20 +787,705 @@ const includesTime = (time: Date, range?: HydrographRange | null) => { ) } +/** + * The series' value at `time`, linearly interpolated between the readings on + * either side of it. + * + * A manual water level is measured whenever the technician is on site, which + * is almost never one of the logger's own timestamps — a 6-hour cadence puts + * the reading up to 3 hours away from the measurement. Comparing against, or + * anchoring to, that reading builds the sampling offset into the correction: + * on a trace that is moving, the further the sample the larger the error. + * Reading the series at the measurement's own instant removes it. + * + * Returns null when `time` falls outside the series. There is no line there to + * read, and extrapolating a transducer trace past its own record would invent + * data; callers decide what to do about it. + */ +export const interpolateSeriesValueAt = ( + series: HydrographPoint[], + time: Date +): number | null => { + if (series.length === 0) return null + + const target = toUnixTime(time) + const sorted = [...series].sort((a, b) => toUnixTime(a.time) - toUnixTime(b.time)) + + const first = toUnixTime(sorted[0].time) + const last = toUnixTime(sorted[sorted.length - 1].time) + if (target < first || target > last) return null + if (target === first) return sorted[0].value + if (target === last) return sorted[sorted.length - 1].value + + // Binary search for the last reading at or before the target. Manual + // measurements are few but the transducer series runs to tens of thousands + // of readings, so a scan per manual is worth avoiding. + let low = 0 + let high = sorted.length - 1 + while (high - low > 1) { + const mid = (low + high) >> 1 + if (toUnixTime(sorted[mid].time) <= target) { + low = mid + } else { + high = mid + } + } + + const before = sorted[low] + const after = sorted[high] + const span = toUnixTime(after.time) - toUnixTime(before.time) + // Duplicate timestamps leave nothing to interpolate across. + if (span === 0) return before.value + + const fraction = (target - toUnixTime(before.time)) / span + return before.value + (after.value - before.value) * fraction +} + +/** The reading closest in time to `time`, or null for an empty series. */ +const nearestReading = ( + series: HydrographPoint[], + time: Date +): HydrographPoint | null => { + if (series.length === 0) return null + return [...series].sort( + (a, b) => + Math.abs(toUnixTime(a.time) - toUnixTime(time)) - + Math.abs(toUnixTime(b.time) - toUnixTime(time)) + )[0] +} + +// The methodology's key QC test: the converted series should pass through +// the bounding manual measurements. A large misfit at a manual means the +// logger (or its barometer) is drifting and the data should not be +// published without review. Reports the misfit at every manual that has a +// converted reading within maxGapMs. +export interface DriftAssessment { + anchorTime: Date + manualValue: number + seriesValue: number + misfit: number +} + +export const assessDriftAtManualObservations = ( + converted: HydrographPoint[], + manualPoints: HydrographPoint[], + { maxGapMs = 12 * 60 * 60 * 1000 }: { maxGapMs?: number } = {} +): DriftAssessment[] => { + if (converted.length === 0) return [] + + return manualPoints + .map((manual) => { + const nearest = nearestReading(converted, manual.time) + if ( + !nearest || + Math.abs(toUnixTime(nearest.time) - toUnixTime(manual.time)) > maxGapMs + ) { + return null + } + + // Read the series at the manual's own instant so the misfit is the + // logger's drift and not the gap to the nearest sample. A manual taken + // just outside the record — the download-day reading, measured after the + // logger was pulled — cannot be interpolated, so it falls back to the + // nearest reading; the maxGapMs guard above keeps that honest. + const seriesValue = + interpolateSeriesValueAt(converted, manual.time) ?? nearest.value + + return { + anchorTime: manual.time, + manualValue: manual.value, + seriesValue: Number(seriesValue.toFixed(4)), + misfit: Number((seriesValue - manual.value).toFixed(4)), + } + }) + .filter((assessment): assessment is DriftAssessment => assessment !== null) +} + +// A Diver pushed past its pressure range records its maximum — a +// flat-topped plateau at the series max. Reports the longest such run +// when it reaches minRunLength consecutive readings. +export const detectOverpressureClipping = ( + measurements: HydrographPoint[], + minRunLength = 6 +): { start: Date; end: Date; value: number; count: number } | null => { + if (measurements.length === 0) return null + + const max = Math.max(...measurements.map((point) => point.value)) + let best: { start: number; count: number } | null = null + let runStart = -1 + + measurements.forEach((point, index) => { + if (Math.abs(point.value - max) < 0.001) { + if (runStart < 0) runStart = index + const count = index - runStart + 1 + if (!best || count > best.count) best = { start: runStart, count } + } else { + runStart = -1 + } + }) + + if (!best) return null + const { start, count } = best as { start: number; count: number } + if (count < minRunLength) return null + + return { + start: measurements[start].time, + end: measurements[start + count - 1].time, + value: max, + count, + } +} + +// Port of wellpy's `Model.calculate_depth_to_water` for pressure-transducer +// data. Water head is the height of the water column above the sensor, so +// depth to water = sensor depth (L) - head. The sensor depth is anchored by +// manual observations: within each pair of consecutive manual observations +// (d0 at t0, d1 at t1), L1 = d1 + head at the end of the bin and +// L0 = d0 + head at the start. Without drift correction the whole bin uses +// L1; with it, L is interpolated linearly from L0 to L1. +// +// Wellpy leaves measurements outside manual coverage at zero; here the +// nearest bin's sensor depth is extended instead so the full trace stays +// plottable. +export const convertWaterHeadToDepthToWater = ({ + measurements, + manualPoints, + correctDrift = false, +}: { + measurements: HydrographPoint[] + manualPoints: HydrographPoint[] + correctDrift?: boolean +}): HydrographPoint[] => { + if (manualPoints.length === 0) { + throw new Error( + 'At least one manual observation is required to convert water head to depth to water.' + ) + } + + // Single anchor (the methodology's "Snap to Selected" flow, eq. 2/3): + // calculated hanging point = manual DTW + head at the manual's time, + // applied as a constant across the whole series. The common case for + // annual site visits, where only the download-day manual exists. + if (manualPoints.length === 1) { + const anchor = manualPoints[0] + const sortedSingle = measurements + .filter((point) => point.value !== 0) + .sort((a, b) => toUnixTime(a.time) - toUnixTime(b.time)) + if (sortedSingle.length === 0) { + throw new Error('No non-zero water-head measurements to convert.') + } + + // Head at the manual's own instant, so the converted trace passes through + // the manual measurement at the time it was taken. Falls back to the + // nearest reading only when the manual lies outside the record entirely. + const headAtAnchor = + interpolateSeriesValueAt(sortedSingle, anchor.time) ?? + nearestReading(sortedSingle, anchor.time)!.value + const hangingPoint = anchor.value + headAtAnchor + + return sortedSingle.map((point) => ({ + time: point.time, + value: Number((hangingPoint - point.value).toFixed(4)), + })) + } + + // Zero head means the sensor was out of the water; converting it would + // chart the bare sensor depth as a false reading, so drop those rows. + const sorted = measurements + .filter((point) => point.value !== 0) + .sort((a, b) => toUnixTime(a.time) - toUnixTime(b.time)) + const manual = [...manualPoints].sort( + (a, b) => toUnixTime(a.time) - toUnixTime(b.time) + ) + + const sensorDepths: Array = sorted.map(() => null) + let firstBinStartDepth: number | null = null + let lastBinEndDepth: number | null = null + for (let i = 0; i < manual.length - 1; i += 1) { + const m0 = manual[i] + const m1 = manual[i + 1] + const indices: number[] = [] + + sorted.forEach((point, index) => { + const t = toUnixTime(point.time) + if (t >= toUnixTime(m0.time) && t < toUnixTime(m1.time)) { + indices.push(index) + } + }) + + if (indices.length === 0) continue + + const firstIndex = indices[0] + const lastIndex = indices[indices.length - 1] + + // Anchor each sensor depth on the head at the manual's own timestamp, not + // on the bin's first and last readings. Those are simply the samples that + // happen to bracket the visit, up to one logging interval away; anchoring + // on them makes the converted trace pass through the manual's value at a + // sample's time instead of at the measurement's time. + // + // A manual outside the logged period has no head to anchor on. Deriving + // one from the closest reading invents a sensor depth at a moment the + // logger never covered, and the whole bin then rides on it — which is how + // a trace ends up offset from the one manual that is inside the record. + // Such an end is left unanchored and the other end carries the bin. + const head0 = interpolateSeriesValueAt(sorted, m0.time) + const head1 = interpolateSeriesValueAt(sorted, m1.time) + const l0 = head0 === null ? null : m0.value + head0 + const l1 = head1 === null ? null : m1.value + head1 + + // Both ends unanchored: nothing in this bin is pinned to a measurement, so + // the closing manual and the nearest reading are all there is to go on. + const start = l0 ?? l1 ?? m0.value + sorted[firstIndex].value + const end = l1 ?? l0 ?? m1.value + sorted[lastIndex].value + + if (firstBinStartDepth === null) { + firstBinStartDepth = start + } + lastBinEndDepth = end + + // Drift is interpolated between the manual timestamps for the same reason. + // It can only be measured when both ends are anchored; with one end the + // sensor depth is held constant at it rather than ramped toward a value + // that was never observed. + const t0 = toUnixTime(m0.time) + const t1 = toUnixTime(m1.time) + const span = t1 - t0 + const canRamp = correctDrift && span > 0 && l0 !== null && l1 !== null + + for (const index of indices) { + const l = canRamp + ? start + ((end - start) * (toUnixTime(sorted[index].time) - t0)) / span + : end + sensorDepths[index] = l + } + } + + if (firstBinStartDepth === null || lastBinEndDepth === null) { + throw new Error( + 'The manual observations do not overlap the uploaded water-head data.' + ) + } + + return sorted.map((point, index) => { + let sensorDepth = sensorDepths[index] + if (sensorDepth === null) { + sensorDepth = + toUnixTime(point.time) < toUnixTime(manual[0].time) + ? firstBinStartDepth + : lastBinEndDepth + } + + return { + time: point.time, + value: Number((sensorDepth - point.value).toFixed(4)), + } + }) + +} + +const OFFSET_WINDOW_HALF_WIDTH = 5 + +// "Remove Offsets/Zeros": drop zero readings (sensor out of water) and +// cancel sustained level shifts (sensor repositioning / cable slip) by +// re-leveling the trace after each step. +// +// This replaces wellpy's `fix_data`, whose single-sample diff detection +// mistook isolated spikes for offsets and estimated the step size from two +// noisy samples. Here a step boundary is a point where the median of the +// window before it and the median of the window after it differ by at +// least the threshold — a single spurious spike cannot move either median, +// so spikes are left for the reflection tool. Consecutive flagged +// boundaries around one step are collapsed to the boundary with the +// largest raw sample-to-sample jump (localization), while the step size +// comes from the median difference (noise-robust magnitude). Each detected +// step shifts everything after it, cumulatively, so multiple slips +// re-level correctly; steps closer together than the window may blur into +// one. With a brush range, only boundaries inside it are corrected. +export const removeOffsetsAndZeros = ( + measurements: HydrographPoint[], + threshold: number, + range?: HydrographRange | null +): HydrographPoint[] => { + const kept = measurements.filter( + (point) => !(point.value === 0 && includesTime(point.time, range)) + ) + const n = kept.length + if (n < 2) return kept.map((point) => ({ ...point })) + + const values = kept.map((point) => point.value) + const half = Math.min(OFFSET_WINDOW_HALF_WIDTH, Math.floor(n / 2)) + + const candidates: Array<{ index: number; delta: number }> = [] + for (let i = half; i <= n - half; i += 1) { + const before = median(values.slice(i - half, i)) + const after = median(values.slice(i, i + half)) + const delta = after - before + if (Math.abs(delta) >= threshold && includesTime(kept[i].time, range)) { + candidates.push({ index: i, delta }) + } + } + + // One step produces a run of consecutive flagged boundaries; keep the one + // sitting on the largest raw jump. + const steps: Array<{ index: number; delta: number }> = [] + let run: typeof candidates = [] + const flushRun = () => { + if (run.length === 0) return + let best = run[0] + for (const candidate of run) { + if ( + Math.abs(values[candidate.index] - values[candidate.index - 1]) > + Math.abs(values[best.index] - values[best.index - 1]) + ) { + best = candidate + } + } + steps.push(best) + run = [] + } + for (const candidate of candidates) { + if (run.length > 0 && candidate.index !== run[run.length - 1].index + 1) { + flushRun() + } + run.push(candidate) + } + flushRun() + + let cumulativeOffset = 0 + let nextStep = 0 + const appliedOffsets = values.map((_value, index) => { + if (nextStep < steps.length && index === steps[nextStep].index) { + cumulativeOffset += steps[nextStep].delta + nextStep += 1 + } + return cumulativeOffset + }) + + return kept.map((point, index) => { + const applied = appliedOffsets[index] + const updated = { + ...point, + value: Number((point.value - applied).toFixed(4)), + } + return applied === 0 + ? updated + : appendCorrectionNote( + updated, + `level offset removed (${applied > 0 ? '-' : '+'}${Math.abs(applied).toFixed(4)} ft)` + ) + }) +} + +const REFLECTION_WINDOW_HALF_WIDTH = 3 + +// Lower median: for even-length windows (truncated at the series edges), +// averaging the two middle values can land between two genuine water +// levels and flag every point in the window; picking an actual observed +// value cannot. +const median = (values: number[]) => { + const sorted = [...values].sort((a, b) => a - b) + return sorted[Math.floor((sorted.length - 1) / 2)] +} + +// Wellntel acoustic sensors occasionally record spurious reflections: an +// echo off a casing joint or other obstruction produces a reading offset +// from the true depth to water. Reflections can be positive or negative +// (longer or shorter echo path) and can land near the true depth (1x) or +// near twice it (2x double-bounce), so magnitude is unbounded. A point is +// flagged when it departs from the median of its surrounding window by at +// least the threshold; a flagged point survives only if it agrees with an +// immediate neighbor within the threshold, which preserves genuine steps +// (sustained excursions) while dropping reflections even when two +// different ones land side by side. This is the workbench analog of +// wellpy's acoustic upspike removal. +const findSpuriousReflectionIndices = ( + measurements: HydrographPoint[], + threshold: number, + range?: HydrographRange | null +) => { + const spurious = new Set() + + measurements.forEach((point, index) => { + if (!includesTime(point.time, range)) return + + const windowValues = measurements + .slice( + Math.max(0, index - REFLECTION_WINDOW_HALF_WIDTH), + index + REFLECTION_WINDOW_HALF_WIDTH + 1 + ) + .map((neighbor) => neighbor.value) + + if (Math.abs(point.value - median(windowValues)) < threshold) { + return + } + + const previous = measurements[index - 1] + const next = measurements[index + 1] + const agreesWithPrevious = + previous !== undefined && + Math.abs(point.value - previous.value) < threshold + const agreesWithNext = + next !== undefined && Math.abs(point.value - next.value) < threshold + + if (!agreesWithPrevious && !agreesWithNext) { + spurious.add(index) + } + }) + + return spurious +} + +// Detection strategies for spurious reflections: +// - 'median': isolated-echo detection (median window + neighbor-agreement +// rescue). Robust for scattered reflections; defeated when spurious +// readings arrive in dense runs that rescue each other. +// - 'baseline': running-baseline rejection for dense ONE-SIDED clusters, +// the behavior real Wellntel wells exhibit (echoes always read deeper). +// Port of wellpy's remove_up_spikes normal mode: track the last accepted +// value and reject anything more than the threshold above it, seeded +// from the lower median of the first window. Handles arbitrarily long +// spurious runs; the tradeoff is that a genuine sustained upward step +// larger than the threshold is also rejected, so scope it with the +// brush when the trace has real steps. +export type ReflectionDetectionMethod = 'median' | 'baseline' + +const BASELINE_WINDOW = 15 +const BASELINE_QUANTILE = 0.25 + +const lowerQuantile = (values: number[], quantile: number) => { + const sorted = [...values].sort((a, b) => a - b) + return sorted[Math.floor(quantile * (sorted.length - 1))] +} + +// The baseline is the trailing lower quantile of the last BASELINE_WINDOW +// readings (spurious included — the quantile ignores them as long as they +// are a minority of the window). Unlike a last-accepted-value baseline, +// this follows genuine level changes: a real seasonal rise fills the +// window and pulls the quantile up within ~a window of samples, while +// dense one-sided reflection clusters stay above it and get flagged. +const findBaselineSpuriousIndices = ( + measurements: HydrographPoint[], + threshold: number, + range?: HydrographRange | null +) => { + const spurious = new Set() + const inRange: number[] = [] + measurements.forEach((point, index) => { + if (includesTime(point.time, range)) inRange.push(index) + }) + if (inRange.length === 0) return spurious + + inRange.forEach((index, position) => { + const windowStart = Math.max(0, position - BASELINE_WINDOW) + const window = inRange + .slice(windowStart, position) + .map((i) => measurements[i].value) + if (window.length < 3) return + + const baseline = lowerQuantile(window, BASELINE_QUANTILE) + if (measurements[index].value - baseline > threshold) { + spurious.add(index) + } + }) + + return spurious +} + +// Reflections correlate with high sensor temperature (real Wellntel data +// shows the spurious population arriving overwhelmingly on warm readings). +// The temperature assist flags readings that are only marginally above the +// value baseline (half the threshold) when their sensor temperature is +// also well above the trailing temperature median — supporting evidence +// that lets marginal echoes be caught without loosening the value +// threshold for everything. Temperatures are compared in the source's +// units. +const TEMPERATURE_ASSIST_DELTA = 5 +const TEMPERATURE_ASSIST_VALUE_FACTOR = 0.5 + +const findTemperatureAssistedIndices = ( + measurements: HydrographPoint[], + threshold: number, + range?: HydrographRange | null +) => { + const flagged = new Set() + const inRange: number[] = [] + measurements.forEach((point, index) => { + if (includesTime(point.time, range)) inRange.push(index) + }) + + inRange.forEach((index, position) => { + const point = measurements[index] + if (point.temperature === undefined) return + + const windowIndices = inRange.slice( + Math.max(0, position - BASELINE_WINDOW), + position + ) + const windowValues = windowIndices.map((i) => measurements[i].value) + const windowTemperatures = windowIndices + .map((i) => measurements[i].temperature) + .filter((temperature): temperature is number => temperature !== undefined) + if (windowValues.length < 3 || windowTemperatures.length < 3) return + + const valueBaseline = lowerQuantile(windowValues, BASELINE_QUANTILE) + const temperatureMedian = median(windowTemperatures) + + if ( + point.value - valueBaseline > threshold * TEMPERATURE_ASSIST_VALUE_FACTOR && + point.temperature - temperatureMedian > TEMPERATURE_ASSIST_DELTA + ) { + flagged.add(index) + } + }) + + return flagged +} + +export interface ReflectionOptions { + useTemperature?: boolean +} + +const findReflectionIndices = ( + measurements: HydrographPoint[], + threshold: number, + range: HydrographRange | null | undefined, + method: ReflectionDetectionMethod, + options?: ReflectionOptions +) => { + const spurious = + method === 'baseline' + ? findBaselineSpuriousIndices(measurements, threshold, range) + : findSpuriousReflectionIndices(measurements, threshold, range) + + if (options?.useTemperature) { + for (const index of findTemperatureAssistedIndices( + measurements, + threshold, + range + )) { + spurious.add(index) + } + } + + return spurious +} + +export const removeSpuriousReflections = ( + measurements: HydrographPoint[], + threshold: number, + range?: HydrographRange | null, + method: ReflectionDetectionMethod = 'median', + options?: ReflectionOptions +): HydrographPoint[] => { + const spurious = findReflectionIndices( + measurements, + threshold, + range, + method, + options + ) + return measurements.filter((_point, index) => !spurious.has(index)) +} + +// Same detection as removeSpuriousReflections, but instead of deleting the +// spurious readings this keeps the sampling cadence and replaces each one +// with a linear interpolation in time between the nearest surviving +// readings on either side (nearest single side at the series edges). +export const interpolateSpuriousReflections = ( + measurements: HydrographPoint[], + threshold: number, + range?: HydrographRange | null, + method: ReflectionDetectionMethod = 'median', + options?: ReflectionOptions +): HydrographPoint[] => { + const spurious = findReflectionIndices( + measurements, + threshold, + range, + method, + options + ) + + return measurements.map((point, index) => { + if (!spurious.has(index)) return point + + let previousIndex = index - 1 + while (previousIndex >= 0 && spurious.has(previousIndex)) previousIndex -= 1 + let nextIndex = index + 1 + while (nextIndex < measurements.length && spurious.has(nextIndex)) + nextIndex += 1 + + const previous = previousIndex >= 0 ? measurements[previousIndex] : null + const next = nextIndex < measurements.length ? measurements[nextIndex] : null + + if (!previous && !next) return point + + let value: number + if (previous && next) { + const span = toUnixTime(next.time) - toUnixTime(previous.time) + value = + span > 0 + ? previous.value + + ((next.value - previous.value) * + (toUnixTime(point.time) - toUnixTime(previous.time))) / + span + : previous.value + } else { + value = (previous ?? next)!.value + } + + return { + ...point, + value: Number(value.toFixed(4)), + correctionNote: `spurious reflection removed; value interpolated from neighbors (was ${point.value})`, + } + }) +} + export const applyOffsetToRange = ( measurements: HydrographPoint[], offset: number, - range?: HydrographRange | null + range?: HydrographRange | null, + note?: string ) => measurements.map((measurement) => includesTime(measurement.time, range) - ? { - ...measurement, - value: Number((measurement.value + offset).toFixed(4)), - } + ? appendCorrectionNote( + { + ...measurement, + value: Number((measurement.value + offset).toFixed(4)), + }, + note + ) : measurement ) +export interface SnapOffset { + offset: number + /** + * `interpolated` — the manual falls inside the trace, so the offset was + * measured against the trace's value at the manual's own instant and the + * corrected line passes exactly through it. + * + * `clamped` — the manual falls outside the trace, where there is no line to + * pass through, so the nearest end of it was used instead. + */ + method: 'interpolated' | 'clamped' + /** The trace value the offset was measured against. */ + anchorValue: number +} + +/** + * How far to move the trace so it passes through `target`. + * + * The offset is measured against the trace's value at the manual measurement's + * own timestamp, interpolated between the readings on either side — not + * against the nearest reading. A logger on a 6-hour cadence puts its nearest + * sample up to 3 hours from the measurement, and on a trace that is moving + * that gap becomes error in the correction: the line ends up passing through + * the manual's value at the sample's time rather than at the manual's time. + */ export const calculateSnapOffset = ({ measurements, target, @@ -538,7 +1494,7 @@ export const calculateSnapOffset = ({ measurements: HydrographPoint[] target: HydrographPoint range?: HydrographRange | null -}) => { +}): SnapOffset => { const candidates = measurements.filter((measurement) => includesTime(measurement.time, range) ) @@ -547,13 +1503,25 @@ export const calculateSnapOffset = ({ throw new Error('No uploaded measurements fall inside the selected range.') } - const nearest = [...candidates].sort( - (a, b) => - Math.abs(toUnixTime(a.time) - toUnixTime(target.time)) - - Math.abs(toUnixTime(b.time) - toUnixTime(target.time)) - )[0] + const interpolated = interpolateSeriesValueAt(candidates, target.time) + if (interpolated !== null) { + return { + offset: Number((target.value - interpolated).toFixed(4)), + method: 'interpolated', + anchorValue: Number(interpolated.toFixed(4)), + } + } - return Number((target.value - nearest.value).toFixed(4)) + // The manual sits outside the trace being corrected — before it starts or + // after it ends, or outside the brushed range. Nothing can pass through that + // instant, so the nearest end is the only defined anchor. The caller is told + // which happened so it can be recorded and shown. + const nearest = nearestReading(candidates, target.time)! + return { + offset: Number((target.value - nearest.value).toFixed(4)), + method: 'clamped', + anchorValue: nearest.value, + } } export const buildCsvFromMeasurements = (measurements: HydrographPoint[]) => { diff --git a/src/components/Hydrographs/hydrographUiMode.ts b/src/components/Hydrographs/hydrographUiMode.ts new file mode 100644 index 00000000..e73ca9d4 --- /dev/null +++ b/src/components/Hydrographs/hydrographUiMode.ts @@ -0,0 +1,98 @@ +import { useCallback, useState } from 'react' + +// PrusaSlicer-style progressive disclosure: one control switches the whole +// corrector between a narrow, opinionated workflow and the full toolset. +// Simple mode is scoped to pressure-transducer (Diver Office) corrections; +// acoustic-logger tooling (reflections) and the detection tuning knobs only +// appear at higher modes. +export type HydrographUiMode = 'simple' | 'intermediate' | 'advanced' + +export const HYDROGRAPH_UI_MODES: readonly HydrographUiMode[] = [ + 'simple', + 'intermediate', + 'advanced', +] + +export const HYDROGRAPH_UI_MODE_LABELS: Record = { + simple: 'Simple', + intermediate: 'Intermediate', + advanced: 'Advanced', +} + +export const HYDROGRAPH_UI_MODE_DESCRIPTIONS: Record = + { + simple: + 'Pressure transducer corrections only (Diver Office): convert water head, remove offsets and zeros, shift, and snap to a manual measurement.', + intermediate: + 'Adds the other logger sources, spurious reflection removal, correction thresholds, and the data table.', + advanced: + 'Everything: reflection detection methods, temperature assist, and interpolation across removals.', + } + +const MODE_RANK: Record = { + simple: 0, + intermediate: 1, + advanced: 2, +} + +export const DEFAULT_HYDROGRAPH_UI_MODE: HydrographUiMode = 'simple' + +// Modes a user can currently select. Intermediate and Advanced are built but +// switched off, so their buttons render disabled rather than disappearing — +// re-enable by adding them back here. +export const ENABLED_HYDROGRAPH_UI_MODES: readonly HydrographUiMode[] = [ + 'simple', +] + +export const isHydrographUiModeEnabled = (mode: HydrographUiMode) => + ENABLED_HYDROGRAPH_UI_MODES.includes(mode) + +/** True when `mode` exposes at least as much as `minimum`. */ +export const isAtLeastMode = ( + mode: HydrographUiMode, + minimum: HydrographUiMode +) => MODE_RANK[mode] >= MODE_RANK[minimum] + +export const isHydrographUiMode = ( + value: unknown +): value is HydrographUiMode => + typeof value === 'string' && + (HYDROGRAPH_UI_MODES as readonly string[]).includes(value) + +export const HYDROGRAPH_UI_MODE_STORAGE_KEY = 'ocotillo.hydrographCorrection.uiMode' + +export const readStoredHydrographUiMode = (): HydrographUiMode => { + try { + const stored = window.localStorage.getItem(HYDROGRAPH_UI_MODE_STORAGE_KEY) + // A mode persisted before it was switched off must not stick. + return isHydrographUiMode(stored) && isHydrographUiModeEnabled(stored) + ? stored + : DEFAULT_HYDROGRAPH_UI_MODE + } catch { + // Private-mode / disabled storage: fall back to the default. + return DEFAULT_HYDROGRAPH_UI_MODE + } +} + +/** + * Mode state for the corrector, persisted like the app color mode so a user + * who works in Advanced does not get dropped back to Simple on every visit. + */ +export const useHydrographUiMode = () => { + const [mode, setModeState] = useState( + readStoredHydrographUiMode + ) + + const setMode = useCallback((next: HydrographUiMode) => { + if (!isHydrographUiModeEnabled(next)) return + + setModeState(next) + try { + window.localStorage.setItem(HYDROGRAPH_UI_MODE_STORAGE_KEY, next) + } catch { + // Persistence is best-effort; the session still switches modes. + } + }, []) + + return { mode, setMode } +} diff --git a/src/components/ListPage.tsx b/src/components/ListPage.tsx index 3e7e3633..90a8705d 100644 --- a/src/components/ListPage.tsx +++ b/src/components/ListPage.tsx @@ -15,6 +15,7 @@ import { useGridSelector, GridColDef, GridRowParams, + MuiEvent, } from '@mui/x-data-grid' import { settings } from '@/settings' import React, { useMemo, useState } from 'react' @@ -22,6 +23,7 @@ import { useNavigate } from 'react-router' import { CanAccess, useExport, + useGetToPath, useNavigation, useResourceParams, } from '@refinedev/core' @@ -154,6 +156,26 @@ function ListPageToolbar({ ) } +/** + * DataGrid rows are not anchors, so modifier clicks would otherwise navigate in + * place. Treat the browser conventions for "open elsewhere" as new-window intent. + */ +export function isNewWindowClick(event: { + ctrlKey?: boolean + metaKey?: boolean + button?: number +}): boolean { + // Shift is left alone: the DataGrid uses it for row range selection. + return Boolean(event.ctrlKey || event.metaKey || event.button === 1) +} + +export function openInNewWindow(href: string) { + // Router paths are basename-relative; window.open is not. + const target = href.startsWith('/') ? `${settings.urlprefix}${href}` : href + const opened = window.open(target, '_blank', 'noopener,noreferrer') + if (opened) opened.opener = null +} + type ListPageProps = { title?: string description?: string @@ -182,6 +204,8 @@ type ListPageProps = { hideBreadcrumb?: boolean /** Hide create/edit header buttons and default export */ hideHeaderButtons?: boolean + /** Explicit access-control resource when Refine cannot infer it from custom routes. */ + accessResource?: string } export const ListPage: React.FC = ({ @@ -207,6 +231,7 @@ export const ListPage: React.FC = ({ hideBreadcrumb = false, hideHeaderButtons = false, getRowHref, + accessResource, }) => { if (!exportProps) { exportProps = { pageSize: 1000 } @@ -219,6 +244,7 @@ export const ListPage: React.FC = ({ const { show } = useNavigation() const { resource } = useResourceParams() + const canAccessResource = accessResource ?? resource?.name const handleSelectionChangeWrapper = (selectionModel: any) => { if (onSelectionChange) { @@ -234,7 +260,9 @@ export const ListPage: React.FC = ({ }) => { return ( <> - {defaultButtons} + + {defaultButtons} + = ({ hideExport: restDataGridProps.paginationMode === 'server', } - const handleRowClick = getRowHref - ? (params: GridRowParams) => { - onRowClick?.(params) - navigate(getRowHref(params)) - } - : disableRowClick - ? onRowClick - ? (params: GridRowParams) => onRowClick(params) - : undefined - : resource - ? (params: GridRowParams) => { - onRowClick?.(params) + const getToPath = useGetToPath() + + // Href for the row's destination, used for modifier-click new-window opens. + const resolveRowHref = (params: GridRowParams): string | undefined => { + if (getRowHref) return getRowHref(params) + if (disableRowClick || !resource) return undefined + return getToPath({ + resource, + action: 'show', + meta: { id: params.id }, + }) + } + + const rowClickNavigates = Boolean( + getRowHref || (!disableRowClick && resource) + ) + + const handleRowClick = + rowClickNavigates || onRowClick + ? (params: GridRowParams, event: MuiEvent) => { + onRowClick?.(params) + + if (!rowClickNavigates) return + + const href = resolveRowHref(params) + if (href && isNewWindowClick(event)) { + openInNewWindow(href) + return + } + + if (getRowHref) { + navigate(getRowHref(params)) + return + } + + if (resource) { show(resource.name, params.id as string | number) } - : undefined + } + : undefined const rowCursor = getRowHref || (!disableRowClick && resource) ? 'pointer' : 'default' return ( - + null : headerButtons || defaultHeaderButtons diff --git a/src/components/MapComponent.tsx b/src/components/MapComponent.tsx index fdf50df5..86a8e931 100644 --- a/src/components/MapComponent.tsx +++ b/src/components/MapComponent.tsx @@ -1,18 +1,23 @@ import { useCallback, useContext, useEffect, useRef, useState } from 'react' -import { Map, MapRef, NavigationControl, Popup } from 'react-map-gl' -import { ControlPosition } from 'react-map-gl' +import { Map, MapRef, NavigationControl, Popup } from 'react-map-gl/maplibre' +import { ControlPosition } from 'react-map-gl/maplibre' import { CircularProgress } from '@mui/material' -import type { MapLayerMouseEvent, MapGeoJSONFeature } from 'react-map-gl' +import type { + MapLayerMouseEvent, + MapGeoJSONFeature, +} from 'react-map-gl/maplibre' import DrawControl from './DrawControl' -import { settings } from '@/settings' - import { ColorModeContext } from '@/contexts' -import { DEFAULT_MAPBOX_BASEMAP, THEMED_MAPBOX_BASEMAPS } from '@/constants' +import { + DEFAULT_BASEMAP_ID, + THEMED_BASEMAP_IDS, + getBasemapStyle, +} from '@/basemaps' -import 'mapbox-gl/dist/mapbox-gl.css' +import 'maplibre-gl/dist/maplibre-gl.css' type SelectionPolygons = Record @@ -33,8 +38,8 @@ interface MapComponentProps { showNavigation?: { show: boolean; position?: ControlPosition } isLoading?: boolean mapRef?: any - basemapUri?: string - onBasemapChange?: (nextBasemap: string) => void + basemapId?: string + onBasemapChange?: (nextBasemapId: string) => void initialViewState?: { longitude: number @@ -67,7 +72,7 @@ export const MapComponent = ({ show: true, position: 'top-right' as ControlPosition, }, - basemapUri = DEFAULT_MAPBOX_BASEMAP, + basemapId = DEFAULT_BASEMAP_ID, onBasemapChange, style = { width: '100%', height: '100%' }, containerRef, @@ -111,15 +116,15 @@ export const MapComponent = ({ return } - const currentThemedBasemap = THEMED_MAPBOX_BASEMAPS[previousMode].uri - const nextThemedBasemap = THEMED_MAPBOX_BASEMAPS[nextMode].uri + const currentThemedBasemap = THEMED_BASEMAP_IDS[previousMode] + const nextThemedBasemap = THEMED_BASEMAP_IDS[nextMode] - if (basemapUri === currentThemedBasemap) { + if (basemapId === currentThemedBasemap) { onBasemapChange?.(nextThemedBasemap) } previousModeRef.current = nextMode - }, [mode, basemapUri, mapRef, onBasemapChange]) + }, [mode, basemapId, mapRef, onBasemapChange]) useEffect(() => { if (!isRectangleDrawInteractionActive || !setPopupContent) return @@ -272,9 +277,7 @@ export const MapComponent = ({ return ( { @@ -282,8 +285,11 @@ export const MapComponent = ({ emitBoundsChange() }} onMouseMove={onMouseMove} + // Style, tile, and glyph failures are otherwise silent — the map just + // renders empty. Surface them so a broken basemap source is diagnosable. + onError={(event) => console.error('Map error:', event.error)} style={style} - mapStyle={basemapUri} + mapStyle={getBasemapStyle(basemapId)} > {showNavigation?.show && ( diff --git a/src/components/MapGeocoderSearch.tsx b/src/components/MapGeocoderSearch.tsx new file mode 100644 index 00000000..c1f6875a --- /dev/null +++ b/src/components/MapGeocoderSearch.tsx @@ -0,0 +1,200 @@ +import { Close, Search } from '@mui/icons-material' +import { + Box, + CircularProgress, + IconButton, + InputAdornment, + List, + ListItemButton, + ListItemText, + Paper, + TextField, + Typography, +} from '@mui/material' +import { useQuery } from '@tanstack/react-query' +import { useEffect, useMemo, useRef, useState } from 'react' + +import { type GeocodeResult, geocodePlaces } from '@/utils/geocode' + +const MIN_QUERY_LENGTH = 3 +const DEBOUNCE_MS = 300 + +interface MapGeocoderSearchProps { + onSelect: (result: GeocodeResult) => void + onClear?: () => void + /** Map center used to bias results toward what the user is looking at. */ + proximity?: [number, number] + placeholder?: string +} + +export const MapGeocoderSearch = ({ + onSelect, + onClear, + proximity, + placeholder = 'Search place, address, or ZIP', +}: MapGeocoderSearchProps) => { + const [value, setValue] = useState('') + const [debouncedValue, setDebouncedValue] = useState('') + const [isOpen, setIsOpen] = useState(false) + const blurTimeoutRef = useRef | null>(null) + + useEffect(() => { + const timeout = setTimeout(() => setDebouncedValue(value), DEBOUNCE_MS) + return () => clearTimeout(timeout) + }, [value]) + + useEffect( + () => () => { + if (blurTimeoutRef.current) clearTimeout(blurTimeoutRef.current) + }, + [] + ) + + const trimmedQuery = debouncedValue.trim() + const isQueryable = trimmedQuery.length >= MIN_QUERY_LENGTH + + // Rounded so small map movements do not invalidate the cached query. + const proximityKey = useMemo( + () => + proximity + ? `${proximity[0].toFixed(2)},${proximity[1].toFixed(2)}` + : 'none', + [proximity] + ) + + const { + data: results = [], + isFetching, + isError, + } = useQuery({ + queryKey: ['photon-geocode', trimmedQuery, proximityKey], + queryFn: ({ signal }) => geocodePlaces(trimmedQuery, { proximity, signal }), + enabled: isQueryable, + staleTime: 5 * 60 * 1000, + }) + + const clear = () => { + setValue('') + setDebouncedValue('') + setIsOpen(false) + onClear?.() + } + + const select = (result: GeocodeResult) => { + setValue(result.label) + setDebouncedValue(result.label) + setIsOpen(false) + onSelect(result) + } + + const showDropdown = isOpen && isQueryable + const hasNoResults = !isFetching && !isError && results.length === 0 + + return ( + + event.stopPropagation()} + onChange={(event) => { + setValue(event.target.value) + setIsOpen(true) + }} + onFocus={() => setIsOpen(true)} + onBlur={() => { + // Delay so a result click registers before the dropdown unmounts. + blurTimeoutRef.current = setTimeout(() => setIsOpen(false), 150) + }} + onKeyDown={(event) => { + if (event.key === 'Escape') { + clear() + return + } + if (event.key === 'Enter' && results[0]) { + event.preventDefault() + select(results[0]) + } + }} + InputProps={{ + startAdornment: ( + + + + ), + endAdornment: ( + + {isFetching ? : null} + {value ? ( + event.preventDefault()} + onClick={clear} + > + + + ) : null} + + ), + }} + /> + {showDropdown && ( + + {isError ? ( + + Search is unavailable right now. + + ) : hasNoResults ? ( + + No matches found. + + ) : ( + <> + + {results.map((result) => ( + event.stopPropagation()} + onClick={() => select(result)} + > + + + ))} + + + Search by Photon · © OpenStreetMap contributors + + + )} + + )} + + ) +} + +export default MapGeocoderSearch diff --git a/src/components/MapPopupComponent.tsx b/src/components/MapPopupComponent.tsx index 5ff1fdcd..f8a34b11 100644 --- a/src/components/MapPopupComponent.tsx +++ b/src/components/MapPopupComponent.tsx @@ -120,20 +120,22 @@ const getFeatureType = (properties: Record): string => const getLayerLabel = (layerKey: string): string => { const labelByLayer: Record = { - 'ogc-latest-depth-to-water': 'Latest Depth to Water', - 'ogc-average-tds': 'Average TDS', 'ogc-latest-tds': 'Latest TDS', 'ogc-major-chemistry': 'Major Chemistry', 'ogc-minor-chemistry': 'Minor Chemistry', 'ogc-depth-to-water-trend': 'Depth to Water Trend', 'ogc-water-elevation-points': 'Water Elevation', - 'ogc-water-elevation-contours': 'Water Elevation Contours', 'ogc-water-well-summary': 'Water Well Summary', 'ogc-water-wells': 'Water Wells', 'ogc-actively-monitored': 'Actively Monitored', 'ogc-springs': 'Springs', 'ogc-project-areas': 'AMP Project Areas', - 'ogc-locations': 'Locations', + 'ogc-geothermal-wells-bht': 'Geothermal Wells (BHT)', + 'ogc-geothermal-wells-temperature-profile': 'Geothermal Wells (Temp-Depth)', + 'ogc-bht-measurements': 'BHT Measurements', + 'ogc-temp-depth-measurements': 'Temperature-Depth Measurements', + 'ogc-heat-flow': 'Heat Flow', + 'ogc-dst': 'Drill Stem Tests', } return labelByLayer[layerKey] || titleCase(layerKey.replace(/^ogc-/, '')) @@ -149,8 +151,6 @@ const isTypeImplicitFromLayer = ( [ 'ogc-water-wells', 'ogc-water-well-summary', - 'ogc-latest-depth-to-water', - 'ogc-average-tds', 'ogc-latest-tds', 'ogc-major-chemistry', 'ogc-minor-chemistry', @@ -203,38 +203,6 @@ const buildFeatureRows = ( const releaseStatus = getString(properties, 'release_status') const layerSpecificRowsByLayer: Record = { - 'ogc-latest-depth-to-water': [ - makeRow( - 'Latest Depth to Water', - formatNumberWithUnit(getNumber(properties, 'depth_to_water_bgs'), 'ft bgs') - ), - makeRow('Observation Date', formatDate(properties.observation_datetime)), - makeRow( - 'Reference Elevation', - formatNumberWithUnit(getNumber(properties, 'depth_to_water_reference'), 'ft') - ), - makeRow( - 'Measuring Point Height', - formatNumberWithUnit(getNumber(properties, 'measuring_point_height'), 'ft') - ), - ], - 'ogc-average-tds': [ - makeRow( - 'Average TDS', - formatNumberWithUnit(getNumber(properties, 'avg_tds_value'), 'mg/L') - ), - makeRow( - 'Records Used', - getNumber(properties, 'tds_observation_count')?.toString() - ), - makeRow( - 'Date Range', - formatDateRange( - getString(properties, 'first_tds_observation_date'), - getString(properties, 'last_tds_observation_date') - ) - ), - ], 'ogc-latest-tds': [ makeRow( 'Latest TDS', @@ -465,17 +433,6 @@ const buildFeatureRows = ( makeRow('Release Status', releaseStatus && titleCase(releaseStatus)), makeRow('Formation Zone', getString(properties, 'nma_formation_zone')), ], - 'ogc-locations': [ - makeRow( - 'Elevation', - formatNumberWithUnit(getNumber(properties, 'elevation'), 'ft') - ), - makeRow('County', getString(properties, 'county')), - makeRow('State', getString(properties, 'state')), - makeRow('Quad', getString(properties, 'quad_name')), - makeRow('Release Status', releaseStatus && titleCase(releaseStatus)), - makeRow('Description', getString(properties, 'description')), - ], } const layerSpecificRows = layerSpecificRowsByLayer[layerKey] diff --git a/src/components/SpatialSearchComponent.tsx b/src/components/SpatialSearchComponent.tsx index f4e760ad..b0957e3b 100644 --- a/src/components/SpatialSearchComponent.tsx +++ b/src/components/SpatialSearchComponent.tsx @@ -4,7 +4,7 @@ import wellknown from 'wellknown' import { Place } from '@mui/icons-material' import { Box } from '@mui/system' import MapComponent from '@/components/MapComponent' -import { MapRef } from 'react-map-gl' +import { MapRef } from 'react-map-gl/maplibre' import Grid from '@mui/material/Grid2' interface SpatialSearchComponentProps { diff --git a/src/components/WellShow/MonitoringInfo.tsx b/src/components/WellShow/MonitoringInfo.tsx index 3814dfe8..73787409 100644 --- a/src/components/WellShow/MonitoringInfo.tsx +++ b/src/components/WellShow/MonitoringInfo.tsx @@ -153,7 +153,9 @@ const FrequencyRow = ({ freq: { monitoring_frequency: string; start_date: string; end_date: string | null } active: boolean }) => ( - + // Rendered as a div, not the default

: Chip renders a

, and a
+ // inside a

is invalid nesting that React warns about. + {active && ( + value.startsWith('https://') + +// Labels carry the source's own definition of the field, so they get a tooltip +// rather than burying the definition in a column the reader has to scroll to. +const ItemLabel = ({ item }: { item: InfoItem }) => { + const label = ( + + {item.label} + + ) + + return item.description ? ( + + {label} + + ) : ( + label + ) +} + +const SummaryView = ({ sections }: { sections: InfoSection[] }) => ( + + {sections.map((section) => ( + + + {section.title} + + + {section.items.map((item) => [ + , + + {item.href && isHttpsUrl(item.href) ? ( + {item.value} + ) : ( + item.value + )} + , + ])} + + + ))} + +) + +const RawView = ({ rows }: { rows: RawAttributeRow[] }) => { + const columns = useMemo[]>( + () => [ + { field: 'field', headerName: 'Attribute', minWidth: 130, flex: 0.6 }, + { field: 'label', headerName: 'Name', minWidth: 160, flex: 0.8 }, + { + field: 'value', + headerName: 'Value', + minWidth: 180, + flex: 1, + renderCell: ({ value }) => + typeof value === 'string' && isHttpsUrl(value) ? ( + Open link + ) : ( + value + ), + }, + { + field: 'description', + headerName: 'Description', + minWidth: 240, + flex: 1.4, + renderCell: ({ value }) => ( + + {value} + + ), + }, + ], + [] + ) + + return ( + + rows={rows} + columns={columns} + getRowId={(row) => row.id} + rowHeight={settings.rowHeight} + disableRowSelectionOnClick + pageSizeOptions={[10, 25, 50, 100]} + initialState={{ + pagination: { paginationModel: { pageSize: 25, page: 0 } }, + }} + sx={{ + border: 'none', + '& .MuiDataGrid-cell': { + borderBottom: '1px solid #f0f0f0', + }, + }} + /> + ) +} + +/** + * Card shell for the external-source records on the well details page (OSE POD, + * USGS). Shows a consolidated summary by default and keeps the full attribute + * table one click away. + */ +export const AttributeInfoCard = ({ + icon, + title, + sections, + rawRows, + emptyMessage, + errorMessage, + isLoading, + isError, +}: AttributeInfoCardProps) => { + const [showRaw, setShowRaw] = useState(false) + + const hasData = sections.length > 0 || rawRows.length > 0 + + return ( + + + + {hasData && ( + + )} + + + {isLoading && ( + + {[0, 1, 2, 3, 4].map((row) => ( + + ))} + + )} + + {!isLoading && isError && ( + + {errorMessage} + + )} + + {!isLoading && !isError && !hasData && ( + + {emptyMessage} + + )} + + {!isLoading && !isError && hasData && ( + <> + {showRaw ? ( + + ) : ( + + )} + + )} + + + ) +} diff --git a/src/components/card/InteractiveSatelliteMap.tsx b/src/components/card/InteractiveSatelliteMap.tsx index 8d9128da..4878d4c2 100644 --- a/src/components/card/InteractiveSatelliteMap.tsx +++ b/src/components/card/InteractiveSatelliteMap.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, useState } from 'react' +import { useContext, useEffect, useMemo, useRef, useState } from 'react' import wellknown from 'wellknown' import { IWell } from '@/interfaces/ocotillo' import type { IGroup } from '@/interfaces/ocotillo/IGroup' @@ -15,13 +15,50 @@ import { Typography, } from '@mui/material' import { ContentCopy, Directions, Map } from '@mui/icons-material' -import { Layer, MapRef, Source } from 'react-map-gl' -import { MapComponent, MapPopup, CardHeaderTitle } from '@/components' +import { Layer, MapRef, Source } from 'react-map-gl/maplibre' +import { + BasemapControl, + MapComponent, + MapPopup, + CardHeaderTitle, +} from '@/components' import { useLayer } from '@/hooks' import { useGo } from '@refinedev/core' +import { captureEvent } from '@/analytics/posthog' +import { ColorModeContext } from '@/contexts' +import { THEMED_BASEMAP_IDS } from '@/basemaps' +import { + MAP_HIGHLIGHT_COLOR, + MAP_HIGHLIGHT_STROKE_COLOR, + MAP_LAYER_COLORS, + MAP_SYMBOL_STROKE_COLOR, +} from '@/constants/mapColors' const MAP_HEIGHT = 450 +/** + * Basemap state for a map card. Seeded from the active color mode so the map + * matches the app theme on first paint; MapComponent keeps the two in sync + * until the user picks a basemap of their own. + */ +const useCardBasemap = (surface: 'well' | 'project') => { + const { mode } = useContext(ColorModeContext) + const [basemapId, setBasemapId] = useState( + () => THEMED_BASEMAP_IDS[mode === 'dark' ? 'dark' : 'light'] + ) + + const onBasemapChange = (nextBasemap: string) => { + setBasemapId(nextBasemap) + } + + const onUserBasemapChange = (nextBasemap: string) => { + setBasemapId(nextBasemap) + captureEvent('map_basemap_changed', { basemap: nextBasemap, surface }) + } + + return { basemapId, onBasemapChange, onUserBasemapChange } +} + const MapCardHeader = ({ title }: { title: string }) => ( } title={title} /> ) @@ -108,6 +145,8 @@ const ProjectMapView = ({ const containerRef = useRef(null) const [popupContent, setPopupContent] = useState(null) const go = useGo() + const { basemapId, onBasemapChange, onUserBasemapChange } = + useCardBasemap('project') const boundary = useMemo(() => parseProjectArea(projectArea), [projectArea]) const wellsFeatureCollection = useMemo( @@ -247,6 +286,8 @@ const ProjectMapView = ({ onMouseMoveCallback={onMapMouseMove} setPopupContent={setPopupContent} popupContent={popupContent} + basemapId={basemapId} + onBasemapChange={onBasemapChange} style={{ flex: 1, width: '100%', height: '100%' }} containerRef={containerRef} > @@ -281,14 +322,18 @@ const ProjectMapView = ({ type="circle" paint={{ 'circle-radius': 6, - 'circle-color': '#2b7dc0', - 'circle-stroke-color': '#ffffff', + 'circle-color': MAP_LAYER_COLORS.waterWells, + 'circle-stroke-color': MAP_SYMBOL_STROKE_COLOR, 'circle-stroke-width': 2, }} /> ) : null} + )} @@ -303,11 +348,13 @@ const WellMapView = ({ well }: { well: IWell }) => { const waterWellsLayer = useLayer({ thing_type: 'water well', label: 'Water Wells', - color: '#2b7dc0', + color: MAP_LAYER_COLORS.waterWells, enabled: loadNearbyWells, }) const [popupContent, setPopupContent] = useState(null) const go = useGo() + const { basemapId, onBasemapChange, onUserBasemapChange } = + useCardBasemap('well') const sourceProps = waterWellsLayer?.sourceProps const layerProps = waterWellsLayer?.layerProps @@ -486,6 +533,8 @@ const WellMapView = ({ well }: { well: IWell }) => { onMouseMoveCallback={onMapMouseMove} setPopupContent={setPopupContent} popupContent={popupContent} + basemapId={basemapId} + onBasemapChange={onBasemapChange} style={{ flex: 1, width: '100%', height: '100%' }} containerRef={containerRef} > @@ -501,14 +550,15 @@ const WellMapView = ({ well }: { well: IWell }) => { type="circle" paint={{ 'circle-radius': 6, - 'circle-color': '#ff4d4d', - 'circle-stroke-color': '#ffffff', + 'circle-color': MAP_HIGHLIGHT_COLOR, + 'circle-stroke-color': MAP_HIGHLIGHT_STROKE_COLOR, 'circle-stroke-width': 2, }} /> ) : null} + {locationNote ? ( <> diff --git a/src/components/card/OSEPODInfo.tsx b/src/components/card/OSEPODInfo.tsx index 1e5b3acf..d2cb7284 100644 --- a/src/components/card/OSEPODInfo.tsx +++ b/src/components/card/OSEPODInfo.tsx @@ -1,22 +1,8 @@ -import { useMemo } from 'react' -import { Box, Paper, Typography } from '@mui/material' -import { DataGrid, GridColDef } from '@mui/x-data-grid' import { Engineering } from '@mui/icons-material' +import { useMemo } from 'react' import { useOSEPODInfo } from '@/hooks' -import { ExternalLink } from '@/components' -import { settings } from '@/settings' - -type InfoRow = { - id: number - name: string - value: unknown -} - -// Detects https values so they render as ExternalLink instead of raw text. -const isHttpsUrl = ( - value: unknown -): value is `https://${string}` => - typeof value === 'string' && value.startsWith('https://') +import { buildOSEPODRawRows, buildOSEPODSections } from '@/utils/osePodSummary' +import { AttributeInfoCard } from './AttributeInfoCard' type OSEPODInfoCardProps = { pod_id: string @@ -24,102 +10,21 @@ type OSEPODInfoCardProps = { export const OSEPODInfoCard = ({ pod_id }: OSEPODInfoCardProps) => { const podInfoQuery = useOSEPODInfo(pod_id) - const rows = useMemo(() => { - // Pin URL rows to the top so the useful links are easy to find for users. - return [...(podInfoQuery.data ?? [])].sort((a, b) => { - const aIsUrl = isHttpsUrl(a.value) - const bIsUrl = isHttpsUrl(b.value) - - if (aIsUrl === bIsUrl) return 0 - return aIsUrl ? -1 : 1 - }) - }, [podInfoQuery.data]) + const attributes = podInfoQuery.data - const columns = useMemo[]>( - () => [ - { - field: 'name', - headerName: 'Name', - minWidth: 175, - flex: 0.8, - headerAlign: 'left', - align: 'left', - }, - { - field: 'value', - headerName: 'Value', - minWidth: 250, - flex: 1.2, - headerAlign: 'left', - align: 'left', - renderCell: ({ row, value }) => { - if (isHttpsUrl(value)) { - return NMWRRS Website Link - } - - return value == null ? '' : String(value) - }, - }, - ], - [] - ) + const sections = useMemo(() => buildOSEPODSections(attributes), [attributes]) + const rawRows = useMemo(() => buildOSEPODRawRows(attributes), [attributes]) return ( - - - - - OSEPOD Information - - - - {podInfoQuery.data?.length === 0 && ( - - No OSE POD data available for this well. - - )} - {podInfoQuery.isError && ( - - Error fetching OSE POD info. - - )} - {rows.length > 0 && ( - - rows={rows} - columns={columns} - getRowId={(row) => row.id} - rowHeight={settings.rowHeight} - disableRowSelectionOnClick - pageSizeOptions={[10, 25, 50]} - initialState={{ - pagination: { - paginationModel: { pageSize: 10, page: 0 }, - }, - }} - sx={{ - border: 'none', - '& .MuiDataGrid-cell': { - borderBottom: '1px solid #f0f0f0', - }, - }} - /> - )} - - + } + title="OSE POD Information" + sections={sections} + rawRows={rawRows} + emptyMessage="No OSE POD data available for this well." + errorMessage="Error fetching OSE POD info." + isLoading={podInfoQuery.isLoading} + isError={podInfoQuery.isError} + /> ) } diff --git a/src/components/card/USGSInfo.tsx b/src/components/card/USGSInfo.tsx index 4f64e2e7..05f69d94 100644 --- a/src/components/card/USGSInfo.tsx +++ b/src/components/card/USGSInfo.tsx @@ -1,22 +1,8 @@ +import { Public } from '@mui/icons-material' import { useMemo } from 'react' -import { Box, Paper, Typography } from '@mui/material' -import { ExternalLink } from '@/components' -import { DataGrid, GridColDef } from '@mui/x-data-grid' import { useUSGSSiteInfo } from '@/hooks' -import { Public } from '@mui/icons-material' -import { settings } from '@/settings' - -type InfoRow = { - id: number - name: string - value: string -} - -// Detects https values so they render as ExternalLink instead of raw text. -const isHttpsUrl = ( - value: unknown -): value is `https://${string}` => - typeof value === 'string' && value.startsWith('https://') +import { buildUSGSRawRows, buildUSGSSections } from '@/utils/usgsSiteSummary' +import { AttributeInfoCard } from './AttributeInfoCard' type USGSInfoCardProps = { site_id: string @@ -24,102 +10,21 @@ type USGSInfoCardProps = { export const USGSInfoCard = ({ site_id }: USGSInfoCardProps) => { const query = useUSGSSiteInfo(site_id) - const rows = useMemo(() => { - // Pin URL rows to the top so the useful links are easy to find. - return [...(query.data ?? [])].sort((a, b) => { - const aIsUrl = isHttpsUrl(a.value) - const bIsUrl = isHttpsUrl(b.value) - - if (aIsUrl === bIsUrl) return 0 - return aIsUrl ? -1 : 1 - }) - }, [query.data]) + const info = query.data - const columns = useMemo[]>( - () => [ - { - field: 'name', - headerName: 'Name', - minWidth: 175, - flex: 0.8, - headerAlign: 'left', - align: 'left', - }, - { - field: 'value', - headerName: 'Value', - minWidth: 250, - flex: 1.2, - headerAlign: 'left', - align: 'left', - renderCell: ({ row, value }) => { - if (isHttpsUrl(value)) { - return Water Services API - } - - return value == null ? '' : String(value) - }, - }, - ], - [] - ) + const sections = useMemo(() => buildUSGSSections(info), [info]) + const rawRows = useMemo(() => buildUSGSRawRows(info), [info]) return ( - - - - - USGS Information - - - - {query.data?.length == 0 && ( - - No USGS data available for this well. - - )} - {query.isError && ( - - Error fetching USGS info. - - )} - {rows.length > 0 && ( - - rows={rows} - columns={columns} - getRowId={(row) => row.id} - rowHeight={settings.rowHeight} - disableRowSelectionOnClick - pageSizeOptions={[10, 25, 50]} - initialState={{ - pagination: { - paginationModel: { pageSize: 10, page: 0 }, - }, - }} - sx={{ - border: 'none', - '& .MuiDataGrid-cell': { - borderBottom: '1px solid #f0f0f0', - }, - }} - /> - )} - - + } + title="USGS Information" + sections={sections} + rawRows={rawRows} + emptyMessage="No USGS data available for this well." + errorMessage="Error fetching USGS info." + isLoading={query.isLoading} + isError={query.isError} + /> ) } diff --git a/src/components/card/index.ts b/src/components/card/index.ts index 9a7efd77..75723b2c 100644 --- a/src/components/card/index.ts +++ b/src/components/card/index.ts @@ -1,7 +1,8 @@ +export * from './AttributeInfoCard' +export * from './CardHeaderTitle' export * from './CoreWellInfo' -export * from './InteractiveSatelliteMap' export * from './Hydrograph' -export * from './RecentWaterLevelObservations' +export * from './InteractiveSatelliteMap' export * from './OSEPODInfo' +export * from './RecentWaterLevelObservations' export * from './USGSInfo' -export * from './CardHeaderTitle' diff --git a/src/components/form/group/CreateEditGroup.tsx b/src/components/form/group/CreateEditGroup.tsx index 0c8951fb..6bfce0ff 100644 --- a/src/components/form/group/CreateEditGroup.tsx +++ b/src/components/form/group/CreateEditGroup.tsx @@ -20,7 +20,7 @@ import Grid from '@mui/material/Grid2' import wellknown from 'wellknown' import { useLexicon } from '@/hooks' import { ControlledSelectField } from '@/components/Controlled/ControlledSelectField' -import { MapRef } from 'react-map-gl' +import { MapRef } from 'react-map-gl/maplibre' type SelectionPolygon = { geometry: GeoJSON.Geometry } diff --git a/src/components/form/location/CreateEditLocation.tsx b/src/components/form/location/CreateEditLocation.tsx index f3219034..de9c59ab 100644 --- a/src/components/form/location/CreateEditLocation.tsx +++ b/src/components/form/location/CreateEditLocation.tsx @@ -17,7 +17,7 @@ import { MapComponent, } from '@/components' import { useLexicon } from '@/hooks' -import { MapRef, ViewState, Source, Layer } from 'react-map-gl' +import { MapRef, ViewState, Source, Layer } from 'react-map-gl/maplibre' import { Typography, FormControlLabel, diff --git a/src/components/form/thing/SelectThingComponent.tsx b/src/components/form/thing/SelectThingComponent.tsx index 2e415293..6f2a7328 100644 --- a/src/components/form/thing/SelectThingComponent.tsx +++ b/src/components/form/thing/SelectThingComponent.tsx @@ -4,7 +4,7 @@ import { IWell } from '@/interfaces/ocotillo/IWell' import { Controller } from 'react-hook-form' import Autocomplete from '@mui/material/Autocomplete' import TextField from '@mui/material/TextField' -import { Layer, LngLatBoundsLike, MapRef, Source } from 'react-map-gl' +import { Layer, LngLatBoundsLike, MapRef, Source } from 'react-map-gl/maplibre' import MapComponent from '@/components/MapComponent' import { useEffect, useRef, useState } from 'react' import { Card, Typography, useTheme } from '@mui/material' diff --git a/src/components/grid/EditableDataGrid.tsx b/src/components/grid/EditableDataGrid.tsx new file mode 100644 index 00000000..91822a22 --- /dev/null +++ b/src/components/grid/EditableDataGrid.tsx @@ -0,0 +1,328 @@ +import { useCallback, useRef, useState } from 'react' +import '@glideapps/glide-data-grid/dist/index.css' +import '@glideapps/glide-data-grid-cells/dist/index.css' +import { + DataEditor, + type DataEditorProps, + type EditableGridCell, + type GridCell, + GridCellKind, + type GridColumn, + type GridMouseEventArgs, + type Item, +} from '@glideapps/glide-data-grid' +import { allCells } from '@glideapps/glide-data-grid-cells' +import { useGdgTheme } from './gdgTheme' +import { useElementSize } from './useElementSize' + +/** Value a single cell can hold. */ +export type CellValue = string | number | boolean | null | undefined + +/** Which Glide cell editor a column renders. */ +export type GridCellType = 'text' | 'number' | 'uri' | 'boolean' | 'dropdown' + +/** + * Entity-agnostic column definition for {@link EditableDataGrid}. + * + * Generic over the row shape `T`. A column reads its display value from a row + * via {@link getValue} and — when editable — produces an updated row via + * {@link setValue}. Both keep the grid decoupled from any particular field + * layout, so the same component drives ocotillo wells, geothermal records, etc. + */ +export interface GridColumnSpec { + /** Stable column id (used as a React/GDG key). */ + id: string + /** Header label. */ + title: string + /** Optional description shown as a tooltip when hovering the column header. */ + tooltip?: string + /** Column width in px. */ + width?: number + /** Optional group header label (for grouped grids). */ + group?: string + /** Editor kind. Defaults to `'text'`. */ + kind?: GridCellType + /** Allowed values for a `'dropdown'` column. Ignored for other kinds. */ + options?: string[] + /** Whether cells in this column can be edited inline. Defaults to `false`. */ + editable?: boolean + /** Read the display value for a row. */ + getValue: (row: T) => CellValue + /** + * Optional display formatter. Overrides the default string rendering for the + * cell without changing the underlying edit value (e.g. round a coordinate + * for display while keeping full precision for save). + */ + format?: (value: CellValue) => string + /** + * Produce an updated row given a new cell value. Required for editable + * columns; ignored otherwise. + */ + setValue?: (row: T, value: CellValue) => T + /** + * Optional validator returning an error message for an invalid value, or + * `undefined` when valid. Reserved for inline validation feedback; not yet + * surfaced by the grid. + */ + validate?: (value: CellValue, row: T) => string | undefined + /** Called when a cell in this column is clicked (e.g. a URI link). */ + onClick?: (row: T, rowIndex: number) => void +} + +export interface EditableDataGridProps + extends Pick< + DataEditorProps, + | 'freezeColumns' + | 'rowMarkers' + | 'rowHeight' + | 'headerHeight' + | 'groupHeaderHeight' + | 'smoothScrollX' + | 'smoothScrollY' + > { + columns: GridColumnSpec[] + rows: T[] + /** + * Called with the full next row array whenever a cell edit lands. Edits are + * applied to a copy — the parent owns the source of truth and can track dirty + * rows for an explicit batch save. + */ + onRowsChange?: (rows: T[]) => void + /** + * Per-row validation errors keyed by column id. Return `undefined` for a row + * with no errors. Errored cells render with an error-tinted background so + * failures (e.g. a rejected batch save) surface inline. + */ + cellErrors?: (rowIndex: number) => Record | undefined + /** Show a centered loading message instead of the grid. */ + isLoading?: boolean + loadingMessage?: string +} + +// Background tint applied to a cell that has a validation error. +const ERROR_CELL_THEME = { bgCell: '#fee2e2', bgCellMedium: '#fee2e2' } + +function toDisplayString(value: CellValue): string { + return value != null ? String(value) : '' +} + +/** + * Reusable spreadsheet-style grid built on Glide Data Grid. + * + * Handles theme, auto-sizing (ResizeObserver), cell-kind dispatch, and + * edit-to-row mapping. Callers supply a typed row array and a column spec; + * edits are lifted back through {@link EditableDataGridProps.onRowsChange}. + */ +export function EditableDataGrid({ + columns, + rows, + onRowsChange, + cellErrors, + isLoading = false, + loadingMessage = 'Loading…', + freezeColumns, + rowMarkers = 'none', + rowHeight = 36, + headerHeight = 38, + groupHeaderHeight, + smoothScrollX = true, + smoothScrollY = true, +}: EditableDataGridProps) { + const theme = useGdgTheme() + const [containerRef, size] = useElementSize() + // Header tooltip: text follows the cursor (fixed viewport position). The + // hovered header's description is tracked in a ref; mousemove positions it. + const hoverTextRef = useRef(null) + const [tooltip, setTooltip] = useState<{ + text: string + x: number + y: number + } | null>(null) + + const onItemHovered = useCallback( + (args: GridMouseEventArgs) => { + const [col, row] = args.location + let text: string | null = null + if (args.kind === 'header') { + text = columns[col]?.tooltip ?? null + } else if (args.kind === 'cell') { + // On an errored cell, show its validation message (guidance). + const colId = columns[col]?.id + text = (colId && cellErrors?.(row)?.[colId]) || null + } + hoverTextRef.current = text + if (!text) setTooltip(null) + }, + [columns, cellErrors] + ) + + const onMouseMove = useCallback((e: React.MouseEvent) => { + const text = hoverTextRef.current + setTooltip(text ? { text, x: e.clientX, y: e.clientY } : null) + }, []) + + const gridColumns: GridColumn[] = columns.map((c) => ({ + id: c.id, + title: c.title, + width: c.width, + ...(c.group ? { group: c.group } : {}), + })) + + const getCellContent = useCallback( + ([col, row]: Item): GridCell => { + const rowData = rows[row] + if (rowData === undefined) { + return { kind: GridCellKind.Loading, allowOverlay: false } + } + const colDef = columns[col] + const value = colDef.getValue(rowData) + const display = colDef.format + ? colDef.format(value) + : toDisplayString(value) + const editable = colDef.editable === true && colDef.setValue !== undefined + const error = cellErrors?.(row)?.[colDef.id] + const errorTheme = error ? { themeOverride: ERROR_CELL_THEME } : {} + + if (colDef.kind === 'uri') { + return { + kind: GridCellKind.Uri, + data: display, + allowOverlay: false, + readonly: true, + hoverEffect: colDef.onClick !== undefined, + ...errorTheme, + } + } + + if (colDef.kind === 'number') { + return { + kind: GridCellKind.Number, + data: typeof value === 'number' ? value : undefined, + displayData: display, + allowOverlay: editable, + readonly: !editable, + ...errorTheme, + } + } + + if (colDef.kind === 'boolean') { + return { + kind: GridCellKind.Boolean, + data: value === true || value === 'true', + allowOverlay: false, + readonly: !editable, + ...errorTheme, + } + } + + if (colDef.kind === 'dropdown') { + return { + kind: GridCellKind.Custom, + allowOverlay: editable, + readonly: !editable, + copyData: display, + data: { + kind: 'dropdown-cell', + value: display, + allowedValues: colDef.options ?? [], + }, + ...errorTheme, + } + } + + return { + kind: GridCellKind.Text, + data: display, + displayData: display, + allowOverlay: editable, + readonly: !editable, + ...errorTheme, + } + }, + [columns, rows, cellErrors] + ) + + const onCellEdited = useCallback( + ([col, row]: Item, newValue: EditableGridCell) => { + const colDef = columns[col] + if (colDef.editable !== true || colDef.setValue === undefined) return + const rowData = rows[row] + if (rowData === undefined) return + + let next: CellValue + if (newValue.kind === GridCellKind.Number) { + next = newValue.data ?? null + } else if (newValue.kind === GridCellKind.Text) { + next = newValue.data === '' ? null : newValue.data + } else if (newValue.kind === GridCellKind.Boolean) { + next = newValue.data === true + } else if (newValue.kind === GridCellKind.Custom) { + // Dropdown cell — read the selected value from its data payload. + const raw = (newValue.data as { value?: string | null })?.value + next = raw == null || raw === '' ? null : raw + } else { + return + } + + const updated = [...rows] + updated[row] = colDef.setValue(rowData, next) + onRowsChange?.(updated) + }, + [columns, rows, onRowsChange] + ) + + const onCellClicked = useCallback( + ([col, row]: Item) => { + const colDef = columns[col] + const rowData = rows[row] + if (colDef?.onClick && rowData !== undefined) colDef.onClick(rowData, row) + }, + [columns, rows] + ) + + return ( +

setTooltip(null)} + className="relative flex flex-col flex-1 min-w-0" + > + {isLoading || size.width === 0 ? ( +
+ {isLoading ? loadingMessage : null} +
+ ) : ( + <> + ({ name: group })} + /> + {tooltip && ( +
+ {tooltip.text} +
+ )} + + )} +
+ ) +} diff --git a/src/components/grid/gdgTheme.ts b/src/components/grid/gdgTheme.ts new file mode 100644 index 00000000..6bda5358 --- /dev/null +++ b/src/components/grid/gdgTheme.ts @@ -0,0 +1,61 @@ +import { useContext } from 'react' +import type { Theme } from '@glideapps/glide-data-grid' +import { ColorModeContext } from '@/contexts' + +// Explicit color themes so the overlay editor's text input is always visible. +// Only colors are set — font properties are left to GDG defaults. +export const GDG_THEME_LIGHT: Partial = { + textDark: '#0f172a', + textMedium: '#475569', + textLight: '#94a3b8', + textHeader: '#64748b', + textHeaderSelected: '#0f172a', + textBubble: '#0f172a', + bgCell: '#ffffff', + bgCellMedium: '#f8fafc', + bgHeader: '#f1f5f9', + bgHeaderHasFocus: '#e2e8f0', + bgHeaderHovered: '#e2e8f0', + bgBubble: '#f1f5f9', + bgBubbleSelected: '#dbeafe', + bgSearchResult: '#fef9c3', + accentColor: '#2563eb', + accentFg: '#ffffff', + accentLight: '#dbeafe', + borderColor: '#e2e8f0', + drilldownBorder: '#e2e8f0', + linkColor: '#2563eb', + bgIconHeader: '#f1f5f9', + fgIconHeader: '#64748b', +} + +export const GDG_THEME_DARK: Partial = { + textDark: '#f1f5f9', + textMedium: '#94a3b8', + textLight: '#64748b', + textHeader: '#94a3b8', + textHeaderSelected: '#f1f5f9', + textBubble: '#f1f5f9', + bgCell: '#18181b', + bgCellMedium: '#27272a', + bgHeader: '#27272a', + bgHeaderHasFocus: '#3f3f46', + bgHeaderHovered: '#3f3f46', + bgBubble: '#27272a', + bgBubbleSelected: '#1e3a5f', + bgSearchResult: '#713f12', + accentColor: '#3b82f6', + accentFg: '#ffffff', + accentLight: '#1e3a5f', + borderColor: '#3f3f46', + drilldownBorder: '#3f3f46', + linkColor: '#60a5fa', + bgIconHeader: '#27272a', + fgIconHeader: '#94a3b8', +} + +/** Pick the Glide Data Grid theme matching the app's current color mode. */ +export function useGdgTheme(): Partial { + const { mode } = useContext(ColorModeContext) + return mode === 'dark' ? GDG_THEME_DARK : GDG_THEME_LIGHT +} diff --git a/src/components/grid/index.ts b/src/components/grid/index.ts new file mode 100644 index 00000000..ee741cf7 --- /dev/null +++ b/src/components/grid/index.ts @@ -0,0 +1,10 @@ +export { EditableDataGrid } from './EditableDataGrid' +export type { + EditableDataGridProps, + GridColumnSpec, + GridCellType, + CellValue, +} from './EditableDataGrid' +export { useElementSize } from './useElementSize' +export type { ElementSize } from './useElementSize' +export { useGdgTheme, GDG_THEME_LIGHT, GDG_THEME_DARK } from './gdgTheme' diff --git a/src/components/grid/useElementSize.ts b/src/components/grid/useElementSize.ts new file mode 100644 index 00000000..860cf8b8 --- /dev/null +++ b/src/components/grid/useElementSize.ts @@ -0,0 +1,38 @@ +import { useEffect, useState } from 'react' + +export interface ElementSize { + width: number + height: number +} + +/** + * Observe an element's content-box size via ResizeObserver. + * + * Returns a callback ref and the current size. The callback ref fires the + * moment the element mounts — including when it mounts inside a portal that + * renders later than the calling component — so the observer always attaches. + */ +export function useElementSize(): [ + (el: HTMLDivElement | null) => void, + ElementSize, +] { + const [el, setEl] = useState(null) + const [size, setSize] = useState({ width: 0, height: 0 }) + + useEffect(() => { + if (!el) return + const observer = new ResizeObserver((entries) => { + const entry = entries[0] + if (entry) { + setSize({ + width: entry.contentRect.width, + height: entry.contentRect.height, + }) + } + }) + observer.observe(el) + return () => observer.disconnect() + }, [el]) + + return [setEl, size] +} diff --git a/src/components/index.ts b/src/components/index.ts index 5c9b65d5..3da9e3f6 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,4 +1,6 @@ export * from './Auth' +export * from './BasemapControl' +export * from './BasemapSelector' export * from './Button' export * from './ContactShow' export * from './WellShow' @@ -15,6 +17,8 @@ export * from './FileSelectionSection' export * from './FilterComponent' export * from './Hydrographs/EditableHydrograph' export * from './Hydrographs/OcotilloHydrographCorrectionWorkbench' +export * from './Hydrographs/HydrographUiModeToggle' +export * from './Hydrographs/hydrographUiMode' export * from './HydrographPngExporter' export * from './LegendComponent' export * from './ListPage' @@ -28,3 +32,4 @@ export * from './ProtectedRoute' export * from './VisuallyHiddenTextField' export * from './WellStatusChips' export * from './WIPAlert' +export * from './MapGeocoderSearch' diff --git a/src/components/layout/sider.tsx b/src/components/layout/sider.tsx index ebe79b21..7a00c81e 100644 --- a/src/components/layout/sider.tsx +++ b/src/components/layout/sider.tsx @@ -79,9 +79,7 @@ export const ThemedSiderV2: React.FC = ({ const isAdminOnly = isResourceListAdminOnly(resourceName) const icon = deprecatedIcon ?? meta?.icon - const derivedLabel = meta?.label || deprecatedLabel || name - const label = - name === 'Sandbox' || name === 'sandbox' ? 'Sandbox' : derivedLabel + const label = meta?.label || deprecatedLabel || name const isSelected = key === selectedKey const isNested = meta?.parent !== undefined const nestedLevel = isNested ? meta?.nestedLevel || 1 : 0 @@ -417,6 +415,7 @@ export const ThemedSiderV2: React.FC = ({ {[ { to: '/about', label: 'About' }, { to: '/ogcapi', label: 'Connect Desktop GIS' }, + { to: '/analytics-disclosure', label: 'Analytics Disclosure' }, { to: '/report-a-bug', label: 'Report a Bug' }, ].map(({ to, label }) => ( diff --git a/src/config/auth.ts b/src/config/auth.ts index 0bbaabe7..a912c516 100644 --- a/src/config/auth.ts +++ b/src/config/auth.ts @@ -1,6 +1,11 @@ export const AUTHENTIK_URL = import.meta.env.VITE_AUTHENTIK_URL || 'http://localhost:8000/' +export const buildAuthentikUrl = ( + path: string, + baseUrl = AUTHENTIK_URL +): URL => new URL(path.replace(/^\/+/, ''), `${baseUrl.replace(/\/+$/, '')}/`) + export const CLIENT_ID = import.meta.env.VITE_AUTHENTIK_CLIENT_ID || 'authentik' const envRedirect = import.meta.env.VITE_AUTHENTIK_REDIRECT_URI diff --git a/src/config/navigation.ts b/src/config/navigation.ts index a4df6f37..f9a9a124 100644 --- a/src/config/navigation.ts +++ b/src/config/navigation.ts @@ -55,9 +55,6 @@ export type NavItem = { children?: NavItem[] } -/** Toggle Example nav (typography, data grid demos). Set true to restore. */ -export const SHOW_EXAMPLE_NAV = false - /** * Top bar: views and tools. * Items without `roles` are visible to every authenticated user. @@ -157,6 +154,6 @@ export const RESOURCE_NAV: NavItem[] = [ href: '/ocotillo/hydrograph-correction', icon: LineChart, resource: 'ocotillo.hydrograph-correction', - roles: adminOnly, + roles: editorAndAbove, }, ] diff --git a/src/constants.ts b/src/constants.ts index dfb05ab4..7b592fd1 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,34 +1,3 @@ -import { MapboxStyleDefinition } from 'mapbox-gl-style-switcher' - -export const MAPBOX_BASEMAPS: MapboxStyleDefinition[] = [ - { title: 'Light', uri: 'mapbox://styles/mapbox/light-v11' }, - { title: 'Dark', uri: 'mapbox://styles/mapbox/dark-v11' }, - { title: 'Streets', uri: 'mapbox://styles/mapbox/streets-v12' }, - { title: 'Outdoors', uri: 'mapbox://styles/mapbox/outdoors-v12' }, - { - title: 'Satellite Streets', - uri: 'mapbox://styles/mapbox/satellite-streets-v12', - }, - { title: 'Satellite', uri: 'mapbox://styles/mapbox/satellite-v9' }, - { title: 'Basic', uri: 'mapbox://styles/mapbox/basic-v9' }, - { title: 'Bright', uri: 'mapbox://styles/mapbox/bright-v9' }, -] - -export const LIGHT_MAPBOX_BASEMAP = 'mapbox://styles/mapbox/light-v11' -export const DARK_MAPBOX_BASEMAP = 'mapbox://styles/mapbox/dark-v11' -export const DEFAULT_MAPBOX_BASEMAP = LIGHT_MAPBOX_BASEMAP - -export const THEMED_MAPBOX_BASEMAPS = { - light: { - title: 'Light', - uri: LIGHT_MAPBOX_BASEMAP, - }, - dark: { - title: 'Dark', - uri: DARK_MAPBOX_BASEMAP, - }, -} as const - export enum GroupType { Wells = 'Wells', Springs = 'Springs', diff --git a/src/constants/mapColors.ts b/src/constants/mapColors.ts new file mode 100644 index 00000000..0e25cfe6 --- /dev/null +++ b/src/constants/mapColors.ts @@ -0,0 +1,91 @@ +// --------------------------------------------------------------------------- +// Map symbol colors +// +// Every point, line, and polygon color on the map comes from the viridis ramp +// (see ./viridis). Two kinds of styling use it differently: +// +// * Classed/continuous layers (TDS, depth to water, water elevation) sample +// the ramp in order, so darker always means lower and yellow means higher. +// * Categorical layers (wells, springs, streams, ...) each pin one fixed +// position on the ramp. Positions are spaced evenly, and the order below +// interleaves related families — wells next to chemistry next to surface +// water — so two layers a user is likely to view together land far apart on +// the ramp and stay easy to tell apart. +// +// Note the inherent limit: a sequential ramp can only carry so many +// categories. With this many layers the gap between adjacent entries is a few +// percent of the ramp, so switching on every layer at once still produces near +// neighbors. In normal use only a handful are visible together. +// --------------------------------------------------------------------------- + +import { VIRIDIS_HIGH, VIRIDIS_LOW, viridisColor } from './viridis' + +/** + * Categorical map layers in ramp order: the first entry gets the dark purple + * end, the last gets yellow, and the rest are spread evenly in between. + */ +const LAYER_RAMP_ORDER = [ + 'locations', + 'waterElevationContours', + 'lakesPondsReservoirs', + 'waterWells', + 'waterElevationPoints', + 'latestDepthToWater', + 'waterWellSummary', + 'perennialStreams', + 'springs', + 'activelyMonitored', + 'meteorologicalStations', + 'latestTds', + 'majorChemistry', + 'averageTds', + 'outfallsReturnFlow', + 'ephemeralStreams', + 'rockSampleLocations', + 'minorChemistry', + 'depthToWaterTrend', + 'surfaceWaterDiversions', + 'projectAreas', + 'soilGasSampleLocations', + 'otherThingTypes', +] as const + +type MapLayerColorKey = (typeof LAYER_RAMP_ORDER)[number] + +/** Viridis hex color for each categorical map layer. */ +export const MAP_LAYER_COLORS = Object.fromEntries( + LAYER_RAMP_ORDER.map((key, index) => [ + key, + viridisColor(index / (LAYER_RAMP_ORDER.length - 1)), + ]) +) as Record + +/** + * Fallback for a layer with no color assigned. Sits mid-ramp so it reads as + * part of the same family rather than as an outlier. + */ +export const MAP_DEFAULT_LAYER_COLOR = viridisColor(0.5) + +/** + * Features in a classed layer whose value is missing or unparseable. Kept + * deliberately outside the viridis ramp so "no data" never looks like a real + * class on the legend gradient. + */ +export const MAP_NO_DATA_COLOR = '#9e9e9e' + +/** White outline that lifts every symbol off the satellite basemap. */ +export const MAP_SYMBOL_STROKE_COLOR = '#ffffff' + +/** + * Translucent white disc drawn behind a highlighted symbol. Neutral on + * purpose — it has to read as a halo, not as another data class. + */ +export const MAP_HIGHLIGHT_HALO_COLOR = '#ffffff' + +/** + * The selected/active symbol. Yellow fill against the dark end of the ramp + * gives the strongest contrast available inside the palette, so a highlighted + * point stands out from both the other points and the imagery underneath. + */ +export const MAP_HIGHLIGHT_COLOR = VIRIDIS_HIGH +export const MAP_HIGHLIGHT_STROKE_COLOR = VIRIDIS_LOW diff --git a/src/constants/osePodDictionary.ts b/src/constants/osePodDictionary.ts new file mode 100644 index 00000000..8865af9e --- /dev/null +++ b/src/constants/osePodDictionary.ts @@ -0,0 +1,2954 @@ +// GENERATED FILE — do not edit by hand. +// Source: nmose_WATERS_PODs_data_dictionary_v8.xlsx (NM OSE WATERS PODs data dictionary). +// Regenerate: python3 scripts/generate_ose_pod_dictionary.py +// +// Keys are the field names returned by the OSE Points of Diversion feature +// service, which truncates the dictionary's column names to 10 characters. +// Service fields with no entry in this dictionary revision: dump_date, license_nb, metered. + +export type OSEPODCodeTable = { + description: string + values: Record +} + +export type OSEPODFieldDefinition = { + /** Column name in the OSE WATERS_PODs table. */ + column: string + /** Short human-readable label for the field. */ + label: string + /** The dictionary's brief description of the field. */ + description: string + dataType: string + /** Key into OSE_POD_CODE_TABLES when the field holds a coded value. */ + codeTable: string | null +} + +export const OSE_POD_CODE_TABLES: Record = { + BASIN_CODE: { + description: + 'Codes for Declared Ground Water Basin designator of the file number', + values: { + A: 'Animas', + B: 'Bluewater', + C: 'Carlsbad', + CC: 'Curry County', + CD: 'Cloverdale', + CL: 'Causey Lingo', + CP: 'Capitan', + CR: 'Canadian River', + CT: 'Clayton', + E: 'Estancia', + FS: 'Fort Sumner', + G: 'Gallup', + GSF: 'Gila San Francisco', + H: 'Hondo', + HA: 'Hachita', + HC: 'Hagerman Canal', + HS: 'Hot Springs Artesian', + HU: 'Hueco', + J: 'Jal', + L: 'Lea County', + LA: 'Las Animas Creek', + LRG: 'Lower Rio Grande', + LV: 'Lordsburg Valley', + LWD: 'Livestock Watering Declaration', + M: 'Mimbres', + MR: 'Mount Riley', + NH: 'Nutt Hockett', + P: 'Portales', + PL: 'Playas Valley', + PN: 'Penasco', + RA: 'Roswell Artesian', + RG: 'Rio Grande', + S: 'Sandia', + SB: 'Salt Basin', + SD: 'Surface Declaration', + SJ: 'San Juan', + SP: 'Surface Permit', + SS: 'San Simon', + T: 'Tularosa', + TU: 'Tucumcari', + UP: 'Upper Pecos', + VV: 'Virden Valley', + Y: 'Yaqui', + }, + }, + COORD_ACC_CODE: { + description: + 'Codes for coordinate accuracy (refers to the map or aerial photo source from which coordinates are derived)', + values: { + L: 'Large-scale map or aerial photo source', + M: 'Medium-scale map or aerial photo source', + S: 'Small-scale map or aerial photo source', + }, + }, + COORD_SOURCE_CODE: { + description: 'Codes for coordinate source', + values: { + UN: 'Unknown Source', + PA: 'Provided by Applicant', + PD: 'Provided by Driller', + UA: 'Updated by Applicant', + EA: 'OSE In-office Geospatial Application', + EM: 'OSE Aerial Photography/Map', + EG: 'OSE On-site Inspection/GPS', + ES: 'OSE Hydrographic Survey', + G: 'PLSS', + N: 'None', + D: 'Disclaimer', + }, + }, + COUNTY_CODE: { + description: 'Codes for New Mexico counties', + values: { + BE: 'Bernalillo', + CA: 'Catron', + CH: 'Chaves', + CI: 'Cibola', + CO: 'Colfax', + CU: 'Curry', + DA: 'Dona Ana', + DB: 'De Baca', + ED: 'Eddy', + GR: 'Grant', + GU: 'Guadalupe', + HA: 'Harding', + HI: 'Hidalgo', + LA: 'Los Alamos', + LE: 'Lea', + LI: 'Lincoln', + LU: 'Luna', + MK: 'McKinley', + MO: 'Mora', + OT: 'Otero', + QU: 'Quay', + RA: 'Rio Arriba', + RO: 'Roosevelt', + SA: 'Sandoval', + SF: 'Santa Fe', + SI: 'Sierra', + SJ: 'San Juan', + SM: 'San Miguel', + SO: 'Socorro', + TA: 'Taos', + TO: 'Torrance', + UN: 'Union', + VA: 'Valencia', + XX: 'Unknown', + }, + }, + CS_CODE: { + description: + 'Codes for geographic coordinate reference systems defined in WATERS', + values: { + '1': 'NAD 1983 UTM Zone 13N', + '2': 'NAD 1983 UTM Zone 12N', + '3': 'NAD 1983 UTM Zone 14N', + '4': 'NAD 1983 SP FT NM East', + '5': 'NAD 1983 SP FT NM Central', + '6': 'NAD 1983 SP FT NM West', + '7': 'NAD 1983 HARN SP FT NM East', + '8': 'NAD 1983 HARN SP FT NM Central', + '9': 'NAD 1983 HARN SP FT NM West', + '10': 'NAD 1983 SP FT CO South', + '11': 'NAD 1983 SP FT AZ East', + '12': 'NAD 1983 SP FT TX Central', + '13': 'NAD 1983 SP FT TX North Central', + '14': 'NAD 1983 SP FT TX North', + '15': 'NAD 1983 SP FT OK North', + '16': 'NAD 1983 TX Statewide Mapping System', + '17': 'NAD 1927 UTM Zone 13N', + '18': 'NAD 1927 UTM Zone 14N', + '19': 'NAD 1927 UTM Zone 12N', + '20': 'NAD 1927 SP FT NM East', + '21': 'NAD 1927 SP FT NM Central', + '22': 'NAD 1927 SP FT NM West', + '23': 'NAD 1927 SP FT CO South', + '24': 'NAD 1927 SP FT AZ East', + '25': 'NAD 1927 SP FT TX Central', + '26': 'NAD 1927 SP FT TX North Central', + '27': 'NAD 1927 SP FT TX North', + '28': 'NAD 1927 SP FT OK North', + '29': 'NAD 1927 TX Statewide Mapping System', + '99': 'Unknown-Further research needed', + }, + }, + DATUM_CODE: { + description: + 'Codes for coordinate reference system datums defined in WATERS', + values: { + NAD27: 'North American Datum of 1927', + NAD83: 'North American Datum of 1983', + }, + }, + GW_SRC_TYPE_CODE: { + description: 'Codes for Type of groundwater', + values: { + A: 'Artesian', + D: 'Dry', + M: 'Mixed', + S: 'Shallow', + }, + }, + LOCATION_ERROR_CODE: { + description: 'Codes for type of POD location error', + values: { + '0 or Null': 'POD location status has not been assigned', + '1': 'POD has no UTM coordinates, location either vague or could not be converted', + '2': 'POD is located Out of State', + '3': 'POD is located in-state, but is located in wrong Declared Groundwater Basin', + '10': 'Previous location information error that has been fixed/corrected', + }, + }, + NMSP_ZONE_CODE: { + description: + 'Codes for New Mexico State Plane (NMSP) coordinate system zones', + values: { + C: 'Central (3002)', + E: 'Eastern (3001)', + W: 'Western (3003)', + }, + }, + POD_STATUS_CODE: { + description: 'Codes for recorded status of POD', + values: { + PEN: 'Pending', + PLG: 'Plugged', + CAP: 'Capped', + INC: 'Inactive', + ACT: 'Active', + }, + }, + PUMP_TYPE_CODE: { + description: 'Codes for type of well pump', + values: { + CENTRI: 'Centrifugal', + SUBMER: 'Sumbersible', + TURBIN: 'Turbine', + JET: 'Jet', + }, + }, + QUARTER_CODE: { + description: + 'Codes for PLSS quarter calls for all sub-divisions (ie. 1/4, 1/16, 1/64, etc.)', + values: { + '1': 'NW quarter', + '2': 'NE quarter', + '3': 'SW quarter', + '4': 'SE quarter', + }, + }, + STATE_CODE: { + description: "Codes for Owner's State as two-letter abbreviations", + values: { + AK: 'Alaska', + AL: 'Alabama', + AR: 'Arkansas', + AZ: 'Arizona', + CA: 'California', + CO: 'Colorado', + CT: 'Connecticut', + DC: 'Washington.,D.C', + DE: 'Delaware', + ES: 'Eastern States', + FL: 'Florida', + GA: 'Georgia', + HI: 'Hawaii', + IA: 'Iowa', + ID: 'Idaho', + IL: 'Illinois', + IN: 'Indiana', + KS: 'Kansas', + KY: 'Kentucky', + LA: 'Louisiana', + MA: 'Massachusetts', + MD: 'Maryland', + ME: 'Maine', + MI: 'Michigan', + MN: 'Minnesota', + MO: 'Missouri', + MS: 'Mississippi', + MT: 'Montana', + NC: 'North Carolina', + ND: 'North Dakota', + NE: 'Nebraska', + NH: 'New Hampshire', + NJ: 'New Jersey', + NM: 'New Mexico', + NV: 'Nevada', + NY: 'New York', + OH: 'Ohio', + OK: 'Oklahoma', + OR: 'Oregon', + PA: 'Pennsylvania', + RI: 'Rhode Island', + SC: 'South Carolina', + SD: 'South Dakota', + TN: 'Tennessee', + TX: 'Texas', + UT: 'Utah', + VA: 'Virginia', + VT: 'Vermont', + WA: 'Washington', + WI: 'Wisconsin', + WV: 'West Virginia', + WY: 'Wyoming', + }, + }, + STATUS_CODE: { + description: 'Codes for current status of a water right', + values: { + ADJ: 'Adjudicated', + ADM: 'Administrative', + APP: 'Application', + APR: 'Application Being Protested', + CAN: 'Cancelled', + CLS: 'Closed File', + DCL: 'Declaration', + DED: 'Dedicated', + DEN: 'Denied', + EXP: 'Expired', + HS: 'Hydrographic Survey', + LIC: 'Licensed', + NOI: 'Notice of Intention', + NOT: 'Not implies that there is no status', + OMS: 'Owner Management Status', + OOJ: 'Offer of Judgment', + PBU: 'Proof of Beneficial Use', + PMT: 'Permit', + PRG: 'Purged Conversion Record', + REN: 'Renumbered', + RET: 'Retired', + TRN: 'Transferred', + WMS: 'Water Master Status', + WTD: 'Withdrawn', + }, + }, + SUBBASIN_CODE: { + description: 'WRAB Sub-basin codes defined in WATERS', + values: { + A: 'Animas', + AP: 'Augustin Plains', + B: 'Bluewater', + C: 'Carlsbad (72121)', + CC: 'Cabrestro Creek', + CD: 'Cloverdale', + CH: 'Rio Chama(72121)', + CH7: 'Rio Chama-Section 7-GW', + CHCB: 'Rio Chama-Section 3-Rio Cebolla', + CHCC: 'Rio Chama-Section 7-Canones Creek', + CHCJ: 'Rio Chama-Section 3-Cajilon Creek', + CHCP: 'Rio Chama-Section 6-Canones/Polvadera', + CHCV: 'Rio Chama-Section 7-Village of Chama', + CHER: 'Rio Chama-Section 4-Rio Chama-El Rito', + CHGA: 'Rio Chama-Section 5-Rio Gallina', + CHMS: 'Rio Chama-Section 1-Mainstem', + CHNU: 'Rio Chama-Section 3-Rio Nutrias', + CHOC: 'Rio Chama-Section 2-Ojo Caliente', + CHRB: 'Rio Chama-Section 7-Rio Brazos', + CHRP: 'Rio Chama-Rio Puerco de Chama', + CHRU: 'Rio Chama-Section 7-Rutheron & Plaza Blanca', + CHTA: 'Rio Chama-Section 7-Rito de Tierra Amarilla', + CL: 'Causey Lingo', + CO: 'Costilla', + CP: 'Capitan', + CR: 'Canadian River', + CT: 'Clayton', + CU: 'Curry County', + CUB: 'Carlsbad Underground Basin', + DRCI: 'Dry Cimarron', + E: 'Estancia', + FS: 'Fort Sumner', + G: 'Gallup', + GR: 'Gallinas River', + GSAR: 'Gila San-Francisco - Aragon', + GSCG: 'Gila San-Francisco - Cliff Gila', + GSGW: 'Gila San-Francisco - Glenwood', + GSLU: 'Gila San-Francisco - Luna', + GSRE: 'Gila San-Francisco - Reserve', + GSRR: 'Gila San-Francisco - Redrock', + GSUG: 'Gila San-Francisco - Upper Gila', + H: 'Hondo', + HA: 'Hachita', + HC: 'Hagerman Canal', + HRB: 'Hondo-Rio Bonito', + HRH: 'Hondo-Rio Hondo', + HRR: 'Hondo-Rio Ruidoso', + HS: 'Hot Springs', + HU: 'Hueco', + J: 'Jal', + JMZ: 'Jemez', + L: 'Lea County', + LA: 'Las Animas Creek', + LR: 'Latir Creek - Rio Grande', + LRN: 'LRG-North Mesilla Valley', + LRO: 'LRG-Outlying areas', + LRR: 'LRG-Rincon Valley', + LRS: 'LRG-South Mesilla Valley', + LT: 'Lower Tularosa', + LV: 'Lordsburg', + M: 'Mimbres River', + MR: 'Mount Riley', + MRG: 'Middle Rio Grande', + NH: 'Nutt-Hocket', + NPT: 'Nambe Pojoaque-Tesuque', + NRG: 'Northern Rio Grande', + P: 'Portales', + PL: 'Playas', + PN: 'Penasco', + PUR: 'Rio Puerco', + RA: 'Roswell Artesian', + RGRT: 'Rio Truchas', + RGSC: 'Rio Grande Santa Cruz', + RR: 'Red River', + S: 'Sandia', + SF: 'Santa Fe River', + SJ: 'San Juan', + SJAR: 'Animas River', + SJCR: 'Chaco River', + SJLP: 'La Plata', + SJM1: 'SJ Main Stem Co State Line to Navajo Dam', + SJM2: 'SJ Main Stem Navajo Dam to Animas River', + SJM3: 'SJ Main Stem From Animas River to AZ St Line', + SJNR: 'Navajo River', + SJPR: 'Pinos (Pine) River', + SS: 'San Simon', + ST: 'Salt Basin', + TA: 'Taos (72121)', + TU: 'Tucumcari', + UP: 'Upper Pecos', + UT: 'Upper Tularosa', + VV: 'Virden Valley', + Y: 'Yaqui', + }, + }, + SURFACE_SOURCE_CODE: { + description: + 'The surface water source for surface diversions (based on USGS hydrography codes)', + values: { + '1': 'ABBOT CREEK', + '2': 'ABBOTT LAKE', + '3': 'ABIQUIU CREEK', + '4': 'ABIQUIU RESERVOIR', + '5': 'ABO ARROYO', + '6': 'ADAMS CANYON', + '7': 'AGUA CALIENTE', + '8': 'AGUA CHIQUITA CREEK', + '9': 'AGUA FRIA CREEK', + '10': 'AGUA FRIA CREEK', + '11': 'AGUA FRIA CREEK /N', + '12': 'AGUA LIMPIA /N', + '13': 'AGUA MEDIA', + '14': 'AGUA NEGRA RIVER /N', + '15': 'AGUA OLYMPIA', + '16': 'AGUA SARCA', + '17': 'AGUAJE ARROYO /NM', + '18': 'AGUILA CANYON', + '19': 'AHOGADERO CREEK', + '20': 'ALAMAGORDO RESERVOIR', + '21': 'ALAMITO CREEK', + '22': 'ALAMITOS CANYON', + '23': 'ALAMITOS CREEK', + '24': 'ALAMO ARROYO', + '25': 'ALAMO CANYON', + '26': 'ALAMO CANYON', + '27': 'ALAMO CANYON', + '28': 'ALAMO CREEK', + '29': 'ALAMO CREEK', + '30': 'ALAMOCITA ARROYO', + '31': 'ALAMOCITA CREEK', + '32': 'ALAMOCITA CREEK', + '33': 'ALAMOCITA CREEK', + '34': 'ALAMOCITO CREEK', + '35': 'ALAMOGORDO RESERVOIR', + '36': 'ALAMOS CREEK', + '37': 'ALAMOS CREEK', + '38': 'ALAMOSA', + '39': 'ALAMOSA CANYON', + '40': 'ALAMOSA CREEK', + '41': 'ALAMOSA CREEK', + '42': 'ALAMOSA CREEK', + '43': 'ALAMOSITA CREEK', + '44': 'ALAMOSITA CREEK', + '45': 'ALEMAN DRAW', + '46': 'ALEXANDER CANYON', + '47': 'ALIES SEEP CANYON', + '48': 'ALKALI DRAW', + '49': 'ALLIE CANYON', + '50': 'ALLISON DRAW /NM', + '51': 'AMARGO CREEK', + '52': 'AMBROSIA LAKE', + '53': 'AMERICAN CANYON', + '54': 'AMERICAN CREEK', + '55': 'AMOLE ARROYO', + '56': 'ANAN CANYON', + '57': 'ANCHA GULCH', + '58': 'ANCHO VALLEY', + '59': 'ANGOSTURA CREEK', + '60': 'ANIMAS BASIN', + '61': 'ANIMAS CREEK', + '62': 'ANIMAS RIVER', + '63': 'ANTHONY ARROYO', + '64': 'APACHE ARROYO', + '65': 'APACHE CANYON', + '66': 'APACHE CANYON', + '67': 'APACHE CREEK', + '68': 'APACHE CREEK', + '69': 'APACHE CREEK', + '70': 'APACHE HILL ARROYO /NM', + '71': 'APODACA ARROYO', + '72': 'ARAGON CREEK', + '73': 'ARCHULETA CREEK', + '74': 'ARENAL GRAVEL PIT', + '75': 'ARMIJO DRAW', + '76': 'ARROW CANYON', + '77': 'ARROYO AGUA SARCA', + '78': 'ARROYO AGUAJE DE LA PETACA', + '79': 'ARROYO AJUELOS', + '80': 'ARROYO ALAMITO', + '81': 'ARROYO ALCALDE', + '82': 'ARROYO ANGOSTURA', + '83': 'ARROYO BARBARA', + '84': 'ARROYO BLANCO', + '85': 'ARROYO CALABASAS', + '86': 'ARROYO CANADA ANCHA', + '87': 'ARROYO CHICO', + '88': 'ARROYO COLORADO', + '89': 'ARROYO COMANCHE', + '90': 'ARROYO CUARAI /N', + '91': 'ARROYO CUERVO', + '92': 'ARROYO DE ANIL', + '93': 'ARROYO DE FRIJOLES', + '94': 'ARROYO DE LA BORREGOS', + '95': 'ARROYO DE LA CEJITA', + '96': 'ARROYO DE LA MORA', + '97': 'ARROYO DE LA MORADA', + '98': 'ARROYO DE LA PRESILLA', + '99': 'ARROYO DE LAS CRUCES', + '100': 'ARROYO DE LAS PALOMAS', + '101': 'ARROYO DE LOS ANGELES', + '102': 'ARROYO DE LOS CHAMISOS', + '103': 'ARROYO DE LOS LOPEZ', + '104': 'ARROYO DE LOS TANQUES', + '105': 'ARROYO DE MANZANO', + '106': 'ARROYO DE TAJIQUE', + '107': 'ARROYO DEL ALAMO', + '108': 'ARROYO DEL ALAMO', + '109': 'ARROYO DEL COYOTE', + '110': 'ARROYO DEL CUERVO', + '111': 'ARROYO DEL EMBUDO', + '112': 'ARROYO DEL GUIQUE', + '113': 'ARROYO DEL MACHO', + '114': 'ARROYO DEL OJO DEL ORNO', + '115': 'ARROYO DEL YESO', + '116': 'ARROYO DOMINGO BACA', + '117': 'ARROYO ENCINOS', + '118': 'ARROYO ESTACA', + '119': 'ARROYO GONZALES /NM', + '120': 'ARROYO HONDO', + '121': 'ARROYO JALAROSA', + '122': 'ARROYO JARIDO', + '123': 'ARROYO JAROSA /N', + '124': 'ARROYO MARTINEZ', + '125': 'ARROYO MONTE LARGO', + '126': 'ARROYO PECOS', + '127': 'ARROYO PEDRO PADILLA', + '128': 'ARROYO PIRA', + '129': 'ARROYO PUEBLITO', + '130': 'ARROYO PUNCHE', + '131': 'ARROYO SALEDO', + '132': 'ARROYO SAN ANTONIO', + '133': 'ARROYO SAN JOSE /N', + '134': 'ARROYO SAN JUAN DE DIOS', + '135': 'ARROYO SAN RAFAEL', + '136': 'ARROYO SECCION', + '137': 'ARROYO SECO', + '138': 'ARROYO SECO', + '139': 'ARROYO SECO', + '140': 'ARROYO SECO', + '141': 'ARROYO SECO', + '142': 'ARROYO SERRANO', + '143': 'ARROYO TAJIQUE', + '144': 'ARROYO TONQUE', + '145': 'ARROYO TRUJILLO /N', + '146': 'ARROYO UNA DE GATO', + '147': 'ARROYO VAQUEROS', + '148': 'ASH CANYON /N', + '149': 'ASH CREEK', + '150': 'ASH SPRING CANYON', + '151': 'ATARQUE CREEK', + '152': 'BALDY MOUNTAIN CANYON', + '153': 'BALES CANYON', + '154': 'BANDERITAS CREEK', + '155': 'BAR B DRAW', + '156': 'BARCLAY DRAW', + '157': 'BARCLAY DRAW', + '158': 'BARELA CANYON', + '159': 'BARILLAS CREEK', + '160': 'BARKER ARROYO', + '161': 'BARRANCA CREEK', + '162': 'BARRANCONES CREEK', + '163': 'BARTON ARROYO', + '164': 'BAYLOR CANYON', + '165': 'BEAR CANYON', + '166': 'BEAR CANYON', + '167': 'BEAR CANYON', + '168': 'BEAR CANYON', + '169': 'BEAR CANYON', + '170': 'BEAR CANYON /N', + '171': 'BEAR CANYON /N', + '172': 'BEAR CANYON /N', + '173': 'BEAR CANYON /N', + '174': 'BEAR CANYON /N', + '175': 'BEAR CREEK', + '176': 'BEAR CREEK', + '177': 'BEAR CREEK', + '178': 'BEAR GRASS DRAW', + '179': 'BEAR SPRINGS CANYON', + '180': 'BEAVER CANYON', + '181': 'BEAVER CREEK', + '182': 'BEAVER CREEK', + '183': 'BEEN DRAW', + '184': 'BELL CANYON', + '185': 'BENADO CANYON', + '186': 'BENNETT CREEK', + '187': 'BENSON CANYON', + '188': 'BERCHAM DRAW', + '189': 'BERRENDA CREEK', + '190': 'BERRENDO CREEK', + '191': 'BIG CANYON', + '192': 'BIG CANYON', + '193': 'BIG CHERRY CANYON', + '194': 'BIG CREEK', + '195': 'BIG DOG CANYON', + '196': 'BIG DRAW', + '197': 'BIG DRY CREEK', + '198': 'BIG NIGGER GULCH /NM', + '199': 'BIG OX YOKE CANYON', + '200': 'BIG PAT CANYON', + '201': 'BIG PIGEON CANYON', + '202': 'BIG RINCON', + '203': 'BIGNELL ARROYO', + '204': 'BIRCHER CANYON', + '205': 'BISBEE DRAW', + '206': 'BISHOPS CAP ARROYO', + '207': 'BITTER CREEK', + '208': 'BITTER CREEK /N', + '209': 'BLACK BILL CANYON', + '210': 'BLACK CANYON', + '211': 'BLACK CANYON', + '212': 'BLACK CANYON', + '213': 'BLACK CREEK (IN ARIZONA)', + '214': 'BLACK MESA ARROYO', + '215': 'BLACK MOUNTAIN DRAW', + '216': 'BLACK RIVER', + '217': 'BLACKSMITH CANYON', + '218': 'BLANCA CREEK', + '219': 'BLANCO CANYON', + '220': 'BLANCO CANYON', + '221': 'BLEA STREAM /N', + '222': 'BLUE CANYON', + '223': 'BLUE CREEK', + '224': 'BLUE RIVER', + '225': 'BLUE ROCK CANYON', + '226': 'BLUE WATER CREEK', + '227': 'BLUEWATER CANYON', + '228': 'BLUEWATER CREEK', + '229': 'BLUEWATER CREEK', + '230': 'BLUEWATER CREEK', + '231': 'BLUEWATER LAKE', + '232': 'BLUFF CREEK', + '233': 'BOB CROSBY DRAW', + '234': 'BOBCAT CREEK', + '235': 'BOHANNON ARROYO', + '236': 'BOLANDER CANYON', + '237': 'BONANZA CREEK', + '238': 'BONITA CANYON', + '239': 'BONITA CREEK', + '240': 'BONITO CREEK', + '241': 'BONITO LAKE', + '242': 'BONTZ ARROYO', + '243': 'BORICA DRAW', + '244': 'BORREGO CANYON', + '245': 'BOX CANYON', + '246': 'BOX CANYON', + '247': 'BOX CANYON', + '248': 'BOX CANYON', + '249': 'BOX S CANYON', + '250': 'BOYER GULCH', + '251': 'BRACKETT ARROYO /NM', + '252': 'BRAGG CANYON', + '253': 'BRANTLEY RESERVOIR', + '254': 'BRAZOS RIVER', + '255': 'BREAD SPRINGS WASH', + '256': 'BRIDGE CANYON', + '257': 'BRIGGS CANYON', + '258': 'BROAD CANYON', + '259': 'BROADHURST ARROYO', + '260': 'BRUSHY CANYON', + '261': 'BRUSHY CREEK', + '262': 'BRUSHY DRAW', + '263': 'BUCK CANYON', + '264': 'BUCK SPRINGS', + '265': 'BUCKHORN CREEK', + '266': 'BUEYEROS CREEK', + '267': 'BUFFALO DRAW', + '268': 'BUFFALO LAKE', + '269': 'BUG SCUFFLE CANYON', + '270': 'BULL CANYON', + '271': 'BULL CANYON CREEK', + '272': 'BULL CREEK', + '273': 'BULL CREEK', + '274': 'BULLARD PEAK CANYON', + '275': 'BURNED CANYON', + '276': 'BURRO CIENAGA', + '277': 'BUSHNELL CREEK', + '278': 'BUZZARD CANYON', + '279': 'CABALLERO CANYON', + '280': 'CABALLO RESERVOIR', + '281': 'CABIN CANYON', + '282': 'CABRESTO CANYON', + '283': 'CABRESTO CREEK', + '284': 'CALABACILLAS ARROYO', + '285': 'CALAVERAS CANYON', + '286': 'CAMERON CREEK', + '287': 'CAMP TWO CANYON', + '288': 'CAMPUS ARROYO', + '289': 'CANADA ALAMOS', + '290': 'CANADA ANCHA', + '291': 'CANADA BONITA', + '292': 'CANADA COLORADO', + '293': 'CANADA DE LA CUEVA', + '294': 'CANADA DE LA PRESA', + '295': 'CANADA DE LAS FUERTES', + '296': 'CANADA DEL BANO', + '297': 'CANADA DEL BORREGO', + '298': 'CANADA DEL PORTRERO', + '299': 'CANADA LARGA', + '300': 'CANADA PINABETE', + '301': 'CANADA RAMONES', + '302': 'CANADA SANTIAGO', + '303': 'CANADA TIO GRANDE', + '304': 'CANADIAN AREA 01', + '305': 'CANADIAN AREA 03', + '306': 'CANADIAN AREA 06', + '307': 'CANADIAN AREA 09', + '308': 'CANARIO CANYON', + '309': 'CANE SPRING CANYON', + '310': 'CANJILON CREEK', + '311': 'CANON ANCHO /N', + '312': 'CANON BLANCO', + '313': 'CANON BONITO /N', + '314': 'CANON CEBOLLITA', + '315': 'CANON COLORADO', + '316': 'CANON CORRALES', + '317': 'CANON DE BARTOLO', + '318': 'CANON DE CALIFIA', + '319': 'CANON DE CHILLILI', + '320': 'CANON DE DOMINGA BACA', + '321': 'CANON DE GALLEGOS', + '322': 'CANON DE HUGHES', + '323': 'CANON DE LA CANADA', + '324': 'CANON DE LA MIGA', + '325': 'CANON DE LA MULA', + '326': 'CANON DE LAS PALAS', + '327': 'CANON DE LOS CORDOVAS', + '328': 'CANON DE LOS PINO REALES', + '329': 'CANON DE MARQUEZ', + '330': 'CANON DE PEDIO PADILLA', + '331': 'CANON DE SALAS', + '332': 'CANON DE SALAS /N', + '333': 'CANON DE TAJIQUE', + '334': 'CANON DE TANQUE HONDO', + '335': 'CANON DE TERRERO', + '336': 'CANON DE TORREON', + '337': 'CANON DEL AGUA', + '338': 'CANON DEL BUEY', + '339': 'CANON DEL DADO', + '340': 'CANON DEL NORTE', + '341': 'CANON DEL OJO DEL INDIO', + '342': 'CANON DEL TRIGO', + '343': 'CANON LARGO', + '344': 'CANON MADERA', + '345': 'CANON MESTENO', + '346': 'CANON MONTE DE ABAJO', + '347': 'CANON MONTE LARGO', + '348': 'CANON NUEVO', + '349': 'CANON OBSCURO', + '350': 'CANON OBSCURO', + '351': 'CANON SALADO', + '352': 'CANON SANTA ROSA', + '353': 'CANON SANTO DOMINGO', + '354': 'CANON SAPATA', + '355': 'CANON SECO', + '356': 'CANON SEGURO', + '357': 'CANON TAPIA', + '358': 'CANONCITO CREEK', + '359': 'CANONCITO DE NUANES /N', + '360': 'CANONES CREEK /N', + '361': 'CANONES CREEK /N', + '362': 'CANORITA DE LAS BACAS', + '363': 'CANOVAS CREEK', + '364': 'CANYON CREEK /NM', + '365': 'CAPULIN CANYON', + '366': 'CAPULIN CANYON', + '367': 'CAPULIN CREEK /N', + '368': 'CARACITA CREEK', + '369': 'CARLISLE CANYON', + '370': 'CARNUDOS DRAW', + '371': 'CARRACAS CANYON', + '372': 'CARRISA CANYON', + '373': 'CARRIZO ARROYO /N', + '374': 'CARRIZO CREEK', + '375': 'CARRIZO CREEK', + '376': 'CARRIZO CREEK', + '377': 'CARRIZO CREEK', + '378': 'CARRIZO CREEK /N', + '379': 'CARRIZO CREEK /N', + '380': 'CARRIZO WASH', + '381': 'CARRIZOZO CREEK', + '382': 'CARRIZOZO CREEK /N', + '383': 'CARROS CREEK', + '384': 'CARROS CREEK', + '385': 'CASAMERO DRAW', + '386': 'CASS DRAW', + '387': 'CEBOLLA CREEK', + '388': 'CEBOLLITA CREEK', + '389': 'CEDAR CREEK', + '390': 'CEDAR CREEK', + '391': 'CEDAR GROVE DRAW', + '392': 'CEDRITO ARROYO', + '393': 'CELSO ARROYO /N', + '394': 'CEMENT CANYON /NM', + '395': 'CENEGA DEL MACHO', + '396': 'CENTERFIRE CREEK', + '397': 'CEREZA CANYON', + '398': 'CERROSOSO CREEK', + '399': 'CHACO RIVER', + '400': 'CHACO WASH', + '401': 'CHALK BLUFF DRAW', + '402': 'CHAMISAL CREEK', + '403': 'CHANEY ARROYO', + '404': 'CHAPO DRAW /NM', + '405': 'CHARETTE LAKE', + '406': 'CHARLEY WHITE DRAW', + '407': 'CHATFIELD CANYON', + '408': 'CHAVEZ CANYON', + '409': 'CHAVEZ CANYON', + '410': 'CHAVEZ CREEK', + '411': 'CHEROKEE BILL CANYON', + '412': 'CHEROKEE CANYON', + '413': 'CHERRY CREEK', + '414': 'CHERRY CREEK', + '415': 'CHESS DRAW', + '416': 'CHICO CREEK', + '417': 'CHICO RICO CREEK /N', + '418': 'CHICORICA CREEK', + '419': 'CHICOSA LAKE', + '420': 'CHIHUAHUENOS CREEK', + '421': 'CHINA DRAW', + '422': 'CHINA DRAW /N', + '423': 'CHINA DRAW /N', + '424': 'CHINA POND DRAW', + '425': 'CHOSIE CANYON', + '426': 'CHUPADERA ARROYO', + '427': 'CHUPADERA ARROYO', + '428': 'CHURCH CANYON', + '429': 'CIBOLO CANYON', + '430': 'CIENEGA AMARILLA', + '431': 'CIENEGA CANON /NM', + '432': 'CIENEGA CANYON', + '433': 'CIENEGA CANYON', + '434': 'CIENEGA CREEK', + '435': 'CIENEGA DRAW', + '436': 'CIENEGUILLA CREEK', + '437': 'CIENEGUILLA CREEK', + '438': 'CIENEGUILLA DEL BURRO ARROYO', + '439': 'CIMARRON CREEK', + '440': 'CIMARRON RIVER', + '441': 'CIMARRONCITO CREEK', + '442': 'CIRUELA CREEK /N', + '443': 'CLANTON DRAW', + '444': 'CLARK CANYON', + '445': 'CLARK DRAW', + '446': 'CLARKE ARROYO /N', + '447': 'CLEAR CREEK', + '448': 'CLEAR LAKE', + '449': 'CLIMAX CANYON', + '450': 'CLOVERDALE CREEK', + '451': 'COAL CANYON', + '452': 'COALBANK CANYON', + '453': 'COCHITI CANYON /N', + '454': 'COCKLEBURR DRAW', + '455': 'COFFELT DRAW', + '456': 'COLE CANYON /N', + '457': 'COLLEGE ARROYO', + '458': 'COLLINS DRAW', + '459': 'COLUMBINE CREEK', + '460': 'COMANCHE CREEK', + '461': 'COMANCHE CREEK', + '462': 'COMANCHEROS CREEK', + '463': 'COMMISSARY CREEK', + '464': 'CONCHAS CANAL', + '465': 'CONCHAS CANYON', + '466': 'CONCHAS LAKE', + '467': 'CONCHAS LAKE', + '468': 'CONCHAS RIVER', + '469': 'COPELAND CANYON', + '470': 'COPPER CREEK', + '471': 'CORAZON CREEK', + '472': 'CORDELL CANYON', + '473': 'CORDUROY CANYON', + '474': 'CORNUCOPIA DRAW', + '475': 'CORNUDAS DRAW', + '476': 'CORRAL CANYON', + '477': 'CORRAL CANYON', + '478': 'CORRALITOS CREEK', + '479': 'CORRUMPA CREEK', + '480': 'COSTILLA CREEK', + '481': 'COTTON CANYON', + '482': 'COTTONWOOD ARROYO /N', + '483': 'COTTONWOOD CANYON', + '484': 'COTTONWOOD CANYON', + '485': 'COTTONWOOD CANYON', + '486': 'COTTONWOOD CANYON', + '487': 'COTTONWOOD CANYON', + '488': 'COTTONWOOD CANYON /N', + '489': 'COTTONWOOD CREEK', + '490': 'COTTONWOOD CREEK', + '491': 'COTTONWOOD CREEK', + '492': 'COTTONWOOD CREEK /N', + '493': 'COTTONWOOD DRAW', + '494': 'COTTONWOOD WASH', + '495': 'COUNTY LINE ARROYO', + '496': 'COW CREEK', + '497': 'COW CREEK', + '498': 'COW SPRINGS DRAW', + '499': 'COX CANYON', + '500': 'COX DRAW', + '501': 'COYOTE CANYON', + '502': 'COYOTE CANYON', + '503': 'COYOTE CANYON', + '504': 'COYOTE CREEK', + '505': 'COYOTE CREEK', + '506': 'COYOTE CREEK', + '507': 'CRAWFORD HOLLOW', + '508': 'CROOKED CANYON', + '509': 'CROOKED CREEK', + '510': 'CROOKED CREEK', + '511': 'CROW CANYON', + '512': 'CROW CANYON', + '513': 'CROW CANYON', + '514': 'CROW CREEK', + '515': 'CROW FLATS /N', + '516': 'CUCHILLO NEGRO CREEK', + '517': 'CUERVO CANYON', + '518': 'CUERVO CREEK', + '519': 'CUEVO CANYON /N', + '520': 'CUNNINGHAM CREEK', + '521': 'CURTIS CANYON', + '522': 'CURTIS CREEK', + '523': 'D BAR O CANYON', + '524': 'DALTON CANYON', + '525': 'DAM CANYON', + '526': 'DARK CANYON /N', + '527': 'DARK CANYON /N', + '528': 'DARLING CREEK', + '529': 'DAVIS CANYON', + '530': 'DAVIS CREEK', + '531': 'DEAD COW CANYON', + '532': 'DEADMAN CANYON', + '533': 'DECKER DRAW', + '534': 'DEEP CANYON', + '535': 'DEEP CREEK', + '536': 'DEEP LAKE', + '537': 'DEER CREEK', + '538': 'DEER CREEK', + '539': 'DEER CREEK', + '540': 'DEER CREEK', + '541': 'DEL MUERTO CREEK', + '542': 'DELAWARE RIVER', + '543': 'DERRICK DRAW', + '544': 'DEVILS CREEK', + '545': 'DEVILS DEN CANYON', + '546': 'DEVILS NORTH FORK', + '547': 'DIAMOND CREEK', + '548': 'DICKEY CANYON', + '549': 'DILLMAN CREEK', + '550': 'DILLON CANYON', + '551': 'DIMMIT LAKE', + '552': 'DIX CANYON', + '553': 'DOCTOR ARROYO', + '554': 'DOG CANYON', + '555': 'DOG CANYON', + '556': 'DOG CANYON DRAW', + '557': 'DOLLINS CANYON', + '558': 'DOMINGA CANYON', + '559': 'DOMINGA CANYON', + '560': 'DOMINGUEZ CANYON', + '561': 'DONA ANA ARROYO', + '562': 'DONAHUE CANYON', + '563': 'DOSS ARROYO /N', + '564': 'DOUBLE ADOBE CREEK', + '565': 'DOUBLE CANYON DRAW', + '566': 'DOYLE CANYON', + '567': 'DRY ARROYO', + '568': 'DRY BURNT CANYON', + '569': 'DRY CANYON', + '570': 'DRY CIMARRON RIVER', + '571': 'DUCK CREEK', + '572': 'DUD CANYON', + '573': 'DULCE DRAW', + '574': 'DULCE DRAW', + '575': 'DURAN CANYON', + '576': 'DURAN CANYON', + '577': 'DURAN MESA', + '578': 'DUWESS CANYON', + '579': 'EAGLE CREEK /N', + '580': 'EAGLE CREEK /N', + '581': 'EAGLE DRAW /N', + '582': 'EAGLE NEST LAKE', + '583': 'EAST CANYON', + '584': 'EAST CEDAR CREEK', + '585': 'EAST FORK BRUSHY CREEK', + '586': 'EAST FORK CHICORICA CREEK', + '587': 'EAST FORK FIVEMILE DRAW', + '588': 'EAST FORK GILA RIVER', + '589': 'EAST FORK JEMEZ RIVER', + '590': 'EAST FORK MASON DRAW /N', + '591': 'EAST FORK MIMBRES RIVER', + '592': 'EAST FORK RED RIVER', + '593': 'EAST FORK WINDHAM CANYON', + '594': 'EAST SAN JUAN /NM', + '595': 'EIGHTMILE DRAW', + '596': 'EL CANON DEL PALO AMARILLO', + '597': 'EL RANCHO GRANDE DAM /N', + '598': 'EL RITO', + '599': 'EL RITO CANYON', + '600': 'EL RITO CREEK /N', + '601': 'EL VADO RESERVOIR', + '602': 'ELEPHANT BUTTE RESERVOIR', + '603': 'ELK CANYON', + '604': 'ELLIOT CANYON', + '605': 'ELLIS CANYON', + '606': 'EMBUDITO CANYON', + '607': 'EMBUDO ARROYO', + '608': 'EMBUDO CANYON', + '609': 'EMBUDO CREEK', + '610': 'EMERSON CANYON', + '611': 'ENCINAL CREEK', + '612': 'ESCAVADA WASH', + '613': 'ESCAVADA WASH', + '614': 'ESCONDIDO CANYON', + '615': 'ESCRITO CANYON', + '616': 'ESQUIBEL CANYON', + '617': 'ESTANCIA BASIN', + '618': 'ESTEROS CREEK', + '619': 'FARMINGTON GLADE', + '620': 'FAULKNER CANYON', + '621': 'FENCED UP HORSE VALLEY', + '622': 'FERNANDEZ DRAW /NM', + '623': 'FILLMORE ARROYO', + '624': 'FINCH ARROYO', + '625': 'FINGER RING DRAW', + '626': 'FIR CANYON', + '627': 'FIRST VALLEY', + '628': 'FISHER CREEK', + '629': 'FITZPATRICK CIENEGA', + '630': 'FIVEMILE CREEK', + '631': 'FIVEMILE DRAW', + '632': 'FLATHEAD CANYON', + '633': 'FLEMING CANYON', + '634': 'FLEMING DRAW', + '635': 'FLORIDA RIVER', + '636': 'FORBES CANYON', + '637': 'FORT CUMMINGS DRAW', + '638': 'FORT WEST DITCH', + '639': 'FOSTER CANYON', + '640': 'FOSTER DRAW', + '641': 'FOSTER DRAW', + '642': 'FOURMILE CANYON', + '643': 'FOURMILE DRAW', + '644': 'FRANCES CREEK', + '645': 'FRESNAL CANYON', + '646': 'FRIO DRAW', + '647': 'FROELICH CREEK /N', + '648': 'FROLIC CREEK', + '649': 'FROST CREEK', + '650': 'FRYING PAN CANYON', + '651': 'FULLERTON SPRING CANYON', + '652': 'FULLERTON SPRING CANYON', + '653': 'GALISTEO CREEK', + '654': 'GALLEGOS CANYON', + '655': 'GALLEGOS CREEK', + '656': 'GALLEGOS CREEK /N', + '657': 'GALLINA CREEK', + '658': 'GALLINAS CANYON', + '659': 'GALLINAS CREEK', + '660': 'GALLINAS CREEK /N', + '661': 'GALLINAS RIVER /N', + '662': 'GALLO ARROYO', + '663': 'GALLO CANYON', + '664': 'GARAPATA CREEK', + '665': 'GARCIA CANYON', + '666': 'GARCIA CANYON', + '667': 'GARCIA CANYON', + '668': 'GARCIA CREEK', + '669': 'GARCIA FALLS CANYON', + '670': 'GARFIELD CANYON', + '671': 'GARITA CREEK', + '672': 'GATLIN CANYON', + '673': 'GAVILAN ARROYO', + '674': 'GAVILAN CANON', + '675': 'GAVILAN CANYON', + '676': 'GEORGE CANYON', + '677': 'GERONIMO DRAW', + '678': 'GILA RIVER AREA 1', + '679': 'GILA RIVER AREA 2', + '680': 'GILLESPIE CREEK', + '681': 'GILLESPIE DRAW', + '682': 'GLORIETA CREEK', + '683': 'GOAT CANYON', + '684': 'GOAT CANYON', + '685': 'GOBERNADOR CANYON', + '686': 'GODFREY ARROYO /NM', + '687': 'GOLD GULCH', + '688': 'GOLD HILL CANYON', + '689': 'GONLANS CREEK /NM', + '690': 'GONZALES CANYON', + '691': 'GONZALES CANYON', + '692': 'GOOSE CREEK', + '693': 'GOOSE LAKE', + '694': 'GOVINA CANYON', + '695': 'GRANEY CREEK', + '696': 'GRAPEVINE CANYON', + '697': 'GRAPEVINE CREEK', + '698': 'GRAPEVINE DRAW', + '699': 'GRAVEYARD CANYON', + '700': 'GREASY CANYON', + '701': 'GREEN CANYON', + '702': 'GREENFIELD ARROYO /N', + '703': 'GREENHORN ARROYO', + '704': 'GREENWOOD CANYON', + '705': 'GUADALUPE ARROYO', + '706': 'GUADALUPE CANYON', + '707': 'GUADALUPE CANYON', + '708': 'GUADALUPE DRAW', + '709': 'GUADALUPITA CREEK /N', + '710': 'GUANA CREEK', + '711': 'GUERRERO CANYON /N', + '712': 'HACHITA VALLEY', + '713': 'HACHITA VALLEY', + '714': 'HACKBERRY DRAW', + '715': 'HACKBERRY DRAW', + '716': 'HACKBERRY GULCH /NM', + '717': 'HACKETT CANYON', + '718': 'HADLEY CANYON', + '719': 'HADLEY DRAW', + '720': 'HAGERMAN CANAL', + '721': 'HAHN ARROYO', + '722': 'HAMMETT ARROYO', + '723': 'HAMPTON DRAW', + '724': 'HANOVER CREEK', + '725': 'HARDCASTLE CANYON', + '726': 'HARDEN CIENEGA', + '727': 'HARRIS CANYON', + '728': 'HARRIS CREEK', + '729': 'HART CANYON', + '730': 'HARVEY DRAW', + '731': 'HASPAROS CANYON /N', + '732': 'HASPEROS CANYON', + '733': 'HAY CANYON', + '734': 'HAY DRAW', + '735': 'HAYNES CANYON', + '736': 'HAYNES CANYON', + '737': 'HAZZARDS CANYON /NM', + '738': 'HELL CANYON', + '739': 'HERNANDEZ DRAW', + '740': 'HEWITT CREEK /NM', + '741': 'HILL CANYON', + '742': 'HINCHLEY CANYON', + '743': 'HITTSON CREEK', + '744': 'HOGADERO DRAW', + '745': 'HOLKEO CREEK', + '746': 'HOLLENBACK CREEK', + '747': 'HOLY GHOST CREEK', + '748': 'HOMBRILLO', + '749': 'HOMESTEAD TANK', + '750': 'HONDO CANYON', + '751': 'HONDO CANYON', + '752': 'HOOPER CANYON', + '753': 'HOP CANYON', + '754': 'HORSE CAMP CANYON', + '755': 'HORSE LAKE CANYON', + '756': 'HORSE LAKE CREEK', + '757': 'HORSEHEAD CANYON', + '758': 'HORSESHOE LAKE', + '759': 'HOT SPRINGS CANYON', + '760': 'HOUGHTON CANYON', + '761': 'HOUSE CANYON', + '762': 'HOYT CREEK', + '763': 'HUBBLE LAKE', + '764': 'HUGGINS CREEK', + '765': 'HUGGINS DRAW', + '766': 'HUGHS CANYON', + '767': 'HUMPHREY CANYON', + '768': 'HUNTER CREEK', + '769': 'HUNTER WASH', + '770': 'HUTCH ARROYO', + '771': 'HYATT CANYON', + '772': 'INDIAN CREEK', + '773': 'INDIAN CREEK', + '774': 'INDIAN CREEK', + '775': 'INDIAN CREEK', + '776': 'INDIAN CREEK', + '777': 'INDIAN DRAW', + '778': 'INDIAN RIDGE CANYON /NM', + '779': 'IRON CREEK', + '780': 'IRON CREEK', + '781': 'IRWIN CREEK /N', + '782': 'JACK SMITH DRAW', + '783': 'JACKSON LAKE', + '784': 'JAMES CANYON', + '785': 'JARALOSA CREEK', + '786': 'JARITAS CREEK /NM', + '787': 'JAROSA CREEK /N', + '788': 'JAROSITO CANYON', + '789': 'JAROSITO CREEK /N', + '790': 'JEMEZ RIVER', + '791': 'JERNIGAN WASH', + '792': 'JIM GOODLOE CANYON /NM', + '793': 'JOE CABIN ARROYO', + '794': 'JOHNSON BASIN', + '795': 'JOHNSON CANYON', + '796': 'JOHNSON CREEK', + '797': 'JONES ARROYO', + '798': 'JONES CANYON /NM', + '799': 'JORDAN CANYON', + '800': 'JORNADA DEL MUERTO', + '801': 'JORNADA DRAW', + '802': 'JOSE PLANCENCIA CANYON', + '803': 'JUAN TABO CANYON', + '804': 'JUAN TAFOYA CANYON /N', + '805': "JUAN'S LAKE", + '806': 'JUAQUILLA CREEK /N', + '807': 'JUG CANYON', + '808': 'JULIAN CANYON', + '809': 'JUNIPER CANYON', + '810': 'JUNIPER DRAW', + '811': 'KANSAS VALLEY LAKE', + '812': 'KAPPIS ARROYO', + '813': 'KARR CANYON', + '814': 'KARTCHNER ARROYO', + '815': 'KEITHLY ARROYO /NM', + '816': 'KELLER CANYON', + '817': 'KELLY CANYON', + '818': 'KELLY CANYON', + '819': 'KIFFEN CANYON', + '820': 'KIM-ME-NI-OLI WASH', + '821': 'KIRKENDALL DRAW', + '822': 'KOCHIS ARROYO', + '823': 'KUTZ CANYON', + '824': 'KYLE HARRISON CANYON', + '825': 'L C CANYON', + '826': 'LA BAJADA SEEP', + '827': 'LA CANADA DE LA LOMA DE ARENA', + '828': 'LA CANADA SANTIAGA', + '829': 'LA CINTA', + '830': 'LA DOUX ARROYO', + '831': 'LA DRAW', + '832': 'LA JARA CANYON', + '833': 'LA JARA CANYON', + '834': 'LA JARA CANYON', + '835': 'LA JARA CANYON', + '836': 'LA JARA CANYON', + '837': 'LA JARA CREEK', + '838': 'LA JARA CREEK', + '839': 'LA JENCIA CREEK', + '840': 'LA LUZ CANYON', + '841': 'LA LUZ CREEK', + '842': 'LA MANGA', + '843': 'LA MANGA CREEK', + '844': 'LA MARIPOSA FLOOD CONTROL', + '845': 'LA MOSCA CANYON', + '846': 'LA PALOMA CANYON', + '847': 'LA PARITA CANYON', + '848': 'LA PLATA RIVER', + '849': 'LA POLVADERA CANYON', + '850': 'LA QUESTA DE TRUJILLO', + '851': 'LABORCITA ARROYO', + '852': 'LABORCITA CANYON', + '853': 'LADD ARROYO', + '854': 'LADERA STORM DRAIN', + '855': 'LAGUNA CANONEROS', + '856': 'LAGUNA DEL PERRO', + '857': 'LAKE ALICE', + '858': 'LAKE AVALON', + '859': 'LAKE CANYON', + '860': 'LAKE FORK CANYON', + '861': 'LAKE FORK RIO HONDO', + '862': 'LAKE MALLOY', + '863': 'LAKE MCMILLAN', + '864': 'LAKE VALLEY DRAW /N', + '865': 'LAKE VAN', + '866': 'LAMPBRIGHT DRAW', + '867': 'LAMY CANYON', + '868': 'LARGA CANYON', + '869': 'LARGO CANYON', + '870': 'LARGO CANYON /N', + '871': 'LARGO CREEK', + '872': 'LAS ANIMAS CREEK', + '873': 'LAS HUERTAS CREEK', + '874': 'LAS TABLAS CANYON', + '875': 'LAS YEGUAS CANYON', + '876': 'LAST CHANCE CANYON', + '877': 'LAST CHANCE DRAW', + '878': 'LATIR LAKE CREEK', + '879': 'LEA PLATEAU AREA 1', + '880': 'LEA PLATEAU AREA 2', + '881': 'LEA PLATEAU AREA 3', + '882': 'LEA PLATEAU AREA 4', + '883': 'LEACHMAN DRAW', + '884': 'LEAD MINE CANYON', + '885': 'LEASBURG ARROYO', + '886': 'LEFEBRES CREEK', + '887': 'LEFT FORK SACATON DRAW', + '888': 'LEGGETT CANYON', + '889': 'LEWIS CANYON', + '890': 'LINCOLN CANYON', + '891': 'LINCOLN CANYON', + '892': 'LITTLE BRUSHY CANYON', + '893': 'LITTLE CHERRY CREEK', + '894': 'LITTLE COYOTE CREEK', + '895': 'LITTLE CREEK', + '896': 'LITTLE DOG CANYON', + '897': 'LITTLE DRY CANYON', + '898': 'LITTLE DRY CREEK', + '899': 'LITTLE FLORIDA DRAWS /N', + '900': 'LITTLE HASPAROS CANYON', + '901': 'LITTLE MCKITTRICK DRAW', + '902': 'LITTLE PALLUCHE CANYON', + '903': 'LITTLE PAT CANYON', + '904': 'LITTLE RECHUELOS', + '905': 'LITTLE WALNUT CREEK', + '906': 'LOBO CANYON', + '907': 'LOBO CANYON', + '908': 'LOBO CREEK', + '909': 'LOBO CREEK', + '910': 'LOCKLER CANYON', + '911': 'LONG ARROYO', + '912': 'LONG CANYON', + '913': 'LONG CANYON', + '914': 'LONG CANYON', + '915': 'LOOKOUT CANYON', + '916': 'LORDSBURG DRAW', + '917': 'LOS ALAMOS CANYON', + '918': 'LOS CASTILLOS ARROYO /NM', + '919': 'LOS CEDROS ARROYO /N', + '920': 'LOS HUERROS CREEK', + '921': 'LOS INDIOS CANYON', + '922': 'LOS OJITOS SALADA', + '923': 'LOS PINOS RIVER', + '924': 'LOS TANOS CREEK', + '925': 'LOST GRAVE CANYON', + '926': 'LOST LAKE', + '927': 'LOST RIVER', + '928': 'LOWER DEER CREEK', + '929': 'LUCAS CANYON', + '930': 'LUMBRE CANYON', + '931': 'LUNA CREEK', + '932': 'LURANCE CANYON', + '933': 'LUTZ CANYON', + '934': 'LYNCH DRAW', + '935': 'LYONS DRAW', + '936': 'MACHO CANYON', + '937': 'MACHO CANYON', + '938': 'MACHO CREEK', + '939': 'MADERA CANON', + '940': 'MAESTAS CREEK', + '941': 'MAGADO CREEK', + '942': 'MALDONADO CANYON', + '943': 'MALONE DRAW', + '944': 'MANGAS CREEK', + '945': 'MANGAS CREEK', + '946': 'MANSFIELD WASH', + '947': 'MANUELITAS CREEK', + '948': 'MARGUERETTE CREEK /N', + '949': 'MARIANO LAKE', + '950': 'MARTHA CREEK', + '951': 'MARTIN DRAW', + '952': 'MARTINEZ CANYON', + '953': 'MARTINEZ CANYON', + '954': 'MASON DRAW', + '955': 'MCDERMOTT ARROYO', + '956': 'MCGAFFEY LAKE', + '957': 'MCKITTRICK', + '958': 'MCLANE DRAW', + '959': 'MCLEOD ARROYO', + '960': 'MEDIO CANYON', + '961': 'MEDIO DEL DIA CANYON', + '962': 'MEERSCHAUR CANYON', + '963': 'MENTZEL CANYON', + '964': 'MESA WELL CANYON', + '965': 'MESCAL CANYON', + '966': 'MESITA CREEK', + '967': 'MESQUITE STREET STORM DRAIN', + '968': 'MESTENO DRAW', + '969': 'MIDDLE ARROYO', + '970': 'MIDDLE CANYON', + '971': 'MIDDLE DOG CANYON', + '972': 'MIDDLE FORK BURRO CREEK /NM', + '973': 'MIDDLE FORK FIVEMILE DRAW', + '974': 'MIDDLE FORK GILA RIVER', + '975': 'MIDDLE FORK LAKE', + '976': 'MIDDLE FORK MASON DRAW /N', + '977': 'MIDDLE FORK RED RIVER', + '978': 'MIDDLE FORK SAPILLO CREEK', + '979': 'MIDDLE PERCHA CREEK', + '980': 'MIDDLE PONIL CREEK', + '981': 'MILAGRO CANYON /N', + '982': 'MILK LAKE', + '983': 'MILK RANCH CANYON', + '984': 'MILL CANYON', + '985': 'MILLER CANYON /N', + '986': 'MILLER CREEK', + '987': 'MILLIGAN GULCH', + '988': 'MILLS CANYON', + '989': 'MIMBRES BASIN', + '990': 'MIMBRES RIVER', + '991': 'MINER CANYON', + '992': 'MINERAL CREEK', + '993': 'MIRANDA ARROYO', + '994': 'MITT AND BAR CANYON', + '995': 'MITTEN BAR CANYON /N', + '996': 'MOCCASIN DRAW', + '997': 'MOGOLLON CREEK', + '998': 'MOGOTITO', + '999': 'MONICA CANYON', + '1000': 'MONTOYA ARROYO', + '1001': 'MONTOYA CANYON', + '1002': 'MONUMENT CANYON', + '1003': 'MONUMENT DRAW', + '1004': 'MOORE CANYON', + '1005': 'MORA RIVER', + '1006': 'MORAS CREEK', + '1007': 'MORENO CREEK', + '1008': 'MORPHY LAKE /N', + '1009': 'MOSLEY CANYON', + '1010': 'MOSLEY CANYON', + '1011': 'MOSSMAN ARROYO', + '1012': 'MUD SPRING CANYON', + '1013': 'MUD SPRINGS CANYON', + '1014': 'MUDHOLE DRAW', + '1015': 'MULE CANYON', + '1016': 'MULE CREEK', + '1017': 'MULE SPRINGS CREEK', + '1018': 'MUNIZ CANYON', + '1019': 'MYERS CANYON', + '1020': 'MYNDUS ARROYO /N', + '1021': 'McALLISTER LAKE', + '1022': 'McKNIGHT CANYON', + '1023': 'NACIMIENTO CREEK', + '1024': 'NANCE CANYON', + '1025': 'NARANJO CANYON', + '1026': 'NAVAJO RESERVOIR', + '1027': 'NAVAJO RIVER', + '1028': 'NEGRITO CREEK', + '1029': 'NEGRO CANYON', + '1030': 'NEGRO CANYON', + '1031': 'NEGRO ED CANYON', + '1032': 'NELSON CANYON', + '1033': 'NESTER CANYON', + '1034': 'NESTER DRAW', + '1035': 'NEW TANK DRAW', + '1036': 'NEW TANK DRAW', + '1037': 'NEWMAN CANYON', + '1038': 'NIELSEN WATERSHED', + '1039': 'NINEMILE CREEK', + '1040': 'NINETYSIX CREEK', + '1041': 'NOGAL ARROYO /N', + '1042': 'NOGAL CANYON /N', + '1043': 'NOGAL CANYON /N', + '1044': 'NOGAL CANYON /N', + '1045': 'NOGAL CREEK /N', + '1046': 'NOGAL DRAW', + '1047': 'NORTH BRANCH CORRUMPA CREEK', + '1048': 'NORTH CANADIAN RIVER', + '1049': 'NORTH CANYON', + '1050': 'NORTH COPPER CANYON', + '1051': 'NORTH COTTONWOOD CREEK', + '1052': 'NORTH DRY CREEK', + '1053': 'NORTH FORK ALAMOCITA CANYON', + '1054': 'NORTH FORK CLARK CANYON', + '1055': 'NORTH FORK CORRAL CANYON', + '1056': 'NORTH FORK DEVILS CREEK', + '1057': 'NORTH FORK GREEN CANYON', + '1058': 'NORTH FORK LITTLE COYOTE CREEK', + '1059': 'NORTH FORK NEGRO ED CANYON', + '1060': 'NORTH FORK PALOMAS CREEK', + '1061': 'NORTH FORK RIO LA CASA', + '1062': 'NORTH FORK RIO QUEMADO', + '1063': 'NORTH FORK SYCAMORE CREEK', + '1064': 'NORTH FORK URRACA CREEK', + '1065': 'NORTH FORK WALNUT CANYON', + '1066': 'NORTH HOLLOW CREEK', + '1067': 'NORTH MILL ARROYO', + '1068': 'NORTH PERCHA CREEK', + '1069': 'NORTH PLAINS', + '1070': 'NORTH PONIL CREEK', + '1071': 'NORTH SALEM ARROYO', + '1072': 'NORTH SEVEN RIVERS', + '1073': 'NORTH SEVEN RIVERS', + '1074': 'NORTH SPRING CANYON', + '1075': 'NORTH TEXAS HILL CANYON /NM', + '1076': 'NORTHRUP CANYON', + '1077': 'NUTRIA NO. 1', + '1078': 'NUTRIA NO. 2', + '1079': 'OAK CANYON', + '1080': 'OAK DRAW', + '1081': 'OAK SPRING CREEK', + '1082': 'OCATE CREEK', + '1083': 'OJITO SECO', + '1084': 'OJITOS CANYON', + '1085': 'OJO CALIENTE', + '1086': 'OJO DE GALLO SPRING', + '1087': 'OJO DE LOS POSOS', + '1088': 'OJO DE PALO BLANCO', + '1089': 'OJO NEGRO CREEK', + '1090': 'OJO REDONDO CANYON', + '1091': 'OJO SARCO CREEK', + '1092': 'OLD CANYON', + '1093': 'OLD MAID CANYON', + '1094': 'ORTEGA CANYON', + '1095': 'OSO CREEK', + '1096': 'OSO CREEK', + '1097': 'OTTO DRAW', + '1098': 'OUTLAW CANYON', + '1099': 'OWL CANYON', + '1100': 'OWL DRAW', + '1101': 'OX CANYON', + '1102': 'OX SPRING CANYON', + '1103': 'PAIGE DRAW', + '1104': 'PAJARITO ARROYO', + '1105': 'PAJARITO CANYON', + '1106': 'PAJARITO CANYON', + '1107': 'PAJARITO CREEK', + '1108': 'PAJARO CANYON', + '1109': 'PALEO CREEK', + '1110': 'PALIZA CANYON', + '1111': 'PALLUCHE CANYON', + '1112': 'PALLUCHE WASH', + '1113': 'PALMILLA DRAW', + '1114': 'PALO BLANCO', + '1115': 'PALOMA CANYON', + '1116': 'PALOMAS CREEK', + '1117': 'PALOMAS GAP CREEK', + '1118': 'PANCHUELA CREEK', + '1119': 'PATOS CREEK', + '1120': 'PATTERSON CANYON', + '1121': 'PECOS RIVER AREA 1', + '1122': 'PECOS RIVER AREA 2', + '1123': 'PECOS RIVER AREA 3', + '1124': 'PECOS RIVER AREA 4', + '1125': 'PECOS RIVER AREA 5', + '1126': 'PEDERNAL', + '1127': 'PENA BLANCA ARROYO', + '1128': 'PENASCO CANYON', + '1129': 'PENNSYLVANIA CANYON', + '1130': 'PEPPER CANYON', + '1131': 'PEPPIN CANYON /N', + '1132': 'PEPPIN CANYON /N', + '1133': 'PERALTA CANYON', + '1134': 'PERCHA CREEK', + '1135': 'PERICO CREEK', + '1136': 'PERK CANYON', + '1137': 'PHILADELPHIA CANYON', + '1138': 'PIEDRA LISA ARROYO', + '1139': 'PIEDRA LUMBRE', + '1140': 'PIEDRAS MARCADAS', + '1141': 'PIERCE CANYON', + '1142': 'PIGPEN CREEK', + '1143': 'PINABETE CREEK', + '1144': 'PINABETES CREEK', + '1145': 'PINE CANYON', + '1146': 'PINE CANYON', + '1147': 'PINE CANYON', + '1148': 'PINE CANYON', + '1149': 'PINE CIENEGA CREEK', + '1150': 'PINE CREEK', + '1151': 'PINE TREE CANYON', + '1152': 'PINKEY WRIGHT CANYON', + '1153': 'PINO CANYON', + '1154': 'PINON CREEK', + '1155': 'PINON WASH', + '1156': 'PINTADA ARROYO', + '1157': 'PIONEER CREEK', + '1158': 'PIPE LINE DRAW', + '1159': 'PLACER CREEK', + '1160': 'PLACITAS ARROYO', + '1161': 'PLACITAS ARROYO', + '1162': 'PLACITAS CREEK /N', + '1163': 'PLAYAS BASIN', + '1164': 'PLAYAS LAKE', + '1165': 'PLAZA LARGA CREEK', + '1166': 'POINT OF ROCKS CANYON', + '1167': 'POJOAQUE RIVER', + '1168': 'POLE CANYON', + '1169': 'POLEO CREEK', + '1170': 'POLVADERA CREEK', + '1171': 'PONIL CREEK', + '1172': 'POPE CANYON', + '1173': 'PORTER DRAW', + '1174': 'PORTER-WISENHUNT ARROYO', + '1175': 'PORVENIR CANYON', + '1176': 'POST OFFICE CANYON', + '1177': 'POTATO CANYON', + '1178': 'POVERTY CREEK', + '1179': 'PRAIRIE CANYON', + '1180': 'PRIDE DRAW', + '1181': 'PRIEST CANYON', + '1182': 'PROP CANYON', + '1183': 'PUEBLO CREEK', + '1184': 'PUERCO RIVER', + '1185': 'PUERTO CREEK', + '1186': 'PUMP CANYON', + '1187': 'PURGATOIRE RIVER', + '1188': 'QUEMADO CANYON', + '1189': 'QUEMADO LAKE', + '1190': 'QUERECHO PLAINS', + '1191': 'RAFAEL CREEK /NM', + '1192': 'RAILROAD CANYON', + '1193': 'RAILROAD WASH', + '1194': 'RAINBOW WASH', + '1195': 'RALPH ARROYO', + '1196': 'RANA CANYON', + '1197': 'RANCHERIA CANYON', + '1198': 'RATON ARROYO /N', + '1199': 'RATON CREEK', + '1200': 'RATTLESNAKE CANYON', + '1201': 'RATTLESNAKE CREEK', + '1202': 'RAYADO CREEK', + '1203': 'RAYMAC ARROYO', + '1204': 'RED BLUFF DRAW', + '1205': 'RED CANYON', + '1206': 'RED CANYON', + '1207': 'RED LAKE', + '1208': 'RED RIVER', + '1209': 'RED RIVER', + '1210': 'RED SPRING DRAW', + '1211': 'REDROCK CANYON /N', + '1212': 'REDROCK CANYON /N', + '1213': 'REECE CANYON', + '1214': 'REED THURMAN ARROYO', + '1215': 'REUNION DRAW', + '1216': 'REVENTON DRAW', + '1217': 'REVUELTO CREEK', + '1218': 'RHODES ARROYO', + '1219': 'RICHARDSON CANYON', + '1220': 'RICKETSON DRAW', + '1221': 'RILEY CANYON', + '1222': 'RINCON ARROYO', + '1223': 'RINCON LARGO', + '1224': 'RINCONADA CANYON /N', + '1225': 'RINCONADA CREEK /N', + '1226': 'RIO AGUA NEGRA /N', + '1227': 'RIO BONITO', + '1228': 'RIO BRAZOS', + '1229': 'RIO CAPULIN /N', + '1230': 'RIO CEBOLLA', + '1231': 'RIO CEBOLLA', + '1232': 'RIO CHAMA', + '1233': 'RIO CHAMITA', + '1234': 'RIO CHIQUITO /N', + '1235': 'RIO CHIQUITO /N', + '1236': 'RIO CHUPADERO', + '1237': 'RIO DE ARENAS', + '1238': 'RIO DE LAS TRAMPAS', + '1239': 'RIO DE LAS VACAS', + '1240': 'RIO DE LOS PINOS', + '1241': 'RIO DE TRUCHAS', + '1242': 'RIO DEL MEDIO', + '1243': 'RIO DEL OSO', + '1244': 'RIO DEL PLANO', + '1245': 'RIO EN MEDIO', + '1246': 'RIO EN MEDIO', + '1247': 'RIO FELIX', + '1248': 'RIO FERNANDO DE TAOS', + '1249': 'RIO FRIJOLES', + '1250': 'RIO GALLINA', + '1251': 'RIO GRANDE AREA 0 (COLORADO)', + '1252': 'RIO GRANDE AREA 1', + '1253': 'RIO GRANDE AREA 2', + '1254': 'RIO GRANDE AREA 3', + '1255': 'RIO GRANDE AREA 4', + '1256': 'RIO GRANDE AREA 5', + '1257': 'RIO GRANDE AREA 6', + '1258': 'RIO GRANDE DE RANCHO', + '1259': 'RIO GUADALUPE', + '1260': 'RIO HONDO', + '1261': 'RIO HONDO', + '1262': 'RIO LA CASA', + '1263': 'RIO LUCERO', + '1264': 'RIO MEDIO', + '1265': 'RIO MOQUINO', + '1266': 'RIO MORA', + '1267': 'RIO NAMBE', + '1268': 'RIO NUTRIA', + '1269': 'RIO NUTRIAS', + '1270': 'RIO NUTRITAS', + '1271': 'RIO PAGUATE', + '1272': 'RIO PENASCO', + '1273': 'RIO PESCADO', + '1274': 'RIO PUEBLO', + '1275': 'RIO PUEBLO DE TAOS', + '1276': 'RIO PUERCO', + '1277': 'RIO PUERCO DE CHAMA', + '1278': 'RIO QUEMADO', + '1279': 'RIO RUIDOSO', + '1280': 'RIO SALADO', + '1281': 'RIO SALADO', + '1282': 'RIO SAN ANTONIO', + '1283': 'RIO SAN JOSE /N', + '1284': 'RIO SAN LEONARDO', + '1285': 'RIO SANTA BARBARA', + '1286': 'RIO TESUQUE', + '1287': 'RIO TIERRA AMARILLA', + '1288': 'RIO TULAROSA', + '1289': 'RIO TUSAS', + '1290': 'RIO VALDEZ', + '1291': 'RIO VALLECITOS /N', + '1292': 'RIO YAQUI', + '1293': 'RITO AZUL', + '1294': 'RITO CEBOLLA', + '1295': 'RITO CIENEGUILLA /N', + '1296': 'RITO CIENEGUILLA /N', + '1297': 'RITO CREEK', + '1298': 'RITO CREEK', + '1299': 'RITO DE AGUA NEGRA CHIQUITA /N', + '1300': 'RITO DE GASCON', + '1301': 'RITO DE LA LAMA', + '1302': 'RITO DE LA OLLA', + '1303': 'RITO DE LAS PALOMAS', + '1304': 'RITO DE LAS SILLAS', + '1305': 'RITO DE LOS FRIJOLES', + '1306': 'RITO DE LOS PINOS', + '1307': 'RITO DE TIERRA AMARILLA', + '1308': 'RITO DEL BUEY /N', + '1309': 'RITO DEL MEDIO', + '1310': 'RITO DEL MEDIO', + '1311': 'RITO DEL OJO', + '1312': 'RITO DEL TANQUE', + '1313': 'RITO ENCINO', + '1314': 'RITO GARCIA', + '1315': 'RITO GRIEGO /N', + '1316': 'RITO JAROSO', + '1317': 'RITO LA PRESA', + '1318': 'RITO LECHE', + '1319': 'RITO MORPHY', + '1320': 'RITO PENAS NEGRAS', + '1321': 'RITO PRIMERO', + '1322': 'RITO QUEMAZON', + '1323': 'RITO ROMERO /N', + '1324': 'RITO SAN JOSE', + '1325': 'RITO SECO', + '1326': 'ROAD CANYON', + '1327': 'ROAD CREEK', + '1328': 'ROBINSON DRAW', + '1329': 'ROCK CANYON', + '1330': 'ROCK CREEK', + '1331': 'ROCK CREEK', + '1332': 'ROCK HOUSE CANYON', + '1333': 'ROCK HOUSE CANYON', + '1334': 'ROCK HOUSE CANYON', + '1335': 'ROCK LAKE', + '1336': 'ROCK LAKE CREEK', + '1337': 'ROCK SPRINGS CANYON', + '1338': 'ROCK TANK CANYON', + '1339': 'ROCK WATERHOLE CANYON', + '1340': 'ROCKY ARROYO', + '1341': 'RODEY ARROYO', + '1342': 'ROMERO CANYON', + '1343': 'ROUGH CANYON', + '1344': 'ROUGH CREEK', + '1345': 'RUSSIA CANYON', + '1346': 'S CURVE CANYON', + '1347': 'S U CANYON', + '1348': 'SABINATA FLAT ARROYO', + '1349': 'SACATON CREEK', + '1350': 'SACATON DRAW', + '1351': 'SACRAMENTO RIVER', + '1352': 'SAGEBRUSH VALLEY', + '1353': 'SALADITO CREEK', + '1354': 'SALADO', + '1355': 'SALADO ARROYO /NM', + '1356': 'SALADO CANYON', + '1357': 'SALADO CREEK', + '1358': 'SALADO CREEK', + '1359': 'SALADO CREEK', + '1360': 'SALADO CREEK', + '1361': 'SALADO CREEK', + '1362': 'SALADO CREEK', + '1363': 'SALADON CREEK', + '1364': 'SALITRAL CREEK', + '1365': 'SALIZ CANYON', + '1366': 'SALOPEK ARROYO', + '1367': 'SALT BASIN', + '1368': 'SALT CREEK', + '1369': 'SALT CREEK', + '1370': 'SALT CREEK /N', + '1371': 'SALT DRAW', + '1372': 'SALTPETER CREEK', + '1373': 'SAMBRITO CREEK', + '1374': 'SAN ANDREAS CANYON', + '1375': 'SAN ANTONIO CREEK', + '1376': 'SAN ANTONIO CREEK /NM', + '1377': 'SAN AUGUSTIN PLAINS', + '1378': 'SAN CRISTOBAL', + '1379': 'SAN FRANCISCO RIVER', + '1380': 'SAN IGNACIO CREEK', + '1381': 'SAN ISIDRO ARROYO /N', + '1382': 'SAN ISIDRO CREEK', + '1383': 'SAN ISIDRO WASH /N', + '1384': 'SAN JON CREEK', + '1385': 'SAN JOSE CANYON /N', + '1386': 'SAN JUAN AREA 1', + '1387': 'SAN JUAN AREA 2', + '1388': 'SAN JUAN CANYON', + '1389': 'SAN JUAN CANYON', + '1390': 'SAN LEONARDO CANYON', + '1391': 'SAN LORENZO ARROYO', + '1392': 'SAN LUCAS CANYON', + '1393': 'SAN MARCOS ARROYO', + '1394': 'SAN MATEO CANYON', + '1395': 'SAN MATEO CREEK', + '1396': 'SAN MIGUEL CANYON /N', + '1397': 'SAN MIGUEL CREEK /N', + '1398': 'SAN PABLO CANYON', + '1399': 'SAN PEDRO CREEK', + '1400': 'SAN SIMON CIENEGA', + '1401': 'SAN SIMON CREEK', + '1402': 'SAN VICENTE ARROYO', + '1403': 'SANCHEZ CANYON', + '1404': 'SANCHEZ CANYON', + '1405': 'SAND DRAW /N', + '1406': 'SAND FLAT CANYON', + '1407': 'SAND HILL ARROYO', + '1408': 'SAND WASH', + '1409': 'SANDY CANYON', + '1410': 'SANGUIJUELA CREEK /N', + '1411': 'SANTA CLARA CREEK', + '1412': 'SANTA CRUZ RIVER', + '1413': 'SANTA FE RIVER', + '1414': 'SANTA RITA CANYON', + '1415': 'SANTA RITA CREEK', + '1416': 'SANTA TERESA', + '1417': 'SANTIAGO CREEK /N', + '1418': 'SAPELLO RIVER', + '1419': 'SAPILLO CREEK', + '1420': 'SARDINAS CANYON', + '1421': 'SARGENT CANYON', + '1422': 'SARGENT CANYON', + '1423': 'SAUBLE ARROYO', + '1424': 'SAUZ CREEK', + '1425': 'SAVENNETA ARROYO /N', + '1426': 'SAWMILL CANYON', + '1427': 'SAWMILL CANYON', + '1428': 'SAWMILL CREEK', + '1429': 'SAWYER CREEK', + '1430': 'SCHOOL SECTION CANYON', + '1431': 'SEBOYETA CREEK', + '1432': 'SEBOYETITA CREEK', + '1433': 'SECO CANYON', + '1434': 'SECO CREEK', + '1435': 'SEEP SPRING DRAW', + '1436': 'SEGREST DRAW', + '1437': 'SENECA CREEK', + '1438': 'SENORITO CANYON', + '1439': 'SEVEN LAKES ARROYO', + '1440': 'SEVENTEEN CANYON', + '1441': 'SEVENTYSIX DRAW', + '1442': 'SHAKESPEARE ARROYO', + '1443': 'SHAW CANYON', + '1444': 'SHEEP CAMP', + '1445': 'SHEEP CANYON', + '1446': 'SHEEP CORRAL DRAW', + '1447': 'SHEEP PEN CANYON', + '1448': 'SHELBY CLARK CANYON', + '1449': 'SHELLY DITCH', + '1450': 'SHIELDS CANYON', + '1451': 'SHILOH DRAW', + '1452': 'SHINGLE CANYON', + '1453': 'SHIPMAN CANYON', + '1454': 'SHOEMAKER CANYON', + '1455': 'SHUMWAY ARROYO', + '1456': 'SIBLEY CANON', + '1457': 'SIBLEY GAP', + '1458': 'SIEGREST DRAW /N', + '1459': 'SILVA CANYON', + '1460': 'SILVER CANYON', + '1461': 'SILVER CITY DRAW /N', + '1462': 'SILVER CREEK', + '1463': 'SILVER SPRINGS CANYON', + '1464': 'SILVER SPRINGS CREEK', + '1465': 'SIMON CANYON', + '1466': 'SIMPSON DRAW', + '1467': 'SITTING BULL CANYON', + '1468': 'SIXMILE CANYON', + '1469': 'SIXMILE CREEK', + '1470': 'SIXTEEN CANYON', + '1471': 'SKULL CANYON', + '1472': 'SKUNK CANYON', + '1473': 'SKUTE STONE ARROYO', + '1474': 'SLATE CREEK', + '1475': 'SLOUGH CANYON', + '1476': 'SMITH CANYON', + '1477': 'SMUGGLER CREEK', + '1478': 'SNAKY CANYON', + '1479': 'SNARE CANYON', + '1480': 'SNARE CANYON', + '1481': 'SNOW CANYON', + '1482': 'SODA POCKET CREEK', + '1483': 'SOL SE METE CANYON', + '1484': 'SOUTH BERRENDO CREEK', + '1485': 'SOUTH BRANCH CORRUMPA CREEK', + '1486': 'SOUTH COPPER CANYON', + '1487': 'SOUTH COTTONWOOD CREEK', + '1488': 'SOUTH FORK ARROYO LEON', + '1489': 'SOUTH FORK BIG CREEK', + '1490': 'SOUTH FORK CLARK CANYON', + '1491': 'SOUTH FORK CORRAL CANYON', + '1492': 'SOUTH FORK PALOMAS CREEK', + '1493': 'SOUTH FORK PUERCO RIVER', + '1494': 'SOUTH FORK ROAD CANYON', + '1495': 'SOUTH FORK SILVER CREEK', + '1496': 'SOUTH FORK SYCAMORE CREEK', + '1497': 'SOUTH FORK URRACA CREEK', + '1498': 'SOUTH FORK WALNUT CANYON', + '1499': 'SOUTH FORK WHITEWATER CREEK', + '1500': 'SOUTH PERCHA CREEK', + '1501': 'SOUTH PONIL CREEK', + '1502': 'SOUTH SEVEN RIVERS', + '1503': 'SOUTH SPRING RIVER', + '1504': 'SOUTH TANK CANYON', + '1505': 'SOUTH WYLIE DRAW', + '1506': 'SOUTHERN PACIFIC RESERVOIR', + '1507': 'SPAR CANYON', + '1508': 'SPRING ARROYO', + '1509': 'SPRING ARROYO /NM', + '1510': 'SPRING CANYON', + '1511': 'SPRING CANYON', + '1512': 'SPRING CANYON', + '1513': 'SPRING CANYON', + '1514': 'SPRING CREEK', + '1515': 'SPRING CREEK', + '1516': 'SPRING LAKE', + '1517': 'SPRINGER ARROYO', + '1518': 'SPUR TRAIL CANYON', + '1519': 'STARKWEATHER CANYON', + '1520': 'STARVATION DRAW', + '1521': 'STEEP HOLLOW', + '1522': 'STEEPLE ROCK CANYON', + '1523': 'STEINS CREEK', + '1524': 'STEVENS DRAW', + '1525': 'STINKING DRAW', + '1526': 'STINKING SPRING DRAW /N', + '1527': 'STONE CANYON', + '1528': 'STORRIE LAKE', + '1529': 'STREET CANYON', + '1530': 'STUBBLEFIELD ARROYO', + '1531': 'STYCHINE DRAW /NM', + '1532': 'SUGARITE CREEK', + '1533': 'SURVEYORS CANYON', + '1534': 'SWAN CANYON', + '1535': 'SWEETWATER CREEK', + '1536': 'SYCAMORE CREEK', + '1537': 'TAFOYA CREEK /NM', + '1538': 'TAIBAN CREEK', + '1539': 'TALLEY CANYON', + '1540': 'TAMPICO DRAW', + '1541': 'TANBARK CANYON', + '1542': 'TAPICITA CREEK', + '1543': 'TATA VIQUE RIVER', + '1544': 'TAYFOYA CANYON', + '1545': 'TAYLOR CANYON', + '1546': 'TAYLOR CREEK', + '1547': 'TECOLOTE CEEK', + '1548': 'TELEPHONE CANYON', + '1549': 'TELEPHONE CANYON', + '1550': 'TELEPHONE CANYON', + '1551': 'TENNESSEE CREEK', + '1552': 'TEQUESQUITE CREEK', + '1553': 'TERRY CANYON', + '1554': 'THIRTEEN MILE DRAW', + '1555': 'THOMPSON CANYON', + '1556': 'THOMPSON CANYON', + '1557': 'THOMPSON DRAW', + '1558': 'THREE RIVERS', + '1559': 'THREEMILE CANYON', + '1560': 'THURMAN DRAW', + '1561': 'TIENDITAS CREEK', + '1562': 'TIERRA BLANCA CREEK', + '1563': 'TIFFANY CANYON', + '1564': 'TIGRE ARROYO', + '1565': 'TIJERAS CANYON', + '1566': 'TIMBER CANYON', + '1567': 'TINAJA CREEK', + '1568': 'TOGEYE FLATS', + '1569': 'TOM MOORE CANYON', + '1570': 'TOMERLIN DRAW', + '1571': 'TORREON DRAW', + '1572': 'TORREON WASH', + '1573': 'TORTOLITA CANYON', + '1574': 'TORTOLITA CREEK', + '1575': 'TORTUGAS ARROYO', + '1576': 'TRABAJO CREEK', + '1577': 'TRAMPEROS CREEK', + '1578': 'TRAMWAY FLOODWATER', + '1579': 'TRAVESSER CREEK', + '1580': 'TRAVESSER CREEK', + '1581': 'TRES HERMANOS', + '1582': 'TRES LAGUNAS', + '1583': 'TRIB OF TRIBUTARY', + '1584': 'TRIB. OF MAJOR RIVER', + '1585': 'TRINCHERA CREEK', + '1586': 'TRINCHERA CREEK', + '1587': 'TRINCHERITA CREEK /NM', + '1588': 'TROUT CREEK', + '1589': 'TROUT SPRINGS CANYON', + '1590': 'TRUCHAS CREEK', + '1591': 'TRUJILLO CANYON', + '1592': 'TRUJILLO CREEK', + '1593': 'TRUJILLO CREEK /N', + '1594': 'TRUSDALE CANYON', + '1595': 'TSE BONITA WASH', + '1596': 'TUCKER DRAW', + '1597': 'TUCUMCARI LAKE', + '1598': 'TULAROSA BASIN', + '1599': 'TULAROSA RIVER', + '1600': 'TUMBLEWEED DRAW', + '1601': 'TURKEY ARROYO /NM', + '1602': 'TURKEY CANYON', + '1603': 'TURKEY CANYON', + '1604': 'TURKEY CREEK', + '1605': 'TUSCOCOILLO CANYON', + '1606': 'TWIN SISTERS CREEK', + '1607': 'UHL DRAW', + '1608': 'UNA DE GATO CREEK', + '1609': 'UPPER CHARETTE LAKE', + '1610': 'UPPER DOG CANYON', + '1611': 'UPPER RANCH CANYON', + '1612': 'UPPER RANCH CANYON', + '1613': 'URRACA', + '1614': 'URRACA CREEK', + '1615': 'UTE CREEK', + '1616': 'UTE CREEK', + '1617': 'UTE RESERVOIR', + '1618': 'UVAS VALLEY', + '1619': 'VACINTE ARROYO /N', + '1620': 'VALLE VIDAL', + '1621': 'VALLECITO CREEK', + '1622': 'VALLECITOS CREEK /N', + '1623': 'VAN BREMMER CANYON', + '1624': 'VANDERITAS CREEK /N', + '1625': 'VAQUEROS CANYON', + '1626': 'VEGALOSO CREEK', + '1627': 'VELARDE ARROYO', + '1628': 'VENTERO CREEK /N', + '1629': 'VERMEJO CREEK', + '1630': 'VERMEJO RIVER', + '1631': 'VERNADO CANYON', + '1632': 'VIGIL CANYON', + '1633': 'VIGIL CANYON', + '1634': 'VIGIL CANYON', + '1635': 'VILLA MORA ARROYO', + '1636': 'VINEYARD ARROYO', + '1637': 'VOGHT DRAW', + '1638': 'WALKER CANYON', + '1639': 'WALNUT CANYON', + '1640': 'WALNUT CREEK', + '1641': 'WALNUT CREEK', + '1642': 'WALNUT CREEK', + '1643': 'WALNUT CREEK', + '1644': 'WALNUT CREEK', + '1645': 'WALNUT DRAW /N', + '1646': 'WALNUT DRAW /N', + '1647': 'WAMEL BASIN', + '1648': 'WAMELS DRAW', + '1649': 'WAMPOO WASH', + '1650': 'WARDY HEDGECOCK ARROYO', + '1651': 'WARM SPRINGS CANYON', + '1652': 'WATER CANYON', + '1653': 'WATER CANYON', + '1654': 'WATER CANYON', + '1655': 'WATER CANYON', + '1656': 'WATER UNCON TO MAJOR', + '1657': 'WATER UNCON TO MINOR', + '1658': 'WATROUS CREEK', + '1659': 'WAYLAND CANYON', + '1660': 'WEBB CREEK', + '1661': 'WELTY CANYON', + '1662': 'WEST ANTELOPE DRAW', + '1663': 'WEST DOG CANYON', + '1664': 'WEST FORK GILA RIVER', + '1665': 'WEST FORK TINAJA CREEK', + '1666': 'WEST LATIR CREEK', + '1667': 'WEST PRONG ASH CREEK', + '1668': 'WEST RED CANYON', + '1669': 'WEST SHIPPING PEN TANK', + '1670': 'WESTWATER ARROYO', + '1671': 'WHEATEN CREEK', + '1672': 'WHISKEY CREEK', + '1673': 'WHITE DEER CANYON', + '1674': 'WHITE HORSE DRAW', + '1675': 'WHITE LAKE', + '1676': 'WHITE OAKS CANYON', + '1677': 'WHITE ROCK CANYON', + '1678': 'WHITEROCK CANYON', + '1679': 'WHITES DRAW', + '1680': 'WHITEWATER ARROYO', + '1681': 'WHITEWATER CANYON', + '1682': 'WHITEWATER CANYON', + '1683': 'WHITEWATER CREEK', + '1684': 'WHITEWATER CREEK', + '1685': 'WHITEWATER CREEK', + '1686': 'WHITMIRE CREEK', + '1687': 'WILD HORSE CANYON', + '1688': 'WILD HORSE CANYON /N', + '1689': 'WILDCAN CANYON', + '1690': 'WILDHORSE CANYON', + '1691': 'WILLIE WHITE CANYON', + '1692': 'WILLOW CANYON', + '1693': 'WILLOW CREEK', + '1694': 'WILLOW CREEK', + '1695': 'WILLOW CREEK /N', + '1696': 'WILLOW CREEK /N', + '1697': 'WILLOW DRAW', + '1698': 'WILLOW DRAW', + '1699': 'WILLOW LAKE', + '1700': 'WILLOW SPRING DRAW', + '1701': 'WILLS CANYON', + '1702': 'WILSON CANYON', + '1703': 'WILSON CREEK', + '1704': 'WIND CANYON', + '1705': 'WIND MOUNTAIN DRAW', + '1706': 'WINDHAM CANYON', + '1707': 'WINN CANYON', + '1708': 'WOLF CREEK', + '1709': 'WOOD CANYON', + '1710': 'WOOD CANYON', + '1711': 'WOOD CANYON', + '1712': 'WOODROW ARROYO /N', + '1713': 'WOOTEN CANYON', + '1714': 'WRIGHT CANYON /NM', + '1715': 'WYLIE DRAW', + '1716': 'WYNN CREEK', + '1717': 'YANKEE CREEK /N', + '1718': 'YELLOW POINT VALLEY', + '1719': 'YESO CREEK', + '1720': 'YL CANYON', + '1721': 'YOAST DRAW', + '1722': 'YORK CANYON', + '1723': 'YOUNG CANYON', + '1724': 'Z SLASH CANYON', + '1725': 'ZEUFELDT ARROYO', + '1726': 'ZUBER /N', + '1727': 'ZUBER HOLLOW /N', + '1728': 'ZUBI DRAW', + '1729': 'ZUNI CANYON', + '1730': 'ZUNI RIVER', + '1731': 'BEAR SPRINGS CANYON', + '1732': 'CHAVEZ CANYON', + '1733': 'BEAR CANYON', + }, + }, + USE_CODE: { + description: 'The primary use of water', + values: { + AGR: 'Agriculture other than irrigation', + AUG: 'Augmentation well', + BPW: 'Brine production well', + CEM: 'Cemetery', + CLS: 'Closed file', + COM: 'Commercial', + CON: 'Construction', + CPS: 'Cathodic protection well', + DAI: 'Dairy operation', + DCN: 'Domestic construction', + DEW: 'Dewatering well', + DOL: '72-12-1 domestic and livestock watering', + DOM: '72-12-1 domestic one household', + EXP: 'Exploration', + FCD: 'Flood control', + FGP: 'Fish and game propogation', + FPO: 'Feed pen operation', + GEO: 'Geothermal boreholes', + HWY: 'Highway construction', + IND: 'Industrial', + INJ: 'Injection', + IRR: 'Irrigation', + MDW: 'Community type use - mdwca, private or commercial supplied', + MFG: 'Manufacturing', + MIL: 'Military - military installations', + MIN: 'Mining or milling or oil', + MOB: 'Mobile home parks', + MON: 'Monitoring well', + MPP: 'Meat packing plant', + MUL: '72-12-1 multiple domestic households', + MUN: 'Municipal - city or county supplied water', + N07: 'No pre-1907 water right exists on this land', + NON: 'Non-profit organizational use', + NOT: 'No use of right or POD', + NRT: 'No right', + OBS: 'Observation', + OFM: 'Oil field maintenance', + OIL: 'Oil production', + PDL: 'Non 72-12-1 domestic and livestock watering', + PDM: 'Non 72-12-1 domestic one household', + PLS: 'Non 72-12-1 livestock watering', + PMH: 'Non 72-12-1 multiple domestic households', + POL: 'Pollution control well', + POU: 'Poultry and egg operation', + PPP: 'Petroleum processing plant', + PRO: '72-12-1 Prospecting or development of natural resource', + PUB: '72-12-1 Construction of public works', + REC: 'Recreation', + SAN: '72-12-1 Sanitary in conjunction with a commercial use', + SCH: 'School use - public, private, parochial, & universities', + SRO: 'Secondary recovery of oil', + STK: '72-12-1 livestock watering', + STO: 'Storage', + STR: 'Strategic water reserve', + SUB: 'Subdivision', + SWR: 'Stacked water right', + TBD: 'To be determined', + UTL: 'Public utility', + }, + }, +} + +export const OSE_POD_FIELDS: Record = { + OBJECTID: { + column: 'OBJECTID', + label: 'Object ID', + description: 'Unique feature identifier', + dataType: 'Long Integer', + codeTable: null, + }, + pod_basin: { + column: 'POD_BASIN', + label: 'POD Basin', + description: 'Basin designator of the POD', + dataType: 'Text', + codeTable: 'BASIN_CODE', + }, + pod_nbr: { + column: 'POD_NBR', + label: 'POD Number', + description: 'Number of the POD', + dataType: 'Text', + codeTable: null, + }, + pod_suffix: { + column: 'POD_SUFFIX', + label: 'POD Suffix', + description: 'Suffix of the POD', + dataType: 'Text', + codeTable: null, + }, + ref: { + column: 'REF', + label: 'Reference', + description: '', + dataType: 'Text', + codeTable: null, + }, + pod_name: { + column: 'POD_NAME', + label: 'POD Name', + description: 'Name of well given by the owner', + dataType: 'Text', + codeTable: null, + }, + tws: { + column: 'TWS', + label: 'Township', + description: 'PLSS Township ID', + dataType: 'Text', + codeTable: null, + }, + rng: { + column: 'RNG', + label: 'Range', + description: 'PLSS Range ID', + dataType: 'Text', + codeTable: null, + }, + sec: { + column: 'SEC', + label: 'Section', + description: 'PLSS Section number', + dataType: 'Text', + codeTable: null, + }, + qtr_4th: { + column: 'QTR_4TH', + label: 'Quarter (1/4 Section)', + description: 'First quarter (1/4th section)', + dataType: 'Text', + codeTable: 'QUARTER_CODE', + }, + qtr_16th: { + column: 'QTR_16TH', + label: 'Quarter (1/16 Section)', + description: 'Second quarter (1/16th section)', + dataType: 'Text', + codeTable: 'QUARTER_CODE', + }, + qtr_64th: { + column: 'QTR_64TH', + label: 'Quarter (1/64 Section)', + description: 'Third quarter (1/64th section)', + dataType: 'Text', + codeTable: 'QUARTER_CODE', + }, + blk: { + column: 'BLK', + label: 'Block', + description: 'Block', + dataType: 'Text', + codeTable: null, + }, + zone_: { + column: 'ZONE', + label: 'State Plane Zone', + description: 'NM State Plane Zone', + dataType: 'Text', + codeTable: 'NMSP_ZONE_CODE', + }, + x: { + column: 'X', + label: 'X Coordinate', + description: 'X coordinate value', + dataType: 'Double', + codeTable: null, + }, + y: { + column: 'Y', + label: 'Y Coordinate', + description: 'Y coordinate value', + dataType: 'Double', + codeTable: null, + }, + landgrant: { + column: 'GRANT', + label: 'Land Grant', + description: 'Land Grant name', + dataType: 'Text', + codeTable: null, + }, + legal: { + column: 'LEGAL', + label: 'Legal Description', + description: 'Other legal description of property', + dataType: 'Text', + codeTable: null, + }, + county: { + column: 'COUNTY', + label: 'County', + description: 'County', + dataType: 'Text', + codeTable: 'COUNTY_CODE', + }, + start_date: { + column: 'START_DATE', + label: 'Start Date', + description: 'Date well drilling started', + dataType: 'Date', + codeTable: null, + }, + finish_dat: { + column: 'FINISH_DATE', + label: 'Finish Date', + description: 'Date well completed', + dataType: 'Date', + codeTable: null, + }, + plug_date: { + column: 'PLUG_DATE', + label: 'Plug Date', + description: 'Date the well is plugged', + dataType: 'Date', + codeTable: null, + }, + pcw_rcv_da: { + column: 'PCW_RCV_DATE', + label: 'Proof of Completion Received', + description: 'Proof of completion of well/works', + dataType: 'Date', + codeTable: null, + }, + elevation: { + column: 'ELEVATION', + label: 'Elevation', + description: 'Elevation of well', + dataType: 'Double', + codeTable: null, + }, + depth_well: { + column: 'DEPTH_WELL', + label: 'Well Depth', + description: 'Total depth of well to nearest foot', + dataType: 'Double', + codeTable: null, + }, + grnd_wtr_s: { + column: 'GRND_WTR_SRC', + label: 'Groundwater Source Type', + description: 'Type of groundwater source', + dataType: 'Text', + codeTable: 'GW_SRC_TYPE_CODE', + }, + percent_sh: { + column: 'PERCENT_SHALLOW', + label: 'Percent Shallow', + description: 'Percent of shallow', + dataType: 'Long Integer', + codeTable: null, + }, + depth_wate: { + column: 'DEPTH_WATER', + label: 'Depth to Water', + description: 'Depth to water at completion of well to the nearest foot', + dataType: 'Double', + codeTable: null, + }, + log_file_d: { + column: 'LOG_FILE_DATE', + label: 'Well Record Filed Date', + description: 'Date the well record was filed with the OSE', + dataType: 'Date', + codeTable: null, + }, + sched_date: { + column: 'SCHED_DATE', + label: 'Well Schedule Date', + description: 'Date of the last well schedule for a well', + dataType: 'Date', + codeTable: null, + }, + use_of_wel: { + column: 'USE_OF_WELL', + label: 'Use Of Well', + description: 'Use of well', + dataType: 'Text', + codeTable: null, + }, + pump_type: { + column: 'PUMP_TYPE', + label: 'Pump Type', + description: 'Description of pump type', + dataType: 'Text', + codeTable: 'PUMP_TYPE_CODE', + }, + pump_seria: { + column: 'PUMP_SERIAL', + label: 'Pump Serial', + description: 'Serial number of the pump on the well', + dataType: 'Text', + codeTable: null, + }, + discharge: { + column: 'DISCHARGE', + label: 'Discharge Pipe Size', + description: 'Size of the discharge pipe from the well', + dataType: 'Text', + codeTable: null, + }, + aquifer: { + column: 'AQUIFER', + label: 'Aquifer', + description: + 'Aquifer in which the well is completed or produces water from', + dataType: 'Text', + codeTable: null, + }, + sys_date: { + column: 'SYS_DATE', + label: 'System Date', + description: 'System date for which record was made', + dataType: 'Date', + codeTable: null, + }, + subdiv_nam: { + column: 'SUBDIV_NAME', + label: 'Subdivision Name', + description: 'Subdivision name', + dataType: 'Text', + codeTable: null, + }, + subdiv_loc: { + column: 'SUBDIV_LOCATION', + label: 'Subdivision Location', + description: 'Subdivision location', + dataType: 'Text', + codeTable: null, + }, + restrict_: { + column: 'RESTRICT', + label: 'Diversion Restriction', + description: 'Maximum amount of water that can be diverted from POD', + dataType: 'Double', + codeTable: null, + }, + lat_deg: { + column: 'LAT_DEG', + label: 'Latitude Degrees', + description: 'Latitude degrees', + dataType: 'Double', + codeTable: null, + }, + lat_min: { + column: 'LAT_MIN', + label: 'Latitude Minutes', + description: 'Latitude minutes', + dataType: 'Double', + codeTable: null, + }, + lat_sec: { + column: 'LAT_SEC', + label: 'Latitude Seconds', + description: 'Latitude seconds', + dataType: 'Double', + codeTable: null, + }, + lon_deg: { + column: 'LON_DEG', + label: 'Longitude Degrees', + description: 'Longitude degrees', + dataType: 'Double', + codeTable: null, + }, + lon_min: { + column: 'LON_MIN', + label: 'Longitude Minutes', + description: 'Longitude minutes', + dataType: 'Double', + codeTable: null, + }, + lon_sec: { + column: 'LON_SEC', + label: 'Longitude Seconds', + description: 'Longitude seconds', + dataType: 'Double', + codeTable: null, + }, + surface_co: { + column: 'SURFACE_CODE', + label: 'Surface Water Source', + description: + 'Surface water code for surface water diversions - derived from USGS river course codes', + dataType: 'Long Integer', + codeTable: 'SURFACE_SOURCE_CODE', + }, + estimate_y: { + column: 'ESTIMATE_YIELD', + label: 'Estimated Yield', + description: 'Estimated yield in gallons per minute', + dataType: 'Double', + codeTable: null, + }, + pod_status: { + column: 'POD_STATUS', + label: 'POD Status', + description: 'Status of the POD', + dataType: 'Text', + codeTable: 'POD_STATUS_CODE', + }, + casing_siz: { + column: 'CASING_SIZE', + label: 'Casing Size', + description: 'Size of hole, diameter in inches', + dataType: 'Double', + codeTable: null, + }, + ditch_name: { + column: 'DITCH_NAME', + label: 'Ditch Name', + description: 'Name of ditch, acequia or spring', + dataType: 'Text', + codeTable: null, + }, + utm_zone: { + column: 'UTM_ZONE', + label: 'UTM Zone', + description: 'UTM coordinate reference system zone', + dataType: 'Text', + codeTable: null, + }, + easting: { + column: 'EASTING', + label: 'Easting', + description: 'UTM Easting coordinate value', + dataType: 'Double', + codeTable: null, + }, + northing: { + column: 'NORTHING', + label: 'Northing', + description: 'UTM Northing coordinate value', + dataType: 'Double', + codeTable: null, + }, + datum: { + column: 'DATUM', + label: 'Datum', + description: 'UTM coordinate reference system datum', + dataType: 'Text', + codeTable: 'DATUM_CODE', + }, + utm_source: { + column: 'UTM_SOURCE', + label: 'UTM Source', + description: 'Source of UTM coordinates', + dataType: 'Text', + codeTable: 'COORD_SOURCE_CODE', + }, + utm_accura: { + column: 'UTM_ACCURACY', + label: 'UTM Accuracy', + description: 'Accuracy of UTM coordinates', + dataType: 'Text', + codeTable: 'COORD_ACC_CODE', + }, + xy_source: { + column: 'XY_SOURCE', + label: 'XY Source', + description: 'Source of XY coordinates', + dataType: 'Text', + codeTable: 'COORD_SOURCE_CODE', + }, + xy_accurac: { + column: 'XY_ACCURACY', + label: 'XY Accuracy', + description: 'Accuracy of XY coordinates', + dataType: 'Text', + codeTable: 'COORD_ACC_CODE', + }, + lat_lon_so: { + column: 'LAT_LON_SOURCE', + label: 'Latitude Longitude Source', + description: 'Source of Lat-Long coordinates', + dataType: 'Text', + codeTable: 'COORD_SOURCE_CODE', + }, + lat_lon_ac: { + column: 'LAT_LON_ACCURACY', + label: 'Latitude Longitude Accuracy', + description: 'Accuracy of Lat-Long coordinates', + dataType: 'Text', + codeTable: 'COORD_ACC_CODE', + }, + tract_nbr: { + column: 'TRACT_NBR', + label: 'Tract Number', + description: 'Hydrographic survey tract number', + dataType: 'Text', + codeTable: null, + }, + map_nbr: { + column: 'MAP_NBR', + label: 'Map Number', + description: 'Hydrographic survey map number', + dataType: 'Text', + codeTable: null, + }, + surv_map: { + column: 'SURV_MAP', + label: 'Survey Map Name', + description: 'Hydrographic survey map name', + dataType: 'Text', + codeTable: null, + }, + other_loc: { + column: 'OTHER_LOC', + label: 'Other Location', + description: + 'Description of other location for POD as derived from documentation', + dataType: 'Text', + codeTable: null, + }, + pod_rec_nb: { + column: 'POD_REC_NBR', + label: 'POD Record Number', + description: + 'Serial key for POD table in WATERS, can be used to relate POD_PERF table', + dataType: 'Long Integer', + codeTable: null, + }, + cfs_start_: { + column: 'CFS_START_MDAY', + label: 'CFS Start (Month/Day)', + description: '', + dataType: 'Date', + codeTable: null, + }, + cfs_end_md: { + column: 'CFS_END_MDAY', + label: 'CFS End (Month/Day)', + description: '', + dataType: 'Date', + codeTable: null, + }, + cfs_cnv_fa: { + column: 'CFS_CNV_FACTOR', + label: 'CFS Conversion Factor', + description: '', + dataType: 'Double', + codeTable: null, + }, + cs_code: { + column: 'CS_CODE', + label: 'Coordinate System Code', + description: 'Coordinate reference system code', + dataType: 'Short Integer', + codeTable: 'CS_CODE', + }, + wrats_s_id: { + column: 'WRATS_S_ID', + label: 'WRATS POD ID', + description: 'WRATS numeric POD identifier', + dataType: 'Long Integer', + codeTable: null, + }, + utm_error: { + column: 'UTM_ERROR', + label: 'UTM Conversion Error', + description: 'Error code returned from coordinate conversion routine', + dataType: 'Text', + codeTable: null, + }, + pod_sub_ba: { + column: 'POD_SUB_BASIN', + label: 'POD Sub Basin', + description: 'Sub-basin ID for POD', + dataType: 'Text', + codeTable: 'SUBBASIN_CODE', + }, + well_tag: { + column: 'WELL_TAG', + label: 'Well Tag', + description: 'OSE well tag ID', + dataType: 'Text', + codeTable: null, + }, + static_lev: { + column: 'STATIC_LEVEL', + label: 'Static Water Level', + description: + 'Depth to water once equilibrium has been reached after drilling, this measurement is used in impairment analysis and indicates whether the aquifer is under confinement', + dataType: 'Long Integer', + codeTable: null, + }, + pod_file: { + column: 'POD_FILE', + label: 'POD File Number', + description: + 'Concatonation of POD_BASIN, POD_NBR and POD_SUFFIX fields to create POD label', + dataType: 'Text', + codeTable: null, + }, + sum_rec_nb: { + column: 'SUM_REC_NBR', + label: 'Water Right Record Number', + description: 'Serial key for Water Right record', + dataType: 'Long Integer', + codeTable: null, + }, + basin: { + column: 'BASIN', + label: 'Basin', + description: 'Basin designator of the water right file', + dataType: 'Text', + codeTable: 'BASIN_CODE', + }, + nbr: { + column: 'NBR', + label: 'File Number', + description: 'Number of the water right file', + dataType: 'Text', + codeTable: null, + }, + suffix: { + column: 'SUFFIX', + label: 'File Suffix', + description: 'Suffix of the water right file', + dataType: 'Text', + codeTable: null, + }, + sub_basin: { + column: 'SUB_BASIN', + label: 'Sub Basin', + description: 'Sub-basin identifier', + dataType: 'Text', + codeTable: 'SUBBASIN_CODE', + }, + status: { + column: 'STATUS', + label: 'Status', + description: 'The current status of a water right', + dataType: 'Text', + codeTable: 'STATUS_CODE', + }, + use_: { + column: 'USE', + label: 'Use', + description: 'Specific water right use', + dataType: 'Text', + codeTable: 'USE_CODE', + }, + total_div: { + column: 'TOTAL_DIV', + label: 'Total Diversion', + description: 'Amount of water in acre-feet allowed to be diverted', + dataType: 'Double', + codeTable: null, + }, + sub_file: { + column: 'SUB_FILE', + label: 'Adjudication Subfile', + description: 'Adjudication sub-file number associated with water right', + dataType: 'Text', + codeTable: null, + }, + sf_header: { + column: 'SF_HEADER', + label: 'Adjudication Subfile Header', + description: 'Adjudication sub-file header associated with water right', + dataType: 'Text', + codeTable: null, + }, + db_file: { + column: 'DB_FILE', + label: 'Water Right File', + description: + 'Concatonation of BASIN, NBR and SUFFIX fields to create water right file label', + dataType: 'Text', + codeTable: null, + }, + own_lname: { + column: 'OWN_LNAME', + label: 'Owner Last Name', + description: 'Last name of applicant/owner', + dataType: 'Text', + codeTable: null, + }, + own_fname: { + column: 'OWN_FNAME', + label: 'Owner First Name', + description: 'First name of applicant/owner', + dataType: 'Text', + codeTable: null, + }, + addr1: { + column: 'ADDR1', + label: 'Address Line 1', + description: 'First line of address', + dataType: 'Text', + codeTable: null, + }, + addr2: { + column: 'ADDR2', + label: 'Address Line 2', + description: 'Second line of address', + dataType: 'Text', + codeTable: null, + }, + city: { + column: 'CITY', + label: 'City', + description: 'City', + dataType: 'Text', + codeTable: null, + }, + state: { + column: 'STATE', + label: 'State', + description: 'State', + dataType: 'Text', + codeTable: 'STATE_CODE', + }, + zip: { + column: 'ZIP', + label: 'ZIP Code', + description: 'USPS Zip code', + dataType: 'Text', + codeTable: null, + }, + contact_ln: { + column: 'CONTACT_LNAME', + label: 'Contact Last Name', + description: 'Contact person last name', + dataType: 'Text', + codeTable: null, + }, + contact_fn: { + column: 'CONTACT_FNAME', + label: 'Contact First Name', + description: 'Contact person first name', + dataType: 'Text', + codeTable: null, + }, + nmwrrs_wrs: { + column: 'NMWRRS_WRSUM_URL', + label: 'NMWRRS Water Right Summary URL', + description: 'URL link to NMWRRS created using BASIN, NBR and SUFFIX field', + dataType: 'Text', + codeTable: null, + }, + in_state: { + column: 'IN_STATE', + label: 'In-State Flag', + description: 'In-state / Out-of-state location flag', + dataType: 'Short Integer', + codeTable: null, + }, + loc_error: { + column: 'LOC_ERROR', + label: 'Location Error', + description: 'POD location error code', + dataType: 'Short Integer', + codeTable: 'LOCATION_ERROR_CODE', + }, + wr_count: { + column: 'WR_COUNT', + label: 'Water Right File Count', + description: + 'Indicates the number of water right files assocated with a given POD', + dataType: 'Short Integer', + codeTable: null, + }, + replaced: { + column: 'REPLACED', + label: 'Replaced', + description: + 'Indicates the number of times that a given POD has been recorded as having been replaced or substitued for another point of diversion.', + dataType: 'Short Integer', + codeTable: null, + }, +} + +/** Decodes a coded value using the field's code table, falling back to the raw value. */ +export const decodeOSEPODValue = ( + field: string, + value: unknown +): string | null => { + if (value == null || value === '') return null + + const definition = OSE_POD_FIELDS[field] + const table = definition?.codeTable + ? OSE_POD_CODE_TABLES[definition.codeTable] + : undefined + + return table?.values[String(value).trim()] ?? String(value) +} diff --git a/src/constants/usgsSiteDictionary.ts b/src/constants/usgsSiteDictionary.ts new file mode 100644 index 00000000..613539ce --- /dev/null +++ b/src/constants/usgsSiteDictionary.ts @@ -0,0 +1,5555 @@ +// GENERATED FILE — do not edit by hand. +// Source: USGS Water Data OGC API reference collections +// (https://api.waterdata.usgs.gov/ogcapi/v0/collections). +// Regenerate: python3 scripts/generate_usgs_site_dictionary.py +// +// The NWIS site service returns coded values and its RDB header supplies the +// column labels, but the code meanings live in these reference lists. + +export type USGSCodeEntry = { + label: string + description?: string +} + +export const USGS_CODE_TABLES: Record> = { + 'agency-codes': { + AK001: { + label: 'Alaska Department of Transportation and Public Facilities', + }, + AK002: { label: 'Alaska Department of Environmental Conservation' }, + AK004: { label: 'Alaska Department of Natural Resources (DNR)' }, + AK008: { label: 'Alaska Department of Fish and Game' }, + AK010: { label: 'Alaska DNR, Division of Land and Water Management' }, + AK011: { + label: 'Alaska DNR, Division of Geological and Geophysical Surveys', + }, + AL001: { label: 'Alabama Geological Survey' }, + AL002: { label: 'Alabama Water Improvement Commission' }, + AL003: { + label: 'Auburn University Water Resources Research Institute, AL', + }, + AL006: { label: 'Alabama State Highway Department' }, + AL012: { label: 'Alabama Office of Water Resources' }, + AL013: { label: 'Auburn University, FL' }, + AR001: { + label: 'Arkansas Dept of Health, Bureau of Environmental Engineering', + }, + AR004: { label: 'Arkansas Geological Survey' }, + AR008: { label: 'Arkansas Natural Resources Commission' }, + AR019: { label: 'International Paper-Pine Bluff, AR' }, + AR025: { label: 'Union County Conservation District, AR' }, + ASCE: { label: 'American Society of Civil Engineers' }, + AWRA: { label: 'American Water Resources Association' }, + AYRES: { label: 'Ayres Associates' }, + AZ001: { label: 'University of Arizona, Water Resources Research Center' }, + AZ002: { label: 'Roosevelt Irrigation District, AZ' }, + AZ003: { label: 'Arizona Game and Fish Department' }, + AZ004: { + label: 'Maricopa County Municipal Water Conservation District #1, AZ', + }, + AZ005: { label: 'Gila Water Commissioner, AZ' }, + AZ006: { label: 'Salt River Valley Water Users Association, AZ' }, + AZ007: { label: 'Arizona Department of Health' }, + AZ008: { label: 'Central Arizona Project' }, + AZ009: { label: 'Arizona Department of Environmental Quality' }, + AZ010: { label: 'Arizona Corporation Commission' }, + AZ011: { label: 'Salt River Project, AZ' }, + AZ012: { label: 'Wellton-Mohawk Irrigation & Drainage District, AZ' }, + AZ013: { label: 'City of Tucson Water and Sewer Department, AZ' }, + AZ014: { label: 'Arizona Department of Water Resources' }, + AZ015: { label: 'Motorola Aerial Remote Sensing, Inc., AZ' }, + AZ017: { label: 'Arizona Department of Transportation' }, + AZ021: { label: 'Arizona State University' }, + AZ044: { label: 'US Water Conservation Laboratory, AZ' }, + AZ047: { label: 'Southwest Rangeland Watershed, AZ' }, + AZ052: { label: 'Arizona State Land Department' }, + AZ056: { label: 'Maricopa County Flood Control District, AZ' }, + AZ063: { label: 'Northern Arizona University' }, + AZ070: { label: 'Pima County Flood Control District, AZ' }, + AZ080: { label: 'University of Arizona' }, + AZ100: { label: 'Arizona Geological Survey' }, + AZ112: { label: 'Colorado River Indian Tribes, AZ' }, + AZ115: { label: 'Havasupai Tribe, AZ' }, + AZ116: { label: 'City of Flagstaff, AZ' }, + AZ117: { label: 'City of Williams, AZ' }, + AZ118: { label: 'Valle Water Users Association, AZ' }, + AZ119: { label: 'Tusayan Water Users Association, AZ' }, + AZ120: { label: 'Yuma County Water Users Association, AZ' }, + AZ121: { label: 'Fort Mojave Indian Tribes, AZ' }, + AZ122: { label: 'Phelps Dodge, AZ' }, + AZ123: { label: 'Freeport-McMoRan Inc., AZ' }, + AZ124: { label: 'US Indian Health Service, AZ' }, + AZ125: { label: 'Arizona Testing Laboratories' }, + AZ126: { label: 'Mohave Valley Irrigation and Drainage District, AZ' }, + CA001: { label: 'California Department of Water Resources' }, + CA002: { label: 'San-Lo Aerial Surveys Inc, CA' }, + CA003: { label: 'Palmdale Water District, CA' }, + CA004: { label: 'City of Oceanside, CA' }, + CA005: { label: 'Los Angeles County Flood Control District, CA' }, + CA006: { label: 'Alameda County Water District, CA' }, + CA007: { label: 'City of Buellton, CA' }, + CA008: { label: 'Whitewater Mutual Water Company, CA' }, + CA009: { label: 'California State Water Resources Control Board' }, + CA011: { label: 'Phelan Pinyon Hills Community Services District, CA' }, + CA012: { label: 'City of Morgan Hill, CA' }, + CA013: { label: 'City of Arcata, CA' }, + CA014: { label: 'Smith River CSD, CA' }, + CA015: { label: 'Coachella Valley County Water District, CA' }, + CA016: { label: 'Ventura County Flood Control District, CA' }, + CA017: { label: 'Shaver Lake Heights Property Assoc., CA' }, + CA018: { label: 'Imperial County Department of Public Works, CA' }, + CA020: { label: 'Turlock Irrigation District, CA' }, + CA025: { label: 'East Bay Municipal Utility District, CA' }, + CA026: { label: 'Modesto Irrigation District, CA' }, + CA036: { label: 'Buena Vista Water Storage District, CA' }, + CA039: { + label: 'Monterey County Flood Control & Water Conservation District, CA', + }, + CA040: { + label: 'San Luis Obispo Cnty Flood Control & Water Conservation Dist, CA', + }, + CA042: { + label: 'Santa Barbara County Flood Control & Water Conservation Dist, CA', + }, + CA043: { label: 'Metropolitan Water District of Southern California' }, + CA045: { label: 'North Marin County Water District, CA' }, + CA047: { + label: 'Alameda County Flood Control & Water Conservation District, CA', + }, + CA048: { label: 'Santa Clara Valley Water District, CA' }, + CA049: { label: 'Tule Irrigation District, CA' }, + CA051: { label: 'Los Angeles City Department of Water and Power, CA' }, + CA052: { label: 'Desert Water Agency, CA' }, + CA057: { + label: 'Riverside County Flood Control & Water Conservation District, CA', + }, + CA065: { label: 'Pacific Gas and Electric Company, CA' }, + CA066: { label: 'Southern California Edison Company, CA' }, + CA087: { + label: 'Napa County Flood Control & Water Conservation District, CA', + }, + CA088: { label: 'San Diego County Health Department, CA' }, + CA092: { label: 'San Luis Obispo County Health Agency, CA' }, + CA093: { label: 'Monterey County Health Department, CA' }, + CA098: { label: 'Humboldt-Del Norte County Public Health Department, CA' }, + CA105: { + label: 'University of California, Berkeley-Lawrence Livermore Lab', + }, + CA111: { label: 'California Department of Health' }, + CA114: { + label: 'Central Coast Region, CA Regional Water Quality Control Board', + }, + CA116: { + label: 'Central Valley Region, CA Regional Water Quality Control Board', + }, + CA154: { label: 'Wheeler Ridge-Maricopa Water Storage District, CA' }, + CA155: { label: 'North Kern Water Storage District, CA' }, + CA160: { label: 'Fresno County Department of Health, CA' }, + CA161: { label: 'Fresno City Department of Public Works, CA' }, + CA163: { label: 'San Joaquin County Department of Public Works, CA' }, + CA166: { label: 'Kern County Water Agency, CA' }, + CA174: { label: 'Foster Farms (Delhi), CA' }, + CA175: { + label: 'Stanislaus County Department of Environmental Resources, CA', + }, + CA176: { label: 'Pacific Fibreboard, CA' }, + CA177: { label: 'Selma-Kingsburg-Fowler Sanitation District, CA' }, + CA178: { label: 'F.M.C. Corporation, CA' }, + CA179: { label: 'Arvin-Edison Water Storage District, CA' }, + CA186: { label: 'Soquel Creek County Water District, CA' }, + CA188: { label: 'Sacramento Municipal Utility District, CA' }, + CA208: { label: 'City of San Diego Water Utilities Department, CA' }, + CA217: { label: 'Santa Margarita Water District, CA' }, + CA220: { label: 'Yucaipa Valley County Water District, CA' }, + CA226: { label: 'San Francisco Water District, CA' }, + CA235: { label: 'California-American Water Company, CA' }, + CA239: { label: 'San Lorenzo Valley County Water District, CA' }, + CA241: { label: 'California Water Service Company' }, + CA244: { label: 'Coastside County Water Department, CA' }, + CA251: { label: 'City of Dos Palos, CA' }, + CA335: { label: 'Sonoma County Water Agency, CA' }, + CA338: { label: 'Envirosphere Company, CA' }, + CA351: { label: 'Benchmark Photography, CA' }, + CA381: { label: 'Mojave Water Agency, CA' }, + CA397: { label: 'Santa Maria Valley Water Conservation District, CA' }, + CA400: { label: 'Sacramento Suburban Water District, Sacramento, CA' }, + CA551: { label: 'City of Santa Barbara, CA' }, + CA552: { label: 'City of Lompoc, CA' }, + CA553: { label: 'Water Replenishment District of Southern California' }, + CA555: { label: 'San Gorgonio Pass Water Agency, CA' }, + CA557: { label: 'Los Angeles County Sanitation Districts, CA' }, + CA558: { label: 'Palo Verde Irrigation District, CA' }, + CA563: { label: 'Riverside County Waste Management Department, CA' }, + CA564: { label: 'Victor Valley Water District, Victorville, CA' }, + CA565: { label: 'Hesperia Water District, CA' }, + CA566: { label: 'Southern California Water, San Bernardino' }, + CA567: { label: 'Twentynine Palms Water District, CA' }, + CA568: { label: 'Bighorn-Desert View Water Agency, Yucca Valley, CA' }, + CA569: { label: 'High Desert Water District, Yucca Valley, CA' }, + CA570: { label: 'Sheep Creek Water Company, Phelan, CA' }, + CA571: { label: 'City of Adelanto, CA' }, + CA572: { label: 'Apple Valley Ranchos Water District, Apple Valley, CA' }, + CA573: { label: 'Joshua Basin Water District, Joshua Tree, CA' }, + CA574: { label: 'Imperial Irrigation District, CA' }, + CA575: { label: 'Glamis Imperial Inc., CA' }, + CA576: { label: 'Borrego Water District, Borrego Springs, CA' }, + CA578: { label: 'San Luis and Delta-Mendota Water Authority, Byron, CA' }, + CA579: { label: 'Western Heights Water Company, Yucaipa, CA' }, + CA582: { label: 'Luhdorff and Scalmanini Consulting Engineers, CA' }, + CA583: { label: 'Karuk Tribe, CA' }, + CA584: { label: 'Yurok Tribe of the Yurok Reservation, CA' }, + CA585: { label: 'Imperial County Planning and Development Services' }, + CA586: { label: 'Riverside County Department of Environmental Health' }, + CA587: { label: 'San Bernardino County Environmental Health Services' }, + CAX01: { + label: 'Inland Waters Directorate, Water Resources Bureau, Canada', + }, + CAX11: { label: 'Environment Canada, Inland Waters Directorate' }, + CGWU: { label: 'Carroll Groundwater Users, KY' }, + CHMH: { label: 'CH2M Hill' }, + CO001: { label: 'Denver Water Department, CO' }, + CO002: { + label: 'Colorado Division of Water Resources, Office of State Engineer', + }, + CO003: { label: 'City of Colorado Springs Water Division, CO' }, + CO004: { label: 'Boulder City-County Health Department, CO' }, + CO005: { label: 'Pueblo Board of Water Works, CO' }, + CO006: { label: 'Colorado Department of Natural Resources' }, + CO008: { label: 'Metropolitan Denver Sewage Disposal District #1, CO' }, + CO012: { label: 'Pikes Peak Area Council of Governments, CO' }, + CO020: { label: 'Colorado Department of Highways' }, + CO025: { label: 'Rio Grande Water Conservancy District, CO' }, + CO026: { label: 'Cherokee Water District, CO' }, + CO034: { label: 'Colorado Geological Survey' }, + CO040: { label: 'EPA Region 8, CO' }, + CO046: { label: 'Colorado State University' }, + CO052: { label: 'Arkansas River Compact Administration, CO' }, + CO054: { label: 'City of Aurora, CO' }, + CO153: { label: 'Upper Clear Creek Advisory Group, CO' }, + CT001: { + label: 'Connecticut Dept of Energy and Environmental Protection-CT DEEP', + }, + CT005: { label: 'University of Connecticut' }, + DC001: { + label: 'District of Columbia Department of Environmental Services', + }, + DE001: { label: 'Delaware Geological Survey' }, + DE002: { + label: 'Delaware Department of Natural Resources & Environmental Control', + }, + DE005: { label: 'Delaware Department of Transportation' }, + DMI: { label: 'Dames & Moore, Inc' }, + DRBC: { label: 'Delaware River Basin Commission' }, + EERCI: { label: 'Energy Environmental Resource Consultants, Inc' }, + ESPD: { label: 'Earth System Processes Division' }, + FL001: { + label: 'Bureau of Geology, Florida Department of Natural Resources', + }, + FL002: { label: 'Florida Department of Transportation' }, + FL005: { label: 'South Florida Water Management District' }, + FL007: { label: 'Southwest Florida Water Management District' }, + FL016: { label: 'Dade County, FL' }, + FL022: { label: 'Lee County, FL' }, + FL030: { label: 'Volusia County, FL' }, + FL039: { label: 'City of Jacksonville Water Conservation, FL' }, + FL043: { label: 'City of Pensacola, FL' }, + FL051: { label: 'Florida Department of Environmental Regulation' }, + FL069: { label: 'Sarasota County Public Works, FL' }, + FL083: { label: 'Northwest Florida Water Management District' }, + FL084: { label: 'Suwannee River Water Management District, FL' }, + FL085: { label: 'St. Johns River Water Management District, FL' }, + FL117: { + label: 'Univ of Miami-Rosenstiel School of Marine & Atmospheric Sci., FL', + }, + FL121: { label: 'University of Miami, FL' }, + FL125: { label: 'Florida Department of Health & Rehabilitative Services' }, + FL128: { label: 'University of Florida' }, + FL129: { label: 'Florida Keys Aqueduct Authority' }, + FL130: { label: 'Florida Geological Survey' }, + FL220: { label: 'Tampa Bay Water, FL' }, + FL221: { + label: 'Alachua County Department of Environmental Protection, FL', + }, + FL222: { label: 'Pinellas County, FL' }, + GA009: { + label: 'Environmental Protection Div, Georgia Dept of Natural Resources', + }, + GA025: { label: 'City of Brunswick, GA' }, + GA027: { label: 'Albany Water, Gas, and Light Commission, GA' }, + GA028: { label: 'Covia Holdings Corporation, GA' }, + GA029: { label: 'Augusta Sulfate Company LLC, GA' }, + GQ012: { label: 'Guam Environmental Protection Agency' }, + HI001: { label: 'Honolulu Board of Water Supply, HI' }, + HI003: { label: 'County of Maui Department of Water Supply, HI' }, + HI007: { + label: 'HI Dept of Land & Natural Resources, Div of Water & Land Dev', + }, + HI021: { + label: 'HI Dept of Land & Natural Resources, Div of Aquatic Resources', + }, + HI022: { label: 'Norman Saito Engineering Consultant Inc, HI' }, + HI023: { label: 'Wailuku Sugar Company, HI' }, + HI024: { label: 'Pioneer Mill Company, HI' }, + HI025: { + label: 'HI Dept of Land & Natural Resources, Comm on Water Resource Mgt', + }, + HI026: { label: 'Hawaiian Commercial & Sugar Company, HI' }, + HI027: { label: 'Department of Water Supply, County of Maui, HI' }, + HI028: { label: 'Hawaii State Department of Health' }, + IA001: { label: 'State Hygienic Laboratory, University of Iowa' }, + IA004: { label: 'Iowa State University, Ames, IA' }, + IA005: { label: 'Department of Civil Engineering, University of Iowa' }, + IA018: { label: 'Iowa Geological Survey, Iowa City' }, + IA021: { label: 'Iowa Department of Water, Air, and Waste Management' }, + IA022: { label: 'City of Anamosa, IA' }, + IA023: { label: 'City of Fayette, IA' }, + IA024: { label: 'City of New Hampton, IA' }, + IA025: { label: 'City of New Sharon, IA' }, + IA026: { label: 'City of Ogden, IA' }, + IA027: { label: 'City of Villisca, IA' }, + IA028: { label: 'City of Vinton, IA' }, + IA029: { label: 'City of Waukon, IA' }, + IA030: { label: 'City of West Liberty, IA' }, + IA031: { label: 'City of Worthington, IA' }, + IA032: { label: 'University of Iowa, Iowa City, IA' }, + ID001: { label: 'Idaho Department of Water Resources' }, + ID002: { label: 'Idaho Fish and Game Department' }, + ID003: { label: 'Water Resources Research Institute, University of Idaho' }, + ID004: { label: 'Idaho Department of Health and Welfare' }, + ID011: { label: 'Idaho Bureau of Mines and Geology' }, + IL001: { label: 'Illinois Department of Public Health' }, + IL002: { + label: 'Metropolitan Water Reclamation District of Greater Chicago, IL', + }, + IL003: { label: 'Illinois State Water Survey' }, + IL004: { label: 'Illinois Department of Transportation' }, + IL006: { label: 'State of Illinois Environmental Protection Agency' }, + IL010: { label: 'Illinois Natural History Survey' }, + IL019: { label: 'Illinois Department of Agriculture' }, + IL028: { label: 'Illinois State Geological Survey' }, + IL032: { label: 'Illinois Water Resources Division' }, + IL038: { label: 'Northern Illinois University' }, + IL040: { label: 'EPA Region 5, IL' }, + IL044: { label: 'Southern Illinois University at Carbondale' }, + IL045: { label: 'University of Illinois' }, + IL048: { + label: 'US Forest Service, Midewin National Tallgrass Prairie, IL', + }, + IL049: { label: 'McHenry County, Illinois Water Resources Department' }, + IL050: { label: 'AECOM, Inc, IL' }, + IL051: { label: 'City of Bushnell, IL' }, + IL052: { label: 'City of Farmington, IL' }, + IL053: { label: 'City of Galena, IL' }, + IL054: { label: 'City of Kewanee, IL' }, + IL055: { label: 'City of Milan, IL' }, + IL056: { label: 'City of Monmouth, IL' }, + IL057: { label: 'City of Toluca, IL' }, + IL058: { label: 'City of Western Springs, IL' }, + IN002: { label: 'Indiana Department of Natural Resources' }, + IN014: { + label: 'Indiana University-Northwest Lab for Environmental Research', + }, + IN015: { label: 'Indiana Geological Survey' }, + IN018: { label: 'Conservation Tillage Information Center, IN' }, + IN021: { label: 'Ball State University, IN' }, + IN032: { label: 'Indiana Department of Transportation' }, + IN033: { + label: 'IN Dept Env Mngmt, Drinking Water Bureau, Groundwater Section', + }, + IN038: { + label: 'Indiana Univ-School of Public and Environmental Affairs (SPEA)', + }, + IN039: { label: 'LaPorte County Health Department, IN' }, + IN040: { label: 'ATC Associates Inc, IN' }, + IN041: { label: 'Peerless-Midwest Inc, IN' }, + IN042: { label: 'USX Corporation, IN' }, + KS001: { label: 'Kansas Department of Health and Environment' }, + KS003: { + label: 'Division of Water Resources, Kansas State Board of Agriculture', + }, + KS009: { label: 'Kansas Water Office' }, + KS014: { label: 'Kansas Geological Survey' }, + KS015: { label: 'City of Wichita, KS' }, + KS016: { label: 'Western Kansas Groundwater Management District No. 1' }, + KS017: { label: 'Equus Beds Groundwater Management District No. 2, KS' }, + KS018: { label: 'Southwest Kansas Groundwater Management District No. 3' }, + KS019: { label: 'Northwest Kansas Groundwater Management District No. 4' }, + KS020: { label: 'Big Bend Groundwater Management District No. 5, KS' }, + KY001: { + label: 'KY Department for Natural Resources & Environmental Protection', + }, + KY002: { label: 'Kentucky Geological Survey, University of Kentucky' }, + KY003: { + label: 'Div of Sanitation Engineering, Kentucky Dept of Human Resources', + }, + KY004: { label: 'Louisville Water Company, KY' }, + LA002: { label: 'Louisiana State Department of Health and Hospitals' }, + LA014: { label: 'Louisiana Department of Transportation & Development' }, + LA015: { label: 'Parish of East Baton Rouge-Engineering Division, LA' }, + LA017: { label: 'Louisiana Coastal Commission' }, + LA018: { label: 'Louisiana Department of Natural Resources' }, + LASD: { label: 'Laboratory and Analytical Services Division' }, + LOX01: { + label: 'Imperial College of Science and Technology, London, England', + }, + MA003: { label: 'Massachusetts Division of Water Pollution Control' }, + MA007: { + label: 'Barnstable County Department of Health and Environment, MA', + }, + MA031: { label: 'Cape Cod Commission, MA' }, + MA032: { label: "Martha's Vineyard Commission, MA" }, + MA033: { label: 'Nantucket Land Council, MA' }, + MA034: { label: 'University of Massachusetts Department of Geosciences' }, + MA035: { label: 'Massachusetts Department of Environmental Protection' }, + MA036: { label: 'Massachusetts DCR, Division of Water Supply Protection' }, + MA037: { label: 'Massachusetts DCR, Office of Water Resources' }, + MD004: { + label: 'Montgomery County Department of Environmental Protection, MD', + }, + MD006: { label: 'Maryland Geological Survey' }, + MD007: { label: 'Maryland Department of the Environment (MDE)' }, + MD008: { label: 'Baltimore County Office of Planning and Zoning, MD' }, + MD030: { + label: 'Maryland University-Center for Environmental & Estuarine Studies', + }, + MD032: { label: 'Maryland Department of Health' }, + MD058: { + label: 'Maryland Department of the Environment, Water Use Conveyance', + }, + MD059: { label: 'Audubon Mid-Atlantic' }, + MD060: { label: 'Maryland Water Resources Administration' }, + ME001: { label: 'Maine Department of Environmental Protection' }, + ME002: { label: 'Maine Geological Survey' }, + ME005: { label: 'Maine Department of Inland Fisheries and Wildlife' }, + ME008: { label: 'Maine Department of Transportation' }, + MI001: { label: 'Michigan Department of Natural Resources' }, + MI002: { label: 'Michigan State University' }, + MI011: { + label: 'Michigan Department of State Highways and Transportation', + }, + MI015: { label: 'Stereo Foto, Inc, MI' }, + MI021: { label: 'Michigan Geological Survey' }, + MI032: { label: 'Tri-County Regional Planning Commission, MI' }, + MI041: { label: 'Clinton River Watershed Council, MI' }, + MI045: { label: 'Huron River Watershed Council, MI' }, + MI056: { label: 'Huron County Health Department, MI' }, + MI057: { label: 'Monroe County Health Department, MI' }, + MI061: { label: 'Huron Conservation District, MI' }, + MI066: { label: 'City of Portage, MI' }, + MI086: { label: 'Kalamazoo County, MI' }, + MI087: { label: 'Department of Environmental Quality (MDEQ), MI' }, + MN003: { label: 'Minnesota Department of Natural Resources' }, + MN005: { label: 'Ramsey County Environmental Service, MN' }, + MN012: { label: 'Minnesota Pollution Control Agency' }, + MN019: { label: 'Minnesota Health Department' }, + MN021: { label: 'Metropolitan Council of the Twin Cities Area, MN' }, + MN022: { + label: 'Minnesota Department of Natural Resources Division of Waters', + }, + MN028: { label: 'Minnesota Soil and Water Conservation Board' }, + MN039: { label: 'Bemidji State University, MN' }, + MN040: { label: 'Minnesota Geological Survey' }, + MN045: { label: 'Fond du Lac Band of Lake Superior Chippewa, MN' }, + MN046: { label: 'Lower Sioux Indian Community, MN' }, + MN047: { label: 'Upper Sioux Community, MN' }, + MN048: { label: 'Dakota County Environmental Management, MN' }, + MN049: { label: 'Rochester Public Utility (RPU), MN' }, + MN050: { label: 'Prairie Island Indian Community, MN' }, + MO001: { label: 'Missouri Department of Health' }, + MO005: { + label: 'Missouri Geological Survey, Department of Natural Resources', + }, + MO011: { label: 'Burns and McDonnell, MO' }, + MO017: { label: 'EPA Region 7, MO' }, + MO021: { label: 'Missouri Public Drinking Water Program' }, + MO022: { label: 'Southeast Missouri Regional Water District' }, + MO023: { label: 'City of Coffey, MO' }, + MO024: { label: 'City of McKittrick, MO' }, + MS003: { label: 'Pearl River Valley Water Supply District, MS' }, + MS007: { + label: 'MS Dept. of Environmental Quality, Office of Pollution Control', + }, + MS008: { label: 'Mississippi Bureau of Land and Water Resources' }, + MS018: { + label: 'Yazoo Mississippi Delta Joint Water Management District, MS', + }, + MT001: { label: 'Montana Department of Fish, Wildlife & Parks' }, + MT002: { label: 'Fort Peck Tribes, MT' }, + MT003: { label: 'Montana Department of Environmental Quality' }, + MT004: { + label: 'Montana Department of Natural Resources and Conservation', + }, + MT005: { label: 'Montana Bureau of Mines and Geology' }, + MT006: { label: 'Northern Cheyenne Tribe, MT' }, + MT007: { label: 'Montana Department of Transportation' }, + MT008: { + label: 'Lewis and Clark County Water Quality Protection District, MT', + }, + MT013: { label: 'Land Management Bureau Montana District' }, + MT014: { label: 'Blackfeet Nation of Montana' }, + MT015: { label: 'USGS - Ecosystems Mission Area' }, + NADP: { label: 'National Atmospheric Deposition Program' }, + NASA: { label: 'National Aeronautics and Space Administration' }, + NC001: { + label: 'Bald Head Island Conservancy and Smith Island Land Trust, NC', + }, + NC004: { + label: 'NC Department of Natural Resources and Community Development', + }, + NC016: { label: 'North Carolina Water Resources Research Institute' }, + NC018: { label: 'North Carolina State University' }, + NC027: { label: 'North Carolina Department of Natural Resources' }, + NC028: { label: 'Elizabeth City State University, NC' }, + NC030: { label: 'City of Jacksonville, NC' }, + NC031: { label: 'USMC Camp Lejeune, NC' }, + NC032: { label: 'Onslow Water and Sewer Authority, NC' }, + NC033: { label: 'NC Department of Environmental Quality' }, + NC035: { label: 'Wake County Environmental Services, NC' }, + NC036: { label: 'Town of Pollocksville, NC' }, + NC037: { label: 'North Carolina Department of Public Safety' }, + ND001: { label: 'North Dakota Game and Fish Department' }, + ND002: { label: 'North Dakota State Department of Health' }, + ND003: { label: 'Minot City Water Treatment Plant, ND' }, + ND004: { label: 'City of Bismarck Water Department, ND' }, + ND005: { label: 'City of Dickinson Water Treatment Plant, ND' }, + ND007: { label: 'North Dakota Geological Survey' }, + ND009: { label: 'North Dakota State Water Commission' }, + ND010: { + label: 'Water Resources Research Institute-North Dakota State University', + }, + ND012: { + label: 'Energy Department Grand Forks Energy Technology Center, ND', + }, + NE001: { label: 'Nebraska Game and Parks Commission' }, + NE004: { label: 'Soil and Water Testing Lab, University of Nebraska' }, + NE006: { label: 'Nebraska Natural Resources Commission' }, + NE008: { + label: 'University of Nebraska, Conservation and Survey Division', + }, + NE009: { label: 'Nebraska Department of Roads' }, + NE010: { label: 'Nebraska Department of Water Resources' }, + NE013: { label: 'Nebraska Conservation and Survey Division' }, + NE021: { label: 'Central Platte Natural Resources District, NE' }, + NE022: { label: 'Lower Republican Natural Resources District, NE' }, + NE023: { label: 'Twin Platte Natural Resources District, NE' }, + NE024: { label: 'Upper Loup Natural Resources District, NE' }, + NE025: { label: 'Little Blue Natural Resources District, NE' }, + NE031: { label: 'Upper Big Blue Natural Resources District, NE' }, + NE032: { label: 'Lower Big Blue Natural Resources District, NE' }, + NE033: { label: 'Lower Platte North Natural Resources District, NE' }, + NE034: { label: 'Lower Platte South Natural Resources District, NE' }, + NE035: { label: 'Papio-Missouri River Natural Resources District, NE' }, + NE036: { label: 'Tri-Basin Natural Resources District, NE' }, + NE037: { label: 'Nemaha Natural Resources District, NE' }, + NE038: { label: 'North Platte Natural Resources District, NE' }, + NE039: { label: 'South Platte Natural Resources District, NE' }, + NE040: { label: 'Upper Niobrara-White Natural Resources District, NE' }, + NE041: { label: 'Middle Niobrara Natural Resources District, NE' }, + NE042: { label: 'Lower Niobrara Natural Resources District, NE' }, + NE043: { label: 'Upper Republican Natural Resources District, NE' }, + NE044: { label: 'Middle Republican Natural Resources District, NE' }, + NE045: { label: 'Lewis and Clark Natural Resources District, NE' }, + NE046: { label: 'Lower Loup Natural Resources District, NE' }, + NE047: { label: 'Upper Elkhorn Natural Resources District, NE' }, + NE048: { label: 'Lower Elkhorn Natural Resources District, NE' }, + NE049: { label: 'Nebraska Public Power District' }, + NE050: { label: 'Central Nebraska Public Power and Irrigation District' }, + NH001: { + label: 'Water Resources Research Center, University of New Hampshire', + }, + NH002: { + label: 'New Hampshire Water Supply and Pollution Control Commission', + }, + NH021: { label: 'New Hampshire Department of Environmental Services' }, + NIH: { label: 'National Intstitutes of Health' }, + NJ001: { label: 'Passaic Valley Water Commission, NJ' }, + NJ002: { + label: 'Division of Water Resources, NJ Dept of Environmental Protection', + }, + NM001: { label: 'New Mexico State Engineers Office' }, + NM002: { label: 'New Mexico Health and Environment Department' }, + NM003: { label: 'New Mexico Institute of Mining and Technology' }, + NM004: { label: 'New Mexico Bureau of Geology, NMBG' }, + NM005: { label: 'Los Alamos Scientific Laboratory, NM' }, + NM010: { label: 'New Mexico Interstate Stream Commission' }, + NM011: { label: 'New Mexico Legislative Council Service' }, + NM016: { label: 'Environmental Improvement Division, NM' }, + NM019: { label: 'Elephant Butte Irrigation District, NM' }, + NM022: { label: 'Pecos Valley Artesian Conservation District, NM' }, + NM030: { label: 'Natural Resource Conservation System, NM' }, + NM031: { label: 'City of Rio Rancho Department of Public Works, NM' }, + NM032: { label: 'Sandia National Lab, NM' }, + NM033: { label: 'Kirkland Air Force Base, NM' }, + NM034: { label: 'City of Las Cruces, NM' }, + NM036: { + label: 'Bernalillo County Public Works-Natural Resource Services, NM', + }, + NM037: { label: 'Taos Soil and Water Conservation District, NM' }, + NM038: { label: 'Southern Ute Indian Tribe from Colorado' }, + NM039: { label: 'Green Analytical- Durango, CO' }, + NM040: { + label: 'Albuquerque Bernalillo County Water Utility Authority, NM', + }, + NNDWR: { label: 'Navajo Nation Department of Water Resources, AZ' }, + NRCS: { label: 'Natural Resources Conservation Service' }, + NV001: { label: 'Nevada Division of Environmental Protection' }, + NV002: { label: 'Walker River Irrigation District, NV' }, + NV003: { + label: 'Div-Water Resources, Nevada Dept-Conservation&Natural Resources', + }, + NV004: { label: 'Roundhill General Improvement District, NV' }, + NV005: { label: 'Mission Support and Test Services, LLC (MSTS), NV' }, + NV006: { label: 'Southern California Edison Company, NV' }, + NV007: { label: 'Newmont Mining Corporation, NV' }, + NV008: { label: 'Nevada State Highway Department' }, + NV012: { label: 'Nevada Consumer Health Protection Service' }, + NV013: { label: 'Desert Research Institute, University of Nevada' }, + NV017: { label: 'Las Vegas Valley Water District, NV' }, + NV018: { label: 'Sierra Pacific Power Company, NV' }, + NV024: { label: 'Bechtel, NV' }, + NV026: { label: 'Carson City Department of Public Works, NV' }, + NV035: { label: 'Energy Department Nevada Operations Office' }, + NV038: { label: 'Nevada Water Resources Division' }, + NV042: { label: 'Fenix & Scisson, NV' }, + NV045: { label: 'Churchill County, NV' }, + NV052: { label: 'Pershing County Water Conservation District, NV' }, + NV053: { + label: 'Harry Reid Center (HRC) for Environmental Studies, UNLV, NV', + }, + NV065: { label: 'International Technology (IT) Corporation, NV' }, + NV066: { label: 'Lawrence Livermore National Laboratory (LLNL), NV' }, + NV067: { label: 'Los Alamos National Laboratory (LANL), NV' }, + NV069: { + label: 'Nye Nclr Wst Repos Prj Off/Erly Wrning Drl Prgm (NWRPO/EWDP), NV', + }, + NV071: { label: 'Reynolds Electrical and Engineering Company (REECO), NV' }, + NV072: { label: 'Shaw E&I, NV' }, + NV073: { label: 'Southern Nevada Water Authority (SNWA)' }, + NV074: { label: 'Stoller-Navarro, NV' }, + NV079: { label: 'Clark County, NV' }, + NV081: { label: 'Hawthorne Utilities-Water, Sewer, and Disposal, NV' }, + NV082: { label: 'Washoe County Department of Water Resources, NV' }, + NV083: { label: 'White Pine County Water Advisory Committee, NV' }, + NV084: { label: 'McGill-Ruth Consolidated Sewer & Water District, NV' }, + NV085: { label: 'Bald Mountain Mine - Placer Dome America, NV' }, + NV086: { label: 'Quadra Mining Ltd. - Robinson Operation, NV' }, + NV087: { label: 'Barrick Gold Corporation, NV' }, + NV088: { label: 'Eureka County, NV' }, + NV089: { label: 'Lander County, NV' }, + NV090: { label: 'Nye County, NV' }, + NV091: { label: 'US Ecology Nevada, Inc., NV' }, + NV092: { label: 'Holmes & Narver, Inc., NV' }, + NV093: { label: 'National Security Technologies LLC (NSTEC), NV' }, + NV094: { label: 'Raytheon Services Nevada' }, + NV095: { label: 'Second Nature Inc., NV' }, + NV096: { label: 'Lyon County, NV' }, + NV097: { label: 'Bently Agrowdynamics, NV' }, + NV098: { label: 'Navarro Nevada Environmental Services, LLC, NV' }, + NV099: { label: 'Central Nevada Regional Water Authority (CNRWA)' }, + NV100: { label: 'Mahannah and Associates, LLC, NV' }, + NV101: { label: 'Beatty Water & Sanitation District, NV' }, + NV102: { label: 'Truckee Meadows Water Authority (TMWA), NV' }, + NV103: { label: 'Navarro-Intera, LLC, NV' }, + NV104: { label: 'Navarro, NV' }, + NV105: { label: 'Nevada Department of Agriculture' }, + NY001: { label: 'New York State Department of Environmental Conservation' }, + NY002: { + label: 'Division-Sanitation & Water Sup, Nassau County Public Supply, NY', + }, + NY011: { label: 'Rennselaer Polytechnic Institute, NY' }, + NY022: { label: 'New York State Department of Health' }, + NY043: { label: 'New York Geological Survey' }, + OH002: { label: 'Miami Conservancy District, OH' }, + OH004: { label: 'Ohio Environmental Protection Agency' }, + OH013: { label: 'Heidelberg College, OH' }, + OH015: { label: 'Ohio Department of Natural Resources' }, + OH044: { label: 'Columbus Department of Public Service, OH' }, + OH060: { + label: 'Ohio State University, Department of Agricultural Engineering', + }, + OK001: { label: 'Oklahoma State Department of Health' }, + OK002: { label: 'Oklahoma Water Resources Board' }, + OK003: { label: 'Oklahoma Department of Environmental Quality' }, + OK005: { label: 'Oklahoma State University, Department of Geology' }, + OK011: { label: 'Oklahoma Geological Survey' }, + OK022: { label: 'City of Ada, OK' }, + OMORA: { label: 'Omora Foundation of Tierra Del Fuego, Chile' }, + OR001: { + label: 'Department of Forest Engineering, Oregon State University', + }, + OR002: { label: 'Johnson Creek Watershed Council, OR' }, + OR003: { label: 'Douglas County Water Resources Survey, OR' }, + OR004: { label: 'Oregon Water Resources Department' }, + OR005: { label: 'WEST Consultants, Inc., OR' }, + OR006: { label: 'Portland General Electric Company, OR' }, + OR028: { label: 'City of Medford Engineering Department, OR' }, + OR044: { label: 'EPA Oregon Operations Office' }, + OR046: { label: 'Oregon Geology and Mineral Industries Department' }, + OR049: { label: 'Oregon Water Resources Research Institute' }, + OR058: { label: 'City of Portland, OR' }, + OR059: { label: 'Douglas County, OR' }, + OR064: { + label: 'Confederated Tribes of the Umatilla Indian Reservation, OR', + }, + OR065: { label: 'Clean Water Services, Hillsboro, OR' }, + PA001: { label: 'Pennsylvania Department of Environmental Protection' }, + PA002: { label: 'Montgomery County, PA' }, + PA028: { label: 'Pennsylvania Topographic and Geologic Survey Bureau' }, + PA034: { label: 'Pennsylvania Geological Survey' }, + PA039: { label: 'Susquenhanna River Basin Commission, PA' }, + PA060: { label: 'Adams County Conservation District, PA' }, + PA061: { label: 'Pike County Conservation District, PA' }, + PA062: { + label: 'Pennsylvania Department of Conservation and Natural Resources', + }, + QUOX0: { + label: 'McGill University-Subarctic Research Station, Quebec, Canada', + }, + RI002: { label: 'Rhode Island Department of Environmental Management' }, + RI007: { label: 'Rhode Island Water Resources Board' }, + RI013: { label: 'Rhode Island Department of Health' }, + RQ001: { label: 'Puerto Rico Department of Health, PRDOH' }, + RQ002: { label: 'Puerto Rico Aqueduct and Sewer Authority, PRASA' }, + RQ008: { label: 'Puerto Rico Department of Natural Resources' }, + RQ020: { label: 'Puerto Rico Emergency Management Agency' }, + RQ021: { label: 'Servicio Nacional De Estudios Territoriales (SNET), ES' }, + RQ022: { + label: 'Ins. Nacional Sismologia Vulcanologia Meteorologia Hidrologia,GT', + }, + RQ023: { + label: "Ministere De L'Agriculture Des Ressources Naturell (MARNDR), HA", + }, + RQ024: { label: 'Empresa Nacional De Energia Electrica (ENEE), HO' }, + RQ025: { + label: 'Secretaria De Recursos Naturales Y Ambientales (SERNA), HO', + }, + RQ026: { + label: 'Comision De Control De Inundaciones Del Valle De Sula, PR', + }, + RQ027: { + label: 'Instituto Nicaraguense De Estudios Territoriales (INETER), NU', + }, + RQ028: { + label: 'St Lucia Ministry of Agriculture & Forestry Botanical Garden', + }, + RQ029: { + label: 'St Vicent Ministry of Agriculture & Forestry Botanical Garden', + }, + RQ030: { + label: 'Dominica Ministry of Agriculture & Forestry Botanical Garden', + }, + SC001: { + label: 'Agricultural Engineering Department, Clemson University, SC', + }, + SC004: { + label: 'South Carolina Department of Health and Environmental Control', + }, + SC008: { label: 'South Carolina Water Resources Commission' }, + SC009: { label: 'Kershaw County Tax Assessor, SC' }, + SD001: { + label: 'Water Resource Research Institute, South Dakota State University', + }, + SD002: { label: 'East Dakota Conservancy Sub-District, SD' }, + SD003: { label: 'South Dakota Geological Survey' }, + SD004: { label: 'SD-RC, City of Rapid City, SD' }, + SD005: { label: 'South Dakota Department of Water and Natural Resources' }, + SD008: { label: 'South Dakota Department of Environmental Protection' }, + SD009: { label: 'South Dakota School of Mines and Technology' }, + SD010: { label: 'Soil Testing Lab, South Dakota State University' }, + SD011: { label: 'South Dakota State Chemist, University of South Dakota' }, + SD012: { label: 'Station Biochemistry, South Dakota State University' }, + SD019: { + label: 'South Dakota State University-SD Agricultural Experiment Station', + }, + SHELL: { label: 'Shell Oil Company' }, + TEEI: { label: 'Ten Ech Environmental Engineers, Inc' }, + TN011: { label: 'Oak Ridge National Laboratory, TN' }, + TN017: { label: 'Tennessee Department of Environment and Conservation' }, + TN018: { label: 'University of Memphis CEASER' }, + TX001: { label: 'Texas Water Development Board' }, + TX003: { label: 'Texas Commission on Environmental Quality' }, + TX006: { label: 'Texas Department of Health' }, + TX009: { label: 'Railroad Commission of Texas' }, + TX011: { label: 'Texas Parks and Wildlife Department' }, + TX038: { label: 'San Antonio Water System, TX' }, + TX057: { label: 'Houston-Galveston Area Council, TX' }, + TX071: { label: 'Lower Colorado River Authority, TX' }, + TX073: { label: 'North Central Texas Municipal Water Authority' }, + TX077: { label: 'San Antonio River Authority, TX' }, + TX078: { label: 'San Jacinto River Authority, TX' }, + TX087: { label: 'Bistone Municipal Water Supply District, TX' }, + TX101: { label: 'Edwards Aquifer Conservation District, TX' }, + TX120: { label: 'Edwards Underground Water District, TX' }, + TX123: { label: 'Texarkana Water Utilities, TX' }, + TX144: { + label: 'Bexar-Medina-Atascosa Water Control and Irr District No 1, TX', + }, + TX145: { label: 'Texas Department of Licensing and Regulation' }, + USA: { label: 'U.S. Army' }, + USAF: { label: 'U.S. Air Force' }, + USAHS: { label: 'U.S. Army Health Services Command' }, + USAID: { label: 'U.S. Agency for International Development' }, + USAPA: { label: 'Alaska Power Administration' }, + USARS: { label: 'U.S. Agricultural Research Service' }, + USBIA: { label: 'U.S. Bureau of Indian Affairs' }, + USBLM: { label: 'U.S. Bureau of Land Management' }, + USBM: { label: 'U.S. Bureau of Mines' }, + USBPA: { label: 'Bonneville Power Administration' }, + USBR: { label: 'U.S. Bureau of Reclamation' }, + USCE: { label: 'U.S. Army Corps of Engineers' }, + USCEQ: { label: 'Council on Environmental Quality' }, + USDA: { label: 'U.S. Department of Agriculture' }, + USDOC: { label: 'U.S. Department of Commerce' }, + USDOE: { label: 'U.S. Department of Energy' }, + USDOT: { label: 'U.S. Department of Transportation' }, + USEMA: { label: 'U.S. Geological Survey - Ecosystems Mission Area' }, + USEPA: { label: 'U.S. Environmental Protection Agency' }, + USESS: { label: 'National Environmental Satellite Service' }, + USFEC: { label: 'Federal Energy Regulatory Commission' }, + USFEM: { label: 'Federal Emergency Management Agency' }, + USFHA: { label: 'Federal Highway Administration' }, + USFS: { label: 'U.S. Forest Service' }, + USFWS: { label: 'U.S. Fish and Wildlife Service' }, + USGS: { label: 'U.S. Geological Survey' }, + USHEW: { label: 'U.S. Department of Health, Education and Welfare' }, + USHHS: { label: 'U.S. Department of Health and Human Services' }, + USIBW: { label: 'International Boundary and Water Commission' }, + USMC: { label: 'U.S. Marine Corps' }, + USN: { label: 'U.S. Navy Department' }, + USNFE: { label: 'U.S. Naval Facilities Engineering Command' }, + USNOA: { label: 'National Oceanic and Atmospheric Administration' }, + USNOS: { label: 'National Ocean Service' }, + USNPS: { label: 'National Park Service' }, + USNWS: { label: 'National Weather Service' }, + USPCC: { label: 'Panama Canal Commission' }, + USSCS: { label: 'U.S. Soil Conservation Service' }, + USSEA: { label: 'U.S. Society for Education Through Art' }, + USSWP: { label: 'Southwestern Power Administration' }, + USTVA: { label: 'Tennessee Valley Authority' }, + USWRC: { label: 'Water Resources Council' }, + UT001: { label: 'Utah Department of Health' }, + UT002: { label: 'Metropolitan Water District of Salt Lake and Sandy, UT' }, + UT003: { + label: 'Utah Department of Natural Resources, Div. of Wildlife Resources', + }, + UT004: { label: 'Jordan Valley Water Conservancy District, UT' }, + UT006: { label: 'Ogden Bay Waterfowl Management Area, UT' }, + UT008: { label: 'Utah Geological and Mineralogical Survey' }, + UT010: { label: 'Weber Distribution System, UT' }, + UT013: { + label: 'Utah Department of Natural Resources, Division of Water Rights', + }, + UT029: { + label: 'Utah Department of Natural Resources, Division Water Resources', + }, + VA001: { label: 'Virginia State Water Control Board' }, + VA007: { label: 'Virginia Department of Highways and Transportation' }, + VA016: { label: 'Bureau of Water Supply Engineering, VA' }, + VA019: { label: 'Virginia Health Department' }, + VA038: { label: 'City of Roanoke, VA' }, + VA086: { label: 'Frederick County Sanitation Authority, VA' }, + VA087: { label: 'Virginia Department of Environmental Quality' }, + VOCNS: { label: 'NAWQA National VOC Quality Assurance Studies' }, + VT001: { label: 'State of Vermont, Agency oF Environmental Conservation' }, + VT002: { label: 'Vermont Department of Health' }, + VT004: { label: 'Vermont Department of Water Resources' }, + VT012: { label: 'Vermont Agency of Natural Resources' }, + WA001: { label: 'Washington Department of Ecology' }, + WA002: { label: 'Public Utility District No 1, Skagit County, WA' }, + WA003: { label: 'Washington Department of Health' }, + WA004: { label: 'Fisheries Research Institute, University of Washington' }, + WA005: { label: 'Environmental Engineering, Washington State University' }, + WA013: { label: 'Washington Water Power Company' }, + WA022: { label: 'Geohydrology Section, Washington State University' }, + WA034: { + label: 'Washington State University, Department of Civil Engineering', + }, + WA050: { label: 'Spokane County Engineers Office, WA' }, + WA051: { label: 'Walla Walla County Engineer, WA' }, + WA052: { label: 'Columbia River Gorge Commission, WA' }, + WA053: { label: 'City of Snohomish Director of Public Works, WA' }, + WA054: { label: 'City of Walla Walla, Engineering Division, WA' }, + WA055: { label: 'East Columbia Basin Irrigation District, WA' }, + WA056: { label: 'Energy Department Richland Operations Office, WA' }, + WA057: { label: 'EPA Region 10, WA' }, + WA058: { label: 'Forest Service Washington' }, + WA059: { label: 'National Park Service Pacific Northwest Region, WA' }, + WA060: { label: 'Pacific Northwest River Basins Commission, WA' }, + WA061: { label: 'Washington Code Reviser Office' }, + WA062: { label: 'Western Snow Conference, WA' }, + WA063: { label: 'R. W. Beck and Associates, WA' }, + WA064: { label: 'Libby Photographers, WA' }, + WA080: { + label: 'Washington State Univ-State of Washington Water Resources Center', + }, + WA097: { label: 'Yakima Tribal Council, WA' }, + WA127: { + label: 'Washington State Department of Social and Health Services', + }, + WA169: { label: 'City of Bainbridge Island, WA' }, + WA170: { label: 'Northwest Indian Fisheries Commission (NWIFC), WA' }, + WA171: { label: 'Spokane Community College, WA' }, + WA172: { label: 'Confederated Tribes of the Colville Reservation, WA' }, + WI001: { label: 'Wisconsin Department of Natural Resources' }, + WI002: { label: 'Waukesha Water Utility, WI' }, + WI003: { label: 'Portage County Planning and Zoning, WI' }, + WI004: { label: 'East Central Wisconsin Regional Planning Commission' }, + WI005: { label: 'Wisconsin State Laboratory of Hygiene' }, + WI006: { label: 'Wisconsin Public Service Commission' }, + WI007: { label: 'University of Wisconsin - Madison' }, + WI008: { label: 'Southeastern Wisconsin Regional Planning Commission' }, + WI009: { label: 'Wisconsin Geological and Natural History Survey' }, + WI010: { label: 'Dane County Regional Planning Commission, WI' }, + WI011: { label: 'Bay-Lake Regional Planning Commission, WI' }, + WI012: { label: 'Bad River Natural Resource Department, WI' }, + WI013: { label: 'Wisconsin Department of Administration' }, + WI014: { label: 'Wisconsin Department of Transportation' }, + WI028: { + label: 'Wisconsin Department of Agriculture, Trade & Consumer Protection', + }, + WI029: { label: 'Wisconsin Department of Health' }, + WI030: { label: 'Northwest Wisconsin Regional Planning Commission' }, + WI034: { label: 'University of Wisconsin - Stevens Point' }, + WI039: { + label: 'Soei Sigurd Olson Environmental Institute(Northland College), WI', + }, + WI040: { label: 'Grant County Land Conservation Department, WI' }, + WI041: { label: 'Green Lake Highway Department, WI' }, + WI042: { label: 'Markesan Water Utility, WI' }, + WI043: { label: 'Pepin County Land Conservation Department, WI' }, + WI044: { label: 'Waupaca County Highway Department, WI' }, + WI045: { label: 'Wausau Water Utility, WI' }, + WV001: { + label: 'Division of Water Resources, West Virginia Dept of Nat Resources', + }, + WV002: { + label: 'Sanitary Engineering Division-West Virginia Department of Health', + }, + WV003: { + label: + 'Division of Water and Waste Management, West Virginia Department of Environmental Protection', + }, + WY003: { label: "Wyoming State Engineer's Office" }, + WY004: { label: 'Wyoming Water Development Commission' }, + WY006: { label: 'Wyoming Department of Environmental Quality' }, + WY014: { label: 'Wyoming Department of Agriculture' }, + WY015: { label: 'City of Cheyenne Board of Public Utilities, WY' }, + WY016: { label: 'Teton Conservation District, WY' }, + }, + 'site-types': { + AG: { + label: 'Aggregate groundwater use', + description: + "An Aggregate Groundwater Withdrawal/Return site represents an aggregate of specific sites whe groundwater is withdrawn or returned which is defined by a geographic area or some other common characteristic. An aggregate groundwatergroundwater site type is used when it is not possible or practical to describe the specific sites as springs or as any type of well including 'multiple wells', or when water-use information is only available for the aggregate. Aggregate sites that span multiple counties should be coded with 000 as the county code, or an aggregate site can be created for each county.", + }, + AS: { + label: 'Aggregate surface-water-use', + description: + 'An Aggregate Surface-Water Diversion/Return site represents an aggregate of specific sites where surface water is diverted or returned which is defined by a geographic area or some other common characteristic. An aggregate surface-water site type is used when it is not possible or practical to describe the specific sites as diversions, outfalls, or land application sites, or when water-use information is only available for the aggregate. Aggregate sites that span multiple counties should be coded with 000 as the county code, or an aggregate site can be created for each county.', + }, + AT: { + label: 'Atmosphere', + description: + 'A site established primarily to measure meteorological properties or atmospheric deposition.', + }, + AW: { + label: 'Aggregate water-use establishment', + description: + 'An Aggregate Water-Use Establishment represents an aggregate class of water-using establishments or individuals that are associated with a specific geographic location and water-use category, such as all the industrial users located within a county or all self-supplied domestic users in a county. The aggregate class of water-using establishments is identified using the national water-use category code and optionally classified using the Standard Industrial Classification System Code (SIC code) or North American Classification System Code (NAICS code). An aggregate water-use establishment site type is used when specific information needed to create sites for the individual facilities or users is not available or when it is not desirable to store the site-specific information in the database. Data entry rules that apply to water-use establishments also apply to aggregate water-use establishments. Aggregate sites that span multiple counties should be coded with 000 as the county code, or an aggregate site can be created for each county.', + }, + ES: { + label: 'Estuary', + description: + 'A coastal inlet of the sea or ocean; esp. the mouth of a river, where tide water normally mixes with stream water (modified, Webster). Salinity in estuaries typically ranges from 1 to 25 Practical Salinity Units (psu), as compared oceanic values around 35-psu. See also: tidal stream and coastal.', + }, + 'FA-AWL': { + label: 'Animal waste lagoon', + description: + 'A facility for storage and/or biological treatment of wastes from livestock operations. Animal-waste lagoons are earthen structures ranging from pits to large ponds, and contain manure which has been diluted with building washwater, rainfall, and surface runoff. In treatment lagoons, the waste becomes partially liquefied and stabilized by bacterial action before the waste is disposed of on the land and the water is discharged or re-used.', + }, + 'FA-CI': { + label: 'Cistern', + description: + 'An artificial, non-pressurized reservoir filled by gravity flow and used for water storage. The reservoir may be located above, at, or below ground level. The water may be supplied from diversion of precipitation, surface, or groundwater sources.', + }, + 'FA-CS': { + label: 'Combined sewer', + description: + 'An underground conduit created to convey storm drainage and waste products into a wastewater-treatment plant, stream, reservoir, or disposal site.', + }, + 'FA-DV': { + label: 'Diversion', + description: + 'A site where water is withdrawn or diverted from a surface-water body (e.g. the point where the upstream end of a canal intersects a stream, or point where water is withdrawn from a reservoir). Includes sites where water is pumped for use elsewhere, and sites where the surface-water body is considered a groundwater source such as a mining excavation with no surface-water inflow.', + }, + 'FA-FON': { + label: 'Field, Pasture, Orchard, or Nursery', + description: + 'A water-using agricultural facility characterized by an area for crop cultivation (field), grazing (pasture), fruit or nut production (orchard), or plant propagation (nursery). Irrigation water may or may not be applied.', + }, + 'FA-GC': { + label: 'Golf course', + description: + 'A place-of-use, either public or private, where the game of golf is played. A golf course typically uses water for irrigation purposes. Should not be used if the site is a specific hydrologic feature or facility; but can be used especially for the water-use sites.', + }, + 'FA-HP': { + label: 'Hydroelectric plant', + description: + 'A facility that generates electric power by converting potential energy of water into kinetic energy. Typically, turbine generators are turned by falling water.', + }, + 'FA-LF': { + label: 'Landfill', + description: + 'A typically dry location on the surface of the land where primarily solid waste products are currently, or previously have been, aggregated and sometimes covered with a veneer of soil. See also: Wastewater disposal and waste-injection well.', + }, + 'FA-OF': { + label: 'Outfall', + description: + 'A site where water or wastewater is returned to a surface-water body, e.g. the point where wastewater is returned to a stream. Typically, the discharge end of an effluent pipe.', + }, + 'FA-PV': { + label: 'Pavement', + description: + 'A surface site where the land surface is covered by a relatively impermeable material, such as concrete or asphalt. Pavement sites are typically part of transportation infrastructure, such as roadways, parking lots, or runways.', + }, + 'FA-QC': { + label: 'Laboratory or sample-preparation area', + description: + 'A site where some types of quality-control samples are collected, and where equipment and supplies for environmental sampling are prepared. Equipment blank samples are commonly collected at this site type, as are samples of locally produced deionized water. This site type is typically used when the data are either not associated with a unique environmental data-collection site, or where blank water supplies are designated by Center offices with unique station IDs.', + }, + 'FA-SEW': { + label: 'Wastewater sewer', + description: + 'An underground conduit created to convey liquid and semisolid domestic, commercial, or industrial waste into a treatment plant, stream, reservoir, or disposal site. If the sewer also conveys storm water, then the "combined sewer" secondary site type should be used.', + }, + 'FA-SPS': { + label: 'Septic system', + description: + 'A site within or in close proximity to a subsurface sewage disposal system that generally consists of: (1) a septic tank where settling of solid material occurs, (2) a distribution system that transfers fluid from the tank to (3) a leaching system that disperses the effluent into the ground.', + }, + 'FA-STS': { + label: 'Storm sewer', + description: + 'An underground conduit created to convey storm drainage into a stream channel or reservoir. If the sewer also conveys liquid waste products, then the "combined sewer" secondary site type should be used.', + }, + 'FA-TEP': { + label: 'Thermoelectric plant', + description: + 'A facility that uses water in the generation of electricity from heat. Typically turbine generators are driven by steam. The heat may be caused by various means, including combustion, nuclear reactions, and geothermal processes.', + }, + 'FA-WDS': { + label: 'Water-distribution system', + description: + 'A site located somewhere on a networked infrastructure that distributes treated or untreated water to multiple domestic, industrial, institutional, and (or) commercial users. May be owned by a municipality or community, a water district, or a private concern.', + }, + 'FA-WIW': { + label: 'Waste injection well', + description: + 'A facility used to convey industrial waste, domestic sewage, brine, mine drainage, radioactive waste, or other fluid into an underground zone. An oil-test or deep-water well converted to waste disposal should be in this category. A well where fresh water is injected to artificially recharge thegroundwaterr supply or to pressurize an oil or gas production zone by injecting a fluid should be classified as a well (not an injection-well facility), with additional information recorded under Use of Site.', + }, + 'FA-WTP': { + label: 'Water-supply treatment plant', + description: + 'A facility where water is treated prior to use for consumption or other purpose.', + }, + 'FA-WU': { + label: 'Water-use establishment', + description: + 'A place-of-use (a water using facility that is associated with a specific geographical point location, such as a business or industrial user) that cannot be specified with any other facility secondary type. Water-use place-of-use sites are establishments such as a factory, mill, store, warehouse, farm, ranch, or bank. A place-of-use site is further classified using the national water-use category code (C39) and optionally classified using the Standard Industrial Classification System Code (SIC code) or North American Classification System Code (NAICS code). See also: Aggregate water-use-establishment.', + }, + 'FA-WWD': { + label: 'Wastewater land application', + description: + 'A site where the disposal of waste water on land occurs. Use "waste-injection well" for underground waste-disposal sites.', + }, + 'FA-WWTP': { + label: 'Wastewater-treatment plant', + description: + 'A facility where wastewater is treated to reduce concentrations of dissolved and (or) suspended materials prior to discharge or reuse.', + }, + GL: { + label: 'Glacier', + description: + 'Body of land ice that consists of recrystallized snow accumulated on the surface of the ground and moves slowly downslope (WSP-1541A) over a period of years or centuries. Since glacial sites move, the lat-long precision for these sites is usually coarse.', + }, + GW: { + label: 'Well', + description: + 'A hole or shaft constructed in the earth intended to be used to locate, sample, or develop groundwater, oil, gas, or some other subsurface material. The diameter of a well is typically much smaller than the depth. Wells are also used to artificially recharge groundwater or to pressurize oil and gas production zones. Additional information about specific kinds of wells should be recorded under the secondary site types or the Use of Site field. Underground waste-disposal wells should be classified as waste-injection wells.', + }, + 'GW-CR': { + label: 'Collector or Ranney type well', + description: + 'An infiltration gallery consisting of one or more underground laterals through which groundwater is collected and a vertical caisson from which groundwater is removed. Also known as a "horizontal well". These wells produce large yield with small drawdown.', + }, + 'GW-EX': { + label: 'Extensometer well', + description: + 'A well equipped to measure small changes in the thickness of the penetrated sediments, such as those caused by groundwater withdrawal or recharge.', + }, + 'GW-HZ': { + label: 'Hyporheic-zone well', + description: + 'A permanent well, drive point, or other device intended to sample a saturated zone in close proximity to a stream.', + }, + 'GW-IW': { + label: 'Interconnected wells', + description: + 'Collector or drainage wells connected by an underground lateral.', + }, + 'GW-MW': { + label: 'Multiple wells', + description: + 'A group of wells that are pumped through a single header and for which little or no data about the individual wells are available.', + }, + 'GW-TH': { + label: 'Test hole not completed as a well', + description: + 'An uncased hole (or one cased only temporarily) that was drilled for water, or for geologic or hydrogeologic testing. It may be equipped temporarily with a pump in order to make a pumping test, but if the hole is destroyed after testing is completed, it is still a test hole. A core hole drilled as a part of mining or quarrying exploration work should be in this class.', + }, + LA: { + label: 'Land', + description: + 'A location on the surface of the earth that is not normally saturated with water. Land sites are appropriate for sampling vegetation, overland flow of water, or measuring land-surface properties such as temperature. (See also: Wetland).', + }, + 'LA-EX': { + label: 'Excavation', + description: + 'An artificially constructed cavity in the earth that is deeper than the soil (see soil hole), larger than a well bore (see well and test hole), and substantially open to the atmosphere. The diameter of an excavation is typically similar or larger than the depth. Excavations include building-foundation diggings, roadway cuts, and surface mines.', + }, + 'LA-OU': { + label: 'Outcrop', + description: + 'The part of a rock formation that appears at the surface of the surrounding land.', + }, + 'LA-PLY': { + label: 'Playa', + description: + 'A dried-up, vegetation-free, flat-floored area composed of thin, evenly stratified sheets of fine clay, silt or sand, and represents the bottom part of a shallow, completely closed or undrained desert lake basin in which water accumulates and is quickly evaporated, usually leaving deposits of soluble salts.', + }, + 'LA-SH': { + label: 'Soil hole', + description: + 'A small excavation into soil at the top few meters of earth surface. Soil generally includes some organic matter derived from plants. Soil holes are created to measure soil composition and properties. Sometimes electronic probes are inserted into soil holes to measure physical properties, and (or) the extracted soil is analyzed.', + }, + 'LA-SNK': { + label: 'Sinkhole', + description: + 'A crater formed when the roof of a cavern collapses; usually found in limestone areas. Surface water and precipitation that enters a sinkhole usually evaporates or infiltrates into the ground, rather than draining into a stream.', + }, + 'LA-SR': { + label: 'Shore', + description: + 'The land along the edge of the sea, a lake, or a wide river where the investigator considers the proximity of the water body to be important. Land adjacent to a reservoir, lake, impoundment, or oceanic site type is considered part of the shore when it includes a beach or bank between the high and low water marks.', + }, + 'LA-VOL': { + label: 'Volcanic vent', + description: + 'Vent from which volcanic gases escape to the atmosphere. Also known as fumarole.', + }, + LK: { + label: 'Lake, Reservoir, Impoundment', + description: + 'An inland body of standing fresh or saline water that is generally too deep to permit submerged aquatic vegetation to take root across the entire body (cf: wetland). This site type includes an expanded part of a river, a reservoir behind a dam, and a natural or excavated depression containing a water body without surface-water inlet and/or outlet.', + }, + OC: { + label: 'Ocean', + description: + 'Site in the open ocean, gulf, or sea. (See also: Coastal, Estuary, and Tidal stream).', + }, + 'OC-CO': { + label: 'Coastal', + description: + 'An oceanic site that is located off-shore beyond the tidal mixing zone (estuary) but close enough to the shore that the investigator considers the presence of the coast to be important. Coastal sites typically are within three nautical miles of the shore.', + }, + SB: { + label: 'Subsurface', + description: + 'A location below the land surface, but not a well, soil hole, or excavation.', + }, + 'SB-CV': { + label: 'Cave', + description: + 'A natural open space within a rock formation large enough to accommodate a human. A cave may have an opening to the outside, is always underground, and sometimes submerged. Caves commonly occur by the dissolution of soluble rocks, generally limestone, but may also be created within the voids of large-rock aggregations, in openings along seismic faults, and in lava formations.', + }, + 'SB-GWD': { + label: 'Groundwater drain', + description: + 'An underground pipe or tunnel through which groundwater is artificially diverted to surface water for the purpose of reducing erosion or lowering the water table. A drain is typically open to the atmosphere at the lowest elevation, in contrast to a well which is open at the highest point.', + }, + 'SB-TSM': { + label: 'Tunnel, shaft, or mine', + description: + 'A constructed subsurface open space large enough to accommodate a human that is not substantially open to the atmosphere and is not a well. The excavation may have been for minerals, transportation, or other purposes. See also: Excavation.', + }, + 'SB-UZ': { + label: 'Unsaturated zone', + description: + 'A site equipped to measure conditions in the subsurface deeper than a soil hole, but above the water table or other zone of saturation.', + }, + SP: { + label: 'Spring', + description: + 'A location at which the water table intersects the land surface, resulting in a natural flow of groundwater to the surface. Springs may be perennial, intermittent, or ephemeral.', + }, + ST: { + label: 'Stream', + description: + 'A body of running water moving under gravity flow in a defined channel. The channel may be entirely natural, or altered by engineering practices through straightening, dredging, and (or) lining. An entirely artificial channel should be qualified with the "canal" or "ditch" secondary site type.', + }, + 'ST-CA': { + label: 'Canal', + description: + 'An artificial watercourse designed for navigation, drainage, or irrigation by connecting two or more bodies of water; it is larger than a ditch.', + }, + 'ST-DCH': { + label: 'Ditch', + description: + 'An excavation artificially dug in the ground, either lined or unlined, for conveying water for drainage or irrigation; it is smaller than a canal.', + }, + 'ST-TS': { + label: 'Tidal stream', + description: + 'A stream reach where the flow is influenced by the tide, but where the water chemistry is not normally influenced. A site where ocean water typically mixes with stream water should be coded as an estuary.', + }, + WE: { + label: 'Wetland', + description: + 'Land where saturation with water is the dominant factor determining the nature of soil development and the types of plant and animal communities living in the soil and on its surface (Cowardin, December 1979). Wetlands are found from the tundra to the tropics and on every continent except Antarctica. Wetlands are areas that are inundated or saturated by surface or groundwater at a frequency and duration sufficient to support, and that under normal circumstances do support, a prevalence of vegetation typically adapted for life in saturated soil conditions. Wetlands generally include swamps, marshes, bogs and similar areas. Wetlands may be forested or unforested, and naturally or artificially created.', + }, + }, + 'coordinate-accuracy-codes': { + '1': { label: 'Accurate to + or - .1 sec (Differentially-Corrected GPS).' }, + '5': { label: 'Accurate to + or - .5 sec (PLGR/PPS GPS).' }, + B: { label: 'Level 1 survey-grade GPS' }, + C: { label: 'Level 2 survey-grade GPS' }, + D: { label: 'Level 3 survey-grade GPS' }, + E: { label: 'Level 4 survey-grade GPS' }, + F: { label: 'Accurate to + or - 5 sec.' }, + H: { label: 'Accurate to + or - .01 sec (Differentially-Corrected GPS).' }, + M: { label: 'Accurate to + or - 1 min.' }, + R: { label: 'Accurate to + or - 3 sec (SPS GPS).' }, + S: { label: 'Accurate to + or - 1 sec.' }, + T: { label: 'Accurate to + or - 10 sec.' }, + U: { label: 'Unknown or unspecified.' }, + }, + 'coordinate-datum-codes': { + ABIDJAN: { label: "Cote d'Ivoire" }, + ACCRA: { label: 'Ghana' }, + ADINDAN: { label: 'Description' }, + AFGOOYE: { label: 'Somalia' }, + AGADEZ: { label: 'Niger' }, + AINELABD: { label: 'Description' }, + ALASKANIS: { label: 'Alaskan islands' }, + ALBANIAN: { label: 'Description' }, + AMERSFOORT: { label: 'Netherlands' }, + AMMASSALIK: { label: 'Greenland -Ammassalik area' }, + ANGUILLA: { label: 'Leeward Islands - Anguilla' }, + ANNAL: { label: 'Cocos Islands' }, + ANTIGUA: { label: 'Leeward Islands - Antigua' }, + ARATU: { label: 'Brazil - coast South -2 deg 55 min' }, + ARC1950: { label: 'Africa - south & east' }, + ARC1960: { label: 'Africa - east' }, + ASCENSION: { label: 'Ascension Island' }, + ASTRO1952: { label: 'Marcus Island' }, + ATFPARIS: { label: 'France' }, + AUSANTARCT: { label: 'Antarctica - Australian sector 1998' }, + AUSTRAL66: { label: 'Australia Geodetic Datum 1966' }, + AUSTRAL84: { label: 'Australia Geodetic Datum 1984' }, + AUSTRAL94: { label: 'Australia' }, + AZORES39OC: { label: 'Azores 1939 - Flores, Corvo' }, + AZORES40OR: { label: 'Azores 1940 -' }, + AZORES48CI: { + label: 'Azores 1948 - Graciosa, Terceira, Sao Jorge, Pico, Faial', + }, + AZORES95CI: { + label: 'Azores 1995 - Graciosa, Terceira, Sao Jorge, Pico, Faial', + }, + AZORES95OR: { label: 'Azores 1995 -' }, + BABSOUTH: { label: 'Republic of Palau - Bablethuap' }, + BARBADOS: { label: 'Barbados' }, + BATAVIAIND: { label: 'Indonesia - Java' }, + BATAVIAJAK: { label: 'Indonesia - Java' }, + BEACON1945: { label: 'Iwo Jima' }, + BEDUARAM: { label: 'Niger' }, + BEIGE1950: { label: 'Belgium' }, + BEIJING: { label: 'China' }, + BELLEVUE: { label: 'Efate & Erromango Islands' }, + BERMUDA00: { label: 'Bermuda' }, + BERMUDA57: { label: 'Bermuda' }, + BERN1898: { label: 'Liechtenstein, Switzerland' }, + BERN1938: { label: 'Liechtenstein, Switzerland' }, + BISSAU: { label: 'Guinea - Bissau' }, + BOGOTA: { label: 'Colombia' }, + BUKIT: { label: 'Indonesia - Banga & Belitung Islands' }, + CAMACUPA: { label: 'Angola' }, + CAMPAREA: { label: 'Astro Antarctica - McMurdo Camp Area' }, + CAMPOINCH: { label: 'Argentina' }, + CANAVERAL: { label: 'Cape Canaveral, Florida, Bahamas' }, + CANTON66: { label: 'Phoenix Islands' }, + CAPE: { label: 'South Africa' }, + CARTHAGE: { label: 'Tunisia' }, + CARTHAGED: { label: 'Tunisia' }, + CARTHAGEP: { label: 'Tunisia' }, + CASGLLO: { label: 'Argentina-Coiradoro Rivadavia' }, + CH1903: { label: 'Liechtenstein, Switzerland' }, + CH1903A: { label: 'Liechtenstein, Switzerland' }, + CHATHAM71: { label: 'New Zealand - Chatham Is.' }, + CHATHAM79: { label: 'Chatham Islands' }, + CHOSMALAL: { label: 'Argentina- Neuquen' }, + CHTRF95: { label: 'Liechtenstein, Switzerland' }, + CHUA: { label: 'Brazil' }, + COMBANI50: { label: 'Mayotte' }, + COMOROS: { label: 'Comoros' }, + CONAKRY05: { label: 'Guinea' }, + CORREGO: { label: 'Brazil' }, + COTE: { label: "Cote d'Ivoire" }, + DABOLA81: { label: 'Guinea' }, + DATUM73: { label: 'Portugal' }, + DEALUL33: { label: 'Romania' }, + DEALUL70: { label: 'Romania' }, + DECEPTION: { label: 'Antarctica - Deception Island' }, + DEGUERRE: { label: 'Nord de Guerre(Paris)- France' }, + DEIREZZOR: { label: 'Syrian Arab Republic' }, + DEUTSCHE: { label: 'Hauptdr Germany' }, + DOMINICA: { label: 'Windward Islands - Dominica' }, + DOS1968: { label: 'New Georgia - Gizo Island' }, + DOS714: { label: 'St. Helena Island' }, + DOUALA: { label: 'Cameroon' }, + DOUALA48: { label: 'Cameroon' }, + EASTER: { label: 'Easter Island' }, + ED1950: { label: 'Iran' }, + EGYPT1907: { label: 'Egypt' }, + EGYPT1930: { label: 'Egypt' }, + EMEP: { label: 'Europe' }, + ESTONIA37: { label: 'Estonia' }, + ESTONIA92: { label: 'Estonia' }, + ESTONIA97: { label: 'Estonia' }, + EUREF: { label: 'Finland' }, + EURFRAME89: { label: 'Europe' }, + EUROLIBYAN: { label: 'Libya' }, + EUROPEAN50: { label: 'Europe (west)' }, + EUROPEAN79: { label: 'Europe' }, + EUROPEAN87: { label: 'Europe (west)' }, + EUROSYS89: { label: 'Europe' }, + EVERESTBAN: { label: 'Bangladesh' }, + EVERESTIN: { label: 'India, Nepal' }, + FAHUD: { label: "Oman '" }, + FINALDATUM: { label: 'Iran - Gulf coast and Arwaz' }, + FTDESAIX: { label: 'Martinique' }, + FTMARIGOT: { + label: 'Guadeloupe - Saint Martin and Saint Barthelemy islands', + }, + FTTHOMAS: { label: 'Nevis & St. Kitts - Leeward Is.' }, + GANDAJIKA: { label: 'Maldives' }, + GAROUA: { label: 'Cameroon' }, + GRACIOSA48: { + label: 'Azores - Faial, Graaosa, Pioo, Sao Jorge, & Terceira Islands', + }, + GRENADA53: { label: 'Windward Islands-Grenada' }, + GUNUNG: { label: 'Indonesia - Kalimantan' }, + GUX1: { label: 'Guadalcanal Island' }, + GUYANAIS67: { label: 'Guyanais 1967, French Guiana' }, + GUYANE: { label: 'French Guyana' }, + HANOI1972: { label: 'Vietnam' }, + HARTEBEEST: { label: 'South Africa' }, + HELLO1954: { label: 'Norway -Jan Mayen' }, + HERATN: { label: 'Afghanistan' }, + HERMANNSKO: { + label: + 'Bosnia & Herzegovina, Croatia, Serbia, Slovenia, Yugoslavia (prior to 1990)', + }, + HFTO1963: { label: 'Chile - Tierra del Fuego' }, + HJORSEY55: { label: 'Iceland' }, + HONGKONG63: { label: 'Hong Kong' }, + HONGKONG80: { label: 'Hong Kong' }, + HUNGARIAN: { label: 'Hungary' }, + HUTZUSHAN: { label: 'Taiwan' }, + IERS1988: { label: 'World Terrestrial Reference Frame1988' }, + IERS1989: { label: 'World Terrestrial Reference Frame 1989' }, + IERS1990: { label: 'World Terrestrial Reference Frame 1990' }, + IERS1991: { label: 'World Terrestrial Reference Frame 1991' }, + IERS1992: { label: 'World Terrestrial Reference Frame 1992' }, + IERS1993: { label: 'World Terrestrial Reference Frame 1993' }, + IERS1994: { label: 'World Terrestrial Reference Frame 1994' }, + IERS1996: { label: 'World Terrestrial Reference Frame 1996' }, + IERS1997: { label: 'World Terrestrial Reference Frame 1997' }, + IERS2000: { label: 'World Terrestrial Reference Frame 2000' }, + IGM1995: { label: 'Italy' }, + IGN53MARE: { label: 'New Caledonia - Mare' }, + IGN56LIFOU: { label: 'New Caledonia- Lifou' }, + IGN72NUKU: { label: 'Marquises Islands - Nuku Hiva' }, + IGN72TERRE: { label: 'New Caledonia - Grande Terre' }, + INDIAN54: { label: 'Myanmar, Thailand' }, + INDIAN60: { label: 'Cambodia, Viet Nam' }, + INDIAN75: { label: 'Thailand' }, + INDONESIAN: { label: 'Indonesia' }, + IRAQKUWAIT: { label: 'Iraq, Kuwait' }, + IRENET95: { label: 'Ireland' }, + ISLANDS93: { label: 'Iceland' }, + ISRAEL: { label: 'Israel' }, + ISTS061: { label: 'South Georgia Islands' }, + ISTS073: { label: 'Diego Garcia' }, + JAMAICA69: { label: 'Jamaica' }, + JAMAICA75: { label: 'Jamaica' }, + JGD2000: { label: 'Japan' }, + JOHNSTON: { label: 'Johnston Island' }, + JORDAN: { label: 'Jordan' }, + K01949: { label: 'Kerguelen Island' }, + KALIANPUR1: { label: 'India, Pakistan' }, + KALIANPUR2: { label: 'Bangladesh, India, Pakistan' }, + KALIANPUR3: { label: 'Pakistan' }, + KALIANPUR4: { label: 'India' }, + KANDAWALA: { label: 'Sri Lanka' }, + KERGUELEN: { label: 'Kerguelen Island!' }, + KERTAU: { label: 'Malaysia - west Malaysia & Singapore' }, + KOREAN85: { label: 'South Korea' }, + KOREAN95: { label: 'South Korea' }, + KOUSSERI: { label: "Cameroon - N'Djamena area" }, + KUSAIE51: { label: 'Fed. States of Micronesia - Caroline Islands' }, + KUWAITOIL: { label: 'Kuwait' }, + KUWAITUTIL: { label: 'Kuwait' }, + LACANOA: { label: 'Venezuela' }, + LAKE: { label: 'Venezuela' }, + LASNIEVES: { label: 'Pico de Las Nieves - Canary Islands' }, + LC51961: { label: 'Cayman Brac Island' }, + LEIGON: { label: 'Ghana' }, + LEONE1924: { label: 'Sierra Leone' }, + LEONE1960: { label: 'Sierra Leone' }, + LEONE1968: { label: 'Sierra Leone' }, + LIBERIA64: { label: 'Liberia' }, + LISBOABES: { label: 'Portugal' }, + LISBOAHAY: { label: 'Portugal' }, + LISBON: { label: 'Portugal' }, + LISBON1890: { label: 'Portugal' }, + LKS1992: { label: 'Latvia' }, + LKS1994: { label: 'Lithuania' }, + LOCODJO65: { label: "Cote d'Ivoire" }, + LOME: { label: 'Togo' }, + LQUINTANA: { label: 'Venezuela' }, + LUXEMBOURG: { label: 'Luxembourg' }, + LUZON1911: { label: 'Philippines' }, + MADEIRA36: { + label: 'Portugal - Madeira, Porto Santo and Desertas islands', + }, + MADRID1870: { label: 'Spain' }, + MADZANSUA: { label: 'Mozambique - west' }, + MAHE1971: { label: 'Seychelles' }, + MAJURO: { label: 'Marshall Islands - Majuro Island' }, + MAKASSAR: { label: 'Indonesia - southwest Sulawesi' }, + MALONGO87: { label: 'Angola - Cabinda' }, + MANOCA: { label: 'Cameroon' }, + MANOCA1962: { label: 'Cameroon' }, + MASSAWA: { label: 'Eritrea' }, + MERCHICH: { label: 'Morocco' }, + MERCHICHD: { label: 'Morocco' }, + MGIFERRO: { label: 'Austria' }, + MHAST: { label: 'Anbola - Cabinda' }, + MIDWAY1961: { label: 'Midway Island' }, + MILITARGEO: { label: 'Austria' }, + MINNA: { label: 'Nigeria' }, + MONTEMARIO: { label: 'Italy' }, + MONTSERRAT: { label: 'Leeward Islands - Montserrat' }, + MOP78: { label: 'Wallis and Futuna - Wallis' }, + MOZNET: { label: 'Mozambique' }, + MPORALOKO: { label: 'Gabon' }, + MTDILLON: { label: 'Tobago' }, + NAD27: { label: 'North American Datum of 1927' }, + NAD83: { label: 'North American Datum of 1983' }, + NAHRWAN67: { label: 'Arabian Gulf' }, + NAPARIMA55: { label: 'Trinidad' }, + NAPARIMA72: { label: 'Trinidad and Tobago' }, + NATGEODET: { label: 'National Geodetic Network - Kuwait' }, + NEA74: { label: 'New Caledonia - Grande Terre - Noumea area' }, + NGO1948: { label: 'Norway' }, + NOIRE: { label: 'Congo' }, + NORDSAHARA: { + label: 'Nord Sahara 1959 (Paris)- Algeria, Morocco, Tunisia.', + }, + NOUVTRIANG: { label: 'Nouvelle Triangulation Francaise - France' }, + NSWC9Z2: { label: 'World' }, + NTFPARIS: { label: 'France' }, + NZGEO1949: { label: 'New Zealand Geodetic Datum 1949 - New Zealand' }, + NZGEO2000: { label: 'New Zealand Geodetic Datum 2000 - New Zealand' }, + OBSERVATAR: { label: 'Mozambique - south' }, + OBSERVMET: { + label: 'Observ Meteorologico 1939 - Azores - Corvo & Rores Islands', + }, + OBSMET65: { label: 'Observatorio Meteorologico 1965 - Macau' }, + OLDAK: { label: 'Old Alaska (Mainland) and Aleutian Islands Datum' }, + OLDGUAM: { label: 'Old Guam Datum' }, + OLDHI: { label: 'Old Hawaiian Datum' }, + OLDPR: { label: 'Old Puerto Rico and Virgin Islands Datum' }, + OLDSAMOA: { label: 'Old American Samoa Datum' }, + OMAN: { label: 'Oman' }, + OSGB1936: { label: 'UK - Great Britain' }, + OSGB70SN: { label: 'UK - Great Britain' }, + OSNI1952: { label: 'UK - Northern Ireland' }, + OSSN80: { label: 'UK & Ireland' }, + PADANG1884: { label: 'Padang 1884 (Jakarta)- Indonesia - Sumatra' }, + PALESTINE: { label: 'Israel, Jordan, Lebanon, Palestine' }, + PDO1993: { label: 'Oman' }, + PERROUD50: { + label: 'Pointe Geologie Perroud 1950 - Antarctica - Adelie Land', + }, + PETRELS72: { label: 'Antarctica - Adelie Land - Petrels Island' }, + PITCAIRN67: { label: 'Pitcairn Island' }, + PITON: { label: 'Reunion' }, + POHNPEI: { label: 'Fed. States of Micronesia - Pohnpei' }, + POINT58: { label: 'Burkina Faso, Niger' }, + POSGAR: { label: 'Argentina' }, + POSGAR98: { label: 'Argentina' }, + PUERTORICO: { label: 'Puerto Rico & Virgin Islands' }, + PUIKOVO42: { + label: 'Armenia, Azerbaijan, Belarus, Estonia, Georgia, Kazakstan,', + }, + PULKOVO42: { + label: + 'Armenia, Azerbaijan, Belarus, Estonia, Georgia, Kazakstan, Kirgistan, Latvia, Lithuania, Moldova, Russia, Tadzhikstan, Turkmenistan, Ukraine, Uzbekistan', + }, + PULKOVO58: { label: 'Poland' }, + PULKOVO83: { label: 'Germany - states of former East Germany (DDR)' }, + PULKOVO95: { label: 'Russia' }, + QATAR1948: { label: 'Qatar - onshore' }, + QATAR1974: { label: 'Qatar - onshore' }, + QATAR1995: { label: 'Qatar - Qatar National Datum 1995' }, + QOMOQ: { label: 'Greenland' }, + QOMOQ1927: { label: 'Greenland' }, + QORNOQ: { label: 'Greenland' }, + RASSADIRAN: { label: 'Iran - Tehari refinery site only' }, + REGVEN: { label: 'Venezuela' }, + RESEAU50: { label: 'Belgium - Reseau National Beige 1950' }, + RESEAU72: { label: 'Belgium' }, + RESEAU91: { + label: 'New Caledonia - Reseau Geodesique Nouvelle Caledonie 1991', + }, + RESEAU92: { label: 'Reunion - Reseau Geodesique de la Reunion 1992' }, + RESEAU95: { + label: 'French Guiana - Reseau Geodesique Francais Guyane 1995', + }, + REUNION: { label: 'Mascarene Islands' }, + REYKJAVIK: { label: 'Iceland' }, + RGF1993: { label: 'France' }, + ROMA1940: { label: 'Italy' }, + RT38: { label: '(Stockholm) Sweden' }, + RT90: { label: 'Sweden' }, + S42: { label: 'Hungary' }, + SAINTEANNE: { + label: + 'Guadeloupe - Basse-Terre, Grande-Terre, Desiride, Made-Galante, Les Saintes', + }, + SAMBOJA: { label: 'Indonesia - east Kalimantan, Mahakam' }, + SAMDAT69: { label: 'South America - South American Datum 1969' }, + SAMERICA: { + label: 'Prov South Amer. Datum 1956 - Bolivia Ecuador Peru Venezuela', + }, + SANTO1936: { label: 'Porto Santo & Madeira Islands' }, + SANTO1965: { label: 'Espirito Santo Island' }, + SANTO1995: { + label: 'Portugal - Madeira, Porto Santo and Desertas islands.', + }, + SAOBRAZ: { label: 'Azores - Sao Miguel & Santa Maria Islands' }, + SAPPERHILL: { label: 'Falkland Islands' }, + SASIA: { label: 'Singapore' }, + SCHWARZEDC: { label: 'Namibia' }, + SCORESBYSU: { label: 'Greenland - Scoresbysund area' }, + SEGARAJAK: { label: 'Indonesia - east Kalimantan' }, + SEGORA: { label: 'Indonesia - SE Kalimantan' }, + SELVAGEM38: { label: 'Salvage Islands - Selvagem Grande 1938' }, + SERINDUNG: { label: 'Indonesia - E Kalimantan' }, + SIRGAS: { label: 'South America' }, + SJTSK: { label: 'Czechoslovakia (prior to 1 Jan 1993)' }, + ST71BELEP: { label: 'New Caledonia - Belep' }, + ST84PINS: { label: 'New Caledonia - lie des Pins' }, + ST87OUVEA: { label: 'New Caledonia - Ouvea' }, + STGEORGE: { label: 'Alaska island' }, + STKITTS55: { label: 'Leeward Islands - St. Kitts' }, + STLAWRENCE: { label: 'Alaska island' }, + STLUCIA: { label: 'Windward Islands - St. Lucia' }, + STPAUL: { label: 'Alaska island' }, + STVINCENT: { label: 'Windward Islands - St. Vincent' }, + SUDAN: { label: 'Sudan - south' }, + SWEREF99: { label: 'Sweden' }, + TAHAA: { + label: + 'French Polynesia - Society Islands - Bora Bora, Huahin, Raiatea, Tahaa', + }, + TAHITI: { label: 'Tahiti' }, + TANANADVE: { label: 'Madagascar' }, + TETE: { label: 'Mozambique' }, + TIMBALAI48: { label: 'Brunei & East Malaysia' }, + TM65: { label: 'Ireland' }, + TM75: { label: 'Ireland' }, + TOKYO: { label: 'Japan, North Korea, South Korea' }, + TOMISLAND: { label: 'Tern Island' }, + TRINIDAD: { label: 'Trinidad' }, + TRISTAN68: { label: 'Tristan da Cunha' }, + TRUCIAL48: { label: 'United Arab Emirates' }, + VITILEVU: { label: 'Fiji - Viti Levu Island' }, + VOIROL60D: { + label: 'Algeria - north of 32N - Voirol Unfie 1960 (degrees)', + }, + VOIROL60P: { label: 'Algeria - north of 32N - Voirol Unfie 1960 (Paris)' }, + VOIROL75: { label: 'Algeria - north of 32N' }, + VOIROL75D: { label: 'Algeria - north of 32N' }, + VOIROL75P: { label: 'Algeria - north of 32N' }, + WAKE52: { label: 'Wake Atoll' }, + WAKE60: { label: 'Marshall Islands' }, + WGS72: { label: 'World Datum 1972' }, + WGS84: { label: 'World Datum 1984' }, + XIAN1980: { label: 'China' }, + YACARE: { label: 'Uruguay' }, + YOFF: { label: 'Senegal' }, + ZSANDERIJ: { label: 'Suriname' }, + }, + 'coordinate-method-codes': { + C: { label: 'Calculated from land net' }, + D: { label: 'Differential Global Positioning System (DGPS)' }, + F: { label: 'Survey-grade Global positioning system (SGPS)' }, + G: { + label: + 'Global positioning system (GPS), Standard Positioning Service (SPS) or Precise Positioning Service (PPS)', + }, + L: { label: 'Long range navigation system' }, + M: { label: 'Interpolated from map' }, + N: { label: 'Interpolated from digital map' }, + R: { label: 'Reported' }, + S: { label: 'Transit, theodolite, or other surveying method' }, + U: { label: 'Unknown' }, + W: { + label: + 'GNSS1 - Level 1 Quality Survey Grade Global Navigation Satellite System', + }, + X: { + label: + 'GNSS2 - Level 1 Quality Survey Grade Global Navigation Satellite System', + }, + Y: { + label: + 'GNSS3 - Level 1 Quality Survey Grade Global Navigation Satellite System', + }, + Z: { + label: + 'GNSS4 - Level 1 Quality Survey Grade Global Navigation Satellite System', + }, + }, + 'altitude-datums': { + ASVD02: { label: 'American Samoa Vertical Datum of 2002' }, + BARGECANAL: { label: 'New York State Barge Canal datum' }, + GUVD04: { label: 'Guam Vertical Datum of 2004' }, + IGLD55: { label: 'International Great Lakes Datum of 1955' }, + IGLD85: { label: 'International Great Lakes Datum of 1985' }, + LMSL: { label: 'Local Mean Sea Level' }, + MLLW: { label: 'Mean Lower Low Water tidal datum' }, + NAVD88: { label: 'North American Vertical Datum of 1988' }, + NGVD29: { label: 'National Geodetic Vertical Datum of 1929' }, + NMVD03: { label: 'Northern Marianas Vertical Datum of 2003' }, + OLDAK: { label: 'Old Alaska (Mainland) and Aleutian Island Datum' }, + OLDPR: { label: 'Old Puerto Rico and Virgin Island Datum' }, + PRVD02: { label: 'Puerto Rico Vertical Datum of 2002' }, + USCGS1912: { + label: + 'U.S. Coast and Geodetic Survey 1912 Fourth General Adjustment of the Precise Level Net in the United States and the Resulting Elevations', + }, + VIVD09: { label: 'Virgin Islands Vertical Datum of 2009' }, + }, + 'reliability-codes': { + C: { label: 'Data have been field checked by the reporting agency.' }, + L: { label: 'Location not accurate.' }, + M: { label: 'Minimal data.' }, + U: { + label: + 'Unchecked data. Data have not been field checked by the reporting agency, but the reporting agency considers the data reliable.', + }, + }, + 'topographic-codes': { + A: { + label: 'Alluvial fan', + description: + 'Stream deposit of loose rock material where it issues from a narrow mountain valley upon a plain.', + }, + B: { + label: 'Playa', + description: + 'Undrained desert basin in which water accumulates and is quickly evaporated.', + }, + C: { + label: 'Stream channel', + description: 'Bed in which a natural stream of water runs.', + }, + D: { + label: 'Local depression', + description: 'An area that has no external surface drainage.', + }, + E: { + label: 'Dunes', + description: 'Mounds and ridges of windblown, or eolian sand.', + }, + F: { + label: 'Flat surface', + description: + 'May be part of a larger feature, such as a plateau, plain, or pediment.', + }, + G: { + label: 'Flood plain', + description: + 'Smooth land surface adjacent to a river channel that is flooded when the river overflows its banks.', + }, + H: { + label: 'Hilltop', + description: + 'Upper part of a hill or ridge above a well-defined break in slope.', + }, + K: { + label: 'Sinkhole', + description: + 'Depression that results from the dissolving of soluble rocks and collapse into the solution cavity.', + }, + L: { + label: 'Lake or Swamp', + description: + 'Inland lake, swamp, or marsh where the ground may be saturated or water stands above the land surface.', + }, + M: { + label: 'Mangrove swamp', + description: + 'Tropical or subtropical marine swamp characterized by abundant mangrove trees.', + }, + O: { + label: 'Offshore', + description: + 'Site along a coast or estuary that is continuously submerged.', + }, + P: { + label: 'Pediment', + description: + 'Plain of combined erosion and deposition that forms at the foot of a mountain range.', + }, + S: { + label: 'Hillside', + description: + 'Sloping side of hill, the area between the hilltop and valley flat.', + }, + T: { + label: 'Alluvial terrace', + description: + 'Generally a flat surface, usually parallel to but elevated above a stream valley or coast line.', + }, + U: { + label: 'Undulating', + description: + 'Topography is characteristic of areas which have many small depressions and low mounds.', + }, + V: { + label: 'Valley flat', + description: + 'Low flat area between the valley walls and bordering a stream channel.', + }, + W: { label: 'Upland draw', description: 'Small natural drainageway' }, + }, + 'aquifer-types': { + C: { label: 'Confined single aquifer' }, + M: { label: 'Confined multiple aquifers' }, + N: { label: 'Unconfined multiple aquifer' }, + U: { label: 'Unconfined single aquifer' }, + X: { label: 'Mixed (confined and unconfined) multiple aquifers' }, + }, + 'national-aquifer-codes': { + N100AKUNCD: { label: 'Alaska unconsolidated-deposit aquifers' }, + N100ALLUVL: { label: 'Alluvial aquifers' }, + N100BSNRGB: { label: 'Basin and Range basin-fill aquifers' }, + N100CACSTL: { label: 'California Coastal Basin aquifers' }, + N100CMBPLB: { label: 'Columbia Plateau basin-fill aquifers' }, + N100GLCIAL: { label: 'Sand and gravel aquifers (glaciated regions)' }, + N100HGHPLN: { label: 'High Plains aquifer' }, + N100MSRVVL: { label: 'Mississippi River Valley alluvial aquifer' }, + N100PCFNWB: { label: 'Pacific Northwest basin-fill aquifers' }, + N100PCFNWV: { label: 'Pacific Northwest volcanic-rock aquifers' }, + N100PCSRVR: { label: 'Pecos River Basin alluvial aquifer' }, + N100SYMOUR: { label: 'Seymour aquifer' }, + N100WLMLWD: { label: 'Willamette Lowland basin-fill aquifers' }, + N300ADAVMS: { label: 'Ada-Vamoosa aquifer' }, + N300CNRLOK: { label: 'Central Oklahoma aquifer' }, + N300COPLTS: { label: 'Colorado Plateaus aquifers' }, + N300ERLMZC: { label: 'Early Mesozoic basin aquifers' }, + N300JCBSVL: { label: 'Jacobsville aquifer' }, + N300LCRTCS: { label: 'Lower Cretaceous aquifers' }, + N300LTRTRY: { label: 'Lower Tertiary aquifers' }, + N300MRSHLL: { label: 'Marshall aquifer' }, + N300NYSDSN: { label: 'New York sandstone aquifers' }, + N300PNSLVN: { label: 'Pennsylvanian aquifers' }, + N300RSHSPG: { label: 'Rush Springs aquifer' }, + N300STHCST: { label: 'South Coast aquifer (Puerto Rico)' }, + N300UPCTCS: { label: 'Upper Cretaceous aquifers' }, + N300WYTRTR: { label: 'Wyoming Tertiary aquifers' }, + N400ABKSMP: { label: 'Arbuckle-Simpson aquifer' }, + N400BISCYN: { label: 'Biscayne aquifer' }, + N400BLAINE: { label: 'Blaine aquifer' }, + N400BSNRGC: { label: 'Basin and Range carbonate-rock aquifers' }, + N400CSLHYN: { label: 'Castle Hayne aquifer' }, + N400KNGSHL: { label: 'Kingshill aquifer (Virgin Islands)' }, + N400NCSTLM: { label: 'North Coast Limestone aquifer system (Puerto Rico)' }, + N400NYNECB: { label: 'New York and New England carbonate-rock aquifers' }, + N400ORDVCN: { label: 'Ordovician aquifers' }, + N400PDMBRC: { label: 'Piedmont and Blue Ridge carbonate-rock aquifers' }, + N400PDMBRX: { label: 'Piedmont and Blue Ridge crystalline-rock aquifers' }, + N400SLRDVN: { label: 'Silurian-Devonian aquifers' }, + N400UPCRBN: { label: 'Upper carbonate aquifer' }, + N500MSSPPI: { label: 'Mississippian aquifers' }, + N500PLOZOC: { label: 'Paleozoic aquifers' }, + N500VLYRDG: { label: 'Valley and Ridge aquifers' }, + N600CMBPLV: { label: 'Columbia Plateau basaltic-rock aquifers' }, + N600HIVLCC: { label: 'Hawaii volcanic-rock aquifers' }, + N600NECRSN: { label: 'New York and New England crystalline-rock aquifers' }, + N600SKRVPB: { label: 'Snake River Plain basin-fill aquifers' }, + N600SKRVPV: { label: 'Snake River Plain basaltic-rock aquifers' }, + N600SRNVDV: { label: 'Southern Nevada volcanic-rock aquifers' }, + N9999OTHER: { label: 'Other aquifers' }, + S100CNRLVL: { label: 'Central Valley aquifer system' }, + S100CSLLWD: { label: 'Coastal lowlands aquifer system' }, + S100MSEMBM: { label: 'Mississippi embayment aquifer system' }, + S100NATLCP: { label: 'Northern Atlantic Coastal Plain aquifer system' }, + S100NRMTIB: { + label: 'Northern Rocky Mountains Intermontane Basins aquifer systems', + }, + S100PGTSND: { label: 'Puget Sound aquifer system' }, + S100RIOGRD: { label: 'Rio Grande aquifer system' }, + S100SECSLP: { label: 'Southeastern Coastal Plain aquifer system' }, + S100SURFCL: { label: 'Surficial aquifer system' }, + S100TXCLUP: { label: 'Texas coastal uplands aquifer system' }, + S300CAMORD: { label: 'Cambrian-Ordovician aquifer system' }, + S300DNVRBS: { label: 'Denver Basin aquifer system' }, + S400FLORDN: { label: 'Floridan aquifer system' }, + S400OZRKPL: { label: 'Ozark Plateaus aquifer system' }, + S400RSWLBS: { label: 'Roswell Basin aquifer system' }, + S500EDRTRN: { label: 'Edwards-Trinity aquifer system' }, + S500INTRMD: { label: 'Intermediate aquifer system' }, + }, + 'time-zone-codes': { + ACST: { label: 'Central Australia Standard Time' }, + AEST: { label: 'Australia Eastern Standard Time' }, + AFT: { label: 'Afghanistan Time' }, + AKST: { label: 'Alaska Standard Time' }, + AST: { label: 'Atlantic Standard Time (Canada)' }, + AWST: { label: 'Australia Western Standard Time' }, + BT: { label: 'Baghdad Time' }, + CAST: { label: 'Central Australia Standard Time' }, + CCT: { label: 'China Coastal Time' }, + CET: { label: 'Central European Time' }, + CST: { label: 'Central Standard Time' }, + DNT: { label: 'Dansk Normal Time' }, + DST: { label: 'Dansk Summer Time' }, + EAST: { label: 'East Australian Standard Time' }, + EET: { label: 'Eastern Europe Standard Time' }, + EST: { label: 'Eastern Standard Time' }, + FST: { label: 'French Summer Time' }, + GST: { label: 'Guam Standard Time' }, + HST: { label: 'Hawaii Standard Time' }, + IDLE: { label: 'International Date Line, East' }, + IDLW: { label: 'International Date Line, West' }, + IST: { label: 'Israel Standard Time' }, + IT: { label: 'Iran Time' }, + JST: { label: 'Japan Standard Time' }, + JT: { label: 'Java Time' }, + KST: { label: 'Korea Standard Time' }, + LIGT: { label: 'Melbourne, Australia' }, + MET: { label: 'Middle Europe Time' }, + MEWT: { label: 'Middle Europe Winter Time' }, + MEZ: { label: 'Middle Europe Zone' }, + MST: { label: 'Mountain Standard Time' }, + MT: { label: 'Moluccas Time' }, + NFT: { label: 'Newfoundland Standard Time' }, + NOR: { label: 'Norway Standard Time' }, + NST: { label: 'Newfoundland Standard Time' }, + NZST: { label: 'New Zealand Standard Time' }, + NZT: { label: 'New Zealand Time' }, + PST: { label: 'Pacific Standard Time' }, + SAT: { label: 'South Australian Standard Time' }, + SET: { label: 'Seychelles Time' }, + SWT: { label: 'Swedish Winter Time' }, + UTC: { label: 'Universal Coordinated Time' }, + WAST: { label: 'West Australian Standard Time' }, + WAT: { label: 'West Africa Time' }, + WET: { label: 'Western Europe' }, + WST: { label: 'West Australian Standard Time' }, + 'ZP-11': { label: 'UTC -11 hours' }, + 'ZP-2': { label: 'UTC -2 hours' }, + 'ZP-3': { label: 'UTC -3 hours' }, + ZP11: { label: 'UTC +11 hours' }, + ZP4: { label: 'UTC +4 hours' }, + ZP5: { label: 'UTC +5 hours' }, + ZP6: { label: 'UTC +6 hours' }, + }, + countries: { + AD: { label: 'Andorra' }, + AE: { label: 'United Arab Emirates' }, + AF: { label: 'Afghanistan' }, + AG: { label: 'Antigua and Barbuda' }, + AI: { label: 'Anguilla' }, + AL: { label: 'Albania' }, + AM: { label: 'Armenia' }, + AO: { label: 'Angola' }, + AQ: { label: 'Antarctica' }, + AR: { label: 'Argentina' }, + AS: { label: 'American Samoa' }, + AT: { label: 'Austria' }, + AU: { label: 'Australia' }, + AW: { label: 'Aruba' }, + AX: { label: 'Åland Islands' }, + AZ: { label: 'Azerbaijan' }, + BA: { label: 'Bosnia and Herzegovina' }, + BB: { label: 'Barbados' }, + BD: { label: 'Bangladesh' }, + BE: { label: 'Belgium' }, + BF: { label: 'Burkina Faso' }, + BG: { label: 'Bulgaria' }, + BH: { label: 'Bahrain' }, + BI: { label: 'Burundi' }, + BJ: { label: 'Benin' }, + BL: { label: 'Saint Barthélemy' }, + BM: { label: 'Bermuda' }, + BN: { label: 'Brunei' }, + BO: { label: 'Bolivia' }, + BQ: { label: 'Caribbean Netherlands' }, + BR: { label: 'Brazil' }, + BS: { label: 'Bahamas' }, + BT: { label: 'Bhutan' }, + BV: { label: 'Bouvet Island' }, + BW: { label: 'Botswana' }, + BY: { label: 'Belarus' }, + BZ: { label: 'Belize' }, + CA: { label: 'Canada' }, + CC: { label: 'Cocos, Keeling Islands' }, + CD: { label: 'Democratic Republic of the Congo' }, + CF: { label: 'Central African Republic' }, + CG: { label: 'Republic of the Congo' }, + CH: { label: 'Switzerland' }, + CI: { label: 'Ivory Coast' }, + CK: { label: 'Cook Islands' }, + CL: { label: 'Chile' }, + CM: { label: 'Cameroon' }, + CN: { label: 'China' }, + CO: { label: 'Colombia' }, + CR: { label: 'Costa Rica' }, + CU: { label: 'Cuba' }, + CV: { label: 'Cabo Verde' }, + CW: { label: 'Curaçao' }, + CX: { label: 'Christmas Island' }, + CY: { label: 'Cyprus' }, + CZ: { label: 'Czechia' }, + DE: { label: 'Germany' }, + DJ: { label: 'Djibouti' }, + DK: { label: 'Denmark' }, + DM: { label: 'Dominica' }, + DO: { label: 'Dominican Republic' }, + DZ: { label: 'Algeria' }, + EC: { label: 'Ecuador' }, + EE: { label: 'Estonia' }, + EG: { label: 'Egypt' }, + EH: { label: 'Western Sahara' }, + ER: { label: 'Eritrea' }, + ES: { label: 'Spain' }, + ET: { label: 'Ethiopia' }, + FI: { label: 'Finland' }, + FJ: { label: 'Fiji' }, + FK: { label: 'Falkland Islands' }, + FM: { label: 'Federated States of Micronesia' }, + FO: { label: 'Faroe Islands' }, + FR: { label: 'France' }, + GA: { label: 'Gabon' }, + GB: { label: 'United Kingdom' }, + GD: { label: 'Grenada' }, + GE: { label: 'Georgia' }, + GF: { label: 'French Guiana' }, + GG: { label: 'Guernsey' }, + GH: { label: 'Ghana' }, + GI: { label: 'Gibraltar' }, + GL: { label: 'Greenland' }, + GM: { label: 'Gambia' }, + GN: { label: 'Guinea' }, + GP: { label: 'Guadeloupe' }, + GQ: { label: 'Equatorial Guinea' }, + GR: { label: 'Greece' }, + GS: { label: 'South Georgia and the South Sandwich Islands' }, + GT: { label: 'Guatemala' }, + GU: { label: 'Guam' }, + GW: { label: 'Guinea-Bissau' }, + GY: { label: 'Guyana' }, + HK: { label: 'Hong Kong' }, + HM: { label: 'Heard Island and McDonald Islands' }, + HN: { label: 'Honduras' }, + HR: { label: 'Croatia' }, + HT: { label: 'Haiti' }, + HU: { label: 'Hungary' }, + ID: { label: 'Indonesia' }, + IE: { label: 'Ireland' }, + IL: { label: 'Israel' }, + IM: { label: 'Isle of Man' }, + IN: { label: 'India' }, + IO: { label: 'British Indian Ocean Territory' }, + IQ: { label: 'Iraq' }, + IR: { label: 'Iran' }, + IS: { label: 'Iceland' }, + IT: { label: 'Italy' }, + JE: { label: 'Jersey' }, + JM: { label: 'Jamaica' }, + JO: { label: 'Jordan' }, + JP: { label: 'Japan' }, + KE: { label: 'Kenya' }, + KG: { label: 'Kyrgyzstan' }, + KH: { label: 'Cambodia' }, + KI: { label: 'Kiribati' }, + KM: { label: 'Comoros' }, + KN: { label: 'Saint Kitts and Nevis' }, + KP: { label: 'North Korea' }, + KR: { label: 'South Korea' }, + KW: { label: 'Kuwait' }, + KY: { label: 'Cayman Islands' }, + KZ: { label: 'Kazakhstan' }, + LA: { label: 'Laos' }, + LB: { label: 'Lebanon' }, + LC: { label: 'Saint Lucia' }, + LI: { label: 'Liechtenstein' }, + LK: { label: 'Sri Lanka' }, + LR: { label: 'Liberia' }, + LS: { label: 'Lesotho' }, + LT: { label: 'Lithuania' }, + LU: { label: 'Luxembourg' }, + LV: { label: 'Latvia' }, + LY: { label: 'Libya' }, + MA: { label: 'Morocco' }, + MC: { label: 'Monaco' }, + MD: { label: 'Moldova' }, + ME: { label: 'Montenegro' }, + MF: { label: 'Saint Martin' }, + MG: { label: 'Madagascar' }, + MH: { label: 'Marshall Islands' }, + MK: { label: 'North Macedonia' }, + ML: { label: 'Mali' }, + MM: { label: 'Myanmar' }, + MN: { label: 'Mongolia' }, + MO: { label: 'Macao' }, + MP: { label: 'Northern Mariana Islands' }, + MQ: { label: 'Martinique' }, + MR: { label: 'Mauritania' }, + MS: { label: 'Montserrat' }, + MT: { label: 'Malta' }, + MU: { label: 'Mauritius' }, + MV: { label: 'Maldives' }, + MW: { label: 'Malawi' }, + MX: { label: 'Mexico' }, + MY: { label: 'Malaysia' }, + MZ: { label: 'Mozambique' }, + NA: { label: 'Namibia' }, + NC: { label: 'New Caledonia' }, + NE: { label: 'Niger' }, + NF: { label: 'Norfolk Island' }, + NG: { label: 'Nigeria' }, + NI: { label: 'Nicaragua' }, + NL: { label: 'Netherlands' }, + NO: { label: 'Norway' }, + NP: { label: 'Nepal' }, + NR: { label: 'Nauru' }, + NU: { label: 'Niue' }, + NZ: { label: 'New Zealand' }, + OM: { label: 'Oman' }, + PA: { label: 'Panama' }, + PE: { label: 'Peru' }, + PF: { label: 'French Polynesia' }, + PG: { label: 'Papua New Guinea' }, + PH: { label: 'Philippines' }, + PK: { label: 'Pakistan' }, + PL: { label: 'Poland' }, + PM: { label: 'Saint Pierre and Miquelon' }, + PN: { label: 'Pitcairn' }, + PR: { label: 'Puerto Rico' }, + PS: { label: 'Palestine' }, + PT: { label: 'Portugal' }, + PW: { label: 'Palau' }, + PY: { label: 'Paraguay' }, + QA: { label: 'Qatar' }, + RE: { label: 'Réunion' }, + RO: { label: 'Romania' }, + RS: { label: 'Serbia' }, + RU: { label: 'Russia' }, + RW: { label: 'Rwanda' }, + SA: { label: 'Saudi Arabia' }, + SB: { label: 'Solomon Islands' }, + SC: { label: 'Seychelles' }, + SD: { label: 'Sudan' }, + SE: { label: 'Sweden' }, + SG: { label: 'Singapore' }, + SH: { label: 'Saint Helena, Ascension and Tristan da Cunha' }, + SI: { label: 'Slovenia' }, + SJ: { label: 'Svalbard and Jan Mayen' }, + SK: { label: 'Slovakia' }, + SL: { label: 'Sierra Leone' }, + SM: { label: 'San Marino' }, + SN: { label: 'Senegal' }, + SO: { label: 'Somalia' }, + SR: { label: 'Suriname' }, + SS: { label: 'South Sudan' }, + ST: { label: 'Sao Tome and Principe' }, + SV: { label: 'El Salvador' }, + SX: { label: 'Sint Maarten' }, + SY: { label: 'Syria' }, + SZ: { label: 'Eswatini' }, + TC: { label: 'Turks and Caicos Islands' }, + TD: { label: 'Chad' }, + TF: { label: 'French Southern and Antarctic Lands' }, + TG: { label: 'Togo' }, + TH: { label: 'Thailand' }, + TJ: { label: 'Tajikistan' }, + TK: { label: 'Tokelau' }, + TL: { label: 'Timor-Leste' }, + TM: { label: 'Turkmenistan' }, + TN: { label: 'Tunisia' }, + TO: { label: 'Tonga' }, + TR: { label: 'Türkiye' }, + TT: { label: 'Trinidad and Tobago' }, + TV: { label: 'Tuvalu' }, + TW: { label: 'Taiwan' }, + TZ: { label: 'Tanzania' }, + UA: { label: 'Ukraine' }, + UG: { label: 'Uganda' }, + UM: { label: 'United States Minor Outlying Islands' }, + US: { label: 'United States of America' }, + UY: { label: 'Uruguay' }, + UZ: { label: 'Uzbekistan' }, + VA: { label: 'Vatican City' }, + VC: { label: 'Saint Vincent and the Grenadines' }, + VE: { label: 'Venezuela' }, + VG: { label: 'Virgin Islands, British' }, + VI: { label: 'Virgin Islands, US' }, + VN: { label: 'Vietnam' }, + VU: { label: 'Vanuatu' }, + WF: { label: 'Wallis and Futuna' }, + WS: { label: 'Samoa' }, + XK: { label: 'Kosovo' }, + YE: { label: 'Yemen' }, + YT: { label: 'Mayotte' }, + ZA: { label: 'South Africa' }, + ZM: { label: 'Zambia' }, + ZW: { label: 'Zimbabwe' }, + }, + states: { + '00': { label: 'Unspecified' }, + '01': { label: 'Alabama' }, + '02': { label: 'Alaska' }, + '04': { label: 'Arizona' }, + '05': { label: 'Arkansas' }, + '06': { label: 'California' }, + '08': { label: 'Colorado' }, + '09': { label: 'Connecticut' }, + '10': { label: 'Delaware' }, + '11': { label: 'District of Columbia' }, + '12': { label: 'Florida' }, + '13': { label: 'Georgia' }, + '15': { label: 'Hawaii' }, + '16': { label: 'Idaho' }, + '17': { label: 'Illinois' }, + '18': { label: 'Indiana' }, + '19': { label: 'Iowa' }, + '20': { label: 'Kansas' }, + '21': { label: 'Kentucky' }, + '22': { label: 'Louisiana' }, + '23': { label: 'Maine' }, + '24': { label: 'Maryland' }, + '25': { label: 'Massachusetts' }, + '26': { label: 'Michigan' }, + '27': { label: 'Minnesota' }, + '28': { label: 'Mississippi' }, + '29': { label: 'Missouri' }, + '30': { label: 'Montana' }, + '31': { label: 'Nebraska' }, + '32': { label: 'Nevada' }, + '33': { label: 'New Hampshire' }, + '34': { label: 'New Jersey' }, + '35': { label: 'New Mexico' }, + '36': { label: 'New York' }, + '37': { label: 'North Carolina' }, + '38': { label: 'North Dakota' }, + '39': { label: 'Ohio' }, + '40': { label: 'Oklahoma' }, + '41': { label: 'Oregon' }, + '42': { label: 'Pennsylvania' }, + '44': { label: 'Rhode Island' }, + '45': { label: 'South Carolina' }, + '46': { label: 'South Dakota' }, + '47': { label: 'Tennessee' }, + '48': { label: 'Texas' }, + '49': { label: 'Utah' }, + '50': { label: 'Vermont' }, + '51': { label: 'Virginia' }, + '53': { label: 'Washington' }, + '54': { label: 'West Virginia' }, + '55': { label: 'Wisconsin' }, + '56': { label: 'Wyoming' }, + '60': { label: 'American Samoa' }, + '65': { label: 'Palmyra Atoll' }, + '66': { label: 'Guam' }, + '67': { label: 'Johnston Atoll' }, + '69': { label: 'Northern Mariana Islands' }, + '71': { label: 'Midway Islands' }, + '72': { label: 'Puerto Rico' }, + '73': { label: 'Ryukyu Islands, Southern' }, + '74': { label: 'Swan Islands' }, + '76': { label: 'Navassa Island' }, + '77': { label: 'U.S. Misc Pacific Islands' }, + '78': { label: 'Virgin Islands' }, + '79': { label: 'Wake Island' }, + }, + counties: { + '00-000': { label: 'Unspecified' }, + '01-000': { label: 'Unspecified' }, + '01-001': { label: 'Autauga County' }, + '01-003': { label: 'Baldwin County' }, + '01-005': { label: 'Barbour County' }, + '01-007': { label: 'Bibb County' }, + '01-009': { label: 'Blount County' }, + '01-011': { label: 'Bullock County' }, + '01-013': { label: 'Butler County' }, + '01-015': { label: 'Calhoun County' }, + '01-017': { label: 'Chambers County' }, + '01-019': { label: 'Cherokee County' }, + '01-021': { label: 'Chilton County' }, + '01-023': { label: 'Choctaw County' }, + '01-025': { label: 'Clarke County' }, + '01-027': { label: 'Clay County' }, + '01-029': { label: 'Cleburne County' }, + '01-031': { label: 'Coffee County' }, + '01-033': { label: 'Colbert County' }, + '01-035': { label: 'Conecuh County' }, + '01-037': { label: 'Coosa County' }, + '01-039': { label: 'Covington County' }, + '01-041': { label: 'Crenshaw County' }, + '01-043': { label: 'Cullman County' }, + '01-045': { label: 'Dale County' }, + '01-047': { label: 'Dallas County' }, + '01-049': { label: 'DeKalb County' }, + '01-051': { label: 'Elmore County' }, + '01-053': { label: 'Escambia County' }, + '01-055': { label: 'Etowah County' }, + '01-057': { label: 'Fayette County' }, + '01-059': { label: 'Franklin County' }, + '01-061': { label: 'Geneva County' }, + '01-063': { label: 'Greene County' }, + '01-065': { label: 'Hale County' }, + '01-067': { label: 'Henry County' }, + '01-069': { label: 'Houston County' }, + '01-071': { label: 'Jackson County' }, + '01-073': { label: 'Jefferson County' }, + '01-075': { label: 'Lamar County' }, + '01-077': { label: 'Lauderdale County' }, + '01-079': { label: 'Lawrence County' }, + '01-081': { label: 'Lee County' }, + '01-083': { label: 'Limestone County' }, + '01-085': { label: 'Lowndes County' }, + '01-087': { label: 'Macon County' }, + '01-089': { label: 'Madison County' }, + '01-091': { label: 'Marengo County' }, + '01-093': { label: 'Marion County' }, + '01-095': { label: 'Marshall County' }, + '01-097': { label: 'Mobile County' }, + '01-099': { label: 'Monroe County' }, + '01-101': { label: 'Montgomery County' }, + '01-103': { label: 'Morgan County' }, + '01-105': { label: 'Perry County' }, + '01-107': { label: 'Pickens County' }, + '01-109': { label: 'Pike County' }, + '01-111': { label: 'Randolph County' }, + '01-113': { label: 'Russell County' }, + '01-115': { label: 'St. Clair County' }, + '01-117': { label: 'Shelby County' }, + '01-119': { label: 'Sumter County' }, + '01-121': { label: 'Talladega County' }, + '01-123': { label: 'Tallapoosa County' }, + '01-125': { label: 'Tuscaloosa County' }, + '01-127': { label: 'Walker County' }, + '01-129': { label: 'Washington County' }, + '01-131': { label: 'Wilcox County' }, + '01-133': { label: 'Winston County' }, + '02-000': { label: 'Unspecified' }, + '02-013': { label: 'Aleutians East Borough' }, + '02-016': { label: 'Aleutians West Census Area' }, + '02-020': { label: 'Anchorage Municipality' }, + '02-050': { label: 'Bethel Census Area' }, + '02-060': { label: 'Bristol Bay Borough' }, + '02-063': { label: 'Chugach Census Area' }, + '02-066': { label: 'Copper River Census Area' }, + '02-068': { label: 'Denali Borough' }, + '02-070': { label: 'Dillingham Census Area' }, + '02-090': { label: 'Fairbanks North Star Borough' }, + '02-100': { label: 'Haines Borough' }, + '02-105': { label: 'Hoonah-Angoon Census Area' }, + '02-110': { label: 'Juneau City and Borough' }, + '02-122': { label: 'Kenai Peninsula Borough' }, + '02-130': { label: 'Ketchikan Gateway Borough' }, + '02-150': { label: 'Kodiak Island Borough' }, + '02-158': { label: 'Kusilvak Census Area' }, + '02-164': { label: 'Lake and Peninsula Borough' }, + '02-170': { label: 'Matanuska-Susitna Borough' }, + '02-180': { label: 'Nome Census Area' }, + '02-185': { label: 'North Slope Borough' }, + '02-188': { label: 'Northwest Arctic Borough' }, + '02-195': { label: 'Petersburg Borough' }, + '02-198': { label: 'Prince of Wales-Hyder Census Area' }, + '02-220': { label: 'City and Borough of Sitka' }, + '02-230': { label: 'Skagway Municipality' }, + '02-240': { label: 'Southeast Fairbanks Census Area' }, + '02-275': { label: 'Wrangell City and Borough' }, + '02-282': { label: 'Yakutat City and Borough' }, + '02-290': { label: 'Yukon-Koyukuk Census Area' }, + '04-000': { label: 'Unspecified' }, + '04-001': { label: 'Apache County' }, + '04-003': { label: 'Cochise County' }, + '04-005': { label: 'Coconino County' }, + '04-007': { label: 'Gila County' }, + '04-009': { label: 'Graham County' }, + '04-011': { label: 'Greenlee County' }, + '04-012': { label: 'La Paz County' }, + '04-013': { label: 'Maricopa County' }, + '04-015': { label: 'Mohave County' }, + '04-017': { label: 'Navajo County' }, + '04-019': { label: 'Pima County' }, + '04-021': { label: 'Pinal County' }, + '04-023': { label: 'Santa Cruz County' }, + '04-025': { label: 'Yavapai County' }, + '04-027': { label: 'Yuma County' }, + '05-000': { label: 'Unspecified' }, + '05-001': { label: 'Arkansas County' }, + '05-003': { label: 'Ashley County' }, + '05-005': { label: 'Baxter County' }, + '05-007': { label: 'Benton County' }, + '05-009': { label: 'Boone County' }, + '05-011': { label: 'Bradley County' }, + '05-013': { label: 'Calhoun County' }, + '05-015': { label: 'Carroll County' }, + '05-017': { label: 'Chicot County' }, + '05-019': { label: 'Clark County' }, + '05-021': { label: 'Clay County' }, + '05-023': { label: 'Cleburne County' }, + '05-025': { label: 'Cleveland County' }, + '05-027': { label: 'Columbia County' }, + '05-029': { label: 'Conway County' }, + '05-031': { label: 'Craighead County' }, + '05-033': { label: 'Crawford County' }, + '05-035': { label: 'Crittenden County' }, + '05-037': { label: 'Cross County' }, + '05-039': { label: 'Dallas County' }, + '05-041': { label: 'Desha County' }, + '05-043': { label: 'Drew County' }, + '05-045': { label: 'Faulkner County' }, + '05-047': { label: 'Franklin County' }, + '05-049': { label: 'Fulton County' }, + '05-051': { label: 'Garland County' }, + '05-053': { label: 'Grant County' }, + '05-055': { label: 'Greene County' }, + '05-057': { label: 'Hempstead County' }, + '05-059': { label: 'Hot Spring County' }, + '05-061': { label: 'Howard County' }, + '05-063': { label: 'Independence County' }, + '05-065': { label: 'Izard County' }, + '05-067': { label: 'Jackson County' }, + '05-069': { label: 'Jefferson County' }, + '05-071': { label: 'Johnson County' }, + '05-073': { label: 'Lafayette County' }, + '05-075': { label: 'Lawrence County' }, + '05-077': { label: 'Lee County' }, + '05-079': { label: 'Lincoln County' }, + '05-081': { label: 'Little River County' }, + '05-083': { label: 'Logan County' }, + '05-085': { label: 'Lonoke County' }, + '05-087': { label: 'Madison County' }, + '05-089': { label: 'Marion County' }, + '05-091': { label: 'Miller County' }, + '05-093': { label: 'Mississippi County' }, + '05-095': { label: 'Monroe County' }, + '05-097': { label: 'Montgomery County' }, + '05-099': { label: 'Nevada County' }, + '05-101': { label: 'Newton County' }, + '05-103': { label: 'Ouachita County' }, + '05-105': { label: 'Perry County' }, + '05-107': { label: 'Phillips County' }, + '05-109': { label: 'Pike County' }, + '05-111': { label: 'Poinsett County' }, + '05-113': { label: 'Polk County' }, + '05-115': { label: 'Pope County' }, + '05-117': { label: 'Prairie County' }, + '05-119': { label: 'Pulaski County' }, + '05-121': { label: 'Randolph County' }, + '05-123': { label: 'St. Francis County' }, + '05-125': { label: 'Saline County' }, + '05-127': { label: 'Scott County' }, + '05-129': { label: 'Searcy County' }, + '05-131': { label: 'Sebastian County' }, + '05-133': { label: 'Sevier County' }, + '05-135': { label: 'Sharp County' }, + '05-137': { label: 'Stone County' }, + '05-139': { label: 'Union County' }, + '05-141': { label: 'Van Buren County' }, + '05-143': { label: 'Washington County' }, + '05-145': { label: 'White County' }, + '05-147': { label: 'Woodruff County' }, + '05-149': { label: 'Yell County' }, + '06-000': { label: 'Unspecified' }, + '06-001': { label: 'Alameda County' }, + '06-003': { label: 'Alpine County' }, + '06-005': { label: 'Amador County' }, + '06-007': { label: 'Butte County' }, + '06-009': { label: 'Calaveras County' }, + '06-011': { label: 'Colusa County' }, + '06-013': { label: 'Contra Costa County' }, + '06-015': { label: 'Del Norte County' }, + '06-017': { label: 'El Dorado County' }, + '06-019': { label: 'Fresno County' }, + '06-021': { label: 'Glenn County' }, + '06-023': { label: 'Humboldt County' }, + '06-025': { label: 'Imperial County' }, + '06-027': { label: 'Inyo County' }, + '06-029': { label: 'Kern County' }, + '06-031': { label: 'Kings County' }, + '06-033': { label: 'Lake County' }, + '06-035': { label: 'Lassen County' }, + '06-037': { label: 'Los Angeles County' }, + '06-039': { label: 'Madera County' }, + '06-041': { label: 'Marin County' }, + '06-043': { label: 'Mariposa County' }, + '06-045': { label: 'Mendocino County' }, + '06-047': { label: 'Merced County' }, + '06-049': { label: 'Modoc County' }, + '06-051': { label: 'Mono County' }, + '06-053': { label: 'Monterey County' }, + '06-055': { label: 'Napa County' }, + '06-057': { label: 'Nevada County' }, + '06-059': { label: 'Orange County' }, + '06-061': { label: 'Placer County' }, + '06-063': { label: 'Plumas County' }, + '06-065': { label: 'Riverside County' }, + '06-067': { label: 'Sacramento County' }, + '06-069': { label: 'San Benito County' }, + '06-071': { label: 'San Bernardino County' }, + '06-073': { label: 'San Diego County' }, + '06-075': { label: 'San Francisco County' }, + '06-077': { label: 'San Joaquin County' }, + '06-079': { label: 'San Luis Obispo County' }, + '06-081': { label: 'San Mateo County' }, + '06-083': { label: 'Santa Barbara County' }, + '06-085': { label: 'Santa Clara County' }, + '06-087': { label: 'Santa Cruz County' }, + '06-089': { label: 'Shasta County' }, + '06-091': { label: 'Sierra County' }, + '06-093': { label: 'Siskiyou County' }, + '06-095': { label: 'Solano County' }, + '06-097': { label: 'Sonoma County' }, + '06-099': { label: 'Stanislaus County' }, + '06-101': { label: 'Sutter County' }, + '06-103': { label: 'Tehama County' }, + '06-105': { label: 'Trinity County' }, + '06-107': { label: 'Tulare County' }, + '06-109': { label: 'Tuolumne County' }, + '06-111': { label: 'Ventura County' }, + '06-113': { label: 'Yolo County' }, + '06-115': { label: 'Yuba County' }, + '08-000': { label: 'Unspecified' }, + '08-001': { label: 'Adams County' }, + '08-003': { label: 'Alamosa County' }, + '08-005': { label: 'Arapahoe County' }, + '08-007': { label: 'Archuleta County' }, + '08-009': { label: 'Baca County' }, + '08-011': { label: 'Bent County' }, + '08-013': { label: 'Boulder County' }, + '08-014': { label: 'Broomfield County' }, + '08-015': { label: 'Chaffee County' }, + '08-017': { label: 'Cheyenne County' }, + '08-019': { label: 'Clear Creek County' }, + '08-021': { label: 'Conejos County' }, + '08-023': { label: 'Costilla County' }, + '08-025': { label: 'Crowley County' }, + '08-027': { label: 'Custer County' }, + '08-029': { label: 'Delta County' }, + '08-031': { label: 'Denver County' }, + '08-033': { label: 'Dolores County' }, + '08-035': { label: 'Douglas County' }, + '08-037': { label: 'Eagle County' }, + '08-039': { label: 'Elbert County' }, + '08-041': { label: 'El Paso County' }, + '08-043': { label: 'Fremont County' }, + '08-045': { label: 'Garfield County' }, + '08-047': { label: 'Gilpin County' }, + '08-049': { label: 'Grand County' }, + '08-051': { label: 'Gunnison County' }, + '08-053': { label: 'Hinsdale County' }, + '08-055': { label: 'Huerfano County' }, + '08-057': { label: 'Jackson County' }, + '08-059': { label: 'Jefferson County' }, + '08-061': { label: 'Kiowa County' }, + '08-063': { label: 'Kit Carson County' }, + '08-065': { label: 'Lake County' }, + '08-067': { label: 'La Plata County' }, + '08-069': { label: 'Larimer County' }, + '08-071': { label: 'Las Animas County' }, + '08-073': { label: 'Lincoln County' }, + '08-075': { label: 'Logan County' }, + '08-077': { label: 'Mesa County' }, + '08-079': { label: 'Mineral County' }, + '08-081': { label: 'Moffat County' }, + '08-083': { label: 'Montezuma County' }, + '08-085': { label: 'Montrose County' }, + '08-087': { label: 'Morgan County' }, + '08-089': { label: 'Otero County' }, + '08-091': { label: 'Ouray County' }, + '08-093': { label: 'Park County' }, + '08-095': { label: 'Phillips County' }, + '08-097': { label: 'Pitkin County' }, + '08-099': { label: 'Prowers County' }, + '08-101': { label: 'Pueblo County' }, + '08-103': { label: 'Rio Blanco County' }, + '08-105': { label: 'Rio Grande County' }, + '08-107': { label: 'Routt County' }, + '08-109': { label: 'Saguache County' }, + '08-111': { label: 'San Juan County' }, + '08-113': { label: 'San Miguel County' }, + '08-115': { label: 'Sedgwick County' }, + '08-117': { label: 'Summit County' }, + '08-119': { label: 'Teller County' }, + '08-121': { label: 'Washington County' }, + '08-123': { label: 'Weld County' }, + '08-125': { label: 'Yuma County' }, + '09-000': { label: 'Unspecified' }, + '09-110': { label: 'Capitol Planning Region' }, + '09-120': { label: 'Greater Bridgeport Planning Region' }, + '09-130': { label: 'Lower Connecticut River Valley Planning Region' }, + '09-140': { label: 'Naugatuck Valley Planning Region' }, + '09-150': { label: 'Northeastern Connecticut Planning Region' }, + '09-160': { label: 'Northwest Hills Planning Region' }, + '09-170': { label: 'South Central Connecticut Planning Region' }, + '09-180': { label: 'Southeastern Connecticut Planning Region' }, + '09-190': { label: 'Western Connecticut Planning Region' }, + '10-000': { label: 'Unspecified' }, + '10-001': { label: 'Kent County' }, + '10-003': { label: 'New Castle County' }, + '10-005': { label: 'Sussex County' }, + '11-000': { label: 'Unspecified' }, + '11-001': { label: 'District of Columbia' }, + '12-000': { label: 'Unspecified' }, + '12-001': { label: 'Alachua County' }, + '12-003': { label: 'Baker County' }, + '12-005': { label: 'Bay County' }, + '12-007': { label: 'Bradford County' }, + '12-009': { label: 'Brevard County' }, + '12-011': { label: 'Broward County' }, + '12-013': { label: 'Calhoun County' }, + '12-015': { label: 'Charlotte County' }, + '12-017': { label: 'Citrus County' }, + '12-019': { label: 'Clay County' }, + '12-021': { label: 'Collier County' }, + '12-023': { label: 'Columbia County' }, + '12-027': { label: 'DeSoto County' }, + '12-029': { label: 'Dixie County' }, + '12-031': { label: 'Duval County' }, + '12-033': { label: 'Escambia County' }, + '12-035': { label: 'Flagler County' }, + '12-037': { label: 'Franklin County' }, + '12-039': { label: 'Gadsden County' }, + '12-041': { label: 'Gilchrist County' }, + '12-043': { label: 'Glades County' }, + '12-045': { label: 'Gulf County' }, + '12-047': { label: 'Hamilton County' }, + '12-049': { label: 'Hardee County' }, + '12-051': { label: 'Hendry County' }, + '12-053': { label: 'Hernando County' }, + '12-055': { label: 'Highlands County' }, + '12-057': { label: 'Hillsborough County' }, + '12-059': { label: 'Holmes County' }, + '12-061': { label: 'Indian River County' }, + '12-063': { label: 'Jackson County' }, + '12-065': { label: 'Jefferson County' }, + '12-067': { label: 'Lafayette County' }, + '12-069': { label: 'Lake County' }, + '12-071': { label: 'Lee County' }, + '12-073': { label: 'Leon County' }, + '12-075': { label: 'Levy County' }, + '12-077': { label: 'Liberty County' }, + '12-079': { label: 'Madison County' }, + '12-081': { label: 'Manatee County' }, + '12-083': { label: 'Marion County' }, + '12-085': { label: 'Martin County' }, + '12-086': { label: 'Miami-Dade County' }, + '12-087': { label: 'Monroe County' }, + '12-089': { label: 'Nassau County' }, + '12-091': { label: 'Okaloosa County' }, + '12-093': { label: 'Okeechobee County' }, + '12-095': { label: 'Orange County' }, + '12-097': { label: 'Osceola County' }, + '12-099': { label: 'Palm Beach County' }, + '12-101': { label: 'Pasco County' }, + '12-103': { label: 'Pinellas County' }, + '12-105': { label: 'Polk County' }, + '12-107': { label: 'Putnam County' }, + '12-109': { label: 'St. Johns County' }, + '12-111': { label: 'St. Lucie County' }, + '12-113': { label: 'Santa Rosa County' }, + '12-115': { label: 'Sarasota County' }, + '12-117': { label: 'Seminole County' }, + '12-119': { label: 'Sumter County' }, + '12-121': { label: 'Suwannee County' }, + '12-123': { label: 'Taylor County' }, + '12-125': { label: 'Union County' }, + '12-127': { label: 'Volusia County' }, + '12-129': { label: 'Wakulla County' }, + '12-131': { label: 'Walton County' }, + '12-133': { label: 'Washington County' }, + '13-000': { label: 'Unspecified' }, + '13-001': { label: 'Appling County' }, + '13-003': { label: 'Atkinson County' }, + '13-005': { label: 'Bacon County' }, + '13-007': { label: 'Baker County' }, + '13-009': { label: 'Baldwin County' }, + '13-011': { label: 'Banks County' }, + '13-013': { label: 'Barrow County' }, + '13-015': { label: 'Bartow County' }, + '13-017': { label: 'Ben Hill County' }, + '13-019': { label: 'Berrien County' }, + '13-021': { label: 'Bibb County' }, + '13-023': { label: 'Bleckley County' }, + '13-025': { label: 'Brantley County' }, + '13-027': { label: 'Brooks County' }, + '13-029': { label: 'Bryan County' }, + '13-031': { label: 'Bulloch County' }, + '13-033': { label: 'Burke County' }, + '13-035': { label: 'Butts County' }, + '13-037': { label: 'Calhoun County' }, + '13-039': { label: 'Camden County' }, + '13-043': { label: 'Candler County' }, + '13-045': { label: 'Carroll County' }, + '13-047': { label: 'Catoosa County' }, + '13-049': { label: 'Charlton County' }, + '13-051': { label: 'Chatham County' }, + '13-053': { label: 'Chattahoochee County' }, + '13-055': { label: 'Chattooga County' }, + '13-057': { label: 'Cherokee County' }, + '13-059': { label: 'Clarke County' }, + '13-061': { label: 'Clay County' }, + '13-063': { label: 'Clayton County' }, + '13-065': { label: 'Clinch County' }, + '13-067': { label: 'Cobb County' }, + '13-069': { label: 'Coffee County' }, + '13-071': { label: 'Colquitt County' }, + '13-073': { label: 'Columbia County' }, + '13-075': { label: 'Cook County' }, + '13-077': { label: 'Coweta County' }, + '13-079': { label: 'Crawford County' }, + '13-081': { label: 'Crisp County' }, + '13-083': { label: 'Dade County' }, + '13-085': { label: 'Dawson County' }, + '13-087': { label: 'Decatur County' }, + '13-089': { label: 'DeKalb County' }, + '13-091': { label: 'Dodge County' }, + '13-093': { label: 'Dooly County' }, + '13-095': { label: 'Dougherty County' }, + '13-097': { label: 'Douglas County' }, + '13-099': { label: 'Early County' }, + '13-101': { label: 'Echols County' }, + '13-103': { label: 'Effingham County' }, + '13-105': { label: 'Elbert County' }, + '13-107': { label: 'Emanuel County' }, + '13-109': { label: 'Evans County' }, + '13-111': { label: 'Fannin County' }, + '13-113': { label: 'Fayette County' }, + '13-115': { label: 'Floyd County' }, + '13-117': { label: 'Forsyth County' }, + '13-119': { label: 'Franklin County' }, + '13-121': { label: 'Fulton County' }, + '13-123': { label: 'Gilmer County' }, + '13-125': { label: 'Glascock County' }, + '13-127': { label: 'Glynn County' }, + '13-129': { label: 'Gordon County' }, + '13-131': { label: 'Grady County' }, + '13-133': { label: 'Greene County' }, + '13-135': { label: 'Gwinnett County' }, + '13-137': { label: 'Habersham County' }, + '13-139': { label: 'Hall County' }, + '13-141': { label: 'Hancock County' }, + '13-143': { label: 'Haralson County' }, + '13-145': { label: 'Harris County' }, + '13-147': { label: 'Hart County' }, + '13-149': { label: 'Heard County' }, + '13-151': { label: 'Henry County' }, + '13-153': { label: 'Houston County' }, + '13-155': { label: 'Irwin County' }, + '13-157': { label: 'Jackson County' }, + '13-159': { label: 'Jasper County' }, + '13-161': { label: 'Jeff Davis County' }, + '13-163': { label: 'Jefferson County' }, + '13-165': { label: 'Jenkins County' }, + '13-167': { label: 'Johnson County' }, + '13-169': { label: 'Jones County' }, + '13-171': { label: 'Lamar County' }, + '13-173': { label: 'Lanier County' }, + '13-175': { label: 'Laurens County' }, + '13-177': { label: 'Lee County' }, + '13-179': { label: 'Liberty County' }, + '13-181': { label: 'Lincoln County' }, + '13-183': { label: 'Long County' }, + '13-185': { label: 'Lowndes County' }, + '13-187': { label: 'Lumpkin County' }, + '13-189': { label: 'McDuffie County' }, + '13-191': { label: 'McIntosh County' }, + '13-193': { label: 'Macon County' }, + '13-195': { label: 'Madison County' }, + '13-197': { label: 'Marion County' }, + '13-199': { label: 'Meriwether County' }, + '13-201': { label: 'Miller County' }, + '13-205': { label: 'Mitchell County' }, + '13-207': { label: 'Monroe County' }, + '13-209': { label: 'Montgomery County' }, + '13-211': { label: 'Morgan County' }, + '13-213': { label: 'Murray County' }, + '13-215': { label: 'Muscogee County' }, + '13-217': { label: 'Newton County' }, + '13-219': { label: 'Oconee County' }, + '13-221': { label: 'Oglethorpe County' }, + '13-223': { label: 'Paulding County' }, + '13-225': { label: 'Peach County' }, + '13-227': { label: 'Pickens County' }, + '13-229': { label: 'Pierce County' }, + '13-231': { label: 'Pike County' }, + '13-233': { label: 'Polk County' }, + '13-235': { label: 'Pulaski County' }, + '13-237': { label: 'Putnam County' }, + '13-239': { label: 'Quitman County' }, + '13-241': { label: 'Rabun County' }, + '13-243': { label: 'Randolph County' }, + '13-245': { label: 'Richmond County' }, + '13-247': { label: 'Rockdale County' }, + '13-249': { label: 'Schley County' }, + '13-251': { label: 'Screven County' }, + '13-253': { label: 'Seminole County' }, + '13-255': { label: 'Spalding County' }, + '13-257': { label: 'Stephens County' }, + '13-259': { label: 'Stewart County' }, + '13-261': { label: 'Sumter County' }, + '13-263': { label: 'Talbot County' }, + '13-265': { label: 'Taliaferro County' }, + '13-267': { label: 'Tattnall County' }, + '13-269': { label: 'Taylor County' }, + '13-271': { label: 'Telfair County' }, + '13-273': { label: 'Terrell County' }, + '13-275': { label: 'Thomas County' }, + '13-277': { label: 'Tift County' }, + '13-279': { label: 'Toombs County' }, + '13-281': { label: 'Towns County' }, + '13-283': { label: 'Treutlen County' }, + '13-285': { label: 'Troup County' }, + '13-287': { label: 'Turner County' }, + '13-289': { label: 'Twiggs County' }, + '13-291': { label: 'Union County' }, + '13-293': { label: 'Upson County' }, + '13-295': { label: 'Walker County' }, + '13-297': { label: 'Walton County' }, + '13-299': { label: 'Ware County' }, + '13-301': { label: 'Warren County' }, + '13-303': { label: 'Washington County' }, + '13-305': { label: 'Wayne County' }, + '13-307': { label: 'Webster County' }, + '13-309': { label: 'Wheeler County' }, + '13-311': { label: 'White County' }, + '13-313': { label: 'Whitfield County' }, + '13-315': { label: 'Wilcox County' }, + '13-317': { label: 'Wilkes County' }, + '13-319': { label: 'Wilkinson County' }, + '13-321': { label: 'Worth County' }, + '15-000': { label: 'Unspecified' }, + '15-001': { label: 'Hawaii County' }, + '15-003': { label: 'Honolulu County' }, + '15-005': { label: 'Kalawao County' }, + '15-007': { label: 'Kauai County' }, + '15-009': { label: 'Maui County' }, + '16-000': { label: 'Unspecified' }, + '16-001': { label: 'Ada County' }, + '16-003': { label: 'Adams County' }, + '16-005': { label: 'Bannock County' }, + '16-007': { label: 'Bear Lake County' }, + '16-009': { label: 'Benewah County' }, + '16-011': { label: 'Bingham County' }, + '16-013': { label: 'Blaine County' }, + '16-015': { label: 'Boise County' }, + '16-017': { label: 'Bonner County' }, + '16-019': { label: 'Bonneville County' }, + '16-021': { label: 'Boundary County' }, + '16-023': { label: 'Butte County' }, + '16-025': { label: 'Camas County' }, + '16-027': { label: 'Canyon County' }, + '16-029': { label: 'Caribou County' }, + '16-031': { label: 'Cassia County' }, + '16-033': { label: 'Clark County' }, + '16-035': { label: 'Clearwater County' }, + '16-037': { label: 'Custer County' }, + '16-039': { label: 'Elmore County' }, + '16-041': { label: 'Franklin County' }, + '16-043': { label: 'Fremont County' }, + '16-045': { label: 'Gem County' }, + '16-047': { label: 'Gooding County' }, + '16-049': { label: 'Idaho County' }, + '16-051': { label: 'Jefferson County' }, + '16-053': { label: 'Jerome County' }, + '16-055': { label: 'Kootenai County' }, + '16-057': { label: 'Latah County' }, + '16-059': { label: 'Lemhi County' }, + '16-061': { label: 'Lewis County' }, + '16-063': { label: 'Lincoln County' }, + '16-065': { label: 'Madison County' }, + '16-067': { label: 'Minidoka County' }, + '16-069': { label: 'Nez Perce County' }, + '16-071': { label: 'Oneida County' }, + '16-073': { label: 'Owyhee County' }, + '16-075': { label: 'Payette County' }, + '16-077': { label: 'Power County' }, + '16-079': { label: 'Shoshone County' }, + '16-081': { label: 'Teton County' }, + '16-083': { label: 'Twin Falls County' }, + '16-085': { label: 'Valley County' }, + '16-087': { label: 'Washington County' }, + '17-000': { label: 'Unspecified' }, + '17-001': { label: 'Adams County' }, + '17-003': { label: 'Alexander County' }, + '17-005': { label: 'Bond County' }, + '17-007': { label: 'Boone County' }, + '17-009': { label: 'Brown County' }, + '17-011': { label: 'Bureau County' }, + '17-013': { label: 'Calhoun County' }, + '17-015': { label: 'Carroll County' }, + '17-017': { label: 'Cass County' }, + '17-019': { label: 'Champaign County' }, + '17-021': { label: 'Christian County' }, + '17-023': { label: 'Clark County' }, + '17-025': { label: 'Clay County' }, + '17-027': { label: 'Clinton County' }, + '17-029': { label: 'Coles County' }, + '17-031': { label: 'Cook County' }, + '17-033': { label: 'Crawford County' }, + '17-035': { label: 'Cumberland County' }, + '17-037': { label: 'DeKalb County' }, + '17-039': { label: 'DeWitt County' }, + '17-041': { label: 'Douglas County' }, + '17-043': { label: 'DuPage County' }, + '17-045': { label: 'Edgar County' }, + '17-047': { label: 'Edwards County' }, + '17-049': { label: 'Effingham County' }, + '17-051': { label: 'Fayette County' }, + '17-053': { label: 'Ford County' }, + '17-055': { label: 'Franklin County' }, + '17-057': { label: 'Fulton County' }, + '17-059': { label: 'Gallatin County' }, + '17-061': { label: 'Greene County' }, + '17-063': { label: 'Grundy County' }, + '17-065': { label: 'Hamilton County' }, + '17-067': { label: 'Hancock County' }, + '17-069': { label: 'Hardin County' }, + '17-071': { label: 'Henderson County' }, + '17-073': { label: 'Henry County' }, + '17-075': { label: 'Iroquois County' }, + '17-077': { label: 'Jackson County' }, + '17-079': { label: 'Jasper County' }, + '17-081': { label: 'Jefferson County' }, + '17-083': { label: 'Jersey County' }, + '17-085': { label: 'Jo Daviess County' }, + '17-087': { label: 'Johnson County' }, + '17-089': { label: 'Kane County' }, + '17-091': { label: 'Kankakee County' }, + '17-093': { label: 'Kendall County' }, + '17-095': { label: 'Knox County' }, + '17-097': { label: 'Lake County' }, + '17-099': { label: 'LaSalle County' }, + '17-101': { label: 'Lawrence County' }, + '17-103': { label: 'Lee County' }, + '17-105': { label: 'Livingston County' }, + '17-107': { label: 'Logan County' }, + '17-109': { label: 'McDonough County' }, + '17-111': { label: 'McHenry County' }, + '17-113': { label: 'McLean County' }, + '17-115': { label: 'Macon County' }, + '17-117': { label: 'Macoupin County' }, + '17-119': { label: 'Madison County' }, + '17-121': { label: 'Marion County' }, + '17-123': { label: 'Marshall County' }, + '17-125': { label: 'Mason County' }, + '17-127': { label: 'Massac County' }, + '17-129': { label: 'Menard County' }, + '17-131': { label: 'Mercer County' }, + '17-133': { label: 'Monroe County' }, + '17-135': { label: 'Montgomery County' }, + '17-137': { label: 'Morgan County' }, + '17-139': { label: 'Moultrie County' }, + '17-141': { label: 'Ogle County' }, + '17-143': { label: 'Peoria County' }, + '17-145': { label: 'Perry County' }, + '17-147': { label: 'Piatt County' }, + '17-149': { label: 'Pike County' }, + '17-151': { label: 'Pope County' }, + '17-153': { label: 'Pulaski County' }, + '17-155': { label: 'Putnam County' }, + '17-157': { label: 'Randolph County' }, + '17-159': { label: 'Richland County' }, + '17-161': { label: 'Rock Island County' }, + '17-163': { label: 'St. Clair County' }, + '17-165': { label: 'Saline County' }, + '17-167': { label: 'Sangamon County' }, + '17-169': { label: 'Schuyler County' }, + '17-171': { label: 'Scott County' }, + '17-173': { label: 'Shelby County' }, + '17-175': { label: 'Stark County' }, + '17-177': { label: 'Stephenson County' }, + '17-179': { label: 'Tazewell County' }, + '17-181': { label: 'Union County' }, + '17-183': { label: 'Vermilion County' }, + '17-185': { label: 'Wabash County' }, + '17-187': { label: 'Warren County' }, + '17-189': { label: 'Washington County' }, + '17-191': { label: 'Wayne County' }, + '17-193': { label: 'White County' }, + '17-195': { label: 'Whiteside County' }, + '17-197': { label: 'Will County' }, + '17-199': { label: 'Williamson County' }, + '17-201': { label: 'Winnebago County' }, + '17-203': { label: 'Woodford County' }, + '18-000': { label: 'Unspecified' }, + '18-001': { label: 'Adams County' }, + '18-003': { label: 'Allen County' }, + '18-005': { label: 'Bartholomew County' }, + '18-007': { label: 'Benton County' }, + '18-009': { label: 'Blackford County' }, + '18-011': { label: 'Boone County' }, + '18-013': { label: 'Brown County' }, + '18-015': { label: 'Carroll County' }, + '18-017': { label: 'Cass County' }, + '18-019': { label: 'Clark County' }, + '18-021': { label: 'Clay County' }, + '18-023': { label: 'Clinton County' }, + '18-025': { label: 'Crawford County' }, + '18-027': { label: 'Daviess County' }, + '18-029': { label: 'Dearborn County' }, + '18-031': { label: 'Decatur County' }, + '18-033': { label: 'DeKalb County' }, + '18-035': { label: 'Delaware County' }, + '18-037': { label: 'Dubois County' }, + '18-039': { label: 'Elkhart County' }, + '18-041': { label: 'Fayette County' }, + '18-043': { label: 'Floyd County' }, + '18-045': { label: 'Fountain County' }, + '18-047': { label: 'Franklin County' }, + '18-049': { label: 'Fulton County' }, + '18-051': { label: 'Gibson County' }, + '18-053': { label: 'Grant County' }, + '18-055': { label: 'Greene County' }, + '18-057': { label: 'Hamilton County' }, + '18-059': { label: 'Hancock County' }, + '18-061': { label: 'Harrison County' }, + '18-063': { label: 'Hendricks County' }, + '18-065': { label: 'Henry County' }, + '18-067': { label: 'Howard County' }, + '18-069': { label: 'Huntington County' }, + '18-071': { label: 'Jackson County' }, + '18-073': { label: 'Jasper County' }, + '18-075': { label: 'Jay County' }, + '18-077': { label: 'Jefferson County' }, + '18-079': { label: 'Jennings County' }, + '18-081': { label: 'Johnson County' }, + '18-083': { label: 'Knox County' }, + '18-085': { label: 'Kosciusko County' }, + '18-087': { label: 'LaGrange County' }, + '18-089': { label: 'Lake County' }, + '18-091': { label: 'LaPorte County' }, + '18-093': { label: 'Lawrence County' }, + '18-095': { label: 'Madison County' }, + '18-097': { label: 'Marion County' }, + '18-099': { label: 'Marshall County' }, + '18-101': { label: 'Martin County' }, + '18-103': { label: 'Miami County' }, + '18-105': { label: 'Monroe County' }, + '18-107': { label: 'Montgomery County' }, + '18-109': { label: 'Morgan County' }, + '18-111': { label: 'Newton County' }, + '18-113': { label: 'Noble County' }, + '18-115': { label: 'Ohio County' }, + '18-117': { label: 'Orange County' }, + '18-119': { label: 'Owen County' }, + '18-121': { label: 'Parke County' }, + '18-123': { label: 'Perry County' }, + '18-125': { label: 'Pike County' }, + '18-127': { label: 'Porter County' }, + '18-129': { label: 'Posey County' }, + '18-131': { label: 'Pulaski County' }, + '18-133': { label: 'Putnam County' }, + '18-135': { label: 'Randolph County' }, + '18-137': { label: 'Ripley County' }, + '18-139': { label: 'Rush County' }, + '18-141': { label: 'St. Joseph County' }, + '18-143': { label: 'Scott County' }, + '18-145': { label: 'Shelby County' }, + '18-147': { label: 'Spencer County' }, + '18-149': { label: 'Starke County' }, + '18-151': { label: 'Steuben County' }, + '18-153': { label: 'Sullivan County' }, + '18-155': { label: 'Switzerland County' }, + '18-157': { label: 'Tippecanoe County' }, + '18-159': { label: 'Tipton County' }, + '18-161': { label: 'Union County' }, + '18-163': { label: 'Vanderburgh County' }, + '18-165': { label: 'Vermillion County' }, + '18-167': { label: 'Vigo County' }, + '18-169': { label: 'Wabash County' }, + '18-171': { label: 'Warren County' }, + '18-173': { label: 'Warrick County' }, + '18-175': { label: 'Washington County' }, + '18-177': { label: 'Wayne County' }, + '18-179': { label: 'Wells County' }, + '18-181': { label: 'White County' }, + '18-183': { label: 'Whitley County' }, + '19-000': { label: 'Unspecified' }, + '19-001': { label: 'Adair County' }, + '19-003': { label: 'Adams County' }, + '19-005': { label: 'Allamakee County' }, + '19-007': { label: 'Appanoose County' }, + '19-009': { label: 'Audubon County' }, + '19-011': { label: 'Benton County' }, + '19-013': { label: 'Black Hawk County' }, + '19-015': { label: 'Boone County' }, + '19-017': { label: 'Bremer County' }, + '19-019': { label: 'Buchanan County' }, + '19-021': { label: 'Buena Vista County' }, + '19-023': { label: 'Butler County' }, + '19-025': { label: 'Calhoun County' }, + '19-027': { label: 'Carroll County' }, + '19-029': { label: 'Cass County' }, + '19-031': { label: 'Cedar County' }, + '19-033': { label: 'Cerro Gordo County' }, + '19-035': { label: 'Cherokee County' }, + '19-037': { label: 'Chickasaw County' }, + '19-039': { label: 'Clarke County' }, + '19-041': { label: 'Clay County' }, + '19-043': { label: 'Clayton County' }, + '19-045': { label: 'Clinton County' }, + '19-047': { label: 'Crawford County' }, + '19-049': { label: 'Dallas County' }, + '19-051': { label: 'Davis County' }, + '19-053': { label: 'Decatur County' }, + '19-055': { label: 'Delaware County' }, + '19-057': { label: 'Des Moines County' }, + '19-059': { label: 'Dickinson County' }, + '19-061': { label: 'Dubuque County' }, + '19-063': { label: 'Emmet County' }, + '19-065': { label: 'Fayette County' }, + '19-067': { label: 'Floyd County' }, + '19-069': { label: 'Franklin County' }, + '19-071': { label: 'Fremont County' }, + '19-073': { label: 'Greene County' }, + '19-075': { label: 'Grundy County' }, + '19-077': { label: 'Guthrie County' }, + '19-079': { label: 'Hamilton County' }, + '19-081': { label: 'Hancock County' }, + '19-083': { label: 'Hardin County' }, + '19-085': { label: 'Harrison County' }, + '19-087': { label: 'Henry County' }, + '19-089': { label: 'Howard County' }, + '19-091': { label: 'Humboldt County' }, + '19-093': { label: 'Ida County' }, + '19-095': { label: 'Iowa County' }, + '19-097': { label: 'Jackson County' }, + '19-099': { label: 'Jasper County' }, + '19-101': { label: 'Jefferson County' }, + '19-103': { label: 'Johnson County' }, + '19-105': { label: 'Jones County' }, + '19-107': { label: 'Keokuk County' }, + '19-109': { label: 'Kossuth County' }, + '19-111': { label: 'Lee County' }, + '19-113': { label: 'Linn County' }, + '19-115': { label: 'Louisa County' }, + '19-117': { label: 'Lucas County' }, + '19-119': { label: 'Lyon County' }, + '19-121': { label: 'Madison County' }, + '19-123': { label: 'Mahaska County' }, + '19-125': { label: 'Marion County' }, + '19-127': { label: 'Marshall County' }, + '19-129': { label: 'Mills County' }, + '19-131': { label: 'Mitchell County' }, + '19-133': { label: 'Monona County' }, + '19-135': { label: 'Monroe County' }, + '19-137': { label: 'Montgomery County' }, + '19-139': { label: 'Muscatine County' }, + '19-141': { label: "O'Brien County" }, + '19-143': { label: 'Osceola County' }, + '19-145': { label: 'Page County' }, + '19-147': { label: 'Palo Alto County' }, + '19-149': { label: 'Plymouth County' }, + '19-151': { label: 'Pocahontas County' }, + '19-153': { label: 'Polk County' }, + '19-155': { label: 'Pottawattamie County' }, + '19-157': { label: 'Poweshiek County' }, + '19-159': { label: 'Ringgold County' }, + '19-161': { label: 'Sac County' }, + '19-163': { label: 'Scott County' }, + '19-165': { label: 'Shelby County' }, + '19-167': { label: 'Sioux County' }, + '19-169': { label: 'Story County' }, + '19-171': { label: 'Tama County' }, + '19-173': { label: 'Taylor County' }, + '19-175': { label: 'Union County' }, + '19-177': { label: 'Van Buren County' }, + '19-179': { label: 'Wapello County' }, + '19-181': { label: 'Warren County' }, + '19-183': { label: 'Washington County' }, + '19-185': { label: 'Wayne County' }, + '19-187': { label: 'Webster County' }, + '19-189': { label: 'Winnebago County' }, + '19-191': { label: 'Winneshiek County' }, + '19-193': { label: 'Woodbury County' }, + '19-195': { label: 'Worth County' }, + '19-197': { label: 'Wright County' }, + '20-000': { label: 'Unspecified' }, + '20-001': { label: 'Allen County' }, + '20-003': { label: 'Anderson County' }, + '20-005': { label: 'Atchison County' }, + '20-007': { label: 'Barber County' }, + '20-009': { label: 'Barton County' }, + '20-011': { label: 'Bourbon County' }, + '20-013': { label: 'Brown County' }, + '20-015': { label: 'Butler County' }, + '20-017': { label: 'Chase County' }, + '20-019': { label: 'Chautauqua County' }, + '20-021': { label: 'Cherokee County' }, + '20-023': { label: 'Cheyenne County' }, + '20-025': { label: 'Clark County' }, + '20-027': { label: 'Clay County' }, + '20-029': { label: 'Cloud County' }, + '20-031': { label: 'Coffey County' }, + '20-033': { label: 'Comanche County' }, + '20-035': { label: 'Cowley County' }, + '20-037': { label: 'Crawford County' }, + '20-039': { label: 'Decatur County' }, + '20-041': { label: 'Dickinson County' }, + '20-043': { label: 'Doniphan County' }, + '20-045': { label: 'Douglas County' }, + '20-047': { label: 'Edwards County' }, + '20-049': { label: 'Elk County' }, + '20-051': { label: 'Ellis County' }, + '20-053': { label: 'Ellsworth County' }, + '20-055': { label: 'Finney County' }, + '20-057': { label: 'Ford County' }, + '20-059': { label: 'Franklin County' }, + '20-061': { label: 'Geary County' }, + '20-063': { label: 'Gove County' }, + '20-065': { label: 'Graham County' }, + '20-067': { label: 'Grant County' }, + '20-069': { label: 'Gray County' }, + '20-071': { label: 'Greeley County' }, + '20-073': { label: 'Greenwood County' }, + '20-075': { label: 'Hamilton County' }, + '20-077': { label: 'Harper County' }, + '20-079': { label: 'Harvey County' }, + '20-081': { label: 'Haskell County' }, + '20-083': { label: 'Hodgeman County' }, + '20-085': { label: 'Jackson County' }, + '20-087': { label: 'Jefferson County' }, + '20-089': { label: 'Jewell County' }, + '20-091': { label: 'Johnson County' }, + '20-093': { label: 'Kearny County' }, + '20-095': { label: 'Kingman County' }, + '20-097': { label: 'Kiowa County' }, + '20-099': { label: 'Labette County' }, + '20-101': { label: 'Lane County' }, + '20-103': { label: 'Leavenworth County' }, + '20-105': { label: 'Lincoln County' }, + '20-107': { label: 'Linn County' }, + '20-109': { label: 'Logan County' }, + '20-111': { label: 'Lyon County' }, + '20-113': { label: 'McPherson County' }, + '20-115': { label: 'Marion County' }, + '20-117': { label: 'Marshall County' }, + '20-119': { label: 'Meade County' }, + '20-121': { label: 'Miami County' }, + '20-123': { label: 'Mitchell County' }, + '20-125': { label: 'Montgomery County' }, + '20-127': { label: 'Morris County' }, + '20-129': { label: 'Morton County' }, + '20-131': { label: 'Nemaha County' }, + '20-133': { label: 'Neosho County' }, + '20-135': { label: 'Ness County' }, + '20-137': { label: 'Norton County' }, + '20-139': { label: 'Osage County' }, + '20-141': { label: 'Osborne County' }, + '20-143': { label: 'Ottawa County' }, + '20-145': { label: 'Pawnee County' }, + '20-147': { label: 'Phillips County' }, + '20-149': { label: 'Pottawatomie County' }, + '20-151': { label: 'Pratt County' }, + '20-153': { label: 'Rawlins County' }, + '20-155': { label: 'Reno County' }, + '20-157': { label: 'Republic County' }, + '20-159': { label: 'Rice County' }, + '20-161': { label: 'Riley County' }, + '20-163': { label: 'Rooks County' }, + '20-165': { label: 'Rush County' }, + '20-167': { label: 'Russell County' }, + '20-169': { label: 'Saline County' }, + '20-171': { label: 'Scott County' }, + '20-173': { label: 'Sedgwick County' }, + '20-175': { label: 'Seward County' }, + '20-177': { label: 'Shawnee County' }, + '20-179': { label: 'Sheridan County' }, + '20-181': { label: 'Sherman County' }, + '20-183': { label: 'Smith County' }, + '20-185': { label: 'Stafford County' }, + '20-187': { label: 'Stanton County' }, + '20-189': { label: 'Stevens County' }, + '20-191': { label: 'Sumner County' }, + '20-193': { label: 'Thomas County' }, + '20-195': { label: 'Trego County' }, + '20-197': { label: 'Wabaunsee County' }, + '20-199': { label: 'Wallace County' }, + '20-201': { label: 'Washington County' }, + '20-203': { label: 'Wichita County' }, + '20-205': { label: 'Wilson County' }, + '20-207': { label: 'Woodson County' }, + '20-209': { label: 'Wyandotte County' }, + '21-000': { label: 'Unspecified' }, + '21-001': { label: 'Adair County' }, + '21-003': { label: 'Allen County' }, + '21-005': { label: 'Anderson County' }, + '21-007': { label: 'Ballard County' }, + '21-009': { label: 'Barren County' }, + '21-011': { label: 'Bath County' }, + '21-013': { label: 'Bell County' }, + '21-015': { label: 'Boone County' }, + '21-017': { label: 'Bourbon County' }, + '21-019': { label: 'Boyd County' }, + '21-021': { label: 'Boyle County' }, + '21-023': { label: 'Bracken County' }, + '21-025': { label: 'Breathitt County' }, + '21-027': { label: 'Breckinridge County' }, + '21-029': { label: 'Bullitt County' }, + '21-031': { label: 'Butler County' }, + '21-033': { label: 'Caldwell County' }, + '21-035': { label: 'Calloway County' }, + '21-037': { label: 'Campbell County' }, + '21-039': { label: 'Carlisle County' }, + '21-041': { label: 'Carroll County' }, + '21-043': { label: 'Carter County' }, + '21-045': { label: 'Casey County' }, + '21-047': { label: 'Christian County' }, + '21-049': { label: 'Clark County' }, + '21-051': { label: 'Clay County' }, + '21-053': { label: 'Clinton County' }, + '21-055': { label: 'Crittenden County' }, + '21-057': { label: 'Cumberland County' }, + '21-059': { label: 'Daviess County' }, + '21-061': { label: 'Edmonson County' }, + '21-063': { label: 'Elliott County' }, + '21-065': { label: 'Estill County' }, + '21-067': { label: 'Fayette County' }, + '21-069': { label: 'Fleming County' }, + '21-071': { label: 'Floyd County' }, + '21-073': { label: 'Franklin County' }, + '21-075': { label: 'Fulton County' }, + '21-077': { label: 'Gallatin County' }, + '21-079': { label: 'Garrard County' }, + '21-081': { label: 'Grant County' }, + '21-083': { label: 'Graves County' }, + '21-085': { label: 'Grayson County' }, + '21-087': { label: 'Green County' }, + '21-089': { label: 'Greenup County' }, + '21-091': { label: 'Hancock County' }, + '21-093': { label: 'Hardin County' }, + '21-095': { label: 'Harlan County' }, + '21-097': { label: 'Harrison County' }, + '21-099': { label: 'Hart County' }, + '21-101': { label: 'Henderson County' }, + '21-103': { label: 'Henry County' }, + '21-105': { label: 'Hickman County' }, + '21-107': { label: 'Hopkins County' }, + '21-109': { label: 'Jackson County' }, + '21-111': { label: 'Jefferson County' }, + '21-113': { label: 'Jessamine County' }, + '21-115': { label: 'Johnson County' }, + '21-117': { label: 'Kenton County' }, + '21-119': { label: 'Knott County' }, + '21-121': { label: 'Knox County' }, + '21-123': { label: 'Larue County' }, + '21-125': { label: 'Laurel County' }, + '21-127': { label: 'Lawrence County' }, + '21-129': { label: 'Lee County' }, + '21-131': { label: 'Leslie County' }, + '21-133': { label: 'Letcher County' }, + '21-135': { label: 'Lewis County' }, + '21-137': { label: 'Lincoln County' }, + '21-139': { label: 'Livingston County' }, + '21-141': { label: 'Logan County' }, + '21-143': { label: 'Lyon County' }, + '21-145': { label: 'McCracken County' }, + '21-147': { label: 'McCreary County' }, + '21-149': { label: 'McLean County' }, + '21-151': { label: 'Madison County' }, + '21-153': { label: 'Magoffin County' }, + '21-155': { label: 'Marion County' }, + '21-157': { label: 'Marshall County' }, + '21-159': { label: 'Martin County' }, + '21-161': { label: 'Mason County' }, + '21-163': { label: 'Meade County' }, + '21-165': { label: 'Menifee County' }, + '21-167': { label: 'Mercer County' }, + '21-169': { label: 'Metcalfe County' }, + '21-171': { label: 'Monroe County' }, + '21-173': { label: 'Montgomery County' }, + '21-175': { label: 'Morgan County' }, + '21-177': { label: 'Muhlenberg County' }, + '21-179': { label: 'Nelson County' }, + '21-181': { label: 'Nicholas County' }, + '21-183': { label: 'Ohio County' }, + '21-185': { label: 'Oldham County' }, + '21-187': { label: 'Owen County' }, + '21-189': { label: 'Owsley County' }, + '21-191': { label: 'Pendleton County' }, + '21-193': { label: 'Perry County' }, + '21-195': { label: 'Pike County' }, + '21-197': { label: 'Powell County' }, + '21-199': { label: 'Pulaski County' }, + '21-201': { label: 'Robertson County' }, + '21-203': { label: 'Rockcastle County' }, + '21-205': { label: 'Rowan County' }, + '21-207': { label: 'Russell County' }, + '21-209': { label: 'Scott County' }, + '21-211': { label: 'Shelby County' }, + '21-213': { label: 'Simpson County' }, + '21-215': { label: 'Spencer County' }, + '21-217': { label: 'Taylor County' }, + '21-219': { label: 'Todd County' }, + '21-221': { label: 'Trigg County' }, + '21-223': { label: 'Trimble County' }, + '21-225': { label: 'Union County' }, + '21-227': { label: 'Warren County' }, + '21-229': { label: 'Washington County' }, + '21-231': { label: 'Wayne County' }, + '21-233': { label: 'Webster County' }, + '21-235': { label: 'Whitley County' }, + '21-237': { label: 'Wolfe County' }, + '21-239': { label: 'Woodford County' }, + '22-000': { label: 'Unspecified' }, + '22-001': { label: 'Acadia Parish' }, + '22-003': { label: 'Allen Parish' }, + '22-005': { label: 'Ascension Parish' }, + '22-007': { label: 'Assumption Parish' }, + '22-009': { label: 'Avoyelles Parish' }, + '22-011': { label: 'Beauregard Parish' }, + '22-013': { label: 'Bienville Parish' }, + '22-015': { label: 'Bossier Parish' }, + '22-017': { label: 'Caddo Parish' }, + '22-019': { label: 'Calcasieu Parish' }, + '22-021': { label: 'Caldwell Parish' }, + '22-023': { label: 'Cameron Parish' }, + '22-025': { label: 'Catahoula Parish' }, + '22-027': { label: 'Claiborne Parish' }, + '22-029': { label: 'Concordia Parish' }, + '22-031': { label: 'De Soto Parish' }, + '22-033': { label: 'East Baton Rouge Parish' }, + '22-035': { label: 'East Carroll Parish' }, + '22-037': { label: 'East Feliciana Parish' }, + '22-039': { label: 'Evangeline Parish' }, + '22-041': { label: 'Franklin Parish' }, + '22-043': { label: 'Grant Parish' }, + '22-045': { label: 'Iberia Parish' }, + '22-047': { label: 'Iberville Parish' }, + '22-049': { label: 'Jackson Parish' }, + '22-051': { label: 'Jefferson Parish' }, + '22-053': { label: 'Jefferson Davis Parish' }, + '22-055': { label: 'Lafayette Parish' }, + '22-057': { label: 'Lafourche Parish' }, + '22-059': { label: 'LaSalle Parish' }, + '22-061': { label: 'Lincoln Parish' }, + '22-063': { label: 'Livingston Parish' }, + '22-065': { label: 'Madison Parish' }, + '22-067': { label: 'Morehouse Parish' }, + '22-069': { label: 'Natchitoches Parish' }, + '22-071': { label: 'Orleans Parish' }, + '22-073': { label: 'Ouachita Parish' }, + '22-075': { label: 'Plaquemines Parish' }, + '22-077': { label: 'Pointe Coupee Parish' }, + '22-079': { label: 'Rapides Parish' }, + '22-081': { label: 'Red River Parish' }, + '22-083': { label: 'Richland Parish' }, + '22-085': { label: 'Sabine Parish' }, + '22-087': { label: 'St. Bernard Parish' }, + '22-089': { label: 'St. Charles Parish' }, + '22-091': { label: 'St. Helena Parish' }, + '22-093': { label: 'St. James Parish' }, + '22-095': { label: 'St. John the Baptist Parish' }, + '22-097': { label: 'St. Landry Parish' }, + '22-099': { label: 'St. Martin Parish' }, + '22-101': { label: 'St. Mary Parish' }, + '22-103': { label: 'St. Tammany Parish' }, + '22-105': { label: 'Tangipahoa Parish' }, + '22-107': { label: 'Tensas Parish' }, + '22-109': { label: 'Terrebonne Parish' }, + '22-111': { label: 'Union Parish' }, + '22-113': { label: 'Vermilion Parish' }, + '22-115': { label: 'Vernon Parish' }, + '22-117': { label: 'Washington Parish' }, + '22-119': { label: 'Webster Parish' }, + '22-121': { label: 'West Baton Rouge Parish' }, + '22-123': { label: 'West Carroll Parish' }, + '22-125': { label: 'West Feliciana Parish' }, + '22-127': { label: 'Winn Parish' }, + '23-000': { label: 'Unspecified' }, + '23-001': { label: 'Androscoggin County' }, + '23-003': { label: 'Aroostook County' }, + '23-005': { label: 'Cumberland County' }, + '23-007': { label: 'Franklin County' }, + '23-009': { label: 'Hancock County' }, + '23-011': { label: 'Kennebec County' }, + '23-013': { label: 'Knox County' }, + '23-015': { label: 'Lincoln County' }, + '23-017': { label: 'Oxford County' }, + '23-019': { label: 'Penobscot County' }, + '23-021': { label: 'Piscataquis County' }, + '23-023': { label: 'Sagadahoc County' }, + '23-025': { label: 'Somerset County' }, + '23-027': { label: 'Waldo County' }, + '23-029': { label: 'Washington County' }, + '23-031': { label: 'York County' }, + '24-000': { label: 'Unspecified' }, + '24-001': { label: 'Allegany County' }, + '24-003': { label: 'Anne Arundel County' }, + '24-005': { label: 'Baltimore County' }, + '24-009': { label: 'Calvert County' }, + '24-011': { label: 'Caroline County' }, + '24-013': { label: 'Carroll County' }, + '24-015': { label: 'Cecil County' }, + '24-017': { label: 'Charles County' }, + '24-019': { label: 'Dorchester County' }, + '24-021': { label: 'Frederick County' }, + '24-023': { label: 'Garrett County' }, + '24-025': { label: 'Harford County' }, + '24-027': { label: 'Howard County' }, + '24-029': { label: 'Kent County' }, + '24-031': { label: 'Montgomery County' }, + '24-033': { label: "Prince George's County" }, + '24-035': { label: "Queen Anne's County" }, + '24-037': { label: "St. Mary's County" }, + '24-039': { label: 'Somerset County' }, + '24-041': { label: 'Talbot County' }, + '24-043': { label: 'Washington County' }, + '24-045': { label: 'Wicomico County' }, + '24-047': { label: 'Worcester County' }, + '24-510': { label: 'Baltimore City' }, + '25-000': { label: 'Unspecified' }, + '25-001': { label: 'Barnstable County' }, + '25-003': { label: 'Berkshire County' }, + '25-005': { label: 'Bristol County' }, + '25-007': { label: 'Dukes County' }, + '25-009': { label: 'Essex County' }, + '25-011': { label: 'Franklin County' }, + '25-013': { label: 'Hampden County' }, + '25-015': { label: 'Hampshire County' }, + '25-017': { label: 'Middlesex County' }, + '25-019': { label: 'Nantucket County' }, + '25-021': { label: 'Norfolk County' }, + '25-023': { label: 'Plymouth County' }, + '25-025': { label: 'Suffolk County' }, + '25-027': { label: 'Worcester County' }, + '26-000': { label: 'Unspecified' }, + '26-001': { label: 'Alcona County' }, + '26-003': { label: 'Alger County' }, + '26-005': { label: 'Allegan County' }, + '26-007': { label: 'Alpena County' }, + '26-009': { label: 'Antrim County' }, + '26-011': { label: 'Arenac County' }, + '26-013': { label: 'Baraga County' }, + '26-015': { label: 'Barry County' }, + '26-017': { label: 'Bay County' }, + '26-019': { label: 'Benzie County' }, + '26-021': { label: 'Berrien County' }, + '26-023': { label: 'Branch County' }, + '26-025': { label: 'Calhoun County' }, + '26-027': { label: 'Cass County' }, + '26-029': { label: 'Charlevoix County' }, + '26-031': { label: 'Cheboygan County' }, + '26-033': { label: 'Chippewa County' }, + '26-035': { label: 'Clare County' }, + '26-037': { label: 'Clinton County' }, + '26-039': { label: 'Crawford County' }, + '26-041': { label: 'Delta County' }, + '26-043': { label: 'Dickinson County' }, + '26-045': { label: 'Eaton County' }, + '26-047': { label: 'Emmet County' }, + '26-049': { label: 'Genesee County' }, + '26-051': { label: 'Gladwin County' }, + '26-053': { label: 'Gogebic County' }, + '26-055': { label: 'Grand Traverse County' }, + '26-057': { label: 'Gratiot County' }, + '26-059': { label: 'Hillsdale County' }, + '26-061': { label: 'Houghton County' }, + '26-063': { label: 'Huron County' }, + '26-065': { label: 'Ingham County' }, + '26-067': { label: 'Ionia County' }, + '26-069': { label: 'Iosco County' }, + '26-071': { label: 'Iron County' }, + '26-073': { label: 'Isabella County' }, + '26-075': { label: 'Jackson County' }, + '26-077': { label: 'Kalamazoo County' }, + '26-079': { label: 'Kalkaska County' }, + '26-081': { label: 'Kent County' }, + '26-083': { label: 'Keweenaw County' }, + '26-085': { label: 'Lake County' }, + '26-087': { label: 'Lapeer County' }, + '26-089': { label: 'Leelanau County' }, + '26-091': { label: 'Lenawee County' }, + '26-093': { label: 'Livingston County' }, + '26-095': { label: 'Luce County' }, + '26-097': { label: 'Mackinac County' }, + '26-099': { label: 'Macomb County' }, + '26-101': { label: 'Manistee County' }, + '26-103': { label: 'Marquette County' }, + '26-105': { label: 'Mason County' }, + '26-107': { label: 'Mecosta County' }, + '26-109': { label: 'Menominee County' }, + '26-111': { label: 'Midland County' }, + '26-113': { label: 'Missaukee County' }, + '26-115': { label: 'Monroe County' }, + '26-117': { label: 'Montcalm County' }, + '26-119': { label: 'Montmorency County' }, + '26-121': { label: 'Muskegon County' }, + '26-123': { label: 'Newaygo County' }, + '26-125': { label: 'Oakland County' }, + '26-127': { label: 'Oceana County' }, + '26-129': { label: 'Ogemaw County' }, + '26-131': { label: 'Ontonagon County' }, + '26-133': { label: 'Osceola County' }, + '26-135': { label: 'Oscoda County' }, + '26-137': { label: 'Otsego County' }, + '26-139': { label: 'Ottawa County' }, + '26-141': { label: 'Presque Isle County' }, + '26-143': { label: 'Roscommon County' }, + '26-145': { label: 'Saginaw County' }, + '26-147': { label: 'St. Clair County' }, + '26-149': { label: 'St. Joseph County' }, + '26-151': { label: 'Sanilac County' }, + '26-153': { label: 'Schoolcraft County' }, + '26-155': { label: 'Shiawassee County' }, + '26-157': { label: 'Tuscola County' }, + '26-159': { label: 'Van Buren County' }, + '26-161': { label: 'Washtenaw County' }, + '26-163': { label: 'Wayne County' }, + '26-165': { label: 'Wexford County' }, + '27-000': { label: 'Unspecified' }, + '27-001': { label: 'Aitkin County' }, + '27-003': { label: 'Anoka County' }, + '27-005': { label: 'Becker County' }, + '27-007': { label: 'Beltrami County' }, + '27-009': { label: 'Benton County' }, + '27-011': { label: 'Big Stone County' }, + '27-013': { label: 'Blue Earth County' }, + '27-015': { label: 'Brown County' }, + '27-017': { label: 'Carlton County' }, + '27-019': { label: 'Carver County' }, + '27-021': { label: 'Cass County' }, + '27-023': { label: 'Chippewa County' }, + '27-025': { label: 'Chisago County' }, + '27-027': { label: 'Clay County' }, + '27-029': { label: 'Clearwater County' }, + '27-031': { label: 'Cook County' }, + '27-033': { label: 'Cottonwood County' }, + '27-035': { label: 'Crow Wing County' }, + '27-037': { label: 'Dakota County' }, + '27-039': { label: 'Dodge County' }, + '27-041': { label: 'Douglas County' }, + '27-043': { label: 'Faribault County' }, + '27-045': { label: 'Fillmore County' }, + '27-047': { label: 'Freeborn County' }, + '27-049': { label: 'Goodhue County' }, + '27-051': { label: 'Grant County' }, + '27-053': { label: 'Hennepin County' }, + '27-055': { label: 'Houston County' }, + '27-057': { label: 'Hubbard County' }, + '27-059': { label: 'Isanti County' }, + '27-061': { label: 'Itasca County' }, + '27-063': { label: 'Jackson County' }, + '27-065': { label: 'Kanabec County' }, + '27-067': { label: 'Kandiyohi County' }, + '27-069': { label: 'Kittson County' }, + '27-071': { label: 'Koochiching County' }, + '27-073': { label: 'Lac qui Parle County' }, + '27-075': { label: 'Lake County' }, + '27-077': { label: 'Lake of the Woods County' }, + '27-079': { label: 'Le Sueur County' }, + '27-081': { label: 'Lincoln County' }, + '27-083': { label: 'Lyon County' }, + '27-085': { label: 'McLeod County' }, + '27-087': { label: 'Mahnomen County' }, + '27-089': { label: 'Marshall County' }, + '27-091': { label: 'Martin County' }, + '27-093': { label: 'Meeker County' }, + '27-095': { label: 'Mille Lacs County' }, + '27-097': { label: 'Morrison County' }, + '27-099': { label: 'Mower County' }, + '27-101': { label: 'Murray County' }, + '27-103': { label: 'Nicollet County' }, + '27-105': { label: 'Nobles County' }, + '27-107': { label: 'Norman County' }, + '27-109': { label: 'Olmsted County' }, + '27-111': { label: 'Otter Tail County' }, + '27-113': { label: 'Pennington County' }, + '27-115': { label: 'Pine County' }, + '27-117': { label: 'Pipestone County' }, + '27-119': { label: 'Polk County' }, + '27-121': { label: 'Pope County' }, + '27-123': { label: 'Ramsey County' }, + '27-125': { label: 'Red Lake County' }, + '27-127': { label: 'Redwood County' }, + '27-129': { label: 'Renville County' }, + '27-131': { label: 'Rice County' }, + '27-133': { label: 'Rock County' }, + '27-135': { label: 'Roseau County' }, + '27-137': { label: 'St. Louis County' }, + '27-139': { label: 'Scott County' }, + '27-141': { label: 'Sherburne County' }, + '27-143': { label: 'Sibley County' }, + '27-145': { label: 'Stearns County' }, + '27-147': { label: 'Steele County' }, + '27-149': { label: 'Stevens County' }, + '27-151': { label: 'Swift County' }, + '27-153': { label: 'Todd County' }, + '27-155': { label: 'Traverse County' }, + '27-157': { label: 'Wabasha County' }, + '27-159': { label: 'Wadena County' }, + '27-161': { label: 'Waseca County' }, + '27-163': { label: 'Washington County' }, + '27-165': { label: 'Watonwan County' }, + '27-167': { label: 'Wilkin County' }, + '27-169': { label: 'Winona County' }, + '27-171': { label: 'Wright County' }, + '27-173': { label: 'Yellow Medicine County' }, + '28-000': { label: 'Unspecified' }, + '28-001': { label: 'Adams County' }, + '28-003': { label: 'Alcorn County' }, + '28-005': { label: 'Amite County' }, + '28-007': { label: 'Attala County' }, + '28-009': { label: 'Benton County' }, + '28-011': { label: 'Bolivar County' }, + '28-013': { label: 'Calhoun County' }, + '28-015': { label: 'Carroll County' }, + '28-017': { label: 'Chickasaw County' }, + '28-019': { label: 'Choctaw County' }, + '28-021': { label: 'Claiborne County' }, + '28-023': { label: 'Clarke County' }, + '28-025': { label: 'Clay County' }, + '28-027': { label: 'Coahoma County' }, + '28-029': { label: 'Copiah County' }, + '28-031': { label: 'Covington County' }, + '28-033': { label: 'DeSoto County' }, + '28-035': { label: 'Forrest County' }, + '28-037': { label: 'Franklin County' }, + '28-039': { label: 'George County' }, + '28-041': { label: 'Greene County' }, + '28-043': { label: 'Grenada County' }, + '28-045': { label: 'Hancock County' }, + '28-047': { label: 'Harrison County' }, + '28-049': { label: 'Hinds County' }, + '28-051': { label: 'Holmes County' }, + '28-053': { label: 'Humphreys County' }, + '28-055': { label: 'Issaquena County' }, + '28-057': { label: 'Itawamba County' }, + '28-059': { label: 'Jackson County' }, + '28-061': { label: 'Jasper County' }, + '28-063': { label: 'Jefferson County' }, + '28-065': { label: 'Jefferson Davis County' }, + '28-067': { label: 'Jones County' }, + '28-069': { label: 'Kemper County' }, + '28-071': { label: 'Lafayette County' }, + '28-073': { label: 'Lamar County' }, + '28-075': { label: 'Lauderdale County' }, + '28-077': { label: 'Lawrence County' }, + '28-079': { label: 'Leake County' }, + '28-081': { label: 'Lee County' }, + '28-083': { label: 'Leflore County' }, + '28-085': { label: 'Lincoln County' }, + '28-087': { label: 'Lowndes County' }, + '28-089': { label: 'Madison County' }, + '28-091': { label: 'Marion County' }, + '28-093': { label: 'Marshall County' }, + '28-095': { label: 'Monroe County' }, + '28-097': { label: 'Montgomery County' }, + '28-099': { label: 'Neshoba County' }, + '28-101': { label: 'Newton County' }, + '28-103': { label: 'Noxubee County' }, + '28-105': { label: 'Oktibbeha County' }, + '28-107': { label: 'Panola County' }, + '28-109': { label: 'Pearl River County' }, + '28-111': { label: 'Perry County' }, + '28-113': { label: 'Pike County' }, + '28-115': { label: 'Pontotoc County' }, + '28-117': { label: 'Prentiss County' }, + '28-119': { label: 'Quitman County' }, + '28-121': { label: 'Rankin County' }, + '28-123': { label: 'Scott County' }, + '28-125': { label: 'Sharkey County' }, + '28-127': { label: 'Simpson County' }, + '28-129': { label: 'Smith County' }, + '28-131': { label: 'Stone County' }, + '28-133': { label: 'Sunflower County' }, + '28-135': { label: 'Tallahatchie County' }, + '28-137': { label: 'Tate County' }, + '28-139': { label: 'Tippah County' }, + '28-141': { label: 'Tishomingo County' }, + '28-143': { label: 'Tunica County' }, + '28-145': { label: 'Union County' }, + '28-147': { label: 'Walthall County' }, + '28-149': { label: 'Warren County' }, + '28-151': { label: 'Washington County' }, + '28-153': { label: 'Wayne County' }, + '28-155': { label: 'Webster County' }, + '28-157': { label: 'Wilkinson County' }, + '28-159': { label: 'Winston County' }, + '28-161': { label: 'Yalobusha County' }, + '28-163': { label: 'Yazoo County' }, + '29-000': { label: 'Unspecified' }, + '29-001': { label: 'Adair County' }, + '29-003': { label: 'Andrew County' }, + '29-005': { label: 'Atchison County' }, + '29-007': { label: 'Audrain County' }, + '29-009': { label: 'Barry County' }, + '29-011': { label: 'Barton County' }, + '29-013': { label: 'Bates County' }, + '29-015': { label: 'Benton County' }, + '29-017': { label: 'Bollinger County' }, + '29-019': { label: 'Boone County' }, + '29-021': { label: 'Buchanan County' }, + '29-023': { label: 'Butler County' }, + '29-025': { label: 'Caldwell County' }, + '29-027': { label: 'Callaway County' }, + '29-029': { label: 'Camden County' }, + '29-031': { label: 'Cape Girardeau County' }, + '29-033': { label: 'Carroll County' }, + '29-035': { label: 'Carter County' }, + '29-037': { label: 'Cass County' }, + '29-039': { label: 'Cedar County' }, + '29-041': { label: 'Chariton County' }, + '29-043': { label: 'Christian County' }, + '29-045': { label: 'Clark County' }, + '29-047': { label: 'Clay County' }, + '29-049': { label: 'Clinton County' }, + '29-051': { label: 'Cole County' }, + '29-053': { label: 'Cooper County' }, + '29-055': { label: 'Crawford County' }, + '29-057': { label: 'Dade County' }, + '29-059': { label: 'Dallas County' }, + '29-061': { label: 'Daviess County' }, + '29-063': { label: 'DeKalb County' }, + '29-065': { label: 'Dent County' }, + '29-067': { label: 'Douglas County' }, + '29-069': { label: 'Dunklin County' }, + '29-071': { label: 'Franklin County' }, + '29-073': { label: 'Gasconade County' }, + '29-075': { label: 'Gentry County' }, + '29-077': { label: 'Greene County' }, + '29-079': { label: 'Grundy County' }, + '29-081': { label: 'Harrison County' }, + '29-083': { label: 'Henry County' }, + '29-085': { label: 'Hickory County' }, + '29-087': { label: 'Holt County' }, + '29-089': { label: 'Howard County' }, + '29-091': { label: 'Howell County' }, + '29-093': { label: 'Iron County' }, + '29-095': { label: 'Jackson County' }, + '29-097': { label: 'Jasper County' }, + '29-099': { label: 'Jefferson County' }, + '29-101': { label: 'Johnson County' }, + '29-103': { label: 'Knox County' }, + '29-105': { label: 'Laclede County' }, + '29-107': { label: 'Lafayette County' }, + '29-109': { label: 'Lawrence County' }, + '29-111': { label: 'Lewis County' }, + '29-113': { label: 'Lincoln County' }, + '29-115': { label: 'Linn County' }, + '29-117': { label: 'Livingston County' }, + '29-119': { label: 'McDonald County' }, + '29-121': { label: 'Macon County' }, + '29-123': { label: 'Madison County' }, + '29-125': { label: 'Maries County' }, + '29-127': { label: 'Marion County' }, + '29-129': { label: 'Mercer County' }, + '29-131': { label: 'Miller County' }, + '29-133': { label: 'Mississippi County' }, + '29-135': { label: 'Moniteau County' }, + '29-137': { label: 'Monroe County' }, + '29-139': { label: 'Montgomery County' }, + '29-141': { label: 'Morgan County' }, + '29-143': { label: 'New Madrid County' }, + '29-145': { label: 'Newton County' }, + '29-147': { label: 'Nodaway County' }, + '29-149': { label: 'Oregon County' }, + '29-151': { label: 'Osage County' }, + '29-153': { label: 'Ozark County' }, + '29-155': { label: 'Pemiscot County' }, + '29-157': { label: 'Perry County' }, + '29-159': { label: 'Pettis County' }, + '29-161': { label: 'Phelps County' }, + '29-163': { label: 'Pike County' }, + '29-165': { label: 'Platte County' }, + '29-167': { label: 'Polk County' }, + '29-169': { label: 'Pulaski County' }, + '29-171': { label: 'Putnam County' }, + '29-173': { label: 'Ralls County' }, + '29-175': { label: 'Randolph County' }, + '29-177': { label: 'Ray County' }, + '29-179': { label: 'Reynolds County' }, + '29-181': { label: 'Ripley County' }, + '29-183': { label: 'St. Charles County' }, + '29-185': { label: 'St. Clair County' }, + '29-186': { label: 'Ste. Genevieve County' }, + '29-187': { label: 'St. Francois County' }, + '29-189': { label: 'St. Louis County' }, + '29-195': { label: 'Saline County' }, + '29-197': { label: 'Schuyler County' }, + '29-199': { label: 'Scotland County' }, + '29-201': { label: 'Scott County' }, + '29-203': { label: 'Shannon County' }, + '29-205': { label: 'Shelby County' }, + '29-207': { label: 'Stoddard County' }, + '29-209': { label: 'Stone County' }, + '29-211': { label: 'Sullivan County' }, + '29-213': { label: 'Taney County' }, + '29-215': { label: 'Texas County' }, + '29-217': { label: 'Vernon County' }, + '29-219': { label: 'Warren County' }, + '29-221': { label: 'Washington County' }, + '29-223': { label: 'Wayne County' }, + '29-225': { label: 'Webster County' }, + '29-227': { label: 'Worth County' }, + '29-229': { label: 'Wright County' }, + '29-510': { label: 'St. Louis City' }, + '30-000': { label: 'Unspecified' }, + '30-001': { label: 'Beaverhead County' }, + '30-003': { label: 'Big Horn County' }, + '30-005': { label: 'Blaine County' }, + '30-007': { label: 'Broadwater County' }, + '30-009': { label: 'Carbon County' }, + '30-011': { label: 'Carter County' }, + '30-013': { label: 'Cascade County' }, + '30-015': { label: 'Chouteau County' }, + '30-017': { label: 'Custer County' }, + '30-019': { label: 'Daniels County' }, + '30-021': { label: 'Dawson County' }, + '30-023': { label: 'Deer Lodge County' }, + '30-025': { label: 'Fallon County' }, + '30-027': { label: 'Fergus County' }, + '30-029': { label: 'Flathead County' }, + '30-031': { label: 'Gallatin County' }, + '30-033': { label: 'Garfield County' }, + '30-035': { label: 'Glacier County' }, + '30-037': { label: 'Golden Valley County' }, + '30-039': { label: 'Granite County' }, + '30-041': { label: 'Hill County' }, + '30-043': { label: 'Jefferson County' }, + '30-045': { label: 'Judith Basin County' }, + '30-047': { label: 'Lake County' }, + '30-049': { label: 'Lewis and Clark County' }, + '30-051': { label: 'Liberty County' }, + '30-053': { label: 'Lincoln County' }, + '30-055': { label: 'McCone County' }, + '30-057': { label: 'Madison County' }, + '30-059': { label: 'Meagher County' }, + '30-061': { label: 'Mineral County' }, + '30-063': { label: 'Missoula County' }, + '30-065': { label: 'Musselshell County' }, + '30-067': { label: 'Park County' }, + '30-069': { label: 'Petroleum County' }, + '30-071': { label: 'Phillips County' }, + '30-073': { label: 'Pondera County' }, + '30-075': { label: 'Powder River County' }, + '30-077': { label: 'Powell County' }, + '30-079': { label: 'Prairie County' }, + '30-081': { label: 'Ravalli County' }, + '30-083': { label: 'Richland County' }, + '30-085': { label: 'Roosevelt County' }, + '30-087': { label: 'Rosebud County' }, + '30-089': { label: 'Sanders County' }, + '30-091': { label: 'Sheridan County' }, + '30-093': { label: 'Silver Bow County' }, + '30-095': { label: 'Stillwater County' }, + '30-097': { label: 'Sweet Grass County' }, + '30-099': { label: 'Teton County' }, + '30-101': { label: 'Toole County' }, + '30-103': { label: 'Treasure County' }, + '30-105': { label: 'Valley County' }, + '30-107': { label: 'Wheatland County' }, + '30-109': { label: 'Wibaux County' }, + '30-111': { label: 'Yellowstone County' }, + '30-113': { label: 'Yellowstone National Park - Part' }, + '31-000': { label: 'Unspecified' }, + '31-001': { label: 'Adams County' }, + '31-003': { label: 'Antelope County' }, + '31-005': { label: 'Arthur County' }, + '31-007': { label: 'Banner County' }, + '31-009': { label: 'Blaine County' }, + '31-011': { label: 'Boone County' }, + '31-013': { label: 'Box Butte County' }, + '31-015': { label: 'Boyd County' }, + '31-017': { label: 'Brown County' }, + '31-019': { label: 'Buffalo County' }, + '31-021': { label: 'Burt County' }, + '31-023': { label: 'Butler County' }, + '31-025': { label: 'Cass County' }, + '31-027': { label: 'Cedar County' }, + '31-029': { label: 'Chase County' }, + '31-031': { label: 'Cherry County' }, + '31-033': { label: 'Cheyenne County' }, + '31-035': { label: 'Clay County' }, + '31-037': { label: 'Colfax County' }, + '31-039': { label: 'Cuming County' }, + '31-041': { label: 'Custer County' }, + '31-043': { label: 'Dakota County' }, + '31-045': { label: 'Dawes County' }, + '31-047': { label: 'Dawson County' }, + '31-049': { label: 'Deuel County' }, + '31-051': { label: 'Dixon County' }, + '31-053': { label: 'Dodge County' }, + '31-055': { label: 'Douglas County' }, + '31-057': { label: 'Dundy County' }, + '31-059': { label: 'Fillmore County' }, + '31-061': { label: 'Franklin County' }, + '31-063': { label: 'Frontier County' }, + '31-065': { label: 'Furnas County' }, + '31-067': { label: 'Gage County' }, + '31-069': { label: 'Garden County' }, + '31-071': { label: 'Garfield County' }, + '31-073': { label: 'Gosper County' }, + '31-075': { label: 'Grant County' }, + '31-077': { label: 'Greeley County' }, + '31-079': { label: 'Hall County' }, + '31-081': { label: 'Hamilton County' }, + '31-083': { label: 'Harlan County' }, + '31-085': { label: 'Hayes County' }, + '31-087': { label: 'Hitchcock County' }, + '31-089': { label: 'Holt County' }, + '31-091': { label: 'Hooker County' }, + '31-093': { label: 'Howard County' }, + '31-095': { label: 'Jefferson County' }, + '31-097': { label: 'Johnson County' }, + '31-099': { label: 'Kearney County' }, + '31-101': { label: 'Keith County' }, + '31-103': { label: 'Keya Paha County' }, + '31-105': { label: 'Kimball County' }, + '31-107': { label: 'Knox County' }, + '31-109': { label: 'Lancaster County' }, + '31-111': { label: 'Lincoln County' }, + '31-113': { label: 'Logan County' }, + '31-115': { label: 'Loup County' }, + '31-117': { label: 'McPherson County' }, + '31-119': { label: 'Madison County' }, + '31-121': { label: 'Merrick County' }, + '31-123': { label: 'Morrill County' }, + '31-125': { label: 'Nance County' }, + '31-127': { label: 'Nemaha County' }, + '31-129': { label: 'Nuckolls County' }, + '31-131': { label: 'Otoe County' }, + '31-133': { label: 'Pawnee County' }, + '31-135': { label: 'Perkins County' }, + '31-137': { label: 'Phelps County' }, + '31-139': { label: 'Pierce County' }, + '31-141': { label: 'Platte County' }, + '31-143': { label: 'Polk County' }, + '31-145': { label: 'Red Willow County' }, + '31-147': { label: 'Richardson County' }, + '31-149': { label: 'Rock County' }, + '31-151': { label: 'Saline County' }, + '31-153': { label: 'Sarpy County' }, + '31-155': { label: 'Saunders County' }, + '31-157': { label: 'Scotts Bluff County' }, + '31-159': { label: 'Seward County' }, + '31-161': { label: 'Sheridan County' }, + '31-163': { label: 'Sherman County' }, + '31-165': { label: 'Sioux County' }, + '31-167': { label: 'Stanton County' }, + '31-169': { label: 'Thayer County' }, + '31-171': { label: 'Thomas County' }, + '31-173': { label: 'Thurston County' }, + '31-175': { label: 'Valley County' }, + '31-177': { label: 'Washington County' }, + '31-179': { label: 'Wayne County' }, + '31-181': { label: 'Webster County' }, + '31-183': { label: 'Wheeler County' }, + '31-185': { label: 'York County' }, + '32-000': { label: 'Unspecified' }, + '32-001': { label: 'Churchill County' }, + '32-003': { label: 'Clark County' }, + '32-005': { label: 'Douglas County' }, + '32-007': { label: 'Elko County' }, + '32-009': { label: 'Esmeralda County' }, + '32-011': { label: 'Eureka County' }, + '32-013': { label: 'Humboldt County' }, + '32-015': { label: 'Lander County' }, + '32-017': { label: 'Lincoln County' }, + '32-019': { label: 'Lyon County' }, + '32-021': { label: 'Mineral County' }, + '32-023': { label: 'Nye County' }, + '32-027': { label: 'Pershing County' }, + '32-029': { label: 'Storey County' }, + '32-031': { label: 'Washoe County' }, + '32-033': { label: 'White Pine County' }, + '32-510': { label: 'Carson City' }, + '33-000': { label: 'Unspecified' }, + '33-001': { label: 'Belknap County' }, + '33-003': { label: 'Carroll County' }, + '33-005': { label: 'Cheshire County' }, + '33-007': { label: 'Coos County' }, + '33-009': { label: 'Grafton County' }, + '33-011': { label: 'Hillsborough County' }, + '33-013': { label: 'Merrimack County' }, + '33-015': { label: 'Rockingham County' }, + '33-017': { label: 'Strafford County' }, + '33-019': { label: 'Sullivan County' }, + '34-000': { label: 'Unspecified' }, + '34-001': { label: 'Atlantic County' }, + '34-003': { label: 'Bergen County' }, + '34-005': { label: 'Burlington County' }, + '34-007': { label: 'Camden County' }, + '34-009': { label: 'Cape May County' }, + '34-011': { label: 'Cumberland County' }, + '34-013': { label: 'Essex County' }, + '34-015': { label: 'Gloucester County' }, + '34-017': { label: 'Hudson County' }, + '34-019': { label: 'Hunterdon County' }, + '34-021': { label: 'Mercer County' }, + '34-023': { label: 'Middlesex County' }, + '34-025': { label: 'Monmouth County' }, + '34-027': { label: 'Morris County' }, + '34-029': { label: 'Ocean County' }, + '34-031': { label: 'Passaic County' }, + '34-033': { label: 'Salem County' }, + '34-035': { label: 'Somerset County' }, + '34-037': { label: 'Sussex County' }, + '34-039': { label: 'Union County' }, + '34-041': { label: 'Warren County' }, + '35-000': { label: 'Unspecified' }, + '35-001': { label: 'Bernalillo County' }, + '35-003': { label: 'Catron County' }, + '35-005': { label: 'Chaves County' }, + '35-006': { label: 'Cibola County' }, + '35-007': { label: 'Colfax County' }, + '35-009': { label: 'Curry County' }, + '35-011': { label: 'De Baca County' }, + '35-013': { label: 'Dona Ana County' }, + '35-015': { label: 'Eddy County' }, + '35-017': { label: 'Grant County' }, + '35-019': { label: 'Guadalupe County' }, + '35-021': { label: 'Harding County' }, + '35-023': { label: 'Hidalgo County' }, + '35-025': { label: 'Lea County' }, + '35-027': { label: 'Lincoln County' }, + '35-028': { label: 'Los Alamos County' }, + '35-029': { label: 'Luna County' }, + '35-031': { label: 'McKinley County' }, + '35-033': { label: 'Mora County' }, + '35-035': { label: 'Otero County' }, + '35-037': { label: 'Quay County' }, + '35-039': { label: 'Rio Arriba County' }, + '35-041': { label: 'Roosevelt County' }, + '35-043': { label: 'Sandoval County' }, + '35-045': { label: 'San Juan County' }, + '35-047': { label: 'San Miguel County' }, + '35-049': { label: 'Santa Fe County' }, + '35-051': { label: 'Sierra County' }, + '35-053': { label: 'Socorro County' }, + '35-055': { label: 'Taos County' }, + '35-057': { label: 'Torrance County' }, + '35-059': { label: 'Union County' }, + '35-061': { label: 'Valencia County' }, + '36-000': { label: 'Unspecified' }, + '36-001': { label: 'Albany County' }, + '36-003': { label: 'Allegany County' }, + '36-005': { label: 'Bronx County' }, + '36-007': { label: 'Broome County' }, + '36-009': { label: 'Cattaraugus County' }, + '36-011': { label: 'Cayuga County' }, + '36-013': { label: 'Chautauqua County' }, + '36-015': { label: 'Chemung County' }, + '36-017': { label: 'Chenango County' }, + '36-019': { label: 'Clinton County' }, + '36-021': { label: 'Columbia County' }, + '36-023': { label: 'Cortland County' }, + '36-025': { label: 'Delaware County' }, + '36-027': { label: 'Dutchess County' }, + '36-029': { label: 'Erie County' }, + '36-031': { label: 'Essex County' }, + '36-033': { label: 'Franklin County' }, + '36-035': { label: 'Fulton County' }, + '36-037': { label: 'Genesee County' }, + '36-039': { label: 'Greene County' }, + '36-041': { label: 'Hamilton County' }, + '36-043': { label: 'Herkimer County' }, + '36-045': { label: 'Jefferson County' }, + '36-047': { label: 'Kings County' }, + '36-049': { label: 'Lewis County' }, + '36-051': { label: 'Livingston County' }, + '36-053': { label: 'Madison County' }, + '36-055': { label: 'Monroe County' }, + '36-057': { label: 'Montgomery County' }, + '36-059': { label: 'Nassau County' }, + '36-061': { label: 'New York County' }, + '36-063': { label: 'Niagara County' }, + '36-065': { label: 'Oneida County' }, + '36-067': { label: 'Onondaga County' }, + '36-069': { label: 'Ontario County' }, + '36-071': { label: 'Orange County' }, + '36-073': { label: 'Orleans County' }, + '36-075': { label: 'Oswego County' }, + '36-077': { label: 'Otsego County' }, + '36-079': { label: 'Putnam County' }, + '36-081': { label: 'Queens County' }, + '36-083': { label: 'Rensselaer County' }, + '36-085': { label: 'Richmond County' }, + '36-087': { label: 'Rockland County' }, + '36-089': { label: 'St. Lawrence County' }, + '36-091': { label: 'Saratoga County' }, + '36-093': { label: 'Schenectady County' }, + '36-095': { label: 'Schoharie County' }, + '36-097': { label: 'Schuyler County' }, + '36-099': { label: 'Seneca County' }, + '36-101': { label: 'Steuben County' }, + '36-103': { label: 'Suffolk County' }, + '36-105': { label: 'Sullivan County' }, + '36-107': { label: 'Tioga County' }, + '36-109': { label: 'Tompkins County' }, + '36-111': { label: 'Ulster County' }, + '36-113': { label: 'Warren County' }, + '36-115': { label: 'Washington County' }, + '36-117': { label: 'Wayne County' }, + '36-119': { label: 'Westchester County' }, + '36-121': { label: 'Wyoming County' }, + '36-123': { label: 'Yates County' }, + '37-000': { label: 'Unspecified' }, + '37-001': { label: 'Alamance County' }, + '37-003': { label: 'Alexander County' }, + '37-005': { label: 'Alleghany County' }, + '37-007': { label: 'Anson County' }, + '37-009': { label: 'Ashe County' }, + '37-011': { label: 'Avery County' }, + '37-013': { label: 'Beaufort County' }, + '37-015': { label: 'Bertie County' }, + '37-017': { label: 'Bladen County' }, + '37-019': { label: 'Brunswick County' }, + '37-021': { label: 'Buncombe County' }, + '37-023': { label: 'Burke County' }, + '37-025': { label: 'Cabarrus County' }, + '37-027': { label: 'Caldwell County' }, + '37-029': { label: 'Camden County' }, + '37-031': { label: 'Carteret County' }, + '37-033': { label: 'Caswell County' }, + '37-035': { label: 'Catawba County' }, + '37-037': { label: 'Chatham County' }, + '37-039': { label: 'Cherokee County' }, + '37-041': { label: 'Chowan County' }, + '37-043': { label: 'Clay County' }, + '37-045': { label: 'Cleveland County' }, + '37-047': { label: 'Columbus County' }, + '37-049': { label: 'Craven County' }, + '37-051': { label: 'Cumberland County' }, + '37-053': { label: 'Currituck County' }, + '37-055': { label: 'Dare County' }, + '37-057': { label: 'Davidson County' }, + '37-059': { label: 'Davie County' }, + '37-061': { label: 'Duplin County' }, + '37-063': { label: 'Durham County' }, + '37-065': { label: 'Edgecombe County' }, + '37-067': { label: 'Forsyth County' }, + '37-069': { label: 'Franklin County' }, + '37-071': { label: 'Gaston County' }, + '37-073': { label: 'Gates County' }, + '37-075': { label: 'Graham County' }, + '37-077': { label: 'Granville County' }, + '37-079': { label: 'Greene County' }, + '37-081': { label: 'Guilford County' }, + '37-083': { label: 'Halifax County' }, + '37-085': { label: 'Harnett County' }, + '37-087': { label: 'Haywood County' }, + '37-089': { label: 'Henderson County' }, + '37-091': { label: 'Hertford County' }, + '37-093': { label: 'Hoke County' }, + '37-095': { label: 'Hyde County' }, + '37-097': { label: 'Iredell County' }, + '37-099': { label: 'Jackson County' }, + '37-101': { label: 'Johnston County' }, + '37-103': { label: 'Jones County' }, + '37-105': { label: 'Lee County' }, + '37-107': { label: 'Lenoir County' }, + '37-109': { label: 'Lincoln County' }, + '37-111': { label: 'McDowell County' }, + '37-113': { label: 'Macon County' }, + '37-115': { label: 'Madison County' }, + '37-117': { label: 'Martin County' }, + '37-119': { label: 'Mecklenburg County' }, + '37-121': { label: 'Mitchell County' }, + '37-123': { label: 'Montgomery County' }, + '37-125': { label: 'Moore County' }, + '37-127': { label: 'Nash County' }, + '37-129': { label: 'New Hanover County' }, + '37-131': { label: 'Northampton County' }, + '37-133': { label: 'Onslow County' }, + '37-135': { label: 'Orange County' }, + '37-137': { label: 'Pamlico County' }, + '37-139': { label: 'Pasquotank County' }, + '37-141': { label: 'Pender County' }, + '37-143': { label: 'Perquimans County' }, + '37-145': { label: 'Person County' }, + '37-147': { label: 'Pitt County' }, + '37-149': { label: 'Polk County' }, + '37-151': { label: 'Randolph County' }, + '37-153': { label: 'Richmond County' }, + '37-155': { label: 'Robeson County' }, + '37-157': { label: 'Rockingham County' }, + '37-159': { label: 'Rowan County' }, + '37-161': { label: 'Rutherford County' }, + '37-163': { label: 'Sampson County' }, + '37-165': { label: 'Scotland County' }, + '37-167': { label: 'Stanly County' }, + '37-169': { label: 'Stokes County' }, + '37-171': { label: 'Surry County' }, + '37-173': { label: 'Swain County' }, + '37-175': { label: 'Transylvania County' }, + '37-177': { label: 'Tyrrell County' }, + '37-179': { label: 'Union County' }, + '37-181': { label: 'Vance County' }, + '37-183': { label: 'Wake County' }, + '37-185': { label: 'Warren County' }, + '37-187': { label: 'Washington County' }, + '37-189': { label: 'Watauga County' }, + '37-191': { label: 'Wayne County' }, + '37-193': { label: 'Wilkes County' }, + '37-195': { label: 'Wilson County' }, + '37-197': { label: 'Yadkin County' }, + '37-199': { label: 'Yancey County' }, + '38-000': { label: 'Unspecified' }, + '38-001': { label: 'Adams County' }, + '38-003': { label: 'Barnes County' }, + '38-005': { label: 'Benson County' }, + '38-007': { label: 'Billings County' }, + '38-009': { label: 'Bottineau County' }, + '38-011': { label: 'Bowman County' }, + '38-013': { label: 'Burke County' }, + '38-015': { label: 'Burleigh County' }, + '38-017': { label: 'Cass County' }, + '38-019': { label: 'Cavalier County' }, + '38-021': { label: 'Dickey County' }, + '38-023': { label: 'Divide County' }, + '38-025': { label: 'Dunn County' }, + '38-027': { label: 'Eddy County' }, + '38-029': { label: 'Emmons County' }, + '38-031': { label: 'Foster County' }, + '38-033': { label: 'Golden Valley County' }, + '38-035': { label: 'Grand Forks County' }, + '38-037': { label: 'Grant County' }, + '38-039': { label: 'Griggs County' }, + '38-041': { label: 'Hettinger County' }, + '38-043': { label: 'Kidder County' }, + '38-045': { label: 'LaMoure County' }, + '38-047': { label: 'Logan County' }, + '38-049': { label: 'McHenry County' }, + '38-051': { label: 'McIntosh County' }, + '38-053': { label: 'McKenzie County' }, + '38-055': { label: 'McLean County' }, + '38-057': { label: 'Mercer County' }, + '38-059': { label: 'Morton County' }, + '38-061': { label: 'Mountrail County' }, + '38-063': { label: 'Nelson County' }, + '38-065': { label: 'Oliver County' }, + '38-067': { label: 'Pembina County' }, + '38-069': { label: 'Pierce County' }, + '38-071': { label: 'Ramsey County' }, + '38-073': { label: 'Ransom County' }, + '38-075': { label: 'Renville County' }, + '38-077': { label: 'Richland County' }, + '38-079': { label: 'Rolette County' }, + '38-081': { label: 'Sargent County' }, + '38-083': { label: 'Sheridan County' }, + '38-085': { label: 'Sioux County' }, + '38-087': { label: 'Slope County' }, + '38-089': { label: 'Stark County' }, + '38-091': { label: 'Steele County' }, + '38-093': { label: 'Stutsman County' }, + '38-095': { label: 'Towner County' }, + '38-097': { label: 'Traill County' }, + '38-099': { label: 'Walsh County' }, + '38-101': { label: 'Ward County' }, + '38-103': { label: 'Wells County' }, + '38-105': { label: 'Williams County' }, + '39-000': { label: 'Unspecified' }, + '39-001': { label: 'Adams County' }, + '39-003': { label: 'Allen County' }, + '39-005': { label: 'Ashland County' }, + '39-007': { label: 'Ashtabula County' }, + '39-009': { label: 'Athens County' }, + '39-011': { label: 'Auglaize County' }, + '39-013': { label: 'Belmont County' }, + '39-015': { label: 'Brown County' }, + '39-017': { label: 'Butler County' }, + '39-019': { label: 'Carroll County' }, + '39-021': { label: 'Champaign County' }, + '39-023': { label: 'Clark County' }, + '39-025': { label: 'Clermont County' }, + '39-027': { label: 'Clinton County' }, + '39-029': { label: 'Columbiana County' }, + '39-031': { label: 'Coshocton County' }, + '39-033': { label: 'Crawford County' }, + '39-035': { label: 'Cuyahoga County' }, + '39-037': { label: 'Darke County' }, + '39-039': { label: 'Defiance County' }, + '39-041': { label: 'Delaware County' }, + '39-043': { label: 'Erie County' }, + '39-045': { label: 'Fairfield County' }, + '39-047': { label: 'Fayette County' }, + '39-049': { label: 'Franklin County' }, + '39-051': { label: 'Fulton County' }, + '39-053': { label: 'Gallia County' }, + '39-055': { label: 'Geauga County' }, + '39-057': { label: 'Greene County' }, + '39-059': { label: 'Guernsey County' }, + '39-061': { label: 'Hamilton County' }, + '39-063': { label: 'Hancock County' }, + '39-065': { label: 'Hardin County' }, + '39-067': { label: 'Harrison County' }, + '39-069': { label: 'Henry County' }, + '39-071': { label: 'Highland County' }, + '39-073': { label: 'Hocking County' }, + '39-075': { label: 'Holmes County' }, + '39-077': { label: 'Huron County' }, + '39-079': { label: 'Jackson County' }, + '39-081': { label: 'Jefferson County' }, + '39-083': { label: 'Knox County' }, + '39-085': { label: 'Lake County' }, + '39-087': { label: 'Lawrence County' }, + '39-089': { label: 'Licking County' }, + '39-091': { label: 'Logan County' }, + '39-093': { label: 'Lorain County' }, + '39-095': { label: 'Lucas County' }, + '39-097': { label: 'Madison County' }, + '39-099': { label: 'Mahoning County' }, + '39-101': { label: 'Marion County' }, + '39-103': { label: 'Medina County' }, + '39-105': { label: 'Meigs County' }, + '39-107': { label: 'Mercer County' }, + '39-109': { label: 'Miami County' }, + '39-111': { label: 'Monroe County' }, + '39-113': { label: 'Montgomery County' }, + '39-115': { label: 'Morgan County' }, + '39-117': { label: 'Morrow County' }, + '39-119': { label: 'Muskingum County' }, + '39-121': { label: 'Noble County' }, + '39-123': { label: 'Ottawa County' }, + '39-125': { label: 'Paulding County' }, + '39-127': { label: 'Perry County' }, + '39-129': { label: 'Pickaway County' }, + '39-131': { label: 'Pike County' }, + '39-133': { label: 'Portage County' }, + '39-135': { label: 'Preble County' }, + '39-137': { label: 'Putnam County' }, + '39-139': { label: 'Richland County' }, + '39-141': { label: 'Ross County' }, + '39-143': { label: 'Sandusky County' }, + '39-145': { label: 'Scioto County' }, + '39-147': { label: 'Seneca County' }, + '39-149': { label: 'Shelby County' }, + '39-151': { label: 'Stark County' }, + '39-153': { label: 'Summit County' }, + '39-155': { label: 'Trumbull County' }, + '39-157': { label: 'Tuscarawas County' }, + '39-159': { label: 'Union County' }, + '39-161': { label: 'Van Wert County' }, + '39-163': { label: 'Vinton County' }, + '39-165': { label: 'Warren County' }, + '39-167': { label: 'Washington County' }, + '39-169': { label: 'Wayne County' }, + '39-171': { label: 'Williams County' }, + '39-173': { label: 'Wood County' }, + '39-175': { label: 'Wyandot County' }, + '40-000': { label: 'Unspecified' }, + '40-001': { label: 'Adair County' }, + '40-003': { label: 'Alfalfa County' }, + '40-005': { label: 'Atoka County' }, + '40-007': { label: 'Beaver County' }, + '40-009': { label: 'Beckham County' }, + '40-011': { label: 'Blaine County' }, + '40-013': { label: 'Bryan County' }, + '40-015': { label: 'Caddo County' }, + '40-017': { label: 'Canadian County' }, + '40-019': { label: 'Carter County' }, + '40-021': { label: 'Cherokee County' }, + '40-023': { label: 'Choctaw County' }, + '40-025': { label: 'Cimarron County' }, + '40-027': { label: 'Cleveland County' }, + '40-029': { label: 'Coal County' }, + '40-031': { label: 'Comanche County' }, + '40-033': { label: 'Cotton County' }, + '40-035': { label: 'Craig County' }, + '40-037': { label: 'Creek County' }, + '40-039': { label: 'Custer County' }, + '40-041': { label: 'Delaware County' }, + '40-043': { label: 'Dewey County' }, + '40-045': { label: 'Ellis County' }, + '40-047': { label: 'Garfield County' }, + '40-049': { label: 'Garvin County' }, + '40-051': { label: 'Grady County' }, + '40-053': { label: 'Grant County' }, + '40-055': { label: 'Greer County' }, + '40-057': { label: 'Harmon County' }, + '40-059': { label: 'Harper County' }, + '40-061': { label: 'Haskell County' }, + '40-063': { label: 'Hughes County' }, + '40-065': { label: 'Jackson County' }, + '40-067': { label: 'Jefferson County' }, + '40-069': { label: 'Johnston County' }, + '40-071': { label: 'Kay County' }, + '40-073': { label: 'Kingfisher County' }, + '40-075': { label: 'Kiowa County' }, + '40-077': { label: 'Latimer County' }, + '40-079': { label: 'Le Flore County' }, + '40-081': { label: 'Lincoln County' }, + '40-083': { label: 'Logan County' }, + '40-085': { label: 'Love County' }, + '40-087': { label: 'McClain County' }, + '40-089': { label: 'McCurtain County' }, + '40-091': { label: 'McIntosh County' }, + '40-093': { label: 'Major County' }, + '40-095': { label: 'Marshall County' }, + '40-097': { label: 'Mayes County' }, + '40-099': { label: 'Murray County' }, + '40-101': { label: 'Muskogee County' }, + '40-103': { label: 'Noble County' }, + '40-105': { label: 'Nowata County' }, + '40-107': { label: 'Okfuskee County' }, + '40-109': { label: 'Oklahoma County' }, + '40-111': { label: 'Okmulgee County' }, + '40-113': { label: 'Osage County' }, + '40-115': { label: 'Ottawa County' }, + '40-117': { label: 'Pawnee County' }, + '40-119': { label: 'Payne County' }, + '40-121': { label: 'Pittsburg County' }, + '40-123': { label: 'Pontotoc County' }, + '40-125': { label: 'Pottawatomie County' }, + '40-127': { label: 'Pushmataha County' }, + '40-129': { label: 'Roger Mills County' }, + '40-131': { label: 'Rogers County' }, + '40-133': { label: 'Seminole County' }, + '40-135': { label: 'Sequoyah County' }, + '40-137': { label: 'Stephens County' }, + '40-139': { label: 'Texas County' }, + '40-141': { label: 'Tillman County' }, + '40-143': { label: 'Tulsa County' }, + '40-145': { label: 'Wagoner County' }, + '40-147': { label: 'Washington County' }, + '40-149': { label: 'Washita County' }, + '40-151': { label: 'Woods County' }, + '40-153': { label: 'Woodward County' }, + '41-000': { label: 'Unspecified' }, + '41-001': { label: 'Baker County' }, + '41-003': { label: 'Benton County' }, + '41-005': { label: 'Clackamas County' }, + '41-007': { label: 'Clatsop County' }, + '41-009': { label: 'Columbia County' }, + '41-011': { label: 'Coos County' }, + '41-013': { label: 'Crook County' }, + '41-015': { label: 'Curry County' }, + '41-017': { label: 'Deschutes County' }, + '41-019': { label: 'Douglas County' }, + '41-021': { label: 'Gilliam County' }, + '41-023': { label: 'Grant County' }, + '41-025': { label: 'Harney County' }, + '41-027': { label: 'Hood River County' }, + '41-029': { label: 'Jackson County' }, + '41-031': { label: 'Jefferson County' }, + '41-033': { label: 'Josephine County' }, + '41-035': { label: 'Klamath County' }, + '41-037': { label: 'Lake County' }, + '41-039': { label: 'Lane County' }, + '41-041': { label: 'Lincoln County' }, + '41-043': { label: 'Linn County' }, + '41-045': { label: 'Malheur County' }, + '41-047': { label: 'Marion County' }, + '41-049': { label: 'Morrow County' }, + '41-051': { label: 'Multnomah County' }, + '41-053': { label: 'Polk County' }, + '41-055': { label: 'Sherman County' }, + '41-057': { label: 'Tillamook County' }, + '41-059': { label: 'Umatilla County' }, + '41-061': { label: 'Union County' }, + '41-063': { label: 'Wallowa County' }, + '41-065': { label: 'Wasco County' }, + '41-067': { label: 'Washington County' }, + '41-069': { label: 'Wheeler County' }, + '41-071': { label: 'Yamhill County' }, + '42-000': { label: 'Unspecified' }, + '42-001': { label: 'Adams County' }, + '42-003': { label: 'Allegheny County' }, + '42-005': { label: 'Armstrong County' }, + '42-007': { label: 'Beaver County' }, + '42-009': { label: 'Bedford County' }, + '42-011': { label: 'Berks County' }, + '42-013': { label: 'Blair County' }, + '42-015': { label: 'Bradford County' }, + '42-017': { label: 'Bucks County' }, + '42-019': { label: 'Butler County' }, + '42-021': { label: 'Cambria County' }, + '42-023': { label: 'Cameron County' }, + '42-025': { label: 'Carbon County' }, + '42-027': { label: 'Centre County' }, + '42-029': { label: 'Chester County' }, + '42-031': { label: 'Clarion County' }, + '42-033': { label: 'Clearfield County' }, + '42-035': { label: 'Clinton County' }, + '42-037': { label: 'Columbia County' }, + '42-039': { label: 'Crawford County' }, + '42-041': { label: 'Cumberland County' }, + '42-043': { label: 'Dauphin County' }, + '42-045': { label: 'Delaware County' }, + '42-047': { label: 'Elk County' }, + '42-049': { label: 'Erie County' }, + '42-051': { label: 'Fayette County' }, + '42-053': { label: 'Forest County' }, + '42-055': { label: 'Franklin County' }, + '42-057': { label: 'Fulton County' }, + '42-059': { label: 'Greene County' }, + '42-061': { label: 'Huntingdon County' }, + '42-063': { label: 'Indiana County' }, + '42-065': { label: 'Jefferson County' }, + '42-067': { label: 'Juniata County' }, + '42-069': { label: 'Lackawanna County' }, + '42-071': { label: 'Lancaster County' }, + '42-073': { label: 'Lawrence County' }, + '42-075': { label: 'Lebanon County' }, + '42-077': { label: 'Lehigh County' }, + '42-079': { label: 'Luzerne County' }, + '42-081': { label: 'Lycoming County' }, + '42-083': { label: 'McKean County' }, + '42-085': { label: 'Mercer County' }, + '42-087': { label: 'Mifflin County' }, + '42-089': { label: 'Monroe County' }, + '42-091': { label: 'Montgomery County' }, + '42-093': { label: 'Montour County' }, + '42-095': { label: 'Northampton County' }, + '42-097': { label: 'Northumberland County' }, + '42-099': { label: 'Perry County' }, + '42-101': { label: 'Philadelphia County' }, + '42-103': { label: 'Pike County' }, + '42-105': { label: 'Potter County' }, + '42-107': { label: 'Schuylkill County' }, + '42-109': { label: 'Snyder County' }, + '42-111': { label: 'Somerset County' }, + '42-113': { label: 'Sullivan County' }, + '42-115': { label: 'Susquehanna County' }, + '42-117': { label: 'Tioga County' }, + '42-119': { label: 'Union County' }, + '42-121': { label: 'Venango County' }, + '42-123': { label: 'Warren County' }, + '42-125': { label: 'Washington County' }, + '42-127': { label: 'Wayne County' }, + '42-129': { label: 'Westmoreland County' }, + '42-131': { label: 'Wyoming County' }, + '42-133': { label: 'York County' }, + '44-000': { label: 'Unspecified' }, + '44-001': { label: 'Bristol County' }, + '44-003': { label: 'Kent County' }, + '44-005': { label: 'Newport County' }, + '44-007': { label: 'Providence County' }, + '44-009': { label: 'Washington County' }, + '45-000': { label: 'Unspecified' }, + '45-001': { label: 'Abbeville County' }, + '45-003': { label: 'Aiken County' }, + '45-005': { label: 'Allendale County' }, + '45-007': { label: 'Anderson County' }, + '45-009': { label: 'Bamberg County' }, + '45-011': { label: 'Barnwell County' }, + '45-013': { label: 'Beaufort County' }, + '45-015': { label: 'Berkeley County' }, + '45-017': { label: 'Calhoun County' }, + '45-019': { label: 'Charleston County' }, + '45-021': { label: 'Cherokee County' }, + '45-023': { label: 'Chester County' }, + '45-025': { label: 'Chesterfield County' }, + '45-027': { label: 'Clarendon County' }, + '45-029': { label: 'Colleton County' }, + '45-031': { label: 'Darlington County' }, + '45-033': { label: 'Dillon County' }, + '45-035': { label: 'Dorchester County' }, + '45-037': { label: 'Edgefield County' }, + '45-039': { label: 'Fairfield County' }, + '45-041': { label: 'Florence County' }, + '45-043': { label: 'Georgetown County' }, + '45-045': { label: 'Greenville County' }, + '45-047': { label: 'Greenwood County' }, + '45-049': { label: 'Hampton County' }, + '45-051': { label: 'Horry County' }, + '45-053': { label: 'Jasper County' }, + '45-055': { label: 'Kershaw County' }, + '45-057': { label: 'Lancaster County' }, + '45-059': { label: 'Laurens County' }, + '45-061': { label: 'Lee County' }, + '45-063': { label: 'Lexington County' }, + '45-065': { label: 'McCormick County' }, + '45-067': { label: 'Marion County' }, + '45-069': { label: 'Marlboro County' }, + '45-071': { label: 'Newberry County' }, + '45-073': { label: 'Oconee County' }, + '45-075': { label: 'Orangeburg County' }, + '45-077': { label: 'Pickens County' }, + '45-079': { label: 'Richland County' }, + '45-081': { label: 'Saluda County' }, + '45-083': { label: 'Spartanburg County' }, + '45-085': { label: 'Sumter County' }, + '45-087': { label: 'Union County' }, + '45-089': { label: 'Williamsburg County' }, + '45-091': { label: 'York County' }, + '46-000': { label: 'Unspecified' }, + '46-003': { label: 'Aurora County' }, + '46-005': { label: 'Beadle County' }, + '46-007': { label: 'Bennett County' }, + '46-009': { label: 'Bon Homme County' }, + '46-011': { label: 'Brookings County' }, + '46-013': { label: 'Brown County' }, + '46-015': { label: 'Brule County' }, + '46-017': { label: 'Buffalo County' }, + '46-019': { label: 'Butte County' }, + '46-021': { label: 'Campbell County' }, + '46-023': { label: 'Charles Mix County' }, + '46-025': { label: 'Clark County' }, + '46-027': { label: 'Clay County' }, + '46-029': { label: 'Codington County' }, + '46-031': { label: 'Corson County' }, + '46-033': { label: 'Custer County' }, + '46-035': { label: 'Davison County' }, + '46-037': { label: 'Day County' }, + '46-039': { label: 'Deuel County' }, + '46-041': { label: 'Dewey County' }, + '46-043': { label: 'Douglas County' }, + '46-045': { label: 'Edmunds County' }, + '46-047': { label: 'Fall River County' }, + '46-049': { label: 'Faulk County' }, + '46-051': { label: 'Grant County' }, + '46-053': { label: 'Gregory County' }, + '46-055': { label: 'Haakon County' }, + '46-057': { label: 'Hamlin County' }, + '46-059': { label: 'Hand County' }, + '46-061': { label: 'Hanson County' }, + '46-063': { label: 'Harding County' }, + '46-065': { label: 'Hughes County' }, + '46-067': { label: 'Hutchinson County' }, + '46-069': { label: 'Hyde County' }, + '46-071': { label: 'Jackson County' }, + '46-073': { label: 'Jerauld County' }, + '46-075': { label: 'Jones County' }, + '46-077': { label: 'Kingsbury County' }, + '46-079': { label: 'Lake County' }, + '46-081': { label: 'Lawrence County' }, + '46-083': { label: 'Lincoln County' }, + '46-085': { label: 'Lyman County' }, + '46-087': { label: 'McCook County' }, + '46-089': { label: 'McPherson County' }, + '46-091': { label: 'Marshall County' }, + '46-093': { label: 'Meade County' }, + '46-095': { label: 'Mellette County' }, + '46-097': { label: 'Miner County' }, + '46-099': { label: 'Minnehaha County' }, + '46-101': { label: 'Moody County' }, + '46-102': { label: 'Oglala Lakota County' }, + '46-103': { label: 'Pennington County' }, + '46-105': { label: 'Perkins County' }, + '46-107': { label: 'Potter County' }, + '46-109': { label: 'Roberts County' }, + '46-111': { label: 'Sanborn County' }, + '46-113': { label: 'Shannon County' }, + '46-115': { label: 'Spink County' }, + '46-117': { label: 'Stanley County' }, + '46-119': { label: 'Sully County' }, + '46-121': { label: 'Todd County' }, + '46-123': { label: 'Tripp County' }, + '46-125': { label: 'Turner County' }, + '46-127': { label: 'Union County' }, + '46-129': { label: 'Walworth County' }, + '46-135': { label: 'Yankton County' }, + '46-137': { label: 'Ziebach County' }, + '47-000': { label: 'Unspecified' }, + '47-001': { label: 'Anderson County' }, + '47-003': { label: 'Bedford County' }, + '47-005': { label: 'Benton County' }, + '47-007': { label: 'Bledsoe County' }, + '47-009': { label: 'Blount County' }, + '47-011': { label: 'Bradley County' }, + '47-013': { label: 'Campbell County' }, + '47-015': { label: 'Cannon County' }, + '47-017': { label: 'Carroll County' }, + '47-019': { label: 'Carter County' }, + '47-021': { label: 'Cheatham County' }, + '47-023': { label: 'Chester County' }, + '47-025': { label: 'Claiborne County' }, + '47-027': { label: 'Clay County' }, + '47-029': { label: 'Cocke County' }, + '47-031': { label: 'Coffee County' }, + '47-033': { label: 'Crockett County' }, + '47-035': { label: 'Cumberland County' }, + '47-037': { label: 'Davidson County' }, + '47-039': { label: 'Decatur County' }, + '47-041': { label: 'DeKalb County' }, + '47-043': { label: 'Dickson County' }, + '47-045': { label: 'Dyer County' }, + '47-047': { label: 'Fayette County' }, + '47-049': { label: 'Fentress County' }, + '47-051': { label: 'Franklin County' }, + '47-053': { label: 'Gibson County' }, + '47-055': { label: 'Giles County' }, + '47-057': { label: 'Grainger County' }, + '47-059': { label: 'Greene County' }, + '47-061': { label: 'Grundy County' }, + '47-063': { label: 'Hamblen County' }, + '47-065': { label: 'Hamilton County' }, + '47-067': { label: 'Hancock County' }, + '47-069': { label: 'Hardeman County' }, + '47-071': { label: 'Hardin County' }, + '47-073': { label: 'Hawkins County' }, + '47-075': { label: 'Haywood County' }, + '47-077': { label: 'Henderson County' }, + '47-079': { label: 'Henry County' }, + '47-081': { label: 'Hickman County' }, + '47-083': { label: 'Houston County' }, + '47-085': { label: 'Humphreys County' }, + '47-087': { label: 'Jackson County' }, + '47-089': { label: 'Jefferson County' }, + '47-091': { label: 'Johnson County' }, + '47-093': { label: 'Knox County' }, + '47-095': { label: 'Lake County' }, + '47-097': { label: 'Lauderdale County' }, + '47-099': { label: 'Lawrence County' }, + '47-101': { label: 'Lewis County' }, + '47-103': { label: 'Lincoln County' }, + '47-105': { label: 'Loudon County' }, + '47-107': { label: 'McMinn County' }, + '47-109': { label: 'McNairy County' }, + '47-111': { label: 'Macon County' }, + '47-113': { label: 'Madison County' }, + '47-115': { label: 'Marion County' }, + '47-117': { label: 'Marshall County' }, + '47-119': { label: 'Maury County' }, + '47-121': { label: 'Meigs County' }, + '47-123': { label: 'Monroe County' }, + '47-125': { label: 'Montgomery County' }, + '47-127': { label: 'Moore County' }, + '47-129': { label: 'Morgan County' }, + '47-131': { label: 'Obion County' }, + '47-133': { label: 'Overton County' }, + '47-135': { label: 'Perry County' }, + '47-137': { label: 'Pickett County' }, + '47-139': { label: 'Polk County' }, + '47-141': { label: 'Putnam County' }, + '47-143': { label: 'Rhea County' }, + '47-145': { label: 'Roane County' }, + '47-147': { label: 'Robertson County' }, + '47-149': { label: 'Rutherford County' }, + '47-151': { label: 'Scott County' }, + '47-153': { label: 'Sequatchie County' }, + '47-155': { label: 'Sevier County' }, + '47-157': { label: 'Shelby County' }, + '47-159': { label: 'Smith County' }, + '47-161': { label: 'Stewart County' }, + '47-163': { label: 'Sullivan County' }, + '47-165': { label: 'Sumner County' }, + '47-167': { label: 'Tipton County' }, + '47-169': { label: 'Trousdale County' }, + '47-171': { label: 'Unicoi County' }, + '47-173': { label: 'Union County' }, + '47-175': { label: 'Van Buren County' }, + '47-177': { label: 'Warren County' }, + '47-179': { label: 'Washington County' }, + '47-181': { label: 'Wayne County' }, + '47-183': { label: 'Weakley County' }, + '47-185': { label: 'White County' }, + '47-187': { label: 'Williamson County' }, + '47-189': { label: 'Wilson County' }, + '48-000': { label: 'Unspecified' }, + '48-001': { label: 'Anderson County' }, + '48-003': { label: 'Andrews County' }, + '48-005': { label: 'Angelina County' }, + '48-007': { label: 'Aransas County' }, + '48-009': { label: 'Archer County' }, + '48-011': { label: 'Armstrong County' }, + '48-013': { label: 'Atascosa County' }, + '48-015': { label: 'Austin County' }, + '48-017': { label: 'Bailey County' }, + '48-019': { label: 'Bandera County' }, + '48-021': { label: 'Bastrop County' }, + '48-023': { label: 'Baylor County' }, + '48-025': { label: 'Bee County' }, + '48-027': { label: 'Bell County' }, + '48-029': { label: 'Bexar County' }, + '48-031': { label: 'Blanco County' }, + '48-033': { label: 'Borden County' }, + '48-035': { label: 'Bosque County' }, + '48-037': { label: 'Bowie County' }, + '48-039': { label: 'Brazoria County' }, + '48-041': { label: 'Brazos County' }, + '48-043': { label: 'Brewster County' }, + '48-045': { label: 'Briscoe County' }, + '48-047': { label: 'Brooks County' }, + '48-049': { label: 'Brown County' }, + '48-051': { label: 'Burleson County' }, + '48-053': { label: 'Burnet County' }, + '48-055': { label: 'Caldwell County' }, + '48-057': { label: 'Calhoun County' }, + '48-059': { label: 'Callahan County' }, + '48-061': { label: 'Cameron County' }, + '48-063': { label: 'Camp County' }, + '48-065': { label: 'Carson County' }, + '48-067': { label: 'Cass County' }, + '48-069': { label: 'Castro County' }, + '48-071': { label: 'Chambers County' }, + '48-073': { label: 'Cherokee County' }, + '48-075': { label: 'Childress County' }, + '48-077': { label: 'Clay County' }, + '48-079': { label: 'Cochran County' }, + '48-081': { label: 'Coke County' }, + '48-083': { label: 'Coleman County' }, + '48-085': { label: 'Collin County' }, + '48-087': { label: 'Collingsworth County' }, + '48-089': { label: 'Colorado County' }, + '48-091': { label: 'Comal County' }, + '48-093': { label: 'Comanche County' }, + '48-095': { label: 'Concho County' }, + '48-097': { label: 'Cooke County' }, + '48-099': { label: 'Coryell County' }, + '48-101': { label: 'Cottle County' }, + '48-103': { label: 'Crane County' }, + '48-105': { label: 'Crockett County' }, + '48-107': { label: 'Crosby County' }, + '48-109': { label: 'Culberson County' }, + '48-111': { label: 'Dallam County' }, + '48-113': { label: 'Dallas County' }, + '48-115': { label: 'Dawson County' }, + '48-117': { label: 'Deaf Smith County' }, + '48-119': { label: 'Delta County' }, + '48-121': { label: 'Denton County' }, + '48-123': { label: 'DeWitt County' }, + '48-125': { label: 'Dickens County' }, + '48-127': { label: 'Dimmit County' }, + '48-129': { label: 'Donley County' }, + '48-131': { label: 'Duval County' }, + '48-133': { label: 'Eastland County' }, + '48-135': { label: 'Ector County' }, + '48-137': { label: 'Edwards County' }, + '48-139': { label: 'Ellis County' }, + '48-141': { label: 'El Paso County' }, + '48-143': { label: 'Erath County' }, + '48-145': { label: 'Falls County' }, + '48-147': { label: 'Fannin County' }, + '48-149': { label: 'Fayette County' }, + '48-151': { label: 'Fisher County' }, + '48-153': { label: 'Floyd County' }, + '48-155': { label: 'Foard County' }, + '48-157': { label: 'Fort Bend County' }, + '48-159': { label: 'Franklin County' }, + '48-161': { label: 'Freestone County' }, + '48-163': { label: 'Frio County' }, + '48-165': { label: 'Gaines County' }, + '48-167': { label: 'Galveston County' }, + '48-169': { label: 'Garza County' }, + '48-171': { label: 'Gillespie County' }, + '48-173': { label: 'Glasscock County' }, + '48-175': { label: 'Goliad County' }, + '48-177': { label: 'Gonzales County' }, + '48-179': { label: 'Gray County' }, + '48-181': { label: 'Grayson County' }, + '48-183': { label: 'Gregg County' }, + '48-185': { label: 'Grimes County' }, + '48-187': { label: 'Guadalupe County' }, + '48-189': { label: 'Hale County' }, + '48-191': { label: 'Hall County' }, + '48-193': { label: 'Hamilton County' }, + '48-195': { label: 'Hansford County' }, + '48-197': { label: 'Hardeman County' }, + '48-199': { label: 'Hardin County' }, + '48-201': { label: 'Harris County' }, + '48-203': { label: 'Harrison County' }, + '48-205': { label: 'Hartley County' }, + '48-207': { label: 'Haskell County' }, + '48-209': { label: 'Hays County' }, + '48-211': { label: 'Hemphill County' }, + '48-213': { label: 'Henderson County' }, + '48-215': { label: 'Hidalgo County' }, + '48-217': { label: 'Hill County' }, + '48-219': { label: 'Hockley County' }, + '48-221': { label: 'Hood County' }, + '48-223': { label: 'Hopkins County' }, + '48-225': { label: 'Houston County' }, + '48-227': { label: 'Howard County' }, + '48-229': { label: 'Hudspeth County' }, + '48-231': { label: 'Hunt County' }, + '48-233': { label: 'Hutchinson County' }, + '48-235': { label: 'Irion County' }, + '48-237': { label: 'Jack County' }, + '48-239': { label: 'Jackson County' }, + '48-241': { label: 'Jasper County' }, + '48-243': { label: 'Jeff Davis County' }, + '48-245': { label: 'Jefferson County' }, + '48-247': { label: 'Jim Hogg County' }, + '48-249': { label: 'Jim Wells County' }, + '48-251': { label: 'Johnson County' }, + '48-253': { label: 'Jones County' }, + '48-255': { label: 'Karnes County' }, + '48-257': { label: 'Kaufman County' }, + '48-259': { label: 'Kendall County' }, + '48-261': { label: 'Kenedy County' }, + '48-263': { label: 'Kent County' }, + '48-265': { label: 'Kerr County' }, + '48-267': { label: 'Kimble County' }, + '48-269': { label: 'King County' }, + '48-271': { label: 'Kinney County' }, + '48-273': { label: 'Kleberg County' }, + '48-275': { label: 'Knox County' }, + '48-277': { label: 'Lamar County' }, + '48-279': { label: 'Lamb County' }, + '48-281': { label: 'Lampasas County' }, + '48-283': { label: 'La Salle County' }, + '48-285': { label: 'Lavaca County' }, + '48-287': { label: 'Lee County' }, + '48-289': { label: 'Leon County' }, + '48-291': { label: 'Liberty County' }, + '48-293': { label: 'Limestone County' }, + '48-295': { label: 'Lipscomb County' }, + '48-297': { label: 'Live Oak County' }, + '48-299': { label: 'Llano County' }, + '48-301': { label: 'Loving County' }, + '48-303': { label: 'Lubbock County' }, + '48-305': { label: 'Lynn County' }, + '48-307': { label: 'McCulloch County' }, + '48-309': { label: 'McLennan County' }, + '48-311': { label: 'McMullen County' }, + '48-313': { label: 'Madison County' }, + '48-315': { label: 'Marion County' }, + '48-317': { label: 'Martin County' }, + '48-319': { label: 'Mason County' }, + '48-321': { label: 'Matagorda County' }, + '48-323': { label: 'Maverick County' }, + '48-325': { label: 'Medina County' }, + '48-327': { label: 'Menard County' }, + '48-329': { label: 'Midland County' }, + '48-331': { label: 'Milam County' }, + '48-333': { label: 'Mills County' }, + '48-335': { label: 'Mitchell County' }, + '48-337': { label: 'Montague County' }, + '48-339': { label: 'Montgomery County' }, + '48-341': { label: 'Moore County' }, + '48-343': { label: 'Morris County' }, + '48-345': { label: 'Motley County' }, + '48-347': { label: 'Nacogdoches County' }, + '48-349': { label: 'Navarro County' }, + '48-351': { label: 'Newton County' }, + '48-353': { label: 'Nolan County' }, + '48-355': { label: 'Nueces County' }, + '48-357': { label: 'Ochiltree County' }, + '48-359': { label: 'Oldham County' }, + '48-361': { label: 'Orange County' }, + '48-363': { label: 'Palo Pinto County' }, + '48-365': { label: 'Panola County' }, + '48-367': { label: 'Parker County' }, + '48-369': { label: 'Parmer County' }, + '48-371': { label: 'Pecos County' }, + '48-373': { label: 'Polk County' }, + '48-375': { label: 'Potter County' }, + '48-377': { label: 'Presidio County' }, + '48-379': { label: 'Rains County' }, + '48-381': { label: 'Randall County' }, + '48-383': { label: 'Reagan County' }, + '48-385': { label: 'Real County' }, + '48-387': { label: 'Red River County' }, + '48-389': { label: 'Reeves County' }, + '48-391': { label: 'Refugio County' }, + '48-393': { label: 'Roberts County' }, + '48-395': { label: 'Robertson County' }, + '48-397': { label: 'Rockwall County' }, + '48-399': { label: 'Runnels County' }, + '48-401': { label: 'Rusk County' }, + '48-403': { label: 'Sabine County' }, + '48-405': { label: 'San Augustine County' }, + '48-407': { label: 'San Jacinto County' }, + '48-409': { label: 'San Patricio County' }, + '48-411': { label: 'San Saba County' }, + '48-413': { label: 'Schleicher County' }, + '48-415': { label: 'Scurry County' }, + '48-417': { label: 'Shackelford County' }, + '48-419': { label: 'Shelby County' }, + '48-421': { label: 'Sherman County' }, + '48-423': { label: 'Smith County' }, + '48-425': { label: 'Somervell County' }, + '48-427': { label: 'Starr County' }, + '48-429': { label: 'Stephens County' }, + '48-431': { label: 'Sterling County' }, + '48-433': { label: 'Stonewall County' }, + '48-435': { label: 'Sutton County' }, + '48-437': { label: 'Swisher County' }, + '48-439': { label: 'Tarrant County' }, + '48-441': { label: 'Taylor County' }, + '48-443': { label: 'Terrell County' }, + '48-445': { label: 'Terry County' }, + '48-447': { label: 'Throckmorton County' }, + '48-449': { label: 'Titus County' }, + '48-451': { label: 'Tom Green County' }, + '48-453': { label: 'Travis County' }, + '48-455': { label: 'Trinity County' }, + '48-457': { label: 'Tyler County' }, + '48-459': { label: 'Upshur County' }, + '48-461': { label: 'Upton County' }, + '48-463': { label: 'Uvalde County' }, + '48-465': { label: 'Val Verde County' }, + '48-467': { label: 'Van Zandt County' }, + '48-469': { label: 'Victoria County' }, + '48-471': { label: 'Walker County' }, + '48-473': { label: 'Waller County' }, + '48-475': { label: 'Ward County' }, + '48-477': { label: 'Washington County' }, + '48-479': { label: 'Webb County' }, + '48-481': { label: 'Wharton County' }, + '48-483': { label: 'Wheeler County' }, + '48-485': { label: 'Wichita County' }, + '48-487': { label: 'Wilbarger County' }, + '48-489': { label: 'Willacy County' }, + '48-491': { label: 'Williamson County' }, + '48-493': { label: 'Wilson County' }, + '48-495': { label: 'Winkler County' }, + '48-497': { label: 'Wise County' }, + '48-499': { label: 'Wood County' }, + '48-501': { label: 'Yoakum County' }, + '48-503': { label: 'Young County' }, + '48-505': { label: 'Zapata County' }, + '48-507': { label: 'Zavala County' }, + '49-000': { label: 'Unspecified' }, + '49-001': { label: 'Beaver County' }, + '49-003': { label: 'Box Elder County' }, + '49-005': { label: 'Cache County' }, + '49-007': { label: 'Carbon County' }, + '49-009': { label: 'Daggett County' }, + '49-011': { label: 'Davis County' }, + '49-013': { label: 'Duchesne County' }, + '49-015': { label: 'Emery County' }, + '49-017': { label: 'Garfield County' }, + '49-019': { label: 'Grand County' }, + '49-021': { label: 'Iron County' }, + '49-023': { label: 'Juab County' }, + '49-025': { label: 'Kane County' }, + '49-027': { label: 'Millard County' }, + '49-029': { label: 'Morgan County' }, + '49-031': { label: 'Piute County' }, + '49-033': { label: 'Rich County' }, + '49-035': { label: 'Salt Lake County' }, + '49-037': { label: 'San Juan County' }, + '49-039': { label: 'Sanpete County' }, + '49-041': { label: 'Sevier County' }, + '49-043': { label: 'Summit County' }, + '49-045': { label: 'Tooele County' }, + '49-047': { label: 'Uintah County' }, + '49-049': { label: 'Utah County' }, + '49-051': { label: 'Wasatch County' }, + '49-053': { label: 'Washington County' }, + '49-055': { label: 'Wayne County' }, + '49-057': { label: 'Weber County' }, + '50-000': { label: 'Unspecified' }, + '50-001': { label: 'Addison County' }, + '50-003': { label: 'Bennington County' }, + '50-005': { label: 'Caledonia County' }, + '50-007': { label: 'Chittenden County' }, + '50-009': { label: 'Essex County' }, + '50-011': { label: 'Franklin County' }, + '50-013': { label: 'Grand Isle County' }, + '50-015': { label: 'Lamoille County' }, + '50-017': { label: 'Orange County' }, + '50-019': { label: 'Orleans County' }, + '50-021': { label: 'Rutland County' }, + '50-023': { label: 'Washington County' }, + '50-025': { label: 'Windham County' }, + '50-027': { label: 'Windsor County' }, + '51-000': { label: 'Unspecified' }, + '51-001': { label: 'Accomack County' }, + '51-003': { label: 'Albemarle County' }, + '51-005': { label: 'Alleghany County' }, + '51-007': { label: 'Amelia County' }, + '51-009': { label: 'Amherst County' }, + '51-011': { label: 'Appomattox County' }, + '51-013': { label: 'Arlington County' }, + '51-015': { label: 'Augusta County' }, + '51-017': { label: 'Bath County' }, + '51-019': { label: 'Bedford County' }, + '51-021': { label: 'Bland County' }, + '51-023': { label: 'Botetourt County' }, + '51-025': { label: 'Brunswick County' }, + '51-027': { label: 'Buchanan County' }, + '51-029': { label: 'Buckingham County' }, + '51-031': { label: 'Campbell County' }, + '51-033': { label: 'Caroline County' }, + '51-035': { label: 'Carroll County' }, + '51-036': { label: 'Charles City County' }, + '51-037': { label: 'Charlotte County' }, + '51-041': { label: 'Chesterfield County' }, + '51-043': { label: 'Clarke County' }, + '51-045': { label: 'Craig County' }, + '51-047': { label: 'Culpeper County' }, + '51-049': { label: 'Cumberland County' }, + '51-051': { label: 'Dickenson County' }, + '51-053': { label: 'Dinwiddie County' }, + '51-057': { label: 'Essex County' }, + '51-059': { label: 'Fairfax County' }, + '51-061': { label: 'Fauquier County' }, + '51-063': { label: 'Floyd County' }, + '51-065': { label: 'Fluvanna County' }, + '51-067': { label: 'Franklin County' }, + '51-069': { label: 'Frederick County' }, + '51-071': { label: 'Giles County' }, + '51-073': { label: 'Gloucester County' }, + '51-075': { label: 'Goochland County' }, + '51-077': { label: 'Grayson County' }, + '51-079': { label: 'Greene County' }, + '51-081': { label: 'Greensville County' }, + '51-083': { label: 'Halifax County' }, + '51-085': { label: 'Hanover County' }, + '51-087': { label: 'Henrico County' }, + '51-089': { label: 'Henry County' }, + '51-091': { label: 'Highland County' }, + '51-093': { label: 'Isle of Wight County' }, + '51-095': { label: 'James City County' }, + '51-097': { label: 'King and Queen County' }, + '51-099': { label: 'King George County' }, + '51-101': { label: 'King William County' }, + '51-103': { label: 'Lancaster County' }, + '51-105': { label: 'Lee County' }, + '51-107': { label: 'Loudoun County' }, + '51-109': { label: 'Louisa County' }, + '51-111': { label: 'Lunenburg County' }, + '51-113': { label: 'Madison County' }, + '51-115': { label: 'Mathews County' }, + '51-117': { label: 'Mecklenburg County' }, + '51-119': { label: 'Middlesex County' }, + '51-121': { label: 'Montgomery County' }, + '51-125': { label: 'Nelson County' }, + '51-127': { label: 'New Kent County' }, + '51-131': { label: 'Northampton County' }, + '51-133': { label: 'Northumberland County' }, + '51-135': { label: 'Nottoway County' }, + '51-137': { label: 'Orange County' }, + '51-139': { label: 'Page County' }, + '51-141': { label: 'Patrick County' }, + '51-143': { label: 'Pittsylvania County' }, + '51-145': { label: 'Powhatan County' }, + '51-147': { label: 'Prince Edward County' }, + '51-149': { label: 'Prince George County' }, + '51-153': { label: 'Prince William County' }, + '51-155': { label: 'Pulaski County' }, + '51-157': { label: 'Rappahannock County' }, + '51-159': { label: 'Richmond County' }, + '51-161': { label: 'Roanoke County' }, + '51-163': { label: 'Rockbridge County' }, + '51-165': { label: 'Rockingham County' }, + '51-167': { label: 'Russell County' }, + '51-169': { label: 'Scott County' }, + '51-171': { label: 'Shenandoah County' }, + '51-173': { label: 'Smyth County' }, + '51-175': { label: 'Southampton County' }, + '51-177': { label: 'Spotsylvania County' }, + '51-179': { label: 'Stafford County' }, + '51-181': { label: 'Surry County' }, + '51-183': { label: 'Sussex County' }, + '51-185': { label: 'Tazewell County' }, + '51-187': { label: 'Warren County' }, + '51-191': { label: 'Washington County' }, + '51-193': { label: 'Westmoreland County' }, + '51-195': { label: 'Wise County' }, + '51-197': { label: 'Wythe County' }, + '51-199': { label: 'York County' }, + '51-510': { label: 'Alexandria City' }, + '51-520': { label: 'Bristol City' }, + '51-530': { label: 'Buena Vista City' }, + '51-540': { label: 'Charlottesville City' }, + '51-550': { label: 'Chesapeake City' }, + '51-560': { label: 'Clifton Forge City' }, + '51-570': { label: 'Colonial Heights City' }, + '51-580': { label: 'Covington City' }, + '51-590': { label: 'Danville City' }, + '51-595': { label: 'Emporia City' }, + '51-600': { label: 'Fairfax City' }, + '51-610': { label: 'Falls Church City' }, + '51-620': { label: 'Franklin City' }, + '51-630': { label: 'Fredericksburg City' }, + '51-640': { label: 'Galax City' }, + '51-650': { label: 'Hampton City' }, + '51-660': { label: 'Harrisonburg City' }, + '51-670': { label: 'Hopewell City' }, + '51-678': { label: 'Lexington City' }, + '51-680': { label: 'Lynchburg City' }, + '51-683': { label: 'Manassas City' }, + '51-685': { label: 'Manassas Park City' }, + '51-690': { label: 'Martinsville City' }, + '51-700': { label: 'Newport News City' }, + '51-710': { label: 'Norfolk City' }, + '51-720': { label: 'Norton City' }, + '51-730': { label: 'Petersburg City' }, + '51-735': { label: 'Poquoson City' }, + '51-740': { label: 'Portsmouth City' }, + '51-750': { label: 'Radford City' }, + '51-760': { label: 'Richmond City' }, + '51-770': { label: 'Roanoke City' }, + '51-775': { label: 'Salem City' }, + '51-780': { label: 'South Boston City' }, + '51-790': { label: 'Staunton City' }, + '51-800': { label: 'Suffolk City' }, + '51-810': { label: 'Virginia Beach City' }, + '51-820': { label: 'Waynesboro City' }, + '51-830': { label: 'Williamsburg City' }, + '51-840': { label: 'Winchester City' }, + '53-000': { label: 'Unspecified' }, + '53-001': { label: 'Adams County' }, + '53-003': { label: 'Asotin County' }, + '53-005': { label: 'Benton County' }, + '53-007': { label: 'Chelan County' }, + '53-009': { label: 'Clallam County' }, + '53-011': { label: 'Clark County' }, + '53-013': { label: 'Columbia County' }, + '53-015': { label: 'Cowlitz County' }, + '53-017': { label: 'Douglas County' }, + '53-019': { label: 'Ferry County' }, + '53-021': { label: 'Franklin County' }, + '53-023': { label: 'Garfield County' }, + '53-025': { label: 'Grant County' }, + '53-027': { label: 'Grays Harbor County' }, + '53-029': { label: 'Island County' }, + '53-031': { label: 'Jefferson County' }, + '53-033': { label: 'King County' }, + '53-035': { label: 'Kitsap County' }, + '53-037': { label: 'Kittitas County' }, + '53-039': { label: 'Klickitat County' }, + '53-041': { label: 'Lewis County' }, + '53-043': { label: 'Lincoln County' }, + '53-045': { label: 'Mason County' }, + '53-047': { label: 'Okanogan County' }, + '53-049': { label: 'Pacific County' }, + '53-051': { label: 'Pend Oreille County' }, + '53-053': { label: 'Pierce County' }, + '53-055': { label: 'San Juan County' }, + '53-057': { label: 'Skagit County' }, + '53-059': { label: 'Skamania County' }, + '53-061': { label: 'Snohomish County' }, + '53-063': { label: 'Spokane County' }, + '53-065': { label: 'Stevens County' }, + '53-067': { label: 'Thurston County' }, + '53-069': { label: 'Wahkiakum County' }, + '53-071': { label: 'Walla Walla County' }, + '53-073': { label: 'Whatcom County' }, + '53-075': { label: 'Whitman County' }, + '53-077': { label: 'Yakima County' }, + '54-000': { label: 'Unspecified' }, + '54-001': { label: 'Barbour County' }, + '54-003': { label: 'Berkeley County' }, + '54-005': { label: 'Boone County' }, + '54-007': { label: 'Braxton County' }, + '54-009': { label: 'Brooke County' }, + '54-011': { label: 'Cabell County' }, + '54-013': { label: 'Calhoun County' }, + '54-015': { label: 'Clay County' }, + '54-017': { label: 'Doddridge County' }, + '54-019': { label: 'Fayette County' }, + '54-021': { label: 'Gilmer County' }, + '54-023': { label: 'Grant County' }, + '54-025': { label: 'Greenbrier County' }, + '54-027': { label: 'Hampshire County' }, + '54-029': { label: 'Hancock County' }, + '54-031': { label: 'Hardy County' }, + '54-033': { label: 'Harrison County' }, + '54-035': { label: 'Jackson County' }, + '54-037': { label: 'Jefferson County' }, + '54-039': { label: 'Kanawha County' }, + '54-041': { label: 'Lewis County' }, + '54-043': { label: 'Lincoln County' }, + '54-045': { label: 'Logan County' }, + '54-047': { label: 'McDowell County' }, + '54-049': { label: 'Marion County' }, + '54-051': { label: 'Marshall County' }, + '54-053': { label: 'Mason County' }, + '54-055': { label: 'Mercer County' }, + '54-057': { label: 'Mineral County' }, + '54-059': { label: 'Mingo County' }, + '54-061': { label: 'Monongalia County' }, + '54-063': { label: 'Monroe County' }, + '54-065': { label: 'Morgan County' }, + '54-067': { label: 'Nicholas County' }, + '54-069': { label: 'Ohio County' }, + '54-071': { label: 'Pendleton County' }, + '54-073': { label: 'Pleasants County' }, + '54-075': { label: 'Pocahontas County' }, + '54-077': { label: 'Preston County' }, + '54-079': { label: 'Putnam County' }, + '54-081': { label: 'Raleigh County' }, + '54-083': { label: 'Randolph County' }, + '54-085': { label: 'Ritchie County' }, + '54-087': { label: 'Roane County' }, + '54-089': { label: 'Summers County' }, + '54-091': { label: 'Taylor County' }, + '54-093': { label: 'Tucker County' }, + '54-095': { label: 'Tyler County' }, + '54-097': { label: 'Upshur County' }, + '54-099': { label: 'Wayne County' }, + '54-101': { label: 'Webster County' }, + '54-103': { label: 'Wetzel County' }, + '54-105': { label: 'Wirt County' }, + '54-107': { label: 'Wood County' }, + '54-109': { label: 'Wyoming County' }, + '55-000': { label: 'Unspecified' }, + '55-001': { label: 'Adams County' }, + '55-003': { label: 'Ashland County' }, + '55-005': { label: 'Barron County' }, + '55-007': { label: 'Bayfield County' }, + '55-009': { label: 'Brown County' }, + '55-011': { label: 'Buffalo County' }, + '55-013': { label: 'Burnett County' }, + '55-015': { label: 'Calumet County' }, + '55-017': { label: 'Chippewa County' }, + '55-019': { label: 'Clark County' }, + '55-021': { label: 'Columbia County' }, + '55-023': { label: 'Crawford County' }, + '55-025': { label: 'Dane County' }, + '55-027': { label: 'Dodge County' }, + '55-029': { label: 'Door County' }, + '55-031': { label: 'Douglas County' }, + '55-033': { label: 'Dunn County' }, + '55-035': { label: 'Eau Claire County' }, + '55-037': { label: 'Florence County' }, + '55-039': { label: 'Fond du Lac County' }, + '55-041': { label: 'Forest County' }, + '55-043': { label: 'Grant County' }, + '55-045': { label: 'Green County' }, + '55-047': { label: 'Green Lake County' }, + '55-049': { label: 'Iowa County' }, + '55-051': { label: 'Iron County' }, + '55-053': { label: 'Jackson County' }, + '55-055': { label: 'Jefferson County' }, + '55-057': { label: 'Juneau County' }, + '55-059': { label: 'Kenosha County' }, + '55-061': { label: 'Kewaunee County' }, + '55-063': { label: 'La Crosse County' }, + '55-065': { label: 'Lafayette County' }, + '55-067': { label: 'Langlade County' }, + '55-069': { label: 'Lincoln County' }, + '55-071': { label: 'Manitowoc County' }, + '55-073': { label: 'Marathon County' }, + '55-075': { label: 'Marinette County' }, + '55-077': { label: 'Marquette County' }, + '55-078': { label: 'Menominee County' }, + '55-079': { label: 'Milwaukee County' }, + '55-081': { label: 'Monroe County' }, + '55-083': { label: 'Oconto County' }, + '55-085': { label: 'Oneida County' }, + '55-087': { label: 'Outagamie County' }, + '55-089': { label: 'Ozaukee County' }, + '55-091': { label: 'Pepin County' }, + '55-093': { label: 'Pierce County' }, + '55-095': { label: 'Polk County' }, + '55-097': { label: 'Portage County' }, + '55-099': { label: 'Price County' }, + '55-101': { label: 'Racine County' }, + '55-103': { label: 'Richland County' }, + '55-105': { label: 'Rock County' }, + '55-107': { label: 'Rusk County' }, + '55-109': { label: 'St. Croix County' }, + '55-111': { label: 'Sauk County' }, + '55-113': { label: 'Sawyer County' }, + '55-115': { label: 'Shawano County' }, + '55-117': { label: 'Sheboygan County' }, + '55-119': { label: 'Taylor County' }, + '55-121': { label: 'Trempealeau County' }, + '55-123': { label: 'Vernon County' }, + '55-125': { label: 'Vilas County' }, + '55-127': { label: 'Walworth County' }, + '55-129': { label: 'Washburn County' }, + '55-131': { label: 'Washington County' }, + '55-133': { label: 'Waukesha County' }, + '55-135': { label: 'Waupaca County' }, + '55-137': { label: 'Waushara County' }, + '55-139': { label: 'Winnebago County' }, + '55-141': { label: 'Wood County' }, + '56-000': { label: 'Unspecified' }, + '56-001': { label: 'Albany County' }, + '56-003': { label: 'Big Horn County' }, + '56-005': { label: 'Campbell County' }, + '56-007': { label: 'Carbon County' }, + '56-009': { label: 'Converse County' }, + '56-011': { label: 'Crook County' }, + '56-013': { label: 'Fremont County' }, + '56-015': { label: 'Goshen County' }, + '56-017': { label: 'Hot Springs County' }, + '56-019': { label: 'Johnson County' }, + '56-021': { label: 'Laramie County' }, + '56-023': { label: 'Lincoln County' }, + '56-025': { label: 'Natrona County' }, + '56-027': { label: 'Niobrara County' }, + '56-029': { label: 'Park County' }, + '56-031': { label: 'Platte County' }, + '56-033': { label: 'Sheridan County' }, + '56-035': { label: 'Sublette County' }, + '56-037': { label: 'Sweetwater County' }, + '56-039': { label: 'Teton County' }, + '56-041': { label: 'Uinta County' }, + '56-043': { label: 'Washakie County' }, + '56-045': { label: 'Weston County' }, + '60-000': { label: 'Unspecified' }, + '60-010': { label: 'Eastern District' }, + '60-020': { label: "Manu'a District" }, + '60-030': { label: 'Rose Island' }, + '60-040': { label: 'Swains Island' }, + '60-050': { label: 'Western District' }, + '65-000': { label: 'Unspecified' }, + '66-000': { label: 'Unspecified' }, + '66-010': { label: 'Guam' }, + '67-000': { label: 'Unspecified' }, + '69-000': { label: 'Unspecified' }, + '69-085': { label: 'Northern Islands Municipality' }, + '69-100': { label: 'Rota Municipality' }, + '69-110': { label: 'Saipan Municipality' }, + '69-120': { label: 'Tinian Municipality' }, + '71-000': { label: 'Unspecified' }, + '72-000': { label: 'Unspecified' }, + '72-001': { label: 'Adjuntas Municipio' }, + '72-003': { label: 'Aguada Municipio' }, + '72-005': { label: 'Aguadilla Municipio' }, + '72-007': { label: 'Aguas Buenas Municipio' }, + '72-009': { label: 'Aibonito Municipio' }, + '72-011': { label: 'Anasco Municipio' }, + '72-013': { label: 'Arecibo Municipio' }, + '72-015': { label: 'Arroyo Municipio' }, + '72-017': { label: 'Barceloneta Municipio' }, + '72-019': { label: 'Barranquitas Municipio' }, + '72-021': { label: 'Bayamon Municipio' }, + '72-023': { label: 'Cabo Rojo Municipio' }, + '72-025': { label: 'Caguas Municipio' }, + '72-027': { label: 'Camuy Municipio' }, + '72-029': { label: 'Canovanas Municipio' }, + '72-031': { label: 'Carolina Municipio' }, + '72-033': { label: 'Catano Municipio' }, + '72-035': { label: 'Cayey Municipio' }, + '72-037': { label: 'Ceiba Municipio' }, + '72-039': { label: 'Ciales Municipio' }, + '72-041': { label: 'Cidra Municipio' }, + '72-043': { label: 'Coamo Municipio' }, + '72-045': { label: 'Comerio Municipio' }, + '72-047': { label: 'Corozal Municipio' }, + '72-049': { label: 'Culebra Municipio' }, + '72-051': { label: 'Dorado Municipio' }, + '72-053': { label: 'Fajardo Municipio' }, + '72-054': { label: 'Florida Municipio' }, + '72-055': { label: 'Guanica Municipio' }, + '72-057': { label: 'Guayama Municipio' }, + '72-059': { label: 'Guayanilla Municipio' }, + '72-061': { label: 'Guaynabo Municipio' }, + '72-063': { label: 'Gurabo Municipio' }, + '72-065': { label: 'Hatillo Municipio' }, + '72-067': { label: 'Hormigueros Municipio' }, + '72-069': { label: 'Humacao Municipio' }, + '72-071': { label: 'Isabela Municipio' }, + '72-073': { label: 'Jayuya Municipio' }, + '72-075': { label: 'Juana Diaz Municipio' }, + '72-077': { label: 'Juncos Municipio' }, + '72-079': { label: 'Lajas Municipio' }, + '72-081': { label: 'Lares Municipio' }, + '72-083': { label: 'Las Marias Municipio' }, + '72-085': { label: 'Las Piedras Municipio' }, + '72-087': { label: 'Loiza Municipio' }, + '72-089': { label: 'Luquillo Municipio' }, + '72-091': { label: 'Manati Municipio' }, + '72-093': { label: 'Maricao Municipio' }, + '72-095': { label: 'Maunabo Municipio' }, + '72-097': { label: 'Mayaquez Municipio' }, + '72-099': { label: 'Moca Municipio' }, + '72-101': { label: 'Morovis Municipio' }, + '72-103': { label: 'Naguabo Municipio' }, + '72-105': { label: 'Naranjito Municipio' }, + '72-107': { label: 'Orocovis Municipio' }, + '72-109': { label: 'Patillas Municipio' }, + '72-111': { label: 'Penuelas Municipio' }, + '72-113': { label: 'Ponce Municipio' }, + '72-115': { label: 'Quebradillas Municipio' }, + '72-117': { label: 'Rincon Municipio' }, + '72-119': { label: 'Rio Grande Municipio' }, + '72-121': { label: 'Sabana Grande Municipio' }, + '72-123': { label: 'Salinas Municipio' }, + '72-125': { label: 'San German Municipio' }, + '72-127': { label: 'San Juan Municipio' }, + '72-129': { label: 'San Lorenzo Municipio' }, + '72-131': { label: 'San Sebastian Municipio' }, + '72-133': { label: 'Santa Isabel Municipio' }, + '72-135': { label: 'Toa Alta Municipio' }, + '72-137': { label: 'Toa Baja Municipio' }, + '72-139': { label: 'Trujillo Alto Municipio' }, + '72-141': { label: 'Utuado Municipio' }, + '72-143': { label: 'Vega Alta Municipio' }, + '72-145': { label: 'Vega Baja Municipio' }, + '72-147': { label: 'Vieques Municipio' }, + '72-149': { label: 'Villalba Municipio' }, + '72-151': { label: 'Yabucoa Municipio' }, + '72-153': { label: 'Yauco Municipio' }, + '73-000': { label: 'Unspecified' }, + '74-000': { label: 'Unspecified' }, + '76-000': { label: 'Unspecified' }, + '77-000': { label: 'Unspecified' }, + '78-000': { label: 'Unspecified' }, + '78-010': { label: 'St. Croix Island' }, + '78-020': { label: 'St. John Island' }, + '78-030': { label: 'St. Thomas Island' }, + '79-000': { label: 'Unspecified' }, + }, +} + +/** + * OGC API monitoring-locations property -> reference collection. Properties + * absent here render their raw code. + * + * The API resolves many codes itself through sibling name properties (e.g. + * site_type alongside site_type_code); these tables cover the ones it does not, + * and supply the code descriptions the API omits. + */ +export const USGS_COLUMN_CODE_TABLES: Record = { + agency_code: 'agency-codes', + aquifer_type_code: 'aquifer-types', + country_code: 'countries', + district_code: 'states', + horizontal_position_method_code: 'coordinate-method-codes', + horizontal_positional_accuracy_code: 'coordinate-accuracy-codes', + national_aquifer_code: 'national-aquifer-codes', + original_horizontal_datum: 'coordinate-datum-codes', + site_type_code: 'site-types', + state_code: 'states', + time_zone_abbreviation: 'time-zone-codes', + vertical_datum: 'altitude-datums', +} + +/** + * Decodes a coded site-file value, e.g. site_type_code "GW" -> "Well". + * County codes need the state FIPS code, which the caller passes as `context`. + */ +export const decodeUSGSValue = ( + column: string, + value: unknown, + context?: { stateFips?: string } +): USGSCodeEntry | null => { + const raw = value == null ? '' : String(value).trim() + if (!raw) return null + + if (column === 'county_code') { + const stateFips = context?.stateFips?.trim() + if (!stateFips) return null + return USGS_CODE_TABLES['counties'][`${stateFips}-${raw}`] ?? null + } + + const collection = USGS_COLUMN_CODE_TABLES[column] + if (!collection) return null + + return USGS_CODE_TABLES[collection][raw] ?? null +} diff --git a/src/constants/viridis.ts b/src/constants/viridis.ts new file mode 100644 index 00000000..4bdaed88 --- /dev/null +++ b/src/constants/viridis.ts @@ -0,0 +1,105 @@ +// --------------------------------------------------------------------------- +// Viridis color palette +// +// Viridis is a perceptually uniform, colorblind-friendly colormap: equal steps +// in the data produce equal-looking steps in color, and the ramp reads the same +// under the common forms of color vision deficiency. It also stays legible on +// satellite imagery because it runs dark-purple -> teal -> green -> yellow, +// none of which collide with the browns and grays of aerial photography. +// +// Everything here is derived from ten evenly spaced anchors taken from the +// reference implementation (viridisLite::viridis(10)). Colors between anchors +// are linearly interpolated, which tracks the full 256-entry reference map +// closely enough to be visually indistinguishable at map-symbol sizes. +// +// Reference: https://sjmgarnier.github.io/viridisLite/reference/viridis.html +// --------------------------------------------------------------------------- + +/** Evenly spaced samples of the reference viridis ramp, dark end first. */ +export const VIRIDIS_ANCHORS = [ + '#440154', + '#482878', + '#3e4a89', + '#31688e', + '#26828e', + '#1f9e89', + '#35b779', + '#6dcd59', + '#b4de2c', + '#fde725', +] as const + +const clamp01 = (value: number): number => { + // NaN has no position on the ramp, so it falls to the low end. The + // infinities are ordered, and clamp like any other out-of-range value. + if (Number.isNaN(value)) return 0 + if (value < 0) return 0 + if (value > 1) return 1 + return value +} + +const hexToRgb = (hex: string): [number, number, number] => [ + Number.parseInt(hex.slice(1, 3), 16), + Number.parseInt(hex.slice(3, 5), 16), + Number.parseInt(hex.slice(5, 7), 16), +] + +const channelToHex = (value: number): string => + Math.round(value).toString(16).padStart(2, '0') + +/** + * Color at `position` along the viridis ramp, where 0 is the dark purple end + * and 1 is the bright yellow end. Values outside 0..1 are clamped to the + * nearer end — including the infinities — and NaN maps to the dark end, so + * callers never have to sanitize a computed ratio. + */ +export const viridisColor = (position: number): string => { + const scaled = clamp01(position) * (VIRIDIS_ANCHORS.length - 1) + const lowerIndex = Math.floor(scaled) + const upperIndex = Math.min(VIRIDIS_ANCHORS.length - 1, lowerIndex + 1) + const fraction = scaled - lowerIndex + + const [r1, g1, b1] = hexToRgb(VIRIDIS_ANCHORS[lowerIndex]) + const [r2, g2, b2] = hexToRgb(VIRIDIS_ANCHORS[upperIndex]) + + const r = r1 + (r2 - r1) * fraction + const g = g1 + (g2 - g1) * fraction + const b = b1 + (b2 - b1) * fraction + + return `#${channelToHex(r)}${channelToHex(g)}${channelToHex(b)}` +} + +/** + * `count` colors spread evenly across the ramp, including both endpoints. + * Use this for binned/classed styling — e.g. six TDS classes get + * `viridisSamples(6)`, dark for the lowest class through yellow for the highest. + */ +export const viridisSamples = (count: number): string[] => { + if (count <= 0) return [] + if (count === 1) return [viridisColor(0.5)] + return Array.from({ length: count }, (_, index) => + viridisColor(index / (count - 1)) + ) +} + +/** + * A CSS `linear-gradient` across the ramp, for legend swatches. More stops + * means a smoother gradient; ten matches the anchor resolution. + */ +export const viridisGradient = (stopCount = 10, angle = '90deg'): string => { + const samples = viridisSamples(Math.max(2, stopCount)) + const stops = samples.map( + (color, index) => + `${color} ${Math.round((index / (samples.length - 1)) * 100)}%` + ) + return `linear-gradient(${angle}, ${stops.join(', ')})` +} + +/** Dark purple end of the ramp — lowest values. */ +export const VIRIDIS_LOW = VIRIDIS_ANCHORS[0] + +/** Teal middle of the ramp — mid values, and the neutral in diverging scales. */ +export const VIRIDIS_MID = viridisColor(0.5) + +/** Bright yellow end of the ramp — highest values. */ +export const VIRIDIS_HIGH = VIRIDIS_ANCHORS[VIRIDIS_ANCHORS.length - 1] diff --git a/src/generated/types.gen.ts b/src/generated/types.gen.ts index b0ef527f..858a261d 100644 --- a/src/generated/types.gen.ts +++ b/src/generated/types.gen.ts @@ -50,6 +50,30 @@ export type AddressResponse = { address_type: AddressType; }; +/** + * AssetAssociationResponse + */ +export type AssetAssociationResponse = { + /** + * Asset Id + */ + asset_id: number; + /** + * Thing Id + */ + thing_id?: number | null; +}; + +/** + * AssetAssociationUpdate + */ +export type AssetAssociationUpdate = { + /** + * Thing Id + */ + thing_id?: number | null; +}; + /** * AssetResponse */ @@ -131,6 +155,28 @@ export type BodyBulkUploadGroundwaterLevelsObservationGroundwaterLevelBulkUpload file: string; }; +/** + * Body_upload_and_record_asset_asset_upload_and_record_post + */ +export type BodyUploadAndRecordAssetAssetUploadAndRecordPost = { + /** + * File + */ + file: string; + /** + * Thing Id + */ + thing_id: number; + /** + * Label + */ + label?: string | null; + /** + * Name + */ + name?: string | null; +}; + /** * Body_upload_asset_asset_upload_post */ @@ -982,6 +1028,70 @@ export type FeatureCollectionResponse = { features?: Array; }; +/** + * FeedbackCreate + */ +export type FeedbackCreate = { + /** + * Type + */ + type: 'bug' | 'feature'; + /** + * Page Url + */ + page_url: string; + /** + * Reporter Name + */ + reporter_name?: string | null; + /** + * Reporter Email + */ + reporter_email?: string | null; + /** + * Browser + */ + browser?: string | null; + /** + * Submitted At + */ + submitted_at?: string | null; + /** + * What Happened + */ + what_happened?: string | null; + /** + * Severity + */ + severity?: string; + /** + * Problem + */ + problem?: string | null; + /** + * Who Would Use + */ + who_would_use?: string | null; + /** + * What It Should Do + */ + what_it_should_do?: string | null; +}; + +/** + * FeedbackResponse + */ +export type FeedbackResponse = { + /** + * Jira Key + */ + jira_key: string; + /** + * Jira Url + */ + jira_url: string; +}; + /** * FieldActivityResponse */ @@ -1131,6 +1241,88 @@ export type GeoJsonutmCoordinates = { horizontal_datum?: string; }; +/** + * GeothermalWellResponse + * + * Read model for a geothermal well sourced from the legacy NM_Wells mirror. + * + * NOTE: This currently reads directly from the ``NMW_WellHeaders`` / + * ``NMW_WellLocations`` staging tables (see ``db/nmw_legacy.py``). Once the + * NM_Wells -> Ocotillo transform lands, these rows will be backed by the + * ``thing`` table and ``thing_id`` will be populated. Until then ``thing_id`` + * is always ``None`` and ``well_data_id`` (legacy GUID) is the identifier. + */ +export type GeothermalWellResponse = { + /** + * Well Data Id + */ + well_data_id: string; + /** + * Thing Id + */ + thing_id?: number | null; + /** + * Api + */ + api?: string | null; + /** + * Name + */ + name?: string | null; + /** + * Well Number + */ + well_number?: string | null; + /** + * Well Class + */ + well_class?: string | null; + /** + * Well Type + */ + well_type?: string | null; + /** + * Status + */ + status?: string | null; + /** + * Operator + */ + operator?: string | null; + /** + * Owner + */ + owner?: string | null; + /** + * Total Depth + */ + total_depth?: number | null; + /** + * Completion Date + */ + completion_date?: string | null; + /** + * Has Geothermal Data + */ + has_geothermal_data?: boolean | null; + /** + * County + */ + county?: string | null; + /** + * State + */ + state?: string | null; + /** + * Latitude + */ + latitude?: number | null; + /** + * Longitude + */ + longitude?: number | null; +}; + /** * GroundwaterLevelObservationResponse */ @@ -1213,6 +1405,10 @@ export type GroupResponse = { * Parent Group Id */ parent_group_id: number | null; + /** + * Well Count + */ + well_count?: number; }; /** @@ -1611,6 +1807,32 @@ export type PageEmailResponse = { pages: number; }; +/** + * Page[GeothermalWellResponse] + */ +export type PageGeothermalWellResponse = { + /** + * Items + */ + items: Array; + /** + * Total + */ + total: number; + /** + * Page + */ + page: number; + /** + * Size + */ + size: number; + /** + * Pages + */ + pages: number; +}; + /** * Page[GroundwaterLevelObservationResponse] */ @@ -3860,7 +4082,7 @@ export type NoteType = 'Access' | 'Directions' | 'Communication' | 'Construction /** * organization */ -export type Organization = 'Unknown' | 'City of Aztec' | 'Daybreak Investments' | 'Vallecitos HOA' | 'SFC, Santa Fe Animal Shelter' | 'El Guicu Ditch Association' | 'Santa Fe Municipal Airport' | 'Uluru Development' | "AllSup's Convenience Stores" | 'Santa Fe Downs Resort' | 'City of Truth or Consequences, WWTP' | 'Riverbend Hotsprings' | 'Armendaris Ranch' | 'El Paso Water' | 'BLM, Socorro Field Office' | 'USFWS' | 'Sile MDWCA' | 'Pena Blanca Water & Sanitation District' | 'Town of Questa' | 'Town of Cerro' | 'Farr Cattle Company' | 'Carrizozo Orchard' | 'USFS, Kiowa Grasslands' | 'Cloud Country West Subdivision' | 'Chama West WUA' | 'El Rito Regional Water and Waste Water Association' | 'West Rim MDWUA' | 'Village of Willard' | 'Quemado Municipal Water & SWA' | 'Coyote Creek MDWUA' | 'Lamy MDWCA' | 'La Joya CWDA' | 'NM Firefighters Training Academy' | 'Cebolleta Land Grant' | 'Madrid Water Co-op' | 'Sun Valley Water and Sanitation' | 'Bluewater Lake MDWCA' | 'Bluewater Acres Domestic WUA' | 'Lybrook MDWCA' | 'New Mexico Museum of Natural History' | 'Hillsboro MDWCA' | 'Tyrone MDWCA' | 'Santa Clara Water System' | 'Casas Adobes MDWCA' | 'Lake Roberts WUA' | 'El Creston MDWCA' | 'Reserve Municipality Water Works' | 'Town of Estancia' | 'Pie Town MDWCA' | 'Roosevelt SWCD' | 'Otis MDWCA' | 'White Cliffs MDWUA' | 'Vista Linda Water Co-op' | 'Anasazi Trails Water Co-op' | 'Canon MDWCA' | 'Placitas Trails Water Co-op' | 'BLM, Roswell Office' | 'Forked Lightning Ranch' | 'Cottonwood RWA' | 'Pinon Ridge WUA' | 'McSherry Farms' | 'Agua Sana WUA' | 'Chamita MDWCA' | 'W Spear-bar Ranch' | 'Village of Capitan' | 'Brazos MDWCA' | 'Alto Alps HOA' | 'Chiricahua Desert Museum' | 'Bike Ranch' | 'Hachita MDWCA' | 'Carrizozo Municipal Water' | 'Dunhill Ranch' | 'Santa Fe Conservation Trust' | 'NMSU' | 'USGS' | 'TWDB' | 'NMED' | 'NMOSE' | 'NMBGMR' | 'Bernalillo County' | 'BLM' | 'BLM Taos Office' | 'SFC' | 'SFC, Fire Facilities' | 'SFC, Utilities Dept.' | 'SFC, Valle Vista Water Utility, Inc.' | 'City of Santa Fe' | 'City of Santa Fe WWTP' | 'City of Santa Fe, Municipal Recreation Complex' | 'City of Santa Fe, Sangre de Cristo Water Co.' | 'NMISC' | 'PVACD' | 'Bayard' | 'SNL' | 'USFS' | 'NMT' | 'NPS' | 'NMRWA' | 'NMDOT' | 'Taos SWCD' | 'Otero SWCD' | 'Northeastern SWCD' | 'CDWR' | 'Pendaries Village' | 'A&T Pump & Well Service, LLC' | 'A. G. Wassenaar, Inc' | 'AMEC' | 'Balleau Groundwater, Inc' | 'CDM Smith' | 'CH2M Hill' | 'Corbin Consulting, Inc' | 'Chevron' | 'Daniel B. Stephens & Associates, Inc' | 'EnecoTech' | 'Faith Engineering, Inc' | 'Foster Well Service, Inc' | 'Glorieta Geoscience, Inc' | 'Golder Associates, Inc' | "Hathorn's Well Service, Inc" | 'Hydroscience Associates, Inc' | 'IC Tech, Inc' | 'John Shomaker & Associates, Inc' | 'Kuckleman Pump Service' | 'Los Golondrinas' | 'Minton Engineers' | 'MJDarrconsult, Inc' | 'Puerta del Canon Ranch' | 'Rodgers & Company, Inc' | 'San Pedro Creek Estates HOA' | 'Statewide Drilling, Inc' | 'Tec Drilling Limited' | 'Tetra Tech, Inc' | 'Thompson Drilling, Inc' | 'Witcher & Associates' | 'Zeigler Geologic Consulting, LLC' | 'Sandia Well Service, Inc' | 'San Marcos Association' | 'URS' | 'Vista del Oro' | 'Abeyta Engineering, Inc' | 'Adobe Ranch' | 'Agua Fria Community Water Association' | 'Apache Gap Ranch' | 'Aspendale Mountain Retreat' | 'Augustin Plains Ranch LLC' | 'B & B Cattle Co' | 'Berridge Distributing Company' | "Bishop's Lodge" | 'Bonanza Creek Ranch' | 'Bug Scuffle Water Association' | 'Wehinahpay Mountain Camp' | 'Campbell Ranch' | 'Capitol Ford Santa Fe' | 'Cemex, Inc' | 'Cerro Community Center' | 'Santa Fe Jewish Center' | 'Chupadero MDWCA' | 'Cielo Lumbre HOA' | 'Circle Cross Ranch' | 'City of Alamogordo' | 'City of Portales, Public Works Dept.' | 'City of Socorro' | 'Commonwealth Conservancy' | 'Costilla MDWCA' | 'Country Club Garden Mobile Home Park' | 'Crossroads Cattle Co., Ltd' | 'Double H Ranch' | 'E.A. Meadows East' | 'El Camino Realty, Inc' | 'Eldorado Area Water & Sanitation District' | 'Bourbon Grill at El Gancho' | 'El Prado HOA' | 'El Rancho de las Golondrinas' | 'El Rito Canyon MDWCA' | 'Encantado Enterprises' | 'Estrella Concepts LLC' | 'Sixteen Springs Fire Department' | 'Fire Water Lodge' | 'Ford County Land & Cattle Company, Inc' | 'Friendly Construction, Inc' | 'Hacienda Del Cerezo' | 'Hefker Vega Ranch' | 'High Nogal Ranch' | 'Holloman Air Force Base' | 'Hyde Park Estates MDWCA' | 'Desert Village RV & Mobile Home Park' | 'K. Schmitt Trust' | 'La Cienega MDWCA' | 'La Vista HOA' | 'Land Ventures LLC' | 'Las Lagunitas' | 'Las Lagunitas HOA' | 'Living World Ministries' | 'Los Atrevidos, Inc' | 'Los Prados HOA' | 'Malaga MDWCA & SWA' | 'Mangas Outfitters' | 'Medina Gravel Pit' | 'Mendenhall Trading Co' | 'Mesa Verde Ranch' | 'NMDGF' | 'NMSU College of Agriculture' | 'Naiche Development' | 'NRAO' | 'NMSA' | 'Nogal MDWCA' | 'O Bar O Ranch' | 'OMI Wastewater Treatment Plant' | 'Old Road Ranch Pardners Ltd' | 'PNM Service Center' | 'Peace Tabernacle Church' | 'Pecos Trail Inn' | 'Pelican Spa' | 'Pistachio Tree Ranch' | 'Rancho Encantado' | 'Rancho San Lucas' | 'Rancho San Marcos' | 'Rancho Viejo Partnership' | 'Ranney Ranch' | 'Rio En Medio MDWCA' | 'San Acacia MDWCA' | 'San Juan Residences' | 'Sangre de Cristo Estates' | 'Santa Fe Community College' | 'Sangre de Cristo Center' | 'Santa Fe Horse Park' | 'Santa Fe Opera' | 'Santa Fe Waldorf School' | 'Shidoni Foundry and Gallery' | 'Sierra Grande Lodge' | 'Sierra Vista Retirement Community' | 'Slash Triangle Ranch' | 'Stagecoach Motel' | 'State of New Mexico' | 'Stephenson Ranch' | 'Sun Broadcasting Network' | 'Tano Rd LLC' | 'UNM-Taos' | 'Tee Pee Ranch/Tee Pee Subdivision' | 'Tent Rock, Inc' | 'Tesuque MDWCA' | 'The Great Cloud Zen Center' | 'Three Rivers Ranch' | 'Timberon Water and Sanitation District' | 'Town of Magdalena' | 'Town of Taos' | 'Town of Taos, National Guard Armory' | 'Trinity Ranch' | 'Tularosa Basin National Desalination Research Facility' | 'Turquoise Trail Charter School' | 'US Bureau of Indian Affairs, Santa Fe Indian School' | 'USFS, Carson NF, Taos Office' | 'USFS, Cibola NF, Magdalena Ranger District' | 'USFS, Santa Fe NF, Espanola Ranger District' | 'Ute Mountain Farms' | 'VA Hospital' | 'Velte' | 'Vereda Serena Property' | 'Village of Corona' | 'Village of Floyd' | 'Village of Melrose' | 'Village of Vaughn' | 'Vista Land Company' | 'Vista Redonda MDWCA' | 'Vista de Oro de Placitas Water Users Coop' | 'Walker Ranch' | 'Wild & Woolley Trailer Ranch' | 'Winter Brothers' | 'Yates Petroleum Corporation' | 'Zamora Accounting Services' | 'Agua Sana MWCD' | 'Canada Los Alamos MDWCA' | 'Canjilon Mutual Domestic Water System' | 'Cebolla Mutual Domestic' | 'Chihuahuan Desert Rangeland Research Center (CDRRC)' | 'East Rio Arriba SWCD' | 'El Prado Municipal Water' | 'Hachita Mutual Domestic' | 'Jornada Experimental Range (JER)' | 'La Canada Way HOA' | 'Los Ojos Mutual Domestic' | 'The Nature Conservancy (TNC)' | 'Smith Ranch LLC' | 'Zia Pueblo' | 'Our Lady of Guadalupe (OLG)' | 'PLSS'; +export type Organization = 'Unknown' | 'City of Aztec' | 'Daybreak Investments' | 'Vallecitos HOA' | 'SFC, Santa Fe Animal Shelter' | 'El Guicu Ditch Association' | 'Santa Fe Municipal Airport' | 'Uluru Development' | "AllSup's Convenience Stores" | 'Santa Fe Downs Resort' | 'City of Truth or Consequences, WWTP' | 'Riverbend Hotsprings' | 'Armendaris Ranch' | 'El Paso Water' | 'BLM, Socorro Field Office' | 'USFWS' | 'Sile MDWCA' | 'Pena Blanca Water & Sanitation District' | 'Town of Questa' | 'Town of Cerro' | 'Cerro MDWCA' | 'Farr Cattle Company' | 'Carrizozo Orchard' | 'White Oaks Pottery' | 'USFS, Kiowa Grasslands' | 'Cloud Country West Subdivision' | 'Chama West WUA' | 'El Rito Regional Water and Waste Water Association' | 'El Rito MDWCA' | 'West Rim MDWUA' | 'Village of Willard' | 'Quemado Municipal Water & SWA' | 'Coyote Creek MDWUA' | 'Lamy MDWCA' | 'La Joya CWDA' | 'NM Firefighters Training Academy' | 'Cebolleta Land Grant' | 'Madrid Water Co-op' | 'Sun Valley Water and Sanitation' | 'Bluewater Lake MDWCA' | 'Bluewater Acres Domestic WUA' | 'Lybrook MDWCA' | 'New Mexico Museum of Natural History' | 'Hillsboro MDWCA' | 'Tyrone MDWCA' | 'Santa Clara Water System' | 'Casas Adobes MDWCA' | 'Lake Roberts WUA' | 'El Creston MDWCA' | 'Reserve Municipality Water Works' | 'Town of Estancia' | 'Pie Town MDWCA' | 'Roosevelt SWCD' | 'Otis MDWCA' | 'White Cliffs MDWUA' | 'Vista Linda Water Co-op' | 'Anasazi Trails Water Co-op' | 'Canon MDWCA' | 'Placitas Trails Water Co-op' | 'BLM, Roswell Office' | 'Forked Lightning Ranch' | 'Cottonwood RWA' | 'Pinon Ridge WUA' | 'McSherry Farms' | 'Agua Sana WUA' | 'Chamita MDWCA' | 'W Spear-bar Ranch' | 'Village of Capitan' | 'Brazos MDWCA' | 'Alto Alps HOA' | 'Chiricahua Desert Museum' | 'Bike Ranch' | 'Hachita MDWCA' | 'Carrizozo Municipal Water' | 'Dunhill Ranch' | 'Santa Fe Conservation Trust' | 'NMSU' | 'USGS' | 'TWDB' | 'NMED' | 'NMOSE' | 'NMBGMR' | 'Bernalillo County' | 'BLM' | 'BLM Taos Office' | 'SFC' | 'SFC, Fire Facilities' | 'SFC, Utilities Dept.' | 'SFC, Valle Vista Water Utility, Inc.' | 'City of Santa Fe' | 'City of Santa Fe WWTP' | 'City of Santa Fe, Municipal Recreation Complex' | 'City of Santa Fe, Sangre de Cristo Water Co.' | 'NMISC' | 'PVACD' | 'Bayard' | 'SNL' | 'USFS' | 'NMT' | 'NPS' | 'NMRWA' | 'NMDOT' | 'Taos SWCD' | 'Otero SWCD' | 'Northeastern SWCD' | 'CDWR' | 'Pendaries Village' | 'A&T Pump & Well Service, LLC' | 'A. G. Wassenaar, Inc' | 'AMEC' | 'Balleau Groundwater, Inc' | 'CDM Smith' | 'CH2M Hill' | 'Corbin Consulting, Inc' | 'Chevron' | 'Daniel B. Stephens & Associates, Inc' | 'EnecoTech' | 'Faith Engineering, Inc' | 'Foster Well Service, Inc' | 'Glorieta Geoscience, Inc' | 'Golder Associates, Inc' | "Hathorn's Well Service, Inc" | 'Hydroscience Associates, Inc' | 'IC Tech, Inc' | 'John Shomaker & Associates, Inc' | 'Kuckleman Pump Service' | 'Los Golondrinas' | 'Minton Engineers' | 'MJDarrconsult, Inc' | 'Puerta del Canon Ranch' | 'Rodgers & Company, Inc' | 'San Pedro Creek Estates HOA' | 'Statewide Drilling, Inc' | 'Tec Drilling Limited' | 'Tetra Tech, Inc' | 'Thompson Drilling, Inc' | 'Witcher & Associates' | 'Zeigler Geologic Consulting, LLC' | 'Sandia Well Service, Inc' | 'San Marcos Association' | 'URS' | 'Vista del Oro' | 'Abeyta Engineering, Inc' | 'Adobe Ranch' | 'Agua Fria Community Water Association' | 'Apache Gap Ranch' | 'Aspendale Mountain Retreat' | 'Augustin Plains Ranch LLC' | 'B & B Cattle Co' | 'Berridge Distributing Company' | "Bishop's Lodge" | 'Bonanza Creek Ranch' | 'Bug Scuffle Water Association' | 'Wehinahpay Mountain Camp' | 'Campbell Ranch' | 'Capitol Ford Santa Fe' | 'Cemex, Inc' | 'Cerro Community Center' | 'Santa Fe Jewish Center' | 'Chupadero MDWCA' | 'Cielo Lumbre HOA' | 'Circle Cross Ranch' | 'City of Alamogordo' | 'City of Portales, Public Works Dept.' | 'City of Socorro' | 'Commonwealth Conservancy' | 'Costilla MDWCA' | 'Country Club Garden Mobile Home Park' | 'Crossroads Cattle Co., Ltd' | 'Double H Ranch' | 'E.A. Meadows East' | 'El Camino Realty, Inc' | 'Eldorado Area Water & Sanitation District' | 'Bourbon Grill at El Gancho' | 'El Prado HOA' | 'El Rancho de las Golondrinas' | 'El Rito Canyon MDWCA' | 'Encantado Enterprises' | 'Estrella Concepts LLC' | 'Sixteen Springs Fire Department' | 'Fire Water Lodge' | 'Ford County Land & Cattle Company, Inc' | 'Friendly Construction, Inc' | 'Hacienda Del Cerezo' | 'Hefker Vega Ranch' | 'High Nogal Ranch' | 'Holloman Air Force Base' | 'Hyde Park Estates MDWCA' | 'Desert Village RV & Mobile Home Park' | 'K. Schmitt Trust' | 'La Cienega MDWCA' | 'La Vista HOA' | 'Land Ventures LLC' | 'Las Lagunitas' | 'Las Lagunitas HOA' | 'Lightning Dock Zanskar' | 'Living World Ministries' | 'Los Atrevidos, Inc' | 'Los Prados HOA' | 'Malaga MDWCA & SWA' | 'Mangas Outfitters' | 'Medina Gravel Pit' | 'Mendenhall Trading Co' | 'Mesa Verde Ranch' | 'NMDGF' | 'NMSU College of Agriculture' | 'Naiche Development' | 'NRAO' | 'NMSA' | 'Nogal MDWCA' | 'O Bar O Ranch' | 'OMI Wastewater Treatment Plant' | 'Old Road Ranch Pardners Ltd' | 'PNM Service Center' | 'Peace Tabernacle Church' | 'Pecos Trail Inn' | 'Pelican Spa' | 'Pistachio Tree Ranch' | 'Rancho Encantado' | 'Rancho San Lucas' | 'Rancho San Marcos' | 'Rancho Viejo Partnership' | 'Ranney Ranch' | 'Rio En Medio MDWCA' | 'San Acacia MDWCA' | 'San Juan Residences' | 'Sangre de Cristo Estates' | 'Santa Fe Community College' | 'Sangre de Cristo Center' | 'Santa Fe Horse Park' | 'Santa Fe Opera' | 'Santa Fe Waldorf School' | 'Shidoni Foundry and Gallery' | 'Sierra Grande Lodge' | 'Sierra Vista Retirement Community' | 'Slash Triangle Ranch' | 'Spanish Stirrup Rockshop' | 'Sparrowhawk Farm' | 'Stagecoach Motel' | 'State of New Mexico' | 'Stephenson Ranch' | 'Sun Broadcasting Network' | 'Tano Rd LLC' | 'UNM-Taos' | 'Tee Pee Ranch/Tee Pee Subdivision' | 'Tent Rock, Inc' | 'Tesuque MDWCA' | 'The Great Cloud Zen Center' | 'Three Rivers Ranch' | 'Timberon Water and Sanitation District' | 'Town of Magdalena' | 'Town of Taos' | 'Town of Taos, National Guard Armory' | 'Trinity Ranch' | 'Tularosa Basin National Desalination Research Facility' | 'Turquoise Trail Charter School' | 'US Bureau of Indian Affairs, Santa Fe Indian School' | 'USFS, Carson NF, Taos Office' | 'USFS, Cibola NF, Magdalena Ranger District' | "USFS, Cibola NF, Supervisor's Office" | 'USFS, Santa Fe NF, Espanola Ranger District' | 'Ute Mountain Farms' | 'VA Hospital' | 'Velte' | 'Vereda Serena Property' | 'Village of Corona' | 'Village of Floyd' | 'Village of Melrose' | 'Village of Vaughn' | 'Vista Land Company' | 'Vista Redonda MDWCA' | 'Vista de Oro de Placitas Water Users Coop' | 'Walker Ranch' | 'Wild & Woolley Trailer Ranch' | 'Winter Brothers' | 'Yates Petroleum Corporation' | 'Zamora Accounting Services' | 'Agua Sana MWCD' | 'Canada Los Alamos MDWCA' | 'Canjilon Mutual Domestic Water System' | 'Cebolla Mutual Domestic' | 'Chihuahuan Desert Rangeland Research Center (CDRRC)' | 'East Rio Arriba SWCD' | 'El Prado Municipal Water' | 'Hachita Mutual Domestic' | 'Jornada Experimental Range (JER)' | 'La Canada Way HOA' | 'Los Ojos Mutual Domestic' | 'The Nature Conservancy (TNC)' | 'Smith Ranch LLC' | 'Santa Ana Pueblo Department of Natural Resources' | 'Village of Hope' | 'WSP' | 'Zia Pueblo' | 'Our Lady of Guadalupe (OLG)' | 'PLSS'; /** * origin_type @@ -3993,6 +4215,20 @@ export type WellPumpType = 'Submersible' | 'Jet' | 'Line Shaft' | 'Hand' | 'Wind */ export type WellPurpose = 'Unknown' | 'Open, unequipped well' | 'Commercial' | 'Domestic' | 'Power generation' | 'Irrigation' | 'Livestock' | 'Mining' | 'Industrial' | 'Observation' | 'Public supply' | 'Shared domestic' | 'Institutional' | 'Unused' | 'Exploration' | 'Monitoring' | 'Production' | 'Injection'; +export type HealthHealthGetData = { + body?: never; + path?: never; + query?: never; + url: '/health'; +}; + +export type HealthHealthGetResponses = { + /** + * Successful Response + */ + 200: unknown; +}; + export type UploadAssetAssetUploadPostData = { body: BodyUploadAssetAssetUploadPost; path?: never; @@ -4022,6 +4258,31 @@ export type UploadAssetAssetUploadPostResponses = { export type UploadAssetAssetUploadPostResponse = UploadAssetAssetUploadPostResponses[keyof UploadAssetAssetUploadPostResponses]; +export type UploadAndRecordAssetAssetUploadAndRecordPostData = { + body: BodyUploadAndRecordAssetAssetUploadAndRecordPost; + path?: never; + query?: never; + url: '/asset/upload-and-record'; +}; + +export type UploadAndRecordAssetAssetUploadAndRecordPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type UploadAndRecordAssetAssetUploadAndRecordPostError = UploadAndRecordAssetAssetUploadAndRecordPostErrors[keyof UploadAndRecordAssetAssetUploadAndRecordPostErrors]; + +export type UploadAndRecordAssetAssetUploadAndRecordPostResponses = { + /** + * Successful Response + */ + 201: AssetResponse; +}; + +export type UploadAndRecordAssetAssetUploadAndRecordPostResponse = UploadAndRecordAssetAssetUploadAndRecordPostResponses[keyof UploadAndRecordAssetAssetUploadAndRecordPostResponses]; + export type ListAssetsAssetGetData = { body?: never; path?: never; @@ -4087,6 +4348,42 @@ export type AddAssetAssetPostResponses = { export type AddAssetAssetPostResponse = AddAssetAssetPostResponses[keyof AddAssetAssetPostResponses]; +export type ListUnassociatedAssetsAssetUnassociatedGetData = { + body?: never; + path?: never; + query?: { + /** + * Page + * + * Page number + */ + page?: number; + /** + * Size + */ + size?: number; + }; + url: '/asset/unassociated'; +}; + +export type ListUnassociatedAssetsAssetUnassociatedGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type ListUnassociatedAssetsAssetUnassociatedGetError = ListUnassociatedAssetsAssetUnassociatedGetErrors[keyof ListUnassociatedAssetsAssetUnassociatedGetErrors]; + +export type ListUnassociatedAssetsAssetUnassociatedGetResponses = { + /** + * Successful Response + */ + 200: PageAssetResponse; +}; + +export type ListUnassociatedAssetsAssetUnassociatedGetResponse = ListUnassociatedAssetsAssetUnassociatedGetResponses[keyof ListUnassociatedAssetsAssetUnassociatedGetResponses]; + export type DeleteAssetAssetAssetIdDeleteData = { body?: never; path: { @@ -4175,6 +4472,36 @@ export type UpdateAssetAssetAssetIdPatchResponses = { 200: unknown; }; +export type UpdateAssetThingAssociationAssetAssetIdAssociationPatchData = { + body: AssetAssociationUpdate; + path: { + /** + * Asset Id + */ + asset_id: number; + }; + query?: never; + url: '/asset/{asset_id}/association'; +}; + +export type UpdateAssetThingAssociationAssetAssetIdAssociationPatchErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type UpdateAssetThingAssociationAssetAssetIdAssociationPatchError = UpdateAssetThingAssociationAssetAssetIdAssociationPatchErrors[keyof UpdateAssetThingAssociationAssetAssetIdAssociationPatchErrors]; + +export type UpdateAssetThingAssociationAssetAssetIdAssociationPatchResponses = { + /** + * Successful Response + */ + 200: AssetAssociationResponse; +}; + +export type UpdateAssetThingAssociationAssetAssetIdAssociationPatchResponse = UpdateAssetThingAssociationAssetAssetIdAssociationPatchResponses[keyof UpdateAssetThingAssociationAssetAssetIdAssociationPatchResponses]; + export type RemoveAssetAssetAssetIdRemoveDeleteData = { body?: never; path: { @@ -4252,7 +4579,7 @@ export type GetContactsContactGetData = { /** * Filter */ - filter?: string; + filter?: Array | null; /** * Thing Id */ @@ -4972,6 +5299,36 @@ export type GetContactAddressesContactContactIdAddressGetResponses = { export type GetContactAddressesContactContactIdAddressGetResponse = GetContactAddressesContactContactIdAddressGetResponses[keyof GetContactAddressesContactContactIdAddressGetResponses]; +export type GetDisclaimerDisclaimerGetData = { + body?: never; + path?: never; + query?: { + /** + * F + * + * Response format. Use 'json' for the text as data. + */ + f?: string | null; + }; + url: '/disclaimer'; +}; + +export type GetDisclaimerDisclaimerGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetDisclaimerDisclaimerGetError = GetDisclaimerDisclaimerGetErrors[keyof GetDisclaimerDisclaimerGetErrors]; + +export type GetDisclaimerDisclaimerGetResponses = { + /** + * The disclaimer as HTML (default) or JSON (?f=json). + */ + 200: unknown; +}; + export type GetGeospatialGeospatialGetData = { body?: never; path?: never; @@ -5105,6 +5462,72 @@ export type CreateGroupGroupPostResponses = { export type CreateGroupGroupPostResponse = CreateGroupGroupPostResponses[keyof CreateGroupGroupPostResponses]; +export type RemoveThingFromGroupRouteGroupGroupIdThingsThingIdDeleteData = { + body?: never; + path: { + /** + * Group Id + */ + group_id: number; + /** + * Thing Id + */ + thing_id: number; + }; + query?: never; + url: '/group/{group_id}/things/{thing_id}'; +}; + +export type RemoveThingFromGroupRouteGroupGroupIdThingsThingIdDeleteErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type RemoveThingFromGroupRouteGroupGroupIdThingsThingIdDeleteError = RemoveThingFromGroupRouteGroupGroupIdThingsThingIdDeleteErrors[keyof RemoveThingFromGroupRouteGroupGroupIdThingsThingIdDeleteErrors]; + +export type RemoveThingFromGroupRouteGroupGroupIdThingsThingIdDeleteResponses = { + /** + * Successful Response + */ + 204: void; +}; + +export type RemoveThingFromGroupRouteGroupGroupIdThingsThingIdDeleteResponse = RemoveThingFromGroupRouteGroupGroupIdThingsThingIdDeleteResponses[keyof RemoveThingFromGroupRouteGroupGroupIdThingsThingIdDeleteResponses]; + +export type AddThingToGroupRouteGroupGroupIdThingsThingIdPostData = { + body?: never; + path: { + /** + * Group Id + */ + group_id: number; + /** + * Thing Id + */ + thing_id: number; + }; + query?: never; + url: '/group/{group_id}/things/{thing_id}'; +}; + +export type AddThingToGroupRouteGroupGroupIdThingsThingIdPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type AddThingToGroupRouteGroupGroupIdThingsThingIdPostError = AddThingToGroupRouteGroupGroupIdThingsThingIdPostErrors[keyof AddThingToGroupRouteGroupGroupIdThingsThingIdPostErrors]; + +export type AddThingToGroupRouteGroupGroupIdThingsThingIdPostResponses = { + /** + * Successful Response + */ + 201: unknown; +}; + export type DeleteGroupGroupGroupIdDeleteData = { body?: never; path: { @@ -6787,6 +7210,86 @@ export type SearchApiSearchGetResponses = { export type SearchApiSearchGetResponse = SearchApiSearchGetResponses[keyof SearchApiSearchGetResponses]; +export type GetGeothermalWellsThingGeothermalWellGetData = { + body?: never; + path?: never; + query?: { + /** + * County + */ + county?: string | null; + /** + * Name Contains + */ + name_contains?: string | null; + /** + * Q + * + * Free-text search across well name, API, well number, operator and county. Whitespace-separated words are ANDed, so each word added narrows the result. Case-insensitive substring match. + */ + q?: string | null; + /** + * Page + * + * Page number + */ + page?: number; + /** + * Size + */ + size?: number; + }; + url: '/thing/geothermal-well'; +}; + +export type GetGeothermalWellsThingGeothermalWellGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetGeothermalWellsThingGeothermalWellGetError = GetGeothermalWellsThingGeothermalWellGetErrors[keyof GetGeothermalWellsThingGeothermalWellGetErrors]; + +export type GetGeothermalWellsThingGeothermalWellGetResponses = { + /** + * Successful Response + */ + 200: PageGeothermalWellResponse; +}; + +export type GetGeothermalWellsThingGeothermalWellGetResponse = GetGeothermalWellsThingGeothermalWellGetResponses[keyof GetGeothermalWellsThingGeothermalWellGetResponses]; + +export type GetGeothermalWellThingGeothermalWellWellDataIdGetData = { + body?: never; + path: { + /** + * Well Data Id + */ + well_data_id: string; + }; + query?: never; + url: '/thing/geothermal-well/{well_data_id}'; +}; + +export type GetGeothermalWellThingGeothermalWellWellDataIdGetErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type GetGeothermalWellThingGeothermalWellWellDataIdGetError = GetGeothermalWellThingGeothermalWellWellDataIdGetErrors[keyof GetGeothermalWellThingGeothermalWellWellDataIdGetErrors]; + +export type GetGeothermalWellThingGeothermalWellWellDataIdGetResponses = { + /** + * Successful Response + */ + 200: GeothermalWellResponse; +}; + +export type GetGeothermalWellThingGeothermalWellWellDataIdGetResponse = GetGeothermalWellThingGeothermalWellWellDataIdGetResponses[keyof GetGeothermalWellThingGeothermalWellWellDataIdGetResponses]; + export type GetWaterWellsThingWaterWellGetData = { body?: never; path?: never; @@ -6802,7 +7305,7 @@ export type GetWaterWellsThingWaterWellGetData = { /** * Filter */ - filter?: string; + filter?: Array | null; /** * Query */ @@ -6811,6 +7314,10 @@ export type GetWaterWellsThingWaterWellGetData = { * Name */ name?: string | null; + /** + * Name Contains + */ + name_contains?: string | null; /** * Include Contacts */ @@ -7148,11 +7655,15 @@ export type GetSpringsThingSpringGetData = { /** * Filter */ - filter?: string; + filter?: Array | null; /** * Query */ query?: string; + /** + * Name Contains + */ + name_contains?: string | null; /** * Page * @@ -7460,7 +7971,11 @@ export type GetThingsThingGetData = { /** * Filter */ - filter?: string; + filter?: Array | null; + /** + * Name Contains + */ + name_contains?: string | null; /** * Page * @@ -7778,3 +8293,33 @@ export type ReadNgwmnLithologyNgwmnLithologyPointidGetResponses = { */ 200: unknown; }; + +export type CreateFeedbackFeedbackPostData = { + body: FeedbackCreate; + path?: never; + query?: { + /** + * User + */ + _user?: unknown; + }; + url: '/feedback'; +}; + +export type CreateFeedbackFeedbackPostErrors = { + /** + * Validation Error + */ + 422: HttpValidationError; +}; + +export type CreateFeedbackFeedbackPostError = CreateFeedbackFeedbackPostErrors[keyof CreateFeedbackFeedbackPostErrors]; + +export type CreateFeedbackFeedbackPostResponses = { + /** + * Successful Response + */ + 200: FeedbackResponse; +}; + +export type CreateFeedbackFeedbackPostResponse = CreateFeedbackFeedbackPostResponses[keyof CreateFeedbackFeedbackPostResponses]; diff --git a/src/generated/zod.gen.ts b/src/generated/zod.gen.ts index 88b206a2..506f1414 100644 --- a/src/generated/zod.gen.ts +++ b/src/generated/zod.gen.ts @@ -57,6 +57,27 @@ export const zAddressResponse = z.object({ address_type: zAddressType }); +/** + * AssetAssociationResponse + */ +export const zAssetAssociationResponse = z.object({ + asset_id: z.int(), + thing_id: z.optional(z.union([ + z.int(), + z.null() + ])) +}); + +/** + * AssetAssociationUpdate + */ +export const zAssetAssociationUpdate = z.object({ + thing_id: z.optional(z.union([ + z.int(), + z.null() + ])) +}); + /** * AssetResponse */ @@ -105,6 +126,22 @@ export const zBodyBulkUploadGroundwaterLevelsObservationGroundwaterLevelBulkUplo file: z.string() }); +/** + * Body_upload_and_record_asset_asset_upload_and_record_post + */ +export const zBodyUploadAndRecordAssetAssetUploadAndRecordPost = z.object({ + file: z.string(), + thing_id: z.int(), + label: z.optional(z.union([ + z.string(), + z.null() + ])), + name: z.optional(z.union([ + z.string(), + z.null() + ])) +}); + /** * Body_upload_asset_asset_upload_post */ @@ -1466,6 +1503,58 @@ export const zFeatureCollectionResponse = z.object({ features: z.optional(z.array(zFeature)).default([]) }); +/** + * FeedbackCreate + */ +export const zFeedbackCreate = z.object({ + type: z.enum([ + 'bug', + 'feature' + ]), + page_url: z.string(), + reporter_name: z.optional(z.union([ + z.string(), + z.null() + ])), + reporter_email: z.optional(z.union([ + z.string(), + z.null() + ])), + browser: z.optional(z.union([ + z.string(), + z.null() + ])), + submitted_at: z.optional(z.union([ + z.string(), + z.null() + ])), + what_happened: z.optional(z.union([ + z.string(), + z.null() + ])), + severity: z.optional(z.string()).default('Low'), + problem: z.optional(z.union([ + z.string(), + z.null() + ])), + who_would_use: z.optional(z.union([ + z.string(), + z.null() + ])), + what_it_should_do: z.optional(z.union([ + z.string(), + z.null() + ])) +}); + +/** + * FeedbackResponse + */ +export const zFeedbackResponse = z.object({ + jira_key: z.string(), + jira_url: z.string() +}); + /** * activity_type */ @@ -1587,6 +1676,87 @@ export const zGeoJsonProperties = z.object({ ])) }); +/** + * GeothermalWellResponse + * + * Read model for a geothermal well sourced from the legacy NM_Wells mirror. + * + * NOTE: This currently reads directly from the ``NMW_WellHeaders`` / + * ``NMW_WellLocations`` staging tables (see ``db/nmw_legacy.py``). Once the + * NM_Wells -> Ocotillo transform lands, these rows will be backed by the + * ``thing`` table and ``thing_id`` will be populated. Until then ``thing_id`` + * is always ``None`` and ``well_data_id`` (legacy GUID) is the identifier. + */ +export const zGeothermalWellResponse = z.object({ + well_data_id: z.uuid(), + thing_id: z.optional(z.union([ + z.int(), + z.null() + ])), + api: z.optional(z.union([ + z.string(), + z.null() + ])), + name: z.optional(z.union([ + z.string(), + z.null() + ])), + well_number: z.optional(z.union([ + z.string(), + z.null() + ])), + well_class: z.optional(z.union([ + z.string(), + z.null() + ])), + well_type: z.optional(z.union([ + z.string(), + z.null() + ])), + status: z.optional(z.union([ + z.string(), + z.null() + ])), + operator: z.optional(z.union([ + z.string(), + z.null() + ])), + owner: z.optional(z.union([ + z.string(), + z.null() + ])), + total_depth: z.optional(z.union([ + z.number(), + z.null() + ])), + completion_date: z.optional(z.union([ + z.iso.datetime({ + offset: true + }), + z.null() + ])), + has_geothermal_data: z.optional(z.union([ + z.boolean(), + z.null() + ])), + county: z.optional(z.union([ + z.string(), + z.null() + ])), + state: z.optional(z.union([ + z.string(), + z.null() + ])), + latitude: z.optional(z.union([ + z.number(), + z.null() + ])), + longitude: z.optional(z.union([ + z.number(), + z.null() + ])) +}); + /** * parameter_name */ @@ -1820,7 +1990,8 @@ export const zGroupResponse = z.object({ parent_group_id: z.union([ z.int(), z.null() - ]) + ]), + well_count: z.optional(z.int()).default(0) }); /** @@ -2065,6 +2236,17 @@ export const zPageEmailResponse = z.object({ pages: z.int().gte(0) }); +/** + * Page[GeothermalWellResponse] + */ +export const zPageGeothermalWellResponse = z.object({ + items: z.array(zGeothermalWellResponse), + total: z.int().gte(0), + page: z.int().gte(1), + size: z.int().gte(1), + pages: z.int().gte(0) +}); + /** * Page[GroundwaterLevelObservationResponse] */ @@ -2177,12 +2359,15 @@ export const zOrganization = z.enum([ 'Pena Blanca Water & Sanitation District', 'Town of Questa', 'Town of Cerro', + 'Cerro MDWCA', 'Farr Cattle Company', 'Carrizozo Orchard', + 'White Oaks Pottery', 'USFS, Kiowa Grasslands', 'Cloud Country West Subdivision', 'Chama West WUA', 'El Rito Regional Water and Waste Water Association', + 'El Rito MDWCA', 'West Rim MDWUA', 'Village of Willard', 'Quemado Municipal Water & SWA', @@ -2349,6 +2534,7 @@ export const zOrganization = z.enum([ 'Land Ventures LLC', 'Las Lagunitas', 'Las Lagunitas HOA', + 'Lightning Dock Zanskar', 'Living World Ministries', 'Los Atrevidos, Inc', 'Los Prados HOA', @@ -2389,6 +2575,8 @@ export const zOrganization = z.enum([ 'Sierra Grande Lodge', 'Sierra Vista Retirement Community', 'Slash Triangle Ranch', + 'Spanish Stirrup Rockshop', + 'Sparrowhawk Farm', 'Stagecoach Motel', 'State of New Mexico', 'Stephenson Ranch', @@ -2410,6 +2598,7 @@ export const zOrganization = z.enum([ 'US Bureau of Indian Affairs, Santa Fe Indian School', 'USFS, Carson NF, Taos Office', 'USFS, Cibola NF, Magdalena Ranger District', + "USFS, Cibola NF, Supervisor's Office", 'USFS, Santa Fe NF, Espanola Ranger District', 'Ute Mountain Farms', 'VA Hospital', @@ -2440,6 +2629,9 @@ export const zOrganization = z.enum([ 'Los Ojos Mutual Domestic', 'The Nature Conservancy (TNC)', 'Smith Ranch LLC', + 'Santa Ana Pueblo Department of Natural Resources', + 'Village of Hope', + 'WSP', 'Zia Pueblo', 'Our Lady of Guadalupe (OLG)', 'PLSS' @@ -3869,6 +4061,12 @@ export const zWellExportResponse = z.object({ deployments: z.optional(z.array(zDeploymentResponse)) }); +export const zHealthHealthGetData = z.object({ + body: z.optional(z.never()), + path: z.optional(z.never()), + query: z.optional(z.never()) +}); + export const zUploadAssetAssetUploadPostData = z.object({ body: zBodyUploadAssetAssetUploadPost, path: z.optional(z.never()), @@ -3882,6 +4080,17 @@ export const zUploadAssetAssetUploadPostData = z.object({ */ export const zUploadAssetAssetUploadPostResponse = z.record(z.string(), z.unknown()); +export const zUploadAndRecordAssetAssetUploadAndRecordPostData = z.object({ + body: zBodyUploadAndRecordAssetAssetUploadAndRecordPost, + path: z.optional(z.never()), + query: z.optional(z.never()) +}); + +/** + * Successful Response + */ +export const zUploadAndRecordAssetAssetUploadAndRecordPostResponse = zAssetResponse; + export const zListAssetsAssetGetData = z.object({ body: z.optional(z.never()), path: z.optional(z.never()), @@ -3908,6 +4117,20 @@ export const zAddAssetAssetPostData = z.object({ */ export const zAddAssetAssetPostResponse = zAssetResponse; +export const zListUnassociatedAssetsAssetUnassociatedGetData = z.object({ + body: z.optional(z.never()), + path: z.optional(z.never()), + query: z.optional(z.object({ + page: z.optional(z.int().gte(1)).default(1), + size: z.optional(z.int().gte(1).lte(10000)).default(25) + })) +}); + +/** + * Successful Response + */ +export const zListUnassociatedAssetsAssetUnassociatedGetResponse = zPageAssetResponse; + export const zDeleteAssetAssetAssetIdDeleteData = z.object({ body: z.optional(z.never()), path: z.object({ @@ -3942,6 +4165,19 @@ export const zUpdateAssetAssetAssetIdPatchData = z.object({ query: z.optional(z.never()) }); +export const zUpdateAssetThingAssociationAssetAssetIdAssociationPatchData = z.object({ + body: zAssetAssociationUpdate, + path: z.object({ + asset_id: z.int() + }), + query: z.optional(z.never()) +}); + +/** + * Successful Response + */ +export const zUpdateAssetThingAssociationAssetAssetIdAssociationPatchResponse = zAssetAssociationResponse; + export const zRemoveAssetAssetAssetIdRemoveDeleteData = z.object({ body: z.optional(z.never()), path: z.object({ @@ -3976,7 +4212,10 @@ export const zGetContactsContactGetData = z.object({ query: z.optional(z.object({ sort: z.optional(z.string()), order: z.optional(z.string()), - filter: z.optional(z.string()), + filter: z.optional(z.union([ + z.array(z.string()), + z.null() + ])), thing_id: z.optional(z.union([ z.int(), z.null() @@ -4261,6 +4500,17 @@ export const zGetContactAddressesContactContactIdAddressGetData = z.object({ */ export const zGetContactAddressesContactContactIdAddressGetResponse = zPageAddressResponse; +export const zGetDisclaimerDisclaimerGetData = z.object({ + body: z.optional(z.never()), + path: z.optional(z.never()), + query: z.optional(z.object({ + f: z.optional(z.union([ + z.string(), + z.null() + ])) + })) +}); + export const zGetGeospatialGeospatialGetData = z.object({ body: z.optional(z.never()), path: z.optional(z.never()), @@ -4313,6 +4563,29 @@ export const zCreateGroupGroupPostData = z.object({ */ export const zCreateGroupGroupPostResponse = zGroupResponse; +export const zRemoveThingFromGroupRouteGroupGroupIdThingsThingIdDeleteData = z.object({ + body: z.optional(z.never()), + path: z.object({ + group_id: z.int(), + thing_id: z.int() + }), + query: z.optional(z.never()) +}); + +/** + * Successful Response + */ +export const zRemoveThingFromGroupRouteGroupGroupIdThingsThingIdDeleteResponse = z.void(); + +export const zAddThingToGroupRouteGroupGroupIdThingsThingIdPostData = z.object({ + body: z.optional(z.never()), + path: z.object({ + group_id: z.int(), + thing_id: z.int() + }), + query: z.optional(z.never()) +}); + export const zDeleteGroupGroupGroupIdDeleteData = z.object({ body: z.optional(z.never()), path: z.object({ @@ -5086,6 +5359,45 @@ export const zSearchApiSearchGetData = z.object({ */ export const zSearchApiSearchGetResponse = zPageDict; +export const zGetGeothermalWellsThingGeothermalWellGetData = z.object({ + body: z.optional(z.never()), + path: z.optional(z.never()), + query: z.optional(z.object({ + county: z.optional(z.union([ + z.string(), + z.null() + ])), + name_contains: z.optional(z.union([ + z.string(), + z.null() + ])), + q: z.optional(z.union([ + z.string(), + z.null() + ])), + page: z.optional(z.int().gte(1)).default(1), + size: z.optional(z.int().gte(1).lte(10000)).default(25) + })) +}); + +/** + * Successful Response + */ +export const zGetGeothermalWellsThingGeothermalWellGetResponse = zPageGeothermalWellResponse; + +export const zGetGeothermalWellThingGeothermalWellWellDataIdGetData = z.object({ + body: z.optional(z.never()), + path: z.object({ + well_data_id: z.uuid() + }), + query: z.optional(z.never()) +}); + +/** + * Successful Response + */ +export const zGetGeothermalWellThingGeothermalWellWellDataIdGetResponse = zGeothermalWellResponse; + export const zGetWaterWellsThingWaterWellGetData = z.object({ body: z.optional(z.never()), path: z.optional(z.never()), @@ -5098,7 +5410,10 @@ export const zGetWaterWellsThingWaterWellGetData = z.object({ z.string(), z.null() ])), - filter: z.optional(z.string()), + filter: z.optional(z.union([ + z.array(z.string()), + z.null() + ])), query: z.optional(z.union([ z.string(), z.null() @@ -5107,6 +5422,10 @@ export const zGetWaterWellsThingWaterWellGetData = z.object({ z.string(), z.null() ])), + name_contains: z.optional(z.union([ + z.string(), + z.null() + ])), include_contacts: z.optional(z.boolean()).default(false), page: z.optional(z.int().gte(1)).default(1), size: z.optional(z.int().gte(1).lte(10000)).default(25) @@ -5244,8 +5563,15 @@ export const zGetSpringsThingSpringGetData = z.object({ query: z.optional(z.object({ sort: z.optional(z.string()), order: z.optional(z.string()), - filter: z.optional(z.string()), + filter: z.optional(z.union([ + z.array(z.string()), + z.null() + ])), query: z.optional(z.string()), + name_contains: z.optional(z.union([ + z.string(), + z.null() + ])), page: z.optional(z.int().gte(1)).default(1), size: z.optional(z.int().gte(1).lte(10000)).default(25) })) @@ -5381,7 +5707,14 @@ export const zGetThingsThingGetData = z.object({ z.null() ])), include_contacts: z.optional(z.boolean()).default(false), - filter: z.optional(z.string()), + filter: z.optional(z.union([ + z.array(z.string()), + z.null() + ])), + name_contains: z.optional(z.union([ + z.string(), + z.null() + ])), page: z.optional(z.int().gte(1)).default(1), size: z.optional(z.int().gte(1).lte(10000)).default(25) })) @@ -5499,3 +5832,16 @@ export const zReadNgwmnLithologyNgwmnLithologyPointidGetData = z.object({ }), query: z.optional(z.never()) }); + +export const zCreateFeedbackFeedbackPostData = z.object({ + body: zFeedbackCreate, + path: z.optional(z.never()), + query: z.optional(z.object({ + _user: z.optional(z.unknown()) + })) +}); + +/** + * Successful Response + */ +export const zCreateFeedbackFeedbackPostResponse = zFeedbackResponse; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index b39fcf72..2cc94a83 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -6,6 +6,7 @@ export * from './useAll' export * from './useAllNotes' export * from './useDebounce' export * from './useElevation' +export * from './useGisArtifacts' export * from './useLayer' export * from './useLexicon' export * from './useMostRecentObservation' diff --git a/src/hooks/useGisArtifacts.ts b/src/hooks/useGisArtifacts.ts new file mode 100644 index 00000000..ebfb3695 --- /dev/null +++ b/src/hooks/useGisArtifacts.ts @@ -0,0 +1,23 @@ +import { useQuery } from '@tanstack/react-query' +import { fetcher } from '@/providers/ocotillo-data-provider' +import { type GisCatalog, zGisCatalog } from '@/utils/gisArtifacts' + +/** + * Fetches the desktop-GIS artifact catalogue. + * + * `?f=json` is passed explicitly: `/gis` is content-negotiated and serves an + * HTML landing page by default, so relying on the Accept header risks parsing + * HTML as JSON. + * + * The catalogue is generated per request from live config, so it is not cached + * hard — a deploy that moves environments must not keep serving stale hrefs. + */ +export const useGisArtifacts = (options?: { enabled?: boolean }) => + useQuery({ + queryKey: ['gis-artifact-catalog'], + enabled: options?.enabled ?? true, + queryFn: async () => { + const response = await fetcher('gis?f=json') + return zGisCatalog.parse(response.data) + }, + }) diff --git a/src/hooks/useLayer.tsx b/src/hooks/useLayer.tsx index de28782c..77f936d5 100644 --- a/src/hooks/useLayer.tsx +++ b/src/hooks/useLayer.tsx @@ -1,4 +1,5 @@ import { useOne } from '@refinedev/core' +import { MAP_SYMBOL_STROKE_COLOR } from '@/constants/mapColors' export const useLayer = ({ thing_type, @@ -37,14 +38,16 @@ export const useLayer = ({ : { type: 'FeatureCollection', features: [] } return { - sourceProps: enabled ? { type: 'geojson', data: safeGeoJSON } : null, + // `type` is asserted so it narrows to the "geojson" literal the Source + // component's discriminated union expects, rather than widening to string. + sourceProps: enabled ? { type: 'geojson' as const, data: safeGeoJSON } : null, layerProps: { label, type: 'circle' as const, paint: { 'circle-radius': 3, 'circle-color': color, - 'circle-stroke-color': '#ffffff', + 'circle-stroke-color': MAP_SYMBOL_STROKE_COLOR, 'circle-stroke-width': 1, }, }, diff --git a/src/hooks/useOGCLayer.ts b/src/hooks/useOGCLayer.ts index fd2c0bee..3ba8d7d1 100644 --- a/src/hooks/useOGCLayer.ts +++ b/src/hooks/useOGCLayer.ts @@ -3,6 +3,11 @@ import { useDataProvider, type BaseKey } from '@refinedev/core' import { useQuery } from '@tanstack/react-query' import { captureEvent } from '@/analytics/posthog' import { withRetry } from '@/utils/httpRetry' +import { DEFAULT_TEXT_FONT } from '@/basemaps' +import { + MAP_DEFAULT_LAYER_COLOR, + MAP_SYMBOL_STROKE_COLOR, +} from '@/constants/mapColors' // --------------------------------------------------------------------------- // useOGCLayer @@ -232,7 +237,7 @@ export const useOGCLayer = ({ collection, label, providerName = 'ogcapi', - color = '#9cd0ab', + color = MAP_DEFAULT_LAYER_COLOR, colorAccessor, textAccessor, textColor = '#111111', @@ -426,7 +431,7 @@ export const useOGCLayer = ({ circle: { 'circle-radius': 3, 'circle-color': effectiveColor, - 'circle-stroke-color': '#ffffff', + 'circle-stroke-color': MAP_SYMBOL_STROKE_COLOR, 'circle-stroke-width': 1, }, line: { @@ -441,7 +446,10 @@ export const useOGCLayer = ({ } return { - sourceProps: { type: 'geojson', data: safeGeoJSON }, // Mapbox source configuration + // MapLibre source configuration. `type` is asserted so it narrows to the + // "geojson" literal the Source component's discriminated union expects, + // rather than widening to string. + sourceProps: { type: 'geojson' as const, data: safeGeoJSON }, sourceData: safeGeoJSON, // Raw GeoJSON for consumers that need it legendColor: fallbackColor, legendScale: hasColorMapping && colorMappingEnabled ? legendScale : undefined, @@ -464,6 +472,10 @@ export const useOGCLayer = ({ type: 'symbol' as const, layout: { 'text-field': ['get', '__label'], + // MapLibre defaults to "Open Sans Regular", which the OpenFreeMap + // glyph server does not serve. Ask for a stack it has, or the + // glyph request 404s and the labels never draw. + 'text-font': DEFAULT_TEXT_FONT, 'text-size': 19, 'text-anchor': 'top-left', 'text-offset': [0.35, 0.35], diff --git a/src/hooks/useOSEPODInfo.ts b/src/hooks/useOSEPODInfo.ts index e6454d34..cf016516 100644 --- a/src/hooks/useOSEPODInfo.ts +++ b/src/hooks/useOSEPODInfo.ts @@ -1,29 +1,44 @@ import { useQuery } from '@tanstack/react-query' +import type { OSEPODAttributes } from '@/utils/osePodSummary' + +const OSE_POD_QUERY_URL = + 'https://services2.arcgis.com/qXZbWTdPDbTjl7Dy/arcgis/rest/services/OSE_Points_of_Diversion/FeatureServer/0/query' + +// Queries the OSE Points of Diversion feature service for one POD's attributes. +const fetchPOD = async (pod_id: string): Promise => { + const url = new URL(OSE_POD_QUERY_URL) + url.search = new URLSearchParams({ + // Single quotes are doubled so they cannot break out of the where clause. + where: `db_file='${pod_id.replace(/'/g, "''")}'`, + f: 'pjson', + outFields: '*', + outSR: '4326', + }).toString() -const fetchPOD = async (pod_id: string) => { - const url = `https://services2.arcgis.com/qXZbWTdPDbTjl7Dy/arcgis/rest/services/OSE_Points_of_Diversion/FeatureServer/0/query?where=+db_file%3D%27${encodeURIComponent(pod_id)}%27&f=pjson&outFields=*&outSR=4326` const res = await fetch(url) + if (!res.ok) { + throw new Error(`OSE POD request failed with status ${res.status}`) + } + const data = await res.json() - if (data.features && data.features.length > 0) { - const attributes = data.features[0].attributes - const newRows = Object.keys(attributes).map((key, index) => ({ - id: index, - name: key, - value: attributes[key], - })) - return newRows + // ArcGIS reports query failures in the body with a 200 status, so check explicitly. + if (data?.error) { + throw new Error(data.error.message ?? 'OSE POD request failed') } - return [] -} + return (data?.features?.[0]?.attributes as OSEPODAttributes) ?? null +} +// React Query hook used by OSEPODInfoCard; skips fetch when pod_id is missing or "N/A". export const useOSEPODInfo = (pod_id: string) => { - const hasValidPodId = Boolean(pod_id?.trim()) && pod_id !== 'N/A' + const normalizedPodId = pod_id?.trim() + const hasValidPodId = Boolean(normalizedPodId) && normalizedPodId !== 'N/A' return useQuery({ - queryKey: ['osepod', pod_id], - queryFn: () => fetchPOD(pod_id), + queryKey: ['osepod', normalizedPodId], + queryFn: () => fetchPOD(normalizedPodId), enabled: hasValidPodId, - initialData: [], + staleTime: 5 * 60 * 1000, // matches the other well-show queries + gcTime: 10 * 60 * 1000, }) } diff --git a/src/hooks/useThingLayers.tsx b/src/hooks/useThingLayers.tsx index 1c72bde9..3522ce51 100644 --- a/src/hooks/useThingLayers.tsx +++ b/src/hooks/useThingLayers.tsx @@ -3,6 +3,15 @@ import { useQuery } from '@tanstack/react-query' import { useMemo } from 'react' import * as turf from '@turf/turf' import { useOGCLayer } from '@/hooks/useOGCLayer' +import { + MAP_LAYER_COLORS, + MAP_NO_DATA_COLOR, +} from '@/constants/mapColors' +import { + VIRIDIS_LOW, + viridisGradient, + viridisSamples, +} from '@/constants/viridis' import { parseNumeric } from '@/utils/parseNumeric' import { getWaterElevationFeet, @@ -12,22 +21,22 @@ import { import { OgcCollectionRecord, resolveCollection, - DEPTH_LEGEND, TDS_LEGEND, TREND_LEGEND, - latestDepthToWaterColorFromFeature, - averageTdsColorFromFeature, latestTdsColorFromFeature, trendColorFromFeature, } from '@/utils/ogcLayerUtils' const WATER_ELEVATION_LEGEND = { - gradient: - 'linear-gradient(90deg, #2c7bb6 0%, #00a6ca 20%, #00ccbc 40%, #90eb9d 55%, #ffff8c 70%, #f9d057 82%, #f29e2e 92%, #d7191c 100%)', + gradient: viridisGradient(), minLabel: 'Lower (ft)', maxLabel: 'Higher (ft)', } +// Seven classes across the viridis ramp: dark purple for the lowest water +// elevations through yellow for the highest. +const waterElevationColors = viridisSamples(7) + const EMPTY_FEATURE_COLLECTION = { type: 'FeatureCollection', features: [], @@ -62,71 +71,9 @@ export const useThingLayers = ( }) const collections = collectionsData ?? [] - const collectionSearchText = (collection: OgcCollectionRecord): string => - [collection.id, collection.collection_id, collection.name, collection.title] - .filter(Boolean) - .join(' ') - .toLowerCase() - - const resolveCollectionByTokenScore = ({ - includeAny, - includeOneOf, - includeAll, - exclude = [], - fallbackLabel, - minScore = 3, - }: { - includeAny: RegExp[] - includeOneOf: RegExp[] - includeAll: RegExp[] - exclude?: RegExp[] - fallbackLabel: string - minScore?: number - }) => { - let bestMatch: OgcCollectionRecord | undefined - let bestScore = -1 - - for (const collection of collections) { - const text = collectionSearchText(collection) - if (exclude.some((pattern) => pattern.test(text))) continue - if (!includeOneOf.some((pattern) => pattern.test(text))) continue - if (!includeAll.every((pattern) => pattern.test(text))) continue - - let score = 0 - for (const pattern of includeAny) { - if (pattern.test(text)) score += 1 - } - - if (score > bestScore) { - bestScore = score - bestMatch = collection - } - } - - const exists = Boolean(bestMatch) && bestScore >= minScore - - return { - id: bestMatch?.id || bestMatch?.collection_id || bestMatch?.name || '', - label: bestMatch?.title || bestMatch?.name || fallbackLabel, - exists, - description: bestMatch?.description || bestMatch?.abstract, - } - } - const isColorMappingEnabled = (layerKey: string): boolean => colorMappingByLayer[layerKey] ?? true - const locations = resolveCollection(collections, ['Locations', 'locations']) - const latestDepthToWater = resolveCollection(collections, [ - 'Latest Depth to Water (Water Wells)', - 'latest_depth_to_water_water_wells', - 'latest_depth_to_water', - ]) - const averageTds = resolveCollection(collections, [ - 'Average TDS (Water Wells)', - 'average_tds_water_wells', - 'average_tds', - ]) const latestTds = resolveCollection(collections, [ 'Latest TDS (Water Wells)', 'latest_tds_water_wells', @@ -166,76 +113,17 @@ export const useThingLayers = ( 'actively_monitored', ]) const springs = resolveCollection(collections, ['Springs', 'springs']) - const waterElevationContoursPrimary = resolveCollection(collections, [ - 'Water Elevation Contours', - 'water_elevation_contours', - 'water_elevation_contour', - 'groundwater_elevation_contours', - 'water_level_contours', - 'water_table_contours', - 'potentiometric_surface_contours', - 'piezometric_contours', - ]) - const waterElevationContours = waterElevationContoursPrimary.exists - ? waterElevationContoursPrimary - : resolveCollectionByTokenScore({ - includeAny: [ - /water/i, - /groundwater/i, - /elevation/i, - /level/i, - /table/i, - /potentiometric/i, - /piezometric/i, - /head/i, - /surface/i, - /contour/i, - /isoline/i, - ], - includeOneOf: [/contour|isoline/i], - includeAll: [ - /potentiometric|piezometric|elevation|water[\s_-]?table|head/i, - ], - exclude: [/depth[\s_-]?to[\s_-]?water/i, /trend/i, /tds/i], - fallbackLabel: 'Water Elevation Contours', - }) - const waterElevationPointsPrimary = resolveCollection(collections, [ + const waterElevationPoints = resolveCollection(collections, [ 'Water Elevation Points', 'water_elevation_points', 'water_elevation_point', 'water_elevation_wells', - 'ogcapi/collections/water_elevation_wells/items', 'groundwater_elevation_points', 'water_level_points', 'water_table_points', 'potentiometric_surface_points', 'piezometric_points', ]) - const waterElevationPoints = waterElevationPointsPrimary.exists - ? waterElevationPointsPrimary - : resolveCollectionByTokenScore({ - includeAny: [ - /water/i, - /groundwater/i, - /elevation/i, - /level/i, - /table/i, - /potentiometric/i, - /piezometric/i, - /head/i, - /surface/i, - /point/i, - /points/i, - /station/i, - /well/i, - ], - includeOneOf: [/point|points|station|well/i], - includeAll: [ - /potentiometric|piezometric|elevation|water[\s_-]?table|head/i, - ], - exclude: [/depth[\s_-]?to[\s_-]?water/i, /trend/i, /tds/i], - fallbackLabel: 'Water Elevation Points', - }) const surfaceWaterDiversions = resolveCollection(collections, [ 'Surface Water Diversions', 'surface_water_diversions', @@ -252,10 +140,6 @@ export const useThingLayers = ( 'Meteorological Stations', 'meteorological_stations', ]) - const otherThingTypes = resolveCollection(collections, [ - 'Other Thing Types', - 'other_thing_types', - ]) const projectAreas = resolveCollection(collections, [ 'Project Areas', 'Project Area', @@ -278,38 +162,32 @@ export const useThingLayers = ( 'Soil Gas Sample Locations', 'soil_gas_sample_locations', ]) - const locationsLayer = useOGCLayer({ - collection: locations.id, - label: locations.label, - color: '#607d8b', - enabled: locations.exists && isLayerActive('ogc-locations'), - }) - const latestDepthToWaterLayer = useOGCLayer({ - collection: latestDepthToWater.id, - label: latestDepthToWater.label, - legendColor: '#fdae61', - color: '#9e9e9e', - colorAccessor: latestDepthToWaterColorFromFeature, - legendScale: DEPTH_LEGEND, - colorMappingEnabled: isColorMappingEnabled('ogc-latest-depth-to-water'), - enabled: - latestDepthToWater.exists && isLayerActive('ogc-latest-depth-to-water'), - }) - const averageTdsLayer = useOGCLayer({ - collection: averageTds.id, - label: averageTds.label, - legendColor: '#f46d43', - color: '#9e9e9e', - colorAccessor: averageTdsColorFromFeature, - legendScale: TDS_LEGEND, - colorMappingEnabled: isColorMappingEnabled('ogc-average-tds'), - enabled: averageTds.exists && isLayerActive('ogc-average-tds'), - }) + const geothermalWellsBht = resolveCollection(collections, [ + 'geothermal_wells_bht', + 'Geothermal Wells — Bottom-Hole Temperature', + ]) + const geothermalWellsTemperatureProfile = resolveCollection(collections, [ + 'geothermal_wells_temperature_profile', + 'Geothermal Wells — Temperature-Depth Profile', + ]) + const bhtMeasurements = resolveCollection(collections, [ + 'bht_measurements', + 'BHT Measurements', + ]) + const tempDepthMeasurements = resolveCollection(collections, [ + 'temp_depth_measurements', + 'Temperature-Depth Measurements', + ]) + const heatFlow = resolveCollection(collections, ['heat_flow', 'Heat Flow']) + const drillStemTests = resolveCollection(collections, [ + 'dst', + 'Drill Stem Tests', + ]) const latestTdsLayer = useOGCLayer({ collection: latestTds.id, label: latestTds.label, - legendColor: '#fdae61', - color: '#9e9e9e', + legendColor: MAP_LAYER_COLORS.latestTds, + color: MAP_NO_DATA_COLOR, colorAccessor: latestTdsColorFromFeature, legendScale: TDS_LEGEND, colorMappingEnabled: isColorMappingEnabled('ogc-latest-tds'), @@ -318,20 +196,20 @@ export const useThingLayers = ( const majorChemistryLayer = useOGCLayer({ collection: majorChemistry.id, label: majorChemistry.label, - color: '#8e24aa', + color: MAP_LAYER_COLORS.majorChemistry, enabled: majorChemistry.exists && isLayerActive('ogc-major-chemistry'), }) const minorChemistryLayer = useOGCLayer({ collection: minorChemistry.id, label: minorChemistry.label, - color: '#6a1b9a', + color: MAP_LAYER_COLORS.minorChemistry, enabled: minorChemistry.exists && isLayerActive('ogc-minor-chemistry'), }) const depthToWaterTrendLayer = useOGCLayer({ collection: depthToWaterTrend.id, label: depthToWaterTrend.label, - legendColor: '#b2182b', - color: '#9e9e9e', + legendColor: MAP_LAYER_COLORS.depthToWaterTrend, + color: MAP_NO_DATA_COLOR, colorAccessor: trendColorFromFeature, legendScale: TREND_LEGEND, colorMappingEnabled: isColorMappingEnabled('ogc-depth-to-water-trend'), @@ -341,50 +219,40 @@ export const useThingLayers = ( const waterWellSummaryLayer = useOGCLayer({ collection: waterWellSummary.id, label: waterWellSummary.label, - color: '#8bc34a', + color: MAP_LAYER_COLORS.waterWellSummary, enabled: waterWellSummary.exists && isLayerActive('ogc-water-well-summary'), }) const waterWellsLayer = useOGCLayer({ collection: waterWells.id, label: waterWells.label, - color: '#2b7dc0', + color: MAP_LAYER_COLORS.waterWells, enabled: waterWells.exists && isLayerActive('ogc-water-wells'), }) const activelyMonitoredLayer = useOGCLayer({ collection: activelyMonitored.id, label: activelyMonitored.label, - color: '#2e7d32', + color: MAP_LAYER_COLORS.activelyMonitored, enabled: activelyMonitored.exists && isLayerActive('ogc-actively-monitored'), }) const springsLayer = useOGCLayer({ collection: springs.id, label: springs.label, - color: '#00acc1', + color: MAP_LAYER_COLORS.springs, enabled: springs.exists && isLayerActive('ogc-springs'), }) - const waterElevationContoursLayer = useOGCLayer({ - collection: waterElevationContours.id, - label: waterElevationContours.label, - color: '#0d47a1', - layerType: 'line', - paint: { - 'line-width': 1.2, - 'line-opacity': 0.85, - }, - enabled: - waterElevationContours.exists && - isLayerActive('ogc-water-elevation-contours'), - }) - const needsDerivedContours = - !waterElevationContours.exists && - isLayerActive('ogc-water-elevation-contours-derived') + + // Derived contours are the only water-elevation contour layer -- the + // catalog publishes elevation points, not contours. + const needsDerivedContours = isLayerActive( + 'ogc-water-elevation-contours-derived' + ) const waterElevationPointsLayer = useOGCLayer({ collection: waterElevationPoints.id, label: `${waterElevationPoints.label} (ft)`, - color: '#1976d2', + color: MAP_LAYER_COLORS.waterElevationPoints, legendScale: WATER_ELEVATION_LEGEND, colorMappingEnabled: isColorMappingEnabled('ogc-water-elevation-points'), enabled: waterElevationPoints.exists, @@ -430,16 +298,6 @@ export const useThingLayers = ( [waterElevationPointFeatures] ) - const waterElevationColors = [ - '#2c7bb6', - '#00a6ca', - '#00ccbc', - '#90eb9d', - '#ffff8c', - '#f29e2e', - '#d7191c', - ] - const buildWaterElevationStepExpression = (propertyName: string): any => waterElevationStats.hasSpread ? [ @@ -459,7 +317,7 @@ export const useThingLayers = ( waterElevationStats.breaks[5], waterElevationColors[6], ] - : '#1976d2' + : MAP_LAYER_COLORS.waterElevationPoints const waterElevationColorExpression = useMemo( () => buildWaterElevationStepExpression('water_elevation_ft'), @@ -477,9 +335,6 @@ export const useThingLayers = ( const isWaterElevationPointsColorMapped = isColorMappingEnabled( 'ogc-water-elevation-points' ) - const isWaterElevationContoursColorMapped = isColorMappingEnabled( - 'ogc-water-elevation-contours' - ) const isWaterElevationDerivedContoursColorMapped = isColorMappingEnabled( 'ogc-water-elevation-contours-derived' ) @@ -506,7 +361,7 @@ export const useThingLayers = ( ...(waterElevationPointsLayer.layerProps?.paint || {}), 'circle-color': isWaterElevationPointsColorMapped ? waterElevationColorExpression - : '#1976d2', + : MAP_LAYER_COLORS.waterElevationPoints, }, }, } @@ -518,31 +373,6 @@ export const useThingLayers = ( isWaterElevationPointsColorMapped, ]) - const waterElevationContoursLayerStyled = useMemo( - () => ({ - ...waterElevationContoursLayer, - legendScale: isWaterElevationContoursColorMapped - ? waterElevationLegendScale - : undefined, - colorMappingAvailable: true, - colorMappingEnabled: isWaterElevationContoursColorMapped, - layerProps: { - ...waterElevationContoursLayer.layerProps, - paint: { - ...(waterElevationContoursLayer.layerProps?.paint || {}), - 'line-color': isWaterElevationContoursColorMapped - ? waterElevationColorExpression - : '#0d47a1', - }, - }, - }), - [ - waterElevationContoursLayer, - waterElevationLegendScale, - waterElevationColorExpression, - isWaterElevationContoursColorMapped, - ] - ) const waterElevationDerivedContourLayerData = useQuery({ queryKey: [ @@ -721,7 +551,7 @@ export const useThingLayers = ( legendScale: isWaterElevationDerivedContoursColorMapped ? waterElevationLegendScale : undefined, - legendColor: '#0d47a1', + legendColor: MAP_LAYER_COLORS.waterElevationContours, colorMappingAvailable: true, colorMappingEnabled: isWaterElevationDerivedContoursColorMapped, layerProps: { @@ -730,7 +560,7 @@ export const useThingLayers = ( paint: { 'line-color': isWaterElevationDerivedContoursColorMapped ? waterElevationColorExpression - : '#0d47a1', + : MAP_LAYER_COLORS.waterElevationContours, 'line-width': 1.2, 'line-opacity': 0.85, }, @@ -776,7 +606,7 @@ export const useThingLayers = ( const surfaceWaterDiversionsLayer = useOGCLayer({ collection: surfaceWaterDiversions.id, label: surfaceWaterDiversions.label, - color: '#ef6c00', + color: MAP_LAYER_COLORS.surfaceWaterDiversions, enabled: surfaceWaterDiversions.exists && isLayerActive('ogc-surface-water-diversions'), @@ -784,13 +614,13 @@ export const useThingLayers = ( const ephemeralStreamsLayer = useOGCLayer({ collection: ephemeralStreams.id, label: ephemeralStreams.label, - color: '#8e24aa', + color: MAP_LAYER_COLORS.ephemeralStreams, enabled: ephemeralStreams.exists && isLayerActive('ogc-ephemeral-streams'), }) const lakesPondsReservoirsLayer = useOGCLayer({ collection: lakesPondsReservoirs.id, label: lakesPondsReservoirs.label, - color: '#3949ab', + color: MAP_LAYER_COLORS.lakesPondsReservoirs, enabled: lakesPondsReservoirs.exists && isLayerActive('ogc-lakes-ponds-reservoirs'), @@ -798,7 +628,7 @@ export const useThingLayers = ( const meteorologicalStationsLayer = useOGCLayer({ collection: meteorologicalStations.id, label: meteorologicalStations.label, - color: '#546e7a', + color: MAP_LAYER_COLORS.meteorologicalStations, enabled: meteorologicalStations.exists && isLayerActive('ogc-meteorological-stations'), @@ -806,48 +636,83 @@ export const useThingLayers = ( const projectAreasLayer = useOGCLayer({ collection: projectAreas.id, label: 'AMP Project Areas', - color: '#7c3aed', + color: MAP_LAYER_COLORS.projectAreas, layerType: 'fill', paint: { 'fill-opacity': 0.16, - 'fill-outline-color': '#5b21b6', + 'fill-outline-color': VIRIDIS_LOW, }, enabled: projectAreas.exists && isLayerActive('ogc-project-areas'), }) - const otherThingTypesLayer = useOGCLayer({ - collection: otherThingTypes.id, - label: otherThingTypes.label, - color: '#9e9d24', - enabled: otherThingTypes.exists && isLayerActive('ogc-other-thing-types'), - }) const outfallsReturnFlowLayer = useOGCLayer({ collection: outfallsReturnFlow.id, label: outfallsReturnFlow.label, - color: '#5d4037', + color: MAP_LAYER_COLORS.outfallsReturnFlow, enabled: outfallsReturnFlow.exists && isLayerActive('ogc-outfalls-return-flow'), }) const perennialStreamsLayer = useOGCLayer({ collection: perennialStreams.id, label: perennialStreams.label, - color: '#1e88e5', + color: MAP_LAYER_COLORS.perennialStreams, enabled: perennialStreams.exists && isLayerActive('ogc-perennial-streams'), }) const rockSampleLocationsLayer = useOGCLayer({ collection: rockSampleLocations.id, label: rockSampleLocations.label, - color: '#6d4c41', + color: MAP_LAYER_COLORS.rockSampleLocations, enabled: rockSampleLocations.exists && isLayerActive('ogc-rock-sample-locations'), }) const soilGasSampleLocationsLayer = useOGCLayer({ collection: soilGasSampleLocations.id, label: soilGasSampleLocations.label, - color: '#7cb342', + color: MAP_LAYER_COLORS.soilGasSampleLocations, enabled: soilGasSampleLocations.exists && isLayerActive('ogc-soil-gas-sample-locations'), }) + const geothermalWellsBhtLayer = useOGCLayer({ + collection: geothermalWellsBht.id, + label: geothermalWellsBht.label, + color: '#b45309', + enabled: + geothermalWellsBht.exists && isLayerActive('ogc-geothermal-wells-bht'), + }) + const geothermalWellsTemperatureProfileLayer = useOGCLayer({ + collection: geothermalWellsTemperatureProfile.id, + label: geothermalWellsTemperatureProfile.label, + color: '#d97706', + enabled: + geothermalWellsTemperatureProfile.exists && + isLayerActive('ogc-geothermal-wells-temperature-profile'), + }) + const bhtMeasurementsLayer = useOGCLayer({ + collection: bhtMeasurements.id, + label: bhtMeasurements.label, + color: '#ea580c', + enabled: bhtMeasurements.exists && isLayerActive('ogc-bht-measurements'), + }) + const tempDepthMeasurementsLayer = useOGCLayer({ + collection: tempDepthMeasurements.id, + label: tempDepthMeasurements.label, + color: '#f59e0b', + enabled: + tempDepthMeasurements.exists && + isLayerActive('ogc-temp-depth-measurements'), + }) + const heatFlowLayer = useOGCLayer({ + collection: heatFlow.id, + label: heatFlow.label, + color: '#dc2626', + enabled: heatFlow.exists && isLayerActive('ogc-heat-flow'), + }) + const drillStemTestsLayer = useOGCLayer({ + collection: drillStemTests.id, + label: drillStemTests.label, + color: '#92400e', + enabled: drillStemTests.exists && isLayerActive('ogc-dst'), + }) return useMemo(() => { const result: Record = {} @@ -870,13 +735,6 @@ export const useThingLayers = ( } } - addLayer('ogc-locations', locations, locationsLayer) - addLayer( - 'ogc-latest-depth-to-water', - latestDepthToWater, - latestDepthToWaterLayer - ) - addLayer('ogc-average-tds', averageTds, averageTdsLayer) addLayer('ogc-latest-tds', latestTds, latestTdsLayer) addLayer( 'ogc-depth-to-water-trend', @@ -888,21 +746,6 @@ export const useThingLayers = ( waterElevationPoints, waterElevationPointsLayerStyled ) - addLayer( - 'ogc-water-elevation-contours', - waterElevationContours, - waterElevationContoursLayerStyled - ) - if (!waterElevationContours.exists) { - result['ogc-water-elevation-contours-derived'] = { - ...waterElevationDerivedContoursLayer, - description: waterElevationPoints.description, - colorMappingAvailable: - waterElevationDerivedContoursLayer.colorMappingAvailable ?? true, - colorMappingEnabled: - waterElevationDerivedContoursLayer.colorMappingEnabled ?? true, - } - } addLayer('ogc-major-chemistry', majorChemistry, majorChemistryLayer) addLayer('ogc-minor-chemistry', minorChemistry, minorChemistryLayer) addLayer('ogc-water-well-summary', waterWellSummary, waterWellSummaryLayer) @@ -913,6 +756,14 @@ export const useThingLayers = ( activelyMonitoredLayer ) addLayer('ogc-springs', springs, springsLayer) + result['ogc-water-elevation-contours-derived'] = { + ...waterElevationDerivedContoursLayer, + description: waterElevationPoints.description, + colorMappingAvailable: + waterElevationDerivedContoursLayer.colorMappingAvailable ?? true, + colorMappingEnabled: + waterElevationDerivedContoursLayer.colorMappingEnabled ?? true, + } addLayer( 'ogc-surface-water-diversions', surfaceWaterDiversions, @@ -930,7 +781,6 @@ export const useThingLayers = ( meteorologicalStationsLayer ) addLayer('ogc-project-areas', projectAreas, projectAreasLayer) - addLayer('ogc-other-thing-types', otherThingTypes, otherThingTypesLayer) addLayer( 'ogc-outfalls-return-flow', outfallsReturnFlow, @@ -947,13 +797,28 @@ export const useThingLayers = ( soilGasSampleLocations, soilGasSampleLocationsLayer ) + addLayer( + 'ogc-geothermal-wells-bht', + geothermalWellsBht, + geothermalWellsBhtLayer + ) + addLayer( + 'ogc-geothermal-wells-temperature-profile', + geothermalWellsTemperatureProfile, + geothermalWellsTemperatureProfileLayer + ) + addLayer('ogc-bht-measurements', bhtMeasurements, bhtMeasurementsLayer) + addLayer( + 'ogc-temp-depth-measurements', + tempDepthMeasurements, + tempDepthMeasurementsLayer + ) + addLayer('ogc-heat-flow', heatFlow, heatFlowLayer) + addLayer('ogc-dst', drillStemTests, drillStemTestsLayer) return result }, [ collectionsData, - locationsLayer, - latestDepthToWaterLayer, - averageTdsLayer, latestTdsLayer, majorChemistryLayer, minorChemistryLayer, @@ -962,7 +827,6 @@ export const useThingLayers = ( waterWellsLayer, activelyMonitoredLayer, springsLayer, - waterElevationContoursLayerStyled, waterElevationPointsLayerStyled, waterElevationDerivedContoursLayer, surfaceWaterDiversionsLayer, @@ -970,10 +834,15 @@ export const useThingLayers = ( lakesPondsReservoirsLayer, meteorologicalStationsLayer, projectAreasLayer, - otherThingTypesLayer, outfallsReturnFlowLayer, perennialStreamsLayer, rockSampleLocationsLayer, soilGasSampleLocationsLayer, + geothermalWellsBhtLayer, + geothermalWellsTemperatureProfileLayer, + bhtMeasurementsLayer, + tempDepthMeasurementsLayer, + heatFlowLayer, + drillStemTestsLayer, ]) } diff --git a/src/hooks/useUSGSSiteInfo.ts b/src/hooks/useUSGSSiteInfo.ts index cf6f8de5..3a261cf9 100644 --- a/src/hooks/useUSGSSiteInfo.ts +++ b/src/hooks/useUSGSSiteInfo.ts @@ -1,67 +1,108 @@ import { useQuery } from '@tanstack/react-query' +import { settings } from '@/settings' + +// Fetches USGS monitoring location metadata for a given site number from the +// OGC API (api.waterdata.usgs.gov/ogcapi), along with the field labels and +// descriptions the service publishes for itself. +// +// This replaces the legacy NWIS RDB site service (waterservices.usgs.gov), +// which USGS has largely decommissioned. + +const API_URL = settings.usgs_nwis_ogc_api_url +const COLLECTION = 'monitoring-locations' + +export type USGSSiteRecord = Record + +export type USGSSiteInfo = { + record: USGSSiteRecord + /** Property name -> the title the API documents for it, e.g. site_type_code -> "Monitoring location type code". */ + labels: Record + /** Property name -> the API's description of that field. */ + descriptions: Record + latitude: number | null + longitude: number | null + url: string +} -// Fetches expanded USGS site metadata for a given site number and exposes it as key/value rows. - -type USGSSiteRecord = Record +type Queryables = { + labels: Record + descriptions: Record +} -type USGSSiteInfoRow = { - id: number - name: string - value: string +type Feature = { + id?: string + geometry?: { type?: string; coordinates?: [number, number] } | null + properties?: Record | null } -// Parses USGS RDB (tab-delimited) site response text into record objects. -// Adapted by AI from Jacob's Data Integration Engine code. -const makeRecords = (text: string, url: string): USGSSiteRecord[] => { - let header: string[] = [] - const records: USGSSiteRecord[] = [] +// The collection describes its own fields, so labels and descriptions come from +// the API rather than being hard-coded here. +const fetchQueryables = async (): Promise => { + const res = await fetch( + `${API_URL}/collections/${COLLECTION}/queryables?f=json` + ) + if (!res.ok) { + throw new Error(`USGS queryables request failed with status ${res.status}`) + } - for (const line of text.split('\n')) { - if (!line.trim() || line.startsWith('#')) { - continue - } + const body = (await res.json()) as { + properties?: Record + } - const values = line.split('\t').map((value) => value.trim()) + const labels: Record = {} + const descriptions: Record = {} - if (values[0] === 'agency_cd') { - header = values - continue - } + for (const [name, schema] of Object.entries(body.properties ?? {})) { + if (schema?.title) labels[name] = schema.title + if (schema?.description) descriptions[name] = schema.description.trim() + } - if (values[0] === '5s') { - continue - } + return { labels, descriptions } +} - if (!header.length || values.length !== header.length || !values[0]) { - continue - } +// The field definitions are effectively static, so they are fetched once per +// session and shared by every site lookup. A failed attempt is not cached. +let queryablesPromise: Promise | null = null - records.push({ - ...Object.fromEntries(header.map((key, index) => [key, values[index]])), - url, - }) - } +const loadQueryables = (): Promise => { + queryablesPromise ??= fetchQueryables().catch((error) => { + queryablesPromise = null + throw error + }) - return records + return queryablesPromise } -// Flattens a site record into DataGrid-friendly { name, value } rows. -const toKeyValueRows = (record: USGSSiteRecord): USGSSiteInfoRow[] => { - return Object.entries(record).map(([name, value], index) => ({ - id: index, - name, - value, - })) +/** Drops the agency prefix so both "USGS-01234567" and "01234567" resolve. */ +const toSiteNumber = (site_no: string): string => { + const trimmed = site_no.trim() + const separator = trimmed.indexOf('-') + return separator === -1 ? trimmed : trimmed.slice(separator + 1) } -// Calls the USGS NWIS site service and returns parsed site fields for one site. -const fetchSiteInfo = async (site_no: string): Promise => { - const url = new URL('https://waterservices.usgs.gov/nwis/site/') +// Flattens a GeoJSON feature's properties into the string record the summary +// builders consume. Nulls and blanks are dropped so they never render. +const toRecord = (feature: Feature): USGSSiteRecord => { + const record: USGSSiteRecord = {} + + if (feature.id) record['id'] = String(feature.id) + + for (const [key, value] of Object.entries(feature.properties ?? {})) { + if (value == null) continue + + const text = String(value).trim() + if (text) record[key] = text + } + + return record +} + +const fetchSiteInfo = async (site_no: string): Promise => { + const url = new URL(`${API_URL}/collections/${COLLECTION}/items`) url.search = new URLSearchParams({ - format: 'rdb', - siteStatus: 'all', - siteOutput: 'expanded', - sites: site_no, + monitoring_location_number: toSiteNumber(site_no), + f: 'json', + limit: '1', }).toString() const res = await fetch(url) @@ -69,12 +110,27 @@ const fetchSiteInfo = async (site_no: string): Promise => { throw new Error(`USGS site info request failed with status ${res.status}`) } - const text = await res.text() - const records = makeRecords(text, url.toString()) + const body = (await res.json()) as { features?: Feature[] } + const feature = body.features?.[0] + if (!feature) return null - return records.length > 0 ? toKeyValueRows(records[0]) : [] -} + // Field definitions are a presentation nicety; a site still renders without them. + const { labels, descriptions } = await loadQueryables().catch(() => ({ + labels: {}, + descriptions: {}, + })) + + const [longitude, latitude] = feature.geometry?.coordinates ?? [] + return { + record: toRecord(feature), + labels, + descriptions, + latitude: typeof latitude === 'number' ? latitude : null, + longitude: typeof longitude === 'number' ? longitude : null, + url: url.toString(), + } +} // React Query hook used by USGSInfoCard; skips fetch when site_no is missing or "N/A". export const useUSGSSiteInfo = (site_no: string) => { @@ -85,6 +141,7 @@ export const useUSGSSiteInfo = (site_no: string) => { queryKey: ['site_no', normalizedSiteNo], queryFn: () => fetchSiteInfo(normalizedSiteNo), enabled: hasValidSiteNo, - initialData: [], + staleTime: 5 * 60 * 1000, // matches the other well-show queries + gcTime: 10 * 60 * 1000, }) } diff --git a/src/hooks/useViewportBbox.ts b/src/hooks/useViewportBbox.ts index 05efcfed..90de2a68 100644 --- a/src/hooks/useViewportBbox.ts +++ b/src/hooks/useViewportBbox.ts @@ -1,5 +1,5 @@ import { useEffect, useMemo, useState } from 'react' -import type { MapRef } from 'react-map-gl' +import type { MapRef } from 'react-map-gl/maplibre' export type ViewportBboxOptions = { /** debounce updates while the user is moving the map */ diff --git a/src/index.css b/src/index.css index 04d5184a..8305302e 100644 --- a/src/index.css +++ b/src/index.css @@ -18,10 +18,15 @@ * all MUI components have been replaced. * * Color notation: oklch(lightness chroma hue) - * Tailwind color references used: - * primary → indigo-500 (light) / indigo-300 (dark) + * Color references used: + * primary → brand-600 (light) / brand-300 (dark) * secondary → amber-600 (light) / amber-300 (dark) + * info → teal-700 (light) / teal-300 (dark) * Colors match the values in src/theme.ts exactly. + * + * `brand` is the Ocotillo blue ramp defined in src/theme.ts — sampled from the + * pixel water-splash mark and the desert sky in the masthead photo. Everything + * else is Tailwind v3. Change a brand value here and change it there too. */ @layer base { :root { @@ -41,11 +46,11 @@ --popover: oklch(1 0 0); --popover-foreground: oklch(0.195 0.026 264.182); - /* Primary — indigo (matches refresh bar) */ - --primary: oklch(0.585 0.233 277.117); /* indigo-500 */ + /* Primary — Ocotillo brand blue */ + --primary: oklch(0.515 0.122 244.044); /* brand-600 #0e6da8 */ --primary-foreground: oklch(1 0 0); /* white */ - --primary-light: oklch(0.785 0.115 274.713); /* indigo-300 */ - --primary-dark: oklch(0.457 0.24 277.023); /* indigo-700 */ + --primary-light: oklch(0.796 0.089 235.66); /* brand-300 #83c6ee */ + --primary-dark: oklch(0.44 0.102 244.066); /* brand-700 #0f5786 */ /* Secondary — amber */ --secondary: oklch(0.666 0.171 60.454); /* amber-600 */ @@ -67,13 +72,23 @@ --success-foreground: oklch(1 0 0); --warning: oklch(0.702 0.191 47.604); /* orange-500 */ --warning-foreground: oklch(1 0 0); - --info: oklch(0.566 0.128 233.997); /* cyan-600 */ + --info: oklch(0.511 0.096 186.391); /* teal-700 #0f766e */ --info-foreground: oklch(1 0 0); /* Borders and inputs */ --border: oklch(0.9 0 0); /* neutral-200 */ --input: oklch(0.9 0 0); /* neutral-200 */ - --ring: oklch(0.585 0.233 277.117); /* indigo-500 */ + --ring: oklch(0.515 0.122 244.044); /* brand-600 #0e6da8 */ + + /* + * Brand identity — NOT semantic slots, and never to be wired to one. + * The bloom sits at hue 36, only 8.7 degrees from --destructive and 11.6 + * from --warning, so anything painted in it inside the UI chrome reads as + * an alarm. Brand surfaces only: the favicon, artwork, splash screens. + * Mode-independent by design — a logo does not change colour with theme. + */ + --bloom: oklch(0.632 0.184 35.973); /* bloom-500 #e2552e */ + --sand: oklch(0.935 0.032 89.142); /* sand-100 #f2e9d2 */ /* Radius — shared by shadcn components (buttons, inputs, cards, etc.) */ --radius: 0.375rem; @@ -81,12 +96,12 @@ /* Sidebar — same background as app, slightly different accent for hovers */ --sidebar: oklch(0.985 0 0); /* neutral-50, matches --background */ --sidebar-foreground: oklch(0.195 0.026 264.182); - --sidebar-primary: oklch(0.585 0.233 277.117); /* indigo-500 */ + --sidebar-primary: oklch(0.515 0.122 244.044); /* brand-600 #0e6da8 */ --sidebar-primary-foreground: oklch(1 0 0); --sidebar-accent: oklch(0.94 0 0); /* neutral-150 for hover */ --sidebar-accent-foreground: oklch(0.195 0.026 264.182); --sidebar-border: oklch(0.9 0 0); /* neutral-200 */ - --sidebar-ring: oklch(0.585 0.233 277.117); /* indigo-500 */ + --sidebar-ring: oklch(0.515 0.122 244.044); /* brand-600 #0e6da8 */ } .dark { @@ -104,11 +119,15 @@ --popover: oklch(0.372 0 0); /* zinc-700 */ --popover-foreground: oklch(0.966 0.007 247.858); - /* Primary — indigo (inverted for dark mode) */ - --primary: oklch(0.785 0.115 274.713); /* indigo-300 */ + /* + * Primary — Ocotillo brand blue, stepped up the ramp for dark mode. + * --primary-dark is the hover/emphasis token, so on dark surfaces it has + * to resolve *lighter* than --primary, not darker. + */ + --primary: oklch(0.796 0.089 235.66); /* brand-300 #83c6ee */ --primary-foreground: oklch(0.21 0 0); /* zinc-900 */ - --primary-light: oklch(0.785 0.115 274.713); /* indigo-300 */ - --primary-dark: oklch(0.457 0.24 277.023); /* indigo-700 */ + --primary-light: oklch(0.692 0.12 237.556); /* brand-400 #47a6dd */ + --primary-dark: oklch(0.883 0.052 234.04); /* brand-200 #b8dff6 */ /* Secondary — amber (inverted for dark mode) */ --secondary: oklch(0.883 0.156 99.265); /* amber-300 */ @@ -130,23 +149,23 @@ --success-foreground: oklch(0.21 0 0); --warning: oklch(0.837 0.128 66.29); /* orange-300 */ --warning-foreground: oklch(0.21 0 0); - --info: oklch(0.865 0.127 207.078); /* cyan-300 */ + --info: oklch(0.855 0.125 181.071); /* teal-300 #5eead4 */ --info-foreground: oklch(0.21 0 0); /* Borders and inputs */ --border: oklch(0.372 0 0); /* zinc-700 / matches MUI divider */ --input: oklch(0.274 0 0); /* zinc-800 */ - --ring: oklch(0.785 0.115 274.713); /* indigo-300 */ + --ring: oklch(0.796 0.089 235.66); /* brand-300 #83c6ee */ /* Sidebar */ --sidebar: oklch(0.274 0 0); /* zinc-800 */ --sidebar-foreground: oklch(0.966 0.007 247.858); - --sidebar-primary: oklch(0.785 0.115 274.713); /* indigo-300 */ + --sidebar-primary: oklch(0.796 0.089 235.66); /* brand-300 #83c6ee */ --sidebar-primary-foreground: oklch(0.21 0 0); --sidebar-accent: oklch(0.372 0 0); /* zinc-700 */ --sidebar-accent-foreground: oklch(0.966 0.007 247.858); --sidebar-border: oklch(0.372 0 0); /* zinc-700 */ - --sidebar-ring: oklch(0.785 0.115 274.713); /* indigo-300 */ + --sidebar-ring: oklch(0.796 0.089 235.66); /* brand-300 #83c6ee */ } * { @@ -231,6 +250,8 @@ --color-info-foreground: var(--info-foreground); --color-primary-light: var(--primary-light); --color-primary-dark: var(--primary-dark); + --color-bloom: var(--bloom); + --color-sand: var(--sand); --color-secondary-light: var(--secondary-light); --color-secondary-dark: var(--secondary-dark); diff --git a/src/interfaces/geothermal/ITempDepthPoint.ts b/src/interfaces/geothermal/ITempDepthPoint.ts new file mode 100644 index 00000000..3e8f85e5 --- /dev/null +++ b/src/interfaces/geothermal/ITempDepthPoint.ts @@ -0,0 +1,12 @@ +// One measurement in a well's temperature-depth log (the core geothermal data: +// depth vs temperature, from which thermal gradient and heat flow are derived). +// PROVISIONAL field names — confirm against the backend once the endpoint lands. +export interface ITempDepthPoint { + depth_m: number | null + depth_ft: number | null + temp_f: number | null + temp_c: number | null + resistance: number | null + gradient_c_km: number | null + comment: string | null +} diff --git a/src/interfaces/geothermal/IWell.ts b/src/interfaces/geothermal/IWell.ts index d50b7fbc..926a875e 100644 --- a/src/interfaces/geothermal/IWell.ts +++ b/src/interfaces/geothermal/IWell.ts @@ -1,3 +1,68 @@ +// Shape of a geothermal well. The first block matches the live +// GET /thing/geothermal-well response; the rest (G2 location, G3 header, G4 +// api) are PROVISIONAL field names modeled from the legacy NM_Wells Geothermal +// DB — confirm against the backend contract once it lands. export interface IWell { - OBJECTID: number; + well_data_id: string + thing_id: number | null + + // ── Identity (G4: api is state-county-well; api_suffix is separate) ── + api: string | null + api_suffix: string | null + name: string | null + well_number: string | null + import_id: string | null + import_db: string | null + guid: string | null + + // ── Classification ── + well_class: string | null + well_type: string | null + well_orient: string | null + status: string | null + + // ── Operator ── + operator: string | null + owner: string | null + prd_pool_count: number | null + + // ── Depth (G3) ── + total_depth: number | null + well_tvd: number | null + plug_back: number | null + fm_td: string | null + age_td: string | null + + // ── Dates (G3) ── + spud_date: string | null + completion_date: string | null + plug_date: string | null + + // ── Location (G2) ── + latitude: number | null + longitude: number | null + source_datum: string | null + basin: string | null + county: string | null + state: string | null + + // ── PLSS (G2) ── single legal-description string, e.g. "T24N R5W S33 SE-SE" + plss: string | null + utm_zone: string | null + + // ── Location accuracy (G2) ── + loc_acc_type: string | null + loc_acc_meas: string | null + loc_acc_val: string | null + + // ── Data-existence flags (G3) ── + scout_ticket: boolean | null + downhole_survey: boolean | null + geo_log: boolean | null + geophys_log: boolean | null + has_geothermal_data: boolean | null + petro_data: boolean | null + core_exists: boolean | null + cuttings: boolean | null + sample_data: boolean | null } diff --git a/src/interfaces/geothermal/index.ts b/src/interfaces/geothermal/index.ts index 5407360f..ac9f586a 100644 --- a/src/interfaces/geothermal/index.ts +++ b/src/interfaces/geothermal/index.ts @@ -1,2 +1,3 @@ export * from "./IWell"; export * from "./IWellRecord"; +export * from "./ITempDepthPoint"; diff --git a/src/interfaces/ocotillo/IFieldActivity.ts b/src/interfaces/ocotillo/IFieldActivity.ts index 5440647c..343859ac 100644 --- a/src/interfaces/ocotillo/IFieldActivity.ts +++ b/src/interfaces/ocotillo/IFieldActivity.ts @@ -31,6 +31,7 @@ export interface IFieldActivitySample { observations?: IFieldActivitySampleObservation[] contact?: { name?: string | null + organization?: string | null } | null } diff --git a/src/pages/example/DataGridPage.tsx b/src/pages/example/DataGridPage.tsx deleted file mode 100644 index 039db866..00000000 --- a/src/pages/example/DataGridPage.tsx +++ /dev/null @@ -1,721 +0,0 @@ -// TEMPORARY — Glide Data Grid specimen page. Delete when design work is settled. -import { useCallback, useContext, useEffect, useRef, useState } from 'react' -import '@glideapps/glide-data-grid/dist/index.css' -import { - DataEditor, - EditableGridCell, - GridCell, - GridCellKind, - GridColumn, - Item, -} from '@glideapps/glide-data-grid' -import { useList, useNavigation } from '@refinedev/core' -import { ColorModeContext } from '@/contexts' -import type { IWell } from '@/interfaces/ocotillo' -import { displayWellSiteName, formatAppDate } from '@/utils' -import { getContactDisplayName } from '@/utils/contactDisplayName' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { Separator } from '@/components/ui/separator' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select' -import { Checkbox } from '@/components/ui/checkbox' -import { Textarea } from '@/components/ui/textarea' -import { - EditPanel, - EditPanelField, - EditPanelLayout, - EditPanelSection, -} from '@/components/editing' -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from '@/components/ui/dialog' -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from '@/components/ui/tooltip' -import { - Download, - ExternalLink, - Filter, - Rows3, - Upload, - X, -} from 'lucide-react' - -const COLUMNS: GridColumn[] = [ - { title: 'Well ID', id: 'name', width: 140 }, - { title: 'Site Name', id: 'site_name', width: 200 }, - { title: 'Monitoring', id: 'monitoring_status', width: 160 }, - { title: 'Well Status', id: 'well_status', width: 150 }, - { title: 'Type', id: 'thing_type', width: 130 }, - { title: 'Release Status', id: 'release_status', width: 130 }, - { title: 'Well Depth (ft)', id: 'well_depth', width: 130 }, - { title: 'First Visit', id: 'first_visit_date', width: 120 }, - { title: 'Aquifers', id: 'aquifers', width: 240 }, - { title: 'Contacts', id: 'contacts', width: 240 }, - { title: 'Created', id: 'created_at', width: 120 }, -] - -function getCellValue(well: IWell, colId: string): string { - switch (colId) { - case 'name': return well.name ?? '' - case 'site_name': return displayWellSiteName(well) - case 'monitoring_status': return well.monitoring_status ?? '' - case 'well_status': return well.well_status ?? '' - case 'thing_type': return well.thing_type ?? '' - case 'release_status': return well.release_status ?? '' - case 'well_depth': return well.well_depth != null ? String(well.well_depth) : '' - case 'first_visit_date': return formatAppDate(well.first_visit_date) - case 'aquifers': return well.aquifers?.map(a => a.aquifer_system).join(', ') ?? '' - case 'contacts': return well.contacts?.map(c => getContactDisplayName(c)).join(', ') ?? '' - case 'created_at': return formatAppDate(well.created_at) - default: return '' - } -} - -function CreateWellPanel({ onClose }: { onClose: () => void }) { - return ( - - - - - } - > - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -