javascript: fix tile id overflow for z > 15 and error assertions in tests.

This commit is contained in:
Brandon Liu
2022-12-22 21:56:03 +08:00
parent 58d47196c7
commit b23c98dd39
2 changed files with 57 additions and 34 deletions

View File

@@ -70,43 +70,50 @@ function rotate(n: number, xy: number[], rx: number, ry: number): void {
} }
function idOnLevel(z: number, pos: number): [number, number, number] { function idOnLevel(z: number, pos: number): [number, number, number] {
const n = 1 << z; const n = Math.pow(2, z);
let rx = pos; let rx = pos;
let ry = pos; let ry = pos;
let t = pos; let t = pos;
const xy = [0, 0]; const xy = [0, 0];
let s = 1; let s = 1;
while (s < n) { while (s < n) {
rx = 1 & ((t / 2) >> 0); rx = 1 & (t / 2);
ry = 1 & (t ^ rx); ry = 1 & (t ^ rx);
rotate(s, xy, rx, ry); rotate(s, xy, rx, ry);
xy[0] += s * rx; xy[0] += s * rx;
xy[1] += s * ry; xy[1] += s * ry;
t = (t / 4) >> 0; t = t / 4;
s *= 2; s *= 2;
} }
return [z, xy[0], xy[1]]; return [z, xy[0], xy[1]];
} }
export function zxyToTileId(z: number, x: number, y: number): number { export function zxyToTileId(z: number, x: number, y: number): number {
if (z > 26) {
throw Error("Tile zoom level exceeds max safe number limit (26)");
}
if (x > Math.pow(2, z) - 1 || y > Math.pow(2, z) - 1) {
throw Error("tile x/y outside zoom level bounds");
}
let acc = 0; let acc = 0;
let tz = 0; let tz = 0;
while (tz < z) { while (tz < z) {
acc += (0x1 << tz) * (0x1 << tz); acc += Math.pow(2, tz) * Math.pow(2, tz);
tz++; tz++;
} }
const n = 1 << z; const n = Math.pow(2, z);
let rx = 0; let rx = 0;
let ry = 0; let ry = 0;
let d = 0; let d = 0;
const xy = [x, y]; const xy = [x, y];
let s = (n / 2) >> 0; let s = n / 2;
while (s > 0) { while (s > 0) {
rx = (xy[0] & s) > 0 ? 1 : 0; rx = (xy[0] & s) > 0 ? 1 : 0;
ry = (xy[1] & s) > 0 ? 1 : 0; ry = (xy[1] & s) > 0 ? 1 : 0;
d += s * s * ((3 * rx) ^ ry); d += s * s * ((3 * rx) ^ ry);
rotate(s, xy, rx, ry); rotate(s, xy, rx, ry);
s = (s / 2) >> 0; s = s / 2;
} }
return acc + d; return acc + d;
} }
@@ -114,14 +121,16 @@ export function zxyToTileId(z: number, x: number, y: number): number {
export function tileIdToZxy(i: number): [number, number, number] { export function tileIdToZxy(i: number): [number, number, number] {
let acc = 0; let acc = 0;
let z = 0; let z = 0;
for (;;) {
for (let z = 0; z < 27; z++) {
const num_tiles = (0x1 << z) * (0x1 << z); const num_tiles = (0x1 << z) * (0x1 << z);
if (acc + num_tiles > i) { if (acc + num_tiles > i) {
return idOnLevel(z, i - acc); return idOnLevel(z, i - acc);
} }
acc += num_tiles; acc += num_tiles;
z++;
} }
throw Error("Tile zoom level exceeds max safe number limit (26)");
} }
export interface Entry { export interface Entry {

View File

@@ -64,6 +64,34 @@ test("a lot of tiles", () => {
} }
}); });
test("tile extremes", () => {
for (var z = 0; z < 27; z++) {
const dim = Math.pow(2, z) - 1;
const tl = tileIdToZxy(zxyToTileId(z, 0, 0));
assert.deepEqual([z, 0, 0], tl);
const tr = tileIdToZxy(zxyToTileId(z, dim, 0));
assert.deepEqual([z, dim, 0], tr);
const bl = tileIdToZxy(zxyToTileId(z, 0, dim));
assert.deepEqual([z, 0, dim], bl);
const br = tileIdToZxy(zxyToTileId(z, dim, dim));
assert.deepEqual([z, dim, dim], br);
}
});
test("invalid tiles", () => {
assert.throws(() => {
tileIdToZxy(Number.MAX_SAFE_INTEGER);
});
assert.throws(() => {
zxyToTileId(27,0,0);
});
assert.throws(() => {
zxyToTileId(0,1,1);
});
});
test("tile search for missing entry", () => { test("tile search for missing entry", () => {
const entries: Entry[] = []; const entries: Entry[] = [];
assert.strictEqual(findTile(entries, 101), null); assert.strictEqual(findTile(entries, 101), null);
@@ -185,34 +213,25 @@ test("getUint64", async () => {
test("cache check against empty", async () => { test("cache check against empty", async () => {
const source = new TestNodeFileSource("test/data/empty.pmtiles", "1"); const source = new TestNodeFileSource("test/data/empty.pmtiles", "1");
const cache = new SharedPromiseCache(); const cache = new SharedPromiseCache();
try { assert.rejects(async () => {
await cache.getHeader(source); await cache.getHeader(source);
assert.fail("Should have thrown"); });
} catch (e) {
assert.ok(e instanceof Error);
}
}); });
test("cache check magic number", async () => { test("cache check magic number", async () => {
const source = new TestNodeFileSource("test/data/invalid.pmtiles", "1"); const source = new TestNodeFileSource("test/data/invalid.pmtiles", "1");
const cache = new SharedPromiseCache(); const cache = new SharedPromiseCache();
try { assert.rejects(async () => {
await cache.getHeader(source); await cache.getHeader(source);
assert.fail("Should have thrown"); });
} catch (e) {
assert.ok(e instanceof Error);
}
}); });
test("cache check future spec version", async () => { test("cache check future spec version", async () => {
const source = new TestNodeFileSource("test/data/invalid_v4.pmtiles", "1"); const source = new TestNodeFileSource("test/data/invalid_v4.pmtiles", "1");
const cache = new SharedPromiseCache(); const cache = new SharedPromiseCache();
try { assert.rejects(async () => {
await cache.getHeader(source); await cache.getHeader(source);
assert.fail("Should have thrown"); });
} catch (e) {
assert.ok(e instanceof Error);
}
}); });
test("cache getDirectory", async () => { test("cache getDirectory", async () => {
@@ -276,17 +295,15 @@ test("etags are part of key", async () => {
source.etag = "etag_2"; source.etag = "etag_2";
try { assert.rejects(async () => {
await cache.getDirectory( await cache.getDirectory(
source, source,
header.rootDirectoryOffset, header.rootDirectoryOffset,
header.rootDirectoryLength, header.rootDirectoryLength,
header header
); );
assert.fail("Should have thrown"); })
} catch (e) {
assert.ok(e instanceof EtagMismatch);
}
cache.invalidate(source, "etag_2"); cache.invalidate(source, "etag_2");
header = await cache.getHeader(source); header = await cache.getHeader(source);
assert.ok( assert.ok(
@@ -311,17 +328,14 @@ test("soft failure on etag weirdness", async () => {
source.etag = "etag_2"; source.etag = "etag_2";
try { assert.rejects(async () => {
await cache.getDirectory( await cache.getDirectory(
source, source,
header.rootDirectoryOffset, header.rootDirectoryOffset,
header.rootDirectoryLength, header.rootDirectoryLength,
header header
); );
assert.fail("Should have thrown"); })
} catch (e) {
assert.ok(e instanceof EtagMismatch);
}
source.etag = "etag_1"; source.etag = "etag_1";
cache.invalidate(source, "etag_2"); cache.invalidate(source, "etag_2");