From e30090996bf56882c275b80463fecccf5c9baebf Mon Sep 17 00:00:00 2001 From: Bryan Housel Date: Tue, 9 Apr 2019 23:49:31 -0400 Subject: [PATCH] Add `loadTileAtLoc` to fetch data tile for a specific location (closes #4890) This lets iD request needed tiles outside of the viewport, for example to properly straighten lines or validate features that may have unloaded connections. --- modules/core/context.js | 90 ++++++++++++++++--------- modules/operations/straighten.js | 17 ++++- modules/services/osm.js | 111 +++++++++++++++++++------------ test/spec/services/osm.js | 4 +- 4 files changed, 144 insertions(+), 78 deletions(-) diff --git a/modules/core/context.js b/modules/core/context.js index dfda07f71..d4e7e6272 100644 --- a/modules/core/context.js +++ b/modules/core/context.js @@ -107,35 +107,63 @@ export function coreContext() { return context; }; - context.loadTiles = utilCallWhenIdle(function(projection, callback) { + + function wrapcb(callback, cid) { + return function(err, result) { + if (err) { + // 400 Bad Request, 401 Unauthorized, 403 Forbidden.. + if (err.status === 400 || err.status === 401 || err.status === 403) { + connection.logout(); + } + return callback.call(context, err); + + } else if (connection.getConnectionId() !== cid) { + return callback.call(context, { message: 'Connection Switched', status: -1 }); + + } else { + return callback.call(context, err, result); + } + }; + } + + + context.loadTiles = function(projection, callback) { var cid; function done(err, result) { - if (connection.getConnectionId() !== cid) { - if (callback) callback({ message: 'Connection Switched', status: -1 }); - return; - } if (!err) history.merge(result.data, result.extent); if (callback) callback(err, result); } if (connection && context.editable()) { cid = connection.getConnectionId(); - connection.loadTiles(projection, done); + utilCallWhenIdle(function() { + connection.loadTiles(projection, wrapcb(done, cid)); + })(); } - }); + }; + + context.loadTileAtLoc = function(loc, callback) { + var cid; + function done(err, result) { + if (!err) history.merge(result.data, result.extent); + if (callback) callback(err, result); + } + if (connection && context.editable()) { + cid = connection.getConnectionId(); + utilCallWhenIdle(function() { + connection.loadTileAtLoc(loc, wrapcb(done, cid)); + })(); + } + }; context.loadEntity = function(entityID, callback) { var cid; function done(err, result) { - if (connection.getConnectionId() !== cid) { - if (callback) callback({ message: 'Connection Switched', status: -1 }); - return; - } if (!err) history.merge(result.data, result.extent); if (callback) callback(err, result); } if (connection) { cid = connection.getConnectionId(); - connection.loadEntity(entityID, done); + connection.loadEntity(entityID, wrapcb(done, cid)); } }; @@ -166,11 +194,11 @@ export function coreContext() { }; var minEditableZoom = 16; - context.minEditableZoom = function(_) { + context.minEditableZoom = function(val) { if (!arguments.length) return minEditableZoom; - minEditableZoom = _; + minEditableZoom = val; if (connection) { - connection.tileZoom(_); + connection.tileZoom(val); } return context; }; @@ -178,9 +206,9 @@ export function coreContext() { /* History */ var inIntro = false; - context.inIntro = function(_) { + context.inIntro = function(val) { if (!arguments.length) return inIntro; - inIntro = _; + inIntro = val; return context; }; @@ -284,9 +312,9 @@ export function coreContext() { /* Copy/Paste */ var copyIDs = [], copyGraph; context.copyGraph = function() { return copyGraph; }; - context.copyIDs = function(_) { + context.copyIDs = function(val) { if (!arguments.length) return copyIDs; - copyIDs = _; + copyIDs = val; copyGraph = history.graph(); return context; }; @@ -355,42 +383,42 @@ export function coreContext() { /* Container */ var container = d3_select(document.body); - context.container = function(_) { + context.container = function(val) { if (!arguments.length) return container; - container = _; + container = val; container.classed('id-container', true); return context; }; var embed; - context.embed = function(_) { + context.embed = function(val) { if (!arguments.length) return embed; - embed = _; + embed = val; return context; }; /* Assets */ var assetPath = ''; - context.assetPath = function(_) { + context.assetPath = function(val) { if (!arguments.length) return assetPath; - assetPath = _; + assetPath = val; return context; }; var assetMap = {}; - context.assetMap = function(_) { + context.assetMap = function(val) { if (!arguments.length) return assetMap; - assetMap = _; + assetMap = val; return context; }; - context.asset = function(_) { - var filename = assetPath + _; + context.asset = function(val) { + var filename = assetPath + val; return assetMap[filename] || filename; }; - context.imagePath = function(_) { - return context.asset('img/' + _); + context.imagePath = function(val) { + return context.asset('img/' + val); }; diff --git a/modules/operations/straighten.js b/modules/operations/straighten.js index 74d20f681..da7c2bd51 100644 --- a/modules/operations/straighten.js +++ b/modules/operations/straighten.js @@ -63,16 +63,29 @@ export function operationStraighten(selectedIDs, context) { operation.disabled = function() { - var osm = context.connection(); var reason = action.disabled(context.graph()); if (reason) { return reason; - } else if (osm && !coords.every(osm.isDataLoaded)) { + } else if (someMissing()) { return 'not_downloaded'; } else if (selectedIDs.some(context.hasHiddenConnections)) { return 'connected_to_hidden'; } + return false; + + + function someMissing() { + var osm = context.connection(); + if (osm) { + var missing = coords.filter(function(loc) { return !osm.isDataLoaded(loc); }); + if (missing.length) { + missing.forEach(function(loc) { context.loadTileAtLoc(loc); }); + return true; + } + } + return false; + } }; diff --git a/modules/services/osm.js b/modules/services/osm.js index 3a3424232..747e747a0 100644 --- a/modules/services/osm.js +++ b/modules/services/osm.js @@ -7,7 +7,7 @@ import osmAuth from 'osm-auth'; import rbush from 'rbush'; import { JXON } from '../util/jxon'; -import { geoExtent, geoVecAdd } from '../geo'; +import { geoExtent, geoRawMercator, geoVecAdd, geoZoomToScale } from '../geo'; import { osmEntity, osmNode, osmNote, osmRelation, osmWay } from '../osm'; import { utilArrayChunk, utilArrayGroupBy, utilArrayUniq, utilRebind, @@ -27,8 +27,8 @@ var oauth = osmAuth({ }); var _blacklists = ['.*\.google(apis)?\..*/(vt|kh)[\?/].*([xyz]=.*){3}.*']; -var _tileCache = { loaded: {}, inflight: {}, seen: {}, rtree: rbush() }; -var _noteCache = { loaded: {}, inflight: {}, inflightPost: {}, note: {}, closed: {}, rtree: rbush() }; +var _tileCache = { toLoad: {}, loaded: {}, inflight: {}, seen: {}, rtree: rbush() }; +var _noteCache = { toLoad: {}, loaded: {}, inflight: {}, inflightPost: {}, note: {}, closed: {}, rtree: rbush() }; var _userCache = { toLoad: {}, user: {} }; var _changeset = {}; @@ -58,13 +58,18 @@ function abortRequest(i) { } -function abortUnwantedRequests(cache, tiles) { +function hasInflightRequests(cache) { + return Object.keys(cache.inflight).length; +} + + +function abortUnwantedRequests(cache, visibleTiles) { Object.keys(cache.inflight).forEach(function(k) { - var wanted = tiles.find(function(tile) { return k === tile.id; }); - if (!wanted) { - abortRequest(cache.inflight[k]); - delete cache.inflight[k]; - } + if (cache.toLoad[k]) return; + if (visibleTiles.find(function(tile) { return k === tile.id; })) return; + + abortRequest(cache.inflight[k]); + delete cache.inflight[k]; }); } @@ -365,8 +370,8 @@ export default { Object.values(_noteCache.inflightPost).forEach(abortRequest); if (_changeset.inflight) abortRequest(_changeset.inflight); - _tileCache = { loaded: {}, inflight: {}, seen: {}, rtree: rbush() }; - _noteCache = { loaded: {}, inflight: {}, inflightPost: {}, note: {}, closed: {}, rtree: rbush() }; + _tileCache = { toLoad: {}, loaded: {}, inflight: {}, seen: {}, rtree: rbush() }; + _noteCache = { toLoad: {}, loaded: {}, inflight: {}, inflightPost: {}, note: {}, closed: {}, rtree: rbush() }; _userCache = { toLoad: {}, user: {} }; _changeset = {}; @@ -774,51 +779,56 @@ export default { loadTiles: function(projection, callback) { if (_off) return; - var that = this; - var path = '/api/0.6/map?bbox='; - // determine the needed tiles to cover the view var tiles = tiler.zoomExtent([_tileZoom, _tileZoom]).getTiles(projection); // abort inflight requests that are no longer needed - var hadRequests = hasInflightRequests(); + var hadRequests = hasInflightRequests(_tileCache); abortUnwantedRequests(_tileCache, tiles); - if (hadRequests && !hasInflightRequests()) { + if (hadRequests && !hasInflightRequests(_tileCache)) { dispatch.call('loaded'); // stop the spinner } // issue new requests.. tiles.forEach(function(tile) { - if (_tileCache.loaded[tile.id] || _tileCache.inflight[tile.id]) return; - if (!hasInflightRequests()) { - dispatch.call('loading'); // start the spinner - } + this.loadTile(tile, callback); + }, this); + }, - var options = { skipSeen: true }; - _tileCache.inflight[tile.id] = that.loadFromAPI( - path + tile.extent.toParam(), - function(err, parsed) { - delete _tileCache.inflight[tile.id]; - if (!err) { - _tileCache.loaded[tile.id] = true; - var bbox = tile.extent.bbox(); - bbox.id = tile.id; - _tileCache.rtree.insert(bbox); - } - if (callback) { - callback(err, Object.assign({ data: parsed }, tile)); - } - if (!hasInflightRequests()) { - dispatch.call('loaded'); // stop the spinner - } - }, - options - ); - }); - function hasInflightRequests() { - return Object.keys(_tileCache.inflight).length; + // Load a single data tile + // GET /api/0.6/map?bbox= + loadTile: function(tile, callback) { + if (_off) return; + if (_tileCache.loaded[tile.id] || _tileCache.inflight[tile.id]) return; + + if (!hasInflightRequests(_tileCache)) { + dispatch.call('loading'); // start the spinner } + + var path = '/api/0.6/map?bbox='; + var options = { skipSeen: true }; + + _tileCache.inflight[tile.id] = this.loadFromAPI( + path + tile.extent.toParam(), + function(err, parsed) { + delete _tileCache.inflight[tile.id]; + if (!err) { + delete _tileCache.toLoad[tile.id]; + _tileCache.loaded[tile.id] = true; + var bbox = tile.extent.bbox(); + bbox.id = tile.id; + _tileCache.rtree.insert(bbox); + } + if (callback) { + callback(err, Object.assign({ data: parsed }, tile)); + } + if (!hasInflightRequests(_tileCache)) { + dispatch.call('loaded'); // stop the spinner + } + }, + options + ); }, @@ -828,6 +838,21 @@ export default { }, + // load the tile that covers the given `loc` + loadTileAtLoc: function(loc, callback) { + var k = geoZoomToScale(_tileZoom + 1); + var offset = geoRawMercator().scale(k)(loc); + var projection = geoRawMercator().transform({ k: k, x: -offset[0], y: -offset[1] }); + var tiles = tiler.zoomExtent([_tileZoom, _tileZoom]).getTiles(projection); + + tiles.forEach(function(tile) { + if (_tileCache.toLoad[tile.id]) return; // already in queue + _tileCache.toLoad[tile.id] = true; + this.loadTile(tile, callback); + }, this); + }, + + // Load notes from the API in tiles // GET /api/0.6/notes?bbox= loadNotes: function(projection, noteOptions) { diff --git a/test/spec/services/osm.js b/test/spec/services/osm.js index 561f25406..fd1f285e3 100644 --- a/test/spec/services/osm.js +++ b/test/spec/services/osm.js @@ -590,8 +590,8 @@ describe('iD.serviceOsm', function () { describe('#caches', function() { it('loads reset caches', function (done) { var caches = connection.caches(); - expect(caches.tile).to.have.all.keys(['loaded','inflight','seen','rtree']); - expect(caches.note).to.have.all.keys(['loaded','inflight','inflightPost','note','closed','rtree']); + expect(caches.tile).to.have.all.keys(['toLoad','loaded','inflight','seen','rtree']); + expect(caches.note).to.have.all.keys(['toLoad','loaded','inflight','inflightPost','note','closed','rtree']); expect(caches.user).to.have.all.keys(['toLoad','user']); done(); });