@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 +56 -2
- package/grid_index.d.ts +2 -0
- package/grid_index.mjs +37 -0
- package/osm_dybuf.d.mts +30 -4
- package/osm_dybuf.mjs +88 -19
- package/package.json +4 -1
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.
|
|
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
|
-
-
|
|
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
|
|
38
|
-
|
|
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
|
|
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
|
|
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
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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 {
|
|
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.
|
|
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
|
}
|