UNPKG

s2-tools

Version:

A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.

193 lines 6.13 kB
import { PriorityQueue } from '../dataStructures/priorityQueue'; /** * # Polylabels * * ## Description * Find the labels for a collection of vector polygons * * ## Usage * ```ts * import { polylabels } from 's2-tools' * import type { VectorMultiPolygon } from 's2-tools' * * const vectorGeometry: VectorMultiPolygon = []; * const polylabelHighPrecision = polylabels(vectorGeometry, 1); * ``` * @param polygons - A collection of vector polygons to find the labels for * @param precision - the precision of the label * @returns - the labels */ export function polylabels(polygons, precision = 1.0) { return polygons.map((polygon) => polylabel(polygon, precision)); } /** * # Polylabel * * ## Description * Find the label for a vector polygon * * ## Usage * ```ts * import { polylabel } from 's2-tools' * import type { VectorPolygon } from 's2-tools' * * const vectorGeometry: VectorPolygon = []; * const polylabelHighPrecision = polylabel(vectorGeometry, 1); * ``` * @param polygon - the vector polygon to find the label for * @param precision - the precision of the label * @returns - the label */ export function polylabel(polygon, precision = 1.0) { // find the bounding box of the outer ring let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity; for (const { x, y } of polygon[0]) { if (x < minX) minX = x; if (y < minY) minY = y; if (x > maxX) maxX = x; if (y > maxY) maxY = y; } const width = maxX - minX; const height = maxY - minY; const cellSize = Math.max(precision, Math.min(width, height)); if (cellSize === precision) return { x: minX, y: minY, m: { distance: 0 } }; // a priority queue of cells in order of their "potential" (max distance to polygon) const cellQueue = new PriorityQueue([], (a, b) => b.max - a.max); // take centroid as the first best guess let bestCell = getCentroidCell(polygon); // second guess: bounding box centroid const bboxCell = buildCell(minX + width / 2, minY + height / 2, 0, polygon); if (bboxCell.d > bestCell.d) bestCell = bboxCell; /** * add a cell to the queue * @param x - the cell x coordinate * @param y - the cell y coordinate * @param h - the cell height */ const potentiallyQueue = (x, y, h) => { const cell = buildCell(x, y, h, polygon); if (cell.max > bestCell.d + precision) cellQueue.push(cell); // update the best cell if we found a better one if (cell.d > bestCell.d) bestCell = cell; }; // cover polygon with initial cells let h = cellSize / 2; for (let x = minX; x < maxX; x += cellSize) { for (let y = minY; y < maxY; y += cellSize) { potentiallyQueue(x + h, y + h, h); } } while (true) { // pick the most promising cell from the queue const cell = cellQueue.pop(); if (cell === undefined) break; const { max, x, y, h: ch } = cell; // do not drill down further if there's no chance of a better solution if (max - bestCell.d <= precision) break; // split the cell into four cells h = ch / 2; potentiallyQueue(x - h, y - h, h); potentiallyQueue(x + h, y - h, h); potentiallyQueue(x - h, y + h, h); potentiallyQueue(x + h, y + h, h); } const result = { x: bestCell.x, y: bestCell.y, m: { distance: bestCell.d } }; return result; } /** * build a cell * @param x - the cell x coordinate * @param y - the cell y coordinate * @param h - half the cell size * @param polygon - the vector polygon * @returns - the cell */ function buildCell(x, y, h, polygon) { const d = pointToPolygonDist(x, y, polygon); return { x, y, h, d, max: d + h * Math.SQRT2 }; } /** * signed distance from point to polygon outline (negative if point is outside) * @param x - the point x coordinate * @param y - the point y coordinate * @param polygon - the vector polygon to check * @returns - the signed distance */ function pointToPolygonDist(x, y, polygon) { let inside = false; let minDistSq = Infinity; for (const ring of polygon) { for (let i = 0, len = ring.length, j = len - 1; i < len; j = i++) { const a = ring[i]; const b = ring[j]; if (a.y > y !== b.y > y && x < ((b.x - a.x) * (y - a.y)) / (b.y - a.y) + a.x) inside = !inside; minDistSq = Math.min(minDistSq, getSegDistSq(x, y, a, b)); } } return minDistSq === 0 ? 0 : (inside ? 1 : -1) * Math.sqrt(minDistSq); } /** * get polygon centroid * @param polygon - the vector polygon * @returns - the centroid as a cell */ function getCentroidCell(polygon) { let area = 0; let x = 0; let y = 0; const points = polygon[0]; for (let i = 0, len = points.length, j = len - 1; i < len; j = i++) { const a = points[i]; const b = points[j]; const f = a.x * b.y - b.x * a.y; x += (a.x + b.x) * f; y += (a.y + b.y) * f; area += f * 3; } const centroid = buildCell(x / area, y / area, 0, polygon); if (area === 0 || centroid.d < 0) return buildCell(points[0].x, points[0].y, 0, polygon); return centroid; } /** * get squared distance from a point to a segment AB * @param px - the segment start point * @param py - the segment end point * @param a - the reference point A * @param b - the reference point B * @returns - the squared distance */ function getSegDistSq(px, py, a, b) { let { x, y } = a; let dx = b.x - x; let dy = b.y - y; if (dx !== 0 || dy !== 0) { const t = ((px - x) * dx + (py - y) * dy) / (dx * dx + dy * dy); if (t > 1) { x = b.x; y = b.y; } else if (t > 0) { x += dx * t; y += dy * t; } } dx = px - x; dy = py - y; return dx * dx + dy * dy; } //# sourceMappingURL=polylabel.js.map