diff --git a/index.html b/index.html
index e1a84a50f..474ea4563 100644
--- a/index.html
+++ b/index.html
@@ -171,6 +171,7 @@
+
@@ -178,6 +179,7 @@
+
diff --git a/js/id/actions/copy_entity.js b/js/id/actions/copy_entity.js
index 63b9eeffe..af4a9138a 100644
--- a/js/id/actions/copy_entity.js
+++ b/js/id/actions/copy_entity.js
@@ -1,11 +1,19 @@
iD.actions.CopyEntity = function(entity, deep) {
- return function(graph) {
- var newEntities = entity.copy(deep, graph);
+ var newEntities = [];
- for (var i = 0, imax = newEntities.length; i !== imax; i++) {
+ var action = function(graph) {
+ newEntities = entity.copy(deep, graph);
+
+ for (var i = 0; i < newEntities.length; i++) {
graph = graph.replace(newEntities[i]);
}
return graph;
};
+
+ action.newEntities = function() {
+ return newEntities;
+ };
+
+ return action;
};
diff --git a/js/id/behavior/copy.js b/js/id/behavior/copy.js
new file mode 100644
index 000000000..e7ca63bea
--- /dev/null
+++ b/js/id/behavior/copy.js
@@ -0,0 +1,78 @@
+iD.behavior.Copy = function(context) {
+ var keybinding = d3.keybinding('copy');
+
+ function groupEntities(ids, graph) {
+ var entities = ids.map(function (id) { return graph.entity(id); });
+ return _.extend({relation: [], way: [], node: []},
+ _.groupBy(entities, function(entity) { return entity.type; }));
+ }
+
+ function getDescendants(id, graph, descendants) {
+ var entity = graph.entity(id),
+ i, children;
+
+ descendants = descendants || {};
+
+ if (entity.type === 'relation') {
+ children = _.pluck(entity.members, 'id');
+ } else if (entity.type === 'way') {
+ children = entity.nodes;
+ } else {
+ children = [];
+ }
+
+ for (i = 0; i < children.length; i++) {
+ if (!descendants[children[i]]) {
+ descendants[children[i]] = true;
+ descendants = getDescendants(children[i], graph, descendants);
+ }
+ }
+
+ return descendants;
+ }
+
+ function doCopy() {
+ d3.event.preventDefault();
+
+ var graph = context.graph(),
+ selected = groupEntities(context.selectedIDs(), graph),
+ canCopy = [],
+ skip = {},
+ i, entity;
+
+ for (i = 0; i < selected.relation.length; i++) {
+ entity = selected.relation[i];
+ if (!skip[entity.id] && entity.isComplete()) {
+ canCopy.push(entity.id);
+ skip = getDescendants(entity.id, graph, skip);
+ }
+ }
+ for (i = 0; i < selected.way.length; i++) {
+ entity = selected.way[i];
+ if (!skip[entity.id]) {
+ canCopy.push(entity.id);
+ skip = getDescendants(entity.id, graph, skip);
+ }
+ }
+ for (i = 0; i < selected.node.length; i++) {
+ entity = selected.node[i];
+ if (!skip[entity.id]) {
+ canCopy.push(entity.id);
+ }
+ }
+
+ context.copiedIDs(canCopy);
+ }
+
+ function copy() {
+ keybinding.on(iD.ui.cmd('⌘C'), doCopy);
+ d3.select(document).call(keybinding);
+ return copy;
+ }
+
+ copy.off = function() {
+ d3.select(document).call(keybinding.off);
+ };
+
+ return copy;
+};
diff --git a/js/id/behavior/paste.js b/js/id/behavior/paste.js
new file mode 100644
index 000000000..92bf92d31
--- /dev/null
+++ b/js/id/behavior/paste.js
@@ -0,0 +1,75 @@
+iD.behavior.Paste = function(context) {
+ var keybinding = d3.keybinding('paste');
+
+ function omitTag(v, k) {
+ return (
+ k === 'phone' ||
+ k === 'fax' ||
+ k === 'email' ||
+ k === 'website' ||
+ k === 'url' ||
+ k === 'note' ||
+ k === 'description' ||
+ k.indexOf('name') !== -1 ||
+ k.indexOf('wiki') === 0 ||
+ k.indexOf('addr:') === 0 ||
+ k.indexOf('contact:') === 0
+ );
+ }
+
+ function doPaste() {
+ d3.event.preventDefault();
+
+ var mouse = context.mouse(),
+ projection = context.projection,
+ viewport = iD.geo.Extent(projection.clipExtent()).polygon();
+
+ if (!iD.geo.pointInPolygon(mouse, viewport)) return;
+
+ var graph = context.graph(),
+ extent = iD.geo.Extent(),
+ oldIDs = context.copiedIDs(),
+ newIDs = [],
+ i, j;
+
+ for (i = 0; i < oldIDs.length; i++) {
+ var oldEntity = graph.entity(oldIDs[i]),
+ action = iD.actions.CopyEntity(oldEntity, true),
+ newEntities;
+
+ extent._extend(oldEntity.extent(graph));
+ context.perform(action);
+
+ // First element in `newEntities` contains the copied Entity,
+ // Subsequent array elements contain any descendants..
+ newEntities = action.newEntities();
+ newIDs.push(newEntities[0].id);
+
+ for (j = 0; j < newEntities.length; j++) {
+ var newEntity = newEntities[j],
+ tags = _.omit(newEntity.tags, omitTag);
+
+ context.perform(iD.actions.ChangeTags(newEntity.id, tags));
+ }
+ }
+
+ // Put pasted objects where mouse pointer is..
+ var center = projection(extent.center()),
+ delta = [ mouse[0] - center[0], mouse[1] - center[1] ];
+
+ context.perform(iD.actions.Move(newIDs, delta, projection));
+ context.enter(iD.modes.Move(context, newIDs));
+ }
+
+ function paste() {
+ keybinding.on(iD.ui.cmd('⌘V'), doPaste);
+ d3.select(document).call(keybinding);
+ return paste;
+ }
+
+ paste.off = function() {
+ d3.select(document).call(keybinding.off);
+ };
+
+ return paste;
+};
diff --git a/js/id/id.js b/js/id/id.js
index 24c276ed5..d9546502d 100644
--- a/js/id/id.js
+++ b/js/id/id.js
@@ -204,6 +204,14 @@ window.iD = function () {
context.surface().call(behavior.off);
};
+ /* Copy/Paste */
+ var copiedIDs = [];
+ context.copiedIDs = function(_) {
+ if (!arguments.length) return copiedIDs;
+ copiedIDs = _;
+ return context;
+ };
+
/* Projection */
context.projection = iD.geo.RawMercator();
diff --git a/js/id/modes/browse.js b/js/id/modes/browse.js
index 1487fa832..9e33a1eb0 100644
--- a/js/id/modes/browse.js
+++ b/js/id/modes/browse.js
@@ -7,6 +7,7 @@ iD.modes.Browse = function(context) {
}, sidebar;
var behaviors = [
+ iD.behavior.Paste(context),
iD.behavior.Hover(context)
.on('hover', context.ui().sidebar.hover),
iD.behavior.Select(context),
diff --git a/js/id/modes/select.js b/js/id/modes/select.js
index 0b182a1b5..45d1c42c0 100644
--- a/js/id/modes/select.js
+++ b/js/id/modes/select.js
@@ -7,6 +7,8 @@ iD.modes.Select = function(context, selectedIDs) {
var keybinding = d3.keybinding('select'),
timeout = null,
behaviors = [
+ iD.behavior.Copy(context),
+ iD.behavior.Paste(context),
iD.behavior.Hover(context),
iD.behavior.Select(context),
iD.behavior.Lasso(context),
diff --git a/test/index.html b/test/index.html
index a5039eedb..c0dbc2970 100644
--- a/test/index.html
+++ b/test/index.html
@@ -150,6 +150,7 @@
+
@@ -157,6 +158,7 @@
+