diff --git a/css/map.css b/css/map.css index 1f31beca4..0e3465cc5 100644 --- a/css/map.css +++ b/css/map.css @@ -44,10 +44,6 @@ path.casing { stroke-width: 3; } -.elastic-true { - pointer-events:none; -} - path.casing.hover { stroke:#FF0F0F !important; opacity:0.8; diff --git a/index.html b/index.html index 38a17504b..4cf24afff 100644 --- a/index.html +++ b/index.html @@ -20,8 +20,10 @@ + + @@ -44,6 +46,7 @@ + @@ -53,9 +56,10 @@ - + + diff --git a/js/id/actions/add_node.js b/js/id/actions/add_node.js index f8916e50b..669a81d49 100644 --- a/js/id/actions/add_node.js +++ b/js/id/actions/add_node.js @@ -1,6 +1,6 @@ // https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/command/AddCommand.java iD.actions.AddNode = function(node) { return function(graph) { - return graph.replace(node, 'added a place'); + return graph.replace(node); }; }; diff --git a/js/id/actions/add_way.js b/js/id/actions/add_way.js new file mode 100644 index 000000000..2be062d3a --- /dev/null +++ b/js/id/actions/add_way.js @@ -0,0 +1,5 @@ +iD.actions.AddWay = function(way) { + return function(graph) { + return graph.replace(way); + }; +}; diff --git a/js/id/actions/add_way_node.js b/js/id/actions/add_way_node.js index 7c8772dba..d685c3295 100644 --- a/js/id/actions/add_way_node.js +++ b/js/id/actions/add_way_node.js @@ -1,8 +1,10 @@ // https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/AddNodeToWayAction.as -iD.actions.AddWayNode = function(way, node, index) { +iD.actions.AddWayNode = function(wayId, nodeId, index) { return function(graph) { - var nodes = way.nodes.slice(); - nodes.splice(index || nodes.length, 0, node.id); - return graph.replace(way.update({nodes: nodes})).replace(node, 'added to a road'); + var way = graph.entity(wayId), + node = graph.entity(nodeId), + nodes = way.nodes.slice(); + nodes.splice((index === undefined) ? nodes.length : index, 0, nodeId); + return graph.replace(way.update({nodes: nodes})); }; }; diff --git a/js/id/actions/change_entity_tags.js b/js/id/actions/change_entity_tags.js index 97c67f0aa..96901c2eb 100644 --- a/js/id/actions/change_entity_tags.js +++ b/js/id/actions/change_entity_tags.js @@ -1,7 +1,6 @@ -iD.actions.ChangeEntityTags = function(entity, tags) { +iD.actions.ChangeEntityTags = function(entityId, tags) { return function(graph) { - return graph.replace(entity.update({ - tags: tags - }), 'changed tags'); + var entity = graph.entity(entityId); + return graph.replace(entity.update({tags: tags})); }; }; diff --git a/js/id/actions/delete_node.js b/js/id/actions/delete_node.js index c7813ce48..e8b6d34da 100644 --- a/js/id/actions/delete_node.js +++ b/js/id/actions/delete_node.js @@ -1,16 +1,18 @@ // https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/DeleteNodeAction.as -iD.actions.DeleteNode = function(node) { +iD.actions.DeleteNode = function(nodeId) { return function(graph) { - graph.parentWays(node.id) + var node = graph.entity(nodeId); + + graph.parentWays(nodeId) .forEach(function(parent) { - graph = iD.actions.RemoveWayNode(parent, node)(graph); + graph = iD.actions.RemoveWayNode(parent.id, nodeId)(graph); }); - graph.parentRelations(node.id) + graph.parentRelations(nodeId) .forEach(function(parent) { - graph = iD.actions.RemoveRelationMember(parent, node)(graph); + graph = iD.actions.RemoveRelationMember(parent.id, nodeId)(graph); }); - return graph.remove(node, 'removed a node'); + return graph.remove(node); }; }; diff --git a/js/id/actions/delete_way.js b/js/id/actions/delete_way.js index 641f61af2..120ac17f5 100644 --- a/js/id/actions/delete_way.js +++ b/js/id/actions/delete_way.js @@ -1,17 +1,23 @@ // https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/DeleteWayAction.as -iD.actions.DeleteWay = function(way) { +iD.actions.DeleteWay = function(wayId) { return function(graph) { - graph.parentRelations(way.id) + var way = graph.entity(wayId); + + graph.parentRelations(wayId) .forEach(function(parent) { - graph = iD.actions.RemoveRelationMember(parent, way)(graph); + graph = iD.actions.RemoveRelationMember(parent.id, wayId)(graph); }); - way.nodes.forEach(function (id) { - var node = graph.entity(id); + way.nodes.forEach(function (nodeId) { + var node = graph.entity(nodeId); - graph = iD.actions.RemoveWayNode(way, node)(graph); + // Circular ways include nodes more than once, so they + // can be deleted on earlier iterations of this loop. + if (!node) return; - if (!graph.parentWays(id).length && !graph.parentRelations(id).length) { + graph = iD.actions.RemoveWayNode(wayId, nodeId)(graph); + + if (!graph.parentWays(nodeId).length && !graph.parentRelations(nodeId).length) { if (!node.hasInterestingTags()) { graph = graph.remove(node); } else { @@ -20,6 +26,6 @@ iD.actions.DeleteWay = function(way) { } }); - return graph.remove(way, 'removed a way'); + return graph.remove(way); }; }; diff --git a/js/id/actions/move.js b/js/id/actions/move.js index 6c15e8bdc..21870c8d4 100644 --- a/js/id/actions/move.js +++ b/js/id/actions/move.js @@ -1,7 +1,8 @@ // https://github.com/openstreetmap/josm/blob/mirror/src/org/openstreetmap/josm/command/MoveCommand.java // https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/MoveNodeAction.as -iD.actions.Move = function(entity, loc) { +iD.actions.Move = function(entityId, loc) { return function(graph) { - return graph.replace(entity.update({loc: loc}), 'moved an element'); + var entity = graph.entity(entityId); + return graph.replace(entity.update({loc: loc})); }; }; diff --git a/js/id/actions/remove_relation_member.js b/js/id/actions/remove_relation_member.js index b10224c26..e72dd7ca0 100644 --- a/js/id/actions/remove_relation_member.js +++ b/js/id/actions/remove_relation_member.js @@ -1,6 +1,9 @@ -iD.actions.RemoveRelationMember = function(relation, member) { +iD.actions.RemoveRelationMember = function(relationId, memberId) { return function(graph) { - var members = _.without(relation.members, member.id); - return graph.replace(relation.update({members: members}), 'removed from a relation'); + var relation = graph.entity(relationId), + members = _.reject(relation.members, function(r) { + return r.id === memberId; + }); + return graph.replace(relation.update({members: members})); }; }; diff --git a/js/id/actions/remove_way_node.js b/js/id/actions/remove_way_node.js index bf645b6b2..8c589a20c 100644 --- a/js/id/actions/remove_way_node.js +++ b/js/id/actions/remove_way_node.js @@ -1,6 +1,17 @@ -iD.actions.RemoveWayNode = function(way, node) { +iD.actions.RemoveWayNode = function(wayId, nodeId) { return function(graph) { - var nodes = _.without(way.nodes, node.id); - return graph.replace(way.update({nodes: nodes}), 'removed from a road'); + var way = graph.entity(wayId), nodes; + // If this is the connecting node in a closed area + if (way.nodes.length > 1 && + _.indexOf(way.nodes, nodeId) === 0 && + _.lastIndexOf(way.nodes, nodeId) === way.nodes.length - 1) { + // Remove the node + nodes = _.without(way.nodes, nodeId); + // And reclose the way on the new first node. + nodes.push(nodes[0]); + } else { + nodes = _.without(way.nodes, nodeId); + } + return graph.replace(way.update({nodes: nodes})); }; }; diff --git a/js/id/actions/reverse_way.js b/js/id/actions/reverse_way.js index 4d14eb8f1..facc75edf 100644 --- a/js/id/actions/reverse_way.js +++ b/js/id/actions/reverse_way.js @@ -1,8 +1,8 @@ // https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/AddNodeToWayAction.as -iD.actions.ReverseWay = function(way) { +iD.actions.ReverseWay = function(wayId) { return function(graph) { - return graph.replace(way.update({ - nodes: way.nodes.slice() - }), 'changed way direction'); + var way = graph.entity(wayId), + nodes = way.nodes.slice().reverse(); + return graph.replace(way.update({nodes: nodes})); }; }; diff --git a/js/id/actions/split_way.js b/js/id/actions/split_way.js new file mode 100644 index 000000000..18fc36992 --- /dev/null +++ b/js/id/actions/split_way.js @@ -0,0 +1,47 @@ +// https://github.com/systemed/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/SplitWayAction.as +iD.actions.SplitWay = function(nodeId) { + return function(graph) { + + var parents = graph.parentWays(nodeId); + + // splitting ways at intersections TODO + if (parents.length !== 1) return graph; + + var way = parents[0]; + + var idx = _.indexOf(way.nodes, nodeId); + + // Create a 'b' way that contains all of the tags in the second + // half of this way + var newWay = iD.Way({ tags: _.clone(way.tags), nodes: way.nodes.slice(idx) }); + graph = graph.replace(newWay); + + // Reduce the original way to only contain the first set of nodes + graph = graph.replace(way.update({ nodes: way.nodes.slice(0, idx + 1) }), 'changed way direction'); + + var parentRelations = graph.parentRelations(way.id); + + function isVia(x) { return x.role = 'via'; } + function isSelf(x) { return x.id = way.id; } + + parentRelations.forEach(function(relation) { + if (relation.tags.type === 'restriction') { + var via = _.find(relation.members, isVia); + var ownrole = _.find(relation.members, isSelf).role; + if (via && !_.contains(newWay.nodes, via.id)) { + // the new way doesn't contain the node that's important + // to the turn restriction, so we don't need to worry + // about adding it to the turn restriction. + } else { + graph = graph.replace(iD.actions.AddRelationMember(relation.id, { + role: ownrole, + id: newWay.id, + type: 'way' + })); + } + } + }); + + return graph; + }; +}; diff --git a/js/id/actions/start_way.js b/js/id/actions/start_way.js deleted file mode 100644 index 6c82e389d..000000000 --- a/js/id/actions/start_way.js +++ /dev/null @@ -1,5 +0,0 @@ -iD.actions.StartWay = function(way) { - return function(graph) { - return graph.replace(way, 'started a road'); - }; -}; diff --git a/js/id/connection.js b/js/id/connection.js index e06260810..0c10d37d3 100644 --- a/js/id/connection.js +++ b/js/id/connection.js @@ -67,8 +67,7 @@ iD.Connection = function() { delete o.lon; delete o.lat; } - o._id = o.id; - o.id = o.type[0] + o.id; + o.id = iD.Entity.id.fromOSM(o.type, o.id); return iD.Entity(o); } @@ -150,10 +149,6 @@ iD.Connection = function() { return true; } - function apiRequestExtent(extent) { - bboxFromAPI(extent, event.load); - } - function loadTiles(projection) { var scaleExtent = [16, 16], s = projection.scale(), @@ -177,10 +172,20 @@ iD.Connection = function() { projection.invert([x + ts, y + ts])]; } - return tiles + var q = queue(2); + + var bboxes = tiles .filter(tileAlreadyLoaded) .map(apiExtentBox) - .map(apiRequestExtent); + .forEach(function(e) { + q.defer(bboxFromAPI, e); + }); + + q.awaitAll(function(err, res) { + var g = iD.Graph(); + res.forEach(function(r) { g = g.merge(r); }); + event.load(err, g); + }); } connection.url = function(_) { diff --git a/js/id/format/xml.js b/js/id/format/xml.js index 1c0fe8ad6..ec036120f 100644 --- a/js/id/format/xml.js +++ b/js/id/format/xml.js @@ -52,17 +52,17 @@ iD.format.XML = { '@version': 0.3, '@generator': 'iD', // TODO: copy elements first - create: nest(changes.create.map(function(c) { + create: nest(changes.created.map(function(c) { var x = iD.Entity(c); x.changeset = changeset_id; return x; }).map(iD.format.XML.rep)), - modify: changes.modify.map(function(c) { + modify: changes.modified.map(function(c) { var x = iD.Entity(c); x.changeset = changeset_id; return x; }).map(iD.format.XML.rep), - 'delete': changes['delete'].map(function(c) { + 'delete': changes.deleted.map(function(c) { var x = iD.Entity(c); x.changeset = changeset_id; return x; diff --git a/js/id/graph/entity.js b/js/id/graph/entity.js index b3f698704..5c89e4890 100644 --- a/js/id/graph/entity.js +++ b/js/id/graph/entity.js @@ -14,8 +14,8 @@ iD.Entity = function(a, b, c) { } } - if (!this.id) { - this.id = iD.util.id(this.type); + if (!this.id && this.type) { + this.id = iD.Entity.id(this.type); this._updated = true; } @@ -28,17 +28,35 @@ iD.Entity = function(a, b, c) { } }; +iD.Entity.id = function (type) { + return iD.Entity.id.fromOSM(type, iD.Entity.id.next[type]--); +}; + +iD.Entity.id.next = {node: -1, way: -1, relation: -1}; + +iD.Entity.id.fromOSM = function (type, id) { + return type[0] + id; +}; + +iD.Entity.id.toOSM = function (id) { + return +id.slice(1); +}; + iD.Entity.prototype = { + osmId: function() { + return iD.Entity.id.toOSM(this.id); + }, + update: function(attrs) { return iD.Entity(this, attrs, {_updated: true}); }, created: function() { - return this._updated && +this.id.slice(1) < 0; + return this._updated && this.osmId() < 0; }, modified: function() { - return this._updated && +this.id.slice(1) > 0; + return this._updated && this.osmId() > 0; }, intersects: function(extent, resolver) { diff --git a/js/id/graph/graph.js b/js/id/graph/graph.js index a830214d4..d5ea1dcf8 100644 --- a/js/id/graph/graph.js +++ b/js/id/graph/graph.js @@ -1,5 +1,5 @@ -iD.Graph = function(entities, annotation) { - if (!(this instanceof iD.Graph)) return new iD.Graph(entities, annotation); +iD.Graph = function(entities) { + if (!(this instanceof iD.Graph)) return new iD.Graph(entities); if (_.isArray(entities)) { this.entities = {}; @@ -10,8 +10,6 @@ iD.Graph = function(entities, annotation) { this.entities = entities || {}; } - this.annotation = annotation; - if (iD.debug) { Object.freeze(this); Object.freeze(this.entities); @@ -26,33 +24,40 @@ iD.Graph.prototype = { parentWays: function(id) { // This is slow and a bad hack. return _.filter(this.entities, function(e) { - return e.type === 'way' && e.nodes.indexOf(id) !== -1; + return e && e.type === 'way' && e.nodes.indexOf(id) !== -1; }); }, parentRelations: function(id) { // This is slow and a bad hack. return _.filter(this.entities, function(e) { - return e.type === 'relation' && e.members.indexOf(id) !== -1; + return e && e.type === 'relation' && + _.pluck(e.members, 'id').indexOf(id) !== -1; }); }, merge: function(graph) { var entities = _.clone(this.entities); _.defaults(entities, graph.entities); - return iD.Graph(entities, this.annotation); + return iD.Graph(entities); }, - replace: function(entity, annotation) { + replace: function(entity) { var entities = _.clone(this.entities); entities[entity.id] = entity; - return iD.Graph(entities, annotation); + return iD.Graph(entities); }, - remove: function(entity, annotation) { + remove: function(entity) { var entities = _.clone(this.entities); - delete entities[entity.id]; - return iD.Graph(entities, annotation); + + if (entity.created()) { + delete entities[entity.id]; + } else { + entities[entity.id] = undefined; + } + + return iD.Graph(entities); }, // get all objects that intersect an extent. @@ -60,7 +65,7 @@ iD.Graph.prototype = { var items = []; for (var i in this.entities) { var entity = this.entities[i]; - if (entity.intersects(extent, this)) { + if (entity && entity.intersects(extent, this)) { items.push(this.fetch(entity.id)); } } @@ -70,26 +75,52 @@ iD.Graph.prototype = { // Resolve the id references in a way, replacing them with actual objects. fetch: function(id) { var entity = this.entities[id], nodes = []; - if (!entity.nodes || !entity.nodes.length) return iD.Entity(entity); // TODO: shouldn't be necessary + if (!entity || !entity.nodes || !entity.nodes.length) return entity; for (var i = 0, l = entity.nodes.length; i < l; i++) { nodes[i] = this.fetch(entity.nodes[i]); } return iD.Entity(entity, {nodes: nodes}); }, - modifications: function() { - return _.filter(this.entities, function(entity) { - return entity.modified(); - }).map(function(e) { - return this.fetch(e.id); - }.bind(this)); + difference: function (graph) { + var result = []; + + _.each(this.entities, function(entity, id) { + if (entity !== graph.entities[id]) { + result.push(id); + } + }); + + _.each(graph.entities, function(entity, id) { + if (entity && !this.entities.hasOwnProperty(id)) { + result.push(id); + } + }, this); + + return result.sort(); }, - creations: function() { - return _.filter(this.entities, function(entity) { - return entity.created(); - }).map(function(e) { - return this.fetch(e.id); - }.bind(this)); + modified: function() { + var result = []; + _.each(this.entities, function(entity, id) { + if (entity && entity.modified()) result.push(id); + }); + return result; + }, + + created: function() { + var result = []; + _.each(this.entities, function(entity, id) { + if (entity && entity.created()) result.push(id); + }); + return result; + }, + + deleted: function() { + var result = []; + _.each(this.entities, function(entity, id) { + if (!entity) result.push(id); + }); + return result; } }; diff --git a/js/id/graph/history.js b/js/id/graph/history.js index 11a6111ce..9836af4dd 100644 --- a/js/id/graph/history.js +++ b/js/id/graph/history.js @@ -2,50 +2,77 @@ iD.History = function() { var stack, index, dispatch = d3.dispatch('change'); - function maybeChange() { - if (stack[index].annotation) { - dispatch.change(); + function perform(actions) { + actions = Array.prototype.slice.call(actions); + + var annotation; + + if (_.isString(_.last(actions))) { + annotation = actions.pop(); } + + var graph = stack[index].graph; + for (var i = 0; i < actions.length; i++) { + graph = actions[i](graph); + } + + return {graph: graph, annotation: annotation}; + } + + function change(previous) { + dispatch.change(history.graph().difference(previous)); } var history = { graph: function () { - return stack[index]; + return stack[index].graph; }, merge: function (graph) { for (var i = 0; i < stack.length; i++) { - stack[i] = stack[i].merge(graph); + stack[i].graph = stack[i].graph.merge(graph); } }, - perform: function (action) { + perform: function () { + var previous = stack[index].graph; + stack = stack.slice(0, index + 1); - stack.push(action(this.graph())); + stack.push(perform(arguments)); index++; - maybeChange(); + + change(previous); }, - replace: function (action) { + replace: function () { + var previous = stack[index].graph; + // assert(index == stack.length - 1) - stack[index] = action(this.graph()); - maybeChange(); + stack[index] = perform(arguments); + + change(previous); }, undo: function () { + var previous = stack[index].graph; + while (index > 0) { index--; if (stack[index].annotation) break; } - dispatch.change(); + + change(previous); }, redo: function () { + var previous = stack[index].graph; + while (index < stack.length - 1) { index++; if (stack[index].annotation) break; } - dispatch.change(); + + change(previous); }, undoAnnotation: function () { @@ -64,34 +91,25 @@ iD.History = function() { } }, - // generate reports of changes for changesets to use - modify: function () { - return stack[index].modifications(); - }, - - create: function () { - return stack[index].creations(); - }, - - 'delete': function () { - return _.difference( - _.pluck(stack[0].entities, 'id'), - _.pluck(stack[index].entities, 'id') - ).map(function (id) { - return stack[0].fetch(id); - }); - }, - changes: function () { + var initial = stack[0].graph, + current = stack[index].graph; + return { - modify: this.modify(), - create: this.create(), - 'delete': this['delete']() + modified: current.modified().map(function (id) { + return current.fetch(id); + }), + created: current.created().map(function (id) { + return current.fetch(id); + }), + deleted: current.deleted().map(function (id) { + return initial.fetch(id); + }) }; }, reset: function () { - stack = [iD.Graph()]; + stack = [{graph: iD.Graph()}]; index = 0; dispatch.change(); } diff --git a/js/id/id.js b/js/id/id.js index d75692706..4700d339e 100644 --- a/js/id/id.js +++ b/js/id/id.js @@ -1,6 +1,7 @@ window.iD = function(container) { var connection = iD.Connection() .url('http://api06.dev.openstreetmap.org'), + // .url('http://www.openstreetmap.org'), history = iD.History(), map = iD.Map() .connection(connection) diff --git a/js/id/modes/add_area.js b/js/id/modes/add_area.js index cf5360112..ae19f193d 100644 --- a/js/id/modes/add_area.js +++ b/js/id/modes/add_area.js @@ -7,29 +7,41 @@ iD.modes.AddArea = function() { }; mode.enter = function() { - mode.map.dblclickEnable(false); - mode.map.hint('Click on the map to start drawing an area, like a park, lake, or building.'); + var map = mode.map, + history = mode.history, + controller = mode.controller; - mode.map.surface.on('click.addarea', function() { + map.dblclickEnable(false) + .hint('Click on the map to start drawing an area, like a park, lake, or building.'); + + map.surface.on('click.addarea', function() { var datum = d3.select(d3.event.target).datum() || {}, - node, - way = iD.Way({tags: { building: 'yes', area: 'yes', elastic: 'true' }}); + way = iD.Way({tags: { building: 'yes', area: 'yes' }}); - // connect a way to an existing way if (datum.type === 'node') { - node = datum; + // start from an existing node + history.perform( + iD.actions.AddWay(way), + iD.actions.AddWayNode(way.id, datum.id), + iD.actions.AddWayNode(way.id, datum.id), + 'started an area'); + } else { - node = iD.Node({loc: mode.map.mouseCoordinates()}); + // start from a new node + var node = iD.Node({loc: map.mouseCoordinates()}); + history.perform( + iD.actions.AddWay(way), + iD.actions.AddNode(node), + iD.actions.AddWayNode(way.id, node.id), + iD.actions.AddWayNode(way.id, node.id), + 'started an area'); } - mode.history.perform(iD.actions.StartWay(way)); - mode.history.perform(iD.actions.AddWayNode(way, node)); - - mode.controller.enter(iD.modes.DrawArea(way.id)); + controller.enter(iD.modes.DrawArea(way.id)); }); - mode.map.keybinding().on('⎋.addarea', function() { - mode.controller.exit(); + map.keybinding().on('⎋.addarea', function() { + controller.exit(); }); }; diff --git a/js/id/modes/add_place.js b/js/id/modes/add_place.js index 576c5fc6f..fe16ac20c 100644 --- a/js/id/modes/add_place.js +++ b/js/id/modes/add_place.js @@ -6,16 +6,24 @@ iD.modes.AddPlace = function() { }; mode.enter = function() { - mode.map.hint('Click on the map to add a place.'); + var map = mode.map, + history = mode.history, + controller = mode.controller; - mode.map.surface.on('click.addplace', function() { - var node = iD.Node({loc: mode.map.mouseCoordinates(), _poi: true}); - mode.history.perform(iD.actions.AddNode(node)); - mode.controller.enter(iD.modes.Select(node)); + map.hint('Click on the map to add a place.'); + + map.surface.on('click.addplace', function() { + var node = iD.Node({loc: map.mouseCoordinates(), _poi: true}); + + history.perform( + iD.actions.AddNode(node), + 'added a place'); + + controller.enter(iD.modes.Select(node)); }); - mode.map.keybinding().on('⎋.addplace', function() { - mode.controller.exit(); + map.keybinding().on('⎋.addplace', function() { + controller.exit(); }); }; diff --git a/js/id/modes/add_road.js b/js/id/modes/add_road.js index e1d4b6c59..471bba894 100644 --- a/js/id/modes/add_road.js +++ b/js/id/modes/add_road.js @@ -7,55 +7,62 @@ iD.modes.AddRoad = function() { }; mode.enter = function() { - mode.map.dblclickEnable(false); + var map = mode.map, + node, + history = mode.history, + controller = mode.controller; - mode.map.hint('Click on the map to start drawing an road, path, or route.'); + map.dblclickEnable(false) + .hint('Click on the map to start drawing an road, path, or route.'); - mode.map.surface.on('click.addroad', function() { + map.surface.on('click.addroad', function() { var datum = d3.select(d3.event.target).datum() || {}, - node, - direction = 'forward', - start = true, - way = iD.Way({ tags: { highway: 'residential', elastic: 'true' } }); + way = iD.Way({ tags: { highway: 'residential' } }), + direction = 'forward'; if (datum.type === 'node') { // continue an existing way - node = datum; - var id = datum.id; - var parents = mode.history.graph().parentWays(id); - if (parents.length) { - if (parents[0].nodes[0] === id) { - way = parents[0]; - direction = 'backward'; - start = false; - } else if (_.last(parents[0].nodes) === id) { - way = parents[0]; - start = false; - } + var parents = history.graph().parentWays(id); + if (parents.length && parents[0].nodes[0] === id) { + way = parents[0]; + direction = 'backward'; + } else if (parents.length && _.last(parents[0].nodes) === id) { + way = parents[0]; + } else { + history.perform( + iD.actions.AddWay(way), + iD.actions.AddWayNode(way.id, datum.id), + 'started a road'); } + } else if (datum.type === 'way') { // begin a new way starting from an existing way - node = iD.Node({loc: mode.map.mouseCoordinates()}); + node = iD.Node({loc: map.mouseCoordinates()}), + index = iD.util.geo.chooseIndex(datum, d3.mouse(map.surface.node()), map); + + history.perform( + iD.actions.AddWay(way), + iD.actions.AddWayNode(datum.id, node, index), + iD.actions.AddWayNode(way.id, node.id), + 'started a road'); - var index = iD.util.geo.chooseIndex(datum, d3.mouse(mode.map.surface.node()), mode.map); - var connectedWay = mode.history.graph().entity(datum.id); - mode.history.perform(iD.actions.AddWayNode(connectedWay, node, index)); } else { // begin a new way - node = iD.Node({loc: mode.map.mouseCoordinates()}); + node = iD.Node({loc: map.mouseCoordinates()}); + + history.perform( + iD.actions.AddWay(way), + iD.actions.AddNode(node), + iD.actions.AddWayNode(way.id, node.id), + 'started a road'); } - if (start) { - mode.history.perform(iD.actions.StartWay(way)); - mode.history.perform(iD.actions.AddWayNode(way, node)); - } - - mode.controller.enter(iD.modes.DrawRoad(way.id, direction)); + controller.enter(iD.modes.DrawRoad(way.id, direction)); }); - mode.map.keybinding().on('⎋.addroad', function() { - mode.controller.exit(); + map.keybinding().on('⎋.addroad', function() { + controller.exit(); }); }; diff --git a/js/id/modes/browse.js b/js/id/modes/browse.js index 0e6789f77..a94511d7a 100644 --- a/js/id/modes/browse.js +++ b/js/id/modes/browse.js @@ -7,6 +7,7 @@ iD.modes.Browse = function() { }; mode.enter = function() { + iD.modes._dragFeatures(mode); mode.map.surface.on('click.browse', function () { var datum = d3.select(d3.event.target).datum(); if (datum instanceof iD.Entity) { @@ -16,6 +17,7 @@ iD.modes.Browse = function() { }; mode.exit = function() { + mode.map.surface.on('mousedown.latedrag', null); mode.map.surface.on('click.browse', null); }; diff --git a/js/id/modes/drag_features.js b/js/id/modes/drag_features.js new file mode 100644 index 000000000..7a38883ed --- /dev/null +++ b/js/id/modes/drag_features.js @@ -0,0 +1,44 @@ +iD.modes._dragFeatures = function(mode) { + var dragging; + + var dragbehavior = d3.behavior.drag() + .origin(function(entity) { + var p = mode.map.projection(entity.loc); + return { x: p[0], y: p[1] }; + }) + .on('drag', function(entity) { + d3.event.sourceEvent.stopPropagation(); + + var loc = mode.map.projection.invert([d3.event.x, d3.event.y]); + + if (!dragging) { + if (entity.accuracy) { + dragging = iD.Node({loc: loc}); + mode.history.perform( + iD.actions.AddNode(dragging), + iD.actions.AddWayNode(entity.way, dragging.id, entity.index)); + } else { + dragging = entity; + mode.history.perform( + iD.actions.Move(dragging.id, loc)); + } + } + + mode.history.replace(iD.actions.Move(dragging.id, loc)); + }) + .on('dragend', function (entity) { + if (!dragging) return; + dragging = undefined; + + mode.history.replace( + iD.actions.Noop(), + entity.accuracy ? 'added a node to a way' : 'moved a node'); + }); + + mode.map.surface + .call(dragbehavior) + .call(d3.latedrag() + .filter(function(d) { + return (d.type === 'node' || d.accuracy); + })); +}; diff --git a/js/id/modes/draw_area.js b/js/id/modes/draw_area.js index 70ef0aaee..0d3fa7c9d 100644 --- a/js/id/modes/draw_area.js +++ b/js/id/modes/draw_area.js @@ -1,73 +1,80 @@ -iD.modes.DrawArea = function(way_id) { +iD.modes.DrawArea = function(wayId) { var mode = { button: 'area' }; mode.enter = function() { + var map = mode.map, + history = mode.history, + controller = mode.controller, + way = history.graph().entity(wayId), + headId = _.last(way.nodes), + tailId = _.first(way.nodes), + node = iD.Node({loc: map.mouseCoordinates()}); - mode.map.hint('Click on the map to add points to your area. Finish the ' + + map.dblclickEnable(false) + .fastEnable(false) + .hint('Click on the map to add points to your area. Finish the ' + 'area by clicking on your first point'); - mode.map.dblclickEnable(false); - var way = mode.history.graph().entity(way_id), - firstnode_id = _.first(way.nodes), - node = iD.Node({loc: mode.map.mouseCoordinates()}); + history.perform( + iD.actions.AddNode(node), + iD.actions.AddWayNode(way.id, node.id, -1)); - function finish(next) { - way = mode.history.graph().entity(way.id); - way.tags = _.omit(way.tags, 'elastic'); - mode.history.perform(iD.actions.ChangeEntityTags(way, way.tags)); - return mode.controller.enter(next); - } - - mode.history.perform(iD.actions.AddWayNode(way, node)); - - mode.map.surface.on('mousemove.drawarea', function() { - mode.history.replace(iD.actions.AddWayNode(way, node.update({loc: mode.map.mouseCoordinates()}))); + map.surface.on('mousemove.drawarea', function() { + history.replace(iD.actions.Move(node.id, map.mouseCoordinates())); }); - mode.map.surface.on('click.drawarea', function() { - d3.event.stopPropagation(); + map.surface.on('click.drawarea', function() { + var datum = d3.select(d3.event.target).datum() || {}; - var datum = d3.select(d3.event.target).datum(); + if (datum.id === tailId) { + history.replace( + iD.actions.DeleteNode(node.id), + iD.actions.AddWayNode(way.id, tailId, -1), + 'added to an area'); - if (datum.type === 'node') { - if (datum.id == firstnode_id) { - mode.history.replace(iD.actions.DeleteNode(node)); - mode.history.replace(iD.actions.AddWayNode(way, - mode.history.graph().entity(way.nodes[0]))); + controller.enter(iD.modes.Select(way)); + + } else if (datum.type === 'node' && datum.id !== node.id) { + // connect the way to an existing node + history.replace( + iD.actions.DeleteNode(node.id), + iD.actions.AddWayNode(way.id, datum.id, -1), + 'added to an area'); + + controller.enter(iD.modes.DrawArea(wayId)); - return finish(iD.modes.Select(way)); - } else { - // connect a way to an existing way - mode.history.replace(iD.actions.AddWayNode(way, datum)); - } } else { - node = node.update({loc: mode.map.mouseCoordinates()}); - mode.history.replace(iD.actions.AddWayNode(way, node)); + history.replace( + iD.actions.Noop(), + 'added to an area'); + + controller.enter(iD.modes.DrawArea(wayId)); } - - mode.controller.enter(iD.modes.DrawArea(way_id)); }); - mode.map.keybinding().on('⎋.drawarea', function() { - finish(iD.modes.Browse()); + map.keybinding().on('⎋.drawarea', function() { + history.replace( + iD.actions.DeleteNode(node.id)); + + controller.enter(iD.modes.Browse()); }); - mode.map.keybinding().on('⌫.drawarea', function() { + map.keybinding().on('⌫.drawarea', function() { d3.event.preventDefault(); - var lastNode = _.last(way.nodes); - mode.history.replace(iD.actions.RemoveWayNode(way, - mode.history.graph().entity(lastNode))); - mode.history.replace(iD.actions.DeleteNode( - mode.history.graph().entity(lastNode))); - mode.history.replace(iD.actions.DeleteNode(node)); - mode.controller.enter(iD.modes.DrawArea(way_id)); + + history.replace( + iD.actions.DeleteNode(node.id), + iD.actions.DeleteNode(headId)); + + controller.enter(iD.modes.DrawArea(wayId)); }); }; mode.exit = function() { mode.map.hint(false); + mode.map.fastEnable(true); mode.map.surface .on('mousemove.drawarea', null) .on('click.drawarea', null); diff --git a/js/id/modes/draw_road.js b/js/id/modes/draw_road.js index bd4ad1507..56058e017 100644 --- a/js/id/modes/draw_road.js +++ b/js/id/modes/draw_road.js @@ -1,94 +1,93 @@ -iD.modes.DrawRoad = function(way_id, direction) { +iD.modes.DrawRoad = function(wayId, direction) { var mode = { button: 'road' }; mode.enter = function() { - mode.map.dblclickEnable(false) - .dragEnable(false) + var map = mode.map, + history = mode.history, + controller = mode.controller, + way = history.graph().entity(wayId), + node = iD.Node({loc: map.mouseCoordinates()}), + index = (direction === 'forward') ? undefined : 0, + headId = (direction === 'forward') ? _.last(way.nodes) : _.first(way.nodes), + tailId = (direction === 'forward') ? _.first(way.nodes) : _.last(way.nodes); + + map.dblclickEnable(false) .fastEnable(false) .hint('Click to add more points to the road. ' + - 'Click on other roads to connect to them, and double-click to ' + - 'end the road.'); + 'Click on other roads to connect to them, and double-click to ' + + 'end the road.'); - var index = (direction === 'forward') ? undefined : -1, - node = iD.Node({loc: mode.map.mouseCoordinates(), tags: { elastic: true } }), - way = mode.history.graph().entity(way_id), - firstNode = way.nodes[0], - lastNode = _.last(way.nodes); + history.perform( + iD.actions.AddNode(node), + iD.actions.AddWayNode(wayId, node.id, index)); - function finish(next) { - way.tags = _.omit(way.tags, 'elastic'); - mode.history.perform(iD.actions.ChangeEntityTags(way, way.tags)); - return mode.controller.enter(next); - } - - mode.history.perform(iD.actions.AddWayNode(way, node, index)); - - mode.map.surface.on('mousemove.drawroad', function() { - mode.history.replace(iD.actions.AddWayNode(way, - node.update({ loc: mode.map.mouseCoordinates() }), index)); + map.surface.on('mousemove.drawroad', function() { + history.replace(iD.actions.Move(node.id, map.mouseCoordinates())); }); - mode.map.surface.on('click.drawroad', function() { - // d3.event.stopPropagation(); - + map.surface.on('click.drawroad', function() { var datum = d3.select(d3.event.target).datum() || {}; + if (datum.id === tailId) { + // connect the way in a loop + history.replace( + iD.actions.DeleteNode(node.id), + iD.actions.AddWayNode(wayId, tailId, index), + 'added to a road'); + + controller.enter(iD.modes.Select(way)); + + } else if (datum.id === headId) { + // finish the way + history.replace(iD.actions.DeleteNode(node.id)); + + controller.enter(iD.modes.Select(way)); + + } else if (datum.type === 'node' && datum.id !== node.id) { + // connect the way to an existing node + history.replace( + iD.actions.DeleteNode(node.id), + iD.actions.AddWayNode(wayId, datum.id, index), + 'added to a road'); + + controller.enter(iD.modes.DrawRoad(wayId, direction)); - if (datum.type === 'node') { - if (datum.id == firstNode || datum.id == lastNode) { - // If mode is drawing a loop and mode is not the drawing - // end of the stick, finish the circle - if (direction === 'forward' && datum.id == firstNode) { - mode.history.replace(iD.actions.AddWayNode(way, - mode.history.graph().entity(firstNode), index)); - } else if (direction === 'backward' && datum.id == lastNode) { - mode.history.replace(iD.actions.AddWayNode(way, - mode.history.graph().entity(lastNode), index)); - } - mode.history.replace(iD.actions.DeleteNode(node)); - return finish(iD.modes.Select(way)); - } else if (datum.id == node.id) { - datum = datum.update({ tags: {} }); - mode.history.replace(iD.actions.ChangeEntityTags(datum, {})); - mode.history.replace(iD.actions.DeleteNode(node)); - mode.history.replace(iD.actions.AddWayNode(way, datum, index)); - } else { - // connect a way to an existing way - mode.history.replace(iD.actions.DeleteNode(node)); - mode.history.replace(iD.actions.AddWayNode(way, datum, index)); - } } else if (datum.type === 'way') { - node = node.update({loc: mode.map.mouseCoordinates() }); - mode.history.replace(iD.actions.AddWayNode(way, node, index)); + // connect the way to an existing way + var connectedIndex = iD.modes.chooseIndex(datum, d3.mouse(map.surface.node()), map); + + history.replace( + iD.actions.AddWayNode(datum.id, node.id, connectedIndex), + 'added to a road'); + + controller.enter(iD.modes.DrawRoad(wayId, direction)); - var connectedWay = mode.history.graph().entity(datum.id); - var connectedIndex = iD.modes.chooseIndex(datum, - d3.mouse(mode.map.surface.node()), - mode.map); - mode.history.perform(iD.actions.AddWayNode(connectedWay, - node, - connectedIndex)); } else { - mode.history.replace(iD.actions.AddWayNode(way, node, index)); + history.replace( + iD.actions.Noop(), + 'added to a road'); + + controller.enter(iD.modes.DrawRoad(wayId, direction)); } - - mode.controller.enter(iD.modes.DrawRoad(way_id, direction)); }); - mode.map.keybinding().on('⎋.drawroad', function() { - finish(iD.modes.Browse()); + map.keybinding().on('⎋.drawroad', function() { + history.replace( + iD.actions.DeleteNode(node.id)); + + controller.enter(iD.modes.Browse()); }); - mode.map.keybinding().on('⌫.drawroad', function() { + map.keybinding().on('⌫.drawroad', function() { d3.event.preventDefault(); - mode.history.replace(iD.actions.RemoveWayNode(way, - mode.history.graph().entity(lastNode))); - mode.history.replace(iD.actions.DeleteNode( - mode.history.graph().entity(lastNode))); - mode.history.replace(iD.actions.DeleteNode(node)); - mode.controller.enter(iD.modes.DrawRoad(way_id, direction)); + + history.replace( + iD.actions.DeleteNode(node.id), + iD.actions.DeleteNode(headId)); + + controller.enter(iD.modes.DrawRoad(wayId, direction)); }); }; @@ -103,7 +102,6 @@ iD.modes.DrawRoad = function(way_id, direction) { .on('⌫.drawroad', null); window.setTimeout(function() { mode.map.dblclickEnable(true); - mode.map.dragEnable(true); }, 1000); }; diff --git a/js/id/modes/select.js b/js/id/modes/select.js index fe18dba6d..60fcbadfd 100644 --- a/js/id/modes/select.js +++ b/js/id/modes/select.js @@ -1,8 +1,9 @@ iD.modes.Select = function (entity) { var mode = { - button: '' - }, - inspector = iD.Inspector(), + button: 'browse' + }; + + var inspector = iD.Inspector(), dragging, target; var dragWay = d3.behavior.drag() @@ -11,25 +12,23 @@ iD.modes.Select = function (entity) { return { x: p[0], y: p[1] }; }) .on('drag', function(entity) { - if (!mode.map.dragEnable()) return; - d3.event.sourceEvent.stopPropagation(); if (!dragging) { - dragging = iD.util.trueObj([entity.id].concat( - _.pluck(mode.history.graph().parentWays(entity.id), 'id'))); + dragging = true; mode.history.perform(iD.actions.Noop()); } entity.nodes.forEach(function(node) { var start = mode.map.projection(node.loc); - var end = mode.map.projection.invert([start[0] + d3.event.dx, start[1] + d3.event.dy]); - node.loc = end; - mode.history.replace(iD.actions.Move(node, end)); + var end = mode.map.projection.invert([ + start[0] + d3.event.dx, + start[1] + d3.event.dy]); + mode.history.replace(iD.actions.Move(node.id, end)); }); }) .on('dragend', function () { - if (!mode.map.dragEnable() || !dragging) return; + if (!dragging) return; dragging = undefined; mode.map.redraw(); }); @@ -37,16 +36,22 @@ iD.modes.Select = function (entity) { function remove() { switch (entity.type) { case 'way': - mode.history.perform(iD.actions.DeleteWay(entity)); + mode.history.perform( + iD.actions.DeleteWay(entity.id), + 'deleted a way'); break; case 'node': - mode.history.perform(iD.actions.DeleteNode(entity)); + mode.history.perform( + iD.actions.DeleteNode(entity.id), + 'deleted a node'); } mode.controller.exit(); } mode.enter = function () { + iD.modes._dragFeatures(mode); + target = mode.map.surface.selectAll("*") .filter(function (d) { return d === entity; }); @@ -57,11 +62,23 @@ iD.modes.Select = function (entity) { .call(inspector); inspector.on('changeTags', function(d, tags) { - mode.history.perform(iD.actions.ChangeEntityTags(mode.history.graph().entity(d.id), tags)); + mode.history.perform( + iD.actions.ChangeEntityTags(d.id, tags), + 'changed tags'); + }).on('changeWayDirection', function(d) { - mode.history.perform(iD.actions.ReverseWay(d)); + mode.history.perform( + iD.actions.ReverseWay(d.id), + 'reversed a way'); + + }).on('splitWay', function(d) { + mode.history.perform( + iD.actions.SplitWay(d.id), + 'split a way on a node'); + }).on('remove', function() { remove(); + }).on('close', function() { mode.controller.exit(); }); @@ -88,6 +105,8 @@ iD.modes.Select = function (entity) { }; mode.exit = function () { + mode.map.surface.on('mousedown.latedrag', null); + d3.select('.inspector-wrap') .style('display', 'none'); diff --git a/js/id/renderer/background.js b/js/id/renderer/background.js index 275a6f2dc..35fb4ea29 100644 --- a/js/id/renderer/background.js +++ b/js/id/renderer/background.js @@ -67,12 +67,20 @@ iD.Background = function() { image.exit().remove(); + function load(d) { + cache[d] = true; + d3.select(this).on('load', null); + } + + function error() { + d3.select(this).remove(); + } + image.enter().append('img') .attr('class', 'tile') .attr('src', function(d) { return d[3]; }) - .on('load', function(d) { - cache[d] = true; - }); + .on('error', error) + .on('load', load); function tileSize(d) { return Math.ceil(256 * Math.pow(2, z - d[2])) / 256; diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js index 11fff6390..d5b8d9950 100644 --- a/js/id/renderer/map.js +++ b/js/id/renderer/map.js @@ -12,38 +12,8 @@ iD.Map = function() { .scaleExtent([1024, 256 * Math.pow(2, 24)]) .on('zoom', zoomPan), dblclickEnabled = true, - dragEnabled = true, + dragging = false, fastEnabled = true, - dragging, - dragbehavior = d3.behavior.drag() - .origin(function(entity) { - if (!dragEnabled) return { x: 0, y: 0 }; - var p = projection(entity.loc); - return { x: p[0], y: p[1] }; - }) - .on('drag', function(entity) { - d3.event.sourceEvent.stopPropagation(); - - if (!dragging) { - if (entity.accuracy) { - var way = history.graph().entity(entity.way), - index = entity.index; - entity = iD.Node(entity); - history.perform(iD.actions.AddWayNode(way, entity, index)); - } - - dragging = iD.util.trueObj([entity.id].concat( - _.pluck(history.graph().parentWays(entity.id), 'id'))); - history.perform(iD.actions.Noop()); - } - - var to = projection.invert([d3.event.x, d3.event.y]); - history.replace(iD.actions.Move(entity, to)); - }) - .on('dragend', function () { - if (!dragEnabled || !dragging) return; - dragging = undefined; - }), background = iD.Background() .projection(projection), class_stroke = iD.Style.styleClasses('stroke'), @@ -51,18 +21,7 @@ iD.Map = function() { class_area = iD.Style.styleClasses('area'), class_casing = iD.Style.styleClasses('casing'), transformProp = iD.util.prefixProperty('Transform'), - support3d = (function() { - // test for translate3d support. Based on https://gist.github.com/3794226 by lorenzopolidori and webinista - var el = document.createElement('div'), - has3d = false; - document.body.insertBefore(el,null); - if (el.style[transformProp] !== undefined) { - el.style[transformProp] = 'translate3d(1px,1px,1px)'; - has3d = window.getComputedStyle(el).getPropertyValue(transformProp); - } - document.body.removeChild(el); - return (has3d && has3d.length>0 && has3d!=="none"); - })(), + support3d = iD.util.support3d(), supersurface, surface, defs, tilegroup, r, g, alength; function map() { @@ -90,7 +49,7 @@ iD.Map = function() { .attr('clip-path', 'url(#clip)'); g = ['fill', 'casing', 'stroke', 'text', 'hit', 'temp'].reduce(function(mem, i) { - return (mem[i] = r.append('g').attr('class', 'layer-g')) && mem; + return (mem[i] = r.append('g').attr('class', 'layer-g layer-' + i)) && mem; }, {}); var arrow = surface.append('text').text('►----'); @@ -110,21 +69,30 @@ iD.Map = function() { return 'M' + _.pluck(d.nodes, 'loc').map(projection).map(iD.util.geo.roundCoords).join('L'); } - function drawVector(only) { + function drawVector(difference) { if (surface.style(transformProp) != 'none') return; - var all = [], ways = [], areas = [], points = [], waynodes = [], + var filter, all, ways = [], areas = [], points = [], waynodes = [], extent = map.extent(), graph = history.graph(); - if (!only) { + if (!difference) { all = graph.intersects(extent); + filter = d3.functor(true); } else { - for (var id in only) all.push(graph.fetch(id)); + var only = {}; + difference.forEach(function (id) { + var entity = graph.fetch(id); + if (entity) { + only[id] = entity; + graph.parentWays(id).forEach(function (entity) { + only[entity.id] = graph.fetch(entity.id); + }); + } + }); + all = _.values(only); + filter = function(d) { return d.accuracy ? only[d.way] : only[d.id]; }; } - var filter = only ? - function(d) { return only[d.id]; } : function() { return true; }; - if (all.length > 200000) return hideVector(); for (var i = 0; i < all.length; i++) { @@ -158,8 +126,7 @@ iD.Map = function() { loc: iD.util.geo.interp(way.nodes[i].loc, way.nodes[i + 1].loc, 0.5), way: way.id, index: i + 1, - accuracy: true, - tags: { name: 'Improve way accuracy' } + accuracy: true }); } } @@ -171,30 +138,32 @@ iD.Map = function() { .filter(filter) .data(waynodes, key); function olderOnTop(a, b) { - return (+a.id.slice(1)) - (+b.id.slice(1)); + return a.osmId() - b.osmId(); } handles.exit().remove(); handles.enter().append('image') - .attr({ width: 6, height: 6, 'class': 'handle', 'xlink:href': 'css/handle.png' }) - .each(function(d) { - if (d.tags && d.tags.elastic) return; - d3.select(this).call(dragbehavior); + .attr({ + width: 6, + height: 6, + 'class': 'handle', + 'xlink:href': 'css/handle.png' }); handles.attr('transform', function(entity) { var p = projection(entity.loc); - return 'translate(' + [~~p[0], ~~p[1]] + ') translate(-3, -3) rotate(45, 3, 3)'; + return 'translate(' + [~~p[0], ~~p[1]] + + ') translate(-3, -3) rotate(45, 3, 3)'; }) .classed('active', classActive) .sort(olderOnTop); } - function drawAccuracyHandles(waynodes) { + function drawAccuracyHandles(waynodes, filter) { var handles = g.hit.selectAll('circle.accuracy-handle') - .data(waynodes, key); + .filter(filter) + .data(waynodes, function (d) { return [d.way, d.index].join(","); }); handles.exit().remove(); handles.enter().append('circle') - .attr({ r: 2, 'class': 'accuracy-handle' }) - .call(dragbehavior); + .attr({ r: 2, 'class': 'accuracy-handle' }); handles.attr('transform', function(entity) { var p = projection(entity.loc); return 'translate(' + [~~p[0], ~~p[1]] + ')'; @@ -236,8 +205,7 @@ iD.Map = function() { .data(points, key); markers.exit().remove(); var marker = markers.enter().append('g') - .attr('class', 'marker') - .call(dragbehavior); + .attr('class', 'marker'); marker.append('circle') .attr({ r: 10, cx: 8, cy: 8 }); marker.append('image') @@ -282,14 +250,14 @@ iD.Map = function() { function connectionLoad(err, result) { history.merge(result); - drawVector(iD.util.trueObj(Object.keys(result.entities))); + drawVector(Object.keys(result.entities)); } function hoverIn() { var datum = d3.select(d3.event.target).datum(); if (datum instanceof iD.Entity) { hover = datum.id; - drawVector(iD.util.trueObj([hover])); + drawVector([hover]); d3.select('.messages').text(datum.tags.name || '#' + datum.id); } } @@ -298,7 +266,7 @@ iD.Map = function() { if (hover) { var oldHover = hover; hover = null; - drawVector(iD.util.trueObj([oldHover])); + drawVector([oldHover]); d3.select('.messages').text(''); } } @@ -340,14 +308,12 @@ iD.Map = function() { redraw(); } - function redraw() { - if (!dragging) { - dispatch.move(map); - tilegroup.call(background); - } + function redraw(difference) { + dispatch.move(map); + tilegroup.call(background); if (map.zoom() > 16) { connection.loadTiles(projection); - drawVector(dragging); + drawVector(difference); } else { hideVector(); } @@ -376,12 +342,6 @@ iD.Map = function() { return map; }; - map.dragEnable = function(_) { - if (!arguments.length) return dragEnabled; - dragEnabled = _; - return map; - }; - map.fastEnable = function(_) { if (!arguments.length) return fastEnabled; fastEnabled = _; diff --git a/js/id/renderer/style.js b/js/id/renderer/style.js index bf6206c88..01bece5c8 100644 --- a/js/id/renderer/style.js +++ b/js/id/renderer/style.js @@ -50,7 +50,7 @@ iD.Style.markerimage = function(d) { iD.Style.TAG_CLASSES = iD.util.trueObj([ 'highway', 'railway', 'motorway', 'amenity', 'natural', - 'landuse', 'building', 'oneway', 'bridge', 'elastic' + 'landuse', 'building', 'oneway', 'bridge' ]); iD.Style.styleClasses = function(pre) { diff --git a/js/id/ui/commit.js b/js/id/ui/commit.js index 3a401d2f8..e5f3de828 100644 --- a/js/id/ui/commit.js +++ b/js/id/ui/commit.js @@ -10,7 +10,7 @@ iD.commit = function() { header.append('p').text('the changes you upload will be visible on all maps using OpenStreetMap data'); var section = body.selectAll('div.commit-section') - .data(['modify', 'delete', 'create'].filter(function(d) { + .data(['modified', 'deleted', 'created'].filter(function(d) { return changes[d].length; })) .enter() diff --git a/js/id/ui/inspector.js b/js/id/ui/inspector.js index ab3e08433..31a3e986a 100644 --- a/js/id/ui/inspector.js +++ b/js/id/ui/inspector.js @@ -1,5 +1,5 @@ iD.Inspector = function() { - var event = d3.dispatch('changeTags', 'changeWayDirection', 'update', 'remove', 'close'), + var event = d3.dispatch('changeTags', 'changeWayDirection', 'update', 'remove', 'close', 'splitWay'), taginfo = iD.taginfo(); function drawhead(selection) { @@ -10,7 +10,7 @@ iD.Inspector = function() { .attr('class', 'permalink') .attr('href', function(d) { return 'http://www.openstreetmap.org/browse/' + - d.type + '/' + d.id.slice(1); + d.type + '/' + d.osmId(); }) .text('View on OSM'); selection.append('a') @@ -32,9 +32,16 @@ iD.Inspector = function() { .attr('href', '#') .text('Reverse Direction') .on('click', function(d) { - event.changeWayDirection(iD.Entity(d, { - nodes: _.pluck(d.nodes.reverse(), 'id') - })); + event.changeWayDirection(iD.Entity(d)); + }); + } + if (selection.datum().type === 'node' && !selection.datum()._poi) { + selection.append('a') + .attr('class', 'permalink') + .attr('href', '#') + .text('Split Way') + .on('click', function(d) { + event.splitWay(iD.Entity(d)); }); } } diff --git a/js/id/ui/userpanel.js b/js/id/ui/userpanel.js index c38c9dffa..1904e0a7c 100644 --- a/js/id/ui/userpanel.js +++ b/js/id/ui/userpanel.js @@ -5,6 +5,7 @@ iD.userpanel = function(connection) { function update() { selection.html(''); if (connection.authenticated()) { + selection.style('display', 'block'); connection.userDetails(function(user_details) { selection.append('span') .text('signed in as ') @@ -24,12 +25,7 @@ iD.userpanel = function(connection) { }); }); } else { - selection - .append('a') - .attr('class', 'login') - .attr('href', '#') - .text('login') - .on('click', event.login); + selection.html('').style('display', 'none'); } } connection.on('auth', update); diff --git a/js/id/util.js b/js/id/util.js index ddc6fc070..bf5a2ef2d 100644 --- a/js/id/util.js +++ b/js/id/util.js @@ -1,13 +1,5 @@ iD.util = {}; -iD.util._counters = {}; - -iD.util.id = function(counter) { - counter = counter || 'default'; - if (!iD.util._counters[counter]) iD.util._counters[counter] = 0; - return counter[0] + (--iD.util._counters[counter]); -}; - iD.util.trueObj = function(arr) { var o = {}; for (var i = 0, l = arr.length; i < l; i++) o[arr[i]] = true; @@ -74,6 +66,20 @@ iD.util.prefixProperty = function(property) { })(prefixes); }; +iD.util.support3d = function() { + // test for translate3d support. Based on https://gist.github.com/3794226 by lorenzopolidori and webinista + var transformProp = iD.util.prefixProperty('Transform'); + var el = document.createElement('div'), + has3d = false; + document.body.insertBefore(el, null); + if (el.style[transformProp] !== undefined) { + el.style[transformProp] = 'translate3d(1px,1px,1px)'; + has3d = window.getComputedStyle(el).getPropertyValue(transformProp); + } + document.body.removeChild(el); + return (has3d && has3d.length>0 && has3d!=="none"); +}; + iD.util.geo = {}; iD.util.geo.roundCoords = function(c) { diff --git a/js/lib/d3.latedrag.js b/js/lib/d3.latedrag.js new file mode 100644 index 000000000..6ef3edd1e --- /dev/null +++ b/js/lib/d3.latedrag.js @@ -0,0 +1,22 @@ +d3.latedrag = function() { + var filter = d3.functor(true); + + function latedrag(selection) { + var mousedown = selection.on('mousedown.drag'); + selection.on('mousedown.drag', null); + selection.on('mousedown.latedrag', function() { + var datum = d3.select(d3.event.target).datum(); + if (datum && filter(datum)) { + mousedown.apply(selection.node(), [datum]); + } + }); + } + + latedrag.filter = function(_) { + if (!arguments.length) return filter; + filter = _; + return latedrag; + }; + + return latedrag; +}; diff --git a/js/lib/queue.js b/js/lib/queue.js new file mode 100644 index 000000000..9a3b9da47 --- /dev/null +++ b/js/lib/queue.js @@ -0,0 +1,84 @@ +(function() { + if (typeof module === "undefined") self.queue = queue; + else module.exports = queue; + + queue.version = "1.0.0"; + + function queue(parallelism) { + var queue = {}, + active = 0, // number of in-flight deferrals + remaining = 0, // number of deferrals remaining + head, tail, // singly-linked list of deferrals + error = null, + results = [], + await = noop, + awaitAll; + + if (arguments.length < 1) parallelism = Infinity; + + queue.defer = function() { + if (!error) { + var node = arguments; + node.index = results.push(undefined) - 1; + if (tail) tail.next = node, tail = tail.next; + else head = tail = node; + ++remaining; + pop(); + } + return queue; + }; + + queue.await = function(f) { + await = f; + awaitAll = false; + if (!remaining) notify(); + return queue; + }; + + queue.awaitAll = function(f) { + await = f; + awaitAll = true; + if (!remaining) notify(); + return queue; + }; + + function pop() { + if (head && active < parallelism) { + var node = head, + f = node[0], + a = Array.prototype.slice.call(node, 1), + i = node.index; + if (head === tail) head = tail = null; + else head = head.next; + ++active; + a.push(function(e, r) { + --active; + if (error != null) return; + if (e != null) { + // clearing remaining cancels subsequent callbacks + // clearing head stops queued tasks from being executed + // setting error ignores subsequent calls to defer + error = e; + remaining = results = head = tail = null; + notify(); + } else { + results[i] = r; + if (--remaining) pop(); + else notify(); + } + }); + f.apply(null, a); + } + } + + function notify() { + if (error != null) await(error); + else if (awaitAll) await(null, results); + else await.apply(null, [null].concat(results)); + } + + return queue; + } + + function noop() {} +})(); diff --git a/test/index.html b/test/index.html index f5234875f..e904c9276 100644 --- a/test/index.html +++ b/test/index.html @@ -45,6 +45,7 @@ + @@ -54,7 +55,6 @@ - @@ -83,10 +83,18 @@ + + + + + + + + diff --git a/test/index_packaged.html b/test/index_packaged.html index f1e70b069..8271e3886 100644 --- a/test/index_packaged.html +++ b/test/index_packaged.html @@ -25,10 +25,18 @@ + + + + + + + + diff --git a/test/spec/actions/add_node.js b/test/spec/actions/add_node.js new file mode 100644 index 000000000..ae066edc5 --- /dev/null +++ b/test/spec/actions/add_node.js @@ -0,0 +1,7 @@ +describe("iD.actions.AddNode", function () { + it("adds a node to the graph", function () { + var node = iD.Node(), + graph = iD.actions.AddNode(node)(iD.Graph()); + expect(graph.entity(node.id)).to.equal(node); + }); +}); diff --git a/test/spec/actions/add_way.js b/test/spec/actions/add_way.js new file mode 100644 index 000000000..19948c5b2 --- /dev/null +++ b/test/spec/actions/add_way.js @@ -0,0 +1,7 @@ +describe("iD.actions.AddWay", function () { + it("adds a way to the graph", function () { + var way = iD.Way(), + graph = iD.actions.AddWay(way)(iD.Graph()); + expect(graph.entity(way.id)).to.equal(way); + }); +}); diff --git a/test/spec/actions/add_way_node.js b/test/spec/actions/add_way_node.js index e1df86158..5c774e5e4 100644 --- a/test/spec/actions/add_way_node.js +++ b/test/spec/actions/add_way_node.js @@ -2,14 +2,28 @@ describe("iD.actions.AddWayNode", function () { it("adds a node to the end of a way", function () { var way = iD.Way(), node = iD.Node({id: "n1"}), - graph = iD.actions.AddWayNode(way, node)(iD.Graph()); + graph = iD.actions.AddWayNode(way.id, node.id)(iD.Graph([way, node])); expect(graph.entity(way.id).nodes).to.eql(["n1"]); }); - it("adds a node to a way at the specified index", function () { + it("adds a node to a way at index 0", function () { var way = iD.Way({nodes: ["n1", "n3"]}), node = iD.Node({id: "n2"}), - graph = iD.actions.AddWayNode(way, node, 1)(iD.Graph()); + graph = iD.actions.AddWayNode(way.id, node.id, 0)(iD.Graph([way, node])); + expect(graph.entity(way.id).nodes).to.eql(["n2", "n1", "n3"]); + }); + + it("adds a node to a way at a positive index", function () { + var way = iD.Way({nodes: ["n1", "n3"]}), + node = iD.Node({id: "n2"}), + graph = iD.actions.AddWayNode(way.id, node.id, 1)(iD.Graph([way, node])); + expect(graph.entity(way.id).nodes).to.eql(["n1", "n2", "n3"]); + }); + + it("adds a node to a way at a negative index", function () { + var way = iD.Way({nodes: ["n1", "n3"]}), + node = iD.Node({id: "n2"}), + graph = iD.actions.AddWayNode(way.id, node.id, -1)(iD.Graph([way, node])); expect(graph.entity(way.id).nodes).to.eql(["n1", "n2", "n3"]); }); }); diff --git a/test/spec/actions/change_entity_tags.js b/test/spec/actions/change_entity_tags.js new file mode 100644 index 000000000..bfa9830c6 --- /dev/null +++ b/test/spec/actions/change_entity_tags.js @@ -0,0 +1,8 @@ +describe("iD.actions.ChangeEntityTags", function () { + it("changes an entity's tags", function () { + var entity = iD.Entity(), + tags = {foo: 'bar'}, + graph = iD.actions.ChangeEntityTags(entity.id, tags)(iD.Graph([entity])); + expect(graph.entity(entity.id).tags).to.eql(tags); + }); +}); diff --git a/test/spec/actions/delete_node.js b/test/spec/actions/delete_node.js index 82388b96c..382999b4f 100644 --- a/test/spec/actions/delete_node.js +++ b/test/spec/actions/delete_node.js @@ -1,7 +1,7 @@ describe("iD.actions.DeleteNode", function () { it("removes the node from the graph", function () { var node = iD.Node(), - action = iD.actions.DeleteNode(node), + action = iD.actions.DeleteNode(node.id), graph = action(iD.Graph([node])); expect(graph.entity(node.id)).to.be.undefined; }); @@ -9,15 +9,15 @@ describe("iD.actions.DeleteNode", function () { it("removes the node from parent ways", function () { var node = iD.Node(), way = iD.Way({nodes: [node.id]}), - action = iD.actions.DeleteNode(node), + action = iD.actions.DeleteNode(node.id), graph = action(iD.Graph([node, way])); expect(graph.entity(way.id).nodes).not.to.contain(node.id); }); it("removes the node from parent relations", function () { var node = iD.Node(), - relation = iD.Relation({members: [node.id]}), - action = iD.actions.DeleteNode(node), + relation = iD.Relation({members: [{ id: node.id }]}), + action = iD.actions.DeleteNode(node.id), graph = action(iD.Graph([node, relation])); expect(graph.entity(relation.id).members).not.to.contain(node.id); }); diff --git a/test/spec/actions/delete_way.js b/test/spec/actions/delete_way.js index 0323b75ee..2a569cb0e 100644 --- a/test/spec/actions/delete_way.js +++ b/test/spec/actions/delete_way.js @@ -1,23 +1,23 @@ describe("iD.actions.DeleteWay", function () { it("removes the way from the graph", function () { var way = iD.Way(), - action = iD.actions.DeleteWay(way), + action = iD.actions.DeleteWay(way.id), graph = action(iD.Graph([way])); expect(graph.entity(way.id)).to.be.undefined; }); it("removes a way from parent relations", function () { var way = iD.Way(), - relation = iD.Relation({members: [way.id]}), - action = iD.actions.DeleteWay(way), + relation = iD.Relation({members: [{ id: way.id }]}), + action = iD.actions.DeleteWay(way.id), graph = action(iD.Graph([way, relation])); - expect(graph.entity(relation.id).members).not.to.contain(way.id); + expect(_.pluck(graph.entity(relation.id).members, 'id')).not.to.contain(way.id); }); it("deletes member nodes not referenced by another parent", function () { var node = iD.Node(), way = iD.Way({nodes: [node.id]}), - action = iD.actions.DeleteWay(way), + action = iD.actions.DeleteWay(way.id), graph = action(iD.Graph([node, way])); expect(graph.entity(node.id)).to.be.undefined; }); @@ -26,7 +26,7 @@ describe("iD.actions.DeleteWay", function () { var node = iD.Node(), way1 = iD.Way({nodes: [node.id]}), way2 = iD.Way({nodes: [node.id]}), - action = iD.actions.DeleteWay(way1), + action = iD.actions.DeleteWay(way1.id), graph = action(iD.Graph([node, way1, way2])); expect(graph.entity(node.id)).not.to.be.undefined; }); @@ -34,7 +34,7 @@ describe("iD.actions.DeleteWay", function () { it("does not delete member nodes with interesting tags", function () { var node = iD.Node({tags: {highway: 'traffic_signals'}}), way = iD.Way({nodes: [node.id]}), - action = iD.actions.DeleteWay(way), + action = iD.actions.DeleteWay(way.id), graph = action(iD.Graph([node, way])); expect(graph.entity(node.id)).not.to.be.undefined; }); @@ -42,7 +42,7 @@ describe("iD.actions.DeleteWay", function () { it("registers member nodes with interesting tags as POIs", function () { var node = iD.Node({tags: {highway: 'traffic_signals'}}), way = iD.Way({nodes: [node.id]}), - action = iD.actions.DeleteWay(way), + action = iD.actions.DeleteWay(way.id), graph = action(iD.Graph([node, way])); expect(graph.entity(node.id)._poi).to.be.ok; }); diff --git a/test/spec/actions/move.js b/test/spec/actions/move.js new file mode 100644 index 000000000..5e4163af7 --- /dev/null +++ b/test/spec/actions/move.js @@ -0,0 +1,8 @@ +describe("iD.actions.Move", function () { + it("changes an entity's location", function () { + var entity = iD.Entity(), + loc = [2, 3], + graph = iD.actions.Move(entity.id, loc)(iD.Graph([entity])); + expect(graph.entity(entity.id).loc).to.eql(loc); + }); +}); diff --git a/test/spec/actions/noop.js b/test/spec/actions/noop.js new file mode 100644 index 000000000..677c3a2e0 --- /dev/null +++ b/test/spec/actions/noop.js @@ -0,0 +1,7 @@ +describe("iD.actions.Noop", function () { + it("does nothing", function () { + var graph = iD.Graph(), + action = iD.actions.Noop(graph); + expect(action(graph)).to.equal(graph); + }); +}); diff --git a/test/spec/actions/remove_relation_member.js b/test/spec/actions/remove_relation_member.js new file mode 100644 index 000000000..d989b749b --- /dev/null +++ b/test/spec/actions/remove_relation_member.js @@ -0,0 +1,8 @@ +describe("iD.actions.RemoveRelationMember", function () { + it("removes a member from a relation", function () { + var node = iD.Node(), + relation = iD.Way({members: [{ id: node.id }]}), + graph = iD.actions.RemoveRelationMember(relation.id, node.id)(iD.Graph([node, relation])); + expect(graph.entity(relation.id).members).to.eql([]); + }); +}); diff --git a/test/spec/actions/remove_way_node.js b/test/spec/actions/remove_way_node.js index 56a302ec0..6e5751b76 100644 --- a/test/spec/actions/remove_way_node.js +++ b/test/spec/actions/remove_way_node.js @@ -2,7 +2,7 @@ describe("iD.actions.RemoveWayNode", function () { it("removes a node from a way", function () { var node = iD.Node({id: "n1"}), way = iD.Way({id: "w1", nodes: ["n1"]}), - graph = iD.actions.RemoveWayNode(way, node)(iD.Graph({n1: node, w1: way})); + graph = iD.actions.RemoveWayNode(way.id, node.id)(iD.Graph({n1: node, w1: way})); expect(graph.entity(way.id).nodes).to.eql([]); }); }); diff --git a/test/spec/actions/reverse_way.js b/test/spec/actions/reverse_way.js new file mode 100644 index 000000000..0df898ae7 --- /dev/null +++ b/test/spec/actions/reverse_way.js @@ -0,0 +1,9 @@ +describe("iD.actions.ReverseWay", function () { + it("reverses the order of nodes in the way", function () { + var node1 = iD.Node(), + node2 = iD.Node(), + way = iD.Way({nodes: [node1.id, node2.id]}), + graph = iD.actions.ReverseWay(way.id)(iD.Graph([node1, node2, way])); + expect(graph.entity(way.id).nodes).to.eql([node2.id, node1.id]); + }); +}); diff --git a/test/spec/graph/entity.js b/test/spec/graph/entity.js index 7b15a4788..ba0d6f05d 100644 --- a/test/spec/graph/entity.js +++ b/test/spec/graph/entity.js @@ -1,4 +1,4 @@ -describe('Entity', function () { +describe('iD.Entity', function () { if (iD.debug) { it("is frozen", function () { expect(Object.isFrozen(iD.Entity())).to.be.true; @@ -13,6 +13,24 @@ describe('Entity', function () { }); } + describe(".id", function () { + it("generates unique IDs", function () { + expect(iD.Entity.id('node')).not.to.equal(iD.Entity.id('node')); + }); + + describe(".fromOSM", function () { + it("returns a ID string unique across entity types", function () { + expect(iD.Entity.id.fromOSM('node', 1)).to.equal("n1"); + }); + }); + + describe(".toOSM", function () { + it("reverses fromOSM", function () { + expect(iD.Entity.id.toOSM(iD.Entity.id.fromOSM('node', 1))).to.equal(1); + }); + }); + }); + describe("#update", function () { it("returns a new Entity", function () { var a = iD.Entity(), @@ -105,7 +123,7 @@ describe('Entity', function () { }); }); -describe('Node', function () { +describe('iD.Node', function () { it("returns a node", function () { expect(iD.Node().type).to.equal("node"); }); @@ -138,7 +156,7 @@ describe('Node', function () { }); }); -describe('Way', function () { +describe('iD.Way', function () { if (iD.debug) { it("freezes nodes", function () { expect(Object.isFrozen(iD.Way().nodes)).to.be.true; @@ -191,7 +209,7 @@ describe('Way', function () { }); }); -describe('Relation', function () { +describe('iD.Relation', function () { if (iD.debug) { it("freezes nodes", function () { expect(Object.isFrozen(iD.Relation().members)).to.be.true; diff --git a/test/spec/graph/graph.js b/test/spec/graph/graph.js index 16cbf6995..20e1d0777 100644 --- a/test/spec/graph/graph.js +++ b/test/spec/graph/graph.js @@ -11,11 +11,6 @@ describe('iD.Graph', function() { expect(graph.entity(entity.id)).to.equal(entity); }); - it('can be constructed with an annotation', function() { - var graph = iD.Graph({}, 'first graph'); - expect(graph.annotation).to.equal('first graph'); - }); - if (iD.debug) { it("is frozen", function () { expect(Object.isFrozen(iD.Graph())).to.be.true; @@ -26,35 +21,46 @@ describe('iD.Graph', function() { }); } - describe('operations', function() { - it('#remove', function() { - var entities = { 'n-1': { - type: 'node', - loc: [-80, 30], - id: 'n-1' - } - }; - var graph = iD.Graph(entities, 'first graph'); - var g2 = graph.remove(entities['n-1'], 'Removed node'); - expect(graph.entity('n-1')).to.equal(entities['n-1']); - expect(g2.entity('n-1')).to.equal(undefined); + describe("#remove", function () { + it("returns a new graph", function () { + var node = iD.Node(), + graph = iD.Graph([node]); + expect(graph.remove(node)).not.to.equal(graph); }); - it('#replace', function() { - var entities = { 'n-1': { - type: 'node', - loc: [-80, 30], - id: 'n-1' - } - }; - var replacement = { - type: 'node', - loc: [-80, 40], - id: 'n-1' - }; - var graph = iD.Graph(entities, 'first graph'); - var g2 = graph.replace(replacement, 'Removed node'); - expect(graph.entity('n-1').loc[1]).to.equal(30); - expect(g2.entity('n-1').loc[1]).to.equal(40); + + it("doesn't modify the receiver", function () { + var node = iD.Node(), + graph = iD.Graph([node]); + graph.remove(node); + expect(graph.entity(node.id)).to.equal(node); + }); + + it("removes the entity from the result", function () { + var node = iD.Node(), + graph = iD.Graph([node]); + expect(graph.remove(node).entity(node.id)).to.be.undefined; + }); + }); + + describe("#replace", function () { + it("returns a new graph", function () { + var node = iD.Node(), + graph = iD.Graph([node]); + expect(graph.replace(node)).not.to.equal(graph); + }); + + it("doesn't modify the receiver", function () { + var node = iD.Node(), + graph = iD.Graph([node]); + graph.replace(node); + expect(graph.entity(node.id)).to.equal(node); + }); + + it("replaces the entity in the result", function () { + var node1 = iD.Node(), + node2 = node1.update({}), + graph = iD.Graph([node1]); + expect(graph.replace(node2).entity(node2.id)).to.equal(node2); }); }); @@ -71,7 +77,7 @@ describe('iD.Graph', function() { describe("#parentRelations", function() { it("returns an array of relations that contain the given entity id", function () { var node = iD.Node({id: "n1"}), - relation = iD.Relation({id: "r1", members: ["n1"]}), + relation = iD.Relation({id: "r1", members: [{ id: "n1", role: 'from' }]}), graph = iD.Graph({n1: node, r1: relation}); expect(graph.parentRelations("n1")).to.eql([relation]); expect(graph.parentRelations("n2")).to.eql([]); @@ -83,25 +89,59 @@ describe('iD.Graph', function() { var node = iD.Node({id: "n1"}), way = iD.Way({id: "w1", nodes: ["n1"]}), graph = iD.Graph({n1: node, w1: way}); - expect(graph.fetch("w1").nodes[0].id).to.equal("n1"); + expect(graph.fetch("w1").nodes).to.eql([node]); }); }); - describe("#modifications", function () { - it("filters entities by modified", function () { - var a = {id: 'a', modified: function () { return true; }}, - b = {id: 'b', modified: function () { return false; }}, - graph = iD.Graph({ 'a': a, 'b': b }); - expect(graph.modifications()).to.eql([graph.fetch('a')]); + describe("#difference", function () { + it("returns an Array of ids of changed entities", function () { + var initial = iD.Node({id: "n1"}), + updated = initial.update({}), + created = iD.Node(), + deleted = iD.Node({id: 'n2'}), + graph1 = iD.Graph([initial, deleted]), + graph2 = graph1.replace(updated).replace(created).remove(deleted); + expect(graph2.difference(graph1)).to.eql([created.id, updated.id, deleted.id]); + }); + + it("includes created entities that were subsequently deleted", function () { + var node = iD.Node(), + graph1 = iD.Graph([node]), + graph2 = graph1.remove(node); + expect(graph2.difference(graph1)).to.eql([node.id]); }); }); - describe("#creations", function () { - it("filters entities by created", function () { - var a = {id: 'a', created: function () { return true; }}, - b = {id: 'b', created: function () { return false; }}, - graph = iD.Graph({ 'a': a, 'b': b }); - expect(graph.creations()).to.eql([graph.fetch('a')]); + 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]); + }); + }); + + describe("#created", function () { + it("returns an Array of ids of created entities", function () { + var node1 = iD.Node({id: 'n-1', _updated: true}), + node2 = iD.Node({id: 'n2'}), + graph = iD.Graph([node1, node2]); + expect(graph.created()).to.eql([node1.id]); + }); + }); + + describe("#deleted", function () { + it("returns an Array of ids of deleted entities", function () { + var node1 = iD.Node({id: "n1"}), + node2 = iD.Node(), + graph = iD.Graph([node1, node2]).remove(node1); + expect(graph.deleted()).to.eql([node1.id]); + }); + + it("doesn't include created entities that were subsequently deleted", function () { + var node = iD.Node(), + graph = iD.Graph([node]).remove(node); + expect(graph.deleted()).to.eql([]); }); }); }); diff --git a/test/spec/graph/history.js b/test/spec/graph/history.js index dda4f3168..15fe10964 100644 --- a/test/spec/graph/history.js +++ b/test/spec/graph/history.js @@ -1,7 +1,6 @@ -describe("History", function () { +describe("iD.History", function () { var history, spy, - graph = iD.Graph([], "action"), - action = function() { return graph; }; + action = function() { return iD.Graph(); }; beforeEach(function () { history = iD.History(); @@ -16,46 +15,79 @@ describe("History", function () { describe("#perform", function () { it("updates the graph", function () { - history.perform(action); + var graph = iD.Graph(); + history.perform(d3.functor(graph)); expect(history.graph()).to.equal(graph); }); - it("pushes the undo stack", function () { - history.perform(action); - expect(history.undoAnnotation()).to.equal("action"); + it("pushes an undo annotation", function () { + history.perform(action, "annotation"); + expect(history.undoAnnotation()).to.equal("annotation"); }); it("emits a change event", function () { history.on('change', spy); history.perform(action); - expect(spy).to.have.been.called; + expect(spy).to.have.been.calledWith([]); }); - it("does not emit a change event when performing a noop", function () { + it("performs multiple actions", function () { + var action1 = sinon.stub().returns(iD.Graph()), + action2 = sinon.stub().returns(iD.Graph()); + history.perform(action1, action2, "annotation"); + expect(action1).to.have.been.called; + expect(action2).to.have.been.called; + expect(history.undoAnnotation()).to.equal("annotation"); + }); + }); + + describe("#replace", function () { + it("updates the graph", function () { + var graph = iD.Graph(); + history.replace(d3.functor(graph)); + expect(history.graph()).to.equal(graph); + }); + + it("replaces the undo annotation", function () { + history.perform(action, "annotation1"); + history.replace(action, "annotation2"); + expect(history.undoAnnotation()).to.equal("annotation2"); + }); + + it("emits a change event", function () { history.on('change', spy); - history.perform(iD.actions.Noop); - expect(spy).not.to.have.been.called; + history.replace(action); + expect(spy).to.have.been.calledWith([]); + }); + + it("performs multiple actions", function () { + var action1 = sinon.stub().returns(iD.Graph()), + action2 = sinon.stub().returns(iD.Graph()); + history.replace(action1, action2, "annotation"); + expect(action1).to.have.been.called; + expect(action2).to.have.been.called; + expect(history.undoAnnotation()).to.equal("annotation"); }); }); describe("#undo", function () { it("pops the undo stack", function () { - history.perform(action); + history.perform(action, "annotation"); history.undo(); expect(history.undoAnnotation()).to.be.undefined; }); it("pushes the redo stack", function () { - history.perform(action); + history.perform(action, "annotation"); history.undo(); - expect(history.redoAnnotation()).to.equal("action"); + expect(history.redoAnnotation()).to.equal("annotation"); }); it("emits a change event", function () { history.perform(action); history.on('change', spy); history.undo(); - expect(spy).to.have.been.called; + expect(spy).to.have.been.calledWith([]); }); }); @@ -65,14 +97,39 @@ describe("History", function () { history.undo(); history.on('change', spy); history.redo(); - expect(spy).to.have.been.called; + expect(spy).to.have.been.calledWith([]); + }); + }); + + describe("#changes", function () { + it("includes created entities", function () { + var node = iD.Node(); + history.perform(function (graph) { return graph.replace(node); }); + expect(history.changes().created).to.eql([node]); + }); + + it("includes modified entities", function () { + var node1 = iD.Node({id: "n1"}), + node2 = node1.update({}), + graph = iD.Graph([node1]); + history.merge(graph); + 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); + history.perform(function (graph) { return graph.remove(node); }); + expect(history.changes().deleted).to.eql([node]); }); }); describe("#reset", function () { it("clears the version stack", function () { - history.perform(action); - history.perform(action); + history.perform(action, "annotation"); + history.perform(action, "annotation"); history.undo(); history.reset(); expect(history.undoAnnotation()).to.be.undefined; diff --git a/test/spec/util.js b/test/spec/util.js index 1dccda623..02d8ca2b1 100644 --- a/test/spec/util.js +++ b/test/spec/util.js @@ -1,16 +1,6 @@ describe('Util', function() { var util; - it('#id', function() { - var a = iD.util.id(), - b = iD.util.id(), - c = iD.util.id(), - d = iD.util.id(); - expect(a === b).to.equal(false); - expect(b === c).to.equal(false); - expect(c === d).to.equal(false); - }); - it('#trueObj', function() { expect(iD.util.trueObj(['a', 'b', 'c'])).to.eql({ a: true, b: true, c: true }); expect(iD.util.trueObj([])).to.eql({});