UNPKG

geo-intersection-util

Version:

Point of intersection defined by its offset from the centerline and distance along the centerline

343 lines (286 loc) 9.82 kB
export interface LatLng { lat: number; lng: number; } export interface Cartesian { x: number; y: number; } interface IntersectionResult { intersectionLatLng: LatLng; distance: number; side: 'left' | 'right' | 'center' | null; } export class Intersection { constructor() {} getInterSection( pathString: string, shotLatLngStr: string, // changed from LatLng to string greenCenter: string ): IntersectionResult | undefined { try { const shotLatLng = this.parseLatLng(shotLatLngStr); // <-- parse it pathString = this.orientCenterLine(pathString, greenCenter); const centerPath = this.getPoint(pathString, 500); const intersectionLatLng = this.findIntersectionPoint(shotLatLng, centerPath); if (intersectionLatLng) { const cartesianPoint1 = this.latLngToCartesian(shotLatLng.lat, shotLatLng.lng); const cartesianIntersection = this.latLngToCartesian( intersectionLatLng.lat, intersectionLatLng.lng ); const distanceInEuclidean = this.euclideanDistance(cartesianPoint1, cartesianIntersection); const side = this.determineSideOnCurvedCenterline(centerPath, shotLatLng) as | 'left' | 'right' | 'center' | null; return { intersectionLatLng, distance: distanceInEuclidean, side, }; } return { intersectionLatLng: { lat: 0, lng: 0 }, distance: 0, side: null, }; } catch (e) { console.error(e, 'getInterSection Error'); } } calculateBearing(lat1: number, lng1: number, lat2: number, lng2: number): number { try { const lat1Rad = (lat1 * Math.PI) / 180; const lat2Rad = (lat2 * Math.PI) / 180; const deltaLngRad = ((lng2 - lng1) * Math.PI) / 180; const y = Math.sin(deltaLngRad) * Math.cos(lat2Rad); const x = Math.cos(lat1Rad) * Math.sin(lat2Rad) - Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(deltaLngRad); const bearingRad = Math.atan2(y, x); const bearingDeg = (bearingRad * 180) / Math.PI; return (bearingDeg + 360) % 360; } catch (e) { console.error(e, 'calculateBearing Error'); return 0; } } extendLatLng(lat: number, lng: number, distance: number, bearing: number): LatLng { try { const R = 6378137; const distanceRad = distance / R; const bearingRad = (bearing * Math.PI) / 180; const latRad = (lat * Math.PI) / 180; const lngRad = (lng * Math.PI) / 180; const newLatRad = Math.asin( Math.sin(latRad) * Math.cos(distanceRad) + Math.cos(latRad) * Math.sin(distanceRad) * Math.cos(bearingRad) ); const newLngRad = lngRad + Math.atan2( Math.sin(bearingRad) * Math.sin(distanceRad) * Math.cos(latRad), Math.cos(distanceRad) - Math.sin(latRad) * Math.sin(newLatRad) ); return { lat: (newLatRad * 180) / Math.PI, lng: (newLngRad * 180) / Math.PI, }; } catch (e) { console.error(e, 'extendLatLng Error'); return { lat, lng }; } } getPoint(pointString: string, extensionDistance: number): LatLng[] { try { const points = this.getLatLngFromPointsStr(pointString); if (points.length === 2) { const [firstPoint, secondPoint] = points; const forwardBearing = this.calculateBearing( firstPoint.lat, firstPoint.lng, secondPoint.lat, secondPoint.lng ); const reverseBearing = (forwardBearing + 180) % 360; const extendedFirst = this.extendLatLng( firstPoint.lat, firstPoint.lng, extensionDistance, reverseBearing ); const extendedSecond = this.extendLatLng( secondPoint.lat, secondPoint.lng, extensionDistance, forwardBearing ); return [extendedFirst, firstPoint, secondPoint, extendedSecond]; } const lastPoint = points[points.length - 1]; const secondLast = points[points.length - 2]; const bearing = this.calculateBearing( secondLast.lat, secondLast.lng, lastPoint.lat, lastPoint.lng ); const newPoint = this.extendLatLng( lastPoint.lat, lastPoint.lng, extensionDistance, bearing ); return [...points, newPoint]; } catch (e) { console.error(e, 'getPoint Error'); return []; } } determineSideOnCurvedCenterline(centerlinePoints: LatLng[], point: LatLng): 'left' | 'right' | 'center' | string { try { let closestSegmentIndex = -1; let minDistance = Infinity; for (let i = 0; i < centerlinePoints.length - 1; i++) { const start = centerlinePoints[i]; const end = centerlinePoints[i + 1]; const t = Math.max( 0, Math.min( 1, ((point.lng - start.lng) * (end.lng - start.lng) + (point.lat - start.lat) * (end.lat - start.lat)) / ((end.lng - start.lng) ** 2 + (end.lat - start.lat) ** 2) ) ); const closest = { lng: start.lng + t * (end.lng - start.lng), lat: start.lat + t * (end.lat - start.lat), }; const distance = this.getDistance(point, closest); if (distance < minDistance) { minDistance = distance; closestSegmentIndex = i; } } if (closestSegmentIndex >= 0) { const start = centerlinePoints[closestSegmentIndex]; const end = centerlinePoints[closestSegmentIndex + 1]; const centerVec = { x: end.lng - start.lng, y: end.lat - start.lat }; const pointVec = { x: point.lng - start.lng, y: point.lat - start.lat }; const crossProduct = centerVec.x * pointVec.y - centerVec.y * pointVec.x; if (crossProduct > 0) return 'left'; else if (crossProduct < 0) return 'right'; else return 'center'; } return 'error - could not determine side'; } catch (e) { console.error(e, 'determineSideOnCurvedCenterline Error'); return 'error'; } } euclideanDistance(p1: Cartesian, p2: Cartesian): number { return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2); } latLngToCartesian(lat: number, lng: number): Cartesian { const R = 6378137; return { x: R * ((lng * Math.PI) / 180), y: R * Math.log(Math.tan(Math.PI / 4 + (lat * Math.PI) / 360)), }; } getDistance(p1: LatLng, p2: LatLng): number { return Math.sqrt((p1.lat - p2.lat) ** 2 + (p1.lng - p2.lng) ** 2); } distance(p1: Cartesian, p2: Cartesian): number { return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2); } findPerpendicularIntersection(p: Cartesian, p1: Cartesian, p2: Cartesian): Cartesian | null { try { const A = p.x - p1.x; const B = p.y - p1.y; const C = p2.x - p1.x; const D = p2.y - p1.y; const dot = A * C + B * D; const lenSq = C ** 2 + D ** 2; const param = lenSq !== 0 ? dot / lenSq : -1; if (param < 0 || param > 1) return null; return { x: p1.x + param * C, y: p1.y + param * D, }; } catch (e) { console.error(e, 'findPerpendicularIntersection Error'); return null; } } cartesianToLatLng(p: Cartesian): LatLng { const R = 6378137; return { lng: (p.x / R) * (180 / Math.PI), lat: (2 * Math.atan(Math.exp(p.y / R)) - Math.PI / 2) * (180 / Math.PI), }; } findIntersectionPoint(point: LatLng, centerLine: LatLng[]): LatLng | null { try { const cartPoint = this.latLngToCartesian(point.lat, point.lng); let closest: Cartesian | null = null; let minDistance = Infinity; for (let i = 0; i < centerLine.length - 1; i++) { const start = this.latLngToCartesian(centerLine[i].lat, centerLine[i].lng); const end = this.latLngToCartesian(centerLine[i + 1].lat, centerLine[i + 1].lng); const inter = this.findPerpendicularIntersection(cartPoint, start, end); const nearest = inter ?? (this.distance(cartPoint, start) < this.distance(cartPoint, end) ? start : end); const dist = this.distance(cartPoint, nearest); if (dist < minDistance) { minDistance = dist; closest = nearest; } } return closest ? this.cartesianToLatLng(closest) : null; } catch (e) { console.error(e, 'findIntersectionPoint Error'); return null; } } getLatLngFromPointsStr(pointsStr: string): LatLng[] { try { const segments = pointsStr.split(','); return segments.map((seg) => { const [lng, lat] = seg.trim().split(' ').map(Number); return { lat, lng }; }); } catch (e) { console.error(e, 'getLatLngFromPointsStr Error'); return []; } } parseCoordinate(coordinate: string): Cartesian { const [x, y] = coordinate.split(' ').map(Number); return { x, y }; } orientCenterLine(centerLine: string, greenCenter: string): string { const coords = centerLine.split(','); const target = this.parseCoordinate(greenCenter); let closestIndex = 0; let minDistance = Infinity; coords.forEach((coord, index) => { const parsed = this.parseCoordinate(coord); const distance = this.euclideanDistance(parsed, target); if (distance < minDistance) { minDistance = distance; closestIndex = index; } }); if (closestIndex === 0) { return coords.reverse().map((x) => x.trim()).join(','); } return coords.map((x) => x.trim()).join(','); } parseLatLng(coordStr: string): LatLng { const [lng, lat] = coordStr.trim().split(' ').map(Number); return { lat, lng }; } }