@allmaps/stdlib
Version:
Allmaps Standard Library
252 lines (251 loc) • 7.8 kB
JavaScript
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));
}