Skip to content

Commit f4bb7b7

Browse files
feat: ✨ adds geolocation utils and rollbar (#13)
- Adds `haversine` and `centroid` calculation equations for location points. - Adds the `rollbarNative` logging system and puts the access token on the `.env` file.
1 parent 7347516 commit f4bb7b7

File tree

4 files changed

+179
-0
lines changed

4 files changed

+179
-0
lines changed

constants.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const ROLLBAR_ACCESS_TOKEN = process.env.EXPO_PUBLIC_ROLLBAR_ACCESS_TOKEN;
2+
3+
export const ROLLBAR_ENV = process.env.EXPO_PUBLIC_ROLLBAR_ENV;
4+
5+
export const MAPBOX_ACCESS_TOKEN = process.env.EXPO_PUBLIC_MAPBOX_ACCESS_TOKEN;
6+
7+
export const EARTH_RADIUS_METERS = 6371e3;

types/geolocation.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
export interface ICoordinates {
2+
latitude: number;
3+
longitude: number;
4+
}
5+
6+
/**
7+
* Interface for Mapbox Geocoding API feature response
8+
*/
9+
export interface IMapboxFeature {
10+
id: string;
11+
type: string;
12+
place_type: string[];
13+
relevance: number;
14+
properties: Record<string, any>;
15+
text: string;
16+
place_name: string;
17+
center: [number, number];
18+
geometry: {
19+
type: string;
20+
coordinates: [number, number];
21+
};
22+
context: Array<{
23+
id: string;
24+
text: string;
25+
wikidata?: string;
26+
short_code?: string;
27+
}>;
28+
}

utils/geolocations/utils.ts

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { EARTH_RADIUS_METERS, MAPBOX_ACCESS_TOKEN } from '@htk/constants';
2+
import { ICoordinates, IMapboxFeature } from '@htk/types/geolocation';
3+
import { rollbarNative } from '@htk/utils/rollbar';
4+
import axios from 'axios';
5+
6+
/**
7+
* Calculates the great-circle distance between two points on a sphere using the Haversine formula.
8+
* This provides the shortest distance over the earth's surface between two points.
9+
* The formula accounts for the earth's spherical shape and is accurate for most practical purposes.
10+
*
11+
* Note: This calculation assumes a spherical Earth, which is accurate enough for most applications
12+
* (error margin < 0.3% due to Earth's actual ellipsoidal shape).
13+
*
14+
* @param lat1 - Latitude of the first point in decimal degrees
15+
* @param lon1 - Longitude of the first point in decimal degrees
16+
* @param lat2 - Latitude of the second point in decimal degrees
17+
* @param lon2 - Longitude of the second point in decimal degrees
18+
* @returns Distance between the points in meters
19+
*/
20+
export function haversineDistanceMeters(
21+
lat1: number,
22+
lon1: number,
23+
lat2: number,
24+
lon2: number
25+
): number {
26+
const R = EARTH_RADIUS_METERS;
27+
const φ1 = (lat1 * Math.PI) / 180;
28+
const φ2 = (lat2 * Math.PI) / 180;
29+
const Δφ = ((lat2 - lat1) * Math.PI) / 180;
30+
const Δλ = ((lon2 - lon1) * Math.PI) / 180;
31+
32+
const a =
33+
Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
34+
Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
35+
36+
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
37+
38+
return R * c;
39+
}
40+
41+
/**
42+
* Calculates the centroid (geometric center) of a set of geographical coordinates.
43+
* Uses a cartesian average method converted back to lat/long coordinates.
44+
* This provides a reasonable approximation for most use cases where points are relatively close together.
45+
* Returns null for empty arrays.
46+
*
47+
* @param trackPoints - Array of track points with lat/long coordinates
48+
* @returns ICoordinates containing the centroid's latitude and longitude, or null if array is empty
49+
*/
50+
export function calculate_centroid(trackPoints: ICoordinates[]): ICoordinates | null {
51+
let coordinates: ICoordinates | null;
52+
53+
if (trackPoints.length === 0) {
54+
coordinates = null;
55+
} else if (trackPoints.length === 1) {
56+
coordinates = {
57+
latitude: trackPoints[0].latitude,
58+
longitude: trackPoints[0].longitude,
59+
};
60+
} else {
61+
// Convert lat/long to cartesian coordinates
62+
const { x, y, z } = trackPoints.reduce(
63+
(acc, point) => {
64+
// Convert to radians
65+
const lat = (point.latitude * Math.PI) / 180;
66+
const lon = (point.longitude * Math.PI) / 180;
67+
68+
// Convert to cartesian coordinates
69+
return {
70+
x: acc.x + Math.cos(lat) * Math.cos(lon),
71+
y: acc.y + Math.cos(lat) * Math.sin(lon),
72+
z: acc.z + Math.sin(lat),
73+
};
74+
},
75+
{ x: 0, y: 0, z: 0 }
76+
);
77+
78+
// Calculate averages
79+
const avgX = x / trackPoints.length;
80+
const avgY = y / trackPoints.length;
81+
const avgZ = z / trackPoints.length;
82+
83+
// Convert back to lat/long
84+
const lon = Math.atan2(avgY, avgX);
85+
const hyp = Math.sqrt(avgX * avgX + avgY * avgY);
86+
const lat = Math.atan2(avgZ, hyp);
87+
88+
coordinates = {
89+
latitude: (lat * 180) / Math.PI,
90+
longitude: (lon * 180) / Math.PI,
91+
};
92+
}
93+
94+
return coordinates;
95+
}
96+
97+
/**
98+
* Reverse geocodes coordinates using Mapbox's Geocoding API and returns the raw feature object
99+
* @param location - The coordinates to reverse geocode
100+
* @returns Promise with the raw Mapbox feature object, or null if no results found or if access token is missing
101+
*
102+
* Returns the first (most accurate) feature from Mapbox's reverse geocoding response.
103+
* The raw feature contains detailed location information that can be parsed as needed.
104+
* If the Mapbox access token is not configured, logs to Rollbar and returns null.
105+
*/
106+
export const reverseGeocode = async (
107+
location: ICoordinates
108+
): Promise<IMapboxFeature | null> => {
109+
let result: IMapboxFeature | null = null;
110+
111+
if (MAPBOX_ACCESS_TOKEN) {
112+
try {
113+
const response = await axios.get<{ features: IMapboxFeature[] }>(
114+
`https://api.mapbox.com/geocoding/v5/mapbox.places/${location.longitude},${location.latitude}.json?access_token=${MAPBOX_ACCESS_TOKEN}`
115+
);
116+
result = response.data.features[0] || null;
117+
} catch (error) {
118+
rollbarNative.error('Reverse geocoding failed', {
119+
error,
120+
where: 'reverseGeocode',
121+
coordinates: location,
122+
});
123+
}
124+
} else {
125+
rollbarNative.error('Mapbox access token is not configured', {
126+
where: 'reverseGeocode',
127+
coordinates: location,
128+
});
129+
}
130+
131+
return result;
132+
};

utils/rollbar.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { ROLLBAR_ACCESS_TOKEN, ROLLBAR_ENV } from '@htk/constants';
2+
import { Client } from 'rollbar-react-native';
3+
4+
export const rollbarNative = new Client({
5+
accessToken: ROLLBAR_ACCESS_TOKEN || '',
6+
environment: ROLLBAR_ENV || 'development',
7+
captureUncaught: true,
8+
captureUnhandledRejections: true,
9+
captureDeviceInfo: true,
10+
});
11+
12+
export const rollbar = rollbarNative.rollbar;

0 commit comments

Comments
 (0)