Merge branch 'master' of github.com:systemed/iD

This commit is contained in:
Tom MacWright
2012-12-03 18:42:13 -05:00
9 changed files with 279 additions and 216 deletions

View File

@@ -42,7 +42,7 @@ iD.modes.AddPlace = {
d3.mouse(surface.node()));
var n = iD.modes._node(ll);
n._poi = true;
this.map.perform(iD.actions.addNode(n));
this.history.perform(iD.actions.addNode(n));
this.map.selectEntity(n);
this.controller.exit();
this.exit();
@@ -100,7 +100,7 @@ iD.modes.AddRoad = {
if (t.data() && t.data()[0] && t.data()[0].type === 'node') {
// continue an existing way
var id = t.data()[0].id;
var parents = this.map.history.graph().parents(id);
var parents = this.history.graph().parents(id);
if (parents.length && parents[0].nodes[0] === id) {
way = parents[0];
direction = 'backward';
@@ -115,18 +115,18 @@ iD.modes.AddRoad = {
var index = iD.modes.chooseIndex(t.data()[0], d3.mouse(surface.node()), this.map);
node = iD.modes._node(this.map.projection.invert(
d3.mouse(surface.node())));
var connectedWay = this.map.history.graph().entity(t.data()[0].id);
var connectedWay = this.history.graph().entity(t.data()[0].id);
connectedWay.nodes.splice(index, 0, node.id);
this.map.perform(iD.actions.addWayNode(connectedWay, node));
this.history.perform(iD.actions.addWayNode(connectedWay, node));
} else {
node = iD.modes._node(this.map.projection.invert(
d3.mouse(surface.node())));
}
if (start) {
this.map.perform(iD.actions.startWay(way));
this.history.perform(iD.actions.startWay(way));
way.nodes.push(node.id);
this.map.perform(iD.actions.addWayNode(way, node));
this.history.perform(iD.actions.addWayNode(way, node));
}
this.controller.enter(iD.modes.DrawRoad(way.id, direction));
}
@@ -160,19 +160,19 @@ iD.modes.DrawRoad = function(way_id, direction) {
nextnode = iD.modes._node([NaN, NaN]);
var nextnode_id = nextnode.id;
var way = this.map.history.graph().entity(way_id),
var way = this.history.graph().entity(way_id),
firstNode = way.nodes[0],
lastNode = _.last(way.nodes);
way.nodes[push](nextnode_id);
this.map.perform(iD.actions.addWayNode(way, nextnode));
this.history.perform(iD.actions.addWayNode(way, nextnode));
function mousemove() {
var ll = this.map.projection.invert(d3.mouse(surface.node()));
var way = this.map.history.graph().entity(way_id);
var node = iD.Entity(this.map.history.graph().entity(nextnode_id), {
var way = this.history.graph().entity(way_id);
var node = iD.Entity(this.history.graph().entity(nextnode_id), {
lon: ll[0], lat: ll[1]
});
this.map.history.replace(iD.actions.addWayNode(way, node));
this.history.replace(iD.actions.addWayNode(way, node));
var only = iD.Util.trueObj([way.id].concat(_.pluck(way.nodes, 'id')));
this.map.redraw(only);
}
@@ -182,21 +182,21 @@ iD.modes.DrawRoad = function(way_id, direction) {
d3.event.stopPropagation();
if (t.data() && t.data()[0] && t.data()[0].type === 'node') {
if (t.data()[0].id == firstNode || t.data()[0].id == lastNode) {
var l = this.map.history.graph().entity(way.nodes[pop]());
this.map.perform(iD.actions.removeWayNode(way, l));
var l = this.history.graph().entity(way.nodes[pop]());
this.history.perform(iD.actions.removeWayNode(way, l));
// If this is drawing a loop and this is not the drawing
// end of the stick, finish the circle
if (direction === 'forward' && t.data()[0].id == firstNode) {
way.nodes[push](firstNode);
this.map.perform(iD.actions.addWayNode(way,
this.map.history.graph().entity(firstNode)));
this.history.perform(iD.actions.addWayNode(way,
this.history.graph().entity(firstNode)));
} else if (direction === 'backward' && t.data()[0].id == lastNode) {
way.nodes[push](lastNode);
this.map.perform(iD.actions.addWayNode(way,
this.map.history.graph().entity(lastNode)));
this.history.perform(iD.actions.addWayNode(way,
this.history.graph().entity(lastNode)));
}
delete way.tags.elastic;
this.map.perform(iD.actions.changeTags(way, way.tags));
this.history.perform(iD.actions.changeTags(way, way.tags));
this.map.selectEntity(way);
// End by clicking on own tail
return this.controller.exit();
@@ -208,17 +208,17 @@ iD.modes.DrawRoad = function(way_id, direction) {
var index = iD.modes.chooseIndex(t.data()[0], d3.mouse(surface.node()), this.map);
node = iD.modes._node(this.map.projection.invert(
d3.mouse(surface.node())));
var connectedWay = this.map.history.graph().entity(t.data()[0].id);
var connectedWay = this.history.graph().entity(t.data()[0].id);
connectedWay.nodes.splice(1, 0, node.id);
this.map.perform(iD.actions.addWayNode(connectedWay, node));
this.history.perform(iD.actions.addWayNode(connectedWay, node));
} else {
node = iD.modes._node(this.map.projection.invert(
d3.mouse(surface.node())));
}
var old = this.map.history.graph().entity(way.nodes[pop]());
this.map.perform(iD.actions.removeWayNode(way, old));
var old = this.history.graph().entity(way.nodes[pop]());
this.history.perform(iD.actions.removeWayNode(way, old));
way.nodes[push](node.id);
this.map.perform(iD.actions.addWayNode(way, node));
this.history.perform(iD.actions.addWayNode(way, node));
way.nodes = way.nodes.slice();
this.controller.enter(iD.modes.DrawRoad(way_id, direction));
}
@@ -273,9 +273,9 @@ iD.modes.AddArea = {
d3.mouse(surface.node())));
}
this.map.perform(iD.actions.startWay(way));
this.history.perform(iD.actions.startWay(way));
way.nodes.push(node.id);
this.map.perform(iD.actions.addWayNode(way, node));
this.history.perform(iD.actions.addWayNode(way, node));
this.map.selectEntity(way);
this.controller.enter(iD.modes.DrawArea(way.id));
}
@@ -304,21 +304,21 @@ iD.modes.DrawArea = function(way_id) {
nextnode = iD.modes._node([NaN, NaN]);
var surface = this.map.surface,
way = this.map.history.graph().entity(way_id),
way = this.history.graph().entity(way_id),
firstnode_id = _.first(way.nodes),
nextnode_id = nextnode.id;
way.nodes.push(nextnode_id);
this.map.perform(iD.actions.addWayNode(way, nextnode));
this.history.perform(iD.actions.addWayNode(way, nextnode));
function mousemove() {
var ll = this.map.projection.invert(d3.mouse(surface.node()));
var way = this.map.history.graph().entity(way_id);
var node = iD.Entity(this.map.history.graph().entity(nextnode_id), {
var way = this.history.graph().entity(way_id);
var node = iD.Entity(this.history.graph().entity(nextnode_id), {
lon: ll[0],
lat: ll[1]
});
this.map.history.replace(iD.actions.addWayNode(way, node));
this.history.replace(iD.actions.addWayNode(way, node));
var only = iD.Util.trueObj([way.id].concat(_.pluck(way.nodes, 'id')));
this.map.redraw(only);
}
@@ -328,13 +328,13 @@ iD.modes.DrawArea = function(way_id) {
d3.event.stopPropagation();
if (t.data() && t.data()[0] && t.data()[0].type === 'node') {
if (t.data()[0].id == firstnode_id) {
var l = this.map.history.graph().entity(way.nodes.pop());
this.map.perform(iD.actions.removeWayNode(way, l));
var l = this.history.graph().entity(way.nodes.pop());
this.history.perform(iD.actions.removeWayNode(way, l));
way.nodes.push(way.nodes[0]);
this.map.perform(iD.actions.addWayNode(way,
this.map.history.graph().entity(way.nodes[0])));
this.history.perform(iD.actions.addWayNode(way,
this.history.graph().entity(way.nodes[0])));
delete way.tags.elastic;
this.map.perform(iD.actions.changeTags(way, way.tags));
this.history.perform(iD.actions.changeTags(way, way.tags));
// End by clicking on own tail
return this.controller.exit();
} else {
@@ -345,10 +345,10 @@ iD.modes.DrawArea = function(way_id) {
node = iD.modes._node(this.map.projection.invert(
d3.mouse(surface.node())));
}
var old = this.map.history.graph().entity(way.nodes.pop());
this.map.perform(iD.actions.removeWayNode(way, old));
var old = this.history.graph().entity(way.nodes.pop());
this.history.perform(iD.actions.removeWayNode(way, old));
way.nodes.push(node.id);
this.map.perform(iD.actions.addWayNode(way, node));
this.history.perform(iD.actions.addWayNode(way, node));
way.nodes = way.nodes.slice();
this.controller.enter(iD.modes.DrawArea(way_id));
}

View File

@@ -1,16 +1,19 @@
// A controller holds a single action at a time and calls `.enter` and `.exit`
// to bind and unbind actions.
iD.Controller = function(map) {
iD.Controller = function(map, history) {
var event = d3.dispatch('enter', 'exit');
var controller = { mode: null };
controller.enter = function(mode) {
mode.controller = controller;
mode.history = history;
mode.map = map;
if (controller.mode) {
controller.mode.exit();
event.exit(controller.mode);
}
mode.enter();
controller.mode = mode;
event.enter(mode);

View File

@@ -1,85 +1,103 @@
iD.History = function() {
if (!(this instanceof iD.History)) return new iD.History();
this.stack = [iD.Graph()];
this.index = 0;
};
var stack, index,
dispatch = d3.dispatch('change');
iD.History.prototype = {
graph: function() {
return this.stack[this.index];
},
merge: function(graph) {
for (var i = 0; i < this.stack.length; i++) {
this.stack[i] = this.stack[i].merge(graph);
function maybeChange() {
if (stack[index].annotation) {
dispatch.change();
}
},
perform: function(action) {
this.stack = this.stack.slice(0, this.index + 1);
this.stack.push(action(this.graph()));
this.index++;
},
replace: function(action) {
// assert(this.index == this.stack.length - 1)
this.stack[this.index] = action(this.graph());
},
undo: function() {
while (this.index > 0) {
this.index--;
if (this.stack[this.index].annotation) break;
}
},
redo: function() {
while (this.index < this.stack.length - 1) {
this.index++;
if (this.stack[this.index].annotation) break;
}
},
undoAnnotation: function() {
var index = this.index;
while (index >= 0) {
if (this.stack[index].annotation) return this.stack[index].annotation;
index--;
}
},
redoAnnotation: function() {
var index = this.index + 1;
while (index <= this.stack.length - 1) {
if (this.stack[index].annotation) return this.stack[index].annotation;
index++;
}
},
// generate reports of changes for changesets to use
modify: function() {
return this.stack[this.index].modifications();
},
create: function() {
return this.stack[this.index].creations();
},
'delete': function() {
return _.difference(
_.pluck(this.stack[0].entities, 'id'),
_.pluck(this.stack[this.index].entities, 'id')
).map(function(id) {
return this.stack[0].fetch(id);
}.bind(this));
},
changes: function() {
return {
modify: this.modify(),
create: this.create(),
'delete': this['delete']()
};
}
var history = {
graph: function () {
return stack[index];
},
merge: function (graph) {
for (var i = 0; i < stack.length; i++) {
stack[i] = stack[i].merge(graph);
}
},
perform: function (action) {
stack = stack.slice(0, index + 1);
stack.push(action(this.graph()));
index++;
maybeChange();
},
replace: function (action) {
// assert(index == stack.length - 1)
stack[index] = action(this.graph());
maybeChange();
},
undo: function () {
while (index > 0) {
index--;
if (stack[index].annotation) break;
}
dispatch.change();
},
redo: function () {
while (index < stack.length - 1) {
index++;
if (stack[index].annotation) break;
}
dispatch.change();
},
undoAnnotation: function () {
var i = index;
while (i >= 0) {
if (stack[i].annotation) return stack[i].annotation;
i--;
}
},
redoAnnotation: function () {
var i = index + 1;
while (i <= stack.length - 1) {
if (stack[i].annotation) return stack[i].annotation;
i++;
}
},
// 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 () {
return {
modify: this.modify(),
create: this.create(),
'delete': this['delete']()
};
},
reset: function () {
stack = [iD.Graph()];
index = 0;
dispatch.change();
}
};
history.reset();
return d3.rebind(history, dispatch, 'on');
};

View File

@@ -1,9 +1,11 @@
var iD = function(container) {
var connection = iD.Connection()
.url('http://api06.dev.openstreetmap.org'),
history = iD.History(),
map = iD.Map()
.connection(connection),
controller = iD.Controller(map);
.connection(connection)
.history(history),
controller = iD.Controller(map, history);
map.background.source(iD.Background.Bing);
@@ -37,13 +39,13 @@ var iD = function(container) {
.attr({ id: 'undo', 'class': 'mini' })
.property('disabled', true)
.html('&larr;<small></small>')
.on('click', map.undo);
.on('click', history.undo);
bar.append('button')
.attr({ id: 'redo', 'class': 'mini' })
.property('disabled', true)
.html('&rarr;<small></small>')
.on('click', map.redo);
.on('click', history.redo);
bar.append('input')
.attr({ type: 'text', placeholder: 'find a place', id: 'geocode-location' })
@@ -75,9 +77,9 @@ var iD = function(container) {
function save(e) {
d3.select('.shaded').remove();
var l = iD.loading('uploading changes to openstreetmap');
connection.putChangeset(map.history.changes(), e.comment, function() {
connection.putChangeset(history.changes(), e.comment, function() {
l.remove();
map.history = iD.History();
history.reset();
map.flush().redraw();
});
}
@@ -89,7 +91,7 @@ var iD = function(container) {
});
var modal = shaded.append('div')
.attr('class', 'modal commit-pane')
.datum(map.history.changes());
.datum(history.changes());
modal.call(iD.commit()
.on('cancel', function() {
shaded.remove();
@@ -107,7 +109,8 @@ var iD = function(container) {
.on('click', function(d) { return d[2](); });
this.append('div')
.attr('class', 'inspector-wrap').style('display', 'none');
.attr('class', 'inspector-wrap')
.style('display', 'none');
this.append('div')
.attr('id', 'about')
@@ -115,9 +118,9 @@ var iD = function(container) {
"<a href='http://github.com/systemed/iD/issues'>report a bug</a> " +
"/ imagery <a href='http://opengeodata.org/microsoft-imagery-details'>&copy; 2012</a> Bing, GeoEye, Getmapping, Intermap, Microsoft.</p>");
map.on('update', function() {
var undo = map.history.undoAnnotation(),
redo = map.history.redoAnnotation();
history.on('change.buttons', function() {
var undo = history.undoAnnotation(),
redo = history.redoAnnotation();
bar.select('#undo')
.property('disabled', !undo)
@@ -137,11 +140,11 @@ var iD = function(container) {
d3.select(document).on('keydown', function() {
// cmd-z
if (d3.event.which === 90 && d3.event.metaKey) {
map.undo();
history.undo();
}
// cmd-shift-z
if (d3.event.which === 90 && d3.event.metaKey && d3.event.shiftKey) {
map.redo();
history.redo();
}
});

View File

@@ -1,7 +1,7 @@
iD.Map = function() {
var connection,
var connection, history,
dimensions = [],
dispatch = d3.dispatch('move', 'update'),
dispatch = d3.dispatch('move'),
inspector = iD.Inspector(),
selection = null,
translateStart,
@@ -12,9 +12,8 @@ iD.Map = function() {
.scale(projection.scale())
.scaleExtent([1024, 256 * Math.pow(2, 24)])
.on('zoom', zoomPan),
only,
dblclickEnabled = true,
dragging = false,
dragging,
dragbehavior = d3.behavior.drag()
.origin(function(entity) {
if (entity.accuracy) {
@@ -31,21 +30,19 @@ iD.Map = function() {
d3.event.sourceEvent.stopPropagation();
if (!dragging) {
dragging = true;
only = iD.Util.trueObj([entity.id].concat(
_.pluck(map.history.graph().parents(entity.id), 'id')));
map.history.perform(iD.actions.noop());
dragging = iD.Util.trueObj([entity.id].concat(
_.pluck(history.graph().parents(entity.id), 'id')));
history.perform(iD.actions.noop());
}
var to = projection.invert([d3.event.x, d3.event.y]);
map.history.replace(iD.actions.move(entity, to));
history.replace(iD.actions.move(entity, to));
redraw(only);
redraw();
})
.on('dragend', function () {
if (dragging) {
dragging = false;
map.update();
dragging = undefined;
redraw();
}
}),
@@ -130,14 +127,9 @@ iD.Map = function() {
arrow.remove();
map.size(this.size());
hideInspector();
map.surface = surface;
}
map.history = iD.History();
function prefixMatch(p) { // via mbostock
var i = -1, n = p.length, s = document.body.style;
while (++i < n) if (p[i] + 'Transform' in s) return '-' + p[i].toLowerCase() + '-';
@@ -167,7 +159,7 @@ iD.Map = function() {
if (surface.style(transformProp) != 'none') return;
var all = [], ways = [], areas = [], points = [], waynodes = [],
extent = map.extent(),
graph = map.history.graph();
graph = history.graph();
if (!only) {
all = graph.intersects(extent);
@@ -408,7 +400,7 @@ iD.Map = function() {
if (result instanceof Error) {
// TODO: handle
} else {
map.history.merge(result);
history.merge(result);
drawVector(iD.Util.trueObj(Object.keys(result.entities)));
}
});
@@ -443,7 +435,8 @@ iD.Map = function() {
selection = entity.id;
d3.select('.inspector-wrap')
.style('display', 'block')
.datum(map.history.graph().fetch(entity.id)).call(inspector);
.datum(history.graph().fetch(entity.id))
.call(inspector);
redraw();
}
@@ -452,29 +445,25 @@ iD.Map = function() {
if (entity) entity = entity[0];
if (!entity || selection === entity.id || (entity.tags && entity.tags.elastic)) return;
if (entity.type === 'way') d3.select(d3.event.target).call(waydragbehavior);
selection = entity.id;
d3.select('.inspector-wrap')
.style('display', 'block')
.datum(map.history.graph().fetch(entity.id)).call(inspector);
redraw();
selectEntity(entity);
}
function removeEntity(entity) {
// Remove this node from any ways that is a member of
map.history.graph().parents(entity.id)
history.graph().parents(entity.id)
.filter(function(d) { return d.type === 'way'; })
.forEach(function(parent) {
parent.nodes = _.without(parent.nodes, entity.id);
map.perform(iD.actions.removeWayNode(parent, entity));
history.perform(iD.actions.removeWayNode(parent, entity));
});
map.perform(iD.actions.remove(entity));
history.perform(iD.actions.remove(entity));
}
inspector.on('changeTags', function(d, tags) {
var entity = map.history.graph().entity(d.id);
map.perform(iD.actions.changeTags(entity, tags));
var entity = history.graph().entity(d.id);
history.perform(iD.actions.changeTags(entity, tags));
}).on('changeWayDirection', function(d) {
map.perform(iD.actions.changeWayDirection(d));
history.perform(iD.actions.changeWayDirection(d));
}).on('remove', function(d) {
removeEntity(d);
hideInspector();
@@ -510,41 +499,20 @@ iD.Map = function() {
redraw();
}
function redraw(only) {
if (!only) {
function redraw() {
if (!dragging) {
dispatch.move(map);
tilegroup.call(background);
}
if (map.zoom() > 16) {
download();
drawVector(only);
drawVector(dragging);
} else {
hideVector();
}
return map;
}
map.perform = function(action) {
map.history.perform(action);
map.update();
redraw();
return map;
};
map.undo = function() {
map.history.undo();
map.update();
redraw();
return map;
};
map.redo = function() {
map.history.redo();
map.update();
redraw();
return map;
};
function dblclickEnable(_) {
if (!arguments.length) return dblclickEnabled;
dblclickEnabled = _;
@@ -618,9 +586,16 @@ iD.Map = function() {
};
map.connection = function(_) {
if (!arguments.length) return connection;
connection = _;
return map;
if (!arguments.length) return connection;
connection = _;
return map;
};
map.history = function (_) {
if (!arguments.length) return history;
history = _;
history.on('change.map', redraw);
return map;
};
map.background = background;
@@ -629,5 +604,5 @@ iD.Map = function() {
map.selectEntity = selectEntity;
map.dblclickEnable = dblclickEnable;
return d3.rebind(map, dispatch, 'on', 'move', 'update');
return d3.rebind(map, dispatch, 'on', 'move');
};

View File

@@ -141,6 +141,5 @@ iD.Inspector = function() {
});
}
return d3.rebind(inspector, event, 'on');
};

View File

@@ -72,6 +72,7 @@
<script src="spec/oauth.js"></script>
<script src="spec/graph.js"></script>
<script src="spec/entity.js"></script>
<script src="spec/history.js"></script>
<script src="spec/connection.js"></script>
<script src="spec/geojson.js"></script>
<script src="spec/xml.js"></script>

View File

@@ -61,30 +61,6 @@ describe('Map', function() {
});
});
describe("update", function () {
var spy;
beforeEach(function () {
spy = sinon.spy();
map.on('update', spy);
});
it("is emitted when performing an action", function () {
map.perform(iD.actions.noop);
expect(spy).to.have.been.called;
});
it("is emitted when undoing an action", function () {
map.undo();
expect(spy).to.have.been.called;
});
it("is emitted when redoing an action", function () {
map.redo();
expect(spy).to.have.been.called;
});
});
describe("surface", function() {
it("is an SVG element", function() {
expect(map.surface.node().tagName).to.equal("svg");

88
test/spec/history.js Normal file
View File

@@ -0,0 +1,88 @@
describe("History", function () {
var history, spy,
graph = iD.Graph([], "action"),
action = function() { return graph; };
beforeEach(function () {
history = iD.History();
spy = sinon.spy();
});
describe("#graph", function () {
it("returns the current graph", function () {
expect(history.graph()).to.be.an.instanceOf(iD.Graph);
});
});
describe("#perform", function () {
it("updates the graph", function () {
history.perform(action);
expect(history.graph()).to.equal(graph);
});
it("pushes the undo stack", function () {
history.perform(action);
expect(history.undoAnnotation()).to.equal("action");
});
});
describe("#undo", function () {
it("pops the undo stack", function () {
history.perform(action);
history.undo();
expect(history.undoAnnotation()).to.be.undefined;
});
it("pushes the redo stack", function () {
history.perform(action);
history.undo();
expect(history.redoAnnotation()).to.equal("action");
});
});
describe("#reset", function () {
it("clears the version stack", function () {
history.perform(action);
history.perform(action);
history.undo();
history.reset();
expect(history.undoAnnotation()).to.be.undefined;
expect(history.redoAnnotation()).to.be.undefined;
});
it("emits a change event", function () {
history.on('change', spy);
history.reset();
expect(spy).to.have.been.called;
});
});
describe("change", function () {
it("is not emitted when performing a noop", function () {
history.on('change', spy);
history.perform(iD.actions.noop);
expect(spy).not.to.have.been.called;
});
it("is emitted when performing an action", function () {
history.on('change', spy);
history.perform(action);
expect(spy).to.have.been.called;
});
it("is emitted when undoing an action", function () {
history.perform(action);
history.on('change', spy);
history.undo();
expect(spy).to.have.been.called;
});
it("is emitted when redoing an action", function () {
history.perform(action);
history.undo();
history.on('change', spy);
history.redo();
expect(spy).to.have.been.called;
});
});
});