├── LICENSE ├── README.md ├── package.json ├── simplify2.js └── test.js /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Anders Goettsche 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are 5 | permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of 8 | conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, this list 11 | of conditions and the following disclaimer in the documentation and/or other materials 12 | provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY 15 | EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 16 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 17 | COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 18 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 19 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 20 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR 21 | TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | simplify2 2 | ========= 3 | 4 | This is another version of [Vladimir Agafonkin]'s awesome [simplify.js] library for doing high-performance polyline 5 | simplification in JavaScript primarily using the Ramer-Douglas-Peucker simplification algorithm. 6 | 7 | Simplify2.js is a collection of similar simplification algorithms that has been somewhat optimized. The douglasPeucker() function does not require any temporary internal memory constructs with a size proportional to the input (for marker array and stack); the use of recursion of cause requires use of some number of stack frames - but this will in practice be very few. The math and flow has also been tweaked so that fewer calculations are needed. 8 | 9 | A quite noticeable speedup can be observed on large inputs (>100K+ point) with a low tolerance < 1 for high fidelity 10 | output (e.g. needed for LOD generation). 11 | 12 | Additional test cases have been added. 13 | 14 | Please see in-code API docs. 15 | 16 | Usage 17 | ----- 18 | var simplify = require('simplify2'); 19 | ... 20 | var points = [...], // array of {x, y} objects. 21 | tolerance = 0.2; 22 | 23 | // run the douglas-peucker simplification 24 | var simplifiedPoints = simplify.douglasPeucker(points, tolerance); 25 | 26 | 27 | [Vladimir Agafonkin]: http://agafonkin.com/en 28 | [simplify.js]: https://github.com/mourner/simplify-js 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simplify2-js", 3 | "version": "1.0.0", 4 | "description": "A collection of line simplification algorithms.", 5 | "author": "Anders Goettsche", 6 | "keywords": [ 7 | "math", 8 | "geometry", 9 | "line", 10 | "simplification", 11 | "douglas", 12 | "peucker" 13 | ], 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/geonome/simplify2-js.git" 17 | }, 18 | "main": "simplify2.js", 19 | "devDependencies": { 20 | "tape": "^3.5.0", 21 | "tap-spec": "1.0.0", 22 | "jshint": "^2.6.3" 23 | }, 24 | "scripts": { 25 | "test": "jshint simplify2.js test.js && node test.js | tap-spec" 26 | }, 27 | "jshintConfig": { 28 | "quotmark": "single", 29 | "trailing": true, 30 | "unused": false 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /simplify2.js: -------------------------------------------------------------------------------- 1 | /** 2 | * (c) 2015, Anders Goettsche 3 | * 4 | * https://github.com/geonome/simplify2-js 5 | */ 6 | (function (name, definition) { 7 | if (typeof module !== 'undefined') { 8 | module.exports = definition.apply(); 9 | } else if (typeof define === 'function' && define.amd) { 10 | define(name, [], definition); 11 | } else { 12 | window[name] = definition.apply(); 13 | } 14 | })('simplify2', function () { 15 | 'use strict'; 16 | 17 | function _appendIfNotSame(points, point) { 18 | var p = points[points.length - 1]; 19 | if (p.x != point.x || p.y != point.y) { 20 | points.push(point); 21 | } 22 | } 23 | 24 | function _simplifyRadialDist(points, simplifiedPoints, tolerance2) { 25 | var length = points.length, 26 | i, 27 | pl = points[0], 28 | pi, 29 | dx, 30 | dy, 31 | r; 32 | 33 | simplifiedPoints.push(pl); 34 | for (i = 1; i < length; i++) { 35 | pi = points[i]; 36 | 37 | dx = pl.x - pi.x; 38 | r = tolerance2 - dx * dx; 39 | if (r >= 0) { 40 | dy = pl.y - pi.y; 41 | if (r - dy * dy < 0) { 42 | simplifiedPoints.push(pi); 43 | pl = pi; 44 | } 45 | } else { 46 | simplifiedPoints.push(pi); 47 | pl = pi; 48 | } 49 | } 50 | 51 | _appendIfNotSame(simplifiedPoints, pi); 52 | 53 | return simplifiedPoints; 54 | } 55 | 56 | function _simplifyDouglasPeucker(points, simplifiedPoints, tolerance2, t0, t1) { 57 | var maxD = tolerance2, 58 | p0 = points[t0], 59 | p1 = points[t1], 60 | x0 = p0.x, 61 | y0 = p0.y, 62 | dx = p1.x - x0, 63 | dy = p1.y - y0, 64 | d2, 65 | id2, 66 | maxIdx, 67 | d, 68 | p, 69 | i, 70 | s; 71 | 72 | if (dx === 0 && dy === 0) { 73 | if (t1 > t0 + 1) { 74 | var m = (t0 + t1) >> 1; 75 | _simplifyDouglasPeucker(points, simplifiedPoints, tolerance2, t0, m); 76 | _appendIfNotSame(simplifiedPoints, points[m]); 77 | _simplifyDouglasPeucker(points, simplifiedPoints, tolerance2, m, t1); 78 | } 79 | return; 80 | } 81 | 82 | d2 = dx * dx + dy * dy; 83 | id2 = 1 / d2; 84 | for (i = t0; i <= t1; i++) { 85 | p = points[i]; 86 | s = dx * (y0 - p.y) + dy * (p.x - x0); 87 | d = s * s * id2; 88 | 89 | if (d > maxD) { 90 | maxIdx = i; 91 | maxD = d; 92 | } 93 | } 94 | 95 | if (maxD > tolerance2) { 96 | _simplifyDouglasPeucker(points, simplifiedPoints, tolerance2, t0, maxIdx); 97 | simplifiedPoints.push(points[maxIdx]); 98 | _simplifyDouglasPeucker(points, simplifiedPoints, tolerance2, maxIdx, t1); 99 | } 100 | } 101 | 102 | return { 103 | /** 104 | * Simplifies the input points by only picking the ones separated by the specified tolerance distance. 105 | * 106 | * @param points the input points. 107 | * @param tolerance the tolerance distance. 108 | * @returns {[]} a new array holding the simplified points. 109 | */ 110 | radialDistance: function (points, tolerance) { 111 | return (points && points.length > 1 && tolerance > 0) ? _simplifyRadialDist(points, [], tolerance * tolerance) : points; 112 | }, 113 | 114 | /** 115 | * Simplifies the input points using the Ramer-Douglas-Peucker algorithm using the specified tolerance distance. 116 | * 117 | * @param points the input points. 118 | * @param tolerance the tolerance distance. 119 | * @returns {[]} a new array holding the simplified points. 120 | */ 121 | douglasPeucker: function (points, tolerance) { 122 | if (points && points.length > 1 && tolerance > 0) { 123 | var sPoints = []; 124 | sPoints.push(points[0]); 125 | _simplifyDouglasPeucker(points, sPoints, tolerance * tolerance, 0, points.length - 1); 126 | _appendIfNotSame(sPoints, points[points.length - 1]); 127 | return sPoints; 128 | } 129 | return points; 130 | }, 131 | 132 | /** 133 | * Simplifies the input points first using a radial distance simplification followed by a 134 | * douglas-peucker simplification. 135 | * 136 | * @param points the input points. 137 | * @param tolerance the tolerance distance. 138 | * @returns {[]} a new array holding the simplified points. 139 | */ 140 | radialDistanceAndDouglasPeucker: function (points, tolerance) { 141 | return this.douglasPeucker(this.radialDistance(points, tolerance), tolerance); 142 | } 143 | }; 144 | }); -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var sample_1 = [ 2 | {x: 224.55, y: 250.15}, {x: 226.91, y: 244.19}, {x: 233.31, y: 241.45}, {x: 234.98, y: 236.06}, 3 | {x: 244.21, y: 232.76}, {x: 262.59, y: 215.31}, {x: 267.76, y: 213.81}, {x: 273.57, y: 201.84}, 4 | {x: 273.12, y: 192.16}, {x: 277.62, y: 189.03}, {x: 280.36, y: 181.41}, {x: 286.51, y: 177.74}, 5 | {x: 292.41, y: 159.37}, {x: 296.91, y: 155.64}, {x: 314.95, y: 151.37}, {x: 319.75, y: 145.16}, 6 | {x: 330.33, y: 137.57}, {x: 341.48, y: 139.96}, {x: 369.98, y: 137.89}, {x: 387.39, y: 142.51}, 7 | {x: 391.28, y: 139.39}, {x: 409.52, y: 141.14}, {x: 414.82, y: 139.75}, {x: 427.72, y: 127.30}, 8 | {x: 439.60, y: 119.74}, {x: 474.93, y: 107.87}, {x: 486.51, y: 106.75}, {x: 489.20, y: 109.45}, 9 | {x: 493.79, y: 108.63}, {x: 504.74, y: 119.66}, {x: 512.96, y: 122.35}, {x: 518.63, y: 120.89}, 10 | {x: 524.09, y: 126.88}, {x: 529.57, y: 127.86}, {x: 534.21, y: 140.93}, {x: 539.27, y: 147.24}, 11 | {x: 567.69, y: 148.91}, {x: 575.25, y: 157.26}, {x: 580.62, y: 158.15}, {x: 601.53, y: 156.85}, 12 | {x: 617.74, y: 159.86}, {x: 622.00, y: 167.04}, {x: 629.55, y: 194.60}, {x: 638.90, y: 195.61}, 13 | {x: 641.26, y: 200.81}, {x: 651.77, y: 204.56}, {x: 671.55, y: 222.55}, {x: 683.68, y: 217.45}, 14 | {x: 695.25, y: 219.15}, {x: 700.64, y: 217.98}, {x: 703.12, y: 214.36}, {x: 712.26, y: 215.87}, 15 | {x: 721.49, y: 212.81}, {x: 727.81, y: 213.36}, {x: 729.98, y: 208.73}, {x: 735.32, y: 208.20}, 16 | {x: 739.94, y: 204.77}, {x: 769.98, y: 208.42}, {x: 779.60, y: 216.87}, {x: 784.20, y: 218.16}, 17 | {x: 800.24, y: 214.62}, {x: 810.53, y: 219.73}, {x: 817.19, y: 226.82}, {x: 820.77, y: 236.17}, 18 | {x: 827.23, y: 236.16}, {x: 829.89, y: 239.89}, {x: 851.00, y: 248.94}, {x: 859.88, y: 255.49}, 19 | {x: 865.21, y: 268.53}, {x: 857.95, y: 280.30}, {x: 865.48, y: 291.45}, {x: 866.81, y: 298.66}, 20 | {x: 864.68, y: 302.71}, {x: 867.79, y: 306.17}, {x: 859.87, y: 311.37}, {x: 860.08, y: 314.35}, 21 | {x: 858.29, y: 314.94}, {x: 858.10, y: 327.60}, {x: 854.54, y: 335.40}, {x: 860.92, y: 343.00}, 22 | {x: 856.43, y: 350.15}, {x: 851.42, y: 352.96}, {x: 849.84, y: 359.59}, {x: 854.56, y: 365.53}, 23 | {x: 849.74, y: 370.38}, {x: 844.09, y: 371.89}, {x: 844.75, y: 380.44}, {x: 841.52, y: 383.67}, 24 | {x: 839.57, y: 390.40}, {x: 845.59, y: 399.05}, {x: 848.40, y: 407.55}, {x: 843.71, y: 411.30}, 25 | {x: 844.09, y: 419.88}, {x: 839.51, y: 432.76}, {x: 841.33, y: 441.04}, {x: 847.62, y: 449.22}, 26 | {x: 847.16, y: 458.44}, {x: 851.38, y: 462.79}, {x: 853.97, y: 471.15}, {x: 866.36, y: 480.77} 27 | ], 28 | dp_sample_1_expect = [ 29 | {x: 224.55, y: 250.15}, {x: 267.76, y: 213.81}, {x: 296.91, y: 155.64}, 30 | {x: 330.33, y: 137.57}, {x: 409.52, y: 141.14}, {x: 439.6, y: 119.74}, 31 | {x: 486.51, y: 106.75}, {x: 529.57, y: 127.86}, {x: 539.27, y: 147.24}, 32 | {x: 617.74, y: 159.86}, {x: 629.55, y: 194.6}, {x: 671.55, y: 222.55}, 33 | {x: 727.81, y: 213.36}, {x: 739.94, y: 204.77}, {x: 769.98, y: 208.42}, 34 | {x: 784.2, y: 218.16}, {x: 800.24, y: 214.62}, {x: 820.77, y: 236.17}, 35 | {x: 859.88, y: 255.49}, {x: 865.21, y: 268.53}, {x: 857.95, y: 280.3}, 36 | {x: 867.79, y: 306.17}, {x: 858.29, y: 314.94}, {x: 854.54, y: 335.4}, 37 | {x: 860.92, y: 343}, {x: 849.84, y: 359.59}, {x: 854.56, y: 365.53}, 38 | {x: 844.09, y: 371.89}, {x: 839.57, y: 390.4}, {x: 848.4, y: 407.55}, 39 | {x: 839.51, y: 432.76}, {x: 853.97, y: 471.15}, {x: 866.36, y: 480.77} 40 | ], 41 | rd_sample_1_expect = [ 42 | {x: 224.55, y: 250.15}, {x: 262.59, y: 215.31}, {x: 286.51, y: 177.74}, 43 | {x: 319.75, y: 145.16}, {x: 369.98, y: 137.89}, {x: 414.82, y: 139.75}, 44 | {x: 474.93, y: 107.87}, {x: 512.96, y: 122.35}, {x: 567.69, y: 148.91}, 45 | {x: 617.74, y: 159.86}, {x: 638.9, y: 195.61}, {x: 671.55, y: 222.55}, 46 | {x: 712.26, y: 215.87}, {x: 769.98, y: 208.42}, {x: 810.53, y: 219.73}, 47 | {x: 851, y: 248.94}, {x: 865.48, y: 291.45}, {x: 854.54, y: 335.4}, 48 | {x: 844.75, y: 380.44}, {x: 839.51, y: 432.76}, {x: 853.97, y: 471.15}, {x: 866.36, y: 480.77} 49 | ], 50 | sample_2 = [ 51 | {x: 0, y: 0}, {x: 0, y: 0}, {x: 0, y: 0}, {x: 5, y: 6}, {x: 10, y: 10}, {x: 10, y: 10}, {x: 10, y: 10}, 52 | {x: 20, y: 19}, {x: 20, y: 19}, {x: 20, y: 19}, {x: 20, y: 19}, {x: 20, y: 19}, {x: 20, y: 19}, {x: 20, y: 19} 53 | ], 54 | dp_sample_2_expect = [ 55 | {x: 0, y: 0}, {x: 20, y: 19} 56 | ], 57 | rd_sample_2_expect = [ 58 | {x: 0, y: 0}, {x: 5, y: 6}, {x: 10, y: 10}, {x: 20, y: 19} 59 | ], 60 | sample_3 = [ 61 | {x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}, {x: 4, y: 4}, {x: 5, y: 5}, 62 | {x: 6, y: 6}, {x: 7, y: 7}, {x: 8, y: 8}, {x: 9, y: 9}, {x: 10, y: 10} 63 | ], 64 | dp_sample_3_expect = [ 65 | {x: 1, y: 1}, {x: 10, y: 10} 66 | ], 67 | rd_sample_3_expect = [ 68 | {x: 1, y: 1}, {x: 3, y: 3}, {x: 5, y: 5}, {x: 7, y: 7}, {x: 9, y: 9}, {x: 10, y: 10} 69 | ], 70 | sample_4 = [ 71 | {x: 1, y: 0}, {x: 2, y: 0}, {x: 3, y: 0}, {x: 2.9, y: 1}, {x: 2.8, y: 2}, {x: 2.9, y: 3}, {x: 3, y: 4}, 72 | {x: 3.1, y: 3}, {x: 3.2, y: 2}, {x: 3.1, y: 1}, {x: 3, y: 0}, {x: 4, y: 0}, {x: 5, y: 0} 73 | ], 74 | dp_sample_4_expect = [ 75 | { x: 1, y: 0 }, { x: 3, y: 0 }, { x: 3, y: 4 }, { x: 3, y: 0 }, { x: 5, y: 0 } 76 | ], 77 | t = require('tape'), 78 | simplify2 = require('./simplify2'), 79 | douglasPeucker = simplify2.douglasPeucker, 80 | radialDistance = simplify2.radialDistance; 81 | 82 | // --- douglasPeucker test cases --- 83 | 84 | t('douglas-peucker: sample 1', function (t) { 85 | var result = douglasPeucker(sample_1, 5); 86 | t.same(result, dp_sample_1_expect); 87 | t.end(); 88 | }); 89 | 90 | t('douglas-peucker: sample 2', function (t) { 91 | var result = douglasPeucker(sample_2, 5); 92 | t.same(result, dp_sample_2_expect); 93 | t.end(); 94 | }); 95 | 96 | t('douglas-peucker: sample 3', function (t) { 97 | var result = douglasPeucker(sample_3, 2); 98 | t.same(result, dp_sample_3_expect); 99 | t.end(); 100 | }); 101 | 102 | t('douglas-peucker: sample 4', function (t) { 103 | var result = douglasPeucker(sample_4, 1); 104 | t.same(result, dp_sample_4_expect); 105 | t.end(); 106 | }); 107 | 108 | t('douglas-peucker: [] -> []', function (t) { 109 | var result = douglasPeucker([], 1); 110 | t.same(result, []); 111 | t.end(); 112 | }); 113 | 114 | t('douglas-peucker: [p] -> [p]', function (t) { 115 | var result = douglasPeucker([{x: 1, y: 2}], 1); 116 | t.same(result, [{x: 1, y: 2}]); 117 | t.end(); 118 | }); 119 | 120 | t('douglas-peucker: [p, p] -> [p]', function (t) { 121 | var result = douglasPeucker([{x: 1, y: 2}, {x: 1, y: 2}], 1); 122 | t.same(result, [{x: 1, y: 2}]); 123 | t.end(); 124 | }); 125 | 126 | t('douglas-peucker: [p*] -> [p]', function (t) { 127 | var result = douglasPeucker([{x: 1, y: 2}, {x: 1, y: 2}, {x: 1, y: 2}, {x: 1, y: 2}], 1); 128 | t.same(result, [{x: 1, y: 2}]); 129 | t.end(); 130 | }); 131 | 132 | t('douglas-peucker: [p1, p2] -> [p1, p2]', function (t) { 133 | var result = douglasPeucker([{x: 1, y: 2}, {x: 4, y: 3}], 1); 134 | t.same(result, [{x: 1, y: 2}, {x: 4, y: 3}]); 135 | t.end(); 136 | }); 137 | 138 | t('douglas-peucker: [p1, p2*, p3] -> [p1, p2, p3]', function (t) { 139 | var result = douglasPeucker([{x: 1, y: 2}, {x: 3, y: 4}, {x: 3, y: 4}, {x: 3, y: 4}, {x: 3, y: 4}, {x: 5, y: 2}], 1); 140 | t.same(result, [{x: 1, y: 2}, {x: 3, y: 4}, {x: 5, y: 2}]); 141 | t.end(); 142 | }); 143 | 144 | t('douglas-peucker: [p1*, p2*] -> [p1, p2]', function (t) { 145 | var result = douglasPeucker([ 146 | {x: 1, y: 2}, {x: 1, y: 2}, {x: 1, y: 2}, 147 | {x: 4, y: 3}, {x: 4, y: 3}, {x: 4, y: 3}, {x: 4, y: 3}], 148 | 1); 149 | t.same(result, [{x: 1, y: 2}, {x: 4, y: 3}]); 150 | t.end(); 151 | }); 152 | 153 | // --- radialDistance test cases --- 154 | 155 | t('radial-distance: sample 1', function (t) { 156 | var result = radialDistance(sample_1, 40); 157 | t.same(result, rd_sample_1_expect); 158 | t.end(); 159 | }); 160 | 161 | t('radial-distance: sample 2', function (t) { 162 | var result = radialDistance(sample_2, 5); 163 | t.same(result, rd_sample_2_expect); 164 | t.end(); 165 | }); 166 | 167 | t('radial-distance: sample 3', function (t) { 168 | var result = radialDistance(sample_3, 2); 169 | t.same(result, rd_sample_3_expect); 170 | t.end(); 171 | }); 172 | 173 | t('radial-distance: [] -> []', function (t) { 174 | var result = radialDistance([], 1); 175 | t.same(result, []); 176 | t.end(); 177 | }); 178 | 179 | t('radial-distance: [p] -> [p]', function (t) { 180 | var result = radialDistance([{x: 1, y: 2}], 1); 181 | t.same(result, [{x: 1, y: 2}]); 182 | t.end(); 183 | }); 184 | 185 | t('radial-distance: [p, p] -> [p]', function (t) { 186 | var result = radialDistance([{x: 1, y: 2}, {x: 1, y: 2}], 1); 187 | t.same(result, [{x: 1, y: 2}]); 188 | t.end(); 189 | }); 190 | 191 | t('radial-distance: [p*] -> [p]', function (t) { 192 | var result = radialDistance([{x: 1, y: 2}, {x: 1, y: 2}, {x: 1, y: 2}, {x: 1, y: 2}], 1); 193 | t.same(result, [{x: 1, y: 2}]); 194 | t.end(); 195 | }); 196 | 197 | t('radial-distance: [p1, p2] -> [p1, p2]', function (t) { 198 | var result = radialDistance([{x: 1, y: 2}, {x: 4, y: 3}], 1); 199 | t.same(result, [{x: 1, y: 2}, {x: 4, y: 3}]); 200 | t.end(); 201 | }); 202 | 203 | t('radial-distance: [p1*, p2*] -> [p1, p2]', function (t) { 204 | var result = radialDistance([ 205 | {x: 1, y: 2}, {x: 1, y: 2}, {x: 1, y: 2}, 206 | {x: 4, y: 3}, {x: 4, y: 3}, {x: 4, y: 3}, {x: 4, y: 3}], 207 | 1); 208 | t.same(result, [{x: 1, y: 2}, {x: 4, y: 3}]); 209 | t.end(); 210 | }); 211 | 212 | 213 | --------------------------------------------------------------------------------