rmaps-online/src/hooks/useLocationSharing.ts

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,
};
}