Commit 18325cf4 authored by Milan John Paul Digiuseppe's avatar Milan John Paul Digiuseppe

Merge branch 'map-location-geosearch' into 'master'

Follow user location + geosearch + location service updates

See merge request !8
parents 338453ee 9bbf6fa0
......@@ -27,6 +27,7 @@
"max-len": "error",
"no-console": "off",
"no-param-reassign": "off",
"no-unused-expressions": "off",
"react/proptypes": [0, {}],
"react/jsx-filename-extension": [
1,
......
......@@ -2,7 +2,6 @@
import React from 'react';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import { getCurrentPosition } from './src/store/location/LocationActions';
import RootReducer from './src/store/RootReducer';
import AppContainer from './src/AppContainer';
......@@ -10,8 +9,6 @@ const store = configureStore({
reducer: RootReducer,
});
store.dispatch(getCurrentPosition());
const App = () => (
<Provider store={store}>
<AppContainer />
......
import React, { useCallback } from 'react';
import { View, ViewStyle } from 'react-native';
import { Icon } from 'react-native-elements';
import { LatLng } from 'react-native-maps';
import theme from '../theme';
interface Props {
onDirectionPress: (locationDelta: LatLng) => void;
style?: ViewStyle;
}
// NOTE: intended to be a dev tool
// paste this JSX where you may need it:
/**
* <ArrowButtons
onDirectionPress={(locationDelta: LatLng) => {
onLocationChange({
latitude: userLocation!.latitude + locationDelta.latitude,
longitude: userLocation!.longitude + locationDelta.longitude,
});
}}
style={{
position: 'absolute',
bottom: 20,
left: 20,
}}
/>
*/
const ArrowButtons: React.FC<Props> = ({ onDirectionPress, style }) => {
const onPress = useCallback((direction: string) => {
const delta = 0.0001;
const locationDelta: LatLng = { latitude: 0, longitude: 0 };
switch (direction) {
case 'up':
locationDelta.latitude += delta;
break;
case 'right':
locationDelta.longitude += delta;
break;
case 'left':
locationDelta.longitude -= delta;
break;
case 'down':
locationDelta.latitude -= delta;
break;
default:
console.warn('bruh');
}
onDirectionPress(locationDelta);
}, [onDirectionPress]);
return (
<View style={style}>
{['up', 'left', 'right', 'down'].map((direction) => (
<Icon
key={direction}
name={`arrow-${direction}`}
type={'font-awesome-5'}
reverse
raised
onPress={() => onPress(direction)}
color={theme.colors.black}
size={34}
/>
))}
</View>
);
};
export default ArrowButtons;
import React, { useEffect } from 'react';
import MapView from 'react-native-maps';
import React, {
useEffect, useState, useRef, useCallback,
} from 'react';
import MapView, { LatLng } from 'react-native-maps';
import { StyleSheet, Keyboard } from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import { getUserLatLng } from '../store/location/LocationSelectors';
import { useDispatch } from 'react-redux';
import { watchPositionAsync } from 'expo-location';
import { geoSearch } from '../store/caches/CachesActions';
import customMapStyle from '../CustomMapStyle.json';
import UserLocationMarker from './UserLocationMarker';
import { distanceBetweenCoordinates } from '../utils/Geography';
const styles = StyleSheet.create({
map: {
......@@ -14,32 +18,55 @@ const styles = StyleSheet.create({
const DiscoverMap: React.FC = ({ children }) => {
const dispatch = useDispatch();
const userLocation = useSelector(getUserLatLng);
// const onUserLocationChange = useCallback((e: EventUserLocation) => {
// const { latitude, longitude } = e.nativeEvent.coordinate;
// setUserLocation({ latitude, longitude });
// }, [setUserLocation]);
const [userLocation, setUserLocation] = useState<LatLng>();
const [lastGeosearch, setLastGeosearch] = useState<LatLng>();
const mapRef = useRef<MapView>(null);
const onLocationChange = useCallback((location: LatLng) => {
setUserLocation(location);
mapRef.current?.animateCamera({
center: location,
}, { duration: 100 });
}, [mapRef, dispatch]);
useEffect(() => {
dispatch(geoSearch(userLocation, '1'));
}, []); // TODO: update deps
// Subscribe to location updates
watchPositionAsync({
accuracy: 5, // "Highest"
distanceInterval: 5, // meters
},
({ coords }) => onLocationChange(coords));
}, []);
useEffect(() => {
// Call geosearch when user has moved 100m since last geosearch location
if (!userLocation) return;
if (!lastGeosearch) {
dispatch(geoSearch(userLocation, '1'));
setLastGeosearch(userLocation);
return;
}
const d = distanceBetweenCoordinates(lastGeosearch, userLocation);
if (d > 0.100) {
dispatch(geoSearch(userLocation, '1'));
setLastGeosearch(userLocation);
}
}, [userLocation, lastGeosearch, setLastGeosearch]);
return (
<MapView
ref={mapRef}
provider={'google'}
style={styles.map}
showsUserLocation
camera={{
center: {
latitude: userLocation.latitude,
longitude: userLocation.longitude,
},
pitch: 80,
customMapStyle={customMapStyle}
initialCamera={{
center: userLocation!,
heading: 0,
altitude: 0, // not used for Google Maps
pitch: 80,
zoom: 18,
altitude: 0, // not used for Google Maps
}}
showsPointsOfInterest={false}
customMapStyle={customMapStyle}
loadingEnabled // TODO: test
pitchEnabled={false}
zoomEnabled={false}
......@@ -47,6 +74,7 @@ const DiscoverMap: React.FC = ({ children }) => {
onPress={() => Keyboard.dismiss()}
>
{children}
<UserLocationMarker location={userLocation!} />
</MapView>
);
};
......
import React, { useCallback } from 'react';
import { View, ViewStyle } from 'react-native';
import { useNavigation } from '@react-navigation/native';
import MenuButton from './MenuButton';
import { Icon } from 'react-native-elements';
import theme from '../theme';
const BUTTON_SIZE = 34;
interface Props {
style?: ViewStyle;
}
......@@ -15,15 +17,23 @@ const DiscoverMenu: React.FC<Props> = ({ style }) => {
return (
<View style={style}>
<MenuButton
<Icon
name={'book'}
type={'font-awesome-5'}
reverse
raised
onPress={onSavedPress}
icon={'book'}
backgroundColor={'blue'}
style={{ marginBottom: theme.spacing.medium }}
color={theme.colors.blue}
size={BUTTON_SIZE}
/>
<MenuButton
<Icon
name={'feather-alt'}
type={'font-awesome-5'}
reverse
raised
onPress={onCreatePress}
icon={'feather-alt'}
color={theme.colors.green}
size={BUTTON_SIZE}
/>
</View>
);
......
import React from 'react';
import { View, ViewStyle } from 'react-native';
import { Icon } from 'react-native-elements';
import { TouchableOpacity } from 'react-native-gesture-handler';
import theme, { Color, COLOR_MAP } from '../theme';
const DEFAULT_SIZE = 34;
interface Props {
onPress: () => void;
icon: string;
size?: number;
backgroundColor?: Color;
style?: ViewStyle;
}
const MenuButton: React.FC<Props> = ({
onPress,
icon,
size = DEFAULT_SIZE,
backgroundColor = 'green',
style,
}) => (
<View
style={[
{
backgroundColor: COLOR_MAP[backgroundColor],
borderRadius: size,
},
style,
]}
>
<TouchableOpacity onPress={onPress} style={{ flex: 1 }}>
<Icon
name={icon}
type={'font-awesome-5'}
color={theme.colors.white}
style={{ padding: DEFAULT_SIZE / 2 }}
size={DEFAULT_SIZE}
/>
</TouchableOpacity>
</View>
);
export default MenuButton;
import React from 'react';
import { Marker, LatLng } from 'react-native-maps';
import { Icon } from 'react-native-elements';
import theme from '../theme';
interface Props {
location: LatLng;
}
const UserLocationMarker: React.FC<Props> = ({ location }) => (
<Marker coordinate={location}>
<Icon
color={theme.colors.darkgray}
name={'male'}
type={'font-awesome-5'}
size={48}
/>
</Marker>
);
export default UserLocationMarker;
export const getCurrentPosition = (callback) => {
navigator.geolocation.getCurrentPosition(
(position) => {
const lat = parseFloat(JSON.stringify(position.coords.latitude));
const long = parseFloat(JSON.stringify(position.coords.longitude));
callback(lat, long);
},
(error) => {
console.log(error.message);
},
// { enableHighAccuracy: true, timeout: 20000, maximumAge: 1000 }
);
};
import React, { useCallback } from 'react';
import { StyleSheet, View } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { reverseGeocodeAsync } from 'expo-location';
import { useNavigation } from '@react-navigation/native';
import { geoSearch } from '../store/caches/CachesActions';
import { getUserLatLng } from '../store/location/LocationSelectors';
import { signOutAsync } from '../store/auth/AuthActions';
import GeoButton from '../components/GeoButton';
import theme from '../theme';
import { setUserLatLng, getCurrentPosition } from '../store/location/LocationActions';
const styles = StyleSheet.create({
fullscreen: {
......@@ -23,16 +23,25 @@ const styles = StyleSheet.create({
const DevScreen: React.FC = () => {
const navigation = useNavigation();
const dispatch = useDispatch();
const userLocation = useSelector(getUserLatLng);
const onClose = useCallback(() => navigation.goBack(), [navigation]);
const geosearch = useCallback(() => {
const onGeosearch = useCallback(async () => {
const userLocation = await getCurrentPosition();
dispatch(geoSearch(userLocation, '1'));
navigation.navigate('Discover');
}, [dispatch, userLocation, navigation]);
}, [dispatch, navigation]);
const reverseGeocode = useCallback(async () => {
const onTokyo = useCallback(() => {
dispatch(setUserLatLng({
latitude: 35.6679191,
longitude: 139.4606805,
}));
navigation.navigate('Discover');
}, [dispatch, navigation]);
const onReverseGeocode = useCallback(async () => {
const userLocation = await getCurrentPosition();
const results = await reverseGeocodeAsync(userLocation);
const {
country,
......@@ -44,7 +53,7 @@ const DevScreen: React.FC = () => {
alert(`${name} ${street} ${city} ${region} ${country}`); // eslint-disable-line no-alert
}, []);
const signout = useCallback(() => {
const onSignout = useCallback(() => {
dispatch(signOutAsync());
}, [dispatch, onClose]);
......@@ -52,17 +61,22 @@ const DevScreen: React.FC = () => {
<View style={styles.fullscreen}>
<GeoButton
title={'Geosearch'}
onPress={geosearch}
onPress={onGeosearch}
style={styles.button}
/>
<GeoButton
title={'Tokyo'}
onPress={onTokyo}
style={styles.button}
/>
<GeoButton
title={'Reverse Geocode'}
onPress={reverseGeocode}
onPress={onReverseGeocode}
style={styles.button}
/>
<GeoButton
title={'Sign Out'}
onPress={signout}
onPress={onSignout}
style={styles.button}
/>
<GeoButton
......
......@@ -13,6 +13,7 @@ import DiscoverMap from '../components/DiscoverMap';
import DiscoverMenu from '../components/DiscoverMenu';
import theme from '../theme';
import GeoSearchBar from '../components/GeoSearchBar';
import ArrowButtons from '../components/ArrowButtons';
const styles = StyleSheet.create({
background: {
......@@ -24,6 +25,7 @@ const styles = StyleSheet.create({
bottom: 20,
right: 20,
},
});
// utils to generate caches for quick testing
......
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { StyleSheet, View } from 'react-native';
import MapView from 'react-native-maps';
import { useNavigation } from '@react-navigation/native';
import theme from '../../theme';
import { getUserLatLng } from '../../store/location/LocationSelectors';
import GeoMarker from '../../components/GeoMarker';
import { createCache } from '../../store/caches/CachesActions';
import { createCache, geoSearch } from '../../store/caches/CachesActions';
import { getPendingCacheContent, getPendingCacheDuration } from '../../store/pendingNewCache/PendingCacheSelectors';
import GeoButton from '../../components/GeoButton';
import DiscoverMap from '../../components/DiscoverMap';
import { getCurrentPosition } from '../../store/location/LocationActions';
const { colors, spacing } = theme;
......@@ -35,11 +34,11 @@ const styles = StyleSheet.create({
const CacheSubmitScreen = () => {
const { navigate } = useNavigation();
const dispatch = useDispatch();
const userLocation = useSelector(getUserLatLng);
const cacheContent = useSelector(getPendingCacheContent);
const cacheDuration = useSelector(getPendingCacheDuration);
const onCreate = useCallback(() => {
const onCreate = useCallback(async () => {
const userLocation = await getCurrentPosition();
dispatch(createCache({
lat: userLocation.latitude,
lng: userLocation.longitude,
......@@ -50,30 +49,14 @@ const CacheSubmitScreen = () => {
isUserCreated: true,
}));
navigate('Discover');
}, [navigate, userLocation]);
dispatch(geoSearch(userLocation, '1'));
}, [navigate]);
return (
<View style={styles.container}>
<View style={styles.mapContainer}>
<MapView
provider={'google'}
style={styles.map}
region={{
latitude: userLocation.latitude,
longitude: userLocation.longitude,
latitudeDelta: 0.0100,
longitudeDelta: 0.0025,
}}
// onUserLocationChange={onUserLocationChange}
showsPointsOfInterest={false}
zoomEnabled={false}
scrollEnabled={false}
mapType={'mutedStandard'}
>
<GeoMarker lat={userLocation.latitude} lng={userLocation.longitude} />
</MapView>
<DiscoverMap />
</View>
<GeoButton
title={'Submit cache here'}
onPress={onCreate}
......
import * as Location from 'expo-location';
import * as Permissions from 'expo-permissions';
import { Dispatch } from '@reduxjs/toolkit';
import locationSlice from './LocationSlice';
export const {
setUserLatLng,
} = locationSlice.actions;
export const getCurrentPosition = () => async (dispatch: Dispatch) => {
export const getCurrentPosition = async () => {
const { status } = await Permissions.askAsync(Permissions.LOCATION);
if (status !== 'granted') {
console.warn('Location permissions not granted');
}
const location = await Location.getCurrentPositionAsync({});
dispatch(setUserLatLng({
return {
latitude: location.coords.latitude,
longitude: location.coords.longitude,
}));
};
};
import { createSlice } from '@reduxjs/toolkit';
import LocationState from './LocationState';
// NOTE: currently deprecated
const locationSlice = createSlice({
name: 'caches',
initialState: {
......
......@@ -2,6 +2,7 @@ const COLOR_NAMES = [
'white',
'blue',
'green',
'darkgray',
'lightgray',
'mediumgray',
'black',
......@@ -17,6 +18,7 @@ export const COLOR_MAP: ColorMap = {
white: '#fff',
blue: '#2089dc',
green: '#1b822e',
darkgray: '#999',
mediumgray: '#aaa',
lightgray: '#eee',
black: '#000',
......
// :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
// ::: :::
// ::: This routine calculates the distance between two points (given the :::
// ::: latitude/longitude of those points). It is being used to calculate :::
// ::: the distance between two locations using GeoDataSource (TM) prodducts :::
// ::: :::
// ::: Definitions: :::
// ::: South latitudes are negative, east longitudes are positive :::
// ::: :::
// ::: Passed to function: :::
// ::: lat1, lon1 = Latitude and Longitude of point 1 (in decimal degrees) :::
// ::: lat2, lon2 = Latitude and Longitude of point 2 (in decimal degrees) :::
// ::: unit = the unit you desire for results :::
// ::: where: 'M' is statute miles (default) :::
// ::: 'K' is kilometers :::
// ::: 'N' is nautical miles :::
// ::: :::
// ::: Worldwide cities and other features databases with latitude longitude :::
// ::: are available at https://www.geodatasource.com :::
// ::: :::
// ::: For enquiries, please contact sales@geodatasource.com :::
// ::: :::
// ::: Official Web site: https://www.geodatasource.com :::
// ::: :::
// ::: GeoDataSource.com (C) All Rights Reserved 2018 :::
// ::: :::
// :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
import { LatLng } from 'react-native-maps';
// export const distance = (lat1, lon1, lat2, lon2, unit) => {
export const distanceBetweenCoordinates = (loc1: LatLng, loc2: LatLng, unit = 'K') => {
const { latitude: lat1, longitude: lon1 } = loc1;
const { latitude: lat2, longitude: lon2 } = loc2;
if ((lat1 === lat2) && (lon1 === lon2)) {
return 0;
}
const radlat1 = (Math.PI * lat1) / 180;
const radlat2 = (Math.PI * lat2) / 180;
const theta = lon1 - lon2;
const radtheta = (Math.PI * theta) / 180;
let dist = Math.sin(radlat1) * Math.sin(radlat2) + Math.cos(radlat1) * Math.cos(radlat2) * Math.cos(radtheta);
if (dist > 1) {
dist = 1;
}
dist = Math.acos(dist);
dist = (dist * 180) / Math.PI;
dist = dist * 60 * 1.1515;
if (unit === 'K') { dist *= 1.609344; }
if (unit === 'N') { dist *= 0.8684; }
return dist;
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment