fuzzyworld

when-am-i / files / src / utils / geo.test.ts

geo.test.ts

3.2 KB · history · blame

1import { describe, it, expect } from 'vitest';
2import { pointInPolygon, findRegions } from './geo';
3import type { CalendarRegion, Geometry } from '../types';
4
5// A 10×10 square from (0,0) to (10,10) in [lng, lat].
6const square: Geometry = {
7 type: 'Polygon',
8 coordinates: [
9 [
10 [0, 0],
11 [10, 0],
12 [10, 10],
13 [0, 10],
14 [0, 0],
15 ],
16 ],
17};
18
19// Same square with a 2..8 hole punched in the middle.
20const squareWithHole: Geometry = {
21 type: 'Polygon',
22 coordinates: [
23 [
24 [0, 0],
25 [10, 0],
26 [10, 10],
27 [0, 10],
28 [0, 0],
29 ],
30 [
31 [2, 2],
32 [8, 2],
33 [8, 8],
34 [2, 8],
35 [2, 2],
36 ],
37 ],
38};
39
40describe('pointInPolygon', () => {
41 it('detects a point inside the polygon', () => {
42 // pointInPolygon takes (lat, lng); the square spans lat/lng 0..10.
43 expect(pointInPolygon(5, 5, square)).toBe(true);
44 });
45
46 it('detects a point outside the polygon', () => {
47 expect(pointInPolygon(15, 5, square)).toBe(false);
48 expect(pointInPolygon(5, -1, square)).toBe(false);
49 });
50
51 it('treats a point inside a hole as outside', () => {
52 expect(pointInPolygon(5, 5, squareWithHole)).toBe(false);
53 expect(pointInPolygon(1, 1, squareWithHole)).toBe(true); // in the ring, outside the hole
54 });
55
56 it('handles MultiPolygon (inside any part)', () => {
57 const multi: Geometry = {
58 type: 'MultiPolygon',
59 coordinates: [
60 square.type === 'Polygon' ? square.coordinates : [],
61 [
62 [
63 [20, 20],
64 [30, 20],
65 [30, 30],
66 [20, 30],
67 [20, 20],
68 ],
69 ],
70 ],
71 };
72 expect(pointInPolygon(5, 5, multi)).toBe(true);
73 expect(pointInPolygon(25, 25, multi)).toBe(true);
74 expect(pointInPolygon(15, 15, multi)).toBe(false);
75 });
76});
77
78describe('findRegions', () => {
79 const overlapping: CalendarRegion[] = [
80 { region: { name: 'A', geo: square }, eras: [] },
81 {
82 region: {
83 name: 'B',
84 geo: {
85 type: 'Polygon',
86 coordinates: [
87 [
88 [5, 5],
89 [15, 5],
90 [15, 15],
91 [5, 15],
92 [5, 5],
93 ],
94 ],
95 },
96 },
97 eras: [],
98 },
99 ];
100
101 it('returns every region containing the point (overlap)', () => {
102 const hits = findRegions(7, 7, overlapping).map((r) => r.region.name);
103 expect(hits).toEqual(['A', 'B']);
104 });
105
106 it('returns a single region when only one contains the point', () => {
107 expect(findRegions(2, 2, overlapping).map((r) => r.region.name)).toEqual(['A']);
108 });
109
110 it('returns nothing when no region contains the point', () => {
111 expect(findRegions(100, 100, overlapping)).toEqual([]);
112 });
113});