UNPKG

gis-tools-ts

Version:

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

261 lines 8.7 kB
import { clipLine } from '../../tools/clip.js'; import { pointFromLonLat as fromLonLat, pointToST as toST } from '../../s2/point.js'; import { fromPoint, mergeBBoxes } from '../../bbox.js'; import { geoToVector } from './vector.js'; /** * Convet a GeoJSON Feature to an S2Feature * @param data - GeoJSON Feature * @param buildBBox - optional - build a bbox for the feature if desired * @returns - S2Feature */ export function toS2(data, buildBBox) { const { id, properties, metadata } = data; const res = []; const vectorGeo = data.type === 'VectorFeature' ? data.geometry : geoToVector(data.geometry, buildBBox); for (const { geometry, face } of vectorGeoToS2(vectorGeo)) { res.push({ id, type: 'S2Feature', face, properties, metadata, geometry, }); } return res; } /** * Underlying conversion mechanic to move GeoJSON Geometry to S2Geometry * @param geometry - GeoJSON Geometry * @returns - S2Geometry */ export function vectorGeoToS2(geometry) { const { type } = geometry; let cGeo; if (type === 'Point') cGeo = convertGeometryPoint(geometry); else if (type === 'MultiPoint') cGeo = convertGeometryMultiPoint(geometry); else if (type === 'LineString') cGeo = convertGeometryLineString(geometry); else if (type === 'MultiLineString') cGeo = convertGeometryMultiLineString(geometry); else if (type === 'Polygon') cGeo = convertGeometryPolygon(geometry); else if (type === 'MultiPolygon') cGeo = convertGeometryMultiPolygon(geometry); else { throw new Error('Either the conversion is not yet supported or Invalid S2Geometry type.'); } return cGeo; } /** * @param geometry - GeoJSON PointGeometry * @returns - S2 PointGeometry */ function convertGeometryPoint(geometry) { const { type, is3D, coordinates, bbox } = geometry; const { z, m } = coordinates; const [face, s, t] = toST(fromLonLat(coordinates)); const vecBBox = fromPoint({ x: s, y: t, z }); return [{ face, geometry: { type, is3D, coordinates: { x: s, y: t, z, m }, bbox, vecBBox } }]; } /** * @param geometry - GeoJSON PointGeometry * @returns - S2 PointGeometry */ function convertGeometryMultiPoint(geometry) { const { is3D, coordinates, bbox } = geometry; return coordinates.flatMap((coordinates) => convertGeometryPoint({ type: 'Point', is3D, coordinates, bbox })); } /** * @param geometry - GeoJSON LineStringGeometry * @returns - S2 LineStringGeometry */ function convertGeometryLineString(geometry) { const { type, is3D, coordinates, bbox } = geometry; return convertLineString(coordinates, false).map(({ face, line, offset, vecBBox }) => { return { face, geometry: { type, is3D, coordinates: line, bbox, offset, vecBBox } }; }); } /** * @param geometry - GeoJSON MultiLineStringGeometry * @returns - S2 MultiLineStringGeometry */ function convertGeometryMultiLineString(geometry) { const { coordinates, is3D, bbox } = geometry; return coordinates .flatMap((line) => convertLineString(line, false)) .map(({ face, line, offset, vecBBox }) => ({ face, geometry: { type: 'LineString', is3D, coordinates: line, bbox, offset, vecBBox }, })); } /** * @param geometry - GeoJSON PolygonGeometry * @returns - S2 PolygonGeometry */ function convertGeometryPolygon(geometry) { const { type, is3D, coordinates, bbox } = geometry; const res = []; // conver all lines const outerRing = convertLineString(coordinates[0], true); const innerRings = coordinates.slice(1).flatMap((line) => convertLineString(line, true)); // for each face, build a new polygon for (const { face, line, offset, vecBBox: polyBBox } of outerRing) { const polygon = [line]; const polygonOffsets = [offset]; for (const { face: innerFace, line: innerLine, offset: innerOffset, vecBBox } of innerRings) { if (innerFace === face) { polygon.push(innerLine); polygonOffsets.push(innerOffset); mergeBBoxes(polyBBox, vecBBox); } } res.push({ face, geometry: { type, coordinates: polygon, is3D, bbox, offset: polygonOffsets, vecBBox: polyBBox, }, }); } return res; } /** * @param geometry - GeoJSON MultiPolygonGeometry * @returns - S2 MultiPolygonGeometry */ function convertGeometryMultiPolygon(geometry) { const { is3D, coordinates, bbox, offset } = geometry; return coordinates.flatMap((polygon, i) => convertGeometryPolygon({ type: 'Polygon', is3D, coordinates: polygon, bbox, offset: offset?.[i], })); } /** * @param line - GeoJSON LineString * @param isPolygon - true if the line originates from a polygon * @returns - S2 LineStrings clipped to it's 0->1 coordinate system */ function convertLineString(line, isPolygon) { const res = []; // find all the faces that exist in the line while reprojectiong const faces = new Set(); // first re-project all the coordinates to S2 const newGeometry = []; for (const { x, y, z, m } of line) { const [face, s, t] = toST(fromLonLat({ x, y })); const point = { face, s, t, z, m }; faces.add(face); newGeometry.push(point); } // for each face, build a line for (const face of faces) { const line = []; for (const stPoint of newGeometry) line.push(stPointToFace(face, stPoint)); const clippedLines = clipLine(line, [0, 0, 1, 1], isPolygon); for (const { line, offset, vecBBox } of clippedLines) res.push({ face, line, offset, vecBBox }); } return res; } /** * @param targetFace - face you want to project to * @param stPoint - the point you want to project * @returns - the projected point */ function stPointToFace(targetFace, stPoint) { const { face: curFace, s, t, z, m } = stPoint; if (targetFace === curFace) return { x: s, y: t, z, m }; const [rot, x, y] = FACE_RULE_SET[targetFace][curFace]; const [newS, newT] = rotate(rot, s, t); return { x: newS + x, y: newT + y, z, m }; } /** * @param rot - rotation * @param s - input s * @param t - input t * @returns - new [s, t] after rotating */ function rotate(rot, s, t) { if (rot === 90) return [t, 1 - s]; else if (rot === -90) return [1 - t, s]; else return [s, t]; // Handles the 0° case and any other unspecified rotations } /** * Ruleset for converting an S2Point from a face to another. * While this this set includes opposite side faces, without axis mirroring, * it is not technically accurate and shouldn't be used. Instead, data should let two points travel * further than a full face width. * FACE_RULE_SET[targetFace][currentFace] = [rot, x, y] */ const FACE_RULE_SET = [ // Target Face 0 [ [0, 0, 0], // Current Face 0 [0, 1, 0], // Current Face 1 [90, 0, 1], // Current Face 2 [-90, 2, 0], // Current Face 3 [-90, -1, 0], /// Current Face 4 [0, 0, -1], /// Current Face 5 ], // Target Face 1 [ [0, -1, 0], // Current Face 0 [0, 0, 0], // Current Face 1 [0, 0, 1], // Current Face 2 [-90, 1, 0], // Current Face 3 [-90, 2, 0], // Current Face 4 [90, 0, -1], // Current Face 5 ], // Target Face 2 [ [-90, -1, 0], // Current Face 0 [0, 0, -1], // Current Face 1 [0, 0, 0], // Current Face 2 [0, 1, 0], // Current Face 3 [90, 0, 1], // Current Face 4 [-90, 2, 0], // Current Face 5 ], // Target Face 3 [ [-90, 2, 0], // Current Face 0 [90, 0, -1], // Current Face 1 [0, -1, 0], // Current Face 2 [0, 0, 0], // Current Face 3 [0, 0, 1], // Current Face 4 [-90, 1, 0], // Current Face 5 ], // Target Face 4 [ [90, 0, 1], // Current Face 0 [-90, 2, 0], // Current Face 1 [-90, -1, 0], // Current Face 2 [0, 0, -1], // Current Face 3 [0, 0, 0], // Current Face 4 [0, 1, 0], // Current Face 5 ], // Target Face 5 [ [0, 0, 1], // Current Face 0 [-90, 1, 0], // Current Face 1 [-90, 2, 0], // Current Face 2 [90, 0, -1], // Current Face 3 [0, -1, 0], // Current Face 4 [0, 0, 0], // Current Face 5 ], ]; //# sourceMappingURL=s2.js.map