diff --git a/index.html b/index.html index f9b627c16..83ae066eb 100644 --- a/index.html +++ b/index.html @@ -85,6 +85,7 @@ + @@ -112,6 +113,7 @@ + diff --git a/js/id/actions/orthogonalize.js b/js/id/actions/orthogonalize.js new file mode 100644 index 000000000..830d1a1eb --- /dev/null +++ b/js/id/actions/orthogonalize.js @@ -0,0 +1,139 @@ +iD.actions.Orthogonalize = function(wayId, projection) { + + var action = function(graph) { + var way = graph.entity(wayId), + nodes = graph.childNodes(way); + + var points = nodes.map(function(n) { + return projection(n.loc); + }), + quad_nodes = []; + + var score = squareness(); + for (var i = 0; i < 1000; i++) { + var motions = points.map(stepMap); + for (var j = 0; j < motions.length; j++) { + points[j] = addPoints(points[j],motions[j]); + } + var newScore = squareness(); + if (newScore > score) { + return graph; + } + score = newScore; + if (score < 1.0e-8) { + break; + } + } + for (i = 0; i < points.length - 1; i++) { + quad_nodes.push(iD.Node({ loc: projection.invert(points[i]) })); + } + + for (i = 0; i < nodes.length; i++) { + if (graph.parentWays(nodes[i]).length > 1) { + var closest, closest_dist = Infinity, dist; + for (var j = 0; j < quad_nodes.length; j++) { + dist = iD.geo.dist(quad_nodes[j].loc, nodes[i].loc); + if (dist < closest_dist) { + closest_dist = dist; + closest = j; + } + } + quad_nodes.splice(closest, 1, nodes[i]); + } + } + + for (i = 0; i < quad_nodes.length; i++) { + graph = graph.replace(quad_nodes[i]); + } + + var ids = _.pluck(quad_nodes, 'id'), + difference = _.difference(_.uniq(way.nodes), ids); + + ids.push(ids[0]); + + graph = graph.replace(way.update({nodes: ids})); + + for (i = 0; i < difference.length; i++) { + graph = iD.actions.DeleteNode(difference[i])(graph); + } + + return graph; + + function stepMap(b, i, array) { + var a, c, p, q = []; + a = array[(i - 1 + array.length) % array.length]; + c = array[(i + 1) % array.length]; + p = subtractPoints(a, b); + q = subtractPoints(c, b); + + var scale = p.length + q.length; + p = normalizePoint(p, 1.0); + q = normalizePoint(q, 1.0); + var dotp = 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 (dotp < -0.707106781186547) { + dotp += 1.0; + } + var v = []; + v = addPoints(p, q); + v = normalizePoint(v, 0.1 * dotp * scale); + return v; + } + + 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, q = []; + 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. + var score = 2.0 * Math.min(Math.abs(dotp - 1.0), Math.min(Math.abs(dotp), Math.abs(dotp + 1))); + return score; + } + + 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, thickness) { + 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] *= thickness; + vector[1] *= thickness; + + return vector; + } + }; + + action.enabled = function(graph) { + return graph.entity(wayId).isClosed(); + }; + + return action; +}; diff --git a/js/id/operations/orthogonalize.js b/js/id/operations/orthogonalize.js new file mode 100644 index 000000000..c59ed13e4 --- /dev/null +++ b/js/id/operations/orthogonalize.js @@ -0,0 +1,25 @@ +iD.operations.Orthogonalize = function(selection, context) { + var entityId = selection[0], + action = iD.actions.Orthogonalize(entityId, context.projection); + + var operation = function() { + var annotation = t('operations.orthogonalize.annotation.' + context.geometry(entityId)); + context.perform(action, annotation); + }; + + operation.available = function() { + return selection.length === 1 && + context.entity(entityId).type === 'way'; + }; + + operation.enabled = function() { + return action.enabled(context.graph()); + }; + + operation.id = "orthogonalize"; + operation.key = t('operations.orthogonalize.key'); + operation.title = t('operations.orthogonalize.title'); + operation.description = t('operations.orthogonalize.description'); + + return operation; +}; diff --git a/locale/en.js b/locale/en.js index 866c7eca3..31d6a13de 100644 --- a/locale/en.js +++ b/locale/en.js @@ -65,6 +65,15 @@ locale.en = { area: "Made an area circular." } }, + orthogonalize: { + title: "Orthogonalize", + description: "Square these corners.", + key: "Q", + annotation: { + line: "Squared the corners of a line.", + area: "Squared the corners of an area." + } + }, delete: { title: "Delete", description: "Remove this from the map.", diff --git a/test/index.html b/test/index.html index b6d925923..551a87621 100644 --- a/test/index.html +++ b/test/index.html @@ -73,6 +73,7 @@ + @@ -108,6 +109,7 @@ +