diff --git a/js/source/raster_tile_source.js b/js/source/raster_tile_source.js index 2e6743bab19..a41f28838b3 100644 --- a/js/source/raster_tile_source.js +++ b/js/source/raster_tile_source.js @@ -63,7 +63,7 @@ RasterTileSource.prototype = util.inherit(Evented, { return; if (err) { - tile.errored = true; + tile.state = 'errored'; this.fire('tile.error', {tile: tile, error: err}); return; } @@ -90,7 +90,7 @@ RasterTileSource.prototype = util.inherit(Evented, { this.map.animationLoop.set(this.style.rasterFadeDuration); tile.source = this; - tile.loaded = true; + tile.state = 'loaded'; this.fire('tile.load', {tile: tile}); } diff --git a/js/source/source.js b/js/source/source.js index c65d411e693..667212182c7 100644 --- a/js/source/source.js +++ b/js/source/source.js @@ -51,7 +51,7 @@ exports.redoPlacement = function() { return; } - var ids = this._pyramid.orderedIDs(); + var ids = this._pyramid.getIds(); for (var i = 0; i < ids.length; i++) { var tile = this._pyramid.getTile(ids[i]); this._redoTilePlacement(tile); @@ -64,7 +64,7 @@ exports._getTile = function(coord) { exports._getVisibleCoordinates = function() { if (!this._pyramid) return []; - else return this._pyramid.renderedIDs().map(TileCoord.fromID); + else return this._pyramid.getRenderableIds().map(TileCoord.fromID); }; function sortTilesIn(a, b) { @@ -124,7 +124,7 @@ exports._querySourceFeatures = function(params) { } var pyramid = this._pyramid; - var tiles = pyramid.renderedIDs().map(function(id) { + var tiles = pyramid.getRenderableIds().map(function(id) { return pyramid.getTile(id); }); diff --git a/js/source/tile.js b/js/source/tile.js index cf40438071e..1c223546a01 100644 --- a/js/source/tile.js +++ b/js/source/tile.js @@ -25,12 +25,19 @@ module.exports = Tile; function Tile(coord, size, sourceMaxZoom) { this.coord = coord; this.uid = util.uniqueId(); - this.loaded = false; - this.isUnloaded = false; this.uses = 0; this.tileSize = size; this.sourceMaxZoom = sourceMaxZoom; this.buckets = {}; + + // `this.state` must be one of + // + // - `loading`: Tile data is in the process of loading. + // - `loaded`: Tile data has been loaded. Tile can be rendered. + // - `reloading`: Tile data has been loaded and is being updated. Tile can be rendered. + // - `unloaded`: Tile data has been deleted. + // - `errored`: Tile data was not loaded because of an error. + this.state = 'loading'; } Tile.prototype = { @@ -45,7 +52,7 @@ Tile.prototype = { * @private */ loadVectorData: function(data, style) { - this.loaded = true; + this.state = 'loaded'; // empty GeoJSON tile if (!data) return; @@ -68,7 +75,7 @@ Tile.prototype = { * @private */ reloadSymbolData: function(data, painter, style) { - if (this.isUnloaded) return; + if (this.state === 'unloaded') return; this.collisionTile = new CollisionTile(data.collisionTile, this.collisionBoxArray); this.featureIndex.setCollisionTile(this.collisionTile); @@ -107,17 +114,16 @@ Tile.prototype = { this.featureIndex = null; this.rawTileData = null; this.buckets = null; - this.loaded = false; - this.isUnloaded = true; + this.state = 'unloaded'; }, redoPlacement: function(source) { - if (!this.loaded || this.redoingPlacement) { + if (this.state !== 'loaded' || this.state === 'reloading') { this.redoWhenDone = true; return; } - this.redoingPlacement = true; + this.state = 'reloading'; source.dispatcher.send('redo placement', { uid: this.uid, @@ -131,7 +137,7 @@ Tile.prototype = { this.reloadSymbolData(data, source.map.painter, source.map.style); source.fire('tile.load', {tile: this}); - this.redoingPlacement = false; + this.state = 'loaded'; if (this.redoWhenDone) { this.redoPlacement(source); this.redoWhenDone = false; @@ -165,6 +171,10 @@ Tile.prototype = { result.push(geojsonFeature); } } + }, + + isRenderable: function() { + return this.state === 'loaded' || this.state === 'reloading'; } }; diff --git a/js/source/tile_pyramid.js b/js/source/tile_pyramid.js index 75e6f75c200..454967def90 100644 --- a/js/source/tile_pyramid.js +++ b/js/source/tile_pyramid.js @@ -38,7 +38,7 @@ function TilePyramid(options) { this._tiles = {}; this._cache = new Cache(0, function(tile) { return this._unload(tile); }.bind(this)); - this._filterRendered = this._filterRendered.bind(this); + this._isIdRenderable = this._isIdRenderable.bind(this); } @@ -47,13 +47,15 @@ TilePyramid.maxUnderzooming = 3; TilePyramid.prototype = { /** - * Confirm that every tracked tile is loaded. - * @returns {boolean} whether all tiles are loaded. + * Return true if no tile data is pending, tiles will not change unless + * an additional API call is received. + * @returns {boolean} * @private */ loaded: function() { for (var t in this._tiles) { - if (!this._tiles[t].loaded && !this._tiles[t].errored) + var tile = this._tiles[t]; + if (tile.state !== 'loaded' && tile.state !== 'errored') return false; } return true; @@ -64,22 +66,32 @@ TilePyramid.prototype = { * @returns {Array} ids * @private */ - orderedIDs: function() { + getIds: function() { return Object.keys(this._tiles).map(Number).sort(compareKeyZoom); }, - renderedIDs: function() { - return this.orderedIDs().filter(this._filterRendered); + getRenderableIds: function() { + return this.getIds().filter(this._isIdRenderable); }, - _filterRendered: function(id) { - return this._tiles[id].loaded && !this._coveredTiles[id]; + _isIdRenderable: function(id) { + return this._tiles[id].isRenderable() && !this._coveredTiles[id]; }, reload: function() { this._cache.reset(); for (var i in this._tiles) { - this._load(this._tiles[i]); + var tile = this._tiles[i]; + + // The difference between "loading" tiles and "reloading" tiles is + // that "reloading" tiles are "renderable". Therefore, a "loading" + // tile cannot become a "reloading" tile without first becoming + // a "loaded" tile. + if (tile.state !== 'loading') { + tile.state = 'reloading'; + } + + this._load(tile); } }, @@ -157,8 +169,8 @@ TilePyramid.prototype = { for (var id in this._tiles) { var tile = this._tiles[id]; - // only consider loaded tiles on higher zoom levels (up to maxCoveringZoom) - if (retain[id] || !tile.loaded || tile.coord.z <= coord.z || tile.coord.z > maxCoveringZoom) continue; + // only consider renderable tiles on higher zoom levels (up to maxCoveringZoom) + if (retain[id] || !tile.isRenderable() || tile.coord.z <= coord.z || tile.coord.z > maxCoveringZoom) continue; // disregard tiles that are not descendants of the given tile coordinate var z2 = Math.pow(2, Math.min(tile.coord.z, this.maxzoom) - Math.min(coord.z, this.maxzoom)); @@ -175,7 +187,7 @@ TilePyramid.prototype = { var parentId = tile.coord.parent(this.maxzoom).id; tile = this._tiles[parentId]; - if (tile && tile.loaded) { + if (tile && tile.isRenderable()) { delete retain[id]; retain[parentId] = true; } @@ -198,7 +210,7 @@ TilePyramid.prototype = { for (var z = coord.z - 1; z >= minCoveringZoom; z--) { coord = coord.parent(this.maxzoom); var tile = this._tiles[coord.id]; - if (tile && tile.loaded) { + if (tile && tile.isRenderable()) { retain[coord.id] = true; return tile; } @@ -261,7 +273,7 @@ TilePyramid.prototype = { retain[coord.id] = true; - if (tile.loaded) + if (tile.isRenderable()) continue; // The tile we require is not yet loaded. @@ -360,7 +372,7 @@ TilePyramid.prototype = { if (tile.uses > 0) return; - if (tile.loaded) { + if (tile.isRenderable()) { this._cache.add(tile.coord.wrapped().id, tile); } else { this._abort(tile); @@ -387,7 +399,7 @@ TilePyramid.prototype = { */ tilesIn: function(queryGeometry) { var tileResults = {}; - var ids = this.orderedIDs(); + var ids = this.getIds(); var minX = Infinity; var minY = Infinity; diff --git a/js/source/vector_tile_source.js b/js/source/vector_tile_source.js index b58517c45cf..5cc5fd6462f 100644 --- a/js/source/vector_tile_source.js +++ b/js/source/vector_tile_source.js @@ -85,7 +85,7 @@ VectorTileSource.prototype = util.inherit(Evented, { return; if (err) { - tile.errored = true; + tile.state = 'errored'; this.fire('tile.error', {tile: tile, error: err}); return; } diff --git a/js/style/style.js b/js/style/style.js index 33e3e6cd420..5cfb5d1ed57 100644 --- a/js/style/style.js +++ b/js/style/style.js @@ -112,6 +112,9 @@ Style.prototype = util.inherit(Evented, { if (!this._loaded) return false; + if (Object.keys(this._updates.sources).length) + return false; + for (var id in this.sources) if (!this.sources[id].loaded()) return false; @@ -500,7 +503,7 @@ Style.prototype = util.inherit(Evented, { var layer = this.getReferentLayer(layerId); - if (this._handleErrors(validateStyle.filter, 'layers.' + layer.id + '.filter', filter)) return this; + if (filter !== null && this._handleErrors(validateStyle.filter, 'layers.' + layer.id + '.filter', filter)) return this; if (util.deepEqual(layer.filter, filter)) return this; layer.filter = util.clone(filter); diff --git a/js/util/dispatcher.js b/js/util/dispatcher.js index b0fb8b1fefd..781240831d3 100644 --- a/js/util/dispatcher.js +++ b/js/util/dispatcher.js @@ -36,6 +36,11 @@ function Dispatcher(length, parent) { this.worker = new Worker(workerBus); this.actor = new Actor(parentBus, parent); + + this.remove = function() { + parentListeners.splice(0, parentListeners.length); + workerListeners.splice(0, workerListeners.length); + }; } Dispatcher.prototype = { @@ -45,9 +50,5 @@ Dispatcher.prototype = { send: function(type, data, callback, targetID, buffers) { this.actor.send(type, data, callback, buffers); - }, - - remove: function() { - // noop } }; diff --git a/js/util/util.js b/js/util/util.js index 58b6c66e2ca..f4f769642e4 100644 --- a/js/util/util.js +++ b/js/util/util.js @@ -403,7 +403,7 @@ exports.deepEqual = function deepEqual(a, b) { } return true; } - if (typeof a === 'object') { + if (typeof a === 'object' && a !== null && b !== null) { if (!(typeof b === 'object')) return false; var keys = Object.keys(a); if (keys.length !== Object.keys(b).length) return false; diff --git a/package.json b/package.json index 68207911eca..969895e1393 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "highlight.js": "9.3.0", "istanbul": "^0.4.2", "lodash": "^4.13.1", - "mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#4868b34cea6bff36a442124e79d5b20b0c60fc9b", + "mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#dd5b5c93e13f8760bad6c6288d18f286f0a752d4", "minifyify": "^7.0.1", "nyc": "6.4.0", "remark": "4.2.2", diff --git a/test/js/source/geojson_source.test.js b/test/js/source/geojson_source.test.js index 5f84b4c82de..e5cf6042d1a 100644 --- a/test/js/source/geojson_source.test.js +++ b/test/js/source/geojson_source.test.js @@ -178,7 +178,7 @@ test('GeoJSONSource#update', function(t) { source.update(transform); source.once('change', function() { - t.deepEqual(source._pyramid.renderedIDs(), []); + t.deepEqual(source._pyramid.getRenderableIds(), []); t.end(); }); }); diff --git a/test/js/source/tile_pyramid.test.js b/test/js/source/tile_pyramid.test.js index 82431d41e7f..9cdea325dbe 100644 --- a/test/js/source/tile_pyramid.test.js +++ b/test/js/source/tile_pyramid.test.js @@ -140,7 +140,7 @@ test('TilePyramid#addTile', function(t) { add = 0; var pyramid = createPyramid({ - load: function(tile) { tile.loaded = true; load++; }, + load: function(tile) { tile.state = 'loaded'; load++; }, add: function() { add++; } }); @@ -164,7 +164,7 @@ test('TilePyramid#addTile', function(t) { add = 0; var pyramid = createPyramid({ - load: function(tile) { tile.loaded = true; load++; }, + load: function(tile) { tile.state = 'loaded'; load++; }, add: function() { add++; } }); @@ -199,7 +199,7 @@ test('TilePyramid#removeTile', function(t) { var coord = new TileCoord(0, 0, 0); var pyramid = createPyramid({ load: function(tile) { - tile.loaded = true; + tile.state = 'loaded'; }, unload: function() { t.fail(); @@ -254,7 +254,7 @@ test('TilePyramid#update', function(t) { var pyramid = createPyramid({}); pyramid.update(false, transform); - t.deepEqual(pyramid.orderedIDs(), []); + t.deepEqual(pyramid.getIds(), []); t.end(); }); @@ -266,7 +266,7 @@ test('TilePyramid#update', function(t) { var pyramid = createPyramid({}); pyramid.update(true, transform); - t.deepEqual(pyramid.orderedIDs(), [new TileCoord(0, 0, 0).id]); + t.deepEqual(pyramid.getIds(), [new TileCoord(0, 0, 0).id]); t.end(); }); @@ -277,17 +277,17 @@ test('TilePyramid#update', function(t) { var pyramid = createPyramid({ load: function(tile) { - tile.loaded = true; + tile.state = 'loaded'; } }); pyramid.update(true, transform); - t.deepEqual(pyramid.orderedIDs(), [new TileCoord(0, 0, 0).id]); + t.deepEqual(pyramid.getIds(), [new TileCoord(0, 0, 0).id]); transform.zoom = 1; pyramid.update(true, transform); - t.deepEqual(pyramid.orderedIDs(), [ + t.deepEqual(pyramid.getIds(), [ new TileCoord(1, 0, 0).id, new TileCoord(1, 1, 0).id, new TileCoord(1, 0, 1).id, @@ -303,17 +303,19 @@ test('TilePyramid#update', function(t) { var pyramid = createPyramid({ load: function(tile) { - tile.loaded = (tile.coord.id === new TileCoord(0, 0, 0).id); + if (tile.coord.id === new TileCoord(0, 0, 0).id) { + tile.state = 'loaded'; + } } }); pyramid.update(true, transform); - t.deepEqual(pyramid.orderedIDs(), [new TileCoord(0, 0, 0).id]); + t.deepEqual(pyramid.getIds(), [new TileCoord(0, 0, 0).id]); transform.zoom = 1; pyramid.update(true, transform); - t.deepEqual(pyramid.orderedIDs(), [ + t.deepEqual(pyramid.getIds(), [ new TileCoord(0, 0, 0).id, new TileCoord(1, 0, 0).id, new TileCoord(1, 1, 0).id, @@ -331,17 +333,19 @@ test('TilePyramid#update', function(t) { var pyramid = createPyramid({ load: function(tile) { - tile.loaded = (tile.coord.id === new TileCoord(0, 0, 0).id); + if (tile.coord.id === new TileCoord(0, 0, 0).id) { + tile.state = 'loaded'; + } } }); pyramid.update(true, transform); - t.deepEqual(pyramid.orderedIDs(), [new TileCoord(0, 0, 0, 1).id]); + t.deepEqual(pyramid.getIds(), [new TileCoord(0, 0, 0, 1).id]); transform.zoom = 1; pyramid.update(true, transform); - t.deepEqual(pyramid.orderedIDs(), [ + t.deepEqual(pyramid.getIds(), [ new TileCoord(0, 0, 0, 1).id, new TileCoord(1, 0, 0, 1).id, new TileCoord(1, 1, 0, 1).id, @@ -359,12 +363,12 @@ test('TilePyramid#update', function(t) { var pyramid = createPyramid({ load: function(tile) { tile.timeAdded = Infinity; - tile.loaded = true; + tile.state = 'loaded'; } }); pyramid.update(true, transform, 100); - t.deepEqual(pyramid.orderedIDs(), [ + t.deepEqual(pyramid.getIds(), [ new TileCoord(2, 1, 1).id, new TileCoord(2, 2, 1).id, new TileCoord(2, 1, 2).id, @@ -374,7 +378,7 @@ test('TilePyramid#update', function(t) { transform.zoom = 0; pyramid.update(true, transform, 100); - t.deepEqual(pyramid.renderedIDs().length, 5); + t.deepEqual(pyramid.getRenderableIds().length, 5); t.end(); }); @@ -386,7 +390,7 @@ test('TilePyramid#update', function(t) { var pyramid = createPyramid({ load: function(tile) { tile.timeAdded = Infinity; - tile.loaded = true; + tile.state = 'loaded'; } }); @@ -409,18 +413,19 @@ test('TilePyramid#update', function(t) { transform.zoom = 16; transform.center = new LngLat(0, 0); - var pyramid = createPyramid({ reparseOverscaled: true, load: function(tile) { - tile.loaded = tile.coord.z === 16; + if (tile.coord.z === 16) { + tile.state = 'loaded'; + } } }); t.equal(pyramid.maxzoom, 14); pyramid.update(true, transform); - t.deepEqual(pyramid.renderedIDs(), [ + t.deepEqual(pyramid.getRenderableIds(), [ new TileCoord(16, 8191, 8191, 0).id, new TileCoord(16, 8192, 8191, 0).id, new TileCoord(16, 8192, 8192, 0).id, @@ -430,7 +435,7 @@ test('TilePyramid#update', function(t) { transform.zoom = 15; pyramid.update(true, transform); - t.deepEqual(pyramid.renderedIDs(), [ + t.deepEqual(pyramid.getRenderableIds(), [ new TileCoord(16, 8191, 8191, 0).id, new TileCoord(16, 8192, 8191, 0).id, new TileCoord(16, 8192, 8192, 0).id, @@ -480,13 +485,13 @@ test('TilePyramid#tilesIn', function (t) { var pyramid = createPyramid({ load: function(tile) { - tile.loaded = true; + tile.state = 'loaded'; } }); pyramid.update(true, transform); - t.deepEqual(pyramid.orderedIDs(), [ + t.deepEqual(pyramid.getIds(), [ new TileCoord(1, 0, 0).id, new TileCoord(1, 1, 0).id, new TileCoord(1, 0, 1).id, @@ -516,7 +521,7 @@ test('TilePyramid#tilesIn', function (t) { t.test('reparsed overscaled tiles', function(t) { var pyramid = createPyramid({ - load: function(tile) { tile.loaded = true; }, + load: function(tile) { tile.state = 'loaded'; }, reparseOverscaled: true, minzoom: 1, maxzoom: 1, @@ -528,7 +533,7 @@ test('TilePyramid#tilesIn', function (t) { transform.zoom = 2.0; pyramid.update(true, transform); - t.deepEqual(pyramid.orderedIDs(), [ + t.deepEqual(pyramid.getIds(), [ new TileCoord(2, 0, 0).id, new TileCoord(2, 1, 0).id, new TileCoord(2, 0, 1).id, @@ -558,7 +563,7 @@ test('TilePyramid#tilesIn', function (t) { t.test('overscaled tiles', function(t) { var pyramid = createPyramid({ - load: function(tile) { tile.loaded = true; }, + load: function(tile) { tile.state = 'loaded'; }, reparseOverscaled: false, minzoom: 1, maxzoom: 1, @@ -571,7 +576,7 @@ test('TilePyramid#tilesIn', function (t) { pyramid.update(true, transform); - t.deepEqual(pyramid.orderedIDs(), [ + t.deepEqual(pyramid.getIds(), [ new TileCoord(1, 0, 0).id, new TileCoord(1, 1, 0).id, new TileCoord(1, 0, 1).id, @@ -605,7 +610,7 @@ test('TilePyramid#tilesIn', function (t) { test('TilePyramid#loaded (no errors)', function (t) { var pyramid = createPyramid({ load: function(tile) { - tile.loaded = true; + tile.state = 'loaded'; } }); @@ -619,7 +624,7 @@ test('TilePyramid#loaded (no errors)', function (t) { test('TilePyramid#loaded (with errors)', function (t) { var pyramid = createPyramid({ load: function(tile) { - tile.errored = true; + tile.state = 'errored'; } }); @@ -630,7 +635,7 @@ test('TilePyramid#loaded (with errors)', function (t) { t.end(); }); -test('TilePyramid#orderedIDs (ascending order by zoom level)', function(t) { +test('TilePyramid#getIds (ascending order by zoom level)', function(t) { var ids = [ new TileCoord(0, 0, 0), new TileCoord(3, 0, 0), @@ -642,8 +647,7 @@ test('TilePyramid#orderedIDs (ascending order by zoom level)', function(t) { for (var i = 0; i < ids.length; i++) { pyramid._tiles[ids[i].id] = {}; } - var orderedIDs = pyramid.orderedIDs(); - t.deepEqual(orderedIDs, [ + t.deepEqual(pyramid.getIds(), [ new TileCoord(0, 0, 0).id, new TileCoord(1, 0, 0).id, new TileCoord(2, 0, 0).id, @@ -664,7 +668,7 @@ test('TilePyramid#findLoadedParent', function(t) { var tile = { coord: new TileCoord(1, 0, 0), - loaded: true + isRenderable: function() { return true; } }; pyramid._tiles[tile.coord.id] = tile; diff --git a/test/js/util/util.test.js b/test/js/util/util.test.js index 9fb082d3a4c..42045ed06ac 100644 --- a/test/js/util/util.test.js +++ b/test/js/util/util.test.js @@ -252,6 +252,9 @@ test('util', function(t) { t.ok(util.deepEqual(a, b)); t.notOk(util.deepEqual(a, c)); + t.notOk(util.deepEqual(a, null)); + t.notOk(util.deepEqual(null, c)); + t.ok(util.deepEqual(null, null)); t.end(); }); diff --git a/test/suite_implementation.js b/test/suite_implementation.js index 6b0840c8939..ef42c283772 100644 --- a/test/suite_implementation.js +++ b/test/suite_implementation.js @@ -69,7 +69,8 @@ module.exports = function(style, options, _callback) { []; map.remove(); - gl.destroy(); + gl.getExtension('STACKGL_destroy_context').destroy(); + delete map.painter.gl; callback(null, data, results.map(function (feature) { feature = feature.toJSON();