From 0e080b3ed638966c7125d20e0efcec74cd571577 Mon Sep 17 00:00:00 2001 From: Paul Mach Date: Fri, 27 Sep 2013 09:40:24 -0700 Subject: [PATCH] Limit squaring to near square or near straight nodes --- data/core.yaml | 1 + dist/locales/en.json | 3 +- js/id/actions/orthogonalize.js | 153 ++++++++++++++++------------- test/spec/actions/orthogonalize.js | 15 +++ 4 files changed, 101 insertions(+), 71 deletions(-) diff --git a/data/core.yaml b/data/core.yaml index 11f7ad974..877389d33 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -64,6 +64,7 @@ en: annotation: line: Squared the corners of a line. area: Squared the corners of an area. + not_squarish: This can't be made square because it is not squarish. straighten: title: Straighten description: Straighten this line. diff --git a/dist/locales/en.json b/dist/locales/en.json index eaeba55f1..60bdd77b2 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -83,7 +83,8 @@ "annotation": { "line": "Squared the corners of a line.", "area": "Squared the corners of an area." - } + }, + "not_squarish": "This can't be made square because it is not squarish." }, "straighten": { "title": "Straighten", diff --git a/js/id/actions/orthogonalize.js b/js/id/actions/orthogonalize.js index d21cd5372..b206c36b8 100644 --- a/js/id/actions/orthogonalize.js +++ b/js/id/actions/orthogonalize.js @@ -3,21 +3,24 @@ */ iD.actions.Orthogonalize = function(wayId, projection) { + var threshold = 7, // degrees within right or straight to alter + lowerThreshold = Math.cos((90 - threshold) * Math.PI / 180), + upperThreshold = Math.cos(threshold * Math.PI / 180); + var action = function(graph) { var way = graph.entity(wayId), nodes = graph.childNodes(way), + points = _.uniq(nodes).map(function(n) { return projection(n.loc); }), corner = {i: 0, dotp: 1}, - epsilon = 1e-8, - points, i, j, score, motions; + epsilon = 1e-4, + i, j, score, motions; if (nodes.length === 4) { - points = _.uniq(nodes).map(function(n) { return projection(n.loc); }); - for (i = 0; i < 1000; i++) { motions = points.map(calcMotion); points[corner.i] = addPoints(points[corner.i],motions[corner.i]); score = corner.dotp; - if (score < 1.0e-8) { + if (score < epsilon) { break; } } @@ -25,8 +28,8 @@ iD.actions.Orthogonalize = function(wayId, projection) { graph = graph.replace(graph.entity(nodes[corner.i].id) .move(projection.invert(points[corner.i]))); } else { - var best; - points = _.uniq(nodes).map(function(n) { return projection(n.loc); }); + var best, + originalPoints = _.clone(points); score = Infinity; for (i = 0; i < 1000; i++) { @@ -34,7 +37,7 @@ iD.actions.Orthogonalize = function(wayId, projection) { for (j = 0; j < motions.length; j++) { points[j] = addPoints(points[j],motions[j]); } - var newScore = squareness(); + var newScore = squareness(points); if (newScore < score) { best = _.clone(points); score = newScore; @@ -47,8 +50,11 @@ iD.actions.Orthogonalize = function(wayId, projection) { points = best; for (i = 0; i < points.length; i++) { - graph = graph.replace(graph.entity(nodes[i].id) - .move(projection.invert(points[i]))); + // only move the points that actually moved + if (originalPoints[i][0] != points[i][0] || originalPoints[i][1] != points[i][1]) { + graph = graph.replace(graph.entity(nodes[i].id) + .move(projection.invert(points[i]))); + } } // remove empty nodes on straight sections @@ -63,13 +69,7 @@ iD.actions.Orthogonalize = function(wayId, projection) { continue; } - a = points[(i - 1 + points.length) % points.length]; - b = points[i]; - c = points[(i + 1) % points.length]; - p = normalizePoint(subtractPoints(a, b), 1.0); - q = normalizePoint(subtractPoints(c, b), 1.0); - dotp = p[0] * q[0] + p[1] * q[1]; - + dotp = normalizedDotProduct(i, points); if (dotp < -1 + epsilon) { graph = iD.actions.DeleteNode(nodes[i].id)(graph); } @@ -82,78 +82,91 @@ iD.actions.Orthogonalize = function(wayId, projection) { var a = array[(i - 1 + array.length) % array.length], c = array[(i + 1) % array.length], p = subtractPoints(a, b), - q = subtractPoints(c, b); + q = subtractPoints(c, b), + scale, dotp; - var scale = 2*Math.min(iD.geo.euclideanDistance(p, [0, 0]), iD.geo.euclideanDistance(q, [0, 0])); + scale = 2 * Math.min(iD.geo.euclideanDistance(p, [0, 0]), iD.geo.euclideanDistance(q, [0, 0])); p = normalizePoint(p, 1.0); q = normalizePoint(q, 1.0); - var dotp = p[0] * q[0] + p[1] * q[1]; + dotp = filterDotProduct(p[0] * q[0] + p[1] * q[1]); // nasty hack to deal with almost-straight segments (angle is closer to 180 than to 90/270). if (array.length > 3) { if (dotp < -0.707106781186547) { dotp += 1.0; } - } else if (Math.abs(dotp) < corner.dotp) { + } else if (dotp && Math.abs(dotp) < corner.dotp) { corner.i = i; corner.dotp = Math.abs(dotp); } return normalizePoint(addPoints(p, q), 0.1 * dotp * scale); } - - function squareness() { - var g = 0.0; - for (var i = 1; i < points.length - 1; i++) { - var score = scoreOfPoints(points[i - 1], points[i], points[i + 1]); - g += score; - } - var startScore = scoreOfPoints(points[points.length - 1], points[0], points[1]); - var endScore = scoreOfPoints(points[points.length - 2], points[points.length - 1], points[0]); - g += startScore; - g += endScore; - return g; - } - - function scoreOfPoints(a, b, c) { - var p = subtractPoints(a, b), - q = subtractPoints(c, b); - - p = normalizePoint(p, 1.0); - q = normalizePoint(q, 1.0); - - var dotp = p[0] * q[0] + p[1] * q[1]; - // score is constructed so that +1, -1 and 0 are all scored 0, any other angle - // is scored higher. - return 2.0 * Math.min(Math.abs(dotp - 1.0), Math.min(Math.abs(dotp), Math.abs(dotp + 1))); - } - - function subtractPoints(a, b) { - return [a[0] - b[0], a[1] - b[1]]; - } - - function addPoints(a, b) { - return [a[0] + b[0], a[1] + b[1]]; - } - - function normalizePoint(point, scale) { - var vector = [0, 0]; - var length = Math.sqrt(point[0] * point[0] + point[1] * point[1]); - if (length !== 0) { - vector[0] = point[0] / length; - vector[1] = point[1] / length; - } - - vector[0] *= scale; - vector[1] *= scale; - - return vector; - } }; + function squareness(points) { + return points.reduce(function(sum, val, i, array) { + var dotp = normalizedDotProduct(i, array); + + dotp = filterDotProduct(dotp); + return sum + 2.0 * Math.min(Math.abs(dotp - 1.0), Math.min(Math.abs(dotp), Math.abs(dotp + 1))); + }, 0); + } + + function normalizedDotProduct(i, points) { + var a = points[(i - 1 + points.length) % points.length], + b = points[i], + c = points[(i + 1) % points.length], + p = subtractPoints(a, b), + q = subtractPoints(c, b); + + p = normalizePoint(p, 1.0); + q = normalizePoint(q, 1.0); + + return p[0] * q[0] + p[1] * q[1]; + } + + function subtractPoints(a, b) { + return [a[0] - b[0], a[1] - b[1]]; + } + + function addPoints(a, b) { + return [a[0] + b[0], a[1] + b[1]]; + } + + function normalizePoint(point, scale) { + var vector = [0, 0]; + var length = Math.sqrt(point[0] * point[0] + point[1] * point[1]); + if (length !== 0) { + vector[0] = point[0] / length; + vector[1] = point[1] / length; + } + + vector[0] *= scale; + vector[1] *= scale; + + return vector; + } + + function filterDotProduct(dotp) { + if (lowerThreshold > Math.abs(dotp) || Math.abs(dotp) > upperThreshold) { + return dotp; + } + + return 0; + } + action.disabled = function(graph) { - return false; + var way = graph.entity(wayId), + nodes = graph.childNodes(way), + points = _.uniq(nodes).map(function(n) { return projection(n.loc); }); + + if (squareness(points)) { + return false; + } + + return 'not_squarish'; }; return action; diff --git a/test/spec/actions/orthogonalize.js b/test/spec/actions/orthogonalize.js index ed889bda0..3f185508e 100644 --- a/test/spec/actions/orthogonalize.js +++ b/test/spec/actions/orthogonalize.js @@ -106,4 +106,19 @@ describe("iD.actions.Orthogonalize", function () { expect(finalWidth / initialWidth).within(0.90, 1.10); } }); + + it("only moves nodes which are near right or near straight", function() { + var graph = iD.Graph({ + 'a': iD.Node({id: 'a', loc: [0, 0]}), + 'b': iD.Node({id: 'b', loc: [3, 0.001]}), + 'c': iD.Node({id: 'c', loc: [3, 1]}), + 'd': iD.Node({id: 'd', loc: [2, 1]}), + 'e': iD.Node({id: 'e', loc: [1, 2]}), + 'f': iD.Node({id: 'f', loc: [0, 2]}), + '-': iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'e', 'f', 'a']}) + }), + diff = iD.Difference(graph, iD.actions.Orthogonalize('-', projection)(graph)); + + expect(Object.keys(diff.changes()).sort()).to.eql(['a', 'b', 'c', 'f']); + }); });