modules/behavior/draw_way.js (327 lines of code) (raw):
import {
event as d3_event,
select as d3_select
} from 'd3-selection';
import { t } from '../util/locale';
import { actionAddMidpoint } from '../actions/add_midpoint';
import { actionChangeTags } from '../actions/change_tags';
import { actionMoveNode } from '../actions/move_node';
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 { utilKeybinding } from '../util';
export function behaviorDrawWay(context, wayID, index, mode, startGraph, baselineGraph) {
var origWay = context.entity(wayID);
var annotation = t((origWay.isDegenerate() ?
'operations.start.annotation.' :
'operations.continue.annotation.') + context.geometry(wayID)
);
var behavior = behaviorDraw(context);
behavior.hover().initialNodeID(index ? origWay.nodes[index] :
(origWay.isClosed() ? origWay.nodes[origWay.nodes.length - 2] : origWay.nodes[origWay.nodes.length - 1]));
var end = osmNode({ loc: context.map().mouseCoordinates() });
// Add the drawing node to the graph.
// We must make sure to remove this edit later if drawing is canceled.
context.pauseChangeDispatch();
context.perform(_actionAddDrawNode(), annotation);
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' || context.presets().allowsVertex(d, context.graph());
}
// 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 && allowsVertex(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;
}
} else {
if (context.storage('line-segments') === 'orthogonal') {
var orthoLoc = orthogonalLoc(loc);
if (orthoLoc) loc = orthoLoc;
}
}
context.replace(actionMoveNode(end.id, loc), annotation);
end = context.entity(end.id);
checkGeometry(false);
}
function orthogonalLoc(mouseLoc) {
var way = context.hasEntity(wayID);
if (!way) return null;
if (way.nodes.length - 1 < (way.isArea() ? 3 : 2)) return null;
var node1, node2;
if (way.isArea() ? way.nodes[way.nodes.length - 2] === end.id : way.last() === end.id) {
var baselineNodeIndex = way.isClosed() ? way.nodes.length - 3 : way.nodes.length - 2;
node1 = context.hasEntity(way.nodes[baselineNodeIndex - 1]);
node2 = context.hasEntity(way.nodes[baselineNodeIndex]);
} else {
node1 = context.hasEntity(way.nodes[2]);
node2 = context.hasEntity(way.nodes[1]);
}
if (!node1 || !node2 ||
node1.loc === node2.loc) return null;
var projection = context.projection;
var pA = projection(node1.loc),
pB = projection(node2.loc),
p3 = projection(mouseLoc);
var xA = pA[0],
yA = pA[1],
xB = pB[0],
yB = pB[1],
x3 = p3[0],
y3 = p3[1];
var x1 = xB,
y1 = yB,
x2 = xB + 1,
y2;
if (xA === xB) {
y2 = y1;
} else {
var slope = (yB-yA)/(xB-xA);
var perpSlope = -1/slope;
var b = yB - perpSlope*xB;
y2 = perpSlope * x2 + b;
}
var k = ((y2-y1) * (x3-x1) - (x2-x1) * (y3-y1)) / (Math.pow(y2-y1, 2) + Math.pow(x2-x1, 2));
var x4 = x3 - k * (y2-y1);
var y4 = y3 + k * (x2-x1);
if (!isFinite(x4) || !isFinite(y4)) return null;
return projection.invert([x4, y4]);
}
// Check whether this edit causes the geometry to break.
// If so, class the surface with a nope cursor.
// `finishDraw` - Only checks the relevant line segments if finishing drawing
function checkGeometry(finishDraw) {
var nopeDisabled = context.surface().classed('nope-disabled');
var isInvalid = isInvalidGeometry(end, context.graph(), finishDraw);
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, finishDraw) {
var parents = graph.parentWays(entity);
for (var i = 0; i < parents.length; i++) {
var parent = parents[i];
var nodes = graph.childNodes(parent).slice(); // shallow copy
if (origWay.isClosed()) { // Check if Area
if (finishDraw) {
if (nodes.length < 3) return false;
nodes.splice(-2, 1);
entity = nodes[nodes.length-2];
} else {
nodes.pop();
}
} else { // Line
if (finishDraw) {
nodes.pop();
}
}
if (geoHasSelfIntersections(nodes, entity.id)) {
return true;
}
}
return false;
}
function undone() {
shouldResetOnOff = false;
context.pauseChangeDispatch();
if (context.graph() === baselineGraph || context.graph() === startGraph) { // We've undone back to the beginning
// baselineGraph may be behind startGraph if this way was added rather than continued
resetToStartGraph();
context.resumeChangeDispatch();
context.enter(modeSelect(context, [wayID]));
} else {
// Remove whatever segment was drawn previously
context.pop(1);
context.resumeChangeDispatch();
// continue drawing
context.enter(mode);
}
}
function setActiveElements() {
context.surface().selectAll('.' + end.id)
.classed('active', true);
}
function resetToStartGraph() {
while (context.graph() !== startGraph) {
context.pop();
}
}
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);
};
var shouldResetOnOff = true;
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 (shouldResetOnOff) {
context.pauseChangeDispatch();
resetToStartGraph();
context.resumeChangeDispatch();
}
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
}
if (mode.defaultNodeTags && Object.keys(mode.defaultNodeTags).length) {
context.replace(actionChangeTags(end.id, mode.defaultNodeTags), annotation);
}
shouldResetOnOff = false;
checkGeometry(false); // finishDraw = 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
}
shouldResetOnOff = false;
context.pauseChangeDispatch();
if (mode.defaultNodeTags && Object.keys(mode.defaultNodeTags).length) {
context.replace(actionChangeTags(end.id, mode.defaultNodeTags), annotation);
}
context.replace(
actionAddMidpoint({ loc: loc, edge: edge }, end),
annotation
);
context.resumeChangeDispatch();
checkGeometry(false); // finishDraw = 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
}
shouldResetOnOff = false;
context.pauseChangeDispatch();
context.replace(
_actionReplaceDrawNode(node),
annotation
);
context.resumeChangeDispatch();
checkGeometry(false); // finishDraw = 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() {
shouldResetOnOff = false;
checkGeometry(true); // finishDraw = true
if (context.surface().classed('nope')) {
return false; // can't click here
}
context.pauseChangeDispatch();
context.pop(1);
var way = context.hasEntity(wayID);
if (!way || way.isDegenerate()) {
drawWay.cancel();
return false;
}
context.resumeChangeDispatch();
window.setTimeout(function() {
context.map().dblclickEnable(true);
}, 1000);
mode.didFinishAdding();
return true;
};
// Cancel the draw operation, delete everything, and return to browse mode.
drawWay.cancel = function() {
shouldResetOnOff = false;
context.pauseChangeDispatch();
resetToStartGraph();
context.resumeChangeDispatch();
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;
}