Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

change tile loading logic to match native #5119

Merged
merged 9 commits into from
Sep 11, 2017
Merged
Show file tree
Hide file tree
Changes from 6 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
145 changes: 102 additions & 43 deletions src/source/source_cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ class SourceCache extends Evented {
if (err) {
tile.state = 'errored';
if (err.status !== 404) this._source.fire('error', {tile: tile, error: err});
// continue to try loading parent/children tiles if a tile doesn't exist (404)
else this.update(this.transform);
return;
}

Expand Down Expand Up @@ -333,34 +335,19 @@ class SourceCache extends Evented {
this.transform = transform;
if (!this._sourceLoaded || this._paused) { return; }

let i;
let coord;
let tile;
let parentTile;

this.updateCacheSize(transform);

// Determine the overzooming/underzooming amounts.
const zoom = (this._source.roundZoom ? Math.round : Math.floor)(this.getZoom(transform));
const minCoveringZoom = Math.max(zoom - SourceCache.maxOverzooming, this._source.minzoom);
const maxCoveringZoom = Math.max(zoom + SourceCache.maxUnderzooming, this._source.minzoom);

// Retain is a list of tiles that we shouldn't delete, even if they are not
// the most ideal tile for the current viewport. This may include tiles like
// parent or child tiles that are *already* loaded.
const retain = {};

// Covered is a list of retained tiles who's areas are full covered by other,
// Covered is a list of retained tiles who's areas are fully covered by other,
// better, retained tiles. They are not drawn separately.
this._coveredTiles = {};

let visibleCoords;
let idealTileCoords;
if (!this.used) {
visibleCoords = [];
idealTileCoords = [];
} else if (this._source.coord) {
visibleCoords = transform.getVisibleWrappedCoordinates((this._source.coord: any));
idealTileCoords = transform.getVisibleWrappedCoordinates((this._source.coord: any));
} else {
visibleCoords = transform.coveringTiles({
idealTileCoords = transform.coveringTiles({
tileSize: this._source.tileSize,
minzoom: this._source.minzoom,
maxzoom: this._source.maxzoom,
Expand All @@ -369,37 +356,28 @@ class SourceCache extends Evented {
});

if (this._source.hasTile) {
visibleCoords = visibleCoords.filter((coord) => (this._source.hasTile: any)(coord));
idealTileCoords = idealTileCoords.filter((coord) => (this._source.hasTile: any)(coord));
}
}

for (i = 0; i < visibleCoords.length; i++) {
coord = visibleCoords[i];
tile = this._addTile(coord);

retain[coord.id] = true;

if (tile.hasData())
continue;
// Determine the overzooming/underzooming amounts.
const zoom = (this._source.roundZoom ? Math.round : Math.floor)(this.getZoom(transform));
const minCoveringZoom = Math.max(zoom - SourceCache.maxOverzooming, this._source.minzoom);
const maxCoveringZoom = Math.max(zoom + SourceCache.maxUnderzooming, this._source.minzoom);

// The tile we require is not yet loaded.
// Retain child or parent tiles that cover the same area.
if (!this._findLoadedChildren(coord, maxCoveringZoom, retain)) {
parentTile = this.findLoadedParent(coord, minCoveringZoom, retain);
if (parentTile) {
this._addTile(parentTile.coord);
}
}
}
// Retain is a list of tiles that we shouldn't delete, even if they are not
// the most ideal tile for the current viewport. This may include tiles like
// parent or child tiles that are *already* loaded.
const retain = this._updateRetainedTiles(idealTileCoords, zoom, minCoveringZoom);

const parentsForFading = {};

if (isRasterType(this._source.type)) {
const ids = Object.keys(retain);
for (let k = 0; k < ids.length; k++) {
const id = ids[k];
coord = TileCoord.fromID(+id);
tile = this._tiles[id];
const coord = TileCoord.fromID(+id);
const tile = this._tiles[id];
if (!tile) continue;

// If the drawRasterTile has never seen this tile, then
Expand All @@ -410,7 +388,7 @@ class SourceCache extends Evented {
if (this._findLoadedChildren(coord, maxCoveringZoom, retain)) {
retain[id] = true;
}
parentTile = this.findLoadedParent(coord, minCoveringZoom, parentsForFading);
const parentTile = this.findLoadedParent(coord, minCoveringZoom, parentsForFading);
if (parentTile) {
this._addTile(parentTile.coord);
}
Expand All @@ -428,14 +406,95 @@ class SourceCache extends Evented {
for (fadedParent in parentsForFading) {
retain[fadedParent] = true;
}

// Remove the tiles we don't need anymore.
const remove = util.keysDifference(this._tiles, retain);
for (i = 0; i < remove.length; i++) {
for (let i = 0; i < remove.length; i++) {
this._removeTile(remove[i]);
}
}

_updateRetainedTiles(idealTileCoords: Array<TileCoord>, zoom: number, minCoveringZoom: number) {
let i, coord, tile, covered;

const retain = {};
const checked = {};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add flow types for these?

for (i = 0; i < idealTileCoords.length; i++) {
coord = idealTileCoords[i];
tile = this._addTile(coord);
let parentWasRequested = false;
if (tile.hasData()) {
retain[coord.id] = true;
} else {
// The tile we require is not yet loaded or does not exist.
// We are now attempting to load child and parent tiles.

// As we descend up and down the tile pyramid of the ideal tile, we check whether the parent
// tile has been previously requested (and errored in this case due to the previous conditional)
// in order to determine if we need to request its parent.
parentWasRequested = tile.wasRequested();

// The tile isn't loaded yet, but retain it anyway because it's an ideal tile.
retain[coord.id] = true;
covered = true;
const overscaledZ = zoom + 1;
if (overscaledZ > this._source.maxzoom) {
// We're looking for an overzoomed child tile.
const childCoord = coord.children(this._source.maxzoom)[0];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I don't follow what's happening in this branch -- why do we only get the first child tile?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overzoomed children are different from regular children. While regular children increase the z by one, and enumerate all four quadrants, an "overzoomed child" is represented with the same z/x/y as its parent, except that the overzoomedZ value is increased by one. We're using overzoomed children to indicate that it's using the same data as the non-overzoomed tile, except "overzoomed" to a particular zoom level. This overzoom information is used while parsing the tile and placing labels.

const childTile = this.getTile(childCoord);
if (!!childTile && childTile.hasData()) {
retain[childCoord.id] = true;
} else {
covered = false;
}
} else {
// Check all four actual child tiles.
const children = coord.children(this._source.maxzoom);
for (let j = 0; j < children.length; j++) {
const childCoord = children[j];
const childTile = childCoord ? this.getTile(childCoord) : null;

if (!!childTile && childTile.hasData()) {
retain[childCoord.id] = true;
} else {
covered = false;
}
}
}

if (!covered) {
// We couldn't find child tiles that entirely cover the ideal tile.
for (let overscaledZ = zoom - 1; overscaledZ >= minCoveringZoom; --overscaledZ) {
const parentId = coord.scaledTo(overscaledZ);
if (checked[parentId.id]) {
// Break parent tile ascent, this route has been previously checked by another child.
break;
} else {
checked[parentId.id] = true;
}


tile = this.getTile(parentId);
if (!tile && parentWasRequested) {
tile = this._addTile(parentId);
}

if (tile) {
retain[parentId.id] = true;
// Save the current values, since they're the parent of the next iteration
// of the parent tile ascent loop.
parentWasRequested = tile.wasRequested();
if (tile.hasData()) {
break;
}
}
}
}
}
}

return retain;
}

/**
* Add a tile, given its coordinate, to the pyramid.
* @private
Expand Down
4 changes: 4 additions & 0 deletions src/source/tile.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ class Tile {
animationLoop.set(this.fadeEndTime - Date.now());
}

wasRequested() {
return this.state === 'errored' || this.state === 'loaded' || this.state === 'reloading';
}

/**
* Given a data object with a 'buffers' property, load it into
* this tile's elementGroups and buffers properties and set loaded
Expand Down
8 changes: 8 additions & 0 deletions src/source/tile_coord.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ class TileCoord {
];
}

scaledTo(targetZ: number) {
if (targetZ <= this.z) {
return new TileCoord(targetZ, this.x >> (this.z - targetZ), this.y >> (this.z - targetZ), this.w); // parent or same
} else {
return new TileCoord(targetZ, this.x << (targetZ - this.z), this.y << (targetZ - this.z), this.w); // child
}
}

static cover(z: number, bounds: [Coordinate, Coordinate, Coordinate, Coordinate],
actualZ: number, renderWorldCopies: boolean | void) {
if (renderWorldCopies === undefined) {
Expand Down
1 change: 0 additions & 1 deletion test/ignores.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"render-tests/fill-extrusion-pattern/opacity": "https://github.com/mapbox/mapbox-gl-js/issues/3327",
"render-tests/geojson/inline-linestring-fill": "current behavior is arbitrary",
"render-tests/line-dasharray/zoom-history": "needs investigation",
"render-tests/raster-loading/missing": "https://github.com/mapbox/mapbox-gl-js/issues/4257",
"render-tests/raster-masking/overlapping": "https://github.com/mapbox/mapbox-gl-js/issues/5003",
"render-tests/regressions/mapbox-gl-js#3682": "skip - true",
"render-tests/runtime-styling/image-add-alpha": "failing on Circle CI 2.0, needs investigation",
Expand Down
2 changes: 1 addition & 1 deletion test/suite_implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ ajax.getImage = function({ url }, callback) {
callback(null, png);
});
} else {
callback(error || new Error(response.statusCode));
callback(error || {status: response.statusCode});
}
});
};
Expand Down
107 changes: 106 additions & 1 deletion test/unit/source/source_cache.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,12 @@ test('SourceCache#update', (t) => {
transform.resize(511, 511);
transform.zoom = 0;

const sourceCache = createSourceCache({});
const sourceCache = createSourceCache({
loadTile: (tile, callback)=>{
tile.state = 'loaded';
callback(null);
}
});

sourceCache.on('data', (e) => {
if (e.sourceDataType === 'metadata') {
Expand Down Expand Up @@ -577,6 +582,7 @@ test('SourceCache#update', (t) => {
transform.resize(511, 511);
transform.zoom = 1;


const sourceCache = createSourceCache({
loadTile: function(tile, callback) {
tile.timeAdded = Date.now();
Expand Down Expand Up @@ -682,6 +688,105 @@ test('SourceCache#update', (t) => {
t.end();
});

test('SourceCache#_updateRetainedTiles', (t)=>{

t.test('loads ideal tiles if they exist', (t)=>{
const stateCache = {};
const sourceCache = createSourceCache({
loadTile: function(tile, callback) {
tile.state = stateCache[tile.coord.id] || 'errored';
callback();
}
});

const getTileSpy = t.spy(sourceCache, 'getTile');
const idealTile = new TileCoord(1, 1, 1);
stateCache[idealTile.id] = 'loaded';
sourceCache._updateRetainedTiles([idealTile], 1, 0);
t.ok(getTileSpy.notCalled);
t.deepEqual(sourceCache.getIds(), [idealTile.id]);
t.end();
});

t.test('adds parent tile if ideal tile errors and no child tiles are loaded', (t)=>{
const stateCache = {};
const sourceCache = createSourceCache({
loadTile: function(tile, callback) {
tile.state = stateCache[tile.coord.id] || 'errored';
callback();
}
});

const addTileSpy = t.spy(sourceCache, '_addTile');
const getTileSpy = t.spy(sourceCache, 'getTile');

const idealTiles = [new TileCoord(1, 1, 1), new TileCoord(1, 0, 1)];
stateCache[idealTiles[0].id] = 'loaded';
sourceCache._updateRetainedTiles(idealTiles, 1, 0);

// checks all child tiles to see if they're loaded before loading parent
t.deepEqual(getTileSpy.getCall(0).args[0], new TileCoord(2, 0, 2));
t.deepEqual(getTileSpy.getCall(1).args[0], new TileCoord(2, 1, 2));
t.deepEqual(getTileSpy.getCall(2).args[0], new TileCoord(2, 0, 3));
t.deepEqual(getTileSpy.getCall(3).args[0], new TileCoord(2, 1, 3));

// when child tiles aren't found, check and request parent tile
t.deepEqual(getTileSpy.getCall(4).args[0], new TileCoord(0, 0, 0));
t.deepEqual(addTileSpy.getCall(2).args[0], new TileCoord(0, 0, 0));
// retained tiles include all ideal tiles and any parents that were loaded to cover
// non-existant tiles
t.deepEqual(sourceCache.getIds(), [0, idealTiles[1].id, idealTiles[0].id]);


addTileSpy.restore();
getTileSpy.restore();
t.end();
});

t.test('don\'t use wrong parent tile', (t)=> {
const sourceCache = createSourceCache({
loadTile: function(tile, callback) {
tile.state = 'errored';
callback();
}
});

const idealTiles = [new TileCoord(2, 0, 0)];
sourceCache._tiles[idealTiles[0].id] = new Tile(idealTiles[0]);
sourceCache._tiles[idealTiles[0].id].state = 'errored';

sourceCache._tiles[new TileCoord(1, 1, 0).id] = new Tile(new TileCoord(1, 1, 0));
sourceCache._tiles[new TileCoord(1, 1, 0).id].state = 'loaded';


const addTileSpy = t.spy(sourceCache, '_addTile');
const getTileSpy = t.spy(sourceCache, 'getTile');

const expectedCalls = [
// all children
new TileCoord(3, 0, 0), // not found
new TileCoord(3, 1, 0), // not found
new TileCoord(3, 0, 1), // not found
new TileCoord(3, 1, 1), // not found
// parents
new TileCoord(1, 0, 0), // not found
new TileCoord(0, 0, 0) // not found
];

getTileSpy.getCalls().forEach((call, i) => {
t.deepEqual(call.args[0], expectedCalls[i]);
});

addTileSpy.restore();
getTileSpy.restore();
t.end();
});



t.end();
});

test('SourceCache#clearTiles', (t) => {
t.test('unloads tiles', (t) => {
const coord = new TileCoord(0, 0, 0);
Expand Down