diff --git a/modules/behavior/drag.js b/modules/behavior/drag.js index 550b8c5a1..943483873 100644 --- a/modules/behavior/drag.js +++ b/modules/behavior/drag.js @@ -168,11 +168,16 @@ export function behaviorDrag() { for (; target && target !== root; target = target.parentNode) { var datum = target.__data__; +<<<<<<< HEAD var entity; if (datum instanceof osmNote) { entity = datum;} else { entity = datum && datum.properties && datum.properties.entity; } +======= + var entity = datum instanceof osmNote ? + datum : datum && datum.properties && datum.properties.entity; +>>>>>>> drag-note 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/lib/d3.geo.tile.js b/modules/lib/d3.geo.tile.js deleted file mode 100644 index 3b0f718ef..000000000 --- a/modules/lib/d3.geo.tile.js +++ /dev/null @@ -1,93 +0,0 @@ -import { range as d3_range } from 'd3-array'; - - -export function d3geoTile() { - var _size = [960, 500]; - var _scale = 256; - var _scaleExtent = [0, 20]; - var _translate = [_size[0] / 2, _size[1] / 2]; - var _zoomDelta = 0; - var _margin = 0; - - function bound(val) { - return Math.min(_scaleExtent[1], Math.max(_scaleExtent[0], val)); - } - - function tile() { - var z = Math.max(Math.log(_scale) / Math.LN2 - 8, 0); - var z0 = bound(Math.round(z + _zoomDelta)); - var k = Math.pow(2, z - z0 + 8); - var origin = [ - (_translate[0] - _scale / 2) / k, - (_translate[1] - _scale / 2) / k - ]; - - var cols = d3_range( - Math.max(0, Math.floor(-origin[0]) - _margin), - Math.max(0, Math.ceil(_size[0] / k - origin[0]) + _margin) - ); - var rows = d3_range( - Math.max(0, Math.floor(-origin[1]) - _margin), - Math.max(0, Math.ceil(_size[1] / k - origin[1]) + _margin) - ); - - var tiles = []; - for (var i = 0; i < rows.length; i++) { - var y = rows[i]; - for (var j = 0; j < cols.length; j++) { - var x = cols[j]; - - if (i >= _margin && i <= rows.length - _margin && - j >= _margin && j <= cols.length - _margin) { - tiles.unshift([x, y, z0]); // tiles in view at beginning - } else { - tiles.push([x, y, z0]); // tiles in margin at the end - } - } - } - - tiles.translate = origin; - tiles.scale = k; - - return tiles; - } - - tile.scaleExtent = function(val) { - if (!arguments.length) return _scaleExtent; - _scaleExtent = val; - return tile; - }; - - tile.size = function(val) { - if (!arguments.length) return _size; - _size = val; - return tile; - }; - - tile.scale = function(val) { - if (!arguments.length) return _scale; - _scale = val; - return tile; - }; - - tile.translate = function(val) { - if (!arguments.length) return _translate; - _translate = val; - return tile; - }; - - tile.zoomDelta = function(val) { - if (!arguments.length) return _zoomDelta; - _zoomDelta = +val; - return tile; - }; - - // number to extend the rows/columns beyond those covering the viewport - tile.margin = function(val) { - if (!arguments.length) return _margin; - _margin = +val; - return tile; - }; - - return tile; -} diff --git a/modules/lib/index.js b/modules/lib/index.js index b6ddad54a..bee98416c 100644 --- a/modules/lib/index.js +++ b/modules/lib/index.js @@ -1,3 +1,2 @@ export { d3combobox } from './d3.combobox'; -export { d3geoTile } from './d3.geo.tile'; export { d3keybinding } from './d3.keybinding'; diff --git a/modules/modes/add_note.js b/modules/modes/add_note.js index 77ec21c13..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', this); - context .selectedNoteID(note.id) diff --git a/modules/modes/browse.js b/modules/modes/browse.js index 70fc21e96..72484df62 100644 --- a/modules/modes/browse.js +++ b/modules/modes/browse.js @@ -8,7 +8,7 @@ import { } from '../behavior'; import { modeDragNode } from './drag_node'; -import { modeDragNote2 } from './drag_note2'; +import { modeDragNote } from './drag_note'; export function modeBrowse(context) { @@ -25,7 +25,7 @@ export function modeBrowse(context) { behaviorSelect(context), behaviorLasso(context), modeDragNode(context).behavior, - modeDragNote2(context).behavior + modeDragNote(context).behavior ]; diff --git a/modules/modes/drag_note.js b/modules/modes/drag_note.js index 2c55f4448..602fac67e 100644 --- a/modules/modes/drag_note.js +++ b/modules/modes/drag_note.js @@ -1,5 +1,3 @@ -import { dispatch as d3_dispatch } from 'd3-dispatch'; - import _find from 'lodash-es/find'; import { @@ -7,14 +5,21 @@ import { 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, - actionMoveNote, + actionMoveNode, actionNoop } from '../actions'; @@ -32,8 +37,8 @@ import { geoViewportEdge } from '../geo'; -import { modeBrowse, modeSelect } from './index'; -import { osmJoinWays, osmNote } from '../osm'; +import { modeBrowse, modeSelectNote } from './index'; +import { osmJoinWays, osmNode } from '../osm'; import { uiFlash } from '../ui'; @@ -46,16 +51,16 @@ export function modeDragNote(context) { .on('hover', context.ui().sidebar.hover); var edit = behaviorEdit(context); + var dispatch = d3_dispatch('redraw', 'change'); + var _nudgeInterval; - var _restoreSelectedNoteIDs = []; + var _restoreSelectedNoteID = []; var _wasMidpoint = false; var _isCancelled = false; var _activeEntity; var _startLoc; var _lastLoc; - var dispatch = d3_dispatch('change'); - function startNudge(entity, nudge) { if (_nudgeInterval) window.clearInterval(_nudgeInterval); @@ -74,17 +79,6 @@ export function modeDragNote(context) { } - 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); } @@ -117,7 +111,7 @@ export function modeDragNote(context) { function start(entity) { - _isCancelled = d3_event.sourceEvent.shiftKey; + context.perform(actionNoop()); _activeEntity = entity; _startLoc = entity.loc; @@ -129,18 +123,22 @@ export function modeDragNote(context) { } - // 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 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(); + // } + } @@ -151,147 +149,20 @@ export function modeDragNote(context) { 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 targetNotes = d; - var edge; + entity = entity.move(geoVecInterp(entity.loc, loc, 1)); - // 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; - // } - // } - } - - actionMoveNote(entity.id, loc); - dispatch.call('change'); - - 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('')(); - } - } - - // Below here: validations - var isInvalid = false; - - - 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; - } - - 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(); + var osm = services.osm; + if (osm) { + osm.replaceNote(entity); // update note cache } + dispatch.call('change', this, 'difference'); } 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; + context + .selectedNoteID(entity.id) + .enter(modeSelectNote(context, entity.id)); } @@ -302,7 +173,7 @@ export function modeDragNote(context) { var drag = behaviorDrag() - .selector('.layer-notes .note') + .selector('.layer-notes .new') .surface(d3_select('#map').node()) .origin(origin) .on('start', start) @@ -314,15 +185,12 @@ export function modeDragNote(context) { context.install(hover); context.install(edit); - // context.selectedIDs(null); TODO: possibly add something like this - // context.selectedNoteID(_activeEntity); + d3_select(window) + .on('keydown.drawWay', keydown) + .on('keyup.drawWay', keyup); - // d3_select(window) - // .on('keydown.drawWay', keydown) - // .on('keyup.drawWay', keyup); - - // context.history() - // .on('undone.drag-node', cancel); + context.history() + .on('undone.drag-note', cancel); }; @@ -331,15 +199,15 @@ export function modeDragNote(context) { context.uninstall(hover); context.uninstall(edit); - // d3_select(window) - // .on('keydown.hover', null) - // .on('keyup.hover', null); + d3_select(window) + .on('keydown.hover', null) + .on('keyup.hover', null); - // context.history() - // .on('undone.drag-node', null); + context.history() + .on('undone.drag-note', null); - // context.map() - // .on('drawn.drag-node', null); + context.map() + .on('drawn.drag-note', null); _activeEntity = null; @@ -354,7 +222,7 @@ export function modeDragNote(context) { }; - mode.selectedIDs = function() { + mode.selectedNoteID = function() { if (!arguments.length) return _activeEntity ? [_activeEntity.id] : []; // no assign return mode; @@ -368,9 +236,9 @@ export function modeDragNote(context) { }; - mode.restoreSelectedNoteIDs = function(_) { - if (!arguments.length) return _restoreSelectedNoteIDs; - _restoreSelectedNoteIDs = _; + mode.restoreSelectedNoteID = function(_) { + if (!arguments.length) return _restoreSelectedNoteID; + _restoreSelectedNoteID = _; return mode; }; diff --git a/modules/modes/index.js b/modules/modes/index.js index 68ae49f05..83838c4e1 100644 --- a/modules/modes/index.js +++ b/modules/modes/index.js @@ -4,7 +4,7 @@ export { modeAddPoint } from './add_point'; export { modeAddNote } from './add_note'; export { modeBrowse } from './browse'; export { modeDragNode } from './drag_node'; -export { modeDragNote2 } from './drag_note2'; +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 c16b5c711..5c4e1d195 100644 --- a/modules/modes/select_note.js +++ b/modules/modes/select_note.js @@ -12,6 +12,10 @@ import { behaviorDrag } from '../behavior'; +<<<<<<< HEAD +======= +import { modeDragNote } from '../modes'; +>>>>>>> drag-note import { services } from '../services'; import { modeBrowse } from './browse'; @@ -40,7 +44,11 @@ export function modeSelectNote(context, selectedNoteID) { behaviorHover(context), behaviorSelect(context), behaviorLasso(context), +<<<<<<< HEAD // modeDragNote(context).restoreSelectedNoteIDs(selectedNoteID).behavior TAH - re-add +======= + modeDragNote(context).behavior +>>>>>>> drag-note ]; var newFeature = false; @@ -81,6 +89,7 @@ export function modeSelectNote(context, selectedNoteID) { } else { selection .classed('selected', true); + context.selectedNoteID(selectedNoteID); } } @@ -101,13 +110,13 @@ export function modeSelectNote(context, selectedNoteID) { d3_select(document) .call(keybinding); + selectNote(); + context.ui().sidebar .show(noteEditor.note(note)); context.map() .on('drawn.select', selectNote); - - selectNote(); }; diff --git a/modules/osm/note.js b/modules/osm/note.js index 0b7b98d6f..ed03c0504 100644 --- a/modules/osm/note.js +++ b/modules/osm/note.js @@ -58,7 +58,7 @@ _extend(osmNote.prototype, { }, move: function(loc) { - return this.update({loc: 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/renderer/tile_layer.js b/modules/renderer/tile_layer.js index 475147e43..d5c59e9b9 100644 --- a/modules/renderer/tile_layer.js +++ b/modules/renderer/tile_layer.js @@ -1,15 +1,14 @@ import { select as d3_select } from 'd3-selection'; import { t } from '../util/locale'; -import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile'; import { geoScaleToZoom, geoVecLength } from '../geo'; -import { utilPrefixCSSProperty } from '../util'; +import { utilPrefixCSSProperty, utilTile } from '../util'; export function rendererTileLayer(context) { var tileSize = 256; var transformProp = utilPrefixCSSProperty('Transform'); - var geotile = d3_geoTile(); + var geotile = utilTile(); var _projection; var _cache = {}; diff --git a/modules/services/mapillary.js b/modules/services/mapillary.js index e264d5cca..cdf04aa3b 100644 --- a/modules/services/mapillary.js +++ b/modules/services/mapillary.js @@ -18,10 +18,12 @@ import { import rbush from 'rbush'; -import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile'; import { geoExtent } from '../geo'; import { svgDefs } from '../svg'; -import { utilQsString, utilRebind } from '../util'; +import { utilDetect } from '../util/detect'; +import { utilQsString, utilRebind, utilTile } from '../util'; + +var geoTile = utilTile(); var apibase = 'https://a.mapillary.com/v3/'; var viewercss = 'mapillary-js/mapillary.min.css'; @@ -63,33 +65,18 @@ function maxPageAtZoom(z) { if (z > 18) return 80; } -function getTiles(projection) { - var s = projection.scale() * 2 * Math.PI; - var z = Math.max(Math.log(s) / Math.log(2) - 8, 0); - var ts = 256 * Math.pow(2, z - tileZoom); - var origin = [ - s / 2 - projection.translate()[0], - s / 2 - projection.translate()[1] - ]; - return d3_geoTile() - .scaleExtent([tileZoom, tileZoom]) - .scale(s) - .size(projection.clipExtent()[1]) - .translate(projection.translate())() - .map(function(tile) { - var x = tile[0] * ts - origin[0]; - var y = tile[1] * ts - origin[1]; - - return { - id: tile.toString(), - xyz: tile, - extent: geoExtent( - projection.invert([x, y + ts]), - projection.invert([x + ts, y]) - ) - }; - }); +function localeTimestamp(s) { + if (!s) return null; + var detected = utilDetect(); + var options = { + day: 'numeric', month: 'short', year: 'numeric', + hour: 'numeric', minute: 'numeric', second: 'numeric', + timeZone: 'UTC' + }; + var d = new Date(s); + if (isNaN(d.getTime())) return null; + return d.toLocaleString(detected.locale, options); } @@ -97,15 +84,11 @@ function loadTiles(which, url, projection) { var s = projection.scale() * 2 * Math.PI; var currZoom = Math.floor(Math.max(Math.log(s) / Math.log(2) - 8, 0)); - var tiles = getTiles(projection).filter(function(t) { - return !nearNullIsland(t.xyz[0], t.xyz[1], t.xyz[2]); - }); + var dimension = projection.clipExtent()[1]; + var tiles = geoTile.getTiles(projection, dimension, tileZoom, 0); + tiles = geoTile.filterNullIsland(tiles); - _filter(which.inflight, function(v, k) { - var wanted = _find(tiles, function(tile) { return k === (tile.id + ',0'); }); - if (!wanted) delete which.inflight[k]; - return !wanted; - }).map(abortRequest); + geoTile.removeInflightRequests(which, tiles, abortRequest, ',0'); tiles.forEach(function(tile) { loadNextTilePage(which, currZoom, url, tile); diff --git a/modules/services/openstreetcam.js b/modules/services/openstreetcam.js index 99be60e12..73483dddc 100644 --- a/modules/services/openstreetcam.js +++ b/modules/services/openstreetcam.js @@ -22,9 +22,9 @@ import { import rbush from 'rbush'; -import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile'; import { geoExtent } from '../geo'; +import { utilTile } from '../util'; import { utilDetect } from '../util/detect'; import { @@ -33,6 +33,7 @@ import { utilSetTransform } from '../util'; +var geoTile = utilTile(); var apibase = 'https://openstreetcam.org'; var maxResults = 1000; @@ -74,48 +75,15 @@ function maxPageAtZoom(z) { } -function getTiles(projection) { - var s = projection.scale() * 2 * Math.PI, - z = Math.max(Math.log(s) / Math.log(2) - 8, 0), - ts = 256 * Math.pow(2, z - tileZoom), - origin = [ - s / 2 - projection.translate()[0], - s / 2 - projection.translate()[1]]; - - return d3_geoTile() - .scaleExtent([tileZoom, tileZoom]) - .scale(s) - .size(projection.clipExtent()[1]) - .translate(projection.translate())() - .map(function(tile) { - var x = tile[0] * ts - origin[0], - y = tile[1] * ts - origin[1]; - - return { - id: tile.toString(), - xyz: tile, - extent: geoExtent( - projection.invert([x, y + ts]), - projection.invert([x + ts, y]) - ) - }; - }); -} - - function loadTiles(which, url, projection) { var s = projection.scale() * 2 * Math.PI, currZoom = Math.floor(Math.max(Math.log(s) / Math.log(2) - 8, 0)); - var tiles = getTiles(projection).filter(function(t) { - return !nearNullIsland(t.xyz[0], t.xyz[1], t.xyz[2]); - }); + var dimension = projection.clipExtent()[1]; + var tiles = geoTile.getTiles(projection, dimension, tileZoom, 0); + tiles = geoTile.filterNullIsland(tiles); - _filter(which.inflight, function(v, k) { - var wanted = _find(tiles, function(tile) { return k === (tile.id + ',0'); }); - if (!wanted) delete which.inflight[k]; - return !wanted; - }).map(abortRequest); + geoTile.removeInflightRequests(which, tiles, abortRequest, ',0'); tiles.forEach(function(tile) { loadNextTilePage(which, currZoom, url, tile); diff --git a/modules/services/osm.js b/modules/services/osm.js index 7b641715f..74e9a5cb8 100644 --- a/modules/services/osm.js +++ b/modules/services/osm.js @@ -17,7 +17,6 @@ import { xml as d3_xml } from 'd3-request'; import osmAuth from 'osm-auth'; import { JXON } from '../util/jxon'; -import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile'; import { geoExtent, geoVecAdd } from '../geo'; import { @@ -31,9 +30,11 @@ import { import { utilRebind, utilIdleWorker, + utilTile, utilQsString } from '../util'; +var geoTile = utilTile(); var dispatch = d3_dispatch('authLoading', 'authDone', 'change', 'loading', 'loaded', 'loadedNotes'); var urlroot = 'https://www.openstreetmap.org'; @@ -326,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) { @@ -789,44 +804,13 @@ export default { tilezoom = _tileZoom; } - var s = projection.scale() * 2 * Math.PI; - var z = Math.max(Math.log(s) / Math.log(2) - 8, 0); - var ts = 256 * Math.pow(2, z - tilezoom); - var origin = [ - s / 2 - projection.translate()[0], - s / 2 - projection.translate()[1] - ]; - - // what tiles cover the view? - var tiler = d3_geoTile() - .scaleExtent([tilezoom, tilezoom]) - .scale(s) - .size(dimensions) - .translate(projection.translate()); - - var tiles = tiler().map(function(tile) { - var x = tile[0] * ts - origin[0]; - var y = tile[1] * ts - origin[1]; - - return { - id: tile.toString(), - extent: geoExtent( - projection.invert([x, y + ts]), - projection.invert([x + ts, y]) - ) - }; - }); + // get tiles + var tiles = geoTile.getTiles(projection, dimensions, tilezoom, 0); + tiles = geoTile.filterNullIsland(tiles); // remove inflight requests that no longer cover the view.. var hadRequests = !_isEmpty(cache.inflight); - _filter(cache.inflight, function(v, i) { - var wanted = _find(tiles, function(tile) { return i === tile.id; }); - if (!wanted) { - delete cache.inflight[i]; - } - return !wanted; - }).map(abortRequest); - + geoTile.removeInflightRequests(cache, tiles, abortRequest); if (hadRequests && !loadingNotes && _isEmpty(cache.inflight)) { dispatch.call('loaded'); // stop the spinner } @@ -877,7 +861,43 @@ export default { // Create a note // POST /api/0.6/notes?params postNoteCreate: function(note, callback) { - // todo + if (!this.authenticated()) { + return callback({ message: 'Not Authenticated', status: -3 }, note); + } + if (_noteCache.inflightPost[note.id]) { + return callback({ message: 'Note update already inflight', status: -2 }, note); + } + + if (!note.loc[0] || !note.loc[1] || !note.newComment) return; // location & description required + + var path = '/api/0.6/notes?' + + 'lat=' + note.loc[1] + + '&lon=' + note.loc[0] + + '&' + utilQsString({ text: note.newComment }); + _noteCache.inflightPost[note.id] = oauth.xhr( + { method: 'POST', path: path }, + wrapcb(this, done, _connectionID) + ); + + + function done(err, xml) { + delete _noteCache.inflightPost[note.id]; + if (err) { return callback(err); } + + // we get the updated note back, remove from caches and reparse.. + var item = encodeNoteRtree(note); + _noteCache.rtree.remove(item, function isEql(a, b) { return a.data.id === b.data.id; }); + delete _noteCache.note[note.id]; + + var options = { skipSeen: false }; + return parseXML(xml, function(err, results) { + if (err) { + return callback(err); + } else { + return callback(undefined, results[0]); + } + }, options); + } }, @@ -900,6 +920,7 @@ export default { action = 'reopen'; } else { action = 'comment'; + if (!note.newComment) return; // when commenting, comment required } var path = '/api/0.6/notes/' + note.id + '/' + action; @@ -1063,23 +1084,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/services/streetside.js b/modules/services/streetside.js index 040f66b6c..52ee2c14f 100644 --- a/modules/services/streetside.js +++ b/modules/services/streetside.js @@ -17,7 +17,6 @@ import { import rbush from 'rbush'; import { t } from '../util/locale'; import { jsonpRequest } from '../util/jsonp_request'; -import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile'; import { geoExtent, @@ -29,10 +28,12 @@ import { } from '../geo'; import { utilDetect } from '../util/detect'; -import { utilQsString, utilRebind } from '../util'; +import { utilQsString, utilRebind, utilTile } from '../util'; import Q from 'q'; +var geoTile = utilTile(); + var bubbleApi = 'https://dev.virtualearth.net/mapcontrol/HumanScaleServices/GetBubbles.ashx?'; var streetsideImagesApi = 'https://t.ssl.ak.tiles.virtualearth.net/tiles/'; var bubbleAppKey = 'AuftgJsO0Xs8Ts4M1xZUQJQXJNsvmh3IV8DkNieCiy3tCwCUMq76-WpkrBtNAuEm'; @@ -85,46 +86,6 @@ function localeTimestamp(s) { return d.toLocaleString(detected.locale, options); } -/** - * getTiles() returns array of d3 geo tiles. - * Using d3.geo.tiles.js from lib, gets tile extents for each grid tile in a grid created from - * an area around (and including) the current map view extents. - */ -function getTiles(projection, margin) { - // s is the current map scale - // z is the 'Level of Detail', or zoom-level, where Level 1 is far from the earth, and Level 23 is close to the ground. - // ts ('tile size') here is the formula for determining the width/height of the map in pixels, but with a modification. - // See 'Ground Resolution and Map Scale': //https://msdn.microsoft.com/en-us/library/bb259689.aspx. - // As used here, by subtracting constant 'tileZoom' from z (the level), you end up with a much smaller value for the tile size (in pixels). - var s = projection.scale() * 2 * Math.PI; - var z = Math.max(Math.log(s) / Math.log(2) - 8, 0); - var ts = 256 * Math.pow(2, z - tileZoom); - var origin = [ - s / 2 - projection.translate()[0], - s / 2 - projection.translate()[1] - ]; - - var tiler = d3_geoTile() - .scaleExtent([tileZoom, tileZoom]) - .scale(s) - .size(projection.clipExtent()[1]) - .translate(projection.translate()) - .margin(margin || 0); // request nearby tiles so we can connect sequences. - - return tiler() - .map(function(tile) { - var x = tile[0] * ts - origin[0]; - var y = tile[1] * ts - origin[1]; - return { - id: tile.toString(), - xyz: tile, - extent: geoExtent( - projection.invert([x, y + ts]), - projection.invert([x + ts, y]) - ) - }; - }); -} /** * loadTiles() wraps the process of generating tiles and then fetching image points for each tile. @@ -133,10 +94,9 @@ function loadTiles(which, url, projection, margin) { var s = projection.scale() * 2 * Math.PI; var currZoom = Math.floor(Math.max(Math.log(s) / Math.log(2) - 8, 0)); - // breakup the map view into tiles - var tiles = getTiles(projection, margin).filter(function (t) { - return !nearNullIsland(t.xyz[0], t.xyz[1], t.xyz[2]); - }); + var dimension = projection.clipExtent()[1]; + var tiles = geoTile.getTiles(projection, dimension, tileZoom, margin); + tiles = geoTile.filterNullIsland(tiles); tiles.forEach(function (tile) { loadNextTilePage(which, currZoom, url, tile); 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 8a93b9ce7..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) { @@ -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..10c78ab9e 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 @@ -318,7 +322,7 @@ export function uiNoteEditor(context) { this.blur(); // avoid keeping focus on the button - #4641 var osm = services.osm; if (osm) { - osm.postNoteAdd(d, d.status, function(err, note) { + osm.postNoteCreate(d, function(err, note) { dispatch.call('change', note); }); } diff --git a/modules/util/index.js b/modules/util/index.js index f64856de8..9673bfef5 100644 --- a/modules/util/index.js +++ b/modules/util/index.js @@ -23,5 +23,6 @@ export { utilSessionMutex } from './session_mutex'; export { utilStringQs } from './util'; export { utilSuggestNames } from './suggest_names'; export { utilTagText } from './util'; +export { utilTile } from './tile'; export { utilTriggerEvent } from './trigger_event'; export { utilWrap } from './util'; diff --git a/modules/util/tile.js b/modules/util/tile.js new file mode 100644 index 000000000..2179525f7 --- /dev/null +++ b/modules/util/tile.js @@ -0,0 +1,179 @@ +import _filter from 'lodash-es/filter'; +import _find from 'lodash-es/find'; +import { range as d3_range } from 'd3-array'; +import { geoExtent } from '../geo'; + + +export function utilTile() { + var _size = [960, 500]; + var _scale = 256; + var _scaleExtent = [0, 20]; + var _translate = [_size[0] / 2, _size[1] / 2]; + var _zoomDelta = 0; + var _margin = 0; + + function bound(val) { + return Math.min(_scaleExtent[1], Math.max(_scaleExtent[0], val)); + } + + function nearNullIsland(x, y, z) { + if (z >= 7) { + var center = Math.pow(2, z - 1); + var width = Math.pow(2, z - 6); + var min = center - (width / 2); + var max = center + (width / 2) - 1; + return x >= min && x <= max && y >= min && y <= max; + } + return false; + } + + function tile() { + var z = Math.max(Math.log(_scale) / Math.LN2 - 8, 0); + var z0 = bound(Math.round(z + _zoomDelta)); + var k = Math.pow(2, z - z0 + 8); + var origin = [ + (_translate[0] - _scale / 2) / k, + (_translate[1] - _scale / 2) / k + ]; + + var cols = d3_range( + Math.max(0, Math.floor(-origin[0]) - _margin), + Math.max(0, Math.ceil(_size[0] / k - origin[0]) + _margin) + ); + var rows = d3_range( + Math.max(0, Math.floor(-origin[1]) - _margin), + Math.max(0, Math.ceil(_size[1] / k - origin[1]) + _margin) + ); + + var tiles = []; + for (var i = 0; i < rows.length; i++) { + var y = rows[i]; + for (var j = 0; j < cols.length; j++) { + var x = cols[j]; + + if (i >= _margin && i <= rows.length - _margin && + j >= _margin && j <= cols.length - _margin) { + tiles.unshift([x, y, z0]); // tiles in view at beginning + } else { + tiles.push([x, y, z0]); // tiles in margin at the end + } + } + } + + tiles.translate = origin; + tiles.scale = k; + + return tiles; + } + + + /** + * getTiles() returns array of d3 geo tiles. + * Using d3.geo.tiles.js from lib, gets tile extents for each grid tile in a grid created from + * an area around (and including) the current map view extents. + */ + tile.getTiles = function(projection, dimensions, tilezoom, margin) { + + // s is the current map scale + // z is the 'Level of Detail', or zoom-level, where Level 1 is far from the earth, and Level 23 is close to the ground. + // ts ('tile size') here is the formula for determining the width/height of the map in pixels, but with a modification. + // See 'Ground Resolution and Map Scale': //https://msdn.microsoft.com/en-us/library/bb259689.aspx. + // As used here, by subtracting constant 'tileZoom' from z (the level), you end up with a much smaller value for the tile size (in pixels). + var s = projection.scale() * 2 * Math.PI; + var z = Math.max(Math.log(s) / Math.log(2) - 8, 0); + var ts = 256 * Math.pow(2, z - tilezoom); + var origin = [ + s / 2 - projection.translate()[0], + s / 2 - projection.translate()[1] + ]; + + var tiler = this + .scaleExtent([tilezoom, tilezoom]) + .scale(s) + .size(dimensions) + .translate(projection.translate()) + .margin(margin || 0); // request nearby tiles so we can connect sequences. + + var tiles = tiler() + .map(function(tile) { + var x = tile[0] * ts - origin[0]; + var y = tile[1] * ts - origin[1]; + + return { + id: tile.toString(), + xyz: tile, + extent: geoExtent( + projection.invert([x, y + ts]), + projection.invert([x + ts, y]) + ) + }; + }); + + return tiles; + }; + + + tile.filterNullIsland = function(tiles) { + return tiles.filter(function(t) { + return !nearNullIsland(t.xyz[0], t.xyz[1], t.xyz[2]); + }); + }; + + + // remove inflight requests that no longer cover the view.. + tile.removeInflightRequests = function(cache, tiles, callback, modifier) { + return _filter(cache.inflight, function(v, i) { + var wanted = _find(tiles, function(tile) { return i === tile.id + modifier; }); + if (!wanted) { + delete cache.inflight[i]; + } + return !wanted; + }).map(callback); // abort request + }; + + + tile.scaleExtent = function(val) { + if (!arguments.length) return _scaleExtent; + _scaleExtent = val; + return tile; + }; + + + tile.size = function(val) { + if (!arguments.length) return _size; + _size = val; + return tile; + }; + + + tile.scale = function(val) { + if (!arguments.length) return _scale; + _scale = val; + return tile; + }; + + + tile.translate = function(val) { + if (!arguments.length) return _translate; + _translate = val; + return tile; + }; + + + tile.zoomDelta = function(val) { + if (!arguments.length) return _zoomDelta; + _zoomDelta = +val; + return tile; + }; + + + // number to extend the rows/columns beyond those covering the viewport + tile.margin = function(val) { + if (!arguments.length) return _margin; + _margin = +val; + return tile; + }; + + + return tile; +} diff --git a/package.json b/package.json index 4c914d053..6148cea94 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "json-stringify-pretty-compact": "^1.1.0", "jsonschema": "^1.1.0", "mapillary-js": "2.12.1", - "mapillary_sprite_source": "^1.4.0", + "mapillary_sprite_source": "^1.5.0", "minimist": "^1.2.0", "mocha": "^5.0.0", "mocha-phantomjs-core": "^2.1.0",