diff --git a/modules/osm/deprecated.js b/modules/osm/deprecated.js new file mode 100644 index 000000000..6df2b566b --- /dev/null +++ b/modules/osm/deprecated.js @@ -0,0 +1,81 @@ +/** @typedef {{ old: Tags; replace?: Tags }[]} DataDeprecated */ + +/** @param {Tags} tags @param {DataDeprecated} dataDeprecated */ +export function getDeprecatedTags(tags, dataDeprecated) { + // if there are no tags, none can be deprecated + if (Object.keys(tags).length === 0) return []; + + /** @type {DataDeprecated} */ + var deprecated = []; + dataDeprecated.forEach((d) => { + var oldKeys = Object.keys(d.old); + if (d.replace) { + var hasExistingValues = Object.keys(d.replace).some((replaceKey) => { + if (!tags[replaceKey] || d.old[replaceKey]) return false; + var replaceValue = d.replace[replaceKey]; + if (replaceValue === '*') return false; + if (replaceValue === tags[replaceKey]) return false; + return true; + }); + // don't flag deprecated tags if the upgrade path would overwrite existing data - #7843 + if (hasExistingValues) return; + } + + var matchesDeprecatedTags = oldKeys.every((oldKey) => { + if (!tags[oldKey]) return false; + if (d.old[oldKey] === '*') return true; + if (d.old[oldKey] === tags[oldKey]) return true; + + var vals = tags[oldKey].split(';').filter(Boolean); + if (vals.length === 0) { + return false; + } else if (vals.length > 1) { + return vals.indexOf(d.old[oldKey]) !== -1; + } else { + if (tags[oldKey] === d.old[oldKey]) { + if (d.replace && d.old[oldKey] === d.replace[oldKey]) { + var replaceKeys = Object.keys(d.replace); + return !replaceKeys.every((replaceKey) => { + return tags[replaceKey] === d.replace[replaceKey]; + }); + } else { + return true; + } + } + } + + return false; + }); + + if (matchesDeprecatedTags) { + deprecated.push(d); + } + }); + + return deprecated; +} + +/** @type {{ [key: string]: string[] }} */ +var _deprecatedTagValuesByKey; + +/** @param {DataDeprecated} dataDeprecated */ +export function deprecatedTagValuesByKey(dataDeprecated) { + if (!_deprecatedTagValuesByKey) { + _deprecatedTagValuesByKey = {}; + dataDeprecated.forEach((d) => { + var oldKeys = Object.keys(d.old); + if (oldKeys.length === 1) { + var oldKey = oldKeys[0]; + var oldValue = d.old[oldKey]; + if (oldValue !== '*') { + if (!_deprecatedTagValuesByKey[oldKey]) { + _deprecatedTagValuesByKey[oldKey] = [oldValue]; + } else { + _deprecatedTagValuesByKey[oldKey].push(oldValue); + } + } + } + }); + } + return _deprecatedTagValuesByKey; +}; diff --git a/modules/osm/entity.js b/modules/osm/entity.js index 9b4c4575c..d19cb14ac 100644 --- a/modules/osm/entity.js +++ b/modules/osm/entity.js @@ -54,29 +54,6 @@ osmEntity.key = function(entity) { return entity.id + 'v' + (entity.v || 0); }; -var _deprecatedTagValuesByKey; - -osmEntity.deprecatedTagValuesByKey = function(dataDeprecated) { - if (!_deprecatedTagValuesByKey) { - _deprecatedTagValuesByKey = {}; - dataDeprecated.forEach(function(d) { - var oldKeys = Object.keys(d.old); - if (oldKeys.length === 1) { - var oldKey = oldKeys[0]; - var oldValue = d.old[oldKey]; - if (oldValue !== '*') { - if (!_deprecatedTagValuesByKey[oldKey]) { - _deprecatedTagValuesByKey[oldKey] = [oldValue]; - } else { - _deprecatedTagValuesByKey[oldKey].push(oldValue); - } - } - } - }); - } - return _deprecatedTagValuesByKey; -}; - osmEntity.prototype = { @@ -185,56 +162,4 @@ osmEntity.prototype = { isDegenerate: function() { return true; }, - - deprecatedTags: function(dataDeprecated) { - var tags = this.tags; - - // if there are no tags, none can be deprecated - if (Object.keys(tags).length === 0) return []; - - var deprecated = []; - dataDeprecated.forEach(function(d) { - var oldKeys = Object.keys(d.old); - if (d.replace) { - var hasExistingValues = Object.keys(d.replace).some(function(replaceKey) { - if (!tags[replaceKey] || d.old[replaceKey]) return false; - var replaceValue = d.replace[replaceKey]; - if (replaceValue === '*') return false; - if (replaceValue === tags[replaceKey]) return false; - return true; - }); - // don't flag deprecated tags if the upgrade path would overwrite existing data - #7843 - if (hasExistingValues) return; - } - var matchesDeprecatedTags = oldKeys.every(function(oldKey) { - if (!tags[oldKey]) return false; - if (d.old[oldKey] === '*') return true; - if (d.old[oldKey] === tags[oldKey]) return true; - - var vals = tags[oldKey].split(';').filter(Boolean); - if (vals.length === 0) { - return false; - } else if (vals.length > 1) { - return vals.indexOf(d.old[oldKey]) !== -1; - } else { - if (tags[oldKey] === d.old[oldKey]) { - if (d.replace && d.old[oldKey] === d.replace[oldKey]) { - var replaceKeys = Object.keys(d.replace); - return !replaceKeys.every(function(replaceKey) { - return tags[replaceKey] === d.replace[replaceKey]; - }); - } else { - return true; - } - } - } - return false; - }); - if (matchesDeprecatedTags) { - deprecated.push(d); - } - }); - - return deprecated; - } }; diff --git a/modules/ui/fields/combo.js b/modules/ui/fields/combo.js index 744a1249e..dcca9c5bf 100644 --- a/modules/ui/fields/combo.js +++ b/modules/ui/fields/combo.js @@ -4,7 +4,6 @@ import { drag as d3_drag } from 'd3-drag'; import * as countryCoder from '@rapideditor/country-coder'; import { fileFetcher } from '../../core/file_fetcher'; -import { osmEntity } from '../../osm/entity'; import { t } from '../../core/localizer'; import { services } from '../../services'; import { uiCombobox } from '../combobox'; @@ -13,6 +12,7 @@ import { svgIcon } from '../../svg/icon'; import { utilKeybinding } from '../../util/keybinding'; import { utilArrayUniq, utilGetSetValue, utilNoAuto, utilRebind, utilTotalExtent, utilUnicodeCharsCount } from '../../util'; import { uiLengthIndicator } from '../length_indicator'; +import { deprecatedTagValuesByKey } from '../../osm/deprecated'; export { uiFieldCombo as uiFieldManyCombo, @@ -258,7 +258,7 @@ export function uiFieldCombo(field, context) { return value === restrictTagValueSpelling(value); }); - var deprecatedValues = osmEntity.deprecatedTagValuesByKey(_dataDeprecated)[field.key]; + var deprecatedValues = deprecatedTagValuesByKey(_dataDeprecated)[field.key]; if (deprecatedValues) { // don't suggest deprecated tag values data = data.filter(d => diff --git a/modules/validations/outdated_tags.js b/modules/validations/outdated_tags.js index f1190e194..0e6be3499 100644 --- a/modules/validations/outdated_tags.js +++ b/modules/validations/outdated_tags.js @@ -9,6 +9,7 @@ import { services } from '../services'; import { utilHashcode, utilTagDiff } from '../util'; import { utilDisplayLabel } from '../util/utilDisplayLabel'; import { validationIssue, validationIssueFix } from '../core/validation'; +import { getDeprecatedTags } from '../osm/deprecated'; /** @import { TagDiff } from '../util/util'. */ @@ -43,7 +44,7 @@ export function validationOutdatedTags() { // Upgrade deprecated tags.. if (_dataDeprecated) { - const deprecatedTags = entity.deprecatedTags(_dataDeprecated); + const deprecatedTags = getDeprecatedTags(entity.tags, _dataDeprecated); if (entity.type === 'way' && entity.isClosed() && entity.tags.traffic_calming === 'island' && !entity.tags.highway) { // https://github.com/openstreetmap/id-tagging-schema/issues/1162#issuecomment-2000356902 diff --git a/test/spec/osm/deprecated.ts b/test/spec/osm/deprecated.ts new file mode 100644 index 000000000..ec7d19662 --- /dev/null +++ b/test/spec/osm/deprecated.ts @@ -0,0 +1,98 @@ +import { + deprecatedTagValuesByKey, + getDeprecatedTags, + type DataDeprecated, +} from '../../../modules/osm/deprecated'; + +var deprecated: DataDeprecated = [ + { old: { highway: 'no' } }, + { old: { amenity: 'toilet' }, replace: { amenity: 'toilets' } }, + { old: { speedlimit: '*' }, replace: { maxspeed: '$1' } }, + { + old: { man_made: 'water_tank' }, + replace: { man_made: 'storage_tank', content: 'water' }, + }, + { + old: { amenity: 'gambling', gambling: 'casino' }, + replace: { amenity: 'casino' }, + }, +]; + +describe('getDeprecatedTags', () => { + it('returns none if entity has no tags', () => { + expect(getDeprecatedTags({}, deprecated)).toStrictEqual([]); + }); + + it('returns none when no tags are deprecated', () => { + expect(getDeprecatedTags({ amenity: 'toilets' }, deprecated)).toStrictEqual( + [], + ); + }); + + it('returns 1:0 replacement', () => { + expect(getDeprecatedTags({ highway: 'no' }, deprecated)).toStrictEqual([ + { old: { highway: 'no' } }, + ]); + }); + + it('returns 1:1 replacement', () => { + expect(getDeprecatedTags({ amenity: 'toilet' }, deprecated)).toStrictEqual([ + { old: { amenity: 'toilet' }, replace: { amenity: 'toilets' } }, + ]); + }); + + it('returns 1:1 wildcard', () => { + expect(getDeprecatedTags({ speedlimit: '50' }, deprecated)).toStrictEqual([ + { old: { speedlimit: '*' }, replace: { maxspeed: '$1' } }, + ]); + }); + + it('returns 1:2 total replacement', () => { + expect( + getDeprecatedTags({ man_made: 'water_tank' }, deprecated), + ).toStrictEqual([ + { + old: { man_made: 'water_tank' }, + replace: { man_made: 'storage_tank', content: 'water' }, + }, + ]); + }); + + it('returns 1:2 partial replacement', () => { + expect( + getDeprecatedTags( + { man_made: 'water_tank', content: 'water' }, + deprecated, + ), + ).toStrictEqual([ + { + old: { man_made: 'water_tank' }, + replace: { man_made: 'storage_tank', content: 'water' }, + }, + ]); + }); + + it('returns 2:1 replacement', () => { + expect( + getDeprecatedTags( + { amenity: 'gambling', gambling: 'casino' }, + deprecated, + ), + ).toStrictEqual([ + { + old: { amenity: 'gambling', gambling: 'casino' }, + replace: { amenity: 'casino' }, + }, + ]); + }); +}); + +describe('deprecatedTagValuesByKey', () => { + it('groups simple deprecations by key', () => { + expect(deprecatedTagValuesByKey(deprecated)).toStrictEqual({ + amenity: ['toilet'], // `gambling` not included + highway: ['no'], + man_made: ['water_tank'], + }); + }); +}); diff --git a/test/spec/osm/entity.js b/test/spec/osm/entity.js index 878815a72..b6b68d26b 100644 --- a/test/spec/osm/entity.js +++ b/test/spec/osm/entity.js @@ -226,60 +226,6 @@ describe('iD.osmEntity', function () { }); }); - describe('#deprecatedTags', function () { - var deprecated = [ - { old: { highway: 'no' } }, - { old: { amenity: 'toilet' }, replace: { amenity: 'toilets' } }, - { old: { speedlimit: '*' }, replace: { maxspeed: '$1' } }, - { old: { man_made: 'water_tank' }, replace: { man_made: 'storage_tank', content: 'water' } }, - { old: { amenity: 'gambling', gambling: 'casino' }, replace: { amenity: 'casino' } } - ]; - - it('returns none if entity has no tags', function () { - expect(iD.osmEntity().deprecatedTags(deprecated)).to.eql([]); - }); - - it('returns none when no tags are deprecated', function () { - expect(iD.osmEntity({ tags: { amenity: 'toilets' } }).deprecatedTags(deprecated)).to.eql([]); - }); - - it('returns 1:0 replacement', function () { - expect(iD.osmEntity({ tags: { highway: 'no' } }).deprecatedTags(deprecated)).to.eql( - [{ old: { highway: 'no' } }] - ); - }); - - it('returns 1:1 replacement', function () { - expect(iD.osmEntity({ tags: { amenity: 'toilet' } }).deprecatedTags(deprecated)).to.eql( - [{ old: { amenity: 'toilet' }, replace: { amenity: 'toilets' } }] - ); - }); - - it('returns 1:1 wildcard', function () { - expect(iD.osmEntity({ tags: { speedlimit: '50' } }).deprecatedTags(deprecated)).to.eql( - [{ old: { speedlimit: '*' }, replace: { maxspeed: '$1' } }] - ); - }); - - it('returns 1:2 total replacement', function () { - expect(iD.osmEntity({ tags: { man_made: 'water_tank' } }).deprecatedTags(deprecated)).to.eql( - [{ old: { man_made: 'water_tank' }, replace: { man_made: 'storage_tank', content: 'water' } }] - ); - }); - - it('returns 1:2 partial replacement', function () { - expect(iD.osmEntity({ tags: { man_made: 'water_tank', content: 'water' } }).deprecatedTags(deprecated)).to.eql( - [{ old: { man_made: 'water_tank' }, replace: { man_made: 'storage_tank', content: 'water' } }] - ); - }); - - it('returns 2:1 replacement', function () { - expect(iD.osmEntity({ tags: { amenity: 'gambling', gambling: 'casino' } }).deprecatedTags(deprecated)).to.eql( - [{ old: { amenity: 'gambling', gambling: 'casino' }, replace: { amenity: 'casino' } }] - ); - }); - }); - describe('#hasInterestingTags', function () { it('returns false if the entity has no tags', function () { expect(iD.osmEntity().hasInterestingTags()).to.equal(false);