Files
iD/modules/presets/preset.js

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;
}