diff --git a/modules/services/osm.js b/modules/services/osm.js index 00d38e297b9..78fb9054c54 100644 --- a/modules/services/osm.js +++ b/modules/services/osm.js @@ -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; @@ -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); @@ -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 } @@ -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)); @@ -1152,6 +1175,7 @@ export default { dispatch.call('change'); this.reloadApiStatus(); } + _isLoading = false; dispatch.call('loaded'); // stop the spinner } } diff --git a/test/spec/services/osm_subdivision.js b/test/spec/services/osm_subdivision.js new file mode 100644 index 00000000000..57f7aa8be1b --- /dev/null +++ b/test/spec/services/osm_subdivision.js @@ -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(); + }); + +}); \ No newline at end of file