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/README.md ADDED
@@ -0,0 +1,274 @@
1
+ # geo-relative-position
2
+
3
+ A stateless Node.js/TypeScript library for calculating relative positions and navigation metrics between users and Points of Interest (POIs).
4
+
5
+ ## Features
6
+
7
+ - **Distance Calculation**: Haversine formula for accurate great-circle distances
8
+ - **Bearing Calculation**: Absolute and relative bearings
9
+ - **Orientation**: Simple (4-way), compass (8-way), and detailed (8-way with nuance)
10
+ - **ETA Calculation**: Estimated time of arrival with approach angle adjustment
11
+ - **POI Boundaries**: Support for POIs with radius (parks, buildings, etc.)
12
+ - **Batch Operations**: Sort and filter multiple POIs efficiently
13
+ - **TypeScript**: Full type definitions included
14
+ - **Zero Dependencies**: No external runtime dependencies
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install geo-relative-position
20
+ ```
21
+
22
+ ## Quick Start
23
+
24
+ ```typescript
25
+ import { getRelativePosition, getETA, filterByCone } from 'geo-relative-position';
26
+
27
+ // User's current position and motion
28
+ const user = {
29
+ latitude: 37.7749,
30
+ longitude: -122.4194,
31
+ bearing: 45, // Heading northeast
32
+ speedMps: 13.4 // ~30 mph
33
+ };
34
+
35
+ // A point of interest
36
+ const poi = {
37
+ latitude: 37.7849,
38
+ longitude: -122.4094,
39
+ radiusMeters: 100,
40
+ name: 'Golden Gate Park'
41
+ };
42
+
43
+ // Get complete relative position
44
+ const position = getRelativePosition(user, poi);
45
+
46
+ console.log(position.distanceMeters); // 1234
47
+ console.log(position.simpleOrientation); // 'ahead'
48
+ console.log(position.detailedOrientation); // 'ahead_right'
49
+ console.log(position.approachStatus); // 'approaching'
50
+
51
+ // Get ETA
52
+ const eta = getETA(user, poi);
53
+ console.log(eta.formatted); // "2 min 30 sec"
54
+ console.log(eta.formattedShort); // "2m"
55
+ ```
56
+
57
+ ## API Reference
58
+
59
+ ### Core Functions
60
+
61
+ #### `getRelativePosition(user, poi)`
62
+
63
+ Calculate complete relative position between user and POI.
64
+
65
+ ```typescript
66
+ const position = getRelativePosition(user, poi);
67
+ // Returns: RelativePosition
68
+ ```
69
+
70
+ **Returns:**
71
+ - `distanceMeters` - Distance to POI center in meters
72
+ - `distanceKm` - Distance in kilometers
73
+ - `distanceToEdgeMeters` - Distance to POI edge (if radius defined)
74
+ - `absoluteBearing` - Bearing from user to POI (0-360°)
75
+ - `relativeBearing` - Bearing relative to user's heading (-180° to 180°)
76
+ - `simpleOrientation` - `'ahead' | 'behind' | 'left' | 'right'`
77
+ - `compassOrientation` - `'north' | 'northeast' | 'east' | ...`
78
+ - `detailedOrientation` - `'directly_ahead' | 'ahead_left' | ...`
79
+ - `isWithinPOI` - Whether user is inside POI boundary
80
+ - `approachStatus` - `'approaching' | 'receding' | 'stationary' | 'unknown'`
81
+
82
+ #### `getDistance(from, to)`
83
+
84
+ Calculate great-circle distance using Haversine formula.
85
+
86
+ ```typescript
87
+ const meters = getDistance(
88
+ { latitude: 37.7749, longitude: -122.4194 },
89
+ { latitude: 34.0522, longitude: -118.2437 }
90
+ );
91
+ // Returns: 559120 (SF to LA in meters)
92
+ ```
93
+
94
+ #### `getBearing(from, to)`
95
+
96
+ Calculate initial bearing (forward azimuth) between two points.
97
+
98
+ ```typescript
99
+ const bearing = getBearing(
100
+ { latitude: 37.7749, longitude: -122.4194 },
101
+ { latitude: 34.0522, longitude: -118.2437 }
102
+ );
103
+ // Returns: 136 (degrees, 0 = North)
104
+ ```
105
+
106
+ #### `getETA(user, poi, relativePosition?)`
107
+
108
+ Calculate estimated time of arrival.
109
+
110
+ ```typescript
111
+ const eta = getETA(user, poi);
112
+ // Returns: ETAResult
113
+
114
+ if (eta.isValid) {
115
+ console.log(`Arriving in ${eta.formatted}`);
116
+ } else {
117
+ console.log(eta.invalidReason); // 'stationary', 'moving_away', etc.
118
+ }
119
+ ```
120
+
121
+ #### `getClosestPoint(user, poi)`
122
+
123
+ Find the closest point on a POI's boundary.
124
+
125
+ ```typescript
126
+ const closest = getClosestPoint(user, poi);
127
+ // Returns: { closestPoint, distanceMeters, bearing, isInside }
128
+ ```
129
+
130
+ ### Batch Operations
131
+
132
+ #### `sortByDistance(user, pois, config?)`
133
+
134
+ Sort POIs by distance, ETA, or relative bearing.
135
+
136
+ ```typescript
137
+ const sorted = sortByDistance(user, pois, { by: 'distance', direction: 'asc' });
138
+ ```
139
+
140
+ #### `filterByProximity(user, pois, filter)`
141
+
142
+ Filter POIs by distance range.
143
+
144
+ ```typescript
145
+ const nearby = filterByProximity(user, pois, {
146
+ maxDistanceMeters: 5000,
147
+ minDistanceMeters: 100,
148
+ approachingOnly: true
149
+ });
150
+ ```
151
+
152
+ #### `filterByCone(user, pois, cone)`
153
+
154
+ Filter POIs within a directional cone (useful for "what's ahead" queries).
155
+
156
+ ```typescript
157
+ const ahead = filterByCone(user, pois, {
158
+ centerAngle: user.bearing,
159
+ halfAngle: 30, // 60° total cone
160
+ maxDistanceMeters: 5000
161
+ });
162
+ ```
163
+
164
+ ### Utility Functions
165
+
166
+ ```typescript
167
+ // Unit conversions
168
+ convertSpeed(60, 'kmh', 'mps'); // 16.67 m/s
169
+
170
+ // Formatting
171
+ formatDistance(1500); // "1.5 km"
172
+ formatDuration(150); // "2 min 30 sec"
173
+ formatDurationShort(150); // "2m"
174
+
175
+ // Validation
176
+ isValidCoordinates({ latitude: 37.7749, longitude: -122.4194 }); // true
177
+ validateUserPosition(user); // { isValid: true } or { isValid: false, error: "..." }
178
+ ```
179
+
180
+ ## Types
181
+
182
+ ```typescript
183
+ interface Coordinates {
184
+ latitude: number; // -90 to 90
185
+ longitude: number; // -180 to 180
186
+ }
187
+
188
+ interface UserPosition extends Coordinates {
189
+ bearing?: number; // 0-360, 0 = North
190
+ speedMps?: number; // meters per second
191
+ accuracy?: number; // GPS accuracy in meters
192
+ }
193
+
194
+ interface POI extends Coordinates {
195
+ id?: string | number;
196
+ name?: string;
197
+ radiusMeters?: number; // For large POIs
198
+ metadata?: Record<string, unknown>;
199
+ }
200
+ ```
201
+
202
+ ## Use Cases
203
+
204
+ ### Mapping App (Turn-by-Turn)
205
+
206
+ ```typescript
207
+ const upcoming = filterByCone(user, allPOIs, {
208
+ centerAngle: user.bearing,
209
+ halfAngle: 30,
210
+ maxDistanceMeters: 5000
211
+ });
212
+
213
+ if (upcoming.length > 0) {
214
+ const next = upcoming[0];
215
+ console.log(`In ${next.eta?.formattedShort}, ${next.name} on your ${next.relativePosition.simpleOrientation}`);
216
+ }
217
+ ```
218
+
219
+ ### Game (Proximity Triggers)
220
+
221
+ ```typescript
222
+ function checkTrigger(user, poi, triggerRadius) {
223
+ const position = getRelativePosition(user, poi);
224
+
225
+ if (position.isWithinPOI || position.distanceToEdgeMeters <= triggerRadius) {
226
+ return { triggered: true, message: `You discovered ${poi.name}!` };
227
+ }
228
+
229
+ return { triggered: false, distanceRemaining: position.distanceToEdgeMeters };
230
+ }
231
+ ```
232
+
233
+ ### Tour Guide (Contextual Narration)
234
+
235
+ ```typescript
236
+ const nearest = getNearestPOIs(user, pois, 1)[0];
237
+ const { detailedOrientation } = nearest.relativePosition;
238
+
239
+ const prefix = {
240
+ directly_ahead: "Straight ahead,",
241
+ ahead_left: "On your left,",
242
+ ahead_right: "On your right,",
243
+ // ...
244
+ }[detailedOrientation];
245
+
246
+ console.log(`${prefix} ${nearest.name} is ${nearest.relativePosition.distanceMeters}m away.`);
247
+ ```
248
+
249
+ ### Delivery App (ETA)
250
+
251
+ ```typescript
252
+ const eta = getETA(driver, destination);
253
+
254
+ return {
255
+ distanceKm: eta.distanceKm,
256
+ etaMinutes: Math.round(eta.etaSeconds / 60),
257
+ status: eta.isValid ? 'en_route' : eta.invalidReason
258
+ };
259
+ ```
260
+
261
+ ## Edge Cases
262
+
263
+ The library handles various edge cases:
264
+
265
+ - **Same location**: Returns distance 0, bearing 0
266
+ - **Polar coordinates**: Haversine formula works correctly
267
+ - **International date line**: Longitude wraparound handled
268
+ - **Stationary user**: Speed < 0.5 m/s considered stationary
269
+ - **No bearing**: Defaults to 0 (north), returns 'unknown' approach status
270
+ - **Large POIs**: Use `radiusMeters` to define boundaries
271
+
272
+ ## License
273
+
274
+ MIT