diff --git a/css/55_cursors.css b/css/55_cursors.css index 0e4732b58..854e2f88b 100644 --- a/css/55_cursors.css +++ b/css/55_cursors.css @@ -1,6 +1,7 @@ /* Cursors */ -.nope { +.nope, +.nope * { cursor: not-allowed !important; } diff --git a/modules/behavior/draw_way.js b/modules/behavior/draw_way.js index 18e270c7e..4a49856e9 100644 --- a/modules/behavior/draw_way.js +++ b/modules/behavior/draw_way.js @@ -7,7 +7,7 @@ import { } from '../actions'; import { behaviorDraw } from './draw'; -import { geoChooseEdge } from '../geo'; +import { geoChooseEdge, geoHasSelfIntersections } from '../geo'; import { modeBrowse, modeSelect } from '../modes'; import { osmNode } from '../osm'; @@ -39,25 +39,48 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // - `behavior/draw.js` `click()` // - `behavior/draw_way.js` `move()` function move(datum) { - var loc; - var target = datum && datum.id && context.hasEntity(datum.id); + var nodeGroups = datum && datum.properties && datum.properties.nodes; + var loc = context.map().mouseCoordinates(); + if (datum.loc) { // snap to node/vertex - a real entity or a nope target with a `loc` loc = datum.loc; - } else if (target && target.type === 'way') { // snap to way - var choice = geoChooseEdge( - context.childNodes(target), context.mouse(), context.projection, end.id - ); - if (choice) { - loc = choice.loc; - } - } - if (!loc) { - loc = context.map().mouseCoordinates(); + } else if (nodeGroups) { // snap to way - a line touch target or nope target with nodes + var best = Infinity; + for (var i = 0; i < nodeGroups.length; i++) { + var childNodes = nodeGroups[i].map(function(id) { return context.entity(id); }); + var choice = geoChooseEdge(childNodes, context.mouse(), context.projection, end.id); + if (choice && choice.distance < best) { + best = choice.distance; + loc = choice.loc; + } + } } context.replace(actionMoveNode(end.id, loc)); end = context.entity(end.id); + + // check if this movement causes the geometry to break + var doBlock = invalidGeometry(end, context.graph()); + context.surface() + .classed('nope', doBlock); + } + + + function invalidGeometry(entity, graph) { + 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 (parent.isClosed()) { + if (geoHasSelfIntersections(nodes, entity.id)) { + return true; + } + } + } + + return false; } @@ -147,7 +170,10 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Accept the current position of the drawing node and continue drawing. drawWay.add = function(loc, datum) { - if (datum && datum.id && /-nope/.test(datum.id)) return; // can't click here + if ((datum && datum.id && /-nope$/.test(datum.id)) || + context.surface().classed('nope')) { + return; // can't click here + } context.pop(_tempEdits); _tempEdits = 0; @@ -163,6 +189,10 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Connect the way to an existing way. drawWay.addWay = function(loc, edge) { + if (context.surface().classed('nope')) { + return; // can't click here + } + context.pop(_tempEdits); _tempEdits = 0; @@ -178,6 +208,10 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // Connect the way to an existing node and continue drawing. drawWay.addNode = function(node) { + if (context.surface().classed('nope')) { + return; // can't click here + } + context.pop(_tempEdits); _tempEdits = 0; @@ -194,6 +228,10 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { // If the way has enough nodes to be valid, it's selected. // Otherwise, delete everything and return to browse mode. drawWay.finish = function() { + if (context.surface().classed('nope')) { + return; // can't click here + } + context.pop(_tempEdits); _tempEdits = 0; @@ -224,6 +262,9 @@ export function behaviorDrawWay(context, wayId, index, mode, startGraph) { context.map().dblclickEnable(true); }, 1000); + context.surface() + .classed('nope', false); + context.enter(modeBrowse(context)); }; diff --git a/modules/geo/geom.js b/modules/geo/geom.js index e8b37c6fe..dbef35800 100644 --- a/modules/geo/geom.js +++ b/modules/geo/geom.js @@ -5,6 +5,7 @@ import { geoVecAngle, geoVecCross, geoVecDot, + geoVecEqual, geoVecInterp, geoVecLength, geoVecSubtract @@ -38,7 +39,7 @@ export function geoRotate(points, angle, around) { // projection onto that edge, if such a projection exists, or the distance to // the closest vertex on that edge. Returns an object with the `index` of the // chosen edge, the chosen `loc` on that edge, and the `distance` to to it. -export function geoChooseEdge(nodes, point, projection, skipID) { +export function geoChooseEdge(nodes, point, projection, activeID) { var dist = geoVecLength; var points = nodes.map(function(n) { return projection(n.loc); }); var ids = nodes.map(function(n) { return n.id; }); @@ -47,7 +48,7 @@ export function geoChooseEdge(nodes, point, projection, skipID) { var loc; for (var i = 0; i < points.length - 1; i++) { - if (ids[i] === skipID || ids[i + 1] === skipID) continue; + if (ids[i] === activeID || ids[i + 1] === activeID) continue; var o = points[i]; var s = geoVecSubtract(points[i + 1], o); @@ -79,6 +80,41 @@ export function geoChooseEdge(nodes, point, projection, skipID) { } +// check active (dragged or drawing) segments against inactive segments +export function geoHasSelfIntersections(nodes, activeID) { + var actives = []; + var inactives = []; + var j, k; + + for (j = 0; j < nodes.length - 1; j++) { + var n1 = nodes[j]; + var n2 = nodes[j+1]; + var segment = [n1.loc, n2.loc]; + if (n1.id === activeID || n2.id === activeID) { + actives.push(segment); + } else { + inactives.push(segment); + } + } + + for (j = 0; j < actives.length; j++) { + for (k = 0; k < inactives.length; k++) { + var p = actives[j]; + var q = inactives[k]; + // skip if segments share an endpoint + if (geoVecEqual(p[1], q[0]) || geoVecEqual(p[0], q[1]) || + geoVecEqual(p[0], q[0]) || geoVecEqual(p[1], q[1]) ) { + continue; + } else if (geoLineIntersection(p, q)) { + return true; + } + } + } + + return false; +} + + // Return the intersection point of 2 line segments. // From https://github.com/pgkelley4/line-segments-intersect // This uses the vector cross product approach described below: diff --git a/modules/geo/index.js b/modules/geo/index.js index fe801aff6..982596f04 100644 --- a/modules/geo/index.js +++ b/modules/geo/index.js @@ -13,6 +13,7 @@ export { geoZoomToScale } from './geo.js'; export { geoAngle } from './geom.js'; export { geoChooseEdge } from './geom.js'; export { geoEdgeEqual } from './geom.js'; +export { geoHasSelfIntersections } from './geom.js'; export { geoRotate } from './geom.js'; export { geoLineIntersection } from './geom.js'; export { geoPathIntersections } from './geom.js'; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 3809a352f..4251fb7a8 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -20,8 +20,7 @@ import { import { geoChooseEdge, - geoLineIntersection, - geoVecEqual, + geoHasSelfIntersections, geoVecSubtract, geoViewportEdge } from '../geo'; @@ -133,7 +132,6 @@ export function modeDragNode(context) { var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc); var currMouse = geoVecSubtract(currPoint, nudge); var loc = context.projection.invert(currMouse); - var didSnap = false; if (!_nudgeInterval) { // If not nudging at the edge of the viewport, try to snap.. // related code @@ -141,21 +139,19 @@ export function modeDragNode(context) { // - `behavior/draw.js` `click()` // - `behavior/draw_way.js` `move()` var d = datum(); - var nodegroups = d && d.properties && d.properties.nodes; + var nodeGroups = d && d.properties && d.properties.nodes; if (d.loc) { // snap to node/vertex - a real entity or a nope target with a `loc` loc = d.loc; - didSnap = true; - } else if (nodegroups) { // snap to way - a line touch target or nope target with nodes + } else if (nodeGroups) { // snap to way - a line touch target or nope target with nodes var best = Infinity; - for (var i = 0; i < nodegroups.length; i++) { - var childNodes = nodegroups[i].map(function(id) { return context.entity(id); }); + for (var i = 0; i < nodeGroups.length; i++) { + var childNodes = nodeGroups[i].map(function(id) { return context.entity(id); }); var choice = geoChooseEdge(childNodes, context.mouse(), context.projection, entity.id); if (choice && choice.distance < best) { best = choice.distance; loc = choice.loc; - didSnap = true; } } } @@ -168,11 +164,7 @@ export function modeDragNode(context) { // check if this movement causes the geometry to break - var doBlock = false; - if (!didSnap) { - doBlock = invalidGeometry(entity, context.graph()); - } - + var doBlock = invalidGeometry(entity, context.graph()); context.surface() .classed('nope', doBlock); @@ -183,41 +175,11 @@ export function modeDragNode(context) { function invalidGeometry(entity, graph) { var parents = graph.parentWays(entity); - function hasSelfIntersections(way, activeID) { - // check active (dragged) segments against inactive segments - var actives = []; - var inactives = []; - var j, k; - for (j = 0; j < way.nodes.length - 1; j++) { - var n1 = graph.entity(way.nodes[j]); - var n2 = graph.entity(way.nodes[j+1]); - var segment = [n1.loc, n2.loc]; - if (n1.id === activeID || n2.id === activeID) { - actives.push(segment); - } else { - inactives.push(segment); - } - } - for (j = 0; j < actives.length; j++) { - for (k = 0; k < inactives.length; k++) { - var p = actives[j]; - var q = inactives[k]; - // skip if segments share an endpoint - if (geoVecEqual(p[1], q[0]) || geoVecEqual(p[0], q[1]) || - geoVecEqual(p[0], q[0]) || geoVecEqual(p[1], q[1]) ) { - continue; - } else if (geoLineIntersection(p, q)) { - return true; - } - } - } - return false; - } - for (var i = 0; i < parents.length; i++) { var parent = parents[i]; - if (parent.isClosed()) { // check for self intersections - if (hasSelfIntersections(parent, entity.id)) { + var nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); }); + if (parent.isClosed()) { + if (geoHasSelfIntersections(nodes, entity.id)) { return true; } }