218 lines
6.5 KiB
TypeScript
218 lines
6.5 KiB
TypeScript
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
import type { ParticipantLocation, LocationSource } from '@/types';
|
|
|
|
interface UseLocationSharingOptions {
|
|
/** Called when location updates */
|
|
onLocationUpdate?: (location: ParticipantLocation) => void;
|
|
/** Update interval in milliseconds (default: 5000) */
|
|
updateInterval?: number;
|
|
/** Enable high accuracy mode (uses more battery) */
|
|
highAccuracy?: boolean;
|
|
/** Maximum age of cached position in ms (default: 5000) - lower = fresher positions */
|
|
maxAge?: number;
|
|
/** Timeout for position request in ms (default: 30000) */
|
|
timeout?: number;
|
|
}
|
|
|
|
interface UseLocationSharingReturn {
|
|
/** Whether location sharing is currently active */
|
|
isSharing: boolean;
|
|
/** Current location (if available) */
|
|
currentLocation: ParticipantLocation | null;
|
|
/** Any error that occurred */
|
|
error: GeolocationPositionError | null;
|
|
/** Start sharing location */
|
|
startSharing: () => void;
|
|
/** Stop sharing location */
|
|
stopSharing: () => void;
|
|
/** Request a single location update */
|
|
requestUpdate: () => void;
|
|
/** Permission state */
|
|
permissionState: PermissionState | null;
|
|
}
|
|
|
|
export function useLocationSharing(
|
|
options: UseLocationSharingOptions = {}
|
|
): UseLocationSharingReturn {
|
|
const {
|
|
onLocationUpdate,
|
|
updateInterval = 5000,
|
|
highAccuracy = true,
|
|
maxAge = 30000, // Allow cached position up to 30 seconds old
|
|
timeout = 15000, // 15 second timeout before fallback
|
|
} = options;
|
|
|
|
const [isSharing, setIsSharing] = useState(false);
|
|
const [currentLocation, setCurrentLocation] = useState<ParticipantLocation | null>(null);
|
|
const [error, setError] = useState<GeolocationPositionError | null>(null);
|
|
const [permissionState, setPermissionState] = useState<PermissionState | null>(null);
|
|
|
|
const watchIdRef = useRef<number | null>(null);
|
|
const onLocationUpdateRef = useRef(onLocationUpdate);
|
|
const timeoutCountRef = useRef(0);
|
|
const usingLowAccuracyRef = useRef(false);
|
|
|
|
// Keep callback ref updated
|
|
useEffect(() => {
|
|
onLocationUpdateRef.current = onLocationUpdate;
|
|
}, [onLocationUpdate]);
|
|
|
|
// Check permission state
|
|
useEffect(() => {
|
|
if ('permissions' in navigator) {
|
|
navigator.permissions
|
|
.query({ name: 'geolocation' })
|
|
.then((result) => {
|
|
setPermissionState(result.state);
|
|
result.addEventListener('change', () => {
|
|
setPermissionState(result.state);
|
|
});
|
|
})
|
|
.catch(() => {
|
|
// Permissions API not supported, will check on first request
|
|
});
|
|
}
|
|
}, []);
|
|
|
|
const handlePosition = useCallback((position: GeolocationPosition) => {
|
|
// Reset timeout counter on successful position
|
|
timeoutCountRef.current = 0;
|
|
|
|
const location: ParticipantLocation = {
|
|
latitude: position.coords.latitude,
|
|
longitude: position.coords.longitude,
|
|
accuracy: position.coords.accuracy,
|
|
altitude: position.coords.altitude ?? undefined,
|
|
altitudeAccuracy: position.coords.altitudeAccuracy ?? undefined,
|
|
heading: position.coords.heading ?? undefined,
|
|
speed: position.coords.speed ?? undefined,
|
|
timestamp: new Date(position.timestamp),
|
|
source: 'gps' as LocationSource,
|
|
};
|
|
|
|
setCurrentLocation(location);
|
|
setError(null);
|
|
onLocationUpdateRef.current?.(location);
|
|
}, []);
|
|
|
|
// Restart watcher with low accuracy mode
|
|
const restartWithLowAccuracy = useCallback(() => {
|
|
if (watchIdRef.current !== null) {
|
|
navigator.geolocation.clearWatch(watchIdRef.current);
|
|
}
|
|
|
|
console.log('Switching to low accuracy mode for faster location');
|
|
usingLowAccuracyRef.current = true;
|
|
|
|
const geoOptions: PositionOptions = {
|
|
enableHighAccuracy: false,
|
|
maximumAge: 60000, // Accept positions up to 1 minute old
|
|
timeout: 60000, // Longer timeout for low accuracy
|
|
};
|
|
|
|
watchIdRef.current = navigator.geolocation.watchPosition(
|
|
handlePosition,
|
|
(err) => {
|
|
setError(err);
|
|
console.warn('Geolocation warning (low accuracy):', err.message, '(code:', err.code, ')');
|
|
},
|
|
geoOptions
|
|
);
|
|
}, [handlePosition]);
|
|
|
|
const handleError = useCallback((err: GeolocationPositionError) => {
|
|
setError(err);
|
|
|
|
// Count consecutive timeouts
|
|
if (err.code === 3) { // TIMEOUT
|
|
timeoutCountRef.current++;
|
|
console.warn('Geolocation timeout', timeoutCountRef.current, '/', 3);
|
|
|
|
// After 2 timeouts, switch to low accuracy mode
|
|
if (timeoutCountRef.current >= 2 && !usingLowAccuracyRef.current && highAccuracy) {
|
|
restartWithLowAccuracy();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Only log as warning, not error - timeouts are common indoors
|
|
console.warn('Geolocation warning:', err.message, '(code:', err.code, ')');
|
|
// Don't stop sharing on errors - keep trying
|
|
}, [highAccuracy, restartWithLowAccuracy]);
|
|
|
|
const startSharing = useCallback(() => {
|
|
if (!('geolocation' in navigator)) {
|
|
console.error('Geolocation is not supported');
|
|
return;
|
|
}
|
|
|
|
if (watchIdRef.current !== null) {
|
|
return; // Already watching
|
|
}
|
|
|
|
// Reset fallback flags
|
|
timeoutCountRef.current = 0;
|
|
usingLowAccuracyRef.current = false;
|
|
|
|
const geoOptions: PositionOptions = {
|
|
enableHighAccuracy: highAccuracy,
|
|
maximumAge: maxAge,
|
|
timeout,
|
|
};
|
|
|
|
// Start watching position
|
|
watchIdRef.current = navigator.geolocation.watchPosition(
|
|
handlePosition,
|
|
handleError,
|
|
geoOptions
|
|
);
|
|
|
|
setIsSharing(true);
|
|
console.log('Started location sharing');
|
|
}, [highAccuracy, maxAge, timeout, handlePosition, handleError]);
|
|
|
|
const stopSharing = useCallback(() => {
|
|
if (watchIdRef.current !== null) {
|
|
navigator.geolocation.clearWatch(watchIdRef.current);
|
|
watchIdRef.current = null;
|
|
}
|
|
setIsSharing(false);
|
|
console.log('Stopped location sharing');
|
|
}, []);
|
|
|
|
const requestUpdate = useCallback(() => {
|
|
if (!('geolocation' in navigator)) {
|
|
return;
|
|
}
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
handlePosition,
|
|
handleError,
|
|
{
|
|
enableHighAccuracy: highAccuracy,
|
|
maximumAge: 0,
|
|
timeout,
|
|
}
|
|
);
|
|
}, [highAccuracy, timeout, handlePosition, handleError]);
|
|
|
|
// Cleanup on unmount
|
|
useEffect(() => {
|
|
return () => {
|
|
if (watchIdRef.current !== null) {
|
|
navigator.geolocation.clearWatch(watchIdRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
return {
|
|
isSharing,
|
|
currentLocation,
|
|
error,
|
|
startSharing,
|
|
stopSharing,
|
|
requestUpdate,
|
|
permissionState,
|
|
};
|
|
}
|