geo-relative-position 1.0.0

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/dist/index.mjs ADDED
@@ -0,0 +1,638 @@
1
+ // src/constants.ts
2
+ var EARTH_RADIUS_METERS = 6371e3;
3
+ var STATIONARY_SPEED_THRESHOLD = 0.5;
4
+ var ORIENTATION_THRESHOLDS = {
5
+ /** Threshold for "directly ahead" (+/- degrees from 0) */
6
+ DIRECTLY_AHEAD: 10,
7
+ /** Threshold for "ahead" region (within +/- degrees from 0) */
8
+ AHEAD: 45,
9
+ /** Threshold for "behind" region (beyond +/- degrees from 0) */
10
+ BEHIND: 135,
11
+ /** Threshold for "directly behind" (+/- degrees from 180) */
12
+ DIRECTLY_BEHIND: 170
13
+ };
14
+ var COMPASS_BOUNDARIES = {
15
+ NORTH_END: 22.5,
16
+ NORTHEAST_END: 67.5,
17
+ EAST_END: 112.5,
18
+ SOUTHEAST_END: 157.5,
19
+ SOUTH_END: 202.5,
20
+ SOUTHWEST_END: 247.5,
21
+ WEST_END: 292.5,
22
+ NORTHWEST_END: 337.5
23
+ };
24
+
25
+ // src/utils/math.ts
26
+ function toRadians(degrees) {
27
+ return degrees * (Math.PI / 180);
28
+ }
29
+ function toDegrees(radians) {
30
+ return radians * (180 / Math.PI);
31
+ }
32
+ function normalizeAngle(degrees) {
33
+ let normalized = degrees % 360;
34
+ if (normalized < 0) {
35
+ normalized += 360;
36
+ }
37
+ return normalized;
38
+ }
39
+ function normalizeLongitude(longitude) {
40
+ let normalized = longitude % 360;
41
+ if (normalized > 180) {
42
+ normalized -= 360;
43
+ }
44
+ if (normalized < -180) {
45
+ normalized += 360;
46
+ }
47
+ return normalized;
48
+ }
49
+ function clamp(value, min, max) {
50
+ return Math.min(Math.max(value, min), max);
51
+ }
52
+ function roundTo(value, decimals) {
53
+ const factor = Math.pow(10, decimals);
54
+ return Math.round(value * factor) / factor;
55
+ }
56
+
57
+ // src/core/distance.ts
58
+ function getDistance(from, to) {
59
+ if (from.latitude === to.latitude && from.longitude === to.longitude) {
60
+ return 0;
61
+ }
62
+ const lat1Rad = toRadians(from.latitude);
63
+ const lat2Rad = toRadians(to.latitude);
64
+ const deltaLat = toRadians(to.latitude - from.latitude);
65
+ const deltaLon = toRadians(to.longitude - from.longitude);
66
+ const a = Math.sin(deltaLat / 2) * Math.sin(deltaLat / 2) + Math.cos(lat1Rad) * Math.cos(lat2Rad) * Math.sin(deltaLon / 2) * Math.sin(deltaLon / 2);
67
+ const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
68
+ return EARTH_RADIUS_METERS * c;
69
+ }
70
+ function getDistanceKm(from, to) {
71
+ return getDistance(from, to) / 1e3;
72
+ }
73
+
74
+ // src/core/bearing.ts
75
+ function getBearing(from, to) {
76
+ if (from.latitude === to.latitude && from.longitude === to.longitude) {
77
+ return 0;
78
+ }
79
+ const lat1 = toRadians(from.latitude);
80
+ const lat2 = toRadians(to.latitude);
81
+ const deltaLon = toRadians(to.longitude - from.longitude);
82
+ const y = Math.sin(deltaLon) * Math.cos(lat2);
83
+ const x = Math.cos(lat1) * Math.sin(lat2) - Math.sin(lat1) * Math.cos(lat2) * Math.cos(deltaLon);
84
+ const bearing = toDegrees(Math.atan2(y, x));
85
+ return normalizeAngle(bearing);
86
+ }
87
+ function getRelativeBearing(userBearing, targetBearing) {
88
+ let relative = targetBearing - userBearing;
89
+ while (relative > 180) {
90
+ relative -= 360;
91
+ }
92
+ while (relative < -180) {
93
+ relative += 360;
94
+ }
95
+ return relative;
96
+ }
97
+ function getFinalBearing(from, to) {
98
+ const reverseBearing = getBearing(to, from);
99
+ return normalizeAngle(reverseBearing + 180);
100
+ }
101
+
102
+ // src/core/orientation.ts
103
+ function getSimpleOrientation(relativeBearing) {
104
+ const abs = Math.abs(relativeBearing);
105
+ if (abs <= ORIENTATION_THRESHOLDS.AHEAD) {
106
+ return "ahead";
107
+ }
108
+ if (abs >= ORIENTATION_THRESHOLDS.BEHIND) {
109
+ return "behind";
110
+ }
111
+ return relativeBearing > 0 ? "right" : "left";
112
+ }
113
+ function getCompassOrientation(absoluteBearing) {
114
+ const normalized = normalizeAngle(absoluteBearing);
115
+ if (normalized < COMPASS_BOUNDARIES.NORTH_END || normalized >= COMPASS_BOUNDARIES.NORTHWEST_END) {
116
+ return "north";
117
+ }
118
+ if (normalized < COMPASS_BOUNDARIES.NORTHEAST_END) {
119
+ return "northeast";
120
+ }
121
+ if (normalized < COMPASS_BOUNDARIES.EAST_END) {
122
+ return "east";
123
+ }
124
+ if (normalized < COMPASS_BOUNDARIES.SOUTHEAST_END) {
125
+ return "southeast";
126
+ }
127
+ if (normalized < COMPASS_BOUNDARIES.SOUTH_END) {
128
+ return "south";
129
+ }
130
+ if (normalized < COMPASS_BOUNDARIES.SOUTHWEST_END) {
131
+ return "southwest";
132
+ }
133
+ if (normalized < COMPASS_BOUNDARIES.WEST_END) {
134
+ return "west";
135
+ }
136
+ return "northwest";
137
+ }
138
+ function getDetailedOrientation(relativeBearing) {
139
+ const abs = Math.abs(relativeBearing);
140
+ const isLeft = relativeBearing < 0;
141
+ if (abs <= ORIENTATION_THRESHOLDS.DIRECTLY_AHEAD) {
142
+ return "directly_ahead";
143
+ }
144
+ if (abs <= ORIENTATION_THRESHOLDS.AHEAD) {
145
+ return isLeft ? "ahead_left" : "ahead_right";
146
+ }
147
+ if (abs <= ORIENTATION_THRESHOLDS.BEHIND) {
148
+ return isLeft ? "left" : "right";
149
+ }
150
+ if (abs <= ORIENTATION_THRESHOLDS.DIRECTLY_BEHIND) {
151
+ return isLeft ? "behind_left" : "behind_right";
152
+ }
153
+ return "directly_behind";
154
+ }
155
+ function getOrientationDescription(orientation) {
156
+ const descriptions = {
157
+ // Simple
158
+ ahead: "ahead of you",
159
+ behind: "behind you",
160
+ left: "to your left",
161
+ right: "to your right",
162
+ // Compass
163
+ north: "to the north",
164
+ northeast: "to the northeast",
165
+ east: "to the east",
166
+ southeast: "to the southeast",
167
+ south: "to the south",
168
+ southwest: "to the southwest",
169
+ west: "to the west",
170
+ northwest: "to the northwest",
171
+ // Detailed
172
+ directly_ahead: "directly ahead",
173
+ ahead_left: "ahead on your left",
174
+ ahead_right: "ahead on your right",
175
+ behind_left: "behind you on the left",
176
+ behind_right: "behind you on the right",
177
+ directly_behind: "directly behind you"
178
+ };
179
+ return descriptions[orientation] ?? orientation;
180
+ }
181
+
182
+ // src/core/closest-point.ts
183
+ function getClosestPoint(user, poi) {
184
+ const distance = getDistance(user, poi);
185
+ const bearing = getBearing(user, poi);
186
+ const poiRadius = poi.radiusMeters ?? 0;
187
+ const isInside = distance <= poiRadius;
188
+ if (poiRadius === 0) {
189
+ return {
190
+ closestPoint: { latitude: poi.latitude, longitude: poi.longitude },
191
+ distanceMeters: Math.round(distance),
192
+ bearing,
193
+ isInside: false
194
+ };
195
+ }
196
+ if (isInside) {
197
+ return {
198
+ closestPoint: { latitude: poi.latitude, longitude: poi.longitude },
199
+ distanceMeters: 0,
200
+ bearing,
201
+ isInside: true
202
+ };
203
+ }
204
+ const distanceToEdge = distance - poiRadius;
205
+ const bearingFromPOIToUser = getBearing(poi, user);
206
+ const closestPoint = destinationPoint(poi, bearingFromPOIToUser, poiRadius);
207
+ return {
208
+ closestPoint,
209
+ distanceMeters: Math.round(distanceToEdge),
210
+ bearing,
211
+ isInside: false
212
+ };
213
+ }
214
+ function destinationPoint(start, bearing, distanceMeters) {
215
+ if (distanceMeters === 0) {
216
+ return { latitude: start.latitude, longitude: start.longitude };
217
+ }
218
+ const angularDistance = distanceMeters / EARTH_RADIUS_METERS;
219
+ const bearingRad = toRadians(bearing);
220
+ const lat1 = toRadians(start.latitude);
221
+ const lon1 = toRadians(start.longitude);
222
+ const lat2 = Math.asin(
223
+ Math.sin(lat1) * Math.cos(angularDistance) + Math.cos(lat1) * Math.sin(angularDistance) * Math.cos(bearingRad)
224
+ );
225
+ const lon2 = lon1 + Math.atan2(
226
+ Math.sin(bearingRad) * Math.sin(angularDistance) * Math.cos(lat1),
227
+ Math.cos(angularDistance) - Math.sin(lat1) * Math.sin(lat2)
228
+ );
229
+ return {
230
+ latitude: toDegrees(lat2),
231
+ longitude: normalizeLongitude(toDegrees(lon2))
232
+ };
233
+ }
234
+ function getDistanceToEdge(user, poi) {
235
+ const distance = getDistance(user, poi);
236
+ const poiRadius = poi.radiusMeters ?? 0;
237
+ return Math.max(0, distance - poiRadius);
238
+ }
239
+ function isWithinPOI(point, poi) {
240
+ const distance = getDistance(point, poi);
241
+ const poiRadius = poi.radiusMeters ?? 0;
242
+ return distance <= poiRadius;
243
+ }
244
+
245
+ // src/utils/formatting.ts
246
+ function formatDuration(seconds) {
247
+ if (!isFinite(seconds) || seconds < 0) {
248
+ return "Unknown";
249
+ }
250
+ if (seconds < 60) {
251
+ return `${Math.round(seconds)} sec`;
252
+ }
253
+ if (seconds < 3600) {
254
+ const minutes2 = Math.floor(seconds / 60);
255
+ const secs = Math.round(seconds % 60);
256
+ return secs > 0 ? `${minutes2} min ${secs} sec` : `${minutes2} min`;
257
+ }
258
+ const hours = Math.floor(seconds / 3600);
259
+ const minutes = Math.round(seconds % 3600 / 60);
260
+ return minutes > 0 ? `${hours} hr ${minutes} min` : `${hours} hr`;
261
+ }
262
+ function formatDurationShort(seconds) {
263
+ if (!isFinite(seconds) || seconds < 0) {
264
+ return "--";
265
+ }
266
+ if (seconds < 60) {
267
+ return `${Math.round(seconds)}s`;
268
+ }
269
+ if (seconds < 3600) {
270
+ return `${Math.round(seconds / 60)}m`;
271
+ }
272
+ const hours = Math.floor(seconds / 3600);
273
+ const minutes = Math.round(seconds % 3600 / 60);
274
+ return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
275
+ }
276
+ function formatDistance(meters) {
277
+ if (!isFinite(meters) || meters < 0) {
278
+ return "Unknown";
279
+ }
280
+ if (meters < 1e3) {
281
+ return `${Math.round(meters)} m`;
282
+ }
283
+ const km = meters / 1e3;
284
+ if (km < 10) {
285
+ return `${km.toFixed(1)} km`;
286
+ }
287
+ return `${Math.round(km)} km`;
288
+ }
289
+
290
+ // src/core/eta.ts
291
+ function getETA(user, poi, relativePosition) {
292
+ const distanceMeters = relativePosition?.distanceMeters ?? getDistance(user, poi);
293
+ const distanceToEdgeMeters = relativePosition?.distanceToEdgeMeters ?? getDistanceToEdge(user, poi);
294
+ const poiRadius = poi.radiusMeters ?? 0;
295
+ if (distanceMeters <= poiRadius) {
296
+ return {
297
+ etaSeconds: 0,
298
+ etaToEdgeSeconds: 0,
299
+ formatted: "At destination",
300
+ formattedShort: "0m",
301
+ isValid: false,
302
+ invalidReason: "at_destination"
303
+ };
304
+ }
305
+ if (user.speedMps === void 0 || user.speedMps === null) {
306
+ return {
307
+ etaSeconds: Infinity,
308
+ etaToEdgeSeconds: Infinity,
309
+ formatted: "Unknown",
310
+ formattedShort: "--",
311
+ isValid: false,
312
+ invalidReason: "no_speed_data"
313
+ };
314
+ }
315
+ if (user.speedMps < STATIONARY_SPEED_THRESHOLD) {
316
+ return {
317
+ etaSeconds: Infinity,
318
+ etaToEdgeSeconds: Infinity,
319
+ formatted: "Stationary",
320
+ formattedShort: "--",
321
+ isValid: false,
322
+ invalidReason: "stationary"
323
+ };
324
+ }
325
+ let relativeBearing = relativePosition?.relativeBearing;
326
+ if (relativeBearing === void 0 && user.bearing !== void 0) {
327
+ const absoluteBearing = getBearing(user, poi);
328
+ relativeBearing = getRelativeBearing(user.bearing, absoluteBearing);
329
+ }
330
+ if (relativeBearing !== void 0 && Math.abs(relativeBearing) > 90) {
331
+ return {
332
+ etaSeconds: Infinity,
333
+ etaToEdgeSeconds: Infinity,
334
+ formatted: "Moving away",
335
+ formattedShort: "--",
336
+ isValid: false,
337
+ invalidReason: "moving_away"
338
+ };
339
+ }
340
+ let effectiveSpeed = user.speedMps;
341
+ if (relativeBearing !== void 0) {
342
+ const angleRadians = toRadians(Math.abs(relativeBearing));
343
+ effectiveSpeed = user.speedMps * Math.cos(angleRadians);
344
+ }
345
+ if (effectiveSpeed <= 0) {
346
+ return {
347
+ etaSeconds: Infinity,
348
+ etaToEdgeSeconds: Infinity,
349
+ formatted: "Moving away",
350
+ formattedShort: "--",
351
+ isValid: false,
352
+ invalidReason: "moving_away"
353
+ };
354
+ }
355
+ const etaSeconds = distanceMeters / effectiveSpeed;
356
+ const etaToEdgeSeconds = distanceToEdgeMeters / effectiveSpeed;
357
+ return {
358
+ etaSeconds: Math.round(etaSeconds),
359
+ etaToEdgeSeconds: Math.round(etaToEdgeSeconds),
360
+ formatted: formatDuration(etaSeconds),
361
+ formattedShort: formatDurationShort(etaSeconds),
362
+ isValid: true
363
+ };
364
+ }
365
+ function getSimpleETA(distanceMeters, speedMps) {
366
+ if (speedMps <= 0 || !isFinite(speedMps)) {
367
+ return Infinity;
368
+ }
369
+ if (distanceMeters <= 0) {
370
+ return 0;
371
+ }
372
+ return distanceMeters / speedMps;
373
+ }
374
+
375
+ // src/core/relative-position.ts
376
+ function getRelativePosition(user, poi) {
377
+ const distanceMeters = getDistance(user, poi);
378
+ const distanceKm = distanceMeters / 1e3;
379
+ const distanceToEdgeMeters = getDistanceToEdge(user, poi);
380
+ const absoluteBearing = getBearing(user, poi);
381
+ const userBearing = user.bearing ?? 0;
382
+ const relativeBearing = getRelativeBearing(userBearing, absoluteBearing);
383
+ const simpleOrientation = getSimpleOrientation(relativeBearing);
384
+ const compassOrientation = getCompassOrientation(absoluteBearing);
385
+ const detailedOrientation = getDetailedOrientation(relativeBearing);
386
+ const poiRadius = poi.radiusMeters ?? 0;
387
+ const isWithinPOI2 = distanceMeters <= poiRadius;
388
+ const approachStatus = determineApproachStatus(relativeBearing, user.speedMps, user.bearing);
389
+ return {
390
+ distanceMeters: Math.round(distanceMeters),
391
+ distanceKm: roundTo(distanceKm, 2),
392
+ distanceToEdgeMeters: Math.round(distanceToEdgeMeters),
393
+ absoluteBearing: roundTo(absoluteBearing, 1),
394
+ relativeBearing: roundTo(relativeBearing, 1),
395
+ simpleOrientation,
396
+ compassOrientation,
397
+ detailedOrientation,
398
+ isWithinPOI: isWithinPOI2,
399
+ approachStatus
400
+ };
401
+ }
402
+ function determineApproachStatus(relativeBearing, speedMps, userBearing) {
403
+ if (userBearing === void 0) {
404
+ return "unknown";
405
+ }
406
+ if (speedMps === void 0 || speedMps === null) {
407
+ return "unknown";
408
+ }
409
+ if (speedMps < STATIONARY_SPEED_THRESHOLD) {
410
+ return "stationary";
411
+ }
412
+ const abs = Math.abs(relativeBearing);
413
+ if (abs <= 90) {
414
+ return "approaching";
415
+ }
416
+ return "receding";
417
+ }
418
+
419
+ // src/batch/sort.ts
420
+ function sortByDistance(user, pois, config = { by: "distance", direction: "asc" }) {
421
+ const results = pois.map((poi) => {
422
+ const relativePosition = getRelativePosition(user, poi);
423
+ return {
424
+ ...poi,
425
+ relativePosition,
426
+ eta: user.speedMps !== void 0 ? getETA(user, poi, relativePosition) : void 0
427
+ };
428
+ });
429
+ return results.sort((a, b) => {
430
+ let valueA;
431
+ let valueB;
432
+ switch (config.by) {
433
+ case "distance":
434
+ valueA = a.relativePosition.distanceMeters;
435
+ valueB = b.relativePosition.distanceMeters;
436
+ break;
437
+ case "eta":
438
+ valueA = a.eta?.etaSeconds ?? Infinity;
439
+ valueB = b.eta?.etaSeconds ?? Infinity;
440
+ break;
441
+ case "relativeBearing":
442
+ valueA = Math.abs(a.relativePosition.relativeBearing);
443
+ valueB = Math.abs(b.relativePosition.relativeBearing);
444
+ break;
445
+ default:
446
+ valueA = a.relativePosition.distanceMeters;
447
+ valueB = b.relativePosition.distanceMeters;
448
+ }
449
+ return config.direction === "asc" ? valueA - valueB : valueB - valueA;
450
+ });
451
+ }
452
+ function getNearestPOIs(user, pois, count) {
453
+ return sortByDistance(user, pois, { by: "distance", direction: "asc" }).slice(0, count);
454
+ }
455
+ function getNextArrivals(user, pois, count) {
456
+ return sortByDistance(user, pois, { by: "eta", direction: "asc" }).filter((poi) => poi.eta?.isValid).slice(0, count);
457
+ }
458
+
459
+ // src/batch/filter.ts
460
+ function filterByProximity(user, pois, filter) {
461
+ return pois.map((poi) => {
462
+ const relativePosition = getRelativePosition(user, poi);
463
+ return {
464
+ ...poi,
465
+ relativePosition,
466
+ eta: user.speedMps !== void 0 ? getETA(user, poi, relativePosition) : void 0
467
+ };
468
+ }).filter((poi) => {
469
+ const distance = poi.relativePosition.distanceMeters;
470
+ if (distance > filter.maxDistanceMeters) {
471
+ return false;
472
+ }
473
+ if (filter.minDistanceMeters !== void 0 && distance < filter.minDistanceMeters) {
474
+ return false;
475
+ }
476
+ if (filter.approachingOnly && poi.relativePosition.approachStatus !== "approaching") {
477
+ return false;
478
+ }
479
+ return true;
480
+ }).sort((a, b) => a.relativePosition.distanceMeters - b.relativePosition.distanceMeters);
481
+ }
482
+ function filterByCone(user, pois, cone) {
483
+ return pois.map((poi) => {
484
+ const relativePosition = getRelativePosition(user, poi);
485
+ return {
486
+ ...poi,
487
+ relativePosition,
488
+ eta: user.speedMps !== void 0 ? getETA(user, poi, relativePosition) : void 0
489
+ };
490
+ }).filter((poi) => {
491
+ const distance = poi.relativePosition.distanceMeters;
492
+ const relativeBearing = Math.abs(poi.relativePosition.relativeBearing);
493
+ if (distance > cone.maxDistanceMeters) {
494
+ return false;
495
+ }
496
+ if (cone.minDistanceMeters !== void 0 && distance < cone.minDistanceMeters) {
497
+ return false;
498
+ }
499
+ if (relativeBearing > cone.halfAngle) {
500
+ return false;
501
+ }
502
+ return true;
503
+ }).sort((a, b) => a.relativePosition.distanceMeters - b.relativePosition.distanceMeters);
504
+ }
505
+ function filterInsidePOIs(user, pois) {
506
+ return pois.map((poi) => {
507
+ const relativePosition = getRelativePosition(user, poi);
508
+ return {
509
+ ...poi,
510
+ relativePosition,
511
+ eta: void 0
512
+ // ETA doesn't make sense when inside
513
+ };
514
+ }).filter((poi) => poi.relativePosition.isWithinPOI);
515
+ }
516
+ function filterByOrientation(user, pois, orientations) {
517
+ return pois.map((poi) => {
518
+ const relativePosition = getRelativePosition(user, poi);
519
+ return {
520
+ ...poi,
521
+ relativePosition,
522
+ eta: user.speedMps !== void 0 ? getETA(user, poi, relativePosition) : void 0
523
+ };
524
+ }).filter((poi) => orientations.includes(poi.relativePosition.simpleOrientation)).sort((a, b) => a.relativePosition.distanceMeters - b.relativePosition.distanceMeters);
525
+ }
526
+
527
+ // src/utils/validation.ts
528
+ function isValidCoordinates(coords) {
529
+ return typeof coords.latitude === "number" && typeof coords.longitude === "number" && !isNaN(coords.latitude) && !isNaN(coords.longitude) && isFinite(coords.latitude) && isFinite(coords.longitude) && coords.latitude >= -90 && coords.latitude <= 90 && coords.longitude >= -180 && coords.longitude <= 180;
530
+ }
531
+ function isValidBearing(bearing) {
532
+ if (bearing === void 0) {
533
+ return true;
534
+ }
535
+ return typeof bearing === "number" && !isNaN(bearing) && isFinite(bearing);
536
+ }
537
+ function isValidSpeed(speed) {
538
+ if (speed === void 0) {
539
+ return true;
540
+ }
541
+ return typeof speed === "number" && !isNaN(speed) && isFinite(speed) && speed >= 0;
542
+ }
543
+ function isValidRadius(radius) {
544
+ if (radius === void 0) {
545
+ return true;
546
+ }
547
+ return typeof radius === "number" && !isNaN(radius) && isFinite(radius) && radius >= 0;
548
+ }
549
+ function validateUserPosition(user) {
550
+ if (!isValidCoordinates(user)) {
551
+ return { isValid: false, error: "Invalid coordinates: latitude must be -90 to 90, longitude must be -180 to 180" };
552
+ }
553
+ if (!isValidBearing(user.bearing)) {
554
+ return { isValid: false, error: "Invalid bearing: must be a finite number" };
555
+ }
556
+ if (!isValidSpeed(user.speedMps)) {
557
+ return { isValid: false, error: "Invalid speed: must be a non-negative number" };
558
+ }
559
+ return { isValid: true };
560
+ }
561
+ function validatePOI(poi) {
562
+ if (!isValidCoordinates(poi)) {
563
+ return { isValid: false, error: "Invalid coordinates: latitude must be -90 to 90, longitude must be -180 to 180" };
564
+ }
565
+ if (!isValidRadius(poi.radiusMeters)) {
566
+ return { isValid: false, error: "Invalid radius: must be a non-negative number" };
567
+ }
568
+ return { isValid: true };
569
+ }
570
+
571
+ // src/index.ts
572
+ function convertSpeed(value, from, to) {
573
+ let mps;
574
+ switch (from) {
575
+ case "mps":
576
+ mps = value;
577
+ break;
578
+ case "kmh":
579
+ mps = value / 3.6;
580
+ break;
581
+ case "mph":
582
+ mps = value / 2.237;
583
+ break;
584
+ }
585
+ switch (to) {
586
+ case "mps":
587
+ return mps;
588
+ case "kmh":
589
+ return mps * 3.6;
590
+ case "mph":
591
+ return mps * 2.237;
592
+ }
593
+ }
594
+ export {
595
+ COMPASS_BOUNDARIES,
596
+ EARTH_RADIUS_METERS,
597
+ ORIENTATION_THRESHOLDS,
598
+ STATIONARY_SPEED_THRESHOLD,
599
+ clamp,
600
+ convertSpeed,
601
+ destinationPoint,
602
+ filterByCone,
603
+ filterByOrientation,
604
+ filterByProximity,
605
+ filterInsidePOIs,
606
+ formatDistance,
607
+ formatDuration,
608
+ formatDurationShort,
609
+ getBearing,
610
+ getClosestPoint,
611
+ getCompassOrientation,
612
+ getDetailedOrientation,
613
+ getDistance,
614
+ getDistanceKm,
615
+ getDistanceToEdge,
616
+ getETA,
617
+ getFinalBearing,
618
+ getNearestPOIs,
619
+ getNextArrivals,
620
+ getOrientationDescription,
621
+ getRelativeBearing,
622
+ getRelativePosition,
623
+ getSimpleETA,
624
+ getSimpleOrientation,
625
+ isValidBearing,
626
+ isValidCoordinates,
627
+ isValidRadius,
628
+ isValidSpeed,
629
+ isWithinPOI,
630
+ normalizeAngle,
631
+ normalizeLongitude,
632
+ roundTo,
633
+ sortByDistance,
634
+ toDegrees,
635
+ toRadians,
636
+ validatePOI,
637
+ validateUserPosition
638
+ };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "geo-relative-position",
3
+ "version": "1.0.0",
4
+ "description": "Stateless library for calculating relative positions and navigation metrics between users and POIs",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "require": "./dist/index.js",
12
+ "import": "./dist/index.mjs"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "lint": "eslint src --ext .ts",
23
+ "prepublishOnly": "npm run build && npm test",
24
+ "publish:npm": "npm publish --access public"
25
+ },
26
+ "keywords": [
27
+ "geo",
28
+ "gps",
29
+ "location",
30
+ "distance",
31
+ "bearing",
32
+ "navigation",
33
+ "haversine",
34
+ "poi",
35
+ "coordinates",
36
+ "geolocation"
37
+ ],
38
+ "author": "Paul Aschmann",
39
+ "license": "MIT",
40
+ "devDependencies": {
41
+ "@types/node": "^20.0.0",
42
+ "tsup": "^8.0.0",
43
+ "typescript": "^5.0.0",
44
+ "vitest": "^1.0.0"
45
+ },
46
+ "engines": {
47
+ "node": ">=16.0.0"
48
+ }
49
+ }