UNPKG

leaflet

Version:

JavaScript library for mobile-friendly interactive maps

386 lines (326 loc) • 9.25 kB
import {Point} from './Point.js'; import {LatLng} from '../geo/LatLng.js'; /** * @import {Bounds} from './Bounds.js' * @import {CRS} from '../geo/crs/CRS.js' * @import {LatLngExpression} from '../types.js' */ /** * Various utility functions for polyline points processing, used by Leaflet internally to make polylines * lightning-fast. * @module LineUtil */ /** * Dramatically reduces the number of points in a polyline while retaining its shape and returns a new * array of simplified points, using the [Ramer-Douglas-Peucker * algorithm](https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm). Used for a huge performance * boost when processing/displaying Leaflet polylines for each zoom level and also reducing visual noise. * tolerance affects the amount of simplification (lesser value means higher quality but slower and with * more points). Also released as a separated micro-library * [Simplify.js](https://mourner.github.io/simplify-js/). * @param {Point[]} points * @param {number} tolerance */ export function simplify(points, tolerance) { if (!tolerance || !points.length) { return points.slice(); } const sqTolerance = tolerance * tolerance; // stage 1: vertex reduction points = _reducePoints(points, sqTolerance); // stage 2: Douglas-Peucker simplification points = _simplifyDP(points, sqTolerance); return points; } /** * Returns the distance between point `p` and segment `p1` to `p2`. * @param {Point} p * @param {Point} p1 * @param {Point} p2 */ export function pointToSegmentDistance(p, p1, p2) { return Math.sqrt(_sqClosestPointOnSegment(p, p1, p2, true)); } /** * Returns the closest point from a point `p` on a segment `p1` to `p2`. * @param {Point} p * @param {Point} p1 * @param {Point} p2 */ export function closestPointOnSegment(p, p1, p2) { return _sqClosestPointOnSegment(p, p1, p2); } // Ramer-Douglas-Peucker simplification, see https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm /** * @param {Point[]} points * @param {number} sqTolerance */ function _simplifyDP(points, sqTolerance) { const len = points.length, markers = new Uint8Array(len); markers[0] = markers[len - 1] = 1; _simplifyDPStep(points, markers, sqTolerance, 0, len - 1); let i; const newPoints = []; for (i = 0; i < len; i++) { if (markers[i]) { newPoints.push(points[i]); } } return newPoints; } /** * @param {Point[]} points * @param {Uint8Array} markers * @param {number} sqTolerance * @param {number} first * @param {number} last */ function _simplifyDPStep(points, markers, sqTolerance, first, last) { let maxSqDist = 0, index = 0, i, sqDist; for (i = first + 1; i <= last - 1; i++) { sqDist = _sqClosestPointOnSegment(points[i], points[first], points[last], true); if (sqDist > maxSqDist) { index = i; maxSqDist = sqDist; } } if (maxSqDist > sqTolerance) { markers[index] = 1; _simplifyDPStep(points, markers, sqTolerance, first, index); _simplifyDPStep(points, markers, sqTolerance, index, last); } } // reduce points that are too close to each other to a single point /** * @param {Point[]} points * @param {number} sqTolerance */ function _reducePoints(points, sqTolerance) { const reducedPoints = [points[0]]; let prev = 0; for (let i = 1; i < points.length; i++) { if (_sqDist(points[i], points[prev]) > sqTolerance) { reducedPoints.push(points[i]); prev = i; } } if (prev < points.length - 1) { reducedPoints.push(points[points.length - 1]); } return reducedPoints; } /** @type {number} */ let _lastCode; /** * Clips the segment a to b by rectangular bounds with the [Cohen-Sutherland * algorithm](https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm). Used by Leaflet to only show * polyline points that are on the screen or near, increasing performance. Returns the clipped endpoints (new points * where clipped, rounded with `round`), or `false` if the segment is entirely outside the bounds. `useLastCode` * reuses the bit code of `b` from the previous call as the code of `a`, for clipping consecutive segments of a line. * @param {Point} a * @param {Point} b * @param {Bounds} bounds * @param {boolean} [useLastCode] * @param {boolean} [round] * @returns {[Point, Point] | false} */ export function clipSegment(a, b, bounds, useLastCode, round) { const a0 = a, b0 = b; let codeA = useLastCode ? _lastCode : _getBitCode(a, bounds), codeB = _getBitCode(b, bounds), codeOut, p, newCode; // save 2nd code to avoid calculating it on the next segment _lastCode = codeB; while (true) { // if a,b is inside the clip window (trivial accept) if (!(codeA | codeB)) { // round only the endpoints that were replaced by a clipped intersection; // rounding while iterating can push a point back outside and loop forever if (round) { if (a !== a0) { a = a.round(); } if (b !== b0) { b = b.round(); } } return [a, b]; } // if a,b is outside the clip window (trivial reject) if (codeA & codeB) { return false; } // other cases codeOut = codeA || codeB; p = _getEdgeIntersection(a, b, codeOut, bounds); newCode = _getBitCode(p, bounds); if (codeOut === codeA) { a = p; codeA = newCode; } else { b = p; codeB = newCode; } } } /** * @param {Point} a * @param {Point} b * @param {number} code * @param {Bounds} bounds * @param {boolean} [round] */ export function _getEdgeIntersection(a, b, code, bounds, round) { const dx = b.x - a.x, dy = b.y - a.y, min = bounds.min, max = bounds.max; let x, y; if (code & 8) { // top x = a.x + dx * (max.y - a.y) / dy; y = max.y; } else if (code & 4) { // bottom x = a.x + dx * (min.y - a.y) / dy; y = min.y; } else if (code & 2) { // right x = max.x; y = a.y + dy * (max.x - a.x) / dx; } else { // left x = min.x; y = a.y + dy * (min.x - a.x) / dx; } return new Point(x, y, round); } /** * @param {Point} p * @param {Bounds} bounds */ export function _getBitCode(p, bounds) { let code = 0; if (p.x < bounds.min.x) { // left code |= 1; } else if (p.x > bounds.max.x) { // right code |= 2; } if (p.y < bounds.min.y) { // bottom code |= 4; } else if (p.y > bounds.max.y) { // top code |= 8; } return code; } // square distance (to avoid unnecessary Math.sqrt calls) /** * @param {Point} p1 * @param {Point} p2 */ function _sqDist(p1, p2) { const dx = p2.x - p1.x, dy = p2.y - p1.y; return dx * dx + dy * dy; } // return closest point on segment or distance to that point /** * @overload * @param {Point} p * @param {Point} p1 * @param {Point} p2 * @param {true} sqDist * @returns {number} */ /** * @overload * @param {Point} p * @param {Point} p1 * @param {Point} p2 * @param {false} [sqDist] * @returns {Point} */ /** * @param {Point} p * @param {Point} p1 * @param {Point} p2 * @param {boolean} [sqDist] */ export function _sqClosestPointOnSegment(p, p1, p2, sqDist) { let x = p1.x, y = p1.y, dx = p2.x - x, dy = p2.y - y, t; const dot = dx * dx + dy * dy; if (dot > 0) { t = ((p.x - x) * dx + (p.y - y) * dy) / dot; if (t > 1) { x = p2.x; y = p2.y; } else if (t > 0) { x += dx * t; y += dy * t; } } dx = p.x - x; dy = p.y - y; return sqDist ? dx * dx + dy * dy : new Point(x, y); } /** * An array of `T`s, nested up to 3 levels deep, like the points of a line, the rings of a polygon or the polygons * of a multipolygon. * @template T * @typedef {T[] | T[][] | T[][][]} NestedCoords */ /** * Returns true if `latlngs` is a flat array, false is nested. * @template {object} T * @param {NestedCoords<T>} latlngs * @returns {latlngs is T[]} */ export function isFlat(latlngs) { return !Array.isArray(latlngs[0]) || (typeof latlngs[0][0] !== 'object' && typeof latlngs[0][0] !== 'undefined'); } /** * Returns the center ([centroid](http://en.wikipedia.org/wiki/Centroid)) * of the passed LatLngs (first ring) from a polyline. * @param {LatLngExpression[]} latlngs * @param {typeof CRS} crs */ export function polylineCenter(latlngs, crs) { const points = _projectRing(latlngs, crs), len = points.length; let halfDist = 0; for (let i = 1; i < len; i++) { halfDist += points[i - 1].distanceTo(points[i]); } halfDist /= 2; // if the line has zero length, the loop below never finds a segment and the first point is the center let center = points[0]; for (let i = 1, dist = 0; i < len; i++) { const p1 = points[i - 1], p2 = points[i], segDist = p1.distanceTo(p2); dist += segDist; if (dist > halfDist) { const ratio = (dist - halfDist) / segDist; center = new Point( p2.x - ratio * (p2.x - p1.x), p2.y - ratio * (p2.y - p1.y) ); break; } } return crs.unproject(center); } // Projects the first ring of the given latlngs. /** * @param {NestedCoords<LatLngExpression>} latlngs * @param {typeof CRS} crs * @returns {Point[]} */ export function _projectRing(latlngs, crs) { if (!latlngs || latlngs.length === 0) { throw new Error('latlngs not passed'); } if (!isFlat(latlngs)) { console.warn('latlngs are not flat! Only the first ring will be used'); return _projectRing(latlngs[0], crs); } return latlngs.map(latlng => crs.project(new LatLng(latlng))); }