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({});