Skip to content
Closed
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
164 changes: 163 additions & 1 deletion src/geo/transform.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ describe('transform', () => {
const transform = new Transform(0, 22, 0, 60, true);
transform.resize(200, 200);

test('generell', () => {
test('general', () => {

// make slightly off center so that sort order is not subject to precision issues
transform.center = new LngLat(-0.01, 0.01);
Expand Down Expand Up @@ -202,6 +202,168 @@ describe('transform', () => {
transform.resize(300, 300);
});

test('general cached', () => {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think it would be better to split this test into several tests and have the title to be a bit more meaningful.
As a rule of thumb, a test should not have more than one assert/expect. Although I think this rule is a bit too strict and test can have a few expects, this test is not the case.

const spy = jest.spyOn(transform, 'coveringZoomLevel');

// make slightly off center so that sort order is not subject to precision issues
transform.center = new LngLat(-0.01, 0.01);

transform.zoom = 0;
expect(transform.coveringTiles(options)).toEqual([]);
expect(spy).toHaveBeenCalledTimes(1);
expect(transform.coveringTiles(options)).toEqual([]);
expect(spy).toHaveBeenCalledTimes(1);

transform.zoom = 1;
expect(transform.coveringTiles(options)).toEqual([
new OverscaledTileID(1, 0, 1, 0, 0),
new OverscaledTileID(1, 0, 1, 1, 0),
new OverscaledTileID(1, 0, 1, 0, 1),
new OverscaledTileID(1, 0, 1, 1, 1)]);
expect(spy).toHaveBeenCalledTimes(2);
expect(transform.coveringTiles(options)).toEqual([
new OverscaledTileID(1, 0, 1, 0, 0),
new OverscaledTileID(1, 0, 1, 1, 0),
new OverscaledTileID(1, 0, 1, 0, 1),
new OverscaledTileID(1, 0, 1, 1, 1)]);
expect(spy).toHaveBeenCalledTimes(2);

transform.zoom = 2.4;
expect(transform.coveringTiles(options)).toEqual([
new OverscaledTileID(2, 0, 2, 1, 1),
new OverscaledTileID(2, 0, 2, 2, 1),
new OverscaledTileID(2, 0, 2, 1, 2),
new OverscaledTileID(2, 0, 2, 2, 2)]);
expect(spy).toHaveBeenCalledTimes(3);
expect(transform.coveringTiles(options)).toEqual([
new OverscaledTileID(2, 0, 2, 1, 1),
new OverscaledTileID(2, 0, 2, 2, 1),
new OverscaledTileID(2, 0, 2, 1, 2),
new OverscaledTileID(2, 0, 2, 2, 2)]);
expect(spy).toHaveBeenCalledTimes(3);

transform.zoom = 10;
expect(transform.coveringTiles(options)).toEqual([
new OverscaledTileID(10, 0, 10, 511, 511),
new OverscaledTileID(10, 0, 10, 512, 511),
new OverscaledTileID(10, 0, 10, 511, 512),
new OverscaledTileID(10, 0, 10, 512, 512)]);
expect(spy).toHaveBeenCalledTimes(4);
expect(transform.coveringTiles(options)).toEqual([
new OverscaledTileID(10, 0, 10, 511, 511),
new OverscaledTileID(10, 0, 10, 512, 511),
new OverscaledTileID(10, 0, 10, 511, 512),
new OverscaledTileID(10, 0, 10, 512, 512)]);
expect(spy).toHaveBeenCalledTimes(4);

transform.zoom = 11;
expect(transform.coveringTiles(options)).toEqual([
new OverscaledTileID(10, 0, 10, 511, 511),
new OverscaledTileID(10, 0, 10, 512, 511),
new OverscaledTileID(10, 0, 10, 511, 512),
new OverscaledTileID(10, 0, 10, 512, 512)]);
expect(spy).toHaveBeenCalledTimes(5);
expect(transform.coveringTiles(options)).toEqual([
new OverscaledTileID(10, 0, 10, 511, 511),
new OverscaledTileID(10, 0, 10, 512, 511),
new OverscaledTileID(10, 0, 10, 511, 512),
new OverscaledTileID(10, 0, 10, 512, 512)]);
expect(spy).toHaveBeenCalledTimes(5);

transform.zoom = 5.1;
transform.pitch = 60.0;
transform.bearing = 32.0;
transform.center = new LngLat(56.90, 48.20);
transform.resize(1024, 768);
expect(transform.coveringTiles(options)).toEqual([
new OverscaledTileID(5, 0, 5, 21, 11),
new OverscaledTileID(5, 0, 5, 20, 11),
new OverscaledTileID(5, 0, 5, 21, 10),
new OverscaledTileID(5, 0, 5, 20, 10),
new OverscaledTileID(5, 0, 5, 21, 12),
new OverscaledTileID(5, 0, 5, 22, 11),
new OverscaledTileID(5, 0, 5, 20, 12),
new OverscaledTileID(5, 0, 5, 22, 10),
new OverscaledTileID(5, 0, 5, 21, 9),
new OverscaledTileID(5, 0, 5, 20, 9),
new OverscaledTileID(5, 0, 5, 22, 9),
new OverscaledTileID(5, 0, 5, 23, 10),
new OverscaledTileID(5, 0, 5, 21, 8),
new OverscaledTileID(5, 0, 5, 20, 8),
new OverscaledTileID(5, 0, 5, 23, 9),
new OverscaledTileID(5, 0, 5, 22, 8),
new OverscaledTileID(5, 0, 5, 23, 8),
new OverscaledTileID(5, 0, 5, 21, 7),
new OverscaledTileID(5, 0, 5, 20, 7),
new OverscaledTileID(5, 0, 5, 24, 9),
new OverscaledTileID(5, 0, 5, 22, 7)
]);
expect(spy).toHaveBeenCalledTimes(6);
expect(transform.coveringTiles(options)).toEqual([
new OverscaledTileID(5, 0, 5, 21, 11),
new OverscaledTileID(5, 0, 5, 20, 11),
new OverscaledTileID(5, 0, 5, 21, 10),
new OverscaledTileID(5, 0, 5, 20, 10),
new OverscaledTileID(5, 0, 5, 21, 12),
new OverscaledTileID(5, 0, 5, 22, 11),
new OverscaledTileID(5, 0, 5, 20, 12),
new OverscaledTileID(5, 0, 5, 22, 10),
new OverscaledTileID(5, 0, 5, 21, 9),
new OverscaledTileID(5, 0, 5, 20, 9),
new OverscaledTileID(5, 0, 5, 22, 9),
new OverscaledTileID(5, 0, 5, 23, 10),
new OverscaledTileID(5, 0, 5, 21, 8),
new OverscaledTileID(5, 0, 5, 20, 8),
new OverscaledTileID(5, 0, 5, 23, 9),
new OverscaledTileID(5, 0, 5, 22, 8),
new OverscaledTileID(5, 0, 5, 23, 8),
new OverscaledTileID(5, 0, 5, 21, 7),
new OverscaledTileID(5, 0, 5, 20, 7),
new OverscaledTileID(5, 0, 5, 24, 9),
new OverscaledTileID(5, 0, 5, 22, 7)
]);
expect(spy).toHaveBeenCalledTimes(6);

transform.zoom = 8;
transform.pitch = 60;
transform.bearing = 45.0;
transform.center = new LngLat(25.02, 60.15);
transform.resize(300, 50);
expect(transform.coveringTiles(options)).toEqual([
new OverscaledTileID(8, 0, 8, 145, 74),
new OverscaledTileID(8, 0, 8, 145, 73),
new OverscaledTileID(8, 0, 8, 146, 74)
]);
expect(spy).toHaveBeenCalledTimes(7);
expect(transform.coveringTiles(options)).toEqual([
new OverscaledTileID(8, 0, 8, 145, 74),
new OverscaledTileID(8, 0, 8, 145, 73),
new OverscaledTileID(8, 0, 8, 146, 74)
]);
expect(spy).toHaveBeenCalledTimes(7);

transform.resize(50, 300);
expect(transform.coveringTiles(options)).toEqual([
new OverscaledTileID(8, 0, 8, 145, 74),
new OverscaledTileID(8, 0, 8, 145, 73),
new OverscaledTileID(8, 0, 8, 146, 74),
new OverscaledTileID(8, 0, 8, 146, 73)
]);
expect(spy).toHaveBeenCalledTimes(8);
expect(transform.coveringTiles(options)).toEqual([
new OverscaledTileID(8, 0, 8, 145, 74),
new OverscaledTileID(8, 0, 8, 145, 73),
new OverscaledTileID(8, 0, 8, 146, 74),
new OverscaledTileID(8, 0, 8, 146, 73)
]);
expect(spy).toHaveBeenCalledTimes(8);

transform.zoom = 2;
transform.pitch = 0;
transform.bearing = 0;
transform.resize(300, 300);
});

test('calculates tile coverage at w > 0', () => {
transform.center = new LngLat(630.01, 0.01);
expect(transform.coveringTiles(options)).toEqual([
Expand Down
47 changes: 41 additions & 6 deletions src/geo/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ import EdgeInsets from './edge_insets';
import {UnwrappedTileID, OverscaledTileID, CanonicalTileID} from '../source/tile_id';
import type {PaddingOptions} from './edge_insets';

type TileParameters = {
overscaledZ: number;
wrap: number;
z: number;
x: number;
y: number;
}

type CacheEntry = Array<TileParameters>

/**
* A single transform, generally used for a single tile to be
* scaled, rotated, and zoomed.
Expand Down Expand Up @@ -53,6 +63,7 @@ class Transform {
_constraining: boolean;
_posMatrixCache: {[_: string]: mat4};
_alignedPosMatrixCache: {[_: string]: mat4};
_coveringTilesCache: {[_: string]: CacheEntry};

constructor(minZoom?: number, maxZoom?: number, minPitch?: number, maxPitch?: number, renderWorldCopies?: boolean) {
this.tileSize = 512; // constant
Expand All @@ -78,6 +89,7 @@ class Transform {
this._edgeInsets = new EdgeInsets();
this._posMatrixCache = {};
this._alignedPosMatrixCache = {};
this._coveringTilesCache = {};
}

clone(): Transform {
Expand Down Expand Up @@ -321,13 +333,24 @@ class Transform {
maxzoom?: number;
roundZoom?: boolean;
reparseOverscaled?: boolean;
renderWorldCopies?: boolean;
}
): Array<OverscaledTileID> {
const castToOverscaledTileIDs = (cacheEntry: CacheEntry): Array<OverscaledTileID> => {
return cacheEntry.map(it => new OverscaledTileID(it.overscaledZ, it.wrap, it.z, it.x, it.y));
};

const optionsKey = JSON.stringify([options, this._renderWorldCopies]);
if (this._coveringTilesCache[optionsKey]) {
return castToOverscaledTileIDs(this._coveringTilesCache[optionsKey]);
}

let z = this.coveringZoomLevel(options);
const actualZ = z;

if (options.minzoom !== undefined && z < options.minzoom) return [];
if (options.minzoom !== undefined && z < options.minzoom) {
this._coveringTilesCache[optionsKey] = [];
return [];
}
if (options.maxzoom !== undefined && z > options.maxzoom) z = options.maxzoom;

const centerCoord = MercatorCoordinate.fromLngLat(this.center);
Expand Down Expand Up @@ -358,7 +381,10 @@ class Transform {

// Do a depth-first traversal to find visible tiles and proper levels of detail
const stack = [];
const result = [];
const preCacheEntry = [] as Array<{
tileParameters: TileParameters;
distanceSq: number;
}>;
const maxZoom = z;
const overscaledZ = options.reparseOverscaled ? actualZ : z;

Expand Down Expand Up @@ -401,8 +427,13 @@ class Transform {

// Have we reached the target depth or is the tile too far away to be any split further?
if (it.zoom === maxZoom || (longestDim > distToSplit && it.zoom >= minZoom)) {
result.push({
tileID: new OverscaledTileID(it.zoom === maxZoom ? overscaledZ : it.zoom, it.wrap, it.zoom, x, y),
preCacheEntry.push({
tileParameters: {
overscaledZ: it.zoom === maxZoom ? overscaledZ : it.zoom,
wrap: it.wrap,
z: it.zoom,
x, y
},
distanceSq: vec2.sqrLen([centerPoint[0] - 0.5 - x, centerPoint[1] - 0.5 - y])
});
continue;
Expand All @@ -416,7 +447,10 @@ class Transform {
}
}

return result.sort((a, b) => a.distanceSq - b.distanceSq).map(a => a.tileID);
const cacheEntry = preCacheEntry.sort((a, b) => a.distanceSq - b.distanceSq).map(a => a.tileParameters);
this._coveringTilesCache[optionsKey] = cacheEntry;

return castToOverscaledTileIDs(cacheEntry);
}

resize(width: number, height: number) {
Expand Down Expand Up @@ -767,6 +801,7 @@ class Transform {

this._posMatrixCache = {};
this._alignedPosMatrixCache = {};
this._coveringTilesCache = {};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this the only place the cache needs to be invalidated? Is this the right place? @JannikGM

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Shouldn't resize() also invalidate the cache?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

            transform.zoom = 0;
            expect(transform.coveringTiles(options)).toEqual([]);

            transform.zoom = 1;
            expect(transform.coveringTiles(options)).toEqual([
                new OverscaledTileID(1, 0, 1, 0, 0),
                new OverscaledTileID(1, 0, 1, 1, 0),
                new OverscaledTileID(1, 0, 1, 0, 1),
                new OverscaledTileID(1, 0, 1, 1, 1)]);

Or like whenever someone changes transform.zoom, the function returns something different for the same input.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Zoom would result in a different cache key, so it's probably ok (assuming I understand what you wrote).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

There are only two hard things in Computer Science: cache invalidation, naming things, and off-by-one errors.

}

maxPitchScaleFactor() {
Expand Down