@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.
Files changed (208) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +49 -19
  3. package/dist/assert.d.ts +24 -0
  4. package/dist/assert.d.ts.map +1 -0
  5. package/dist/assert.js +28 -0
  6. package/dist/assert.js.map +1 -0
  7. package/dist/bbox-intersects.d.ts +15 -0
  8. package/dist/bbox-intersects.d.ts.map +1 -0
  9. package/dist/bbox-intersects.js +24 -0
  10. package/dist/bbox-intersects.js.map +1 -0
  11. package/dist/bytes-to-stream.d.ts +18 -0
  12. package/dist/bytes-to-stream.d.ts.map +1 -0
  13. package/dist/bytes-to-stream.js +25 -0
  14. package/dist/bytes-to-stream.js.map +1 -0
  15. package/dist/color.d.ts +4 -0
  16. package/dist/color.d.ts.map +1 -0
  17. package/dist/color.js +33 -0
  18. package/dist/color.js.map +1 -0
  19. package/dist/concat-bytes.d.ts +5 -0
  20. package/dist/concat-bytes.d.ts.map +1 -0
  21. package/dist/concat-bytes.js +14 -0
  22. package/dist/concat-bytes.js.map +1 -0
  23. package/dist/coordinates.d.ts +28 -0
  24. package/dist/coordinates.d.ts.map +1 -0
  25. package/dist/coordinates.js +38 -0
  26. package/dist/coordinates.js.map +1 -0
  27. package/dist/haversine-distance.d.ts +16 -0
  28. package/dist/haversine-distance.d.ts.map +1 -0
  29. package/dist/haversine-distance.js +26 -0
  30. package/dist/haversine-distance.js.map +1 -0
  31. package/dist/lineclip.d.ts +2 -0
  32. package/dist/lineclip.d.ts.map +1 -0
  33. package/dist/lineclip.js +3 -0
  34. package/dist/lineclip.js.map +1 -0
  35. package/dist/progress.d.ts +42 -0
  36. package/dist/progress.d.ts.map +1 -0
  37. package/dist/progress.js +53 -0
  38. package/dist/progress.js.map +1 -0
  39. package/dist/relation-kind.d.ts +69 -0
  40. package/dist/relation-kind.d.ts.map +1 -0
  41. package/dist/relation-kind.js +375 -0
  42. package/dist/relation-kind.js.map +1 -0
  43. package/dist/relation-multipolygon.d.ts +43 -0
  44. package/dist/relation-multipolygon.d.ts.map +1 -0
  45. package/dist/relation-multipolygon.js +195 -0
  46. package/dist/relation-multipolygon.js.map +1 -0
  47. package/dist/src/assert.d.ts +20 -1
  48. package/dist/src/assert.d.ts.map +1 -1
  49. package/dist/src/assert.js +20 -1
  50. package/dist/src/assert.js.map +1 -1
  51. package/dist/src/bbox-intersects.d.ts +15 -0
  52. package/dist/src/bbox-intersects.d.ts.map +1 -0
  53. package/dist/src/bbox-intersects.js +24 -0
  54. package/dist/src/bbox-intersects.js.map +1 -0
  55. package/dist/src/bytes-to-stream.d.ts +17 -1
  56. package/dist/src/bytes-to-stream.d.ts.map +1 -1
  57. package/dist/src/bytes-to-stream.js +16 -0
  58. package/dist/src/bytes-to-stream.js.map +1 -1
  59. package/dist/src/color.d.ts +4 -0
  60. package/dist/src/color.d.ts.map +1 -0
  61. package/dist/src/color.js +33 -0
  62. package/dist/src/color.js.map +1 -0
  63. package/dist/src/coordinates.d.ts +28 -0
  64. package/dist/src/coordinates.d.ts.map +1 -0
  65. package/dist/src/coordinates.js +38 -0
  66. package/dist/src/coordinates.js.map +1 -0
  67. package/dist/src/haversine-distance.d.ts +9 -1
  68. package/dist/src/haversine-distance.d.ts.map +1 -1
  69. package/dist/src/haversine-distance.js +9 -1
  70. package/dist/src/haversine-distance.js.map +1 -1
  71. package/dist/src/progress.d.ts +42 -0
  72. package/dist/src/progress.d.ts.map +1 -0
  73. package/dist/src/progress.js +53 -0
  74. package/dist/src/progress.js.map +1 -0
  75. package/dist/src/relation-kind.d.ts +69 -0
  76. package/dist/src/relation-kind.d.ts.map +1 -0
  77. package/dist/src/relation-kind.js +375 -0
  78. package/dist/src/relation-kind.js.map +1 -0
  79. package/dist/src/relation-multipolygon.d.ts +43 -0
  80. package/dist/src/relation-multipolygon.d.ts.map +1 -0
  81. package/dist/src/relation-multipolygon.js +195 -0
  82. package/dist/src/relation-multipolygon.js.map +1 -0
  83. package/dist/src/stream-to-bytes.d.ts +16 -0
  84. package/dist/src/stream-to-bytes.d.ts.map +1 -1
  85. package/dist/src/stream-to-bytes.js +16 -0
  86. package/dist/src/stream-to-bytes.js.map +1 -1
  87. package/dist/src/test/fixtures.d.ts +1 -1
  88. package/dist/src/test/fixtures.d.ts.map +1 -1
  89. package/dist/src/test/fixtures.js +16 -8
  90. package/dist/src/test/fixtures.js.map +1 -1
  91. package/dist/src/throttle.d.ts +25 -0
  92. package/dist/src/throttle.d.ts.map +1 -0
  93. package/dist/src/throttle.js +34 -0
  94. package/dist/src/throttle.js.map +1 -0
  95. package/dist/src/tile.d.ts +34 -0
  96. package/dist/src/tile.d.ts.map +1 -0
  97. package/dist/src/tile.js +72 -0
  98. package/dist/src/tile.js.map +1 -0
  99. package/dist/src/transform-bytes.d.ts +22 -0
  100. package/dist/src/transform-bytes.d.ts.map +1 -1
  101. package/dist/src/transform-bytes.js +22 -0
  102. package/dist/src/transform-bytes.js.map +1 -1
  103. package/dist/src/types.d.ts +76 -1
  104. package/dist/src/types.d.ts.map +1 -1
  105. package/dist/src/types.js +8 -0
  106. package/dist/src/types.js.map +1 -1
  107. package/dist/src/utils.d.ts +30 -0
  108. package/dist/src/utils.d.ts.map +1 -0
  109. package/dist/src/utils.js +70 -0
  110. package/dist/src/utils.js.map +1 -0
  111. package/dist/src/way-is-area.d.ts +24 -0
  112. package/dist/src/way-is-area.d.ts.map +1 -0
  113. package/dist/src/way-is-area.js +104 -0
  114. package/dist/src/way-is-area.js.map +1 -0
  115. package/dist/src/zigzag.d.ts +33 -0
  116. package/dist/src/zigzag.d.ts.map +1 -0
  117. package/dist/src/zigzag.js +40 -0
  118. package/dist/src/zigzag.js.map +1 -0
  119. package/dist/stream-to-bytes.d.ts +18 -0
  120. package/dist/stream-to-bytes.d.ts.map +1 -0
  121. package/dist/stream-to-bytes.js +30 -0
  122. package/dist/stream-to-bytes.js.map +1 -0
  123. package/dist/test/fixtures.d.ts +36 -0
  124. package/dist/test/fixtures.d.ts.map +1 -0
  125. package/dist/test/fixtures.js +175 -0
  126. package/dist/test/fixtures.js.map +1 -0
  127. package/dist/test/haversine-distance.test.js +2 -2
  128. package/dist/test/haversine-distance.test.js.map +1 -1
  129. package/dist/test/relation-kind.test.d.ts +2 -0
  130. package/dist/test/relation-kind.test.d.ts.map +1 -0
  131. package/dist/test/relation-kind.test.js +367 -0
  132. package/dist/test/relation-kind.test.js.map +1 -0
  133. package/dist/test/relation-multipolygon.test.d.ts +2 -0
  134. package/dist/test/relation-multipolygon.test.d.ts.map +1 -0
  135. package/dist/test/relation-multipolygon.test.js +237 -0
  136. package/dist/test/relation-multipolygon.test.js.map +1 -0
  137. package/dist/test/utils.test.d.ts +2 -0
  138. package/dist/test/utils.test.d.ts.map +1 -0
  139. package/dist/test/utils.test.js +76 -0
  140. package/dist/test/utils.test.js.map +1 -0
  141. package/dist/test/way-is-area.test.d.ts +2 -0
  142. package/dist/test/way-is-area.test.d.ts.map +1 -0
  143. package/dist/test/way-is-area.test.js +31 -0
  144. package/dist/test/way-is-area.test.js.map +1 -0
  145. package/dist/throttle.d.ts +25 -0
  146. package/dist/throttle.d.ts.map +1 -0
  147. package/dist/throttle.js +34 -0
  148. package/dist/throttle.js.map +1 -0
  149. package/dist/tile.d.ts +34 -0
  150. package/dist/tile.d.ts.map +1 -0
  151. package/dist/tile.js +72 -0
  152. package/dist/tile.js.map +1 -0
  153. package/dist/transform-bytes.d.ts +24 -0
  154. package/dist/transform-bytes.d.ts.map +1 -0
  155. package/dist/transform-bytes.js +28 -0
  156. package/dist/transform-bytes.js.map +1 -0
  157. package/dist/types.d.ts +99 -0
  158. package/dist/types.d.ts.map +1 -0
  159. package/dist/types.js +9 -0
  160. package/dist/types.js.map +1 -0
  161. package/dist/utils.d.ts +30 -0
  162. package/dist/utils.d.ts.map +1 -0
  163. package/dist/utils.js +70 -0
  164. package/dist/utils.js.map +1 -0
  165. package/dist/way-is-area.d.ts +24 -0
  166. package/dist/way-is-area.d.ts.map +1 -0
  167. package/dist/way-is-area.js +104 -0
  168. package/dist/way-is-area.js.map +1 -0
  169. package/dist/zigzag.d.ts +33 -0
  170. package/dist/zigzag.d.ts.map +1 -0
  171. package/dist/zigzag.js +40 -0
  172. package/dist/zigzag.js.map +1 -0
  173. package/package.json +11 -10
  174. package/src/assert.ts +21 -1
  175. package/src/bbox-intersects.ts +30 -0
  176. package/src/bytes-to-stream.ts +17 -0
  177. package/src/color.ts +37 -0
  178. package/src/coordinates.ts +45 -0
  179. package/src/haversine-distance.ts +10 -1
  180. package/src/progress.ts +74 -0
  181. package/src/relation-kind.ts +446 -0
  182. package/src/relation-multipolygon.ts +225 -0
  183. package/src/stream-to-bytes.ts +17 -0
  184. package/src/test/fixtures.ts +16 -12
  185. package/src/throttle.ts +37 -0
  186. package/src/tile.ts +89 -0
  187. package/src/transform-bytes.ts +23 -0
  188. package/src/types.ts +93 -1
  189. package/src/utils.ts +79 -0
  190. package/src/way-is-area.ts +107 -0
  191. package/src/zigzag.ts +42 -0
  192. package/test/haversine-distance.test.ts +2 -2
  193. package/test/relation-kind.test.ts +426 -0
  194. package/test/relation-multipolygon.test.ts +265 -0
  195. package/test/utils.test.ts +103 -0
  196. package/test/way-is-area.test.ts +42 -0
  197. package/tsconfig/test.json +1 -0
  198. package/tsconfig.build.json +5 -0
  199. package/dist/src/spherical-mercator.d.ts +0 -15
  200. package/dist/src/spherical-mercator.d.ts.map +0 -1
  201. package/dist/src/spherical-mercator.js +0 -35
  202. package/dist/src/spherical-mercator.js.map +0 -1
  203. package/dist/src/spherical-mercator.test.d.ts +0 -2
  204. package/dist/src/spherical-mercator.test.d.ts.map +0 -1
  205. package/dist/src/spherical-mercator.test.js +0 -25
  206. package/dist/src/spherical-mercator.test.js.map +0 -1
  207. package/src/spherical-mercator.test.ts +0 -42
  208. package/src/spherical-mercator.ts +0 -42
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Multipolygon relation building utilities.
3
+ *
4
+ * Implements the OSM multipolygon relation specification to construct
5
+ * polygon geometries from way members. Handles outer/inner roles,
6
+ * way connection, and ring closure.
7
+ *
8
+ * @see https://wiki.openstreetmap.org/wiki/Relation:multipolygon
9
+ *
10
+ * @module
11
+ */
12
+
13
+ import type { LonLat, OsmRelation, OsmRelationMember, OsmWay } from "./types"
14
+
15
+ /**
16
+ * Get way members from a relation, grouped by role (outer/inner).
17
+ */
18
+ export function getWayMembersByRole(relation: OsmRelation): {
19
+ outer: OsmRelationMember[]
20
+ inner: OsmRelationMember[]
21
+ } {
22
+ const outer: OsmRelationMember[] = []
23
+ const inner: OsmRelationMember[] = []
24
+
25
+ for (const member of relation.members) {
26
+ if (member.type !== "way") continue
27
+ const role = member.role?.toLowerCase() ?? ""
28
+ if (role === "outer") {
29
+ outer.push(member)
30
+ } else if (role === "inner") {
31
+ inner.push(member)
32
+ }
33
+ }
34
+
35
+ return { outer, inner }
36
+ }
37
+
38
+ /**
39
+ * Connect ways that share endpoints to form a continuous ring.
40
+ * Returns an array of rings (each ring is an array of way IDs in order).
41
+ *
42
+ * This function handles:
43
+ * - Ways that are reversed (end matches end, or start matches start).
44
+ * - Closed ways (single ways that form a ring).
45
+ * - Disconnected chains (multiple independent rings).
46
+ */
47
+ export function connectWaysToRings(wayMembers: OsmWay[]): OsmWay[][] {
48
+ if (wayMembers.length === 0) return []
49
+
50
+ const rings: OsmWay[][] = []
51
+ const used = new Set<number>()
52
+ const wayMap = new Map<number, OsmWay>()
53
+
54
+ // Build map of way ID to member
55
+ for (const member of wayMembers) {
56
+ wayMap.set(member.id, member)
57
+ }
58
+
59
+ // Helper to reverse a way's refs
60
+ const reverseWay = (way: OsmWay): OsmWay => ({
61
+ ...way,
62
+ refs: [...way.refs].reverse(),
63
+ })
64
+
65
+ // Build rings by connecting ways
66
+ for (const startWay of wayMembers) {
67
+ if (used.has(startWay.id)) continue
68
+ if (startWay.refs.length < 2) throw Error("Way has less than 2 refs")
69
+
70
+ const ring: OsmWay[] = [startWay]
71
+ used.add(startWay.id)
72
+
73
+ let currentStart = startWay.refs[0]!
74
+ let currentEnd = startWay.refs[startWay.refs.length - 1]!
75
+
76
+ // Try to extend the ring forward
77
+ while (true) {
78
+ let found = false
79
+ for (const nextWay of wayMembers) {
80
+ if (used.has(nextWay.id)) continue
81
+ if (nextWay.refs.length < 2) throw Error("Way has less than 2 refs")
82
+ const nextStart = nextWay.refs[0]!
83
+ const nextEnd = nextWay.refs[nextWay.refs.length - 1]!
84
+
85
+ // Check if next way connects to current end
86
+ if (currentEnd === nextStart) {
87
+ ring.push(nextWay)
88
+ used.add(nextWay.id)
89
+ currentEnd = nextEnd
90
+ found = true
91
+ break
92
+ }
93
+ if (currentEnd === nextEnd) {
94
+ // Need to reverse next way
95
+ ring.push(reverseWay(nextWay))
96
+ used.add(nextWay.id)
97
+ currentEnd = nextStart
98
+ found = true
99
+ break
100
+ }
101
+ }
102
+
103
+ if (!found) break
104
+ }
105
+
106
+ // Try to extend the ring backward
107
+ currentStart = startWay.refs[0]!
108
+ currentEnd = startWay.refs[startWay.refs.length - 1]!
109
+
110
+ while (true) {
111
+ let found = false
112
+ for (const nextWay of wayMembers) {
113
+ if (used.has(nextWay.id)) continue
114
+ if (nextWay.refs.length < 2) throw Error("Way has less than 2 refs")
115
+
116
+ const nextStart = nextWay.refs[0]!
117
+ const nextEnd = nextWay.refs[nextWay.refs.length - 1]!
118
+
119
+ // Check if next way connects to current start
120
+ if (currentStart === nextEnd) {
121
+ ring.unshift(nextWay)
122
+ used.add(nextWay.id)
123
+ currentStart = nextStart
124
+ found = true
125
+ break
126
+ }
127
+ if (currentStart === nextStart) {
128
+ // Need to reverse next way
129
+ ring.unshift(reverseWay(nextWay))
130
+ used.add(nextWay.id)
131
+ currentStart = nextEnd
132
+ found = true
133
+ break
134
+ }
135
+ }
136
+
137
+ if (!found) break
138
+ }
139
+
140
+ // Only add ring if it's closed (first and last node are the same)
141
+ if (ring.length > 0) {
142
+ const firstWay = ring[0]
143
+ const lastWay = ring[ring.length - 1]
144
+ if (firstWay?.refs[0] === lastWay?.refs[lastWay.refs.length - 1]) {
145
+ rings.push(ring)
146
+ }
147
+ }
148
+ }
149
+
150
+ return rings
151
+ }
152
+
153
+ /**
154
+ * Build polygon rings from way members of a relation.
155
+ * Returns an array where each element is an array of coordinate rings (outer + inner).
156
+ *
157
+ * Based on OSM multipolygon relation specification:
158
+ * https://wiki.openstreetmap.org/wiki/Relation:multipolygon
159
+ *
160
+ * This implementation connects way members into closed rings, and then groups them
161
+ * into polygons. Currently, it associates all inner rings with every outer ring
162
+ * found in the relation, which is a simplification. A robust implementation would
163
+ * use point-in-polygon checks to strictly nest holes inside their parent outer ring.
164
+ */
165
+ export function buildRelationRings(
166
+ relation: OsmRelation,
167
+ getWay: (wayId: number) => OsmWay | null,
168
+ getNodeCoordinates: (nodeId: number) => LonLat | undefined,
169
+ ): LonLat[][][] {
170
+ const { outer, inner } = getWayMembersByRole(relation)
171
+
172
+ // Connect outer ways into rings
173
+ const outerRings = connectWaysToRings(
174
+ outer.map((m) => getWay(m.ref)).filter((w) => w !== null),
175
+ )
176
+ // Connect inner ways into rings
177
+ const innerRings = connectWaysToRings(
178
+ inner.map((m) => getWay(m.ref)).filter((w) => w !== null),
179
+ )
180
+
181
+ const wayRingToCoords = (ring: OsmWay[]): LonLat[] => {
182
+ const coords: LonLat[] = []
183
+ for (const way of ring) {
184
+ for (const nodeId of way.refs) {
185
+ const coord = getNodeCoordinates(nodeId)
186
+ if (coord) coords.push(coord)
187
+ }
188
+ }
189
+
190
+ // Ensure ring is closed
191
+ if (coords.length > 0) {
192
+ const first = coords[0]
193
+ const last = coords[coords.length - 1]
194
+ if (first && last && (first[0] !== last[0] || first[1] !== last[1])) {
195
+ coords.push([first[0], first[1]])
196
+ }
197
+ }
198
+ return coords
199
+ }
200
+
201
+ // Convert way rings to coordinate rings
202
+ const coordinateRings: LonLat[][][] = []
203
+
204
+ for (const outerRing of outerRings) {
205
+ const outerCoordinates: LonLat[] = wayRingToCoords(outerRing)
206
+
207
+ if (outerCoordinates.length >= 3) {
208
+ // Find inner rings that belong to this outer ring
209
+ const innerCoordinates: LonLat[][] = []
210
+ for (const innerRing of innerRings) {
211
+ const innerCoords: LonLat[] = wayRingToCoords(innerRing)
212
+
213
+ if (innerCoords.length >= 3) {
214
+ // TODO: do proper point-in-polygon test with https://github.com/rowanwins/point-in-polygon-hao
215
+ innerCoordinates.push(innerCoords)
216
+ }
217
+ }
218
+
219
+ // Create polygon: [outer ring, ...inner rings]
220
+ coordinateRings.push([outerCoordinates, ...innerCoordinates])
221
+ }
222
+ }
223
+
224
+ return coordinateRings
225
+ }
@@ -1,5 +1,22 @@
1
+ /**
2
+ * Stream to byte array conversion.
3
+ *
4
+ * Consumes a ReadableStream and concatenates all chunks into a single Uint8Array.
5
+ *
6
+ * @module
7
+ */
8
+
1
9
  import { concatBytes } from "./concat-bytes"
