diff --git a/modules/behavior/drag.js b/modules/behavior/drag.js index f32c952ce..ad0428fa1 100644 --- a/modules/behavior/drag.js +++ b/modules/behavior/drag.js @@ -9,6 +9,8 @@ import { touches as d3_touches } from 'd3-selection'; +import { osmNote } from '../osm'; + import { utilRebind } from '../util/rebind'; import { @@ -162,7 +164,13 @@ export function behaviorDrag() { var target = d3_event.target; for (; target && target !== root; target = target.parentNode) { var datum = target.__data__; - var entity = datum && datum.properties && datum.properties.entity; + + var entity; + if (datum instanceof osmNote) { entity = datum;} + else { + entity = datum && datum.properties && datum.properties.entity; + } + if (entity && target[matchesSelector](_selector)) { return dragstart.call(target, entity); } diff --git a/modules/modes/add_note.js b/modules/modes/add_note.js index e44a2f763..77ec21c13 100644 --- a/modules/modes/add_note.js +++ b/modules/modes/add_note.js @@ -35,7 +35,7 @@ export function modeAddNote(context) { }); services.osm.replaceNote(note); - dispatch.call('change'); + dispatch.call('change', this); context diff --git a/modules/modes/browse.js b/modules/modes/browse.js index 5ba0e7fee..1638b6451 100644 --- a/modules/modes/browse.js +++ b/modules/modes/browse.js @@ -8,6 +8,7 @@ import { } from '../behavior'; import { modeDragNode } from './drag_node'; +import { modeDragNote } from './drag_note'; export function modeBrowse(context) { @@ -23,7 +24,8 @@ export function modeBrowse(context) { behaviorHover(context).on('hover', context.ui().sidebar.hover), behaviorSelect(context), behaviorLasso(context), - modeDragNode(context).behavior + modeDragNode(context).behavior, + modeDragNote(context).behavior // TAH - possibly change ]; diff --git a/modules/modes/drag_note.js b/modules/modes/drag_note.js new file mode 100644 index 000000000..b5f1eef0f --- /dev/null +++ b/modules/modes/drag_note.js @@ -0,0 +1,491 @@ +import _find from 'lodash-es/find'; + +import { + event as d3_event, + select as d3_select +} from 'd3-selection'; + +import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; + +import { t } from '../util/locale'; + +import { + actionAddMidpoint, + actionConnect, + actionMoveNode, + actionNoop +} from '../actions'; + +import { + behaviorEdit, + behaviorHover, + behaviorDrag +} from '../behavior'; + +import { + geoChooseEdge, + geoHasLineIntersections, + geoHasSelfIntersections, + geoVecSubtract, + geoViewportEdge +} from '../geo'; + +import { modeBrowse, modeSelect } from './index'; +import { osmJoinWays, osmNote } from '../osm'; +import { uiFlash } from '../ui'; + + +export function modeDragNote(context) { + var mode = { + id: 'drag-note', + button: 'browse' + }; + var hover = behaviorHover(context).altDisables(true) + .on('hover', context.ui().sidebar.hover); + var edit = behaviorEdit(context); + + var _nudgeInterval; + var _restoreSelectedNoteIDs = []; + var _wasMidpoint = false; + var _isCancelled = false; + var _activeEntity; + var _startLoc; + var _lastLoc; + + + function startNudge(entity, nudge) { + if (_nudgeInterval) window.clearInterval(_nudgeInterval); + _nudgeInterval = window.setInterval(function() { + context.pan(nudge); + doMove(entity, nudge); + }, 50); + } + + + function stopNudge() { + if (_nudgeInterval) { + window.clearInterval(_nudgeInterval); + _nudgeInterval = null; + } + } + + + function moveAnnotation(entity) { + console.log('entity') + return t('operations.move.annotation.' + entity.geometry(context.graph())); + } + + + function connectAnnotation(entity) { + return t('operations.connect.annotation.' + entity.geometry(context.graph())); + } + + + function origin(entity) { + return context.projection(entity.loc); + } + + + 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); + } + } + + + function start(entity) { + _wasMidpoint = entity.type === 'midpoint'; + var hasHidden = context.features().hasHiddenConnections(entity, context.graph()); + _isCancelled = d3_event.sourceEvent.shiftKey || hasHidden; + + + if (_isCancelled) { + if (hasHidden) { + uiFlash() + .duration(4000) + .text(t('modes.drag_node.connected_to_hidden'))(); + } + return drag.cancel(); + } + + if (_wasMidpoint) { + var midpoint = entity; + entity = osmNote(); + context.perform(actionAddMidpoint(midpoint, entity)); + entity = context.entity(entity.id); // get post-action entity + + var vertex = context.surface().selectAll('.' + entity.id); + drag.target(vertex.node(), entity); + + } else { + context.perform(actionNoop()); + } + + _activeEntity = entity; + _startLoc = entity.loc; + + context.surface().selectAll('.' + _activeEntity.id) + .classed('active', true); + + context.enter(mode); + } + + + // related code + // - `behavior/draw.js` `datum()` + function datum() { + var event = d3_event && d3_event.sourceEvent; + if (!event || event.altKey) { + return {}; + } else { + // When dragging, snap only to touch targets.. + // (this excludes area fills and active drawing elements) + var d = event.target.__data__; + return (d && d.properties && d.properties.target) ? d : {}; + } + } + + + function doMove(entity, nudge) { + nudge = nudge || [0, 0]; + + var currPoint = (d3_event && d3_event.point) || context.projection(_lastLoc); + var currMouse = geoVecSubtract(currPoint, nudge); + var loc = context.projection.invert(currMouse); + + if (!_nudgeInterval) { // If not nudging at the edge of the viewport, try to snap.. + // related code + // - `mode/drag_node.js` `doMode()` + // - `behavior/draw.js` `click()` + // - `behavior/draw_way.js` `move()` + var d = datum(); + var target = d; + var targetLoc = target && target.loc; + var targetNodes = d && d.properties && d.properties.nodes; + var edge; + + if (targetLoc) { // snap to node/vertex - a point target with `.loc` + loc = targetLoc; + + } else if (targetNodes) { // snap to way - a line target with `.nodes` + edge = geoChooseEdge(targetNodes, context.mouse(), context.projection, end.id); + if (edge) { + loc = edge.loc; + } + } + } + + context.replace( + actionMoveNode(entity.id, loc), + // moveAnnotation(entity) TODO: - likely replace + ); + + // Below here: validations + var isInvalid = false; + + // Check if this connection to `target` could cause relations to break.. + if (target) { + isInvalid = hasRelationConflict(entity, target, edge, context.graph()); + } + + // Check if this drag causes the geometry to break.. + if (!isInvalid) { + isInvalid = hasInvalidGeometry(entity, context.graph()); + } + + + var nope = context.surface().classed('nope'); + if (isInvalid === 'relation' || isInvalid === 'restriction') { + if (!nope) { // about to nope - show hint + uiFlash() + .duration(4000) + .text(t('operations.connect.' + isInvalid, + { relation: context.presets().item('type/restriction').name() } + ))(); + } + } else { + if (nope) { // about to un-nope, remove hint + uiFlash() + .duration(1) + .text('')(); + } + } + + + var nopeDisabled = context.surface().classed('nope-disabled'); + if (nopeDisabled) { + context.surface() + .classed('nope', false) + .classed('nope-suppressed', isInvalid); + } else { + context.surface() + .classed('nope', isInvalid) + .classed('nope-suppressed', false); + } + + _lastLoc = loc; + } + + + // Uses `actionConnect.disabled()` to know whether this connection is ok.. + function hasRelationConflict(entity, target, edge, graph) { + var testGraph = graph.update(); // copy + + // if snapping to way - add midpoint there and consider that the target.. + if (edge) { + var midpoint = osmNote(); + var action = actionAddMidpoint({ + loc: edge.loc, + edge: [target.nodes[edge.index - 1], target.nodes[edge.index]] + }, midpoint); + + testGraph = action(testGraph); + target = midpoint; + } + + // can we connect to it? + var ids = [entity.id, target.id]; + return actionConnect(ids).disabled(testGraph); + } + + + function hasInvalidGeometry(entity, graph) { + var parents = graph.parentWays(entity); + var i, j, k; + + for (i = 0; i < parents.length; i++) { + var parent = parents[i]; + var nodes = []; + var activeIndex = null; // which multipolygon ring contains node being dragged + + // test any parent multipolygons for valid geometry + var relations = graph.parentRelations(parent); + for (j = 0; j < relations.length; j++) { + if (!relations[j].isMultipolygon()) continue; + + var rings = osmJoinWays(relations[j].members, graph); + + // find active ring and test it for self intersections + for (k = 0; k < rings.length; k++) { + nodes = rings[k].nodes; + if (_find(nodes, function(n) { return n.id === entity.id; })) { + activeIndex = k; + if (geoHasSelfIntersections(nodes, entity.id)) { + return true; + } + } + rings[k].coords = nodes.map(function(n) { return n.loc; }); + } + + // test active ring for intersections with other rings in the multipolygon + for (k = 0; k < rings.length; k++) { + if (k === activeIndex) continue; + + // make sure active ring doesnt cross passive rings + if (geoHasLineIntersections(rings[activeIndex].nodes, rings[k].nodes, entity.id)) { + return true; + } + } + } + + + // If we still haven't tested this node's parent way for self-intersections. + // (because it's not a member of a multipolygon), test it now. + if (activeIndex === null) { + nodes = parent.nodes.map(function(nodeID) { return graph.entity(nodeID); }); + if (nodes.length && geoHasSelfIntersections(nodes, entity.id)) { + return true; + } + } + + } + + return false; + } + + + function move(entity) { + if (_isCancelled) return; + d3_event.sourceEvent.stopPropagation(); + + context.surface().classed('nope-disabled', d3_event.sourceEvent.altKey); + + _lastLoc = context.projection.invert(d3_event.point); + + doMove(entity); + var nudge = geoViewportEdge(d3_event.point, context.map().dimensions()); + if (nudge) { + startNudge(entity, nudge); + } else { + stopNudge(); + } + } + + + function end(entity) { + if (_isCancelled) return; + + var d = datum(); + var nope = (d && d.properties && d.properties.nope) || context.surface().classed('nope'); + var target = d && d.properties && d.properties.entity; // entity to snap to + + if (nope) { // bounce back + context.perform( + _actionBounceBack(entity.id, _startLoc) + ); + + } else if (target && target.type === 'way') { + var choice = geoChooseEdge(context.childNodes(target), context.mouse(), context.projection, entity.id); + context.replace( + actionAddMidpoint({ + loc: choice.loc, + edge: [target.nodes[choice.index - 1], target.nodes[choice.index]] + }, entity), + // connectAnnotation(target) TODO: - likely replace + ); + + } else if (target && target.type === 'node') { + context.replace( + actionConnect([target.id, entity.id]), + // connectAnnotation(target) TODO: - likely replace + ); + + } else if (_wasMidpoint) { + context.replace( + actionNoop(), + t('operations.add.annotation.vertex') + ); + + } else { + context.replace( + actionNoop(), + // moveAnnotation(entity) TODO: - likely replace + ); + } + + var reselection = _restoreSelectedNoteIDs.filter(function(id) { + return context.graph().hasEntity(id); + }); + + if (reselection.length) { + context.enter(modeSelect(context, reselection)); + } else { + context.enter(modeBrowse(context)); + } + } + + + function _actionBounceBack(nodeID, toLoc) { + var moveNode = actionMoveNode(nodeID, toLoc); + var action = function(graph, t) { + // last time through, pop off the bounceback perform. + // it will then overwrite the initial perform with a moveNode that does nothing + if (t === 1) context.pop(); + return moveNode(graph, t); + }; + action.transitionable = true; + return action; + } + + + function cancel() { + drag.cancel(); + context.enter(modeBrowse(context)); + } + + + var drag = behaviorDrag() + .selector('.layer-notes .note') + .surface(d3_select('#map').node()) + .origin(origin) + .on('start', start) + .on('move', move) + .on('end', end); + + + mode.enter = function() { + context.install(hover); + context.install(edit); + + // d3_select(window) + // .on('keydown.drawWay', keydown) + // .on('keyup.drawWay', keyup); + + context.history() + .on('undone.drag-node', cancel); + }; + + + mode.exit = function() { + context.ui().sidebar.hover.cancel(); + context.uninstall(hover); + context.uninstall(edit); + + d3_select(window) + .on('keydown.hover', null) + .on('keyup.hover', null); + + context.history() + .on('undone.drag-node', null); + + context.map() + .on('drawn.drag-node', null); + + _activeEntity = null; + + context.surface() + .classed('nope', false) + .classed('nope-suppressed', false) + .classed('nope-disabled', false) + .selectAll('.active') + .classed('active', false); + + stopNudge(); + }; + + + mode.selectedIDs = function() { + if (!arguments.length) return _activeEntity ? [_activeEntity.id] : []; + // no assign + return mode; + }; + + + mode.activeID = function() { + if (!arguments.length) return _activeEntity && _activeEntity.id; + // no assign + return mode; + }; + + + mode.restoreSelectedNoteIDs = function(_) { + if (!arguments.length) return _restoreSelectedNoteIDs; + _restoreSelectedNoteIDs = _; + return mode; + }; + + + mode.behavior = drag; + + + return mode; +} diff --git a/modules/modes/select.js b/modules/modes/select.js index 924314de9..90168b12c 100644 --- a/modules/modes/select.js +++ b/modules/modes/select.js @@ -36,6 +36,7 @@ import { import { modeBrowse } from './browse'; import { modeDragNode } from './drag_node'; +import { modeDragNote } from './drag_note'; import * as Operations from '../operations/index'; import { uiEditMenu, uiSelectionList } from '../ui'; import { uiCmd } from '../ui/cmd'; @@ -63,7 +64,8 @@ export function modeSelect(context, selectedIDs) { behaviorHover(context), behaviorSelect(context), behaviorLasso(context), - modeDragNode(context).restoreSelectedIDs(selectedIDs).behavior + modeDragNode(context).restoreSelectedIDs(selectedIDs).behavior, + modeDragNote(context).restoreSelectedNoteIDs(selectedIDs).behavior // TODO: - likely remove ]; var inspector; var editMenu; diff --git a/modules/modes/select_note.js b/modules/modes/select_note.js index e2c008fbc..430870e96 100644 --- a/modules/modes/select_note.js +++ b/modules/modes/select_note.js @@ -8,11 +8,14 @@ import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; import { behaviorHover, behaviorLasso, - behaviorSelect + behaviorSelect, + behaviorDrag } from '../behavior'; + import { services } from '../services'; import { modeBrowse } from './browse'; +import { modeDragNote } from './drag_note'; import { uiNoteEditor } from '../ui'; @@ -37,6 +40,7 @@ export function modeSelectNote(context, selectedNoteID) { behaviorHover(context), behaviorSelect(context), behaviorLasso(context), + modeDragNote(context).restoreSelectedNoteIDs(selectedNoteID).behavior ]; var newFeature = false;