import React, { useCallback, useEffect, useRef, useState } from 'react'; import { View, Text, TouchableOpacity, ActivityIndicator, Platform, Linking } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useNavigation } from '@react-navigation/native'; import * as turf from '@turf/turf'; import * as MapLibreRN from '@maplibre/maplibre-react-native'; import * as Location from 'expo-location'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import { Header, Modal, FlatList as List, WarningModal } from 'src/components'; import { VECTOR_MAP_HOST } from 'src/constants'; import { Colors } from 'src/theme'; import { NAVIGATION_PAGES } from 'src/types'; import { RegionAddData } from '../utils/types'; import { useGetRegionsForTripsQuery } from '@api/trips'; import { useGetListRegionsQuery } from '@api/regions'; import { styles } from './styles'; import SearchSvg from '../../../../../assets/icons/search.svg'; import SaveSvg from '../../../../../assets/icons/travels-screens/save.svg'; import LocationIcon from 'assets/icons/location.svg'; const generateFilter = (ids: number[]) => { return ids?.length ? ['any', ...ids.map((id) => ['==', 'id', id])] : ['==', 'id', -1]; }; let nm_regions = { id: 'regions', type: 'fill', source: 'regions', 'source-layer': 'regions', style: { fillColor: 'rgba(15, 63, 79, 0)' }, filter: ['all'], maxzoom: 16 }; let selected_region = { id: 'selected_region', type: 'fill', source: 'regions', 'source-layer': 'regions', style: { fillColor: 'rgba(237, 147, 52, 0.7)' }, maxzoom: 12 }; const AddRegionsScreen = ({ route }: { route: any }) => { const { regionsParams }: { regionsParams: RegionAddData[] } = route.params; const { data } = useGetRegionsForTripsQuery(true); const { data: regionsList } = useGetListRegionsQuery(true); const navigation = useNavigation(); const tabBarHeight = useBottomTabBarHeight(); const [regions, setRegions] = useState(null); const [isModalVisible, setIsModalVisible] = useState(false); const [selectedRegions, setSelectedRegions] = useState([]); const [regionsToSave, setRegionsToSave] = useState([]); const [regionData, setRegionData] = useState(null); const [regionPopupVisible, setRegionPopupVisible] = useState(false); const mapRef = useRef(null); const cameraRef = useRef(null); const [renderCamera, setRenderCamera] = useState(Platform.OS === 'ios'); const isAnimatingRef = useRef(false); const animationTimeoutRef = useRef(null); const [filterSelectedRegions, setFilterSelectedRegions] = useState(generateFilter([])); const [isLocationLoading, setIsLocationLoading] = useState(false); const [location, setLocation] = useState(null); const [askLocationVisible, setAskLocationVisible] = useState(false); const [openSettingsVisible, setOpenSettingsVisible] = useState(false); const cameraController = { flyTo: useCallback((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); } }, []), setCamera: useCallback((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 ); } }, []), fitBounds: useCallback((ne: number[], sw: number[], padding: number[], duration: number) => { isAnimatingRef.current = true; if (animationTimeoutRef.current) { clearTimeout(animationTimeoutRef.current); } if (Platform.OS === 'android') { setRenderCamera(true); requestAnimationFrame(() => { cameraRef.current?.fitBounds(ne, sw, padding, duration); }); animationTimeoutRef.current = setTimeout(() => { isAnimatingRef.current = false; setRenderCamera(false); }, duration + 200); } else { cameraRef.current?.fitBounds(ne, sw, padding, duration); animationTimeoutRef.current = setTimeout(() => { isAnimatingRef.current = false; }, duration + 100); } }, []) }; useEffect(() => { if (data && data.regions) { setRegions(data.regions); } }, [data]); useEffect(() => { const ids = selectedRegions.map((region) => region.id); setFilterSelectedRegions(generateFilter(ids)); }, [selectedRegions]); useEffect(() => { const addRegionsAsync = async () => { if (regionsParams) { setRegionsToSave((prevRegions) => [...prevRegions, ...regionsParams]); setSelectedRegions( (prevSelectedRegions) => [...prevSelectedRegions, ...regionsParams] as any ); } }; addRegionsAsync(); }, [regionsParams]); useEffect(() => { return () => { if (animationTimeoutRef.current) { clearTimeout(animationTimeoutRef.current); } }; }, []); const addRegionFromSearch = async (searchRegion: RegionAddData) => { const regionIndex = selectedRegions.findIndex((region) => region.id === searchRegion.id); const regionFromApi = regions?.find((region) => region.id === searchRegion.id); if (regionIndex < 0 && regionFromApi) { const newRegion = { id: searchRegion.id, name: searchRegion.name }; setSelectedRegions([...selectedRegions, newRegion] as any); setRegionsToSave((prevRegions) => [...prevRegions, regionFromApi]); setRegionPopupVisible(true); if (regionsList) { const region = regionsList.data.find((region) => region.id === searchRegion.id); if (region) { const bounds = turf.bbox(region.bbox); cameraController.fitBounds( [bounds[2], bounds[3]], [bounds[0], bounds[1]], [50, 50, 50, 50], 600 ); } } } }; const handleSavePress = () => { if (route.params?.isSharedTrip) { navigation.popTo( ...([ NAVIGATION_PAGES.CREATE_SHARED_TRIP, { regionsToSave: regionsToSave, eventId: route.params?.editId } ] as never) ); } else { navigation.popTo( ...([ NAVIGATION_PAGES.ADD_TRIP, { regionsToSave: regionsToSave, editTripId: route.params?.editId } ] as never) ); } }; const handleSetRegionData = (regionId: number) => { const foundRegion = regions?.find((region) => region.id === regionId); if (foundRegion) { setRegionData(foundRegion); setRegionsToSave((prevRegions) => [...prevRegions, foundRegion]); } }; const handleMapPress = useCallback( async (event: any) => { if (!mapRef.current) return; try { const { screenPointX, screenPointY } = event.properties; const { features } = await mapRef.current.queryRenderedFeaturesAtPoint( [screenPointX, screenPointY], undefined, ['regions'] ); if (features?.length) { const selectedRegion = features[0]; if (selectedRegion.properties) { const id = selectedRegion.properties.id; const regionIndex = selectedRegions.findIndex((region) => region.id === id); if (regionIndex >= 0) { let newSelectedRegions = [...selectedRegions]; newSelectedRegions = newSelectedRegions.filter((region) => region.id !== id); setSelectedRegions(newSelectedRegions); setRegionsToSave(regionsToSave.filter((region) => region.id !== id)); setRegionPopupVisible(false); return; } else { setSelectedRegions([...selectedRegions, selectedRegion.properties] as any); } handleSetRegionData(id); setRegionPopupVisible(true); if (regionsList) { const region = regionsList.data.find((region) => region.id === id); if (region) { const bounds = turf.bbox(region.bbox); cameraController.fitBounds( [bounds[2], bounds[3]], [bounds[0], bounds[1]], [50, 50, 50, 50], 600 ); } } } } } catch (error) { console.error('Failed to get coordinates on AddRegionsScreen', error); } }, [selectedRegions, regions] ); 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); } }; return (
setIsModalVisible(true)}> Search Save {(Platform.OS === 'ios' || renderCamera) && } {selectedRegions && selectedRegions.length > 0 ? ( ) : null} {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' }); }} > )} {isLocationLoading ? ( ) : ( )} {regionPopupVisible && regionData && ( {regionData.name ?? regionData.region_name} )} setIsModalVisible(false)} headerTitle={'Select Regions'} visible={isModalVisible} > { setIsModalVisible(false); setRegionData(object); addRegionFromSearch(object); }} /> 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?" /> 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." /> ); }; export default AddRegionsScreen;