From 7fd2f2954c24cd300f48c21e1e0b8f438640c190 Mon Sep 17 00:00:00 2001 From: jakeross Date: Sat, 22 Aug 2026 07:55:12 -0700 Subject: [PATCH 1/2] refactor(map-layers): match OGC collections exactly and drop unpublished layers Layer resolution scored partial matches: exact 100, prefix 60, suffix 50, substring 20, with no minimum score and `exists: Boolean(bestMatch)`. Any substring hit bound a layer. With `locations` removed from the catalog per BDMS-978, the Locations layer would have silently rebound to `rock_sample_locations` (normalized, it ends with "locations"), rendering that collection twice on the map and displacing the real rock-sample entry on the collections page through its `seenCollectionIds` dedupe. Resolution is now exact against `id`/`collection_id`, then `name`/`title`, candidates tried in order. Separators and case are still normalized so one candidate covers both `water_elevation_contours` and its display title. A layer either names a published collection or does not exist. The regex token scorer that backstopped the water-elevation layers is gone too. Verified every registered layer against the deployed catalog: all 18 that survive the BDMS-977/978/979 trim resolve exactly, in both the map hook and the collections page. Removes the layers whose collections those tickets unpublish: ogc-locations locations BDMS-978 ogc-latest-depth-to-water latest_depth_to_water_wells BDMS-977 ogc-average-tds avg_tds_wells BDMS-977 ogc-other-thing-types other_things BDMS-979 All three bound by title, not id, which is why the shorter candidate lists still matched them today. Also removes ogc-water-elevation-contours: the catalog publishes no contour collection and never did, so the entry was dead before this change. Its derived counterpart, computed from water_elevation_wells via turf, was gated on `!waterElevationContours.exists` -- always true -- so that gate is now dropped and the derived layer is unconditional. Same runtime behavior. DEFAULT_VISIBLE_LAYERS pointed at ogc-latest-depth-to-water, so the map would have opened with nothing. It now opens on ogc-water-well-summary, the layer BDMS-977 names as making latest-depth-to-water redundant. DEPTH_LEGEND, latestDepthToWaterColorFromFeature, and averageTdsColorFromFeature had no remaining consumers and are removed. Merge order matters: this must not reach staging before the pygeoapi catalog change deploys, or four working layers disappear with nothing behind it. Co-Authored-By: Claude Opus 5 --- public/content/ogcapi.md | 3 +- src/components/MapPopupComponent.tsx | 49 ---- src/hooks/useThingLayers.tsx | 249 ++------------------- src/pages/ocotillo/collections/list.tsx | 42 ---- src/pages/ocotillo/map/list.tsx | 14 +- src/test/utils/mapPointInteraction.test.ts | 2 +- src/test/utils/ogcLayerUtils.test.ts | 76 +++++++ src/utils/mapSelection.ts | 2 - src/utils/ogcLayerUtils.ts | 142 +++--------- 9 files changed, 123 insertions(+), 456 deletions(-) create mode 100644 src/test/utils/ogcLayerUtils.test.ts 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, + } } From fef6720a2b7ad4197fce5bea1b3b7d6f1d430072 Mon Sep 17 00:00:00 2001 From: jakeross Date: Sat, 22 Aug 2026 07:55:21 -0700 Subject: [PATCH 2/2] docs: add CLAUDE.md pointing at the agent guide AGENTS.md already documents that feature, fix, chore, docs, and CI branches all base off staging, but there was no CLAUDE.md on any branch, so Claude Code never loaded it. Cutting a branch from the default branch, production, produces a PR into staging that drags every commit staging is missing. Imports AGENTS.md and restates the branching rule up front. Carries the model-attribution frontmatter the repo's documentation rule requires. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 CLAUDE.md 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