mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-23 16:49:40 +02:00
2b2a71f597
This was taking a lot of time at app startup. Instad now we resolve and index only the include and exclude parts. We can still determine the valid locationSets at runtime in `locationSetsAt()` by checking the `_locationIncludedIn` and `_locationExcludedIn` caches. This also upgrades the locationManger to an ES6 class. This also includes some hacky code in nsi.js so that the NSI will continue to work. The NSI matcher can build its own location index, but it doesn't need to do this. We monkeypatch a few of the matcher collections to work with the new LocationManager.
198 lines
6.4 KiB
JavaScript
198 lines
6.4 KiB
JavaScript
import { locationManager } from '../core/LocationManager';
|
|
import { utilArrayUniq } from '../util/array';
|
|
import { utilEditDistance } from '../util';
|
|
|
|
|
|
//
|
|
// `presetCollection` is a wrapper around an `Array` of presets `collection`,
|
|
// and decorated with some extra methods for searching and matching geometry
|
|
//
|
|
export function presetCollection(collection) {
|
|
const MAXRESULTS = 50;
|
|
let _this = {};
|
|
let _memo = {};
|
|
|
|
_this.collection = collection;
|
|
|
|
_this.item = (id) => {
|
|
if (_memo[id]) return _memo[id];
|
|
const found = _this.collection.find(d => d.id === id);
|
|
if (found) _memo[id] = found;
|
|
return found;
|
|
};
|
|
|
|
_this.index = (id) => _this.collection.findIndex(d => d.id === id);
|
|
|
|
_this.matchGeometry = (geometry) => {
|
|
return presetCollection(
|
|
_this.collection.filter(d => d.matchGeometry(geometry))
|
|
);
|
|
};
|
|
|
|
_this.matchAllGeometry = (geometries) => {
|
|
return presetCollection(
|
|
_this.collection.filter(d => d && d.matchAllGeometry(geometries))
|
|
);
|
|
};
|
|
|
|
_this.matchAnyGeometry = (geometries) => {
|
|
return presetCollection(
|
|
_this.collection.filter(d => geometries.some(geom => d.matchGeometry(geom)))
|
|
);
|
|
};
|
|
|
|
_this.fallback = (geometry) => {
|
|
let id = geometry;
|
|
if (id === 'vertex') id = 'point';
|
|
return _this.item(id);
|
|
};
|
|
|
|
_this.search = (value, geometry, loc) => {
|
|
if (!value) return _this;
|
|
|
|
// don't remove diacritical characters since we're assuming the user is being intentional
|
|
value = value.toLowerCase().trim();
|
|
|
|
// match at name beginning or just after a space (e.g. "office" -> match "Law Office")
|
|
function leading(a) {
|
|
const index = a.indexOf(value);
|
|
return index === 0 || a[index - 1] === ' ';
|
|
}
|
|
|
|
// match at name beginning only
|
|
function leadingStrict(a) {
|
|
const index = a.indexOf(value);
|
|
return index === 0;
|
|
}
|
|
|
|
function sortPresets(nameProp, aliasesProp) {
|
|
return function sortNames(a, b) {
|
|
let aCompare = a[nameProp]();
|
|
let bCompare = b[nameProp]();
|
|
if (aliasesProp) {
|
|
// also search in aliases
|
|
const findMatchingAlias = strings => {
|
|
if (strings.some(s => s === value)) {
|
|
return strings.find(s => s === value);
|
|
} else {
|
|
return strings.find(s => s.includes(value));
|
|
}
|
|
};
|
|
aCompare = findMatchingAlias([aCompare].concat(a[aliasesProp]()));
|
|
bCompare = findMatchingAlias([bCompare].concat(b[aliasesProp]()));
|
|
}
|
|
|
|
// priority if search string matches preset name exactly - #4325
|
|
if (value === aCompare) return -1;
|
|
if (value === bCompare) return 1;
|
|
|
|
// priority for higher matchScore
|
|
let i = b.originalScore - a.originalScore;
|
|
if (i !== 0) return i;
|
|
|
|
// priority if search string appears earlier in preset name
|
|
i = aCompare.indexOf(value) - bCompare.indexOf(value);
|
|
if (i !== 0) return i;
|
|
|
|
// priority for shorter preset names
|
|
return aCompare.length - bCompare.length;
|
|
};
|
|
}
|
|
|
|
let pool = _this.collection;
|
|
if (Array.isArray(loc)) {
|
|
const validHere = locationManager.locationSetsAt(loc);
|
|
pool = pool.filter(a => !a.locationSetID || validHere[a.locationSetID]);
|
|
}
|
|
|
|
const searchable = pool.filter(a => a.searchable !== false && a.suggestion !== true);
|
|
const suggestions = pool.filter(a => a.suggestion === true);
|
|
|
|
// matches value to preset.name
|
|
const leadingNames = searchable
|
|
.filter(a => leading(a.searchName()) || a.searchAliases().some(leading))
|
|
.sort(sortPresets('searchName', 'searchAliases'));
|
|
|
|
// matches value to preset suggestion name
|
|
const leadingSuggestions = suggestions
|
|
.filter(a => leadingStrict(a.searchName()))
|
|
.sort(sortPresets('searchName'));
|
|
|
|
const leadingNamesStripped = searchable
|
|
.filter(a => leading(a.searchNameStripped()) || a.searchAliasesStripped().some(leading))
|
|
.sort(sortPresets('searchNameStripped', 'searchAliasesStripped'));
|
|
|
|
const leadingSuggestionsStripped = suggestions
|
|
.filter(a => leadingStrict(a.searchNameStripped()))
|
|
.sort(sortPresets('searchNameStripped'));
|
|
|
|
// matches value to preset.terms values
|
|
const leadingTerms = searchable
|
|
.filter(a => (a.terms() || []).some(leading));
|
|
|
|
const leadingSuggestionTerms = suggestions
|
|
.filter(a => (a.terms() || []).some(leading));
|
|
|
|
// matches value to preset.tags values
|
|
const leadingTagValues = searchable
|
|
.filter(a => Object.values(a.tags || {}).filter(val => val !== '*').some(leading));
|
|
|
|
// finds close matches to value in preset.name
|
|
const similarName = searchable
|
|
.map(a => ({ preset: a, dist: utilEditDistance(value, a.searchName()) }))
|
|
.filter(a => a.dist + Math.min(value.length - a.preset.searchName().length, 0) < 3)
|
|
.sort((a, b) => a.dist - b.dist)
|
|
.map(a => a.preset);
|
|
|
|
// finds close matches to value to preset suggestion name
|
|
const similarSuggestions = suggestions
|
|
.map(a => ({ preset: a, dist: utilEditDistance(value, a.searchName()) }))
|
|
.filter(a => a.dist + Math.min(value.length - a.preset.searchName().length, 0) < 1)
|
|
.sort((a, b) => a.dist - b.dist)
|
|
.map(a => a.preset);
|
|
|
|
// finds close matches to value in preset.terms
|
|
const similarTerms = searchable
|
|
.filter(a => {
|
|
return (a.terms() || []).some(b => {
|
|
return utilEditDistance(value, b) + Math.min(value.length - b.length, 0) < 3;
|
|
});
|
|
});
|
|
|
|
// matches key=value to preset.tags
|
|
let leadingTagKeyValues = [];
|
|
if (value.includes('=')) {
|
|
leadingTagKeyValues = searchable.filter(a => a.tags &&
|
|
Object.keys(a.tags).some(key => key + '=' + a.tags[key] === value))
|
|
.concat(searchable.filter(a => a.tags &&
|
|
Object.keys(a.tags).some(key => leading(key + '=' + a.tags[key]))));
|
|
}
|
|
|
|
let results = leadingNames.concat(
|
|
leadingSuggestions,
|
|
leadingNamesStripped,
|
|
leadingSuggestionsStripped,
|
|
leadingTerms,
|
|
leadingSuggestionTerms,
|
|
leadingTagValues,
|
|
similarName,
|
|
similarSuggestions,
|
|
similarTerms,
|
|
leadingTagKeyValues
|
|
).slice(0, MAXRESULTS - 1);
|
|
|
|
if (geometry) {
|
|
if (typeof geometry === 'string') {
|
|
results.push(_this.fallback(geometry));
|
|
} else {
|
|
geometry.forEach(geom => results.push(_this.fallback(geom)));
|
|
}
|
|
}
|
|
|
|
return presetCollection(utilArrayUniq(results));
|
|
};
|
|
|
|
|
|
return _this;
|
|
}
|