perfect-postcode/frontend/src/lib/h3-selection.ts
2026-05-06 23:13:58 +01:00

125 lines
3.8 KiB
TypeScript

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;
}