mirror of
https://github.com/protomaps/PMTiles.git
synced 2026-02-04 19:01:08 +00:00
javascript: fix tile id overflow for z > 15 and error assertions in tests.
This commit is contained in:
27
js/index.ts
27
js/index.ts
@@ -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 {
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user