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 @@
+