Код: Выделить всё
import { useEffect, useRef, useState } from 'react';
import { showNativeToast } from '@/utils/useNativeToast';
import { useMapContext } from '@/context/MapContext';
import useToast from '@/hooks/useToast';
import { Platform } from 'react-native';
import * as Location from 'expo-location';
import * as Notifications from 'expo-notifications';
import { BreadcrumbFeature } from '@/types/breadcrumbTypes';
import { TRAIL_COLORS } from '@/constants/colors';
import * as TaskManager from 'expo-task-manager';
import { checkBatteryOptimization } from '@/utils/batteryOptimization';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { ActivityType } from 'expo-location';
import { StorageKeys } from '@/constants/storageKeys';
import { calculateDistance } from '@/utils/calculateDistance';
const LOCATION_TASK_NAME = 'TrailTracking';
export const MIN_DISTANCE_METERS = 0;
const MAX_ACCURACY_METERS = 30;
const MAX_DISTANCE_JUMP_METERS = 50;
interface ReturnUseBreadcrumbTrail {
startTracking: () => Promise;
stopTracking: () => Promise;
discardPath: () => void;
setTrailName: (name: string) => void;
setTrailColor: (color: string) => void;
trailProperties: { name: string; color?: string };
addLocationPoint: (
coords:
| {
latitude: number;
longitude: number;
accuracy?: number;
speed?: number;
}
| {
latitude: number;
longitude: number;
accuracy?: number;
speed?: number;
}[],
) => void;
getCurrentBreadcrumb: () => BreadcrumbFeature;
}
const foregroundService = {
notificationTitle: 'Trail Tracking',
notificationBody: 'Your location is being tracked.',
channelId: 'location-tracking',
};
const locationConfig = {
accuracy: Location.Accuracy.BestForNavigation,
timeInterval: 0,
distanceInterval: 0,
foregroundService: Platform.OS === 'android' ? foregroundService : undefined,
pausesUpdatesAutomatically: false,
activityType: ActivityType.OtherNavigation,
mayRequireBackgroundAccess: true,
...(Platform.OS === 'ios' && {
allowsBackgroundLocationUpdates: true,
showsBackgroundLocationIndicator: true,
}),
};
export function useBreadcrumbTrail(): ReturnUseBreadcrumbTrail {
const { setIsBreadcrumbTracking, setLiveBreadcrumb, liveBreadcrumb } =
useMapContext();
const { showSuccessToast, showErrorToast } = useToast();
const [trailProperties, setTrailProperties] = useState({
name: '',
color: TRAIL_COLORS[0].color,
});
const isTracking = useRef(false);
const addLocationPoint = async (
coords:
| {
latitude: number;
longitude: number;
accuracy?: number;
speed?: number;
}
| {
latitude: number;
longitude: number;
accuracy?: number;
speed?: number;
}[],
) => {
setLiveBreadcrumb(prev => {
const coordinates = [...prev.geometry.coordinates];
const points = Array.isArray(coords) ? coords : [coords];
points.forEach(p => {
const isDriving = p.speed !== undefined && p.speed > 3.6; // ~10km/h
if (!isDriving && p.accuracy !== undefined && p.accuracy > MAX_ACCURACY_METERS) {
return;
}
const lastCoord =
coordinates.length > 0 ? coordinates[coordinates.length - 1] : null;
if (!lastCoord) {
coordinates.push([p.longitude, p.latitude]);
return;
}
const lastPoint = { latitude: lastCoord[1], longitude: lastCoord[0] };
const dist = calculateDistance(lastPoint, p);
if (isDriving || dist MIN_DISTANCE_METERS) {
coordinates.push([p.longitude, p.latitude]);
}
}
});
const updatedBreadcrumb = {
...prev,
geometry: {
...prev.geometry,
coordinates,
},
};
AsyncStorage.setItem(
StorageKeys.CURRENT_BREADCRUMB,
JSON.stringify(updatedBreadcrumb),
);
return updatedBreadcrumb;
});
try {
await AsyncStorage.removeItem('breadcrumb_points');
} catch (e) {
console.error('Failed to clear persisted breadcrumb points:', e);
}
};
useEffect(() => {
(async () => {
try {
const stored = await AsyncStorage.getItem('breadcrumb_points');
if (stored) {
const points = JSON.parse(stored);
if (points && points.length > 0) {
await addLocationPoint(points);
}
}
} catch (e) {
console.error('Failed to load persisted breadcrumb points:', e);
}
})();
return () => {
global.addLocationPoint = null;
};
}, []);
const startTracking = async (): Promise => {
try {
const { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
showErrorToast('Permission to access location was denied');
return false;
}
if (Platform.OS === 'android') {
const isOptimized = await checkBatteryOptimization();
if (!isOptimized) {
return false;
}
}
const { granted } = await Notifications.requestPermissionsAsync({
ios: {
allowAlert: true,
allowBadge: true,
allowSound: true,
},
});
if (!granted) {
showErrorToast('Permission to display notification was denied');
return false;
}
if (Platform.OS === 'ios') {
const backgroundStatus =
await Location.requestBackgroundPermissionsAsync();
if (backgroundStatus.status !== 'granted') {
showErrorToast('Background location permission is required');
return false;
}
}
const isTaskRegistered =
await TaskManager.isTaskRegisteredAsync(LOCATION_TASK_NAME);
if (isTaskRegistered) {
await Location.stopLocationUpdatesAsync(LOCATION_TASK_NAME);
}
global.addLocationPoint = addLocationPoint;
await Location.startLocationUpdatesAsync(LOCATION_TASK_NAME, {
...locationConfig,
...(Platform.OS === 'android' && { foregroundService }),
});
isTracking.current = true;
setIsBreadcrumbTracking(true);
showSuccessToast('Tracking started');
return true;
} catch (error) {
console.warn('Error starting tracking:', error);
showNativeToast(
'Failed to start tracking. Please ensure location services are enabled and permissions are granted.',
);
return false;
}
};
const setTrailName = (name: string) => {
setTrailProperties(prev => ({ ...prev, name }));
setLiveBreadcrumb(prev => ({
...prev,
properties: { ...prev.properties, name },
}));
};
const setTrailColor = (color: string) => {
setTrailProperties(prev => ({ ...prev, color }));
setLiveBreadcrumb(prev => ({
...prev,
properties: { ...prev.properties, color },
}));
};
const stopTracking = async () => {
try {
const isTaskRegistered =
await TaskManager.isTaskRegisteredAsync(LOCATION_TASK_NAME);
if (isTaskRegistered) {
await Location.stopLocationUpdatesAsync(LOCATION_TASK_NAME);
}
isTracking.current = false;
setIsBreadcrumbTracking(false);
global.addLocationPoint = null;
} catch (error) {
console.warn('Error stopping tracking:', error);
isTracking.current = false;
setIsBreadcrumbTracking(false);
global.addLocationPoint = null;
}
};
const discardPath = () => {
if (isTracking.current) {
stopTracking();
}
setLiveBreadcrumb({
type: 'Feature',
geometry: {
type: 'LineString',
coordinates: [],
},
properties: {
name: '',
color: TRAIL_COLORS[0].color,
createdAt: Date.now(),
},
});
setTrailProperties({ name: '', color: TRAIL_COLORS[0].color });
};
const getCurrentBreadcrumb = (): BreadcrumbFeature => {
return {
...liveBreadcrumb,
properties: {
...liveBreadcrumb.properties,
color: trailProperties.color || TRAIL_COLORS[0].color,
createdAt: liveBreadcrumb.properties.createdAt || Date.now(),
},
};
};
useEffect(() => {
return () => {
if (isTracking.current) {
stopTracking();
}
};
}, []);
return {
startTracking,
stopTracking,
discardPath,
setTrailName,
setTrailColor,
trailProperties,
addLocationPoint,
getCurrentBreadcrumb,
};
}
предоставляется уведомления
Оптимизация батареи проверяется, и пользователи предлагаются, чтобы отключить его
Оптимирование, если
Определение. /> global.addlocationPoint () вызывается из defanetask (), чтобы добавить новые точки < /p>
Проблема < /strong>
на Android, если экран отключен или приложение на фона на некоторое время, отслеживание иногда молча останавливается < /p>
icon следом /> Похоже, что приложение, возможно, было убито или перезапущено, но нет никакого журнала аварии < /p>
ios работает, как и ожидалось - нет проблем < /p>
Подробнее здесь: https://stackoverflow.com/questions/796 ... f-trail-tr