when-am-i / files / src / utils / geo.ts
geo.ts
a71096e6 2026-06-18 Pavel Safronov | 1 | // Point-in-region geometry. GeoJSON coordinates are [lng, lat]. |
| 2 | ||
| 3 | import type { CalendarRegion, Geometry, LinearRing } from '../types'; | |
| 4 | ||
| 5 | // Standard ray-casting test against a single ring (x = lng, y = lat). | |
| 6 | function pointInRing(lng: number, lat: number, ring: LinearRing): boolean { | |
| 7 | let inside = false; | |
| 8 | for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) { | |
| 9 | const [xi, yi] = ring[i]; | |
| 10 | const [xj, yj] = ring[j]; | |
| 11 | const intersects = | |
| 12 | yi > lat !== yj > lat && | |
| 13 | lng < ((xj - xi) * (lat - yi)) / (yj - yi) + xi; | |
| 14 | if (intersects) inside = !inside; | |
| 15 | } | |
| 16 | return inside; | |
| 17 | } | |
| 18 | ||
| 19 | // A polygon is its outer ring (index 0) minus any holes (further rings). | |
| 20 | function pointInPolygonRings(lng: number, lat: number, rings: LinearRing[]): boolean { | |
| 21 | if (rings.length === 0) return false; | |
| 22 | const [outer, ...holes] = rings; | |
| 23 | if (!pointInRing(lng, lat, outer)) return false; | |
| 24 | return !holes.some((hole) => pointInRing(lng, lat, hole)); | |
| 25 | } | |
| 26 | ||
| 27 | /** Whether the point (lat, lng) lies inside a Polygon or MultiPolygon. */ | |
| 28 | export function pointInPolygon(lat: number, lng: number, geometry: Geometry): boolean { | |
| 29 | if (geometry.type === 'Polygon') { | |
| 30 | return pointInPolygonRings(lng, lat, geometry.coordinates); | |
| 31 | } | |
| 32 | return geometry.coordinates.some((polygon) => pointInPolygonRings(lng, lat, polygon)); | |
| 33 | } | |
| 34 | ||
| 35 | /** All regions whose geometry contains the point — regions may overlap. */ | |
| 36 | export function findRegions( | |
| 37 | lat: number, | |
| 38 | lng: number, | |
| 39 | regions: CalendarRegion[], | |
| 40 | ): CalendarRegion[] { | |
| 41 | return regions.filter((r) => pointInPolygon(lat, lng, r.region.geo)); | |
| 42 | } |