From 1603b638f6c9d0ab0b02352f3d4955ff194c2b5d Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 6 Jan 2015 22:22:04 -0500 Subject: [PATCH] Add Copy/Paste behaviors, context copybuffer --- index.html | 2 + js/id/actions/copy_entity.js | 14 +++++-- js/id/behavior/copy.js | 78 ++++++++++++++++++++++++++++++++++++ js/id/behavior/paste.js | 75 ++++++++++++++++++++++++++++++++++ js/id/id.js | 8 ++++ js/id/modes/browse.js | 1 + js/id/modes/select.js | 2 + test/index.html | 2 + 8 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 js/id/behavior/copy.js create mode 100644 js/id/behavior/paste.js 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 @@ +