diff --git a/Makefile b/Makefile
index b14e6f989..9869abb14 100644
--- a/Makefile
+++ b/Makefile
@@ -13,6 +13,7 @@ all: \
.INTERMEDIATE iD.js: \
js/lib/d3.v3.js \
js/lib/d3.geo.tile.js \
+ js/lib/d3.keybinding.js \
js/lib/d3.one.js \
js/lib/d3.size.js \
js/lib/d3.typeahead.js \
@@ -26,6 +27,7 @@ all: \
js/id/oauth.js \
js/id/taginfo.js \
js/id/util.js \
+ js/id/actions.js \
js/id/actions/*.js \
js/id/modes.js \
js/id/modes/*.js \
diff --git a/img/source/design.svg b/img/source/design.svg
new file mode 100644
index 000000000..0d245cfc2
--- /dev/null
+++ b/img/source/design.svg
@@ -0,0 +1,1797 @@
+
+
+
+
diff --git a/img/source/renders/basic-ui.png b/img/source/renders/basic-ui.png
new file mode 100644
index 000000000..aa6acfccd
Binary files /dev/null and b/img/source/renders/basic-ui.png differ
diff --git a/index.html b/index.html
index ab9e86956..a674a2e7c 100644
--- a/index.html
+++ b/index.html
@@ -40,7 +40,18 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/js/id/actions.js b/js/id/actions.js
new file mode 100644
index 000000000..481fa8285
--- /dev/null
+++ b/js/id/actions.js
@@ -0,0 +1 @@
+iD.actions = {};
diff --git a/js/id/actions/actions.js b/js/id/actions/actions.js
deleted file mode 100644
index 593e33d3f..000000000
--- a/js/id/actions/actions.js
+++ /dev/null
@@ -1,80 +0,0 @@
-iD.actions = {};
-
-iD.actions.noop = function() {
- return function(graph) {
- return graph;
- };
-};
-
-// 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');
- };
-};
-
-iD.actions.startWay = function(way) {
- return function(graph) {
- return graph.replace(way, 'started a road');
- };
-};
-
-// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/DeleteWayAction.as
-iD.actions.remove = function(node) {
- return function(graph) {
- return graph.remove(node, 'removed a feature');
- };
-};
-
-// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/AddNodeToWayAction.as
-iD.actions.addWayNode = function(way, node, 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');
- };
-};
-
-iD.actions.removeWayNode = function(way, node) {
- return function(graph) {
- var nodes = _.without(way.nodes, node.id);
- return graph.replace(way.update({nodes: nodes}), 'removed from a road');
- };
-};
-
-// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/AddNodeToWayAction.as
-iD.actions.changeWayDirection = function(way) {
- return function(graph) {
- return graph.replace(way.update({
- nodes: way.nodes.slice()
- }), 'changed way direction');
- };
-};
-
-iD.actions.changeTags = function(node, tags) {
- return function(graph) {
- return graph.replace(node.update({
- tags: tags
- }), 'changed tags');
- };
-};
-
-// 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) {
- return function(graph) {
- return graph.replace(entity.update({loc: loc}), 'moved an element');
- };
-};
-
-iD.actions.addTemporary = function(node) {
- return function(graph) {
- return graph.replace(node);
- };
-};
-
-iD.actions.removeTemporary = function(node) {
- return function(graph) {
- return graph.remove(node);
- };
-};
diff --git a/js/id/actions/add_node.js b/js/id/actions/add_node.js
new file mode 100644
index 000000000..291811409
--- /dev/null
+++ b/js/id/actions/add_node.js
@@ -0,0 +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');
+ };
+};
diff --git a/js/id/actions/add_way_node.js b/js/id/actions/add_way_node.js
new file mode 100644
index 000000000..bc4b7f347
--- /dev/null
+++ b/js/id/actions/add_way_node.js
@@ -0,0 +1,8 @@
+// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/AddNodeToWayAction.as
+iD.actions.addWayNode = function(way, node, 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');
+ };
+};
diff --git a/js/id/actions/change_tags.js b/js/id/actions/change_tags.js
new file mode 100644
index 000000000..81b99760e
--- /dev/null
+++ b/js/id/actions/change_tags.js
@@ -0,0 +1,7 @@
+iD.actions.changeTags = function(node, tags) {
+ return function(graph) {
+ return graph.replace(node.update({
+ tags: tags
+ }), 'changed tags');
+ };
+};
diff --git a/js/id/actions/change_way_direction.js b/js/id/actions/change_way_direction.js
new file mode 100644
index 000000000..cfa637336
--- /dev/null
+++ b/js/id/actions/change_way_direction.js
@@ -0,0 +1,8 @@
+// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/AddNodeToWayAction.as
+iD.actions.changeWayDirection = function(way) {
+ return function(graph) {
+ return graph.replace(way.update({
+ nodes: way.nodes.slice()
+ }), 'changed way direction');
+ };
+};
diff --git a/js/id/actions/delete_node.js b/js/id/actions/delete_node.js
new file mode 100644
index 000000000..1d3737a86
--- /dev/null
+++ b/js/id/actions/delete_node.js
@@ -0,0 +1,16 @@
+// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/DeleteNodeAction.as
+iD.actions.DeleteNode = function(node) {
+ return function(graph) {
+ graph.parentWays(node.id)
+ .forEach(function(parent) {
+ graph = iD.actions.removeWayNode(parent, node)(graph);
+ });
+
+ graph.parentRelations(node.id)
+ .forEach(function(parent) {
+ graph = iD.actions.removeRelationEntity(parent, node)(graph);
+ });
+
+ return graph.remove(node, 'removed a node');
+ };
+};
diff --git a/js/id/actions/delete_way.js b/js/id/actions/delete_way.js
new file mode 100644
index 000000000..157a190a7
--- /dev/null
+++ b/js/id/actions/delete_way.js
@@ -0,0 +1,21 @@
+// https://github.com/openstreetmap/potlatch2/blob/master/net/systemeD/halcyon/connection/actions/DeleteWayAction.as
+iD.actions.DeleteWay = function(way) {
+ return function(graph) {
+ graph.parentRelations(way.id)
+ .forEach(function(parent) {
+ graph = iD.actions.removeRelationEntity(parent, way)(graph);
+ });
+
+ way.nodes.forEach(function (id) {
+ var node = graph.entity(id);
+
+ graph = iD.actions.removeWayNode(way, node)(graph);
+
+ if (!graph.parentWays(id).length && !graph.parentRelations(id).length) {
+ graph = graph.remove(node);
+ }
+ });
+
+ return graph.remove(way, 'removed a way');
+ };
+};
diff --git a/js/id/actions/move.js b/js/id/actions/move.js
new file mode 100644
index 000000000..d67af763f
--- /dev/null
+++ b/js/id/actions/move.js
@@ -0,0 +1,7 @@
+// 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) {
+ return function(graph) {
+ return graph.replace(entity.update({loc: loc}), 'moved an element');
+ };
+};
diff --git a/js/id/actions/noop.js b/js/id/actions/noop.js
new file mode 100644
index 000000000..ddae5dde5
--- /dev/null
+++ b/js/id/actions/noop.js
@@ -0,0 +1,5 @@
+iD.actions.noop = function() {
+ return function(graph) {
+ return graph;
+ };
+};
diff --git a/js/id/actions/remove_relation_entity.js b/js/id/actions/remove_relation_entity.js
new file mode 100644
index 000000000..4a9fd1c70
--- /dev/null
+++ b/js/id/actions/remove_relation_entity.js
@@ -0,0 +1,6 @@
+iD.actions.removeRelationEntity = function(relation, entity) {
+ return function(graph) {
+ var members = _.without(relation.members, entity.id);
+ return graph.replace(relation.update({members: members}), 'removed from a relation');
+ };
+};
diff --git a/js/id/actions/remove_way_node.js b/js/id/actions/remove_way_node.js
new file mode 100644
index 000000000..16f9883bb
--- /dev/null
+++ b/js/id/actions/remove_way_node.js
@@ -0,0 +1,6 @@
+iD.actions.removeWayNode = function(way, node) {
+ return function(graph) {
+ var nodes = _.without(way.nodes, node.id);
+ return graph.replace(way.update({nodes: nodes}), 'removed from a road');
+ };
+};
diff --git a/js/id/actions/start_way.js b/js/id/actions/start_way.js
new file mode 100644
index 000000000..8e7ea7631
--- /dev/null
+++ b/js/id/actions/start_way.js
@@ -0,0 +1,5 @@
+iD.actions.startWay = function(way) {
+ return function(graph) {
+ return graph.replace(way, 'started a road');
+ };
+};
diff --git a/js/id/graph/entity.js b/js/id/graph/entity.js
index 47487cc91..a10da5d3a 100644
--- a/js/id/graph/entity.js
+++ b/js/id/graph/entity.js
@@ -12,6 +12,9 @@ iD.Entity = function(a, b, c) {
if (iD.debug) {
Object.freeze(this);
Object.freeze(this.tags);
+
+ if (this.nodes) Object.freeze(this.nodes);
+ if (this.members) Object.freeze(this.members);
}
};
@@ -30,11 +33,11 @@ iD.Entity.prototype = {
};
iD.Node = function(attrs) {
- return iD.Entity({tags: {}}, attrs || {}, {type: 'node'});
+ return iD.Entity(attrs || {}, {type: 'node'});
};
iD.Way = function(attrs) {
- return iD.Entity({tags: {}, nodes: []}, attrs || {}, {type: 'way'});
+ return iD.Entity({nodes: []}, attrs || {}, {type: 'way'});
};
iD.Way.isOneWay = function(d) {
@@ -50,5 +53,5 @@ iD.Way.isArea = function(d) {
};
iD.Relation = function(attrs) {
- return iD.Entity({tags: {}}, attrs || {}, {type: 'relation'});
+ return iD.Entity({members: []}, attrs || {}, {type: 'relation'});
};
diff --git a/js/id/graph/graph.js b/js/id/graph/graph.js
index c829f9a60..a97356ff1 100644
--- a/js/id/graph/graph.js
+++ b/js/id/graph/graph.js
@@ -1,7 +1,15 @@
iD.Graph = function(entities, annotation) {
if (!(this instanceof iD.Graph)) return new iD.Graph(entities, annotation);
- this.entities = entities || {};
+ if (_.isArray(entities)) {
+ this.entities = {};
+ for (var i = 0; i < entities.length; i++) {
+ this.entities[entities[i].id] = entities[i];
+ }
+ } else {
+ this.entities = entities || {};
+ }
+
this.annotation = annotation;
if (iD.debug) {
@@ -15,11 +23,17 @@ iD.Graph.prototype = {
return this.entities[id];
},
- parents: function(id) {
+ parentWays: function(id) {
// This is slow and a bad hack.
return _.filter(this.entities, function(e) {
- if (e.type !== 'way') return false;
- return e.nodes.indexOf(id) !== -1;
+ return 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;
});
},
diff --git a/js/id/modes/add_road.js b/js/id/modes/add_road.js
index 5887959fd..b2a89cc62 100644
--- a/js/id/modes/add_road.js
+++ b/js/id/modes/add_road.js
@@ -22,7 +22,7 @@ iD.modes.AddRoad = function() {
node = datum;
var id = datum.id;
- var parents = mode.history.graph().parents(id);
+ var parents = mode.history.graph().parentWays(id);
if (parents.length) {
if (parents[0].nodes[0] === id) {
way = parents[0];
diff --git a/js/id/modes/draw_area.js b/js/id/modes/draw_area.js
index 31c121162..ca1352432 100644
--- a/js/id/modes/draw_area.js
+++ b/js/id/modes/draw_area.js
@@ -27,8 +27,7 @@ iD.modes.DrawArea = function(way_id) {
mode.history.replace(iD.actions.addWayNode(way,
mode.history.graph().entity(way.nodes[0])));
- delete way.tags.elastic;
- mode.history.perform(iD.actions.changeTags(way, way.tags));
+ mode.history.perform(iD.actions.changeTags(way, _.omit(way.tags, 'elastic')));
// End by clicking on own tail
return mode.controller.enter(iD.modes.Select(way));
diff --git a/js/id/modes/draw_road.js b/js/id/modes/draw_road.js
index ec7e942fa..6b31ada42 100644
--- a/js/id/modes/draw_road.js
+++ b/js/id/modes/draw_road.js
@@ -37,8 +37,7 @@ iD.modes.DrawRoad = function(way_id, direction) {
mode.history.graph().entity(lastNode), index));
}
- delete way.tags.elastic;
- mode.history.perform(iD.actions.changeTags(way, way.tags));
+ mode.history.perform(iD.actions.changeTags(way, _.omit(way.tags, 'elastic')));
// End by clicking on own tail
return mode.controller.enter(iD.modes.Select(way));
diff --git a/js/id/modes/select.js b/js/id/modes/select.js
index b650ac561..82fbd7204 100644
--- a/js/id/modes/select.js
+++ b/js/id/modes/select.js
@@ -15,7 +15,7 @@ iD.modes.Select = function (entity) {
if (!dragging) {
dragging = iD.util.trueObj([entity.id].concat(
- _.pluck(mode.history.graph().parents(entity.id), 'id')));
+ _.pluck(mode.history.graph().parentWays(entity.id), 'id')));
mode.history.perform(iD.actions.noop());
}
@@ -33,13 +33,13 @@ iD.modes.Select = function (entity) {
});
function remove() {
- // Remove this node from any ways that is a member of
- mode.history.graph().parents(entity.id)
- .filter(function(d) { return d.type === 'way'; })
- .forEach(function(parent) {
- mode.history.perform(iD.actions.removeWayNode(parent, entity));
- });
- mode.history.perform(iD.actions.remove(entity));
+ switch (entity.type) {
+ case 'way':
+ mode.history.perform(iD.actions.DeleteWay(entity));
+ case 'node':
+ mode.history.perform(iD.actions.DeleteNode(entity));
+ }
+
mode.controller.exit();
}
diff --git a/js/id/renderer/map.js b/js/id/renderer/map.js
index fd018df45..85f9faaae 100644
--- a/js/id/renderer/map.js
+++ b/js/id/renderer/map.js
@@ -30,7 +30,7 @@ iD.Map = function() {
}
dragging = iD.util.trueObj([entity.id].concat(
- _.pluck(history.graph().parents(entity.id), 'id')));
+ _.pluck(history.graph().parentWays(entity.id), 'id')));
history.perform(iD.actions.noop());
}
diff --git a/test/index.html b/test/index.html
index 340b68903..8381f45fa 100644
--- a/test/index.html
+++ b/test/index.html
@@ -42,7 +42,18 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
@@ -72,6 +83,8 @@
+
+
diff --git a/test/index_packaged.html b/test/index_packaged.html
index 3cab98faa..f1e70b069 100644
--- a/test/index_packaged.html
+++ b/test/index_packaged.html
@@ -26,6 +26,8 @@
+
+
diff --git a/test/spec/actions/delete_node.js b/test/spec/actions/delete_node.js
new file mode 100644
index 000000000..82388b96c
--- /dev/null
+++ b/test/spec/actions/delete_node.js
@@ -0,0 +1,24 @@
+describe("iD.actions.DeleteNode", function () {
+ it("removes the node from the graph", function () {
+ var node = iD.Node(),
+ action = iD.actions.DeleteNode(node),
+ graph = action(iD.Graph([node]));
+ expect(graph.entity(node.id)).to.be.undefined;
+ });
+
+ it("removes the node from parent ways", function () {
+ var node = iD.Node(),
+ way = iD.Way({nodes: [node.id]}),
+ action = iD.actions.DeleteNode(node),
+ 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),
+ 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
new file mode 100644
index 000000000..ed86e45a0
--- /dev/null
+++ b/test/spec/actions/delete_way.js
@@ -0,0 +1,36 @@
+describe("iD.actions.DeleteWay", function () {
+ it("removes the way from the graph", function () {
+ var way = iD.Way(),
+ action = iD.actions.DeleteWay(way),
+ 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),
+ graph = action(iD.Graph([way, relation]));
+ expect(graph.entity(relation.id).members).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),
+ graph = action(iD.Graph([node, way]));
+ expect(graph.entity(node.id)).to.be.undefined;
+ });
+
+ it("does not delete member nodes referenced by another parent", function () {
+ var node = iD.Node(),
+ way1 = iD.Way({nodes: [node.id]}),
+ way2 = iD.Way({nodes: [node.id]}),
+ action = iD.actions.DeleteWay(way1),
+ graph = action(iD.Graph([node, way1, way2]));
+ expect(graph.entity(node.id)).not.to.be.undefined;
+ });
+
+ it("does not delete member nodes with interesting tags");
+ it("registers member nodes with interesting tags as POIs");
+});
diff --git a/test/spec/graph/entity.js b/test/spec/graph/entity.js
index baa15cc4f..e28c782ef 100644
--- a/test/spec/graph/entity.js
+++ b/test/spec/graph/entity.js
@@ -131,6 +131,14 @@ describe('Relation', function () {
expect(iD.Relation({id: 'r1234'}).modified()).not.to.be.ok;
});
+ it("defaults members to an empty array", function () {
+ expect(iD.Relation().members).to.eql([]);
+ });
+
+ it("sets members as specified", function () {
+ expect(iD.Relation({members: ["n-1"]}).members).to.eql(["n-1"]);
+ });
+
it("defaults tags to an empty object", function () {
expect(iD.Relation().tags).to.eql({});
});
diff --git a/test/spec/graph/graph.js b/test/spec/graph/graph.js
index bd12753e0..31ef038b9 100644
--- a/test/spec/graph/graph.js
+++ b/test/spec/graph/graph.js
@@ -1,21 +1,19 @@
-describe('Graph', function() {
+describe('iD.Graph', function() {
+ it("can be constructed with an entities Object", function () {
+ var entity = iD.Entity(),
+ graph = iD.Graph({'n-1': entity});
+ expect(graph.entity('n-1')).to.equal(entity);
+ });
- describe('Construction and access', function() {
- it('entity', function() {
- var entities = { 'n-1': {
- type: 'node',
- loc: [-80, 30],
- id: 'n-1'
- }
- };
- var graph = iD.Graph(entities, 'first graph');
- expect(graph.entity('n-1')).to.equal(entities['n-1']);
- });
+ it("can be constructed with an entities Array", function () {
+ var entity = iD.Entity(),
+ graph = iD.Graph([entity]);
+ expect(graph.entity(entity.id)).to.equal(entity);
+ });
- it('annotation', function() {
- var graph = iD.Graph({}, 'first graph');
- expect(graph.annotation).to.equal('first graph');
- });
+ it('can be constructed with an annotation', function() {
+ var graph = iD.Graph({}, 'first graph');
+ expect(graph.annotation).to.equal('first graph');
});
describe('operations', function() {
@@ -50,6 +48,26 @@ describe('Graph', function() {
});
});
+ describe("#parentWays", function() {
+ it("returns an array of ways that contain the given node id", function () {
+ var node = iD.Node({id: "n1"}),
+ way = iD.Way({id: "w1", nodes: ["n1"]}),
+ graph = iD.Graph({n1: node, w1: way});
+ expect(graph.parentWays("n1")).to.eql([way]);
+ expect(graph.parentWays("n2")).to.eql([]);
+ });
+ });
+
+ 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"]}),
+ graph = iD.Graph({n1: node, r1: relation});
+ expect(graph.parentRelations("n1")).to.eql([relation]);
+ expect(graph.parentRelations("n2")).to.eql([]);
+ });
+ });
+
describe("#fetch", function () {
it("replaces node ids with references", function () {
var node = iD.Node({id: "n1"}),