import fs from 'fs'; import path from 'path'; import { logger } from '../middleware/logger'; interface CountryFeature { type: 'Feature'; properties: { 'ISO3166-1-Alpha-2': string; name: string }; geometry: { type: 'Polygon' | 'MultiPolygon'; coordinates: number[][][] | number[][][][]; }; } interface CountriesGeoJSON { type: 'FeatureCollection'; features: CountryFeature[]; } let countriesData: CountriesGeoJSON | null = null; function loadBoundaries(): CountriesGeoJSON { if (countriesData) return countriesData; try { const dataPath = path.join(__dirname, '../../data/boundaries.geojson'); const raw = fs.readFileSync(dataPath, 'utf-8'); countriesData = JSON.parse(raw) as CountriesGeoJSON; logger.info({ features: countriesData.features.length }, 'loaded country boundaries'); } catch (err) { logger.warn({ err }, 'could not load boundaries.geojson, border detection disabled'); countriesData = { type: 'FeatureCollection', features: [] }; } return countriesData; } function pointInPolygon(point: [number, number], polygon: number[][]): boolean { const [x, y] = point; let inside = false; for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { const [xi, yi] = polygon[i]; const [xj, yj] = polygon[j]; if (((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) { inside = !inside; } } return inside; } function getCountryForPoint(lat: number, lng: number, boundaries: CountriesGeoJSON): string { for (const feature of boundaries.features) { const { geometry } = feature; if (geometry.type === 'Polygon') { if (pointInPolygon([lng, lat], geometry.coordinates[0] as number[][])) { return feature.properties['ISO3166-1-Alpha-2']; } } else if (geometry.type === 'MultiPolygon') { for (const polygon of geometry.coordinates as number[][][][]) { if (pointInPolygon([lng, lat], polygon[0])) { return feature.properties['ISO3166-1-Alpha-2']; } } } } return 'UNKNOWN'; } export async function detectBorderCrossing( start: [number, number], end: [number, number], routeCoords: [number, number][] ): Promise<{ crossesBorder: boolean; countries: string[] }> { try { const boundaries = loadBoundaries(); if (boundaries.features.length === 0) { return { crossesBorder: false, countries: [] }; } const MAX_SAMPLES = 40; const step = Math.max(1, Math.floor(routeCoords.length / MAX_SAMPLES)); const sampledPoints: [number, number][] = [start]; for (let i = step; i < routeCoords.length - 1; i += step) { sampledPoints.push(routeCoords[i]); if (sampledPoints.length >= MAX_SAMPLES) break; } sampledPoints.push(end); const countriesSet = new Set(); for (const [lat, lng] of sampledPoints) { const country = getCountryForPoint(lat, lng, boundaries); if (country && country !== 'UNKNOWN' && country !== '-99' && country !== '-1') { countriesSet.add(country); } } const countries = Array.from(countriesSet); const crossesBorder = countries.length > 1; logger.info({ countries, crossesBorder }, 'border detection result'); return { crossesBorder, countries }; } catch (err) { logger.error({ err }, 'border detection failed'); return { crossesBorder: false, countries: [] }; } }