1+ import { actionChangeTags } from '../actions/change_tags' ;
2+ import { t } from '../core/localizer' ;
3+ import { utilDisplayLabel } from '../util' ;
4+ import { validationIssue , validationIssueFix } from '../core/validation' ;
5+ import { osmMutuallyExclusiveTagPairs } from '../osm/tags' ;
6+
7+ export function validationMutuallyExclusiveTags ( /* context */ ) {
8+ const type = 'mutually_exclusive_tags' ;
9+
10+ // https://wiki.openstreetmap.org/wiki/Special:WhatLinksHere/Property:P44
11+ const tagKeyPairs = osmMutuallyExclusiveTagPairs ;
12+
13+ const validation = function checkMutuallyExclusiveTags ( entity /*, graph */ ) {
14+
15+ let pairsFounds = tagKeyPairs . filter ( ( pair ) => {
16+ return ( pair [ 0 ] in entity . tags && pair [ 1 ] in entity . tags ) ;
17+ } ) . filter ( ( pair ) => {
18+ // noname=no is double-negation, thus positive and not conflicting. We'll ignore those
19+ return ! ( ( pair [ 0 ] . match ( / ^ ( a d d r : ) ? n o [ a - z ] / ) && entity . tags [ pair [ 0 ] ] === 'no' ) ||
20+ ( pair [ 1 ] . match ( / ^ ( a d d r : ) ? n o [ a - z ] / ) && entity . tags [ pair [ 1 ] ] === 'no' ) ) ;
21+ } ) ;
22+
23+ // Additional:
24+ // Check if name and not:name (and similar) are set and both have the same value
25+ // not:name can actually have multiple values, separate by ;
26+ // https://taginfo.openstreetmap.org/search?q=not%3A#keys
27+ Object . keys ( entity . tags ) . forEach ( ( key ) => {
28+ let negative_key = 'not:' + key ;
29+ if ( negative_key in entity . tags && entity . tags [ negative_key ] . split ( ';' ) . includes ( entity . tags [ key ] ) ) {
30+ pairsFounds . push ( [ negative_key , key , 'same_value' ] ) ;
31+ }
32+ // For name:xx we also compare against the not:name tag
33+ if ( key . match ( / ^ n a m e : [ a - z ] + / ) ) {
34+ negative_key = 'not:name' ;
35+ if ( negative_key in entity . tags && entity . tags [ negative_key ] . split ( ';' ) . includes ( entity . tags [ key ] ) ) {
36+ pairsFounds . push ( [ negative_key , key , 'same_value' ] ) ;
37+ }
38+ }
39+ } ) ;
40+
41+ let issues = pairsFounds . map ( ( pair ) => {
42+ const subtype = pair [ 2 ] || 'default' ;
43+ return new validationIssue ( {
44+ type : type ,
45+ subtype : subtype ,
46+ severity : 'warning' ,
47+ message : function ( context ) {
48+ let entity = context . hasEntity ( this . entityIds [ 0 ] ) ;
49+ return entity ? t . append ( `issues.${ type } .${ subtype } .message` , {
50+ feature : utilDisplayLabel ( entity , context . graph ( ) ) ,
51+ tag1 : pair [ 0 ] ,
52+ tag2 : pair [ 1 ]
53+ } ) : '' ;
54+ } ,
55+ reference : ( selection ) => showReference ( selection , pair , subtype ) ,
56+ entityIds : [ entity . id ] ,
57+ dynamicFixes : ( ) => pair . slice ( 0 , 2 ) . map ( ( tagToRemove ) => createIssueFix ( tagToRemove ) )
58+ } ) ;
59+ } ) ;
60+
61+ function createIssueFix ( tagToRemove ) {
62+ return new validationIssueFix ( {
63+ icon : 'iD-operation-delete' ,
64+ title : t . append ( 'issues.fix.remove_named_tag.title' , { tag : tagToRemove } ) ,
65+ onClick : function ( context ) {
66+ const entityId = this . issue . entityIds [ 0 ] ;
67+ const entity = context . entity ( entityId ) ;
68+ let tags = Object . assign ( { } , entity . tags ) ; // shallow copy
69+ delete tags [ tagToRemove ] ;
70+ context . perform (
71+ actionChangeTags ( entityId , tags ) ,
72+ t ( 'issues.fix.remove_named_tag.annotation' , { tag : tagToRemove } )
73+ ) ;
74+ }
75+ } ) ;
76+ }
77+
78+ function showReference ( selection , pair , subtype ) {
79+ selection . selectAll ( '.issue-reference' )
80+ . data ( [ 0 ] )
81+ . enter ( )
82+ . append ( 'div' )
83+ . attr ( 'class' , 'issue-reference' )
84+ . call ( t . append ( `issues.${ type } .${ subtype } .reference` , { tag1 : pair [ 0 ] , tag2 : pair [ 1 ] } ) ) ;
85+ }
86+
87+ return issues ;
88+ } ;
89+
90+ validation . type = type ;
91+
92+ return validation ;
93+ }
0 commit comments