squirreling 0.9.3 → 0.9.4

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.
@@ -0,0 +1,222 @@
1
+ /**
2
+ * @import { Geometry } from './geometry.js'
3
+ */
4
+
5
+ /**
6
+ * Parse a WKT string into a GeoJSON geometry.
7
+ *
8
+ * @param {string} wkt
9
+ * @returns {Geometry | null}
10
+ */
11
+ export function parseWkt(wkt) {
12
+ const s = wkt.trim()
13
+ const upper = s.toUpperCase()
14
+
15
+ if (upper.startsWith('POINT')) {
16
+ const coords = parseWktCoordinate(s.slice(5).trim())
17
+ if (!coords) return null
18
+ return { type: 'Point', coordinates: coords }
19
+ }
20
+
21
+ if (upper.startsWith('MULTIPOINT')) {
22
+ const inner = extractParens(s.slice(10).trim())
23
+ if (inner == null) return null
24
+ const coords = parseWktCoordinateList(inner)
25
+ if (!coords) return null
26
+ return { type: 'MultiPoint', coordinates: coords }
27
+ }
28
+
29
+ if (upper.startsWith('MULTILINESTRING')) {
30
+ const inner = extractParens(s.slice(15).trim())
31
+ if (inner == null) return null
32
+ const rings = parseWktRingList(inner)
33
+ if (!rings) return null
34
+ return { type: 'MultiLineString', coordinates: rings }
35
+ }
36
+
37
+ if (upper.startsWith('MULTIPOLYGON')) {
38
+ const inner = extractParens(s.slice(12).trim())
39
+ if (inner == null) return null
40
+ const polys = parseWktPolygonList(inner)
41
+ if (!polys) return null
42
+ return { type: 'MultiPolygon', coordinates: polys }
43
+ }
44
+
45
+ if (upper.startsWith('LINESTRING')) {
46
+ const inner = extractParens(s.slice(10).trim())
47
+ if (inner == null) return null
48
+ const coords = parseWktCoordinateList(inner)
49
+ if (!coords) return null
50
+ return { type: 'LineString', coordinates: coords }
51
+ }
52
+
53
+ if (upper.startsWith('POLYGON')) {
54
+ const inner = extractParens(s.slice(7).trim())
55
+ if (inner == null) return null
56
+ const rings = parseWktRingList(inner)
57
+ if (!rings) return null
58
+ return { type: 'Polygon', coordinates: rings }
59
+ }
60
+
61
+ return null
62
+ }
63
+
64
+ /**
65
+ * Convert a GeoJSON geometry to WKT.
66
+ *
67
+ * @param {Geometry} geom
68
+ * @returns {string}
69
+ */
70
+ export function geomToWkt(geom) {
71
+ switch (geom.type) {
72
+ case 'Point':
73
+ return `POINT (${coordToWkt(geom.coordinates)})`
74
+ case 'MultiPoint':
75
+ return `MULTIPOINT (${geom.coordinates.map(c => `(${coordToWkt(c)})`).join(', ')})`
76
+ case 'LineString':
77
+ return `LINESTRING (${coordListToWkt(geom.coordinates)})`
78
+ case 'MultiLineString':
79
+ return `MULTILINESTRING (${geom.coordinates.map(l => `(${coordListToWkt(l)})`).join(', ')})`
80
+ case 'Polygon':
81
+ return `POLYGON (${geom.coordinates.map(r => `(${coordListToWkt(r)})`).join(', ')})`
82
+ case 'MultiPolygon':
83
+ return `MULTIPOLYGON (${geom.coordinates.map(p => `(${p.map(r => `(${coordListToWkt(r)})`).join(', ')})`).join(', ')})`
84
+ case 'GeometryCollection':
85
+ return `GEOMETRYCOLLECTION (${(geom.geometries || []).map(g => geomToWkt(g)).join(', ')})`
86
+ default:
87
+ return ''
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Extract content inside outer parentheses.
93
+ *
94
+ * @param {string} s
95
+ * @returns {string | null}
96
+ */
97
+ function extractParens(s) {
98
+ const trimmed = s.trim()
99
+ if (!trimmed.startsWith('(') || !trimmed.endsWith(')')) return null
100
+ return trimmed.slice(1, -1).trim()
101
+ }
102
+
103
+ /**
104
+ * Parse a single coordinate like "(1 2)" or "1 2".
105
+ *
106
+ * @param {string} s
107
+ * @returns {number[] | null}
108
+ */
109
+ function parseWktCoordinate(s) {
110
+ const inner = s.trim().replace(/^\(/, '').replace(/\)$/, '').trim()
111
+ const parts = inner.split(/\s+/)
112
+ if (parts.length < 2) return null
113
+ const x = Number(parts[0])
114
+ const y = Number(parts[1])
115
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return null
116
+ return [x, y]
117
+ }
118
+
119
+ /**
120
+ * Parse a comma-separated list of coordinates like "1 2, 3 4, 5 6".
121
+ *
122
+ * @param {string} s
123
+ * @returns {number[][] | null}
124
+ */
125
+ function parseWktCoordinateList(s) {
126
+ const parts = s.split(',')
127
+ /** @type {number[][]} */
128
+ const coords = []
129
+ for (const part of parts) {
130
+ const trimmed = part.trim().replace(/^\(/, '').replace(/\)$/, '').trim()
131
+ const nums = trimmed.split(/\s+/)
132
+ if (nums.length < 2) return null
133
+ const x = Number(nums[0])
134
+ const y = Number(nums[1])
135
+ if (!Number.isFinite(x) || !Number.isFinite(y)) return null
136
+ coords.push([x, y])
137
+ }
138
+ return coords.length ? coords : null
139
+ }
140
+
141
+ /**
142
+ * Parse a list of rings like "(1 2, 3 4), (5 6, 7 8)".
143
+ *
144
+ * @param {string} s
145
+ * @returns {number[][][] | null}
146
+ */
147
+ function parseWktRingList(s) {
148
+ /** @type {number[][][]} */
149
+ const rings = []
150
+ const ringStrs = splitTopLevel(s)
151
+ for (const ringStr of ringStrs) {
152
+ const inner = extractParens(ringStr.trim())
153
+ if (inner == null) return null
154
+ const coords = parseWktCoordinateList(inner)
155
+ if (!coords) return null
156
+ rings.push(coords)
157
+ }
158
+ return rings.length ? rings : null
159
+ }
160
+
161
+ /**
162
+ * Parse a list of polygons like "((ring1), (ring2)), ((ring3))".
163
+ *
164
+ * @param {string} s
165
+ * @returns {number[][][][] | null}
166
+ */
167
+ function parseWktPolygonList(s) {
168
+ /** @type {number[][][][]} */
169
+ const polys = []
170
+ const polyStrs = splitTopLevel(s)
171
+ for (const polyStr of polyStrs) {
172
+ const inner = extractParens(polyStr.trim())
173
+ if (inner == null) return null
174
+ const rings = parseWktRingList(inner)
175
+ if (!rings) return null
176
+ polys.push(rings)
177
+ }
178
+ return polys.length ? polys : null
179
+ }
180
+
181
+ /**
182
+ * Split a string by commas at the top-level (not inside parentheses).
183
+ *
184
+ * @param {string} s
185
+ * @returns {string[]}
186
+ */
187
+ function splitTopLevel(s) {
188
+ /** @type {string[]} */
189
+ const parts = []
190
+ let depth = 0
191
+ let start = 0
192
+ for (let i = 0; i < s.length; i++) {
193
+ if (s[i] === '(') depth++
194
+ else if (s[i] === ')') depth--
195
+ else if (s[i] === ',' && depth === 0) {
196
+ parts.push(s.slice(start, i))
197
+ start = i + 1
198
+ }
199
+ }
200
+ parts.push(s.slice(start))
201
+ return parts
202
+ }
203
+
204
+ /**
205
+ * Format a single coordinate to WKT.
206
+ *
207
+ * @param {number[]} coord
208
+ * @returns {string}
209
+ */
210
+ function coordToWkt(coord) {
211
+ return `${coord[0]} ${coord[1]}`
212
+ }
213
+
214
+ /**
215
+ * Format a coordinate list to WKT.
216
+ *
217
+ * @param {number[][]} coords
218
+ * @returns {string}
219
+ */
220
+ function coordListToWkt(coords) {
221
+ return coords.map(coordToWkt).join(', ')
222
+ }