diff --git a/data/core.yaml b/data/core.yaml index b317df32a..f06a69983 100644 --- a/data/core.yaml +++ b/data/core.yaml @@ -229,6 +229,12 @@ en: annotation: create: Added a turn restriction delete: Deleted a turn restriction + detachNode: + title: Detach + key: T + description: Detach this node from these lines/areas. + annotation: Detached a node from owning lines/areas. + via_restriction: "This can't be detached because it would damage a turn restriction." restriction: controls: distance: Distance diff --git a/dist/locales/en.json b/dist/locales/en.json index 8781f6338..8363a101f 100644 --- a/dist/locales/en.json +++ b/dist/locales/en.json @@ -297,6 +297,13 @@ "create": "Added a turn restriction", "delete": "Deleted a turn restriction" } + }, + "detachNode": { + "title": "Detach", + "key": "T", + "description": "Detach this node from these lines/areas.", + "annotation": "Detached a node from owning lines/areas.", + "via_restriction": "This can't be detached because it would damage a turn restriction." } }, "restriction": { diff --git a/modules/actions/detach_node.js b/modules/actions/detach_node.js new file mode 100644 index 000000000..7ff5cd8e3 --- /dev/null +++ b/modules/actions/detach_node.js @@ -0,0 +1,39 @@ +import { osmNode } from '../osm'; + +export function actionDetachNode(nodeId) { + return function (graph) { + // Get the point in question + var node = graph.entity(nodeId); + // Get all of the ways it's currently attached to + var parentWays = graph.parentWays(node); + // Create a new node to replace the one we will detach + var replacementNode = osmNode({ loc: node.loc }); + // We need to process each way in turn, updating the graph as we go + var updatedWaysGraph = parentWays + .reduce(function (accGraph, parentWay) { + // Make a note of where in the way our target node is inside this way + var originalIndex = parentWay.nodes.indexOf(nodeId); + // Swap out the target node for the replacement + var updatedWay = parentWay.removeNode(nodeId) // Remove our target node from the parent way + .addNode(replacementNode.id, originalIndex); // Add in the replacement node in its place + // Update the graph with the updated way and pass into the next cycle of the reduce operation + return accGraph.replace(updatedWay); + }, + // Seed the reduction with the input graph, updated to include the replacementNode so + // that is accessible to the ways when we add it in to them + graph.replace(replacementNode)); + // Process any relations too + var parentRels = updatedWaysGraph.parentRelations(node); + return parentRels + .reduce(function (accGraph, parentRel) { + // Move the relationship to the new node + var originalMember = parentRel.memberById(nodeId); + var newMember = { id: replacementNode.id, type: 'node', role: originalMember.role }; + // Remove & replace with the new member + var updatedRel = parentRel.removeMembersWithID(nodeId) + .addMember(newMember, originalMember.index); + // Update graph and pass into the next cycle of the reduce operation + return accGraph.replace(updatedRel); + }, updatedWaysGraph); + }; +} diff --git a/modules/actions/index.js b/modules/actions/index.js index 330690db2..4f15456d8 100644 --- a/modules/actions/index.js +++ b/modules/actions/index.js @@ -33,3 +33,4 @@ export { actionSplit } from './split'; export { actionStraighten } from './straighten'; export { actionUnrestrictTurn } from './unrestrict_turn'; export { actionReflect } from './reflect.js'; +export { actionDetachNode } from './detach_node'; \ No newline at end of file diff --git a/modules/operations/detach_node.js b/modules/operations/detach_node.js new file mode 100644 index 000000000..bb1f985cf --- /dev/null +++ b/modules/operations/detach_node.js @@ -0,0 +1,104 @@ +import { actionDetachNode } from '../actions/index'; +import { behaviorOperation } from '../behavior/index'; +import { modeMove } from '../modes/index'; +import { t } from '../util/locale'; +import _flatMap from 'lodash-es/flatMap'; +import _uniq from 'lodash-es/uniq'; + +export function operationDetachNode(selectedIDs, context) { + var selectedNode = selectedIDs[0]; + var operation = function () { + context.perform(actionDetachNode(selectedNode)); + context.enter(modeMove(context, [selectedNode], context.graph)); + }; + var hasTags = function (entity) { + return Object.keys(entity.tags).length > 0; + }; + operation.available = function () { + // Check multiple items aren't selected + if (selectedIDs.length !== 1) { + return false; + } + // Get the entity itself + var graph = context.graph(); + var entity = graph.hasEntity(selectedNode); + if (!entity) { + // This probably isn't possible + return false; + } + // Confirm entity is a node with tags + if (entity.type === 'node' && hasTags(entity)) { + // Confirm that the node is owned by at least 1 parent way + var parentWays = graph.parentWays(entity); + return parentWays && parentWays.length > 0; + } + // Not appropriate + return false; + }; + operation.disabled = function () { + return false; + }; + operation.tooltip = function () { + var disableReason = operation.disabled(); + return disableReason + ? t('operations.detachNode.' + disableReason) + : t('operations.detachNode.description'); + }; + operation.annotation = function () { + return t('operations.detachNode.annotation'); + }; + operation.id = 'detachNode'; + operation.keys = [t('operations.detachNode.key')]; + operation.title = t('operations.detachNode.title'); + operation.behavior = behaviorOperation(context).which(operation); + + operation.disabled = function () { + // We should prevent the node being detached if it represents a via/location_hint node of a turn restriction + var graph = context.graph(); + // Get nodes for the Ids (although there should only be one, we can handle multiple here) + var nodes = selectedIDs.map(function (i) { return graph.hasEntity(i); }) + .filter(isNotNullOrUndefined); + // Get all via nodes of restrictions involving the target nodes + var restrictionNodeIds = _flatMap(nodes, function (node) { + // Get the relations that this node belongs to + var relationsFromNode = graph.parentRelations(node); + // Check each relation in turn + return _flatMap(relationsFromNode, function (relation) { + // Check to see if this is a restriction relation, if not return null + if (!relation.isValidRestriction()) { + return null; + } + // We have identified that it is a restriction. + // https://wiki.openstreetmap.org/wiki/Relation:restriction indicates that + // from & to roles are only appropriate for Ways + // The via members can be either nodes or ways. Via-Ways do not prevent us removing a node + // from within them, as it is the way itself which is in the relation with the via role, + // and not the consitutent nodes (so if we switch out a constituent node, the way id + // does not change and therefore the relation will not be affected). Therefore we + // only need to examine the standalone nodes + return relation.members.filter(function (m) { + return (m.role === 'via' || m.role === 'location_hint') && m.type === 'node'; + }).map(function (m) { return m.id; }); + }); + }).filter(isNotNullOrUndefined); + + // Get unique list of ids in restrictionNodeIds to simplify checking + var nodeIds = _uniq(restrictionNodeIds); + + // Now we have a list of via/location_hint nodes, we should prevent detachment if the target node is in this list + var anyInhibits = nodes.filter(function (n) { + return nodeIds.indexOf(n.id) !== -1; + }); + if (anyInhibits.length > 0) { + // The node is a via/location_hint, do not permit + return 'via_restriction'; + } + // We are ok to proceed + return false; + }; + return operation; +} + +function isNotNullOrUndefined(i) { + return i !== undefined && i !== null; +} \ No newline at end of file diff --git a/modules/operations/index.js b/modules/operations/index.js index 1ad24cd97..8339d8205 100644 --- a/modules/operations/index.js +++ b/modules/operations/index.js @@ -10,3 +10,4 @@ export { operationReverse } from './reverse'; export { operationRotate } from './rotate'; export { operationSplit } from './split'; export { operationStraighten } from './straighten'; +export { operationDetachNode } from './detach_node'; diff --git a/modules/util/index.js b/modules/util/index.js index 4d4c53269..bc06a0a41 100644 --- a/modules/util/index.js +++ b/modules/util/index.js @@ -12,7 +12,7 @@ export { utilFunctor } from './util'; export { utilGetAllNodes } from './util'; export { utilGetPrototypeOf } from './util'; export { utilGetSetValue } from './get_set_value'; -export { utilIdleWorker} from './idle_worker'; +export { utilIdleWorker } from './idle_worker'; export { utilNoAuto } from './util'; export { utilPrefixCSSProperty } from './util'; export { utilPrefixDOMProperty } from './util'; @@ -25,4 +25,4 @@ export { utilSuggestNames } from './suggest_names'; export { utilTagText } from './util'; export { utilTiler } from './tiler'; export { utilTriggerEvent } from './trigger_event'; -export { utilWrap } from './util'; +export { utilWrap } from './util'; \ No newline at end of file diff --git a/svg/iD-sprite/operations/operation-detachNode.svg b/svg/iD-sprite/operations/operation-detachNode.svg new file mode 100644 index 000000000..504acc2cb --- /dev/null +++ b/svg/iD-sprite/operations/operation-detachNode.svg @@ -0,0 +1,9 @@ + + + diff --git a/test/index.html b/test/index.html index b094f4b32..3f03cdda0 100644 --- a/test/index.html +++ b/test/index.html @@ -1,5 +1,6 @@ +