@truelies/osm-dybuf 0.2.7 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -3,6 +3,19 @@
3
3
  Gridex `osm.dybuf` parser utilities shared by the exporter, inspection scripts, and Next.js frontend.
4
4
  This package wraps the shared schema tables (`schema_ids`) and exposes a high-level `parseOsmDybuf` helper.
5
5
  It depends on the published [`dybuf`](https://www.npmjs.com/package/dybuf) runtime (≥ 0.4.2).
6
+ Feature IDs are packed as `feature_id = sub_id * 64 + main_id` (both 0–63). Use `schema_ids` to decode feature strings.
7
+
8
+ ## Current Compatibility Status
9
+
10
+ `parseOsmDybuf` now supports the current cloud formats and keeps a legacy fallback.
11
+
12
+ Support matrix:
13
+
14
+ - Flat V1 cell file (`osm.dybuf`): `version + payload` -> supported (`format="flat_v1"`)
15
+ - V2 overview/bundle (`overview.dybuf`, `bundle.dybuf`): `version=2 + index entries` -> supported (`format="v2_indexed"`)
16
+ - Legacy V1 with region chunks: `version + [region_id + region_payload]...` -> supported as fallback (`format="legacy_v1_regions"`)
17
+
18
+ `inspect_dybuf.mjs` now prints by detected format.
6
19
 
7
20
  ## Usage
8
21
 
@@ -21,10 +34,25 @@ import {
21
34
  const data = await fetch("/tiles/08/213/161/osm.dybuf").then((res) => res.arrayBuffer());
22
35
  const cell = { level: 8, londex: 213, latdex: 161 };
23
36
  const parsed = parseOsmDybuf(data, cell);
37
+
38
+ if (parsed.format === "flat_v1") {
39
+ console.log(parsed.features.length);
40
+ } else if (parsed.format === "v2_indexed") {
41
+ console.log(parsed.entries.length);
42
+ } else {
43
+ console.log(parsed.regions.length);
44
+ }
24
45
  ```
25
46
 
26
47
  - CLI inspection (development only):
27
48
  ```bash
49
+ # Preferred: only provide file path.
50
+ # For v1 osm.dybuf, the tool auto-infers level/londex/latdex from .../<lv>/<x>/<y>/osm.dybuf
51
+ node js/inspect_dybuf.mjs \
52
+ --file /path/to/osm.dybuf \
53
+ --limit 5
54
+
55
+ # Optional fallback when path does not contain cell coordinates (needed for v1):
28
56
  node js/inspect_dybuf.mjs \
29
57
  --file /path/to/osm.dybuf \
30
58
  --level 8 --londex 213 --latdex 161 --limit 5
@@ -32,7 +60,7 @@ const parsed = parseOsmDybuf(data, cell);
32
60
 
33
61
  ### Grid helpers (grid_index)
34
62
 
35
- `grid_index.ts` exposes the minimal Gridex helpers (integers, pole triangles) aligned with the exporter:
63
+ `grid_index.mjs` exposes Gridex helpers (integers, pole triangles) aligned with the exporter:
36
64
 
37
65
  ```ts
38
66
  import { GridUnit, Gridex, INT_COORD_SCALE } from "@truelies/osm-dybuf/grid_index";
@@ -40,18 +68,44 @@ import { GridUnit, Gridex, INT_COORD_SCALE } from "@truelies/osm-dybuf/grid_inde
40
68
  const unit = new GridUnit(8); // level 8
41
69
  const cell = new Gridex(213, 161);
42
70
  const [minLon, minLat, maxLon, maxLat] = unit.boundsOfGrid(cell);
71
+
72
+ // parent/child conversion (same rules as Python GridUnit)
73
+ const children = unit.toLowerLevelGridexes(cell); // level 9, 4 cells
74
+ const parent = new GridUnit(9).toUpperLevelGridex(children[0]); // back to level 8
43
75
  ```
44
76
 
77
+ - `toLowerLevelGridexes(gridex)`:
78
+ - returns 4 child cells at `level + 1`
79
+ - throws when current unit is already max level (`16`)
80
+ - `toUpperLevelGridex(gridex)`:
81
+ - returns the parent cell at `level - 1`
82
+ - throws when current unit is level `0`
83
+
45
84
  ## Local Development
46
85
 
47
86
  ```bash
48
87
  cd js
49
88
  npm install # installs dybuf runtime for this package
89
+ npm test # runs unit tests (node:test)
50
90
  ```
51
91
 
92
+ Unit test example (`tests/grid_index.test.mjs`) covers:
93
+ - `gridexesAt` near axis (no zero index cell)
94
+ - `toLowerLevelGridexes` and `toUpperLevelGridex` roundtrip
95
+ - level boundary errors (`lv0` has no upper level, `lv16` has no lower level)
96
+
52
97
  - `npm pack`: build a tarball so other repos can install via `npm install ./path/to/osm-dybuf-*.tgz`
53
98
  - `npm link`: link the package locally (`npm link` here, then `npm link @truelies/osm-dybuf` in the consumer repo)
54
- - `npm publish --access public`: publish to npm (requires the `@truelies` scope). If the scope is not available, rename `package.json` → `"name": "truelies-osm-dybuf"` and publish without a scope.
99
+ - Publish to npm (requires `@truelies` scope):
100
+ ```bash
101
+ cd js
102
+ npm version <new-version> --no-git-tag-version
103
+ NPM_CONFIG_CACHE=/tmp/npm-cache npm login --auth-type=web --registry=https://registry.npmjs.org/ --scope=@truelies
104
+ NPM_CONFIG_CACHE=/tmp/npm-cache npm whoami
105
+ NPM_CONFIG_CACHE=/tmp/npm-cache npm publish --access public
106
+ ```
107
+ - If `~/.npm` has permission issues (root-owned cache/logs), keep using `NPM_CONFIG_CACHE=/tmp/npm-cache`.
108
+ - If the scope is not available, rename `package.json` → `"name": "truelies-osm-dybuf"` and publish without a scope.
55
109
 
56
110
  ## Files
57
111
 
package/grid_index.d.ts CHANGED
@@ -31,6 +31,8 @@ export declare class GridUnit {
31
31
  makeGridex(londex: number, latdex: number): GridexAtLv;
32
32
  gridexesAt(longitudeInt: number, latitudeInt: number): GridexAtLv[];
33
33
  gridexesWithExt(longitudeInt: number, latitudeInt: number, ext?: number): GridexAtLv[];
34
+ toLowerLevelGridexes(gridex: Gridex): GridexAtLv[];
35
+ toUpperLevelGridex(gridex: Gridex): GridexAtLv;
34
36
  boundsOfGridFine(gridex: Gridex, applyTriangle?: boolean): [number, number, number, number];
35
37
  boundsOfGrid(gridex: Gridex, applyTriangle?: boolean): [number, number, number, number];
36
38
  gridexRangeForBounds(
package/grid_index.mjs CHANGED
@@ -158,6 +158,43 @@ export class GridUnit {
158
158
  return result;
159
159
  }
160
160
 
161
+ toLowerLevelGridexes(gridex) {
162
+ if (this.level >= MAX_LEVEL) {
163
+ throw new Error(`No lower level exists for level ${this.level}`);
164
+ }
165
+ const lon = gridex.londex;
166
+ const lat = gridex.latdex;
167
+ const lonInc = lon > 0 ? -1 : 1;
168
+ const latInc = lat > 0 ? -1 : 1;
169
+ const childUnit = new GridUnit(this.level + 1);
170
+ return [
171
+ childUnit.makeGridex(lon * 2 + lonInc, lat * 2 + latInc),
172
+ childUnit.makeGridex(lon * 2 + lonInc, lat * 2),
173
+ childUnit.makeGridex(lon * 2, lat * 2 + latInc),
174
+ childUnit.makeGridex(lon * 2, lat * 2),
175
+ ];
176
+ }
177
+
178
+ toUpperLevelGridex(gridex) {
179
+ if (this.level <= 0) {
180
+ throw new Error("No upper level exists for level 0");
181
+ }
182
+ const lon = gridex.londex;
183
+ const lat = gridex.latdex;
184
+ const lonInc = lon > 0 ? -1 : 1;
185
+ const latInc = lat > 0 ? -1 : 1;
186
+ const upperLon =
187
+ lon > 0
188
+ ? Math.floor((lon - lonInc) / 2)
189
+ : Math.ceil((lon - lonInc) / 2);
190
+ const upperLat =
191
+ lat > 0
192
+ ? Math.floor((lat - latInc) / 2)
193
+ : Math.ceil((lat - latInc) / 2);
194
+ const upperUnit = new GridUnit(this.level - 1);
195
+ return upperUnit.makeGridex(upperLon, upperLat);
196
+ }
197
+
161
198
  boundsOfGridFine(gridex, applyTriangle = false) {
162
199
  const { londex, latdex } = gridex;
163
200
  const minLonFine = (londex - (londex > 0 ? 1 : 0)) * this.unitFine;
package/osm_dybuf.d.mts CHANGED
@@ -10,7 +10,7 @@ export interface GeometryCoordinate {
10
10
  }
11
11
 
12
12
  export interface GeometrySegment {
13
- role: string;
13
+ role: string | null;
14
14
  coordinates: GeometryCoordinate[];
15
15
  }
16
16
 
@@ -34,10 +34,36 @@ export interface RegionChunk {
34
34
  features: GridFeature[];
35
35
  }
36
36
 
37
- export interface OsmDybuf {
38
- version: number;
37
+ export interface V2IndexEntry {
38
+ cell: GridCellRef;
39
+ features: GridFeature[];
40
+ }
41
+
42
+ export interface FlatV1Dybuf {
43
+ version: 1;
44
+ format: "flat_v1";
45
+ cell: GridCellRef;
46
+ features: GridFeature[];
47
+ regions: RegionChunk[];
48
+ }
49
+
50
+ export interface LegacyV1Dybuf {
51
+ version: 1;
52
+ format: "legacy_v1_regions";
39
53
  regions: RegionChunk[];
40
54
  }
41
55
 
56
+ export interface V2Dybuf {
57
+ version: 2;
58
+ format: "v2_indexed";
59
+ entries: V2IndexEntry[];
60
+ }
61
+
62
+ export type OsmDybuf = FlatV1Dybuf | LegacyV1Dybuf | V2Dybuf | {
63
+ version: number;
64
+ format: string;
65
+ };
66
+
42
67
  export declare function parseOsmDybuf(data: ArrayBuffer | ArrayBufferView, cell: GridCellRef): OsmDybuf;
43
- export declare function decodeRegionPayload(payload: ArrayBuffer, cell: GridCellRef): GridFeature[];
68
+ export declare function decodePayload(payload: ArrayBuffer | ArrayBufferView, cell: GridCellRef): GridFeature[];
69
+ export declare function decodeRegionPayload(payload: ArrayBuffer | ArrayBufferView, cell: GridCellRef): GridFeature[];
package/osm_dybuf.mjs CHANGED
@@ -1,11 +1,7 @@
1
1
  import {
2
2
  DyBuf,
3
- TYPDEX_TYP_BOOL,
4
- TYPDEX_TYP_INT,
5
3
  TYPDEX_TYP_UINT,
6
- TYPDEX_TYP_STRING,
7
4
  TYPDEX_TYP_BYTES,
8
- TYPDEX_TYP_ARRAY,
9
5
  TYPDEX_TYP_MAP,
10
6
  } from "dybuf";
11
7
  import {
@@ -106,11 +102,11 @@ function decodeGeometrySegments(bytes, cell) {
106
102
  const segmentCount = readVarUint(buf);
107
103
  for (let i = 0; i < segmentCount; i += 1) {
108
104
  const roleId = buf.getByte();
109
- let role = "";
105
+ let role = null;
110
106
  if (roleId === SEGMENT_ROLE_ID_UNSUPPORTED) {
111
107
  role = "<unsupported>";
112
108
  } else if (roleId !== SEGMENT_ROLE_ID_UNSET) {
113
- role = ID_TO_SEGMENT_ROLE[String(roleId)] ?? "";
109
+ role = ID_TO_SEGMENT_ROLE[String(roleId)] ?? "<unsupported>";
114
110
  }
115
111
  const pointCount = readVarUint(buf);
116
112
  const points = [];
@@ -192,14 +188,14 @@ function decodeGridPayload(bytes, cell) {
192
188
  };
193
189
  }
194
190
 
195
- function decodeRegionPayload(payloadBytes, cell) {
191
+ function decodePayload(payloadBytes, cell) {
196
192
  const buf = new DyBuf(payloadBytes, false);
197
193
  const featureCount = readVarUint(buf);
198
194
  const features = [];
199
195
  for (let i = 0; i < featureCount; i += 1) {
200
196
  const featureNumericId = readVarUint(buf);
201
197
  const featureId =
202
- ID_TO_FEATURE_KIND[featureNumericId] ?? `feature:${featureNumericId}`;
198
+ ID_TO_FEATURE_KIND[String(featureNumericId)] ?? `feature:${featureNumericId}`;
203
199
  const entryCount = readVarUint(buf);
204
200
  const elements = [];
205
201
  for (let j = 0; j < entryCount; j += 1) {
@@ -208,9 +204,48 @@ function decodeRegionPayload(payloadBytes, cell) {
208
204
  }
209
205
  features.push({ featureId, elements });
210
206
  }
207
+ if (buf.position() !== buf.limit()) {
208
+ throw new Error(
209
+ `Payload has trailing bytes (${buf.limit() - buf.position()} bytes remain)`
210
+ );
211
+ }
211
212
  return features;
212
213
  }
213
214
 
215
+ function decodeLegacyRegions(bodyBytes, cell) {
216
+ const buf = new DyBuf(bodyBytes, false);
217
+ const regions = [];
218
+ while (buf.position() < buf.limit()) {
219
+ const regionId = readVarUint(buf);
220
+ const regionName = REGION_ID_TO_NAME[String(regionId)] ?? `region:${regionId}`;
221
+ const regionPayload = readVarBytes(buf);
222
+ const features = decodePayload(regionPayload, cell);
223
+ regions.push({ id: regionId, name: regionName, features });
224
+ }
225
+ return regions;
226
+ }
227
+
228
+ function decodeV2Entries(bodyBytes) {
229
+ const buf = new DyBuf(bodyBytes, false);
230
+ const count = readVarUint(buf);
231
+ const entries = [];
232
+ for (let i = 0; i < count; i += 1) {
233
+ const level = buf.getByte();
234
+ const londex = readVarInt(buf);
235
+ const latdex = readVarInt(buf);
236
+ const payloadBytes = readVarBytes(buf);
237
+ const cell = { level, londex, latdex };
238
+ const features = decodePayload(payloadBytes, cell);
239
+ entries.push({ cell, features });
240
+ }
241
+ if (buf.position() !== buf.limit()) {
242
+ throw new Error(
243
+ `V2 payload has trailing bytes (${buf.limit() - buf.position()} bytes remain)`
244
+ );
245
+ }
246
+ return entries;
247
+ }
248
+
214
249
  /**
215
250
  * Parse a single cell's osm.dybuf payload.
216
251
  * @param {ArrayBuffer | ArrayBufferView} data
@@ -221,17 +256,51 @@ export function parseOsmDybuf(data, cell) {
221
256
  throw new TypeError("Cell metadata (level, londex, latdex) is required");
222
257
  }
223
258
  const buffer = toArrayBuffer(data);
224
- const buf = new DyBuf(buffer, false);
225
- const version = buf.getByte();
226
- const regions = [];
227
- while (buf.position() < buf.limit()) {
228
- const regionId = readVarUint(buf);
229
- const regionName = REGION_ID_TO_NAME[String(regionId)] ?? `region:${regionId}`;
230
- const regionPayload = readVarBytes(buf);
231
- const features = decodeRegionPayload(regionPayload, cell);
232
- regions.push({ id: regionId, name: regionName, features });
259
+ const bytes = new Uint8Array(buffer);
260
+ if (bytes.length === 0) {
261
+ throw new Error("Empty dybuf payload");
262
+ }
263
+
264
+ const version = bytes[0];
265
+ const body = bytes.buffer.slice(bytes.byteOffset + 1, bytes.byteOffset + bytes.byteLength);
266
+
267
+ if (version === 2) {
268
+ return {
269
+ version,
270
+ format: "v2_indexed",
271
+ entries: decodeV2Entries(body),
272
+ };
273
+ }
274
+
275
+ if (version !== 1) {
276
+ throw new Error(`Unsupported dybuf version ${version}`);
277
+ }
278
+
279
+ // Prefer current flat V1: version + payload
280
+ try {
281
+ const features = decodePayload(body, cell);
282
+ return {
283
+ version,
284
+ format: "flat_v1",
285
+ cell: { level: cell.level, londex: cell.londex, latdex: cell.latdex },
286
+ features,
287
+ regions: [],
288
+ };
289
+ } catch (flatErr) {
290
+ // Fallback to legacy format: version + [region_id + region_payload]...
291
+ try {
292
+ const regions = decodeLegacyRegions(body, cell);
293
+ return {
294
+ version,
295
+ format: "legacy_v1_regions",
296
+ regions,
297
+ };
298
+ } catch (legacyErr) {
299
+ throw new Error(
300
+ `Unable to decode V1 dybuf as flat or legacy regions. flat=${String(flatErr)} legacy=${String(legacyErr)}`
301
+ );
302
+ }
233
303
  }
234
- return { version, regions };
235
304
  }
236
305
 
237
- export { decodeRegionPayload, decodeGridPayload };
306
+ export { decodePayload, decodeGridPayload, decodePayload as decodeRegionPayload };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truelies/osm-dybuf",
3
- "version": "0.2.7",
3
+ "version": "0.3.1",
4
4
  "description": "Gridex OSM DyBuf parser and schema utilities",
5
5
  "type": "module",
6
6
  "main": "./osm_dybuf.mjs",
@@ -39,6 +39,9 @@
39
39
  ],
40
40
  "author": "TrueLies",
41
41
  "license": "UNLICENSED",
42
+ "scripts": {
43
+ "test": "node --test tests/*.test.mjs"
44
+ },
42
45
  "dependencies": {
43
46
  "dybuf": "^0.4.2"
44
47
  }