Preserve Entity immutability

Reintroduced an Entity class. Entity mutations will be
expressed as methods that return a new Entity.

Extract a move operation from the drag behavior. Instead
of updating an entity in place, the drag event produces
a new entity and graph and replaces the current history
version, which was created by doing a noop on dragstart.

pdata is no longer used. It was previously removed from
Graph, and I think it makes more sense to have a specialized
Entity class as well.
This commit is contained in:
John Firebaugh
2012-11-09 18:36:18 -08:00
parent b3f3d0eff4
commit 3b8d640cfb
9 changed files with 64 additions and 85 deletions

View File

@@ -53,7 +53,7 @@
<script type='text/javascript' src='js/iD/format/GeoJSON.js'></script>
<script type='text/javascript' src='js/iD/format/XML.js'></script>
<script type='text/javascript' src='js/iD/graph/pdata.js'></script>
<script type='text/javascript' src='js/iD/graph/Entity.js'></script>
<script type='text/javascript' src='js/iD/graph/Node.js'></script>
<script type='text/javascript' src='js/iD/graph/Relation.js'></script>
<script type='text/javascript' src='js/iD/graph/Way.js'></script>

View File

@@ -78,7 +78,7 @@ iD.Connection = function() {
if (o.lon) o.lon = parseFloat(o.lon);
o._id = o.id;
o.id = o.type[0] + o.id;
return o;
return iD.Entity(o);
}
function parse(callback) {

View File

@@ -7,14 +7,14 @@ iD.actions = {};
// into operations.
iD.actions._node = function(ll) {
return {
return iD.Entity({
type: 'node',
lat: ll[1],
lon: ll[0],
id: iD.Util.id('node'),
tags: {}
};
},
});
};
iD.actions.AddPlace = {
enter: function() {
@@ -58,7 +58,7 @@ iD.actions.AddPlace = {
iD.actions.AddRoad = {
way: function(ll) {
return {
return iD.Entity({
type: 'way',
nodes: [],
tags: {
@@ -66,7 +66,7 @@ iD.actions.AddRoad = {
},
modified: true,
id: iD.Util.id('way')
};
});
},
enter: function() {
d3.selectAll('button').classed('active', false);

View File

@@ -1,5 +1,11 @@
iD.operations = {};
iD.operations.noop = function() {
return function(graph) {
return graph;
};
};
iD.operations.addNode = function(node) {
return function(graph) {
return graph.replace(node, 'added a place');
@@ -20,16 +26,26 @@ iD.operations.remove = function(node) {
iD.operations.changeWayNodes = function(way, node) {
return function(graph) {
way.nodes = way.nodes.slice();
way = pdata.object(way).get();
return graph.replace(way).replace(node, 'added to a road');
return graph.replace(way.update({
nodes: way.nodes.slice()
})).replace(node, 'added to a road');
};
};
iD.operations.changeTags = function(node, tags) {
return function(graph) {
var node = pdata.object(node).set({ tags: tags }).get();
return graph.replace(node, 'changed tags');
return graph.replace(node.update({
tags: tags
}), 'changed tags');
};
};
iD.operations.move = function(entity, to) {
return function(graph) {
return graph.replace(entity.update({
lon: to.lon || to[0],
lat: to.lat || to[1]
}), 'moved an element');
};
};

15
js/iD/graph/Entity.js Normal file
View File

@@ -0,0 +1,15 @@
iD.Entity = function(a, b) {
if (!(this instanceof iD.Entity)) return new iD.Entity(a, b);
_.extend(this, a, b);
if (b) {
this.modified = true;
}
};
iD.Entity.prototype = {
update: function(attrs) {
return iD.Entity(this, attrs);
}
};

View File

@@ -56,12 +56,11 @@ iD.Graph.prototype = {
// Resolve the id references in a way, replacing them with actual objects.
fetch: function(id) {
var o = this.entities[id];
var f = _.clone(o);
if (!f.nodes || !f.nodes.length) return f;
f.nodes = f.nodes.map(function(c) {
return this.fetch(c);
var entity = iD.Entity(this.entities[id]);
if (!entity.nodes || !entity.nodes.length) return entity;
entity.nodes = entity.nodes.map(function(id) {
return this.fetch(id);
}.bind(this));
return f;
return entity;
}
};

View File

@@ -21,6 +21,11 @@ iD.History.prototype = {
this.index++;
},
replace: function(operation) {
// assert(this.index == this.stack.length - 1)
this.stack[this.index] = operation(this.graph());
},
undo: function() {
while (this.index > 0) {
this.index--;

View File

@@ -1,46 +0,0 @@
var pdata = {};
pdata.object = function(input) {
var v = clone(input),
proxy = {};
function clone(x) {
var v = {};
for (var k in x) v[k] = x[k];
return v;
}
// Remove a key from the object. This is like `delete`,
// but does not delete the key in this closure's object
proxy.remove = function(key) {
var n = {}, k, i;
if (typeof key === 'object') {
var keys = {};
for (i = 0; i < key.length; i++) keys[key[i]] = true;
for (k in v) if (!keys[k]) n[k] = v[k];
} else {
for (k in v) if (k !== key) n[k] = v[k];
}
return pdata.object(n);
};
// Set a value or values in the object. Overwrites
// existing values.
proxy.set = function(vals) {
var n = clone(v);
for (var j in vals) {
n[j] = vals[j];
}
return pdata.object(n);
};
// Get the contained object.
proxy.get = function() {
return clone(v);
};
return proxy;
};
if (typeof module !== 'undefined') module.exports = pdata;

View File

@@ -37,29 +37,19 @@ iD.Map = function(elem) {
.on('zoom', zoomPan),
// this is used with handles
dragbehavior = d3.behavior.drag()
.origin(function(d) {
var entity = (typeof d === 'string') ? history.entity(d) : d;
history.do(function(graph) {
var node = pdata.object(entity).set({ modified: true }).get();
return graph.replace(node);
});
.origin(function(entity) {
var p = projection(ll2a(entity));
return { x: p[0], y: p[1] };
})
.on('drag', function(d) {
d3.select(this).attr('transform', function() {
return 'translate(' + d3.event.x + ',' + d3.event.y + ')';
});
var ll = projection.invert([d3.event.x, d3.event.y]);
history.entity(d).lon = ll[0];
history.entity(d).lat = ll[1];
.on('dragstart', function() {
history.do(iD.operations.noop());
})
.on('drag', function(entity) {
var to = projection.invert([d3.event.x, d3.event.y]);
history.replace(iD.operations.move(entity, to));
drawVector();
})
.on('dragend', function(d) {
var entity = (typeof d === 'string') ? history.entity(d) : d;
history.do(function(graph) {
return graph.replace(entity, 'moved an element');
});
.on('dragend', function() {
map.update();
}),
// geo
@@ -181,15 +171,15 @@ iD.Map = function(elem) {
});
var handles = hit_g.selectAll('circle.handle')
.data(selection.length ? (active_entity.length ? active_entity[0].nodes : []) : []);
.data(active_entity.length ? graph.fetch(active_entity[0].id).nodes : []);
handles.exit().remove();
handles.enter().append('circle')
.attr('class', 'handle')
.attr('r', 5)
.call(dragbehavior);
handles.attr('transform', function(d) {
return 'translate(' + projection(ll2a(graph.entity(d))) + ')';
handles.attr('transform', function(entity) {
return 'translate(' + projection(ll2a(entity)) + ')';
});
}