2
10
 
11
+ /**
12
+ * Consume a ReadableStream and return all data as a single Uint8Array.
13
+ *
14
+ * Reads all chunks from the stream and concatenates them.
15
+ * The stream will be fully consumed after this function returns.
16
+ *
17
+ * @param stream - The stream to consume.
18
+ * @returns A Promise resolving to the concatenated bytes.
19
+ */
3
20
  export async function streamToBytes(
4
21
  stream: ReadableStream<Uint8Array<ArrayBuffer>>,
5
22
  ): Promise<Uint8Array<ArrayBuffer>> {
@@ -1,7 +1,4 @@
1
- import { createReadStream, createWriteStream } from "node:fs"
2
- import { readFile, writeFile } from "node:fs/promises"
3
1
  import { dirname, join, resolve } from "node:path"
4
- import { Readable, Writable } from "node:stream"
5
2
  import { fileURLToPath } from "node:url"
6
3
 
7
4
  const __dirname = dirname(fileURLToPath(import.meta.url))
@@ -25,27 +22,34 @@ export async function getFixtureFile(
25
22
  ): Promise<Uint8Array<ArrayBufferLike>> {
26
23
  const filePath = getFixturePath(url)
27
24
  try {
28
- const file = await readFile(filePath)
29
- return new Uint8Array<ArrayBuffer>(file.buffer as ArrayBuffer)
25
+ const file = await Bun.file(filePath).arrayBuffer()
26
+ return new Uint8Array<ArrayBuffer>(file)
30
27
  } catch (_error) {
28
+ if (process.env["CI"]) {
29
+ throw Error(`Do not download fixtures in CI: ${url} not found`)
30
+ }
31
31
  const response = await fetch(url)
32
32
  const buffer = await response.arrayBuffer()
33
33
  const data = new Uint8Array<ArrayBuffer>(buffer)
34
- await writeFile(filePath, data)
34
+ await Bun.write(filePath, data)
35
35
  return data
36
36
  }
37
37
  }
38
38
 
39
39
  export function getFixtureFileReadStream(url: string) {
40
- return Readable.toWeb(
41
- createReadStream(getFixturePath(url)),
42
- ) as unknown as ReadableStream<Uint8Array<ArrayBufferLike>>
40
+ return Bun.file(getFixturePath(url)).stream()
43
41
  }
44
42
 
45
43
  export function getFixtureFileWriteStream(url: string) {
46
- return Writable.toWeb(
47
- createWriteStream(getFixturePath(url)),
48
- ) as unknown as WritableStream<Uint8Array<ArrayBufferLike>>
44
+ const incrementalWriter = Bun.file(getFixturePath(url)).writer()
45
+ return new WritableStream<Uint8Array<ArrayBufferLike>>({
46
+ write: (chunk: Uint8Array<ArrayBufferLike>) => {
47
+ incrementalWriter.write(chunk)
48
+ },
49
+ close: () => {
50
+ incrementalWriter.end()
51
+ },
52
+ })
49
53
  }
50
54
 
51
55
  export type PbfFixture = {
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Throttling utilities for rate-limiting function calls.
3
+ *
4
+ * Useful for limiting progress updates from workers or other high-frequency
5
+ * operations to avoid overwhelming the UI or logging.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ /**
11
+ * Create a throttled version of a function that only executes at most
12
+ * once per `timeFrame` milliseconds.
13
+ *
14
+ * @param func - The function to throttle.
15
+ * @param timeFrame - Minimum time between calls in milliseconds.
16
+ * @returns A throttled version of the function.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * const logThrottled = throttle((msg) => console.log(msg), 1000)
21
+ * // Only logs at most once per second
22
+ * for (let i = 0; i < 1000; i++) logThrottled(`Progress: ${i}`)
23
+ * ```
24
+ */
25
+ export function throttle<T extends unknown[]>(
26
+ func: (...args: T) => void,
27
+ timeFrame: number,
28
+ ) {
29
+ let lastTime = 0
30
+ return (...args: T) => {
31
+ const now = Date.now()
32
+ if (now - lastTime >= timeFrame) {
33
+ func(...args)
34
+ lastTime = now
35
+ }
36
+ }
37
+ }
package/src/tile.ts ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * XYZ tile coordinate utilities.
3
+ *
4
+ * Provides conversions between geographic coordinates (lon/lat) and
5
+ * tile pixel coordinates for XYZ tiled map systems. Supports various
6
+ * tile sizes and zoom levels.
7
+ *
8
+ * @module
9
+ */
10
+
11
+ import { pointToTileFraction } from "@mapbox/tilebelt"
12
+ import type { GeoBbox2D, LonLat, Tile, TilePxBbox, XY } from "./types"
13
+
14
+ const RADIANS_TO_DEGREES = 180 / Math.PI
15
+
16
+ function tile2lon(x: number, z: number): number {
17
+ return (x / 2 ** z) * 360 - 180
18
+ }
19
+
20
+ function tile2lat(y: number, z: number): number {
21
+ const n = Math.PI - (2 * Math.PI * y) / 2 ** z
22
+ return RADIANS_TO_DEGREES * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)))
23
+ }
24
+
25
+ /**
26
+ * Get the geographic bounding box of a tile.
27
+ * Returns [west, south, east, north].
28
+ */
29
+ export function tileToBbox(tile: Tile): GeoBbox2D {
30
+ const [tx, ty, tz] = tile
31
+ const n = tile2lat(ty, tz)
32
+ const s = tile2lat(ty + 1, tz)
33
+ const e = tile2lon(tx + 1, tz)
34
+ const w = tile2lon(tx, tz)
35
+ return [w, s, e, n]
36
+ }
37
+
38
+ /**
39
+ * Convert a bounding box from geographic coordinates to tile pixel coordinates.
40
+ */
41
+ export function bboxToTilePx(
42
+ bbox: GeoBbox2D,
43
+ tile: Tile,
44
+ tileSize = 256,
45
+ ): TilePxBbox {
46
+ const [minX, minY] = llToTilePx([bbox[0], bbox[3]], tile, tileSize)
47
+ const [maxX, maxY] = llToTilePx([bbox[2], bbox[1]], tile, tileSize)
48
+ return [minX, minY, maxX, maxY]
49
+ }
50
+
51
+ /**
52
+ * Convert a geographic coordinate to tile pixel coordinates.
53
+ * Returns [x, y] in pixels relative to the top-left of the tile.
54
+ */
55
+ export function llToTilePx(ll: LonLat, tile: Tile, tileSize = 256): XY {
56
+ const [tx, ty, tz] = tile
57
+ const tf = pointToTileFraction(ll[0], ll[1], tz)
58
+ const x = (tf[0] - tx) * tileSize
59
+ const y = (tf[1] - ty) * tileSize
60
+ return [x, y]
61
+ }
62
+
63
+ /**
64
+ * Convert tile pixel coordinates to geographic coordinates.
65
+ */
66
+ export function tilePxToLonLat(px: XY, tile: Tile, tileSize = 256): LonLat {
67
+ const [tx, ty, tz] = tile
68
+ const lon = tile2lon(px[0] / tileSize + tx, tz)
69
+ const lat = tile2lat(px[1] / tileSize + ty, tz)
70
+ return [lon, lat]
71
+ }
72
+
73
+ /**
74
+ * Clamp a pixel coordinate to the tile bounds and round to the nearest integer. Useful for converting a floating point pixel
75
+ * coordinate to a the exact tile pixel it is contained by.
76
+ */
77
+ export function clampAndRoundPx(
78
+ px: XY,
79
+ tileSizeOrBbox: number | GeoBbox2D,
80
+ ): XY {
81
+ const [minX, minY, maxX, maxY] =
82
+ typeof tileSizeOrBbox === "number"
83
+ ? [0, 0, tileSizeOrBbox, tileSizeOrBbox]
84
+ : tileSizeOrBbox
85
+ return [
86
+ Math.max(minX, Math.min(maxX, Math.round(px[0]))),
87
+ Math.max(minY, Math.min(maxY, Math.round(px[1]))),
88
+ ]
89
+ }
@@ -1,6 +1,29 @@
1
+ /**
2
+ * Transform stream utilities.
3
+ *
4
+ * Helpers for piping byte arrays through TransformStreams (e.g., compression).
5
+ *
6
+ * @module
7
+ */
8
+
1
9
  import { bytesToStream } from "./bytes-to-stream"
