UNPKG

@allmaps/stdlib

Version:

Allmaps Standard Library

252 lines (251 loc) 7.8 kB
import monotoneChainConvexHull from 'monotone-chain-convex-hull'; import { isGeojsonGeometry, geojsonGeometryToGeometry } from './geojson.js'; import { isPoint, isPolygon, isMultiPolygon, distance } from './geometry.js'; export const MIN_POINT_LNG_LAT_PROJECTION = [-180, -90]; export const MAX_POINT_LNG_LAT_PROJECTION = [180, 90]; export const MIN_POINT_WEBMERCATOR_PROJECTION = [ -20037508.34, -20048966.1 ]; export const MAX_POINT_WEBMERCATOR_PROJECTION = [ 20037508.34, 20048966.1 ]; // Compute export function computeMinMax(values) { let min = Number.POSITIVE_INFINITY; let max = Number.NEGATIVE_INFINITY; for (const value of values) { if (min === undefined) { if (value >= value) min = max = value; } else { if (min > value) min = value; if (max < value) max = value; } } return [min, max]; } export function bindValue(value, min, max) { return Math.max(Math.min(value, max), min); } export function bindPoint(point, min, max) { return [ bindValue(point[0], min[0], max[0]), bindValue(point[1], min[1], max[1]) ]; } export function bindPointLngLatProjection(point) { return bindPoint(point, MIN_POINT_LNG_LAT_PROJECTION, MAX_POINT_LNG_LAT_PROJECTION); } export function bindPointWebMercatorProjection(point) { return bindPoint(point, MIN_POINT_WEBMERCATOR_PROJECTION, MAX_POINT_WEBMERCATOR_PROJECTION); } // Note: bbox order is minX, minY, maxX, maxY export function computeBbox(points, options) { if (isPoint(points)) { points = [points]; } if (isPolygon(points)) { points = points.flat(); } if (isMultiPolygon(points)) { points = points.flat(); } if (isGeojsonGeometry(points)) { return computeBbox(geojsonGeometryToGeometry(points), options); } points = points; if (options?.clipLngLat) { points = points.map((point) => bindPointLngLatProjection(point)); } if (options?.clipWebMercator) { points = points.map((point) => bindPointWebMercatorProjection(point)); } // TODO: do this without making two new arrays const xs = []; const ys = []; for (const point of points) { xs.push(point[0]); ys.push(point[1]); } const [minX, maxX] = computeMinMax(xs); const [minY, maxY] = computeMinMax(ys); return [minX, minY, maxX, maxY]; } export function combineBboxes(...bboxes) { if (bboxes.length === 0) { return undefined; } return [ Math.min(...bboxes.map((bbox) => bbox[0])), Math.min(...bboxes.map((bbox) => bbox[1])), Math.max(...bboxes.map((bbox) => bbox[2])), Math.max(...bboxes.map((bbox) => bbox[3])) ]; } export function doBboxesIntersect(bbox0, bbox1) { const isOverlappingInX = bbox0[2] >= bbox1[0] && bbox1[2] >= bbox0[0]; const isOverlappingInY = bbox0[3] >= bbox1[1] && bbox1[3] >= bbox0[1]; return isOverlappingInX && isOverlappingInY; } export function intersectBboxes(bbox0, bbox1) { const minX = Math.max(bbox0[0], bbox1[0]); const maxX = Math.min(bbox0[2], bbox1[2]); const minY = Math.max(bbox0[1], bbox1[1]); const maxY = Math.min(bbox0[3], bbox1[3]); if (minX < maxX && minY < maxY) { return [minX, minY, maxX, maxY]; } else { return undefined; } } export function pointInBbox(point, bbox) { return doBboxesIntersect([point[0], point[1], point[0], point[1]], bbox); } export function bufferBbox(bbox, dist0, dist1) { if (dist1 === undefined) { dist1 = dist0; } return [bbox[0] - dist0, bbox[1] - dist1, bbox[2] + dist0, bbox[3] + dist1]; } // Ratio 2 adds half the current width (or height) both left and right of the current (width or height) // so the total width (or height) goes * 2 and the total surface goes * 4 export function bufferBboxByRatio(bbox, ratio) { if (!ratio || ratio === 0) { return bbox; } const size = bboxToSize(bbox); return bufferBbox(bbox, ...size.map((widthOrHeigth) => (widthOrHeigth * ratio) / 2)); } // Transform // Returns a rectangle with four points, starting from lower left and going anti-clockwise. export function bboxToRectangle(bbox) { return [ [bbox[0], bbox[1]], [bbox[2], bbox[1]], [bbox[2], bbox[3]], [bbox[0], bbox[3]] ]; } export function bboxToPolygon(bbox) { return [bboxToRectangle(bbox)]; } export function bboxToLine(bbox) { return [ [bbox[0], bbox[1]], [bbox[2], bbox[3]] ]; } export function bboxToDiameter(bbox) { return distance(bboxToLine(bbox)); } export function geometryToDiameter(geometry) { return distance(bboxToLine(computeBbox(geometry))); } export function bboxToCenter(bbox) { return [(bbox[0] + bbox[2]) / 2, (bbox[1] + bbox[3]) / 2]; } export function bboxToSize(bbox) { return [bbox[2] - bbox[0], bbox[3] - bbox[1]]; } export function bboxToResolution(bbox) { return sizeToResolution(bboxToSize(bbox)); } // Approximate results for quadrilaterals, exact for rectangles (e.g. coming from bboxes). // A more precise result would require a minimal-covering-rectangle algorithm // Or computing and comparing rectangle surfaces export function rectangleToSize(rectangle) { return [ 0.5 * (distance(rectangle[0], rectangle[1]) + distance(rectangle[2], rectangle[3])), 0.5 * (distance(rectangle[1], rectangle[2]) + distance(rectangle[3], rectangle[0])) ]; } // Convex hull export function convexHull(points) { if (points.length === 0) { return undefined; } return monotoneChainConvexHull(points); } // Sizes and Scales /** * Compute a size from two scales * * For unspecified 'fit', the scale is computed based on the surface area derived from the sizes. * * For specified 'fit': * * Example for square rectangles '*' and '+': * * 'contain' where '*' contains '.' * (in the first image size0 is relatively wider) * * **** * * * * **....** .... * * . . * . . * **....** .... * * * * **** * * * 'cover' where '*' is covered by '.' * (in the first image size0 is relatively wider) * * .... * . . * ..****.. **** * . * * . * * * ..****.. **** * . . * .... * * @export * @param size0 - first size * @param size1 - second size * @param fit - fit */ export function sizesToScale(size0, size1, fit) { if (!fit) { return Math.sqrt((size0[0] * size0[1]) / (size1[0] * size1[1])); } else if (fit === 'contain') { return size0[0] / size0[1] >= size1[0] / size1[1] // size0 is relatively wider ? size0[0] / size1[0] : size0[1] / size1[1]; } else { // fit = 'cover' return size0[0] / size0[1] >= size1[0] / size1[1] // size0 is relatively wider ? size0[1] / size1[1] : size0[0] / size1[0]; } } export function scaleSize(size, scale) { return [size[0] * scale, size[1] * scale]; } export function sizeToResolution(size) { return size[0] * size[1]; } export function sizeToCenter(size) { return [size[0] / 2, size[1] / 2]; } export function sizeToBbox(size) { return [0, 0, ...size]; } export function sizeToRectangle(size) { return bboxToRectangle(sizeToBbox(size)); } export function bboxesToScale(bbox0, bbox1) { return sizesToScale(bboxToSize(bbox0), bboxToSize(bbox1)); } export function rectanglesToScale(rectangle0, rectangle1) { return sizesToScale(rectangleToSize(rectangle0), rectangleToSize(rectangle1)); }