@truelies/osm-dybuf 0.4.2 → 0.4.5

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
@@ -88,7 +88,22 @@ Feature ID usage notes:
88
88
  `grid_index.mjs` exposes Gridex helpers (integers, pole triangles) aligned with the exporter:
89
89
 
90
90
  ```ts
91
- import { GridUnit, Gridex, INT_COORD_SCALE } from "@truelies/osm-dybuf/grid_index";
91
+ import {
92
+ GridUnit,
93
+ Gridex,
94
+ INT_COORD_SCALE,
95
+ CANONICAL_CODE_LEVEL,
96
+ gridexAtLonLat,
97
+ gridCodeAtLonLat,
98
+ gridCodeComponentsAtLonLat,
99
+ lonCodeFromLondex,
100
+ latCodeFromLatdex,
101
+ londexPrefixCodeFromLonCode,
102
+ lonLatCodeRangeForGridex,
103
+ encodeAxisCode,
104
+ decodeAxisCode,
105
+ lonCodeMagnitudeBits,
106
+ } from "@truelies/osm-dybuf/grid_index";
92
107
 
93
108
  const unit = new GridUnit(8); // level 8
94
109
  const cell = new Gridex(213, 161);
@@ -97,6 +112,28 @@ const [minLon, minLat, maxLon, maxLat] = unit.boundsOfGrid(cell);
97
112
  // parent/child conversion (same rules as Python GridUnit)
98
113
  const children = unit.toLowerLevelGridexes(cell); // level 9, 4 cells
99
114
  const parent = new GridUnit(9).toUpperLevelGridex(children[0]); // back to level 8
115
+
116
+ // GPS -> gridex / lonCode / latCode / gridCode
117
+ const gridex = gridexAtLonLat(16, 121.56, 25.03);
118
+ const parts = gridCodeComponentsAtLonLat(16, 121.56, 25.03);
119
+ console.log(gridex.londex, gridex.latdex);
120
+ console.log(parts.lonCode, parts.latCode, parts.gridCode);
121
+
122
+ // gridex <-> code
123
+ const lonCode = lonCodeFromLondex(16, -123);
124
+ const latCode = latCodeFromLatdex(16, 456);
125
+ console.log(lonCode, latCode);
126
+ console.log(gridCodeAtLonLat(16, 121.56, 25.03));
127
+
128
+ // canonical prefix / range for coarser level query
129
+ const lonPrefix = londexPrefixCodeFromLonCode(15, lonCode, CANONICAL_CODE_LEVEL);
130
+ const range = lonLatCodeRangeForGridex(15, -3, 4, CANONICAL_CODE_LEVEL);
131
+ console.log(lonPrefix, range.lonCode.from, range.lonCode.to);
132
+
133
+ // axis code roundtrip
134
+ const lonBits = lonCodeMagnitudeBits(CANONICAL_CODE_LEVEL);
135
+ const axisCode = encodeAxisCode(-123, lonBits);
136
+ console.log(decodeAxisCode(axisCode, lonBits)); // -123
100
137
  ```
101
138
 
102
139
  - `toLowerLevelGridexes(gridex)`:
@@ -105,6 +142,20 @@ const parent = new GridUnit(9).toUpperLevelGridex(children[0]); // back to level
105
142
  - `toUpperLevelGridex(gridex)`:
106
143
  - returns the parent cell at `level - 1`
107
144
  - throws when current unit is level `0`
145
+ - `gridexAtLonLat(level, lon, lat)`:
146
+ - converts GPS directly to a `Gridex`
147
+ - `gridCodeComponentsAtLonLat(level, lon, lat)`:
148
+ - returns `{ gridex, lonCode, latCode, gridCode }`
149
+ - `lonCodeFromLondex(level, londex)` / `latCodeFromLatdex(level, latdex)`:
150
+ - converts axis indices to canonical axis codes
151
+ - `londexPrefixCodeFromLonCode(targetLevel, lonCode, sourceLevel?)`:
152
+ - trims a finer `lonCode` into a coarser-level prefix view
153
+ - `lonLatCodeRangeForGridex(level, londex, latdex, sourceLevel?)`:
154
+ - expands any `lv <= sourceLevel` cell into the corresponding canonical axis-code range
155
+ - `gridCodeAtLonLat(level, lon, lat)` / `gridCodeFromGridex(level, gridex)`:
156
+ - returns the packed `gridCode`
157
+ - `encodeAxisCode(index, magnitudeBits)` / `decodeAxisCode(code, magnitudeBits)`:
158
+ - encodes grid index as `sign bit + (abs(index) - 1)`
108
159
 
