Enforce Ingress-style link-crossing prevention
A new link can no longer cross any existing link from either team — segmentsIntersect() does a standard orientation-based segment test, and routes/links.ts excludes shared-endpoint pairs (fanning multiple links out of the same portal is fine, that's not a crossing). 6 new tests; 37 total passing.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { haversineMeters, triangleAreaKm2 } from './geo.js';
|
||||
import { haversineMeters, segmentsIntersect, triangleAreaKm2 } from './geo.js';
|
||||
|
||||
describe('haversineMeters', () => {
|
||||
it('returns ~0 for the same point', () => {
|
||||
@@ -29,3 +29,39 @@ describe('triangleAreaKm2', () => {
|
||||
expect(area).toBeLessThan(1000); // sanity bound, not a precise reference value
|
||||
});
|
||||
});
|
||||
|
||||
describe('segmentsIntersect', () => {
|
||||
it('detects a clean X crossing', () => {
|
||||
const p1 = { lat: 10, lon: 10 };
|
||||
const q1 = { lat: 10, lon: 12 };
|
||||
const p2 = { lat: 9, lon: 11 };
|
||||
const q2 = { lat: 11, lon: 11 };
|
||||
expect(segmentsIntersect(p1, q1, p2, q2)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for parallel, non-crossing segments', () => {
|
||||
const p1 = { lat: 10, lon: 10 };
|
||||
const q1 = { lat: 10, lon: 12 };
|
||||
const p2 = { lat: 12, lon: 10 };
|
||||
const q2 = { lat: 12, lon: 12 };
|
||||
expect(segmentsIntersect(p1, q1, p2, q2)).toBe(false);
|
||||
});
|
||||
|
||||
it('treats a shared endpoint as an intersection at the raw geometry level', () => {
|
||||
// The shared point is a valid intersection point, so the raw test reports
|
||||
// true here — callers must explicitly exclude shared-endpoint pairs
|
||||
// themselves before treating this as a "crossing" (see routes/links.ts).
|
||||
const shared = { lat: 10, lon: 10 };
|
||||
const a = { lat: 10, lon: 12 };
|
||||
const b = { lat: 12, lon: 10 };
|
||||
expect(segmentsIntersect(shared, a, shared, b)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for two segments far apart', () => {
|
||||
const p1 = { lat: 0, lon: 0 };
|
||||
const q1 = { lat: 0, lon: 1 };
|
||||
const p2 = { lat: 5, lon: 5 };
|
||||
const q2 = { lat: 5, lon: 6 };
|
||||
expect(segmentsIntersect(p1, q1, p2, q2)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,3 +32,40 @@ export function triangleAreaKm2(a: LatLon, b: LatLon, c: LatLon): number {
|
||||
const pc = toXY(c);
|
||||
return Math.abs(pb.x * pc.y - pc.x * pb.y) / 2;
|
||||
}
|
||||
|
||||
function orientation(p: LatLon, q: LatLon, r: LatLon): 0 | 1 | 2 {
|
||||
const val = (q.lat - p.lat) * (r.lon - q.lon) - (q.lon - p.lon) * (r.lat - q.lat);
|
||||
if (Math.abs(val) < 1e-12) return 0; // collinear
|
||||
return val > 0 ? 1 : 2;
|
||||
}
|
||||
|
||||
function onSegment(p: LatLon, q: LatLon, r: LatLon): boolean {
|
||||
return (
|
||||
q.lon <= Math.max(p.lon, r.lon) + 1e-12 &&
|
||||
q.lon >= Math.min(p.lon, r.lon) - 1e-12 &&
|
||||
q.lat <= Math.max(p.lat, r.lat) + 1e-12 &&
|
||||
q.lat >= Math.min(p.lat, r.lat) - 1e-12
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard orientation-based segment-segment intersection test. Used to
|
||||
* enforce Ingress's "links can't cross" rule — treats lat/lon as a flat
|
||||
* plane, which is fine for the crossing question (a topological property)
|
||||
* even though it's not metrically accurate at scale.
|
||||
*/
|
||||
export function segmentsIntersect(p1: LatLon, q1: LatLon, p2: LatLon, q2: LatLon): boolean {
|
||||
const o1 = orientation(p1, q1, p2);
|
||||
const o2 = orientation(p1, q1, q2);
|
||||
const o3 = orientation(p2, q2, p1);
|
||||
const o4 = orientation(p2, q2, q1);
|
||||
|
||||
if (o1 !== o2 && o3 !== o4) return true;
|
||||
|
||||
if (o1 === 0 && onSegment(p1, p2, q1)) return true;
|
||||
if (o2 === 0 && onSegment(p1, q2, q1)) return true;
|
||||
if (o3 === 0 && onSegment(p2, p1, q2)) return true;
|
||||
if (o4 === 0 && onSegment(p2, q1, q2)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user