From f58349864c37ae9fcd3ddb12b59d3be9b1be2744 Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Wed, 20 Dec 2017 13:52:16 -0500 Subject: [PATCH] Add support for nope targets, line sub-segment targeting --- css/20_map.css | 6 ++ modules/behavior/draw.js | 5 +- modules/behavior/hover.js | 16 ++-- modules/behavior/select.js | 22 ++--- modules/modes/drag_node.js | 3 +- modules/modes/select.js | 3 + modules/svg/lines.js | 163 +++++++++++++++++++++++++++++++++++-- modules/svg/path.js | 28 ++++--- modules/svg/vertices.js | 96 ++++++++++++++++++++-- 9 files changed, 293 insertions(+), 49 deletions(-) diff --git a/css/20_map.css b/css/20_map.css index 87fea9309..1004a3d1d 100644 --- a/css/20_map.css +++ b/css/20_map.css @@ -31,6 +31,12 @@ stroke: currentColor; } +/* `.target-nope` objects are explicitly forbidden to join to */ +.node.target.target-nope, +.way.target.target-nope { + cursor: not-allowed; +} + /* `.active` objects (currently being drawn or dragged) are not interactive */ /* This is important to allow the events to drop through to whatever is */ /* below them on the map, so you can still hover and connect to other things. */ diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index 4bce319dd..d2421ffc3 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -58,7 +58,10 @@ export function behaviorDraw(context) { // When drawing, connect only to things classed as targets.. // (this excludes area fills and active drawing elements) var selection = d3_select(element); - return (selection.classed('target') && element.__data__) || {}; + if (selection.classed('target')) return {}; + + var d = selection.datum(); + return (d && d.id && context.hasEntity(d.id)) || {}; } diff --git a/modules/behavior/hover.js b/modules/behavior/hover.js index b9c4bfa5c..1a496ee2d 100644 --- a/modules/behavior/hover.js +++ b/modules/behavior/hover.js @@ -6,7 +6,6 @@ import { } from 'd3-selection'; import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; -import { osmEntity } from '../osm/index'; import { utilRebind } from '../util/rebind'; @@ -107,19 +106,20 @@ export function behaviorHover(context) { _selection.selectAll('.hover-suppressed') .classed('hover-suppressed', false); - if (_target instanceof osmEntity && _target.id !== _newId) { + var entity = _target && _target.id && context.hasEntity(_target.id); + if (entity && entity.id !== _newId) { // If drawing a way, don't hover on a node that was just placed. #3974 var mode = context.mode() && context.mode().id; - if ((mode === 'draw-line' || mode === 'draw-area') && !_newId && _target.type === 'node') { - _newId = _target.id; + if ((mode === 'draw-line' || mode === 'draw-area') && !_newId && entity.type === 'node') { + _newId = entity.id; return; } - var selector = '.' + _target.id; + var selector = '.' + entity.id; - if (_target.type === 'relation') { - _target.members.forEach(function(member) { + if (entity.type === 'relation') { + entity.members.forEach(function(member) { selector += ', .' + member.id; }); } @@ -129,7 +129,7 @@ export function behaviorHover(context) { _selection.selectAll(selector) .classed(suppressed ? 'hover-suppressed' : 'hover', true); - dispatch.call('hover', this, !suppressed && _target.id); + dispatch.call('hover', this, !suppressed && entity.id); } else { dispatch.call('hover', this, null); diff --git a/modules/behavior/select.js b/modules/behavior/select.js index f3daf0bc2..a77044133 100644 --- a/modules/behavior/select.js +++ b/modules/behavior/select.js @@ -17,10 +17,10 @@ import { osmEntity } from '../osm'; export function behaviorSelect(context) { - var lastMouse = null, - suppressMenu = true, - tolerance = 4, - p1 = null; + var lastMouse = null; + var suppressMenu = true; + var tolerance = 4; + var p1 = null; function point() { @@ -102,19 +102,21 @@ export function behaviorSelect(context) { .on('mouseup.select', null, true); if (!p1) return; - var p2 = point(), - dist = geoEuclideanDistance(p1, p2); + var p2 = point(); + var dist = geoEuclideanDistance(p1, p2); p1 = null; if (dist > tolerance) { return; } - var isMultiselect = d3_event.shiftKey || d3_select('#surface .lasso').node(), - isShowAlways = +context.storage('edit-menu-show-always') === 1, - datum = d3_event.target.__data__ || (lastMouse && lastMouse.target.__data__), - mode = context.mode(); + var isMultiselect = d3_event.shiftKey || d3_select('#surface .lasso').node(); + var isShowAlways = +context.storage('edit-menu-show-always') === 1; + var datum = d3_event.target.__data__ || (lastMouse && lastMouse.target.__data__); + var mode = context.mode(); + var entity = datum && datum.id && context.hasEntity(datum.id); + if (entity) datum = entity; if (datum && datum.type === 'midpoint') { datum = datum.parents[0]; diff --git a/modules/modes/drag_node.js b/modules/modes/drag_node.js index 956b78314..2446ffbfa 100644 --- a/modules/modes/drag_node.js +++ b/modules/modes/drag_node.js @@ -127,7 +127,8 @@ export function modeDragNode(context) { if (!event || event.altKey || !d3_select(event.target).classed('target')) { return {}; } else { - return event.target.__data__ || {}; + var d = event.target.__data__; + return (d && d.id && context.hasEntity(d.id)) || {}; } } diff --git a/modules/modes/select.js b/modules/modes/select.js index 941c41751..1c87a96ed 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -246,7 +246,10 @@ export function modeSelect(context, selectedIDs) { function dblclick() { var target = d3_select(d3_event.target); + var datum = target.datum(); + var entity = datum && datum.id && context.hasEntity(datum.id); + if (entity) datum = entity; if (datum instanceof osmWay && target.classed('target')) { var choice = geoChooseEdge(context.childNodes(datum), context.mouse(), context.projection); diff --git a/modules/svg/lines.js b/modules/svg/lines.js index 4770bebd6..b9012f0d9 100644 --- a/modules/svg/lines.js +++ b/modules/svg/lines.js @@ -4,6 +4,7 @@ import _flatten from 'lodash-es/flatten'; import _forOwn from 'lodash-es/forOwn'; import _map from 'lodash-es/map'; +import { geoPath as d3_geoPath } from 'd3-geo'; import { range as d3_range } from 'd3-array'; import { @@ -37,16 +38,145 @@ export function svgLines(projection, context) { function drawTargets(selection, graph, entities, filter) { - var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; - var getPath = svgPath(projection, graph); - var passive = entities.filter(function(d) { - return true; - // return context.activeIDs().indexOf(d.id) === -1; + var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor '; + var getPath = svgPath(projection, graph).geojson; + // var getPath = d3_geoPath(projection); + + var activeID = context.activeID(); + + // Rather than drawing lines directly, we'll cut out pieces + // depending on which parts are active. + var data = { targets: [], nopes: [] }; + + // Touch targets control which other vertices we can drag a vertex onto. + // - the activeID - nope + // - next to the activeID - yes (vertices will be merged) + // - 2 away from the activeID - nope (would create a self intersecting segment) + // - all others on a closed way - nope (would create a self intersecting polygon) + // + // 0 = active vertex - no touch/connect + // 1 = passive vertex - yes touch/connect + // 2 = adjacent vertex - special rules + function passive(d) { + if (!activeID) return 1; + if (activeID === d.id) return 0; + + var parents = graph.parentWays(d); + var i, j; + + for (i = 0; i < parents.length; i++) { + var nodes = parents[i].nodes; + var isClosed = parents[i].isClosed(); + for (j = 0; j < nodes.length; j++) { // find this vertex, look nearby + if (nodes[j] === d.id) { + var ix1 = j - 2; + var ix2 = j - 1; + var ix3 = j + 1; + var ix4 = j + 2; + + if (isClosed) { // wraparound if needed + var max = nodes.length - 1; + if (ix1 < 0) ix1 = max + ix1; + if (ix2 < 0) ix2 = max + ix2; + if (ix3 > max) ix3 = ix3 - max; + if (ix4 > max) ix4 = ix4 - max; + } + + if (nodes[ix1] === activeID) return 0; // prevent self intersect + else if (nodes[ix2] === activeID) return 2; // adjacent - ok! + else if (nodes[ix3] === activeID) return 2; // adjacent - ok! + else if (nodes[ix4] === activeID) return 0; // prevent self intersect + else if (isClosed && nodes.indexOf(activeID) !== -1) return 0; // prevent self intersect + } + } + } + + return 1; + } + + entities.forEach(function(way) { + var coordGroups = { passive: [], active: [] }; + var segment = []; + var startType = null; // 0 = active, 1 = passive, 2 = adjacent + var currType = null; + var node; + + for (var i = 0; i < way.nodes.length; i++) { + + if (way.nodes[i] === activeID) { // vertex is the activeID + segment = []; // draw no segment here + startType = null; + continue; + } + + node = graph.entity(way.nodes[i]); + currType = passive(node); + + if (startType === null) { + startType = currType; + } + + if (currType !== startType) { // line changes here - try to save a segment + + if (segment.length > 0) { // finish previous segment + segment.push(node.loc); + + if (startType === 2 || currType === 2) { // one adjacent vertex + coordGroups.active.push(segment); + } else if (startType === 0 && currType === 0) { // both active vertices + coordGroups.active.push(segment); + } else { + coordGroups.passive.push(segment); + } + } + + segment = []; + startType = currType; + } + + segment.push(node.loc); + } + + // complete whatever segment we ended on + if (segment.length > 1) { + if (startType === 2 || currType === 2) { // one adjacent vertex + coordGroups.active.push(segment); + } else if (startType === 0 && currType === 0) { // both active vertices + coordGroups.active.push(segment); + } else { + coordGroups.passive.push(segment); + } + } + + if (coordGroups.passive.length) { + data.targets.push({ + 'type': 'Feature', + 'id': way.id, + 'geometry': { + 'type': 'MultiLineString', + 'coordinates': coordGroups.passive + } + }); + } + + if (coordGroups.active.length) { + data.nopes.push({ + 'type': 'Feature', + 'id': way.id + '-nope', // break the ids on purpose + 'geometry': { + 'type': 'MultiLineString', + 'coordinates': coordGroups.active + } + }); + } }); - var targets = selection.selectAll('.line.target') + + // Places to hover and connect + var targets = selection.selectAll('.line.target-allowed') .filter(filter) - .data(passive, function key(d) { return d.id; }); + .data(data.targets, function key(d) { return d.id; }); // exit targets.exit() @@ -57,7 +187,24 @@ export function svgLines(projection, context) { .append('path') .merge(targets) .attr('d', getPath) - .attr('class', function(d) { return 'way line target ' + fillClass + d.id; }); + .attr('class', function(d) { return 'way line target target-allowed ' + targetClass + d.id; }); + + + // NOPE + var nopes = selection.selectAll('.line.target-nope') + .data(data.nopes, function key(d) { return d.id; }); + + // exit + nopes.exit() + .remove(); + + // enter/update + nopes.enter() + .append('path') + .merge(nopes) + .attr('d', getPath) + .attr('class', function(d) { return 'way line target target-nope ' + nopeClass + d.id; }); + } diff --git a/modules/svg/path.js b/modules/svg/path.js index d2e522995..cf9f09b44 100644 --- a/modules/svg/path.js +++ b/modules/svg/path.js @@ -15,23 +15,27 @@ export function svgPath(projection, graph, isArea) { // When drawing areas, pad viewport by 65px in each direction to allow // for 60px area fill stroke (see ".fill-partial path.fill" css rule) - var cache = {}, - padding = isArea ? 65 : 5, - viewport = projection.clipExtent(), - paddedExtent = [ - [viewport[0][0] - padding, viewport[0][1] - padding], - [viewport[1][0] + padding, viewport[1][1] + padding] - ], - clip = d3_geoIdentity().clipExtent(paddedExtent).stream, - project = projection.stream, - path = d3_geoPath() - .projection({stream: function(output) { return project(clip(output)); }}); + var cache = {}; + var padding = isArea ? 65 : 5; + var viewport = projection.clipExtent(); + var paddedExtent = [ + [viewport[0][0] - padding, viewport[0][1] - padding], + [viewport[1][0] + padding, viewport[1][1] + padding] + ]; + var clip = d3_geoIdentity().clipExtent(paddedExtent).stream; + var project = projection.stream; + var path = d3_geoPath() + .projection({stream: function(output) { return project(clip(output)); }}); - return function(entity) { + var svgpath = function(entity) { if (entity.id in cache) { return cache[entity.id]; } else { return cache[entity.id] = path(entity.asGeoJSON(graph)); } }; + + svgpath.geojson = path; + + return svgpath; } diff --git a/modules/svg/vertices.js b/modules/svg/vertices.js index e880363f7..14b0ab369 100644 --- a/modules/svg/vertices.js +++ b/modules/svg/vertices.js @@ -182,17 +182,75 @@ export function svgVertices(projection, context) { function drawTargets(selection, graph, entities, filter) { - var fillClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var targetClass = context.getDebug('target') ? 'pink ' : 'nocolor '; + var nopeClass = context.getDebug('target') ? 'red ' : 'nocolor '; + var activeID = context.activeID(); + var data = { targets: [], nopes: [] }; - // no targets for entities that are active, or adjacent to active. + // Touch targets control which other vertices we can drag a vertex onto. + // - the activeID - nope + // - next to the activeID - yes (vertices will be merged) + // - 2 away from the activeID - nope (would create a self intersecting segment) + // - all others on a closed way - nope (would create a self intersecting polygon) + // + // 0 = active vertex - no touch/connect + // 1 = passive vertex - yes touch/connect + // 2 = adjacent vertex - special rules function passive(d) { - return d.id !== context.activeID(); + if (!activeID) return 1; + if (activeID === d.id) return 0; + + var parents = graph.parentWays(d); + var i, j; + + for (i = 0; i < parents.length; i++) { + var nodes = parents[i].nodes; + var isClosed = parents[i].isClosed(); + for (j = 0; j < nodes.length; j++) { // find this vertex, look nearby + if (nodes[j] === d.id) { + var ix1 = j - 2; + var ix2 = j - 1; + var ix3 = j + 1; + var ix4 = j + 2; + + if (isClosed) { // wraparound if needed + var max = nodes.length - 1; + if (ix1 < 0) ix1 = max + ix1; + if (ix2 < 0) ix2 = max + ix2; + if (ix3 > max) ix3 = ix3 - max; + if (ix4 > max) ix4 = ix4 - max; + } + + if (nodes[ix1] === activeID) return 0; // prevent self intersect + else if (nodes[ix2] === activeID) return 2; // adjacent - ok! + else if (nodes[ix3] === activeID) return 2; // adjacent - ok! + else if (nodes[ix4] === activeID) return 0; // prevent self intersect + else if (isClosed && nodes.indexOf(activeID) !== -1) return 0; // prevent self intersect + } + } + } + + return 1; } - var data = entities.filter(passive); - var targets = selection.selectAll('.vertex.target') + + entities.forEach(function(node) { + if (activeID === node.id) return; // draw no vertex on the activeID + + var currType = passive(node); + if (currType !== 0) { + data.targets.push(node); // passive or adjacent - allow to connect + } else { + data.nopes.push({ + id: node.id + '-nope', // not a real osmNode, break the id on purpose + loc: node.loc + }); + } + }); + + var targets = selection.selectAll('.vertex.target-allowed') .filter(filter) - .data(data, function key(d) { return d.id; }); + .data(data.targets, function key(d) { return d.id; }); // exit targets.exit() @@ -201,9 +259,26 @@ export function svgVertices(projection, context) { // enter/update targets.enter() .append('circle') - .attr('r', function(d) { return _radii[d.id] || radiuses.shadow[3]; }) + .attr('r', function(d) { return (_radii[d.id] || radiuses.shadow[3]); }) .merge(targets) - .attr('class', function(d) { return 'node vertex target ' + fillClass + d.id; }) + .attr('class', function(d) { return 'node vertex target target-allowed ' + targetClass + d.id; }) + .attr('transform', svgPointTransform(projection)); + + + // NOPE + var nopes = selection.selectAll('.vertex.target-nope') + .data(data.nopes, function key(d) { return d.id; }); + + // exit + nopes.exit() + .remove(); + + // enter/update + nopes.enter() + .append('circle') + .attr('r', function(d) { return (_radii[d.id.replace('-nope','')] || radiuses.shadow[3]); }) + .merge(nopes) + .attr('class', function(d) { return 'node vertex target target-nope ' + nopeClass + d.id; }) .attr('transform', svgPointTransform(projection)); } @@ -321,8 +396,11 @@ export function svgVertices(projection, context) { .call(draw, graph, currentVisible(all), sets, filterRendered); // Draw touch targets.. + var filterTargets = function(d) { + return isMoving ? true : filterRendered(d); + }; selection.selectAll('.layer-points .layer-points-targets') - .call(drawTargets, graph, currentVisible(all), filterRendered); + .call(drawTargets, graph, currentVisible(all), filterTargets); function currentVisible(which) {