import { Coords, Boundaries, PolygonDocument, PinOptions, } from "../types/index.js"; import { isExtent } from "../guardians.js"; import { DecimalPrecision } from "./numbers.js"; export const EXTENT: Boundaries = { top: 89.9, right: 180, bottom: -89.9, left: -180, }; export function getExtent(coords?: Coords[]): Boundaries | undefined { if (!coords?.length) return; let extent: Boundaries | undefined; for (const i in coords) { const duplet = coords[i]; const [lng, lat] = duplet; if (extent) { extent = { top: Math.max(extent.top, lat), right: Math.max(extent.right, lng), bottom: Math.min(extent.bottom, lat), left: Math.min(extent.left, lng), }; } else { extent = { top: lat, right: lng, bottom: lat, left: lng, }; } } return extent; } export function getBoundaryExtent(extent: Boundaries, boundaries: Boundaries) { return { top: Math.min(boundaries.top, extent.top), right: Math.min(boundaries.right, extent.right), bottom: Math.max(boundaries.bottom, extent.bottom), left: Math.max(boundaries.left, extent.left), }; } export function getCoords({ top, right, bottom, left, }: Boundaries): [Coords, Coords] { return [ [right, top], [left, bottom], ]; } export function getPolygon({ top, right, bottom, left }: Boundaries): Coords[] { return [ [left, top], [right, top], [right, bottom], [left, bottom], [left, top], ]; } export function compareExtents( first: Boundaries, ...others: (Boundaries | unknown)[] ): Boundaries { const extent = others.reduce( (value, extent) => { if (isExtent(extent)) { value.top = Math.max(value.top, extent.top); value.right = Math.max(value.right, extent.right); value.bottom = Math.min(value.bottom, extent.bottom); value.left = Math.min(value.left, extent.left); } return value; }, { ...first } ); return extent; } export function getExtentCoords( extent: Boundaries ): [Coords, Coords, Coords, Coords] { return [ [extent.left, extent.top], [extent.right, extent.top], [extent.right, extent.bottom], [extent.left, extent.bottom], ]; } export function extentArea(extent: Boundaries) { const rightBottom: Coords = [extent.right, extent.bottom]; const leftTop: Coords = [extent.left, extent.top]; return coordsArea(rightBottom, leftTop); } export function getReverseExtent(extent: Boundaries): Boundaries { return { top: extent.bottom, right: extent.left, bottom: extent.top, left: extent.right, }; } export function center(extent: Boundaries): Coords { return [ (extent.right - extent.left) / 2 + extent.left, (extent.top - extent.bottom) / 2 + extent.bottom, ]; } export function distance([x1, y1]: Coords, [x2, y2]: Coords): number { return Math.sqrt( Math.pow(Math.abs(x1 - x2), 2) + Math.pow(Math.abs(y1 - y2), 2) ); } export function angle([x1, y1]: Coords, [x2, y2]: Coords): number { return (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI; } export function coordsArea([x1, y1]: Coords, [x2, y2]: Coords) { const width = Math.abs(x2 - x1); const height = Math.abs(y2 - y1); return width * height; } export function includesCoords(container: Boundaries, shape: Boundaries) { return !( shape.top <= container.bottom || shape.right <= container.left || shape.bottom >= container.top || shape.left >= container.right ); } export function includesPoint(container: Boundaries, point: Coords) { return !( point[0] <= container.left || point[0] >= container.right || point[1] <= container.bottom || point[1] >= container.top ); } export function getPinExtent(pin: PinOptions): Boundaries { const { coords, radius = 0 } = pin; return { top: coords[1] + radius, right: coords[0] + radius, bottom: coords[1] - radius, left: coords[0] - radius, }; } export function calcAreaCentroid(coords: Coords[]) { const [x0, y0] = coords[0]; let x = 0, y = 0, twiceArea = 0; let prev = coords[coords.length - 1]; let maxDecimals = 0; for (const next of coords) { const [xp, yp] = prev; const [xn, yn] = next; const x1 = xp - x0; const y1 = yp - y0; const x2 = xn - x0; const y2 = yn - y0; const a = x1 * y2 - x2 * y1; twiceArea += a; x += (x1 + x2) * a; y += (y1 + y2) * a; maxDecimals = Math.max( maxDecimals, DecimalPrecision.countDecimals(next[0]), DecimalPrecision.countDecimals(next[1]) ); prev = next; } const factor = twiceArea ? 3 * twiceArea : 1; x /= factor; y /= factor; x += x0; y += y0; const centroid: Coords = [x, y]; const area: number = Math.abs(twiceArea / 2); return { centroid, area, maxDecimals, }; } export function polygonProps(coords: Coords[]): PolygonDocument { const { area, centroid, maxDecimals } = calcAreaCentroid(coords); return { area, centroid, maxDecimals, coordsCount: coords.length, extent: getExtent(coords), }; } /** * Performs the even-odd-rule Algorithm (a raycasting algorithm) to find out whether a point is in a given polygon. * This runs in O(n) where n is the number of edges of the polygon. * * @param {Coords} polygon an array representation of the polygon where polygon[i][0] is the x Value of the i-th point and polygon[i][1] is the y Value. * @param {Coords} point an array representation of the point where point[0] is its x Value and point[1] is its y Value * @return {boolean} whether the point is in the polygon (not on the edge, just turn < into <= and > into >= for that) */ export function pointInPolygon(polygon: Coords[], point: Coords) { //A point is in a polygon if a line from the point to infinity crosses the polygon an odd number of times let odd = false; //For each edge (In this case for each point of the polygon and the previous one) for (let i = 0, j = polygon.length - 1; i < polygon.length; i++) { //If a line from the point into infinity crosses this edge if ( polygon[i][1] > point[1] !== polygon[j][1] > point[1] && // One point needs to be above, one below our y coordinate // ...and the edge doesn't cross our Y corrdinate before our x coordinate (but between our x coordinate and infinity) point[0] < ((polygon[j][0] - polygon[i][0]) * (point[1] - polygon[i][1])) / (polygon[j][1] - polygon[i][1]) + polygon[i][0] ) { // Invert odd odd = !odd; } j = i; } //If the number of crossings was odd, the point is in the polygon return odd; }