UNPKG

s2-tools

Version:

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

390 lines 14.3 kB
import { Tile } from '../../dataStructures'; import { childrenIJ } from '../id'; import { clipBBox, extendBBox } from '../bbox'; /** * @param tile - the tile to split * @param buffer - the buffer around the tile for lines and polygons * @returns - the tile's children split into 4 sub-tiles */ export function splitTile(tile, buffer = 0.0625) { const { face, zoom, i, j } = tile; const [blID, brID, tlID, trID] = childrenIJ(face, zoom, i, j); const children = [ { id: blID, tile: new Tile(blID) }, { id: brID, tile: new Tile(brID) }, { id: tlID, tile: new Tile(tlID) }, { id: trID, tile: new Tile(trID) }, ]; const scale = 1 << zoom; const k1 = 0; const k2 = 0.5; const k3 = 0.5; const k4 = 1; let tl = null; let bl = null; let tr = null; let br = null; for (const [name, { features }] of Object.entries(tile.layers)) { const left = _clip(features, scale, i - k1, i + k3, 0, buffer); const right = _clip(features, scale, i + k2, i + k4, 0, buffer); if (left !== null) { bl = _clip(left, scale, j - k1, j + k3, 1, buffer); tl = _clip(left, scale, j + k2, j + k4, 1, buffer); if (bl !== null) for (const d of bl) children[0].tile.addFeature(d, name); if (tl !== null) for (const d of tl) children[2].tile.addFeature(d, name); } if (right !== null) { br = _clip(right, scale, j - k1, j + k3, 1, buffer); tr = _clip(right, scale, j + k2, j + k4, 1, buffer); if (br !== null) for (const d of br) children[1].tile.addFeature(d, name); if (tr !== null) for (const d of tr) children[3].tile.addFeature(d, name); } } return children; } /** * @param features - input features to clip * @param scale - the tile scale * @param k1 - minimum accepted value of the axis * @param k2 - maximum accepted value of the axis * @param axis - the axis 0 for x, 1 for y * @param baseBuffer - the top level buffer value * @returns - the clipped features */ function _clip(features, scale, k1, k2, axis, baseBuffer) { // scale k1 /= scale; k2 /= scale; // prep buffer and result container const buffer = baseBuffer / scale; const k1b = k1 - buffer; const k2b = k2 + buffer; const clipped = []; for (const feature of features) { const { geometry } = feature; const { type } = geometry; // build the new clipped geometry let newGeometry = undefined; if (type === 'Point') newGeometry = clipPoint(geometry, axis, k1, k2); else if (type === 'MultiPoint') newGeometry = clipMultiPoint(geometry, axis, k1, k2); else if (type === 'LineString') newGeometry = clipLineString(geometry, axis, k1b, k2b); else if (type === 'MultiLineString') newGeometry = clipMultiLineString(geometry, axis, k1b, k2b); else if (type === 'Polygon') newGeometry = clipPolygon(geometry, axis, k1b, k2b); else if (type === 'MultiPolygon') newGeometry = clipMultiPolygon(geometry, axis, k1b, k2b); // store if the geometry was inside the range if (newGeometry !== undefined) { newGeometry.vecBBox = clipBBox(newGeometry.vecBBox, axis, k1b, k2b); clipped.push({ ...feature, geometry: newGeometry }); } } return clipped.length > 0 ? clipped : null; } /** * @param geometry - input vector geometry * @param axis - 0 for x, 1 for y * @param k1 - minimum accepted value of the axis * @param k2 - maximum accepted value of the axis * @returns - the clipped geometry or undefined if the geometry was not inside the range */ export function clipPoint(geometry, axis, k1, k2) { const { type, is3D, coordinates, bbox, vecBBox } = geometry; const value = axis === 0 ? coordinates.x : coordinates.y; if (value >= k1 && value < k2) return { type, is3D, coordinates: { ...coordinates }, bbox, vecBBox }; } /** * @param geometry - input vector geometry * @param axis - 0 for x, 1 for y * @param k1 - minimum accepted value of the axis * @param k2 - maximum accepted value of the axis * @returns - the clipped geometry or undefined if the geometry was not inside the range */ function clipMultiPoint(geometry, axis, k1, k2) { const { type, is3D, coordinates, bbox } = geometry; let vecBBox = undefined; const points = coordinates .filter((point) => { const value = axis === 0 ? point.x : point.y; return value >= k1 && value < k2; }) .map((p) => ({ ...p })); points.forEach((p) => (vecBBox = extendBBox(vecBBox, p))); if (points.length > 0) return { type, is3D, coordinates: points, bbox, vecBBox }; } /** * @param geometry - input vector geometry * @param axis - 0 for x, 1 for y * @param k1 - minimum accepted value of the axis * @param k2 - maximum accepted value of the axis * @returns - the clipped geometry or undefined if the geometry was not inside the range */ function clipLineString(geometry, axis, k1, k2) { const { is3D, coordinates: line, bbox, vecBBox } = geometry; const initO = geometry.offset ?? 0; const newOffsets = []; const newLines = []; for (const clip of _clipLine({ line, offset: initO }, k1, k2, axis, false)) { newOffsets.push(clip.offset); newLines.push(clip.line); } if (newLines.length === 0) return undefined; return { type: 'MultiLineString', is3D, coordinates: newLines, bbox, offset: newOffsets, vecBBox, }; } /** * @param geometry - input vector geometry * @param axis - 0 for x, 1 for y * @param k1 - minimum accepted value of the axis * @param k2 - maximum accepted value of the axis * @param isPolygon - true if the geometry is a polygon * @returns - the clipped geometry or undefined if the geometry was not inside the range */ function clipMultiLineString(geometry, axis, k1, k2, isPolygon = false) { const { is3D, coordinates, bbox, vecBBox } = geometry; const initO = geometry.offset ?? coordinates.map((_) => 0); const newOffsets = []; const newLines = []; coordinates.forEach((line, i) => { for (const clip of _clipLine({ line, offset: initO[i] }, k1, k2, axis, isPolygon)) { newOffsets.push(clip.offset); newLines.push(clip.line); } }); if (newLines.length === 0 || (isPolygon && newLines[0].length === 0)) return undefined; return { type: isPolygon ? 'Polygon' : 'MultiLineString', is3D, coordinates: newLines, bbox, offset: newOffsets, vecBBox, }; } /** * @param geometry - input vector geometry * @param axis - 0 for x, 1 for y * @param k1 - minimum accepted value of the axis * @param k2 - maximum accepted value of the axis * @returns - the clipped geometry or undefined if the geometry was not inside the range */ function clipPolygon(geometry, axis, k1, k2) { return clipMultiLineString(geometry, axis, k1, k2, true); } /** * @param geometry - input vector geometry * @param axis - 0 for x, 1 for y * @param k1 - minimum accepted value of the axis * @param k2 - maximum accepted value of the axis * @returns - the clipped geometry or undefined if the geometry was not inside the range */ function clipMultiPolygon(geometry, axis, k1, k2) { const { is3D, coordinates, bbox, vecBBox } = geometry; const initO = geometry.offset ?? coordinates.map((l) => l.map(() => 0)); const newCoordinates = []; const newOffsets = []; coordinates.forEach((polygon, p) => { const newPolygon = clipPolygon({ type: 'Polygon', is3D, coordinates: polygon, bbox, offset: initO[p] }, axis, k1, k2); if (newPolygon !== undefined) { newCoordinates.push(newPolygon.coordinates); if (newPolygon.offset !== undefined) newOffsets.push(newPolygon.offset); } }); if (newCoordinates.length === 0) return undefined; return { type: 'MultiPolygon', is3D, coordinates: newCoordinates, bbox, vecBBox, offset: newOffsets, }; } /** * Data should always be in a 0->1 coordinate system to use this clip function * @param geom - the original geometry line * @param bbox - the bounding box to clip the line to * @param isPolygon - true if the line comes from a polygon * @param offset - the starting offset the line starts at * @param buffer - the buffer to apply to the line (spacing outside the bounding box) * @returns - the clipped geometry */ export function clipLine(geom, bbox, isPolygon, offset = 0, buffer = 0.0625) { const res = []; const [left, bottom, right, top] = bbox; // clip horizontally const horizontalClips = _clipLine({ line: geom, offset, vecBBox: [0, 0, 0, 0] }, left - buffer, right + buffer, 0, isPolygon); for (const clip of horizontalClips) { // clip vertically res.push(..._clipLine(clip, bottom - buffer, top + buffer, 1, isPolygon)); } return res.map((clip) => { let vecBBox; for (const p of clip.line) vecBBox = extendBBox(vecBBox, p); clip.vecBBox = vecBBox; return clip; }); } /** * @param input - the original geometry line * @param k1 - the lower bound * @param k2 - the upper bound * @param axis - 0 for x, 1 for y * @param isPolygon - true if the line comes from a polygon * @returns - the clipped geometry */ function _clipLine(input, k1, k2, axis, isPolygon) { const { line: geom, offset: startOffset } = input; const newGeom = []; let slice = []; let last = geom.length - 1; const intersect = axis === 0 ? intersectX : intersectY; let curOffset = startOffset; let accOffset = startOffset; let prevP = geom[0]; let firstEnter = false; for (let i = 0; i < last; i++) { const { x: ax, y: ay, z: az, m: am } = geom[i]; const { x: bx, y: by, z: bz, m: bm } = geom[i + 1]; const a = axis === 0 ? ax : ay; const b = axis === 0 ? bx : by; const azNU = az !== undefined; const bzNU = bz !== undefined; const z = azNU && bzNU ? (az + bz) / 2 : azNU ? az : bzNU ? bz : undefined; let entered = false; let exited = false; let intP; // ENTER OR CONTINUE CASES if (a < k1) { // ---|--> | (line enters the clip region from the left) if (b > k1) { intP = intersect(ax, ay, bx, by, k1, z, bm); slice.push(intP); entered = true; } } else if (a > k2) { // | <--|--- (line enters the clip region from the right) if (b < k2) { intP = intersect(ax, ay, bx, by, k2, z, bm); slice.push(intP); entered = true; } } else { intP = { x: ax, y: ay, z: az, m: am }; slice.push(intP); } // Update the intersection point and offset if the intP exists if (intP !== undefined) { // our first enter will change the offset for the line if (entered && !firstEnter) { curOffset = accOffset + distance(prevP, intP); firstEnter = true; } } // EXIT CASES if (b < k1 && a >= k1) { // <--|--- | or <--|-----|--- (line exits the clip region on the left) intP = intersect(ax, ay, bx, by, k1, z, bm ?? am); slice.push(intP); exited = true; } if (b > k2 && a <= k2) { // | ---|--> or ---|-----|--> (line exits the clip region on the right) intP = intersect(ax, ay, bx, by, k2, z, bm ?? am); slice.push(intP); exited = true; } // update the offset accOffset += distance(prevP, geom[i + 1]); prevP = geom[i + 1]; // If not a polygon, we can cut it into parts, otherwise we just keep tracking the edges if (!isPolygon && exited) { newGeom.push({ line: slice, offset: curOffset }); slice = []; firstEnter = false; } } // add the last point if inside the clip const lastPoint = geom[last]; const a = axis === 0 ? lastPoint.x : lastPoint.y; if (a >= k1 && a <= k2) slice.push({ ...lastPoint }); // close the polygon if its endpoints are not the same after clipping if (slice.length > 0 && isPolygon) { last = slice.length - 1; const firstP = slice[0]; if (last >= 1 && (slice[last].x !== firstP.x || slice[last].y !== firstP.y)) { slice.push({ ...firstP }); } } // add the final slice if (slice.length > 0) newGeom.push({ line: slice, offset: curOffset }); return newGeom; } /** * @param ax - the first x * @param ay - the first y * @param bx - the second x * @param by - the second y * @param x - the x to intersect * @param z - the elevation if it exists * @param m - the MValue * @returns - the intersecting point */ function intersectX(ax, ay, bx, by, x, z, m) { const t = (x - ax) / (bx - ax); return { x, y: ay + (by - ay) * t, z, m, t: 1 }; } /** * @param ax - the first x * @param ay - the first y * @param bx - the second x * @param by - the second y * @param y - the y to intersect * @param z - the elevation if it exists * @param m - the MValue * @returns - the intersecting point */ function intersectY(ax, ay, bx, by, y, z, m) { const t = (y - ay) / (by - ay); return { x: ax + (bx - ax) * t, y, z, m, t: 1 }; } /** * Calculate the Euclidean distance between two points. * @param p1 - The first point. * @param p2 - The second point. * @returns - The distance between the points. */ function distance(p1, p2) { const { sqrt, pow } = Math; return sqrt(pow(p2.x - p1.x, 2) + pow(p2.y - p1.y, 2)); } //# sourceMappingURL=clip.js.map