2
10
  import { streamToBytes } from "./stream-to-bytes"
3
11
 
12
+ /**
13
+ * Pipe a byte array through a TransformStream and return the result.
14
+ *
15
+ * Useful for applying compression/decompression or other transformations
16
+ * to byte data using the Web Streams API.
17
+ *
18
+ * @param bytes - The input bytes.
19
+ * @param transformStream - The transform to apply (e.g., CompressionStream).
20
+ * @returns A Promise resolving to the transformed bytes.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const compressed = await transformBytes(data, new CompressionStream('gzip'))
25
+ * ```
26
+ */
4
27
  export async function transformBytes(
5
28
  bytes: Uint8Array<ArrayBuffer>,
6
29
  transformStream: TransformStream<
package/src/types.ts CHANGED
@@ -1,9 +1,23 @@
1
+ /**
2
+ * Shared type definitions used across all Osmix packages.
3
+ *
4
+ * Includes geographic primitives (LonLat, Bbox), tile coordinates,
5
+ * and core OSM entity types (Node, Way, Relation).
6
+ *
7
+ * @module
8
+ */
9
+
10
+ /** Geographic coordinate as [longitude, latitude] tuple. */
1
11
  export type LonLat = [lon: number, lat: number]
12
+ /** 2D pixel or cartesian coordinate as [x, y] tuple. */
2
13
  export type XY = [x: number, y: number]
14
+
15
+ /** Geographic coordinate as object with lon/lat properties. */
3
16
  export interface ILonLat {
4
17
  lon: number
5
18
  lat: number
6
19
  }
20
+ /** XYZ tile coordinate as [x, y, z] tuple. */
7
21
  export type Tile = [x: number, y: number, z: number]
8
22
 
9
23
  /**
@@ -13,7 +27,9 @@ export type LonLatToPixel = (ll: LonLat, zoom: number) => XY
13
27
 
14
28
  export type LonLatToTilePixel = (ll: LonLat, z: number, extent: number) => XY
15
29
 
16
- export type Rgba = [number, number, number, number] | Uint8ClampedArray
30
+ export type Rgba =
31
+ | [r: number, g: number, b: number, a: number]
32
+ | Uint8ClampedArray
17
33
 
18
34
  /**
19
35
  * A bounding box in the format [minLon, minLat, maxLon, maxLat].
@@ -25,3 +41,79 @@ export type GeoBbox2D = [
25
41
  maxLon: number,
26
42
  maxLat: number,
27
43
  ]
44
+
45
+ /**
46
+ * A pixel bounding box in the format [minX, minY, maxX, maxY]. Note: minY is north. maxY is south.
47
+ */
48
+ export type TilePxBbox = [
49
+ minX: number,
50
+ minY: number,
51
+ maxX: number,
52
+ maxY: number,
53
+ ]
54
+
55
+ /**
56
+ * Shared OSM Types
57
+ */
58
+
59
+ export type OsmEntityType = "node" | "way" | "relation"
60
+
61
+ export interface OsmInfoParsed {
62
+ version?: number
63
+ timestamp?: number
64
+ changeset?: number
65
+ uid?: number
66
+ user_sid?: number
67
+ visible?: boolean
68
+ user?: string
69
+ }
70
+
71
+ export interface OsmTags {
72
+ [key: string]: string | number
73
+ }
74
+
75
+ interface IOsmEntity {
76
+ id: number
77
+ info?: OsmInfoParsed
78
+ tags?: OsmTags
79
+ }
80
+
81
+ export interface OsmNode extends IOsmEntity, ILonLat {}
82
+
83
+ export interface OsmWay extends IOsmEntity {
84
+ // OSM IDs of the nodes that make up this way
85
+ refs: number[]
86
+ }
87
+
88
+ export interface OsmRelationMember {
89
+ type: OsmEntityType
90
+ ref: number
91
+ role?: string
92
+ }
93
+
94
+ export interface OsmRelation extends IOsmEntity {
95
+ members: OsmRelationMember[]
96
+ }
97
+
98
+ export type OsmEntity = OsmNode | OsmWay | OsmRelation
99
+
100
+ export interface OsmEntityTypeMap extends Record<OsmEntityType, IOsmEntity> {
101
+ node: OsmNode
102
+ way: OsmWay
103
+ relation: OsmRelation
104
+ }
105
+
106
+ /**
107
+ * Semantic kinds of OSM relations based on their type tag and structure.
108
+ */
109
+ export type RelationKind = "area" | "line" | "point" | "logic" | "super"
110
+
111
+ /**
112
+ * Metadata about a relation kind, including expected roles and whether member order matters.
113
+ */
114
+ export interface RelationKindMetadata {
115
+ kind: RelationKind
116
+ expectedRoles?: string[]
117
+ orderMatters: boolean
118
+ description: string
119
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,79 @@
1
+ /**
2
+ * General OSM entity utilities.
3
+ *
4
+ * Provides type guards, equality checks, and type detection for OSM entities.
5
+ * Uses deep equality checking for tags, info, and entity-specific properties.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ import { dequal } from "dequal/lite"
11
+ import type {
12
+ OsmEntity,
13
+ OsmEntityType,
14
+ OsmNode,
15
+ OsmRelation,
16
+ OsmWay,
17
+ } from "./types"
18
+
19
+ /**
20
+ * Check if two entities have equal tags and info.
21
+ */
22
+ function isTagsAndInfoEqual(a: OsmEntity, b: OsmEntity) {
23
+ return dequal(a.tags, b.tags) && dequal(a.info, b.info)
24
+ }
25
+
26
+ /** Type guard: check if entity is a Node. */
27
+ export function isNode(entity: OsmEntity): entity is OsmNode {
28
+ return "lon" in entity && "lat" in entity
29
+ }
30
+
31
+ /** Check if two nodes are equal (position, tags, and info). */
32
+ export function isNodeEqual(a: OsmNode, b: OsmNode) {
33
+ return a.lat === b.lat && a.lon === b.lon && isTagsAndInfoEqual(a, b)
34
+ }
35
+
36
+ /** Type guard: check if entity is a Way. */
37
+ export function isWay(entity: OsmEntity): entity is OsmWay {
38
+ return "refs" in entity
39
+ }
40
+
41
+ /** Type guard: check if entity is a Relation. */
42
+ export function isRelation(entity: OsmEntity): entity is OsmRelation {
43
+ return "members" in entity
44
+ }
45
+
46
+ /** Check if two ways are equal (refs, tags, and info). */
47
+ export function isWayEqual(a: OsmWay, b: OsmWay) {
48
+ return dequal(a.refs, b.refs) && isTagsAndInfoEqual(a, b)
49
+ }
50
+
51
+ /** Check if two relations are equal (members, tags, and info). */
52
+ export function isRelationEqual(a: OsmRelation, b: OsmRelation) {
53
+ return dequal(a.members, b.members) && isTagsAndInfoEqual(a, b)
54
+ }
55
+
56
+ /** Check if two entities have equal properties (type-aware comparison). */
57
+ export function entityPropertiesEqual(a: OsmEntity, b: OsmEntity) {
58
+ if (!dequal(a.tags, b.tags)) return false
59
+ if (!dequal(a.info, b.info)) return false
60
+ if (isNode(a) && isNode(b)) return isNodeEqual(a, b)
61
+ if (isWay(a) && isWay(b)) return isWayEqual(a, b)
62
+ if (isRelation(a) && isRelation(b)) return isRelationEqual(a, b)
63
+ return false
64
+ }
65
+
66
+ /** Get the entity type ("node", "way", or "relation") for an entity. */
67
+ export function getEntityType(entity: OsmEntity): OsmEntityType {
68
+ if (isNode(entity)) return "node"
69
+ if (isWay(entity)) return "way"
70
+ if (isRelation(entity)) return "relation"
71
+ throw Error("Unknown entity type")
72
+ }
73
+
74
+ /**
75
+ * Check if a relation is a multipolygon relation.
76
+ */
77
+ export function isMultipolygonRelation(relation: OsmRelation): boolean {
78
+ return relation.tags?.["type"] === "multipolygon"
79
+ }