terra-route 0.0.3 → 0.0.5

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.
@@ -22,3 +22,372 @@ export const createLineStringFeature = (coordinates: Position[]): Feature<LineSt
22
22
  },
23
23
  properties: {},
24
24
  });
25
+
26
+ export function generateGridWithDiagonals(n: number, spacing: number): FeatureCollection<LineString> {
27
+ const features: Feature<LineString>[] = [];
28
+
29
+ const coord = (x: number, y: number): Position => [x * spacing, y * spacing];
30
+
31
+ for (let y = 0; y < n; y++) {
32
+ for (let x = 0; x < n; x++) {
33
+ // Horizontal edge (to the right)
34
+ if (x < n - 1) {
35
+ features.push({
36
+ type: "Feature",
37
+ geometry: {
38
+ type: "LineString",
39
+ coordinates: [
40
+ coord(x, y),
41
+ coord(x + 1, y)
42
+ ]
43
+ },
44
+ properties: {}
45
+ });
46
+ }
47
+
48
+ // Vertical edge (upward)
49
+ if (y < n - 1) {
50
+ features.push({
51
+ type: "Feature",
52
+ geometry: {
53
+ type: "LineString",
54
+ coordinates: [
55
+ coord(x, y),
56
+ coord(x, y + 1)
57
+ ]
58
+ },
59
+ properties: {}
60
+ });
61
+ }
62
+
63
+ // Diagonal bottom-left to top-right
64
+ if (x < n - 1 && y < n - 1) {
65
+ features.push({
66
+ type: "Feature",
67
+ geometry: {
68
+ type: "LineString",
69
+ coordinates: [
70
+ coord(x, y),
71
+ coord(x + 1, y + 1)
72
+ ]
73
+ },
74
+ properties: {}
75
+ });
76
+ }
77
+
78
+ // Diagonal bottom-right to top-left
79
+ if (x > 0 && y < n - 1) {
80
+ features.push({
81
+ type: "Feature",
82
+ geometry: {
83
+ type: "LineString",
84
+ coordinates: [
85
+ coord(x, y),
86
+ coord(x - 1, y + 1)
87
+ ]
88
+ },
89
+ properties: {}
90
+ });
91
+ }
92
+ }
93
+ }
94
+
95
+ return {
96
+ type: "FeatureCollection",
97
+ features
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Generate a star-like polygon with n vertices.
103
+ * If connectAll is true, connects every vertex to every other (complete graph).
104
+ * If false, connects only the outer ring to form a polygon perimeter.
105
+ *
106
+ * @param n - Number of vertices (>= 3)
107
+ * @param radius - Radius in degrees for placing vertices in a circle
108
+ * @param center - Center of the polygon [lng, lat]
109
+ * @param connectAll - If true, connects every pair of vertices. If false, only connects the outer ring.
110
+ * @returns FeatureCollection of LineStrings
111
+ */
112
+ export function generateStarPolygon(
113
+ n: number,
114
+ radius: number = 0.01,
115
+ center: Position = [0, 0],
116
+ connectAll: boolean = true
117
+ ): FeatureCollection<LineString> {
118
+ if (n < 3) {
119
+ throw new Error("Star polygon requires at least 3 vertices.");
120
+ }
121
+
122
+ const angleStep = (2 * Math.PI) / n;
123
+ const vertices: Position[] = [];
124
+
125
+ // Generate points in a circle
126
+ for (let i = 0; i < n; i++) {
127
+ const angle = i * angleStep;
128
+ const x = center[0] + radius * Math.cos(angle);
129
+ const y = center[1] + radius * Math.sin(angle);
130
+ vertices.push([x, y]);
131
+ }
132
+
133
+ const features: Feature<LineString>[] = [];
134
+
135
+ if (connectAll) {
136
+ // Connect every vertex to every other vertex
137
+ for (let i = 0; i < n; i++) {
138
+ for (let j = i + 1; j < n; j++) {
139
+ features.push({
140
+ type: "Feature",
141
+ geometry: {
142
+ type: "LineString",
143
+ coordinates: [vertices[i], vertices[j]],
144
+ },
145
+ properties: {},
146
+ });
147
+ }
148
+ }
149
+ } else {
150
+ // Connect outer ring only
151
+ for (let i = 0; i < n; i++) {
152
+ const next = (i + 1) % n;
153
+ features.push({
154
+ type: "Feature",
155
+ geometry: {
156
+ type: "LineString",
157
+ coordinates: [vertices[i], vertices[next]],
158
+ },
159
+ properties: {},
160
+ });
161
+ }
162
+ }
163
+
164
+ return {
165
+ type: "FeatureCollection",
166
+ features,
167
+ };
168
+ }
169
+
170
+
171
+ /**
172
+ * Extracts unique coordinates from a FeatureCollection of LineStrings.
173
+ *
174
+ * @param collection - A GeoJSON FeatureCollection of LineStrings
175
+ * @returns An array of unique Position coordinates
176
+ */
177
+ export function getUniqueCoordinatesFromLineStrings(
178
+ collection: FeatureCollection<LineString>
179
+ ): Position[] {
180
+ const seen = new Set<string>();
181
+ const unique: Position[] = [];
182
+
183
+ for (const feature of collection.features) {
184
+ if (feature.geometry.type !== "LineString") {
185
+ continue;
186
+ }
187
+
188
+ for (const coord of feature.geometry.coordinates) {
189
+ const key = `${coord[0]},${coord[1]}`;
190
+
191
+ if (!seen.has(key)) {
192
+ seen.add(key);
193
+ unique.push(coord);
194
+ }
195
+ }
196
+ }
197
+
198
+ return unique;
199
+ }
200
+
201
+
202
+ /**
203
+ * Generate a spatial n-depth tree as a FeatureCollection<LineString>.
204
+ *
205
+ * @param depth - Number of depth levels (>= 1)
206
+ * @param branchingFactor - Number of children per node
207
+ * @param root - Root position [lng, lat]
208
+ * @param length - Distance between each parent and child
209
+ * @returns FeatureCollection of LineStrings representing the tree
210
+ */
211
+ export function generateTreeFeatureCollection(
212
+ depth: number,
213
+ branchingFactor: number,
214
+ root: Position = [0, 0],
215
+ length: number = 0.01
216
+ ): FeatureCollection<LineString> {
217
+ if (depth < 1) {
218
+ throw new Error("Tree must have at least depth 1.");
219
+ }
220
+
221
+ const features: Feature<LineString>[] = [];
222
+
223
+ interface TreeNode {
224
+ position: Position;
225
+ level: number;
226
+ }
227
+
228
+ const nodes: TreeNode[] = [{ position: root, level: 0 }];
229
+
230
+ const RAD = Math.PI / 180;
231
+
232
+ for (let level = 0; level < depth; level++) {
233
+ const newNodes: TreeNode[] = [];
234
+
235
+ for (const node of nodes.filter(n => n.level === level)) {
236
+ const angleStart = -90 - ((branchingFactor - 1) * 20) / 2;
237
+
238
+ for (let i = 0; i < branchingFactor; i++) {
239
+ const angle = angleStart + i * 20; // spread branches 20 degrees apart
240
+ const radians = angle * RAD;
241
+
242
+ const dx = length * Math.cos(radians);
243
+ const dy = length * Math.sin(radians);
244
+
245
+ const child: Position = [node.position[0] + dx, node.position[1] + dy];
246
+
247
+ features.push({
248
+ type: "Feature",
249
+ geometry: {
250
+ type: "LineString",
251
+ coordinates: [node.position, child],
252
+ },
253
+ properties: {},
254
+ });
255
+
256
+ newNodes.push({ position: child, level: level + 1 });
257
+ }
258
+ }
259
+
260
+ nodes.push(...newNodes);
261
+ }
262
+
263
+ return {
264
+ type: "FeatureCollection",
265
+ features,
266
+ };
267
+ }
268
+
269
+ /**
270
+ * Generates a connected graph of concentric rings, each ring fully connected
271
+ * around itself and connected radially to the next ring.
272
+ *
273
+ * @param numRings - Number of concentric rings
274
+ * @param pointsPerRing - How many points (nodes) on each ring
275
+ * @param spacing - Distance between consecutive rings
276
+ * @param center - [lng, lat] center of the rings
277
+ * @returns A FeatureCollection of LineStrings for the rings + radial connections
278
+ */
279
+ export function generateConcentricRings(
280
+ numRings: number,
281
+ pointsPerRing: number,
282
+ spacing: number,
283
+ center: Position = [0, 0]
284
+ ): FeatureCollection<LineString> {
285
+ // Holds all the ring coordinates: ringPoints[i][j] => coordinate
286
+ const ringPoints: Position[][] = [];
287
+
288
+ // Create ring points
289
+ for (let i = 0; i < numRings; i++) {
290
+ const ringRadius = (i + 1) * spacing;
291
+ const ring: Position[] = [];
292
+
293
+ for (let j = 0; j < pointsPerRing; j++) {
294
+ const angle = (2 * Math.PI * j) / pointsPerRing;
295
+ const x = center[0] + ringRadius * Math.cos(angle);
296
+ const y = center[1] + ringRadius * Math.sin(angle);
297
+ ring.push([x, y]);
298
+ }
299
+
300
+ ringPoints.push(ring);
301
+ }
302
+
303
+ // Build the graph as a collection of LineStrings
304
+ const features: Feature<LineString>[] = [];
305
+
306
+ // 1. Add each ring as a closed loop
307
+ for (let i = 0; i < numRings; i++) {
308
+ const coords = ringPoints[i];
309
+ // Close the ring by appending the first point again
310
+ const ringWithClosure = [...coords, coords[0]];
311
+
312
+ features.push({
313
+ type: "Feature",
314
+ properties: {},
315
+ geometry: {
316
+ type: "LineString",
317
+ coordinates: ringWithClosure,
318
+ },
319
+ });
320
+ }
321
+
322
+ // 2. Connect rings radially
323
+ // (i.e., ring i node j to ring i+1 node j)
324
+ for (let i = 0; i < numRings - 1; i++) {
325
+ for (let j = 0; j < pointsPerRing; j++) {
326
+ const start = ringPoints[i][j];
327
+ const end = ringPoints[i + 1][j];
328
+
329
+ features.push({
330
+ type: "Feature",
331
+ properties: {},
332
+ geometry: {
333
+ type: "LineString",
334
+ coordinates: [start, end],
335
+ },
336
+ });
337
+ }
338
+ }
339
+
340
+ return {
341
+ type: "FeatureCollection",
342
+ features,
343
+ };
344
+ }
345
+
346
+
347
+ /**
348
+ * Validates a GeoJSON Feature<LineString> route.
349
+ *
350
+ * @param route - The GeoJSON feature to validate
351
+ * @returns A boolean indicating if it is a valid LineString route
352
+ */
353
+ export function getReasonIfLineStringInvalid(
354
+ route: Feature<LineString> | null | undefined
355
+ ): string | undefined {
356
+ // 1. Must exist
357
+ if (!route) {
358
+ return 'No feature';
359
+ }
360
+
361
+ // 2. Must be a Feature
362
+ if (route.type !== "Feature") {
363
+ return 'Not a Feature';
364
+ }
365
+
366
+ // 3. Must have a geometry of type LineString
367
+ if (!route.geometry || route.geometry.type !== "LineString") {
368
+ return 'Not a LineString';
369
+ }
370
+
371
+ // 4. Coordinates must be an array with length >= 2
372
+ const coords = route.geometry.coordinates;
373
+ if (!Array.isArray(coords) || coords.length < 2) {
374
+ return `Not enough coordinates: ${coords.length} (${coords})`;
375
+ }
376
+
377
+ // 5. Validate each coordinate is a valid Position
378
+ // (At minimum, [number, number] or [number, number, number])
379
+ for (const position of coords) {
380
+ if (!Array.isArray(position)) {
381
+ return 'Not a Position; not an array';
382
+ }
383
+
384
+ // Check numeric values, ignoring optional altitude
385
+ if (
386
+ position.length < 2 ||
387
+ typeof position[0] !== "number" ||
388
+ typeof position[1] !== "number"
389
+ ) {
390
+ return 'Not a Position; elements are not a numbers';
391
+ }
392
+ }
393
+ }