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
text/typescript
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 };
}
}