modules/ui/preset_browser.js (559 lines of code) (raw):

import { event as d3_event, select as d3_select, selectAll as d3_selectAll } from 'd3-selection'; import { t, textDirection } from '../util/locale'; import { services } from '../services'; import { svgIcon } from '../svg/index'; import { tooltip } from '../util/tooltip'; import { popover } from '../util/popover'; import { uiTagReference } from './tag_reference'; import { uiPresetFavoriteButton } from './preset_favorite_button'; import { uiPresetIcon } from './preset_icon'; import { groupManager } from '../entities/group_manager'; import { utilKeybinding, utilNoAuto } from '../util'; export function uiPresetBrowser(context, allowedGeometry, onChoose, onCancel) { // multiple preset browsers could be instantiated at once, give each a unique ID var uid = (new Date()).getTime().toString(); var presets; var shownGeometry = []; updateShownGeometry(allowedGeometry); var search = d3_select(null), poplistContent = d3_select(null), poplistFooter = d3_select(null); var _countryCode; var browser = popover('poplist preset-browser fillL') .placement('bottom') .alignment('leading') .hasArrow(false); browser.content(function() { return function(selection) { var header = selection.selectAll('.poplist-header') .data([0]) .enter() .append('div') .attr('class', 'poplist-header'); header .append('input') .attr('class', 'search-input') .attr('placeholder', t('modes.add_feature.search_placeholder')) .attr('type', 'search') .call(utilNoAuto) .on('blur', function() { browser.hide(); }) .on('keypress', keypress) .on('keydown', keydown) .on('input', updateResultsList); header .call(svgIcon('#iD-icon-search', 'search-icon pre-text')); selection.selectAll('.poplist-content') .data([0]) .enter() .append('div') .attr('class', 'poplist-content') .on('mousedown', function() { // don't blur the search input (and thus close results) d3_event.preventDefault(); d3_event.stopPropagation(); }) .append('div') .attr('class', 'list'); var footer = selection.selectAll('.poplist-footer') .data([0]) .enter() .append('div') .attr('class', 'poplist-footer') .on('mousedown', function() { // don't blur the search input (and thus close results) d3_event.preventDefault(); d3_event.stopPropagation(); }); footer.append('div') .attr('class', 'message'); footer.append('div') .attr('class', 'filter-wrap'); search = selection.selectAll('.search-input'); poplistContent = selection.selectAll('.poplist-content'); poplistFooter = selection.selectAll('.poplist-footer'); renderFilterButtons(); }; }); var parentShow = browser.show; browser.show = function() { parentShow(); search.node().focus(); search.node().setSelectionRange(0, search.property('value').length); updateResultsList(); context.features() .on('change.preset-browser.' + uid , updateForFeatureHiddenState); // reload in case the user moved countries reloadCountryCode(); }; var parentHide = browser.hide; browser.hide = function() { parentHide(); if (onCancel) onCancel(); }; function renderFilterButtons() { var selection = poplistFooter.select('.filter-wrap'); var geomForButtons = allowedGeometry.slice(); var vertexIndex = geomForButtons.indexOf('vertex'); if (vertexIndex !== -1) geomForButtons.splice(vertexIndex, 1); if (geomForButtons.length === 1) { // don't show filter buttons if only one geometry allowed geomForButtons = []; } var buttons = selection .selectAll('button.filter') .data(geomForButtons, function(d) { return d; }); buttons.exit() .remove(); buttons .enter() .append('button') .attr('class', 'filter active') .attr('title', function(d) { return t('modes.add_' + d + '.filter_tooltip'); }) .each(function(d) { d3_select(this).call(svgIcon('#iD-icon-' + d)); }) .on('click', function(d) { toggleShownGeometry(d); if (shownGeometry.length === 0) { updateShownGeometry(allowedGeometry); toggleShownGeometry(d); } updateFilterButtonsStates(); updateResultsList(); }); updateFilterButtonsStates(); } browser.setAllowedGeometry = function(array) { allowedGeometry = array; updateShownGeometry(array); renderFilterButtons(); updateResultsList(); }; function updateShownGeometry(geom) { shownGeometry = geom.slice().sort(); presets = context.presets().matchAnyGeometry(shownGeometry); } function toggleShownGeometry(d) { var geom = shownGeometry; var index = geom.indexOf(d); if (index === -1) { geom.push(d); if (d === 'point') geom.push('vertex'); } else { geom.splice(index, 1); if (d === 'point') geom.splice(geom.indexOf('vertex'), 1); } updateShownGeometry(geom); } function updateFilterButtonsStates() { poplistFooter.selectAll('button.filter') .classed('active', function(d) { return shownGeometry.indexOf(d) !== -1; }); } function keypress() { if (d3_event.keyCode === utilKeybinding.keyCodes.enter) { poplistContent.selectAll('.list .list-item.focused button.choose') .each(function(d) { d.choose.call(this); }); d3_event.preventDefault(); d3_event.stopPropagation(); } } function keydown() { var nextFocus, priorFocus, parentSubsection; if (d3_event.keyCode === utilKeybinding.keyCodes['↓'] || d3_event.keyCode === utilKeybinding.keyCodes.tab && !d3_event.shiftKey) { d3_event.preventDefault(); d3_event.stopPropagation(); priorFocus = poplistContent.selectAll('.list .list-item.focused'); if (priorFocus.empty()) { nextFocus = poplistContent.selectAll('.list > .list-item:first-child'); } else { nextFocus = d3_select(priorFocus.nodes()[0].nextElementSibling); if (!nextFocus.empty() && !nextFocus.classed('list-item')) { nextFocus = nextFocus.selectAll('.list-item:first-child'); } if (nextFocus.empty()) { parentSubsection = priorFocus.nodes()[0].closest('.list .subsection'); if (parentSubsection && parentSubsection.nextElementSibling) { nextFocus = d3_select(parentSubsection.nextElementSibling); } } } if (!nextFocus.empty()) { focusListItem(nextFocus, true); priorFocus.classed('focused', false); } } else if (d3_event.keyCode === utilKeybinding.keyCodes['↑'] || d3_event.keyCode === utilKeybinding.keyCodes.tab && d3_event.shiftKey) { d3_event.preventDefault(); d3_event.stopPropagation(); priorFocus = poplistContent.selectAll('.list .list-item.focused'); if (!priorFocus.empty()) { nextFocus = d3_select(priorFocus.nodes()[0].previousElementSibling); if (!nextFocus.empty() && !nextFocus.classed('list-item')) { nextFocus = nextFocus.selectAll('.list-item:last-child'); } if (nextFocus.empty()) { parentSubsection = priorFocus.nodes()[0].closest('.list .subsection'); if (parentSubsection && parentSubsection.previousElementSibling) { nextFocus = d3_select(parentSubsection.previousElementSibling); } } if (!nextFocus.empty()) { focusListItem(nextFocus, true); priorFocus.classed('focused', false); } } } else if (d3_event.keyCode === utilKeybinding.keyCodes.esc) { search.node().blur(); d3_event.preventDefault(); d3_event.stopPropagation(); } } function getDefaultResults() { var graph = context.graph(); var superGroups = groupManager.groupsWithNearby; var scoredGroups = {}; var scoredPresets = {}; context.presets().getRecents().slice(0, 15).forEach(function(item, index) { var score = (15 - index) / 15; var id = item.preset.id; if (!scoredPresets[id]) { scoredPresets[id] = { preset: item.preset, score: score }; } }); var queryExtent = context.map().extent(); var nearbyEntities = context.history().tree().intersects(queryExtent, graph); for (var i in nearbyEntities) { var entity = nearbyEntities[i]; // ignore boring features if (!entity.hasInterestingTags()) continue; var geom = entity.geometry(graph); // evaluate preset var preset = context.presets().match(entity, graph); if (preset.searchable !== false && // don't recommend unsearchables !preset.isFallback() && // don't recommend generics !preset.suggestion) { // don't recommend brand suggestions again if (!scoredPresets[preset.id]) { scoredPresets[preset.id] = { preset: preset, score: 0 }; } scoredPresets[preset.id].score += 1; } // evaluate groups for (var j in superGroups) { var group = superGroups[j]; if (group.matchesTags(entity.tags, geom)) { var nearbyGroupID = group.nearby; if (!scoredGroups[nearbyGroupID]) { scoredGroups[nearbyGroupID] = { group: groupManager.group(nearbyGroupID), score: 0 }; } var entityScore; if (geom === 'area') { // significantly prefer area features that dominate the viewport // (e.g. editing within a park or school grounds) var containedPercent = queryExtent.percentContainedIn(entity.extent(graph)); entityScore = Math.max(1, containedPercent * 10); } else { entityScore = 1; } scoredGroups[nearbyGroupID].score += entityScore; } } } Object.values(scoredGroups).forEach(function(scoredGroupItem) { scoredGroupItem.group.scoredPresets().forEach(function(groupScoredPreset) { var combinedScore = groupScoredPreset.score * scoredGroupItem.score; if (!scoredPresets[groupScoredPreset.preset.id]) { scoredPresets[groupScoredPreset.preset.id] = { preset: groupScoredPreset.preset, score: combinedScore }; } else { scoredPresets[groupScoredPreset.preset.id].score += combinedScore; } }); }); return Object.values(scoredPresets).sort(function(item1, item2) { return item2.score - item1.score; }).map(function(item) { return item.preset ? item.preset : item; }).filter(function(d) { var preset = d.preset || d; // skip non-visible if (preset.addable && !preset.addable()) return false; // skip presets not valid in this country if (_countryCode && preset.countryCodes && preset.countryCodes.indexOf(_countryCode) === -1) return false; return preset.defaultAddGeometry(context, shownGeometry); }).slice(0, 50); } function reloadCountryCode() { if (!services.countryCoder) return; var center = context.map().center(); var countryCode = services.countryCoder.iso1A2Code(center); if (countryCode) countryCode = countryCode.toLowerCase(); if (_countryCode !== countryCode) { _countryCode = countryCode; updateResultsList(); } } function getRawResults() { if (search.empty()) return []; var value = search.property('value'); var results; if (value.length) { results = presets.search(value, shownGeometry, _countryCode).collection .filter(function(d) { if (d.members) { return d.members.collection.some(function(preset) { return preset.addable(); }); } return d.addable(); }); } else { results = getDefaultResults(); } return results; } function updateResultsList() { if (!browser.isShown()) return; var list = poplistContent.selectAll('.list'); if (search.empty() || list.empty()) return; var results = getRawResults(); list.call(drawList, results); list.selectAll('.list-item.focused') .classed('focused', false); focusListItem(poplistContent.selectAll('.list > .list-item:first-child'), false); poplistContent.node().scrollTop = 0; var resultCount = results.length; poplistFooter.selectAll('.message') .text(t('modes.add_feature.' + (resultCount === 1 ? 'result' : 'results'), { count: resultCount })); } function focusListItem(selection, scrollingToShow) { if (!selection.empty()) { selection.classed('focused', true); if (scrollingToShow) { // scroll to keep the focused item visible scrollPoplistToShow(selection); } } } function scrollPoplistToShow(selection) { if (selection.empty()) return; var node = selection.nodes()[0]; var scrollableNode = poplistContent.node(); if (node.offsetTop < scrollableNode.scrollTop) { scrollableNode.scrollTop = node.offsetTop; } else if (node.offsetTop + node.offsetHeight > scrollableNode.scrollTop + scrollableNode.offsetHeight && node.offsetHeight < scrollableNode.offsetHeight) { scrollableNode.scrollTop = node.offsetTop + node.offsetHeight - scrollableNode.offsetHeight; } } function itemForPreset(d) { if (d.members) { return CategoryItem(d); } var preset = d.preset || d; return AddablePresetItem(preset); } function drawList(list, rawItems) { list.selectAll('.subsection.subitems').remove(); var dataItems = rawItems.map(function(rawItem) { return itemForPreset(rawItem); }); var items = list.selectAll('.list-item') .data(dataItems, function(d) { return d.id(); }); items.order(); items.exit() .remove(); drawItems(items.enter()); list.selectAll('.list-item.expanded') .classed('expanded', false) .selectAll('.label svg.icon use') .attr('href', textDirection === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward'); updateForFeatureHiddenState(); } function drawItems(selection) { var item = selection .append('div') .attr('class', 'list-item') .attr('id', function(d) { return 'search-add-list-item-preset-' + d.id().replace(/[^a-zA-Z\d:]/g, '-'); }) .on('mouseover', function() { poplistContent.selectAll('.list .list-item.focused') .classed('focused', false); d3_select(this) .classed('focused', true); }) .on('mouseout', function() { d3_select(this) .classed('focused', false); }); var row = item.append('div') .attr('class', 'row'); row.append('button') .attr('class', 'choose') .on('click', function(d) { d.choose.call(this); }); row.each(function(d) { var geometry = d.preset && d.preset.geometry[0]; if ((d.preset && d.preset.geometry.length !== 1) || (geometry !== 'area' && geometry !== 'line' && geometry !== 'vertex')) { geometry = null; } d3_select(this).call( uiPresetIcon(context) .geometry(geometry) .preset(d.preset || d.category) .sizeClass('small') ); }); var label = row.append('div') .attr('class', 'label'); label.each(function(d) { if (d.subitems) { d3_select(this) .call(svgIcon((textDirection === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward'), 'inline')); } }); label.each(function(d) { // NOTE: split/join on en-dash, not a hypen (to avoid conflict with fr - nl names in Brussels etc) d3_select(this) .append('div') .attr('class', 'label-inner') .selectAll('.namepart') .data(d.name().split(' – ')) .enter() .append('div') .attr('class', 'namepart') .text(function(d) { return d; }); }); row.each(function(d) { if (!d.preset) return; var presetFavorite = uiPresetFavoriteButton(d.preset, null, context, 'accessory'); d3_select(this).call(presetFavorite.button); }); item.each(function(d) { if (!d.preset) return; var reference = uiTagReference(d.preset.reference(d.preset.defaultAddGeometry(context, shownGeometry)), context); var thisItem = d3_select(this); thisItem.selectAll('.row').call(reference.button, 'accessory', 'info'); var subsection = thisItem .append('div') .attr('class', 'subsection reference'); subsection.call(reference.body); }); } function updateForFeatureHiddenState() { var listItem = d3_selectAll('.add-feature .poplist .list-item'); // remove existing tooltips listItem.selectAll('button.choose').call(tooltip().destroyAny); listItem.each(function(item, index) { if (!item.preset) return; var hiddenPresetFeatures; for (var i in item.preset.geometry) { if (shownGeometry.indexOf(item.preset.geometry[i]) !== -1) { hiddenPresetFeatures = context.features().isHiddenPreset(item.preset, item.preset.geometry[i]); if (!hiddenPresetFeatures) { break; } } } var button = d3_select(this).selectAll('button.choose'); d3_select(this).classed('disabled', !!hiddenPresetFeatures); button.classed('disabled', !!hiddenPresetFeatures); if (!hiddenPresetFeatures) return; var isAutoHidden = context.features().autoHidden(hiddenPresetFeatures.key); var tooltipIdSuffix = isAutoHidden ? 'zoom' : 'manual'; var tooltipObj = { features: hiddenPresetFeatures.title }; button.call(tooltip('dark') .html(true) .title(t('inspector.hidden_preset.' + tooltipIdSuffix, tooltipObj)) .placement(index < 2 ? 'bottom' : 'top') ); }); } function chooseExpandable(item, itemSelection) { var shouldExpand = !itemSelection.classed('expanded'); itemSelection.classed('expanded', shouldExpand); var iconName = shouldExpand ? '#iD-icon-down' : (textDirection === 'rtl' ? '#iD-icon-backward' : '#iD-icon-forward'); itemSelection.selectAll('.label svg.icon use') .attr('href', iconName); if (shouldExpand) { var subitems = item.subitems(); var selector = '#' + itemSelection.node().id + ' + *'; item.subsection = d3_select(itemSelection.node().parentNode).insert('div', selector) .attr('class', 'subsection subitems'); var subitemsEnter = item.subsection.selectAll('.list-item') .data(subitems) .enter(); drawItems(subitemsEnter); updateForFeatureHiddenState(); scrollPoplistToShow(item.subsection); } else { item.subsection.remove(); } } function CategoryItem(category) { var item = {}; item.id = function() { return category.id; }; item.name = function() { return category.name(); }; item.subsection = d3_select(null); item.category = category; item.choose = function() { var selection = d3_select(this); if (selection.classed('disabled')) return; chooseExpandable(item, d3_select(selection.node().closest('.list-item'))); }; item.subitems = function() { return category.members.matchAnyGeometry(shownGeometry).collection .filter(function(preset) { return preset.addable(); }) .map(function(preset) { return itemForPreset(preset); }); }; return item; } function AddablePresetItem(preset, isSubitem) { var item = {}; item.id = function() { return preset.id + isSubitem; }; item.name = function() { return preset.name(); }; item.isSubitem = isSubitem; item.preset = preset; item.choose = function() { if (d3_select(this).classed('disabled')) return; if (onChoose) onChoose(preset, preset.defaultAddGeometry(context, shownGeometry)); search.node().blur(); }; return item; } // load the initial country code reloadCountryCode(); return browser; }