mirror of
https://github.com/FoggedLens/iD.git
synced 2026-05-23 16:49:40 +02:00
Merge remote-tracking branch 'mtmail/mutually-exclusive-tags-validator' into develop
This commit is contained in:
@@ -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']
|
||||
];
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user