gis-tools-ts
Version:
A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.
261 lines • 8.7 kB
JavaScript
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