diff --git a/modules/services/osm.js b/modules/services/osm.js index 1aab53baa4..c52f7c0ad1 100644 --- a/modules/services/osm.js +++ b/modules/services/osm.js @@ -314,6 +314,53 @@ export default { }); return ordered; } + // sort relations in a changeset by dependencies + function sort(changes) { + // find a referenced relation in the current changeset + function resolve(item){ + return _.find(relations, function(relation) { + return item.keyAttributes.type === 'relation' + && item.keyAttributes.ref === relation['@id']; + }); + } + // a new item is an item that has not been already processed + function isNew(item) { + return !sorted[ item['@id'] ] && !_.find(processing, function(proc){ + return proc['@id'] === item['@id']; + }); + } + var processing = [], + sorted = {}, + relations = changes.relation; + + if (!relations) return changes; + + for (var i = 0; i < relations.length; i++) { + + var relation = relations[i]; + + // skip relation if already sorted + if ( !sorted[relation['@id']] ) { + processing.push( relation ); + } + + while ( processing.length > 0 ){ + var next = processing[0], + deps = _.filter( _.compact(next.member.map(resolve)), isNew); + + if ( deps.length === 0 ){ + sorted[ next['@id'] ] = next; + processing.shift(); + } else { + processing = deps.concat( processing ); + } + } + + } + + changes.relation = _.values(sorted); + return changes; + } function rep(entity) { return entity.asJXON(changeset_id); @@ -323,7 +370,7 @@ export default { osmChange: { '@version': 0.6, '@generator': 'iD', - 'create': nest(changes.created.map(rep), ['node', 'way', 'relation']), + 'create': sort(nest(changes.created.map(rep), ['node', 'way', 'relation'])), 'modify': nest(changes.modified.map(rep), ['node', 'way', 'relation']), 'delete': _.extend(nest(changes.deleted.map(rep), ['relation', 'way', 'node']), {'@if-unused': true}) } diff --git a/test/spec/services/osm.js b/test/spec/services/osm.js index b75c233784..e4bc236034 100644 --- a/test/spec/services/osm.js +++ b/test/spec/services/osm.js @@ -362,6 +362,35 @@ describe('iD.serviceOsm', function () { ]); }); + it('includes creations ordered by dependencies', function() { + var n = iD.Node({loc: [0, 0]}), + w = iD.Way({nodes: [n.id]}), + r1 = iD.Relation({members: [{id: w.id, type: 'way'}]}), + r2 = iD.Relation({members: [{id: r1.id, type: 'relation'}]}), + changes = {created: [r2, r1, w, n], modified: [], deleted: []}, + jxon = connection.osmChangeJXON('1234', changes); + + expect(d3.entries(jxon.osmChange.create)).to.eql([ + {key: 'node', value: [n.asJXON('1234').node]}, + {key: 'way', value: [w.asJXON('1234').way]}, + {key: 'relation', value: [r1.asJXON('1234').relation, r2.asJXON('1234').relation]}, + ]); + }); + + it('includes creations ignoring circular dependencies', function() { + var r1 = iD.Relation(), + r2 = iD.Relation(), + changes, jxon; + r1.addMember({id: r2.id, type: 'relation'}); + r2.addMember({id: r1.id, type: 'relation'}); + changes = {created: [r1,r2], modified: [], deleted: []}; + jxon = connection.osmChangeJXON('1234', changes); + + expect(d3.entries(jxon.osmChange.create)).to.eql([ + {key: 'relation', value: [r1.asJXON('1234').relation, r2.asJXON('1234').relation]}, + ]); + }); + it('includes modifications', function() { var n = iD.Node({loc: [0, 0]}), w = iD.Way(),