import { FontAwesome } from "@expo/vector-icons";
import * as Location from 'expo-location';
import React, { useEffect, useState } from "react";
import { Keyboard, KeyboardAvoidingView, TouchableOpacity, View } from "react-native";
import MapView, { LatLng, Region } from "react-native-maps";
import Modal from "react-native-modal";
import { colors } from "../globals";
import { Landmark, useLandmarks } from "../hooks/useLandmarks";
import AddLandmark from "./AddLandmark";
import LandmarkDetails from "./LandmarkDetails";
import LandmarkPin from "./LandmarkPin";
/**
* An interface representing the user location retrieved from [expo-location]{@link https://docs.expo.dev/versions/latest/sdk/location/}.
* @category Map
*/
export interface UserLocation {
latitude: number;
longitude: number;
heading?: number;
}
const directionsUrl = "https://maps.googleapis.com/maps/api/directions/son?mode=walking&alternatives=true&key=AIzaSyD06YUMazlb4Fu0Q81y_YNyEBz8PmtZyeY";
/**
* The screen component containing the [react-native-maps]{@link https://github.com/react-native-maps/react-native-maps} Map and all related functionality.
* @category Map
* @component
*/
export const Map: React.FC = () => {
/**
* State that contains the new {@link Landmark} object which is passed down to the {@link AddLandmark} modal.
*/
const newLandmarkState: Landmark = {};
const [newLandmark, setNewLandmark] = useState<Landmark>(newLandmarkState);
/**
* State that contains the selected {@link Landmark} object which is passed down to the {@link LandmarkDetails} modal.
*/
const selectedLandmarkState: Landmark = {};
const [selectedLandmark, setSelectedLandmark] = useState<Landmark>(selectedLandmarkState);
/**
* Holds the visibility state of the {@link AddLandmark} modal.
*/
const lmAddVisibleState = false;
const [lmAddVisible, toggleLmAdd] = useState<boolean>(false);
/**
* Holds the visibility state of the {@link LandmarkDetails} modal.
*/
const lmDetailsVisibleState = false;
const [lmDetailsVisible, toggleLmDetails] = useState<boolean>(false);
/**
* Flag that toggles whether or not editing is enabled in the {@link LandmarkDetails} modal.
* The parent Map component has access to it so that it can disable closing the modal on backdrop press when it is enabled.
*/
const lmDetailsEditingState = false;
const [lmDetailsEditing, toggleLmDetailsEditing] = useState<boolean>(false);
/**
* State that holds a {@link UserLocation} object retrieved from location services.
*/
const userLocationState: UserLocation | undefined = undefined;
const [userLocation, setUserLocation] = useState<UserLocation>(userLocationState);
/**
* Flag that determines whether the map should focus and follow the user's location.
*/
const followUserState = false;
const [followUser, toggleFollowUser] = useState<boolean>(followUserState);
//const [prevFetchedBounds, setFetchedBounds] = useState<Region>();
const { landmarks, getLandmarksStatus, refetchLandmarks } = useLandmarks(undefined);
/**
* Ref that holds the loaded [MapView]{@link https://github.com/react-native-maps/react-native-maps/blob/master/docs/mapview.md} instance.
*/
const mapRef = React.createRef<MapView>();
useEffect(() => {
/**
* Prompts user to give permission to track their location using [expo-location]{@link https://docs.expo.dev/versions/latest/sdk/location/}.
* If permission is granted, user location will be retrieved and stored in {@linkcode userLocationState}.
* @memberOf Map
*/
const requestLocationPermissions = async () => {
let { status } = await Location.requestForegroundPermissionsAsync();
if (status !== 'granted') {
return;
}
const location = await Location.getCurrentPositionAsync();
setUserLocation({latitude: location.coords.latitude, longitude: location.coords.longitude})
// setFetchedBounds({
// latitude: location.coords.latitude,
// longitude: location.coords.longitude,
// latitudeDelta: 0.01,
// longitudeDelta: 0.01
// });
};
requestLocationPermissions();
}, []);
/**
* Triggered by long pressing on the map.
* Sets {@linkcode newLandmarkState} to a skeleton {@link Landmark} object that only contains the pressed coordinates.
* Triggers {@link openAddLandmark} via useEffect because the asyncronous nature of useState does not set the coordinates fast enough to toggle the modal directly through this method.
*/
const promptAddLandmark = (coordinate: LatLng) => {
setNewLandmark({latitude: coordinate.latitude, longitude: coordinate.longitude});
}
useEffect(() => {
/**
* Opens the {@link AddLandmark} modal when the user creates {@link newLandmarkState} by longpressing the map.
* Embedded in a [useEffect]{@link https://reactjs.org/docs/hooks-effect.html} hook that listens to {@linkcode newLandmarkState}.
* @memberOf Map
*/
function openAddLandmark() {
if (newLandmark) {
toggleLmAdd(true)
toggleLmDetails(false)
console.log(newLandmark)
}
}
openAddLandmark();
}, [newLandmark]);
// useEffect(() => {
// if (prevFetchedBounds) {
// refetchLandmarks();
// }
// }, [prevFetchedBounds]);
/**
* Triggered by on a {@link Landmark} displayed on the map.
* Sets {@linkcode selectedLandmark} to the pressed {@link Landmark} object's value and toggles the {@link LandmarkDetails} modal.
*/
const focusLandmark = (landmark: Landmark) => {
setSelectedLandmark(landmark);
toggleLmDetails(true)
}
/**
* Animates the map to fly over to and focus on the user's location.
*/
const flyToUser = () => {
if (userLocation) {
mapRef.current?.animateToRegion({latitude: userLocation.latitude, longitude: userLocation.longitude, latitudeDelta: 0.01, longitudeDelta: 0.01})
}
}
// const checkBounds = (bounds: Region) => {
// if (prevFetchedBounds) {
// if (
// bounds.latitude < prevFetchedBounds.latitude - prevFetchedBounds.latitudeDelta || // check if new lat exceeds old left bounds
// bounds.latitude > prevFetchedBounds.latitude + prevFetchedBounds.latitudeDelta || // check if new lat exceeds old right bounds
// bounds.longitude < prevFetchedBounds.longitude - prevFetchedBounds.longitudeDelta || // check if new lat exceeds bottom bounds
// bounds.longitude > prevFetchedBounds.longitude + prevFetchedBounds.longitudeDelta ||
// bounds.latitudeDelta < prevFetchedBounds.latitudeDelta / 2) // check if user zoomed in to atleast half scale of previous
// {
// console.log('new bounds')
// setFetchedBounds(bounds);
// }
// }
// else if (bounds.latitudeDelta < 5) {
// console.log('initialize bounds')
// setFetchedBounds(bounds);
// }
// }
return (
<View>
<MapView
testID="mapView"
ref={mapRef}
style={{width: "100%", height: "100%"}}
initialRegion={userLocation ? {latitude: userLocation.latitude, longitude: userLocation.longitude, latitudeDelta: 0.01, longitudeDelta: 0.01} : undefined}
onLongPress={e => promptAddLandmark(e.nativeEvent.coordinate)}
// onRegionChangeComplete={bounds => checkBounds(bounds)}
showsUserLocation={true}
onUserLocationChange={e => setUserLocation(e.nativeEvent.coordinate)}
followsUserLocation={followUser}>
{landmarks?.map((landmark) => {
return (
<LandmarkPin key={landmark.id} landmark={landmark} focusLandmark={focusLandmark}/>)})}
</MapView>
<TouchableOpacity style={{position: 'absolute', bottom: 30, right: 30, backgroundColor: colors.red, height: 60, width: 60, borderRadius: 30, justifyContent: "center", alignItems: 'center'}} onPress={flyToUser}>
<FontAwesome name="location-arrow" size={20} color="white"/>
</TouchableOpacity>
{/* <TouchableOpacity style={{position: 'absolute', bottom: 120, right: 30, backgroundColor: colors.red, height: 60, width: 60, borderRadius: 30, justifyContent: "center", alignItems: 'center'}}>
<FontAwesome name="" size={20} color="white"/>
</TouchableOpacity> */}
<Modal
testID="addLMModal"
avoidKeyboard={true}
onBackdropPress={() => toggleLmAdd(false)}
style={{justifyContent: "flex-end", height: '100%', margin: 0}}
isVisible={lmAddVisible} >
<KeyboardAvoidingView>
<AddLandmark setVisible={toggleLmAdd} landmark={newLandmark} />
</KeyboardAvoidingView>
</Modal>
<Modal
avoidKeyboard={true}
onBackdropPress={() => {
if (lmDetailsEditing) {
Keyboard.dismiss();
} else {
toggleLmDetails(false)
}
}}
style={{justifyContent: "flex-end", height: '100%', margin: 0}}
isVisible={lmDetailsVisible}>
<LandmarkDetails setVisible={toggleLmDetails} setEditing={toggleLmDetailsEditing} editingEnabled={lmDetailsEditing} landmark={selectedLandmark} />
</Modal>
</View> )
}
Source