/** * GeoValidationService * * Service for validating geographic data in discovery locations. * All validation is done locally - no external API calls. */ import { isCoordinateValid, isWithinUS, isWithinCanada } from '../utils/GeoUtils'; export interface DiscoveryLocationGeoData { latitude: number | null; longitude: number | null; state_code: string | null; country_code: string | null; } export interface GeoValidationResult { ok: boolean; reason?: string; warnings?: string[]; } /** * Simple state-to-region mapping for rough validation. * This is a heuristic - not precise polygon matching. */ const STATE_REGION_HINTS: Record = { // West Coast 'WA': { latRange: [45.5, 49.0], lonRange: [-125, -116.9] }, 'OR': { latRange: [42.0, 46.3], lonRange: [-124.6, -116.5] }, 'CA': { latRange: [32.5, 42.0], lonRange: [-124.5, -114.1] }, // Southwest 'AZ': { latRange: [31.3, 37.0], lonRange: [-115, -109] }, 'NV': { latRange: [35.0, 42.0], lonRange: [-120, -114] }, 'NM': { latRange: [31.3, 37.0], lonRange: [-109, -103] }, 'UT': { latRange: [37.0, 42.0], lonRange: [-114, -109] }, // Mountain 'CO': { latRange: [37.0, 41.0], lonRange: [-109, -102] }, 'WY': { latRange: [41.0, 45.0], lonRange: [-111, -104] }, 'MT': { latRange: [45.0, 49.0], lonRange: [-116, -104] }, 'ID': { latRange: [42.0, 49.0], lonRange: [-117, -111] }, // Midwest 'MI': { latRange: [41.7, 48.3], lonRange: [-90.5, -82.4] }, 'IL': { latRange: [37.0, 42.5], lonRange: [-91.5, -87.0] }, 'OH': { latRange: [38.4, 42.0], lonRange: [-84.8, -80.5] }, 'MO': { latRange: [36.0, 40.6], lonRange: [-95.8, -89.1] }, // Northeast 'NY': { latRange: [40.5, 45.0], lonRange: [-79.8, -71.9] }, 'MA': { latRange: [41.2, 42.9], lonRange: [-73.5, -69.9] }, 'PA': { latRange: [39.7, 42.3], lonRange: [-80.5, -74.7] }, 'NJ': { latRange: [38.9, 41.4], lonRange: [-75.6, -73.9] }, // Southeast 'FL': { latRange: [24.5, 31.0], lonRange: [-87.6, -80.0] }, 'GA': { latRange: [30.4, 35.0], lonRange: [-85.6, -80.8] }, 'TX': { latRange: [25.8, 36.5], lonRange: [-106.6, -93.5] }, 'NC': { latRange: [34.0, 36.6], lonRange: [-84.3, -75.5] }, // Alaska & Hawaii 'AK': { latRange: [51.0, 72.0], lonRange: [-180, -130] }, 'HI': { latRange: [18.5, 22.5], lonRange: [-161, -154] }, // Canadian provinces (rough) 'ON': { latRange: [41.7, 57.0], lonRange: [-95.2, -74.3] }, 'BC': { latRange: [48.3, 60.0], lonRange: [-139, -114.0] }, 'AB': { latRange: [49.0, 60.0], lonRange: [-120, -110] }, 'QC': { latRange: [45.0, 62.6], lonRange: [-79.8, -57.1] }, }; export class GeoValidationService { /** * Validate a discovery location's geographic data. * * @param location Discovery location data with lat/lng and state/country codes * @returns Validation result with ok status and optional reason/warnings */ validateLocationState(location: DiscoveryLocationGeoData): GeoValidationResult { const warnings: string[] = []; // Check if coordinates exist if (location.latitude === null || location.longitude === null) { return { ok: true, // Not a failure - just no coordinates to validate reason: 'No coordinates available for validation', }; } // Check basic coordinate validity if (!isCoordinateValid(location.latitude, location.longitude)) { return { ok: false, reason: `Invalid coordinates: lat=${location.latitude}, lon=${location.longitude}`, }; } const lat = location.latitude; const lon = location.longitude; // Check country code consistency if (location.country_code === 'US') { if (!isWithinUS(lat, lon)) { return { ok: false, reason: `Coordinates (${lat}, ${lon}) are outside US bounds but country_code is US`, }; } } else if (location.country_code === 'CA') { if (!isWithinCanada(lat, lon)) { return { ok: false, reason: `Coordinates (${lat}, ${lon}) are outside Canada bounds but country_code is CA`, }; } } // Check state code consistency (if we have a hint for this state) if (location.state_code) { const hint = STATE_REGION_HINTS[location.state_code]; if (hint) { const [minLat, maxLat] = hint.latRange; const [minLon, maxLon] = hint.lonRange; // Allow some tolerance (coordinates might be near borders) const tolerance = 0.5; // degrees if ( lat < minLat - tolerance || lat > maxLat + tolerance || lon < minLon - tolerance || lon > maxLon + tolerance ) { warnings.push( `Coordinates (${lat.toFixed(4)}, ${lon.toFixed(4)}) may not match state ${location.state_code} ` + `(expected lat: ${minLat}-${maxLat}, lon: ${minLon}-${maxLon})` ); } } } return { ok: true, warnings: warnings.length > 0 ? warnings : undefined, }; } /** * Batch validate multiple locations. * * @param locations Array of discovery location data * @returns Map of validation results keyed by index */ validateLocations(locations: DiscoveryLocationGeoData[]): Map { const results = new Map(); locations.forEach((location, index) => { results.set(index, this.validateLocationState(location)); }); return results; } /** * Get a summary of validation results. * * @param results Map of validation results * @returns Summary with counts */ summarizeValidation(results: Map): { total: number; valid: number; invalid: number; noCoordinates: number; withWarnings: number; } { let valid = 0; let invalid = 0; let noCoordinates = 0; let withWarnings = 0; results.forEach((result) => { if (!result.ok) { invalid++; } else if (result.reason?.includes('No coordinates')) { noCoordinates++; } else { valid++; if (result.warnings && result.warnings.length > 0) { withWarnings++; } } }); return { total: results.size, valid, invalid, noCoordinates, withWarnings, }; } } export default GeoValidationService;