diff --git a/index.html b/index.html
index 9c633bc18..6701f2b82 100644
--- a/index.html
+++ b/index.html
@@ -108,6 +108,7 @@
+
@@ -133,6 +134,7 @@
+
@@ -142,6 +144,7 @@
+
diff --git a/js/id/actions/rotate_way.js b/js/id/actions/rotate_way.js
new file mode 100644
index 000000000..9de319780
--- /dev/null
+++ b/js/id/actions/rotate_way.js
@@ -0,0 +1,39 @@
+iD.actions.RotateWay = function(wayId, ref_points, pivot, mousePoint, projection) {
+ return function(graph) {
+ return graph.update(function(graph) {
+ var way = graph.entity(wayId),
+ nodes = _.uniq(graph.childNodes(way)),
+ angle, i, points;
+
+ points = deepCopy(ref_points);
+
+ angle = Math.atan2(mousePoint[1] - pivot[1], mousePoint[0] - pivot[0]);
+
+ for (i = 0; i < points.length; i++) {
+ var radial = [0,0];
+
+ radial[0] = points[i][0] - pivot[0];
+ radial[1] = points[i][1] - pivot[1];
+
+ points[i][0] = radial[0] * Math.cos(angle) - radial[1] * Math.sin(angle) + pivot[0];
+ points[i][1] = radial[0] * Math.sin(angle) + radial[1] * Math.cos(angle) + pivot[1];
+
+ }
+
+ for (i = 0; i < points.length; i++) {
+ graph = graph.replace(graph.entity(nodes[i].id).move(projection.invert(points[i])));
+ }
+
+ function deepCopy(o) {
+ var copy = o,k;
+ if (o && typeof o === 'object') {
+ copy = Object.prototype.toString.call(o) === '[object Array]' ? [] : {};
+ for (k in o) {
+ copy[k] = deepCopy(o[k]);
+ }
+ }
+ return copy;
+ }
+ });
+ };
+};
diff --git a/js/id/modes/rotate_way.js b/js/id/modes/rotate_way.js
new file mode 100644
index 000000000..c7b0e4a83
--- /dev/null
+++ b/js/id/modes/rotate_way.js
@@ -0,0 +1,74 @@
+iD.modes.RotateWay = function(context, wayId) {
+ var mode = {
+ id: 'rotate-way',
+ button: 'browse'
+ };
+
+ var keybinding = d3.keybinding('rotate-way');
+
+ mode.enter = function() {
+
+ var annotation = t('operations.rotate.annotation.' + context.geometry(wayId)),
+ way = context.graph().entity(wayId),
+ nodes = _.uniq(context.graph().childNodes(way)),
+ ref_points = nodes.map(function(n) { return context.projection(n.loc); }),
+ pivot = d3.geom.polygon(ref_points).centroid();
+
+ context.perform(
+ iD.actions.Noop(),
+ annotation);
+
+ function point() {
+ return d3.mouse(context.map().surface.node());
+ }
+
+ function rotate() {
+ var mousePoint = point();
+
+ context.replace(
+ iD.actions.RotateWay(wayId, ref_points, pivot, mousePoint, context.projection),
+ annotation);
+ }
+
+ function finish() {
+ d3.event.stopPropagation();
+ context.enter(iD.modes.Select(context, [wayId], true));
+ }
+
+ function cancel() {
+ context.pop();
+ context.enter(iD.modes.Select(context, [wayId], true));
+ }
+
+ function undone() {
+ context.enter(iD.modes.Browse(context));
+ }
+
+ context.surface()
+ .on('mousemove.rotate-way', rotate)
+ .on('click.rotate-way', finish);
+
+ context.history()
+ .on('undone.rotate-way', undone);
+
+ keybinding
+ .on('⎋', cancel)
+ .on('↩', finish);
+
+ d3.select(document)
+ .call(keybinding);
+ };
+
+ mode.exit = function() {
+ context.surface()
+ .on('mousemove.rotate-way', null)
+ .on('click.rotate-way', null);
+
+ context.history()
+ .on('undone.rotate-way', null);
+
+ keybinding.off();
+ };
+
+ return mode;
+};
diff --git a/js/id/operations/rotate.js b/js/id/operations/rotate.js
new file mode 100644
index 000000000..75a18770a
--- /dev/null
+++ b/js/id/operations/rotate.js
@@ -0,0 +1,24 @@
+iD.operations.Rotate = function(selection, context) {
+ var entityId = selection[0];
+
+ var operation = function() {
+ context.enter(iD.modes.RotateWay(context, entityId));
+ };
+
+ operation.available = function() {
+ return selection.length === 1 &&
+ context.entity(entityId).type === 'way' &&
+ context.entity(entityId).isClosed();
+ };
+
+ operation.enabled = function() {
+ return true;
+ };
+
+ operation.id = "rotate";
+ operation.key = t('operations.rotate.key');
+ operation.title = t('operations.rotate.title');
+ operation.description = t('operations.rotate.description');
+
+ return operation;
+};
diff --git a/locale/en.js b/locale/en.js
index 17b58275e..943ea9af4 100644
--- a/locale/en.js
+++ b/locale/en.js
@@ -115,6 +115,15 @@ locale.en = {
multiple: "Moved multiple objects"
}
},
+ rotate: {
+ title: "Rotate",
+ description: "Rotate this object around its centre point.",
+ key: "R",
+ annotation: {
+ line: "Rotated a line.",
+ area: "Rotated an area."
+ }
+ },
reverse: {
title: "Reverse",
description: "Make this line go in the opposite direction.",