diff --git a/data/core.yaml b/data/core.yaml index 6e046f93d..ea179364e 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -1943,6 +1943,9 @@ en: tag_as_disconnected: title: Tag as disconnected annotation: Tagged very close features as disconnected. + tag_as_not: + title: "Tag as not the same \"{name}\"" + annotation: "Added not: tag" tag_as_unsquare: title: Tag as physically unsquare annotation: Tagged a way as having unsquare corners. diff --git a/modules/services/nsi.js b/modules/services/nsi.js index d3094ef29..af7c4162a 100644 --- a/modules/services/nsi.js +++ b/modules/services/nsi.js @@ -399,7 +399,11 @@ function gatherTuples(tryKVs, tryNames) { // `tags`: `Object` containing the feature's OSM tags // `loc`: Location where this feature exists, as a [lon, lat] // Returns -// `Object`: The tags the the feature should have, or `null` if no changes needed +// `Object` containing the result, or `null` if no changes needed: +// { +// 'newTags': `Object` - The tags the the feature should have +// 'matched': `Object` - The matched item +// } // function _upgradeTags(tags, loc) { let newTags = Object.assign({}, tags); // shallow copy @@ -438,7 +442,9 @@ function _upgradeTags(tags, loc) { // Gather key/value tag pairs to try to match const tryKVs = gatherKVs(tags); - if (!tryKVs.primary.size && !tryKVs.alternate.size) return changed ? newTags : null; + if (!tryKVs.primary.size && !tryKVs.alternate.size) { + return changed ? { newTags: newTags, matched: null } : null; + } // Gather namelike tag values to try to match const tryNames = gatherNames(tags); @@ -448,7 +454,9 @@ function _upgradeTags(tags, loc) { const foundQID = _nsi.qids.get(tags.wikidata) || _nsi.qids.get(tags.wikipedia); if (foundQID) tryNames.primary.add(foundQID); // matcher will recognize the Wikidata QID as name too - if (!tryNames.primary.size && !tryNames.alternate.size) return changed ? newTags : null; + if (!tryNames.primary.size && !tryNames.alternate.size) { + return changed ? { newTags: newTags, matched: null } : null; + } // Order the [key,value,name] tuples - test primary before alternate const tuples = gatherTuples(tryKVs, tryNames); @@ -489,6 +497,7 @@ function _upgradeTags(tags, loc) { if (!item) continue; // At this point we have matched a canonical item and can suggest tag upgrades.. + item = JSON.parse(JSON.stringify(item)); // deep copy const tkv = item.tkv; const parts = tkv.split('/', 3); // tkv = "tree/key/value" const k = parts[1]; @@ -579,10 +588,10 @@ function _upgradeTags(tags, loc) { } } - return newTags; + return { newTags: newTags, matched: item }; } - return changed ? newTags : null; + return changed ? { newTags: newTags, matched: null } : null; } @@ -682,7 +691,11 @@ export default { // `tags`: `Object` containing the feature's OSM tags // `loc`: Location where this feature exists, as a [lon, lat] // Returns - // `Object`: The tags the the feature should have, or `null` if no change + // `Object` containing the result, or `null` if no changes needed: + // { + // 'newTags': `Object` - The tags the the feature should have + // 'matched': `Object` - The matched item + // } // upgradeTags: (tags, loc) => _upgradeTags(tags, loc), diff --git a/modules/validations/outdated_tags.js b/modules/validations/outdated_tags.js index 2116cc262..430e6bd16 100644 --- a/modules/validations/outdated_tags.js +++ b/modules/validations/outdated_tags.js @@ -66,13 +66,14 @@ export function validationOutdatedTags() { // Attempt to match a canonical record in the name-suggestion-index. const nsi = services.nsi; let waitingForNsi = false; + let nsiResult; if (nsi) { waitingForNsi = (nsi.status() === 'loading'); if (!waitingForNsi) { const loc = entity.extent(graph).center(); - const result = nsi.upgradeTags(newTags, loc); - if (result) { - newTags = result; + nsiResult = nsi.upgradeTags(newTags, loc); + if (nsiResult) { + newTags = nsiResult.newTags; subtype = 'noncanonical_brand'; } } @@ -88,7 +89,7 @@ export function validationOutdatedTags() { const isOnlyAddingTags = tagDiff.every(d => d.type === '+'); let prefix = ''; - if (subtype === 'noncanonical_brand') { + if (nsiResult) { prefix = 'noncanonical_brand.'; } else if (subtype === 'deprecated_tags' && isOnlyAddingTags) { subtype = 'incomplete_tags'; @@ -107,7 +108,7 @@ export function validationOutdatedTags() { entityIds: [entity.id], hash: utilHashcode(JSON.stringify(tagDiff)), dynamicFixes: () => { - return [ + let fixes = [ new validationIssueFix({ autoArgs: autoArgs, title: t.html('issues.fix.upgrade_tags.title'), @@ -116,6 +117,20 @@ export function validationOutdatedTags() { } }) ]; + + const item = nsiResult && nsiResult.matched; + if (item) { + fixes.push( + new validationIssueFix({ + title: t.html('issues.fix.tag_as_not.title', { name: item.displayName }), + onClick: (context) => { + context.perform(addNotTag, t('issues.fix.tag_as_not.annotation')); + } + }) + ); + } + return fixes; + } })); return issues; @@ -138,6 +153,29 @@ export function validationOutdatedTags() { } + function addNotTag(graph) { + const currEntity = graph.hasEntity(entity.id); + if (!currEntity) return graph; + + const item = nsiResult && nsiResult.matched; + if (!item) return graph; + + let newTags = Object.assign({}, currEntity.tags); // shallow copy + const wd = item.mainTag; // e.g. `brand:wikidata` + const notwd = `not:${wd}`; // e.g. `not:brand:wikidata` + const qid = item.tags[wd]; + newTags[notwd] = qid; + + if (newTags[wd] === qid) { // if `brand:wikidata` was set to that qid + const wp = item.mainTag.replace('wikidata', 'wikipedia'); + delete newTags[wd]; // remove `brand:wikidata` + delete newTags[wp]; // remove `brand:wikipedia` + } + + return actionChangeTags(currEntity.id, newTags)(graph); + } + + function showMessage(context) { const currEntity = context.hasEntity(entity.id); if (!currEntity) return '';