mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-14 09:42:56 +00:00
1. All services are disabled in testing now to prevent network accesses 2. Only services are enabled when needed to test something 3. Many changes throughout code to allow iD to run with services disabled (e.g. check for osm service instead of assuming context.connection() will work) 4. Actually export the services so we can disable and enable them
593 lines
18 KiB
JavaScript
593 lines
18 KiB
JavaScript
import * as d3 from 'd3';
|
|
import _ from 'lodash';
|
|
import * as Validations from '../validations/index';
|
|
import { coreDifference } from './difference';
|
|
import { coreGraph } from './graph';
|
|
import { coreTree } from './tree';
|
|
import { osmEntity } from '../osm/entity';
|
|
import { uiLoading } from '../ui/index';
|
|
import { utilSessionMutex } from '../util/index';
|
|
import { utilRebind } from '../util/rebind';
|
|
|
|
|
|
export function coreHistory(context) {
|
|
var imageryUsed = ['Bing'],
|
|
dispatch = d3.dispatch('change', 'undone', 'redone'),
|
|
lock = utilSessionMutex('lock'),
|
|
duration = 150,
|
|
checkpoints = {},
|
|
stack, index, tree;
|
|
|
|
|
|
// internal _act, accepts list of actions and eased time
|
|
function _act(actions, t) {
|
|
actions = Array.prototype.slice.call(actions);
|
|
|
|
var annotation;
|
|
|
|
if (!_.isFunction(_.last(actions))) {
|
|
annotation = actions.pop();
|
|
}
|
|
|
|
stack[index].transform = context.projection.transform();
|
|
stack[index].selectedIDs = context.selectedIDs();
|
|
|
|
var graph = stack[index].graph;
|
|
for (var i = 0; i < actions.length; i++) {
|
|
graph = actions[i](graph, t);
|
|
}
|
|
|
|
return {
|
|
graph: graph,
|
|
annotation: annotation,
|
|
imageryUsed: imageryUsed
|
|
};
|
|
}
|
|
|
|
|
|
// internal _perform with eased time
|
|
function _perform(args, t) {
|
|
var previous = stack[index].graph;
|
|
stack = stack.slice(0, index + 1);
|
|
stack.push(_act(args, t));
|
|
index++;
|
|
return change(previous);
|
|
}
|
|
|
|
|
|
// internal _replace with eased time
|
|
function _replace(args, t) {
|
|
var previous = stack[index].graph;
|
|
// assert(index == stack.length - 1)
|
|
stack[index] = _act(args, t);
|
|
return change(previous);
|
|
}
|
|
|
|
|
|
// internal _overwrite with eased time
|
|
function _overwrite(args, t) {
|
|
var previous = stack[index].graph;
|
|
if (index > 0) {
|
|
index--;
|
|
stack.pop();
|
|
}
|
|
stack = stack.slice(0, index + 1);
|
|
stack.push(_act(args, t));
|
|
index++;
|
|
return change(previous);
|
|
}
|
|
|
|
|
|
// determine diffrence and dispatch a change event
|
|
function change(previous) {
|
|
var difference = coreDifference(previous, history.graph());
|
|
dispatch.call('change', this, difference);
|
|
return difference;
|
|
}
|
|
|
|
|
|
// iD uses namespaced keys so multiple installations do not conflict
|
|
function getKey(n) {
|
|
return 'iD_' + window.location.origin + '_' + n;
|
|
}
|
|
|
|
|
|
var history = {
|
|
|
|
graph: function() {
|
|
return stack[index].graph;
|
|
},
|
|
|
|
|
|
base: function() {
|
|
return stack[0].graph;
|
|
},
|
|
|
|
|
|
merge: function(entities, extent) {
|
|
stack[0].graph.rebase(entities, _.map(stack, 'graph'), false);
|
|
tree.rebase(entities, false);
|
|
|
|
dispatch.call('change', this, undefined, extent);
|
|
},
|
|
|
|
|
|
perform: function() {
|
|
// complete any transition already in progress
|
|
d3.select(document).interrupt('history.perform');
|
|
|
|
var transitionable = false,
|
|
action0 = arguments[0];
|
|
|
|
if (arguments.length === 1 ||
|
|
arguments.length === 2 && !_.isFunction(arguments[1])) {
|
|
transitionable = !!action0.transitionable;
|
|
}
|
|
|
|
if (transitionable) {
|
|
var origArguments = arguments;
|
|
d3.select(document)
|
|
.transition('history.perform')
|
|
.duration(duration)
|
|
.ease(d3.easeLinear)
|
|
.tween('history.tween', function() {
|
|
return function(t) {
|
|
if (t < 1) _overwrite([action0], t);
|
|
};
|
|
})
|
|
.on('start', function() {
|
|
_perform([action0], 0);
|
|
})
|
|
.on('end interrupt', function() {
|
|
_overwrite(origArguments, 1);
|
|
});
|
|
|
|
} else {
|
|
return _perform(arguments);
|
|
}
|
|
},
|
|
|
|
|
|
replace: function() {
|
|
d3.select(document).interrupt('history.perform');
|
|
return _replace(arguments, 1);
|
|
},
|
|
|
|
|
|
// Same as calling pop and then perform
|
|
overwrite: function() {
|
|
d3.select(document).interrupt('history.perform');
|
|
return _overwrite(arguments, 1);
|
|
},
|
|
|
|
|
|
pop: function(n) {
|
|
d3.select(document).interrupt('history.perform');
|
|
|
|
var previous = stack[index].graph;
|
|
if (isNaN(+n) || +n < 0) {
|
|
n = 1;
|
|
}
|
|
while (n-- > 0 && index > 0) {
|
|
index--;
|
|
stack.pop();
|
|
}
|
|
return change(previous);
|
|
},
|
|
|
|
|
|
// Back to the previous annotated state or index = 0.
|
|
undo: function() {
|
|
d3.select(document).interrupt('history.perform');
|
|
|
|
var previous = stack[index].graph;
|
|
while (index > 0) {
|
|
index--;
|
|
if (stack[index].annotation) break;
|
|
}
|
|
|
|
dispatch.call('undone', this, stack[index]);
|
|
return change(previous);
|
|
},
|
|
|
|
|
|
// Forward to the next annotated state.
|
|
redo: function() {
|
|
d3.select(document).interrupt('history.perform');
|
|
|
|
var previous = stack[index].graph;
|
|
var tryIndex = index;
|
|
while (tryIndex < stack.length - 1) {
|
|
tryIndex++;
|
|
if (stack[tryIndex].annotation) {
|
|
index = tryIndex;
|
|
dispatch.call('redone', this, stack[index]);
|
|
break;
|
|
}
|
|
}
|
|
|
|
return change(previous);
|
|
},
|
|
|
|
|
|
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++;
|
|
}
|
|
},
|
|
|
|
|
|
intersects: function(extent) {
|
|
return tree.intersects(extent, stack[index].graph);
|
|
},
|
|
|
|
|
|
difference: function() {
|
|
var base = stack[0].graph,
|
|
head = stack[index].graph;
|
|
return coreDifference(base, head);
|
|
},
|
|
|
|
|
|
changes: function(action) {
|
|
var base = stack[0].graph,
|
|
head = stack[index].graph;
|
|
|
|
if (action) {
|
|
head = action(head);
|
|
}
|
|
|
|
var difference = coreDifference(base, head);
|
|
|
|
return {
|
|
modified: difference.modified(),
|
|
created: difference.created(),
|
|
deleted: difference.deleted()
|
|
};
|
|
},
|
|
|
|
|
|
validate: function(changes) {
|
|
return _(Validations)
|
|
.map(function(fn) { return fn()(changes, stack[index].graph); })
|
|
.flatten()
|
|
.value();
|
|
},
|
|
|
|
|
|
hasChanges: function() {
|
|
return this.difference().length() > 0;
|
|
},
|
|
|
|
|
|
imageryUsed: function(sources) {
|
|
if (sources) {
|
|
imageryUsed = sources;
|
|
return history;
|
|
} else {
|
|
return _(stack.slice(1, index + 1))
|
|
.map('imageryUsed')
|
|
.flatten()
|
|
.uniq()
|
|
.without(undefined, 'Custom')
|
|
.value();
|
|
}
|
|
},
|
|
|
|
|
|
// save the current history state
|
|
checkpoint: function(key) {
|
|
checkpoints[key] = {
|
|
stack: _.cloneDeep(stack),
|
|
index: index
|
|
};
|
|
return history;
|
|
},
|
|
|
|
|
|
// restore history state to a given checkpoint or reset completely
|
|
reset: function(key) {
|
|
if (key !== undefined && checkpoints.hasOwnProperty(key)) {
|
|
stack = _.cloneDeep(checkpoints[key].stack);
|
|
index = checkpoints[key].index;
|
|
} else {
|
|
stack = [{graph: coreGraph()}];
|
|
index = 0;
|
|
tree = coreTree(stack[0].graph);
|
|
checkpoints = {};
|
|
}
|
|
dispatch.call('change');
|
|
return history;
|
|
},
|
|
|
|
|
|
toIntroGraph: function() {
|
|
var nextId = { n: 0, r: 0, w: 0 },
|
|
permIds = {},
|
|
graph = this.graph(),
|
|
baseEntities = {};
|
|
|
|
// clone base entities..
|
|
_.forEach(graph.base().entities, function(entity) {
|
|
var copy = _.cloneDeepWith(entity, customizer);
|
|
baseEntities[copy.id] = copy;
|
|
});
|
|
|
|
// replace base entities with head entities..
|
|
_.forEach(graph.entities, function(entity, id) {
|
|
if (entity) {
|
|
var copy = _.cloneDeepWith(entity, customizer);
|
|
baseEntities[copy.id] = copy;
|
|
} else {
|
|
delete baseEntities[id];
|
|
}
|
|
});
|
|
|
|
// swap temporary for permanent ids..
|
|
_.forEach(baseEntities, function(entity) {
|
|
if (Array.isArray(entity.nodes)) {
|
|
entity.nodes = entity.nodes.map(function(node) {
|
|
return permIds[node] || node;
|
|
});
|
|
}
|
|
if (Array.isArray(entity.members)) {
|
|
entity.members = entity.members.map(function(member) {
|
|
member.id = permIds[member.id] || member.id;
|
|
return member;
|
|
});
|
|
}
|
|
});
|
|
|
|
return JSON.stringify({ dataIntroGraph: baseEntities });
|
|
|
|
|
|
function customizer(src) {
|
|
var copy = _.omit(_.cloneDeep(src), ['type', 'user', 'v', 'version', 'visible']);
|
|
if (_.isEmpty(copy.tags)) {
|
|
delete copy.tags;
|
|
}
|
|
|
|
if (Array.isArray(copy.loc)) {
|
|
copy.loc[0] = +copy.loc[0].toFixed(6);
|
|
copy.loc[1] = +copy.loc[1].toFixed(6);
|
|
}
|
|
|
|
var match = src.id.match(/([nrw])-\d*/); // temporary id
|
|
if (match !== null) {
|
|
var nrw = match[1], permId;
|
|
do { permId = nrw + (++nextId[nrw]); }
|
|
while (baseEntities.hasOwnProperty(permId));
|
|
|
|
copy.id = permIds[src.id] = permId;
|
|
}
|
|
return copy;
|
|
}
|
|
},
|
|
|
|
|
|
toJSON: function() {
|
|
if (!this.hasChanges()) return;
|
|
|
|
var allEntities = {},
|
|
baseEntities = {},
|
|
base = stack[0];
|
|
|
|
var s = stack.map(function(i) {
|
|
var modified = [], deleted = [];
|
|
|
|
_.forEach(i.graph.entities, function(entity, id) {
|
|
if (entity) {
|
|
var key = osmEntity.key(entity);
|
|
allEntities[key] = entity;
|
|
modified.push(key);
|
|
} else {
|
|
deleted.push(id);
|
|
}
|
|
|
|
// make sure that the originals of changed or deleted entities get merged
|
|
// into the base of the stack after restoring the data from JSON.
|
|
if (id in base.graph.entities) {
|
|
baseEntities[id] = base.graph.entities[id];
|
|
}
|
|
// get originals of parent entities too
|
|
_.forEach(base.graph._parentWays[id], function(parentId) {
|
|
if (parentId in base.graph.entities) {
|
|
baseEntities[parentId] = base.graph.entities[parentId];
|
|
}
|
|
});
|
|
});
|
|
|
|
var x = {};
|
|
|
|
if (modified.length) x.modified = modified;
|
|
if (deleted.length) x.deleted = deleted;
|
|
if (i.imageryUsed) x.imageryUsed = i.imageryUsed;
|
|
if (i.annotation) x.annotation = i.annotation;
|
|
|
|
return x;
|
|
});
|
|
|
|
return JSON.stringify({
|
|
version: 3,
|
|
entities: _.values(allEntities),
|
|
baseEntities: _.values(baseEntities),
|
|
stack: s,
|
|
nextIDs: osmEntity.id.next,
|
|
index: index
|
|
});
|
|
},
|
|
|
|
|
|
fromJSON: function(json, loadChildNodes) {
|
|
var h = JSON.parse(json),
|
|
loadComplete = true;
|
|
|
|
osmEntity.id.next = h.nextIDs;
|
|
index = h.index;
|
|
|
|
if (h.version === 2 || h.version === 3) {
|
|
var allEntities = {};
|
|
|
|
h.entities.forEach(function(entity) {
|
|
allEntities[osmEntity.key(entity)] = osmEntity(entity);
|
|
});
|
|
|
|
if (h.version === 3) {
|
|
// This merges originals for changed entities into the base of
|
|
// the stack even if the current stack doesn't have them (for
|
|
// example when iD has been restarted in a different region)
|
|
var baseEntities = h.baseEntities.map(function(d) { return osmEntity(d); });
|
|
stack[0].graph.rebase(baseEntities, _.map(stack, 'graph'), true);
|
|
tree.rebase(baseEntities, true);
|
|
|
|
// When we restore a modified way, we also need to fetch any missing
|
|
// childnodes that would normally have been downloaded with it.. #2142
|
|
if (loadChildNodes) {
|
|
var osm = context.connection();
|
|
var missing = _(baseEntities)
|
|
.filter({ type: 'way' })
|
|
.map('nodes')
|
|
.flatten()
|
|
.uniq()
|
|
.reject(function(n) { return stack[0].graph.hasEntity(n); })
|
|
.value();
|
|
|
|
if (!_.isEmpty(missing) && osm) {
|
|
loadComplete = false;
|
|
context.redrawEnable(false);
|
|
|
|
var loading = uiLoading(context).blocking(true);
|
|
context.container().call(loading);
|
|
|
|
var childNodesLoaded = function(err, result) {
|
|
if (!err) {
|
|
var visible = _.groupBy(result.data, 'visible');
|
|
if (!_.isEmpty(visible.true)) {
|
|
missing = _.difference(missing, _.map(visible.true, 'id'));
|
|
stack[0].graph.rebase(visible.true, _.map(stack, 'graph'), true);
|
|
tree.rebase(visible.true, true);
|
|
}
|
|
|
|
// fetch older versions of nodes that were deleted..
|
|
_.each(visible.false, function(entity) {
|
|
osm.loadEntityVersion(entity.id, +entity.version - 1, childNodesLoaded);
|
|
});
|
|
}
|
|
|
|
if (err || _.isEmpty(missing)) {
|
|
loading.close();
|
|
context.redrawEnable(true);
|
|
dispatch.call('change');
|
|
}
|
|
};
|
|
|
|
osm.loadMultiple(missing, childNodesLoaded);
|
|
}
|
|
}
|
|
}
|
|
|
|
stack = h.stack.map(function(d) {
|
|
var entities = {}, entity;
|
|
|
|
if (d.modified) {
|
|
d.modified.forEach(function(key) {
|
|
entity = allEntities[key];
|
|
entities[entity.id] = entity;
|
|
});
|
|
}
|
|
|
|
if (d.deleted) {
|
|
d.deleted.forEach(function(id) {
|
|
entities[id] = undefined;
|
|
});
|
|
}
|
|
|
|
return {
|
|
graph: coreGraph(stack[0].graph).load(entities),
|
|
annotation: d.annotation,
|
|
imageryUsed: d.imageryUsed
|
|
};
|
|
});
|
|
|
|
} else { // original version
|
|
stack = h.stack.map(function(d) {
|
|
var entities = {};
|
|
|
|
for (var i in d.entities) {
|
|
var entity = d.entities[i];
|
|
entities[i] = entity === 'undefined' ? undefined : osmEntity(entity);
|
|
}
|
|
|
|
d.graph = coreGraph(stack[0].graph).load(entities);
|
|
return d;
|
|
});
|
|
}
|
|
|
|
if (loadComplete) {
|
|
dispatch.call('change');
|
|
}
|
|
|
|
return history;
|
|
},
|
|
|
|
|
|
save: function() {
|
|
if (lock.locked()) context.storage(getKey('saved_history'), history.toJSON() || null);
|
|
return history;
|
|
},
|
|
|
|
|
|
clearSaved: function() {
|
|
context.debouncedSave.cancel();
|
|
if (lock.locked()) context.storage(getKey('saved_history'), null);
|
|
return history;
|
|
},
|
|
|
|
|
|
lock: function() {
|
|
return lock.lock();
|
|
},
|
|
|
|
|
|
unlock: function() {
|
|
lock.unlock();
|
|
},
|
|
|
|
|
|
// is iD not open in another window and it detects that
|
|
// there's a history stored in localStorage that's recoverable?
|
|
restorableChanges: function() {
|
|
return lock.locked() && !!context.storage(getKey('saved_history'));
|
|
},
|
|
|
|
|
|
// load history from a version stored in localStorage
|
|
restore: function() {
|
|
if (!lock.locked()) return;
|
|
|
|
var json = context.storage(getKey('saved_history'));
|
|
if (json) history.fromJSON(json, true);
|
|
},
|
|
|
|
|
|
_getKey: getKey
|
|
|
|
};
|
|
|
|
|
|
history.reset();
|
|
|
|
return utilRebind(history, dispatch, 'on');
|
|
}
|