From 07eaf102a9e2f97df3137bf8bfd4cc864f2ae300 Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Mon, 19 Sep 2022 20:41:08 -0700 Subject: [PATCH] base v3 tileID and lookup functions [#41] --- js/index.test.ts | 77 ++++++++++++++++++++++++++++++++++++ js/v3.ts | 101 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 178 insertions(+) create mode 100644 js/v3.ts diff --git a/js/index.test.ts b/js/index.test.ts index b033c7e..77fa2bd 100644 --- a/js/index.test.ts +++ b/js/index.test.ts @@ -11,6 +11,8 @@ import { createDirectory, } from "./index"; +import { Entry as EntryV3, zxyToTileId, tileIdToZxy, findTile } from "./v3"; + test("stub data", (assertion) => { let dataview = createDirectory([ { z: 5, x: 1000, y: 2000, offset: 1000, length: 2000, is_dir: false }, @@ -145,3 +147,78 @@ test("convert spec v1 directory to spec v2 directory", (assertion) => { entry = parseEntry(view, 2); assertion.ok(entry!.offset === 3); }); + +test("zxy to tile id", (assertion) => { + assertion.eq(zxyToTileId(0, 0, 0), 0); + assertion.eq(zxyToTileId(1, 0, 0), 1); + assertion.eq(zxyToTileId(1, 0, 1), 2); + assertion.eq(zxyToTileId(1, 1, 1), 3); + assertion.eq(zxyToTileId(1, 1, 0), 4); + assertion.eq(zxyToTileId(2, 0, 0), 5); +}); + +test("tile id to zxy", (assertion) => { + assertion.eq(tileIdToZxy(0), [0, 0, 0]); + assertion.eq(tileIdToZxy(1), [1, 0, 0]); + assertion.eq(tileIdToZxy(2), [1, 0, 1]); + assertion.eq(tileIdToZxy(3), [1, 1, 1]); + assertion.eq(tileIdToZxy(4), [1, 1, 0]); + assertion.eq(tileIdToZxy(5), [2, 0, 0]); +}); + +test("a lot of tiles", (assertion) => { + for (var z = 0; z < 9; z++) { + for (var x = 0; x < 1 << z; x++) { + for (var y = 0; y < 1 << z; y++) { + let result = tileIdToZxy(zxyToTileId(z, x, y)); + if (result[0] !== z || result[1] !== x || result[2] !== y) { + assertion.fail("roundtrip failed"); + } + } + } + } +}); + +test("tile search for first entry == id", (assertion) => { + let entries: EntryV3[] = []; + assertion.eq(findTile(entries, 101), null); +}); + +test("tile search for first entry == id", (assertion) => { + let entries: EntryV3[] = [{ tileId: 100, offset: 1, length: 1, runLength: 1 }]; + let entry = findTile(entries, 100)!; + assertion.eq(entry.offset, 1); + assertion.eq(entry.length, 1); + assertion.eq(findTile(entries, 101), null); +}); + +test("tile search for first entry == id", (assertion) => { + let entries: EntryV3[] = [{ tileId: 100, offset: 1, length: 1, runLength: 2 }]; + let entry = findTile(entries, 101)!; + assertion.eq(entry.offset, 1); + assertion.eq(entry.length, 1); + + entries = [ + { tileId: 100, offset: 1, length: 1, runLength: 1 }, + { tileId: 150, offset: 2, length: 2, runLength: 2 }, + ]; + entry = findTile(entries, 151)!; + assertion.eq(entry.offset, 2); + assertion.eq(entry.length, 2); + + entries = [ + { tileId: 50, offset: 1, length: 1, runLength: 2 }, + { tileId: 100, offset: 2, length: 2, runLength: 1 }, + { tileId: 150, offset: 3, length: 3, runLength: 1 }, + ]; + entry = findTile(entries, 51)!; + assertion.eq(entry.offset, 1); + assertion.eq(entry.length, 1); +}); + +test("leaf search", (assertion) => { + let entries: EntryV3[] = [{ tileId: 100, offset: 1, length: 1, runLength: 0 }]; + let entry = findTile(entries, 150); + assertion.eq(entry!.offset, 1); + assertion.eq(entry!.length, 1); +}); diff --git a/js/v3.ts b/js/v3.ts new file mode 100644 index 0000000..d4855d6 --- /dev/null +++ b/js/v3.ts @@ -0,0 +1,101 @@ +function rotate(n: number, xy: number[], rx: number, ry: number): void { + if (ry == 0) { + if (rx == 1) { + xy[0] = n - 1 - xy[0]; + xy[1] = n - 1 - xy[1]; + } + let t = xy[0]; + xy[0] = xy[1]; + xy[1] = t; + } +} + +function idOnLevel(z: number, pos: number): [number, number, number] { + let n = 1 << z; + let rx = pos; + let ry = pos; + let t = pos; + let xy = [0, 0]; + let s = 1; + while (s < n) { + rx = 1 & ((t / 2) >> 0); + ry = 1 & (t ^ rx); + rotate(s, xy, rx, ry); + xy[0] += s * rx; + xy[1] += s * ry; + t = (t / 4) >> 0; + s *= 2; + } + return [z, xy[0], xy[1]]; +} + +export function zxyToTileId(z: number, x: number, y: number): number { + let acc = 0; + let tz = 0; + while (tz < z) { + acc += (0x1 << tz) * (0x1 << tz); + tz++; + } + let n = 1 << z; + let rx = 0; + let ry = 0; + let d = 0; + let xy = [x, y]; + let s = (n / 2) >> 0; + while (s > 0) { + rx = (xy[0] & s) > 0 ? 1 : 0; + ry = (xy[1] & s) > 0 ? 1 : 0; + d += s * s * ((3 * rx) ^ ry); + rotate(s, xy, rx, ry); + s = (s / 2) >> 0; + } + return acc + d; +} + +export function tileIdToZxy(i: number): [number, number, number] { + let acc = 0; + let z = 0; + while (true) { + let num_tiles = (0x1 << z) * (0x1 << z); + if (acc + num_tiles > i) { + return idOnLevel(z, i - acc); + } + acc += num_tiles; + z++; + } +} + +export interface Entry { + tileId: number; + offset: number; + length: number; + runLength: number; +} + +export function findTile(entries: Entry[], tileId: number): Entry | null { + let m = 0; + let n = entries.length - 1; + while (m <= n) { + const k = (n + m) >> 1; + const cmp = tileId - entries[k].tileId; + if (cmp > 0) { + m = k + 1; + } else if (cmp < 0) { + n = k - 1; + } else { + return entries[k]; + } + } + + // at this point, m > n + if (n >= 0) { + if (entries[n].runLength === 0) { + return entries[n]; + } + if (tileId - entries[n].tileId < entries[n].runLength) { + return entries[n]; + } + } + return null; +} +