From d08dcb9720a740118970ab57eed2e9c841e605e2 Mon Sep 17 00:00:00 2001 From: Emmy D'Anello Date: Fri, 6 Dec 2024 21:49:28 +0100 Subject: [PATCH] =?UTF-8?q?Stockage=20de=20la=20g=C3=A9olocalisation=20en?= =?UTF-8?q?=20arri=C3=A8re-plan=20et=20utilisation=20sur=20la=20carte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/app/(tabs)/index.tsx | 15 ++-- client/app/_layout.tsx | 80 ++++------------- client/components/Map.tsx | 15 ++-- client/components/Map.web.tsx | 41 ++++++--- client/hooks/useLocation.ts | 9 ++ client/hooks/useStore.ts | 5 ++ client/package-lock.json | 88 ++++++++++++++++++- client/package.json | 4 +- .../utils/features/location/locationSlice.ts | 24 +++++ client/utils/geolocation.ts | 60 +++++++++++++ client/utils/store.ts | 13 +++ 11 files changed, 257 insertions(+), 97 deletions(-) create mode 100644 client/hooks/useLocation.ts create mode 100644 client/hooks/useStore.ts create mode 100644 client/utils/features/location/locationSlice.ts create mode 100644 client/utils/geolocation.ts create mode 100644 client/utils/store.ts diff --git a/client/app/(tabs)/index.tsx b/client/app/(tabs)/index.tsx index ce7bb02..b82dd56 100644 --- a/client/app/(tabs)/index.tsx +++ b/client/app/(tabs)/index.tsx @@ -1,21 +1,18 @@ import { StyleSheet } from 'react-native' import { ThemedView } from '@/components/ThemedView' -import { useEffect, useState } from 'react' import "maplibre-gl/dist/maplibre-gl.css" - -import * as Location from 'expo-location' -import Map from '@/components/map' +import { useBackgroundPermissions } from 'expo-location' +import Map from '@/components/Map' import { ThemedText } from '@/components/ThemedText' export default function MapScreen() { - const [location, setLocation] = useState(null) - const [locationAccessGranted, setLocationAccessGranted] = useState(false) - - + const [backgroundStatus, requestBackgroundPermission] = useBackgroundPermissions() + if (!backgroundStatus?.granted && backgroundStatus?.canAskAgain) + requestBackgroundPermission() return ( - {locationAccessGranted ? : La géolocalisation est requise pour utiliser la carte.} + {backgroundStatus?.granted ? : La géolocalisation est requise pour utiliser la carte.} ) } diff --git a/client/app/_layout.tsx b/client/app/_layout.tsx index e21c480..f624ca8 100644 --- a/client/app/_layout.tsx +++ b/client/app/_layout.tsx @@ -1,74 +1,24 @@ -import { Dispatch, useEffect, useState } from 'react' import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native' import { Stack } from "expo-router" import { useColorScheme } from '@/hooks/useColorScheme' import { StatusBar } from 'expo-status-bar' -import * as Location from 'expo-location' -import * as TaskManager from 'expo-task-manager' -import { Platform } from 'react-native' - -TaskManager.defineTask("fetch-geolocation", async ({ data, error }: any) => { - if (error) { - console.error(error) - return - } - const { locations } = data - for (let location of locations) { - console.log(location) - } - }) - -async function manageGelocation(setLocation: Dispatch) { - await Location.enableNetworkProviderAsync().catch(error => alert(error)) - const { status: foregroundStatus } = await Location.requestForegroundPermissionsAsync() - if (foregroundStatus === 'granted') { - setLocation(await Location.getLastKnownPositionAsync()) - const { status: backgroundStatus } = await Location.requestBackgroundPermissionsAsync() - if (backgroundStatus === 'granted') { - if (Platform.OS !== "web") { - if (!await Location.hasStartedLocationUpdatesAsync("fetch-geolocation")) { - await Location.startLocationUpdatesAsync("fetch-geolocation", { - accuracy: Location.Accuracy.BestForNavigation, - activityType: Location.ActivityType.OtherNavigation, - deferredUpdatesInterval: 100, - foregroundService: { - killServiceOnDestroy: false, - notificationBody: "Géolocalisation activée pour « Traintrape-moi »", - notificationTitle: "Traintrape-moi", - notificationColor: "#FFFF00", - } - }) - } - } - else { - await Location.watchPositionAsync({accuracy: Location.Accuracy.BestForNavigation}, location_nouveau => setLocation(location_nouveau)) - } - } - else { - alert("Vous devez activer votre géolocalisation en arrière-plan pour utiliser l'application.") - } - } - else { - alert("Vous devez activer votre géolocalisation pour utiliser l'application.") - } -} +import { Provider } from 'react-redux' +import store from '@/utils/store' +import { useStartGeolocationServiceEffect } from '@/utils/geolocation' export default function RootLayout() { - const [location, setLocation] = useState(null) - useEffect(() => { - manageGelocation(setLocation) - return () => { - Location.stopLocationUpdatesAsync("fetch-geolocation") - } - }, []) - + useStartGeolocationServiceEffect() const colorScheme = useColorScheme() - return - - - - - - + return ( + + + + + + + + + + ) } diff --git a/client/components/Map.tsx b/client/components/Map.tsx index 4a29830..f1f638b 100644 --- a/client/components/Map.tsx +++ b/client/components/Map.tsx @@ -1,20 +1,21 @@ -import { StyleSheet, Text } from 'react-native' +import { StyleSheet } from 'react-native' import MapLibreGL, { Camera, FillLayer, LineLayer, MapView, PointAnnotation, RasterLayer, RasterSource, ShapeSource, UserLocation } from '@maplibre/maplibre-react-native' -import { LocationObject } from 'expo-location' import { FontAwesome5 } from '@expo/vector-icons' import { circle } from '@turf/circle' +import { useLocation } from '@/hooks/useLocation' -export default function Map({ location }: { location: LocationObject | null }) { +export default function Map() { + const userLocation = useLocation() MapLibreGL.setAccessToken(null) - const accuracyCircle = circle([location?.coords.longitude ?? 0, location?.coords.latitude ?? 0], location?.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 ( {/* FIXME Il faudra pouvoir avoir un bouton de suivi pour activer le suivi de la caméro */} - {location && } + {userLocation && } @@ -22,7 +23,7 @@ export default function Map({ location }: { location: LocationObject | null }) { - + {/* */} diff --git a/client/components/Map.web.tsx b/client/components/Map.web.tsx index fb088cf..1be6dd6 100644 --- a/client/components/Map.web.tsx +++ b/client/components/Map.web.tsx @@ -1,26 +1,39 @@ +import { useLocation } from "@/hooks/useLocation" import { circle } from "@turf/circle" -import { LocationObject } from "expo-location" -import { RLayer, RMap, RMarker, RNavigationControl, RSource } from "maplibre-react-components" +import { type Map as MaplibreGLMap } from "maplibre-gl" +import { RLayer, RMap, RMarker, RNavigationControl, RSource, useMap } from "maplibre-react-components" +import { useState } from "react" -export default function Map({ location }: { location: LocationObject }) { - if (!location) - // FIXME On devrait avoir la position qui se centre sur la position une fois qu'elle est établie - return <> - const accuracyCircle = circle([location?.coords.longitude ?? 0, location?.coords.latitude ?? 0], location?.coords.accuracy ?? 0, {steps: 64, units: 'meters'}) +export default function Map() { return ( - - - - {location && } + ) -} \ No newline at end of file +} + +function UserLocation() { + const userLocation = useLocation() + const [firstUserPositionFetched, setFirstUserPositionFetched] = useState(false) + const map: MaplibreGLMap = useMap() + if (userLocation != null && !firstUserPositionFetched) { + setFirstUserPositionFetched(true) + map.flyTo({center: [userLocation.coords.longitude, userLocation.coords.latitude], zoom: 15}) + } + const accuracyCircle = circle([userLocation?.coords.longitude ?? 0, userLocation?.coords.latitude ?? 0], userLocation?.coords.accuracy ?? 0, {steps: 64, units: 'meters'}) + const marker = userLocation ? : <> + return <> + + + + {marker} + +} diff --git a/client/hooks/useLocation.ts b/client/hooks/useLocation.ts new file mode 100644 index 0000000..240f852 --- /dev/null +++ b/client/hooks/useLocation.ts @@ -0,0 +1,9 @@ +import { LocationObject } from "expo-location" +import { useAppDispatch, useAppSelector } from "./useStore" +import { setLocation } from "@/utils/features/location/locationSlice" + +export const useLocation = () => useAppSelector((state) => state.location.location) +export const useSetLocation = () => (location: LocationObject) => { + const dispatch = useAppDispatch() + dispatch(setLocation(location)) +} diff --git a/client/hooks/useStore.ts b/client/hooks/useStore.ts new file mode 100644 index 0000000..64eb34c --- /dev/null +++ b/client/hooks/useStore.ts @@ -0,0 +1,5 @@ +import { AppDispatch, RootState } from '@/utils/store' +import { useDispatch, useSelector } from 'react-redux' + +export const useAppDispatch = useDispatch.withTypes() +export const useAppSelector = useSelector.withTypes() diff --git a/client/package-lock.json b/client/package-lock.json index 7c47d28..02695f8 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -12,6 +12,7 @@ "@maplibre/maplibre-react-native": "^10.0.0-alpha.28", "@react-navigation/bottom-tabs": "^7.0.0", "@react-navigation/native": "^7.0.0", + "@reduxjs/toolkit": "^2.4.0", "@turf/circle": "^7.1.0", "expo": "~52.0.11", "expo-blur": "~14.0.1", @@ -39,7 +40,8 @@ "react-native-safe-area-context": "4.12.0", "react-native-screens": "~4.1.0", "react-native-web": "~0.19.13", - "react-native-webview": "13.12.2" + "react-native-webview": "13.12.2", + "react-redux": "^9.1.2" }, "devDependencies": { "@babel/core": "^7.25.2", @@ -4215,6 +4217,30 @@ "nanoid": "3.3.7" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.4.0.tgz", + "integrity": "sha512-wJZEuSKj14tvNfxiIiJws0tQN77/rDqucBq528ApebMIRHyWpCanJVQRxQ8WWZC19iCDKxDsGlbAir3F1layxA==", + "license": "MIT", + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@remix-run/node": { "version": "2.15.0", "resolved": "https://registry.npmjs.org/@remix-run/node/-/node-2.15.0.tgz", @@ -4845,6 +4871,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -8830,6 +8862,16 @@ "node": ">=16.x" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", @@ -13246,6 +13288,29 @@ "async-limiter": "~1.0.0" } }, + "node_modules/react-redux": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", + "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.3", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25", + "react": "^18.0", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -13335,6 +13400,21 @@ "node": ">=0.10.0" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/regenerate": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", @@ -13456,6 +13536,12 @@ "dev": true, "license": "MIT" }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", diff --git a/client/package.json b/client/package.json index 48ecd90..bcea19a 100644 --- a/client/package.json +++ b/client/package.json @@ -18,6 +18,7 @@ "@maplibre/maplibre-react-native": "^10.0.0-alpha.28", "@react-navigation/bottom-tabs": "^7.0.0", "@react-navigation/native": "^7.0.0", + "@reduxjs/toolkit": "^2.4.0", "@turf/circle": "^7.1.0", "expo": "~52.0.11", "expo-blur": "~14.0.1", @@ -45,7 +46,8 @@ "react-native-safe-area-context": "4.12.0", "react-native-screens": "~4.1.0", "react-native-web": "~0.19.13", - "react-native-webview": "13.12.2" + "react-native-webview": "13.12.2", + "react-redux": "^9.1.2" }, "devDependencies": { "@babel/core": "^7.25.2", diff --git a/client/utils/features/location/locationSlice.ts b/client/utils/features/location/locationSlice.ts new file mode 100644 index 0000000..ed606a4 --- /dev/null +++ b/client/utils/features/location/locationSlice.ts @@ -0,0 +1,24 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import { LocationObject } from 'expo-location' + +interface LocationState { + location: LocationObject | null +} + +const initialState: LocationState = { + location: null +} + +export const locationSlice = createSlice({ + name: 'location', + initialState: initialState, + reducers: { + setLocation: (state, action: PayloadAction) => { + state.location = action.payload + }, + }, +}) + +export const { setLocation } = locationSlice.actions + +export default locationSlice.reducer diff --git a/client/utils/geolocation.ts b/client/utils/geolocation.ts new file mode 100644 index 0000000..f7c6a62 --- /dev/null +++ b/client/utils/geolocation.ts @@ -0,0 +1,60 @@ +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 store from './store' +import { useEffect } from 'react' + +const LOCATION_TASK = "fetch-geolocation" + +TaskManager.defineTask(LOCATION_TASK, async ({ data, error }: any) => { + if (error) { + console.error(error) + return + } + const { locations } = data + store.dispatch(setLocation(locations.at(-1))) + for (let location of locations) { + // TODO Envoyer les positions au serveur + } +}) + +export async function startGeolocationService(): Promise void)> { + if (Platform.OS !== "web" && await Location.hasStartedLocationUpdatesAsync(LOCATION_TASK)) + return async () => await Location.stopLocationUpdatesAsync(LOCATION_TASK) + + await Location.enableNetworkProviderAsync().catch(error => alert(error)) + + const { status: foregroundStatus } = await Location.requestForegroundPermissionsAsync() + if (foregroundStatus !== 'granted') + alert("Vous devez activer votre géolocalisation pour utiliser l'application.") + + const { status: backgroundStatus } = await Location.requestBackgroundPermissionsAsync() + if (backgroundStatus !== 'granted') + alert("Vous devez activer votre géolocalisation en arrière-plan pour utiliser l'application.") + + if (Platform.OS !== "web") { + await Location.startLocationUpdatesAsync(LOCATION_TASK, { + accuracy: Location.Accuracy.BestForNavigation, + activityType: Location.ActivityType.OtherNavigation, + deferredUpdatesInterval: 100, + foregroundService: { + killServiceOnDestroy: false, + notificationBody: "Géolocalisation activée pour « Traintrape-moi »", + notificationTitle: "Traintrape-moi", + notificationColor: "#FFFF00", + } + }) + return async () => await Location.stopLocationUpdatesAsync(LOCATION_TASK) + } + else { + const locationSubscription = await Location.watchPositionAsync({accuracy: Location.Accuracy.BestForNavigation}, location_nouveau => store.dispatch(setLocation(location_nouveau))) + return locationSubscription.remove + } +} + +export const useStartGeolocationServiceEffect = () => useEffect(() => { + let cleanup: void | (() => void) = () => {} + startGeolocationService().then(result => cleanup = result) + return cleanup +}, []) diff --git a/client/utils/store.ts b/client/utils/store.ts new file mode 100644 index 0000000..35fd24e --- /dev/null +++ b/client/utils/store.ts @@ -0,0 +1,13 @@ +import { configureStore } from '@reduxjs/toolkit' +import locationReducer from './features/location/locationSlice' + +const store = configureStore({ + reducer: { + location: locationReducer, + }, +}) + +export default store + +export type RootState = ReturnType +export type AppDispatch = typeof store.dispatch \ No newline at end of file