modules/presets/preset.js (229 lines of code) (raw):
import { utilArrayUniq, utilObjectOmit, utilSafeString } from '@id-sdk/util';
import { t } from '../core/localizer';
import { osmAreaKeys } from '../osm/tags';
//
// `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 _resolvedFields; // cache
let _resolvedMoreFields; // cache
let _searchName; // cache
let _searchNameStripped; // cache
_this.id = presetID;
_this.safeid = utilSafeString(presetID); // for use in css classes, selectors, element ids
_this.originalTerms = (_this.terms || []).join();
_this.originalName = _this.name || '';
_this.originalScore = _this.matchScore || 1;
_this.originalReference = _this.reference || {};
_this.originalFields = (_this.fields || []);
_this.originalMoreFields = (_this.moreFields || []);
_this.fields = () => _resolvedFields || (_resolvedFields = resolve('fields'));
_this.moreFields = () => _resolvedMoreFields || (_resolvedMoreFields = resolve('moreFields'));
_this.resetFields = () => _resolvedFields = _resolvedMoreFields = null;
_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;
}
}
return score;
};
_this.t = (scope, options) => {
const textID = `_tagging.presets.presets.${presetID}.${scope}`;
return t(textID, options);
};
_this.t.html = (scope, options) => {
const textID = `_tagging.presets.presets.${presetID}.${scope}`;
return t.html(textID, options);
};
_this.name = () => {
return _this.t('name', { 'default': _this.originalName });
};
_this.nameLabel = () => {
return _this.t.html('name', { 'default': _this.originalName });
};
_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.html('_tagging.presets.presets.' + path.join('/') + '.name');
}
return null;
};
_this.terms = () => _this.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 = _this.searchName();
// split combined diacritical characters into their parts
if (_searchNameStripped.normalize) _searchNameStripped = _searchNameStripped.normalize('NFD');
// remove diacritics
_searchNameStripped = _searchNameStripped.replace(/[\u0300-\u036f]/g, '');
}
return _searchNameStripped;
};
_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) => {
// 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().forEach(field => {
if (field.matchGeometry(geometry) && field.key && field.default === tags[field.key]) {
delete tags[field.key];
}
});
}
delete tags.area;
return tags;
};
_this.setTags = (tags, geometry, skipFieldDefaults) => {
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] === 'no') {
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`)
if (!addTags.hasOwnProperty('area')) {
delete tags.area;
if (geometry === 'area') {
let needsAreaTag = true;
if (_this.geometry.indexOf('line') === -1) {
for (let k in addTags) {
if (k in osmAreaKeys) {
needsAreaTag = false;
break;
}
}
}
if (needsAreaTag) {
tags.area = 'yes';
}
}
}
if (geometry && !skipFieldDefaults) {
_this.fields().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 resolve(which) {
const fieldIDs = (which === 'fields' ? _this.originalFields : _this.originalMoreFields);
let resolved = [];
fieldIDs.forEach(fieldID => {
const match = fieldID.match(/\{(.*)\}/);
if (match !== null) { // a presetID wrapped in braces {}
resolved = resolved.concat(inheritFields(match[1], which));
} 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) {
resolved = inheritFields(parentID, which);
}
}
return utilArrayUniq(resolved);
// returns an array of fields to inherit from the given presetID, if found
function inheritFields(presetID, which) {
const parent = allPresets[presetID];
if (!parent) return [];
if (which === 'fields') {
return parent.fields().filter(shouldInherit);
} else if (which === 'moreFields') {
return parent.moreFields();
} 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;
return true;
}
}
return _this;
}