modules/core/context.js (499 lines of code) (raw):
import _debounce from 'lodash-es/debounce';
import { dispatch as d3_dispatch } from 'd3-dispatch';
import { json as d3_json } from 'd3-fetch';
import { select as d3_select } from 'd3-selection';
import { t, currentLocale, addTranslation, setLocale } from '../util/locale';
import { coreHistory } from './history';
import { coreValidator } from './validator';
import { dataLocales, dataEn } from '../../data';
import { geoRawMercator } from '../geo/raw_mercator';
import { modeSelect } from '../modes/select';
import { osmSetAreaKeys, osmSetPointTags, osmSetVertexTags } from '../osm/tags';
import { presetIndex } from '../presets';
import { rendererBackground, rendererFeatures, rendererMap, rendererPhotos } from '../renderer';
import { services } from '../services';
import { uiInit } from '../ui/init';
import { utilDetect } from '../util/detect';
import { utilKeybinding, utilRebind, utilStringQs } from '../util';
export function coreContext() {
var dispatch = d3_dispatch('enter', 'exit', 'change');
var context = utilRebind({}, dispatch, 'on');
var _deferred = new Set();
context.version = '2.16.0';
// create a special translation that contains the keys in place of the strings
var tkeys = JSON.parse(JSON.stringify(dataEn)); // clone deep
var parents = [];
function traverser(v, k, obj) {
parents.push(k);
if (typeof v === 'object') {
forOwn(v, traverser);
} else if (typeof v === 'string') {
obj[k] = parents.join('.');
}
parents.pop();
}
function forOwn(obj, fn) {
Object.keys(obj).forEach(function(k) { fn(obj[k], k, obj); });
}
forOwn(tkeys, traverser);
addTranslation('_tkeys_', tkeys);
addTranslation('en', dataEn);
setLocale('en');
// https://github.com/openstreetmap/iD/issues/772
// http://mathiasbynens.be/notes/localstorage-pattern#comment-9
var storage;
try { storage = localStorage; } catch (e) {} // eslint-disable-line no-empty
storage = storage || (function() {
var s = {};
return {
getItem: function(k) { return s[k]; },
setItem: function(k, v) { s[k] = v; },
removeItem: function(k) { delete s[k]; }
};
})();
context.storage = function(k, v) {
try {
if (arguments.length === 1) return storage.getItem(k);
else if (v === null) storage.removeItem(k);
else storage.setItem(k, v);
} catch (e) {
// localstorage quota exceeded
/* eslint-disable no-console */
if (typeof console !== 'undefined') console.error('localStorage quota exceeded');
/* eslint-enable no-console */
}
};
/* User interface and keybinding */
var ui;
context.ui = function() { return ui; };
var keybinding = utilKeybinding('context');
context.keybinding = function() { return keybinding; };
d3_select(document).call(keybinding);
/* Straight accessors. Avoid using these if you can. */
var connection, history, validator;
context.connection = function() { return connection; };
context.history = function() { return history; };
context.validator = function() { return validator; };
/* Connection */
context.preauth = function(options) {
if (connection) {
connection.switch(options);
}
return context;
};
function afterLoad(callback) {
return function(err, result) {
if (!err && result && result.data) {
history.merge(result.data, result.extent);
}
if (callback) {
callback(err, result);
}
};
}
context.loadTiles = function(projection, callback) {
var handle = window.requestIdleCallback(function() {
_deferred.delete(handle);
if (connection && context.editableDataEnabled()) {
connection.loadTiles(projection, afterLoad(callback));
}
});
_deferred.add(handle);
};
context.loadTileAtLoc = function(loc, callback) {
var handle = window.requestIdleCallback(function() {
_deferred.delete(handle);
if (connection && context.editableDataEnabled()) {
connection.loadTileAtLoc(loc, afterLoad(callback));
}
});
_deferred.add(handle);
};
context.loadEntity = function(entityID, callback) {
if (connection) {
connection.loadEntity(entityID, afterLoad(callback));
}
};
context.loadEntities = function(entityIDs, callback) {
var handle = window.requestIdleCallback(function() {
_deferred.delete(handle);
if (connection) {
connection.loadMultiple(entityIDs, loadedMultiple);
}
});
_deferred.add(handle);
function loadedMultiple(err, result) {
if (err || !result) {
afterLoad(callback)(err, result);
return;
}
// `loadMultiple` doesn't fetch child nodes, so we have to fetch them
// manually before merging ways
var unloadedNodeIDs = new Set();
var okayResults = [];
var waitingEntities = [];
result.data.forEach(function(entity) {
var hasUnloaded = false;
if (entity.type === 'way') {
entity.nodes.forEach(function(nodeID) {
if (!context.hasEntity(nodeID)) {
hasUnloaded = true;
// mark that we still need this node
unloadedNodeIDs.add(nodeID);
}
});
}
if (hasUnloaded) {
// don't merge ways with unloaded nodes
waitingEntities.push(entity);
} else {
okayResults.push(entity);
}
});
if (okayResults.length) {
// merge valid results right away
afterLoad(callback)(err, { data: okayResults });
}
if (waitingEntities.length) {
// run a followup request to fetch missing nodes
connection.loadMultiple(Array.from(unloadedNodeIDs), function(err, result) {
if (err || !result) {
afterLoad(callback)(err, result);
return;
}
result.data.forEach(function(entity) {
// mark that we successfully received this node
unloadedNodeIDs.delete(entity.id);
// schedule this node to be merged
waitingEntities.push(entity);
});
// since `loadMultiple` could send multiple requests, wait until all have completed
if (unloadedNodeIDs.size === 0) {
// merge the ways and their nodes all at once
afterLoad(callback)(err, { data: waitingEntities });
}
});
}
}
};
context.zoomToEntity = function(entityID, zoomTo) {
if (zoomTo !== false) {
this.loadEntity(entityID, function(err, result) {
if (err) return;
var entity = result.data.find(function(e) { return e.id === entityID; });
if (entity) {
map.zoomTo(entity);
}
});
}
map.on('drawn.zoomToEntity', function() {
if (!context.hasEntity(entityID)) return;
map.on('drawn.zoomToEntity', null);
context.on('enter.zoomToEntity', null);
context.enter(modeSelect(context, [entityID]));
});
context.on('enter.zoomToEntity', function() {
if (mode.id !== 'browse') {
map.on('drawn.zoomToEntity', null);
context.on('enter.zoomToEntity', null);
}
});
};
context.zoomToEntities = function(entityIDs) {
context.loadEntities(entityIDs);
map.on('drawn.zoomToEntities', function() {
if (entityIDs.some(function(entityID) {
return !context.hasEntity(entityID);
})) return;
map.on('drawn.zoomToEntities', null);
context.on('enter.zoomToEntities', null);
var mode = modeSelect(context, entityIDs);
context.enter(mode);
mode.zoomToSelected();
});
context.on('enter.zoomToEntities', function() {
if (mode.id !== 'browse') {
map.on('drawn.zoomToEntities', null);
context.on('enter.zoomToEntities', null);
}
});
};
var minEditableZoom = 16;
context.minEditableZoom = function(val) {
if (!arguments.length) return minEditableZoom;
minEditableZoom = val;
if (connection) {
connection.tileZoom(val);
}
return context;
};
/* History */
var inIntro = false;
context.inIntro = function(val) {
if (!arguments.length) return inIntro;
inIntro = val;
return context;
};
context.save = function() {
// no history save, no message onbeforeunload
if (inIntro || d3_select('.modal').size()) return;
var canSave;
if (mode && mode.id === 'save') {
canSave = false;
// Attempt to prevent user from creating duplicate changes - see #5200
if (services.osm && services.osm.isChangesetInflight()) {
history.clearSaved();
return;
}
} else {
canSave = context.selectedIDs().every(function(id) {
var entity = context.hasEntity(id);
return entity && !entity.isDegenerate();
});
}
if (canSave) {
history.save();
}
if (history.hasChanges()) {
return t('save.unsaved_changes');
}
};
/* Graph */
context.hasEntity = function(id) {
return history.graph().hasEntity(id);
};
context.entity = function(id) {
return history.graph().entity(id);
};
context.childNodes = function(way) {
return history.graph().childNodes(way);
};
context.geometry = function(id) {
return context.entity(id).geometry(history.graph());
};
/* Modes */
var mode;
context.mode = function() {
return mode;
};
context.enter = function(newMode) {
if (mode) {
mode.exit();
container.classed('mode-' + mode.id, false);
dispatch.call('exit', this, mode);
}
mode = newMode;
mode.enter();
container.classed('mode-' + newMode.id, true);
dispatch.call('enter', this, mode);
};
context.selectedIDs = function() {
if (mode && mode.selectedIDs) {
return mode.selectedIDs();
} else {
return [];
}
};
context.activeID = function() {
return mode && mode.activeID && mode.activeID();
};
/* Behaviors */
context.install = function(behavior) {
context.surface().call(behavior);
};
context.uninstall = function(behavior) {
context.surface().call(behavior.off);
};
/* Copy/Paste */
var copyIDs = [], copyGraph;
context.copyGraph = function() { return copyGraph; };
context.copyIDs = function(val) {
if (!arguments.length) return copyIDs;
copyIDs = val;
copyGraph = history.graph();
return context;
};
/* Background */
var background;
context.background = function() { return background; };
/* Features */
var features;
context.features = function() { return features; };
context.hasHiddenConnections = function(id) {
var graph = history.graph();
var entity = graph.entity(id);
return features.hasHiddenConnections(entity, graph);
};
/* Photos */
var photos;
context.photos = function() { return photos; };
/* Presets */
var presets;
context.presets = function() { return presets; };
/* Map */
var map;
context.map = function() { return map; };
context.layers = function() { return map.layers; };
context.surface = function() { return map.surface; };
context.editableDataEnabled = function() { return map.editableDataEnabled(); };
context.editable = function() {
// don't allow editing during save
var mode = context.mode();
if (!mode || mode.id === 'save') return false;
return map.editableDataEnabled();
};
context.surfaceRect = function() {
return map.surface.node().getBoundingClientRect();
};
/* Debug */
var debugFlags = {
tile: false, // tile boundaries
collision: false, // label collision bounding boxes
imagery: false, // imagery bounding polygons
community: false, // community bounding polygons
imperial: false, // imperial (not metric) bounding polygons
driveLeft: false, // driveLeft bounding polygons
target: false, // touch targets
downloaded: false // downloaded data from osm
};
context.debugFlags = function() {
return debugFlags;
};
context.setDebug = function(flag, val) {
if (arguments.length === 1) val = true;
debugFlags[flag] = val;
dispatch.call('change');
return context;
};
context.getDebug = function(flag) {
return flag && debugFlags[flag];
};
/* Container */
var container = d3_select(document.body);
context.container = function(val) {
if (!arguments.length) return container;
container = val;
container.classed('id-container', true);
return context;
};
var embed;
context.embed = function(val) {
if (!arguments.length) return embed;
embed = val;
return context;
};
/* Assets */
var assetPath = '';
context.assetPath = function(val) {
if (!arguments.length) return assetPath;
assetPath = val;
return context;
};
var assetMap = {};
context.assetMap = function(val) {
if (!arguments.length) return assetMap;
assetMap = val;
return context;
};
context.asset = function(val) {
var filename = assetPath + val;
return assetMap[filename] || filename;
};
context.imagePath = function(val) {
return context.asset('img/' + val);
};
/* locales */
// `locale` variable contains a "requested locale".
// It won't become the `currentLocale` until after loadLocale() is called.
var locale, localePath;
context.locale = function(loc, path) {
if (!arguments.length) return currentLocale;
locale = loc;
localePath = path;
return context;
};
context.loadLocale = function(callback) {
if (locale && locale !== 'en' && dataLocales.hasOwnProperty(locale)) {
localePath = localePath || context.asset('locales/' + locale + '.json');
d3_json(localePath)
.then(function(result) {
addTranslation(locale, result[locale]);
setLocale(locale);
utilDetect(true);
if (callback) callback();
})
.catch(function(err) {
if (callback) callback(err.message);
});
} else {
if (locale) {
setLocale(locale);
utilDetect(true);
}
if (callback) {
callback();
}
}
};
/* reset (aka flush) */
context.reset = context.flush = function() {
context.debouncedSave.cancel();
Array.from(_deferred).forEach(function(handle) {
window.cancelIdleCallback(handle);
_deferred.delete(handle);
});
Object.values(services).forEach(function(service) {
if (service && typeof service.reset === 'function') {
service.reset(context);
}
});
validator.reset();
features.reset();
history.reset();
return context;
};
/* Init */
context.projection = geoRawMercator();
context.curtainProjection = geoRawMercator();
locale = utilDetect().locale;
if (locale && !dataLocales.hasOwnProperty(locale)) {
locale = locale.split('-')[0];
}
history = coreHistory(context);
validator = coreValidator(context);
context.graph = history.graph;
context.changes = history.changes;
context.intersects = history.intersects;
context.pauseChangeDispatch = history.pauseChangeDispatch;
context.resumeChangeDispatch = history.resumeChangeDispatch;
// Debounce save, since it's a synchronous localStorage write,
// and history changes can happen frequently (e.g. when dragging).
context.debouncedSave = _debounce(context.save, 350);
function withDebouncedSave(fn) {
return function() {
var result = fn.apply(history, arguments);
context.debouncedSave();
return result;
};
}
context.perform = withDebouncedSave(history.perform);
context.replace = withDebouncedSave(history.replace);
context.pop = withDebouncedSave(history.pop);
context.overwrite = withDebouncedSave(history.overwrite);
context.undo = withDebouncedSave(history.undo);
context.redo = withDebouncedSave(history.redo);
ui = uiInit(context);
connection = services.osm;
background = rendererBackground(context);
features = rendererFeatures(context);
photos = rendererPhotos(context);
presets = presetIndex(context);
if (services.maprules && utilStringQs(window.location.hash).maprules) {
var maprules = utilStringQs(window.location.hash).maprules;
d3_json(maprules)
.then(function(mapcss) {
services.maprules.init();
mapcss.forEach(function(mapcssSelector) {
return services.maprules.addRule(mapcssSelector);
});
})
.catch(function() {
/* ignore */
});
}
map = rendererMap(context);
context.mouse = map.mouse;
context.extent = map.extent;
context.pan = map.pan;
context.zoomIn = map.zoomIn;
context.zoomOut = map.zoomOut;
context.zoomInFurther = map.zoomInFurther;
context.zoomOutFurther = map.zoomOutFurther;
context.redrawEnable = map.redrawEnable;
Object.values(services).forEach(function(service) {
if (service && typeof service.init === 'function') {
service.init(context);
}
});
validator.init();
background.init();
features.init();
photos.init();
var presetsParameter = utilStringQs(window.location.hash).presets;
if (presetsParameter && presetsParameter.indexOf('://') !== -1) {
// assume URL of external presets file
presets.fromExternal(external, function(externalPresets) {
context.presets = function() { return externalPresets; }; // default + external presets...
osmSetAreaKeys(presets.areaKeys());
osmSetPointTags(presets.pointTags());
osmSetVertexTags(presets.vertexTags());
});
} else {
var addablePresetIDs;
if (presetsParameter) {
// assume list of allowed preset IDs
addablePresetIDs = presetsParameter.split(',');
}
presets.init(addablePresetIDs);
osmSetAreaKeys(presets.areaKeys());
osmSetPointTags(presets.pointTags());
osmSetVertexTags(presets.vertexTags());
}
context.isFirstSession = !context.storage('sawSplash');
return context;
}