diff --git a/modules/services/index.js b/modules/services/index.js index 789628ed5..646ba2e7a 100644 --- a/modules/services/index.js +++ b/modules/services/index.js @@ -1,5 +1,6 @@ import serviceMapillary from './mapillary'; import serviceNominatim from './nominatim'; +import serviceNotes from './notes'; import serviceOpenstreetcam from './openstreetcam'; import serviceOsm from './osm'; import serviceStreetside from './streetside'; @@ -10,6 +11,7 @@ import serviceWikipedia from './wikipedia'; export var services = { geocoder: serviceNominatim, mapillary: serviceMapillary, + notes: serviceNotes, openstreetcam: serviceOpenstreetcam, osm: serviceOsm, streetside: serviceStreetside, diff --git a/modules/services/notes.js b/modules/services/notes.js new file mode 100644 index 000000000..dc6664550 --- /dev/null +++ b/modules/services/notes.js @@ -0,0 +1,175 @@ +import _extend from 'lodash-es/extend'; +import _filter from 'lodash-es/filter'; +import _find from 'lodash-es/find'; +import _forEach from 'lodash-es/forEach'; +import _isEmpty from 'lodash-es/isEmpty'; + +import osmAuth from 'osm-auth'; + +import rbush from 'rbush'; + +var _entityCache = {}; + +import { dispatch as d3_dispatch } from 'd3-dispatch'; +import { xml as d3_xml } from 'd3-request'; + +import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile'; +import { geoExtent } from '../geo'; + +import { + utilRebind, + utilIdleWorker +} from '../util'; + +var urlroot = 'https://api.openstreetmap.org', + _notesCache = { notes: { inflight: {}, loaded: {} } }, + __notesSelectedNote, + dispatch = d3_dispatch('loadedNotes', 'loading'), + tileZoom = 14; + +var oauth = osmAuth({ + url: urlroot, + oauth_consumer_key: '5A043yRSEugj4DJ5TljuapfnrflWDte8jTOcWLlT', + oauth_secret: 'aB3jKq1TRsCOUrfOIZ6oQMEDmv2ptV76PA54NGLL', + loading: authLoading, + done: authDone +}); + +function authLoading() { + dispatch.call('authLoading'); +} + + +function authDone() { + dispatch.call('authDone'); +} + +function abortRequest(i) { + i.abort(); +} + +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 nearNullIsland(x, y, z) { + if (z >= 7) { + var center = Math.pow(2, z - 1), + width = Math.pow(2, z - 6), + min = center - (width / 2), + max = center + (width / 2) - 1; + return x >= min && x <= max && y >= min && y <= max; + } + return false; +} + +export default { + + init: function() { + if (!_notesCache) { + this.reset(); + } + + this.event = utilRebind(this, dispatch, 'on'); + }, + + reset: function() { + var cache = _notesCache; + + if (cache) { + if (cache.notes && cache.notes.inflight) { + _forEach(cache.notes.inflight, abortRequest); + } + } + + _notesCache = { notes: { inflight: {}, loaded: {} } }; + + __notesSelectedNote = null; + }, + + authenticated: function() { + return oauth.authenticated(); + }, + + loadFromAPI(path, callback, options) { + options = _extend({ cache: true }, options); + + function done(err, xml) {} + + if (this.authenticated()) { + return oauth.xhr({ method: 'GET', path: path }, done); + } else { + return d3_xml(path).get(done); + } + }, + + loadTile(which, currZoom, url, tile) { + var cache = _notesCache[which]; + var bbox = tile.extent.toParam(); + + var id = tile.id; + + if (cache.loaded[id] || cache.inflight[id]) return; + + if (_isEmpty(cache.inflight)) { + dispatch.call('loading'); + } + + cache.inflight[id] = this.loadFromAPI( + url + bbox, + function () { + + }, + [] + ); + }, + + loadTiles(which, url, projection) { + var that = this; + 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]); + }); + + _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); + + tiles.forEach(function(tile) { + that.loadTile(which, currZoom, url, tile); + }); + }, + + loadNotes: function(projection) { + var url = urlroot + '/api/0.6/notes?bbox='; + this.loadTiles('notes', url, projection); + } +}; \ No newline at end of file diff --git a/modules/svg/notes.js b/modules/svg/notes.js index 070dd3b49..7d2fd3b35 100644 --- a/modules/svg/notes.js +++ b/modules/svg/notes.js @@ -1,31 +1,121 @@ +import _throttle from 'lodash-es/throttle'; +import { select as d3_select } from 'd3-selection'; +import { services } from '../services'; + export function svgNotes(projection, context, dispatch) { - var enabled = false; + var throttledRedraw = _throttle(function () { dispatch.call('change'); }, 1000); + var minZoom = 12; + var minMarkerZoom = 16; + var minViewfieldZoom = 18; + var layer = d3_select(null); + var _notes; - function drawNotes() { + function init() { + if (svgNotes.initialized) return; // run once + svgNotes.enabled = false; + svgNotes.initialized = true; + } + function editOn() { + layer.style('display', 'block'); + } + + + function editOff() { + layer.selectAll('.viewfield-group').remove(); + layer.style('display', 'none'); + } + + function getService() { + if (services.notes && !_notes) { + _notes = services.notes; + _notes.event.on('loadedNotes', throttledRedraw); + } else if (!services.notes && _notes) { + _notes = null; + } + + return _notes; } function showLayer() { + var service = getService(); + if (!service) return; + // service.loadViewer(context); + editOn(); + + layer + .style('opacity', 0) + .transition() + .duration(250) + .style('opacity', 1) + .on('end', function () { dispatch.call('change'); }); } function hideLayer() { + var service = getService(); + if (service) { + // service.hideViewer(); + } + throttledRedraw.cancel(); + + layer + .transition() + .duration(250) + .style('opacity', 0) + .on('end', editOff); + } + + function drawNotes(selection) { + var enabled = svgNotes.enabled, + service = getService(); + + layer = selection.selectAll('.layer-notes') + .data(service ? [0] : []); + + layer.exit() + .remove(); + + var layerEnter = layer.enter() + .append('g') + .attr('class', 'layer-notes') + .style('display', enabled ? 'block' : 'none'); + + // layerEnter + // .append('g') + // .attr('class', 'sequences'); + + layerEnter + .append('g') + .attr('class', 'notes'); + + layer = layerEnter + .merge(layer); + + if (enabled) { + if (service && ~~context.map().zoom() >= minZoom) { + editOn(); + // update(); + service.loadNotes(projection); + } else { + editOff(); + } + } } drawNotes.enabled = function(_) { - if (!arguments.length) return enabled; - enabled = _; - - if (enabled) { + if (!arguments.length) return svgNotes.enabled; + svgNotes.enabled = _; + if (svgNotes.enabled) { showLayer(); } else { hideLayer(); } - - dispatch('change'); + dispatch.call('change'); return this; }; + init(); return drawNotes; } \ No newline at end of file