Compare commits
	
		
			2 Commits
		
	
	
		
			db7a0b970d
			...
			9176eb014f
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 9176eb014f | |||
| bdd53eb8bb | 
| @@ -12,8 +12,8 @@ import { useReactQueryDevTools } from '@dev-plugins/react-query' | ||||
| import { useColorScheme } from '@/hooks/useColorScheme' | ||||
| import store from '@/utils/store' | ||||
| import { useStartBackgroundFetchServiceEffect } from '@/utils/background' | ||||
| import { useStartGeolocationServiceEffect } from '@/utils/geolocation' | ||||
| import LoginProvider from '@/components/LoginProvider' | ||||
| import GeolocationProvider from '@/components/GeolocationProvider' | ||||
|  | ||||
| const queryClient = new QueryClient({ | ||||
|   defaultOptions: { | ||||
| @@ -26,7 +26,6 @@ const queryClient = new QueryClient({ | ||||
| }) | ||||
|  | ||||
| export default function RootLayout() { | ||||
|   useStartGeolocationServiceEffect() | ||||
|   useStartBackgroundFetchServiceEffect() | ||||
|   const colorScheme = useColorScheme() | ||||
|  | ||||
| @@ -45,6 +44,7 @@ export default function RootLayout() { | ||||
|           persistOptions={{ persister: asyncStoragePersister }} | ||||
|           onSuccess={() => queryClient.resumePausedMutations().then(() => queryClient.invalidateQueries())}> | ||||
|         <LoginProvider loginRedirect={'/login'}> | ||||
|           <GeolocationProvider> | ||||
|             <PaperProvider theme={colorScheme === 'dark' ? MD3DarkTheme : MD3LightTheme}> | ||||
|               <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> | ||||
|                 <Stack> | ||||
| @@ -55,6 +55,7 @@ export default function RootLayout() { | ||||
|                 <StatusBar style="auto" /> | ||||
|               </ThemeProvider> | ||||
|             </PaperProvider> | ||||
|           </GeolocationProvider> | ||||
|         </LoginProvider> | ||||
|       </PersistQueryClientProvider> | ||||
|     </StoreProvider> | ||||
|   | ||||
							
								
								
									
										33
									
								
								client/components/GeolocationProvider.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								client/components/GeolocationProvider.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| import { ReactNode, useEffect } from 'react' | ||||
| import { useAuth } from '@/hooks/useAuth' | ||||
| import { useQueuedLocations, useUnqueueLocation } from '@/hooks/useLocation' | ||||
| import { useGeolocationMutation } from '@/hooks/mutations/useGeolocationMutation' | ||||
| import { useStartGeolocationServiceEffect } from '@/utils/geolocation' | ||||
| import { Platform } from 'react-native' | ||||
|  | ||||
| export default function GeolocationProvider({ children }: { children: ReactNode }) { | ||||
|   useStartGeolocationServiceEffect() | ||||
|  | ||||
|   const auth = useAuth() | ||||
|   const geolocationsQueue = useQueuedLocations() | ||||
|   const unqueueLocation = useUnqueueLocation() | ||||
|   const geolocationMutation = useGeolocationMutation({ | ||||
|     auth, | ||||
|     onPostSuccess: ({ data, variables: location }) => { | ||||
|       unqueueLocation(location) | ||||
|       geolocationMutation.reset() | ||||
|     }, | ||||
|     onError: ({ response, error }) => { console.error(response, error) } | ||||
|   }) | ||||
|    | ||||
|     useEffect(() => { | ||||
|     if (geolocationsQueue.length === 0 || geolocationMutation.isPending || Platform.OS === "web") | ||||
|       return | ||||
|     const locToSend = geolocationsQueue[0] | ||||
|     geolocationMutation.mutate(locToSend) | ||||
|   }, [auth, geolocationsQueue]) | ||||
|  | ||||
|   return <> | ||||
|     {children} | ||||
|   </> | ||||
| } | ||||
| @@ -2,10 +2,10 @@ import { StyleSheet } from 'react-native' | ||||
| import MapLibreGL, { Camera, FillLayer, LineLayer, MapView, PointAnnotation, RasterLayer, RasterSource, ShapeSource, UserLocation } from '@maplibre/maplibre-react-native' | ||||
| import { FontAwesome5 } from '@expo/vector-icons' | ||||
| import { circle } from '@turf/circle' | ||||
| import { useLocation } from '@/hooks/useLocation' | ||||
| import { useLastOwnLocation } from '@/hooks/useLocation' | ||||
|  | ||||
| export default function Map() { | ||||
|   const userLocation = useLocation() | ||||
|   const userLocation = useLastOwnLocation() | ||||
|   MapLibreGL.setAccessToken(null) | ||||
|   const accuracyCircle = circle([userLocation?.coords.longitude ?? 0, userLocation?.coords.latitude ?? 0], userLocation?.coords.accuracy ?? 0, {steps: 64, units: 'meters'}) | ||||
|   return ( | ||||
|   | ||||
| @@ -1,8 +1,10 @@ | ||||
| import { useLocation } from "@/hooks/useLocation" | ||||
| import { useAuth } from "@/hooks/useAuth" | ||||
| import { useLastOwnLocation } from "@/hooks/useLocation" | ||||
| import { useQuery } from "@tanstack/react-query" | ||||
| import { circle } from "@turf/circle" | ||||
| import { type Map as MaplibreGLMap } from "maplibre-gl" | ||||
| import { RLayer, RMap, RMarker, RNavigationControl, RSource, useMap } from "maplibre-react-components" | ||||
| import { useState } from "react" | ||||
| import { useEffect, useMemo, useState } from "react" | ||||
|  | ||||
| export default function Map() { | ||||
|   return ( | ||||
| @@ -16,12 +18,13 @@ export default function Map() { | ||||
|       <RLayer id="railwaymap-layer" type="raster" source="railwaymap-source" paint={{"raster-opacity": 0.7}} /> | ||||
|  | ||||
|       <UserLocation /> | ||||
|       <DownloadedLocation /> | ||||
|     </RMap> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| function UserLocation() { | ||||
|   const userLocation = useLocation() | ||||
|   const userLocation = useLastOwnLocation() | ||||
|   const [firstUserPositionFetched, setFirstUserPositionFetched] = useState(false) | ||||
|   const map: MaplibreGLMap = useMap() | ||||
|   if (userLocation != null && !firstUserPositionFetched) { | ||||
| @@ -37,3 +40,37 @@ function UserLocation() { | ||||
|     {marker} | ||||
|   </> | ||||
| } | ||||
|  | ||||
| function DownloadedLocation() { | ||||
|   const auth = useAuth() | ||||
|   const query = useQuery({ | ||||
|     queryKey: ['get-last-locations'], | ||||
|     queryFn: () => fetch(`${process.env.EXPO_PUBLIC_TRAINTRAPE_MOI_SERVER}/geolocations/last-locations/`, { | ||||
|       method: "GET", | ||||
|       headers: { | ||||
|         "Authorization": `Bearer ${auth.token}`, | ||||
|         "Content-Type": "application/json", | ||||
|       }} | ||||
|     ).then(resp => resp.json()), | ||||
|   }) | ||||
|   useEffect(() => { | ||||
|     const interval = setInterval(() => query.refetch(), 5000) | ||||
|     return () => clearInterval(interval) | ||||
|   }, []) | ||||
|   console.log(query.data) | ||||
|   const userLocation = query.isSuccess ? query.data[0] : { longitude: 0, latitude: 0, accuracy: 0 } | ||||
|   const [firstUserPositionFetched, setFirstUserPositionFetched] = useState(false) | ||||
|   const map: MaplibreGLMap = useMap() | ||||
|   if (userLocation != null && !firstUserPositionFetched) { | ||||
|     setFirstUserPositionFetched(true) | ||||
|     map.flyTo({center: [userLocation.longitude, userLocation.latitude], zoom: 15}) | ||||
|   } | ||||
|   const accuracyCircle = useMemo(() => circle([userLocation?.longitude ?? 0, userLocation?.latitude ?? 0], userLocation?.accuracy ?? 0, {steps: 64, units: 'meters'}), [userLocation]) | ||||
|   const marker = userLocation ? <RMarker longitude={userLocation?.longitude} latitude={userLocation?.latitude} /> : <></> | ||||
|   return <> | ||||
|     <RSource id="accuracy-radius-2" type="geojson" data={accuracyCircle} /> | ||||
|     <RLayer id="accuracy-radius-fill-2" type="fill" source="accuracy-radius-2" paint={{"fill-color": "pink", "fill-opacity": 0.4}} /> | ||||
|     <RLayer id="accuracy-radius-border-2" type="line" source="accuracy-radius-2" paint={{"line-color": "red", "line-opacity": 0.4}} /> | ||||
|     {marker} | ||||
|   </> | ||||
| } | ||||
|   | ||||
							
								
								
									
										3
									
								
								client/constants/Constants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								client/constants/Constants.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| export const Constants = { | ||||
|   MIN_DELAY_LOCATION_SENT: 20 | ||||
| } | ||||
							
								
								
									
										56
									
								
								client/hooks/mutations/useGeolocationMutation.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								client/hooks/mutations/useGeolocationMutation.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| import { AuthState } from "@/utils/features/location/authSlice" | ||||
| import { useMutation } from "@tanstack/react-query" | ||||
| import { LocationObject } from "expo-location" | ||||
|  | ||||
| type ErrorResponse = { | ||||
|   error: string | ||||
|   message: string | ||||
|   statusCode: number | ||||
| } | ||||
|  | ||||
| type LoginForm = { | ||||
|   name: string | ||||
|   password: string | ||||
| } | ||||
|  | ||||
| type onPostSuccessFunc = (data: any, variables: LocationObject, context: unknown) => void | ||||
| type ErrorFuncProps = { response?: ErrorResponse, error?: Error } | ||||
| type onErrorFunc = (props: ErrorFuncProps) => void | ||||
|  | ||||
| type PostProps = { | ||||
|   auth: AuthState | ||||
|   onPostSuccess?: onPostSuccessFunc | ||||
|   onError?: onErrorFunc | ||||
| } | ||||
|  | ||||
| export const useGeolocationMutation = ({ auth, onPostSuccess, onError }: PostProps) => { | ||||
|   return useMutation({ | ||||
|     mutationFn: async (location: LocationObject) => { | ||||
|       return fetch(`${process.env.EXPO_PUBLIC_TRAINTRAPE_MOI_SERVER}/geolocations/`, { | ||||
|         method: "POST", | ||||
|         headers: { | ||||
|           "Authorization": `Bearer ${auth.token}`, | ||||
|           "Content-Type": "application/json", | ||||
|         }, | ||||
|         body: JSON.stringify({ | ||||
|           longitude: location.coords.longitude, | ||||
|           latitude: location.coords.latitude, | ||||
|           speed: location.coords.speed, | ||||
|           accuracy: location.coords.accuracy, | ||||
|           altitude: location.coords.altitude, | ||||
|           altitudeAccuracy: location.coords.altitudeAccuracy, | ||||
|           timestamp: location.timestamp, | ||||
|         }) | ||||
|       }).then(resp => resp.json()) | ||||
|     }, | ||||
|     networkMode: 'offlineFirst', | ||||
|     onSuccess: async (data, location: LocationObject, context: unknown) => { | ||||
|       if (onPostSuccess) | ||||
|         onPostSuccess(data, location, context) | ||||
|     }, | ||||
|     onError: async (error: Error) => { | ||||
|       if (onError) | ||||
|         onError({ error: error }) | ||||
|     } | ||||
|   }) | ||||
| } | ||||
| @@ -1,9 +1,15 @@ | ||||
| import { LocationObject } from "expo-location" | ||||
| import { useAppDispatch, useAppSelector } from "./useStore" | ||||
| import { setLocation } from "@/utils/features/location/locationSlice" | ||||
| import { setLastLocation, unqueueLocation } from "@/utils/features/location/locationSlice" | ||||
|  | ||||
| export const useLocation = () => useAppSelector((state) => state.location.location) | ||||
| export const useSetLocation = () => { | ||||
| export const useLastOwnLocation = () => useAppSelector((state) => state.location.lastOwnLocation) | ||||
| export const useQueuedLocations = () => useAppSelector((state) => state.location.queuedLocations) | ||||
|  | ||||
| export const useSetLastLocation = () => { | ||||
|     const dispatch = useAppDispatch() | ||||
|     return (location: LocationObject) => dispatch(setLocation(location)) | ||||
|     return (location: LocationObject) => dispatch(setLastLocation(location)) | ||||
| } | ||||
| export const useUnqueueLocation = () => { | ||||
|     const dispatch = useAppDispatch() | ||||
|     return (location: LocationObject) => dispatch(unqueueLocation(location)) | ||||
| } | ||||
|   | ||||
| @@ -4,7 +4,7 @@ import { Platform } from 'react-native' | ||||
| import { useEffect } from 'react' | ||||
|  | ||||
| const BACKGROUND_FETCH_TASK = "background-fetch" | ||||
| const BACKGROUND_FETCH_INTERVAL = 60 | ||||
| const BACKGROUND_FETCH_INTERVAL = 60000 | ||||
|  | ||||
| async function backgroundUpdate() { | ||||
|   const now = Date.now() | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' | ||||
| import * as SecureStore from '@/utils/SecureStore' | ||||
| import { Platform } from 'react-native' | ||||
|  | ||||
| interface AuthState { | ||||
| export interface AuthState { | ||||
|   loggedIn: boolean, | ||||
|   name: string | null, | ||||
|   token: string | null, | ||||
|   | ||||
| @@ -1,24 +1,37 @@ | ||||
| import { Constants } from '@/constants/Constants' | ||||
| import { createSlice, PayloadAction } from '@reduxjs/toolkit' | ||||
| import { LocationObject } from 'expo-location' | ||||
|  | ||||
| interface LocationState { | ||||
|   location: LocationObject | null | ||||
|   lastOwnLocation: LocationObject | null | ||||
|   lastSentLocation: LocationObject | null | ||||
|   queuedLocations: LocationObject[] | ||||
| } | ||||
|  | ||||
| const initialState: LocationState = { | ||||
|   location: null | ||||
|   lastOwnLocation: null, | ||||
|   lastSentLocation: null, | ||||
|   queuedLocations: [] | ||||
| } | ||||
|  | ||||
| export const locationSlice = createSlice({ | ||||
|   name: 'location', | ||||
|   initialState: initialState, | ||||
|   reducers: { | ||||
|     setLocation: (state, action: PayloadAction<LocationObject>) => { | ||||
|       state.location = action.payload | ||||
|     setLastLocation: (state, action: PayloadAction<LocationObject>) => { | ||||
|       const location: LocationObject = action.payload | ||||
|       state.lastOwnLocation = location | ||||
|       if (state.lastSentLocation === null || (location.timestamp - state.lastSentLocation.timestamp) >= Constants.MIN_DELAY_LOCATION_SENT * 1000) { | ||||
|         state.lastSentLocation = location | ||||
|         state.queuedLocations.push(location) | ||||
|       } | ||||
|     }, | ||||
|     unqueueLocation: (state, action: PayloadAction<LocationObject>) => { | ||||
|       state.queuedLocations.pop() | ||||
|     }, | ||||
|   }, | ||||
| }) | ||||
|  | ||||
| export const { setLocation } = locationSlice.actions | ||||
| export const { setLastLocation, unqueueLocation } = locationSlice.actions | ||||
|  | ||||
| export default locationSlice.reducer | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import * as Location from 'expo-location' | ||||
| import * as TaskManager from 'expo-task-manager' | ||||
| import { Platform } from 'react-native' | ||||
| import { setLocation } from './features/location/locationSlice' | ||||
| import { setLastLocation } from './features/location/locationSlice' | ||||
| import store from './store' | ||||
| import { useEffect } from 'react' | ||||
|  | ||||
| @@ -13,10 +13,7 @@ TaskManager.defineTask(LOCATION_TASK, async ({ data, error }: any) => { | ||||
|     return | ||||
|   } | ||||
|   const { locations } = data | ||||
|   store.dispatch(setLocation(locations.at(-1))) | ||||
|   for (let location of locations) { | ||||
|     // TODO Envoyer les positions au serveur | ||||
|   } | ||||
|   store.dispatch(setLastLocation(locations.at(-1))) | ||||
| }) | ||||
|    | ||||
| export async function startGeolocationService(): Promise<void | (() => void)> { | ||||
| @@ -48,7 +45,7 @@ export async function startGeolocationService(): Promise<void | (() => void)> { | ||||
|     return async () => await Location.stopLocationUpdatesAsync(LOCATION_TASK) | ||||
|   } | ||||
|   else { | ||||
|     const locationSubscription = await Location.watchPositionAsync({accuracy: Location.Accuracy.BestForNavigation}, location_nouveau => store.dispatch(setLocation(location_nouveau))) | ||||
|     const locationSubscription = await Location.watchPositionAsync({accuracy: Location.Accuracy.BestForNavigation}, location_nouveau => store.dispatch(setLastLocation(location_nouveau))) | ||||
|     return locationSubscription.remove | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user