From c5d691d79d723545852236f45c3738efb5dc58d9 Mon Sep 17 00:00:00 2001 From: John Firebaugh Date: Mon, 28 Jan 2013 13:35:47 -0500 Subject: [PATCH 01/13] Graph#rebase --- js/id/graph/graph.js | 35 ++++++----- js/id/graph/history.js | 8 ++- test/spec/graph/graph.js | 121 +++++++++++++++++++++++++++++++++------ 3 files changed, 131 insertions(+), 33 deletions(-) diff --git a/js/id/graph/graph.js b/js/id/graph/graph.js index 120fa2fe7..3eea6a59a 100644 --- a/js/id/graph/graph.js +++ b/js/id/graph/graph.js @@ -1,13 +1,15 @@ -iD.Graph = function(entities, mutable) { - if (!(this instanceof iD.Graph)) return new iD.Graph(entities, mutable); +iD.Graph = function(other, mutable) { + if (!(this instanceof iD.Graph)) return new iD.Graph(other, mutable); - if (_.isArray(entities)) { - this.entities = {}; - for (var i = 0; i < entities.length; i++) { - this.entities[entities[i].id] = entities[i]; + if (_.isArray(other)) { + this.entities = Object.create({}); + for (var i = 0; i < other.length; i++) { + this.entities[other[i].id] = other[i]; } + } else if (other instanceof iD.Graph) { + this.rebase(other.base(), other.entities); } else { - this.entities = entities || {}; + this.entities = Object.create(other || {}); } this.transients = {}; @@ -97,10 +99,18 @@ iD.Graph.prototype = { return (this._childNodes[entity.id] = nodes); }, - merge: function(graph) { - return this.update(function () { - _.defaults(this.entities, graph.entities); - }); + base: function() { + return Object.getPrototypeOf ? + Object.getPrototypeOf(this.entities) : + this.entities.__proto__; + }, + + // Unlike other graph methods, rebase mutates in place. This is because it + // is used only during the history operation that merges newly downloaded + // data into each state. To external consumers, it should appear as if the + // graph always contained the newly downloaded data. + rebase: function(base, entities) { + this.entities = _.assign(Object.create(base), entities || this.entities); }, replace: function(entity) { @@ -120,7 +130,7 @@ iD.Graph.prototype = { }, update: function() { - var graph = this.frozen ? iD.Graph(_.clone(this.entities), true) : this; + var graph = this.frozen ? iD.Graph(this, true) : this; for (var i = 0; i < arguments.length; i++) { arguments[i].call(graph, graph); @@ -133,7 +143,6 @@ iD.Graph.prototype = { this.frozen = true; if (iD.debug) { - Object.freeze(this); Object.freeze(this.entities); } diff --git a/js/id/graph/history.js b/js/id/graph/history.js index 29b652554..c9e3e19f5 100644 --- a/js/id/graph/history.js +++ b/js/id/graph/history.js @@ -30,8 +30,12 @@ iD.History = function() { }, merge: function (graph) { - for (var i = 0; i < stack.length; i++) { - stack[i].graph = stack[i].graph.merge(graph); + var base = stack[0].graph.base(); + + _.defaults(base, graph.entities); + + for (var i = 1; i < stack.length; i++) { + stack[i].graph.rebase(base); } }, diff --git a/test/spec/graph/graph.js b/test/spec/graph/graph.js index 25382e7c9..fa83a3a03 100644 --- a/test/spec/graph/graph.js +++ b/test/spec/graph/graph.js @@ -1,25 +1,110 @@ describe('iD.Graph', function() { - it("can be constructed with an entities Object", function () { - var entity = iD.Entity(), - graph = iD.Graph({'n-1': entity}); - expect(graph.entity('n-1')).to.equal(entity); - }); - - it("can be constructed with an entities Array", function () { - var entity = iD.Entity(), - graph = iD.Graph([entity]); - expect(graph.entity(entity.id)).to.equal(entity); - }); - - if (iD.debug) { - it("is frozen", function () { - expect(Object.isFrozen(iD.Graph())).to.be.true; + describe("constructor", function () { + it("accepts an entities Object", function () { + var entity = iD.Entity(), + graph = iD.Graph({'n-1': entity}); + expect(graph.entity('n-1')).to.equal(entity); }); - it("freezes entities", function () { - expect(Object.isFrozen(iD.Graph().entities)).to.be.true; + it("accepts an entities Array", function () { + var entity = iD.Entity(), + graph = iD.Graph([entity]); + expect(graph.entity(entity.id)).to.equal(entity); }); - } + + it("accepts a Graph", function () { + var entity = iD.Entity(), + graph = iD.Graph(iD.Graph([entity])); + expect(graph.entity(entity.id)).to.equal(entity); + }); + + it("copies other's entities", function () { + var entity = iD.Entity(), + base = iD.Graph([entity]), + graph = iD.Graph(base); + expect(graph.entities).not.to.equal(base.entities); + }); + + it("rebases on other's base", function () { + var base = iD.Graph(), + graph = iD.Graph(base); + expect(graph.base()).to.equal(base.base()); + }); + + it("freezes by default", function () { + expect(iD.Graph().frozen).to.be.true; + }); + + it("remains mutable if passed true as second argument", function () { + expect(iD.Graph([], true).frozen).not.to.be.true; + }); + }); + + describe("#freeze", function () { + it("sets the frozen flag", function () { + expect(iD.Graph([], true).freeze().frozen).to.be.true; + }); + + if (iD.debug) { + it("freezes entities", function () { + expect(Object.isFrozen(iD.Graph().entities)).to.be.true; + }); + } + }); + + describe("#rebase", function () { + it("preserves existing entities", function () { + var node = iD.Node({id: 'n'}), + graph = iD.Graph([node]); + graph.rebase({}); + expect(graph.entity('n')).to.equal(node); + }); + + it("includes new entities", function () { + var node = iD.Node({id: 'n'}), + graph = iD.Graph(); + graph.rebase({'n': node}); + expect(graph.entity('n')).to.equal(node); + }); + + it("gives precedence to existing entities", function () { + var a = iD.Node({id: 'n'}), + b = iD.Node({id: 'n'}), + graph = iD.Graph([a]); + graph.rebase({'n': b}); + expect(graph.entity('n')).to.equal(a); + }); + + it("inherits entities from base prototypally", function () { + var graph = iD.Graph(); + graph.rebase({'n': iD.Node()}); + expect(graph.entities).not.to.have.ownProperty('n'); + }); + + xit("updates parentWays", function () { + var n = iD.Node({id: 'n'}), + w1 = iD.Way({id: 'w1', nodes: ['n']}), + w2 = iD.Way({id: 'w2', nodes: ['n']}), + graph = iD.Graph([n, w1]); + + graph.parentWays(n); + graph.rebase({'w2': w2}); + + expect(graph.parentWays(n)).to.eql([w1, w2]); + }); + + xit("updates parentRelations", function () { + var n = iD.Node({id: 'n'}), + r1 = iD.Relation({id: 'r1', members: [{id: 'n'}]}), + r2 = iD.Relation({id: 'r2', members: [{id: 'n'}]}), + graph = iD.Graph([n, r1]); + + graph.parentRelations(n); + graph.rebase({'r2': r2}); + + expect(graph.parentRelations(n)).to.eql([r1, r2]); + }); + }); describe("#remove", function () { it("returns a new graph", function () { From 7d32af52640fc8d5519849eb7c47a120e3ce0dae Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 29 Jan 2013 12:49:00 -0500 Subject: [PATCH 02/13] work towards using prototypes for entities --- js/id/graph/graph.js | 30 ++++++++++++++++++------------ js/id/graph/history.js | 9 ++++++--- js/id/renderer/map.js | 2 +- js/id/util.js | 2 ++ 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/js/id/graph/graph.js b/js/id/graph/graph.js index 3eea6a59a..d35f616a0 100644 --- a/js/id/graph/graph.js +++ b/js/id/graph/graph.js @@ -1,19 +1,25 @@ iD.Graph = function(other, mutable) { if (!(this instanceof iD.Graph)) return new iD.Graph(other, mutable); - if (_.isArray(other)) { - this.entities = Object.create({}); - for (var i = 0; i < other.length; i++) { - this.entities[other[i].id] = other[i]; - } - } else if (other instanceof iD.Graph) { + if (other instanceof iD.Graph) { + var base = other.base(); + this.entities = _.assign(Object.create(base.entities), other.entities); + this._parentWays = _.assign(Object.create(base.parentWays), other._parentWays); this.rebase(other.base(), other.entities); + } else { - this.entities = Object.create(other || {}); + if (_.isArray(other)) { + this.entities = Object.create({}); + for (var i = 0; i < other.length; i++) { + this.entities[other[i].id] = other[i]; + } + } else { + this.entities = Object.create(other || {}); + } + this._parentWays = Object.create({}); } this.transients = {}; - this._parentWays = {}; this._parentRels = {}; this._childNodes = {}; @@ -100,9 +106,10 @@ iD.Graph.prototype = { }, base: function() { - return Object.getPrototypeOf ? - Object.getPrototypeOf(this.entities) : - this.entities.__proto__; + return { + 'entities': iD.util.getPrototypeOf(this.entities), + 'parentWays': iD.util.getPrototypeOf(this._parentWays) + }; }, // Unlike other graph methods, rebase mutates in place. This is because it @@ -110,7 +117,6 @@ iD.Graph.prototype = { // data into each state. To external consumers, it should appear as if the // graph always contained the newly downloaded data. rebase: function(base, entities) { - this.entities = _.assign(Object.create(base), entities || this.entities); }, replace: function(entity) { diff --git a/js/id/graph/history.js b/js/id/graph/history.js index c9e3e19f5..e5bfd353f 100644 --- a/js/id/graph/history.js +++ b/js/id/graph/history.js @@ -30,11 +30,14 @@ iD.History = function() { }, merge: function (graph) { - var base = stack[0].graph.base(); + var base = stack[0].graph.base(), + mergee = graph.base(); - _.defaults(base, graph.entities); + for (var i in base) { + _.defaults(base[i], mergee[i]); + } - for (var i = 1; i < stack.length; i++) { + for (i = 1; i < stack.length; i++) { stack[i].graph.rebase(base); } }, diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index bc4ca8e03..e27fdc5b9 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -123,7 +123,7 @@ iD.Map = function() { function connectionLoad(err, result) { history.merge(result); - redraw(Object.keys(result.entities)); + redraw(Object.keys(iD.util.getPrototypeOf(result.entities))); } function zoomPan() { diff --git a/js/id/util.js b/js/id/util.js index da31a162b..645d15363 100644 --- a/js/id/util.js +++ b/js/id/util.js @@ -74,3 +74,5 @@ iD.util.getStyle = function(selector) { } } }; + +iD.util.getPrototypeOf = Object.getPrototypeOf || function(obj) { return obj.__proto__; }; From 6c1e4e5b4d4f019b2643f358a2152ab71f67d8fe Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Tue, 29 Jan 2013 13:32:39 -0500 Subject: [PATCH 03/13] Working more-efficient parentWays --- js/id/graph/graph.js | 57 ++++++++++++++++++++++++++++++++---------- js/id/graph/history.js | 9 ++++--- js/id/renderer/map.js | 2 +- 3 files changed, 50 insertions(+), 18 deletions(-) diff --git a/js/id/graph/graph.js b/js/id/graph/graph.js index d35f616a0..2705fd029 100644 --- a/js/id/graph/graph.js +++ b/js/id/graph/graph.js @@ -8,15 +8,12 @@ iD.Graph = function(other, mutable) { this.rebase(other.base(), other.entities); } else { - if (_.isArray(other)) { - this.entities = Object.create({}); - for (var i = 0; i < other.length; i++) { - this.entities[other[i].id] = other[i]; - } - } else { - this.entities = Object.create(other || {}); - } + this.entities = other || Object.create({}); this._parentWays = Object.create({}); + + for (var i in this.entities) { + this._updateCalculated(undefined, this.entities[i]); + } } this.transients = {}; @@ -119,19 +116,53 @@ iD.Graph.prototype = { rebase: function(base, entities) { }, + _updateCalculated: function(oldentity, entity) { + + var type = entity && entity.type || oldentity && oldentity.type, + removed, added, parentWays, i; + + + if (type === 'way') { + + // Update parentWays + if (oldentity && entity) { + removed = _.difference(oldentity.nodes, entity.nodes); + added = _.difference(entity.nodes, oldentity.nodes); + } else if (oldentity) { + removed = oldentity.nodes; + added = []; + } else if (entity) { + removed = []; + added = entity.nodes; + } + for (i = 0; i < removed.length; i++) { + this._parentWays[removed[i]] = _.without(this._parentWays[removed[i]], oldentity.id); + } + for (i = 0; i < added.length; i++) { + parentWays = _.without(this._parentWays[added[i]], entity.id); + parentWays.push(entity.id); + this._parentWays[added[i]] = parentWays; + } + } else if (type === 'node') { + + } else if (type === 'relation') { + + // TODO: iterate over members + + } + }, + replace: function(entity) { return this.update(function () { + this._updateCalculated(this.entities[entity.id], entity); this.entities[entity.id] = entity; }); }, remove: function(entity) { return this.update(function () { - if (entity.created()) { - delete this.entities[entity.id]; - } else { - this.entities[entity.id] = undefined; - } + this._updateCalculated(entity, undefined); + this.entities[entity.id] = undefined; }); }, diff --git a/js/id/graph/history.js b/js/id/graph/history.js index e5bfd353f..95e499ef2 100644 --- a/js/id/graph/history.js +++ b/js/id/graph/history.js @@ -30,12 +30,13 @@ iD.History = function() { }, merge: function (graph) { - var base = stack[0].graph.base(), - mergee = graph.base(); + var base = stack[0].graph.base(); - for (var i in base) { - _.defaults(base[i], mergee[i]); + _.defaults(base.entities, graph.entities); + for (var i in graph.entities) { + base.parentWays[i] = _.unique((base.parentWays[id] || []).concat(graph._parentWays[i] || [])); } + _.defaults(base.parentWays, graph._parentWays); for (i = 1; i < stack.length; i++) { stack[i].graph.rebase(base); diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index e27fdc5b9..bc4ca8e03 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -123,7 +123,7 @@ iD.Map = function() { function connectionLoad(err, result) { history.merge(result); - redraw(Object.keys(iD.util.getPrototypeOf(result.entities))); + redraw(Object.keys(result.entities)); } function zoomPan() { From ba68a238fe2cd70e7c08382c9c08f6afca8a8542 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 30 Jan 2013 12:26:40 -0500 Subject: [PATCH 04/13] Working rebase --- js/id/graph/graph.js | 90 ++++++++++++++++++++++------------------ js/id/graph/history.js | 12 +----- test/spec/graph/graph.js | 35 ++++++++++------ 3 files changed, 74 insertions(+), 63 deletions(-) diff --git a/js/id/graph/graph.js b/js/id/graph/graph.js index 2705fd029..76842cbf5 100644 --- a/js/id/graph/graph.js +++ b/js/id/graph/graph.js @@ -5,15 +5,19 @@ iD.Graph = function(other, mutable) { var base = other.base(); this.entities = _.assign(Object.create(base.entities), other.entities); this._parentWays = _.assign(Object.create(base.parentWays), other._parentWays); - this.rebase(other.base(), other.entities); + this.inherited = true; } else { - this.entities = other || Object.create({}); - this._parentWays = Object.create({}); - - for (var i in this.entities) { - this._updateCalculated(undefined, this.entities[i]); + if (_.isArray(other)) { + var entities = {}; + for (var i = 0; i < other.length; i++) { + entities[other[i].id] = other[i]; + } + other = entities; } + this.entities = Object.create({}); + this._parentWays = Object.create({}); + this.rebase(other || {}); } this.transients = {}; @@ -43,25 +47,7 @@ iD.Graph.prototype = { }, parentWays: function(entity) { - var ent, id, parents; - - if (!this._parentWays.calculated) { - for (var i in this.entities) { - ent = this.entities[i]; - if (ent && ent.type === 'way') { - for (var j = 0; j < ent.nodes.length; j++) { - id = ent.nodes[j]; - parents = this._parentWays[id] = this._parentWays[id] || []; - if (parents.indexOf(ent) < 0) { - parents.push(ent); - } - } - } - } - this._parentWays.calculated = true; - } - - return this._parentWays[entity.id] || []; + return _.map(this._parentWays[entity.id], _.bind(this.entity, this)); }, isPoi: function(entity) { @@ -113,13 +99,31 @@ iD.Graph.prototype = { // is used only during the history operation that merges newly downloaded // data into each state. To external consumers, it should appear as if the // graph always contained the newly downloaded data. - rebase: function(base, entities) { + rebase: function(entities) { + var i; + // Merging of data only needed if graph is the base graph + if (!this.inherited) { + _.defaults(this.base().entities, entities); + for (i in entities) { + this._updateCalculated(undefined, entities[i], this.base().parentWays); + } + } + + var keys = Object.keys(this._parentWays); + for (i = 0; i < keys.length; i++) { + this._parentWays[keys[i]] = _.unique((this._parentWays[keys[i]] || []) + .concat(this.base().parentWays[keys[i]] || [])); + } }, - _updateCalculated: function(oldentity, entity) { + // Updates calculated properties (parentWays, parentRels) for the specified change + _updateCalculated: function(oldentity, entity, parentWays, parentRels) { + + parentWays = parentWays || this._parentWays; + parentRels = parentRels || this._parentRels; var type = entity && entity.type || oldentity && oldentity.type, - removed, added, parentWays, i; + removed, added, ways, i; if (type === 'way') { @@ -136,12 +140,12 @@ iD.Graph.prototype = { added = entity.nodes; } for (i = 0; i < removed.length; i++) { - this._parentWays[removed[i]] = _.without(this._parentWays[removed[i]], oldentity.id); + parentWays[removed[i]] = _.without(parentWays[removed[i]], oldentity.id); } for (i = 0; i < added.length; i++) { - parentWays = _.without(this._parentWays[added[i]], entity.id); - parentWays.push(entity.id); - this._parentWays[added[i]] = parentWays; + ways = _.without(parentWays[added[i]], entity.id); + ways.push(entity.id); + parentWays[added[i]] = ways; } } else if (type === 'node') { @@ -199,9 +203,12 @@ iD.Graph.prototype = { }, difference: function (graph) { - var result = [], entity, oldentity, id; + var result = [], + keys = Object.keys(this.entities), + entity, oldentity, id, i; - for (id in this.entities) { + for (i = 0; i < keys.length; i++) { + id = keys[i]; entity = this.entities[id]; oldentity = graph.entities[id]; if (entity !== oldentity) { @@ -223,7 +230,8 @@ iD.Graph.prototype = { } } - for (id in graph.entities) { + keys = Object.keys(graph.entities); + for (i = 0; i < keys.length; i++) { entity = graph.entities[id]; if (entity && !this.entities.hasOwnProperty(id)) { result.push(id); @@ -235,25 +243,25 @@ iD.Graph.prototype = { }, modified: function() { - var result = []; + var result = [], base = this.base().entities; _.each(this.entities, function(entity, id) { - if (entity && entity.modified()) result.push(id); + if (entity && base[id]) result.push(id); }); return result; }, created: function() { - var result = []; + var result = [], base = this.base().entities; _.each(this.entities, function(entity, id) { - if (entity && entity.created()) result.push(id); + if (entity && !base[id]) result.push(id); }); return result; }, deleted: function() { - var result = []; + var result = [], base = this.base().entities; _.each(this.entities, function(entity, id) { - if (!entity) result.push(id); + if (!entity && base[id]) result.push(id); }); return result; } diff --git a/js/id/graph/history.js b/js/id/graph/history.js index 95e499ef2..3630bb915 100644 --- a/js/id/graph/history.js +++ b/js/id/graph/history.js @@ -30,16 +30,8 @@ iD.History = function() { }, merge: function (graph) { - var base = stack[0].graph.base(); - - _.defaults(base.entities, graph.entities); - for (var i in graph.entities) { - base.parentWays[i] = _.unique((base.parentWays[id] || []).concat(graph._parentWays[i] || [])); - } - _.defaults(base.parentWays, graph._parentWays); - - for (i = 1; i < stack.length; i++) { - stack[i].graph.rebase(base); + for (var i = 0; i < stack.length; i++) { + stack[i].graph.rebase(graph.base().entities); } }, diff --git a/test/spec/graph/graph.js b/test/spec/graph/graph.js index fa83a3a03..61b620ef3 100644 --- a/test/spec/graph/graph.js +++ b/test/spec/graph/graph.js @@ -28,7 +28,7 @@ describe('iD.Graph', function() { it("rebases on other's base", function () { var base = iD.Graph(), graph = iD.Graph(base); - expect(graph.base()).to.equal(base.base()); + expect(graph.base().entities).to.equal(base.base().entities); }); it("freezes by default", function () { @@ -81,16 +81,27 @@ describe('iD.Graph', function() { expect(graph.entities).not.to.have.ownProperty('n'); }); - xit("updates parentWays", function () { + it("updates parentWays", function () { var n = iD.Node({id: 'n'}), w1 = iD.Way({id: 'w1', nodes: ['n']}), w2 = iD.Way({id: 'w2', nodes: ['n']}), + w3 = iD.Way({id: 'w3', nodes: ['n']}), graph = iD.Graph([n, w1]); - graph.parentWays(n); - graph.rebase({'w2': w2}); + var graph2 = graph.replace(w2); + graph.rebase({ 'w3': w3 }); + graph2.rebase({ 'w3': w3 }); - expect(graph.parentWays(n)).to.eql([w1, w2]); + expect(graph2.parentWays(n)).to.eql([w1, w2, w3]); + }); + + it("only sets non-base parentWays when necessary", function () { + var n = iD.Node({id: 'n'}), + w1 = iD.Way({id: 'w1', nodes: ['n']}), + graph = iD.Graph([n, w1]); + graph.rebase({ 'w1': w1 }); + expect(graph._parentWays.hasOwnProperty('n')).to.be.false; + expect(graph.parentWays(n)).to.eql([w1]); }); xit("updates parentRelations", function () { @@ -244,18 +255,18 @@ describe('iD.Graph', function() { describe("#modified", function () { it("returns an Array of ids of modified entities", function () { - var node1 = iD.Node({id: 'n1', _updated: true}), - node2 = iD.Node({id: 'n2'}), - graph = iD.Graph([node1, node2]); - expect(graph.modified()).to.eql([node1.id]); + var node = iD.Node({id: 'n1'}), + node_ = iD.Node({id: 'n1'}), + graph = iD.Graph([node]).replace(node_); + expect(graph.modified()).to.eql([node.id]); }); }); describe("#created", function () { it("returns an Array of ids of created entities", function () { - var node1 = iD.Node({id: 'n-1', _updated: true}), + var node1 = iD.Node({id: 'n-1'}), node2 = iD.Node({id: 'n2'}), - graph = iD.Graph([node1, node2]); + graph = iD.Graph([node2]).replace(node1); expect(graph.created()).to.eql([node1.id]); }); }); @@ -270,7 +281,7 @@ describe('iD.Graph', function() { it("doesn't include created entities that were subsequently deleted", function () { var node = iD.Node(), - graph = iD.Graph([node]).remove(node); + graph = iD.Graph().replace(node).remove(node); expect(graph.deleted()).to.eql([]); }); }); From e08364d492685224ef3abeb5db60851be053bc91 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 30 Jan 2013 13:18:44 -0500 Subject: [PATCH 05/13] working parentRels --- js/id/graph/graph.js | 57 ++++++++++++++++++++++++---------------- test/spec/graph/graph.js | 40 +++++++++++++++++++--------- 2 files changed, 61 insertions(+), 36 deletions(-) diff --git a/js/id/graph/graph.js b/js/id/graph/graph.js index 76842cbf5..055ab8df8 100644 --- a/js/id/graph/graph.js +++ b/js/id/graph/graph.js @@ -5,6 +5,7 @@ iD.Graph = function(other, mutable) { var base = other.base(); this.entities = _.assign(Object.create(base.entities), other.entities); this._parentWays = _.assign(Object.create(base.parentWays), other._parentWays); + this._parentRels = _.assign(Object.create(base.parentRels), other._parentRels); this.inherited = true; } else { @@ -17,11 +18,11 @@ iD.Graph = function(other, mutable) { } this.entities = Object.create({}); this._parentWays = Object.create({}); + this._parentRels = Object.create({}); this.rebase(other || {}); } this.transients = {}; - this._parentRels = {}; this._childNodes = {}; if (!mutable) { @@ -55,23 +56,9 @@ iD.Graph.prototype = { }, parentRelations: function(entity) { + return _.map(this._parentRels[entity.id], _.bind(this.entity, this)); var ent, id, parents; - if (!this._parentRels.calculated) { - for (var i in this.entities) { - ent = this.entities[i]; - if (ent && ent.type === 'relation') { - for (var j = 0; j < ent.members.length; j++) { - id = ent.members[j].id; - parents = this._parentRels[id] = this._parentRels[id] || []; - if (parents.indexOf(ent) < 0) { - parents.push(ent); - } - } - } - } - this._parentRels.calculated = true; - } return this._parentRels[entity.id] || []; }, @@ -91,7 +78,8 @@ iD.Graph.prototype = { base: function() { return { 'entities': iD.util.getPrototypeOf(this.entities), - 'parentWays': iD.util.getPrototypeOf(this._parentWays) + 'parentWays': iD.util.getPrototypeOf(this._parentWays), + 'parentRels': iD.util.getPrototypeOf(this._parentRels) }; }, @@ -100,20 +88,26 @@ iD.Graph.prototype = { // data into each state. To external consumers, it should appear as if the // graph always contained the newly downloaded data. rebase: function(entities) { - var i; + var i, keys; // Merging of data only needed if graph is the base graph if (!this.inherited) { _.defaults(this.base().entities, entities); for (i in entities) { - this._updateCalculated(undefined, entities[i], this.base().parentWays); + this._updateCalculated(undefined, entities[i], this.base().parentWays, this.base().parentRels); } } - var keys = Object.keys(this._parentWays); + keys = Object.keys(this._parentWays); for (i = 0; i < keys.length; i++) { this._parentWays[keys[i]] = _.unique((this._parentWays[keys[i]] || []) .concat(this.base().parentWays[keys[i]] || [])); } + + keys = Object.keys(this._parentRels); + for (i = 0; i < keys.length; i++) { + this._parentRels[keys[i]] = _.unique((this._parentRels[keys[i]] || []) + .concat(this.base().parentRels[keys[i]] || [])); + } }, // Updates calculated properties (parentWays, parentRels) for the specified change @@ -123,7 +117,7 @@ iD.Graph.prototype = { parentRels = parentRels || this._parentRels; var type = entity && entity.type || oldentity && oldentity.type, - removed, added, ways, i; + removed, added, ways, rels, i; if (type === 'way') { @@ -151,8 +145,25 @@ iD.Graph.prototype = { } else if (type === 'relation') { - // TODO: iterate over members - + // Update parentRels + if (oldentity && entity) { + removed = _.difference(oldentity.members, entity.members); + added = _.difference(entity.members, oldentity); + } else if (oldentity) { + removed = oldentity.members; + added = []; + } else if (entity) { + removed = []; + added = entity.members; + } + for (i = 0; i < removed.length; i++) { + parentRels[removed[i].id] = _.without(parentRels[removed[i].id], oldentity.id); + } + for (i = 0; i < added.length; i++) { + rels = _.without(parentRels[added[i].id], entity.id); + rels.push(entity.id); + parentRels[added[i].id] = rels; + } } }, diff --git a/test/spec/graph/graph.js b/test/spec/graph/graph.js index 61b620ef3..9d918ae04 100644 --- a/test/spec/graph/graph.js +++ b/test/spec/graph/graph.js @@ -85,35 +85,49 @@ describe('iD.Graph', function() { var n = iD.Node({id: 'n'}), w1 = iD.Way({id: 'w1', nodes: ['n']}), w2 = iD.Way({id: 'w2', nodes: ['n']}), - w3 = iD.Way({id: 'w3', nodes: ['n']}), graph = iD.Graph([n, w1]); - var graph2 = graph.replace(w2); + graph.rebase({ 'w2': w2 }); + expect(graph.parentWays(n)).to.eql([w1, w2]); + expect(graph._parentWays.hasOwnProperty('n')).to.be.false; + }); + + it("updates parentWays for nodes with modified parentWays", function () { + var n = iD.Node({id: 'n'}), + w1 = iD.Way({id: 'w1', nodes: ['n']}), + w2 = iD.Way({id: 'w2', nodes: ['n']}), + w3 = iD.Way({id: 'w3', nodes: ['n']}), + graph = iD.Graph([n, w1]), + graph2 = graph.replace(w2); graph.rebase({ 'w3': w3 }); graph2.rebase({ 'w3': w3 }); expect(graph2.parentWays(n)).to.eql([w1, w2, w3]); }); - it("only sets non-base parentWays when necessary", function () { - var n = iD.Node({id: 'n'}), - w1 = iD.Way({id: 'w1', nodes: ['n']}), - graph = iD.Graph([n, w1]); - graph.rebase({ 'w1': w1 }); - expect(graph._parentWays.hasOwnProperty('n')).to.be.false; - expect(graph.parentWays(n)).to.eql([w1]); - }); - - xit("updates parentRelations", function () { + it("updates parentRelations", function () { var n = iD.Node({id: 'n'}), r1 = iD.Relation({id: 'r1', members: [{id: 'n'}]}), r2 = iD.Relation({id: 'r2', members: [{id: 'n'}]}), graph = iD.Graph([n, r1]); - graph.parentRelations(n); graph.rebase({'r2': r2}); expect(graph.parentRelations(n)).to.eql([r1, r2]); + expect(graph._parentRels.hasOwnProperty('n')).to.be.false; + }); + + it("updates parentWays for nodes with modified parentWays", function () { + var n = iD.Node({id: 'n'}), + r1 = iD.Relation({id: 'r1', members: [{id: 'n'}]}), + r2 = iD.Relation({id: 'r2', members: [{id: 'n'}]}), + r3 = iD.Relation({id: 'r3', members: [{id: 'n'}]}), + graph = iD.Graph([n, r1]), + graph2 = graph.replace(r2); + + graph.rebase({'r3': r3}); + graph2.rebase({'r3': r3}); + expect(graph2.parentRelations(n)).to.eql([r1, r2, r3]); }); }); From 5a35b6bfa4c84509ffa2d41ddb7a641c90fafe87 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 30 Jan 2013 14:11:54 -0500 Subject: [PATCH 06/13] Fix difference --- js/id/graph/graph.js | 1 + 1 file changed, 1 insertion(+) diff --git a/js/id/graph/graph.js b/js/id/graph/graph.js index 055ab8df8..f2525daf5 100644 --- a/js/id/graph/graph.js +++ b/js/id/graph/graph.js @@ -243,6 +243,7 @@ iD.Graph.prototype = { keys = Object.keys(graph.entities); for (i = 0; i < keys.length; i++) { + id = keys[i]; entity = graph.entities[id]; if (entity && !this.entities.hasOwnProperty(id)) { result.push(id); From 1b5a1b8268675f53fce8b9138550a063e3d6174e Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 30 Jan 2013 14:20:39 -0500 Subject: [PATCH 07/13] Merge takes an entities object, not graph --- js/id/connection.js | 2 +- js/id/graph/history.js | 4 ++-- js/id/renderer/map.js | 2 +- test/spec/connection.js | 8 ++++---- test/spec/graph/history.js | 12 +++++------- 5 files changed, 13 insertions(+), 15 deletions(-) diff --git a/js/id/connection.js b/js/id/connection.js index 53c96bd96..5eca647cd 100644 --- a/js/id/connection.js +++ b/js/id/connection.js @@ -120,7 +120,7 @@ iD.Connection = function() { } } - return iD.Graph(entities); + return entities; } function authenticated() { diff --git a/js/id/graph/history.js b/js/id/graph/history.js index 3630bb915..42e1b1604 100644 --- a/js/id/graph/history.js +++ b/js/id/graph/history.js @@ -29,9 +29,9 @@ iD.History = function() { return stack[index].graph; }, - merge: function (graph) { + merge: function (entities) { for (var i = 0; i < stack.length; i++) { - stack[i].graph.rebase(graph.base().entities); + stack[i].graph.rebase(entities); } }, diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index bc4ca8e03..139abb4c7 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -123,7 +123,7 @@ iD.Map = function() { function connectionLoad(err, result) { history.merge(result); - redraw(Object.keys(result.entities)); + redraw(Object.keys(result)); } function zoomPan() { diff --git a/test/spec/connection.js b/test/spec/connection.js index 2f5d93808..652c1419b 100644 --- a/test/spec/connection.js +++ b/test/spec/connection.js @@ -26,24 +26,24 @@ describe('iD.Connection', function () { c.loadFromURL('data/node.xml', done); }); - it('returns a graph', function (done) { + it('returns an object', function (done) { c.loadFromURL('data/node.xml', function (err, graph) { expect(err).to.not.be.ok; - expect(graph).to.be.instanceOf(iD.Graph); + expect(typeof graph).to.eql('object'); done(); }); }); it('parses a node', function (done) { c.loadFromURL('data/node.xml', function (err, graph) { - expect(graph.entity('n356552551')).to.be.instanceOf(iD.Entity); + expect(graph.n356552551).to.be.instanceOf(iD.Entity); done(); }); }); it('parses a way', function (done) { c.loadFromURL('data/way.xml', function (err, graph) { - expect(graph.entity('w19698713')).to.be.instanceOf(iD.Entity); + expect(graph.w19698713).to.be.instanceOf(iD.Entity); done(); }); }); diff --git a/test/spec/graph/history.js b/test/spec/graph/history.js index b59190c0f..dc685b446 100644 --- a/test/spec/graph/history.js +++ b/test/spec/graph/history.js @@ -153,17 +153,15 @@ describe("iD.History", function () { it("includes modified entities", function () { var node1 = iD.Node({id: "n1"}), - node2 = node1.update({}), - graph = iD.Graph([node1]); - history.merge(graph); + node2 = node1.update({}); + history.merge({ n1: node1}); history.perform(function (graph) { return graph.replace(node2); }); expect(history.changes().modified).to.eql([node2]); }); it("includes deleted entities", function () { - var node = iD.Node({id: "n1"}), - graph = iD.Graph([node]); - history.merge(graph); + var node = iD.Node({id: "n1"}); + history.merge({ n1: node }); history.perform(function (graph) { return graph.remove(node); }); expect(history.changes().deleted).to.eql([node]); }); @@ -189,7 +187,7 @@ describe("iD.History", function () { it("is the sum of all types of changes", function() { var node1 = iD.Node({id: "n1"}), node2 = iD.Node(); - history.merge(iD.Graph([node1])); + history.merge({ n1: node1 }); history.perform(function (graph) { return graph.remove(node1); }); expect(history.numChanges()).to.eql(1); history.perform(function (graph) { return graph.replace(node2); }); From a317859b0f288eecfcfddc9b3f7fb014cd9e1ba3 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 30 Jan 2013 15:44:03 -0500 Subject: [PATCH 08/13] Don't re-add removed parentways, don't duplicate --- js/id/graph/graph.js | 28 +++++++++++++++++++++------- test/spec/graph/graph.js | 23 ++++++++++++++++++++++- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/js/id/graph/graph.js b/js/id/graph/graph.js index f2525daf5..5ef0f54b1 100644 --- a/js/id/graph/graph.js +++ b/js/id/graph/graph.js @@ -88,25 +88,39 @@ iD.Graph.prototype = { // data into each state. To external consumers, it should appear as if the // graph always contained the newly downloaded data. rebase: function(entities) { - var i, keys; + var base = this.base(), + i, k, child, id, keys; // Merging of data only needed if graph is the base graph if (!this.inherited) { - _.defaults(this.base().entities, entities); for (i in entities) { - this._updateCalculated(undefined, entities[i], this.base().parentWays, this.base().parentRels); + if (!base.entities[i]) { + base.entities[i] = entities[i]; + this._updateCalculated(undefined, entities[i], + base.parentWays, base.parentRels); + } } } keys = Object.keys(this._parentWays); for (i = 0; i < keys.length; i++) { - this._parentWays[keys[i]] = _.unique((this._parentWays[keys[i]] || []) - .concat(this.base().parentWays[keys[i]] || [])); + child = keys[i]; + for (k = 0; k < base.parentWays[child].length; k++) { + id = base.parentWays[child][k]; + if (this.entity(id) && !_.contains(this._parentWays[child], id)) { + this._parentWays[child].push(id); + } + } } keys = Object.keys(this._parentRels); for (i = 0; i < keys.length; i++) { - this._parentRels[keys[i]] = _.unique((this._parentRels[keys[i]] || []) - .concat(this.base().parentRels[keys[i]] || [])); + child = keys[i]; + for (k = 0; k < base.parentRels[child].length; k++) { + id = base.parentRels[child][k]; + if (this.entity(id) && !_.contains(this._parentRels[child], id)) { + this._parentRels[child].push(id); + } + } } }, diff --git a/test/spec/graph/graph.js b/test/spec/graph/graph.js index 9d918ae04..667cfe149 100644 --- a/test/spec/graph/graph.js +++ b/test/spec/graph/graph.js @@ -105,6 +105,16 @@ describe('iD.Graph', function() { expect(graph2.parentWays(n)).to.eql([w1, w2, w3]); }); + it("avoids re-adding removed parentWays", function() { + var n = iD.Node({id: 'n'}), + w1 = iD.Way({id: 'w1', nodes: ['n']}), + graph = iD.Graph([n, w1]), + graph2 = graph.remove(w1); + graph.rebase({ 'w1': w1 }); + graph2.rebase({ 'w1': w1 }); + expect(graph2.parentWays(n)).to.eql([]); + }); + it("updates parentRelations", function () { var n = iD.Node({id: 'n'}), r1 = iD.Relation({id: 'r1', members: [{id: 'n'}]}), @@ -117,7 +127,17 @@ describe('iD.Graph', function() { expect(graph._parentRels.hasOwnProperty('n')).to.be.false; }); - it("updates parentWays for nodes with modified parentWays", function () { + it("avoids re-adding removed parentRels", function() { + var n = iD.Node({id: 'n'}), + r1 = iD.Relation({id: 'r1', members: [{id: 'n'}]}), + graph = iD.Graph([n, r1]), + graph2 = graph.remove(r1); + graph.rebase({ 'w1': r1 }); + graph2.rebase({ 'w1': r1 }); + expect(graph2.parentWays(n)).to.eql([]); + }); + + it("updates parentRels for nodes with modified parentWays", function () { var n = iD.Node({id: 'n'}), r1 = iD.Relation({id: 'r1', members: [{id: 'n'}]}), r2 = iD.Relation({id: 'r2', members: [{id: 'n'}]}), @@ -129,6 +149,7 @@ describe('iD.Graph', function() { graph2.rebase({'r3': r3}); expect(graph2.parentRelations(n)).to.eql([r1, r2, r3]); }); + }); describe("#remove", function () { From 2fa48df9a15c81198af4ef9a78f0b119c0285ff9 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 30 Jan 2013 16:09:15 -0500 Subject: [PATCH 09/13] Small parentWays, parentRelations perf improvements --- js/id/graph/graph.js | 17 ++++++++++------- js/id/renderer/map.js | 2 +- js/id/svg/vertices.js | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/js/id/graph/graph.js b/js/id/graph/graph.js index 5ef0f54b1..bd5b87f2e 100644 --- a/js/id/graph/graph.js +++ b/js/id/graph/graph.js @@ -24,6 +24,7 @@ iD.Graph = function(other, mutable) { this.transients = {}; this._childNodes = {}; + this.getEntity = _.bind(this.entity, this); if (!mutable) { this.freeze(); @@ -48,19 +49,21 @@ iD.Graph.prototype = { }, parentWays: function(entity) { - return _.map(this._parentWays[entity.id], _.bind(this.entity, this)); + return _.map(this._parentWays[entity.id], this.getEntity); }, isPoi: function(entity) { - return this.parentWays(entity).length === 0; + var parentWays = this._parentWays[entity.id]; + return !parentWays || parentWays.length === 0; + }, + + isShared: function(entity) { + var parentWays = this._parentWays[entity.id]; + return parentWays && parentWays.length > 1; }, parentRelations: function(entity) { - return _.map(this._parentRels[entity.id], _.bind(this.entity, this)); - var ent, id, parents; - - - return this._parentRels[entity.id] || []; + return _.map(this._parentRels[entity.id], this.getEntity); }, childNodes: function(entity) { diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index 139abb4c7..0a22e1911 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -64,7 +64,7 @@ iD.Map = function() { for (var i = 0; i < parents.length; i++) { var parent = parents[i]; if (only[parent.id] === undefined) { - only[parent.id] = graph.entity(parent.id); + only[parent.id] = parent; addParents(graph.parentRelations(parent)); } } diff --git a/js/id/svg/vertices.js b/js/id/svg/vertices.js index 06be536ec..ba28c968a 100644 --- a/js/id/svg/vertices.js +++ b/js/id/svg/vertices.js @@ -36,7 +36,7 @@ iD.svg.Vertices = function(projection) { groups.attr('transform', iD.svg.PointTransform(projection)) .call(iD.svg.TagClasses()) .call(iD.svg.MemberClasses(graph)) - .classed('shared', function(entity) { return graph.parentWays(entity).length > 1; }); + .classed('shared', function(entity) { return graph.isShared(entity); }); // Selecting the following implicitly // sets the data (vertix entity) on the elements From a82b81a3c227ec02d7f36302bd8f4904cb8700b0 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 30 Jan 2013 16:40:00 -0500 Subject: [PATCH 10/13] Add tests for #replace #remove #rebase --- test/spec/graph/graph.js | 65 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/test/spec/graph/graph.js b/test/spec/graph/graph.js index 667cfe149..327d66295 100644 --- a/test/spec/graph/graph.js +++ b/test/spec/graph/graph.js @@ -92,6 +92,14 @@ describe('iD.Graph', function() { expect(graph._parentWays.hasOwnProperty('n')).to.be.false; }); + it("avoids adding duplicate parentWays", function () { + var n = iD.Node({id: 'n'}), + w1 = iD.Way({id: 'w1', nodes: ['n']}), + graph = iD.Graph([n, w1]); + graph.rebase({ 'w1': w1 }); + expect(graph.parentWays(n)).to.eql([w1]); + }); + it("updates parentWays for nodes with modified parentWays", function () { var n = iD.Node({id: 'n'}), w1 = iD.Way({id: 'w1', nodes: ['n']}), @@ -171,6 +179,20 @@ describe('iD.Graph', function() { graph = iD.Graph([node]); expect(graph.remove(node).entity(node.id)).to.be.undefined; }); + + it("removes the entity as a parentWay", function () { + var node = iD.Node({id: 'n' }), + w1 = iD.Way({id: 'w', nodes: ['n']}), + graph = iD.Graph([node, w1]); + expect(graph.remove(w1).parentWays(node)).to.eql([]); + }); + + it("removes the entity as a parentRelation", function () { + var node = iD.Node({id: 'n' }), + r1 = iD.Relation({id: 'w', members: [{id: 'n' }]}), + graph = iD.Graph([node, r1]); + expect(graph.remove(r1).parentRelations(node)).to.eql([]); + }); }); describe("#replace", function () { @@ -193,6 +215,49 @@ describe('iD.Graph', function() { graph = iD.Graph([node1]); expect(graph.replace(node2).entity(node2.id)).to.equal(node2); }); + + it("adds parentWays", function () { + var node = iD.Node({id: 'n' }), + w1 = iD.Way({id: 'w', nodes: ['n']}), + graph = iD.Graph([node]); + expect(graph.replace(w1).parentWays(node)).to.eql([w1]); + }); + + it("removes parentWays", function () { + var node = iD.Node({id: 'n' }), + w1 = iD.Way({id: 'w', nodes: ['n']}), + graph = iD.Graph([node, w1]); + expect(graph.remove(w1).parentWays(node)).to.eql([]); + }); + + it("doesn't add duplicate parentWays", function () { + var node = iD.Node({id: 'n' }), + w1 = iD.Way({id: 'w', nodes: ['n']}), + graph = iD.Graph([node, w1]); + expect(graph.replace(w1).parentWays(node)).to.eql([w1]); + }); + + it("adds parentRels", function () { + var node = iD.Node({id: 'n' }), + r1 = iD.Relation({id: 'w', members: [{id: 'n'}]}), + graph = iD.Graph([node]); + expect(graph.replace(r1).parentRelations(node)).to.eql([r1]); + }); + + it("removes parentRelations", function () { + var node = iD.Node({id: 'n' }), + r1 = iD.Relation({id: 'w', members: [{id: 'n'}]}), + graph = iD.Graph([node, r1]); + expect(graph.remove(r1).parentRelations(node)).to.eql([]); + }); + + it("doesn't add duplicate parentRelations", function () { + var node = iD.Node({id: 'n' }), + r1 = iD.Relation({id: 'w', members: [{id: 'n'}]}), + graph = iD.Graph([node, r1]); + expect(graph.replace(r1).parentRelations(node)).to.eql([r1]); + }); + }); describe("#update", function () { From 9862238a842756b5beb825e35d8222fb2b375576 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 30 Jan 2013 17:04:51 -0500 Subject: [PATCH 11/13] Fix graph.rebase() --- js/id/graph/graph.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/js/id/graph/graph.js b/js/id/graph/graph.js index bd5b87f2e..5e865b9ec 100644 --- a/js/id/graph/graph.js +++ b/js/id/graph/graph.js @@ -107,10 +107,12 @@ iD.Graph.prototype = { keys = Object.keys(this._parentWays); for (i = 0; i < keys.length; i++) { child = keys[i]; - for (k = 0; k < base.parentWays[child].length; k++) { - id = base.parentWays[child][k]; - if (this.entity(id) && !_.contains(this._parentWays[child], id)) { - this._parentWays[child].push(id); + if (base.parentWays[child]) { + for (k = 0; k < base.parentWays[child].length; k++) { + id = base.parentWays[child][k]; + if (this.entity(id) && !_.contains(this._parentWays[child], id)) { + this._parentWays[child].push(id); + } } } } @@ -118,10 +120,12 @@ iD.Graph.prototype = { keys = Object.keys(this._parentRels); for (i = 0; i < keys.length; i++) { child = keys[i]; - for (k = 0; k < base.parentRels[child].length; k++) { - id = base.parentRels[child][k]; - if (this.entity(id) && !_.contains(this._parentRels[child], id)) { - this._parentRels[child].push(id); + if (base.parentRels[child]) { + for (k = 0; k < base.parentRels[child].length; k++) { + id = base.parentRels[child][k]; + if (this.entity(id) && !_.contains(this._parentRels[child], id)) { + this._parentRels[child].push(id); + } } } } From 35cefceba67fcf2ee1bfa63bb1511a51f4eeee2d Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 30 Jan 2013 17:43:14 -0500 Subject: [PATCH 12/13] Fix partial redraws --- js/id/renderer/map.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index 79fcd3240..0d1532948 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -165,7 +165,8 @@ iD.Map = function() { } function resetTransform() { - if (!surface.style(transformProp)) return false; + var prop = surface.style(transformProp); + if (!prop || prop === 'none') return false; surface.style(transformProp, ''); tilegroup.style(transformProp, ''); return true; From eb504a3e23ad1697eb942ae1bd1ba7b1c81aebf5 Mon Sep 17 00:00:00 2001 From: Tom MacWright Date: Wed, 30 Jan 2013 18:18:19 -0500 Subject: [PATCH 13/13] Correctly rewrite users --- js/id/ui/contributors.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/js/id/ui/contributors.js b/js/id/ui/contributors.js index c0036a677..c64d8a2ac 100644 --- a/js/id/ui/contributors.js +++ b/js/id/ui/contributors.js @@ -16,7 +16,7 @@ iD.ui.contributors = function(map) { var l = selection .select('.contributor-list') .selectAll('a.user-link') - .data(subset); + .data(subset, function(d) { return d; }); l.enter().append('a') @@ -43,6 +43,10 @@ iD.ui.contributors = function(map) { ext[1][0], ext[1][1]]; }) .text(' and ' + (u.length - limit) + ' others'); + } else { + selection + .select('.contributor-count') + .html(''); } if (!u.length) {