diff --git a/data/core.yaml b/data/core.yaml index 1f04f2d41..027b8a8c6 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -149,6 +149,23 @@ en: has_wikidata_tag: single: This feature can't be deleted because it has a Wikidata tag. multiple: These features can't be deleted because some have Wikidata tags. + downgrade: + title: Downgrade + description: + building_address: Remove all non-address and non-building tags. + building: Remove all non-building tags. + address: Remove all non-address tags. + annotation: + building: + single: Downgraded a feature to a basic building. + multiple: "Downgraded {n} features to basic buildings." + address: + single: Downgraded a feature to an address. + multiple: "Downgraded {n} features to addresses." + multiple: "Downgraded {n} features." + has_wikidata_tag: + single: This feature can't be downgraded because it has a Wikidata tag. + multiple: These features can't be downgraded because some have Wikidata tags. add_member: annotation: Added a member to a relation. delete_member: diff --git a/dist/locales/en.json b/dist/locales/en.json index db576b2c0..1effc790d 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -191,6 +191,29 @@ "multiple": "These features can't be deleted because some have Wikidata tags." } }, + "downgrade": { + "title": "Downgrade", + "description": { + "building_address": "Remove all non-address and non-building tags.", + "building": "Remove all non-building tags.", + "address": "Remove all non-address tags." + }, + "annotation": { + "building": { + "single": "Downgraded a feature to a basic building.", + "multiple": "Downgraded {n} features to basic buildings." + }, + "address": { + "single": "Downgraded a feature to an address.", + "multiple": "Downgraded {n} features to addresses." + }, + "multiple": "Downgraded {n} features." + }, + "has_wikidata_tag": { + "single": "This feature can't be downgraded because it has a Wikidata tag.", + "multiple": "These features can't be downgraded because some have Wikidata tags." + } + }, "add_member": { "annotation": "Added a member to a relation." }, diff --git a/modules/modes/select.js b/modules/modes/select.js index 6b10d50c2..e38e8f761 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -241,15 +241,19 @@ export function modeSelect(context, selectedIDs) { var operations = Object.values(Operations) .map(function(o) { return o(selectedIDs, context); }) - .filter(function(o) { return o.available() && o.id !== 'delete'; }); + .filter(function(o) { return o.available() && o.id !== 'delete' && o.id !== 'downgrade'; }); + + var downgradeOperation = Operations.operationDowngrade(selectedIDs, context); + // don't allow delete if downgrade is available + var lastOperation = downgradeOperation.available() ? downgradeOperation : Operations.operationDelete(selectedIDs, context); // deprecation warning - Radial Menu to be removed in iD v3 var isRadialMenu = context.storage('edit-menu-style') === 'radial'; if (isRadialMenu) { operations = operations.slice(0,7); - operations.unshift(Operations.operationDelete(selectedIDs, context)); + operations.unshift(lastOperation); } else { - operations.push(Operations.operationDelete(selectedIDs, context)); + operations.push(lastOperation); } operations.forEach(function(operation) { diff --git a/modules/operations/downgrade.js b/modules/operations/downgrade.js new file mode 100644 index 000000000..13a69c49d --- /dev/null +++ b/modules/operations/downgrade.js @@ -0,0 +1,135 @@ +import { actionChangeTags } from '../actions'; +import { behaviorOperation } from '../behavior'; +import { modeSelect } from '../modes'; +import { t } from '../util/locale'; +import { uiCmd } from '../ui'; + + +export function operationDowngrade(selectedIDs, context) { + + var affectedFeatureCount = 0; + var downgradeType; + + setDowngradeTypeForEntityIDs(); + + var multi = affectedFeatureCount === 1 ? 'single' : 'multiple'; + + function setDowngradeTypeForEntityIDs() { + for (var i in selectedIDs) { + var entityID = selectedIDs[i]; + var type = downgradeTypeForEntityID(entityID); + if (type) { + affectedFeatureCount += 1; + if (downgradeType && type !== downgradeType) { + downgradeType = 'building_address'; + } else { + downgradeType = type; + } + } + } + } + + function downgradeTypeForEntityID(entityID) { + var graph = context.graph(); + var entity = graph.entity(entityID); + var preset = context.presets().match(entity, graph); + + if (preset.isFallback()) return null; + + if (entity.type === 'node' && + preset.id !== 'address' && + Object.keys(entity.tags).some(function(key) { + return key.match(/^addr:.{1,}/); + })) { + + return 'address'; + } + if (entity.geometry(graph) === 'area' && + entity.tags.building && + !preset.tags.building) { + + return 'building'; + } + + return null; + } + + var buildingKeysToKeep = ['architect', 'building', 'height', 'layer', 'source', 'type', 'wheelchair']; + var addressKeysToKeep = ['source']; + + var operation = function () { + context.perform(function(graph) { + + for (var i in selectedIDs) { + var entityID = selectedIDs[i]; + var type = downgradeTypeForEntityID(entityID); + if (!type) continue; + + var tags = Object.assign({}, graph.entity(entityID).tags); // shallow copy + for (var key in tags) { + if (type === 'address' && addressKeysToKeep.indexOf(key) !== -1) continue; + if (type === 'building') { + if (buildingKeysToKeep.indexOf(key) !== -1 || + key.match(/^building:.{1,}/) || + key.match(/^roof:.{1,}/)) continue; + } + // keep address tags for buildings too + if (key.match(/^addr:.{1,}/)) continue; + + delete tags[key]; + } + graph = actionChangeTags(entityID, tags)(graph); + } + return graph; + }, operation.annotation()); + + // refresh the select mode to enable the delete operation + context.enter(modeSelect(context, selectedIDs)); + }; + + + operation.available = function () { + return downgradeType; + }; + + + operation.disabled = function () { + var reason; + if (selectedIDs.some(hasWikidataTag)) { + reason = 'has_wikidata_tag'; + } + function hasWikidataTag(id) { + var entity = context.entity(id); + return entity.tags.wikidata && entity.tags.wikidata.trim().length > 0; + } + return reason; + }; + + + operation.tooltip = function () { + var disable = operation.disabled(); + return disable ? + t('operations.downgrade.' + disable + '.' + multi) : + t('operations.downgrade.description.' + downgradeType); + }; + + + operation.annotation = function () { + var suffix; + if (downgradeType === 'building_address') { + suffix = 'multiple'; + } else { + suffix = downgradeType + '.' + multi; + } + return t('operations.downgrade.annotation.' + suffix, { n: affectedFeatureCount}); + }; + + + operation.id = 'downgrade'; + operation.keys = [uiCmd('⌘⌫'), uiCmd('⌘⌦'), uiCmd('⌦')]; + operation.title = t('operations.downgrade.title'); + operation.behavior = behaviorOperation(context).which(operation); + + + return operation; +} diff --git a/modules/operations/index.js b/modules/operations/index.js index 8339d8205..d5724aa28 100644 --- a/modules/operations/index.js +++ b/modules/operations/index.js @@ -2,6 +2,7 @@ export { operationCircularize } from './circularize'; export { operationContinue } from './continue'; export { operationDelete } from './delete'; export { operationDisconnect } from './disconnect'; +export { operationDowngrade } from './downgrade'; export { operationMerge } from './merge'; export { operationMove } from './move'; export { operationOrthogonalize } from './orthogonalize'; diff --git a/svg/iD-sprite/operations/operation-downgrade.svg b/svg/iD-sprite/operations/operation-downgrade.svg new file mode 100644 index 000000000..544c4f6a9 --- /dev/null +++ b/svg/iD-sprite/operations/operation-downgrade.svg @@ -0,0 +1,5 @@ + + + + +