modules/ui/feature_list.js (284 lines of code) (raw):

import { select as d3_select } from 'd3-selection'; import * as sexagesimal from '@mapbox/sexagesimal'; import { presetManager } from '../presets'; import { t } from '../core/localizer'; import { dmsCoordinatePair } from '../util/units'; import { coreGraph } from '../core/graph'; import { geoSphericalDistance } from '@id-sdk/geo'; import { Extent } from '@id-sdk/extent'; import { modeSelect } from '../modes/select'; import { osmEntity } from '../osm/entity'; import { services } from '../services'; import { svgIcon } from '../svg/icon'; import { uiCmd } from './cmd'; import { utilDisplayName, utilDisplayType, utilHighlightEntities, utilNoAuto } from '../util'; export function uiFeatureList(context) { var _geocodeResults; function featureList(selection) { var header = selection .append('div') .attr('class', 'header fillL'); header .append('h3') .html(t.html('inspector.feature_list')); var searchWrap = selection .append('div') .attr('class', 'search-header'); searchWrap .call(svgIcon('#iD-icon-search', 'pre-text')); var search = searchWrap .append('input') .attr('placeholder', t('inspector.search')) .attr('type', 'search') .call(utilNoAuto) .on('keypress', keypress) .on('keydown', keydown) .on('input', inputevent); var listWrap = selection .append('div') .attr('class', 'inspector-body'); var list = listWrap .append('div') .attr('class', 'feature-list'); context .on('exit.feature-list', clearSearch); context.map() .on('drawn.feature-list', mapDrawn); context.keybinding() .on(uiCmd('⌘F'), focusSearch); function focusSearch(d3_event) { var mode = context.mode() && context.mode().id; if (mode !== 'browse') return; d3_event.preventDefault(); search.node().focus(); } function keydown(d3_event) { if (d3_event.keyCode === 27) { // escape search.node().blur(); } } function keypress(d3_event) { var q = search.property('value'), items = list.selectAll('.feature-list-item'); if (d3_event.keyCode === 13 && // ↩ Return q.length && items.size()) { click(d3_event, items.datum()); } } function inputevent() { _geocodeResults = undefined; drawList(); } function clearSearch() { search.property('value', ''); drawList(); } function mapDrawn(e) { if (e.full) { drawList(); } } function features() { var result = []; var graph = context.graph(); var visibleCenter = context.map().extent().center(); var q = search.property('value').toLowerCase(); if (!q) return result; var locationMatch = sexagesimal.pair(q.toUpperCase()) || q.match(/^(-?\d+\.?\d*)\s+(-?\d+\.?\d*)$/); if (locationMatch) { var loc = [parseFloat(locationMatch[0]), parseFloat(locationMatch[1])]; result.push({ id: -1, geometry: 'point', type: t('inspector.location'), name: dmsCoordinatePair([loc[1], loc[0]]), location: loc }); } // A location search takes priority over an ID search var idMatch = !locationMatch && q.match(/(?:^|\W)(node|way|relation|[nwr])\W?0*([1-9]\d*)(?:\W|$)/i); if (idMatch) { var elemType = idMatch[1].charAt(0); var elemId = idMatch[2]; result.push({ id: elemType + elemId, geometry: elemType === 'n' ? 'point' : elemType === 'w' ? 'line' : 'relation', type: elemType === 'n' ? t('inspector.node') : elemType === 'w' ? t('inspector.way') : t('inspector.relation'), name: elemId }); } var allEntities = graph.entities; var localResults = []; for (var id in allEntities) { var entity = allEntities[id]; if (!entity) continue; var name = utilDisplayName(entity) || ''; if (name.toLowerCase().indexOf(q) < 0) continue; var matched = presetManager.match(entity, graph); var type = (matched && matched.name()) || utilDisplayType(entity.id); var extent = entity.extent(graph); var distance = extent ? geoSphericalDistance(visibleCenter, extent.center()) : 0; localResults.push({ id: entity.id, entity: entity, geometry: entity.geometry(graph), type: type, name: name, distance: distance }); if (localResults.length > 100) break; } localResults = localResults.sort(function byDistance(a, b) { return a.distance - b.distance; }); result = result.concat(localResults); (_geocodeResults || []).forEach(function(d) { if (d.osm_type && d.osm_id) { // some results may be missing these - #1890 // Make a temporary osmEntity so we can preset match // and better localize the search result - #4725 var id = osmEntity.id.fromOSM(d.osm_type, d.osm_id); var tags = {}; tags[d.class] = d.type; var attrs = { id: id, type: d.osm_type, tags: tags }; if (d.osm_type === 'way') { // for ways, add some fake closed nodes attrs.nodes = ['a','a']; // so that geometry area is possible } var tempEntity = osmEntity(attrs); var tempGraph = coreGraph([tempEntity]); var matched = presetManager.match(tempEntity, tempGraph); var type = (matched && matched.name()) || utilDisplayType(id); result.push({ id: tempEntity.id, geometry: tempEntity.geometry(tempGraph), type: type, name: d.display_name, extent: new Extent( [parseFloat(d.boundingbox[3]), parseFloat(d.boundingbox[0])], [parseFloat(d.boundingbox[2]), parseFloat(d.boundingbox[1])] ) }); } }); if (q.match(/^[0-9]+$/)) { // if query is just a number, possibly an OSM ID without a prefix result.push({ id: 'n' + q, geometry: 'point', type: t('inspector.node'), name: q }); result.push({ id: 'w' + q, geometry: 'line', type: t('inspector.way'), name: q }); result.push({ id: 'r' + q, geometry: 'relation', type: t('inspector.relation'), name: q }); } return result; } function drawList() { var value = search.property('value'); var results = features(); list.classed('filtered', value.length); var resultsIndicator = list.selectAll('.no-results-item') .data([0]) .enter() .append('button') .property('disabled', true) .attr('class', 'no-results-item') .call(svgIcon('#iD-icon-alert', 'pre-text')); resultsIndicator.append('span') .attr('class', 'entity-name'); list.selectAll('.no-results-item .entity-name') .html(t.html('geocoder.no_results_worldwide')); if (services.geocoder) { list.selectAll('.geocode-item') .data([0]) .enter() .append('button') .attr('class', 'geocode-item secondary-action') .on('click', geocoderSearch) .append('div') .attr('class', 'label') .append('span') .attr('class', 'entity-name') .html(t.html('geocoder.search')); } list.selectAll('.no-results-item') .style('display', (value.length && !results.length) ? 'block' : 'none'); list.selectAll('.geocode-item') .style('display', (value && _geocodeResults === undefined) ? 'block' : 'none'); list.selectAll('.feature-list-item') .data([-1]) .remove(); var items = list.selectAll('.feature-list-item') .data(results, function(d) { return d.id; }); var enter = items.enter() .insert('button', '.geocode-item') .attr('class', 'feature-list-item') .on('mouseover', mouseover) .on('mouseout', mouseout) .on('click', click); var label = enter .append('div') .attr('class', 'label'); label .each(function(d) { d3_select(this) .call(svgIcon('#iD-icon-' + d.geometry, 'pre-text')); }); label .append('span') .attr('class', 'entity-type') .html(function(d) { return d.type; }); label .append('span') .attr('class', 'entity-name') .html(function(d) { return d.name; }); enter .style('opacity', 0) .transition() .style('opacity', 1); items.order(); items.exit() .remove(); } function mouseover(d3_event, d) { if (d.id === -1) return; utilHighlightEntities([d.id], true, context); } function mouseout(d3_event, d) { if (d.id === -1) return; utilHighlightEntities([d.id], false, context); } function click(d3_event, d) { d3_event.preventDefault(); if (d.location) { context.map().centerZoomEase([d.location[1], d.location[0]], 19); } else if (d.entity) { utilHighlightEntities([d.id], false, context); context.enter(modeSelect(context, [d.entity.id])); context.map().zoomToEase(d.entity); } else { // download, zoom to, and select the entity with the given ID context.zoomToEntity(d.id); } } function geocoderSearch() { services.geocoder.search(search.property('value'), function (err, resp) { _geocodeResults = resp || []; drawList(); }); } } return featureList; }