| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511 |
- 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<RegionAddData[] | null>(null);
- const [isModalVisible, setIsModalVisible] = useState(false);
- const [selectedRegions, setSelectedRegions] = useState<any[]>([]);
- const [regionsToSave, setRegionsToSave] = useState<RegionAddData[]>([]);
- const [regionData, setRegionData] = useState<RegionAddData | null>(null);
- const [regionPopupVisible, setRegionPopupVisible] = useState(false);
- const mapRef = useRef<MapLibreRN.MapViewRef>(null);
- const cameraRef = useRef<MapLibreRN.CameraRef>(null);
- const [renderCamera, setRenderCamera] = useState(Platform.OS === 'ios');
- const isAnimatingRef = useRef(false);
- const animationTimeoutRef = useRef<NodeJS.Timeout | null>(null);
- const [filterSelectedRegions, setFilterSelectedRegions] = useState<any[]>(generateFilter([]));
- const [isLocationLoading, setIsLocationLoading] = useState(false);
- const [location, setLocation] = useState<any | null>(null);
- const [askLocationVisible, setAskLocationVisible] = useState<boolean>(false);
- const [openSettingsVisible, setOpenSettingsVisible] = useState<boolean>(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 (
- <SafeAreaView style={{ height: '100%' }} edges={['top']}>
- <View style={styles.wrapper}>
- <Header label={'Add Regions'} />
- <View style={styles.searchContainer}>
- <TouchableOpacity style={[styles.regionSelector]} onPress={() => setIsModalVisible(true)}>
- <SearchSvg fill={Colors.LIGHT_GRAY} />
- <Text style={styles.regionText}>Search</Text>
- </TouchableOpacity>
- <TouchableOpacity
- style={[
- styles.saveBtn,
- selectedRegions.length ? styles.saveBtnActive : styles.saveBtnDisabled
- ]}
- onPress={handleSavePress}
- disabled={!selectedRegions.length}
- >
- <Text
- style={{
- fontSize: 12,
- fontWeight: '600',
- color: !selectedRegions.length ? Colors.LIGHT_GRAY : Colors.WHITE
- }}
- >
- Save
- </Text>
- </TouchableOpacity>
- </View>
- </View>
- <View style={styles.container}>
- <MapLibreRN.MapView
- ref={mapRef}
- style={styles.map}
- mapStyle={VECTOR_MAP_HOST + '/nomadmania-maps2025.json'}
- rotateEnabled={false}
- attributionEnabled={false}
- onPress={handleMapPress}
- >
- {(Platform.OS === 'ios' || renderCamera) && <MapLibreRN.Camera ref={cameraRef} />}
- <MapLibreRN.LineLayer
- id="nm-regions-line-layer"
- sourceID={nm_regions.source}
- sourceLayerID={nm_regions['source-layer']}
- filter={nm_regions.filter as any}
- maxZoomLevel={nm_regions.maxzoom}
- style={{
- lineColor: 'rgba(14, 80, 109, 1)',
- lineWidth: ['interpolate', ['linear'], ['zoom'], 0, 0.2, 4, 1, 5, 1.5, 12, 3],
- lineWidthTransition: { duration: 300, delay: 0 }
- }}
- belowLayerID="waterway-name"
- />
- <MapLibreRN.FillLayer
- id={nm_regions.id}
- sourceID={nm_regions.source}
- sourceLayerID={nm_regions['source-layer']}
- filter={nm_regions.filter as any}
- style={nm_regions.style}
- maxZoomLevel={nm_regions.maxzoom}
- belowLayerID="nm-regions-line-layer"
- />
- {selectedRegions && selectedRegions.length > 0 ? (
- <MapLibreRN.FillLayer
- id={selected_region.id}
- sourceID={nm_regions.source}
- sourceLayerID={nm_regions['source-layer']}
- filter={filterSelectedRegions as any}
- style={selected_region.style}
- maxZoomLevel={selected_region.maxzoom}
- belowLayerID="nm-regions-line-layer"
- />
- ) : null}
- {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={handleGetLocation}
- style={[
- styles.cornerButton,
- styles.bottomButton,
- styles.bottomRightButton,
- { bottom: 20 }
- ]}
- >
- {isLocationLoading ? (
- <ActivityIndicator size="small" color={Colors.DARK_BLUE} />
- ) : (
- <LocationIcon />
- )}
- </TouchableOpacity>
- </View>
- {regionPopupVisible && regionData && (
- <View style={styles.popupWrapper}>
- <View style={styles.popupContainer}>
- <Text style={styles.popupText}>{regionData.name ?? regionData.region_name}</Text>
- </View>
- </View>
- )}
- <Modal
- onRequestClose={() => setIsModalVisible(false)}
- headerTitle={'Select Regions'}
- visible={isModalVisible}
- >
- <List
- itemObject={(object) => {
- setIsModalVisible(false);
- setRegionData(object);
- addRegionFromSearch(object);
- }}
- />
- </Modal>
- <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?"
- />
- <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."
- />
- </SafeAreaView>
- );
- };
- export default AddRegionsScreen;
|