Files
iD/modules/presets/preset.js
Quincy Morgan a1af118f0e Ensure locales and presets are loaded before the UI loads (close #7406)
Consolidate localization behavior and init to a coreLocalizer function and singleton
Explicitly support `en-US` locale
Rename coreData to coreFileFetcher and export a singleton rather than using a property of coreContext
Add `apiConnections` property of coreContext to simplify adding a source switcher
Replace some init functions with re-callable, promise-supporting `ensureLoaded` functions
Make coreContext itself load the UI if a container has been specified at init time
Fix code tests
2020-03-31 12:23:31 -07:00

267 lines
7.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { t } from '../core/localizer';
import { osmAreaKeys } from '../osm/tags';
import { utilArrayUniq, utilObjectOmit } from '../util';
import { utilSafeClassName } from '../util/util';
//
// `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
_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.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;
};
let _textCache = {};
_this.t = (scope, options) => {
const textID = `presets.presets.${presetID}.${scope}`;
if (_textCache[textID]) return _textCache[textID];
return _textCache[textID] = t(textID, options);
};
_this.name = () => {
if (_this.suggestion) {
let path = presetID.split('/');
path.pop(); // remove brand name
// NOTE: insert an en-dash, not a hypen (to avoid conflict with fr - nl names in Brussels etc)
return _this.originalName + ' ' + t('presets.presets.' + path.join('/') + '.name');
}
return _this.t('name', { 'default': _this.originalName });
};
_this.terms = () => _this.t('terms', { 'default': _this.originalTerms })
.toLowerCase().trim().split(/\s*,+\s*/);
_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 = (geom) => {
// Lookup documentation on Wikidata...
const qid = _this.tags.wikidata || _this.tags['brand: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 (geom === 'relation' && key === 'type') {
if (value in _this.tags) {
key = value;
value = _this.tags[key];
} else {
return { rtype: value };
}
}
if (value === '*') {
return { key: key };
} else {
return { key: key, value: value };
}
};
_this.unsetTags = (tags, geometry, skipFieldDefaults) => {
tags = utilObjectOmit(tags, Object.keys(_this.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] === '*') {
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 !== 'check'
) return false;
return true;
}
}
return _this;
}