| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400 |
- 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<MapLibreRN.MapViewRef>(null);
- const cameraRef = useRef<MapLibreRN.CameraRef>(null);
- const [renderCamera, setRenderCamera] = useState(Platform.OS === 'ios');
- const animationTimeoutRef = useRef<NodeJS.Timeout | null>(null);
- const isAnimatingRef = useRef(false);
- const [isLocationLoading, setIsLocationLoading] = useState(false);
- const [location, setLocation] = useState<any | null>(null);
- const [zoom, setZoom] = useState(0);
- const [center, setCenter] = useState<number[] | null>(null);
- const [isZooming, setIsZooming] = useState(true);
- const [askLocationVisible, setAskLocationVisible] = useState<boolean>(false);
- const [openSettingsVisible, setOpenSettingsVisible] = useState<boolean>(false);
- const hideTimer = useRef<ReturnType<typeof setTimeout> | 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 (
- <SafeAreaView style={{ height: '100%' }}>
- <StatusBar translucent backgroundColor="transparent" />
- <MapLibreRN.MapView
- ref={mapRef}
- style={styles.map}
- mapStyle={VECTOR_MAP_HOST + '/nomadmania-maps2025.json'}
- rotateEnabled={false}
- attributionEnabled={false}
- onRegionDidChange={() => {
- hideTimer.current = setTimeout(() => {
- setIsZooming(false);
- }, 2000);
- }}
- // onRegionIsChanging={handleMapChange}
- onRegionWillChange={_.debounce(handleMapChange, 200)}
- >
- {(Platform.OS === 'ios' || renderCamera) && (
- <MapLibreRN.Camera
- ref={cameraRef}
- defaultSettings={{ centerCoordinate: [lng, lat], zoomLevel: 12 }}
- />
- )}
- <MapLibreRN.MarkerView coordinate={[lng, lat]}>
- <View style={styles.marker} />
- </MapLibreRN.MarkerView>
- {location && (
- <MapLibreRN.UserLocation
- animated={true}
- showsUserHeadingIndicator={true}
- onPress={async () => {
- const currentZoom = await mapRef.current?.getZoom();
- const newZoom = (currentZoom || 0) + 2;
- cameraController.setCamera({
- centerCoordinate: [location.longitude, location.latitude],
- zoomLevel: newZoom,
- animationDuration: 500,
- animationMode: 'flyTo'
- });
- }}
- ></MapLibreRN.UserLocation>
- )}
- </MapLibreRN.MapView>
- <TouchableOpacity
- onPress={() => {
- navigation.goBack();
- }}
- style={[
- styles.backButtonContainer,
- { top: Platform.OS === 'android' ? insets.top + 20 : insets.top }
- ]}
- >
- <View style={styles.backButton}>
- <ChevronLeft fill={Colors.WHITE} />
- </View>
- </TouchableOpacity>
- <TouchableOpacity
- onPress={handleGetLocation}
- style={[
- styles.cornerButton,
- styles.bottomButton,
- styles.bottomRightButton,
- { bottom: tabBarHeight + insets.bottom + 20 }
- ]}
- >
- {isLocationLoading ? (
- <ActivityIndicator size="small" color={Colors.DARK_BLUE} />
- ) : (
- <LocationIcon />
- )}
- </TouchableOpacity>
- <TouchableOpacity
- onPress={() => openInExternalMaps(lat, lng)}
- style={[
- styles.cornerButton,
- styles.bottomButton,
- styles.topRightButton,
- { top: Platform.OS === 'android' ? insets.top + 24 : insets.top + 4 }
- ]}
- >
- <MapSvg fill={Colors.DARK_BLUE} />
- </TouchableOpacity>
- {center ? (
- <ScaleBar
- zoom={zoom}
- latitude={center[1]}
- isVisible={isZooming}
- bottom={tabBarHeight + insets.bottom + 38}
- />
- ) : null}
- <WarningModal
- type={'success'}
- isVisible={askLocationVisible}
- onClose={() => 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."
- />
- <WarningModal
- type={'success'}
- isVisible={openSettingsVisible}
- onClose={() => 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?"
- />
- </SafeAreaView>
- );
- };
- 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;
|