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 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 { useLastOwnLocation, useLastPlayerLocations } from '@/hooks/useLocation' | ||||
| import React, { useMemo, useState } from 'react' | ||||
| import { PlayerLocation } from '@/utils/features/location/locationSlice' | ||||
| import { reverseGeocodeAsync } from 'expo-location' | ||||
| import React, { useEffect, useMemo, useState } from 'react' | ||||
| 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 { 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() { | ||||
|   const [followUser, setFollowUser] = useState(true) | ||||
|  | ||||
|   return ( | ||||
|     <> | ||||
|       <MapComponent followUser={followUser} setFollowUser={setFollowUser} /> | ||||
|       <FAB | ||||
|           style={{ position: 'absolute', right: 25, bottom: 25 }} | ||||
|           icon={(props) => <MaterialIcons name={followUser ? 'my-location' : 'location-searching'} {...props} />} | ||||
|           onPress={() => setFollowUser(followUser => !followUser)} /> | ||||
|       <MapWrapper followUser={followUser} setFollowUser={setFollowUser} /> | ||||
|       <FollowUserButton key={'follow-userr-btn-component'} followUser={followUser} setFollowUser={setFollowUser} /> | ||||
|     </> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| 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) | ||||
|   const userLocation = useLastOwnLocation() | ||||
|   return ( | ||||
| @@ -44,22 +70,22 @@ function MapComponent({ followUser, setFollowUser }: { followUser?: boolean, set | ||||
|       <RasterLayer id="railwaymap-layer" sourceID="railwaymap-source" style={{rasterOpacity: 0.7}} /> | ||||
|  | ||||
|       <UserLocation animated={true} renderMode="native" androidRenderMode="compass" showsUserHeadingIndicator={true} /> | ||||
|       <PlayerLocationsMarkers /> | ||||
|       <PlayerLocationsMarkers setDisplayedPlayerId={setDisplayedPlayerId} /> | ||||
|     </MapView> | ||||
|   ) | ||||
| } | ||||
|  | ||||
|  | ||||
| function PlayerLocationsMarkers() { | ||||
| function PlayerLocationsMarkers({ setDisplayedPlayerId }: { setDisplayedPlayerId: React.Dispatch<React.SetStateAction<number | null>> }) { | ||||
|   const game = useGame() | ||||
|   const lastPlayerLocations = useLastPlayerLocations() | ||||
|   return lastPlayerLocations ? lastPlayerLocations | ||||
|     .filter(() => game.currentRunner === true || !game.gameStarted) | ||||
|     .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]) | ||||
|   return <> | ||||
|     <ShapeSource | ||||
| @@ -76,7 +102,8 @@ function PlayerLocationMarker({ playerLocation }: { playerLocation: PlayerLocati | ||||
|         style={{lineOpacity: 0.4, lineColor: 'red'}} | ||||
|         aboveLayerID={`accuracy-radius-fill-${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" /> | ||||
|     </PointAnnotation> | ||||
|   </> | ||||
| @@ -88,3 +115,68 @@ const styles = StyleSheet.create({ | ||||
|     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 { router } from 'expo-router' | ||||
| import { useShareIntentContext } from 'expo-share-intent' | ||||
| import { ReactNode, useEffect } from 'react' | ||||
| import React, { ReactNode, useEffect } from 'react' | ||||
|  | ||||
| export default function GameProvider({ children }: { children: ReactNode }) { | ||||
|   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 { isAuthValid } from '@/utils/features/auth/authSlice' | ||||
| import { socket } from '@/utils/socket' | ||||
| import { PlayerLocation } from '@/utils/features/location/locationSlice' | ||||
| import React, { ReactNode, useEffect } from 'react' | ||||
| import { Platform } from 'react-native' | ||||
| 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 }) { | ||||
|   useStartGeolocationServiceEffect() | ||||
| @@ -51,10 +51,14 @@ export default function GeolocationProvider({ children }: { children: ReactNode | ||||
|       setLastPlayerLocations(lastLocationsQuery.data) | ||||
|   }, [lastLocationsQuery.status, lastLocationsQuery.dataUpdatedAt]) | ||||
|  | ||||
|   socket.on('last-location', (data: PlayerLocation) => { | ||||
|   useEffect(() => { | ||||
|     const locationListener = async (data: PlayerLocation) => { | ||||
|       if (data.playerId) | ||||
|         setLastPlayerLocation(data) | ||||
|   }) | ||||
|     } | ||||
|     socket.on('last-location', locationListener) | ||||
|     return () => { socket.off('last-location', locationListener) } | ||||
|   }, []) | ||||
|  | ||||
|   return <> | ||||
|     {children} | ||||
|   | ||||
| @@ -1,4 +1,7 @@ | ||||
| import { Accuracy } from 'expo-location' | ||||
|  | ||||
| export const Constants = { | ||||
|   LOCATION_ACCURACY: Accuracy.BestForNavigation, | ||||
|   MIN_DELAY_LOCATION_SENT: 20, | ||||
|   QUERY_REFETCH_INTERVAL: 15, | ||||
| } | ||||
|   | ||||
| @@ -20,6 +20,13 @@ export interface PenaltyPayload { | ||||
|   penaltyEnd: number | null | ||||
| } | ||||
|  | ||||
| export interface Player { | ||||
|   id: number | ||||
|   name: string | ||||
|   money: number | ||||
|   activeChallengeId: number | null | ||||
| } | ||||
|  | ||||
| export interface GameState { | ||||
|   playerId: number | null | ||||
|   runId: number | null | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import { PlayerLocation, setLastLocation } from './features/location/locationSli | ||||
| import store from './store' | ||||
| import { useEffect } from 'react' | ||||
| import { socket } from './socket' | ||||
| import { Constants } from '@/constants/Constants' | ||||
|  | ||||
| const LOCATION_TASK = "fetch-geolocation" | ||||
|  | ||||
| @@ -16,7 +17,6 @@ TaskManager.defineTask(LOCATION_TASK, async ({ data, error }: any) => { | ||||
|   const { locations } = data | ||||
|   const lastLoc: Location.LocationObject = locations.at(-1) | ||||
|   store.dispatch(setLastLocation(lastLoc)) | ||||
|   console.log("sending-loc", lastLoc, socket.active) | ||||
|   const playerId = store.getState().game.playerId | ||||
|   if (socket.active && playerId) { | ||||
|     const lastLocToSend: PlayerLocation = { | ||||
| @@ -25,11 +25,11 @@ TaskManager.defineTask(LOCATION_TASK, async ({ data, error }: any) => { | ||||
|       latitude: lastLoc.coords.latitude, | ||||
|       speed: lastLoc.coords.speed ?? 0, | ||||
|       accuracy: lastLoc.coords.accuracy ?? 0, | ||||
|       altitude: lastLoc.coords.accuracy ?? 0, | ||||
|       altitude: lastLoc.coords.altitude ?? 0, | ||||
|       altitudeAccuracy: lastLoc.coords.altitudeAccuracy ?? 0, | ||||
|       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") { | ||||
|     await Location.startLocationUpdatesAsync(LOCATION_TASK, { | ||||
|       accuracy: Location.Accuracy.BestForNavigation, | ||||
|       accuracy: Constants.LOCATION_ACCURACY, | ||||
|       activityType: Location.ActivityType.OtherNavigation, | ||||
|       deferredUpdatesInterval: 100, | ||||
|       timeInterval: 100, | ||||
|       deferredUpdatesInterval: 1000, | ||||
|       timeInterval: 1000, | ||||
|       foregroundService: { | ||||
|         killServiceOnDestroy: false, | ||||
|         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) | ||||
|   } | ||||
|   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 | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user