mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-12 16:52:50 +00:00
improves preset matching in cases like https://github.com/openstreetmap/id-tagging-schema/issues/632
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.filter(s => s.includes(value)).sort((a,b) => a.length - b.length)[0];
|
|
}
|
|
};
|
|
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;
|
|
}
|