mirror of
https://github.com/FoggedLens/iD.git
synced 2026-02-13 01:02:58 +00:00
381 lines
11 KiB
JavaScript
381 lines
11 KiB
JavaScript
import { isEqual } from 'lodash-es';
|
|
|
|
import { t } from '../core/localizer';
|
|
import { osmAreaKeys, osmAreaKeysExceptions } from '../osm/tags';
|
|
import { utilObjectOmit } from '../util';
|
|
import { utilSafeClassName } from '../util/util';
|
|
import { locationManager } from '../core/LocationManager';
|
|
|
|
|
|
//
|
|
// `presetPreset` decorates a given `preset` Object
|
|
// with some extra methods for searching and matching geometry
|
|
//
|
|
export function presetPreset(presetID, preset, addable, allFields, allPresets) {
|
|
allFields = allFields || {};
|
|
allPresets = allPresets || {};
|
|
let _this = Object.assign({}, preset); // shallow copy
|
|
let _addable = addable || false;
|
|
let _searchName; // cache
|
|
let _searchNameStripped; // cache
|
|
let _searchAliases; // cache
|
|
let _searchAliasesStripped; // cache
|
|
|
|
const referenceRegex = /^\{(.*)\}$/;
|
|
|
|
_this.id = presetID;
|
|
|
|
_this.safeid = utilSafeClassName(presetID); // for use in css classes, selectors, element ids
|
|
|
|
_this.originalTerms = (_this.terms || []).join();
|
|
|
|
_this.originalName = _this.name || '';
|
|
|
|
_this.originalAliases = (_this.aliases || []).join('\n');
|
|
|
|
_this.originalScore = _this.matchScore || 1;
|
|
|
|
_this.originalReference = _this.reference || {};
|
|
|
|
_this.originalFields = (_this.fields || []);
|
|
|
|
_this.originalMoreFields = (_this.moreFields || []);
|
|
|
|
_this.fields = loc => resolveFields('fields', loc);
|
|
|
|
_this.moreFields = loc => resolveFields('moreFields', loc);
|
|
|
|
_this.tags = _this.tags || {};
|
|
|
|
_this.addTags = _this.addTags || _this.tags;
|
|
|
|
_this.removeTags = _this.removeTags || _this.addTags;
|
|
|
|
_this.geometry = (_this.geometry || []);
|
|
|
|
_this.matchGeometry = (geom) => _this.geometry.indexOf(geom) >= 0;
|
|
|
|
_this.matchAllGeometry = (geoms) => geoms.every(_this.matchGeometry);
|
|
|
|
_this.matchScore = (entityTags) => {
|
|
const tags = _this.tags;
|
|
let seen = {};
|
|
let score = 0;
|
|
|
|
// match on tags
|
|
for (let k in tags) {
|
|
seen[k] = true;
|
|
if (entityTags[k] === tags[k]) {
|
|
score += _this.originalScore;
|
|
} else if (tags[k] === '*' && k in entityTags) {
|
|
score += _this.originalScore / 2;
|
|
} else {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
// boost score for additional matches in addTags - #6802
|
|
const addTags = _this.addTags;
|
|
for (let k in addTags) {
|
|
if (!seen[k] && entityTags[k] === addTags[k]) {
|
|
score += _this.originalScore;
|
|
}
|
|
}
|
|
|
|
if (_this.searchable === false) {
|
|
score *= 0.999;
|
|
}
|
|
|
|
return score;
|
|
};
|
|
|
|
|
|
_this.t = (scope, options) => {
|
|
const textID = `_tagging.presets.presets.${presetID}.${scope}`;
|
|
return t(textID, options);
|
|
};
|
|
|
|
_this.t.append = (scope, options) => {
|
|
const textID = `_tagging.presets.presets.${presetID}.${scope}`;
|
|
return t.append(textID, options);
|
|
};
|
|
|
|
function resolveReference(which) {
|
|
const match = (_this[which] || '').match(referenceRegex);
|
|
if (match) {
|
|
const preset = allPresets[match[1]];
|
|
if (preset) {
|
|
return preset;
|
|
}
|
|
console.error(`Unable to resolve referenced preset: ${match[1]}`); // eslint-disable-line no-console
|
|
}
|
|
return _this;
|
|
}
|
|
|
|
_this.name = () => {
|
|
return resolveReference('originalName')
|
|
.t('name', { 'default': _this.originalName || presetID });
|
|
};
|
|
|
|
_this.nameLabel = () => {
|
|
return resolveReference('originalName')
|
|
.t.append('name', { 'default': _this.originalName || presetID });
|
|
};
|
|
|
|
_this.subtitle = () => {
|
|
if (_this.suggestion) {
|
|
let path = presetID.split('/');
|
|
path.pop(); // remove brand name
|
|
return t('_tagging.presets.presets.' + path.join('/') + '.name');
|
|
}
|
|
return null;
|
|
};
|
|
|
|
_this.subtitleLabel = () => {
|
|
if (_this.suggestion) {
|
|
let path = presetID.split('/');
|
|
path.pop(); // remove brand name
|
|
return t.append('_tagging.presets.presets.' + path.join('/') + '.name');
|
|
}
|
|
return null;
|
|
};
|
|
|
|
_this.aliases = () => {
|
|
return resolveReference('originalName')
|
|
.t('aliases', { 'default': _this.originalAliases })
|
|
.trim()
|
|
.split(/\s*[\r\n]+\s*/)
|
|
.filter(Boolean);
|
|
};
|
|
|
|
_this.terms = () => {
|
|
return resolveReference('originalName')
|
|
.t('terms', { 'default': _this.originalTerms })
|
|
.toLowerCase().trim().split(/\s*,+\s*/);
|
|
};
|
|
|
|
_this.searchName = () => {
|
|
if (!_searchName) {
|
|
_searchName = (_this.suggestion ? _this.originalName : _this.name()).toLowerCase();
|
|
}
|
|
return _searchName;
|
|
};
|
|
|
|
_this.searchNameStripped = () => {
|
|
if (!_searchNameStripped) {
|
|
_searchNameStripped = stripDiacritics(_this.searchName());
|
|
}
|
|
return _searchNameStripped;
|
|
};
|
|
|
|
_this.searchAliases = () => {
|
|
if (!_searchAliases) {
|
|
_searchAliases = _this.aliases().map(alias => alias.toLowerCase());
|
|
}
|
|
return _searchAliases;
|
|
};
|
|
|
|
_this.searchAliasesStripped = () => {
|
|
if (!_searchAliasesStripped) {
|
|
_searchAliasesStripped = _this.searchAliases();
|
|
_searchAliasesStripped = _searchAliasesStripped.map(stripDiacritics);
|
|
}
|
|
return _searchAliasesStripped;
|
|
};
|
|
|
|
_this.isFallback = () => {
|
|
const tagCount = Object.keys(_this.tags).length;
|
|
return tagCount === 0 || (tagCount === 1 && _this.tags.hasOwnProperty('area'));
|
|
};
|
|
|
|
|
|
_this.addable = function(val) {
|
|
if (!arguments.length) return _addable;
|
|
_addable = val;
|
|
return _this;
|
|
};
|
|
|
|
|
|
_this.reference = () => {
|
|
// Lookup documentation on Wikidata...
|
|
const qid = (
|
|
_this.tags.wikidata ||
|
|
_this.tags['flag:wikidata'] ||
|
|
_this.tags['brand:wikidata'] ||
|
|
_this.tags['network:wikidata'] ||
|
|
_this.tags['operator:wikidata']
|
|
);
|
|
if (qid) {
|
|
return { qid: qid };
|
|
}
|
|
|
|
// Lookup documentation on OSM Wikibase...
|
|
let key = _this.originalReference.key || Object.keys(utilObjectOmit(_this.tags, 'name'))[0];
|
|
let value = _this.originalReference.value || _this.tags[key];
|
|
|
|
if (value === '*') {
|
|
return { key: key };
|
|
} else {
|
|
return { key: key, value: value };
|
|
}
|
|
};
|
|
|
|
|
|
_this.unsetTags = (tags, geometry, ignoringKeys, skipFieldDefaults, loc) => {
|
|
// allow manually keeping some tags
|
|
let removeTags = ignoringKeys ? utilObjectOmit(_this.removeTags, ignoringKeys) : _this.removeTags;
|
|
tags = utilObjectOmit(tags, Object.keys(removeTags));
|
|
|
|
if (geometry && !skipFieldDefaults) {
|
|
_this.fields(loc).forEach(field => {
|
|
if (field.matchGeometry(geometry) && field.key &&
|
|
field.default === tags[field.key] &&
|
|
(!ignoringKeys || ignoringKeys.indexOf(field.key) === -1)) {
|
|
delete tags[field.key];
|
|
}
|
|
});
|
|
}
|
|
|
|
delete tags.area;
|
|
return tags;
|
|
};
|
|
|
|
|
|
_this.setTags = (tags, geometry, skipFieldDefaults, loc) => {
|
|
const addTags = _this.addTags;
|
|
tags = Object.assign({}, tags); // shallow copy
|
|
|
|
for (let k in addTags) {
|
|
if (addTags[k] === '*') {
|
|
// if this tag is ancillary, don't override an existing value since any value is okay
|
|
if (_this.tags[k] || !tags[k]) {
|
|
tags[k] = 'yes';
|
|
}
|
|
} else {
|
|
tags[k] = addTags[k];
|
|
}
|
|
}
|
|
|
|
// Add area=yes if necessary.
|
|
// This is necessary if the geometry is already an area (e.g. user drew an area) AND any of:
|
|
// 1. chosen preset could be either an area or a line (`barrier=city_wall`)
|
|
// 2. chosen preset doesn't have a key in osmAreaKeys (`railway=station`),
|
|
// and is not an "exceptional area" tag (e.g. `waterway=dam`)
|
|
if (!addTags.hasOwnProperty('area')) {
|
|
delete tags.area;
|
|
if (geometry === 'area') {
|
|
let needsAreaTag = true;
|
|
for (let k in addTags) {
|
|
if (_this.geometry.indexOf('line') === -1 && k in osmAreaKeys
|
|
|| k in osmAreaKeysExceptions && addTags[k] in osmAreaKeysExceptions[k]) {
|
|
needsAreaTag = false;
|
|
break;
|
|
}
|
|
}
|
|
if (needsAreaTag) {
|
|
tags.area = 'yes';
|
|
}
|
|
}
|
|
}
|
|
|
|
if (geometry && !skipFieldDefaults) {
|
|
_this.fields(loc).forEach(field => {
|
|
if (field.matchGeometry(geometry) && field.key && !tags[field.key] && field.default) {
|
|
tags[field.key] = field.default;
|
|
}
|
|
});
|
|
}
|
|
|
|
return tags;
|
|
};
|
|
|
|
|
|
// For a preset without fields, use the fields of the parent preset.
|
|
// Replace {preset} placeholders with the fields of the specified presets.
|
|
function resolveFields(which, loc) {
|
|
const fieldIDs = (which === 'fields' ? _this.originalFields : _this.originalMoreFields);
|
|
let resolved = [];
|
|
|
|
fieldIDs.forEach(fieldID => {
|
|
const match = fieldID.match(referenceRegex);
|
|
if (match !== null) { // a presetID wrapped in braces {}
|
|
resolved = resolved.concat(inheritFields(allPresets[match[1]], which, loc));
|
|
} else if (allFields[fieldID]) { // a normal fieldID
|
|
resolved.push(allFields[fieldID]);
|
|
} else {
|
|
console.log(`Cannot resolve "${fieldID}" found in ${_this.id}.${which}`); // eslint-disable-line no-console
|
|
}
|
|
});
|
|
|
|
// no fields resolved, so use the parent's if possible
|
|
if (!resolved.length) {
|
|
const endIndex = _this.id.lastIndexOf('/');
|
|
const parentID = endIndex && _this.id.substring(0, endIndex);
|
|
if (parentID) {
|
|
let parent = allPresets[parentID];
|
|
if (loc) {
|
|
const validHere = locationManager.locationSetsAt(loc);
|
|
if (parent?.locationSetID && !validHere[parent.locationSetID]) {
|
|
// this is a preset for which a regional variant of the main preset exists
|
|
const candidateIDs = Object.keys(allPresets).filter(k => k.startsWith(parentID));
|
|
parent = allPresets[candidateIDs.find(candidateID => {
|
|
const candidate = allPresets[candidateID];
|
|
return validHere[candidate.locationSetID] && isEqual(candidate.tags, parent.tags);
|
|
})];
|
|
}
|
|
}
|
|
resolved = inheritFields(parent, which, loc);
|
|
}
|
|
}
|
|
|
|
if (loc) {
|
|
const validHere = locationManager.locationSetsAt(loc);
|
|
resolved = resolved.filter(field => !field.locationSetID || validHere[field.locationSetID]);
|
|
}
|
|
|
|
return resolved;
|
|
|
|
|
|
// returns an array of fields to inherit from the given presetID, if found
|
|
function inheritFields(parent, which, loc) {
|
|
if (!parent) return [];
|
|
|
|
if (which === 'fields') {
|
|
return parent.fields(loc).filter(shouldInherit);
|
|
} else if (which === 'moreFields') {
|
|
return parent.moreFields(loc).filter(shouldInherit);
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
|
|
// Skip `fields` for the keys which define the preset.
|
|
// These are usually `typeCombo` fields like `shop=*`
|
|
function shouldInherit(f) {
|
|
if (f.key && _this.tags[f.key] !== undefined &&
|
|
// inherit anyway if multiple values are allowed or just a checkbox
|
|
f.type !== 'multiCombo' && f.type !== 'semiCombo' && f.type !== 'manyCombo' && f.type !== 'check'
|
|
) return false;
|
|
if (f.key && (_this.originalFields.some(originalField => f.key === allFields[originalField]?.key)
|
|
|| _this.originalMoreFields.some(originalField => f.key === allFields[originalField]?.key))) {
|
|
// current preset already has a field for this field
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
|
|
function stripDiacritics(s) {
|
|
// split combined diacritical characters into their parts
|
|
if (s.normalize) s = s.normalize('NFD');
|
|
// remove diacritics
|
|
s = s.replace(/[\u0300-\u036f]/g, '');
|
|
return s;
|
|
}
|
|
|
|
return _this;
|
|
}
|