s2-tools
Version:
A collection of geospatial tools primarily designed for WGS84, Web Mercator, and S2.
167 lines • 6.03 kB
JavaScript
/**
* Builds squared distances for the vector geometry using the Douglas-Peucker algorithm.
* @param geometry - input vector geometry
* @param tolerance - simplification tolerance
* @param maxzoom - max zoom level to simplify
*/
export function buildSqDists(geometry, tolerance, maxzoom = 16) {
const tol = Math.pow(tolerance / ((1 << maxzoom) * 4_096), 2);
const { type, coordinates: coords } = geometry;
if (type === 'LineString')
buildSqDist(coords, 0, coords.length - 1, tol);
else if (type === 'MultiLineString')
coords.forEach((line) => buildSqDist(line, 0, line.length - 1, tol));
else if (type === 'Polygon')
coords.forEach((line) => buildSqDist(line, 0, line.length - 1, tol));
else if (type === 'MultiPolygon')
coords.forEach((polygon) => polygon.forEach((line) => buildSqDist(line, 0, line.length - 1, tol)));
}
/**
* calculate simplification of line vector data using
* optimized Douglas-Peucker algorithm
* @param coords - input coordinates
* @param first - first point index
* @param last - last points index
* @param sqTolerance - simplification tolerance (higher means simpler)
*/
export function buildSqDist(coords, first, last, sqTolerance) {
coords[first].t = 1;
_buildSqDist(coords, first, last, sqTolerance);
coords[last].t = 1;
}
/**
* calculate simplification of line vector data using
* optimized Douglas-Peucker algorithm
* @param coords - input coordinates
* @param first - first point index
* @param last - last points index
* @param sqTolerance - simplification tolerance (higher means simpler)
*/
function _buildSqDist(coords, first, last, sqTolerance) {
let maxSqDist = sqTolerance;
const mid = (last - first) >> 1;
let minPosToMid = last - first;
let index;
const { x: as, y: at } = coords[first];
const { x: bs, y: bt } = coords[last];
for (let i = first; i < last; i++) {
const { x, y } = coords[i];
const d = getSqSegDist(x, y, as, at, bs, bt);
if (d > maxSqDist) {
index = i;
maxSqDist = d;
}
else if (d === maxSqDist) {
// a workaround to ensure we choose a pivot close to the middle of the list,
// reducing recursion depth, for certain degenerate inputs
const posToMid = Math.abs(i - mid);
if (posToMid < minPosToMid) {
index = i;
minPosToMid = posToMid;
}
}
}
if (index !== undefined && maxSqDist > sqTolerance) {
if (index - first > 1)
_buildSqDist(coords, first, index, sqTolerance);
coords[index].t = maxSqDist;
if (last - index > 1)
_buildSqDist(coords, index, last, sqTolerance);
}
}
/**
* square distance from a point to a segment
* @param ps - the reference point x
* @param pt - the reference point y
* @param s - the first point x in the segment
* @param t - the first point y in the segment
* @param bs - the last point x in the segment
* @param bt - the last point y in the segment
* @returns - the square distance
*/
function getSqSegDist(ps, pt, s, t, bs, bt) {
let ds = bs - s;
let dt = bt - t;
if (ds !== 0 || dt !== 0) {
const m = ((ps - s) * ds + (pt - t) * dt) / (ds * ds + dt * dt);
if (m > 1) {
s = bs;
t = bt;
}
else if (m > 0) {
s += ds * m;
t += dt * m;
}
}
ds = ps - s;
dt = pt - t;
return ds * ds + dt * dt;
}
/**
* Simplifies the vector geometry based on zoom level and tolerance.
* @param geometry - input vector geometry
* @param tolerance - simplification tolerance
* @param zoom - curent zoom
* @param maxzoom - max zoom level
*/
export function simplify(geometry, tolerance, zoom, maxzoom = 16) {
const zoomTol = zoom >= maxzoom ? 0 : tolerance / ((1 << zoom) * 4_096);
const { type, coordinates: coords } = geometry;
if (type === 'LineString')
geometry.coordinates = simplifyLine(coords, zoomTol, false, false);
else if (type === 'MultiLineString')
geometry.coordinates = coords.map((line) => simplifyLine(line, zoomTol, false, false));
else if (type === 'Polygon')
geometry.coordinates = coords.map((line, i) => simplifyLine(line, zoomTol, true, i === 0));
else if (type === 'MultiPolygon')
geometry.coordinates = coords.map((polygon) => polygon.map((line, i) => simplifyLine(line, zoomTol, true, i === 0)));
}
/**
* @param line - input vector line
* @param tolerance - simplification tolerance
* @param isPolygon - whether the line is a polygon
* @param isOuter - whether the line is an outer ring or inner ring (for polygons)
* @returns - simplified line
*/
function simplifyLine(line, tolerance, isPolygon, isOuter) {
const sqTolerance = tolerance * tolerance;
const size = line.length;
if (tolerance > 0 && size < (isPolygon ? sqTolerance : tolerance))
return line;
const ring = [];
for (const point of line) {
if (tolerance === 0 || (point.t ?? 0) > sqTolerance)
ring.push({ ...point });
}
if (isPolygon)
rewind(ring, isOuter);
return ring;
}
/**
* In place adjust the ring if necessary
* @param ring - the ring to rewind
* @param clockwise - whether the ring needs to be clockwise
*/
export function rewind(ring, clockwise) {
let area = 0;
for (let i = 0, len = ring.length, j = len - 2; i < len; j = i, i += 2) {
area += (ring[i].x - ring[j].x) * (ring[i].y + ring[j].y);
}
if (area > 0 === clockwise) {
for (let i = 0, len = ring.length; i < len / 2; i += 2) {
swapPoints(ring, i, len - i - 1);
}
}
}
/**
* Only swap the x, y, and z coordinates
* @param ring - the ring
* @param i - i position in the ring
* @param j - j position in the ring
*/
function swapPoints(ring, i, j) {
const tmp = ring[i];
ring[i] = ring[j];
ring[j] = tmp;
}
//# sourceMappingURL=simplify.js.map