@osmix/shared 0.0.2 → 0.0.7
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 +33 -0
- package/README.md +49 -19
- package/dist/assert.d.ts +24 -0
- package/dist/assert.d.ts.map +1 -0
- package/dist/assert.js +28 -0
- package/dist/assert.js.map +1 -0
- package/dist/bbox-intersects.d.ts +15 -0
- package/dist/bbox-intersects.d.ts.map +1 -0
- package/dist/bbox-intersects.js +24 -0
- package/dist/bbox-intersects.js.map +1 -0
- package/dist/bytes-to-stream.d.ts +18 -0
- package/dist/bytes-to-stream.d.ts.map +1 -0
- package/dist/bytes-to-stream.js +25 -0
- package/dist/bytes-to-stream.js.map +1 -0
- package/dist/color.d.ts +4 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/color.js +33 -0
- package/dist/color.js.map +1 -0
- package/dist/concat-bytes.d.ts +5 -0
- package/dist/concat-bytes.d.ts.map +1 -0
- package/dist/concat-bytes.js +14 -0
- package/dist/concat-bytes.js.map +1 -0
- package/dist/coordinates.d.ts +28 -0
- package/dist/coordinates.d.ts.map +1 -0
- package/dist/coordinates.js +38 -0
- package/dist/coordinates.js.map +1 -0
- package/dist/haversine-distance.d.ts +16 -0
- package/dist/haversine-distance.d.ts.map +1 -0
- package/dist/haversine-distance.js +26 -0
- package/dist/haversine-distance.js.map +1 -0
- package/dist/lineclip.d.ts +2 -0
- package/dist/lineclip.d.ts.map +1 -0
- package/dist/lineclip.js +3 -0
- package/dist/lineclip.js.map +1 -0
- package/dist/progress.d.ts +42 -0
- package/dist/progress.d.ts.map +1 -0
- package/dist/progress.js +53 -0
- package/dist/progress.js.map +1 -0
- package/dist/relation-kind.d.ts +69 -0
- package/dist/relation-kind.d.ts.map +1 -0
- package/dist/relation-kind.js +375 -0
- package/dist/relation-kind.js.map +1 -0
- package/dist/relation-multipolygon.d.ts +43 -0
- package/dist/relation-multipolygon.d.ts.map +1 -0
- package/dist/relation-multipolygon.js +195 -0
- package/dist/relation-multipolygon.js.map +1 -0
- package/dist/src/assert.d.ts +20 -1
- package/dist/src/assert.d.ts.map +1 -1
- package/dist/src/assert.js +20 -1
- package/dist/src/assert.js.map +1 -1
- package/dist/src/bbox-intersects.d.ts +15 -0
- package/dist/src/bbox-intersects.d.ts.map +1 -0
- package/dist/src/bbox-intersects.js +24 -0
- package/dist/src/bbox-intersects.js.map +1 -0
- package/dist/src/bytes-to-stream.d.ts +17 -1
- package/dist/src/bytes-to-stream.d.ts.map +1 -1
- package/dist/src/bytes-to-stream.js +16 -0
- package/dist/src/bytes-to-stream.js.map +1 -1
- package/dist/src/color.d.ts +4 -0
- package/dist/src/color.d.ts.map +1 -0
- package/dist/src/color.js +33 -0
- package/dist/src/color.js.map +1 -0
- package/dist/src/coordinates.d.ts +28 -0
- package/dist/src/coordinates.d.ts.map +1 -0
- package/dist/src/coordinates.js +38 -0
- package/dist/src/coordinates.js.map +1 -0
- package/dist/src/haversine-distance.d.ts +9 -1
- package/dist/src/haversine-distance.d.ts.map +1 -1
- package/dist/src/haversine-distance.js +9 -1
- package/dist/src/haversine-distance.js.map +1 -1
- package/dist/src/progress.d.ts +42 -0
- package/dist/src/progress.d.ts.map +1 -0
- package/dist/src/progress.js +53 -0
- package/dist/src/progress.js.map +1 -0
- package/dist/src/relation-kind.d.ts +69 -0
- package/dist/src/relation-kind.d.ts.map +1 -0
- package/dist/src/relation-kind.js +375 -0
- package/dist/src/relation-kind.js.map +1 -0
- package/dist/src/relation-multipolygon.d.ts +43 -0
- package/dist/src/relation-multipolygon.d.ts.map +1 -0
- package/dist/src/relation-multipolygon.js +195 -0
- package/dist/src/relation-multipolygon.js.map +1 -0
- package/dist/src/stream-to-bytes.d.ts +16 -0
- package/dist/src/stream-to-bytes.d.ts.map +1 -1
- package/dist/src/stream-to-bytes.js +16 -0
- package/dist/src/stream-to-bytes.js.map +1 -1
- package/dist/src/test/fixtures.d.ts +1 -1
- package/dist/src/test/fixtures.d.ts.map +1 -1
- package/dist/src/test/fixtures.js +16 -8
- package/dist/src/test/fixtures.js.map +1 -1
- package/dist/src/throttle.d.ts +25 -0
- package/dist/src/throttle.d.ts.map +1 -0
- package/dist/src/throttle.js +34 -0
- package/dist/src/throttle.js.map +1 -0
- package/dist/src/tile.d.ts +34 -0
- package/dist/src/tile.d.ts.map +1 -0
- package/dist/src/tile.js +72 -0
- package/dist/src/tile.js.map +1 -0
- package/dist/src/transform-bytes.d.ts +22 -0
- package/dist/src/transform-bytes.d.ts.map +1 -1
- package/dist/src/transform-bytes.js +22 -0
- package/dist/src/transform-bytes.js.map +1 -1
- package/dist/src/types.d.ts +76 -1
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/types.js +8 -0
- package/dist/src/types.js.map +1 -1
- package/dist/src/utils.d.ts +30 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/utils.js +70 -0
- package/dist/src/utils.js.map +1 -0
- package/dist/src/way-is-area.d.ts +24 -0
- package/dist/src/way-is-area.d.ts.map +1 -0
- package/dist/src/way-is-area.js +104 -0
- package/dist/src/way-is-area.js.map +1 -0
- package/dist/src/zigzag.d.ts +33 -0
- package/dist/src/zigzag.d.ts.map +1 -0
- package/dist/src/zigzag.js +40 -0
- package/dist/src/zigzag.js.map +1 -0
- package/dist/stream-to-bytes.d.ts +18 -0
- package/dist/stream-to-bytes.d.ts.map +1 -0
- package/dist/stream-to-bytes.js +30 -0
- package/dist/stream-to-bytes.js.map +1 -0
- package/dist/test/fixtures.d.ts +36 -0
- package/dist/test/fixtures.d.ts.map +1 -0
- package/dist/test/fixtures.js +175 -0
- package/dist/test/fixtures.js.map +1 -0
- package/dist/test/haversine-distance.test.js +2 -2
- package/dist/test/haversine-distance.test.js.map +1 -1
- package/dist/test/relation-kind.test.d.ts +2 -0
- package/dist/test/relation-kind.test.d.ts.map +1 -0
- package/dist/test/relation-kind.test.js +367 -0
- package/dist/test/relation-kind.test.js.map +1 -0
- package/dist/test/relation-multipolygon.test.d.ts +2 -0
- package/dist/test/relation-multipolygon.test.d.ts.map +1 -0
- package/dist/test/relation-multipolygon.test.js +237 -0
- package/dist/test/relation-multipolygon.test.js.map +1 -0
- package/dist/test/utils.test.d.ts +2 -0
- package/dist/test/utils.test.d.ts.map +1 -0
- package/dist/test/utils.test.js +76 -0
- package/dist/test/utils.test.js.map +1 -0
- package/dist/test/way-is-area.test.d.ts +2 -0
- package/dist/test/way-is-area.test.d.ts.map +1 -0
- package/dist/test/way-is-area.test.js +31 -0
- package/dist/test/way-is-area.test.js.map +1 -0
- package/dist/throttle.d.ts +25 -0
- package/dist/throttle.d.ts.map +1 -0
- package/dist/throttle.js +34 -0
- package/dist/throttle.js.map +1 -0
- package/dist/tile.d.ts +34 -0
- package/dist/tile.d.ts.map +1 -0
- package/dist/tile.js +72 -0
- package/dist/tile.js.map +1 -0
- package/dist/transform-bytes.d.ts +24 -0
- package/dist/transform-bytes.d.ts.map +1 -0
- package/dist/transform-bytes.js +28 -0
- package/dist/transform-bytes.js.map +1 -0
- package/dist/types.d.ts +99 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +30 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +70 -0
- package/dist/utils.js.map +1 -0
- package/dist/way-is-area.d.ts +24 -0
- package/dist/way-is-area.d.ts.map +1 -0
- package/dist/way-is-area.js +104 -0
- package/dist/way-is-area.js.map +1 -0
- package/dist/zigzag.d.ts +33 -0
- package/dist/zigzag.d.ts.map +1 -0
- package/dist/zigzag.js +40 -0
- package/dist/zigzag.js.map +1 -0
- package/package.json +11 -10
- package/src/assert.ts +21 -1
- package/src/bbox-intersects.ts +30 -0
- package/src/bytes-to-stream.ts +17 -0
- package/src/color.ts +37 -0
- package/src/coordinates.ts +45 -0
- package/src/haversine-distance.ts +10 -1
- package/src/progress.ts +74 -0
- package/src/relation-kind.ts +446 -0
- package/src/relation-multipolygon.ts +225 -0
- package/src/stream-to-bytes.ts +17 -0
- package/src/test/fixtures.ts +16 -12
- package/src/throttle.ts +37 -0
- package/src/tile.ts +89 -0
- package/src/transform-bytes.ts +23 -0
- package/src/types.ts +93 -1
- package/src/utils.ts +79 -0
- package/src/way-is-area.ts +107 -0
- package/src/zigzag.ts +42 -0
- package/test/haversine-distance.test.ts +2 -2
- package/test/relation-kind.test.ts +426 -0
- package/test/relation-multipolygon.test.ts +265 -0
- package/test/utils.test.ts +103 -0
- package/test/way-is-area.test.ts +42 -0
- package/tsconfig/test.json +1 -0
- package/tsconfig.build.json +5 -0
- package/dist/src/spherical-mercator.d.ts +0 -15
- package/dist/src/spherical-mercator.d.ts.map +0 -1
- package/dist/src/spherical-mercator.js +0 -35
- package/dist/src/spherical-mercator.js.map +0 -1
- package/dist/src/spherical-mercator.test.d.ts +0 -2
- package/dist/src/spherical-mercator.test.d.ts.map +0 -1
- package/dist/src/spherical-mercator.test.js +0 -25
- package/dist/src/spherical-mercator.test.js.map +0 -1
- package/src/spherical-mercator.test.ts +0 -42
- package/src/spherical-mercator.ts +0 -42
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Area detection for OSM ways.
|
|
3
|
+
*
|
|
4
|
+
* Implements the OSM wiki heuristics for determining whether a closed way
|
|
5
|
+
* should be rendered as an area (polygon) or a closed linear feature.
|
|
6
|
+
*
|
|
7
|
+
* @see https://wiki.openstreetmap.org/wiki/Key:area
|
|
8
|
+
* @see https://wiki.openstreetmap.org/wiki/Overpass_turbo/Polygon_Features
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { OsmWay } from "./types"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Tags that imply an area unless their value is exactly "no"
|
|
17
|
+
*/
|
|
18
|
+
const IMPLIED_ANY_VALUE_BUT_NO = new Set([
|
|
19
|
+
"amenity",
|
|
20
|
+
"boundary",
|
|
21
|
+
"building",
|
|
22
|
+
"building:part",
|
|
23
|
+
"craft",
|
|
24
|
+
// "golf", Seeing a lot of golf=cartpath which is not an area
|
|
25
|
+
"historic",
|
|
26
|
+
"indoor",
|
|
27
|
+
"landuse",
|
|
28
|
+
"leisure",
|
|
29
|
+
"military",
|
|
30
|
+
"office",
|
|
31
|
+
"place",
|
|
32
|
+
"public_transport",
|
|
33
|
+
"ruins",
|
|
34
|
+
"shop",
|
|
35
|
+
"tourism",
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Tags that imply an area only for these specific values
|
|
40
|
+
*/
|
|
41
|
+
const INCLUDED_VALUE_TAGS = {
|
|
42
|
+
barrier: new Set([
|
|
43
|
+
"city_wall",
|
|
44
|
+
"ditch",
|
|
45
|
+
"hedge",
|
|
46
|
+
"retaining_wall",
|
|
47
|
+
"wall",
|
|
48
|
+
"spikes",
|
|
49
|
+
]),
|
|
50
|
+
highway: new Set(["services", "rest_area", "escape", "elevator"]),
|
|
51
|
+
power: new Set(["plant", "substation", "generator", "transformer"]),
|
|
52
|
+
railway: new Set(["station", "turntable", "roundhouse", "platform"]),
|
|
53
|
+
waterway: new Set(["riverbank", "dock", "boatyard", "dam"]),
|
|
54
|
+
} as const
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Tags that imply an area unless the value is in this exclusion list
|
|
58
|
+
*/
|
|
59
|
+
const EXCLUDED_VALUE_TAGS = {
|
|
60
|
+
aeroway: new Set(["no", "taxiway"]),
|
|
61
|
+
"area:highway": new Set(["no"]),
|
|
62
|
+
man_made: new Set(["no", "cutline", "embankment", "pipeline"]),
|
|
63
|
+
natural: new Set(["no", "coastline", "cliff", "ridge", "arete", "tree_row"]),
|
|
64
|
+
} as const
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Determine if a way is an area based on its tags and nodes.
|
|
68
|
+
*
|
|
69
|
+
* This function implements the logic described in the OSM wiki:
|
|
70
|
+
* https://wiki.openstreetmap.org/wiki/Key:area
|
|
71
|
+
* https://wiki.openstreetmap.org/wiki/Overpass_turbo/Polygon_Features
|
|
72
|
+
*
|
|
73
|
+
* @param way - The way to check.
|
|
74
|
+
* @returns `true` if the way is an area, `false` otherwise.
|
|
75
|
+
*/
|
|
76
|
+
export function wayIsArea(way?: OsmWay): boolean {
|
|
77
|
+
if (!way) return false
|
|
78
|
+
const { refs, tags } = way
|
|
79
|
+
if (refs.length < 3) return false
|
|
80
|
+
if (refs[0] !== refs[refs.length - 1]) return false
|
|
81
|
+
|
|
82
|
+
// End refs are equal, no tags, assume it is an area.
|
|
83
|
+
if (!tags || Object.keys(tags).length === 0) return true
|
|
84
|
+
|
|
85
|
+
// Explicit override
|
|
86
|
+
if ("area" in tags) return tags["area"] !== "no"
|
|
87
|
+
|
|
88
|
+
// Tags that count if value is NOT "no"
|
|
89
|
+
for (const key of IMPLIED_ANY_VALUE_BUT_NO) {
|
|
90
|
+
const v = tags[key]
|
|
91
|
+
if (v && v !== "no") return true
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Tags that are area only for INCLUDED values
|
|
95
|
+
for (const [key, included] of Object.entries(INCLUDED_VALUE_TAGS)) {
|
|
96
|
+
const v = tags[key]
|
|
97
|
+
if (v && included.has(`${v}`)) return true
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Tags that are area unless value is excluded
|
|
101
|
+
for (const [key, excluded] of Object.entries(EXCLUDED_VALUE_TAGS)) {
|
|
102
|
+
const v = tags[key]
|
|
103
|
+
if (v && !excluded.has(`${v}`)) return true
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return false
|
|
107
|
+
}
|
package/src/zigzag.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zigzag encoding/decoding for protobuf-style varints.
|
|
3
|
+
*
|
|
4
|
+
* Zigzag encoding converts signed integers to unsigned by interleaving
|
|
5
|
+
* negative and positive values: 0, -1, 1, -2, 2, ... This allows efficient
|
|
6
|
+
* varint encoding of small negative numbers.
|
|
7
|
+
*
|
|
8
|
+
* @module
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Zigzag encode a number using arithmetic operations.
|
|
13
|
+
* This supports the full safe integer range (up to Number.MAX_SAFE_INTEGER).
|
|
14
|
+
* Formula: n < 0 ? -2*n - 1 : 2*n
|
|
15
|
+
*
|
|
16
|
+
* Used for encoding IDs in vector tiles to convert negative IDs to positive numbers
|
|
17
|
+
* for unsigned varint encoding.
|
|
18
|
+
*/
|
|
19
|
+
export function zigzag(num: number): number {
|
|
20
|
+
return num < 0 ? -2 * num - 1 : 2 * num
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Zigzag encode using bitwise operations (for geometry deltas only).
|
|
25
|
+
* This is faster but limited to 32-bit signed integers.
|
|
26
|
+
* Used for small coordinate deltas in geometry encoding.
|
|
27
|
+
*/
|
|
28
|
+
export function zigzag32(num: number): number {
|
|
29
|
+
return (num << 1) ^ (num >> 31)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Decode zigzag-encoded number back to original value.
|
|
34
|
+
* Zigzag encoding is used to convert negative IDs to positive numbers for unsigned varint
|
|
35
|
+
* encoding in vector tiles. Uses arithmetic-based decoding to support the full safe integer range.
|
|
36
|
+
*
|
|
37
|
+
* Formula: (encoded & 1) === 1 ? -(encoded + 1) / 2 : encoded / 2
|
|
38
|
+
*/
|
|
39
|
+
export function decodeZigzag(encoded: number): number {
|
|
40
|
+
// Check if encoded is odd (negative) using bitwise, then use arithmetic
|
|
41
|
+
return (encoded & 1) === 1 ? -(encoded + 1) / 2 : encoded / 2
|
|
42
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { expect, test } from "bun:test"
|
|
2
2
|
import { haversineDistance } from "../src/haversine-distance"
|
|
3
3
|
|
|
4
4
|
test("haversineDistance", () => {
|
|
5
5
|
const p1: [number, number] = [-75.343, 39.984]
|
|
6
6
|
const p2: [number, number] = [-75.534, 39.123]
|
|
7
|
-
|
|
7
|
+
expect(haversineDistance(p1, p2)).toBeCloseTo(97129.2211, 3)
|
|
8
8
|
})
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test"
|
|
2
|
+
import {
|
|
3
|
+
buildRelationLineStrings,
|
|
4
|
+
collectRelationPoints,
|
|
5
|
+
getRelationKind,
|
|
6
|
+
getRelationKindMetadata,
|
|
7
|
+
isAreaRelation,
|
|
8
|
+
isLineRelation,
|
|
9
|
+
isLogicRelation,
|
|
10
|
+
isPointRelation,
|
|
11
|
+
isSuperRelation,
|
|
12
|
+
resolveRelationMembers,
|
|
13
|
+
} from "../src/relation-kind"
|
|
14
|
+
import type { LonLat, OsmRelation, OsmWay } from "../src/types"
|
|
15
|
+
|
|
16
|
+
describe("relation-kind", () => {
|
|
17
|
+
describe("getRelationKind", () => {
|
|
18
|
+
it("identifies multipolygon as area", () => {
|
|
19
|
+
const relation: OsmRelation = {
|
|
20
|
+
id: 1,
|
|
21
|
+
tags: { type: "multipolygon" },
|
|
22
|
+
members: [],
|
|
23
|
+
}
|
|
24
|
+
expect(getRelationKind(relation)).toBe("area")
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("identifies boundary as area", () => {
|
|
28
|
+
const relation: OsmRelation = {
|
|
29
|
+
id: 1,
|
|
30
|
+
tags: { type: "boundary" },
|
|
31
|
+
members: [],
|
|
32
|
+
}
|
|
33
|
+
expect(getRelationKind(relation)).toBe("area")
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("identifies route as line", () => {
|
|
37
|
+
const relation: OsmRelation = {
|
|
38
|
+
id: 1,
|
|
39
|
+
tags: { type: "route" },
|
|
40
|
+
members: [],
|
|
41
|
+
}
|
|
42
|
+
expect(getRelationKind(relation)).toBe("line")
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("identifies multilinestring as line", () => {
|
|
46
|
+
const relation: OsmRelation = {
|
|
47
|
+
id: 1,
|
|
48
|
+
tags: { type: "multilinestring" },
|
|
49
|
+
members: [],
|
|
50
|
+
}
|
|
51
|
+
expect(getRelationKind(relation)).toBe("line")
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it("identifies multipoint as point", () => {
|
|
55
|
+
const relation: OsmRelation = {
|
|
56
|
+
id: 1,
|
|
57
|
+
tags: { type: "multipoint" },
|
|
58
|
+
members: [],
|
|
59
|
+
}
|
|
60
|
+
expect(getRelationKind(relation)).toBe("point")
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("identifies restriction as logic", () => {
|
|
64
|
+
const relation: OsmRelation = {
|
|
65
|
+
id: 1,
|
|
66
|
+
tags: { type: "restriction" },
|
|
67
|
+
members: [],
|
|
68
|
+
}
|
|
69
|
+
expect(getRelationKind(relation)).toBe("logic")
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it("identifies super-relation by having relation members", () => {
|
|
73
|
+
const relation: OsmRelation = {
|
|
74
|
+
id: 1,
|
|
75
|
+
tags: { type: "collection" },
|
|
76
|
+
members: [{ type: "relation", ref: 2 }],
|
|
77
|
+
}
|
|
78
|
+
expect(getRelationKind(relation)).toBe("super")
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it("defaults to logic for untyped relations", () => {
|
|
82
|
+
const relation: OsmRelation = {
|
|
83
|
+
id: 1,
|
|
84
|
+
members: [],
|
|
85
|
+
}
|
|
86
|
+
expect(getRelationKind(relation)).toBe("logic")
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it("handles case-insensitive type tags", () => {
|
|
90
|
+
const relation: OsmRelation = {
|
|
91
|
+
id: 1,
|
|
92
|
+
tags: { type: "MULTIPOLYGON" },
|
|
93
|
+
members: [],
|
|
94
|
+
}
|
|
95
|
+
expect(getRelationKind(relation)).toBe("area")
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe("getRelationKindMetadata", () => {
|
|
100
|
+
it("returns correct metadata for area relations", () => {
|
|
101
|
+
const relation: OsmRelation = {
|
|
102
|
+
id: 1,
|
|
103
|
+
tags: { type: "multipolygon" },
|
|
104
|
+
members: [],
|
|
105
|
+
}
|
|
106
|
+
const metadata = getRelationKindMetadata(relation)
|
|
107
|
+
expect(metadata.kind).toBe("area")
|
|
108
|
+
expect(metadata.expectedRoles).toEqual(["outer", "inner"])
|
|
109
|
+
expect(metadata.orderMatters).toBe(false)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it("returns correct metadata for line relations", () => {
|
|
113
|
+
const relation: OsmRelation = {
|
|
114
|
+
id: 1,
|
|
115
|
+
tags: { type: "route" },
|
|
116
|
+
members: [],
|
|
117
|
+
}
|
|
118
|
+
const metadata = getRelationKindMetadata(relation)
|
|
119
|
+
expect(metadata.kind).toBe("line")
|
|
120
|
+
expect(metadata.orderMatters).toBe(true)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe("isAreaRelation", () => {
|
|
125
|
+
it("returns true for multipolygon", () => {
|
|
126
|
+
const relation: OsmRelation = {
|
|
127
|
+
id: 1,
|
|
128
|
+
tags: { type: "multipolygon" },
|
|
129
|
+
members: [],
|
|
130
|
+
}
|
|
131
|
+
expect(isAreaRelation(relation)).toBe(true)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it("returns false for route", () => {
|
|
135
|
+
const relation: OsmRelation = {
|
|
136
|
+
id: 1,
|
|
137
|
+
tags: { type: "route" },
|
|
138
|
+
members: [],
|
|
139
|
+
}
|
|
140
|
+
expect(isAreaRelation(relation)).toBe(false)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe("isLineRelation", () => {
|
|
145
|
+
it("returns true for route", () => {
|
|
146
|
+
const relation: OsmRelation = {
|
|
147
|
+
id: 1,
|
|
148
|
+
tags: { type: "route" },
|
|
149
|
+
members: [],
|
|
150
|
+
}
|
|
151
|
+
expect(isLineRelation(relation)).toBe(true)
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it("returns true for multilinestring", () => {
|
|
155
|
+
const relation: OsmRelation = {
|
|
156
|
+
id: 1,
|
|
157
|
+
tags: { type: "multilinestring" },
|
|
158
|
+
members: [],
|
|
159
|
+
}
|
|
160
|
+
expect(isLineRelation(relation)).toBe(true)
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
describe("isPointRelation", () => {
|
|
165
|
+
it("returns true for multipoint", () => {
|
|
166
|
+
const relation: OsmRelation = {
|
|
167
|
+
id: 1,
|
|
168
|
+
tags: { type: "multipoint" },
|
|
169
|
+
members: [],
|
|
170
|
+
}
|
|
171
|
+
expect(isPointRelation(relation)).toBe(true)
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe("isSuperRelation", () => {
|
|
176
|
+
it("returns true for relation with relation members", () => {
|
|
177
|
+
const relation: OsmRelation = {
|
|
178
|
+
id: 1,
|
|
179
|
+
tags: { type: "collection" },
|
|
180
|
+
members: [{ type: "relation", ref: 2 }],
|
|
181
|
+
}
|
|
182
|
+
expect(isSuperRelation(relation)).toBe(true)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it("returns false for relation without relation members", () => {
|
|
186
|
+
const relation: OsmRelation = {
|
|
187
|
+
id: 1,
|
|
188
|
+
tags: { type: "route" },
|
|
189
|
+
members: [{ type: "way", ref: 2 }],
|
|
190
|
+
}
|
|
191
|
+
expect(isSuperRelation(relation)).toBe(false)
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
describe("isLogicRelation", () => {
|
|
196
|
+
it("returns true for restriction", () => {
|
|
197
|
+
const relation: OsmRelation = {
|
|
198
|
+
id: 1,
|
|
199
|
+
tags: { type: "restriction" },
|
|
200
|
+
members: [],
|
|
201
|
+
}
|
|
202
|
+
expect(isLogicRelation(relation)).toBe(true)
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
describe("buildRelationLineStrings", () => {
|
|
207
|
+
it("builds linestrings from connected ways", () => {
|
|
208
|
+
const relation: OsmRelation = {
|
|
209
|
+
id: 1,
|
|
210
|
+
tags: { type: "route" },
|
|
211
|
+
members: [
|
|
212
|
+
{ type: "way", ref: 1 },
|
|
213
|
+
{ type: "way", ref: 2 },
|
|
214
|
+
],
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const way1: OsmWay = { id: 1, refs: [1, 2] }
|
|
218
|
+
const way2: OsmWay = { id: 2, refs: [2, 3] }
|
|
219
|
+
|
|
220
|
+
const getWay = (id: number) => {
|
|
221
|
+
if (id === 1) return way1
|
|
222
|
+
if (id === 2) return way2
|
|
223
|
+
return null
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const getNodeCoordinates = (id: number) => {
|
|
227
|
+
const coords: Record<number, [number, number]> = {
|
|
228
|
+
1: [0.0, 0.0],
|
|
229
|
+
2: [1.0, 0.0],
|
|
230
|
+
3: [2.0, 0.0],
|
|
231
|
+
}
|
|
232
|
+
return coords[id]
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const lineStrings = buildRelationLineStrings(
|
|
236
|
+
relation,
|
|
237
|
+
getWay,
|
|
238
|
+
getNodeCoordinates,
|
|
239
|
+
)
|
|
240
|
+
expect(lineStrings).toHaveLength(1)
|
|
241
|
+
expect(lineStrings[0]).toHaveLength(3)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
it("handles disconnected ways as separate linestrings", () => {
|
|
245
|
+
const relation: OsmRelation = {
|
|
246
|
+
id: 1,
|
|
247
|
+
tags: { type: "route" },
|
|
248
|
+
members: [
|
|
249
|
+
{ type: "way", ref: 1 },
|
|
250
|
+
{ type: "way", ref: 2 },
|
|
251
|
+
],
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const way1: OsmWay = { id: 1, refs: [1, 2] }
|
|
255
|
+
const way2: OsmWay = { id: 2, refs: [3, 4] }
|
|
256
|
+
|
|
257
|
+
const getWay = (id: number) => {
|
|
258
|
+
if (id === 1) return way1
|
|
259
|
+
if (id === 2) return way2
|
|
260
|
+
return null
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const getNodeCoordinates = (id: number) => {
|
|
264
|
+
const coords: Record<number, [number, number]> = {
|
|
265
|
+
1: [0.0, 0.0],
|
|
266
|
+
2: [1.0, 0.0],
|
|
267
|
+
3: [10.0, 10.0],
|
|
268
|
+
4: [11.0, 10.0],
|
|
269
|
+
}
|
|
270
|
+
return coords[id]
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const lineStrings = buildRelationLineStrings(
|
|
274
|
+
relation,
|
|
275
|
+
getWay,
|
|
276
|
+
getNodeCoordinates,
|
|
277
|
+
)
|
|
278
|
+
expect(lineStrings.length).toBeGreaterThanOrEqual(2)
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
describe("collectRelationPoints", () => {
|
|
283
|
+
it("collects point coordinates from node members", () => {
|
|
284
|
+
const relation: OsmRelation = {
|
|
285
|
+
id: 1,
|
|
286
|
+
tags: { type: "multipoint" },
|
|
287
|
+
members: [
|
|
288
|
+
{ type: "node", ref: 1 },
|
|
289
|
+
{ type: "node", ref: 2 },
|
|
290
|
+
{ type: "way", ref: 10 }, // Should be ignored
|
|
291
|
+
],
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const getNodeCoordinates = (id: number) => {
|
|
295
|
+
const coords: Record<number, [number, number]> = {
|
|
296
|
+
1: [0.0, 0.0],
|
|
297
|
+
2: [1.0, 1.0],
|
|
298
|
+
}
|
|
299
|
+
return coords[id]
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const points = collectRelationPoints(relation, getNodeCoordinates)
|
|
303
|
+
expect(points).toHaveLength(2)
|
|
304
|
+
expect(points[0]).toEqual([0.0, 0.0])
|
|
305
|
+
expect(points[1]).toEqual([1.0, 1.0])
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it("handles missing node coordinates", () => {
|
|
309
|
+
const relation: OsmRelation = {
|
|
310
|
+
id: 1,
|
|
311
|
+
tags: { type: "multipoint" },
|
|
312
|
+
members: [
|
|
313
|
+
{ type: "node", ref: 1 },
|
|
314
|
+
{ type: "node", ref: 2 },
|
|
315
|
+
],
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const getNodeCoordinates = (id: number): LonLat | undefined => {
|
|
319
|
+
if (id === 1) return [0.0, 0.0] as LonLat
|
|
320
|
+
return undefined // Missing coordinate
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const points = collectRelationPoints(relation, getNodeCoordinates)
|
|
324
|
+
expect(points).toHaveLength(1)
|
|
325
|
+
})
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
describe("resolveRelationMembers", () => {
|
|
329
|
+
it("resolves direct members", () => {
|
|
330
|
+
const relation: OsmRelation = {
|
|
331
|
+
id: 1,
|
|
332
|
+
tags: { type: "collection" },
|
|
333
|
+
members: [
|
|
334
|
+
{ type: "node", ref: 1 },
|
|
335
|
+
{ type: "way", ref: 10 },
|
|
336
|
+
],
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const getRelation = () => null
|
|
340
|
+
|
|
341
|
+
const resolved = resolveRelationMembers(relation, getRelation)
|
|
342
|
+
expect(resolved.nodes).toEqual([1])
|
|
343
|
+
expect(resolved.ways).toEqual([10])
|
|
344
|
+
expect(resolved.relations).toEqual([])
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
it("resolves nested relation members", () => {
|
|
348
|
+
const relation1: OsmRelation = {
|
|
349
|
+
id: 1,
|
|
350
|
+
tags: { type: "collection" },
|
|
351
|
+
members: [
|
|
352
|
+
{ type: "relation", ref: 2 },
|
|
353
|
+
{ type: "node", ref: 1 },
|
|
354
|
+
],
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const relation2: OsmRelation = {
|
|
358
|
+
id: 2,
|
|
359
|
+
tags: { type: "collection" },
|
|
360
|
+
members: [
|
|
361
|
+
{ type: "way", ref: 10 },
|
|
362
|
+
{ type: "node", ref: 2 },
|
|
363
|
+
],
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const getRelation = (id: number) => {
|
|
367
|
+
if (id === 2) return relation2
|
|
368
|
+
return null
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const resolved = resolveRelationMembers(relation1, getRelation)
|
|
372
|
+
expect(resolved.nodes).toContain(1)
|
|
373
|
+
expect(resolved.nodes).toContain(2)
|
|
374
|
+
expect(resolved.ways).toContain(10)
|
|
375
|
+
expect(resolved.relations).toContain(2)
|
|
376
|
+
})
|
|
377
|
+
|
|
378
|
+
it("detects cycles and prevents infinite recursion", () => {
|
|
379
|
+
const relation1: OsmRelation = {
|
|
380
|
+
id: 1,
|
|
381
|
+
tags: { type: "collection" },
|
|
382
|
+
members: [{ type: "relation", ref: 2 }],
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const relation2: OsmRelation = {
|
|
386
|
+
id: 2,
|
|
387
|
+
tags: { type: "collection" },
|
|
388
|
+
members: [{ type: "relation", ref: 1 }], // Circular reference
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const getRelation = (id: number) => {
|
|
392
|
+
if (id === 1) return relation1
|
|
393
|
+
if (id === 2) return relation2
|
|
394
|
+
return null
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const resolved = resolveRelationMembers(relation1, getRelation, 10)
|
|
398
|
+
// Should not crash and should include relation2 but not recurse infinitely
|
|
399
|
+
expect(resolved.relations).toContain(2)
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it("respects maxDepth limit", () => {
|
|
403
|
+
const relation1: OsmRelation = {
|
|
404
|
+
id: 1,
|
|
405
|
+
tags: { type: "collection" },
|
|
406
|
+
members: [{ type: "relation", ref: 2 }],
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const relation2: OsmRelation = {
|
|
410
|
+
id: 2,
|
|
411
|
+
tags: { type: "collection" },
|
|
412
|
+
members: [{ type: "node", ref: 100 }],
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const getRelation = (id: number) => {
|
|
416
|
+
if (id === 1) return relation1
|
|
417
|
+
if (id === 2) return relation2
|
|
418
|
+
return null
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const resolved = resolveRelationMembers(relation1, getRelation, 0)
|
|
422
|
+
// Should not resolve nested relation when maxDepth is 0
|
|
423
|
+
expect(resolved.nodes).not.toContain(100)
|
|
424
|
+
})
|
|
425
|
+
})
|
|
426
|
+
})
|