From dae0d2d55e228a75b5a9183582a79e496a49f504 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sun, 21 Dec 2014 12:51:23 -0500 Subject: [PATCH 1/5] Add entity copy methods --- js/id/core/entity.js | 23 +++++++++++++++++- js/id/core/relation.js | 26 +++++++++++++++++++++ js/id/core/way.js | 25 ++++++++++++++++++++ test/spec/core/entity.js | 26 +++++++++++++++++++++ test/spec/core/relation.js | 48 ++++++++++++++++++++++++++++++++++++++ test/spec/core/way.js | 47 +++++++++++++++++++++++++++++++++++++ 6 files changed, 194 insertions(+), 1 deletion(-) diff --git a/js/id/core/entity.js b/js/id/core/entity.js index 53a7ca17f..5b2412623 100644 --- a/js/id/core/entity.js +++ b/js/id/core/entity.js @@ -44,7 +44,11 @@ iD.Entity.prototype = { var source = sources[i]; for (var prop in source) { if (Object.prototype.hasOwnProperty.call(source, prop)) { - this[prop] = source[prop]; + if (source[prop] === undefined) { + delete this[prop]; + } else { + this[prop] = source[prop]; + } } } } @@ -65,6 +69,23 @@ iD.Entity.prototype = { return this; }, + copy: function(attrs) { + var clone = {}, + omit = {}; + + _.each(['tags', 'loc', 'nodes', 'members'], function(prop) { + if (this.hasOwnProperty(prop)) clone[prop] = _.cloneDeep(this[prop]); + }, this); + + _.each(['id', 'user', 'v', 'version'], function(prop) { + omit[prop] = undefined; + }); + + // Returns an array so that we can support deep copying ways and relations. + // The first array element will contain this.copy, followed by any descendants. + return [ iD.Entity(this, _.extend(clone, (attrs || {}), omit)) ]; + }, + osmId: function() { return iD.Entity.id.toOSM(this.id); }, diff --git a/js/id/core/relation.js b/js/id/core/relation.js index 038785b81..ebafd89c1 100644 --- a/js/id/core/relation.js +++ b/js/id/core/relation.js @@ -20,6 +20,32 @@ _.extend(iD.Relation.prototype, { type: 'relation', members: [], + copy: function(attrs, deep, resolver) { + var fn = iD.Entity.prototype.copy; + + if (deep && resolver && this.isComplete(resolver)) { + var members = [], + descendants = [], + replacements = {}, + i, oldmember, oldid, newid, child; + + for (i = 0; i < this.members.length; i++) { + oldmember = this.members[i]; + oldid = oldmember.id; + newid = replacements[oldid]; + if (!newid) { + child = resolver.entity(oldid).copy({}, true, resolver); + newid = replacements[oldid] = child[0].id; + descendants = child.concat(descendants); + } + members.push({id: newid, type: oldmember.type, role: oldmember.role}); + } + return fn.call(this, _.extend(attrs, {members: members})).concat(descendants); + } else { + return fn.call(this, attrs); + } + }, + extent: function(resolver, memo) { return resolver.transient(this, 'extent', function() { if (memo && memo[this.id]) return iD.geo.Extent(); diff --git a/js/id/core/way.js b/js/id/core/way.js index a1a9f88a8..f31ebfc25 100644 --- a/js/id/core/way.js +++ b/js/id/core/way.js @@ -12,6 +12,31 @@ _.extend(iD.Way.prototype, { type: 'way', nodes: [], + copy: function(attrs, deep, resolver) { + var fn = iD.Entity.prototype.copy; + + if (deep && resolver) { + var nodes = [], + descendants = [], + replacements = {}, + i, oldid, newid, child; + + for (i = 0; i < this.nodes.length; i++) { + oldid = this.nodes[i]; + newid = replacements[oldid]; + if (!newid) { + child = resolver.entity(oldid).copy(); + newid = replacements[oldid] = child[0].id; + descendants = child.concat(descendants); + } + nodes.push(newid); + } + return fn.call(this, _.extend(attrs, {nodes: nodes})).concat(descendants); + } else { + return fn.call(this, attrs); + } + }, + extent: function(resolver) { return resolver.transient(this, 'extent', function() { var extent = iD.geo.Extent(); diff --git a/test/spec/core/entity.js b/test/spec/core/entity.js index 85dd41ec7..2c0efb487 100644 --- a/test/spec/core/entity.js +++ b/test/spec/core/entity.js @@ -36,6 +36,32 @@ describe('iD.Entity', function () { }); }); + describe("#copy", function () { + it("returns a new Entity", function () { + var a = iD.Entity(), + result = a.copy(); + expect(result).to.have.length(1); + expect(result[0]).to.be.an.instanceof(iD.Entity); + expect(a).not.to.equal(result[0]); + }); + + it("resets 'id', 'user', 'v', and 'version' properties", function () { + var a = iD.Entity({id: 'n1234', version: 10, v: 4, user: 'bot-mode'}), + b = a.copy()[0]; + expect(b.isNew()).to.be.ok; + expect(b.version).to.be.undefined; + expect(b.v).to.be.undefined; + expect(b.user).to.be.undefined; + }); + + it("copies tags", function () { + var a = iD.Entity({id: 'n1234', version: 10, user: 'test', tags: {foo: 'foo'}}), + b = a.copy()[0]; + expect(b.tags).not.to.equal(a.tags); + expect(b.tags).to.deep.equal(a.tags); + }); + }); + describe("#update", function () { it("returns a new Entity", function () { var a = iD.Entity(), diff --git a/test/spec/core/relation.js b/test/spec/core/relation.js index 4a617421b..12593100b 100644 --- a/test/spec/core/relation.js +++ b/test/spec/core/relation.js @@ -26,6 +26,54 @@ describe('iD.Relation', function () { expect(iD.Relation({tags: {foo: 'bar'}}).tags).to.eql({foo: 'bar'}); }); + describe("#copy", function () { + it("returns a new Relation", function () { + var r1 = iD.Relation({id: 'r1'}), + result = r1.copy(), + r2 = result[0]; + + expect(result).to.have.length(1); + expect(r2).to.be.an.instanceof(iD.Relation); + expect(r1).not.to.equal(r2); + }); + + it("keeps same members when deep = false", function () { + var a = iD.Node({id: 'a'}), + b = iD.Node({id: 'b'}), + c = iD.Node({id: 'c'}), + w1 = iD.Way({id: 'w1', nodes: ['a','b','c','a']}), + r1 = iD.Relation({id: 'r1', members: [{id: 'w1', role: 'outer'}]}), + graph = iD.Graph([a, b, c, w1, r1]), + result = r1.copy(), + r2 = result[0]; + + expect(result).to.have.length(1); + expect(r1.members).not.to.equal(r2.members); + expect(r1.members).to.deep.equal(r2.members); + }); + + it("makes new members when deep = true", function () { + var a = iD.Node({id: 'a'}), + b = iD.Node({id: 'b'}), + c = iD.Node({id: 'c'}), + w1 = iD.Way({id: 'w1', nodes: ['a','b','c','a']}), + r1 = iD.Relation({id: 'r1', members: [{id: 'w1', role: 'outer'}]}), + graph = iD.Graph([a, b, c, w1, r1]), + result = r1.copy({}, true, graph), + r2 = result[0]; + + expect(result).to.have.length(5); + expect(result[0]).to.be.an.instanceof(iD.Relation); + expect(result[1]).to.be.an.instanceof(iD.Way); + expect(result[2]).to.be.an.instanceof(iD.Node); + expect(result[3]).to.be.an.instanceof(iD.Node); + expect(result[4]).to.be.an.instanceof(iD.Node); + + expect(r2.members[0].id).not.to.equal(r1.members[0].id); + expect(r2.members[0].role).to.equal(r1.members[0].role); + }); + }); + describe("#extent", function () { it("returns the minimal extent containing the extents of all members", function () { var a = iD.Node({loc: [0, 0]}), diff --git a/test/spec/core/way.js b/test/spec/core/way.js index 0ff1a56fc..d3bdc57a1 100644 --- a/test/spec/core/way.js +++ b/test/spec/core/way.js @@ -26,6 +26,53 @@ describe('iD.Way', function() { expect(iD.Way({tags: {foo: 'bar'}}).tags).to.eql({foo: 'bar'}); }); + describe("#copy", function () { + it("returns a new Way", function () { + var w1 = iD.Way({id: 'w1'}), + result = w1.copy(), + w2 = result[0]; + + expect(result).to.have.length(1); + expect(w2).to.be.an.instanceof(iD.Way); + expect(w1).not.to.equal(w2); + }); + + it("keeps same nodes when deep = false", function () { + var a = iD.Node({id: 'a'}), + b = iD.Node({id: 'b'}), + c = iD.Node({id: 'c'}), + w1 = iD.Entity({id: 'w1', nodes: ['a','b','c','a']}), + graph = iD.Graph([a, b, c, w1]), + result = w1.copy(), + w2 = result[0]; + + expect(result).to.have.length(1); + expect(w1.nodes).not.to.equal(w2.nodes); + expect(w1.nodes).to.deep.equal(w2.nodes); + }); + + it("makes new nodes when deep = true", function () { + var a = iD.Node({id: 'a'}), + b = iD.Node({id: 'b'}), + c = iD.Node({id: 'c'}), + w1 = iD.Entity({id: 'w1', nodes: ['a','b','c','a']}), + graph = iD.Graph([a, b, c, w1]), + result = w1.copy({}, true, graph), + w2 = result[0]; + + expect(result).to.have.length(4); + expect(result[0]).to.be.an.instanceof(iD.Way); + expect(result[1]).to.be.an.instanceof(iD.Node); + expect(result[2]).to.be.an.instanceof(iD.Node); + expect(result[3]).to.be.an.instanceof(iD.Node); + + expect(w2.nodes[0]).not.to.equal(w1.nodes[0]); + expect(w2.nodes[1]).not.to.equal(w1.nodes[1]); + expect(w2.nodes[2]).not.to.equal(w1.nodes[2]); + expect(w2.nodes[3]).to.equal(w2.nodes[0]); + }); + }); + describe("#first", function () { it("returns the first node", function () { expect(iD.Way({nodes: ['a', 'b', 'c']}).first()).to.equal('a'); From 4b6abf7a429fc12b24e782e7359f960f88a012d5 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Mon, 22 Dec 2014 23:24:24 -0500 Subject: [PATCH 2/5] Added iD.actions.CopyEntity --- index.html | 1 + js/id/actions/copy_entity.js | 11 +++++ test/index.html | 2 + test/index_packaged.html | 1 + test/spec/actions/copy_entity.js | 76 ++++++++++++++++++++++++++++++++ 5 files changed, 91 insertions(+) create mode 100644 js/id/actions/copy_entity.js create mode 100644 test/spec/actions/copy_entity.js diff --git a/index.html b/index.html index f3129da2d..e1a84a50f 100644 --- a/index.html +++ b/index.html @@ -147,6 +147,7 @@ + diff --git a/js/id/actions/copy_entity.js b/js/id/actions/copy_entity.js new file mode 100644 index 000000000..3db8678b4 --- /dev/null +++ b/js/id/actions/copy_entity.js @@ -0,0 +1,11 @@ +iD.actions.CopyEntity = function(entity, deep) { + return function(graph) { + var newEntities = entity.copy({}, deep, graph); + + for (var i = 0, imax = newEntities.length; i !== imax; i++) { + graph = graph.replace(newEntities[i]); + } + + return graph; + }; +}; diff --git a/test/index.html b/test/index.html index f5cc7da2a..a5039eedb 100644 --- a/test/index.html +++ b/test/index.html @@ -126,6 +126,7 @@ + @@ -225,6 +226,7 @@ + diff --git a/test/index_packaged.html b/test/index_packaged.html index 9df77e3cc..324ed0b69 100644 --- a/test/index_packaged.html +++ b/test/index_packaged.html @@ -35,6 +35,7 @@ + diff --git a/test/spec/actions/copy_entity.js b/test/spec/actions/copy_entity.js new file mode 100644 index 000000000..14f2de0fb --- /dev/null +++ b/test/spec/actions/copy_entity.js @@ -0,0 +1,76 @@ +describe("iD.actions.CopyEntity", function () { + it("copies a Node and adds it to the graph", function () { + var a = iD.Node({id: 'a'}), + base = iD.Graph([a]), + head = iD.actions.CopyEntity(a)(base), + diff = iD.Difference(base, head), + created = diff.created(); + + expect(head.hasEntity('a')).to.be.ok; + expect(created).to.have.length(1); + expect(created[0]).to.be.an.instanceof(iD.Node); + }); + + it("shallow copies a Way and adds it to the graph", function () { + var a = iD.Node({id: 'a'}), + b = iD.Node({id: 'b'}), + w = iD.Way({id: 'w', nodes: ['a', 'b']}), + base = iD.Graph([a, b, w]), + head = iD.actions.CopyEntity(w)(base), + diff = iD.Difference(base, head), + created = diff.created(); + + expect(head.hasEntity('w')).to.be.ok; + expect(created).to.have.length(1); + expect(created[0]).to.be.an.instanceof(iD.Way); + }); + + it("deep copies a Way and child Nodes and adds them to the graph", function () { + var a = iD.Node({id: 'a'}), + b = iD.Node({id: 'b'}), + w = iD.Way({id: 'w', nodes: ['a', 'b']}), + base = iD.Graph([a, b, w]), + head = iD.actions.CopyEntity(w, true)(base), + diff = iD.Difference(base, head), + created = diff.created(); + + expect(head.hasEntity('w')).to.be.ok; + expect(created).to.have.length(3); + expect(created[0]).to.be.an.instanceof(iD.Way); + expect(created[1]).to.be.an.instanceof(iD.Node); + expect(created[2]).to.be.an.instanceof(iD.Node); + }); + + it("shallow copies a Relation and adds it to the graph", function () { + var a = iD.Node({id: 'a'}), + b = iD.Node({id: 'b'}), + w = iD.Way({id: 'w', nodes: ['a', 'b']}), + r = iD.Relation({id: 'r', members: [{id: 'w'}]}), + base = iD.Graph([a, b, w, r]), + head = iD.actions.CopyEntity(r)(base), + diff = iD.Difference(base, head), + created = diff.created(); + + expect(head.hasEntity('r')).to.be.ok; + expect(created).to.have.length(1); + expect(created[0]).to.be.an.instanceof(iD.Relation); + }); + + it("deep copies a Relation, member Ways, and child Nodes and adds them to the graph", function () { + var a = iD.Node({id: 'a'}), + b = iD.Node({id: 'b'}), + w = iD.Way({id: 'w', nodes: ['a', 'b']}), + r = iD.Relation({id: 'r', members: [{id: 'w'}]}), + base = iD.Graph([a, b, w, r]), + head = iD.actions.CopyEntity(r, true)(base), + diff = iD.Difference(base, head), + created = diff.created(); + + expect(head.hasEntity('r')).to.be.ok; + expect(created).to.have.length(4); + expect(created[0]).to.be.an.instanceof(iD.Relation); + expect(created[1]).to.be.an.instanceof(iD.Way); + expect(created[2]).to.be.an.instanceof(iD.Node); + expect(created[3]).to.be.an.instanceof(iD.Node); + }); +}); From f6d144c151f071fba21707337c21b4d31b8cc9e6 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Thu, 1 Jan 2015 22:49:44 -0500 Subject: [PATCH 3/5] Improvements, and simplify code * don't worry about deep copying, because immutability * don't need `attrs` parameter which is usually empty * don't worry about resetting `v` entity version --- js/id/actions/copy_entity.js | 2 +- js/id/core/entity.js | 15 ++---------- js/id/core/relation.js | 45 ++++++++++++++++++------------------ js/id/core/way.js | 43 +++++++++++++++++----------------- test/spec/core/entity.js | 6 ++--- test/spec/core/relation.js | 3 +-- test/spec/core/way.js | 3 +-- 7 files changed, 52 insertions(+), 65 deletions(-) diff --git a/js/id/actions/copy_entity.js b/js/id/actions/copy_entity.js index 3db8678b4..63b9eeffe 100644 --- a/js/id/actions/copy_entity.js +++ b/js/id/actions/copy_entity.js @@ -1,6 +1,6 @@ iD.actions.CopyEntity = function(entity, deep) { return function(graph) { - var newEntities = entity.copy({}, deep, graph); + var newEntities = entity.copy(deep, graph); for (var i = 0, imax = newEntities.length; i !== imax; i++) { graph = graph.replace(newEntities[i]); diff --git a/js/id/core/entity.js b/js/id/core/entity.js index 5b2412623..f4f52274c 100644 --- a/js/id/core/entity.js +++ b/js/id/core/entity.js @@ -69,21 +69,10 @@ iD.Entity.prototype = { return this; }, - copy: function(attrs) { - var clone = {}, - omit = {}; - - _.each(['tags', 'loc', 'nodes', 'members'], function(prop) { - if (this.hasOwnProperty(prop)) clone[prop] = _.cloneDeep(this[prop]); - }, this); - - _.each(['id', 'user', 'v', 'version'], function(prop) { - omit[prop] = undefined; - }); - + copy: function() { // Returns an array so that we can support deep copying ways and relations. // The first array element will contain this.copy, followed by any descendants. - return [ iD.Entity(this, _.extend(clone, (attrs || {}), omit)) ]; + return [iD.Entity(this, {id: undefined, user: undefined, version: undefined})]; }, osmId: function() { diff --git a/js/id/core/relation.js b/js/id/core/relation.js index ebafd89c1..174904cad 100644 --- a/js/id/core/relation.js +++ b/js/id/core/relation.js @@ -20,30 +20,31 @@ _.extend(iD.Relation.prototype, { type: 'relation', members: [], - copy: function(attrs, deep, resolver) { - var fn = iD.Entity.prototype.copy; + copy: function(deep, resolver) { + var copy = iD.Entity.prototype.copy.call(this); - if (deep && resolver && this.isComplete(resolver)) { - var members = [], - descendants = [], - replacements = {}, - i, oldmember, oldid, newid, child; - - for (i = 0; i < this.members.length; i++) { - oldmember = this.members[i]; - oldid = oldmember.id; - newid = replacements[oldid]; - if (!newid) { - child = resolver.entity(oldid).copy({}, true, resolver); - newid = replacements[oldid] = child[0].id; - descendants = child.concat(descendants); - } - members.push({id: newid, type: oldmember.type, role: oldmember.role}); - } - return fn.call(this, _.extend(attrs, {members: members})).concat(descendants); - } else { - return fn.call(this, attrs); + if (!deep || !resolver || !this.isComplete(resolver)) { + return copy; } + + var members = [], + replacements = {}, + i, oldmember, oldid, newid, children; + + for (i = 0; i < this.members.length; i++) { + oldmember = this.members[i]; + oldid = oldmember.id; + newid = replacements[oldid]; + if (!newid) { + children = resolver.entity(oldid).copy(true, resolver); + newid = replacements[oldid] = children[0].id; + copy = copy.concat(children); + } + members.push({id: newid, type: oldmember.type, role: oldmember.role}); + } + + copy[0] = copy[0].update({members: members}); + return copy; }, extent: function(resolver, memo) { diff --git a/js/id/core/way.js b/js/id/core/way.js index f31ebfc25..7fad36eba 100644 --- a/js/id/core/way.js +++ b/js/id/core/way.js @@ -12,29 +12,30 @@ _.extend(iD.Way.prototype, { type: 'way', nodes: [], - copy: function(attrs, deep, resolver) { - var fn = iD.Entity.prototype.copy; + copy: function(deep, resolver) { + var copy = iD.Entity.prototype.copy.call(this); - if (deep && resolver) { - var nodes = [], - descendants = [], - replacements = {}, - i, oldid, newid, child; - - for (i = 0; i < this.nodes.length; i++) { - oldid = this.nodes[i]; - newid = replacements[oldid]; - if (!newid) { - child = resolver.entity(oldid).copy(); - newid = replacements[oldid] = child[0].id; - descendants = child.concat(descendants); - } - nodes.push(newid); - } - return fn.call(this, _.extend(attrs, {nodes: nodes})).concat(descendants); - } else { - return fn.call(this, attrs); + if (!deep || !resolver) { + return copy; } + + var nodes = [], + replacements = {}, + i, oldid, newid, child; + + for (i = 0; i < this.nodes.length; i++) { + oldid = this.nodes[i]; + newid = replacements[oldid]; + if (!newid) { + child = resolver.entity(oldid).copy(); + newid = replacements[oldid] = child[0].id; + copy = copy.concat(child); + } + nodes.push(newid); + } + + copy[0] = copy[0].update({nodes: nodes}); + return copy; }, extent: function(resolver) { diff --git a/test/spec/core/entity.js b/test/spec/core/entity.js index 2c0efb487..5a6e0aa79 100644 --- a/test/spec/core/entity.js +++ b/test/spec/core/entity.js @@ -45,19 +45,17 @@ describe('iD.Entity', function () { expect(a).not.to.equal(result[0]); }); - it("resets 'id', 'user', 'v', and 'version' properties", function () { - var a = iD.Entity({id: 'n1234', version: 10, v: 4, user: 'bot-mode'}), + it("resets 'id', 'user', and 'version' properties", function () { + var a = iD.Entity({id: 'n1234', version: 10, user: 'bot-mode'}), b = a.copy()[0]; expect(b.isNew()).to.be.ok; expect(b.version).to.be.undefined; - expect(b.v).to.be.undefined; expect(b.user).to.be.undefined; }); it("copies tags", function () { var a = iD.Entity({id: 'n1234', version: 10, user: 'test', tags: {foo: 'foo'}}), b = a.copy()[0]; - expect(b.tags).not.to.equal(a.tags); expect(b.tags).to.deep.equal(a.tags); }); }); diff --git a/test/spec/core/relation.js b/test/spec/core/relation.js index 12593100b..cd13d2dd4 100644 --- a/test/spec/core/relation.js +++ b/test/spec/core/relation.js @@ -48,7 +48,6 @@ describe('iD.Relation', function () { r2 = result[0]; expect(result).to.have.length(1); - expect(r1.members).not.to.equal(r2.members); expect(r1.members).to.deep.equal(r2.members); }); @@ -59,7 +58,7 @@ describe('iD.Relation', function () { w1 = iD.Way({id: 'w1', nodes: ['a','b','c','a']}), r1 = iD.Relation({id: 'r1', members: [{id: 'w1', role: 'outer'}]}), graph = iD.Graph([a, b, c, w1, r1]), - result = r1.copy({}, true, graph), + result = r1.copy(true, graph), r2 = result[0]; expect(result).to.have.length(5); diff --git a/test/spec/core/way.js b/test/spec/core/way.js index d3bdc57a1..274835fb1 100644 --- a/test/spec/core/way.js +++ b/test/spec/core/way.js @@ -47,7 +47,6 @@ describe('iD.Way', function() { w2 = result[0]; expect(result).to.have.length(1); - expect(w1.nodes).not.to.equal(w2.nodes); expect(w1.nodes).to.deep.equal(w2.nodes); }); @@ -57,7 +56,7 @@ describe('iD.Way', function() { c = iD.Node({id: 'c'}), w1 = iD.Entity({id: 'w1', nodes: ['a','b','c','a']}), graph = iD.Graph([a, b, c, w1]), - result = w1.copy({}, true, graph), + result = w1.copy(true, graph), w2 = result[0]; expect(result).to.have.length(4); From 90147b23dbf314f975c2cae56b97b9f8508984ac Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sun, 4 Jan 2015 23:15:47 -0500 Subject: [PATCH 4/5] Make copy handle circular and other degenerate Relations --- js/id/core/relation.js | 9 +++--- test/spec/core/relation.js | 58 ++++++++++++++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 9 deletions(-) diff --git a/js/id/core/relation.js b/js/id/core/relation.js index 174904cad..761db2a65 100644 --- a/js/id/core/relation.js +++ b/js/id/core/relation.js @@ -20,23 +20,24 @@ _.extend(iD.Relation.prototype, { type: 'relation', members: [], - copy: function(deep, resolver) { + copy: function(deep, resolver, replacements) { var copy = iD.Entity.prototype.copy.call(this); - if (!deep || !resolver || !this.isComplete(resolver)) { return copy; } var members = [], - replacements = {}, i, oldmember, oldid, newid, children; + replacements = replacements || {}; + replacements[this.id] = copy[0].id; + for (i = 0; i < this.members.length; i++) { oldmember = this.members[i]; oldid = oldmember.id; newid = replacements[oldid]; if (!newid) { - children = resolver.entity(oldid).copy(true, resolver); + children = resolver.entity(oldid).copy(true, resolver, replacements); newid = replacements[oldid] = children[0].id; copy = copy.concat(children); } diff --git a/test/spec/core/relation.js b/test/spec/core/relation.js index cd13d2dd4..8588f3781 100644 --- a/test/spec/core/relation.js +++ b/test/spec/core/relation.js @@ -45,10 +45,10 @@ describe('iD.Relation', function () { r1 = iD.Relation({id: 'r1', members: [{id: 'w1', role: 'outer'}]}), graph = iD.Graph([a, b, c, w1, r1]), result = r1.copy(), - r2 = result[0]; + r1_copy = result[0]; expect(result).to.have.length(1); - expect(r1.members).to.deep.equal(r2.members); + expect(r1.members).to.deep.equal(r1_copy.members); }); it("makes new members when deep = true", function () { @@ -59,7 +59,7 @@ describe('iD.Relation', function () { r1 = iD.Relation({id: 'r1', members: [{id: 'w1', role: 'outer'}]}), graph = iD.Graph([a, b, c, w1, r1]), result = r1.copy(true, graph), - r2 = result[0]; + r1_copy = result[0]; expect(result).to.have.length(5); expect(result[0]).to.be.an.instanceof(iD.Relation); @@ -68,9 +68,57 @@ describe('iD.Relation', function () { expect(result[3]).to.be.an.instanceof(iD.Node); expect(result[4]).to.be.an.instanceof(iD.Node); - expect(r2.members[0].id).not.to.equal(r1.members[0].id); - expect(r2.members[0].role).to.equal(r1.members[0].role); + expect(r1_copy.members[0].id).not.to.equal(r1.members[0].id); + expect(r1_copy.members[0].role).to.equal(r1.members[0].role); }); + + it("deep copies non-tree relation graphs without duplicating children", function () { + var w = iD.Way({id: 'w'}), + r1 = iD.Relation({id: 'r1', members: [{id: 'r2'}, {id: 'w'}]}), + r2 = iD.Relation({id: 'r2', members: [{id: 'w'}]}), + graph = iD.Graph([w, r1, r2]), + result = r1.copy(true, graph), + r1_copy = result[0], + r2_copy = result[1], + w_copy = result[2]; + + expect(result).to.have.length(3); + expect(r1_copy).to.be.an.instanceof(iD.Relation); + expect(r2_copy).to.be.an.instanceof(iD.Relation); + expect(w_copy).to.be.an.instanceof(iD.Way); + + expect(r1_copy.members[0].id).to.equal(r2_copy.id); + expect(r1_copy.members[1].id).to.equal(r2_copy.members[0].id); + }); + + // it("deep copies cyclical relation graphs without issue", function () { + // var r1 = iD.Relation({id: 'r1', members: [{id: 'r2'}]}), + // r2 = iD.Relation({id: 'r2', members: [{id: 'r1'}]}), + // graph = iD.Graph([r1, r2]), + // result = r1.copy(true, graph), + // r1_copy = result[0], + // r2_copy = result[1]; + + // expect(result).to.have.length(2); + // expect(r1_copy).to.be.an.instanceof(iD.Relation); + // expect(r2_copy).to.be.an.instanceof(iD.Relation); + + // var msg = 'r1_copy = ' + JSON.stringify(r1_copy) + + // 'r2_copy = ' + JSON.stringify(r2_copy); + // expect(r1_copy.members[0].id).to.equal(r2_copy.id, msg); + // expect(r2_copy.members[0].id).to.equal(r1_copy.id, msg); + // }); + + // it("deep copies self-refrencing relations without issue", function () { + // var r1 = iD.Relation({id: 'r1', members: [{id: 'r1'}]}), + // graph = iD.Graph([r1]), + // result = r1.copy(true, graph), + // r1_copy = result[0]; + + // expect(result).to.have.length(1); + // expect(r1_copy).to.be.an.instanceof(iD.Relation); + // expect(r1_copy.members[0].id).to.equal(r1_copy.id); + // }); }); describe("#extent", function () { From 1603b638f6c9d0ab0b02352f3d4955ff194c2b5d Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 6 Jan 2015 22:22:04 -0500 Subject: [PATCH 5/5] 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 @@ +