import { cellToBoundary, cellToLatLng, getResolution } from 'h3-js'; import type { HexagonData } from '../types'; type Point = [number, number]; const EPSILON = 1e-12; function samePoint(a: Point, b: Point): boolean { return Math.abs(a[0] - b[0]) <= EPSILON && Math.abs(a[1] - b[1]) <= EPSILON; } function cellBoundary(h3: string): Point[] { const boundary = cellToBoundary(h3, true) as Point[]; if (boundary.length > 1 && samePoint(boundary[0], boundary[boundary.length - 1])) { return boundary.slice(0, -1); } return boundary; } function orientation(a: Point, b: Point, c: Point): number { return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]); } function pointOnSegment(point: Point, start: Point, end: Point): boolean { if (Math.abs(orientation(start, end, point)) > EPSILON) return false; return ( point[0] >= Math.min(start[0], end[0]) - EPSILON && point[0] <= Math.max(start[0], end[0]) + EPSILON && point[1] >= Math.min(start[1], end[1]) - EPSILON && point[1] <= Math.max(start[1], end[1]) + EPSILON ); } function segmentsIntersect(a: Point, b: Point, c: Point, d: Point): boolean { const abC = orientation(a, b, c); const abD = orientation(a, b, d); const cdA = orientation(c, d, a); const cdB = orientation(c, d, b); if ( ((abC > EPSILON && abD < -EPSILON) || (abC < -EPSILON && abD > EPSILON)) && ((cdA > EPSILON && cdB < -EPSILON) || (cdA < -EPSILON && cdB > EPSILON)) ) { return true; } return ( pointOnSegment(c, a, b) || pointOnSegment(d, a, b) || pointOnSegment(a, c, d) || pointOnSegment(b, c, d) ); } function pointInPolygon(point: Point, polygon: Point[]): boolean { let inside = false; for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { const current = polygon[i]; const previous = polygon[j]; if (pointOnSegment(point, previous, current)) return true; if (current[1] > point[1] !== previous[1] > point[1]) { const x = ((previous[0] - current[0]) * (point[1] - current[1])) / (previous[1] - current[1]) + current[0]; if (point[0] < x) inside = !inside; } } return inside; } export function polygonsOverlap(a: Point[], b: Point[]): boolean { if (a.length < 3 || b.length < 3) return false; if (a.some((point) => pointInPolygon(point, b))) return true; if (b.some((point) => pointInPolygon(point, a))) return true; for (let i = 0; i < a.length; i++) { const aNext = (i + 1) % a.length; for (let j = 0; j < b.length; j++) { const bNext = (j + 1) % b.length; if (segmentsIntersect(a[i], a[aNext], b[j], b[bNext])) return true; } } return false; } export function hasMatchingHexagonAtResolution( hexagons: HexagonData[], resolution: number ): boolean { return hexagons.some((hexagon) => hexagon.count > 0 && getResolution(hexagon.h3) === resolution); } export function findOverlappingMatchingHexagon( previousH3: string, hexagons: HexagonData[], resolution: number ): HexagonData | null { const previousBoundary = cellBoundary(previousH3); const [previousLat, previousLng] = cellToLatLng(previousH3); let best: HexagonData | null = null; let bestDistance = Infinity; for (const hexagon of hexagons) { if (hexagon.count <= 0 || getResolution(hexagon.h3) !== resolution) continue; if (!polygonsOverlap(previousBoundary, cellBoundary(hexagon.h3))) continue; const distance = (hexagon.lat - previousLat) ** 2 + (hexagon.lon - previousLng) ** 2; if ( !best || distance < bestDistance - EPSILON || (Math.abs(distance - bestDistance) <= EPSILON && (hexagon.count > best.count || (hexagon.count === best.count && hexagon.h3.localeCompare(best.h3) < 0))) ) { best = hexagon; bestDistance = distance; } } return best; }