Compare commits

..

No commits in common. "9176eb014f893038f134741094f4863f51577441" and "db7a0b970d57f6b41993d963488469626c99f1d5" have entirely different histories.

11 changed files with 34 additions and 180 deletions

View File

@ -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,6 +26,7 @@ const queryClient = new QueryClient({
}) })
export default function RootLayout() { export default function RootLayout() {
useStartGeolocationServiceEffect()
useStartBackgroundFetchServiceEffect() useStartBackgroundFetchServiceEffect()
const colorScheme = useColorScheme() const colorScheme = useColorScheme()
@ -44,18 +45,16 @@ 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'}>
<GeolocationProvider> <PaperProvider theme={colorScheme === 'dark' ? MD3DarkTheme : MD3LightTheme}>
<PaperProvider theme={colorScheme === 'dark' ? MD3DarkTheme : MD3LightTheme}> <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}> <Stack>
<Stack> <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} /> <Stack.Screen name="login" options={{ headerShown: false }} />
<Stack.Screen name="login" options={{ headerShown: false }} /> <Stack.Screen name="+not-found" />
<Stack.Screen name="+not-found" /> </Stack>
</Stack> <StatusBar style="auto" />
<StatusBar style="auto" /> </ThemeProvider>
</ThemeProvider> </PaperProvider>
</PaperProvider>
</GeolocationProvider>
</LoginProvider> </LoginProvider>
</PersistQueryClientProvider> </PersistQueryClientProvider>
</StoreProvider> </StoreProvider>

View File

@ -1,33 +0,0 @@
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}
</>
}

View File

@ -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 { useLastOwnLocation } from '@/hooks/useLocation' import { useLocation } from '@/hooks/useLocation'
export default function Map() { export default function Map() {
const userLocation = useLastOwnLocation() const userLocation = useLocation()
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 (

View File

@ -1,10 +1,8 @@
import { useAuth } from "@/hooks/useAuth" import { useLocation } from "@/hooks/useLocation"
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 { useEffect, useMemo, useState } from "react" import { useState } from "react"
export default function Map() { export default function Map() {
return ( return (
@ -18,13 +16,12 @@ 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 = useLastOwnLocation() const userLocation = useLocation()
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) {
@ -40,37 +37,3 @@ 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}
</>
}

View File

@ -1,3 +0,0 @@
export const Constants = {
MIN_DELAY_LOCATION_SENT: 20
}

View File

@ -1,56 +0,0 @@
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 })
}
})
}

View File

@ -1,15 +1,9 @@
import { LocationObject } from "expo-location" import { LocationObject } from "expo-location"
import { useAppDispatch, useAppSelector } from "./useStore" import { useAppDispatch, useAppSelector } from "./useStore"
import { setLastLocation, unqueueLocation } from "@/utils/features/location/locationSlice" import { setLocation } from "@/utils/features/location/locationSlice"
export const useLastOwnLocation = () => useAppSelector((state) => state.location.lastOwnLocation) export const useLocation = () => useAppSelector((state) => state.location.location)
export const useQueuedLocations = () => useAppSelector((state) => state.location.queuedLocations) export const useSetLocation = () => {
export const useSetLastLocation = () => {
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
return (location: LocationObject) => dispatch(setLastLocation(location)) return (location: LocationObject) => dispatch(setLocation(location))
}
export const useUnqueueLocation = () => {
const dispatch = useAppDispatch()
return (location: LocationObject) => dispatch(unqueueLocation(location))
} }

View File

@ -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 = 60000 const BACKGROUND_FETCH_INTERVAL = 60
async function backgroundUpdate() { async function backgroundUpdate() {
const now = Date.now() const now = Date.now()

View File

@ -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'
export interface AuthState { interface AuthState {
loggedIn: boolean, loggedIn: boolean,
name: string | null, name: string | null,
token: string | null, token: string | null,

View File

@ -1,37 +1,24 @@
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 {
lastOwnLocation: LocationObject | null location: LocationObject | null
lastSentLocation: LocationObject | null
queuedLocations: LocationObject[]
} }
const initialState: LocationState = { const initialState: LocationState = {
lastOwnLocation: null, location: null
lastSentLocation: null,
queuedLocations: []
} }
export const locationSlice = createSlice({ export const locationSlice = createSlice({
name: 'location', name: 'location',
initialState: initialState, initialState: initialState,
reducers: { reducers: {
setLastLocation: (state, action: PayloadAction<LocationObject>) => { setLocation: (state, action: PayloadAction<LocationObject>) => {
const location: LocationObject = action.payload state.location = 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 { setLastLocation, unqueueLocation } = locationSlice.actions export const { setLocation } = locationSlice.actions
export default locationSlice.reducer export default locationSlice.reducer

View File

@ -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 { setLastLocation } from './features/location/locationSlice' import { setLocation } from './features/location/locationSlice'
import store from './store' import store from './store'
import { useEffect } from 'react' import { useEffect } from 'react'
@ -13,7 +13,10 @@ TaskManager.defineTask(LOCATION_TASK, async ({ data, error }: any) => {
return return
} }
const { locations } = data const { locations } = data
store.dispatch(setLastLocation(locations.at(-1))) store.dispatch(setLocation(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)> {
@ -45,7 +48,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(setLastLocation(location_nouveau))) const locationSubscription = await Location.watchPositionAsync({accuracy: Location.Accuracy.BestForNavigation}, location_nouveau => store.dispatch(setLocation(location_nouveau)))
return locationSubscription.remove return locationSubscription.remove
} }
} }