From bf9b19359ae24d297ac2837e8eacfa21660608a6 Mon Sep 17 00:00:00 2001 From: Thomas Hervey Date: Fri, 20 Jul 2018 21:41:44 -0400 Subject: [PATCH] WIP drag note --- modules/behavior/drag.js | 7 +- modules/behavior/draw.js | 1 - modules/behavior/hash.js | 2 + modules/modes/add_note.js | 3 +- modules/modes/browse.js | 4 +- modules/modes/drag_note.js | 254 +++++++++++++++++++++++++++++++++++ modules/modes/index.js | 1 + modules/modes/select_note.js | 3 + modules/osm/note.js | 4 + modules/renderer/map.js | 8 +- modules/services/osm.js | 36 +++-- modules/svg/notes.js | 2 + modules/ui/modes.js | 4 +- modules/ui/note_editor.js | 6 +- 14 files changed, 315 insertions(+), 20 deletions(-) create mode 100644 modules/modes/drag_note.js diff --git a/modules/behavior/drag.js b/modules/behavior/drag.js index f32c952ce..9735b77ae 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,10 @@ 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 = datum instanceof osmNote ? + datum : datum && datum.properties && datum.properties.entity; + if (entity && target[matchesSelector](_selector)) { return dragstart.call(target, entity); } diff --git a/modules/behavior/draw.js b/modules/behavior/draw.js index 80b158d71..21d17d2f5 100644 --- a/modules/behavior/draw.js +++ b/modules/behavior/draw.js @@ -138,7 +138,6 @@ export function behaviorDraw(context) { return; } } - dispatch.call('click', this, context.map().mouseCoordinates(), d); } diff --git a/modules/behavior/hash.js b/modules/behavior/hash.js index 2aa76ff2b..cb073c40a 100644 --- a/modules/behavior/hash.js +++ b/modules/behavior/hash.js @@ -51,6 +51,8 @@ export function behaviorHash(context) { var newParams = {}; delete q.id; + console.log('TAH - hash: context.selectedIDs()', context.selectedIDs()); + console.log('TAH - hash: context.selectedNoteID()', context.selectedNoteID()); var selected = context.selectedIDs().filter(function(id) { return !context.entity(id).isNew(); }); diff --git a/modules/modes/add_note.js b/modules/modes/add_note.js index e44a2f763..f0ab36571 100644 --- a/modules/modes/add_note.js +++ b/modules/modes/add_note.js @@ -28,6 +28,7 @@ export function modeAddNote(context) { function add(loc) { var note = osmNote({ + id: -1, loc: loc, status: 'open', comments: {}, @@ -35,8 +36,6 @@ export function modeAddNote(context) { }); services.osm.replaceNote(note); - dispatch.call('change'); - context .selectedNoteID(note.id) diff --git a/modules/modes/browse.js b/modules/modes/browse.js index 5ba0e7fee..72484df62 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 ]; diff --git a/modules/modes/drag_note.js b/modules/modes/drag_note.js new file mode 100644 index 000000000..d6d1f2b9b --- /dev/null +++ b/modules/modes/drag_note.js @@ -0,0 +1,254 @@ +import _find from 'lodash-es/find'; + +import { + event as d3_event, + select as d3_select +} from 'd3-selection'; + +import { dispatch as d3_dispatch } from 'd3-dispatch'; + +import { d3keybinding as d3_keybinding } from '../lib/d3.keybinding.js'; + +import { geoVecInterp } from '../geo'; + +import { t } from '../util/locale'; + +import { services } from '../services'; + + +import { + actionAddMidpoint, + actionConnect, + actionMoveNode, + actionNoop +} from '../actions'; + +import { + behaviorEdit, + behaviorHover, + behaviorDrag +} from '../behavior'; + +import { + geoChooseEdge, + geoHasLineIntersections, + geoHasSelfIntersections, + geoVecSubtract, + geoViewportEdge +} from '../geo'; + +import { modeBrowse, modeSelectNote } from './index'; +import { osmJoinWays, osmNode } 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 dispatch = d3_dispatch('redraw', 'change'); + + var _nudgeInterval; + var _restoreSelectedNoteID = []; + 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 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) { + console.log('TAH - drag_note start()'); + + context.perform(actionNoop()); + + _activeEntity = entity; + _startLoc = entity.loc; + + context.surface().selectAll('.note-' + _activeEntity.id) + .classed('active', true); + + context.enter(mode); + } + + + 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 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); + + entity = entity.move(geoVecInterp(entity.loc, loc, 1)); + + var osm = services.osm; + if (osm) { + osm.replaceNote(entity); // update note cache + } + dispatch.call('change', this, 'difference'); + console.log('moved: ', entity.loc); + } + + + function end(entity) { + console.log('TAH - drag_note end()'); + context + .selectedNoteID(entity.id) + .enter(modeSelectNote(context, entity.id)); + } + + + function cancel() { + drag.cancel(); + context.enter(modeBrowse(context)); + } + + + var drag = behaviorDrag() + .selector('.layer-notes .new') + .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-note', 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-note', null); + + context.map() + .on('drawn.drag-note', null); + + _activeEntity = null; + + context.surface() + .classed('nope', false) + .classed('nope-suppressed', false) + .classed('nope-disabled', false) + .selectAll('.active') + .classed('active', false); + + stopNudge(); + }; + + + mode.selectedNoteID = 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.restoreSelectedNoteID = function(_) { + if (!arguments.length) return _restoreSelectedNoteID; + _restoreSelectedNoteID = _; + return mode; + }; + + + mode.behavior = drag; + + + return mode; +} diff --git a/modules/modes/index.js b/modules/modes/index.js index 56ed999d0..83838c4e1 100644 --- a/modules/modes/index.js +++ b/modules/modes/index.js @@ -4,6 +4,7 @@ export { modeAddPoint } from './add_point'; export { modeAddNote } from './add_note'; export { modeBrowse } from './browse'; export { modeDragNode } from './drag_node'; +export { modeDragNote } from './drag_note'; export { modeDrawArea } from './draw_area'; export { modeDrawLine } from './draw_line'; export { modeMove } from './move'; diff --git a/modules/modes/select_note.js b/modules/modes/select_note.js index e2c008fbc..56fe0f198 100644 --- a/modules/modes/select_note.js +++ b/modules/modes/select_note.js @@ -11,6 +11,8 @@ import { behaviorSelect } from '../behavior'; +import { modeDragNote } from '../modes'; + import { services } from '../services'; import { modeBrowse } from './browse'; import { uiNoteEditor } from '../ui'; @@ -37,6 +39,7 @@ export function modeSelectNote(context, selectedNoteID) { behaviorHover(context), behaviorSelect(context), behaviorLasso(context), + modeDragNote(context).behavior ]; var newFeature = false; diff --git a/modules/osm/note.js b/modules/osm/note.js index 3e10e8cf1..ed03c0504 100644 --- a/modules/osm/note.js +++ b/modules/osm/note.js @@ -55,6 +55,10 @@ _extend(osmNote.prototype, { isNew: function() { return this.id < 0; + }, + + move: function(loc) { + return this.update({ loc: loc }); } }); diff --git a/modules/renderer/map.js b/modules/renderer/map.js index 1118488b7..d6c7d809c 100644 --- a/modules/renderer/map.js +++ b/modules/renderer/map.js @@ -38,7 +38,8 @@ import { svgLines, svgMidpoints, svgPoints, - svgVertices + svgVertices, + svgNotes } from '../svg'; import { uiFlash } from '../ui'; @@ -73,6 +74,8 @@ export function rendererMap(context) { var drawMidpoints = svgMidpoints(projection, context); var drawLabels = svgLabels(projection, context); + var drawNotes = svgNotes(projection, context); + var _selection = d3_select(null); var supersurface = d3_select(null); var wrapper = d3_select(null); @@ -341,6 +344,9 @@ export function rendererMap(context) { .call(drawLabels, graph, data, filter, dimensions, fullRedraw) .call(drawPoints, graph, data, filter); + surface.selectAll('.data-layer-notes') + .call(drawNotes); + dispatch.call('drawn', this, {full: true}); } diff --git a/modules/services/osm.js b/modules/services/osm.js index 079a64c1a..9afd91fb8 100644 --- a/modules/services/osm.js +++ b/modules/services/osm.js @@ -327,6 +327,20 @@ function parseXML(xml, callback, options) { } +// replace or remove note from rtree +function updateRtree(item, replace) { // update (or insert) in _noteCache.rtree + + // TODO: other checks needed? (e.g., if cache.data.children.length decrements ...) + + // remove note + _noteCache.rtree.remove(item, function isEql(a, b) { return a.data.id === b.data.id; }); + if (replace) { + _noteCache.rtree.insert(item); // add note (updated) + } + +} + + function wrapcb(thisArg, callback, cid) { return function(err, result) { if (err) { @@ -1033,23 +1047,23 @@ export default { }, + // remove a single note from the cache + removeNote: function(note) { + if (!(note instanceof osmNote) || !note.id) return; + + delete _noteCache.note[note.id]; + + updateRtree(encodeNoteRtree(note), false); + }, + + // replace a single note in the cache replaceNote: function(note) { if (!(note instanceof osmNote) || !note.id) return; _noteCache.note[note.id] = note; // update (or insert) in _noteCache.note - function updateRtree(item) { // update (or insert) in _noteCache.rtree - - // TODO: other checks needed? (e.g., if cache.data.children.length decrements ...) - - // remove note - _noteCache.rtree.remove(item, function isEql(a, b) { return a.data.id === b.data.id; }); - _noteCache.rtree.insert(item); // add note (updated) - - } - - updateRtree(encodeNoteRtree(note)); + updateRtree(encodeNoteRtree(note), true); return note; } diff --git a/modules/svg/notes.js b/modules/svg/notes.js index 511f10c00..e5f7d88c3 100644 --- a/modules/svg/notes.js +++ b/modules/svg/notes.js @@ -1,12 +1,14 @@ import _throttle from 'lodash-es/throttle'; import { select as d3_select } from 'd3-selection'; +import { dispatch as d3_dispatch } from 'd3-dispatch'; import { svgPointTransform } from './index'; import { services } from '../services'; export function svgNotes(projection, context, dispatch) { + if (!dispatch) { dispatch = d3_dispatch('change'); } var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000); var minZoom = 12; var layer = d3_select(null); diff --git a/modules/ui/modes.js b/modules/ui/modes.js index 4d01b6820..ad00af2b4 100644 --- a/modules/ui/modes.js +++ b/modules/ui/modes.js @@ -16,6 +16,7 @@ import { import { svgIcon } from '../svg'; import { tooltip } from '../util/tooltip'; import { uiTooltipHtml } from './tooltipHtml'; +import { services } from '../services/index.js'; export function uiModes(context) { @@ -36,7 +37,7 @@ export function uiModes(context) { function toggleNewNote() { return svgNotes().enabled() && context.connection().authenticated() - && ~~context.map().zoom() >= 12; + && ~~context.map().zoom() >= 16; } @@ -97,7 +98,6 @@ export function uiModes(context) { modes.forEach(function(mode) { keybinding.on(mode.key, function() { - // TODO: allow zooming out beyond minZoom when adding new note. Currently prevented if ((editable() && mode.id !== 'add-note') || (toggleNewNote() && mode.id === 'add-note')) { if (mode.id === context.mode().id) { context.enter(modeBrowse(context)); diff --git a/modules/ui/note_editor.js b/modules/ui/note_editor.js index 1ded94d46..fd526076f 100644 --- a/modules/ui/note_editor.js +++ b/modules/ui/note_editor.js @@ -40,7 +40,11 @@ export function uiNoteEditor(context) { headerEnter .append('button') .attr('class', 'fr note-editor-close') - .on('click', function() { context.enter(modeBrowse(context)); }) + .on('click', function() { + var osm = services.osm; + if (_note.isNew()) { osm.removeNote(_note); } // delete new note + context.enter(modeBrowse(context)); + }) .call(svgIcon('#iD-icon-close')); headerEnter