@osmix/core 0.0.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/src/ways.ts ADDED
@@ -0,0 +1,241 @@
1
+ import type { OsmTags, OsmWay } from "@osmix/json"
2
+ import type { OsmPbfWay } from "@osmix/pbf"
3
+ import type { GeoBbox2D, LonLat } from "@osmix/shared/types"
4
+ import Flatbush from "flatbush"
5
+ import { Entities, type EntitiesTransferables } from "./entities"
6
+ import { type IdOrIndex, Ids } from "./ids"
7
+ import type { Nodes } from "./nodes"
8
+ import type StringTable from "./stringtable"
9
+ import { Tags } from "./tags"
10
+ import {
11
+ BufferConstructor,
12
+ type BufferType,
13
+ CoordinateArrayType,
14
+ IdArrayType,
15
+ ResizeableTypedArray,
16
+ } from "./typed-arrays"
17
+
18
+ export interface WaysTransferables extends EntitiesTransferables {
19
+ refStart: BufferType
20
+ refCount: BufferType
21
+ refs: BufferType
22
+ bbox: BufferType
23
+ spatialIndex: BufferType
24
+ }
25
+
26
+ export class Ways extends Entities<OsmWay> {
27
+ spatialIndex: Flatbush = new Flatbush(1)
28
+
29
+ refStart: ResizeableTypedArray<Uint32Array>
30
+ refCount: ResizeableTypedArray<Uint16Array> // Maximum 2,000 nodes per way
31
+
32
+ // Node IDs
33
+ refs: ResizeableTypedArray<Float64Array>
34
+
35
+ // Bounding box of the way in geographic coordinates
36
+ bbox: ResizeableTypedArray<Float64Array>
37
+
38
+ static from(stringTable: StringTable, wits: WaysTransferables) {
39
+ const idIndex = Ids.from(wits)
40
+ const tagIndex = Tags.from(stringTable, wits)
41
+ const wayIndex = new Ways(stringTable, idIndex, tagIndex)
42
+ wayIndex.refStart = ResizeableTypedArray.from(Uint32Array, wits.refStart)
43
+ wayIndex.refCount = ResizeableTypedArray.from(Uint16Array, wits.refCount)
44
+ wayIndex.refs = ResizeableTypedArray.from(IdArrayType, wits.refs)
45
+ wayIndex.bbox = ResizeableTypedArray.from(CoordinateArrayType, wits.bbox)
46
+ wayIndex.spatialIndex = Flatbush.from(wits.spatialIndex)
47
+ return wayIndex
48
+ }
49
+
50
+ constructor(stringTable: StringTable, idIndex?: Ids, tagIndex?: Tags) {
51
+ super("way", stringTable, idIndex, tagIndex)
52
+ this.refStart = new ResizeableTypedArray(Uint32Array)
53
+ this.refCount = new ResizeableTypedArray(Uint16Array)
54
+ this.refs = new ResizeableTypedArray(IdArrayType)
55
+ this.bbox = new ResizeableTypedArray(CoordinateArrayType)
56
+ }
57
+
58
+ transferables(): WaysTransferables {
59
+ return {
60
+ refStart: this.refStart.array.buffer,
61
+ refCount: this.refCount.array.buffer,
62
+ refs: this.refs.array.buffer,
63
+ bbox: this.bbox.array.buffer,
64
+ spatialIndex: this.spatialIndex.data,
65
+ ...this.ids.transferables(),
66
+ ...this.tags.transferables(),
67
+ }
68
+ }
69
+
70
+ addWay(way: OsmWay) {
71
+ this.ids.add(way.id)
72
+ this.tags.addTags(way.tags)
73
+ this.refStart.push(this.refs.length)
74
+ this.refCount.push(way.refs.length)
75
+ for (const ref of way.refs) this.refs.push(ref)
76
+ }
77
+
78
+ /**
79
+ * Bulk add ways directly from a PBF PrimitiveBlock.
80
+ */
81
+ addWays(
82
+ ways: OsmPbfWay[],
83
+ blockStringIndexMap: Map<number, number>,
84
+ filter?: (way: OsmWay) => OsmWay | null,
85
+ ) {
86
+ let added = 0
87
+ for (const way of ways) {
88
+ let prevRefId = 0
89
+ const refs = way.refs.map((refId) => {
90
+ prevRefId += refId
91
+ return prevRefId
92
+ })
93
+ const filteredWay = filter
94
+ ? filter({
95
+ id: way.id,
96
+ refs,
97
+ tags: this.tags.getTagsFromIndices(way.keys, way.vals),
98
+ })
99
+ : null
100
+ if (filter && filteredWay === null) continue
101
+
102
+ this.ids.add(way.id)
103
+ this.refStart.push(this.refs.length)
104
+ this.refCount.push(filteredWay?.refs.length ?? refs.length)
105
+ this.refs.pushMany(filteredWay?.refs ?? refs)
106
+
107
+ const tagKeys: number[] = way.keys.map((key) => {
108
+ const index = blockStringIndexMap.get(key)
109
+ if (index === undefined) throw Error("Tag key not found")
110
+ return index
111
+ })
112
+ const tagValues: number[] = way.vals.map((val) => {
113
+ const index = blockStringIndexMap.get(val)
114
+ if (index === undefined) throw Error("Tag value not found")
115
+ return index
116
+ })
117
+ this.tags.addTagKeysAndValues(tagKeys, tagValues)
118
+ added++
119
+ }
120
+ return added
121
+ }
122
+
123
+ buildEntityIndex() {
124
+ this.refStart.compact()
125
+ this.refCount.compact()
126
+ this.refs.compact()
127
+ }
128
+
129
+ buildSpatialIndex(nodeIndex: Nodes) {
130
+ console.time("WayIndex.buildSpatialIndex")
131
+ this.spatialIndex = new Flatbush(
132
+ this.size,
133
+ 128,
134
+ Float64Array,
135
+ BufferConstructor,
136
+ )
137
+ for (let i = 0; i < this.size; i++) {
138
+ let minX = Number.POSITIVE_INFINITY
139
+ let minY = Number.POSITIVE_INFINITY
140
+ let maxX = Number.NEGATIVE_INFINITY
141
+ let maxY = Number.NEGATIVE_INFINITY
142
+ const start = this.refStart.at(i)
143
+ const count = this.refCount.at(i)
144
+ for (let j = start; j < start + count; j++) {
145
+ const refId = this.refs.at(j)
146
+ const [lon, lat] = nodeIndex.getNodeLonLat({ id: refId })
147
+ if (lon < minX) minX = lon
148
+ if (lon > maxX) maxX = lon
149
+ if (lat < minY) minY = lat
150
+ if (lat > maxY) maxY = lat
151
+ }
152
+ this.bbox.push(minX)
153
+ this.bbox.push(minY)
154
+ this.bbox.push(maxX)
155
+ this.bbox.push(maxY)
156
+ this.spatialIndex.add(minX, minY, maxX, maxY)
157
+ }
158
+ this.spatialIndex.finish()
159
+ console.timeEnd("WayIndex.buildSpatialIndex")
160
+ return this.spatialIndex
161
+ }
162
+
163
+ getFullEntity(index: number, id: number, tags?: OsmTags): OsmWay {
164
+ return {
165
+ id,
166
+ refs: [...this.getRefIds(index)],
167
+ tags,
168
+ }
169
+ }
170
+
171
+ getRefIds(index: number) {
172
+ const start = this.refStart.at(index)
173
+ const count = this.refCount.at(index)
174
+ return this.refs.slice(start, start + count)
175
+ }
176
+
177
+ getBbox(idOrIndex: IdOrIndex): GeoBbox2D {
178
+ const index =
179
+ "index" in idOrIndex ? idOrIndex.index : this.ids.idOrIndex(idOrIndex)[0]
180
+ return [
181
+ this.bbox.at(index * 4),
182
+ this.bbox.at(index * 4 + 1),
183
+ this.bbox.at(index * 4 + 2),
184
+ this.bbox.at(index * 4 + 3),
185
+ ]
186
+ }
187
+
188
+ getLine(index: number, nodeIndex: Nodes) {
189
+ const count = this.refCount.at(index)
190
+ const start = this.refStart.at(index)
191
+ const line = new Float64Array(count * 2)
192
+ for (let i = 0; i < count; i++) {
193
+ const ref = this.refs.at(start + i)
194
+ const [lon, lat] = nodeIndex.getNodeLonLat({ id: ref })
195
+ line[i * 2] = lon
196
+ line[i * 2 + 1] = lat
197
+ }
198
+ return line
199
+ }
200
+
201
+ getCoordinates(index: number, nodeIndex: Nodes): LonLat[] {
202
+ const count = this.refCount.at(index)
203
+ const start = this.refStart.at(index)
204
+ const coords: [number, number][] = []
205
+ for (let refIndex = start; refIndex < start + count; refIndex++) {
206
+ const ref = this.refs.at(refIndex)
207
+ const coord = nodeIndex.getNodeLonLat({ id: ref })
208
+ if (
209
+ coord === undefined ||
210
+ coord[0] === undefined ||
211
+ coord[1] === undefined
212
+ ) {
213
+ console.error("node index has ref", nodeIndex.ids.has(ref))
214
+ throw Error(
215
+ `Invalid coordinate for way id ${this.ids.at(index)}, index ${index}, node ref ${ref}, ref index ${refIndex}`,
216
+ )
217
+ }
218
+ coords.push(coord)
219
+ }
220
+ return coords
221
+ }
222
+
223
+ intersects(bbox: GeoBbox2D, filterFn?: (index: number) => boolean): number[] {
224
+ return this.spatialIndex.search(
225
+ bbox[0],
226
+ bbox[1],
227
+ bbox[2],
228
+ bbox[3],
229
+ filterFn,
230
+ )
231
+ }
232
+
233
+ neighbors(
234
+ x: number,
235
+ y: number,
236
+ maxResults?: number,
237
+ maxDistance?: number,
238
+ ): number[] {
239
+ return this.spatialIndex.neighbors(x, y, maxResults, maxDistance)
240
+ }
241
+ }
@@ -0,0 +1,29 @@
1
+ import { getFixtureFile } from "@osmix/shared/test/fixtures"
2
+ import type { GeoBbox2D } from "@osmix/shared/types"
3
+ import { beforeAll, bench, describe } from "vitest"
4
+ import { Osmix } from "../src/osmix"
5
+
6
+ const MONACO_BBOX: GeoBbox2D = [7.4053929, 43.7232244, 7.4447259, 43.7543687]
7
+ // const SEATTLE_BBOX: GeoBbox2D = [-122.33, 47.48, -122.29, 47.52]
8
+
9
+ const BBOX = MONACO_BBOX
10
+ const PBF = "monaco.pbf"
11
+
12
+ let buffer: ArrayBufferLike
13
+
14
+ beforeAll(async () => {
15
+ buffer = await getFixtureFile(PBF)
16
+ })
17
+
18
+ describe("simple extract benchmark", () => {
19
+ bench("two-step parse then extract", async () => {
20
+ const data = buffer.slice(0)
21
+ const full = await Osmix.fromPbf(data)
22
+ full.extract(BBOX)
23
+ })
24
+
25
+ bench("streaming extract during parse", async () => {
26
+ const data = buffer.slice(0)
27
+ await Osmix.fromPbf(data, { extractBbox: BBOX })
28
+ })
29
+ })
@@ -0,0 +1,156 @@
1
+ import { getFixtureFileReadStream } from "@osmix/shared/test/fixtures"
2
+ import type { GeoBbox2D } from "@osmix/shared/types"
3
+ import { assert, test } from "vitest"
4
+ import { Osmix } from "../src/osmix"
5
+
6
+ const TEST_BBOX: GeoBbox2D = [-0.1, -0.1, 1, 1]
7
+ const SEATTLE_BBOX: GeoBbox2D = [-122.463226, 47.469878, -122.180328, 47.82883]
8
+
9
+ function buildSourceOsm() {
10
+ const osm = new Osmix({ id: "source" })
11
+ osm.nodes.addNode({
12
+ id: 1,
13
+ lat: 0,
14
+ lon: 0,
15
+ tags: {
16
+ name: "inside-a",
17
+ },
18
+ })
19
+ osm.nodes.addNode({
20
+ id: 2,
21
+ lat: 0,
22
+ lon: 2,
23
+ tags: {
24
+ name: "outside-a",
25
+ },
26
+ })
27
+ osm.nodes.addNode({
28
+ id: 3,
29
+ lat: 0.5,
30
+ lon: 0.5,
31
+ })
32
+ osm.nodes.addNode({
33
+ id: 4,
34
+ lat: 0.5,
35
+ lon: 1.5,
36
+ })
37
+
38
+ osm.ways.addWay({
39
+ id: 10,
40
+ refs: [1, 2],
41
+ tags: {
42
+ highway: "residential",
43
+ },
44
+ })
45
+ osm.ways.addWay({
46
+ id: 11,
47
+ refs: [3, 4],
48
+ tags: {
49
+ highway: "service",
50
+ },
51
+ })
52
+
53
+ osm.relations.addRelation({
54
+ id: 20,
55
+ members: [
56
+ {
57
+ type: "way",
58
+ ref: 10,
59
+ role: "outer",
60
+ },
61
+ ],
62
+ tags: {
63
+ type: "multipolygon",
64
+ },
65
+ })
66
+
67
+ osm.buildIndexes()
68
+ return osm
69
+ }
70
+
71
+ test("extract a BBOX while reading a PBF", async () => {
72
+ const source = buildSourceOsm()
73
+ const transform = new TransformStream<Uint8Array, ArrayBufferLike>()
74
+ const extract = new Osmix()
75
+ const extractPromise = extract.readPbf(transform.readable, {
76
+ extractBbox: TEST_BBOX,
77
+ })
78
+ await source.toPbfStream().pipeTo(transform.writable)
79
+ await extractPromise
80
+
81
+ assert.equal(extract.nodes.size, 2)
82
+ assert.isTrue(extract.nodes.ids.has(1))
83
+ assert.isTrue(extract.nodes.ids.has(3))
84
+ assert.isNotTrue(extract.nodes.ids.has(2))
85
+ assert.isNotTrue(extract.nodes.ids.has(4))
86
+
87
+ const truncatedWay = extract.ways.getById(10)
88
+ assert.exists(truncatedWay)
89
+ assert.deepEqual(truncatedWay?.refs, [1])
90
+
91
+ assert.isTrue(extract.ways.ids.has(11))
92
+ assert.isTrue(extract.relations.ids.has(20))
93
+ })
94
+
95
+ test("extract a BBOX after reading a PBF", async () => {
96
+ const source = buildSourceOsm()
97
+ const buffer = await source.toPbfBuffer()
98
+
99
+ const streaming = new Osmix()
100
+ await streaming.readPbf(buffer.slice(0), { extractBbox: TEST_BBOX })
101
+
102
+ const twoStepOsmix = new Osmix()
103
+ await twoStepOsmix.readPbf(buffer.slice(0))
104
+ const twoStep = twoStepOsmix.extract(TEST_BBOX)
105
+
106
+ assert.equal(streaming.nodes.size, twoStep.nodes.size)
107
+ assert.equal(streaming.ways.size, twoStep.ways.size)
108
+ assert.equal(streaming.relations.size, twoStep.relations.size)
109
+
110
+ assert.deepEqual(
111
+ Array.from(streaming.nodes.ids.sorted),
112
+ Array.from(twoStep.nodes.ids.sorted),
113
+ )
114
+ assert.deepEqual(
115
+ Array.from(streaming.ways.ids.sorted),
116
+ Array.from(twoStep.ways.ids.sorted),
117
+ )
118
+ assert.deepEqual(
119
+ Array.from(streaming.relations.ids.sorted),
120
+ Array.from(twoStep.relations.ids.sorted),
121
+ )
122
+
123
+ for (const nodeId of streaming.nodes.ids.sorted) {
124
+ const streamingNode = streaming.nodes.getById(nodeId)
125
+ const twoStepNode = twoStep.nodes.getById(nodeId)
126
+ assert.deepEqual(streamingNode, twoStepNode)
127
+ }
128
+
129
+ for (const wayId of streaming.ways.ids.sorted) {
130
+ const streamingWay = streaming.ways.getById(wayId)
131
+ const twoStepWay = twoStep.ways.getById(wayId)
132
+ assert.deepEqual(streamingWay, twoStepWay)
133
+ }
134
+
135
+ for (const relationId of streaming.relations.ids.sorted) {
136
+ const streamingRelation = streaming.relations.getById(relationId)
137
+ const twoStepRelation = twoStep.relations.getById(relationId)
138
+ assert.deepEqual(streamingRelation, twoStepRelation)
139
+ }
140
+ })
141
+
142
+ test.skip("extract from a large PBF", { timeout: 500_000 }, async () => {
143
+ const seattle = new Osmix({ logger: console.error })
144
+ await seattle.readPbf(getFixtureFileReadStream("usa.pbf"), {
145
+ extractBbox: SEATTLE_BBOX,
146
+ })
147
+
148
+ assert.deepEqual(
149
+ {
150
+ nodes: seattle.nodes.size,
151
+ ways: seattle.ways.size,
152
+ relations: seattle.relations.size,
153
+ },
154
+ { nodes: 802_541, ways: 241_248, relations: 3_622 },
155
+ )
156
+ })
@@ -0,0 +1,140 @@
1
+ import type { OsmNode } from "@osmix/json"
2
+ import { Osmix } from "../src/osmix"
3
+
4
+ const YAKIM_LAT = 46.60207
5
+ const YAKIM_LON = -120.505898
6
+
7
+ const ONE_KM_LON = 0.0131 // approximately
8
+ const ONE_KM_LAT = 0.009
9
+
10
+ const node0: OsmNode = {
11
+ id: 0,
12
+ lat: YAKIM_LAT,
13
+ lon: YAKIM_LON,
14
+ }
15
+
16
+ const node1: OsmNode = {
17
+ id: 1,
18
+ lat: YAKIM_LAT,
19
+ lon: YAKIM_LON - ONE_KM_LON, // approximately 1km west
20
+ }
21
+
22
+ function addBaseNodes(osm: Osmix) {
23
+ osm.nodes.addNode(node0)
24
+ osm.nodes.addNode(node1)
25
+ }
26
+
27
+ function addBaseWays(osm: Osmix) {
28
+ osm.ways.addWay({
29
+ id: 1,
30
+ refs: [0, 1],
31
+ tags: {
32
+ highway: "primary",
33
+ },
34
+ })
35
+ }
36
+
37
+ /**
38
+ * Create a base OSM with one way and two nodes.
39
+ */
40
+ export function createBaseOsm(): Osmix {
41
+ const base = new Osmix()
42
+ addBaseNodes(base)
43
+ base.nodes.buildIndex()
44
+ addBaseWays(base)
45
+ base.buildIndexes()
46
+ return base
47
+ }
48
+
49
+ /**
50
+ * Create a patch OSM with:
51
+ * - The same way as the base OSM, with new tags
52
+ * - A way that has an overlapping node with the first way, but does not share a node and so starts disconnected
53
+ * - A way that crosses over the first way, but does not share a node and so starts disconnected
54
+ */
55
+ export function createPatchOsm(): Osmix {
56
+ const osm = new Osmix()
57
+
58
+ addBaseNodes(osm)
59
+
60
+ // Add way the connects with base way node 0 (will replace it)
61
+ osm.nodes.addNode({
62
+ ...node0,
63
+ id: 2,
64
+ tags: {
65
+ crossing: "yes",
66
+ },
67
+ })
68
+ osm.nodes.addNode({
69
+ ...node1,
70
+ id: 3,
71
+ lon: YAKIM_LON + ONE_KM_LON, // ends 1km east of the center point
72
+ })
73
+
74
+ // Add nodes for disconnected way
75
+ osm.nodes.addNode({
76
+ id: 4,
77
+ lat: YAKIM_LAT - ONE_KM_LAT, // 1km south
78
+ lon: YAKIM_LON + ONE_KM_LON / 4, // 250m east
79
+ })
80
+ osm.nodes.addNode({
81
+ id: 5,
82
+ lat: YAKIM_LAT + ONE_KM_LAT, // 1km north
83
+ lon: YAKIM_LON + ONE_KM_LON / 4, // 250m east
84
+ })
85
+
86
+ // Add nodes for crossing way
87
+ osm.nodes.addNode({
88
+ id: 6,
89
+ lat: YAKIM_LAT - ONE_KM_LAT, // 1km south
90
+ lon: YAKIM_LON + ONE_KM_LON / 2, // 500m east
91
+ })
92
+ osm.nodes.addNode({
93
+ id: 7,
94
+ lat: YAKIM_LAT + ONE_KM_LAT, // 1km north
95
+ lon: YAKIM_LON + ONE_KM_LON / 2, // 500m east
96
+ })
97
+
98
+ osm.nodes.buildIndex()
99
+
100
+ // Add same base way with new tags
101
+ osm.ways.addWay({
102
+ id: 1,
103
+ refs: [0, 1],
104
+ tags: {
105
+ highway: "primary",
106
+ version: "2",
107
+ },
108
+ })
109
+
110
+ // Add way that overlaps with base way node 1. Node should be de-duplicated.
111
+ osm.ways.addWay({
112
+ id: 2,
113
+ refs: [2, 3],
114
+ tags: {
115
+ highway: "secondary",
116
+ },
117
+ })
118
+
119
+ // Add way that crosses way 2, and should generate 1 intersection.
120
+ osm.ways.addWay({
121
+ id: 3,
122
+ refs: [4, 5],
123
+ tags: {
124
+ highway: "primary",
125
+ },
126
+ })
127
+
128
+ // Add a way that crosses way 2, but has a tag indicating it is an underpass and should be left alone
129
+ osm.ways.addWay({
130
+ id: 4,
131
+ refs: [6, 7],
132
+ tags: {
133
+ highway: "underpass",
134
+ tunnel: "yes",
135
+ },
136
+ })
137
+
138
+ osm.buildIndexes()
139
+ return osm
140
+ }
@@ -0,0 +1,22 @@
1
+ import {
2
+ getFixtureFile,
3
+ getFixtureFileReadStream,
4
+ PBFs,
5
+ } from "@osmix/shared/test/fixtures"
6
+ import { assert, beforeAll, describe, it } from "vitest"
7
+ import { Osmix } from "../src/osmix"
8
+
9
+ describe("read", () => {
10
+ describe.each(Object.entries(PBFs))("%s", async (name, pbf) => {
11
+ beforeAll(() => getFixtureFile(pbf.url))
12
+
13
+ it("into OSM class", async () => {
14
+ const fileStream = getFixtureFileReadStream(pbf.url)
15
+ const osm = await Osmix.fromPbf(fileStream, { id: name })
16
+ assert.equal(osm.nodes.size, pbf.nodes)
17
+ assert.equal(osm.stringTable.length, pbf.uniqueStrings)
18
+ assert.deepEqual(osm.nodes.getByIndex(0), pbf.node0)
19
+ assert.equal(osm.ways.size, pbf.ways)
20
+ })
21
+ })
22
+ })