import { describe, it, expect } from 'vitest'; import { computeOffset, sectorPath, pointInRing, distanceBetween, pointInShape, pointsInShape, } from './map-geometry-math'; const POA = { lat: -30.0346, lng: -51.2177 }; describe('computeOffset', () => { it('moves north for heading 0', () => { const north = computeOffset(POA, 1000, 0); expect(north.lat).toBeGreaterThan(POA.lat); expect(north.lng).toBeCloseTo(POA.lng, 6); }); it('moves east for heading 90', () => { const east = computeOffset(POA, 1000, 90); expect(east.lng).toBeGreaterThan(POA.lng); expect(east.lat).toBeCloseTo(POA.lat, 4); }); it('travels the requested distance', () => { const target = computeOffset(POA, 5000, 42); expect(distanceBetween(POA, target)).toBeCloseTo(5000, 0); }); it('is reversible', () => { const out = computeOffset(POA, 2500, 137); const back = computeOffset(out, 2500, 137 - 180); expect(back.lat).toBeCloseTo(POA.lat, 5); expect(back.lng).toBeCloseTo(POA.lng, 5); }); }); describe('sectorPath', () => { it('starts and ends at the vertex so the beam is closed', () => { const path = sectorPath({ center: POA, azimuth: 250, beamWidth: 120, radiusMeters: 1600 }); expect(path[0]).toEqual(POA); expect(path[path.length - 1]).toEqual(POA); }); it('keeps every arc point at the declared radius', () => { const path = sectorPath({ center: POA, azimuth: 0, beamWidth: 90, radiusMeters: 3000 }); path.slice(1, -1).forEach(point => { expect(distanceBetween(POA, point)).toBeCloseTo(3000, 0); }); }); it('spans the beam width around the azimuth', () => { const path = sectorPath({ center: POA, azimuth: 90, beamWidth: 60, radiusMeters: 1000 }); const arc = path.slice(1, -1); // Edges sit at 60 and 120 degrees; both are east of the site. expect(arc[0].lng).toBeGreaterThan(POA.lng); expect(arc[arc.length - 1].lng).toBeGreaterThan(POA.lng); // The 60-degree edge is north of the 120-degree edge. expect(arc[0].lat).toBeGreaterThan(arc[arc.length - 1].lat); }); it('drops the vertex for a full circle so the ring does not fold back', () => { const path = sectorPath({ center: POA, azimuth: 0, beamWidth: 360, radiusMeters: 500 }); expect(path[0]).not.toEqual(POA); path.forEach(point => expect(distanceBetween(POA, point)).toBeCloseTo(500, 0)); }); }); describe('pointInRing', () => { const square = [ { lat: 0, lng: 0 }, { lat: 0, lng: 10 }, { lat: 10, lng: 10 }, { lat: 10, lng: 0 }, ]; it('accepts an interior point', () => { expect(pointInRing({ lat: 5, lng: 5 }, square)).toBe(true); }); it('rejects an exterior point', () => { expect(pointInRing({ lat: 15, lng: 5 }, square)).toBe(false); expect(pointInRing({ lat: 5, lng: -1 }, square)).toBe(false); }); it('handles a concave ring', () => { // An L shape: the notch must read as outside. const l = [ { lat: 0, lng: 0 }, { lat: 0, lng: 10 }, { lat: 4, lng: 10 }, { lat: 4, lng: 4 }, { lat: 10, lng: 4 }, { lat: 10, lng: 0 }, ]; expect(pointInRing({ lat: 2, lng: 2 }, l)).toBe(true); expect(pointInRing({ lat: 8, lng: 8 }, l)).toBe(false); }); }); describe('pointInShape', () => { it('tests a circle by great-circle distance', () => { const shape = { type: 'circle' as const, center: POA, radiusMeters: 1000 }; expect(pointInShape(computeOffset(POA, 500, 30), shape)).toBe(true); expect(pointInShape(computeOffset(POA, 1500, 30), shape)).toBe(false); }); it('tests a rectangle by its bounds', () => { const shape = { type: 'rectangle' as const, bounds: { north: 10, south: 0, east: 10, west: 0 }, }; expect(pointInShape({ lat: 5, lng: 5 }, shape)).toBe(true); expect(pointInShape({ lat: 11, lng: 5 }, shape)).toBe(false); }); }); describe('pointsInShape', () => { it('answers "who was inside the fence"', () => { const points = [ { lat: 5, lng: 5, id: 'dentro' }, { lat: 50, lng: 50, id: 'fora' }, ]; const inside = pointsInShape(points, { type: 'rectangle', bounds: { north: 10, south: 0, east: 10, west: 0 }, }); expect(inside).toHaveLength(1); expect(inside[0].id).toBe('dentro'); }); it('preserves the caller payload', () => { const points = [{ lat: 5, lng: 5, caso: '2026-014' }]; const [found] = pointsInShape(points, { type: 'rectangle', bounds: { north: 10, south: 0, east: 10, west: 0 }, }); expect(found.caso).toBe('2026-014'); }); });