Slide 1

Slide 1 text

A New Editor for OpenStreetMap John Firebaugh / @jfire

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

No content

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

No content

Slide 10

Slide 10 text

Motivation

Slide 11

Slide 11 text

Methods

Slide 12

Slide 12 text

design usability 37 111

Slide 13

Slide 13 text

Modality

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

No content

Slide 16

Slide 16 text

Point Line Area

Slide 17

Slide 17 text

Presets

Slide 18

Slide 18 text

No content

Slide 19

Slide 19 text

No content

Slide 20

Slide 20 text

No content

Slide 21

Slide 21 text

Details

Slide 22

Slide 22 text

Drag to fix connectivity

Slide 23

Slide 23 text

Address autocompletion

Slide 24

Slide 24 text

High-level design determines whether your app is familiar or foreign.

Slide 25

Slide 25 text

Low-level details determine whether your app is delightful or tolerable.

Slide 26

Slide 26 text

Technology

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

No content

Slide 29

Slide 29 text

No content

Slide 30

Slide 30 text

No content

Slide 31

Slide 31 text

d3

Slide 32

Slide 32 text

d3.geo.mercator

Slide 33

Slide 33 text

d3.xhr d3.dispatch d3.geo.path d3.behavior.zoom d3.geom.polygon d3.transition

Slide 34

Slide 34 text

No content

Slide 35

Slide 35 text

No content

Slide 36

Slide 36 text

No content

Slide 37

Slide 37 text

Node Way Relation Graph

Slide 38

Slide 38 text

Node Way Relation Graph History Current Graph

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

Splitting a Way

Slide 43

Slide 43 text

Splitting a Way

Slide 44

Slide 44 text

Splitting a Way

Slide 45

Slide 45 text

Splitting a Way from

Slide 46

Slide 46 text

Splitting a Way from via

Slide 47

Slide 47 text

Splitting a Way from via to

Slide 48

Slide 48 text

Splitting a Way from via

Slide 49

Slide 49 text

Splitting a Way from via to

Slide 50

Slide 50 text

// 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

Slide 51

Slide 51 text

osm-auth editor-imagery-index OSM Lab osm-live-map osm-gpx https://github.com/osmlab

Slide 52

Slide 52 text

Choose libraries based on the needs of the project, not popularity.

Slide 53

Slide 53 text

Good libraries are extracted from working projects, not built in isolation.

Slide 54

Slide 54 text

No content

Slide 55

Slide 55 text

ideditor.com