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.
This commit is contained in:
Bryan Housel
2019-04-09 23:49:31 -04:00
parent 95a1bbaf97
commit e30090996b
4 changed files with 144 additions and 78 deletions

View File

@@ -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);
};

View File

@@ -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;
}
};

View File

@@ -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) {

View File

@@ -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();
});