mirror of
https://github.com/FoggedLens/iD.git
synced 2026-03-07 11:51:35 +00:00
Block creation of invalid geometries due to connecting to invalid sibling segments or nodes on the drawn way on touchscreens
476 lines
14 KiB
JavaScript
476 lines
14 KiB
JavaScript
import { dispatch as d3_dispatch } from 'd3-dispatch';
|
|
|
|
import {
|
|
event as d3_event,
|
|
select as d3_select
|
|
} from 'd3-selection';
|
|
|
|
import { presetManager } from '../presets';
|
|
import { t } from '../core/localizer';
|
|
import { actionAddMidpoint } from '../actions/add_midpoint';
|
|
import { actionMoveNode } from '../actions/move_node';
|
|
import { actionNoop } from '../actions/noop';
|
|
import { behaviorDraw } from './draw';
|
|
import { geoChooseEdge, geoHasSelfIntersections } from '../geo';
|
|
import { modeBrowse } from '../modes/browse';
|
|
import { modeSelect } from '../modes/select';
|
|
import { osmNode } from '../osm/node';
|
|
import { utilRebind } from '../util/rebind';
|
|
import { utilKeybinding } from '../util';
|
|
|
|
export function behaviorDrawWay(context, wayID, mode, startGraph) {
|
|
|
|
var dispatch = d3_dispatch('rejectedSelfIntersection');
|
|
|
|
var behavior = behaviorDraw(context);
|
|
|
|
// Must be set by `drawWay.nodeIndex` before each install of this behavior.
|
|
var _nodeIndex;
|
|
|
|
var _origWay;
|
|
var _wayGeometry;
|
|
var _headNodeID;
|
|
var _annotation;
|
|
|
|
var _pointerHasMoved = false;
|
|
|
|
// The osmNode to be placed.
|
|
// This is temporary and just follows the mouse cursor until an "add" event occurs.
|
|
var _drawNode;
|
|
|
|
var _didResolveTempEdit = false;
|
|
|
|
function createDrawNode(loc) {
|
|
// don't make the draw node until we actually need it
|
|
_drawNode = osmNode({ loc: loc });
|
|
|
|
context.pauseChangeDispatch();
|
|
context.replace(function actionAddDrawNode(graph) {
|
|
// add the draw node to the graph and insert it into the way
|
|
var way = graph.entity(wayID);
|
|
return graph
|
|
.replace(_drawNode)
|
|
.replace(way.addNode(_drawNode.id, _nodeIndex));
|
|
}, _annotation);
|
|
context.resumeChangeDispatch();
|
|
|
|
setActiveElements();
|
|
}
|
|
|
|
function removeDrawNode() {
|
|
|
|
context.pauseChangeDispatch();
|
|
context.replace(
|
|
function actionDeleteDrawNode(graph) {
|
|
var way = graph.entity(wayID);
|
|
return graph
|
|
.replace(way.removeNode(_drawNode.id))
|
|
.remove(_drawNode);
|
|
},
|
|
_annotation
|
|
);
|
|
_drawNode = undefined;
|
|
context.resumeChangeDispatch();
|
|
}
|
|
|
|
|
|
function keydown() {
|
|
if (d3_event.keyCode === utilKeybinding.modifierCodes.alt) {
|
|
if (context.surface().classed('nope')) {
|
|
context.surface()
|
|
.classed('nope-suppressed', true);
|
|
}
|
|
context.surface()
|
|
.classed('nope', false)
|
|
.classed('nope-disabled', true);
|
|
}
|
|
}
|
|
|
|
|
|
function keyup() {
|
|
if (d3_event.keyCode === utilKeybinding.modifierCodes.alt) {
|
|
if (context.surface().classed('nope-suppressed')) {
|
|
context.surface()
|
|
.classed('nope', true);
|
|
}
|
|
context.surface()
|
|
.classed('nope-suppressed', false)
|
|
.classed('nope-disabled', false);
|
|
}
|
|
}
|
|
|
|
|
|
function allowsVertex(d) {
|
|
return d.geometry(context.graph()) === 'vertex' || presetManager.allowsVertex(d, context.graph());
|
|
}
|
|
|
|
|
|
// related code
|
|
// - `mode/drag_node.js` `doMove()`
|
|
// - `behavior/draw.js` `click()`
|
|
// - `behavior/draw_way.js` `move()`
|
|
function move(datum) {
|
|
|
|
var loc = context.map().mouseCoordinates();
|
|
|
|
if (!_drawNode) createDrawNode(loc);
|
|
|
|
context.surface().classed('nope-disabled', d3_event.altKey);
|
|
|
|
var targetLoc = datum && datum.properties && datum.properties.entity &&
|
|
allowsVertex(datum.properties.entity) && datum.properties.entity.loc;
|
|
var targetNodes = datum && datum.properties && datum.properties.nodes;
|
|
|
|
if (targetLoc) { // snap to node/vertex - a point target with `.loc`
|
|
loc = targetLoc;
|
|
|
|
} else if (targetNodes) { // snap to way - a line target with `.nodes`
|
|
var choice = geoChooseEdge(targetNodes, context.map().mouse(), context.projection, _drawNode.id);
|
|
if (choice) {
|
|
loc = choice.loc;
|
|
}
|
|
}
|
|
|
|
context.replace(actionMoveNode(_drawNode.id, loc), _annotation);
|
|
_drawNode = context.entity(_drawNode.id);
|
|
checkGeometry(true /* includeDrawNode */);
|
|
}
|
|
|
|
|
|
// Check whether this edit causes the geometry to break.
|
|
// If so, class the surface with a nope cursor.
|
|
// `includeDrawNode` - Only check the relevant line segments if finishing drawing
|
|
function checkGeometry(includeDrawNode) {
|
|
var nopeDisabled = context.surface().classed('nope-disabled');
|
|
var isInvalid = isInvalidGeometry(includeDrawNode);
|
|
|
|
if (nopeDisabled) {
|
|
context.surface()
|
|
.classed('nope', false)
|
|
.classed('nope-suppressed', isInvalid);
|
|
} else {
|
|
context.surface()
|
|
.classed('nope', isInvalid)
|
|
.classed('nope-suppressed', false);
|
|
}
|
|
}
|
|
|
|
|
|
function isInvalidGeometry(includeDrawNode) {
|
|
|
|
var testNode = _drawNode;
|
|
|
|
// we only need to test the single way we're drawing
|
|
var parentWay = context.graph().entity(wayID);
|
|
var nodes = context.graph().childNodes(parentWay).slice(); // shallow copy
|
|
|
|
if (includeDrawNode) {
|
|
if (parentWay.isClosed()) {
|
|
// don't test the last segment for closed ways - #4655
|
|
// (still test the first segement)
|
|
nodes.pop();
|
|
}
|
|
} else { // discount the draw node
|
|
|
|
if (parentWay.isClosed()) {
|
|
if (nodes.length < 3) return false;
|
|
if (_drawNode) nodes.splice(-2, 1);
|
|
testNode = nodes[nodes.length - 2];
|
|
} else {
|
|
// there's nothing we need to test if we ignore the draw node on open ways
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return testNode && geoHasSelfIntersections(nodes, testNode.id);
|
|
}
|
|
|
|
|
|
function undone() {
|
|
|
|
// undoing removed the temp edit
|
|
_didResolveTempEdit = true;
|
|
|
|
context.pauseChangeDispatch();
|
|
|
|
var nextMode;
|
|
|
|
if (context.graph() === startGraph) { // we've undone back to the beginning
|
|
nextMode = modeSelect(context, [wayID]);
|
|
} else {
|
|
context.history()
|
|
.on('undone.draw', null);
|
|
// remove whatever segment was drawn previously
|
|
context.undo();
|
|
|
|
if (context.graph() === startGraph) { // we've undone back to the beginning
|
|
nextMode = modeSelect(context, [wayID]);
|
|
} else {
|
|
// continue drawing
|
|
nextMode = mode;
|
|
}
|
|
}
|
|
|
|
// clear the redo stack by adding and removing an edit
|
|
context.perform(actionNoop());
|
|
context.pop(1);
|
|
|
|
context.resumeChangeDispatch();
|
|
context.enter(nextMode);
|
|
}
|
|
|
|
|
|
function setActiveElements() {
|
|
if (!_drawNode) return;
|
|
|
|
context.surface().selectAll('.' + _drawNode.id)
|
|
.classed('active', true);
|
|
}
|
|
|
|
|
|
function resetToStartGraph() {
|
|
while (context.graph() !== startGraph) {
|
|
context.pop();
|
|
}
|
|
}
|
|
|
|
|
|
var drawWay = function(surface) {
|
|
_drawNode = undefined;
|
|
_didResolveTempEdit = false;
|
|
_origWay = context.entity(wayID);
|
|
_headNodeID = typeof _nodeIndex === 'number' ? _origWay.nodes[_nodeIndex] :
|
|
(_origWay.isClosed() ? _origWay.nodes[_origWay.nodes.length - 2] : _origWay.nodes[_origWay.nodes.length - 1]);
|
|
_wayGeometry = _origWay.geometry(context.graph());
|
|
_annotation = t((_origWay.isDegenerate() ?
|
|
'operations.start.annotation.' :
|
|
'operations.continue.annotation.') + _wayGeometry
|
|
);
|
|
_pointerHasMoved = false;
|
|
|
|
// Push an annotated state for undo to return back to.
|
|
// We must make sure to replace or remove it later.
|
|
context.pauseChangeDispatch();
|
|
context.perform(actionNoop(), _annotation);
|
|
context.resumeChangeDispatch();
|
|
|
|
behavior.hover()
|
|
.initialNodeID(_headNodeID);
|
|
|
|
behavior
|
|
.on('move', function() {
|
|
_pointerHasMoved = true;
|
|
move.apply(this, arguments);
|
|
})
|
|
.on('down', function() {
|
|
move.apply(this, arguments);
|
|
})
|
|
.on('downcancel', function() {
|
|
if (_drawNode) removeDrawNode();
|
|
})
|
|
.on('click', drawWay.add)
|
|
.on('clickWay', drawWay.addWay)
|
|
.on('clickNode', drawWay.addNode)
|
|
.on('undo', context.undo)
|
|
.on('cancel', drawWay.cancel)
|
|
.on('finish', drawWay.finish);
|
|
|
|
d3_select(window)
|
|
.on('keydown.drawWay', keydown)
|
|
.on('keyup.drawWay', keyup);
|
|
|
|
context.map()
|
|
.dblclickZoomEnable(false)
|
|
.on('drawn.draw', setActiveElements);
|
|
|
|
setActiveElements();
|
|
|
|
surface.call(behavior);
|
|
|
|
context.history()
|
|
.on('undone.draw', undone);
|
|
};
|
|
|
|
|
|
drawWay.off = function(surface) {
|
|
|
|
if (!_didResolveTempEdit) {
|
|
// Drawing was interrupted unexpectedly.
|
|
// This can happen if the user changes modes,
|
|
// clicks geolocate button, a hashchange event occurs, etc.
|
|
|
|
context.pauseChangeDispatch();
|
|
resetToStartGraph();
|
|
context.resumeChangeDispatch();
|
|
}
|
|
|
|
_drawNode = undefined;
|
|
_nodeIndex = undefined;
|
|
|
|
context.map()
|
|
.on('drawn.draw', null);
|
|
|
|
surface.call(behavior.off)
|
|
.selectAll('.active')
|
|
.classed('active', false);
|
|
|
|
surface
|
|
.classed('nope', false)
|
|
.classed('nope-suppressed', false)
|
|
.classed('nope-disabled', false);
|
|
|
|
d3_select(window)
|
|
.on('keydown.drawWay', null)
|
|
.on('keyup.drawWay', null);
|
|
|
|
context.history()
|
|
.on('undone.draw', null);
|
|
};
|
|
|
|
|
|
function attemptAdd(d, loc, doAdd) {
|
|
|
|
if (_drawNode) {
|
|
// move the node to the final loc in case move wasn't called
|
|
// consistently (e.g. on touch devices)
|
|
context.replace(actionMoveNode(_drawNode.id, loc), _annotation);
|
|
_drawNode = context.entity(_drawNode.id);
|
|
} else {
|
|
createDrawNode(loc);
|
|
}
|
|
|
|
checkGeometry(true /* includeDrawNode */);
|
|
if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) {
|
|
if (!_pointerHasMoved) {
|
|
// prevent the temporary draw node from appearing on touch devices
|
|
removeDrawNode();
|
|
}
|
|
dispatch.call('rejectedSelfIntersection', this);
|
|
return; // can't click here
|
|
}
|
|
|
|
context.pauseChangeDispatch();
|
|
doAdd();
|
|
// we just replaced the temporary edit with the real one
|
|
_didResolveTempEdit = true;
|
|
context.resumeChangeDispatch();
|
|
|
|
context.enter(mode);
|
|
}
|
|
|
|
|
|
// Accept the current position of the drawing node
|
|
drawWay.add = function(loc, d) {
|
|
attemptAdd(d, loc, function() {
|
|
// don't need to do anything extra
|
|
});
|
|
};
|
|
|
|
|
|
// Connect the way to an existing way
|
|
drawWay.addWay = function(loc, edge, d) {
|
|
attemptAdd(d, loc, function() {
|
|
context.replace(
|
|
actionAddMidpoint({ loc: loc, edge: edge }, _drawNode),
|
|
_annotation
|
|
);
|
|
});
|
|
};
|
|
|
|
|
|
// Connect the way to an existing node
|
|
drawWay.addNode = function(node, d) {
|
|
|
|
// finish drawing if the mapper targets the prior node
|
|
if (node.id === _headNodeID ||
|
|
// or the first node when drawing an area
|
|
(_wayGeometry === 'area' && node.id === _origWay.first())) {
|
|
drawWay.finish();
|
|
return;
|
|
}
|
|
|
|
attemptAdd(d, node.loc, function() {
|
|
context.replace(
|
|
function actionReplaceDrawNode(graph) {
|
|
// remove the temporary draw node and insert the existing node
|
|
// at the same index
|
|
|
|
graph = graph
|
|
.replace(graph.entity(wayID).removeNode(_drawNode.id))
|
|
.remove(_drawNode);
|
|
return graph
|
|
.replace(graph.entity(wayID).addNode(node.id, _nodeIndex));
|
|
},
|
|
_annotation
|
|
);
|
|
});
|
|
};
|
|
|
|
|
|
// Finish the draw operation, removing the temporary edit.
|
|
// If the way has enough nodes to be valid, it's selected.
|
|
// Otherwise, delete everything and return to browse mode.
|
|
drawWay.finish = function() {
|
|
checkGeometry(false /* includeDrawNode */);
|
|
if (context.surface().classed('nope')) {
|
|
dispatch.call('rejectedSelfIntersection', this);
|
|
return; // can't click here
|
|
}
|
|
|
|
context.pauseChangeDispatch();
|
|
// remove the temporary edit
|
|
context.pop(1);
|
|
_didResolveTempEdit = true;
|
|
context.resumeChangeDispatch();
|
|
|
|
var way = context.hasEntity(wayID);
|
|
if (!way || way.isDegenerate()) {
|
|
drawWay.cancel();
|
|
return;
|
|
}
|
|
|
|
window.setTimeout(function() {
|
|
context.map().dblclickZoomEnable(true);
|
|
}, 1000);
|
|
|
|
var isNewFeature = !mode.isContinuing;
|
|
context.enter(modeSelect(context, [wayID]).newFeature(isNewFeature));
|
|
};
|
|
|
|
|
|
// Cancel the draw operation, delete everything, and return to browse mode.
|
|
drawWay.cancel = function() {
|
|
context.pauseChangeDispatch();
|
|
resetToStartGraph();
|
|
context.resumeChangeDispatch();
|
|
|
|
window.setTimeout(function() {
|
|
context.map().dblclickZoomEnable(true);
|
|
}, 1000);
|
|
|
|
context.surface()
|
|
.classed('nope', false)
|
|
.classed('nope-disabled', false)
|
|
.classed('nope-suppressed', false);
|
|
|
|
context.enter(modeBrowse(context));
|
|
};
|
|
|
|
|
|
drawWay.nodeIndex = function(val) {
|
|
if (!arguments.length) return _nodeIndex;
|
|
_nodeIndex = val;
|
|
return drawWay;
|
|
};
|
|
|
|
|
|
drawWay.activeID = function() {
|
|
if (!arguments.length) return _drawNode && _drawNode.id;
|
|
// no assign
|
|
return drawWay;
|
|
};
|
|
|
|
|
|
return utilRebind(drawWay, dispatch, 'on');
|
|
}
|