import React, { useEffect, useRef, useState } from 'react'; import { View, StyleSheet, StatusBar, TouchableOpacity, ActivityIndicator, Platform, Linking } from 'react-native'; import * as MapLibreRN from '@maplibre/maplibre-react-native'; import * as Location from 'expo-location'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { useNavigation } from '@react-navigation/native'; import _ from 'lodash'; import { VECTOR_MAP_HOST } from 'src/constants'; import { WarningModal } from 'src/components'; import { Colors } from 'src/theme'; import ScaleBar from 'src/components/ScaleBar'; import ChevronLeft from 'assets/icons/chevron-left.svg'; import LocationIcon from 'assets/icons/location.svg'; import MapSvg from 'assets/icons/travels-screens/map-location.svg'; const FullMapScreen = ({ route }: { route: any }) => { const { lat, lng } = route.params; const tabBarHeight = useBottomTabBarHeight(); const insets = useSafeAreaInsets(); const navigation = useNavigation(); const mapRef = useRef(null); const cameraRef = useRef(null); const [renderCamera, setRenderCamera] = useState(Platform.OS === 'ios'); const animationTimeoutRef = useRef(null); const isAnimatingRef = useRef(false); const [isLocationLoading, setIsLocationLoading] = useState(false); const [location, setLocation] = useState(null); const [zoom, setZoom] = useState(0); const [center, setCenter] = useState(null); const [isZooming, setIsZooming] = useState(true); const [askLocationVisible, setAskLocationVisible] = useState(false); const [openSettingsVisible, setOpenSettingsVisible] = useState(false); const hideTimer = useRef | null>(null); const cameraController = { setCamera: (config: any) => { isAnimatingRef.current = true; if (animationTimeoutRef.current) { clearTimeout(animationTimeoutRef.current); } if (Platform.OS === 'android') { setRenderCamera(true); requestAnimationFrame(() => { cameraRef.current?.setCamera(config); }); animationTimeoutRef.current = setTimeout( () => { isAnimatingRef.current = false; setRenderCamera(false); }, (config.animationDuration || 1000) + 200 ); } else { cameraRef.current?.setCamera(config); animationTimeoutRef.current = setTimeout( () => { isAnimatingRef.current = false; }, (config.animationDuration || 1000) + 100 ); } }, flyTo: (coordinates: number[], duration: number = 1000) => { isAnimatingRef.current = true; if (animationTimeoutRef.current) { clearTimeout(animationTimeoutRef.current); } if (Platform.OS === 'android') { setRenderCamera(true); requestAnimationFrame(() => { cameraRef.current?.flyTo(coordinates, duration); }); animationTimeoutRef.current = setTimeout(() => { isAnimatingRef.current = false; setRenderCamera(false); }, duration + 200); } else { cameraRef.current?.flyTo(coordinates, duration); animationTimeoutRef.current = setTimeout(() => { isAnimatingRef.current = false; }, duration + 100); } } }; useEffect(() => { (async () => { let { status } = await Location.getForegroundPermissionsAsync(); const isServicesEnabled = await Location.hasServicesEnabledAsync(); if (status !== 'granted' || !isServicesEnabled) { return; } try { let currentLocation = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced }); setLocation(currentLocation.coords); } catch (error) { console.error('Error fetching user location:', error); } })(); }, []); useEffect(() => { return () => { if (animationTimeoutRef.current) { clearTimeout(animationTimeoutRef.current); } }; }, []); const handleMapChange = async () => { if (!mapRef.current) return; if (hideTimer.current) clearTimeout(hideTimer.current); setIsZooming(true); const currentZoom = await mapRef.current.getZoom(); setZoom(currentZoom); if (mapRef.current) { const currentCenter = await mapRef.current?.getCenter(); setCenter(currentCenter); } }; const handleGetLocation = async () => { setIsLocationLoading(true); try { let { status, canAskAgain } = await Location.getForegroundPermissionsAsync(); const isServicesEnabled = await Location.hasServicesEnabledAsync(); if (status === 'granted' && isServicesEnabled) { await getLocation(); } else if (!canAskAgain || !isServicesEnabled) { setOpenSettingsVisible(true); } else { setAskLocationVisible(true); } } finally { setIsLocationLoading(false); } }; const getLocation = async () => { try { let currentLocation = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced }); setLocation(currentLocation.coords); if (currentLocation.coords) { cameraController?.flyTo( [currentLocation.coords.longitude, currentLocation.coords.latitude], 1000 ); } } catch (error) { console.error('Error fetching user location:', error); } }; const handleAcceptPermission = async () => { setAskLocationVisible(false); let { status, canAskAgain } = await Location.requestForegroundPermissionsAsync(); const isServicesEnabled = await Location.hasServicesEnabledAsync(); if (status === 'granted' && isServicesEnabled) { getLocation(); } else if (!canAskAgain || !isServicesEnabled) { setOpenSettingsVisible(true); } }; const openInExternalMaps = async (latitude: number, longitude: number) => { const appleMapsURL = `http://maps.apple.com/?q=${latitude},${longitude}`; const defaultGeoURL = `geo:${latitude},${longitude}?q=${latitude},${longitude}`; if (Platform.OS === 'ios') { await Linking.openURL(appleMapsURL); } else { await Linking.openURL(defaultGeoURL); } }; return ( { hideTimer.current = setTimeout(() => { setIsZooming(false); }, 2000); }} // onRegionIsChanging={handleMapChange} onRegionWillChange={_.debounce(handleMapChange, 200)} > {(Platform.OS === 'ios' || renderCamera) && ( )} {location && ( { const currentZoom = await mapRef.current?.getZoom(); const newZoom = (currentZoom || 0) + 2; cameraController.setCamera({ centerCoordinate: [location.longitude, location.latitude], zoomLevel: newZoom, animationDuration: 500, animationMode: 'flyTo' }); }} > )} { navigation.goBack(); }} style={[ styles.backButtonContainer, { top: Platform.OS === 'android' ? insets.top + 20 : insets.top } ]} > {isLocationLoading ? ( ) : ( )} openInExternalMaps(lat, lng)} style={[ styles.cornerButton, styles.bottomButton, styles.topRightButton, { top: Platform.OS === 'android' ? insets.top + 24 : insets.top + 4 } ]} > {center ? ( ) : null} setAskLocationVisible(false)} action={handleAcceptPermission} message="To use this feature we need your permission to access your location. If you press OK your system will ask you to approve location sharing with NomadMania app." /> setOpenSettingsVisible(false)} action={async () => { const isServicesEnabled = await Location.hasServicesEnabledAsync(); if (!isServicesEnabled) { Platform.OS === 'ios' ? Linking.openURL('app-settings:') : Linking.sendIntent('android.settings.LOCATION_SOURCE_SETTINGS'); } else { Platform.OS === 'ios' ? Linking.openURL('app-settings:') : Linking.openSettings(); } }} message="NomadMania app needs location permissions to function properly. Open settings?" /> ); }; const styles = StyleSheet.create({ map: { ...StyleSheet.absoluteFillObject }, backButtonContainer: { position: 'absolute', width: 50, height: 50, top: 50, left: 12, justifyContent: 'center', alignItems: 'center', zIndex: 2 }, backButton: { width: 42, height: 42, borderRadius: 21, justifyContent: 'center', alignItems: 'center', backgroundColor: 'rgba(0, 0, 0, 0.3)' }, marker: { width: 20, height: 20, borderRadius: 10, backgroundColor: Colors.ORANGE, borderWidth: 2, borderColor: Colors.WHITE }, cornerButton: { position: 'absolute', backgroundColor: Colors.WHITE, padding: 12, width: 48, height: 48, borderRadius: 24, alignItems: 'center', justifyContent: 'center', shadowColor: '#000', shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.25, shadowRadius: 1.5, elevation: 2 }, bottomButton: { width: 42, height: 42, borderRadius: 21 }, bottomRightButton: { right: 16 }, topRightButton: { top: 54, right: 16 } }); export default FullMapScreen;