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 +274 -0
- package/dist/index.d.mts +604 -0
- package/dist/index.d.ts +604 -0
- package/dist/index.js +707 -0
- package/dist/index.mjs +638 -0
- package/package.json +49 -0
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
|