import * as d3 from 'd3'; import _ from 'lodash'; import { d3keybinding } from '../lib/d3.keybinding.js'; import { t, textDirection } from '../util/locale'; import { geoMetersToOffset, geoOffsetToMeters } from '../geo/index'; import { utilDetect } from '../util/detect'; import { utilSetTransform, utilCallWhenIdle } from '../util/index'; import { svgIcon } from '../svg/index'; import { uiMapInMap } from './map_in_map'; import { uiCmd } from './cmd'; import { uiTooltipHtml } from './tooltipHtml'; import { tooltip } from '../util/tooltip'; export function uiBackground(context) { var key = t('background.key'), detected = utilDetect(), opacities = [1, 0.75, 0.5, 0.25], directions = [ ['right', [0.5, 0]], ['top', [0, -0.5]], ['left', [-0.5, 0]], ['bottom', [0, 0.5]]], opacityDefault = (context.storage('background-opacity') !== null) ? (+context.storage('background-opacity')) : 1.0, customSource = context.background().findSource('custom'), previous; // Can be 0 from <1.3.0 use or due to issue #1923. if (opacityDefault === 0) opacityDefault = 1.0; function background(selection) { function sortSources(a, b) { return a.best() && !b.best() ? -1 : b.best() && !a.best() ? 1 : d3.descending(a.area(), b.area()) || d3.ascending(a.name(), b.name()) || 0; } function setOpacity(d) { var bg = context.container().selectAll('.layer-background') .transition() .style('opacity', d) .attr('data-opacity', d); if (!detected.opera) { utilSetTransform(bg, 0, 0); } opacityList.selectAll('li') .classed('active', function(_) { return _ === d; }); context.storage('background-opacity', d); } function setTooltips(selection) { selection.each(function(d, i, nodes) { var item = d3.select(this).select('label'), span = item.select('span'), placement = (i < nodes.length / 2) ? 'bottom' : 'top', description = d.description(), isOverflowing = (span.property('clientWidth') !== span.property('scrollWidth')); if (d === previous) { item.call(tooltip() .placement(placement) .html(true) .title(function() { var tip = '
' + t('background.switch') + '
'; return uiTooltipHtml(tip, uiCmd('⌘' + key)); }) ); } else if (description || isOverflowing) { item.call(tooltip() .placement(placement) .title(description || d.name()) ); } else { item.call(tooltip().destroy); } }); } function selectLayer() { function active(d) { return context.background().showsLayer(d); } content.selectAll('.layer') .classed('active', active) .classed('switch', function(d) { return d === previous; }) .call(setTooltips) .selectAll('input') .property('checked', active); } function clickSetSource(d) { if (d.id === 'custom' && !d.template()) { return editCustom(); } d3.event.preventDefault(); previous = context.background().baseLayerSource(); context.background().baseLayerSource(d); selectLayer(); document.activeElement.blur(); } function editCustom() { d3.event.preventDefault(); var example = 'https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png'; var template = window.prompt( t('background.custom_prompt', { example: example }), customSource.template() || example ); if (template) { context.storage('background-custom-template', template); customSource.template(template); clickSetSource(customSource); } else { selectLayer(); } } function clickSetOverlay(d) { d3.event.preventDefault(); context.background().toggleOverlayLayer(d); selectLayer(); document.activeElement.blur(); } function drawList(layerList, type, change, filter) { var sources = context.background() .sources(context.map().extent()) .filter(filter); var layerLinks = layerList.selectAll('li.layer') .data(sources, function(d) { return d.name(); }); layerLinks.exit() .remove(); var enter = layerLinks.enter() .append('li') .attr('class', 'layer') .classed('layer-custom', function(d) { return d.id === 'custom'; }) .classed('best', function(d) { return d.best(); }); enter.filter(function(d) { return d.id === 'custom'; }) .append('button') .attr('class', 'layer-browse') .call(tooltip() .title(t('background.custom_button')) .placement((textDirection === 'rtl') ? 'right' : 'left')) .on('click', editCustom) .call(svgIcon('#icon-search')); enter.filter(function(d) { return d.best(); }) .append('div') .attr('class', 'best') .call(tooltip() .title(t('background.best_imagery')) .placement((textDirection === 'rtl') ? 'right' : 'left')) .append('span') .html('★'); var label = enter .append('label'); label .append('input') .attr('type', type) .attr('name', 'layers') .on('change', change); label .append('span') .text(function(d) { return d.name(); }); layerList.selectAll('li.layer') .sort(sortSources) .style('display', layerList.selectAll('li.layer').data().length > 0 ? 'block' : 'none'); } function update() { backgroundList.call(drawList, 'radio', clickSetSource, function(d) { return !d.overlay; }); overlayList.call(drawList, 'checkbox', clickSetOverlay, function(d) { return d.overlay; }); selectLayer(); updateOffsetVal(); } function updateOffsetVal() { var meters = geoOffsetToMeters(context.background().offset()), x = +meters[0].toFixed(2), y = +meters[1].toFixed(2); d3.selectAll('.nudge-inner-rect') .select('input') .classed('error', false) .property('value', x + ', ' + y); d3.selectAll('.nudge-reset') .classed('disabled', function() { return (x === 0 && y === 0); }); } function resetOffset() { if (d3.event.button !== 0) return; context.background().offset([0, 0]); updateOffsetVal(); } function nudge(d) { context.background().nudge(d, context.map().zoom()); updateOffsetVal(); } function buttonOffset(d) { if (d3.event.button !== 0) return; var timeout = window.setTimeout(function() { interval = window.setInterval(nudge.bind(null, d), 100); }, 500), interval; function doneNudge() { window.clearTimeout(timeout); window.clearInterval(interval); d3.select(window) .on('mouseup.buttonoffset', null, true) .on('mousedown.buttonoffset', null, true); } d3.select(window) .on('mouseup.buttonoffset', doneNudge, true) .on('mousedown.buttonoffset', doneNudge, true); nudge(d); } function inputOffset() { if (d3.event.button !== 0) return; var input = d3.select(this); var d = input.node().value; if (d === '') return resetOffset(); d = d.replace(/;/g, ',').split(',').map(function(n) { // if n is NaN, it will always get mapped to false. return !isNaN(n) && n; }); if (d.length !== 2 || !d[0] || !d[1]) { input.classed('error', true); return; } context.background().offset(geoMetersToOffset(d)); updateOffsetVal(); } function dragOffset() { if (d3.event.button !== 0) return; var origin = [d3.event.clientX, d3.event.clientY]; context.container() .append('div') .attr('class', 'nudge-surface'); d3.select(window) .on('mousemove.offset', function() { var latest = [d3.event.clientX, d3.event.clientY]; var d = [ -(origin[0] - latest[0]) / 4, -(origin[1] - latest[1]) / 4 ]; origin = latest; nudge(d); }) .on('mouseup.offset', function() { if (d3.event.button !== 0) return; d3.selectAll('.nudge-surface') .remove(); d3.select(window) .on('mousemove.offset', null) .on('mouseup.offset', null); }); d3.event.preventDefault(); } function hide() { setVisible(false); } function toggle() { if (d3.event) { d3.event.preventDefault(); } tooltipBehavior.hide(button); setVisible(!button.classed('active')); } function quickSwitch() { if (d3.event) { d3.event.stopImmediatePropagation(); d3.event.preventDefault(); } if (previous) { clickSetSource(previous); } } function setVisible(show) { if (show !== shown) { button.classed('active', show); shown = show; if (show) { selection .on('mousedown.background-inside', function() { d3.event.stopPropagation(); }); content .style('display', 'block') .style('right', '-300px') .transition() .duration(200) .style('right', '0px'); content.selectAll('.layer') .call(setTooltips); } else { content .style('display', 'block') .style('right', '0px') .transition() .duration(200) .style('right', '-300px') .on('end', function() { d3.select(this).style('display', 'none'); }); selection .on('mousedown.background-inside', null); } } } var content = selection .append('div') .attr('class', 'fillL map-overlay col3 content hide'), tooltipBehavior = tooltip() .placement((textDirection === 'rtl') ? 'right' : 'left') .html(true) .title(uiTooltipHtml(t('background.description'), key)), button = selection .append('button') .attr('tabindex', -1) .on('click', toggle) .call(svgIcon('#icon-layers', 'light')) .call(tooltipBehavior), shown = false; /* opacity switcher */ var opawrap = content .append('div') .attr('class', 'opacity-options-wrapper'); opawrap .append('h4') .text(t('background.title')); var opacityList = opawrap .append('ul') .attr('class', 'opacity-options'); opacityList.selectAll('div.opacity') .data(opacities) .enter() .append('li') .attr('data-original-title', function(d) { return t('background.percent_brightness', { opacity: (d * 100) }); }) .on('click.set-opacity', setOpacity) .html('
') .call(tooltip() .placement((textDirection === 'rtl') ? 'right' : 'left')) .append('div') .attr('class', 'opacity') .style('opacity', function(d) { return 1.25 - d; }); /* background list */ var backgroundList = content .append('ul') .attr('class', 'layer-list') .attr('dir', 'auto'); content .append('div') .attr('class', 'imagery-faq') .append('a') .attr('target', '_blank') .attr('tabindex', -1) .call(svgIcon('#icon-out-link', 'inline')) .attr('href', 'https://github.com/openstreetmap/iD/blob/master/FAQ.md#how-can-i-report-an-issue-with-background-imagery') .append('span') .text(t('background.imagery_source_faq')); /* overlay list */ var overlayList = content .append('ul') .attr('class', 'layer-list'); var controls = content .append('div') .attr('class', 'controls-list'); /* minimap toggle */ var minimapLabel = controls .append('label') .call(tooltip() .html(true) .title(uiTooltipHtml(t('background.minimap.tooltip'), t('background.minimap.key'))) .placement('top') ); minimapLabel .classed('minimap-toggle', true) .append('input') .attr('type', 'checkbox') .on('change', function() { uiMapInMap.toggle(); d3.event.preventDefault(); }); minimapLabel .append('span') .text(t('background.minimap.description')); /* imagery offset controls */ var adjustments = content .append('div') .attr('class', 'adjustments'); adjustments .append('a') .text(t('background.fix_misalignment')) .attr('href', '#') .classed('hide-toggle', true) .classed('expanded', false) .on('click', function() { if (d3.event.button !== 0) return; var exp = d3.select(this).classed('expanded'); nudgeContainer.style('display', exp ? 'none' : 'block'); d3.select(this).classed('expanded', !exp); d3.event.preventDefault(); }); var nudgeContainer = adjustments .append('div') .attr('class', 'nudge-container cf') .style('display', 'none'); nudgeContainer .append('div') .attr('class', 'nudge-instructions') .text(t('background.offset')); var nudgeRect = nudgeContainer .append('div') .attr('class', 'nudge-outer-rect') .on('mousedown', dragOffset); nudgeRect .append('div') .attr('class', 'nudge-inner-rect') .append('input') .on('change', inputOffset) .on('mousedown', function() { if (d3.event.button !== 0) return; d3.event.stopPropagation(); }); nudgeContainer .append('div') .selectAll('button') .data(directions).enter() .append('button') .attr('class', function(d) { return d[0] + ' nudge'; }) .on('mousedown', function(d) { if (d3.event.button !== 0) return; buttonOffset(d[1]); }); nudgeContainer .append('button') .attr('title', t('background.reset')) .attr('class', 'nudge-reset disabled') .on('click', resetOffset) .call( (textDirection === 'rtl') ? svgIcon('#icon-redo') : svgIcon('#icon-undo') ); context.map() .on('move.background-update', _.debounce(utilCallWhenIdle(update), 1000)); context.background() .on('change.background-update', update); update(); setOpacity(opacityDefault); var keybinding = d3keybinding('background') .on(key, toggle) .on(uiCmd('⌘' + key), quickSwitch) .on([t('map_data.key'), t('help.key')], hide); d3.select(document) .call(keybinding); context.surface().on('mousedown.background-outside', hide); context.container().on('mousedown.background-outside', hide); } return background; }