Upgrade to Pro — share decks privately, control downloads, hide ads and more …

iD: A New Editor for OpenStreetMap

iD: A New Editor for OpenStreetMap

Considers the motivation for building a new editor for OSM, and the design and technology involved in building it. Presented at the Bay Area GeoMeetup April 2, 2013.

John Firebaugh

April 02, 2013
Tweet

More Decks by John Firebaugh

Other Decks in Technology

Transcript

  1. d3

  2. Node Way Relation Graph Node Way Relation Graph 2 History

    Current Graph Undo History edit tags Action
  3. Node Way Relation Graph Node Way Relation Graph 2 Node

    Way Relation Graph Node History Current Graph Undo History Undo History add node Action edit tags Action
  4. Node Way Relation Graph Node Way Relation Graph 2 Node

    Way Relation Graph Node History Current Graph Redo History Undo History add node Action edit tags Action
  5. // Split a way at the given node. // //

    Optionally, split only the given ways, if multiple ways share // the given node. // // This is the inverse of `iD.actions.Join`. // // For testing convenience, accepts an ID to assign to the new way. // Normally, this will be undefined and the way will automatically // be assigned a new ID. // // Reference: // https://github.com/systemed/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/SplitWayAction.as // iD.actions.Split = function(nodeId, wayIds, newWayIds) { function split(graph, wayA, newWayId) { var wayB = iD.Way({id: newWayId, tags: wayA.tags}), nodesA, nodesB, isArea = wayA.isArea(); if (wayA.isClosed()) { var nodes = wayA.nodes.slice(0, -1), idxA = _.indexOf(nodes, nodeId), idxB = idxA + Math.floor(nodes.length / 2); if (idxB >= nodes.length) { idxB %= nodes.length; nodesA = nodes.slice(idxA).concat(nodes.slice(0, idxB + 1)); nodesB = nodes.slice(idxB, idxA + 1); } else { nodesA = nodes.slice(idxA, idxB + 1); nodesB = nodes.slice(idxB).concat(nodes.slice(0, idxA + 1)); } } else { var idx = _.indexOf(wayA.nodes, nodeId, 1); nodesA = wayA.nodes.slice(0, idx + 1); nodesB = wayA.nodes.slice(idx); } wayA = wayA.update({nodes: nodesA}); wayB = wayB.update({nodes: nodesB}); graph = graph.replace(wayA); graph = graph.replace(wayB); graph.parentRelations(wayA).forEach(function(relation) { if (relation.isRestriction()) { var via = relation.memberByRole('via'); if (via && wayB.contains(via.id)) { relation = relation.updateMember({id: wayB.id}, relation.memberById(wayA.id).index); graph = graph.replace(relation); } } else { var role = relation.memberById(wayA.id).role, last = wayB.last(), i = relation.memberById(wayA.id).index, j; for (j = 0; j < relation.members.length; j++) { var entity = graph.entity(relation.members[j].id); if (entity && entity.type === 'way' && entity.contains(last)) { break; } } relation = relation.addMember({id: wayB.id, type: 'way', role: role}, i <= j ? i + 1 : i); graph = graph.replace(relation); } }); if (isArea) { var multipolygon = iD.Relation({ tags: _.extend({}, wayA.tags, {type: 'multipolygon'}), members: [ {id: wayA.id, role: 'outer', type: 'way'}, {id: wayB.id, role: 'outer', type: 'way'} ]}); graph = graph.replace(multipolygon); graph = graph.replace(wayA.update({tags: {}})); graph = graph.replace(wayB.update({tags: {}})); } return graph; } var action = function(graph) { var candidates = action.ways(graph); for (var i = 0; i < candidates.length; i++) { graph = split(graph, candidates[i], newWayIds && newWayIds[i]); } return graph; }; action.ways = function(graph) { var node = graph.entity(nodeId), parents = graph.parentWays(node); return parents.filter(function(parent) { if (wayIds && wayIds.length && wayIds.indexOf(parent.id) === -1) return false; if (parent.isClosed()) { return true; } for (var i = 1; i < parent.nodes.length - 1; i++) { if (parent.nodes[i] === nodeId) { return true; } } return false; }); }; action.disabled = function(graph) { var candidates = action.ways(graph); if (candidates.length === 0 || (wayIds && wayIds.length && wayIds.length !== candidates.length)) return 'not_eligible'; }; return action; }; describe("iD.actions.Split", function () { describe("#disabled", function () { it("returns falsy for a non-end node of a single way", function () { var graph = iD.Graph({ 'a': iD.Node({id: 'a'}), 'b': iD.Node({id: 'b'}), 'c': iD.Node({id: 'c'}), '-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}) }); expect(iD.actions.Split('b').disabled(graph)).not.to.be.ok; }); it("returns falsy for an intersection of two ways", function () { var graph = iD.Graph({ 'a': iD.Node({id: 'a'}), 'b': iD.Node({id: 'b'}), 'c': iD.Node({id: 'c'}), 'd': iD.Node({id: 'c'}), '*': iD.Node({id: '*'}), '-': iD.Way({id: '-', nodes: ['a', '*', 'b']}), '|': iD.Way({id: '|', nodes: ['c', '*', 'd']}) }); expect(iD.actions.Split('*').disabled(graph)).not.to.be.ok; }); it("returns falsy for an intersection of two ways with parent way specified", function () { var graph = iD.Graph({ 'a': iD.Node({id: 'a'}), 'b': iD.Node({id: 'b'}), 'c': iD.Node({id: 'c'}), 'd': iD.Node({id: 'c'}), '*': iD.Node({id: '*'}), '-': iD.Way({id: '-', nodes: ['a', '*', 'b']}), '|': iD.Way({id: '|', nodes: ['c', '*', 'd']}) }); expect(iD.actions.Split('*', ['-']).disabled(graph)).not.to.be.ok; }); it("returns falsy for a self-intersection", function () { var graph = iD.Graph({ 'a': iD.Node({id: 'a'}), 'b': iD.Node({id: 'b'}), 'c': iD.Node({id: 'c'}), 'd': iD.Node({id: 'c'}), '-': iD.Way({id: '-', nodes: ['a', 'b', 'c', 'a', 'd']}) }); expect(iD.actions.Split('a').disabled(graph)).not.to.be.ok; }); it("returns 'not_eligible' for the first node of a single way", function () { var graph = iD.Graph({ 'a': iD.Node({id: 'a'}), 'b': iD.Node({id: 'b'}), '-': iD.Way({id: '-', nodes: ['a', 'b']}) }); expect(iD.actions.Split('a').disabled(graph)).to.equal('not_eligible'); }); it("returns 'not_eligible' for the last node of a single way", function () { var graph = iD.Graph({ 'a': iD.Node({id: 'a'}), 'b': iD.Node({id: 'b'}), '-': iD.Way({id: '-', nodes: ['a', 'b']}) }); expect(iD.actions.Split('b').disabled(graph)).to.equal('not_eligible'); }); it("returns 'not_eligible' for an intersection of two ways with non-parent way specified", function () { var graph = iD.Graph({ 'a': iD.Node({id: 'a'}), 'b': iD.Node({id: 'b'}), 'c': iD.Node({id: 'c'}), 'd': iD.Node({id: 'c'}), '*': iD.Node({id: '*'}), '-': iD.Way({id: '-', nodes: ['a', '*', 'b']}), '|': iD.Way({id: '|', nodes: ['c', '*', 'd']}) }); expect(iD.actions.Split('*', ['-', '=']).disabled(graph)).to.equal('not_eligible'); }); }); it("creates a new way with the appropriate nodes", function () { // Situation: // a ---- b ---- c // // Split at b. // // Expected result: // a ---- b ==== c // var graph = iD.Graph({ 'a': iD.Node({id: 'a'}), 'b': iD.Node({id: 'b'}), 'c': iD.Node({id: 'c'}), '-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}) }); graph = iD.actions.Split('b', undefined, ['='])(graph); expect(graph.entity('-').nodes).to.eql(['a', 'b']); expect(graph.entity('=').nodes).to.eql(['b', 'c']); }); it("copies tags to the new way", function () { var tags = {highway: 'residential'}, graph = iD.Graph({ 'a': iD.Node({id: 'a'}), 'b': iD.Node({id: 'b'}), 'c': iD.Node({id: 'c'}), '-': iD.Way({id: '-', nodes: ['a', 'b', 'c'], tags: tags}) }); graph = iD.actions.Split('b', undefined, ['='])(graph); // Immutable tags => should be shared by identity. expect(graph.entity('-').tags).to.equal(tags); expect(graph.entity('=').tags).to.equal(tags); }); it("splits a way at a T-junction", function () { // Situation: // a ---- b ---- c // | // d // // Split at b. // // Expected result: // a ---- b ==== c // | // d // var graph = iD.Graph({ 'a': iD.Node({id: 'a'}), 'b': iD.Node({id: 'b'}), 'c': iD.Node({id: 'c'}), 'd': iD.Node({id: 'd'}), '-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}), '|': iD.Way({id: '|', nodes: ['d', 'b']}) }); graph = iD.actions.Split('b', undefined, ['='])(graph); expect(graph.entity('-').nodes).to.eql(['a', 'b']); expect(graph.entity('=').nodes).to.eql(['b', 'c']); expect(graph.entity('|').nodes).to.eql(['d', 'b']); }); it("splits multiple ways at an intersection", function () { // Situation: // c // | // a ---- * ---- b // ¦ // d // // Split at b. // // Expected result: // c // | // a ---- * ==== b // ¦ // d // var graph = iD.Graph({ 'a': iD.Node({id: 'a'}), 'b': iD.Node({id: 'b'}), 'c': iD.Node({id: 'c'}), 'd': iD.Node({id: 'c'}), '*': iD.Node({id: '*'}), '-': iD.Way({id: '-', nodes: ['a', '*', 'b']}), '|': iD.Way({id: '|', nodes: ['c', '*', 'd']}) }); graph = iD.actions.Split('*', undefined, ['=', '¦'])(graph); expect(graph.entity('-').nodes).to.eql(['a', '*']); expect(graph.entity('=').nodes).to.eql(['*', 'b']); expect(graph.entity('|').nodes).to.eql(['c', '*']); expect(graph.entity('¦').nodes).to.eql(['*', 'd']); }); it("splits the specified ways at an intersection", function () { var graph = iD.Graph({ 'a': iD.Node({id: 'a'}), 'b': iD.Node({id: 'b'}), 'c': iD.Node({id: 'c'}), 'd': iD.Node({id: 'c'}), '*': iD.Node({id: '*'}), '-': iD.Way({id: '-', nodes: ['a', '*', 'b']}), '|': iD.Way({id: '|', nodes: ['c', '*', 'd']}) }); var g1 = iD.actions.Split('*', ['-'], ['='])(graph); expect(g1.entity('-').nodes).to.eql(['a', '*']); expect(g1.entity('=').nodes).to.eql(['*', 'b']); expect(g1.entity('|').nodes).to.eql(['c', '*', 'd']); var g2 = iD.actions.Split('*', ['|'], ['¦'])(graph); expect(g2.entity('-').nodes).to.eql(['a', '*', 'b']); expect(g2.entity('|').nodes).to.eql(['c', '*']); expect(g2.entity('¦').nodes).to.eql(['*', 'd']); var g3 = iD.actions.Split('*', ['-', '|'], ['=', '¦'])(graph); expect(g3.entity('-').nodes).to.eql(['a', '*']); expect(g3.entity('=').nodes).to.eql(['*', 'b']); expect(g3.entity('|').nodes).to.eql(['c', '*']); expect(g3.entity('¦').nodes).to.eql(['*', 'd']); }); it("splits self-intersecting ways", function () { // Situation: // b // / | // / | // c - a -- d // // Split at a. // // Expected result: // b // / | // / | // c - a == d // var graph = iD.Graph({ 'a': iD.Node({id: 'a'}), 'b': iD.Node({id: 'b'}), 'c': iD.Node({id: 'c'}), 'd': iD.Node({id: 'c'}), '-': iD.Way({id: '-', nodes: ['a', 'b', 'c', 'a', 'd']}) }); graph = iD.actions.Split('a', undefined, ['='])(graph); expect(graph.entity('-').nodes).to.eql(['a', 'b', 'c', 'a']); expect(graph.entity('=').nodes).to.eql(['a', 'd']); }); it("splits a closed way at the given point and its antipode", function () { // Situation: // a ---- b // | | // d ---- c // // Split at a. // // Expected result: // a ---- b // || | // d ==== c // var graph = iD.Graph({ 'a': iD.Node({id: 'a'}), 'b': iD.Node({id: 'b'}), 'c': iD.Node({id: 'c'}), 'd': iD.Node({id: 'd'}), '-': iD.Way({id: '-', nodes: ['a', 'b', 'c', 'd', 'a']}) }); var g1 = iD.actions.Split('a', undefined, ['='])(graph); expect(g1.entity('-').nodes).to.eql(['a', 'b', 'c']); expect(g1.entity('=').nodes).to.eql(['c', 'd', 'a']); var g2 = iD.actions.Split('b', undefined, ['='])(graph); expect(g2.entity('-').nodes).to.eql(['b', 'c', 'd']); expect(g2.entity('=').nodes).to.eql(['d', 'a', 'b']); var g3 = iD.actions.Split('c', undefined, ['='])(graph); expect(g3.entity('-').nodes).to.eql(['c', 'd', 'a']); expect(g3.entity('=').nodes).to.eql(['a', 'b', 'c']); var g4 = iD.actions.Split('d', undefined, ['='])(graph); expect(g4.entity('-').nodes).to.eql(['d', 'a', 'b']); expect(g4.entity('=').nodes).to.eql(['b', 'c', 'd']); }); it("splits an area by converting it to a multipolygon", function () { var graph = iD.Graph({ 'a': iD.Node({id: 'a'}), 'b': iD.Node({id: 'b'}), 'c': iD.Node({id: 'c'}), 'd': iD.Node({id: 'd'}), '-': iD.Way({id: '-', tags: {building: 'yes'}, nodes: ['a', 'b', 'c', 'd', 'a']}) }); graph = iD.actions.Split('a', undefined, ['='])(graph); expect(graph.entity('-').tags).to.eql({}); expect(graph.entity('=').tags).to.eql({}); expect(graph.parentRelations(graph.entity('-'))).to.have.length(1); var relation = graph.parentRelations(graph.entity('-'))[0]; expect(relation.tags).to.eql({type: 'multipolygon', building: 'yes'}); expect(relation.members).to.eql([ {id: '-', role: 'outer', type: 'way'}, {id: '=', role: 'outer', type: 'way'} ]); }); it("adds the new way to parent relations (no connections)", function () { // Situation: // a ---- b ---- c // Relation: [----] // // Split at b. // // Expected result: // a ---- b ==== c // Relation: [----, ====] // var graph = iD.Graph({ 'a': iD.Node({id: 'a'}), 'b': iD.Node({id: 'b'}), 'c': iD.Node({id: 'c'}), '-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}), 'r': iD.Relation({id: 'r', members: [{id: '-', type: 'way', role: 'forward'}]}) }); graph = iD.actions.Split('b', undefined, ['='])(graph); expect(graph.entity('r').members).to.eql([ {id: '-', type: 'way', role: 'forward'}, {id: '=', type: 'way', role: 'forward'} ]); }); it("adds the new way to parent relations (forward order)", function () { // Situation: // a ---- b ---- c ~~~~ d // Relation: [----, ~~~~] // // Split at b. // // Expected result: // a ---- b ==== c ~~~~ d // Relation: [----, ====, ~~~~] // var graph = iD.Graph({ 'a': iD.Node({id: 'a'}), 'b': iD.Node({id: 'b'}), 'c': iD.Node({id: 'c'}), 'd': iD.Node({id: 'd'}), '-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}), '~': iD.Way({id: '~', nodes: ['c', 'd']}), 'r': iD.Relation({id: 'r', members: [{id: '-', type: 'way'}, {id: '~', type: 'way'}]}) }); graph = iD.actions.Split('b', undefined, ['='])(graph); expect(_.pluck(graph.entity('r').members, 'id')).to.eql(['-', '=', '~']); }); it("adds the new way to parent relations (reverse order)", function () { // Situation: // a ---- b ---- c ~~~~ d // Relation: [~~~~, ----] // // Split at b. // // Expected result: // a ---- b ==== c ~~~~ d // Relation: [~~~~, ====, ----] // var graph = iD.Graph({ 'a': iD.Node({id: 'a'}), 'b': iD.Node({id: 'b'}), 'c': iD.Node({id: 'c'}), 'd': iD.Node({id: 'd'}), '-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}), '~': iD.Way({id: '~', nodes: ['c', 'd']}), 'r': iD.Relation({id: 'r', members: [{id: '~', type: 'way'}, {id: '-', type: 'way'}]}) }); graph = iD.actions.Split('b', undefined, ['='])(graph); expect(_.pluck(graph.entity('r').members, 'id')).to.eql(['~', '=', '-']); }); it("handles incomplete relations", function () { var graph = iD.Graph({ 'a': iD.Node({id: 'a'}), 'b': iD.Node({id: 'b'}), 'c': iD.Node({id: 'c'}), '-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}), 'r': iD.Relation({id: 'r', members: [{id: '~', type: 'way'}, {id: '-', type: 'way'}]}) }); graph = iD.actions.Split('b', undefined, ['='])(graph); expect(_.pluck(graph.entity('r').members, 'id')).to.eql(['~', '-', '=']); }); ['restriction', 'restriction:bus'].forEach(function (type) { it("updates a restriction's 'from' role", function () { // Situation: // a ----> b ----> c ~~~~ d // A restriction from ---- to ~~~~ via c. // // Split at b. // // Expected result: // a ----> b ====> c ~~~~ d // A restriction from ==== to ~~~~ via c. // var graph = iD.Graph({ 'a': iD.Node({id: 'a'}), 'b': iD.Node({id: 'b'}), 'c': iD.Node({id: 'c'}), 'd': iD.Node({id: 'd'}), '-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}), '~': iD.Way({id: '~', nodes: ['c', 'd']}), 'r': iD.Relation({id: 'r', tags: {type: type}, members: [ {id: '-', role: 'from'}, {id: '~', role: 'to'}, {id: 'c', role: 'via'}]}) }); graph = iD.actions.Split('b', undefined, ['='])(graph); expect(graph.entity('r').members).to.eql([ {id: '=', role: 'from'}, {id: '~', role: 'to'}, {id: 'c', role: 'via'}]); }); it("updates a restriction's 'to' role", function () { // Situation: // a ----> b ----> c ~~~~ d // A restriction from ~~~~ to ---- via c. // // Split at b. // // Expected result: // a ----> b ====> c ~~~~ d // A restriction from ~~~~ to ==== via c. // var graph = iD.Graph({ 'a': iD.Node({id: 'a'}), 'b': iD.Node({id: 'b'}), 'c': iD.Node({id: 'c'}), 'd': iD.Node({id: 'd'}), '-': iD.Way({id: '-', nodes: ['a', 'b', 'c']}), '~': iD.Way({id: '~', nodes: ['c', 'd']}), 'r': iD.Relation({id: 'r', tags: {type: type}, members: [ {id: '~', role: 'from'}, {id: '-', role: 'to'}, {id: 'c', role: 'via'}]}) }); graph = iD.actions.Split('b', undefined, ['='])(graph); expect(graph.entity('r').members).to.eql([ {id: '~', role: 'from'}, {id: '=', role: 'to'}, {id: 'c', role: 'via'}]); }); it("leaves unaffected restrictions unchanged", function () { // Situation: // a <---- b <---- c ~~~~ d // A restriction from ---- to ~~~~ via c. // // Split at b. // // Expected result: // a <==== b <---- c ~~~~ d // A restriction from ---- to ~~~~ via c. // var graph = iD.Graph({ 'a': iD.Node({id: 'a'}), 'b': iD.Node({id: 'b'}), 'c': iD.Node({id: 'c'}), 'd': iD.Node({id: 'd'}), '-': iD.Way({id: '-', nodes: ['c', 'b', 'a']}), '~': iD.Way({id: '~', nodes: ['c', 'd']}), 'r': iD.Relation({id: 'r', tags: {type: type}, members: [ {id: '-', role: 'from'}, {id: '~', role: 'to'}, {id: 'c', role: 'via'}]}) }); graph = iD.actions.Split('b', undefined, ['='])(graph); expect(graph.entity('r').members).to.eql([ {id: '-', role: 'from'}, {id: '~', role: 'to'}, {id: 'c', role: 'via'}]); }); }); }); Code Test