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