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 { useColorScheme } from '@/hooks/useColorScheme' | ||||||
| import store from '@/utils/store' | import store from '@/utils/store' | ||||||
| import { useStartBackgroundFetchServiceEffect } from '@/utils/background' | import { useStartBackgroundFetchServiceEffect } from '@/utils/background' | ||||||
| import { useStartGeolocationServiceEffect } from '@/utils/geolocation' |  | ||||||
| import LoginProvider from '@/components/LoginProvider' | import LoginProvider from '@/components/LoginProvider' | ||||||
|  | import GeolocationProvider from '@/components/GeolocationProvider' | ||||||
|  |  | ||||||
| const queryClient = new QueryClient({ | const queryClient = new QueryClient({ | ||||||
|   defaultOptions: { |   defaultOptions: { | ||||||
| @@ -26,7 +26,6 @@ const queryClient = new QueryClient({ | |||||||
| }) | }) | ||||||
|  |  | ||||||
| export default function RootLayout() { | export default function RootLayout() { | ||||||
|   useStartGeolocationServiceEffect() |  | ||||||
|   useStartBackgroundFetchServiceEffect() |   useStartBackgroundFetchServiceEffect() | ||||||
|   const colorScheme = useColorScheme() |   const colorScheme = useColorScheme() | ||||||
|  |  | ||||||
| @@ -45,16 +44,18 @@ export default function RootLayout() { | |||||||
|           persistOptions={{ persister: asyncStoragePersister }} |           persistOptions={{ persister: asyncStoragePersister }} | ||||||
|           onSuccess={() => queryClient.resumePausedMutations().then(() => queryClient.invalidateQueries())}> |           onSuccess={() => queryClient.resumePausedMutations().then(() => queryClient.invalidateQueries())}> | ||||||
|         <LoginProvider loginRedirect={'/login'}> |         <LoginProvider loginRedirect={'/login'}> | ||||||
|           <PaperProvider theme={colorScheme === 'dark' ? MD3DarkTheme : MD3LightTheme}> |           <GeolocationProvider> | ||||||
|             <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> |             <PaperProvider theme={colorScheme === 'dark' ? MD3DarkTheme : MD3LightTheme}> | ||||||
|               <Stack> |               <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> | ||||||
|                 <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> |                 <Stack> | ||||||
|                 <Stack.Screen name="login" options={{ headerShown: false }} /> |                   <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> | ||||||
|                 <Stack.Screen name="+not-found" /> |                   <Stack.Screen name="login" options={{ headerShown: false }} /> | ||||||
|               </Stack> |                   <Stack.Screen name="+not-found" /> | ||||||
|               <StatusBar style="auto" /> |                 </Stack> | ||||||
|             </ThemeProvider> |                 <StatusBar style="auto" /> | ||||||
|           </PaperProvider> |               </ThemeProvider> | ||||||
|  |             </PaperProvider> | ||||||
|  |           </GeolocationProvider> | ||||||
|         </LoginProvider> |         </LoginProvider> | ||||||
|       </PersistQueryClientProvider> |       </PersistQueryClientProvider> | ||||||
|     </StoreProvider> |     </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 MapLibreGL, { Camera, FillLayer, LineLayer, MapView, PointAnnotation, RasterLayer, RasterSource, ShapeSource, UserLocation } from '@maplibre/maplibre-react-native' | ||||||
| import { FontAwesome5 } from '@expo/vector-icons' | import { FontAwesome5 } from '@expo/vector-icons' | ||||||
| import { circle } from '@turf/circle' | import { circle } from '@turf/circle' | ||||||
| import { useLocation } from '@/hooks/useLocation' | import { useLastOwnLocation } from '@/hooks/useLocation' | ||||||
|  |  | ||||||
| export default function Map() { | export default function Map() { | ||||||
|   const userLocation = useLocation() |   const userLocation = useLastOwnLocation() | ||||||
|   MapLibreGL.setAccessToken(null) |   MapLibreGL.setAccessToken(null) | ||||||
|   const accuracyCircle = circle([userLocation?.coords.longitude ?? 0, userLocation?.coords.latitude ?? 0], userLocation?.coords.accuracy ?? 0, {steps: 64, units: 'meters'}) |   const accuracyCircle = circle([userLocation?.coords.longitude ?? 0, userLocation?.coords.latitude ?? 0], userLocation?.coords.accuracy ?? 0, {steps: 64, units: 'meters'}) | ||||||
|   return ( |   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 { circle } from "@turf/circle" | ||||||
| import { type Map as MaplibreGLMap } from "maplibre-gl" | import { type Map as MaplibreGLMap } from "maplibre-gl" | ||||||
| import { RLayer, RMap, RMarker, RNavigationControl, RSource, useMap } from "maplibre-react-components" | 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() { | export default function Map() { | ||||||
|   return ( |   return ( | ||||||
| @@ -16,12 +18,13 @@ export default function Map() { | |||||||
|       <RLayer id="railwaymap-layer" type="raster" source="railwaymap-source" paint={{"raster-opacity": 0.7}} /> |       <RLayer id="railwaymap-layer" type="raster" source="railwaymap-source" paint={{"raster-opacity": 0.7}} /> | ||||||
|  |  | ||||||
|       <UserLocation /> |       <UserLocation /> | ||||||
|  |       <DownloadedLocation /> | ||||||
|     </RMap> |     </RMap> | ||||||
|   ) |   ) | ||||||
| } | } | ||||||
|  |  | ||||||
| function UserLocation() { | function UserLocation() { | ||||||
|   const userLocation = useLocation() |   const userLocation = useLastOwnLocation() | ||||||
|   const [firstUserPositionFetched, setFirstUserPositionFetched] = useState(false) |   const [firstUserPositionFetched, setFirstUserPositionFetched] = useState(false) | ||||||
|   const map: MaplibreGLMap = useMap() |   const map: MaplibreGLMap = useMap() | ||||||
|   if (userLocation != null && !firstUserPositionFetched) { |   if (userLocation != null && !firstUserPositionFetched) { | ||||||
| @@ -37,3 +40,37 @@ function UserLocation() { | |||||||
|     {marker} |     {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 { LocationObject } from "expo-location" | ||||||
| import { useAppDispatch, useAppSelector } from "./useStore" | 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 useLastOwnLocation = () => useAppSelector((state) => state.location.lastOwnLocation) | ||||||
| export const useSetLocation = () => { | export const useQueuedLocations = () => useAppSelector((state) => state.location.queuedLocations) | ||||||
|  |  | ||||||
|  | export const useSetLastLocation = () => { | ||||||
|     const dispatch = useAppDispatch() |     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' | import { useEffect } from 'react' | ||||||
|  |  | ||||||
| const BACKGROUND_FETCH_TASK = "background-fetch" | const BACKGROUND_FETCH_TASK = "background-fetch" | ||||||
| const BACKGROUND_FETCH_INTERVAL = 60 | const BACKGROUND_FETCH_INTERVAL = 60000 | ||||||
|  |  | ||||||
| async function backgroundUpdate() { | async function backgroundUpdate() { | ||||||
|   const now = Date.now() |   const now = Date.now() | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' | |||||||
| import * as SecureStore from '@/utils/SecureStore' | import * as SecureStore from '@/utils/SecureStore' | ||||||
| import { Platform } from 'react-native' | import { Platform } from 'react-native' | ||||||
|  |  | ||||||
| interface AuthState { | export interface AuthState { | ||||||
|   loggedIn: boolean, |   loggedIn: boolean, | ||||||
|   name: string | null, |   name: string | null, | ||||||
|   token: string | null, |   token: string | null, | ||||||
|   | |||||||
| @@ -1,24 +1,37 @@ | |||||||
|  | import { Constants } from '@/constants/Constants' | ||||||
| import { createSlice, PayloadAction } from '@reduxjs/toolkit' | import { createSlice, PayloadAction } from '@reduxjs/toolkit' | ||||||
| import { LocationObject } from 'expo-location' | import { LocationObject } from 'expo-location' | ||||||
|  |  | ||||||
| interface LocationState { | interface LocationState { | ||||||
|   location: LocationObject | null |   lastOwnLocation: LocationObject | null | ||||||
|  |   lastSentLocation: LocationObject | null | ||||||
|  |   queuedLocations: LocationObject[] | ||||||
| } | } | ||||||
|  |  | ||||||
| const initialState: LocationState = { | const initialState: LocationState = { | ||||||
|   location: null |   lastOwnLocation: null, | ||||||
|  |   lastSentLocation: null, | ||||||
|  |   queuedLocations: [] | ||||||
| } | } | ||||||
|  |  | ||||||
| export const locationSlice = createSlice({ | export const locationSlice = createSlice({ | ||||||
|   name: 'location', |   name: 'location', | ||||||
|   initialState: initialState, |   initialState: initialState, | ||||||
|   reducers: { |   reducers: { | ||||||
|     setLocation: (state, action: PayloadAction<LocationObject>) => { |     setLastLocation: (state, action: PayloadAction<LocationObject>) => { | ||||||
|       state.location = action.payload |       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 | export default locationSlice.reducer | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import * as Location from 'expo-location' | import * as Location from 'expo-location' | ||||||
| import * as TaskManager from 'expo-task-manager' | import * as TaskManager from 'expo-task-manager' | ||||||
| import { Platform } from 'react-native' | import { Platform } from 'react-native' | ||||||
| import { setLocation } from './features/location/locationSlice' | import { setLastLocation } from './features/location/locationSlice' | ||||||
| import store from './store' | import store from './store' | ||||||
| import { useEffect } from 'react' | import { useEffect } from 'react' | ||||||
|  |  | ||||||
| @@ -13,10 +13,7 @@ TaskManager.defineTask(LOCATION_TASK, async ({ data, error }: any) => { | |||||||
|     return |     return | ||||||
|   } |   } | ||||||
|   const { locations } = data |   const { locations } = data | ||||||
|   store.dispatch(setLocation(locations.at(-1))) |   store.dispatch(setLastLocation(locations.at(-1))) | ||||||
|   for (let location of locations) { |  | ||||||
|     // TODO Envoyer les positions au serveur |  | ||||||
|   } |  | ||||||
| }) | }) | ||||||
|    |    | ||||||
| export async function startGeolocationService(): Promise<void | (() => void)> { | 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) |     return async () => await Location.stopLocationUpdatesAsync(LOCATION_TASK) | ||||||
|   } |   } | ||||||
|   else { |   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 |     return locationSubscription.remove | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user