From dae0d2d55e228a75b5a9183582a79e496a49f504 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Sun, 21 Dec 2014 12:51:23 -0500 Subject: [PATCH] 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');