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 / data / semantic.ts
semantic.ts 25.5K
1// The semantic vNext graph as the browser reads it.2//3// Nothing in this module knows the v3 document shapes. Stable field identities4// select columns, wire kinds select decoders, and unknown typed properties stay5// attached to the entity that owns them.67import { PackedTable, type Schema } from "./vnext.ts";89export type Position = readonly [number, number];1011export interface GeometryPart {12 readonly rings: readonly (readonly Position[])[];13}1415export interface Geometry {16 readonly kind: 1 | 2 | 3;17 readonly parts: readonly GeometryPart[];18}1920export type PropertyValue = boolean | bigint | number | string | Uint8Array;2122export interface Property {23 readonly id: string;24 readonly name: string;25 readonly kind: number;26 readonly value: PropertyValue;27}2829export interface Relationship {30 readonly predicate: string;31 readonly target: string;32}3334export interface Provenance {35 readonly source: string;36 readonly nativeID: string;37 readonly capturedAt: string;38}3940export interface Feature {41 readonly id: string;42 readonly title: string;43 readonly subtitle: string;44 readonly description: string;45 readonly center: Position | null;46 readonly shard: number;47 readonly geometry: Geometry;48 readonly properties: readonly Property[];49 readonly relationships: readonly Relationship[];50 readonly provenance: readonly Provenance[];51}5253export interface FeatureSet {54 readonly id: string;55 readonly title: string;56 readonly semanticType: string;57 readonly properties: readonly FieldDefinition[];58 readonly claims: readonly Property[];59 readonly features: readonly Feature[];60}6162export interface FieldDefinition {63 readonly id: string;64 readonly name: string;65 readonly kind: number;66 readonly optional: boolean;67}6869export interface CoordinateSpace {70 readonly id: string;71 readonly kind: string;72 readonly unit: string;73 readonly definition: string;74 readonly extent: readonly [number, number, number, number];75 readonly sourceZoom: number;76 readonly originX: number;77 readonly originY: number;78 readonly tileSize: number;79 readonly size: number;80}8182export interface RasterRect {83 readonly x: number;84 readonly y: number;85 readonly width: number;86 readonly height: number;87}8889export interface RasterCoverage {90 readonly zoom: number;91 readonly x: number;92 readonly y: number;93 readonly w: number;94 readonly h: number;95 readonly bits: Uint8Array;96}9798export interface RasterPyramid {99 readonly id: string;100 readonly name: string;101 readonly codec: string;102 readonly tileSize: number;103 readonly minZoom: number;104 readonly maxZoom: number;105 readonly fullZoom: number;106 readonly sourceZoom: number;107 readonly template: string;108 readonly formats: readonly string[];109 readonly bounds: RasterRect | null;110 readonly surface: RasterRect | null;111 readonly interpolate: boolean;112 readonly background: string;113 readonly shard: number;114 readonly coverage: readonly RasterCoverage[];115}116117export interface Style {118 readonly id: string;119 readonly symbol: string;120 readonly icon: string;121 readonly iconAsset: string;122 readonly iconPicture: boolean;123 readonly renderAs: string;124 readonly stroke: string;125 readonly fill: string;126}127128export interface Layer {129 readonly id: string;130 readonly featureSet: string;131 readonly style: string;132 readonly group: string;133 readonly labelPolicy: string;134 readonly visible: boolean;135 readonly order: number;136 readonly minZoom: number;137 readonly maxZoom: number;138}139140export interface LegendEntry {141 readonly layer: string;142 readonly label: string;143 readonly order: number;144}145146export interface Presentation {147 readonly id: string;148 readonly title: string;149 readonly styles: readonly Style[];150 readonly layers: readonly Layer[];151 readonly legend: readonly LegendEntry[];152}153154export interface World {155 readonly id: string;156 readonly title: string;157 readonly coordinateSpace: CoordinateSpace;158 readonly claims: readonly Property[];159 readonly featureSets: readonly FeatureSet[];160 readonly rasterPyramids: readonly RasterPyramid[];161 readonly presentation: Presentation;162}163164export interface Asset {165 readonly id: string;166 readonly mediaType: string;167 readonly path: string;168 readonly provenance: string;169}170171export interface Volume {172 readonly id: string;173 readonly title: string;174 readonly worlds: readonly World[];175 readonly assets: ReadonlyMap<string, Asset>;176}177178export const TABLE_NAMES = [179 "volumes", "worlds", "coordinate-spaces", "feature-sets",180 "property-definitions", "features", "relationships", "provenance",181 "raster-pyramids", "raster-formats", "raster-coverage", "presentations",182 "styles", "layers", "legend", "assets",183] as const;184185export type TableName = typeof TABLE_NAMES[number];186export type NativeTables = ReadonlyMap<TableName, PackedTable>;187188/** The browser-side form of format/vnext.CoreID. */189export async function coreID(name: string): Promise<string> {190 const source = new TextEncoder().encode(`dev.atlas.core\0${name}`);191 const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", source)).slice(0, 16);192 digest[6] = ((digest[6] ?? 0) & 0x0f) | 0x50;193 digest[8] = ((digest[8] ?? 0) & 0x3f) | 0x80;194 const plain = [...digest].map((byte) => byte.toString(16).padStart(2, "0")).join("");195 return `${plain.slice(0, 8)}-${plain.slice(8, 12)}-${plain.slice(12, 16)}-${plain.slice(16, 20)}-${plain.slice(20)}`;196}197198/** Decode the durable geometry encoding without translating through GeoJSON. */199export function decodeGeometry(bytes: Uint8Array): Geometry {200 if (bytes.byteLength < 5) throw new Error("geometry is shorter than its header");201 const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);202 const kind = bytes[0];203 if (kind !== 1 && kind !== 2 && kind !== 3) throw new Error(`geometry kind ${kind ?? 0} is unsupported`);204 const count = view.getUint32(1, true);205 const parts: GeometryPart[] = [];206 let at = 5;207 for (let part = 0; part < count; part++) {208 const rings = readCount(view, at, "geometry part");209 at += 4;210 const decoded: Position[][] = [];211 for (let ring = 0; ring < rings; ring++) {212 const positions = readCount(view, at, "geometry ring");213 at += 4;214 const length = positions * 16;215 if (at > bytes.byteLength || length > bytes.byteLength - at) throw new Error("geometry positions are truncated");216 const line: Position[] = [];217 for (let position = 0; position < positions; position++) {218 line.push([view.getFloat64(at, true), view.getFloat64(at + 8, true)]);219 at += 16;220 }221 decoded.push(line);222 }223 parts.push({ rings: decoded });224 }225 if (at !== bytes.byteLength) throw new Error("geometry has trailing bytes");226 return { kind, parts };227}228229function readCount(view: DataView, at: number, subject: string): number {230 if (at < 0 || at + 4 > view.byteLength) throw new Error(`${subject} is truncated`);231 return view.getUint32(at, true);232}233234const CORE_FIELDS = [235 "volume.id", "volume.title",236 "world.id", "world.title", "world.coordinateSpace",237 "coordinate.id", "coordinate.world", "coordinate.kind", "coordinate.unit",238 "coordinate.definition", "coordinate.extent", "coordinate.sourceZoom",239 "coordinate.originX", "coordinate.originY", "coordinate.tileSize", "coordinate.size",240 "featureSet.id", "featureSet.world", "featureSet.title", "featureSet.semanticType",241 "propertyDefinition.featureSet", "propertyDefinition.field", "propertyDefinition.name",242 "propertyDefinition.kind", "propertyDefinition.optional",243 "feature.id", "feature.featureSet", "feature.title", "feature.subtitle",244 "feature.description", "feature.center", "feature.shard", "feature.geometry",245 "relationship.feature", "relationship.predicate", "relationship.target",246 "provenance.feature", "provenance.source", "provenance.nativeID", "provenance.capturedAt",247 "rasterPyramid.id", "rasterPyramid.world", "rasterPyramid.name", "rasterPyramid.codec",248 "rasterPyramid.tileSize", "rasterPyramid.minZoom", "rasterPyramid.maxZoom",249 "rasterPyramid.fullZoom", "rasterPyramid.sourceZoom", "rasterPyramid.template",250 "rasterPyramid.bounds", "rasterPyramid.surface", "rasterPyramid.interpolate",251 "rasterPyramid.background", "rasterPyramid.shard",252 "rasterFormat.raster", "rasterFormat.zoom", "rasterFormat.format",253 "rasterCoverage.raster", "rasterCoverage.zoom", "rasterCoverage.x", "rasterCoverage.y",254 "rasterCoverage.w", "rasterCoverage.h", "rasterCoverage.bits",255 "presentation.id", "presentation.world", "presentation.title",256 "style.id", "style.presentation", "style.symbol", "style.icon", "style.iconAsset",257 "style.iconPicture", "style.renderAs", "style.stroke", "style.fill",258 "layer.id", "layer.presentation", "layer.featureSet", "layer.style", "layer.group",259 "layer.labelPolicy", "layer.visible", "layer.order", "layer.minZoom", "layer.maxZoom",260 "legend.presentation", "legend.layer", "legend.label", "legend.order",261 "asset.id", "asset.mediaType", "asset.path", "asset.provenance",262] as const;263264type CoreField = typeof CORE_FIELDS[number];265266class NativeTable {267 readonly #table: PackedTable;268 readonly #schema: Schema;269 readonly #ids: ReadonlyMap<string, string>;270 readonly #fields: ReadonlyMap<string, { readonly id: string; readonly name: string; readonly kind: number }>;271272 constructor(table: PackedTable, schema: Schema, ids: ReadonlyMap<string, string>) {273 this.#table = table;274 this.#schema = schema;275 this.#ids = ids;276 const type = schema.types.find((candidate) => candidate.id === table.rootType);277 if (!type) throw new Error(`schema does not describe table type ${table.rootType}`);278 this.#fields = new Map(type.fields.map((field) => [field.id, field]));279 }280281 get rows(): number { return this.#table.rows; }282283 string(name: CoreField, row: number): string {284 const value = this.#column(name).string(row);285 if (value === null) throw new Error(`${name} is null at row ${row}`);286 return value;287 }288289 optionalString(name: CoreField, row: number): string {290 return this.#column(name).string(row) ?? "";291 }292293 int(name: CoreField, row: number): number {294 const value = this.#column(name).int64(row);295 if (value === null) throw new Error(`${name} is null at row ${row}`);296 return safeNumber(value, name);297 }298299 bool(name: CoreField, row: number): boolean {300 const value = this.#column(name).bool(row);301 if (value === null) throw new Error(`${name} is null at row ${row}`);302 return value;303 }304305 bytes(name: CoreField, row: number): Uint8Array {306 const value = this.#column(name).bytes(row);307 if (value === null) throw new Error(`${name} is null at row ${row}`);308 return value;309 }310311 optionalBytes(name: CoreField, row: number): Uint8Array | null {312 return this.#column(name).bytes(row);313 }314315 identifier(name: CoreField, row: number): string {316 const value = this.#column(name).identifier(row);317 if (value === null) throw new Error(`${name} is null at row ${row}`);318 return value;319 }320321 extensions(row: number, owner: string): Property[] {322 const core = new Set(CORE_FIELDS.filter((name) => name.startsWith(`${owner}.`)).map((name) => this.#ids.get(name)));323 const properties: Property[] = [];324 for (const column of this.#table.columns) {325 if (core.has(column.id) || column.isNull(row)) continue;326 const field = this.#fields.get(column.id);327 if (!field) throw new Error(`schema does not describe extension field ${column.id}`);328 const value = columnValue(column, row);329 if (value !== null) properties.push({ id: field.id, name: field.name, kind: field.kind, value });330 }331 return properties;332 }333334 #column(name: CoreField) {335 const id = this.#ids.get(name);336 if (!id) throw new Error(`browser core schema has no ${name}`);337 const column = this.#table.column(id);338 if (!column) throw new Error(`table ${this.#table.rootType} has no ${name}`);339 return column;340 }341}342343interface WorldBuilder {344 id: string;345 title: string;346 coordinateSpace: CoordinateSpace | null;347 claims: Property[];348 featureSets: FeatureSetBuilder[];349 rasterPyramids: RasterBuilder[];350 presentation: PresentationBuilder | null;351}352353interface FeatureSetBuilder extends Omit<FeatureSet, "features" | "properties"> {354 features: FeatureBuilder[];355 properties: FieldDefinition[];356}357358interface FeatureBuilder extends Omit<Feature, "relationships" | "provenance"> {359 relationships: Relationship[];360 provenance: Provenance[];361}362363interface RasterBuilder extends Omit<RasterPyramid, "formats" | "coverage"> {364 formats: string[];365 coverage: RasterCoverage[];366}367368interface PresentationBuilder extends Omit<Presentation, "styles" | "layers" | "legend"> {369 styles: Style[];370 layers: Layer[];371 legend: LegendEntry[];372}373374/** Reconstruct the semantic graph directly from schema-described packed tables. */375export async function decodeVolume(schema: Schema, tables: NativeTables): Promise<Volume> {376 const ids = new Map<string, string>();377 await Promise.all(CORE_FIELDS.map(async (name) => ids.set(name, await coreID(name))));378 const table = (name: TableName): NativeTable => {379 const held = tables.get(name);380 if (!held) throw new Error(`volume has no data/${name}.pack`);381 return new NativeTable(held, schema, ids);382 };383384 const root = table("volumes");385 if (root.rows !== 1) throw new Error(`volume table has ${root.rows} rows`);386 const worlds = decodeWorlds(table);387 const sets = decodeFeatureSets(table, worlds);388 const features = decodeFeatures(table, sets);389 decodeEvidence(table, features);390 decodeRasters(table, worlds);391 decodePresentation(table, worlds);392 const assets = decodeAssets(table);393 const complete = [...worlds.values()].map(completeWorld);394 return { id: root.string("volume.id", 0), title: root.string("volume.title", 0), worlds: complete, assets };395}396397function decodeWorlds(table: (name: TableName) => NativeTable): Map<string, WorldBuilder> {398 const source = table("worlds");399 const worlds = new Map<string, WorldBuilder>();400 for (let row = 0; row < source.rows; row++) {401 const id = source.string("world.id", row);402 worlds.set(id, {403 id, title: source.string("world.title", row), coordinateSpace: null,404 claims: source.extensions(row, "world"), featureSets: [], rasterPyramids: [], presentation: null,405 });406 }407 const coordinates = table("coordinate-spaces");408 for (let row = 0; row < coordinates.rows; row++) {409 const world = requireFrom(worlds, coordinates.string("coordinate.world", row), "coordinate world");410 world.coordinateSpace = {411 id: coordinates.string("coordinate.id", row), kind: coordinates.string("coordinate.kind", row),412 unit: coordinates.string("coordinate.unit", row), definition: coordinates.string("coordinate.definition", row),413 extent: decodeExtent(coordinates.bytes("coordinate.extent", row)),414 sourceZoom: coordinates.int("coordinate.sourceZoom", row),415 originX: coordinates.int("coordinate.originX", row), originY: coordinates.int("coordinate.originY", row),416 tileSize: coordinates.int("coordinate.tileSize", row), size: coordinates.int("coordinate.size", row),417 };418 }419 return worlds;420}421422function decodeFeatureSets(423 table: (name: TableName) => NativeTable,424 worlds: ReadonlyMap<string, WorldBuilder>,425): Map<string, FeatureSetBuilder> {426 const source = table("feature-sets");427 const sets = new Map<string, FeatureSetBuilder>();428 for (let row = 0; row < source.rows; row++) {429 const id = source.string("featureSet.id", row);430 const set: FeatureSetBuilder = {431 id, title: source.string("featureSet.title", row), semanticType: source.string("featureSet.semanticType", row),432 claims: source.extensions(row, "featureSet"), properties: [], features: [],433 };434 requireFrom(worlds, source.string("featureSet.world", row), "feature-set world").featureSets.push(set);435 sets.set(id, set);436 }437 const definitions = table("property-definitions");438 for (let row = 0; row < definitions.rows; row++) {439 requireFrom(sets, definitions.string("propertyDefinition.featureSet", row), "property definition set").properties.push({440 id: definitions.identifier("propertyDefinition.field", row),441 name: definitions.string("propertyDefinition.name", row), kind: definitions.int("propertyDefinition.kind", row),442 optional: definitions.bool("propertyDefinition.optional", row),443 });444 }445 return sets;446}447448function decodeFeatures(449 table: (name: TableName) => NativeTable,450 sets: ReadonlyMap<string, FeatureSetBuilder>,451): Map<string, FeatureBuilder> {452 const source = table("features");453 const features = new Map<string, FeatureBuilder>();454 for (let row = 0; row < source.rows; row++) {455 const id = source.string("feature.id", row);456 const center = source.optionalBytes("feature.center", row);457 const feature: FeatureBuilder = {458 id, title: source.string("feature.title", row), subtitle: source.optionalString("feature.subtitle", row),459 description: source.optionalString("feature.description", row), center: center ? decodePosition(center) : null,460 shard: source.int("feature.shard", row), geometry: decodeGeometry(source.bytes("feature.geometry", row)),461 properties: source.extensions(row, "feature"), relationships: [], provenance: [],462 };463 requireFrom(sets, source.string("feature.featureSet", row), "feature set").features.push(feature);464 features.set(id, feature);465 }466 return features;467}468469function decodeEvidence(470 table: (name: TableName) => NativeTable,471 features: ReadonlyMap<string, FeatureBuilder>,472): void {473 const relationships = table("relationships");474 for (let row = 0; row < relationships.rows; row++) {475 requireFrom(features, relationships.string("relationship.feature", row), "relationship feature").relationships.push({476 predicate: relationships.string("relationship.predicate", row), target: relationships.string("relationship.target", row),477 });478 }479 const provenance = table("provenance");480 for (let row = 0; row < provenance.rows; row++) {481 requireFrom(features, provenance.string("provenance.feature", row), "provenance feature").provenance.push({482 source: provenance.string("provenance.source", row), nativeID: provenance.string("provenance.nativeID", row),483 capturedAt: provenance.string("provenance.capturedAt", row),484 });485 }486}487488function decodeRasters(489 table: (name: TableName) => NativeTable,490 worlds: ReadonlyMap<string, WorldBuilder>,491): void {492 const source = table("raster-pyramids");493 const rasters = new Map<string, RasterBuilder>();494 for (let row = 0; row < source.rows; row++) {495 const id = source.string("rasterPyramid.id", row);496 const bounds = source.optionalBytes("rasterPyramid.bounds", row);497 const surface = source.optionalBytes("rasterPyramid.surface", row);498 const raster: RasterBuilder = {499 id, name: source.string("rasterPyramid.name", row), codec: source.string("rasterPyramid.codec", row),500 tileSize: source.int("rasterPyramid.tileSize", row), minZoom: source.int("rasterPyramid.minZoom", row),501 maxZoom: source.int("rasterPyramid.maxZoom", row), fullZoom: source.int("rasterPyramid.fullZoom", row),502 sourceZoom: source.int("rasterPyramid.sourceZoom", row), template: source.string("rasterPyramid.template", row),503 bounds: bounds ? decodeRect(bounds) : null, surface: surface ? decodeRect(surface) : null,504 interpolate: source.bool("rasterPyramid.interpolate", row), background: source.optionalString("rasterPyramid.background", row),505 shard: source.int("rasterPyramid.shard", row), formats: [], coverage: [],506 };507 requireFrom(worlds, source.string("rasterPyramid.world", row), "raster world").rasterPyramids.push(raster);508 rasters.set(id, raster);509 }510 const formats = table("raster-formats");511 const byRaster = new Map<string, Map<number, string>>();512 for (let row = 0; row < formats.rows; row++) {513 const id = formats.string("rasterFormat.raster", row);514 requireFrom(rasters, id, "raster format raster");515 const held = byRaster.get(id) ?? new Map<number, string>();516 held.set(formats.int("rasterFormat.zoom", row), formats.string("rasterFormat.format", row));517 byRaster.set(id, held);518 }519 for (const [id, raster] of rasters) {520 const held = byRaster.get(id) ?? new Map<number, string>();521 for (let zoom = raster.minZoom; zoom <= raster.maxZoom; zoom++) raster.formats.push(held.get(zoom) ?? "");522 }523 const coverage = table("raster-coverage");524 for (let row = 0; row < coverage.rows; row++) {525 requireFrom(rasters, coverage.string("rasterCoverage.raster", row), "raster coverage raster").coverage.push({526 zoom: coverage.int("rasterCoverage.zoom", row), x: coverage.int("rasterCoverage.x", row),527 y: coverage.int("rasterCoverage.y", row), w: coverage.int("rasterCoverage.w", row),528 h: coverage.int("rasterCoverage.h", row), bits: coverage.bytes("rasterCoverage.bits", row),529 });530 }531}532533function decodePresentation(534 table: (name: TableName) => NativeTable,535 worlds: ReadonlyMap<string, WorldBuilder>,536): void {537 const source = table("presentations");538 const presentations = new Map<string, PresentationBuilder>();539 for (let row = 0; row < source.rows; row++) {540 const id = source.string("presentation.id", row);541 const presentation: PresentationBuilder = {542 id, title: source.string("presentation.title", row), styles: [], layers: [], legend: [],543 };544 requireFrom(worlds, source.string("presentation.world", row), "presentation world").presentation = presentation;545 presentations.set(id, presentation);546 }547 const styles = table("styles");548 for (let row = 0; row < styles.rows; row++) {549 requireFrom(presentations, styles.string("style.presentation", row), "style presentation").styles.push({550 id: styles.string("style.id", row), symbol: styles.optionalString("style.symbol", row),551 icon: styles.optionalString("style.icon", row), iconAsset: styles.optionalString("style.iconAsset", row),552 iconPicture: styles.bool("style.iconPicture", row), renderAs: styles.optionalString("style.renderAs", row),553 stroke: styles.optionalString("style.stroke", row), fill: styles.optionalString("style.fill", row),554 });555 }556 const layers = table("layers");557 for (let row = 0; row < layers.rows; row++) {558 requireFrom(presentations, layers.string("layer.presentation", row), "layer presentation").layers.push({559 id: layers.string("layer.id", row), featureSet: layers.string("layer.featureSet", row),560 style: layers.string("layer.style", row), group: layers.optionalString("layer.group", row),561 labelPolicy: layers.optionalString("layer.labelPolicy", row), visible: layers.bool("layer.visible", row),562 order: layers.int("layer.order", row), minZoom: layers.int("layer.minZoom", row), maxZoom: layers.int("layer.maxZoom", row),563 });564 }565 const legend = table("legend");566 for (let row = 0; row < legend.rows; row++) {567 requireFrom(presentations, legend.string("legend.presentation", row), "legend presentation").legend.push({568 layer: legend.string("legend.layer", row), label: legend.string("legend.label", row), order: legend.int("legend.order", row),569 });570 }571 for (const presentation of presentations.values()) {572 presentation.layers.sort((left, right) => left.order - right.order || left.id.localeCompare(right.id));573 presentation.legend.sort((left, right) => left.order - right.order || left.layer.localeCompare(right.layer));574 }575}576577function decodeAssets(table: (name: TableName) => NativeTable): ReadonlyMap<string, Asset> {578 const source = table("assets");579 const assets = new Map<string, Asset>();580 for (let row = 0; row < source.rows; row++) {581 const id = source.string("asset.id", row);582 assets.set(id, {583 id, mediaType: source.string("asset.mediaType", row), path: source.string("asset.path", row),584 provenance: source.optionalString("asset.provenance", row),585 });586 }587 return assets;588}589590function completeWorld(builder: WorldBuilder): World {591 if (!builder.coordinateSpace) throw new Error(`world ${builder.id} has no coordinate space`);592 if (!builder.presentation) throw new Error(`world ${builder.id} has no presentation`);593 return {594 id: builder.id, title: builder.title, coordinateSpace: builder.coordinateSpace,595 claims: builder.claims, featureSets: builder.featureSets, rasterPyramids: builder.rasterPyramids,596 presentation: builder.presentation,597 };598}599600function decodeExtent(bytes: Uint8Array): readonly [number, number, number, number] {601 if (bytes.byteLength !== 32) throw new Error("extent is not four float64 values");602 const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);603 return [view.getFloat64(0, true), view.getFloat64(8, true), view.getFloat64(16, true), view.getFloat64(24, true)];604}605606function decodePosition(bytes: Uint8Array): Position {607 if (bytes.byteLength !== 16) throw new Error("position is not two float64 values");608 const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);609 return [view.getFloat64(0, true), view.getFloat64(8, true)];610}611612function decodeRect(bytes: Uint8Array): RasterRect {613 const [x, y, width, height] = decodeExtent(bytes);614 return { x, y, width, height };615}616617function columnValue(column: ReturnType<PackedTable["column"]> & {}, row: number): PropertyValue | null {618 switch (column.kind) {619 case 1: return column.bool(row);620 case 2: return column.int64(row);621 case 3: return column.float64(row);622 case 4: return column.string(row);623 case 5: return column.bytes(row);624 case 6: return column.identifier(row);625 default: throw new Error(`field ${column.id} has unsupported wire kind ${column.kind}`);626 }627}628629function safeNumber(value: bigint, subject: string): number {630 if (value > BigInt(Number.MAX_SAFE_INTEGER) || value < BigInt(Number.MIN_SAFE_INTEGER)) {631 throw new Error(`${subject} exceeds JavaScript's safe integer range`);632 }633 return Number(value);634}635636function requireFrom<K, V>(source: ReadonlyMap<K, V>, key: K, subject: string): V {637 const held = source.get(key);638 if (held === undefined) throw new Error(`${subject} ${String(key)} does not exist`);639 return held;640}