Files
iD/modules/behavior/draw_way.js
Bryan Housel f0a27bc1ec Simplify way segmentation and fix bug with adjacent segment type
(closes #4669)

Now instead of creating MultiLineString targets, we just create a bunch of
LineString targets.  This makes the code simpler, and anyway the entity is
still there in `properties` for drawing code to decide what to do with the target.

Incidentally, this change allows iD to support an extrusion operation.
(Because each way segment has its own unique GeoJSON target now)
2018-01-09 10:12:29 -05:00

351 lines
9.7 KiB
JavaScript

import { t } from '../util/locale';
import {
event as d3_event,
select as d3_select
} from 'd3-selection';
import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js';
import {
actionAddMidpoint,
actionMoveNode,
actionNoop
} from '../actions';
import { behaviorDraw } from './draw';
import { geoChooseEdge, geoHasSelfIntersections } from '../geo';
import { modeBrowse, modeSelect } from '../modes';
import { osmNode } from '../osm';
export function behaviorDrawWay(context, wayId, index, mode, startGraph) {
var origWay = context.entity(wayId);
var annotation = t((origWay.isDegenerate() ?
'operations.start.annotation.' :
'operations.continue.annotation.') + context.geometry(wayId)
);
var behavior = behaviorDraw(context);
var _tempEdits = 0;
var end = osmNode({ loc: context.map().mouseCoordinates() });
// Push an annotated state for undo to return back to.
// We must make sure to remove this edit later.
context.perform(actionNoop(), annotation);
_tempEdits++;
// Add the drawing node to the graph.
// We must make sure to remove this edit later.
context.perform(_actionAddDrawNode());
_tempEdits++;
function keydown() {
if (d3_event.keyCode === d3_keybinding.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 === d3_keybinding.modifierCodes.alt) {
if (context.surface().classed('nope-suppressed')) {
context.surface()
.classed('nope', true);
}
context.surface()
.classed('nope-suppressed', false)
.classed('nope-disabled', false);
}
}
// related code
// - `mode/drag_node.js` `doMode()`
// - `behavior/draw.js` `click()`
// - `behavior/draw_way.js` `move()`
function move(datum) {
context.surface().classed('nope-disabled', d3_event.altKey);
var targetLoc = datum && datum.properties && datum.properties.entity && datum.properties.entity.loc;
var targetNodes = datum && datum.properties && datum.properties.nodes;
var loc = context.map().mouseCoordinates();
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.mouse(), context.projection, end.id);
if (choice) {
loc = choice.loc;
}
}
context.replace(actionMoveNode(end.id, loc));
end = context.entity(end.id);
checkGeometry(origWay.isClosed()); // skipLast = true when drawing areas
}
// Check whether this edit causes the geometry to break.
// If so, class the surface with a nope cursor.
// `skipLast` - include closing segment in the check, see #4655
function checkGeometry(skipLast) {
var nopeDisabled = context.surface().classed('nope-disabled');
var isInvalid = isInvalidGeometry(end, context.graph(), skipLast);
if (nopeDisabled) {
context.surface()
.classed('nope', false)
.classed('nope-suppressed', isInvalid);
} else {
context.surface()
.classed('nope', isInvalid)
.classed('nope-suppressed', false);
}
}
function isInvalidGeometry(entity, graph, skipLast) {
var parents = graph.parentWays(entity);
for (var i = 0; i < parents.length; i++) {
var parent = parents[i];
var nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); });
if (skipLast) nodes.pop(); // disregard closing segment - #4655
if (geoHasSelfIntersections(nodes, entity.id)) {
return true;
}
}
return false;
}
function undone() {
// Undo popped the history back to the initial annotated no-op edit.
// Remove initial no-op edit and whatever edit happened immediately before it.
context.pop(2);
_tempEdits = 0;
if (context.hasEntity(wayId)) {
context.enter(mode);
} else {
context.enter(modeBrowse(context));
}
}
function setActiveElements() {
context.surface().selectAll('.' + end.id)
.classed('active', true);
}
var drawWay = function(surface) {
behavior
.on('move', move)
.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()
.dblclickEnable(false)
.on('drawn.draw', setActiveElements);
setActiveElements();
surface.call(behavior);
context.history()
.on('undone.draw', undone);
};
drawWay.off = function(surface) {
// Drawing was interrupted unexpectedly.
// This can happen if the user changes modes,
// clicks geolocate button, a hashchange event occurs, etc.
if (_tempEdits) {
context.pop(_tempEdits);
while (context.graph() !== startGraph) {
context.pop();
}
}
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.hover', null)
.on('keyup.hover', null);
context.history()
.on('undone.draw', null);
};
function _actionAddDrawNode() {
return function(graph) {
return graph
.replace(end)
.replace(origWay.addNode(end.id, index));
};
}
function _actionReplaceDrawNode(newNode) {
return function(graph) {
return graph
.replace(origWay.addNode(newNode.id, index))
.remove(end);
};
}
// Accept the current position of the drawing node and continue drawing.
drawWay.add = function(loc, d) {
if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) {
return; // can't click here
}
context.pop(_tempEdits);
_tempEdits = 0;
context.perform(
_actionAddDrawNode(),
annotation
);
checkGeometry(false); // skipLast = false
context.enter(mode);
};
// Connect the way to an existing way.
drawWay.addWay = function(loc, edge, d) {
if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) {
return; // can't click here
}
context.pop(_tempEdits);
_tempEdits = 0;
context.perform(
_actionAddDrawNode(),
actionAddMidpoint({ loc: loc, edge: edge }, end),
annotation
);
checkGeometry(false); // skipLast = false
context.enter(mode);
};
// Connect the way to an existing node and continue drawing.
drawWay.addNode = function(node, d) {
if ((d && d.properties && d.properties.nope) || context.surface().classed('nope')) {
return; // can't click here
}
context.pop(_tempEdits);
_tempEdits = 0;
context.perform(
_actionReplaceDrawNode(node),
annotation
);
checkGeometry(false); // skipLast = false
context.enter(mode);
};
// Finish the draw operation, removing the temporary edits.
// If the way has enough nodes to be valid, it's selected.
// Otherwise, delete everything and return to browse mode.
drawWay.finish = function() {
checkGeometry(true); // skipLast = true
if (context.surface().classed('nope')) {
return; // can't click here
}
context.pop(_tempEdits);
_tempEdits = 0;
var way = context.hasEntity(wayId);
if (!way || way.isDegenerate()) {
drawWay.cancel();
return;
}
window.setTimeout(function() {
context.map().dblclickEnable(true);
}, 1000);
context.enter(modeSelect(context, [wayId]).newFeature(true));
};
// Cancel the draw operation, delete everything, and return to browse mode.
drawWay.cancel = function() {
context.pop(_tempEdits);
_tempEdits = 0;
while (context.graph() !== startGraph) {
context.pop();
}
window.setTimeout(function() {
context.map().dblclickEnable(true);
}, 1000);
context.surface()
.classed('nope', false)
.classed('nope-disabled', false)
.classed('nope-suppressed', false);
context.enter(modeBrowse(context));
};
drawWay.activeID = function() {
if (!arguments.length) return end.id;
// no assign
return drawWay;
};
drawWay.tail = function(text) {
behavior.tail(text);
return drawWay;
};
return drawWay;
}