mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-13 17:23:02 +00:00
540 lines
16 KiB
JavaScript
540 lines
16 KiB
JavaScript
import _debounce from 'lodash-es/debounce';
|
|
|
|
import { dispatch as d3_dispatch } from 'd3-dispatch';
|
|
import { json as d3_json } from 'd3-request';
|
|
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 { 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 { utilCallWhenIdle, utilKeybinding, utilRebind, utilStringQs } from '../util';
|
|
|
|
|
|
export var areaKeys = {};
|
|
|
|
export function setAreaKeys(value) {
|
|
areaKeys = value;
|
|
}
|
|
|
|
|
|
export function coreContext() {
|
|
var context = {};
|
|
context.version = '2.14.3';
|
|
|
|
// 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');
|
|
|
|
var dispatch = d3_dispatch('enter', 'exit', 'change');
|
|
context = utilRebind(context, dispatch, 'on');
|
|
|
|
// 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;
|
|
};
|
|
|
|
context.loadTiles = utilCallWhenIdle(function(projection, callback) {
|
|
var cid;
|
|
function done(err, result) {
|
|
if (connection.getConnectionId() !== cid) {
|
|
if (callback) callback({ message: 'Connection Switched', status: -1 });
|
|
return;
|
|
}
|
|
if (!err) history.merge(result.data, result.extent);
|
|
if (callback) callback(err, result);
|
|
}
|
|
if (connection && context.editable()) {
|
|
cid = connection.getConnectionId();
|
|
connection.loadTiles(projection, done);
|
|
}
|
|
});
|
|
|
|
context.loadEntity = function(entityID, callback) {
|
|
var cid;
|
|
function done(err, result) {
|
|
if (connection.getConnectionId() !== cid) {
|
|
if (callback) callback({ message: 'Connection Switched', status: -1 });
|
|
return;
|
|
}
|
|
if (!err) history.merge(result.data, result.extent);
|
|
if (callback) callback(err, result);
|
|
}
|
|
if (connection) {
|
|
cid = connection.getConnectionId();
|
|
connection.loadEntity(entityID, done);
|
|
}
|
|
};
|
|
|
|
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);
|
|
}
|
|
});
|
|
};
|
|
|
|
var minEditableZoom = 16;
|
|
context.minEditableZoom = function(_) {
|
|
if (!arguments.length) return minEditableZoom;
|
|
minEditableZoom = _;
|
|
if (connection) {
|
|
connection.tileZoom(_);
|
|
}
|
|
return context;
|
|
};
|
|
|
|
|
|
/* History */
|
|
var inIntro = false;
|
|
context.inIntro = function(_) {
|
|
if (!arguments.length) return inIntro;
|
|
inIntro = _;
|
|
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();
|
|
dispatch.call('exit', this, mode);
|
|
}
|
|
|
|
mode = newMode;
|
|
mode.enter();
|
|
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();
|
|
};
|
|
|
|
var _selectedNoteID;
|
|
context.selectedNoteID = function(noteID) {
|
|
if (!arguments.length) return _selectedNoteID;
|
|
_selectedNoteID = noteID;
|
|
return context;
|
|
};
|
|
|
|
var _selectedErrorID;
|
|
context.selectedErrorID = function(errorID) {
|
|
if (!arguments.length) return _selectedErrorID;
|
|
_selectedErrorID = errorID;
|
|
return context;
|
|
};
|
|
|
|
|
|
/* 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(_) {
|
|
if (!arguments.length) return copyIDs;
|
|
copyIDs = _;
|
|
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.editable = function() { return map.editable(); };
|
|
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
|
|
};
|
|
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(_) {
|
|
if (!arguments.length) return container;
|
|
container = _;
|
|
container.classed('id-container', true);
|
|
return context;
|
|
};
|
|
var embed;
|
|
context.embed = function(_) {
|
|
if (!arguments.length) return embed;
|
|
embed = _;
|
|
return context;
|
|
};
|
|
|
|
|
|
/* Assets */
|
|
var assetPath = '';
|
|
context.assetPath = function(_) {
|
|
if (!arguments.length) return assetPath;
|
|
assetPath = _;
|
|
return context;
|
|
};
|
|
|
|
var assetMap = {};
|
|
context.assetMap = function(_) {
|
|
if (!arguments.length) return assetMap;
|
|
assetMap = _;
|
|
return context;
|
|
};
|
|
|
|
context.asset = function(_) {
|
|
var filename = assetPath + _;
|
|
return assetMap[filename] || filename;
|
|
};
|
|
|
|
context.imagePath = function(_) {
|
|
return context.asset('img/' + _);
|
|
};
|
|
|
|
|
|
/* 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, function(err, result) {
|
|
if (!err) {
|
|
addTranslation(locale, result[locale]);
|
|
setLocale(locale);
|
|
utilDetect(true);
|
|
}
|
|
if (callback) {
|
|
callback(err);
|
|
}
|
|
});
|
|
} else {
|
|
if (locale) {
|
|
setLocale(locale);
|
|
utilDetect(true);
|
|
}
|
|
if (callback) {
|
|
callback();
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
/* reset (aka flush) */
|
|
context.reset = context.flush = function() {
|
|
context.debouncedSave.cancel();
|
|
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, function (err, mapcss) {
|
|
if (err) return;
|
|
services.maprules.init();
|
|
mapcss.forEach(function(mapcssSelector) {
|
|
return services.maprules.addRule(mapcssSelector);
|
|
});
|
|
});
|
|
}
|
|
|
|
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();
|
|
|
|
if (utilStringQs(window.location.hash).presets) {
|
|
var external = utilStringQs(window.location.hash).presets;
|
|
presets.fromExternal(external, function(externalPresets) {
|
|
context.presets = function() { return externalPresets; }; // default + external presets...
|
|
areaKeys = presets.areaKeys();
|
|
});
|
|
} else {
|
|
presets.init();
|
|
areaKeys = presets.areaKeys();
|
|
}
|
|
|
|
return context;
|
|
}
|