From 05335d36ffc11f5771768f643b9dab1ab37502ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9lian=20Riboulet?= Date: Mon, 5 Sep 2022 10:17:37 +0200 Subject: [PATCH] Geohash (#192) * Setup geohash zones around user * Add socket implementation * Cleanup * Requested changes --- package-lock.json | 14 +++++ package.json | 1 + src/components/DropyMap.js | 67 +++++++++++++++++++++--- src/components/Sonar.js | 7 ++- src/hooks/useDropiesAroundSocket.js | 24 +++------ src/hooks/useTravelDistanceCallback.js | 62 ---------------------- src/states/GeolocationContextProvider.js | 15 +++++- 7 files changed, 100 insertions(+), 90 deletions(-) delete mode 100644 src/hooks/useTravelDistanceCallback.js diff --git a/package-lock.json b/package-lock.json index 431bdad0..85020dd3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "expo": "^45.0.5", "expo-font": "^10.1.0", "expo-haptics": "^11.2.0", + "ngeohash": "^0.6.3", "react": "17.0.2", "react-native": "0.68.2", "react-native-background-fetch": "^4.1.0", @@ -12195,6 +12196,14 @@ "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.0.1.tgz", "integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==" }, + "node_modules/ngeohash": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/ngeohash/-/ngeohash-0.6.3.tgz", + "integrity": "sha512-kltF0cOxgx1AbmVzKxYZaoB0aj7mOxZeHaerEtQV0YaqnkXNq26WWqMmJ6lTqShYxVRWZ/mwvvTrNeOwdslWiw==", + "engines": { + "node": ">=v0.2.0" + } + }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -25019,6 +25028,11 @@ "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.0.1.tgz", "integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==" }, + "ngeohash": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/ngeohash/-/ngeohash-0.6.3.tgz", + "integrity": "sha512-kltF0cOxgx1AbmVzKxYZaoB0aj7mOxZeHaerEtQV0YaqnkXNq26WWqMmJ6lTqShYxVRWZ/mwvvTrNeOwdslWiw==" + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", diff --git a/package.json b/package.json index 7a230142..570acbc2 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "expo": "^45.0.5", "expo-font": "^10.1.0", "expo-haptics": "^11.2.0", + "ngeohash": "^0.6.3", "react": "17.0.2", "react-native": "0.68.2", "react-native-background-fetch": "^4.1.0", diff --git a/src/components/DropyMap.js b/src/components/DropyMap.js index 863b300a..f758fd39 100644 --- a/src/components/DropyMap.js +++ b/src/components/DropyMap.js @@ -1,8 +1,9 @@ import React, { useEffect, useRef, useState } from 'react'; import { StyleSheet, Platform } from 'react-native'; -import MapView, { PROVIDER_GOOGLE } from 'react-native-maps'; +import MapView, { Circle, Polygon, PROVIDER_GOOGLE } from 'react-native-maps'; import LinearGradient from 'react-native-linear-gradient'; +import Geohash from 'ngeohash'; import { useNavigation } from '@react-navigation/native'; import { useInitializedGeolocation } from '../hooks/useGeolocation'; @@ -14,12 +15,14 @@ import mapStyleIOS from '../assets/mapStyleIOS.json'; import API from '../services/API'; import Haptics from '../utils/haptics'; +import useCurrentUser from '../hooks/useCurrentUser'; +import { GEOHASH_SIZE } from '../states/GeolocationContextProvider'; import { coordinatesDistance } from '../utils/coordinates'; import MapLoadingOverlay from './overlays/MapLoadingOverlay'; -import Sonar from './Sonar'; import DropyMapMarker from './DropyMapMarker'; import DebugText from './DebugText'; import RetrievedDropyMapMarker from './RetrievedDropyMapMarker'; +import Sonar from './Sonar'; const INITIAL_PITCH = 10; const INITIAL_ZOOM = 17; @@ -31,6 +34,10 @@ const DropyMap = ({ dropiesAround, retrieveDropy, museumVisible, selectedDropyIn const { sendBottomAlert } = useOverlay(); const { userCoordinates, compassHeading, initialized: geolocationInitialized } = useInitializedGeolocation(); + const { developerMode } = useCurrentUser(); + + const mapRef = useRef(null); + const [mapIsReady, setMapIsReady] = useState(false); const handleDropyPressed = async (dropy) => { try { @@ -57,10 +64,6 @@ const DropyMap = ({ dropiesAround, retrieveDropy, museumVisible, selectedDropyIn } }; - const mapRef = useRef(null); - - const [mapIsReady, setMapIsReady] = useState(false); - useEffect(() => { if(mapIsReady === false) return; if(mapRef?.current == null) return; @@ -145,6 +148,7 @@ const DropyMap = ({ dropiesAround, retrieveDropy, museumVisible, selectedDropyIn ))} )} + {developerMode && } @@ -156,9 +160,58 @@ const DropyMap = ({ dropiesAround, retrieveDropy, museumVisible, selectedDropyIn style={StyleSheet.absoluteFillObject} /> {JSON.stringify(userCoordinates, null, 2)} - {JSON.stringify(dropiesAround, null, 2)} + {JSON.stringify(dropiesAround, null, 2)} ); }; export default DropyMap; + +const MapDebugger = ({ userCoordinates }) => { + const [debugPolygons, setDebugPolygons] = useState([]); + + useEffect(() => { + if(!userCoordinates) return; + + const polygons = []; + for (const chunkInt of userCoordinates.geoHashs) { + const [ + minlat, + minlon, + maxlat, + maxlon + ] = Geohash.decode_bbox_int(chunkInt, GEOHASH_SIZE); + polygons.push([ + { latitude: minlat, longitude: minlon }, + { latitude: maxlat, longitude: minlon }, + { latitude: maxlat, longitude: maxlon }, + { latitude: minlat, longitude: maxlon } + ]); + } + + setDebugPolygons(polygons); + }, [userCoordinates]); + + return ( + <> + {debugPolygons.map((polygon, index) => ( + + + + ))} + + + + ); +}; diff --git a/src/components/Sonar.js b/src/components/Sonar.js index 4b37e60c..c3645c28 100644 --- a/src/components/Sonar.js +++ b/src/components/Sonar.js @@ -5,6 +5,8 @@ import Svg, { Circle, RadialGradient, Stop } from 'react-native-svg'; import Styles, { Colors } from '../styles/Styles'; import GlassCircleButton from './GlassCircleButton'; +const CENTER_ICON_SIZE = 15; + const AnimatedSvg = Animated.createAnimatedComponent(Svg); const Sonar = ({ visible }) => { @@ -60,7 +62,8 @@ const Sonar = ({ visible }) => { pointerEvents='none' style={{ ...Styles.center, - transform: [{ scale: visibleAnimatedValue }], + ...StyleSheet.absoluteFillObject, + transform: [ { translateY: CENTER_ICON_SIZE / 2 }, { scale: visibleAnimatedValue }], }}> { cx="50" cy="50" r="50" fill="url(#grad)" /> - + ); }; diff --git a/src/hooks/useDropiesAroundSocket.js b/src/hooks/useDropiesAroundSocket.js index 9dc3adc7..950b0866 100644 --- a/src/hooks/useDropiesAroundSocket.js +++ b/src/hooks/useDropiesAroundSocket.js @@ -1,6 +1,5 @@ import { useEffect, useState } from 'react'; import useCurrentUser from './useCurrentUser'; -import useTravelDistanceCallback from './useTravelDistanceCallback'; import { useInitializedGeolocation } from './useGeolocation'; import useSocket from './useSocket'; @@ -13,17 +12,11 @@ const useDropiesAroundSocket = () => { const [dropiesAround, setDropiesAround] = useState([]); - useTravelDistanceCallback(updateAllDropiesAround, 100); - useEffect(() => { if (geolocationInitialized === false) return; if (dropySocket == null) return; - updateAllDropiesAround(); - dropySocket.on('connect', updateAllDropiesAround); - dropySocket.on('dropy_created', (response) => { - if (response.error != null) { console.error('Error getting created dropy', response.error); return; @@ -50,26 +43,22 @@ const useDropiesAroundSocket = () => { }; }, [geolocationInitialized]); - function updateAllDropiesAround() { - if (dropySocket == null) return; - if (dropySocket.connected === false) return; - if(userCoordinates == null) return; + useEffect(() => { + dropySocket.emit('zones_update', { zones: userCoordinates.geoHashs }, (response) => { - dropySocket.emit('all_dropies_around', { - latitude: userCoordinates.latitude, - longitude: userCoordinates.longitude, - }, (response) => { if(response.error != null) { - console.error('Error getting dropies around', response.error); + console.error('Error updating zones', response.error); return; } + const dropies = response.data.slice(0, 30).map((dropy) => ({ ...dropy, isUserDropy: dropy.emitterId === user.id, })); + setDropiesAround(dropies ?? []); }); - } + }, [userCoordinates.geoHashs[0]]); const createDropy = (latitude, longitude, mediaType, content) => { return new Promise((resolve) => { @@ -78,6 +67,7 @@ const useDropiesAroundSocket = () => { }; const retrieveDropy = (dropyId) => { + setDropiesAround(olds => olds.filter(dropy => dropy.id !== dropyId)); return new Promise((resolve) => { dropySocket.emit('dropy_retrieved', { dropyId }, resolve); }); diff --git a/src/hooks/useTravelDistanceCallback.js b/src/hooks/useTravelDistanceCallback.js deleted file mode 100644 index a3541b42..00000000 --- a/src/hooks/useTravelDistanceCallback.js +++ /dev/null @@ -1,62 +0,0 @@ -import { useEffect, useRef } from 'react'; -import { coordinatesDistance } from '../utils/coordinates'; -import useGeolocation from './useGeolocation'; - -const useTravelDistanceCallback = ( - /** - * Called when the user has moved more than the specified trigger - * distance - */ - distanceCallback = undefined, - /** - * Distance in meters the user need to move to trigger the callback - * @default 20 - */ - triggerDistanceMeters = 20, - /** - * Call the callback even if the user has not moved after the specified - * time delay (undefined = no timeout behaviour) - * @default undefined - */ - timeoutTime = undefined -) => { - - const lastTrigger = useRef({ coordinates: null, timestamp: null }); - - const { userCoordinates } = useGeolocation(); - - useEffect(() => { - if(timeoutTime == null) return; - const interval = setInterval(() => { - if(Date.now() - lastTrigger.current.timestamp > timeoutTime) { - trigger(); - } - }, 1000); - return () => clearInterval(interval); - }, []); - - useEffect(() => { - if(userCoordinates == null) return; - if(distanceCallback == null) return; - - if(lastTrigger.current.coordinates == null) { - trigger(); - return; - } - - const distance = coordinatesDistance(userCoordinates, lastTrigger.current.coordinates); - if(distance > triggerDistanceMeters) { - trigger(); - } - }, [userCoordinates]); - - const trigger = () => { - lastTrigger.current = { - coordinates: userCoordinates, - timestamp: Date.now(), - }; - distanceCallback(); - }; -}; - -export default useTravelDistanceCallback; diff --git a/src/states/GeolocationContextProvider.js b/src/states/GeolocationContextProvider.js index dd366533..9767c897 100644 --- a/src/states/GeolocationContextProvider.js +++ b/src/states/GeolocationContextProvider.js @@ -1,6 +1,7 @@ import React, { createContext, useState } from 'react'; import Geolocation from 'react-native-geolocation-service'; import CompassHeading from 'react-native-compass-heading'; +import Geohash from 'ngeohash'; import usePermissions from '../hooks/usePermissions'; import useEffectForegroundOnly from '../hooks/useEffectForegroundOnly'; @@ -9,6 +10,8 @@ import GeolocationModal from '../components/overlays/GeolocationModal'; export const GeolocationContext = createContext(null); +export const GEOHASH_SIZE = 32; + const GeolocationProvider = ({ children }) => { const [userCoordinates, setUserCoordinates] = useState(null); @@ -28,8 +31,16 @@ const GeolocationProvider = ({ children }) => { const registerGeolocationListener = () => Geolocation.watchPosition( (infos) => { - const { coords } = infos; - setUserCoordinates(coords); + const { latitude, longitude } = infos.coords; + + const hash = Geohash.encode_int(latitude, longitude, GEOHASH_SIZE); + const geoHashs = [hash, ...Geohash.neighbors_int(hash, GEOHASH_SIZE)]; + + setUserCoordinates({ + latitude, + longitude, + geoHashs, + }); }, console.warn, {