@osmix/core 0.1.10 → 0.3.0
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/CHANGELOG.md +56 -0
- package/LICENSE +21 -0
- package/README.md +136 -72
- package/dist/contracts.d.ts +27 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +1 -0
- package/dist/contracts.js.map +1 -0
- package/dist/entities.d.ts +22 -2
- package/dist/entities.d.ts.map +1 -1
- package/dist/entities.js +54 -10
- package/dist/entities.js.map +1 -1
- package/dist/ids.d.ts +20 -5
- package/dist/ids.d.ts.map +1 -1
- package/dist/ids.js +87 -29
- package/dist/ids.js.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -4
- package/dist/index.js.map +1 -1
- package/dist/indirect-kd-index.d.ts +28 -0
- package/dist/indirect-kd-index.d.ts.map +1 -0
- package/dist/indirect-kd-index.js +163 -0
- package/dist/indirect-kd-index.js.map +1 -0
- package/dist/limits.d.ts +30 -0
- package/dist/limits.d.ts.map +1 -0
- package/dist/limits.js +51 -0
- package/dist/limits.js.map +1 -0
- package/dist/mocks.d.ts.map +1 -1
- package/dist/mocks.js.map +1 -1
- package/dist/nodes.d.ts +36 -15
- package/dist/nodes.d.ts.map +1 -1
- package/dist/nodes.js +185 -62
- package/dist/nodes.js.map +1 -1
- package/dist/osm.d.ts +60 -4
- package/dist/osm.d.ts.map +1 -1
- package/dist/osm.js +32 -6
- package/dist/osm.js.map +1 -1
- package/dist/relations.d.ts +11 -4
- package/dist/relations.d.ts.map +1 -1
- package/dist/relations.js +73 -29
- package/dist/relations.js.map +1 -1
- package/dist/renumber.d.ts +23 -0
- package/dist/renumber.d.ts.map +1 -0
- package/dist/renumber.js +64 -0
- package/dist/renumber.js.map +1 -0
- package/dist/stringtable.d.ts +2 -1
- package/dist/stringtable.d.ts.map +1 -1
- package/dist/stringtable.js +9 -14
- package/dist/stringtable.js.map +1 -1
- package/dist/tags.d.ts +45 -12
- package/dist/tags.d.ts.map +1 -1
- package/dist/tags.js +153 -27
- package/dist/tags.js.map +1 -1
- package/dist/typed-arrays.d.ts +63 -48
- package/dist/typed-arrays.d.ts.map +1 -1
- package/dist/typed-arrays.js +184 -107
- package/dist/typed-arrays.js.map +1 -1
- package/dist/ways.d.ts +52 -3
- package/dist/ways.d.ts.map +1 -1
- package/dist/ways.js +223 -46
- package/dist/ways.js.map +1 -1
- package/package.json +45 -31
- package/src/contracts.ts +29 -0
- package/src/entities.ts +242 -194
- package/src/ids.ts +278 -235
- package/src/index.ts +22 -13
- package/src/indirect-kd-index.ts +212 -0
- package/src/limits.ts +73 -0
- package/src/mocks.ts +114 -113
- package/src/nodes.ts +538 -397
- package/src/osm.ts +282 -207
- package/src/relations.ts +580 -558
- package/src/renumber.ts +80 -0
- package/src/stringtable.ts +189 -184
- package/src/tags.ts +406 -272
- package/src/typed-arrays.ts +307 -205
- package/src/types/geoflatbush.d.ts +28 -33
- package/src/ways.ts +577 -406
- package/test/entity-index-errors.test.ts +41 -0
- package/test/ids.bench.ts +33 -0
- package/test/ids.test.ts +122 -0
- package/test/limits.test.ts +83 -0
- package/test/memory.test.ts +29 -30
- package/test/no-shared-array-buffer.test.ts +41 -0
- package/test/nodes-spatial-index.test.ts +261 -0
- package/test/osm-bbox.test.ts +30 -0
- package/test/relations.bench.ts +32 -0
- package/test/relations.test.ts +124 -0
- package/test/renumber.test.ts +54 -0
- package/test/spatial-index-rebuild.test.ts +33 -0
- package/test/storage-compaction.test.ts +269 -0
- package/test/tags-search.test.ts +89 -56
- package/test/typed-arrays.test.ts +252 -132
- package/tsconfig.build.json +6 -6
- package/tsconfig.json +7 -7
- package/src/types/geokdbush.d.ts +0 -37
- package/src/types/kdbush.d.ts +0 -47
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, test } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { Osm } from "../src/osm";
|
|
4
|
+
|
|
5
|
+
function createManyMemberRelations() {
|
|
6
|
+
const osm = new Osm({ id: "relations-benchmark" });
|
|
7
|
+
osm.nodes.addNode({ id: 1, lon: 0, lat: 0 });
|
|
8
|
+
for (let id = 0; id < 10_000; id++) osm.ways.addWay({ id, refs: [1] });
|
|
9
|
+
for (let relationId = 0; relationId < 100; relationId++) {
|
|
10
|
+
osm.relations.addRelation({
|
|
11
|
+
id: relationId,
|
|
12
|
+
members: Array.from({ length: 100 }, (_, index) => ({
|
|
13
|
+
type: "way" as const,
|
|
14
|
+
ref: relationId * 100 + index,
|
|
15
|
+
role: "",
|
|
16
|
+
})),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
osm.buildIndexes();
|
|
20
|
+
return osm;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const osm = createManyMemberRelations();
|
|
24
|
+
osm.relations.isWayMember(0);
|
|
25
|
+
|
|
26
|
+
describe("relation way membership cache", () => {
|
|
27
|
+
test("1000 repeated cached lookups", async ({ bench }) => {
|
|
28
|
+
await bench("1000 repeated cached lookups", () => {
|
|
29
|
+
for (let i = 0; i < 1000; i++) osm.relations.isWayMember(i);
|
|
30
|
+
}).run();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { Osm, type Osm as OsmType } from "../src/osm";
|
|
4
|
+
|
|
5
|
+
function createOsmWithRelations(): OsmType {
|
|
6
|
+
const osm = new Osm({ id: "relations" });
|
|
7
|
+
osm.nodes.addNode({ id: 1, lon: -120, lat: 46 });
|
|
8
|
+
osm.nodes.addNode({ id: 2, lon: -120.01, lat: 46.01 });
|
|
9
|
+
osm.nodes.addNode({ id: 3, lon: -120.02, lat: 46.02 });
|
|
10
|
+
osm.ways.addWay({ id: 10, refs: [1, 2], tags: { highway: "primary" } });
|
|
11
|
+
osm.ways.addWay({ id: 20, refs: [2, 3], tags: { highway: "secondary" } });
|
|
12
|
+
osm.relations.addRelation({
|
|
13
|
+
id: 100,
|
|
14
|
+
members: [
|
|
15
|
+
{ type: "way", ref: 10, role: "outer" },
|
|
16
|
+
{ type: "node", ref: 1, role: "label" },
|
|
17
|
+
{ type: "way", ref: 10, role: "outer" },
|
|
18
|
+
],
|
|
19
|
+
});
|
|
20
|
+
osm.relations.addRelation({
|
|
21
|
+
id: 200,
|
|
22
|
+
members: [
|
|
23
|
+
{ type: "way", ref: 20, role: "outer" },
|
|
24
|
+
{ type: "relation", ref: 100, role: "child" },
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
osm.buildIndexes();
|
|
28
|
+
return osm;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Way membership for every way in `osm`, as sorted way IDs. */
|
|
32
|
+
function wayMemberIds(osm: OsmType): number[] {
|
|
33
|
+
const ids: number[] = [];
|
|
34
|
+
for (let i = 0; i < osm.ways.size; i++) {
|
|
35
|
+
if (osm.relations.isWayMember(i)) ids.push(osm.ways.ids.at(i));
|
|
36
|
+
}
|
|
37
|
+
return ids.sort((a, b) => a - b);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Osm with ways 10, 20 and 30 and no relations. */
|
|
41
|
+
function createOsmWithWays(): OsmType {
|
|
42
|
+
const osm = new Osm();
|
|
43
|
+
osm.nodes.addNode({ id: 1, lon: 0, lat: 0 });
|
|
44
|
+
for (const id of [10, 20, 30]) osm.ways.addWay({ id, refs: [1] });
|
|
45
|
+
return osm;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe("relation way membership cache", () => {
|
|
49
|
+
it("reports no members when there are no relations", () => {
|
|
50
|
+
const osm = createOsmWithWays();
|
|
51
|
+
osm.buildIndexes();
|
|
52
|
+
|
|
53
|
+
expect(wayMemberIds(osm)).toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("collects unique direct and nested way members", () => {
|
|
57
|
+
expect(wayMemberIds(createOsmWithRelations())).toEqual([10, 20]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("does not traverse relations again while the cache is warm", () => {
|
|
61
|
+
const osm = createOsmWithRelations();
|
|
62
|
+
const getByIndex = vi.spyOn(osm.relations, "getByIndex");
|
|
63
|
+
|
|
64
|
+
osm.relations.isWayMember(0);
|
|
65
|
+
const callsAfterFirstLookup = getByIndex.mock.calls.length;
|
|
66
|
+
osm.relations.isWayMember(1);
|
|
67
|
+
|
|
68
|
+
expect(callsAfterFirstLookup).toBeGreaterThan(0);
|
|
69
|
+
expect(getByIndex).toHaveBeenCalledTimes(callsAfterFirstLookup);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Relations cannot be added after their ID index is built, so these tests only index nodes and
|
|
73
|
+
// ways. Membership resolution without nested relations never consults the relation ID index.
|
|
74
|
+
it("invalidates the cache when a relation is added", () => {
|
|
75
|
+
const osm = createOsmWithWays();
|
|
76
|
+
osm.nodes.buildIndex();
|
|
77
|
+
osm.ways.buildIndex();
|
|
78
|
+
osm.relations.addRelation({ id: 1, members: [{ type: "way", ref: 10, role: "" }] });
|
|
79
|
+
expect(wayMemberIds(osm)).toEqual([10]);
|
|
80
|
+
|
|
81
|
+
osm.relations.addRelation({ id: 2, members: [{ type: "way", ref: 20, role: "" }] });
|
|
82
|
+
|
|
83
|
+
expect(wayMemberIds(osm)).toEqual([10, 20]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("invalidates the cache when relations are added in bulk", () => {
|
|
87
|
+
const osm = createOsmWithWays();
|
|
88
|
+
osm.stringTable.add("");
|
|
89
|
+
osm.nodes.buildIndex();
|
|
90
|
+
osm.ways.buildIndex();
|
|
91
|
+
osm.relations.addRelation({ id: 1, members: [{ type: "way", ref: 10, role: "" }] });
|
|
92
|
+
expect(wayMemberIds(osm)).toEqual([10]);
|
|
93
|
+
|
|
94
|
+
osm.relations.addRelations(
|
|
95
|
+
[{ id: 2, keys: [], vals: [], memids: [20], roles_sid: [0], types: [1] }],
|
|
96
|
+
new Uint32Array([0]),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
expect(wayMemberIds(osm)).toEqual([10, 20]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("ignores way members that are not in the dataset", () => {
|
|
103
|
+
const osm = createOsmWithWays();
|
|
104
|
+
osm.relations.addRelation({
|
|
105
|
+
id: 1,
|
|
106
|
+
members: [
|
|
107
|
+
{ type: "way", ref: 30, role: "" },
|
|
108
|
+
{ type: "way", ref: 99, role: "" },
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
osm.buildIndexes();
|
|
112
|
+
|
|
113
|
+
expect(wayMemberIds(osm)).toEqual([30]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("starts with an empty cache after transfer and reconstruction", () => {
|
|
117
|
+
const source = createOsmWithRelations();
|
|
118
|
+
const reconstructed = new Osm(source);
|
|
119
|
+
const getByIndex = vi.spyOn(reconstructed.relations, "getByIndex");
|
|
120
|
+
|
|
121
|
+
expect(wayMemberIds(reconstructed)).toEqual([10, 20]);
|
|
122
|
+
expect(getByIndex.mock.calls.length).toBeGreaterThan(0);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { Osm } from "../src/osm.ts";
|
|
4
|
+
import { renumberNegativeIds } from "../src/renumber.ts";
|
|
5
|
+
|
|
6
|
+
function mixedIds() {
|
|
7
|
+
const osm = new Osm({ id: "mixed" });
|
|
8
|
+
osm.nodes.addNode({ id: 5, lon: 0, lat: 0 });
|
|
9
|
+
osm.nodes.addNode({ id: -1, lon: 0.001, lat: 0, tags: { name: "New entrance" } });
|
|
10
|
+
osm.nodes.addNode({ id: -2, lon: 0.002, lat: 0 });
|
|
11
|
+
osm.ways.addWay({ id: 7, refs: [5, -1], tags: { highway: "footway" } });
|
|
12
|
+
osm.ways.addWay({ id: -3, refs: [-1, -2], tags: { highway: "footway" } });
|
|
13
|
+
osm.relations.addRelation({
|
|
14
|
+
id: -1,
|
|
15
|
+
members: [
|
|
16
|
+
{ type: "node", ref: -2, role: "stop" },
|
|
17
|
+
{ type: "way", ref: -3, role: "" },
|
|
18
|
+
{ type: "way", ref: 7, role: "" },
|
|
19
|
+
],
|
|
20
|
+
tags: { type: "route" },
|
|
21
|
+
});
|
|
22
|
+
osm.buildIndexes();
|
|
23
|
+
osm.buildSpatialIndexes();
|
|
24
|
+
return osm;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("renumberNegativeIds", () => {
|
|
28
|
+
it("gives each type's negative IDs new ones after its highest ID and follows every reference", () => {
|
|
29
|
+
const input = mixedIds();
|
|
30
|
+
const { osm, idMap } = renumberNegativeIds(input);
|
|
31
|
+
expect(idMap).toEqual({
|
|
32
|
+
nodes: { "-1": 6, "-2": 7 },
|
|
33
|
+
ways: { "-3": 8 },
|
|
34
|
+
relations: { "-1": 1 },
|
|
35
|
+
});
|
|
36
|
+
expect(osm.nodes.getById(6)?.tags).toEqual({ name: "New entrance" });
|
|
37
|
+
expect(osm.ways.getById(7)?.refs).toEqual([5, 6]);
|
|
38
|
+
expect(osm.ways.getById(8)?.refs).toEqual([6, 7]);
|
|
39
|
+
expect(osm.relations.getById(1)?.members.map((member) => member.ref)).toEqual([7, 8, 7]);
|
|
40
|
+
expect([...osm.nodes].every((node) => node.id > 0)).toBe(true);
|
|
41
|
+
// The input keeps its IDs.
|
|
42
|
+
expect(input.nodes.getById(-1)).not.toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("is deterministic and a no-op without negative IDs", () => {
|
|
46
|
+
expect(renumberNegativeIds(mixedIds()).idMap).toEqual(renumberNegativeIds(mixedIds()).idMap);
|
|
47
|
+
const positive = new Osm({ id: "positive" });
|
|
48
|
+
positive.nodes.addNode({ id: 1, lon: 0, lat: 0 });
|
|
49
|
+
positive.buildIndexes();
|
|
50
|
+
const { osm, idMap } = renumberNegativeIds(positive);
|
|
51
|
+
expect(idMap).toEqual({ nodes: {}, ways: {}, relations: {} });
|
|
52
|
+
expect([...osm.nodes]).toEqual([...positive.nodes]);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { Osm } from "../src/osm.ts";
|
|
4
|
+
|
|
5
|
+
function buildIndexedOsm() {
|
|
6
|
+
const osm = new Osm();
|
|
7
|
+
osm.nodes.addNode({ id: 1, lon: 0, lat: 0 });
|
|
8
|
+
osm.nodes.addNode({ id: 2, lon: 1, lat: 1 });
|
|
9
|
+
osm.ways.addWay({ id: 10, refs: [1, 2], tags: { highway: "path" } });
|
|
10
|
+
osm.relations.addRelation({
|
|
11
|
+
id: 100,
|
|
12
|
+
members: [{ type: "way", ref: 10, role: "" }],
|
|
13
|
+
tags: { type: "route" },
|
|
14
|
+
});
|
|
15
|
+
osm.buildIndexes();
|
|
16
|
+
osm.buildSpatialIndexes();
|
|
17
|
+
return osm;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("spatial index rebuilds", () => {
|
|
21
|
+
it("reuses built way and relation indexes", () => {
|
|
22
|
+
const osm = buildIndexedOsm();
|
|
23
|
+
const waysIndex = osm.ways.buildSpatialIndex();
|
|
24
|
+
const relationsIndex = osm.relations.buildSpatialIndex();
|
|
25
|
+
|
|
26
|
+
osm.buildSpatialIndexes();
|
|
27
|
+
|
|
28
|
+
expect(osm.ways.buildSpatialIndex()).toBe(waysIndex);
|
|
29
|
+
expect(osm.relations.buildSpatialIndex()).toBe(relationsIndex);
|
|
30
|
+
expect(osm.ways.intersects([-1, -1, 2, 2])).toEqual([0]);
|
|
31
|
+
expect(osm.relations.intersects([-1, -1, 2, 2])).toEqual([0]);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { inspectBackingBuffers } from "@osmix/shared/backing-buffers";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { OSM_CONTENT_HASH_VERSION, OSM_TRANSFER_VERSION, Osm } from "../src/osm.ts";
|
|
5
|
+
import StringTable from "../src/stringtable.ts";
|
|
6
|
+
import { Tags } from "../src/tags.ts";
|
|
7
|
+
|
|
8
|
+
describe("sparse tag storage", () => {
|
|
9
|
+
it("rejects duplicate and out-of-order tagged entity indexes", () => {
|
|
10
|
+
const tags = new Tags(new StringTable());
|
|
11
|
+
tags.addTagKeysAndValues(2, [0], [1]);
|
|
12
|
+
|
|
13
|
+
expect(() => tags.addTagKeysAndValues(2, [0], [1])).toThrow(/added out of order/);
|
|
14
|
+
expect(() => tags.addTagKeysAndValues(1, [0], [1])).toThrow(/added out of order/);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("preserves tags across rank boundaries and supports more than 255 tags", () => {
|
|
18
|
+
const osm = new Osm({ id: "sparse-tags" });
|
|
19
|
+
const manyTags = Object.fromEntries(
|
|
20
|
+
Array.from({ length: 300 }, (_, index) => [`key-${index}`, `value-${index}`]),
|
|
21
|
+
);
|
|
22
|
+
const taggedIndexes = new Set([0, 255, 256, 511, 512]);
|
|
23
|
+
|
|
24
|
+
for (let index = 0; index <= 512; index++) {
|
|
25
|
+
osm.nodes.addNode({
|
|
26
|
+
id: index + 1,
|
|
27
|
+
lon: index / 10,
|
|
28
|
+
lat: index / 20,
|
|
29
|
+
tags: index === 0 ? manyTags : taggedIndexes.has(index) ? { boundary: `${index}` } : {},
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
osm.buildIndexes();
|
|
33
|
+
|
|
34
|
+
expect(osm.nodes.tags.cardinality(0)).toBe(300);
|
|
35
|
+
expect(osm.nodes.tags.cardinality(1)).toBe(0);
|
|
36
|
+
expect(osm.nodes.tags.cardinality(255)).toBe(1);
|
|
37
|
+
expect(osm.nodes.tags.cardinality(256)).toBe(1);
|
|
38
|
+
expect(osm.nodes.tags.cardinality(511)).toBe(1);
|
|
39
|
+
expect(osm.nodes.tags.cardinality(512)).toBe(1);
|
|
40
|
+
expect([...osm.nodes.tags.taggedEntityIndexes()]).toEqual([0, 255, 256, 511, 512]);
|
|
41
|
+
expect(osm.nodes.tags.taggedEntityCount).toBe(5);
|
|
42
|
+
expect(Object.keys(osm.nodes.getByIndex(0).tags ?? {})).toHaveLength(300);
|
|
43
|
+
|
|
44
|
+
const transferables = osm.transferables();
|
|
45
|
+
expect(transferables.transferVersion).toBe(OSM_TRANSFER_VERSION);
|
|
46
|
+
expect(transferables.contentHashVersion).toBe(OSM_CONTENT_HASH_VERSION);
|
|
47
|
+
expect(transferables.nodes.taggedEntityBits.byteLength).toBe(
|
|
48
|
+
Math.ceil(513 / 32) * Uint32Array.BYTES_PER_ELEMENT,
|
|
49
|
+
);
|
|
50
|
+
expect(transferables.nodes.tagRankCheckpoints.byteLength).toBe(
|
|
51
|
+
(Math.ceil(513 / 256) + 1) * Uint32Array.BYTES_PER_ELEMENT,
|
|
52
|
+
);
|
|
53
|
+
expect(transferables.nodes.tagOffsets.byteLength).toBe(
|
|
54
|
+
(taggedIndexes.size + 1) * Uint32Array.BYTES_PER_ELEMENT,
|
|
55
|
+
);
|
|
56
|
+
expect(osm.info().spatialIndexes).toEqual({
|
|
57
|
+
nodes: { all: false, tagged: false },
|
|
58
|
+
ways: false,
|
|
59
|
+
relations: false,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const reconstructed = new Osm(transferables);
|
|
63
|
+
expect([...reconstructed.nodes.tags.taggedEntityIndexes()]).toEqual([0, 255, 256, 511, 512]);
|
|
64
|
+
expect(reconstructed.nodes.getByIndex(512).tags).toEqual({ boundary: "512" });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("stores and searches an all-tagged collection across rank boundaries", () => {
|
|
68
|
+
const osm = new Osm({ id: "all-tagged" });
|
|
69
|
+
const entityCount = 257;
|
|
70
|
+
|
|
71
|
+
for (let index = 0; index < entityCount; index++) {
|
|
72
|
+
osm.nodes.addNode({
|
|
73
|
+
id: index + 1,
|
|
74
|
+
lon: index / 10,
|
|
75
|
+
lat: index / 20,
|
|
76
|
+
tags: { shared: index % 2 === 0 ? "even" : "odd", position: `${index}` },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
osm.buildIndexes();
|
|
80
|
+
|
|
81
|
+
expect(osm.nodes.tags.taggedEntityCount).toBe(entityCount);
|
|
82
|
+
expect([...osm.nodes.tags.taggedEntityIndexes()]).toEqual(
|
|
83
|
+
Array.from({ length: entityCount }, (_, index) => index),
|
|
84
|
+
);
|
|
85
|
+
for (const index of [0, 31, 32, 255, 256]) {
|
|
86
|
+
expect(osm.nodes.tags.cardinality(index)).toBe(2);
|
|
87
|
+
expect(osm.nodes.getByIndex(index).tags).toEqual({
|
|
88
|
+
shared: index % 2 === 0 ? "even" : "odd",
|
|
89
|
+
position: `${index}`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
expect([...osm.nodes].map(({ id }) => id)).toEqual(
|
|
93
|
+
Array.from({ length: entityCount }, (_, index) => index + 1),
|
|
94
|
+
);
|
|
95
|
+
expect(osm.nodes.search("shared")).toHaveLength(entityCount);
|
|
96
|
+
expect(osm.nodes.search("shared", "even").map(({ id }) => id)).toEqual(
|
|
97
|
+
Array.from({ length: 129 }, (_, index) => index * 2 + 1),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const transferables = osm.transferables();
|
|
101
|
+
expect(new Uint32Array(transferables.nodes.taggedEntityBits)).toEqual(
|
|
102
|
+
new Uint32Array([
|
|
103
|
+
0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
|
|
104
|
+
0xffffffff, 1,
|
|
105
|
+
]),
|
|
106
|
+
);
|
|
107
|
+
expect(new Uint32Array(transferables.nodes.tagRankCheckpoints)).toEqual(
|
|
108
|
+
new Uint32Array([0, 256, 257]),
|
|
109
|
+
);
|
|
110
|
+
expect(new Uint32Array(transferables.nodes.tagOffsets)).toEqual(
|
|
111
|
+
new Uint32Array(Array.from({ length: entityCount + 1 }, (_, index) => index * 2)),
|
|
112
|
+
);
|
|
113
|
+
expect(Tags.getBytesRequired(entityCount, entityCount)).toBe(
|
|
114
|
+
9 * Uint32Array.BYTES_PER_ELEMENT +
|
|
115
|
+
3 * Uint32Array.BYTES_PER_ELEMENT +
|
|
116
|
+
(entityCount + 1) * Uint32Array.BYTES_PER_ELEMENT,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
const reconstructed = new Osm(transferables);
|
|
120
|
+
expect(reconstructed.nodes.tags.taggedEntityCount).toBe(entityCount);
|
|
121
|
+
expect(reconstructed.nodes.getByIndex(256).tags).toEqual({
|
|
122
|
+
shared: "even",
|
|
123
|
+
position: "256",
|
|
124
|
+
});
|
|
125
|
+
expect(reconstructed.nodes.search("shared", "odd")).toHaveLength(128);
|
|
126
|
+
expect([...reconstructed.nodes.tags.taggedEntityIndexes()]).toHaveLength(entityCount);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("stores and round-trips an all-untagged collection with only the offset sentinel", () => {
|
|
130
|
+
const osm = new Osm({ id: "all-untagged" });
|
|
131
|
+
const entityCount = 257;
|
|
132
|
+
|
|
133
|
+
for (let index = 0; index < entityCount; index++) {
|
|
134
|
+
osm.nodes.addNode({ id: index + 1, lon: index / 10, lat: index / 20 });
|
|
135
|
+
}
|
|
136
|
+
osm.buildIndexes();
|
|
137
|
+
|
|
138
|
+
expect(osm.nodes.tags.taggedEntityCount).toBe(0);
|
|
139
|
+
expect([...osm.nodes.tags.taggedEntityIndexes()]).toEqual([]);
|
|
140
|
+
for (const index of [0, 31, 32, 255, 256]) {
|
|
141
|
+
expect(osm.nodes.tags.cardinality(index)).toBe(0);
|
|
142
|
+
expect(osm.nodes.getByIndex(index).tags).toBeUndefined();
|
|
143
|
+
}
|
|
144
|
+
expect([...osm.nodes].every((node) => node.tags === undefined)).toBe(true);
|
|
145
|
+
expect(osm.nodes.search("missing-key")).toEqual([]);
|
|
146
|
+
|
|
147
|
+
const transferables = osm.transferables();
|
|
148
|
+
expect(new Uint32Array(transferables.nodes.taggedEntityBits)).toEqual(new Uint32Array(9));
|
|
149
|
+
expect(new Uint32Array(transferables.nodes.tagRankCheckpoints)).toEqual(
|
|
150
|
+
new Uint32Array([0, 0, 0]),
|
|
151
|
+
);
|
|
152
|
+
expect(new Uint32Array(transferables.nodes.tagOffsets)).toEqual(new Uint32Array([0]));
|
|
153
|
+
expect(transferables.nodes.tagKeys.byteLength).toBe(0);
|
|
154
|
+
expect(transferables.nodes.tagVals.byteLength).toBe(0);
|
|
155
|
+
expect(Tags.getBytesRequired(entityCount, 0)).toBe(
|
|
156
|
+
9 * Uint32Array.BYTES_PER_ELEMENT +
|
|
157
|
+
3 * Uint32Array.BYTES_PER_ELEMENT +
|
|
158
|
+
Uint32Array.BYTES_PER_ELEMENT,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const reconstructed = new Osm(transferables);
|
|
162
|
+
expect(reconstructed.nodes.tags.taggedEntityCount).toBe(0);
|
|
163
|
+
expect([...reconstructed.nodes.tags.taggedEntityIndexes()]).toEqual([]);
|
|
164
|
+
expect(reconstructed.nodes.getByIndex(256)).toEqual({
|
|
165
|
+
id: 257,
|
|
166
|
+
lon: 25.6,
|
|
167
|
+
lat: 12.8,
|
|
168
|
+
});
|
|
169
|
+
expect(reconstructed.nodes.search("missing-key")).toEqual([]);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("preserves plain load diagnostics through info and transfer", () => {
|
|
173
|
+
const osm = new Osm();
|
|
174
|
+
osm.buildIndexes();
|
|
175
|
+
osm.setLoadDiagnostics({
|
|
176
|
+
requestedProfile: "auto",
|
|
177
|
+
selectedProfile: "view",
|
|
178
|
+
reasons: [{ code: "large", level: "warning", message: "View selected" }],
|
|
179
|
+
bytes: {
|
|
180
|
+
residentTypedBuffers: 100,
|
|
181
|
+
projectedTypedBufferPeak: 200,
|
|
182
|
+
largestPlannedAllocation: 50,
|
|
183
|
+
storageBytes: 75,
|
|
184
|
+
},
|
|
185
|
+
budgets: { workingSet: 150, singleAllocation: 60 },
|
|
186
|
+
phaseTimingsMs: { parse: 10 },
|
|
187
|
+
counters: { taggedNodes: 0, wayReferences: 0, relationMembers: 0 },
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(osm.info().loadDiagnostics?.selectedProfile).toBe("view");
|
|
191
|
+
expect(new Osm(osm.transferables()).info().loadDiagnostics).toEqual(osm.info().loadDiagnostics);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("indexed way references", () => {
|
|
196
|
+
it("stores resolved node indexes and losslessly preserves sparse missing IDs", () => {
|
|
197
|
+
const osm = new Osm({ id: "way-refs" });
|
|
198
|
+
osm.nodes.addNode({ id: 10, lon: 1, lat: 2 });
|
|
199
|
+
osm.nodes.addNode({ id: 20, lon: 3, lat: 4 });
|
|
200
|
+
osm.ways.addWay({ id: 100, refs: [999, 10, 998] });
|
|
201
|
+
osm.ways.addWay({ id: 101, refs: [10, 997, 20] });
|
|
202
|
+
osm.buildIndexes();
|
|
203
|
+
|
|
204
|
+
expect(osm.ways.getRefIds(0)).toEqual([999, 10, 998]);
|
|
205
|
+
expect(osm.ways.getRefIds(1)).toEqual([10, 997, 20]);
|
|
206
|
+
expect(() => osm.ways.getCoordinates(0)).toThrow("Node 999 not found for way geometry");
|
|
207
|
+
|
|
208
|
+
const transferables = osm.transferables();
|
|
209
|
+
expect(transferables.ways.refs.byteLength).toBe(6 * Uint32Array.BYTES_PER_ELEMENT);
|
|
210
|
+
expect(new Uint32Array(transferables.ways.missingRefPositions)).toEqual(
|
|
211
|
+
new Uint32Array([0, 2, 4]),
|
|
212
|
+
);
|
|
213
|
+
expect(new Float64Array(transferables.ways.missingRefIds)).toEqual(
|
|
214
|
+
new Float64Array([999, 998, 997]),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const reconstructed = new Osm(transferables);
|
|
218
|
+
expect(reconstructed.ways.getRefIds(0)).toEqual([999, 10, 998]);
|
|
219
|
+
expect(reconstructed.ways.getRefIds(1)).toEqual([10, 997, 20]);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("produces the same content hash for pending IDs and directly indexed refs", () => {
|
|
223
|
+
const deferred = new Osm({ id: "deferred" });
|
|
224
|
+
deferred.nodes.addNode({ id: 10, lon: 1, lat: 2 });
|
|
225
|
+
deferred.nodes.addNode({ id: 20, lon: 3, lat: 4 });
|
|
226
|
+
deferred.ways.addWay({ id: 100, refs: [10, 20] });
|
|
227
|
+
deferred.buildIndexes();
|
|
228
|
+
|
|
229
|
+
const direct = new Osm({ id: "direct" });
|
|
230
|
+
direct.nodes.addNode({ id: 10, lon: 1, lat: 2 });
|
|
231
|
+
direct.nodes.addNode({ id: 20, lon: 3, lat: 4 });
|
|
232
|
+
direct.nodes.buildIndex();
|
|
233
|
+
direct.ways.addWays(
|
|
234
|
+
[{ id: 100, keys: [], vals: [], refs: [10, 10] }],
|
|
235
|
+
new Uint32Array(),
|
|
236
|
+
undefined,
|
|
237
|
+
(id) => direct.nodes.ids.getIndexFromId(id),
|
|
238
|
+
);
|
|
239
|
+
direct.buildIndexes();
|
|
240
|
+
|
|
241
|
+
expect(direct.ways.getRefIds(0)).toEqual([10, 20]);
|
|
242
|
+
expect(direct.contentHash()).toBe(deferred.contentHash());
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe("empty spatial capabilities", () => {
|
|
247
|
+
it("marks requested empty way and relation indexes as built", () => {
|
|
248
|
+
const osm = new Osm({ id: "empty-spatial-indexes" });
|
|
249
|
+
osm.buildIndexes();
|
|
250
|
+
|
|
251
|
+
osm.ways.buildSpatialIndex();
|
|
252
|
+
osm.relations.buildSpatialIndex();
|
|
253
|
+
|
|
254
|
+
expect(osm.ways.hasSpatialIndex()).toBe(true);
|
|
255
|
+
expect(osm.relations.hasSpatialIndex()).toBe(true);
|
|
256
|
+
expect(osm.info().spatialIndexes).toMatchObject({
|
|
257
|
+
ways: true,
|
|
258
|
+
relations: true,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
const transferables = osm.transferables();
|
|
262
|
+
const inspection = inspectBackingBuffers(transferables);
|
|
263
|
+
expect(inspection.arrayBuffers).toBe(0);
|
|
264
|
+
|
|
265
|
+
const restored = new Osm(transferables);
|
|
266
|
+
expect(restored.ways.hasSpatialIndex()).toBe(true);
|
|
267
|
+
expect(restored.relations.hasSpatialIndex()).toBe(true);
|
|
268
|
+
});
|
|
269
|
+
});
|
package/test/tags-search.test.ts
CHANGED
|
@@ -1,60 +1,93 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
import
|
|
1
|
+
import type { OsmNode, OsmWay } from "@osmix/types";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { Nodes } from "../src/nodes";
|
|
5
|
+
import StringTable from "../src/stringtable";
|
|
6
|
+
import { Ways } from "../src/ways";
|
|
6
7
|
|
|
7
8
|
describe("Tags.search on Nodes", () => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
9
|
+
it("returns null for an unknown ID in an empty collection", () => {
|
|
10
|
+
const nodes = new Nodes(new StringTable());
|
|
11
|
+
nodes.buildIndex();
|
|
12
|
+
|
|
13
|
+
expect(nodes.getNodeLonLat({ id: 999 })).toBeNull();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("finds by key and key+value", () => {
|
|
17
|
+
const st = new StringTable();
|
|
18
|
+
const nodes = new Nodes(st);
|
|
19
|
+
const node1: OsmNode = { id: 1, lon: -1, lat: 1, tags: { curb: "yes" } };
|
|
20
|
+
const node2: OsmNode = { id: 2, lon: -2, lat: 2, tags: { curb: "no" } };
|
|
21
|
+
const node3: OsmNode = { id: 3, lon: -3, lat: 3 };
|
|
22
|
+
nodes.addNode(node1);
|
|
23
|
+
nodes.addNode(node2);
|
|
24
|
+
nodes.addNode(node3);
|
|
25
|
+
nodes.buildIndex();
|
|
26
|
+
|
|
27
|
+
expect(nodes.getNodeLonLat({ id: 999 })).toBeNull();
|
|
28
|
+
|
|
29
|
+
const allCurb = nodes.search("curb");
|
|
30
|
+
expect(allCurb).toEqual([node1, node2]);
|
|
31
|
+
const curbYes = nodes.search("curb", "yes");
|
|
32
|
+
expect(curbYes).toEqual([node1]);
|
|
33
|
+
const curbNo = nodes.search("curb", "no");
|
|
34
|
+
expect(curbNo).toEqual([node2]);
|
|
35
|
+
const none = nodes.search("highway");
|
|
36
|
+
expect(none).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("returns no matches for a shared string-table key absent from the collection", () => {
|
|
40
|
+
const st = new StringTable();
|
|
41
|
+
const nodes = new Nodes(st);
|
|
42
|
+
nodes.addNode({ id: 1, lon: -1, lat: 1, tags: { curb: "yes" } });
|
|
43
|
+
nodes.buildIndex();
|
|
44
|
+
|
|
45
|
+
const ways = new Ways(st, nodes);
|
|
46
|
+
ways.addWay({ id: 10, refs: [1], tags: { highway: "residential" } });
|
|
47
|
+
ways.buildIndex();
|
|
48
|
+
|
|
49
|
+
expect(nodes.search("highway")).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
29
52
|
|
|
30
53
|
describe("Tags.search on Ways", () => {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
54
|
+
it("rejects block-local tag indexes missing from the string table map", () => {
|
|
55
|
+
const st = new StringTable();
|
|
56
|
+
const nodes = new Nodes(st);
|
|
57
|
+
const ways = new Ways(st, nodes);
|
|
58
|
+
|
|
59
|
+
expect(() =>
|
|
60
|
+
ways.addWays([{ id: 1, keys: [9], vals: [0], refs: [1] }], new Uint32Array()),
|
|
61
|
+
).toThrow("Tag key not found");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("finds by key and key+value", () => {
|
|
65
|
+
const st = new StringTable();
|
|
66
|
+
const nodes = new Nodes(st);
|
|
67
|
+
const ways = new Ways(st, nodes);
|
|
68
|
+
const way1: OsmWay = {
|
|
69
|
+
id: 10,
|
|
70
|
+
refs: [1, 2],
|
|
71
|
+
tags: { highway: "residential" },
|
|
72
|
+
};
|
|
73
|
+
const way2: OsmWay = { id: 11, refs: [2, 3], tags: { area: "yes" } };
|
|
74
|
+
const way3: OsmWay = {
|
|
75
|
+
id: 12,
|
|
76
|
+
refs: [3, 4],
|
|
77
|
+
tags: { highway: "primary", area: "no" },
|
|
78
|
+
};
|
|
79
|
+
ways.addWay(way1);
|
|
80
|
+
ways.addWay(way2);
|
|
81
|
+
ways.addWay(way3);
|
|
82
|
+
ways.buildIndex();
|
|
83
|
+
|
|
84
|
+
const allHighway = ways.search("highway");
|
|
85
|
+
expect(allHighway).toEqual([way1, way3]);
|
|
86
|
+
const areaYes = ways.search("area", "yes");
|
|
87
|
+
expect(areaYes).toEqual([way2]);
|
|
88
|
+
const areaAll = ways.search("area");
|
|
89
|
+
expect(areaAll).toEqual([way2, way3]);
|
|
90
|
+
const areaNo = ways.search("area", "no");
|
|
91
|
+
expect(areaNo).toEqual([way3]);
|
|
92
|
+
});
|
|
93
|
+
});
|