From 5b1bbe15dd71f0c3b776394c164725676c7513ee Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Fri, 13 Dec 2013 13:51:58 +0100 Subject: [PATCH] Add ol.geom.simplify.schaub --- src/ol/geom/simplifygeom.js | 164 +++++++++++++++++++++++++ test/spec/ol/geom/simplifygeom.test.js | 65 ++++++++++ 2 files changed, 229 insertions(+) diff --git a/src/ol/geom/simplifygeom.js b/src/ol/geom/simplifygeom.js index d7cab5ea77..cd60faf37d 100644 --- a/src/ol/geom/simplifygeom.js +++ b/src/ol/geom/simplifygeom.js @@ -233,3 +233,167 @@ ol.geom.simplify.radialDistance = function(flatCoordinates, offset, end, } return simplifiedOffset; }; + + +/** + * @param {number} value Value. + * @param {number} tolerance Squared tolerance. + * @return {number} Rounded value. + */ +ol.geom.simplify.snap = function(value, tolerance) { + return tolerance * Math.round(value / tolerance); +}; + + +/** + * Simplifies a line string using an algorithm designed by Tim Schaub. + * Coordinates are snapped to the nearest value in a virtual grid and + * consecutive duplicate coordinates are discarded. This effectively preserves + * topology as the simplification of any subsection of a line string is + * independent of the rest of the line string. This means that, for examples, + * the common edge between two polygons will be simplified to the same line + * string independently in both polygons. This implementation uses a single + * pass over the coordinates and eliminates intermediate collinear points. + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {number} end End. + * @param {number} stride Stride. + * @param {number} tolerance Squared tolerance. + * @param {Array.} simplifiedFlatCoordinates Simplified flat + * coordinates. + * @param {number} simplifiedOffset Simplified offset. + * @return {number} Simplified offset. + */ +ol.geom.simplify.schaub = function(flatCoordinates, offset, end, stride, + tolerance, simplifiedFlatCoordinates, simplifiedOffset) { + // do nothing if the line is empty + if (offset == end) { + return simplifiedOffset; + } + // snap the first coordinate (P1) + var x1 = ol.geom.simplify.snap(flatCoordinates[offset], tolerance); + var y1 = ol.geom.simplify.snap(flatCoordinates[offset + 1], tolerance); + offset += stride; + // add the first coordinate to the output + simplifiedFlatCoordinates[simplifiedOffset++] = x1; + simplifiedFlatCoordinates[simplifiedOffset++] = y1; + // find the next coordinate that does not snap to the same value as the first + // coordinate (P2) + var x2, y2; + do { + x2 = ol.geom.simplify.snap(flatCoordinates[offset], tolerance); + y2 = ol.geom.simplify.snap(flatCoordinates[offset + 1], tolerance); + offset += stride; + if (offset == end) { + // all coordinates snap to the same value, the line collapses to a point + // push the last snapped value anyway to ensure that the output contains + // at least two points + // FIXME should we really return at least two points anyway? + simplifiedFlatCoordinates[simplifiedOffset++] = x2; + simplifiedFlatCoordinates[simplifiedOffset++] = y2; + return simplifiedOffset; + } + } while (x2 == x1 && y2 == y1); + while (offset < end) { + var x3, y3; + // snap the next coordinate (P3) + x3 = ol.geom.simplify.snap(flatCoordinates[offset], tolerance); + y3 = ol.geom.simplify.snap(flatCoordinates[offset + 1], tolerance); + offset += stride; + // skip P3 if it is equal to P2 + if (x3 == x2 && y3 == y2) { + continue; + } + // calculate the delta between P1 and P2 + var dx1 = x2 - x1; + var dy1 = y2 - y1; + // calculate the delta between P3 and P1 + var dx2 = x3 - x1; + var dy2 = y3 - y1; + // if P1, P2, and P3 are colinear and P3 is further from P1 than P2 is from + // P1 in the same direction then P2 is on the straight line between P1 and + // P3 + if ((dx1 * dy2 == dy1 * dx2) && + ((dx1 < 0 && dx2 < dx1) || dx1 == dx2 || (dx1 > 0 && dx2 > dx1)) && + ((dy1 < 0 && dy2 < dy1) || dy1 == dy2 || (dy1 > 0 && dy2 > dy1))) { + // discard P2 and set P2 = P3 + x2 = x3; + y2 = y3; + continue; + } + // either P1, P2, and P3 are not colinear, or they are colinear but P3 is + // between P3 and P1 or on the opposite half of the line to P2. add P2, + // and continue with P1 = P2 and P2 = P3 + simplifiedFlatCoordinates[simplifiedOffset++] = x2; + simplifiedFlatCoordinates[simplifiedOffset++] = y2; + x1 = x2; + y1 = y2; + x2 = x3; + y2 = y3; + } + // add the last point (P2) + simplifiedFlatCoordinates[simplifiedOffset++] = x2; + simplifiedFlatCoordinates[simplifiedOffset++] = y2; + return simplifiedOffset; +}; + + +/** + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {Array.} ends Ends. + * @param {number} stride Stride. + * @param {number} tolerance Squared tolerance. + * @param {Array.} simplifiedFlatCoordinates Simplified flat + * coordinates. + * @param {number} simplifiedOffset Simplified offset. + * @param {Array.} simplifiedEnds Simplified ends. + * @return {number} Simplified offset. + */ +ol.geom.simplify.schaubs = function( + flatCoordinates, offset, ends, stride, + tolerance, + simplifiedFlatCoordinates, simplifiedOffset, simplifiedEnds) { + var i, ii; + for (i = 0, ii = ends.length; i < ii; ++i) { + var end = ends[i]; + simplifiedOffset = ol.geom.simplify.schaub( + flatCoordinates, offset, end, stride, + tolerance, + simplifiedFlatCoordinates, simplifiedOffset); + simplifiedEnds.push(simplifiedOffset); + offset = end; + } + return simplifiedOffset; +}; + + +/** + * @param {Array.} flatCoordinates Flat coordinates. + * @param {number} offset Offset. + * @param {Array.>} endss Endss. + * @param {number} stride Stride. + * @param {number} tolerance Squared tolerance. + * @param {Array.} simplifiedFlatCoordinates Simplified flat + * coordinates. + * @param {number} simplifiedOffset Simplified offset. + * @param {Array.>} simplifiedEndss Simplified endss. + * @return {number} Simplified offset. + */ +ol.geom.simplify.schaubss = function( + flatCoordinates, offset, endss, stride, + tolerance, + simplifiedFlatCoordinates, simplifiedOffset, simplifiedEndss) { + var i, ii; + for (i = 0, ii = endss.length; i < ii; ++i) { + var ends = endss[i]; + var simplifiedEnds = []; + simplifiedOffset = ol.geom.simplify.schaubs( + flatCoordinates, offset, ends, stride, + tolerance, + simplifiedFlatCoordinates, simplifiedOffset, simplifiedEnds); + simplifiedEndss.push(simplifiedEnds); + offset = ends[ends.length - 1]; + } + return simplifiedOffset; +}; diff --git a/test/spec/ol/geom/simplifygeom.test.js b/test/spec/ol/geom/simplifygeom.test.js index 00795806d7..ee2b9d3a12 100644 --- a/test/spec/ol/geom/simplifygeom.test.js +++ b/test/spec/ol/geom/simplifygeom.test.js @@ -287,6 +287,71 @@ describe('ol.geom.simplify', function() { }); + describe('ol.geom.simplify.schaub', function() { + + it('handles empty coordinates', function() { + var simplifiedFlatCoordinates = []; + expect(ol.geom.simplify.schaub( + [], 0, 0, 2, 2, simplifiedFlatCoordinates, 0)).to.be(0); + expect(simplifiedFlatCoordinates).to.be.empty(); + }); + + it('expands points to a zero-length line', function() { + var simplifiedFlatCoordinates = []; + expect(ol.geom.simplify.schaub( + [0, 0, 0, 0], 0, 4, 2, 2, simplifiedFlatCoordinates, 0)).to.be(4); + expect(simplifiedFlatCoordinates).to.eql([0, 0, 0, 0]); + }); + + it('snaps near-by points to the same value', function() { + var simplifiedFlatCoordinates = []; + expect(ol.geom.simplify.schaub( + [0.1, 0, 0, 0.1], 0, 4, 2, 2, simplifiedFlatCoordinates, 0)).to.be(4); + expect(simplifiedFlatCoordinates).to.eql([0, 0, 0, 0]); + }); + + it('eliminates duplicate snapped points', function() { + var simplifiedFlatCoordinates = []; + expect(ol.geom.simplify.schaub( + [0.1, 0, 2, 0, 2.1, 0, 2, 0.1, 1.9, 0, 2, -0.1], 0, 12, 2, 2, + simplifiedFlatCoordinates, 0)).to.be(4); + expect(simplifiedFlatCoordinates).to.eql([0, 0, 2, 0]); + }); + + it('eliminates horizontal colinear points', function() { + var simplifiedFlatCoordinates = []; + expect(ol.geom.simplify.schaub( + [0, 0, 2, 0, 4, 0, 6, 0], 0, 8, 2, 2, + simplifiedFlatCoordinates, 0)).to.be(4); + expect(simplifiedFlatCoordinates).to.eql([0, 0, 6, 0]); + }); + + it('eliminates vertical colinear points', function() { + var simplifiedFlatCoordinates = []; + expect(ol.geom.simplify.schaub( + [0, 0, 0, -2, 0, -4, 0, -6], 0, 8, 2, 2, + simplifiedFlatCoordinates, 0)).to.be(4); + expect(simplifiedFlatCoordinates).to.eql([0, 0, 0, -6]); + }); + + it('eliminates diagonal colinear points', function() { + var simplifiedFlatCoordinates = []; + expect(ol.geom.simplify.schaub( + [0, 0, 2, -2, 4, -4, 6, -6], 0, 8, 2, 2, + simplifiedFlatCoordinates, 0)).to.be(4); + expect(simplifiedFlatCoordinates).to.eql([0, 0, 6, -6]); + }); + + it('handles switchbacks', function() { + var simplifiedFlatCoordinates = []; + expect(ol.geom.simplify.schaub( + [0, 0, 2, 0, 0, 0, 4, 0], 0, 8, 2, 2, + simplifiedFlatCoordinates, 0)).to.be(8); + expect(simplifiedFlatCoordinates).to.eql([0, 0, 2, 0, 0, 0, 4, 0]); + }); + + }); + });