clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

feat/atlas-next-horizon
atlas / render / test / vnext.test.ts
vnext.test.ts 9.1K
1import { strict as assert } from "node:assert";2import test from "node:test";3import {4 mergeSchemas, PackedTable, projectFields,5 type FieldSchema, type Schema,6} from "../data/vnext.ts";7import { coreID, decodeGeometry } from "../data/semantic.ts";8import { presentedGrid, presentedKind, project } from "../world/model.ts";910const ROOT = "11111111-1111-5111-8111-111111111111";11const TITLE = "22222222-2222-5222-8222-222222222222";12const EMERGENCY = "33333333-3333-5333-8333-333333333333";1314test("vNext packed columns open as zero-copy typed views", () => {15 const buffer = fixtureBlock();16 const envelope = new Uint8Array(buffer.byteLength + 13);17 envelope.set(new Uint8Array(buffer), 7);18 const table = PackedTable.over(envelope.subarray(7, 7 + buffer.byteLength));19 assert.equal(table.rows, 2);20 assert.equal(table.rootType, ROOT);21 assert.equal(table.schemaHash, Array.from({ length: 32 }, (_, i) => i.toString(16).padStart(2, "0")).join(""));2223 const title = table.column(TITLE);24 const emergency = table.column(EMERGENCY);25 assert.ok(title);26 assert.ok(emergency);27 assert.equal(title.string(0), "Clinic");28 assert.equal(title.isNull(1), true);29 assert.equal(emergency.int64(0), 4n);30 assert.equal(emergency.int64(1), 5n);31});3233test("old browser schemas classify rather than discard foreign fields", () => {34 const title: FieldSchema = { id: TITLE, name: "title", kind: 4 };35 const emergency: FieldSchema = { id: EMERGENCY, name: "emergencyLevel", kind: 2, optional: true };36 const base: Schema = { types: [{ id: ROOT, name: "Feature", fields: [title] }] };37 const branch: Schema = { types: [{ id: ROOT, name: "Feature", fields: [emergency] }] };38 const file = mergeSchemas(base, branch);39 const projection = projectFields(PackedTable.over(fixtureBlock()), base, file);40 assert.deepEqual(projection.known, [TITLE]);41 assert.deepEqual(projection.foreign, [EMERGENCY]);42});4344test("sideward schema union accepts new IDs and refuses changed meanings", () => {45 const left: Schema = { types: [{ id: ROOT, name: "Feature", fields: [{ id: TITLE, name: "title", kind: 4 }] }] };46 const right: Schema = { types: [{ id: ROOT, name: "Feature", fields: [{ id: EMERGENCY, name: "level", kind: 2 }] }] };47 assert.equal(mergeSchemas(left, right).types[0]?.fields.length, 2);4849 const conflict: Schema = { types: [{ id: ROOT, name: "Feature", fields: [{ id: TITLE, name: "title", kind: 2 }] }] };50 assert.throws(() => mergeSchemas(left, conflict), /wire kind/);51});5253test("a corrupt column is refused at the browser boundary", () => {54 const buffer = fixtureBlock();55 const bytes = new Uint8Array(buffer);56 bytes[buffer.byteLength - 1] = (bytes[buffer.byteLength - 1] ?? 0) ^ 0xff;57 assert.throws(() => PackedTable.over(buffer), /checksum/);58});5960test("the browser derives the same stable core identities as the writer", async () => {61 assert.equal(await coreID("feature.id"), "3aaf6328-ca62-58e2-8645-eab4421aa628");62 assert.notEqual(await coreID("feature.id"), await coreID("feature.title"));63});6465test("native geometry preserves parts, rings, and string identities without GeoJSON", () => {66 const bytes = geometry(3, [67 [68 [[0, 0], [4, 0], [4, 4], [0, 0]],69 [[1, 1], [2, 1], [1, 1]],70 ],71 [[[10, 10], [12, 10], [10, 10]]],72 ]);73 assert.deepEqual(decodeGeometry(bytes), {74 kind: 3,75 parts: [76 { rings: [[[0, 0], [4, 0], [4, 4], [0, 0]], [[1, 1], [2, 1], [1, 1]]] },77 { rings: [[[10, 10], [12, 10], [10, 10]]] },78 ],79 });80});8182test("semantic meaning does not have to masquerade as geometry", () => {83 assert.equal(presentedKind([{ geometry: { kind: 1 } }], "place"), "point");84 assert.equal(presentedKind([{ geometry: { kind: 2 } }], "route"), "path");85 assert.equal(presentedKind([{ geometry: { kind: 3 } }], "region", "area"), "area");86 assert.equal(presentedKind([], "place", "point"), "point");87 assert.equal(presentedKind([], "geometry.area"), "area");88 assert.throws(89 () => presentedKind([{ geometry: { kind: 1 } }, { geometry: { kind: 2 } }], "place"),90 /mixes geometry kinds/,91 );92 assert.throws(93 () => presentedKind([{ geometry: { kind: 1 } }], "place", "area"),94 /declares area but carries point/,95 );96});9798test("feature-only coordinate spaces derive a finite presentation grid", () => {99 assert.deepEqual(presentedGrid({100 id: "sample-grid", kind: "projected", unit: "metre", definition: "local sample grid",101 extent: [10, 20, 110, 70], sourceZoom: 0, originX: 7, originY: 11, tileSize: 0, size: 0,102 }), {103 sourceZoom: 0, originX: 7, originY: 11, firstTile: 0, tileSize: 256, size: 100,104 extent: [10, -70, 110, -20],105 });106});107108test("native tile windows use independent column and row origins", () => {109 const grid = {110 sourceZoom: 4, originX: 3, originY: 6, firstTile: 99, tileSize: 256, size: 1024,111 };112 const atOrigin = project(grid, mercatorLatitude(6 / 16), -112.5);113 assert.ok(Math.abs(atOrigin[0]) < 1e-9, `${atOrigin[0]} is the local x origin`);114 assert.ok(Math.abs(atOrigin[1]) < 1e-9, `${atOrigin[1]} is the local y origin`);115});116117function mercatorLatitude(y: number): number {118 return Math.atan(Math.sinh(Math.PI * (1 - 2 * y))) * 180 / Math.PI;119}120121function fixtureBlock(): ArrayBuffer {122 const title = variableColumn([new TextEncoder().encode("Clinic"), null]);123 const emergency = fixedInt64Column([4n, 5n]);124 return block([125 { id: TITLE, kind: 4, optional: true, ...title },126 { id: EMERGENCY, kind: 2, optional: false, ...emergency },127 ]);128}129130interface EncodedColumn {131 readonly id: string;132 readonly kind: number;133 readonly optional: boolean;134 readonly present: Uint8Array;135 readonly data: Uint8Array;136}137138function block(columns: readonly EncodedColumn[]): ArrayBuffer {139 const header = 80;140 const directory = 64;141 const payloadAt = header + directory * columns.length;142 const length = columns.reduce((sum, column) => sum + column.present.length + column.data.length, payloadAt);143 const buffer = new ArrayBuffer(length);144 const bytes = new Uint8Array(buffer);145 const view = new DataView(buffer);146 bytes.set(new TextEncoder().encode("ATLASPK\0"), 0);147 view.setUint16(8, 1, true);148 view.setUint32(12, 2, true);149 view.setUint32(16, columns.length, true);150 for (let i = 0; i < 32; i++) bytes[20 + i] = i;151 bytes.set(idBytes(ROOT), 52);152 view.setUint32(68, directory, true);153 view.setBigUint64(72, BigInt(payloadAt), true);154155 let payload = payloadAt;156 columns.forEach((column, index) => {157 const at = header + index * directory;158 bytes.set(idBytes(column.id), at);159 bytes[at + 16] = column.kind;160 bytes[at + 17] = column.kind;161 view.setUint16(at + 18, column.optional ? 1 : 0, true);162 view.setBigUint64(at + 20, BigInt(payload), true);163 view.setBigUint64(at + 28, BigInt(column.present.length), true);164 bytes.set(column.present, payload);165 payload += column.present.length;166 view.setBigUint64(at + 36, BigInt(payload), true);167 view.setBigUint64(at + 44, BigInt(column.data.length), true);168 bytes.set(column.data, payload);169 payload += column.data.length;170 view.setUint32(at + 52, crc32(column.present, column.data), true);171 });172 return buffer;173}174175function variableColumn(values: readonly (Uint8Array | null)[]): Pick<EncodedColumn, "present" | "data"> {176 const present = new Uint8Array([0b01]);177 const offsets = new Uint32Array(values.length + 1);178 const length = values.reduce((sum, value) => sum + (value?.length ?? 0), 0);179 const data = new Uint8Array(offsets.byteLength + length);180 let at = 0;181 values.forEach((value, index) => {182 new DataView(data.buffer).setUint32(index * 4, at, true);183 if (value) {184 data.set(value, offsets.byteLength + at);185 at += value.length;186 }187 });188 new DataView(data.buffer).setUint32(values.length * 4, at, true);189 return { present, data };190}191192function fixedInt64Column(values: readonly bigint[]): Pick<EncodedColumn, "present" | "data"> {193 const present = new Uint8Array([0b11]);194 const data = new Uint8Array(values.length * 8);195 values.forEach((value, index) => new DataView(data.buffer).setBigInt64(index * 8, value, true));196 return { present, data };197}198199function idBytes(id: string): Uint8Array {200 return Uint8Array.from(id.replaceAll("-", "").match(/../g) ?? [], (pair) => Number.parseInt(pair, 16));201}202203function crc32(...parts: readonly Uint8Array[]): number {204 let crc = 0xffffffff;205 for (const part of parts) {206 for (const byte of part) {207 crc ^= byte;208 for (let bit = 0; bit < 8; bit++) crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));209 }210 }211 return (crc ^ 0xffffffff) >>> 0;212}213214function geometry(kind: number, parts: readonly (readonly (readonly [number, number][])[])[]): Uint8Array {215 const length = 5 + parts.reduce((partSum, rings) => partSum + 4 + rings.reduce(216 (ringSum, positions) => ringSum + 4 + positions.length * 16, 0), 0);217 const bytes = new Uint8Array(length);218 const view = new DataView(bytes.buffer);219 bytes[0] = kind;220 view.setUint32(1, parts.length, true);221 let at = 5;222 for (const rings of parts) {223 view.setUint32(at, rings.length, true);224 at += 4;225 for (const positions of rings) {226 view.setUint32(at, positions.length, true);227 at += 4;228 for (const [x, y] of positions) {229 view.setFloat64(at, x, true);230 view.setFloat64(at + 8, y, true);231 at += 16;232 }233 }234 }235 return bytes;236}