geo-relative-position
v1.0.3
Published
Stateless library for calculating relative positions and navigation metrics between users and POIs
Maintainers
Readme
geo-relative-position
A stateless Node.js/TypeScript library for calculating relative positions and navigation metrics between users and Points of Interest (POIs).
Features
- Distance Calculation: Haversine formula for accurate great-circle distances
- Bearing Calculation: Absolute and relative bearings
- Orientation: Simple (4-way), compass (8-way), and detailed (8-way with nuance)
- ETA Calculation: Estimated time of arrival with approach angle adjustment
- POI Boundaries: Support for POIs with radius (parks, buildings, etc.)
- Batch Operations: Sort and filter multiple POIs efficiently
- TypeScript: Full type definitions included
- Zero Dependencies: No external runtime dependencies
Installation
npm install geo-relative-positionQuick Start
import { getRelativePosition, getETA, filterByCone } from 'geo-relative-position';
// User's current position and motion
const user = {
latitude: 37.7749,
longitude: -122.4194,
bearing: 45, // Heading northeast
speedMps: 13.4 // ~30 mph
};
// A point of interest
const poi = {
latitude: 37.7849,
longitude: -122.4094,
radiusMeters: 100,
name: 'Golden Gate Park'
};
// Get complete relative position
const position = getRelativePosition(user, poi);
console.log(position.distanceMeters); // 1234
console.log(position.simpleOrientation); // 'ahead'
console.log(position.detailedOrientation); // 'ahead_right'
console.log(position.approachStatus); // 'approaching'
// Get ETA
const eta = getETA(user, poi);
console.log(eta.formatted); // "2 min 30 sec"
console.log(eta.formattedShort); // "2m"API Reference
Core Functions
getRelativePosition(user, poi)
Calculate complete relative position between user and POI.
const position = getRelativePosition(user, poi);
// Returns: RelativePositionReturns:
distanceMeters- Distance to POI center in metersdistanceKm- Distance in kilometersdistanceToEdgeMeters- Distance to POI edge (if radius defined)absoluteBearing- Bearing from user to POI (0-360°)relativeBearing- Bearing relative to user's heading (-180° to 180°)simpleOrientation-'ahead' | 'behind' | 'left' | 'right'compassOrientation-'north' | 'northeast' | 'east' | ...detailedOrientation-'directly_ahead' | 'ahead_left' | ...isWithinPOI- Whether user is inside POI boundaryapproachStatus-'approaching' | 'receding' | 'stationary' | 'unknown'
getDistance(from, to)
Calculate great-circle distance using Haversine formula.
const meters = getDistance(
{ latitude: 37.7749, longitude: -122.4194 },
{ latitude: 34.0522, longitude: -118.2437 }
);
// Returns: 559120 (SF to LA in meters)getBearing(from, to)
Calculate initial bearing (forward azimuth) between two points.
const bearing = getBearing(
{ latitude: 37.7749, longitude: -122.4194 },
{ latitude: 34.0522, longitude: -118.2437 }
);
// Returns: 136 (degrees, 0 = North)getETA(user, poi, relativePosition?)
Calculate estimated time of arrival.
const eta = getETA(user, poi);
// Returns: ETAResult
if (eta.isValid) {
console.log(`Arriving in ${eta.formatted}`);
} else {
console.log(eta.invalidReason); // 'stationary', 'moving_away', etc.
}getClosestPoint(user, poi)
Find the closest point on a POI's boundary.
const closest = getClosestPoint(user, poi);
// Returns: { closestPoint, distanceMeters, bearing, isInside }Batch Operations
sortByDistance(user, pois, config?)
Sort POIs by distance, ETA, or relative bearing.
const sorted = sortByDistance(user, pois, { by: 'distance', direction: 'asc' });filterByProximity(user, pois, filter)
Filter POIs by distance range.
const nearby = filterByProximity(user, pois, {
maxDistanceMeters: 5000,
minDistanceMeters: 100,
approachingOnly: true
});filterByCone(user, pois, cone)
Filter POIs within a directional cone (useful for "what's ahead" queries).
const ahead = filterByCone(user, pois, {
centerAngle: user.bearing,
halfAngle: 30, // 60° total cone
maxDistanceMeters: 5000
});Utility Functions
// Unit conversions
convertSpeed(60, 'kmh', 'mps'); // 16.67 m/s
// Formatting
formatDistance(1500); // "1.5 km"
formatDuration(150); // "2 min 30 sec"
formatDurationShort(150); // "2m"
// Validation
isValidCoordinates({ latitude: 37.7749, longitude: -122.4194 }); // true
validateUserPosition(user); // { isValid: true } or { isValid: false, error: "..." }Types
interface Coordinates {
latitude: number; // -90 to 90
longitude: number; // -180 to 180
}
interface UserPosition extends Coordinates {
bearing?: number; // 0-360, 0 = North
speedMps?: number; // meters per second
accuracy?: number; // GPS accuracy in meters
}
interface POI extends Coordinates {
id?: string | number;
name?: string;
radiusMeters?: number; // For large POIs
metadata?: Record<string, unknown>;
}Use Cases
Mapping App (Turn-by-Turn)
const upcoming = filterByCone(user, allPOIs, {
centerAngle: user.bearing,
halfAngle: 30,
maxDistanceMeters: 5000
});
if (upcoming.length > 0) {
const next = upcoming[0];
console.log(`In ${next.eta?.formattedShort}, ${next.name} on your ${next.relativePosition.simpleOrientation}`);
}Game (Proximity Triggers)
function checkTrigger(user, poi, triggerRadius) {
const position = getRelativePosition(user, poi);
if (position.isWithinPOI || position.distanceToEdgeMeters <= triggerRadius) {
return { triggered: true, message: `You discovered ${poi.name}!` };
}
return { triggered: false, distanceRemaining: position.distanceToEdgeMeters };
}Tour Guide (Contextual Narration)
const nearest = getNearestPOIs(user, pois, 1)[0];
const { detailedOrientation } = nearest.relativePosition;
const prefix = {
directly_ahead: "Straight ahead,",
ahead_left: "On your left,",
ahead_right: "On your right,",
// ...
}[detailedOrientation];
console.log(`${prefix} ${nearest.name} is ${nearest.relativePosition.distanceMeters}m away.`);Delivery App (ETA)
const eta = getETA(driver, destination);
return {
distanceKm: eta.distanceKm,
etaMinutes: Math.round(eta.etaSeconds / 60),
status: eta.isValid ? 'en_route' : eta.invalidReason
};Edge Cases
The library handles various edge cases:
- Same location: Returns distance 0, bearing 0
- Polar coordinates: Haversine formula works correctly
- International date line: Longitude wraparound handled
- Stationary user: Speed < 0.5 m/s considered stationary
- No bearing: Defaults to 0 (north), returns 'unknown' approach status
- Large POIs: Use
radiusMetersto define boundaries
License
MIT
