diff --git a/src/ol/geom/multipolygon.js b/src/ol/geom/multipolygon.js index 74880c43e9..87adb0c827 100644 --- a/src/ol/geom/multipolygon.js +++ b/src/ol/geom/multipolygon.js @@ -143,10 +143,10 @@ ol.geom.MultiPolygon.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) { var simplifiedFlatCoordinates = []; var simplifiedEndss = []; - simplifiedFlatCoordinates.length = - ol.geom.simplify.douglasPeuckerss(this.flatCoordinates, 0, - this.endss_, this.stride, squaredTolerance, simplifiedFlatCoordinates, - 0, simplifiedEndss); + simplifiedFlatCoordinates.length = ol.geom.simplify.quantizess( + this.flatCoordinates, 0, this.endss_, this.stride, + Math.sqrt(squaredTolerance), + simplifiedFlatCoordinates, 0, simplifiedEndss); var simplifiedMultiPolygon = new ol.geom.MultiPolygon(null); simplifiedMultiPolygon.setFlatCoordinates( ol.geom.GeometryLayout.XY, simplifiedFlatCoordinates, simplifiedEndss); diff --git a/src/ol/geom/polygon.js b/src/ol/geom/polygon.js index 9300613f4b..b6d67d87fd 100644 --- a/src/ol/geom/polygon.js +++ b/src/ol/geom/polygon.js @@ -157,8 +157,9 @@ ol.geom.Polygon.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) { var simplifiedFlatCoordinates = []; var simplifiedEnds = []; - simplifiedFlatCoordinates.length = ol.geom.simplify.douglasPeuckers( - this.flatCoordinates, 0, this.ends_, this.stride, squaredTolerance, + simplifiedFlatCoordinates.length = ol.geom.simplify.quantizes( + this.flatCoordinates, 0, this.ends_, this.stride, + Math.sqrt(squaredTolerance), simplifiedFlatCoordinates, 0, simplifiedEnds); var simplifiedPolygon = new ol.geom.Polygon(null); simplifiedPolygon.setFlatCoordinates( diff --git a/src/ol/geom/simplifygeom.js b/src/ol/geom/simplifygeom.js index d7cab5ea77..427f8b22ce 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.quantize = 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.quantizes = 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.quantize( + 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.quantizess = 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.quantizes( + 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/polygon.test.js b/test/spec/ol/geom/polygon.test.js index a4410ff881..1f6e57089d 100644 --- a/test/spec/ol/geom/polygon.test.js +++ b/test/spec/ol/geom/polygon.test.js @@ -321,17 +321,17 @@ describe('ol.geom.Polygon', function() { describe('#getSimplifiedGeometry', function() { it('returns the expected result', function() { - var simplifiedGeometry = polygon.getSimplifiedGeometry(1); + var simplifiedGeometry = polygon.getSimplifiedGeometry(9); expect(simplifiedGeometry).to.be.an(ol.geom.Polygon); expect(simplifiedGeometry.getCoordinates()).to.eql( - [[[3, 0], [0, 6], [6, 6], [4, 3]]]); + [[[3, 0], [0, 3], [0, 6], [6, 6], [3, 3]]]); }); it('caches multiple simplified geometries', function() { - var simplifiedGeometry1 = polygon.getSimplifiedGeometry(1); - var simplifiedGeometry2 = polygon.getSimplifiedGeometry(2); - var simplifiedGeometry3 = polygon.getSimplifiedGeometry(1); - var simplifiedGeometry4 = polygon.getSimplifiedGeometry(2); + var simplifiedGeometry1 = polygon.getSimplifiedGeometry(4); + var simplifiedGeometry2 = polygon.getSimplifiedGeometry(9); + var simplifiedGeometry3 = polygon.getSimplifiedGeometry(4); + var simplifiedGeometry4 = polygon.getSimplifiedGeometry(9); expect(simplifiedGeometry1).to.be(simplifiedGeometry3); expect(simplifiedGeometry2).to.be(simplifiedGeometry4); }); diff --git a/test/spec/ol/geom/simplifygeom.test.js b/test/spec/ol/geom/simplifygeom.test.js index 00795806d7..a59f128304 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.quantize', function() { + + it('handles empty coordinates', function() { + var simplifiedFlatCoordinates = []; + expect(ol.geom.simplify.quantize( + [], 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.quantize( + [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.quantize( + [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.quantize( + [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.quantize( + [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.quantize( + [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.quantize( + [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.quantize( + [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]); + }); + + }); + });