Merge remote-tracking branch 'mtmail/mutually-exclusive-tags-validator' into develop

This commit is contained in:
Martin Raifer
2024-02-26 14:11:14 +01:00
8 changed files with 214 additions and 101 deletions
+12
View File
@@ -255,3 +255,15 @@ export function isColourValid(value) {
}
return true;
}
// https://wiki.openstreetmap.org/wiki/Special:WhatLinksHere/Property:P44
export var osmMutuallyExclusiveTagPairs = [
['noname', 'name'],
['noref', 'ref'],
['nohousenumber', 'addr:housenumber'],
['noaddress', 'addr:housenumber'],
['noaddress', 'addr:housename'],
['noaddress', 'addr:unit'],
['addr:nostreet', 'addr:street']
];
+1
View File
@@ -10,6 +10,7 @@ export { validationMaprules } from './maprules';
export { validationMismatchedGeometry } from './mismatched_geometry';
export { validationMissingRole } from './missing_role';
export { validationMissingTag } from './missing_tag';
export { validationMutuallyExclusiveTags } from './mutually_exclusive_tags';
export { validationOutdatedTags } from './outdated_tags';
export { validationPrivateData } from './private_data';
export { validationSuspiciousName } from './suspicious_name';
@@ -0,0 +1,93 @@
import { actionChangeTags } from '../actions/change_tags';
import { t } from '../core/localizer';
import { utilDisplayLabel } from '../util';
import { validationIssue, validationIssueFix } from '../core/validation';
import { osmMutuallyExclusiveTagPairs } from '../osm/tags';
export function validationMutuallyExclusiveTags(/* context */) {
const type = 'mutually_exclusive_tags';
// https://wiki.openstreetmap.org/wiki/Special:WhatLinksHere/Property:P44
const tagKeyPairs = osmMutuallyExclusiveTagPairs;
const validation = function checkMutuallyExclusiveTags(entity /*, graph */) {
let pairsFounds = tagKeyPairs.filter((pair) => {
return (pair[0] in entity.tags && pair[1] in entity.tags);
}).filter((pair) => {
// noname=no is double-negation, thus positive and not conflicting. We'll ignore those
return !((pair[0].match(/^(addr:)?no[a-z]/) && entity.tags[pair[0]] === 'no') ||
(pair[1].match(/^(addr:)?no[a-z]/) && entity.tags[pair[1]] === 'no'));
});
// Additional:
// Check if name and not:name (and similar) are set and both have the same value
// not:name can actually have multiple values, separate by ;
// https://taginfo.openstreetmap.org/search?q=not%3A#keys
Object.keys(entity.tags).forEach((key) => {
let negative_key = 'not:' + key;
if (negative_key in entity.tags && entity.tags[negative_key].split(';').includes(entity.tags[key])) {
pairsFounds.push([negative_key, key, 'same_value']);
}
// For name:xx we also compare against the not:name tag
if (key.match(/^name:[a-z]+/)) {
negative_key = 'not:name';
if (negative_key in entity.tags && entity.tags[negative_key].split(';').includes(entity.tags[key])) {
pairsFounds.push([negative_key, key, 'same_value']);
}
}
});
let issues = pairsFounds.map((pair) => {
const subtype = pair[2] || 'default';
return new validationIssue({
type: type,
subtype: subtype,
severity: 'warning',
message: function(context) {
let entity = context.hasEntity(this.entityIds[0]);
return entity ? t.append(`issues.${type}.${subtype}.message`, {
feature: utilDisplayLabel(entity, context.graph()),
tag1: pair[0],
tag2: pair[1]
}) : '';
},
reference: (selection) => showReference(selection, pair, subtype),
entityIds: [entity.id],
dynamicFixes: () => pair.slice(0,2).map((tagToRemove) => createIssueFix(tagToRemove))
});
});
function createIssueFix(tagToRemove) {
return new validationIssueFix({
icon: 'iD-operation-delete',
title: t.append('issues.fix.remove_named_tag.title', { tag: tagToRemove }),
onClick: function(context) {
const entityId = this.issue.entityIds[0];
const entity = context.entity(entityId);
let tags = Object.assign({}, entity.tags); // shallow copy
delete tags[tagToRemove];
context.perform(
actionChangeTags(entityId, tags),
t('issues.fix.remove_named_tag.annotation', { tag: tagToRemove })
);
}
});
}
function showReference(selection, pair, subtype) {
selection.selectAll('.issue-reference')
.data([0])
.enter()
.append('div')
.attr('class', 'issue-reference')
.call(t.append(`issues.${type}.${subtype}.reference`, { tag1: pair[0], tag2: pair[1] }));
}
return issues;
};
validation.type = type;
return validation;
}
+1 -57
View File
@@ -96,53 +96,6 @@ export function validationSuspiciousName() {
}
}
function makeIncorrectNameIssue(entityId, nameKey, incorrectName, langCode) {
return new validationIssue({
type: type,
subtype: 'not_name',
severity: 'warning',
message: function(context) {
const entity = context.hasEntity(this.entityIds[0]);
if (!entity) return '';
const preset = presetManager.match(entity, context.graph());
const langName = langCode && localizer.languageName(langCode);
return t.append('issues.incorrect_name.message' + (langName ? '_language' : ''),
{ feature: preset.name(), name: incorrectName, language: langName }
);
},
reference: showReference,
entityIds: [entityId],
hash: `${nameKey}=${incorrectName}`,
dynamicFixes: function() {
return [
new validationIssueFix({
icon: 'iD-operation-delete',
title: t.append('issues.fix.remove_the_name.title'),
onClick: function(context) {
const entityId = this.issue.entityIds[0];
const entity = context.entity(entityId);
let tags = Object.assign({}, entity.tags); // shallow copy
delete tags[nameKey];
context.perform(
actionChangeTags(entityId, tags), t('issues.fix.remove_mistaken_name.annotation')
);
}
})
];
}
});
function showReference(selection) {
selection.selectAll('.issue-reference')
.data([0])
.enter()
.append('div')
.attr('class', 'issue-reference')
.call(t.append('issues.generic_name.reference'));
}
}
let validation = function checkGenericName(entity) {
const tags = entity.tags;
@@ -151,7 +104,6 @@ export function validationSuspiciousName() {
if (hasWikidata) return [];
let issues = [];
const notNames = (tags['not:name'] || '').split(';');
for (let key in tags) {
const m = key.match(/^name(?:(?::)([a-zA-Z_-]+))?$/);
@@ -159,15 +111,7 @@ export function validationSuspiciousName() {
const langCode = m.length >= 2 ? m[1] : null;
const value = tags[key];
if (notNames.length) {
for (let i in notNames) {
const notName = notNames[i];
if (notName && value === notName) {
issues.push(makeIncorrectNameIssue(entity.id, key, value, langCode));
continue;
}
}
}
if (isGenericName(value, tags)) {
issues.provisional = _waitingForNsi; // retry later if we are waiting on NSI to finish loading
issues.push(makeGenericNameIssue(entity.id, key, value, langCode));