UNPKG

s2-tools

Version:

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

406 lines 14.9 kB
import { clipLine } from '../tools/clip'; import { buildSqDists, radToDeg } from '../'; import { extendBBox, fromPoint, mergeBBoxes } from '../bbox'; import { fromLonLat, toST } from '../s2/point'; /** * Convet a GeoJSON Feature to an S2Feature * @param data - GeoJSON Feature * @param tolerance - optional tolerance * @param maxzoom - optional maxzoom * @param buildBBox - optional - build a bbox for the feature if desired * @returns - S2Feature */ export function toS2(data, tolerance, maxzoom, buildBBox) { const { id, properties, metadata } = data; const res = []; const vectorGeo = data.type === 'VectorFeature' ? data.geometry : convertGeometry(data.geometry, buildBBox); for (const { geometry, face } of convertVectorGeometry(vectorGeo, tolerance, maxzoom)) { res.push({ id, type: 'S2Feature', face, properties, metadata, geometry, }); } return res; } /** * Convert a GeoJSON Feature to a GeoJSON Vector Feature * @param data - GeoJSON Feature * @param buildBBox - optional - build a bbox for the feature if desired * @returns - GeoJson Vector Feature */ export function toVector(data, buildBBox) { const { id, properties, metadata } = data; const vectorGeo = convertGeometry(data.geometry, buildBBox); return { id, type: 'VectorFeature', properties, metadata, geometry: vectorGeo, }; } /** * Mutate a GeoJSON Point to a GeoJson Vector Point * @param point - GeoJSON flat Point * @param m - optional m-value * @param bbox - if bbox is provided, we will extend the bbox * @returns - GeoJson Vector Point */ function convertPoint(point, m, bbox) { const newPoint = { x: point[0], y: point[1], z: point[2], m }; if (bbox !== undefined) { const newBBox = extendBBox(bbox, newPoint); for (let i = 0; i < newBBox.length; i++) bbox[i] = newBBox[i]; } return newPoint; } /** * Convert a GeoJSON Geometry to an Vector Geometry * @param geometry - GeoJSON Geometry * @param buildBBox - optional - build a bbox for the feature if desired * @returns - GeoJson Vector Geometry */ function convertGeometry(geometry, buildBBox) { const { type, coordinates: coords, mValues, bbox } = geometry; const newBBox = buildBBox === true && bbox === undefined ? [] : undefined; let coordinates; if (type === 'Point' || type === 'Point3D') coordinates = convertPoint(coords, mValues, newBBox); else if (type === 'MultiPoint' || type === 'MultiPoint3D') coordinates = coords.map((point, i) => convertPoint(point, mValues?.[i], newBBox)); else if (type === 'LineString' || type === 'LineString3D') coordinates = coords.map((point, i) => convertPoint(point, mValues?.[i], newBBox)); else if (type === 'MultiLineString' || type === 'MultiLineString3D') coordinates = coords.map((line, i) => line.map((point, j) => convertPoint(point, mValues?.[i]?.[j], newBBox))); else if (type === 'Polygon' || type === 'Polygon3D') coordinates = coords.map((line, i) => line.map((point, j) => convertPoint(point, mValues?.[i]?.[j], newBBox))); else if (type === 'MultiPolygon' || type === 'MultiPolygon3D') coordinates = coords.map((polygon, i) => polygon.map((line, j) => line.map((point, k) => convertPoint(point, mValues?.[i]?.[j]?.[k], newBBox)))); else { throw new Error('Invalid GeoJSON type'); } const is3D = type.slice(-2) === '3D'; // @ts-expect-error - coordinates complains, but the way this is all written is simpler return { type: type.replace('3D', ''), is3D, coordinates, bbox: newBBox ?? bbox }; } /** * Underlying conversion mechanic to move GeoJSON Geometry to S2Geometry * @param geometry - GeoJSON Geometry * @param tolerance - if provided, geometry will be prepared for simplification by this tolerance * @param maxzoom - if provided, geometry will be prepared for simplification up to this zoom * @returns - S2Geometry */ function convertVectorGeometry(geometry, tolerance, maxzoom) { 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.'); } if (tolerance !== undefined) for (const { geometry } of cGeo) buildSqDists(geometry, tolerance, maxzoom); return cGeo; } /** * @param geometry - GeoJSON PointGeometry * @returns - S2 PointGeometry */ function convertGeometryPoint(geometry) { const { type, is3D, coordinates, bbox } = geometry; const { x: lon, y: lat, z, m } = coordinates; const [face, s, t] = toST(fromLonLat(lon, lat)); 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 = []; // first re-project all the coordinates to S2 const newGeometry = []; for (const { x: lon, y: lat, z, m } of line) { const [face, s, t] = toST(fromLonLat(lon, lat)); newGeometry.push({ face, s, t, z, m }); } // find all the faces that exist in the line const faces = new Set(); newGeometry.forEach(({ face }) => faces.add(face)); // 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; } /** * Reproject GeoJSON geometry coordinates from lon-lat to a 0->1 coordinate system in place * @param feature - input GeoJSON * @param tolerance - if provided, geometry will be prepared for simplification by this tolerance * @param maxzoom - if provided, */ export function toUnitScale(feature, tolerance, maxzoom) { const { geometry } = feature; const { type, coordinates } = geometry; if (type === 'Point') projectPoint(coordinates, geometry); else if (type === 'MultiPoint') coordinates.map((p) => projectPoint(p, geometry)); else if (type === 'LineString') coordinates.map((p) => projectPoint(p, geometry)); else if (type === 'MultiLineString') coordinates.map((l) => l.map((p) => projectPoint(p, geometry))); else if (type === 'Polygon') coordinates.map((l) => l.map((p) => projectPoint(p, geometry))); else if (type === 'MultiPolygon') coordinates.map((p) => p.map((l) => l.map((p) => projectPoint(p, geometry)))); else { throw new Error('Either the conversion is not yet supported or Invalid S2Geometry type.'); } if (tolerance !== undefined) buildSqDists(geometry, tolerance, maxzoom); } /** * Reproject GeoJSON geometry coordinates from 0->1 coordinate system to lon-lat in place * @param feature - input GeoJSON */ export function toLL(feature) { const { type, coordinates } = feature.geometry; if (type === 'Point') unprojectPoint(coordinates); else if (type === 'MultiPoint') coordinates.map((p) => unprojectPoint(p)); else if (type === 'LineString') coordinates.map((p) => unprojectPoint(p)); else if (type === 'MultiLineString') coordinates.map((l) => l.map((p) => unprojectPoint(p))); else if (type === 'Polygon') coordinates.map((l) => l.map((p) => unprojectPoint(p))); else if (type === 'MultiPolygon') coordinates.map((p) => p.map((l) => l.map((p) => unprojectPoint(p)))); else { throw new Error('Either the conversion is not yet supported or Invalid S2Geometry type.'); } } /** * Project a point from lon-lat to a 0->1 coordinate system in place * @param input - input point * @param geo - input geometry (used to update the bbox) */ function projectPoint(input, geo) { const { x, y } = input; const sin = Math.sin((y * Math.PI) / 180); const y2 = 0.5 - (0.25 * Math.log((1 + sin) / (1 - sin))) / Math.PI; input.x = x / 360 + 0.5; input.y = y2 < 0 ? 0 : y2 > 1 ? 1 : y2; // update bbox geo.vecBBox = extendBBox(geo.vecBBox, input); } /** * Project a point from 0->1 coordinate space to lon-lat in place * @param input - input vector to mutate */ function unprojectPoint(input) { const { x, y } = input; // Revert the x coordinate const lon = (x - 0.5) * 360; // Revert the y coordinate const y2 = 0.5 - y; const lat = radToDeg(Math.atan(Math.sinh(Math.PI * (y2 * 2)))); input.x = lon; input.y = lat; } /** * @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=convert.js.map