125 lines
3.8 KiB
TypeScript
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;
|
|
}
|