diff --git a/CHANGELOG.md b/CHANGELOG.md index e2a9ee89..a7e0bafe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Leaflet Changelog * Map now preserves its center after resize. * When panning to another copy of the world (that's infinite horizontally), map overlays now jump to corresponding positions. [#273](https://github.com/CloudMade/Leaflet/issues/273) * Limited maximum zoom change on a single mouse wheel movement (so you won't zoom across the whole zoom range in one scroll). [#149](https://github.com/CloudMade/Leaflet/issues/149) + * Significantly improved line simplification performance (noticeable when rendering polylines/polygons with tens of thousands of points) * Improved circles performance by not drawing them if they're off the clip region. #### API improvements diff --git a/src/geometry/LineUtil.js b/src/geometry/LineUtil.js index 21ccfd9e..ad0bded3 100644 --- a/src/geometry/LineUtil.js +++ b/src/geometry/LineUtil.js @@ -13,72 +13,80 @@ L.LineUtil = { return points.slice(); } + var sqTolerance = tolerance * tolerance; + // stage 1: vertex reduction - points = this.reducePoints(points, tolerance); + points = this._reducePoints(points, sqTolerance); // stage 2: Douglas-Peucker simplification - points = this.simplifyDP(points, tolerance); + points = this._simplifyDP(points, sqTolerance); return points; }, // distance from a point to a segment between two points pointToSegmentDistance: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2) { - return Math.sqrt(this._sqPointToSegmentDist(p, p1, p2)); + return Math.sqrt(this._sqClosestPointOnSegment(p, p1, p2, true)); }, closestPointOnSegment: function (/*Point*/ p, /*Point*/ p1, /*Point*/ p2) { - var point = this._sqClosestPointOnSegment(p, p1, p2); - point.distance = Math.sqrt(point._sqDist); - return point; + return this._sqClosestPointOnSegment(p, p1, p2); }, // Douglas-Peucker simplification, see http://en.wikipedia.org/wiki/Douglas-Peucker_algorithm - simplifyDP: function (points, tol) { - var maxDist2 = 0, - index = 0, - t2 = tol * tol, - len = points.length, - i, dist2; + _simplifyDP: function (points, sqTolerance) { - if (len < 3) { - return points; - } + var len = points.length, + ArrayConstructor = typeof Uint8Array !== 'undefined' ? Uint8Array : Array, + markers = new ArrayConstructor(len); - for (i = 0; i < len - 1; i++) { - dist2 = this._sqPointToSegmentDist(points[i], points[0], points[len - 1]); - if (dist2 > maxDist2) { - index = i; - maxDist2 = dist2; + markers[0] = markers[len - 1] = 1; + + this._simplifyDPStep(points, markers, sqTolerance, 0, len - 1); + + var i, + newPoints = []; + + for (i = 0; i < len; i++) { + if (markers[i]) { + newPoints.push(points[i]); } } - var part1, part2; + return newPoints; + }, - if (maxDist2 >= t2) { - part1 = points.slice(0, index); - part2 = points.slice(index); + _simplifyDPStep: function (points, markers, sqTolerance, first, last) { - part1 = this.simplifyDP(part1, tol); - part2 = this.simplifyDP(part2, tol); + var maxSqDist = 0, + index, i, sqDist; - return part1.concat(part2); - } else { - return [points[0], points[len - 1]]; + for (i = first + 1; i <= last - 1; i++) { + sqDist = this._sqClosestPointOnSegment(points[i], points[first], points[last], true); + + if (sqDist > maxSqDist) { + index = i; + maxSqDist = sqDist; + } + } + + if (maxSqDist > sqTolerance) { + markers[index] = 1; + + this._simplifyDPStep(points, markers, sqTolerance, first, index); + this._simplifyDPStep(points, markers, sqTolerance, index, last); } }, // reduce points that are too close to each other to a single point - reducePoints: function (points, tol) { - var reducedPoints = [points[0]], - t2 = tol * tol; + _reducePoints: function (points, sqTolerance) { + var reducedPoints = [points[0]]; for (var i = 1, prev = 0, len = points.length; i < len; i++) { - if (this._sqDist(points[i], points[prev]) < t2) { - continue; + if (this._sqDist(points[i], points[prev]) > sqTolerance) { + reducedPoints.push(points[i]); + prev = i; } - reducedPoints.push(points[i]); - prev = i; } if (prev < len - 1) { reducedPoints.push(points[len - 1]); @@ -168,27 +176,30 @@ L.LineUtil = { return dx * dx + dy * dy; }, - // return closest point on segment with attribute _sqDist - square distance to segment - _sqClosestPointOnSegment: function (p, p1, p2) { - var x2 = p2.x - p1.x, - y2 = p2.y - p1.y, - apoint = p1; - if (x2 || y2) { - var dot = (p.x - p1.x) * x2 + (p.y - p1.y) * y2, - t = dot / this._sqDist(p1, p2); + // return closest point on segment or distance to that point + _sqClosestPointOnSegment: function (p, p1, p2, sqDist) { + var x = p1.x, + y = p1.y, + dx = p2.x - x, + dy = p2.y - y, + dot = dx * dx + dy * dy, + t; + + if (dot > 0) { + t = ((p.x - x) * dx + (p.y - y) * dy) / dot; if (t > 1) { - apoint = p2; + x = p2.x; + y = p2.y; } else if (t > 0) { - apoint = new L.Point(p1.x + x2 * t, p1.y + y2 * t); + x += dx * t; + y += dy * t; } } - apoint._sqDist = this._sqDist(p, apoint); - return apoint; - }, - // distance from a point to a segment between two points - _sqPointToSegmentDist: function (p, p1, p2) { - return this._sqClosestPointOnSegment(p, p1, p2)._sqDist; + dx = p.x - x; + dy = p.y - y; + + return sqDist ? dx * dx + dy * dy : new L.Point(x, y); } };