modules/ui/sections/background_list.js (291 lines of code) (raw):

import _debounce from 'lodash-es/debounce'; import { descending as d3_descending, ascending as d3_ascending } from 'd3-array'; import { select as d3_select } from 'd3-selection'; import { easeCubicInOut as d3_easeCubicInOut } from 'd3-ease'; import { prefs } from '../../core/preferences'; import { t, localizer } from '../../core/localizer'; import { uiTooltip } from '../tooltip'; import { svgIcon } from '../../svg/icon'; import { uiCmd } from '../cmd'; import { uiSettingsCustomBackground } from '../settings/custom_background'; import { uiMapInMap } from '../map_in_map'; import { uiSection } from '../section'; export function uiSectionBackgroundList(context) { let _backgroundList = d3_select(null); const _customSource = context.background().findSource('custom'); const _settingsCustomBackground = uiSettingsCustomBackground(context) .on('change', customChanged); const section = uiSection('background-list', context) .label(t('background.backgrounds')) .disclosureContent(renderDisclosureContent); const favoriteBackgroundsJSON = prefs('background-favorites'); const _favoriteBackgrounds = favoriteBackgroundsJSON ? JSON.parse(favoriteBackgroundsJSON) : {}; function previousBackgroundID() { return prefs('background-last-used-toggle'); } function renderDisclosureContent(selection) { // the background list const container = selection.selectAll('.layer-background-list') .data([0]); _backgroundList = container.enter() .append('ul') .attr('class', 'layer-list layer-background-list') .attr('dir', 'auto') .merge(container); // add minimap toggle below list const bgExtrasListEnter = selection.selectAll('.bg-extras-list') .data([0]) .enter() .append('ul') .attr('class', 'layer-list bg-extras-list'); const minimapLabelEnter = bgExtrasListEnter .append('li') .attr('class', 'minimap-toggle-item') .append('label') .call(uiTooltip() .title(t('background.minimap.tooltip')) .keys([t('background.minimap.key')]) .placement('top') ); minimapLabelEnter .append('input') .attr('type', 'checkbox') .on('change', (d3_event) => { d3_event.preventDefault(); uiMapInMap.toggle(); }); minimapLabelEnter .append('span') .text(t('background.minimap.description')); const panelLabelEnter = bgExtrasListEnter .append('li') .attr('class', 'background-panel-toggle-item') .append('label') .call(uiTooltip() .title(t('background.panel.tooltip')) .keys([uiCmd('⌘⇧' + t('info_panels.background.key'))]) .placement('top') ); panelLabelEnter .append('input') .attr('type', 'checkbox') .on('change', (d3_event) => { d3_event.preventDefault(); context.ui().info.toggle('background'); }); panelLabelEnter .append('span') .text(t('background.panel.description')); const locPanelLabelEnter = bgExtrasListEnter .append('li') .attr('class', 'location-panel-toggle-item') .append('label') .call(uiTooltip() .title(t('background.location_panel.tooltip')) .keys([uiCmd('⌘⇧' + t('info_panels.location.key'))]) .placement('top') ); locPanelLabelEnter .append('input') .attr('type', 'checkbox') .on('change', (d3_event) => { d3_event.preventDefault(); context.ui().info.toggle('location'); }); locPanelLabelEnter .append('span') .text(t('background.location_panel.description')); // "Info / Report a Problem" link selection.selectAll('.imagery-faq') .data([0]) .enter() .append('div') .attr('class', 'imagery-faq') .append('a') .attr('target', '_blank') .call(svgIcon('#iD-icon-out-link', 'inline')) .attr('href', 'https://github.com/openstreetmap/iD/blob/develop/FAQ.md#how-can-i-report-an-issue-with-background-imagery') .append('span') .text(t('background.imagery_problem_faq')); _backgroundList .call(drawListItems, 'radio', chooseBackground, (d) => { return !d.isHidden() && !d.overlay; }); } function setTooltips(selection) { selection.each((d, i, nodes) => { const item = d3_select(nodes[i]).select('label'); const span = item.select('span'); const placement = (i < nodes.length / 2) ? 'bottom' : 'top'; const description = d.description(); const isOverflowing = (span.property('clientWidth') !== span.property('scrollWidth')); item.call(uiTooltip().destroyAny); if (d.id === previousBackgroundID()) { item.call(uiTooltip() .placement(placement) .title('<div>' + t('background.switch') + '</div>') .keys([uiCmd('⌘' + t('background.key'))]) ); } else if (description || isOverflowing) { item.call(uiTooltip() .placement(placement) .title(description || d.name()) ); } }); } function sortSources(a, b) { return _favoriteBackgrounds[a.id] && !_favoriteBackgrounds[b.id] ? -1 : _favoriteBackgrounds[b.id] && !_favoriteBackgrounds[a.id] ? 1 : a.best() && !b.best() ? -1 : b.best() && !a.best() ? 1 : d3_descending(a.area(), b.area()) || d3_ascending(a.name(), b.name()) || 0; } function drawListItems(layerList, type, change, filter) { const sources = context.background() .sources(context.map().extent(), context.map().zoom(), true) .filter(filter); const layerLinks = layerList.selectAll('li') .data(sources, (d) => { return d.id; }); layerLinks.exit() .remove(); const layerLinksEnter = layerLinks.enter() .append('li') .classed('layer-custom', (d) => { return d.id === 'custom'; }) .classed('best', (d) => { return d.best(); }); const label = layerLinksEnter .append('label'); label .append('input') .attr('type', type) .attr('name', 'layers') .on('change', change); label .append('span') .attr('class', 'background-name') .text((d) => { return d.name(); }); layerLinksEnter .append('button') .attr('class', 'background-favorite-button') .classed('active', (d) => { return !!_favoriteBackgrounds[d.id]; }) .attr('tabindex', -1) .call(svgIcon('#iD-icon-favorite')) .on('click', (d3_event, d) => { if (_favoriteBackgrounds[d.id]) { d3_select(d3_event.currentTarget).classed('active', false); delete _favoriteBackgrounds[d.id]; } else { d3_select(d3_event.currentTarget).classed('active', true); _favoriteBackgrounds[d.id] = true; } prefs('background-favorites', JSON.stringify(_favoriteBackgrounds)); d3_select(d3_event.currentTarget.parentElement) .transition() .duration(300) .ease(d3_easeCubicInOut) .style('background-color', 'orange') .transition() .duration(300) .ease(d3_easeCubicInOut) .style('background-color', null); layerList.selectAll('li') .sort(sortSources); layerList .call(updateLayerSelections); }); layerLinksEnter.filter((d) => { return d.id === 'custom'; }) .append('button') .attr('class', 'layer-browse') .call(uiTooltip() .title(t('settings.custom_background.tooltip')) .placement((localizer.textDirection() === 'rtl') ? 'right' : 'left') ) .on('click', editCustom) .call(svgIcon('#iD-icon-more')); layerLinksEnter.filter((d) => { return d.best(); }) .selectAll('label') .append('span') .attr('class', 'best') .call(uiTooltip() .title(t('background.best_imagery')) .placement('bottom') ) .call(svgIcon('#iD-icon-best-background')); layerList.selectAll('li') .sort(sortSources); layerList .call(updateLayerSelections); } function updateLayerSelections(selection) { function active(d) { return context.background().showsLayer(d); } selection.selectAll('li') .classed('active', active) .classed('switch', (d) => { return d.id === previousBackgroundID(); }) .call(setTooltips) .selectAll('input') .property('checked', active); } function chooseBackground(d3_event, d) { if (d.id === 'custom' && !d.template()) { return editCustom(); } const previousBackground = context.background().baseLayerSource(); prefs('background-last-used-toggle', previousBackground.id); prefs('background-last-used', d.id); context.background().baseLayerSource(d); document.activeElement.blur(); } function customChanged(d) { if (d && d.template) { _customSource.template(d.template); chooseBackground(undefined, _customSource); } else { _customSource.template(''); chooseBackground(undefined, context.background().findSource('none')); } } function editCustom(d3_event) { d3_event.preventDefault(); context.container() .call(_settingsCustomBackground); } context.background() .on('change.background_list', () => { _backgroundList.call(updateLayerSelections); }); context.map() .on('move.background_list', _debounce(() => { // layers in-view may have changed due to map move window.requestIdleCallback(section.reRender); }, 1000) ); function getBackgrounds(filter) { return context.background() .sources(context.map().extent(), context.map().zoom(), true) .filter(filter); } function chooseBackgroundAtOffset(offset) { const backgrounds = getBackgrounds((d) => { return !d.isHidden() && !d.overlay; }); backgrounds.sort(sortSources); const currentBackground = context.background().baseLayerSource(); const foundIndex = backgrounds.indexOf(currentBackground); if (foundIndex === -1) { // Can't find the current background, so just do nothing return; } let nextBackgroundIndex = (foundIndex + offset + backgrounds.length) % backgrounds.length; let nextBackground = backgrounds[nextBackgroundIndex]; if (nextBackground.id === 'custom' && !nextBackground.template()) { nextBackgroundIndex = (nextBackgroundIndex + offset + backgrounds.length) % backgrounds.length; nextBackground = backgrounds[nextBackgroundIndex]; } chooseBackground(undefined, nextBackground); } function nextBackground() { chooseBackgroundAtOffset(1); } function previousBackground() { chooseBackgroundAtOffset(-1); } context.keybinding() .on(t('background.next_background.key'), nextBackground) .on(t('background.previous_background.key'), previousBackground); return section; }