Compare commits
	
		
			3 Commits
		
	
	
		
			55aff5f900
			...
			5e61cabcdc
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 5e61cabcdc | |||
| 8ba47fe2f0 | |||
| 834b1643de | 
| @@ -1,27 +1,53 @@ | |||||||
| import { StyleSheet } from 'react-native' |  | ||||||
| import MapLibreGL, { Camera, FillLayer, LineLayer, MapView, PointAnnotation, RasterLayer, RasterSource, ShapeSource, UserLocation, UserTrackingMode } from '@maplibre/maplibre-react-native' |  | ||||||
| import { FontAwesome5, MaterialIcons } from '@expo/vector-icons' | import { FontAwesome5, MaterialIcons } from '@expo/vector-icons' | ||||||
|  | import MapLibreGL, { Camera, FillLayer, LineLayer, MapView, PointAnnotation, RasterLayer, RasterSource, ShapeSource, UserLocation, UserTrackingMode } from '@maplibre/maplibre-react-native' | ||||||
|  | import { useQuery } from '@tanstack/react-query' | ||||||
| import { circle } from '@turf/circle' | import { circle } from '@turf/circle' | ||||||
| import { useLastOwnLocation, useLastPlayerLocations } from '@/hooks/useLocation' | import { reverseGeocodeAsync } from 'expo-location' | ||||||
| import React, { useMemo, useState } from 'react' | import React, { useEffect, useMemo, useState } from 'react' | ||||||
| import { PlayerLocation } from '@/utils/features/location/locationSlice' | import { StyleSheet } from 'react-native' | ||||||
|  | import { Button, Dialog, FAB, Portal, Text } from 'react-native-paper' | ||||||
|  | import { useAuth } from '@/hooks/useAuth' | ||||||
| import { useGame } from '@/hooks/useGame' | import { useGame } from '@/hooks/useGame' | ||||||
| import { FAB } from 'react-native-paper' | import { useLastOwnLocation, useLastPlayerLocations } from '@/hooks/useLocation' | ||||||
|  | import { isAuthValid } from '@/utils/features/auth/authSlice' | ||||||
|  | import { Player } from '@/utils/features/game/gameSlice' | ||||||
|  | import { PlayerLocation } from '@/utils/features/location/locationSlice' | ||||||
|  |  | ||||||
| export default function Map() { | export default function Map() { | ||||||
|   const [followUser, setFollowUser] = useState(true) |   const [followUser, setFollowUser] = useState(true) | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <MapComponent followUser={followUser} setFollowUser={setFollowUser} /> |       <MapWrapper followUser={followUser} setFollowUser={setFollowUser} /> | ||||||
|       <FAB |       <FollowUserButton key={'follow-userr-btn-component'} followUser={followUser} setFollowUser={setFollowUser} /> | ||||||
|           style={{ position: 'absolute', right: 25, bottom: 25 }} |  | ||||||
|           icon={(props) => <MaterialIcons name={followUser ? 'my-location' : 'location-searching'} {...props} />} |  | ||||||
|           onPress={() => setFollowUser(followUser => !followUser)} /> |  | ||||||
|     </> |     </> | ||||||
|   ) |   ) | ||||||
| } | } | ||||||
|  |  | ||||||
| function MapComponent({ followUser, setFollowUser }: { followUser?: boolean, setFollowUser: React.Dispatch<React.SetStateAction<boolean>> }) { | type FollowUserProps = { | ||||||
|  |   followUser: boolean, | ||||||
|  |   setFollowUser: React.Dispatch<React.SetStateAction<boolean>>, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function MapWrapper({ followUser, setFollowUser }: FollowUserProps) { | ||||||
|  |   const [displayedPlayerId, setDisplayedPlayerId] = useState<number | null>(null) | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <MapComponent followUser={followUser} setFollowUser={setFollowUser} setDisplayedPlayerId={setDisplayedPlayerId} /> | ||||||
|  |       <Portal> | ||||||
|  |         <PlayerLocationDialog displayedPlayerId={displayedPlayerId} onDismiss={() => setDisplayedPlayerId(null)} /> | ||||||
|  |       </Portal> | ||||||
|  |     </> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | type MapComponentProps = { | ||||||
|  |   followUser?: boolean, | ||||||
|  |   setFollowUser: React.Dispatch<React.SetStateAction<boolean>>, | ||||||
|  |   setDisplayedPlayerId: React.Dispatch<React.SetStateAction<number | null>> | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function MapComponent({ followUser, setFollowUser, setDisplayedPlayerId }: MapComponentProps) { | ||||||
|   MapLibreGL.setAccessToken(null) |   MapLibreGL.setAccessToken(null) | ||||||
|   const userLocation = useLastOwnLocation() |   const userLocation = useLastOwnLocation() | ||||||
|   return ( |   return ( | ||||||
| @@ -44,22 +70,22 @@ function MapComponent({ followUser, setFollowUser }: { followUser?: boolean, set | |||||||
|       <RasterLayer id="railwaymap-layer" sourceID="railwaymap-source" style={{rasterOpacity: 0.7}} /> |       <RasterLayer id="railwaymap-layer" sourceID="railwaymap-source" style={{rasterOpacity: 0.7}} /> | ||||||
|  |  | ||||||
|       <UserLocation animated={true} renderMode="native" androidRenderMode="compass" showsUserHeadingIndicator={true} /> |       <UserLocation animated={true} renderMode="native" androidRenderMode="compass" showsUserHeadingIndicator={true} /> | ||||||
|       <PlayerLocationsMarkers /> |       <PlayerLocationsMarkers setDisplayedPlayerId={setDisplayedPlayerId} /> | ||||||
|     </MapView> |     </MapView> | ||||||
|   ) |   ) | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| function PlayerLocationsMarkers() { | function PlayerLocationsMarkers({ setDisplayedPlayerId }: { setDisplayedPlayerId: React.Dispatch<React.SetStateAction<number | null>> }) { | ||||||
|   const game = useGame() |   const game = useGame() | ||||||
|   const lastPlayerLocations = useLastPlayerLocations() |   const lastPlayerLocations = useLastPlayerLocations() | ||||||
|   return lastPlayerLocations ? lastPlayerLocations |   return lastPlayerLocations ? lastPlayerLocations | ||||||
|     .filter(() => game.currentRunner === true || !game.gameStarted) |     .filter(() => game.currentRunner === true || !game.gameStarted) | ||||||
|     .filter(playerLoc => playerLoc.playerId !== game.playerId) |     .filter(playerLoc => playerLoc.playerId !== game.playerId) | ||||||
|     .map(playerLoc => <PlayerLocationMarker key={`player-${playerLoc.playerId}-loc`} playerLocation={playerLoc} />) : <></> |     .map(playerLoc => <PlayerLocationMarker key={`player-${playerLoc.playerId}-loc`} playerLocation={playerLoc} setDisplayedPlayerId={setDisplayedPlayerId} />) : <></> | ||||||
| } | } | ||||||
|  |  | ||||||
| function PlayerLocationMarker({ playerLocation }: { playerLocation: PlayerLocation }) { | function PlayerLocationMarker({ playerLocation, setDisplayedPlayerId }: { playerLocation: PlayerLocation, setDisplayedPlayerId: React.Dispatch<React.SetStateAction<number | null>> }) { | ||||||
|   const accuracyCircle = useMemo(() => circle([playerLocation.longitude, playerLocation.latitude], playerLocation.accuracy, {steps: 64, units: 'meters'}), [playerLocation]) |   const accuracyCircle = useMemo(() => circle([playerLocation.longitude, playerLocation.latitude], playerLocation.accuracy, {steps: 64, units: 'meters'}), [playerLocation]) | ||||||
|   return <> |   return <> | ||||||
|     <ShapeSource |     <ShapeSource | ||||||
| @@ -76,7 +102,8 @@ function PlayerLocationMarker({ playerLocation }: { playerLocation: PlayerLocati | |||||||
|         style={{lineOpacity: 0.4, lineColor: 'red'}} |         style={{lineOpacity: 0.4, lineColor: 'red'}} | ||||||
|         aboveLayerID={`accuracy-radius-fill-${playerLocation.playerId}`} /> |         aboveLayerID={`accuracy-radius-fill-${playerLocation.playerId}`} /> | ||||||
|     <PointAnnotation id={`player-location-marker-${playerLocation.playerId}`} |     <PointAnnotation id={`player-location-marker-${playerLocation.playerId}`} | ||||||
|         coordinate={[playerLocation.longitude, playerLocation.latitude]}> |         coordinate={[playerLocation.longitude, playerLocation.latitude]} | ||||||
|  |         onSelected={() => { setDisplayedPlayerId(playerLocation.playerId) }}> | ||||||
|       <FontAwesome5 name="map-marker-alt" size={24} color="red" /> |       <FontAwesome5 name="map-marker-alt" size={24} color="red" /> | ||||||
|     </PointAnnotation> |     </PointAnnotation> | ||||||
|   </> |   </> | ||||||
| @@ -88,3 +115,68 @@ const styles = StyleSheet.create({ | |||||||
|     alignSelf: 'stretch', |     alignSelf: 'stretch', | ||||||
|   } |   } | ||||||
| }) | }) | ||||||
|  |  | ||||||
|  | function FollowUserButton({ followUser, setFollowUser }: FollowUserProps) { | ||||||
|  |   return ( | ||||||
|  |     <FAB | ||||||
|  |       key={'follow-user-btn'} | ||||||
|  |       style={{ position: 'absolute', right: 25, bottom: 25 }} | ||||||
|  |       icon={(props) => <MaterialIcons name={followUser ? 'my-location' : 'location-searching'} {...props} />} | ||||||
|  |       onPress={() => setFollowUser(followUser => !followUser)} /> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | function PlayerLocationDialog({ displayedPlayerId, onDismiss }: { displayedPlayerId: number | null, onDismiss: () => void }) { | ||||||
|  |   const auth = useAuth() | ||||||
|  |   const lastPlayerLocations = useLastPlayerLocations() | ||||||
|  |   const playersQuery = useQuery({ | ||||||
|  |       queryKey: ['get-players', auth.token], | ||||||
|  |       queryFn: () => fetch(`${process.env.EXPO_PUBLIC_TRAINTRAPE_MOI_SERVER}/players/`, { | ||||||
|  |         headers: { "Authorization": `Bearer ${auth.token}` }} | ||||||
|  |       ).then(resp => resp.json()), | ||||||
|  |       enabled: isAuthValid(auth), | ||||||
|  |       initialData: { data: [], meta: { currentPage: 0, lastPage: 0, nextPage: 0, prevPage: 0, total: 0, totalPerPage: 0 } }, | ||||||
|  |     }) | ||||||
|  |  | ||||||
|  |   const displayedPlayerLoc = useMemo(() => { | ||||||
|  |     return lastPlayerLocations.find(loc => loc.playerId === displayedPlayerId) | ||||||
|  |   }, [displayedPlayerId, lastPlayerLocations]) | ||||||
|  |  | ||||||
|  |   const displayedPlayerName = useMemo(() => { | ||||||
|  |     if (!playersQuery.isSuccess || !displayedPlayerId) | ||||||
|  |       return "Chargement…" | ||||||
|  |     const player: Player | undefined = playersQuery.data.data.find((player: Player) => player.id === displayedPlayerId) | ||||||
|  |     if (!player) | ||||||
|  |       return "Chargement…" | ||||||
|  |     return player.name | ||||||
|  |   }, [displayedPlayerId, playersQuery]) | ||||||
|  |  | ||||||
|  |   const [address, setAddress] = useState("Adresse inconnue") | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (!displayedPlayerLoc) | ||||||
|  |       return setAddress("Adresse inconnue") | ||||||
|  |     reverseGeocodeAsync(displayedPlayerLoc).then(addresses => setAddress(addresses[0].formattedAddress ?? "Adresse inconnue")) | ||||||
|  |   }, [displayedPlayerLoc]) | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <Dialog visible={displayedPlayerId !== null} onDismiss={onDismiss}> | ||||||
|  |       <Dialog.Title>{displayedPlayerName}</Dialog.Title> | ||||||
|  |       <Dialog.Content> | ||||||
|  |         <Text> | ||||||
|  |           Dernière position : {new Date(displayedPlayerLoc?.timestamp ?? 0).toLocaleString()} | ||||||
|  |         </Text> | ||||||
|  |         <Text> | ||||||
|  |           Précision : {displayedPlayerLoc?.accuracy.toPrecision(3)} m | ||||||
|  |         </Text> | ||||||
|  |         <Text> | ||||||
|  |           Adresse estimée : {address} | ||||||
|  |         </Text> | ||||||
|  |       </Dialog.Content> | ||||||
|  |       <Dialog.Actions> | ||||||
|  |         <Button onPress={onDismiss}> | ||||||
|  |           Fermer | ||||||
|  |         </Button> | ||||||
|  |       </Dialog.Actions> | ||||||
|  |     </Dialog> | ||||||
|  |   ) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ import { Challenge } from '@/utils/features/challenges/challengesSlice' | |||||||
| import { useQuery } from '@tanstack/react-query' | import { useQuery } from '@tanstack/react-query' | ||||||
| import { router } from 'expo-router' | import { router } from 'expo-router' | ||||||
| import { useShareIntentContext } from 'expo-share-intent' | import { useShareIntentContext } from 'expo-share-intent' | ||||||
| import { ReactNode, useEffect } from 'react' | import React, { ReactNode, useEffect } from 'react' | ||||||
|  |  | ||||||
| export default function GameProvider({ children }: { children: ReactNode }) { | export default function GameProvider({ children }: { children: ReactNode }) { | ||||||
|   const auth = useAuth() |   const auth = useAuth() | ||||||
|   | |||||||
| @@ -1,14 +1,14 @@ | |||||||
| import { ReactNode, useEffect } from 'react' |  | ||||||
| import { useAuth } from '@/hooks/useAuth' |  | ||||||
| import { useQueuedLocations, useSetLastPlayerLocation, useSetLastPlayerLocations, useUnqueueLocation } from '@/hooks/useLocation' |  | ||||||
| import { useGeolocationMutation } from '@/hooks/mutations/useGeolocationMutation' |  | ||||||
| import { useStartGeolocationServiceEffect } from '@/utils/geolocation' |  | ||||||
| import { Platform } from 'react-native' |  | ||||||
| import { useQuery } from '@tanstack/react-query' | import { useQuery } from '@tanstack/react-query' | ||||||
| import { isAuthValid } from '@/utils/features/auth/authSlice' | import React, { ReactNode, useEffect } from 'react' | ||||||
| import { socket } from '@/utils/socket' | import { Platform } from 'react-native' | ||||||
| import { PlayerLocation } from '@/utils/features/location/locationSlice' |  | ||||||
| import { Constants } from '@/constants/Constants' | import { Constants } from '@/constants/Constants' | ||||||
|  | import { useAuth } from '@/hooks/useAuth' | ||||||
|  | import { useGeolocationMutation } from '@/hooks/mutations/useGeolocationMutation' | ||||||
|  | import { useQueuedLocations, useSetLastPlayerLocation, useSetLastPlayerLocations, useUnqueueLocation } from '@/hooks/useLocation' | ||||||
|  | import { isAuthValid } from '@/utils/features/auth/authSlice' | ||||||
|  | import { PlayerLocation } from '@/utils/features/location/locationSlice' | ||||||
|  | import { useStartGeolocationServiceEffect } from '@/utils/geolocation' | ||||||
|  | import { socket } from '@/utils/socket' | ||||||
|  |  | ||||||
| export default function GeolocationProvider({ children }: { children: ReactNode }) { | export default function GeolocationProvider({ children }: { children: ReactNode }) { | ||||||
|   useStartGeolocationServiceEffect() |   useStartGeolocationServiceEffect() | ||||||
| @@ -51,10 +51,14 @@ export default function GeolocationProvider({ children }: { children: ReactNode | |||||||
|       setLastPlayerLocations(lastLocationsQuery.data) |       setLastPlayerLocations(lastLocationsQuery.data) | ||||||
|   }, [lastLocationsQuery.status, lastLocationsQuery.dataUpdatedAt]) |   }, [lastLocationsQuery.status, lastLocationsQuery.dataUpdatedAt]) | ||||||
|  |  | ||||||
|   socket.on('last-location', (data: PlayerLocation) => { |   useEffect(() => { | ||||||
|     if (data.playerId) |     const locationListener = async (data: PlayerLocation) => { | ||||||
|       setLastPlayerLocation(data) |       if (data.playerId) | ||||||
|   }) |         setLastPlayerLocation(data) | ||||||
|  |     } | ||||||
|  |     socket.on('last-location', locationListener) | ||||||
|  |     return () => { socket.off('last-location', locationListener) } | ||||||
|  |   }, []) | ||||||
|  |  | ||||||
|   return <> |   return <> | ||||||
|     {children} |     {children} | ||||||
|   | |||||||
| @@ -1,4 +1,7 @@ | |||||||
|  | import { Accuracy } from 'expo-location' | ||||||
|  |  | ||||||
| export const Constants = { | export const Constants = { | ||||||
|  |   LOCATION_ACCURACY: Accuracy.BestForNavigation, | ||||||
|   MIN_DELAY_LOCATION_SENT: 20, |   MIN_DELAY_LOCATION_SENT: 20, | ||||||
|   QUERY_REFETCH_INTERVAL: 15, |   QUERY_REFETCH_INTERVAL: 15, | ||||||
| } | } | ||||||
|   | |||||||
| @@ -20,6 +20,13 @@ export interface PenaltyPayload { | |||||||
|   penaltyEnd: number | null |   penaltyEnd: number | null | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export interface Player { | ||||||
|  |   id: number | ||||||
|  |   name: string | ||||||
|  |   money: number | ||||||
|  |   activeChallengeId: number | null | ||||||
|  | } | ||||||
|  |  | ||||||
| export interface GameState { | export interface GameState { | ||||||
|   playerId: number | null |   playerId: number | null | ||||||
|   runId: number | null |   runId: number | null | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ import { PlayerLocation, setLastLocation } from './features/location/locationSli | |||||||
| import store from './store' | import store from './store' | ||||||
| import { useEffect } from 'react' | import { useEffect } from 'react' | ||||||
| import { socket } from './socket' | import { socket } from './socket' | ||||||
|  | import { Constants } from '@/constants/Constants' | ||||||
|  |  | ||||||
| const LOCATION_TASK = "fetch-geolocation" | const LOCATION_TASK = "fetch-geolocation" | ||||||
|  |  | ||||||
| @@ -16,7 +17,6 @@ TaskManager.defineTask(LOCATION_TASK, async ({ data, error }: any) => { | |||||||
|   const { locations } = data |   const { locations } = data | ||||||
|   const lastLoc: Location.LocationObject = locations.at(-1) |   const lastLoc: Location.LocationObject = locations.at(-1) | ||||||
|   store.dispatch(setLastLocation(lastLoc)) |   store.dispatch(setLastLocation(lastLoc)) | ||||||
|   console.log("sending-loc", lastLoc, socket.active) |  | ||||||
|   const playerId = store.getState().game.playerId |   const playerId = store.getState().game.playerId | ||||||
|   if (socket.active && playerId) { |   if (socket.active && playerId) { | ||||||
|     const lastLocToSend: PlayerLocation = { |     const lastLocToSend: PlayerLocation = { | ||||||
| @@ -25,11 +25,11 @@ TaskManager.defineTask(LOCATION_TASK, async ({ data, error }: any) => { | |||||||
|       latitude: lastLoc.coords.latitude, |       latitude: lastLoc.coords.latitude, | ||||||
|       speed: lastLoc.coords.speed ?? 0, |       speed: lastLoc.coords.speed ?? 0, | ||||||
|       accuracy: lastLoc.coords.accuracy ?? 0, |       accuracy: lastLoc.coords.accuracy ?? 0, | ||||||
|       altitude: lastLoc.coords.accuracy ?? 0, |       altitude: lastLoc.coords.altitude ?? 0, | ||||||
|       altitudeAccuracy: lastLoc.coords.altitudeAccuracy ?? 0, |       altitudeAccuracy: lastLoc.coords.altitudeAccuracy ?? 0, | ||||||
|       timestamp: new Date(lastLoc.timestamp).toISOString(), |       timestamp: new Date(lastLoc.timestamp).toISOString(), | ||||||
|     } |     } | ||||||
|     socket.emit('last-location', { playerId: playerId, loc: lastLocToSend }) |     socket.emit('last-location', lastLocToSend) | ||||||
|   } |   } | ||||||
| }) | }) | ||||||
|    |    | ||||||
| @@ -49,10 +49,10 @@ export async function startGeolocationService(): Promise<void | (() => void)> { | |||||||
|  |  | ||||||
|   if (Platform.OS !== "web") { |   if (Platform.OS !== "web") { | ||||||
|     await Location.startLocationUpdatesAsync(LOCATION_TASK, { |     await Location.startLocationUpdatesAsync(LOCATION_TASK, { | ||||||
|       accuracy: Location.Accuracy.BestForNavigation, |       accuracy: Constants.LOCATION_ACCURACY, | ||||||
|       activityType: Location.ActivityType.OtherNavigation, |       activityType: Location.ActivityType.OtherNavigation, | ||||||
|       deferredUpdatesInterval: 100, |       deferredUpdatesInterval: 1000, | ||||||
|       timeInterval: 100, |       timeInterval: 1000, | ||||||
|       foregroundService: { |       foregroundService: { | ||||||
|         killServiceOnDestroy: false, |         killServiceOnDestroy: false, | ||||||
|         notificationBody: "Géolocalisation activée pour « Traintrape-moi »", |         notificationBody: "Géolocalisation activée pour « Traintrape-moi »", | ||||||
| @@ -63,7 +63,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: Constants.LOCATION_ACCURACY }, location_nouveau => store.dispatch(setLastLocation(location_nouveau))) | ||||||
|     return locationSubscription.remove |     return locationSubscription.remove | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user