109
160
  ## Local Development
110
161
 
@@ -118,6 +169,9 @@ Unit test example (`tests/grid_index.test.mjs`) covers:
118
169
  - `gridexesAt` near axis (no zero index cell)
119
170
  - `toLowerLevelGridexes` and `toUpperLevelGridex` roundtrip
120
171
  - level boundary errors (`lv0` has no upper level, `lv16` has no lower level)
172
+ - canonical axis-code encode/decode roundtrip
173
+ - GPS -> `gridex / lonCode / latCode / gridCode`
174
+ - `lv15` cell -> canonical code range expansion correctness
121
175
 
122
176
  - `npm pack`: build a tarball so other repos can install via `npm install ./path/to/osm-dybuf-*.tgz`
123
177
  - `npm link`: link the package locally (`npm link` here, then `npm link @truelies/osm-dybuf` in the consumer repo)
@@ -139,4 +193,4 @@ Unit test example (`tests/grid_index.test.mjs`) covers:
139
193
  - `region_numeric_codes.json`: latest region ID table (generated via `scripts/dump_region_codes.py`)
140
194
  - `inspect_dybuf.mjs`: CLI tool that uses the package locally
141
195
 
142
- When schema IDs or DyBuf layouts change in the exporter, remember to bump the version here and re-publish/install so frontends and tools stay in sync. Version `0.4.2` aligns the package with the current `lv0~13` export range, the current frontend feature set (`water_wetland`, `building`, derived `label_*`, no exported `place_label`), and the numeric `featureId` parser contract.
196
+ When schema IDs or DyBuf layouts change in the exporter, remember to bump the version here and re-publish/install so frontends and tools stay in sync. Version `0.4.3` aligns the package with the current `lv0~13` export range, the current frontend feature set (`water_wetland`, `building`, derived `label_*`, no exported `place_label`), the numeric `featureId` parser contract, and the canonical grid conversion helpers shared by Firestore / Redis / frontend indexing.
package/grid_index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export declare const INT_COORD_SCALE: number;
2
2
  export declare const GRID_FINE_RES: number;
3
3
  export declare const GRID_LV0_UNIT: number;
4
+ export declare const MAX_LEVEL: number;
5
+ export declare const CANONICAL_CODE_LEVEL: number;
4
6
  export declare class Gridex {
5
7
  readonly londex: number;
6
8
  readonly latdex: number;
@@ -47,3 +49,67 @@ export declare class GridUnit {
47
49
  maxLatdex: number;
48
50
  };
49
51
  }
52
+ export declare function gridexAtLonLat(level: number, longitude: number, latitude: number): GridexAtLv;
53
+ export declare function gridexStringAtLonLat(level: number, longitude: number, latitude: number): string;
54
+ export declare function lonCodeMagnitudeBits(level: number): number;
55
+ export declare function latCodeMagnitudeBits(level: number): number;
56
+ export declare function encodeAxisCode(index: number, magnitudeBits: number): number;
57
+ export declare function decodeAxisCode(code: number, magnitudeBits: number): number;
58
+ export declare function lonCodeFromLondex(level: number, londex: number): number;
59
+ export declare function latCodeFromLatdex(level: number, latdex: number): number;
60
+ export declare function londexFromLonCode(level: number, lonCode: number): number;
61
+ export declare function latdexFromLatCode(level: number, latCode: number): number;
62
+ export declare function londexPrefixCodeFromLonCode(targetLevel: number, lonCode: number, sourceLevel?: number): number;
63
+ export declare function latdexPrefixCodeFromLatCode(targetLevel: number, latCode: number, sourceLevel?: number): number;
64
+ export declare function lonCodeRangeForLondex(
65
+ level: number,
66
+ londex: number,
67
+ sourceLevel?: number
68
+ ): {
69
+ from: number;
70
+ to: number;
71
+ };
72
+ export declare function latCodeRangeForLatdex(
73
+ level: number,
74
+ latdex: number,
75
+ sourceLevel?: number
76
+ ): {
77
+ from: number;
78
+ to: number;
79
+ };
80
+ export declare function lonLatCodeRangeForGridex(
81
+ level: number,
82
+ londex: number,
83
+ latdex: number,
84
+ sourceLevel?: number
85
+ ): {
86
+ lonCode: {
87
+ from: number;
88
+ to: number;
89
+ };
90
+ latCode: {
91
+ from: number;
92
+ to: number;
93
+ };
94
+ };
95
+ export declare function gridCodeFromLonLatCodes(level: number, lonCode: number, latCode: number): number;
96
+ export declare function lonLatCodesFromGridCode(
97
+ level: number,
98
+ gridCode: number
99
+ ): {
100
+ lonCode: number;
101
+ latCode: number;
102
+ };
103
+ export declare function gridCodeFromGridex(level: number, gridex: Gridex | number, latdex?: number): number;
104
+ export declare function gridexFromGridCode(level: number, gridCode: number): GridexAtLv;
105
+ export declare function gridCodeAtLonLat(level: number, longitude: number, latitude: number): number;
106
+ export declare function gridCodeComponentsAtLonLat(
107
+ level: number,
108
+ longitude: number,
109
+ latitude: number
110
+ ): {
111
+ gridex: GridexAtLv;
112
+ lonCode: number;
113
+ latCode: number;
114
+ gridCode: number;
115
+ };
package/grid_index.mjs CHANGED
@@ -6,11 +6,12 @@ export const GRID_LV0_UNIT = 30 * INT_COORD_SCALE;
6
6
  const GRID_LV0_UNIT_FINE = GRID_LV0_UNIT * GRID_FINE_RES;
