import { test } from 'node:test'; import assert from 'node:assert/strict'; import { adjustGeometry, distanceMeters, distanceToGeometry, overlayAnchor, IDENTITY_OVERLAY, type AreaGeometry, type LonLat, } from '../src/features/citymap/geo'; import bundled from '../src/features/citymap/parkingAreas.json'; /** * The city-map geometry, checked against the real bundled area set. * * This is the maths that decides which street you are tracking time on, so it is * tested against the actual 49 areas rather than toy shapes — the awkward cases * (an L-shaped run down two streets, a crescent-shaped beach lot) only exist in * the real data. * * Run with: npm test --workspace app */ interface Area { id: string; shape: 'line' | 'polygon'; geometry: AreaGeometry; } const areas: Area[] = (bundled as any).features.map((f: any) => ({ ...f.properties, geometry: f.geometry, })); const geoms: AreaGeometry[] = areas.map((a) => a.geometry); /** * Points that genuinely lie on an area: edge midpoints. A centroid is no good — * an L-shaped run's lands mid-block and the crescent City Beach lot's lands in * the water. */ function onGeometry(g: AreaGeometry): LonLat[] { const rings = g.type === 'Polygon' ? g.coordinates : [g.coordinates]; const out: LonLat[] = []; for (const r of rings) { for (let i = 1; i < r.length; i++) { out.push([(r[i - 1][0] + r[i][0]) / 2, (r[i - 1][1] + r[i][1]) / 2]); } } return out; } function nearest(p: LonLat): { area: Area; dist: number } { let best = areas[0]; let bd = Infinity; for (const a of areas) { const d = distanceToGeometry(p, a.geometry); if (d < bd) { best = a; bd = d; } } return { area: best, dist: bd }; } test('the bundled map has the expected shape', () => { assert.equal(areas.length, 49); assert.ok(areas.some((a) => a.shape === 'polygon'), 'city lots should be polygons'); assert.ok(areas.some((a) => a.shape === 'line'), 'on-street runs should be lines'); }); test('a point on an area measures zero distance to it', () => { for (const a of areas) { for (const p of onGeometry(a.geometry)) { const d = distanceToGeometry(p, a.geometry); assert.ok(d < 0.01, `${a.id}: a point on it measured ${d.toFixed(3)} m away`); } } }); test('hit-testing resolves each area from points on it', () => { for (const a of areas) { for (const p of onGeometry(a.geometry)) { const hit = nearest(p); if (hit.area.id === a.id) continue; // Categories meet at intersections, so an exact tie is acceptable; silently // resolving to something FURTHER away is the bug this guards against. assert.ok( hit.dist < 0.01, `${a.id}: a point on it resolved to ${hit.area.id} at ${hit.dist.toFixed(2)} m`, ); } } }); test('a point off the map does not snap to an area', () => { // East of the highway, across Sand Creek — no mapped parking anywhere near. assert.ok(nearest([-116.5445, 48.2705]).dist > 40); }); test('the identity overlay is a no-op', () => { const anchor = overlayAnchor(geoms); for (const g of geoms) assert.deepEqual(adjustGeometry(g, IDENTITY_OVERLAY, anchor), g); }); test('a shift moves every vertex by exactly that distance', () => { const anchor = overlayAnchor(geoms); for (const g of geoms) { const moved = adjustGeometry(g, { ...IDENTITY_OVERLAY, dxMeters: 10 }, anchor); const a = g.type === 'Polygon' ? g.coordinates[0] : g.coordinates; const b = moved.type === 'Polygon' ? moved.coordinates[0] : moved.coordinates; for (let i = 0; i < a.length; i++) { assert.ok(Math.abs(distanceMeters(a[i], b[i]) - 10) < 0.05, 'shift distance'); assert.ok(b[i][0] > a[i][0], 'positive dxMeters must move east'); } } }); test('rotation is rigid about the anchor and 360° returns home', () => { const anchor = overlayAnchor(geoms); const g = geoms.find((x) => x.type === 'LineString') as Extract< AreaGeometry, { type: 'LineString' } >; const spun = adjustGeometry(g, { ...IDENTITY_OVERLAY, rotationDeg: 360 }, anchor) as typeof g; for (let i = 0; i < g.coordinates.length; i++) { assert.ok(distanceMeters(g.coordinates[i], spun.coordinates[i]) < 0.01, '360° round trip'); } const rot = adjustGeometry(g, { ...IDENTITY_OVERLAY, rotationDeg: 5 }, anchor) as typeof g; for (let i = 0; i < g.coordinates.length; i++) { const r0 = distanceMeters(anchor, g.coordinates[i]); const r1 = distanceMeters(anchor, rot.coordinates[i]); assert.ok(Math.abs(r0 - r1) < 0.5, `rotation changed radius ${r0.toFixed(1)} -> ${r1.toFixed(1)}`); } }); test('scale is anchored and proportional', () => { const anchor = overlayAnchor(geoms); const far = (geoms.find((x) => x.type === 'LineString') as any).coordinates[0] as LonLat; const scaled = adjustGeometry( { type: 'LineString', coordinates: [anchor, far] }, { ...IDENTITY_OVERLAY, scale: 2 }, anchor, ) as Extract; assert.ok(distanceMeters(scaled.coordinates[0], anchor) < 0.01, 'the anchor must not move'); const before = distanceMeters(anchor, far); const after = distanceMeters(anchor, scaled.coordinates[1]); assert.ok(Math.abs(after - 2 * before) < 0.5, `×2: ${before.toFixed(1)} -> ${after.toFixed(1)}`); });