as-app/src/hooks/useLocation.js

136 lines
3.8 KiB
JavaScript

import * as Location from "expo-location";
import { useState, useRef, useEffect, useCallback } from "react";
import { storeLocation, getStoredLocation } from "~/utils/location/storage";
import { createLogger } from "~/lib/logger";
import { BACKGROUND_SCOPES, UI_SCOPES } from "~/lib/logger/scopes";
const locationLogger = createLogger({
module: BACKGROUND_SCOPES.GEOLOCATION,
feature: UI_SCOPES.HOOKS,
});
const LOCATION_TIMEOUT = 5000; // 5 seconds
// Default coordinates when no location is available
const DEFAULT_COORDS = {
accuracy: null,
altitude: null,
altitudeAccuracy: null,
heading: null,
latitude: null,
longitude: null,
speed: null,
};
export default function useLocation() {
const [location, setLocation] = useState({ coords: DEFAULT_COORDS });
const [isLastKnown, setIsLastKnown] = useState(false);
const [lastKnownTimestamp, setLastKnownTimestamp] = useState(null);
const watcher = useRef();
const timeoutRef = useRef();
const isWatchingRef = useRef(false);
const hasLocationRef = useRef(false);
const setupLocationWatching = useCallback(async () => {
// Clean up existing watcher and timeout
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
if (watcher.current) {
await watcher.current.remove();
watcher.current = null;
}
// Reset flags
isWatchingRef.current = false;
hasLocationRef.current = false;
// Prevent multiple watchers
if (isWatchingRef.current) {
return;
}
const watchPositionOptions = {
accuracy: Location.Accuracy.BestForNavigation,
distanceInterval: 10,
mayShowUserSettingsDialog: false,
};
try {
isWatchingRef.current = true;
// Start timeout to check for last known location
timeoutRef.current = setTimeout(async () => {
if (!hasLocationRef.current) {
const lastKnown = await getStoredLocation();
if (lastKnown) {
setLocation({ coords: lastKnown.coords });
setIsLastKnown(true);
setLastKnownTimestamp(lastKnown.timestamp);
}
}
}, LOCATION_TIMEOUT);
watcher.current = await Location.watchPositionAsync(
watchPositionOptions,
async (newLocation) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
timeoutRef.current = null;
}
hasLocationRef.current = true;
setLocation(newLocation);
setIsLastKnown(false);
setLastKnownTimestamp(null);
// Store the location whenever we get a new one
await storeLocation(newLocation.coords, newLocation.timestamp);
},
);
} catch (error) {
locationLogger.error("Failed to watch position", {
error: error.message,
stack: error.stack,
});
// If we fail to get current location, try to use last known
const lastKnown = await getStoredLocation();
if (lastKnown) {
setLocation({ coords: lastKnown.coords });
setIsLastKnown(true);
setLastKnownTimestamp(lastKnown.timestamp);
}
}
}, []);
// Expose reload function to allow manual refresh
const reload = useCallback(() => {
setupLocationWatching();
}, [setupLocationWatching]);
useEffect(() => {
setupLocationWatching();
return () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
if (watcher.current) {
watcher.current.remove();
}
isWatchingRef.current = false;
hasLocationRef.current = false;
};
}, [setupLocationWatching]); // Add setupLocationWatching to dependencies since it's stable
// Always ensure we have valid coords object
const coords = location?.coords || DEFAULT_COORDS;
return {
coords,
isLastKnown,
lastKnownTimestamp,
reload,
};
}