Restore field inheritance

This commit is contained in:
Bryan Housel
2020-02-10 16:22:02 -05:00
parent 7364cb38fe
commit 3608659d21
6 changed files with 104 additions and 90 deletions

View File

@@ -95,8 +95,7 @@ export function presetIndex(context) {
const p = d.presets[presetID];
if (p) { // add or replace
const isAddable = !_addablePresetIDs || _addablePresetIDs.has(presetID);
_presets[presetID] = presetPreset(presetID, p, _fields, isAddable);
// _presets[presetID] = presetPreset(presetID, p, _fields, isAddable, _presets);
_presets[presetID] = presetPreset(presetID, p, isAddable, _fields, _presets);
} else { // remove (but not if it's a fallback)
const existing = _presets[presetID];
if (existing && !existing.isFallback()) {
@@ -132,11 +131,11 @@ export function presetIndex(context) {
});
}
// Rebuild universal fields
_universal = Object.values(_fields).reduce((acc, field) => {
if (field.universal) acc.push(field);
return acc;
}, []);
// Rebuild universal fields array
_universal = Object.values(_fields).filter(field => field.universal);
// Reset all the preset fields - they'll need to be resolved again
Object.values(_presets).forEach(preset => preset.resetFields());
// Rebuild _this.collection
_this.collection = Object.values(_presets).concat(Object.values(_categories));

View File

@@ -8,9 +8,13 @@ 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, allFields, addable, rawPresets) {
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;
@@ -24,13 +28,15 @@ export function presetPreset(presetID, preset, allFields, addable, rawPresets) {
_this.originalReference = _this.reference || {};
_this.fields = (_this.fields || []).map(f => allFields[f]);
_this.originalFields = (_this.fields || []);
_this.moreFields = (_this.moreFields || []).map(f => allFields[f]);
_this.originalMoreFields = (_this.moreFields || []);
if (rawPresets) {
resolveFieldInheritance();
}
_this.fields = () => _resolvedFields || (_resolvedFields = resolve('fields'));
_this.moreFields = () => _resolvedMoreFields || (_resolvedMoreFields = resolve('moreFields'));
_this.resetFields = () => _resolvedFields = _resolvedMoreFields = null;
_this.tags = _this.tags || {};
@@ -141,7 +147,7 @@ export function presetPreset(presetID, preset, allFields, addable, rawPresets) {
tags = utilObjectOmit(tags, Object.keys(_this.removeTags));
if (geometry && !skipFieldDefaults) {
_this.fields.forEach(field => {
_this.fields().forEach(field => {
if (field.matchGeometry(geometry) && field.key && field.default === tags[field.key]) {
delete tags[field.key];
}
@@ -188,7 +194,7 @@ export function presetPreset(presetID, preset, allFields, addable, rawPresets) {
}
if (geometry && !skipFieldDefaults) {
_this.fields.forEach(field => {
_this.fields().forEach(field => {
if (field.matchGeometry(geometry) && field.key && !tags[field.key] && field.default) {
tags[field.key] = field.default;
}
@@ -201,66 +207,57 @@ export function presetPreset(presetID, preset, allFields, addable, rawPresets) {
// For a preset without fields, use the fields of the parent preset.
// Replace {preset} placeholders with the fields of the specified presets.
function resolveFieldInheritance() {
function resolve(which) {
const fieldIDs = (which === 'fields' ? _this.originalFields : _this.originalMoreFields);
let resolved = [];
['fields', 'moreFields'].forEach(prop => {
let fieldIDs = [];
if (preset[prop] && preset[prop].length) { // fields were defined
preset[prop].forEach(fieldID => {
const match = fieldID.match(/\{(.*)\}/);
if (match !== null) { // presetID wrapped in braces {}
const inheritIDs = inheritedFieldIDs(match[1], prop);
if (inheritIDs !== null) {
fieldIDs = fieldIDs.concat(inheritIDs);
} else {
/* eslint-disable no-console */
console.log(`Cannot resolve presetID ${match[0]} found in ${_this.id} ${prop}`);
/* eslint-enable no-console */
}
} else {
fieldIDs.push(fieldID); // no braces - just a normal field
}
});
} else { // no fields defined, so use the parent's if possible
const endIndex = _this.id.lastIndexOf('/');
const parentID = endIndex && _this.id.substring(0, endIndex);
if (parentID) {
fieldIDs = inheritedFieldIDs(parentID, prop);
}
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
}
fieldIDs = utilArrayUniq(fieldIDs);
preset[prop] = fieldIDs;
rawPresets[_this.id][prop] = fieldIDs;
});
// Skip `fields` for the keys which define the _this.
// These are usually `typeCombo` fields like `shop=*`
function shouldInheritFieldWithID(fieldID) {
const f = allFields[fieldID];
if (f.key) {
if (_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;
// 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 true;
}
// returns an array of field IDs to inherit from the given presetID, if found
function inheritedFieldIDs(presetID, prop) {
if (!presetID) return null;
return utilArrayUniq(resolved);
const inheritPreset = rawPresets[presetID];
if (!inheritPreset) return null;
let inheritFieldIDs = inheritPreset[prop] || [];
if (prop === 'fields') {
inheritFieldIDs = inheritFieldIDs.filter(shouldInheritFieldWithID);
// 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 [];
}
}
return inheritFieldIDs;
// 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;
}
}

View File

@@ -106,7 +106,7 @@ export function uiFieldLocalized(field, context) {
var preset = context.presets().match(entity, context.graph());
var isSuggestion = preset && preset.suggestion;
var showsBrand = preset && preset.fields.filter(function(d) {
var showsBrand = preset && preset.originalFields.filter(function(d) {
return d.id === 'brand';
}).length;
// protect standardized brand names

View File

@@ -43,18 +43,22 @@ export function uiPresetEditor(context) {
var presetsManager = context.presets();
var allFields = [], allMoreFields = [];
var allFields = [];
var allMoreFields = [];
var sharedTotalFields;
_presets.forEach(function(preset) {
allFields = utilArrayUnion(allFields, preset.fields);
allMoreFields = utilArrayUnion(allMoreFields, preset.moreFields);
var fields = preset.fields();
var moreFields = preset.moreFields();
allFields = utilArrayUnion(allFields, fields);
allMoreFields = utilArrayUnion(allMoreFields, moreFields);
if (!sharedTotalFields) {
sharedTotalFields = utilArrayUnion(preset.fields, preset.moreFields);
sharedTotalFields = utilArrayUnion(fields, moreFields);
} else {
sharedTotalFields = sharedTotalFields.filter(function(field) {
return preset.fields.indexOf(field) !== -1 || preset.moreFields.indexOf(field) !== -1;
return fields.indexOf(field) !== -1 || moreFields.indexOf(field) !== -1;
});
}
});

View File

@@ -1,25 +1,25 @@
describe('iD.actionChangePreset', function() {
var oldPreset = iD.presetPreset('old', {tags: {old: 'true'}}),
newPreset = iD.presetPreset('new', {tags: {new: 'true'}});
var oldPreset = iD.presetPreset('old', {tags: {old: 'true'}});
var newPreset = iD.presetPreset('new', {tags: {new: 'true'}});
it('changes from one preset\'s tags to another\'s', function() {
var entity = iD.osmNode({tags: {old: 'true'}}),
graph = iD.coreGraph([entity]),
action = iD.actionChangePreset(entity.id, oldPreset, newPreset);
var entity = iD.osmNode({tags: {old: 'true'}});
var graph = iD.coreGraph([entity]);
var action = iD.actionChangePreset(entity.id, oldPreset, newPreset);
expect(action(graph).entity(entity.id).tags).to.eql({new: 'true'});
});
it('adds the tags of a new preset to an entity without an old preset', function() {
var entity = iD.osmNode(),
graph = iD.coreGraph([entity]),
action = iD.actionChangePreset(entity.id, null, newPreset);
var entity = iD.osmNode();
var graph = iD.coreGraph([entity]);
var action = iD.actionChangePreset(entity.id, null, newPreset);
expect(action(graph).entity(entity.id).tags).to.eql({new: 'true'});
});
it('removes the tags of an old preset from an entity without a new preset', function() {
var entity = iD.osmNode({tags: {old: 'true'}}),
graph = iD.coreGraph([entity]),
action = iD.actionChangePreset(entity.id, oldPreset, null);
var entity = iD.osmNode({tags: {old: 'true'}});
var graph = iD.coreGraph([entity]);
var action = iD.actionChangePreset(entity.id, oldPreset, null);
expect(action(graph).entity(entity.id).tags).to.eql({});
});
});

View File

@@ -1,7 +1,17 @@
describe('iD.presetPreset', function() {
it('has optional fields', function() {
var preset = iD.presetPreset('test', {});
expect(preset.fields).to.eql([]);
describe('#fields', function() {
it('has no fields by default', function() {
var preset = iD.presetPreset('test', {});
expect(preset.fields()).to.eql([]);
});
});
describe('#moreFields', function() {
it('has no moreFields by default', function() {
var preset = iD.presetPreset('test', {});
expect(preset.moreFields()).to.eql([]);
});
});
describe('#matchGeometry', function() {
@@ -136,14 +146,16 @@ describe('iD.presetPreset', function() {
});
it('adds default tags of fields with matching geometry', function() {
var isAddable = true;
var field = iD.presetField('field', {key: 'building', geometry: 'area', default: 'yes'});
var preset = iD.presetPreset('test', {fields: ['field']}, {field: field});
var preset = iD.presetPreset('test', {fields: ['field']}, isAddable, {field: field});
expect(preset.setTags({}, 'area')).to.eql({area: 'yes', building: 'yes'});
});
it('adds no default tags of fields with non-matching geometry', function() {
var isAddable = true;
var field = iD.presetField('field', {key: 'building', geometry: 'area', default: 'yes'});
var preset = iD.presetPreset('test', {fields: ['field']}, {field: field});
var preset = iD.presetPreset('test', {fields: ['field']}, isAddable, {field: field});
expect(preset.setTags({}, 'point')).to.eql({});
});
@@ -179,8 +191,9 @@ describe('iD.presetPreset', function() {
});
it('removes tags that match field default tags', function() {
var field = iD.presetField('field', {key: 'building', geometry: 'area', default: 'yes'}),
preset = iD.presetPreset('test', {fields: ['field']}, {field: field});
var isAddable = true;
var field = iD.presetField('field', {key: 'building', geometry: 'area', default: 'yes'});
var preset = iD.presetPreset('test', {fields: ['field']}, isAddable, {field: field});
expect(preset.unsetTags({building: 'yes'}, 'area')).to.eql({});
});
@@ -190,8 +203,9 @@ describe('iD.presetPreset', function() {
});
it('preserves tags that do not match field default tags', function() {
var field = iD.presetField('field', {key: 'building', geometry: 'area', default: 'yes'}),
preset = iD.presetPreset('test', {fields: ['field']}, {field: field});
var isAddable = true;
var field = iD.presetField('field', {key: 'building', geometry: 'area', default: 'yes'});
var preset = iD.presetPreset('test', {fields: ['field']}, isAddable, {field: field});
expect(preset.unsetTags({building: 'yep'}, 'area')).to.eql({ building: 'yep'});
});