modules/ui/rapid_feature_inspector.js (216 lines of code) (raw):
import { select as d3_select } from 'd3-selection';
import { t } from '../core/localizer';
import { actionNoop, actionRapidAcceptFeature } from '../actions';
import { modeBrowse, modeSelect } from '../modes';
import { services } from '../services';
import { svgIcon } from '../svg';
import { uiFlash } from './flash';
import { uiTooltip } from './tooltip';
import { uiRapidFirstEditDialog } from './rapid_first_edit_dialog';
export function uiRapidFeatureInspector(context, keybinding) {
const rapidContext = context.rapidContext();
const showPowerUser = rapidContext.showPowerUser;
const ACCEPT_FEATURES_LIMIT = showPowerUser ? Infinity : 50;
let _datum;
function isAddFeatureDisabled() {
// when task GPX is set in URL (TM mode), "add roads" is always enabled
const gpxInUrl = context.initialHashParams.hasOwnProperty('gpx');
if (gpxInUrl) return false;
const annotations = context.history().peekAllAnnotations();
const aiFeatureAccepts = annotations.filter(a => a.type === 'rapid_accept_feature');
return aiFeatureAccepts.length >= ACCEPT_FEATURES_LIMIT;
}
function onAcceptFeature() {
if (!_datum) return;
if (isAddFeatureDisabled()) {
const flash = uiFlash(context)
.duration(5000)
.label(t(
'rapid_feature_inspector.option_accept.disabled_flash',
{ n: ACCEPT_FEATURES_LIMIT }
));
flash();
return;
}
// In place of a string annotation, this introduces an "object-style"
// annotation, where "type" and "description" are standard keys,
// and there may be additional properties. Note that this will be
// serialized to JSON while saving undo/redo state in history.save().
let annotation = {
type: 'rapid_accept_feature',
description: t('rapid_feature_inspector.option_accept.annotation'),
id: _datum.id,
origid: _datum.__origid__
};
const service = _datum.__service__ === 'esri' ? services.esriData : services.fbMLRoads;
const graph = service.graph(_datum.__datasetid__);
const sourceTag = _datum.tags && _datum.tags.source;
if (sourceTag) annotation.source = sourceTag;
context.perform(actionRapidAcceptFeature(_datum.id, graph), annotation);
context.enter(modeSelect(context, [_datum.id]));
if (context.inIntro()) return;
// remember sources for later when we prepare the changeset
rapidContext.sources.add('mapwithai'); // always add 'mapwithai'
if (sourceTag && /^esri/.test(sourceTag)) {
rapidContext.sources.add('esri'); // add 'esri' for esri sources
}
if (window.sessionStorage.getItem('acknowledgedLogin') === 'true') return;
window.sessionStorage.setItem('acknowledgedLogin', 'true');
const osm = context.connection();
if (!osm.authenticated()) {
context.container()
.call(uiRapidFirstEditDialog(context));
}
}
function onIgnoreFeature() {
if (!_datum) return;
const annotation = {
type: 'rapid_ignore_feature',
description: t('rapid_feature_inspector.option_ignore.annotation'),
id: _datum.id,
origid: _datum.__origid__
};
context.perform(actionNoop(), annotation);
context.enter(modeBrowse(context));
}
// https://www.w3.org/TR/AERT#color-contrast
// https://trendct.org/2016/01/22/how-to-choose-a-label-color-to-contrast-with-background/
// pass color as a hexstring like '#rgb', '#rgba', '#rrggbb', '#rrggbbaa' (alpha values are ignored)
function getBrightness(color) {
const short = (color.length < 6);
const r = parseInt(short ? color[1] + color[1] : color[1] + color[2], 16);
const g = parseInt(short ? color[2] + color[2] : color[3] + color[4], 16);
const b = parseInt(short ? color[3] + color[3] : color[5] + color[6], 16);
return ((r * 299) + (g * 587) + (b * 114)) / 1000;
}
function featureInfo(selection) {
if (!_datum) return;
const datasetID = _datum.__datasetid__.replace('-conflated', '');
const dataset = rapidContext.datasets()[datasetID];
const color = dataset.color;
let featureInfo = selection.selectAll('.feature-info')
.data([color]);
// enter
let featureInfoEnter = featureInfo
.enter()
.append('div')
.attr('class', 'feature-info');
featureInfoEnter
.append('div')
.attr('class', 'dataset-label')
.text(dataset.label || dataset.id); // fallback to dataset ID
if (dataset.beta) {
featureInfoEnter
.append('div')
.attr('class', 'dataset-beta beta')
.attr('title', t('rapid_poweruser_features.beta'));
}
// update
featureInfo = featureInfo
.merge(featureInfoEnter)
.style('background', d => d)
.style('color', d => getBrightness(d) > 140.5 ? '#333' : '#fff');
}
function tagInfo(selection) {
const tags = _datum && _datum.tags;
if (!tags) return;
let tagInfoEnter = selection.selectAll('.tag-info')
.data([0])
.enter()
.append('div')
.attr('class', 'tag-info');
let tagBagEnter = tagInfoEnter
.append('div')
.attr('class', 'tag-bag');
tagBagEnter
.append('div')
.attr('class', 'tag-heading')
.text(t('rapid_feature_inspector.tags'));
const tagEntries = Object.keys(tags).map(k => ({ key: k, value: tags[k] }) );
tagEntries.forEach(e => {
let entryDiv = tagBagEnter.append('div')
.attr('class', 'tag-entry');
entryDiv.append('div').attr('class', 'tag-key').text(e.key);
entryDiv.append('div').attr('class', 'tag-value').text(e.value);
});
}
function rapidInspector(selection) {
let inspector = selection.selectAll('.rapid-inspector')
.data([0]);
let inspectorEnter = inspector
.enter()
.append('div')
.attr('class', 'rapid-inspector');
inspector = inspector
.merge(inspectorEnter);
// Header
let headerEnter = inspector.selectAll('.header')
.data([0])
.enter()
.append('div')
.attr('class', 'header');
headerEnter
.append('h3')
.append('svg')
// .attr('class', 'logo-rapid dark')
.attr('class', 'logo-rapid')
.append('use')
.attr('xlink:href', '#iD-logo-rapid');
headerEnter
.append('button')
.attr('class', 'fr rapid-inspector-close')
.on('click', () => {
context.enter(modeBrowse(context));
})
.call(svgIcon('#iD-icon-close'));
// Body
let body = inspector.selectAll('.body')
.data([0]);
let bodyEnter = body
.enter()
.append('div')
.attr('class', 'body');
body = body
.merge(bodyEnter)
.call(featureInfo)
.call(tagInfo);
// Choices
const choiceData = [
{
key: 'accept',
iconName: '#iD-icon-rapid-plus-circle',
label: t('rapid_feature_inspector.option_accept.label'),
description: t('rapid_feature_inspector.option_accept.description'),
onClick: onAcceptFeature
}, {
key: 'ignore',
iconName: '#iD-icon-rapid-minus-circle',
label: t('rapid_feature_inspector.option_ignore.label'),
description: t('rapid_feature_inspector.option_ignore.description'),
onClick: onIgnoreFeature
}
];
let choices = body.selectAll('.rapid-inspector-choices')
.data([0]);
let choicesEnter = choices
.enter()
.append('div')
.attr('class', 'rapid-inspector-choices');
choicesEnter
.append('p')
.text(t('rapid_feature_inspector.prompt'));
choicesEnter.selectAll('.rapid-inspector-choice')
.data(choiceData, d => d.key)
.enter()
.append('div')
.attr('class', d => `rapid-inspector-choice rapid-inspector-choice-${d.key}`)
.each(showChoice);
}
function showChoice(d, i, nodes) {
let selection = d3_select(nodes[i]);
const disableClass = (d.key === 'accept' && isAddFeatureDisabled()) ? 'secondary disabled': '';
let choiceWrap = selection
.append('div')
.attr('class', `choice-wrap choice-wrap-${d.key}`);
let choiceReference = selection
.append('div')
.attr('class', 'tag-reference-body');
choiceReference
.text(d.description);
const onClick = d.onClick;
let choiceButton = choiceWrap
.append('button')
.attr('class', `choice-button choice-button-${d.key} ${disableClass}`)
.on('click', onClick);
// build tooltips
let title, keys;
if (d.key === 'accept') {
if (isAddFeatureDisabled()) {
title = t('rapid_feature_inspector.option_accept.disabled', { n: ACCEPT_FEATURES_LIMIT } );
keys = [];
} else {
title = t('rapid_feature_inspector.option_accept.tooltip');
keys = [t('rapid_feature_inspector.option_accept.key')];
}
} else if (d.key === 'ignore') {
title = t('rapid_feature_inspector.option_ignore.tooltip');
keys = [t('rapid_feature_inspector.option_ignore.key')];
}
if (title && keys) {
choiceButton = choiceButton
.call(uiTooltip().placement('bottom').title(title).keys(keys));
}
choiceButton
.append('svg')
.attr('class', 'choice-icon icon')
.append('use')
.attr('xlink:href', d.iconName);
choiceButton
.append('div')
.attr('class', 'choice-label')
.text(d.label);
choiceWrap
.append('button')
.attr('class', `tag-reference-button ${disableClass}`)
.attr('title', 'info')
.attr('tabindex', '-1')
.on('click', () => {
choiceReference.classed('expanded', !choiceReference.classed('expanded'));
})
.call(svgIcon('#iD-icon-inspect'));
}
rapidInspector.datum = function(val) {
if (!arguments.length) return _datum;
_datum = val;
return this;
};
if (keybinding) {
keybinding()
.on(t('rapid_feature_inspector.option_accept.key'), onAcceptFeature)
.on(t('rapid_feature_inspector.option_ignore.key'), onIgnoreFeature);
}
return rapidInspector;
}