Files
iD/modules/ui/map_data.js
Quincy Morgan 523a467836 2.x: Make toolbar horizontally scrollable when it overflows (re: #6755, re: 7545f67063b5e7007ef2d8367e3181e59c04a487)
Generalize tooltip into popover control
Use the same popover control for tooltip as the preset browser and tools list popovers
Smartly position the preset browser popover and menu bar tooltips to stay fully onscreen
Position most tooltips closer to their controls
Fix small gap that could appear between a tooltip and its arrow
Allow wider toolbar tooltips
2019-12-16 13:30:07 -05:00

920 lines
27 KiB
JavaScript

import {
event as d3_event,
select as d3_select
} from 'd3-selection';
import { svgIcon } from '../svg/icon';
import { t, textDirection } from '../util/locale';
import { tooltip } from '../util/tooltip';
import { geoExtent } from '../geo';
import { modeBrowse } from '../modes/browse';
import { uiDisclosure } from './disclosure';
import { uiSettingsCustomData } from './settings/custom_data';
import { uiTooltipHtml } from './tooltipHtml';
import { uiCmd } from './cmd';
export function uiMapData(context) {
var key = t('map_data.key');
var osmDataToggleKey = uiCmd('⌥' + t('area_fill.wireframe.key'));
var features = context.features().keys();
var layers = context.layers();
var fills = ['wireframe', 'partial', 'full'];
var settingsCustomData = uiSettingsCustomData(context)
.on('change', customChanged);
var _pane = d3_select(null);
var _fillSelected = context.storage('area-fill') || 'partial';
var _dataLayerContainer = d3_select(null);
var _photoOverlayContainer = d3_select(null);
var _fillList = d3_select(null);
var _featureList = d3_select(null);
var _visualDiffList = d3_select(null);
var _QAList = d3_select(null);
function showsFeature(d) {
return context.features().enabled(d);
}
function autoHiddenFeature(d) {
if (d.type === 'kr_error') return context.errors().autoHidden(d);
return context.features().autoHidden(d);
}
function clickFeature(d) {
context.features().toggle(d);
update();
}
function showsQA(d) {
var QAKeys = [d];
var QALayers = layers.all().filter(function(obj) { return QAKeys.indexOf(obj.id) !== -1; });
var data = QALayers.filter(function(obj) { return obj.layer.supported(); });
function layerSupported(d) {
return d.layer && d.layer.supported();
}
function layerEnabled(d) {
return layerSupported(d) && d.layer.enabled();
}
return layerEnabled(data[0]);
}
function showsFill(d) {
return _fillSelected === d;
}
function setFill(d) {
fills.forEach(function(opt) {
context.surface().classed('fill-' + opt, Boolean(opt === d));
});
_fillSelected = d;
context.storage('area-fill', d);
if (d !== 'wireframe') {
context.storage('area-fill-toggle', d);
}
update();
}
function toggleHighlightEdited() {
d3_event.preventDefault();
var surface = context.surface();
surface.classed('highlight-edited', !surface.classed('highlight-edited'));
updateVisualDiffList();
context.map().pan([0,0]); // trigger a redraw
}
function showsLayer(which) {
var layer = layers.layer(which);
if (layer) {
return layer.enabled();
}
return false;
}
function setLayer(which, enabled) {
// Don't allow layer changes while drawing - #6584
var mode = context.mode();
if (mode && /^draw/.test(mode.id)) return;
var layer = layers.layer(which);
if (layer) {
layer.enabled(enabled);
if (!enabled && (which === 'osm' || which === 'notes')) {
context.enter(modeBrowse(context));
}
update();
}
}
function toggleLayer(which) {
setLayer(which, !showsLayer(which));
}
function drawPhotoItems(selection) {
var photoKeys = context.photos().overlayLayerIDs();
var photoLayers = layers.all().filter(function(obj) { return photoKeys.indexOf(obj.id) !== -1; });
var data = photoLayers.filter(function(obj) { return obj.layer.supported(); });
function layerSupported(d) {
return d.layer && d.layer.supported();
}
function layerEnabled(d) {
return layerSupported(d) && d.layer.enabled();
}
var ul = selection
.selectAll('.layer-list-photos')
.data([0]);
ul = ul.enter()
.append('ul')
.attr('class', 'layer-list layer-list-photos')
.merge(ul);
var li = ul.selectAll('.list-item-photos')
.data(data);
li.exit()
.remove();
var liEnter = li.enter()
.append('li')
.attr('class', function(d) {
var classes = 'list-item-photos list-item-' + d.id;
if (d.id === 'mapillary-signs' || d.id === 'mapillary-map-features') {
classes += ' indented';
}
return classes;
});
var labelEnter = liEnter
.append('label')
.each(function(d) {
var titleID;
if (d.id === 'mapillary-signs') titleID = 'mapillary.signs.tooltip';
else if (d.id === 'mapillary') titleID = 'mapillary_images.tooltip';
else if (d.id === 'openstreetcam') titleID = 'openstreetcam_images.tooltip';
else titleID = d.id.replace(/-/g, '_') + '.tooltip';
d3_select(this)
.call(tooltip()
.title(t(titleID))
.placement('top')
);
});
labelEnter
.append('input')
.attr('type', 'checkbox')
.on('change', function(d) { toggleLayer(d.id); });
labelEnter
.append('span')
.text(function(d) {
var id = d.id;
if (id === 'mapillary-signs') id = 'photo_overlays.traffic_signs';
return t(id.replace(/-/g, '_') + '.title');
});
labelEnter
.filter(function(d) { return d.id === 'mapillary-map-features'; })
.append('a')
.attr('class', 'request-data-link')
.attr('target', '_blank')
.attr('tabindex', -1)
.call(svgIcon('#iD-icon-out-link', 'inline'))
.attr('href', 'https://mapillary.github.io/mapillary_solutions/data-request')
.append('span')
.text(t('mapillary_map_features.request_data'));
// Update
li
.merge(liEnter)
.classed('active', layerEnabled)
.selectAll('input')
.property('checked', layerEnabled);
}
function drawPhotoTypeItems(selection) {
var data = context.photos().allPhotoTypes();
function typeEnabled(d) {
return context.photos().showsPhotoType(d);
}
var ul = selection
.selectAll('.layer-list-photo-types')
.data(context.photos().shouldFilterByPhotoType() ? [0] : []);
ul.exit()
.remove();
ul = ul.enter()
.append('ul')
.attr('class', 'layer-list layer-list-photo-types')
.merge(ul);
var li = ul.selectAll('.list-item-photo-types')
.data(data);
li.exit()
.remove();
var liEnter = li.enter()
.append('li')
.attr('class', function(d) {
return 'list-item-photo-types list-item-' + d;
});
var labelEnter = liEnter
.append('label')
.each(function(d) {
d3_select(this)
.call(tooltip()
.title(t('photo_overlays.photo_type.' + d + '.tooltip'))
.placement('top')
);
});
labelEnter
.append('input')
.attr('type', 'checkbox')
.on('change', function(d) {
context.photos().togglePhotoType(d);
update();
});
labelEnter
.append('span')
.text(function(d) {
return t('photo_overlays.photo_type.' + d + '.title');
});
// Update
li
.merge(liEnter)
.classed('active', typeEnabled)
.selectAll('input')
.property('checked', typeEnabled);
}
function drawOsmItems(selection) {
var osmKeys = ['osm', 'notes'];
var osmLayers = layers.all().filter(function(obj) { return osmKeys.indexOf(obj.id) !== -1; });
var ul = selection
.selectAll('.layer-list-osm')
.data([0]);
ul = ul.enter()
.append('ul')
.attr('class', 'layer-list layer-list-osm')
.merge(ul);
var li = ul.selectAll('.list-item')
.data(osmLayers);
li.exit()
.remove();
var liEnter = li.enter()
.append('li')
.attr('class', function(d) { return 'list-item list-item-' + d.id; });
var labelEnter = liEnter
.append('label')
.each(function(d) {
if (d.id === 'osm') {
d3_select(this)
.call(tooltip()
.html(true)
.title(uiTooltipHtml(t('map_data.layers.' + d.id + '.tooltip'), osmDataToggleKey))
.placement('bottom')
);
} else {
d3_select(this)
.call(tooltip()
.title(t('map_data.layers.' + d.id + '.tooltip'))
.placement('bottom')
);
}
});
labelEnter
.append('input')
.attr('type', 'checkbox')
.on('change', function(d) { toggleLayer(d.id); });
labelEnter
.append('span')
.text(function(d) { return t('map_data.layers.' + d.id + '.title'); });
// Update
li
.merge(liEnter)
.classed('active', function (d) { return d.layer.enabled(); })
.selectAll('input')
.property('checked', function (d) { return d.layer.enabled(); });
}
function drawQAItems(selection) {
var qaKeys = ['keepRight', 'improveOSM'];
var qaLayers = layers.all().filter(function(obj) { return qaKeys.indexOf(obj.id) !== -1; });
var ul = selection
.selectAll('.layer-list-qa')
.data([0]);
ul = ul.enter()
.append('ul')
.attr('class', 'layer-list layer-list-qa')
.merge(ul);
var li = ul.selectAll('.list-item')
.data(qaLayers);
li.exit()
.remove();
var liEnter = li.enter()
.append('li')
.attr('class', function(d) { return 'list-item list-item-' + d.id; });
var labelEnter = liEnter
.append('label')
.each(function(d) {
d3_select(this)
.call(tooltip()
.title(t('map_data.layers.' + d.id + '.tooltip'))
.placement('bottom')
);
});
labelEnter
.append('input')
.attr('type', 'checkbox')
.on('change', function(d) { toggleLayer(d.id); });
labelEnter
.append('span')
.text(function(d) { return t('map_data.layers.' + d.id + '.title'); });
// Update
li
.merge(liEnter)
.classed('active', function (d) { return d.layer.enabled(); })
.selectAll('input')
.property('checked', function (d) { return d.layer.enabled(); });
}
// Beta feature - sample vector layers to support Detroit Mapping Challenge
// https://github.com/osmus/detroit-mapping-challenge
function drawVectorItems(selection) {
var dataLayer = layers.layer('data');
var vtData = [
{
name: 'Detroit Neighborhoods/Parks',
src: 'neighborhoods-parks',
tooltip: 'Neighborhood boundaries and parks as compiled by City of Detroit in concert with community groups.',
template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmur6x34562qp9iv1u3ksf-54hev,jonahadkins.cjksmqxdx33jj2wp90xd9x2md-4e5y2/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA'
}, {
name: 'Detroit Composite POIs',
src: 'composite-poi',
tooltip: 'Fire Inspections, Business Licenses, and other public location data collated from the City of Detroit.',
template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmm6a02sli31myxhsr7zf3-2sw8h/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA'
}, {
name: 'Detroit All-The-Places POIs',
src: 'alltheplaces-poi',
tooltip: 'Public domain business location data created by web scrapers.',
template: 'https://{switch:a,b,c,d}.tiles.mapbox.com/v4/jonahadkins.cjksmswgk340g2vo06p1w9w0j-8fjjc/{z}/{x}/{y}.vector.pbf?access_token=pk.eyJ1Ijoiam9uYWhhZGtpbnMiLCJhIjoiRlVVVkx3VSJ9.9sdVEK_B_VkEXPjssU5MqA'
}
];
// Only show this if the map is around Detroit..
var detroit = geoExtent([-83.5, 42.1], [-82.8, 42.5]);
var showVectorItems = (context.map().zoom() > 9 && detroit.contains(context.map().center()));
var container = selection.selectAll('.vectortile-container')
.data(showVectorItems ? [0] : []);
container.exit()
.remove();
var containerEnter = container.enter()
.append('div')
.attr('class', 'vectortile-container');
containerEnter
.append('h4')
.attr('class', 'vectortile-header')
.text('Detroit Vector Tiles (Beta)');
containerEnter
.append('ul')
.attr('class', 'layer-list layer-list-vectortile');
containerEnter
.append('div')
.attr('class', 'vectortile-footer')
.append('a')
.attr('target', '_blank')
.attr('tabindex', -1)
.call(svgIcon('#iD-icon-out-link', 'inline'))
.attr('href', 'https://github.com/osmus/detroit-mapping-challenge')
.append('span')
.text('About these layers');
container = container
.merge(containerEnter);
var ul = container.selectAll('.layer-list-vectortile');
var li = ul.selectAll('.list-item')
.data(vtData);
li.exit()
.remove();
var liEnter = li.enter()
.append('li')
.attr('class', function(d) { return 'list-item list-item-' + d.src; });
var labelEnter = liEnter
.append('label')
.each(function(d) {
d3_select(this).call(
tooltip().title(d.tooltip).placement('top')
);
});
labelEnter
.append('input')
.attr('type', 'radio')
.attr('name', 'vectortile')
.on('change', selectVTLayer);
labelEnter
.append('span')
.text(function(d) { return d.name; });
// Update
li
.merge(liEnter)
.classed('active', isVTLayerSelected)
.selectAll('input')
.property('checked', isVTLayerSelected);
function isVTLayerSelected(d) {
return dataLayer && dataLayer.template() === d.template;
}
function selectVTLayer(d) {
context.storage('settings-custom-data-url', d.template);
if (dataLayer) {
dataLayer.template(d.template, d.src);
dataLayer.enabled(true);
}
}
}
function drawCustomDataItems(selection) {
var dataLayer = layers.layer('data');
var hasData = dataLayer && dataLayer.hasData();
var showsData = hasData && dataLayer.enabled();
var ul = selection
.selectAll('.layer-list-data')
.data(dataLayer ? [0] : []);
// Exit
ul.exit()
.remove();
// Enter
var ulEnter = ul.enter()
.append('ul')
.attr('class', 'layer-list layer-list-data');
var liEnter = ulEnter
.append('li')
.attr('class', 'list-item-data');
var labelEnter = liEnter
.append('label')
.call(tooltip()
.title(t('map_data.layers.custom.tooltip'))
.placement('top')
);
labelEnter
.append('input')
.attr('type', 'checkbox')
.on('change', function() { toggleLayer('data'); });
labelEnter
.append('span')
.text(t('map_data.layers.custom.title'));
liEnter
.append('button')
.call(tooltip()
.title(t('settings.custom_data.tooltip'))
.placement((textDirection === 'rtl') ? 'right' : 'left')
)
.on('click', editCustom)
.call(svgIcon('#iD-icon-more'));
liEnter
.append('button')
.call(tooltip()
.title(t('map_data.layers.custom.zoom'))
.placement((textDirection === 'rtl') ? 'right' : 'left')
)
.on('click', function() {
d3_event.preventDefault();
d3_event.stopPropagation();
dataLayer.fitZoom();
})
.call(svgIcon('#iD-icon-search'));
// Update
ul = ul
.merge(ulEnter);
ul.selectAll('.list-item-data')
.classed('active', showsData)
.selectAll('label')
.classed('deemphasize', !hasData)
.selectAll('input')
.property('disabled', !hasData)
.property('checked', showsData);
}
function editCustom() {
d3_event.preventDefault();
context.container()
.call(settingsCustomData);
}
function customChanged(d) {
var dataLayer = layers.layer('data');
if (d && d.url) {
dataLayer.url(d.url);
} else if (d && d.fileList) {
dataLayer.fileList(d.fileList);
}
}
function drawListItems(selection, data, type, name, change, active) {
var items = selection.selectAll('li')
.data(data);
// Exit
items.exit()
.remove();
// Enter
var enter = items.enter()
.append('li')
.call(tooltip()
.html(true)
.title(function(d) {
var tip = t(name + '.' + d + '.tooltip');
var key = (d === 'wireframe' ? t('area_fill.wireframe.key') : null);
if (d === 'highlight_edits') key = t('map_data.highlight_edits.key');
if ((name === 'feature' || name === 'keepRight') && autoHiddenFeature(d)) {
var msg = showsLayer('osm') ? t('map_data.autohidden') : t('map_data.osmhidden');
tip += '<div>' + msg + '</div>';
}
return uiTooltipHtml(tip, key);
})
.placement('top')
);
var label = enter
.append('label');
label
.append('input')
.attr('type', type)
.attr('name', name)
.on('change', change);
label
.append('span')
.text(function(d) { return t(name + '.' + d + '.description'); });
// Update
items = items
.merge(enter);
items
.classed('active', active)
.selectAll('input')
.property('checked', active)
.property('indeterminate', function(d) {
return ((name === 'feature' || name === 'keepRight') && autoHiddenFeature(d));
});
}
function renderDataLayers(selection) {
var container = selection.selectAll('.data-layer-container')
.data([0]);
_dataLayerContainer = container.enter()
.append('div')
.attr('class', 'data-layer-container')
.merge(container);
updateDataLayers();
}
function renderPhotoOverlays(selection) {
var container = selection.selectAll('.photo-overlay-container')
.data([0]);
_photoOverlayContainer = container.enter()
.append('div')
.attr('class', 'photo-overlay-container')
.merge(container);
updatePhotoOverlays();
}
function renderStyleOptions(selection) {
var container = selection.selectAll('.layer-fill-list')
.data([0]);
_fillList = container.enter()
.append('ul')
.attr('class', 'layer-list layer-fill-list')
.merge(container);
updateFillList();
var container2 = selection.selectAll('.layer-visual-diff-list')
.data([0]);
_visualDiffList = container2.enter()
.append('ul')
.attr('class', 'layer-list layer-visual-diff-list')
.merge(container2);
updateVisualDiffList();
}
function renderFeatureList(selection) {
var container = selection.selectAll('.layer-feature-list-container')
.data([0]);
var containerEnter = container.enter()
.append('div')
.attr('class', 'layer-feature-list-container');
containerEnter
.append('ul')
.attr('class', 'layer-list layer-feature-list');
var footer = containerEnter
.append('div')
.attr('class', 'feature-list-links section-footer');
footer
.append('a')
.attr('class', 'feature-list-link')
.attr('href', '#')
.text(t('issues.enable_all'))
.on('click', function() {
context.features().enableAll();
});
footer
.append('a')
.attr('class', 'feature-list-link')
.attr('href', '#')
.text(t('issues.disable_all'))
.on('click', function() {
context.features().disableAll();
});
// Update
container = container
.merge(containerEnter);
_featureList = container.selectAll('.layer-feature-list');
updateFeatureList();
}
function updatePhotoOverlays() {
_photoOverlayContainer
.call(drawPhotoItems)
.call(drawPhotoTypeItems);
}
function updateDataLayers() {
_dataLayerContainer
.call(drawOsmItems)
.call(drawQAItems)
.call(drawCustomDataItems)
.call(drawVectorItems); // Beta - Detroit mapping challenge
}
function updateFillList() {
_fillList
.call(drawListItems, fills, 'radio', 'area_fill', setFill, showsFill);
}
function updateVisualDiffList() {
_visualDiffList
.call(drawListItems, ['highlight_edits'], 'checkbox', 'visual_diff', toggleHighlightEdited, function() {
return context.surface().classed('highlight-edited');
});
}
function updateFeatureList() {
_featureList
.call(drawListItems, features, 'checkbox', 'feature', clickFeature, showsFeature);
}
function update() {
if (!_pane.select('.disclosure-wrap-data_layers').classed('hide')) {
updateDataLayers();
}
if (!_pane.select('.disclosure-wrap-photo_overlays').classed('hide')) {
updatePhotoOverlays();
}
if (!_pane.select('.disclosure-wrap-fill_area').classed('hide')) {
updateFillList();
}
if (!_pane.select('.disclosure-wrap-map_features').classed('hide')) {
updateFeatureList();
}
_QAList
.call(drawListItems, ['keep-right'], 'checkbox', 'QA', function(d) { toggleLayer(d); }, showsQA);
}
function toggleWireframe() {
if (d3_event) {
d3_event.preventDefault();
d3_event.stopPropagation();
}
if (_fillSelected === 'wireframe') {
_fillSelected = context.storage('area-fill-toggle') || 'partial';
} else {
_fillSelected = 'wireframe';
}
setFill(_fillSelected);
context.map().pan([0,0]); // trigger a redraw
}
var paneTooltip = tooltip()
.placement((textDirection === 'rtl') ? 'right' : 'left')
.html(true)
.title(uiTooltipHtml(t('map_data.description'), key));
function hidePane() {
context.ui().togglePanes();
}
uiMapData.togglePane = function() {
if (d3_event) d3_event.preventDefault();
paneTooltip.hide();
context.ui().togglePanes(!_pane.classed('shown') ? _pane : undefined);
};
uiMapData.renderToggleButton = function(selection) {
selection
.append('button')
.on('click', uiMapData.togglePane)
.call(svgIcon('#iD-icon-data', 'light'))
.call(paneTooltip);
};
uiMapData.renderPane = function(selection) {
_pane = selection
.append('div')
.attr('class', 'fillL map-pane map-data-pane hide')
.attr('pane', 'map-data');
var heading = _pane
.append('div')
.attr('class', 'pane-heading');
heading
.append('h2')
.text(t('map_data.title'));
heading
.append('button')
.on('click', hidePane)
.call(svgIcon('#iD-icon-close'));
var content = _pane
.append('div')
.attr('class', 'pane-content');
// data layers
content
.append('div')
.attr('class', 'map-data-data-layers')
.call(uiDisclosure(context, 'data_layers', true)
.title(t('map_data.data_layers'))
.content(renderDataLayers)
);
// photo overlays
content
.append('div')
.attr('class', 'map-data-photo-overlays')
.call(uiDisclosure(context, 'photo_overlays', false)
.title(t('photo_overlays.title'))
.content(renderPhotoOverlays)
);
// area fills
content
.append('div')
.attr('class', 'map-data-area-fills')
.call(uiDisclosure(context, 'fill_area', false)
.title(t('map_data.style_options'))
.content(renderStyleOptions)
);
// feature filters
content
.append('div')
.attr('class', 'map-data-feature-filters')
.call(uiDisclosure(context, 'map_features', false)
.title(t('map_data.map_features'))
.content(renderFeatureList)
);
// add listeners
context.features()
.on('change.map_data-update', update);
update();
setFill(_fillSelected);
context.keybinding()
.on(key, uiMapData.togglePane)
.on(t('area_fill.wireframe.key'), toggleWireframe)
.on(osmDataToggleKey, function() {
d3_event.preventDefault();
d3_event.stopPropagation();
toggleLayer('osm');
})
.on(t('map_data.highlight_edits.key'), toggleHighlightEdited);
};
return uiMapData;
}