7
7
  const GRID_LV0_LONDEX = 6;
8
8
  const GRID_LV0_LATDEX = 3;
9
- const MAX_LEVEL = 16;
9
+ export const MAX_LEVEL = 16;
10
10
  const MAX_LONGITUDE_FINE = 180 * INT_COORD_SCALE * GRID_FINE_RES;
11
11
  const MIN_LONGITUDE_FINE = -MAX_LONGITUDE_FINE;
12
12
  const MAX_LATITUDE_FINE = 90 * INT_COORD_SCALE * GRID_FINE_RES;
13
13
  const MIN_LATITUDE_FINE = -MAX_LATITUDE_FINE;
14
+ export const CANONICAL_CODE_LEVEL = MAX_LEVEL;
14
15
 
15
16
  export class Gridex {
16
17
  constructor(londex, latdex) {
@@ -230,3 +231,208 @@ export class GridUnit {
230
231
  return { minLondex, maxLondex, minLatdex, maxLatdex };
231
232
  }
232
233
  }
234
+
235
+ function wrapLongitudeDegrees(longitude) {
236
+ let value = longitude;
237
+ while (value > 180) value -= 360;
238
+ while (value < -180) value += 360;
239
+ return value;
240
+ }
241
+
242
+ function clampLatitudeDegrees(latitude) {
243
+ return Math.max(-90, Math.min(90, latitude));
244
+ }
245
+
246
+ function normalizeLonLatDegrees(longitude, latitude) {
247
+ return {
248
+ longitudeInt: Math.round(wrapLongitudeDegrees(longitude) * INT_COORD_SCALE),
249
+ latitudeInt: Math.round(clampLatitudeDegrees(latitude) * INT_COORD_SCALE),
250
+ };
251
+ }
252
+
253
+ export function gridexAtLonLat(level, longitude, latitude) {
254
+ const unit = new GridUnit(level);
255
+ const { longitudeInt, latitudeInt } = normalizeLonLatDegrees(longitude, latitude);
256
+ const candidates = unit.gridexesAt(longitudeInt, latitudeInt);
257
+ return candidates.find((candidate) => candidate.londex !== 0 && candidate.latdex !== 0)
258
+ ?? candidates[0]
259
+ ?? unit.makeGridex(0, 0);
260
+ }
261
+
262
+ export function gridexStringAtLonLat(level, longitude, latitude) {
263
+ const gridex = gridexAtLonLat(level, longitude, latitude);
264
+ return `${gridex.londex},${gridex.latdex}`;
265
+ }
266
+
267
+ export function lonCodeMagnitudeBits(level) {
268
+ const unit = new GridUnit(level);
269
+ return Math.ceil(Math.log2(unit.maxLondex));
270
+ }
271
+
272
+ export function latCodeMagnitudeBits(level) {
273
+ const unit = new GridUnit(level);
274
+ return Math.ceil(Math.log2(unit.maxLatdex));
275
+ }
276
+
277
+ export function encodeAxisCode(index, magnitudeBits) {
278
+ if (!Number.isInteger(index) || index === 0) {
279
+ throw new Error(`Grid index must be a non-zero integer: ${index}`);
280
+ }
281
+ const signBit = index > 0 ? 1 : 0;
282
+ const magnitude = Math.abs(index) - 1;
283
+ const maxMagnitude = (1 << magnitudeBits) - 1;
284
+ if (magnitude < 0 || magnitude > maxMagnitude) {
285
+ throw new Error(`Grid index ${index} exceeds ${magnitudeBits} magnitude bits`);
286
+ }
287
+ return (signBit << magnitudeBits) | magnitude;
288
+ }
289
+
290
+ export function decodeAxisCode(code, magnitudeBits) {
291
+ if (!Number.isInteger(code) || code < 0) {
292
+ throw new Error(`Grid code must be a non-negative integer: ${code}`);
293
+ }
294
+ const signBit = code >> magnitudeBits;
295
+ const magnitudeMask = (1 << magnitudeBits) - 1;
296
+ const magnitude = code & magnitudeMask;
297
+ return (magnitude + 1) * (signBit === 1 ? 1 : -1);
298
+ }
299
+
300
+ export function lonCodeFromLondex(level, londex) {
301
+ return encodeAxisCode(londex, lonCodeMagnitudeBits(level));
302
+ }
303
+
304
+ export function latCodeFromLatdex(level, latdex) {
305
+ return encodeAxisCode(latdex, latCodeMagnitudeBits(level));
306
+ }
307
+
308
+ export function londexFromLonCode(level, lonCode) {
309
+ return decodeAxisCode(lonCode, lonCodeMagnitudeBits(level));
310
+ }
311
+
312
+ export function latdexFromLatCode(level, latCode) {
313
+ return decodeAxisCode(latCode, latCodeMagnitudeBits(level));
314
+ }
315
+
316
+ function axisCodePrefixFromCode(targetLevel, axisCode, sourceLevel, magnitudeBits) {
317
+ if (!Number.isInteger(targetLevel) || targetLevel < 0 || targetLevel > sourceLevel) {
318
+ throw new Error(`targetLevel must be an integer between 0 and ${sourceLevel}: ${targetLevel}`);
319
+ }
320
+ const signBase = 2 ** magnitudeBits;
321
+ const signBit = Math.floor(axisCode / signBase);
322
+ const magnitude = axisCode % signBase;
323
+ const shift = sourceLevel - targetLevel;
324
+ const prefixMagnitude = shift === 0 ? magnitude : Math.floor(magnitude / (2 ** shift)) * (2 ** shift);
325
+ return signBit * signBase + prefixMagnitude;
326
+ }
327
+
328
+ export function londexPrefixCodeFromLonCode(targetLevel, lonCode, sourceLevel = CANONICAL_CODE_LEVEL) {
329
+ return axisCodePrefixFromCode(targetLevel, lonCode, sourceLevel, lonCodeMagnitudeBits(sourceLevel));
330
+ }
331
+
332
+ export function latdexPrefixCodeFromLatCode(targetLevel, latCode, sourceLevel = CANONICAL_CODE_LEVEL) {
333
+ return axisCodePrefixFromCode(targetLevel, latCode, sourceLevel, latCodeMagnitudeBits(sourceLevel));
334
+ }
335
+
336
+ function axisCodeRangeForIndex(targetLevel, index, sourceLevel, magnitudeBits) {
337
+ if (!Number.isInteger(sourceLevel) || sourceLevel < 0 || sourceLevel > MAX_LEVEL) {
338
+ throw new Error(`sourceLevel must be an integer between 0 and ${MAX_LEVEL}: ${sourceLevel}`);
339
+ }
340
+ if (!Number.isInteger(targetLevel) || targetLevel < 0 || targetLevel > sourceLevel) {
341
+ throw new Error(`targetLevel must be an integer between 0 and ${sourceLevel}: ${targetLevel}`);
342
+ }
343
+ if (!Number.isInteger(index) || index === 0) {
344
+ throw new Error(`Grid index must be a non-zero integer: ${index}`);
345
+ }
346
+ const signBit = index > 0 ? 1 : 0;
347
+ const shift = sourceLevel - targetLevel;
348
+ const magnitude = Math.abs(index) - 1;
349
+ const fromMagnitude = magnitude << shift;
350
+ const toMagnitude = ((magnitude + 1) << shift) - 1;
351
+ const maxMagnitude = (2 ** magnitudeBits) - 1;
352
+ if (toMagnitude > maxMagnitude) {
353
+ throw new Error(`Grid index ${index} at level ${targetLevel} exceeds source level ${sourceLevel} range`);
354
+ }
355
+ const signBase = 2 ** magnitudeBits;
356
+ return {
357
+ from: signBit * signBase + fromMagnitude,
358
+ to: signBit * signBase + toMagnitude,
359
+ };
360
+ }
361
+
362
+ export function lonCodeRangeForLondex(level, londex, sourceLevel = CANONICAL_CODE_LEVEL) {
363
+ return axisCodeRangeForIndex(level, londex, sourceLevel, lonCodeMagnitudeBits(sourceLevel));
364
+ }
365
+
366
+ export function latCodeRangeForLatdex(level, latdex, sourceLevel = CANONICAL_CODE_LEVEL) {
367
+ return axisCodeRangeForIndex(level, latdex, sourceLevel, latCodeMagnitudeBits(sourceLevel));
368
+ }
369
+
370
+ export function lonLatCodeRangeForGridex(level, londex, latdex, sourceLevel = CANONICAL_CODE_LEVEL) {
371
+ return {
372
+ lonCode: lonCodeRangeForLondex(level, londex, sourceLevel),
373
+ latCode: latCodeRangeForLatdex(level, latdex, sourceLevel),
374
+ };
375
+ }
376
+
377
+ export function gridCodeFromLonLatCodes(level, lonCode, latCode) {
378
+ const latCodeWidth = latCodeMagnitudeBits(level) + 1;
379
+ const latCodeSpan = 2 ** latCodeWidth;
380
+ if (!Number.isInteger(lonCode) || lonCode < 0) {
381
+ throw new Error(`lonCode must be a non-negative integer: ${lonCode}`);
382
+ }
383
+ if (!Number.isInteger(latCode) || latCode < 0 || latCode >= latCodeSpan) {
384
+ throw new Error(`latCode out of range for level ${level}: ${latCode}`);
385
+ }
386
+ return lonCode * latCodeSpan + latCode;
387
+ }
388
+
389
+ export function lonLatCodesFromGridCode(level, gridCode) {
390
+ if (!Number.isInteger(gridCode) || gridCode < 0) {
391
+ throw new Error(`gridCode must be a non-negative integer: ${gridCode}`);
392
+ }
393
+ const latCodeWidth = latCodeMagnitudeBits(level) + 1;
394
+ const latCodeSpan = 2 ** latCodeWidth;
395
+ return {
396
+ lonCode: Math.floor(gridCode / latCodeSpan),
397
+ latCode: gridCode % latCodeSpan,
398
+ };
399
+ }
400
+
401
+ export function gridCodeFromGridex(level, gridex, latdex) {
402
+ const londex = typeof gridex === "object" ? gridex.londex : gridex;
403
+ const resolvedLatdex = typeof gridex === "object" ? gridex.latdex : latdex;
404
+ if (!Number.isInteger(londex) || !Number.isInteger(resolvedLatdex)) {
405
+ throw new Error("gridCodeFromGridex expects londex/latdex integers or a Gridex-like object");
406
+ }
407
+ return gridCodeFromLonLatCodes(
408
+ level,
409
+ lonCodeFromLondex(level, londex),
410
+ latCodeFromLatdex(level, resolvedLatdex),
411
+ );
412
+ }
413
+
414
+ export function gridexFromGridCode(level, gridCode) {
415
+ const { lonCode, latCode } = lonLatCodesFromGridCode(level, gridCode);
416
+ const unit = new GridUnit(level);
417
+ return unit.makeGridex(
418
+ londexFromLonCode(level, lonCode),
419
+ latdexFromLatCode(level, latCode),
420
+ );
421
+ }
422
+
423
+ export function gridCodeAtLonLat(level, longitude, latitude) {
424
+ const gridex = gridexAtLonLat(level, longitude, latitude);
425
+ return gridCodeFromGridex(level, gridex);
426
+ }
427
+
428
+ export function gridCodeComponentsAtLonLat(level, longitude, latitude) {
429
+ const gridex = gridexAtLonLat(level, longitude, latitude);
430
+ const lonCode = lonCodeFromLondex(level, gridex.londex);
431
+ const latCode = latCodeFromLatdex(level, gridex.latdex);
432
+ return {
433
+ gridex,
434
+ lonCode,
435
+ latCode,
436
+ gridCode: gridCodeFromLonLatCodes(level, lonCode, latCode),
437
+ };
438
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truelies/osm-dybuf",
3
- "version": "0.4.2",
3
+ "version": "0.4.5",
4
4
  "description": "Gridex OSM DyBuf parser and schema utilities",
5
5
  "type": "module",
6
6
  "main": "./osm_dybuf.mjs",