Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 34 additions & 10 deletions modules/services/osm.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ var _rateLimitError;
var _userChangesets;
var _userDetails;
var _off;
var _isLoading = false;
var _maxSubdivisionDepth = 3;

// set a default but also load this from the API status
var _maxWayNodes = 2000;
Expand Down Expand Up @@ -553,6 +555,7 @@ export default {
_userChangesets = undefined;
_userDetails = undefined;
_rateLimitError = undefined;
_isLoading = false;

Object.values(_tileCache.inflight).forEach(abortRequest);
Object.values(_noteCache.inflight).forEach(abortRequest);
Expand Down Expand Up @@ -1102,11 +1105,13 @@ export default {

// Load a single data tile
// GET /api/0.6/map?bbox=
loadTile: function(tile, callback) {
loadTile: function(tile, callback, depth) {
depth = depth || 0;
if (_off) return;
if (_tileCache.loaded[tile.id] || _tileCache.inflight[tile.id]) return;

if (!hasInflightRequests(_tileCache)) {
if (!hasInflightRequests(_tileCache) && !_isLoading) {
_isLoading = true;
dispatch.call('loading'); // start the spinner
}

Expand All @@ -1128,19 +1133,37 @@ export default {
bbox.id = tile.id;
_tileCache.rtree.insert(bbox);
} else {
// map tile loading error: e.g. network connection error,
// 509 Bandwidth Limit Exceeded, 429 Too Many Requests
if (!_rateLimitError && err.status === 509 || err.status === 429) {
// show "API rate limiting" warning

// 400 → subdivision (50k node limit)
if (err && err.status === 400) {

delete _tileCache.inflight[tile.id];
delete _tileCache.toLoad[tile.id];

if (depth < _maxSubdivisionDepth) {
var quadrants = tile.extent.split();
quadrants.forEach(function(extent, i) {
var childTile = {
id: tile.id + '-' + depth + '-' + i,
extent: extent
};
this.loadTile(childTile, callback, depth + 1);
}.bind(this));
return;
}

} else if (!_rateLimitError && (err.status === 509 || err.status === 429)) {

_rateLimitError = err;
dispatch.call('change');
this.reloadApiStatus();

}
setTimeout(() => {
// retry loading the tiles

setTimeout(function() {
delete _tileCache.inflight[tile.id];
this.loadTile(tile, callback);
}, 8000);
this.loadTile(tile, callback, depth);
}.bind(this), 8000);
}
if (callback) {
callback(err, Object.assign({ data: parsed }, tile));
Expand All @@ -1152,6 +1175,7 @@ export default {
dispatch.call('change');
this.reloadApiStatus();
}
_isLoading = false;
dispatch.call('loaded'); // stop the spinner
}
}
Expand Down
97 changes: 97 additions & 0 deletions test/spec/services/osm_subdivision.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import osmService from '../../../modules/services/osm.js';
import { geoExtent } from '../../../modules/geo/index.js';

describe('OSM 400 tile subdivision', () => {

let service;
let fakeTile;

beforeEach(() => {
service = osmService;
service.reset();

fakeTile = {
id: '0,0,16',
extent: geoExtent([[0, 0], [1, 1]])
};
});


it('subdivides only once on initial 400', () => {

let callCount = 0;

vi.spyOn(service, 'loadFromAPI').mockImplementation((path, cb) => {

callCount++;

// Only first call returns 400
if (callCount === 1) {
cb({
status: 400,
statusText: 'You requested too many nodes (limit is 50000)'
});
} else {
cb(null, []); // children succeed
}

return { abort: () => {} }; // important
});

const spy = vi.spyOn(service, 'loadTile');

service.loadTile(fakeTile, () => {});

// 1 original + 4 children
expect(spy).toHaveBeenCalledTimes(5);

spy.mockRestore();
});


it('does not subdivide beyond max depth', () => {

vi.spyOn(service, 'loadFromAPI').mockImplementation((path, cb) => {
cb({
status: 400,
statusText: 'You requested too many nodes (limit is 50000)'
});
return { abort: () => {} };
});

const spy = vi.spyOn(service, 'loadTile');

service.loadTile(fakeTile, () => {}, 3);

// should not create children
expect(spy).toHaveBeenCalledTimes(1);

spy.mockRestore();
});


it('retries for non-400 errors', () => {

vi.useFakeTimers();

let retryTriggered = false;

vi.spyOn(service, 'loadFromAPI').mockImplementation((path, cb) => {
cb({ status: 429 });
return { abort: () => {} };
});

const spy = vi.spyOn(service, 'loadTile');

service.loadTile(fakeTile, () => {});

vi.runAllTimers();

expect(spy).toHaveBeenCalled();

spy.mockRestore();
vi.useRealTimers();
});

});