mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-12 16:52:50 +00:00
* CDNs for OCI, NSI, etc. * OSM API (URL and oauth API settings) * taginfo API * nominatim API
694 lines
22 KiB
JavaScript
694 lines
22 KiB
JavaScript
import {
|
||
select as d3_select
|
||
} from 'd3-selection';
|
||
|
||
import { prefs } from '../core/preferences';
|
||
import { t, localizer } from '../core/localizer';
|
||
import { presetManager } from '../presets';
|
||
import { behaviorHash } from '../behavior';
|
||
import { modeBrowse } from '../modes/browse';
|
||
import { svgDefs, svgIcon } from '../svg';
|
||
import { utilDetect } from '../util/detect';
|
||
import { utilGetDimensions } from '../util/dimensions';
|
||
|
||
import { uiAccount } from './account';
|
||
import { uiAttribution } from './attribution';
|
||
import { uiContributors } from './contributors';
|
||
import { uiEditMenu } from './edit_menu';
|
||
import { uiFeatureInfo } from './feature_info';
|
||
import { uiFlash } from './flash';
|
||
import { uiFullScreen } from './full_screen';
|
||
import { uiGeolocate } from './geolocate';
|
||
import { uiInfo } from './info';
|
||
import { uiIntro } from './intro';
|
||
import { uiIssuesInfo } from './issues_info';
|
||
import { uiLoading } from './loading';
|
||
import { uiMapInMap } from './map_in_map';
|
||
import { uiNotice } from './notice';
|
||
import { uiPhotoviewer } from './photoviewer';
|
||
import { uiRestore } from './restore';
|
||
import { uiScale } from './scale';
|
||
import { uiShortcuts } from './shortcuts';
|
||
import { uiSidebar } from './sidebar';
|
||
import { uiSourceSwitch } from './source_switch';
|
||
import { uiSpinner } from './spinner';
|
||
import { uiSplash } from './splash';
|
||
import { uiStatus } from './status';
|
||
import { uiTooltip } from './tooltip';
|
||
import { uiTopToolbar } from './top_toolbar';
|
||
import { uiVersion } from './version';
|
||
import { uiZoom } from './zoom';
|
||
import { uiZoomToSelection } from './zoom_to_selection';
|
||
import { uiCmd } from './cmd';
|
||
|
||
import { uiPaneBackground } from './panes/background';
|
||
import { uiPaneHelp } from './panes/help';
|
||
import { uiPaneIssues } from './panes/issues';
|
||
import { uiPaneMapData } from './panes/map_data';
|
||
import { uiPanePreferences } from './panes/preferences';
|
||
|
||
export function uiInit(context) {
|
||
var _initCounter = 0;
|
||
var _needWidth = {};
|
||
|
||
var _lastPointerType;
|
||
|
||
|
||
function render(container) {
|
||
|
||
container
|
||
.on('click.ui', function(d3_event) {
|
||
// we're only concerned with the primary mouse button
|
||
if (d3_event.button !== 0) return;
|
||
|
||
if (!d3_event.composedPath) return;
|
||
|
||
// some targets have default click events we don't want to override
|
||
var isOkayTarget = d3_event.composedPath().some(function(node) {
|
||
// we only care about element nodes
|
||
return node.nodeType === 1 &&
|
||
// clicking <input> focuses it and/or changes a value
|
||
(node.nodeName === 'INPUT' ||
|
||
// clicking <label> affects its <input> by default
|
||
node.nodeName === 'LABEL' ||
|
||
// clicking <a> opens a hyperlink by default
|
||
node.nodeName === 'A');
|
||
});
|
||
if (isOkayTarget) return;
|
||
|
||
// disable double-tap-to-zoom on touchscreens
|
||
d3_event.preventDefault();
|
||
});
|
||
|
||
var detected = utilDetect();
|
||
|
||
// only WebKit supports gesture events
|
||
if ('GestureEvent' in window &&
|
||
// Listening for gesture events on iOS 13.4+ breaks double-tapping,
|
||
// but we only need to do this on desktop Safari anyway. – #7694
|
||
!detected.isMobileWebKit) {
|
||
|
||
// On iOS we disable pinch-to-zoom of the UI via the `touch-action`
|
||
// CSS property, but on desktop Safari we need to manually cancel the
|
||
// default gesture events.
|
||
container.on('gesturestart.ui gesturechange.ui gestureend.ui', function(d3_event) {
|
||
// disable pinch-to-zoom of the UI via multitouch trackpads on macOS Safari
|
||
d3_event.preventDefault();
|
||
});
|
||
}
|
||
|
||
if ('PointerEvent' in window) {
|
||
d3_select(window)
|
||
.on('pointerdown.ui pointerup.ui', function(d3_event) {
|
||
var pointerType = d3_event.pointerType || 'mouse';
|
||
if (_lastPointerType !== pointerType) {
|
||
_lastPointerType = pointerType;
|
||
container
|
||
.attr('pointer', pointerType);
|
||
}
|
||
}, true);
|
||
} else {
|
||
_lastPointerType = 'mouse';
|
||
container
|
||
.attr('pointer', 'mouse');
|
||
}
|
||
|
||
container
|
||
.attr('lang', localizer.localeCode())
|
||
.attr('dir', localizer.textDirection());
|
||
|
||
// setup fullscreen keybindings (no button shown at this time)
|
||
container
|
||
.call(uiFullScreen(context));
|
||
|
||
var map = context.map();
|
||
map.redrawEnable(false); // don't draw until we've set zoom/lat/long
|
||
|
||
map
|
||
.on('hitMinZoom.ui', function() {
|
||
ui.flash
|
||
.iconName('#iD-icon-no')
|
||
.label(t.append('cannot_zoom'))();
|
||
});
|
||
|
||
container
|
||
.append('svg')
|
||
.attr('id', 'ideditor-defs')
|
||
.call(ui.svgDefs);
|
||
|
||
container
|
||
.append('div')
|
||
.attr('class', 'sidebar')
|
||
.call(ui.sidebar);
|
||
|
||
var content = container
|
||
.append('div')
|
||
.attr('class', 'main-content active');
|
||
|
||
// Top toolbar
|
||
content
|
||
.append('div')
|
||
.attr('class', 'top-toolbar-wrap')
|
||
.append('div')
|
||
.attr('class', 'top-toolbar fillD')
|
||
.call(uiTopToolbar(context));
|
||
|
||
content
|
||
.append('div')
|
||
.attr('class', 'main-map')
|
||
.attr('dir', 'ltr')
|
||
.call(map);
|
||
|
||
var overMap = content
|
||
.append('div')
|
||
.attr('class', 'over-map');
|
||
|
||
// HACK: Mobile Safari 14 likes to select anything selectable when long-
|
||
// pressing, even if it's not targeted. This conflicts with long-pressing
|
||
// to show the edit menu. We add a selectable offscreen element as the first
|
||
// child to trick Safari into not showing the selection UI.
|
||
overMap
|
||
.append('div')
|
||
.attr('class', 'select-trap')
|
||
.text('t');
|
||
|
||
overMap
|
||
.call(uiMapInMap(context))
|
||
.call(uiNotice(context));
|
||
|
||
overMap
|
||
.append('div')
|
||
.attr('class', 'spinner')
|
||
.call(uiSpinner(context));
|
||
|
||
// Map controls
|
||
var controlsWrap = overMap
|
||
.append('div')
|
||
.attr('class', 'map-controls-wrap');
|
||
|
||
var controls = controlsWrap
|
||
.append('div')
|
||
.attr('class', 'map-controls');
|
||
|
||
controls
|
||
.append('div')
|
||
.attr('class', 'map-control zoombuttons')
|
||
.call(uiZoom(context));
|
||
|
||
controls
|
||
.append('div')
|
||
.attr('class', 'map-control zoom-to-selection-control')
|
||
.call(uiZoomToSelection(context));
|
||
|
||
controls
|
||
.append('div')
|
||
.attr('class', 'map-control geolocate-control')
|
||
.call(uiGeolocate(context));
|
||
|
||
controlsWrap.on('wheel.mapControls', function(d3_event) {
|
||
if (!d3_event.deltaX) {
|
||
controlsWrap.node().scrollTop += d3_event.deltaY;
|
||
}
|
||
});
|
||
|
||
// Add panes
|
||
// This should happen after map is initialized, as some require surface()
|
||
var panes = overMap
|
||
.append('div')
|
||
.attr('class', 'map-panes');
|
||
|
||
var uiPanes = [
|
||
uiPaneBackground(context),
|
||
uiPaneMapData(context),
|
||
uiPaneIssues(context),
|
||
uiPanePreferences(context),
|
||
uiPaneHelp(context)
|
||
];
|
||
|
||
uiPanes.forEach(function(pane) {
|
||
controls
|
||
.append('div')
|
||
.attr('class', 'map-control map-pane-control ' + pane.id + '-control')
|
||
.call(pane.renderToggleButton);
|
||
|
||
panes
|
||
.call(pane.renderPane);
|
||
});
|
||
|
||
ui.info = uiInfo(context);
|
||
|
||
overMap
|
||
.call(ui.info);
|
||
|
||
overMap
|
||
.append('div')
|
||
.attr('class', 'photoviewer')
|
||
.classed('al', true) // 'al'=left, 'ar'=right
|
||
.classed('hide', true)
|
||
.call(ui.photoviewer);
|
||
|
||
overMap
|
||
.append('div')
|
||
.attr('class', 'attribution-wrap')
|
||
.attr('dir', 'ltr')
|
||
.call(uiAttribution(context));
|
||
|
||
|
||
// Add footer
|
||
var about = content
|
||
.append('div')
|
||
.attr('class', 'map-footer');
|
||
|
||
about
|
||
.append('div')
|
||
.attr('class', 'api-status')
|
||
.call(uiStatus(context));
|
||
|
||
|
||
var footer = about
|
||
.append('div')
|
||
.attr('class', 'map-footer-bar fillD');
|
||
|
||
footer
|
||
.append('div')
|
||
.attr('class', 'flash-wrap footer-hide');
|
||
|
||
var footerWrap = footer
|
||
.append('div')
|
||
.attr('class', 'main-footer-wrap footer-show');
|
||
|
||
footerWrap
|
||
.append('div')
|
||
.attr('class', 'scale-block')
|
||
.call(uiScale(context));
|
||
|
||
var aboutList = footerWrap
|
||
.append('div')
|
||
.attr('class', 'info-block')
|
||
.append('ul')
|
||
.attr('class', 'map-footer-list');
|
||
|
||
aboutList
|
||
.append('li')
|
||
.attr('class', 'user-list')
|
||
.call(uiContributors(context));
|
||
|
||
var apiConnections = context.connection().apiConnections();
|
||
if (apiConnections && apiConnections.length > 1) {
|
||
aboutList
|
||
.append('li')
|
||
.attr('class', 'source-switch')
|
||
.call(uiSourceSwitch(context)
|
||
.keys(apiConnections)
|
||
);
|
||
}
|
||
|
||
aboutList
|
||
.append('li')
|
||
.attr('class', 'issues-info')
|
||
.call(uiIssuesInfo(context));
|
||
|
||
aboutList
|
||
.append('li')
|
||
.attr('class', 'feature-warning')
|
||
.call(uiFeatureInfo(context));
|
||
|
||
var issueLinks = aboutList
|
||
.append('li');
|
||
|
||
issueLinks
|
||
.append('a')
|
||
.attr('target', '_blank')
|
||
.attr('href', 'https://github.com/openstreetmap/iD/issues')
|
||
.attr('aria-label', t('report_a_bug'))
|
||
.call(svgIcon('#iD-icon-bug', 'light'))
|
||
.call(uiTooltip()
|
||
.title(() => t.append('report_a_bug'))
|
||
.placement('top'));
|
||
|
||
issueLinks
|
||
.append('a')
|
||
.attr('target', '_blank')
|
||
.attr('href', 'https://github.com/openstreetmap/iD/blob/develop/CONTRIBUTING.md#translating')
|
||
.attr('aria-label', t('help_translate'))
|
||
.call(svgIcon('#iD-icon-translate', 'light'))
|
||
.call(uiTooltip()
|
||
.title(() => t.append('help_translate'))
|
||
.placement('top'));
|
||
|
||
aboutList
|
||
.append('li')
|
||
.attr('class', 'version')
|
||
.call(uiVersion(context));
|
||
|
||
if (!context.embed()) {
|
||
aboutList
|
||
.call(uiAccount(context));
|
||
}
|
||
|
||
|
||
// Setup map dimensions and move map to initial center/zoom.
|
||
// This should happen after .main-content and toolbars exist.
|
||
ui.onResize();
|
||
map.redrawEnable(true);
|
||
|
||
ui.hash = behaviorHash(context);
|
||
ui.hash();
|
||
if (!ui.hash.hadLocation) {
|
||
map.centerZoom([0, 0], 2);
|
||
}
|
||
|
||
// Bind events
|
||
window.onbeforeunload = function() {
|
||
return context.save();
|
||
};
|
||
window.onunload = function() {
|
||
context.history().unlock();
|
||
};
|
||
|
||
d3_select(window)
|
||
.on('resize.editor', function() {
|
||
ui.onResize();
|
||
});
|
||
|
||
|
||
var panPixels = 80;
|
||
context.keybinding()
|
||
.on('⌫', function(d3_event) { d3_event.preventDefault(); })
|
||
.on([t('sidebar.key'), '`', '²', '@'], ui.sidebar.toggle) // #5663, #6864 - common QWERTY, AZERTY
|
||
.on('←', pan([panPixels, 0]))
|
||
.on('↑', pan([0, panPixels]))
|
||
.on('→', pan([-panPixels, 0]))
|
||
.on('↓', pan([0, -panPixels]))
|
||
.on(uiCmd('⌥←'), pan([map.dimensions()[0], 0]))
|
||
.on(uiCmd('⌥↑'), pan([0, map.dimensions()[1]]))
|
||
.on(uiCmd('⌥→'), pan([-map.dimensions()[0], 0]))
|
||
.on(uiCmd('⌥↓'), pan([0, -map.dimensions()[1]]))
|
||
.on(uiCmd('⌘' + t('background.key')), function quickSwitch(d3_event) {
|
||
if (d3_event) {
|
||
d3_event.stopImmediatePropagation();
|
||
d3_event.preventDefault();
|
||
}
|
||
var previousBackground = context.background().findSource(prefs('background-last-used-toggle'));
|
||
if (previousBackground) {
|
||
var currentBackground = context.background().baseLayerSource();
|
||
prefs('background-last-used-toggle', currentBackground.id);
|
||
prefs('background-last-used', previousBackground.id);
|
||
context.background().baseLayerSource(previousBackground);
|
||
}
|
||
})
|
||
.on(t('area_fill.wireframe.key'), function toggleWireframe(d3_event) {
|
||
d3_event.preventDefault();
|
||
d3_event.stopPropagation();
|
||
context.map().toggleWireframe();
|
||
})
|
||
.on(uiCmd('⌥' + t('area_fill.wireframe.key')), function toggleOsmData(d3_event) {
|
||
d3_event.preventDefault();
|
||
d3_event.stopPropagation();
|
||
|
||
// Don't allow layer changes while drawing - #6584
|
||
var mode = context.mode();
|
||
if (mode && /^draw/.test(mode.id)) return;
|
||
|
||
var layer = context.layers().layer('osm');
|
||
if (layer) {
|
||
layer.enabled(!layer.enabled());
|
||
if (!layer.enabled()) {
|
||
context.enter(modeBrowse(context));
|
||
}
|
||
}
|
||
})
|
||
.on(t('map_data.highlight_edits.key'), function toggleHighlightEdited(d3_event) {
|
||
d3_event.preventDefault();
|
||
context.map().toggleHighlightEdited();
|
||
});
|
||
|
||
context
|
||
.on('enter.editor', function(entered) {
|
||
container
|
||
.classed('mode-' + entered.id, true);
|
||
})
|
||
.on('exit.editor', function(exited) {
|
||
container
|
||
.classed('mode-' + exited.id, false);
|
||
});
|
||
|
||
context.enter(modeBrowse(context));
|
||
|
||
if (!_initCounter++) {
|
||
if (!ui.hash.startWalkthrough) {
|
||
context.container()
|
||
.call(uiSplash(context))
|
||
.call(uiRestore(context));
|
||
}
|
||
|
||
context.container()
|
||
.call(ui.shortcuts);
|
||
}
|
||
|
||
var osm = context.connection();
|
||
var auth = uiLoading(context).message(t.html('loading_auth')).blocking(true);
|
||
|
||
if (osm && auth) {
|
||
osm
|
||
.on('authLoading.ui', function() {
|
||
context.container()
|
||
.call(auth);
|
||
})
|
||
.on('authDone.ui', function() {
|
||
auth.close();
|
||
});
|
||
}
|
||
|
||
_initCounter++;
|
||
|
||
if (ui.hash.startWalkthrough) {
|
||
ui.hash.startWalkthrough = false;
|
||
context.container().call(uiIntro(context));
|
||
}
|
||
|
||
|
||
function pan(d) {
|
||
return function(d3_event) {
|
||
if (d3_event.shiftKey) return;
|
||
if (context.container().select('.combobox').size()) return;
|
||
d3_event.preventDefault();
|
||
context.map().pan(d, 100);
|
||
};
|
||
}
|
||
|
||
}
|
||
|
||
|
||
let ui = {};
|
||
|
||
let _loadPromise;
|
||
// renders the iD interface into the container node
|
||
ui.ensureLoaded = () => {
|
||
|
||
if (_loadPromise) return _loadPromise;
|
||
|
||
return _loadPromise = Promise.all([
|
||
// must have strings and presets before loading the UI
|
||
localizer.ensureLoaded(),
|
||
presetManager.ensureLoaded()
|
||
])
|
||
.then(() => {
|
||
if (!context.container().empty()) render(context.container());
|
||
})
|
||
.catch(err => console.error(err)); // eslint-disable-line
|
||
};
|
||
|
||
|
||
// `ui.restart()` will destroy and rebuild the entire iD interface,
|
||
// for example to switch the locale while iD is running.
|
||
ui.restart = function() {
|
||
context.keybinding().clear();
|
||
|
||
_loadPromise = null;
|
||
|
||
context.container().selectAll('*').remove();
|
||
|
||
ui.ensureLoaded();
|
||
};
|
||
|
||
ui.lastPointerType = function() {
|
||
return _lastPointerType;
|
||
};
|
||
|
||
ui.svgDefs = svgDefs(context);
|
||
|
||
ui.flash = uiFlash(context);
|
||
|
||
ui.sidebar = uiSidebar(context);
|
||
|
||
ui.photoviewer = uiPhotoviewer(context);
|
||
|
||
ui.shortcuts = uiShortcuts(context);
|
||
|
||
ui.onResize = function(withPan) {
|
||
var map = context.map();
|
||
|
||
// Recalc dimensions of map and sidebar.. (`true` = force recalc)
|
||
// This will call `getBoundingClientRect` and trigger reflow,
|
||
// but the values will be cached for later use.
|
||
var mapDimensions = utilGetDimensions(context.container().select('.main-content'), true);
|
||
utilGetDimensions(context.container().select('.sidebar'), true);
|
||
|
||
if (withPan !== undefined) {
|
||
map.redrawEnable(false);
|
||
map.pan(withPan);
|
||
map.redrawEnable(true);
|
||
}
|
||
map.dimensions(mapDimensions);
|
||
|
||
ui.photoviewer.onMapResize();
|
||
|
||
// check if header or footer have overflowed
|
||
ui.checkOverflow('.top-toolbar');
|
||
ui.checkOverflow('.map-footer-bar');
|
||
|
||
// Use outdated code so it works on Explorer
|
||
var resizeWindowEvent = document.createEvent('Event');
|
||
|
||
resizeWindowEvent.initEvent('resizeWindow', true, true);
|
||
|
||
document.dispatchEvent(resizeWindowEvent);
|
||
};
|
||
|
||
|
||
// Call checkOverflow when resizing or whenever the contents change.
|
||
ui.checkOverflow = function(selector, reset) {
|
||
if (reset) {
|
||
delete _needWidth[selector];
|
||
}
|
||
|
||
var selection = context.container().select(selector);
|
||
if (selection.empty()) return;
|
||
|
||
var scrollWidth = selection.property('scrollWidth');
|
||
var clientWidth = selection.property('clientWidth');
|
||
var needed = _needWidth[selector] || scrollWidth;
|
||
|
||
if (scrollWidth > clientWidth) { // overflow happening
|
||
selection.classed('narrow', true);
|
||
if (!_needWidth[selector]) {
|
||
_needWidth[selector] = scrollWidth;
|
||
}
|
||
|
||
} else if (scrollWidth >= needed) {
|
||
selection.classed('narrow', false);
|
||
}
|
||
};
|
||
|
||
ui.togglePanes = function(showPane) {
|
||
var hidePanes = context.container().selectAll('.map-pane.shown');
|
||
|
||
var side = localizer.textDirection() === 'ltr' ? 'right' : 'left';
|
||
|
||
hidePanes
|
||
.classed('shown', false)
|
||
.classed('hide', true);
|
||
|
||
context.container().selectAll('.map-pane-control button')
|
||
.classed('active', false);
|
||
|
||
if (showPane) {
|
||
hidePanes
|
||
.classed('shown', false)
|
||
.classed('hide', true)
|
||
.style(side, '-500px');
|
||
|
||
context.container().selectAll('.' + showPane.attr('pane') + '-control button')
|
||
.classed('active', true);
|
||
|
||
showPane
|
||
.classed('shown', true)
|
||
.classed('hide', false);
|
||
if (hidePanes.empty()) {
|
||
showPane
|
||
.style(side, '-500px')
|
||
.transition()
|
||
.duration(200)
|
||
.style(side, '0px');
|
||
} else {
|
||
showPane
|
||
.style(side, '0px');
|
||
}
|
||
} else {
|
||
hidePanes
|
||
.classed('shown', true)
|
||
.classed('hide', false)
|
||
.style(side, '0px')
|
||
.transition()
|
||
.duration(200)
|
||
.style(side, '-500px')
|
||
.on('end', function() {
|
||
d3_select(this)
|
||
.classed('shown', false)
|
||
.classed('hide', true);
|
||
});
|
||
}
|
||
};
|
||
|
||
|
||
var _editMenu = uiEditMenu(context);
|
||
|
||
ui.editMenu = function() {
|
||
return _editMenu;
|
||
};
|
||
|
||
ui.showEditMenu = function(anchorPoint, triggerType, operations) {
|
||
|
||
// remove any displayed menu
|
||
ui.closeEditMenu();
|
||
|
||
if (!operations && context.mode().operations) operations = context.mode().operations();
|
||
if (!operations || !operations.length) return;
|
||
|
||
// disable menu if in wide selection, for example
|
||
if (!context.map().editableDataEnabled()) return;
|
||
|
||
var surfaceNode = context.surface().node();
|
||
if (surfaceNode.focus) { // FF doesn't support it
|
||
// focus the surface or else clicking off the menu may not trigger modeBrowse
|
||
surfaceNode.focus();
|
||
}
|
||
|
||
operations.forEach(function(operation) {
|
||
if (operation.point) operation.point(anchorPoint);
|
||
});
|
||
|
||
_editMenu
|
||
.anchorLoc(anchorPoint)
|
||
.triggerType(triggerType)
|
||
.operations(operations);
|
||
|
||
// render the menu
|
||
context.map().supersurface.call(_editMenu);
|
||
};
|
||
|
||
ui.closeEditMenu = function() {
|
||
// remove any existing menu no matter how it was added
|
||
context.map().supersurface
|
||
.select('.edit-menu').remove();
|
||
};
|
||
|
||
|
||
var _saveLoading = d3_select(null);
|
||
|
||
context.uploader()
|
||
.on('saveStarted.ui', function() {
|
||
_saveLoading = uiLoading(context)
|
||
.message(t.html('save.uploading'))
|
||
.blocking(true);
|
||
context.container().call(_saveLoading); // block input during upload
|
||
})
|
||
.on('saveEnded.ui', function() {
|
||
_saveLoading.close();
|
||
_saveLoading = d3_select(null);
|
||
});
|
||
|
||
return ui;
|
||
}
|