modules/ui/map_data.js (715 lines of code) (raw):
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().featuresArray();
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.key);
}
function autoHiddenFeature(d) {
if (d.type === 'kr_error') return context.errors().autoHidden(d);
return context.features().autoHidden(d.key);
}
function clickFeature(d) {
context.features().toggle(d.key);
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;
if (name === 'feature') {
tip = d.description;
} else {
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) {
if (name === 'feature') {
return d.title;
}
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;
}