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/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/src/components/MapPopupComponent.tsx b/src/components/MapPopupComponent.tsx index 5ff1fdcd..d5633785 100644 --- a/src/components/MapPopupComponent.tsx +++ b/src/components/MapPopupComponent.tsx @@ -120,20 +120,16 @@ 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', } return labelByLayer[layerKey] || titleCase(layerKey.replace(/^ogc-/, '')) @@ -149,8 +145,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 +197,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 +427,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/hooks/useThingLayers.tsx b/src/hooks/useThingLayers.tsx index 1c72bde9..64861906 100644 --- a/src/hooks/useThingLayers.tsx +++ b/src/hooks/useThingLayers.tsx @@ -12,11 +12,8 @@ import { import { OgcCollectionRecord, resolveCollection, - DEPTH_LEGEND, TDS_LEGEND, TREND_LEGEND, - latestDepthToWaterColorFromFeature, - averageTdsColorFromFeature, latestTdsColorFromFeature, trendColorFromFeature, } from '@/utils/ogcLayerUtils' @@ -62,71 +59,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 +101,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 +128,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,33 +150,6 @@ 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 latestTdsLayer = useOGCLayer({ collection: latestTds.id, label: latestTds.label, @@ -363,23 +208,13 @@ export const useThingLayers = ( color: '#00acc1', 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, @@ -477,9 +312,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' ) @@ -518,31 +350,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: [ @@ -814,12 +621,6 @@ export const useThingLayers = ( }, 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, @@ -870,13 +671,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 +682,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 +692,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 +717,6 @@ export const useThingLayers = ( meteorologicalStationsLayer ) addLayer('ogc-project-areas', projectAreas, projectAreasLayer) - addLayer('ogc-other-thing-types', otherThingTypes, otherThingTypesLayer) addLayer( 'ogc-outfalls-return-flow', outfallsReturnFlow, @@ -951,9 +737,6 @@ export const useThingLayers = ( return result }, [ collectionsData, - locationsLayer, - latestDepthToWaterLayer, - averageTdsLayer, latestTdsLayer, majorChemistryLayer, minorChemistryLayer, @@ -962,7 +745,6 @@ export const useThingLayers = ( waterWellsLayer, activelyMonitoredLayer, springsLayer, - waterElevationContoursLayerStyled, waterElevationPointsLayerStyled, waterElevationDerivedContoursLayer, surfaceWaterDiversionsLayer, @@ -970,7 +752,6 @@ export const useThingLayers = ( lakesPondsReservoirsLayer, meteorologicalStationsLayer, projectAreasLayer, - otherThingTypesLayer, outfallsReturnFlowLayer, perennialStreamsLayer, rockSampleLocationsLayer, diff --git a/src/pages/ocotillo/collections/list.tsx b/src/pages/ocotillo/collections/list.tsx index 0cc4c444..612ad570 100644 --- a/src/pages/ocotillo/collections/list.tsx +++ b/src/pages/ocotillo/collections/list.tsx @@ -93,29 +93,6 @@ const GROUP_STYLES: Record< } const REGISTERED_MAP_COLLECTIONS: RegisteredMapCollection[] = [ - { - layerKey: 'ogc-locations', - groupKey: 'reference', - candidates: ['Locations', 'locations'], - }, - { - layerKey: 'ogc-latest-depth-to-water', - groupKey: 'groundwater', - candidates: [ - 'Latest Depth to Water (Water Wells)', - 'latest_depth_to_water_water_wells', - 'latest_depth_to_water', - ], - }, - { - layerKey: 'ogc-average-tds', - groupKey: 'groundwater', - candidates: [ - 'Average TDS (Water Wells)', - 'average_tds_water_wells', - 'average_tds', - ], - }, { layerKey: 'ogc-latest-tds', groupKey: 'groundwater', @@ -181,20 +158,6 @@ const REGISTERED_MAP_COLLECTIONS: RegisteredMapCollection[] = [ groupKey: 'surfaceWater', candidates: ['Springs', 'springs'], }, - { - layerKey: 'ogc-water-elevation-contours', - groupKey: 'groundwater', - candidates: [ - 'Water Elevation Contours', - 'water_elevation_contours', - 'water_elevation_contour', - 'groundwater_elevation_contours', - 'water_level_contours', - 'water_table_contours', - 'potentiometric_surface_contours', - 'piezometric_contours', - ], - }, { layerKey: 'ogc-water-elevation-points', groupKey: 'groundwater', @@ -245,11 +208,6 @@ const REGISTERED_MAP_COLLECTIONS: RegisteredMapCollection[] = [ ], displayLabel: 'AMP Project Areas', }, - { - layerKey: 'ogc-other-thing-types', - groupKey: 'reference', - candidates: ['Other Thing Types', 'other_thing_types'], - }, { layerKey: 'ogc-outfalls-return-flow', groupKey: 'surfaceWater', diff --git a/src/pages/ocotillo/map/list.tsx b/src/pages/ocotillo/map/list.tsx index 6f2eae0e..38f68743 100644 --- a/src/pages/ocotillo/map/list.tsx +++ b/src/pages/ocotillo/map/list.tsx @@ -63,7 +63,7 @@ function localDateStampForExport(): string { return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` } -const DEFAULT_VISIBLE_LAYERS = ['ogc-latest-depth-to-water'] +const DEFAULT_VISIBLE_LAYERS = ['ogc-water-well-summary'] const VISIBLE_FEATURES_DRAWER_WIDTH = 360 const VISIBLE_FEATURES_PAGE_SIZE = 10 type VisibleFeatureGroup = { @@ -85,16 +85,6 @@ const PRINCIPAL_VISIBLE_FEATURE_DETAIL_BY_LAYER: Record< string, { column: string; label: string; dateColumn?: string } > = { - 'ogc-latest-depth-to-water': { - column: 'depth_to_water_bgs', - label: 'Depth to water', - dateColumn: 'observation_datetime', - }, - 'ogc-average-tds': { - column: 'avg_tds_value', - label: 'Avg TDS', - dateColumn: 'first_tds_observation_date', - }, 'ogc-latest-tds': { column: 'latest_tds_value', label: 'Latest TDS', @@ -786,8 +776,6 @@ export const MapView: React.FC = () => { const isWaterWellLayer = layerId.includes('ogc-water-wells') || layerId.includes('ogc-water-well-summary') || - layerId.includes('ogc-latest-depth-to-water') || - layerId.includes('ogc-average-tds') || layerId.includes('ogc-latest-tds') || layerId.includes('ogc-depth-to-water-trend') diff --git a/src/test/utils/mapPointInteraction.test.ts b/src/test/utils/mapPointInteraction.test.ts index b224365f..eadd585b 100644 --- a/src/test/utils/mapPointInteraction.test.ts +++ b/src/test/utils/mapPointInteraction.test.ts @@ -15,7 +15,7 @@ describe('map point interaction', () => { it('deduplicates one well rendered in multiple data layers', () => { const features = [ point(42, [-106.1, 35.1], 'location-ogc-water-wells'), - point(42, [-106.1, 35.1], 'location-ogc-latest-depth-to-water'), + point(42, [-106.1, 35.1], 'location-ogc-latest-tds'), ] expect(getDistinctMapPoints(features)).toEqual([features[0]]) diff --git a/src/test/utils/ogcLayerUtils.test.ts b/src/test/utils/ogcLayerUtils.test.ts new file mode 100644 index 00000000..6180725f --- /dev/null +++ b/src/test/utils/ogcLayerUtils.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from 'vitest' +import { resolveCollection } from '@/utils/ogcLayerUtils' + +const collection = (id: string, title?: string) => ({ id, title }) + +describe('resolveCollection', () => { + it('matches a collection id exactly', () => { + const resolved = resolveCollection( + [collection('water_wells', 'Water Wells')], + ['Water Wells', 'water_wells'] + ) + + expect(resolved.exists).toBe(true) + expect(resolved.id).toBe('water_wells') + expect(resolved.label).toBe('Water Wells') + }) + + it('ignores separators and case when comparing names', () => { + const resolved = resolveCollection( + [collection('Water_Elevation_Contours')], + ['water elevation contours'] + ) + + expect(resolved.id).toBe('Water_Elevation_Contours') + }) + + it('does not bind to a collection that merely contains the candidate', () => { + const resolved = resolveCollection( + [collection('rock_sample_locations', 'Rock Sample Locations')], + ['Locations', 'locations'] + ) + + expect(resolved.exists).toBe(false) + expect(resolved.id).toBe('') + }) + + it('does not bind to a collection the candidate is a prefix of', () => { + const resolved = resolveCollection( + [collection('latest_depth_to_water_wells')], + ['latest_depth_to_water'] + ) + + expect(resolved.exists).toBe(false) + }) + + it('reports no match when the catalog no longer publishes the collection', () => { + const resolved = resolveCollection( + [collection('water_wells'), collection('springs')], + ['Average TDS (Water Wells)', 'avg_tds_wells'] + ) + + expect(resolved.exists).toBe(false) + expect(resolved.label).toBe('Average TDS') + }) + + it('prefers a canonical identifier over another collection title', () => { + const resolved = resolveCollection( + [ + collection('legacy_springs', 'springs'), + collection('springs', 'Springs'), + ], + ['springs'] + ) + + expect(resolved.id).toBe('springs') + }) + + it('tries candidates in order', () => { + const resolved = resolveCollection( + [collection('project_area'), collection('project_areas')], + ['project_areas', 'project_area'] + ) + + expect(resolved.id).toBe('project_areas') + }) +}) diff --git a/src/utils/mapSelection.ts b/src/utils/mapSelection.ts index e5418b16..7272c773 100644 --- a/src/utils/mapSelection.ts +++ b/src/utils/mapSelection.ts @@ -292,7 +292,6 @@ export const getSelectedPointColumns = ( ] const preferredColumnsByLayer: Record = { - 'ogc-average-tds': ['name'], 'ogc-actively-monitored': [ 'name', 'last_water_level_datetime', @@ -300,7 +299,6 @@ export const getSelectedPointColumns = ( 'total_water_levels', ], 'ogc-depth-to-water-trend': ['name'], - 'ogc-latest-depth-to-water': ['name', 'observation_datetime'], 'ogc-latest-tds': ['name', 'observation_datetime'], 'ogc-major-chemistry': ['name', 'latest_chemistry_date', 'analyte_count'], 'ogc-minor-chemistry': ['name', 'latest_chemistry_date', 'analyte_count'], diff --git a/src/utils/ogcLayerUtils.ts b/src/utils/ogcLayerUtils.ts index 1d7b307e..e8bcbbb7 100644 --- a/src/utils/ogcLayerUtils.ts +++ b/src/utils/ogcLayerUtils.ts @@ -21,13 +21,6 @@ export const TDS_LEGEND = { maxLabel: '5000+ mg/L', } -export const DEPTH_LEGEND = { - gradient: - 'linear-gradient(90deg, #1a9850 0%, #66bd63 25%, #a6d96a 50%, #fee08b 70%, #f46d43 85%, #d73027 100%)', - minLabel: 'Shallow', - maxLabel: 'Deep', -} - export const TREND_LEGEND = { gradient: 'linear-gradient(90deg, #2c7bb6 0%, #bdbdbd 50%, #d73027 100%)', minLabel: 'Declining', @@ -111,63 +104,6 @@ export const latestTdsColorFromFeature = (feature: any): string | undefined => { return '#d73027' } -export const averageTdsColorFromFeature = (feature: any): string | undefined => { - const value = findNumericPropertyWithPriority( - feature, - [ - /(average|avg|mean).*(tds|dissolved.*solids)/i, - /(tds|dissolved.*solids).*(average|avg|mean)/i, - ], - [/tds/i, /dissolved.*solids/i], - [/count/i, /num/i, /code/i, /id$/i, /unit/i, /rank/i, /class/i, /flag/i, /latest/i] - ) - if (value === undefined) return undefined - if (value < 300) return '#2b83ba' - if (value < 500) return '#4daf4a' - if (value < 1000) return '#a6d96a' - if (value < 2000) return '#fee08b' - if (value < 5000) return '#f46d43' - return '#d73027' -} - -export const latestDepthToWaterColorFromFeature = ( - feature: any -): string | undefined => { - const value = findNumericPropertyWithPriority( - feature, - [ - /(latest|recent|most).*(depth.*water|depth_to_water|water_level|depth_to_water_bgs)/i, - /(depth.*water|depth_to_water|water_level|depth_to_water_bgs).*(latest|recent|most)/i, - ], - [/depth.*water/i, /depth_to_water/i, /water_level/i, /depth_to_water_bgs/i], - [ - /count/i, - /num/i, - /code/i, - /id$/i, - /unit/i, - /rank/i, - /class/i, - /flag/i, - /avg/i, - /average/i, - /mean/i, - /median/i, - /min/i, - /max/i, - /trend/i, - /slope/i, - ] - ) - if (value === undefined) return undefined - if (value < 25) return '#1a9850' - if (value < 75) return '#66bd63' - if (value < 150) return '#a6d96a' - if (value < 250) return '#fee08b' - if (value < 400) return '#f46d43' - return '#d73027' -} - export const trendColorFromFeature = (feature: any): string | undefined => { const label = findStringProperty(feature, [/trend/i, /trend_class/i])?.toLowerCase() if (label) { @@ -195,56 +131,36 @@ export const resolveCollection = ( collections: OgcCollectionRecord[], candidates: string[] ): ResolvedCollection => { - const normalizedCandidates = candidates.map(normalize) - const keysForCollection = (collection: OgcCollectionRecord) => ({ - primary: [normalize(collection.id), normalize(collection.collection_id)].filter( - Boolean - ), - secondary: [normalize(collection.name), normalize(collection.title)].filter( - Boolean - ), - }) - - const scoreMatch = (key: string, candidate: string): number => { - if (!key || !candidate) return 0 - if (key === candidate) return 100 - if (key.startsWith(candidate)) return 60 - if (key.endsWith(candidate)) return 50 - if (key.includes(candidate)) return 20 - return 0 - } - + const primaryKeys = (collection: OgcCollectionRecord): string[] => + [collection.id, collection.collection_id].map(normalize).filter(Boolean) + const secondaryKeys = (collection: OgcCollectionRecord): string[] => + [collection.name, collection.title].map(normalize).filter(Boolean) + + const findExact = ( + candidate: string, + keysFor: (collection: OgcCollectionRecord) => string[] + ): OgcCollectionRecord | undefined => + collections.find((collection) => keysFor(collection).includes(candidate)) + + // Matching is exact. A registered layer names the collections it can bind + // to, and binds to none of them if the catalog does not publish one -- + // partial matching used to bind a layer to an unrelated collection whose + // name merely contained the candidate. let bestMatch: OgcCollectionRecord | undefined - let bestScore = 0 - - for (const collection of collections) { - const { primary, secondary } = keysForCollection(collection) - const allKeys = [...primary, ...secondary] - - for (const candidate of normalizedCandidates) { - for (const key of allKeys) { - const baseScore = scoreMatch(key, candidate) - if (baseScore === 0) continue - - // Prefer canonical identifiers over display labels. - const canonicalBoost = primary.includes(key) ? 5 : 0 - const score = baseScore + canonicalBoost - - if (score > bestScore) { - bestScore = score - bestMatch = collection - } - } - } + + for (const candidate of candidates.map(normalize).filter(Boolean)) { + bestMatch = + findExact(candidate, primaryKeys) ?? findExact(candidate, secondaryKeys) + if (bestMatch) break } - return { - id: bestMatch?.id || bestMatch?.collection_id || bestMatch?.name || '', - label: - bestMatch?.title || - bestMatch?.name || - candidates[0].replace(/\s*\(Water Wells\)\s*/g, ''), - exists: Boolean(bestMatch), - description: bestMatch?.description || bestMatch?.abstract, - } + return { + id: bestMatch?.id || bestMatch?.collection_id || bestMatch?.name || '', + label: + bestMatch?.title || + bestMatch?.name || + candidates[0].replace(/\s*\(Water Wells\)\s*/g, ''), + exists: Boolean(bestMatch), + description: bestMatch?.description || bestMatch?.abstract, + } }