Created
December 8, 2024 15:13
-
-
Save ptts/8514cfa4cf3ae73d8b126d5325c8e519 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useQuery } from "@tanstack/react-query"; | |
export const GeoLocationErrorCode = { | |
NOT_SUPPORTED: "NOT_SUPPORTED", | |
PERMISSION_DENIED: "PERMISSION_DENIED", | |
POSITION_UNAVAILABLE: "POSITION_UNAVAILABLE", | |
TIMEOUT: "TIMEOUT", | |
UNKNOWN: "UNKNOWN", | |
} as const; | |
type GeoLocationErrorCode = | |
(typeof GeoLocationErrorCode)[keyof typeof GeoLocationErrorCode]; | |
class GeoLocationError extends Error { | |
public code: GeoLocationErrorCode; | |
constructor(code: GeoLocationErrorCode) { | |
super(code); | |
this.name = "GeoLocationError"; | |
this.code = code; | |
} | |
} | |
export function useGeoLocation({ | |
options, | |
}: { options?: PositionOptions } = {}) { | |
const query = useQuery<GeolocationPosition, GeoLocationError>({ | |
queryKey: ["geolocation", options], | |
staleTime: 1000 * 60 * 30, // 30 minutes | |
queryFn: () => getCurrentPosition(), | |
enabled: false, | |
}); | |
const isSupported = checkIsGeolocationSupported(); | |
const requestLocation = () => query.refetch(); | |
const calculateDistanceToCurrentPosition = ({ | |
lat, | |
lon, | |
}: { | |
lat: number; | |
lon: number; | |
}) => { | |
if (!query.data) { | |
return null; | |
} | |
return calculateDistance( | |
query.data.coords.latitude, | |
query.data.coords.longitude, | |
lat, | |
lon, | |
); | |
}; | |
return { | |
query, | |
requestLocation, | |
isSupported, | |
calculateDistanceToCurrentPosition, | |
}; | |
} | |
function checkIsGeolocationSupported() { | |
return typeof navigator !== "undefined" && !!navigator.geolocation; | |
} | |
/** | |
* Returns the distance between two points in kilometers | |
*/ | |
function calculateDistance( | |
lat1: number, | |
lon1: number, | |
lat2: number, | |
lon2: number, | |
): number { | |
const toRad = (value: number) => (value * Math.PI) / 180; | |
const R = 6371; // Radius of the Earth in kilometers | |
const dLat = toRad(lat2 - lat1); | |
const dLon = toRad(lon2 - lon1); | |
const a = | |
Math.sin(dLat / 2) * Math.sin(dLat / 2) + | |
Math.cos(toRad(lat1)) * | |
Math.cos(toRad(lat2)) * | |
Math.sin(dLon / 2) * | |
Math.sin(dLon / 2); | |
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); | |
const distance = R * c; // Distance in kilometers | |
return distance; | |
} | |
export function getCurrentPosition( | |
options?: PositionOptions, | |
): Promise<GeolocationPosition> { | |
const defaultOptions: PositionOptions = { | |
enableHighAccuracy: false, | |
timeout: 5_000, | |
maximumAge: 1_000 * 60 * 60, // 1 hour | |
}; | |
return new Promise((resolve, reject) => { | |
if (!navigator.geolocation) { | |
reject(new GeoLocationError(GeoLocationErrorCode.NOT_SUPPORTED)); | |
} | |
navigator.geolocation.getCurrentPosition( | |
resolve, | |
(error) => { | |
switch (error.code) { | |
case error.PERMISSION_DENIED: { | |
reject( | |
new GeoLocationError(GeoLocationErrorCode.PERMISSION_DENIED), | |
); | |
break; | |
} | |
case error.POSITION_UNAVAILABLE: { | |
reject( | |
new GeoLocationError(GeoLocationErrorCode.POSITION_UNAVAILABLE), | |
); | |
break; | |
} | |
case error.TIMEOUT: { | |
reject(new GeoLocationError(GeoLocationErrorCode.TIMEOUT)); | |
break; | |
} | |
default: { | |
reject(new GeoLocationError(GeoLocationErrorCode.UNKNOWN)); | |
break; | |
} | |
} | |
}, | |
{ ...defaultOptions, ...options }, | |
); | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment