@truelies/osm-dybuf 0.4.1 → 0.4.3
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 +40 -5
- package/grid_index.d.ts +39 -0
- package/grid_index.mjs +98 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ Current frontend-facing schema notes:
|
|
|
28
28
|
- Breaking change in `0.4.x`: `GridFeature.featureId` is numeric. Consumers should map it with `ID_TO_FEATURE_KIND[String(featureId)]` or `unpackFeatureId(featureId)` when string/debug output is needed.
|
|
29
29
|
- `road_express` covers motorway corridors plus `trunk` ways tagged with `motorroad=yes` (for example many Taiwan expressway mainlines).
|
|
30
30
|
- `water_wetland` separates marsh / bog / fen / wetland polygons from deeper open water.
|
|
31
|
-
-
|
|
31
|
+
- Geometry payload exports currently stop at `lv13`, but Gridex/index helpers remain valid through `lv16` for higher-detail indexing metadata.
|
|
32
32
|
|
|
33
33
|
## Usage
|
|
34
34
|
|
|
@@ -88,7 +88,18 @@ 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 {
|
|
91
|
+
import {
|
|
92
|
+
GridUnit,
|
|
93
|
+
Gridex,
|
|
94
|
+
INT_COORD_SCALE,
|
|
95
|
+
GRID16_LEVEL,
|
|
96
|
+
GRID16_LON_MAG_BITS,
|
|
97
|
+
GRID16_LAT_MAG_BITS,
|
|
98
|
+
grid16CodesFromLonLat,
|
|
99
|
+
buildGrid16LonLatRangeForLevelCell,
|
|
100
|
+
encodeSignedGridIndex,
|
|
101
|
+
decodeSignedGridIndex,
|
|
102
|
+
} from "@truelies/osm-dybuf/grid_index";
|
|
92
103
|
|
|
93
104
|
const unit = new GridUnit(8); // level 8
|
|
94
105
|
const cell = new Gridex(213, 161);
|
|
@@ -97,14 +108,35 @@ const [minLon, minLat, maxLon, maxLat] = unit.boundsOfGrid(cell);
|
|
|
97
108
|
// parent/child conversion (same rules as Python GridUnit)
|
|
98
109
|
const children = unit.toLowerLevelGridexes(cell); // level 9, 4 cells
|
|
99
110
|
const parent = new GridUnit(9).toUpperLevelGridex(children[0]); // back to level 8
|
|
111
|
+
|
|
112
|
+
// canonical lv16 codes for Firestore / Redis
|
|
113
|
+
const codes = grid16CodesFromLonLat(121.56, 25.03);
|
|
114
|
+
console.log(codes.grid16LonCode, codes.grid16LatCode);
|
|
115
|
+
|
|
116
|
+
// query a lv15 cell by expanding it into a lv16 numeric range
|
|
117
|
+
const range = buildGrid16LonLatRangeForLevelCell(15, -3, 4);
|
|
118
|
+
console.log(range.lon.from, range.lon.to, range.lat.from, range.lat.to);
|
|
119
|
+
|
|
120
|
+
// signed code roundtrip
|
|
121
|
+
const lonCode = encodeSignedGridIndex(-123, GRID16_LON_MAG_BITS);
|
|
122
|
+
console.log(decodeSignedGridIndex(lonCode, GRID16_LON_MAG_BITS)); // -123
|
|
100
123
|
```
|
|
101
124
|
|
|
102
125
|
- `toLowerLevelGridexes(gridex)`:
|
|
103
126
|
- returns 4 child cells at `level + 1`
|
|
104
|
-
- throws when current unit is already max level (`
|
|
127
|
+
- throws when current unit is already max level (`16`)
|
|
105
128
|
- `toUpperLevelGridex(gridex)`:
|
|
106
129
|
- returns the parent cell at `level - 1`
|
|
107
130
|
- throws when current unit is level `0`
|
|
131
|
+
- `grid16CodesFromLonLat(lon, lat)`:
|
|
132
|
+
- converts GPS directly to canonical `lv16` codes
|
|
133
|
+
- returns `{ grid16LonCode, grid16LatCode, londex16, latdex16 }`
|
|
134
|
+
- `buildGrid16LonLatRangeForLevelCell(level, londex, latdex)`:
|
|
135
|
+
- converts any `lv <= 16` cell into the corresponding `lv16` prefix range
|
|
136
|
+
- intended for Firestore numeric range query / Redis derived bucket lookup
|
|
137
|
+
- `encodeSignedGridIndex(index, magnitudeBits)` / `decodeSignedGridIndex(code, magnitudeBits)`:
|
|
138
|
+
- encodes grid index as `sign bit + (abs(index) - 1)`
|
|
139
|
+
- for `lv16`, use `GRID16_LON_MAG_BITS = 19` and `GRID16_LAT_MAG_BITS = 18`
|
|
108
140
|
|
|
109
141
|
## Local Development
|
|
110
142
|
|
|
@@ -117,7 +149,10 @@ npm test # runs unit tests (node:test)
|
|
|
117
149
|
Unit test example (`tests/grid_index.test.mjs`) covers:
|
|
118
150
|
- `gridexesAt` near axis (no zero index cell)
|
|
119
151
|
- `toLowerLevelGridexes` and `toUpperLevelGridex` roundtrip
|
|
120
|
-
- level boundary errors (`lv0` has no upper level, `
|
|
152
|
+
- level boundary errors (`lv0` has no upper level, `lv16` has no lower level)
|
|
153
|
+
- canonical signed grid index encode/decode roundtrip
|
|
154
|
+
- GPS -> `lv16` code conversion
|
|
155
|
+
- `lv15` cell -> `lv16` range expansion correctness
|
|
121
156
|
|
|
122
157
|
- `npm pack`: build a tarball so other repos can install via `npm install ./path/to/osm-dybuf-*.tgz`
|
|
123
158
|
- `npm link`: link the package locally (`npm link` here, then `npm link @truelies/osm-dybuf` in the consumer repo)
|
|
@@ -139,4 +174,4 @@ Unit test example (`tests/grid_index.test.mjs`) covers:
|
|
|
139
174
|
- `region_numeric_codes.json`: latest region ID table (generated via `scripts/dump_region_codes.py`)
|
|
140
175
|
- `inspect_dybuf.mjs`: CLI tool that uses the package locally
|
|
141
176
|
|
|
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.
|
|
177
|
+
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 new `lv16` canonical grid code helpers for Firestore / Redis indexing.
|
package/grid_index.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
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 GRID16_LEVEL: number;
|
|
6
|
+
export declare const GRID16_LON_MAG_BITS: number;
|
|
7
|
+
export declare const GRID16_LAT_MAG_BITS: number;
|
|
4
8
|
export declare class Gridex {
|
|
5
9
|
readonly londex: number;
|
|
6
10
|
readonly latdex: number;
|
|
@@ -47,3 +51,38 @@ export declare class GridUnit {
|
|
|
47
51
|
maxLatdex: number;
|
|
48
52
|
};
|
|
49
53
|
}
|
|
54
|
+
export declare function gridexAtLonLat(level: number, longitude: number, latitude: number): GridexAtLv;
|
|
55
|
+
export declare function gridexStringAtLonLat(level: number, longitude: number, latitude: number): string;
|
|
56
|
+
export declare function encodeSignedGridIndex(index: number, magnitudeBits: number): number;
|
|
57
|
+
export declare function decodeSignedGridIndex(code: number, magnitudeBits: number): number;
|
|
58
|
+
export declare function grid16CodesFromLonLat(
|
|
59
|
+
longitude: number,
|
|
60
|
+
latitude: number
|
|
61
|
+
): {
|
|
62
|
+
grid16LonCode: number;
|
|
63
|
+
grid16LatCode: number;
|
|
64
|
+
londex16: number;
|
|
65
|
+
latdex16: number;
|
|
66
|
+
};
|
|
67
|
+
export declare function buildGrid16RangeForLevelIndex(
|
|
68
|
+
level: number,
|
|
69
|
+
index: number,
|
|
70
|
+
magnitudeBits: number
|
|
71
|
+
): {
|
|
72
|
+
from: number;
|
|
73
|
+
to: number;
|
|
74
|
+
};
|
|
75
|
+
export declare function buildGrid16LonLatRangeForLevelCell(
|
|
76
|
+
level: number,
|
|
77
|
+
londex: number,
|
|
78
|
+
latdex: number
|
|
79
|
+
): {
|
|
80
|
+
lon: {
|
|
81
|
+
from: number;
|
|
82
|
+
to: number;
|
|
83
|
+
};
|
|
84
|
+
lat: {
|
|
85
|
+
from: number;
|
|
86
|
+
to: number;
|
|
87
|
+
};
|
|
88
|
+
};
|
package/grid_index.mjs
CHANGED
|
@@ -6,11 +6,14 @@ 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 =
|
|
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 GRID16_LEVEL = 16;
|
|
15
|
+
export const GRID16_LON_MAG_BITS = 19;
|
|
16
|
+
export const GRID16_LAT_MAG_BITS = 18;
|
|
14
17
|
|
|
15
18
|
export class Gridex {
|
|
16
19
|
constructor(londex, latdex) {
|
|
@@ -230,3 +233,97 @@ export class GridUnit {
|
|
|
230
233
|
return { minLondex, maxLondex, minLatdex, maxLatdex };
|
|
231
234
|
}
|
|
232
235
|
}
|
|
236
|
+
|
|
237
|
+
function wrapLongitudeDegrees(longitude) {
|
|
238
|
+
let value = longitude;
|
|
239
|
+
while (value > 180) value -= 360;
|
|
240
|
+
while (value < -180) value += 360;
|
|
241
|
+
return value;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function clampLatitudeDegrees(latitude) {
|
|
245
|
+
return Math.max(-90, Math.min(90, latitude));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function normalizeLonLatDegrees(longitude, latitude) {
|
|
249
|
+
return {
|
|
250
|
+
longitudeInt: Math.round(wrapLongitudeDegrees(longitude) * INT_COORD_SCALE),
|
|
251
|
+
latitudeInt: Math.round(clampLatitudeDegrees(latitude) * INT_COORD_SCALE),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function gridexAtLonLat(level, longitude, latitude) {
|
|
256
|
+
const unit = new GridUnit(level);
|
|
257
|
+
const { longitudeInt, latitudeInt } = normalizeLonLatDegrees(longitude, latitude);
|
|
258
|
+
const candidates = unit.gridexesAt(longitudeInt, latitudeInt);
|
|
259
|
+
return candidates.find((candidate) => candidate.londex !== 0 && candidate.latdex !== 0)
|
|
260
|
+
?? candidates[0]
|
|
261
|
+
?? unit.makeGridex(0, 0);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function gridexStringAtLonLat(level, longitude, latitude) {
|
|
265
|
+
const gridex = gridexAtLonLat(level, longitude, latitude);
|
|
266
|
+
return `${gridex.londex},${gridex.latdex}`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function encodeSignedGridIndex(index, magnitudeBits) {
|
|
270
|
+
if (!Number.isInteger(index) || index === 0) {
|
|
271
|
+
throw new Error(`Grid index must be a non-zero integer: ${index}`);
|
|
272
|
+
}
|
|
273
|
+
const signBit = index > 0 ? 1 : 0;
|
|
274
|
+
const magnitude = Math.abs(index) - 1;
|
|
275
|
+
const maxMagnitude = (1 << magnitudeBits) - 1;
|
|
276
|
+
if (magnitude < 0 || magnitude > maxMagnitude) {
|
|
277
|
+
throw new Error(`Grid index ${index} exceeds ${magnitudeBits} magnitude bits`);
|
|
278
|
+
}
|
|
279
|
+
return (signBit << magnitudeBits) | magnitude;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function decodeSignedGridIndex(code, magnitudeBits) {
|
|
283
|
+
if (!Number.isInteger(code) || code < 0) {
|
|
284
|
+
throw new Error(`Grid code must be a non-negative integer: ${code}`);
|
|
285
|
+
}
|
|
286
|
+
const signBit = code >> magnitudeBits;
|
|
287
|
+
const magnitudeMask = (1 << magnitudeBits) - 1;
|
|
288
|
+
const magnitude = code & magnitudeMask;
|
|
289
|
+
return (magnitude + 1) * (signBit === 1 ? 1 : -1);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function grid16CodesFromLonLat(longitude, latitude) {
|
|
293
|
+
const gridex16 = gridexAtLonLat(GRID16_LEVEL, longitude, latitude);
|
|
294
|
+
return {
|
|
295
|
+
grid16LonCode: encodeSignedGridIndex(gridex16.londex, GRID16_LON_MAG_BITS),
|
|
296
|
+
grid16LatCode: encodeSignedGridIndex(gridex16.latdex, GRID16_LAT_MAG_BITS),
|
|
297
|
+
londex16: gridex16.londex,
|
|
298
|
+
latdex16: gridex16.latdex,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
export function buildGrid16RangeForLevelIndex(level, index, magnitudeBits) {
|
|
303
|
+
if (!Number.isInteger(level) || level < 0 || level > GRID16_LEVEL) {
|
|
304
|
+
throw new Error(`Level must be an integer between 0 and ${GRID16_LEVEL}: ${level}`);
|
|
305
|
+
}
|
|
306
|
+
if (!Number.isInteger(index) || index === 0) {
|
|
307
|
+
throw new Error(`Grid index must be a non-zero integer: ${index}`);
|
|
308
|
+
}
|
|
309
|
+
const signBit = index > 0 ? 1 : 0;
|
|
310
|
+
const shift = GRID16_LEVEL - level;
|
|
311
|
+
const magnitude = Math.abs(index) - 1;
|
|
312
|
+
const fromMagnitude = magnitude << shift;
|
|
313
|
+
const toMagnitude = ((magnitude + 1) << shift) - 1;
|
|
314
|
+
const maxMagnitude = (1 << magnitudeBits) - 1;
|
|
315
|
+
if (toMagnitude > maxMagnitude) {
|
|
316
|
+
throw new Error(`Grid index ${index} at level ${level} exceeds lv16 range`);
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
from: (signBit << magnitudeBits) | fromMagnitude,
|
|
320
|
+
to: (signBit << magnitudeBits) | toMagnitude,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function buildGrid16LonLatRangeForLevelCell(level, londex, latdex) {
|
|
325
|
+
return {
|
|
326
|
+
lon: buildGrid16RangeForLevelIndex(level, londex, GRID16_LON_MAG_BITS),
|
|
327
|
+
lat: buildGrid16RangeForLevelIndex(level, latdex, GRID16_LAT_MAG_BITS),
|
|
328
|
+
};
|
|
329
|
+
}
|