import { Dimensions, Linking, Platform, Text, TextInput, TouchableOpacity, View, Image, StatusBar, ActivityIndicator, ScrollView } from 'react-native'; import React, { useEffect, useRef, useState, useCallback } from 'react'; import * as MapLibreRN from '@maplibre/maplibre-react-native'; import { styles } from './style'; import { SafeAreaView } from 'react-native-safe-area-context'; import { Colors } from 'src/theme'; import { storage, StoreType } from 'src/storage'; import * as turf from '@turf/turf'; import * as Location from 'expo-location'; import { Image as ExpoImage } from 'expo-image'; import SearchIcon from 'assets/icons/search.svg'; import LocationIcon from 'assets/icons/location.svg'; import CloseSvg from 'assets/icons/close.svg'; import FilterIcon from 'assets/icons/filter.svg'; import ProfileIcon from 'assets/icons/bottom-navigation/profile.svg'; import RegionPopup from 'src/components/RegionPopup'; import { useRegion } from 'src/contexts/RegionContext'; import { qualityOptions } from '../TravelsScreen/utils/constants'; import { AvatarWithInitials, EditNmModal, WarningModal } from 'src/components'; import { API_HOST, VECTOR_MAP_HOST } from 'src/constants'; import { NAVIGATION_PAGES } from 'src/types'; import Animated, { Easing, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import { getData } from 'src/modules/map/regionData'; import { getCountriesDatabase, getFirstDatabase, getSecondDatabase, refreshDatabases } from 'src/db'; import { fetchUserData, fetchUserDataDare, useGetListRegionsQuery } from '@api/regions'; import { SQLiteDatabase } from 'expo-sqlite/legacy'; import { useFocusEffect } from '@react-navigation/native'; import { useGetUniversalSearch } from '@api/search'; import { fetchCountryUserData, useGetListCountriesQuery } from '@api/countries'; import SearchModal from './UniversalSearch'; import EditModal from '../TravelsScreen/Components/EditSlowModal'; import * as FileSystem from 'expo-file-system'; import CheckSvg from 'assets/icons/mark.svg'; import moment from 'moment'; import { usePostGetVisitedCountriesIdsQuery, usePostGetVisitedDareIdsQuery, usePostGetVisitedRegionsIdsQuery, usePostGetVisitedSeriesIdsQuery } from '@api/maps'; import FilterModal from './FilterModal'; import { useGetListDareQuery } from '@api/myDARE'; import { useGetIconsQuery, usePostSetToggleItem } from '@api/series'; import MarkerItem from './MarkerItem'; import { usePostGetSettingsQuery, usePostGetUsersCountQuery, usePostGetUsersLocationQuery, usePostUpdateLocationMutation } from '@api/location'; import UserItem from './UserItem'; import { useConnection } from 'src/contexts/ConnectionContext'; import TravelsIcon from 'assets/icons/bottom-navigation/globe-solid.svg'; import SeriesIcon from 'assets/icons/travels-section/series.svg'; import NomadsIcon from 'assets/icons/bottom-navigation/travellers.svg'; import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs'; import MapButton from 'src/components/MapButton'; import { useAvatarStore } from 'src/stores/avatarVersionStore'; import _ from 'lodash'; import ScaleBar from 'src/components/ScaleBar'; import MessagesDot from 'src/components/MessagesDot'; import { restartBackgroundLocationUpdates, startBackgroundLocationUpdates, stopBackgroundLocationUpdates } from 'src/utils/backgroundLocation'; const clusteredUsersIcon = require('assets/icons/icon-clustered-users.png'); const defaultUserAvatar = require('assets/icon-user-share-location-solid.png'); const logo = require('assets/logo-ua.png'); const defaultSeriesIcon = require('assets/series-default.png'); MapLibreRN.Logger.setLogLevel('error'); const generateFilter = (ids: number[]) => { return ids?.length ? ['any', ...ids.map((id) => ['==', 'id', id])] : ['==', 'id', -1]; }; // to do refactor let regions_visited = { id: 'regions_visited', type: 'fill', source: 'regions', 'source-layer': 'regions', style: { fillColor: 'rgba(255, 126, 0, 1)', fillOpacity: 0.6, fillOutlineColor: 'rgba(14, 80, 109, 0)' }, filter: generateFilter([]), maxzoom: 10 }; let countries_visited = { id: 'countries_visited', type: 'fill', source: 'countries', 'source-layer': 'countries', style: { fillColor: 'rgba(255, 126, 0, 1)', fillOpacity: 0.6, fillOutlineColor: 'rgba(14, 80, 109, 0)' }, filter: generateFilter([]), maxzoom: 10 }; let dare_visited = { id: 'dare_visited', type: 'fill', source: 'dare', 'source-layer': 'dare', style: { fillColor: 'rgba(255, 126, 0, 0.6)', fillOutlineColor: 'rgba(255, 126, 0, 0)' }, filter: generateFilter([]), maxzoom: 12 }; let regions = { id: 'regions', type: 'fill', source: 'regions', 'source-layer': 'regions', style: { fillColor: 'rgba(15, 63, 79, 0)', fillOutlineColor: 'rgba(14, 80, 109, 0)' }, filter: ['all'], maxzoom: 16 }; let countries = { id: 'countries', type: 'fill', source: 'countries', 'source-layer': 'countries', style: { fillColor: 'rgba(15, 63, 79, 0)', fillOutlineColor: 'rgba(14, 80, 109, 0)' }, filter: ['all'], maxzoom: 16 }; let dare = { id: 'dare', type: 'fill', source: 'dare', 'source-layer': 'dare', style: { fillColor: 'rgba(14, 80, 109, 0.6)', fillOutlineColor: 'rgba(14, 80, 109, 0)' }, filter: ['all'], maxzoom: 16 }; let selected_region = { id: 'selected_region', type: 'fill', source: 'regions', 'source-layer': 'regions', style: { fillColor: 'rgba(57, 115, 172, 0.3)' }, maxzoom: 12 }; let selected_region_outline = { id: 'selected_region_outline', type: 'line', source: 'regions', 'source-layer': 'regions', style: { lineColor: '#ED9334', lineTranslate: [0, 0], lineTranslateAnchor: 'map', lineWidth: ['interpolate', ['linear'], ['zoom'], 0, 2, 4, 3, 5, 4, 12, 5] }, maxzoom: 12 }; let series_layer = { id: 'series_layer', type: 'symbol', source: 'nomadmania_series', 'source-layer': 'series', minzoom: 6, maxzoom: 60, layout: { 'symbol-spacing': 1, 'icon-image': '{series_id}', 'icon-size': 0.15, 'icon-allow-overlap': true, 'icon-ignore-placement': true, 'text-anchor': 'top', 'text-field': '{name}', 'text-font': ['Noto Sans Regular'], 'text-max-width': 9, 'text-offset': [0, 0.6], 'text-padding': 2, 'text-size': 12, visibility: 'visible', 'text-optional': true, 'text-ignore-placement': false, 'text-allow-overlap': false }, paint: { 'text-color': '#666', 'text-halo-blur': 0.5, 'text-halo-color': '#ffffff', 'text-halo-width': 1 }, filter: generateFilter([]) }; let series_visited = { id: 'series_visited', type: 'symbol', source: 'nomadmania_series', 'source-layer': 'series', minzoom: 6, maxzoom: 60, layout: { 'symbol-spacing': 1, 'icon-image': '{series_id}v', 'icon-size': 0.15, 'icon-allow-overlap': true, 'icon-ignore-placement': true, 'text-anchor': 'top', 'text-field': '{name}', 'text-font': ['Noto Sans Regular'], 'text-max-width': 9, 'text-offset': [0, 0.6], 'text-padding': 2, 'text-size': 12, visibility: 'visible', 'text-optional': true, 'text-ignore-placement': false, 'text-allow-overlap': false }, paint: { 'text-color': '#666', 'text-halo-blur': 0.5, 'text-halo-color': '#ffffff', 'text-halo-width': 1 }, filter: generateFilter([]) }; const INITIAL_REGION = { latitude: 0, longitude: 0, latitudeDelta: 180, longitudeDelta: 180 }; const ICONS_DIR = FileSystem.documentDirectory + 'series_icons/'; const MapScreen: any = ({ navigation, route }: { navigation: any; route: any }) => { const tabBarHeight = useBottomTabBarHeight(); const userId = storage.get('uid', StoreType.STRING) as string; const token = storage.get('token', StoreType.STRING) as string; const cleanupTimeoutRef = useRef(null); const [isConnected, setIsConnected] = useState(true); const netInfo = useConnection(); const { avatarVersion } = useAvatarStore(); const { data: usersOnMapCount } = usePostGetUsersCountQuery(token, !!token && isConnected); const { data: regionsList } = useGetListRegionsQuery(isConnected); const { data: countriesList } = useGetListCountriesQuery(isConnected); const { data: dareList } = useGetListDareQuery(isConnected); const [tilesType, setTilesType] = useState({ label: 'NM regions', value: 0 }); const tilesTypes = [ { label: 'Blank', value: -1 }, { label: 'NM regions', value: 0 }, { label: 'UN countries', value: 1 }, { label: 'DARE places', value: 2 } ]; const [type, setType] = useState<'regions' | 'countries' | 'dare' | 'blank'>('regions'); const [seriesFilter, setSeriesFilter] = useState({ visible: true, groups: [], applied: false, status: -1 }); const [regionsFilter, setRegionsFilter] = useState({ visitedLabel: 'by', year: moment().year() }); const [showNomads, setShowNomads] = useState( (storage.get('showNomads', StoreType.BOOLEAN) as boolean) ?? false ); const { data: locationSettings, refetch } = usePostGetSettingsQuery( token, !!token && isConnected ); const { mutateAsync: updateLocation } = usePostUpdateLocationMutation(); const { data: visitedRegionIds, refetch: refetchVisitedRegions } = usePostGetVisitedRegionsIdsQuery( token, regionsFilter.visitedLabel, regionsFilter.year, +userId, type === 'regions' && !!userId && isConnected ); const { data: visitedCountryIds, refetch: refetchVisitedCountries } = usePostGetVisitedCountriesIdsQuery( token, regionsFilter.visitedLabel, regionsFilter.year, +userId, type === 'countries' && !!userId && isConnected ); const { data: visitedDareIds, refetch: refetchVisitedDare } = usePostGetVisitedDareIdsQuery( token, +userId, type === 'dare' && !!userId && isConnected ); const { data: visitedSeriesIds } = usePostGetVisitedSeriesIdsQuery( token, !!userId && isConnected ); const { data: seriesIcons } = useGetIconsQuery(isConnected); const userInfo = storage.get('currentUserData', StoreType.STRING) as string; const { mutateAsync: mutateUserData } = fetchUserData(); const { mutateAsync: mutateUserDataDare } = fetchUserDataDare(); const { mutateAsync: mutateCountriesData } = fetchCountryUserData(); const [selectedRegion, setSelectedRegion] = useState(null); const [initialRegion, setInitialRegion] = useState(INITIAL_REGION); const [regionPopupVisible, setRegionPopupVisible] = useState(false); const [regionData, setRegionData] = useState(null); const [location, setLocation] = useState(null); const [userAvatars, setUserAvatars] = useState([]); const [userInfoData, setUserInfoData] = useState(null); const [selectedMarker, setSelectedMarker] = useState(null); const [askLocationVisible, setAskLocationVisible] = useState(false); const [openSettingsVisible, setOpenSettingsVisible] = useState(false); const [isWarningModalVisible, setIsWarningModalVisible] = useState(false); const [isEditSlowModalVisible, setIsEditSlowModalVisible] = useState(false); const [isEditModalVisible, setIsEditModalVisible] = useState(false); const [isFilterVisible, setIsFilterVisible] = useState(null); const [isLocationLoading, setIsLocationLoading] = useState(false); const [modalState, setModalState] = useState({ selectedFirstYear: 2021, selectedLastYear: 2021, selectedQuality: qualityOptions[2], selectedNoOfVisits: 1, years: [], id: null }); const [isExpanded, setIsExpanded] = useState(false); const [search, setSearch] = useState(''); const { data: searchData } = useGetUniversalSearch(search, search.length > 0); const [searchInput, setSearchInput] = useState(''); const [searchVisible, setSearchVisible] = useState(false); const [index, setIndex] = useState(0); const width = useSharedValue(48); const usableWidth = Dimensions.get('window').width - 32; const { handleUpdateNM, handleUpdateDare, handleUpdateSlow, userData, setUserData } = useRegion(); const [db1, setDb1] = useState(null); const [db2, setDb2] = useState(null); const [db3, setDb3] = useState(null); const [regionsVisitedFilter, setRegionsVisitedFilter] = useState(generateFilter([])); const [countriesVisitedFilter, setCountriesVisitedFilter] = useState(generateFilter([])); const [dareVisitedFilter, setDareVisitedFilter] = useState(generateFilter([])); const [seriesVisitedFilter, setSeriesVisitedFilter] = useState(generateFilter([])); const [seriesNotVisitedFilter, setSeriesNotVisitedFilter] = useState(generateFilter([])); const [regionsVisited, setRegionsVisited] = useState([]); const [countriesVisited, setCountriesVisited] = useState([]); const [dareVisited, setDareVisited] = useState([]); const [seriesVisited, setSeriesVisited] = useState([]); const [images, setImages] = useState({}); const { mutateAsync: updateSeriesItem } = usePostSetToggleItem(); const [nomads, setNomads] = useState(null); const { data: usersLocation, refetch: refetchUsersLocation } = usePostGetUsersLocationQuery( token, !!token && showNomads && Boolean(location) && isConnected ); const [selectedUser, setSelectedUser] = useState(null); const [zoom, setZoom] = useState(0); const [center, setCenter] = useState(null); const [isZooming, setIsZooming] = useState(true); const hideTimer = useRef | null>(null); const [markerCoords, setMarkerCoords] = useState(null); const [refreshInterval, setRefreshInterval] = useState(0); const isSmallScreen = Dimensions.get('window').width < 383; const processedImages = useRef(new Set()); const [didFinishLoadingStyle, setDidFinishLoadingStyle] = useState(false); useEffect(() => { if (netInfo && netInfo.isConnected !== null) { setIsConnected(netInfo.isConnected); } }, [netInfo]); useEffect(() => { if (showNomads) { refetchUsersLocation(); } }, [showNomads]); useEffect(() => { if (usersLocation && usersLocation.geojson && showNomads) { const filteredNomads: GeoJSON.FeatureCollection = { type: 'FeatureCollection', features: usersLocation.geojson.features.filter( (feature: GeoJSON.Feature) => feature.properties?.id !== +userId ) }; if (!nomads || JSON.stringify(filteredNomads) !== JSON.stringify(nomads)) { setNomads(filteredNomads); } } }, [usersLocation, showNomads, refreshInterval]); useEffect(() => { const loadCachedIcons = async () => { try { const dirInfo = await FileSystem.getInfoAsync(ICONS_DIR); if (!dirInfo.exists) return; const files = await FileSystem.readDirectoryAsync(ICONS_DIR); const cachedImages: Record = {}; files.forEach((fileName) => { if (!fileName.endsWith('.png')) return; const key = fileName.replace('.png', ''); cachedImages[key] = { uri: ICONS_DIR + fileName }; processedImages.current.add(key); }); setImages((prev: any) => ({ ...prev, ...cachedImages })); } catch (e) { console.warn('Error loading cached icons:', e); } }; didFinishLoadingStyle && loadCachedIcons(); }, [didFinishLoadingStyle]); useEffect(() => { if (!seriesIcons || !didFinishLoadingStyle) return; const updateCacheFromAPI = async () => { const loadedImages: Record = {}; const dirInfo = await FileSystem.getInfoAsync(ICONS_DIR); if (!dirInfo.exists) { await FileSystem.makeDirectoryAsync(ICONS_DIR, { intermediates: true }); } const promises = seriesIcons.data.map(async (icon) => { const id = icon.id?.toString(); if (!id || processedImages.current.has(id)) return; const imgUrl = `${API_HOST}/static/img/series_new2_small/${icon.new_icon_png}`; const imgVisitedUrl = `${API_HOST}/static/img/series_new2_small/${icon.new_icon_visited_png}`; const localPath = `${ICONS_DIR}${id}.png`; const localPathVisited = `${ICONS_DIR}${id}v.png`; const [imgInfo, visitedInfo] = await Promise.all([ FileSystem.getInfoAsync(localPath), FileSystem.getInfoAsync(localPathVisited) ]); try { if (!imgInfo.exists) { await FileSystem.downloadAsync(imgUrl, localPath); } if (!visitedInfo.exists) { await FileSystem.downloadAsync(imgVisitedUrl, localPathVisited); } } catch (e) { console.warn(`Download failed for ${id}:`, e); return; } processedImages.current.add(id); processedImages.current.add(`${id}v`); loadedImages[id] = { uri: localPath }; loadedImages[`${id}v`] = { uri: localPathVisited }; }); await Promise.all(promises); setImages((prev: any) => ({ ...prev, ...loadedImages })); }; updateCacheFromAPI(); }, [seriesIcons, didFinishLoadingStyle]); useEffect(() => { const loadDatabases = async () => { const firstDb = await getFirstDatabase(); const secondDb = await getSecondDatabase(); const countriesDb = await getCountriesDatabase(); setDb1(firstDb); setDb2(secondDb); setDb3(countriesDb); }; if (!db1 || !db2 || !db3) { loadDatabases(); } }, [db1, db2, db3]); useEffect(() => { const savedFilterSettings = storage.get('filterSettings', StoreType.STRING) as string; if (savedFilterSettings) { const filterSettings = JSON.parse(savedFilterSettings); setTilesType(filterSettings.tilesType); setType(filterSettings.type); setRegionsFilter({ visitedLabel: filterSettings.selectedVisible?.value && filterSettings.selectedVisible.value === 1 ? 'in' : 'by', year: filterSettings.selectedYear?.value ?? moment().year() }); setSeriesFilter(filterSettings.seriesFilter); } }, []); useFocusEffect( useCallback(() => { if (token) { refetchVisitedRegions(); refetchVisitedCountries(); refetchVisitedDare(); } return () => { if (cleanupTimeoutRef.current) { clearTimeout(cleanupTimeoutRef.current); } cleanupTimeoutRef.current = setTimeout(() => { setImages({}); processedImages.current.clear(); }, 1000); }; }, [navigation, token]) ); // to do refactor useEffect(() => { if (visitedRegionIds) { setRegionsVisited(visitedRegionIds.ids); storage.set('visitedRegions', JSON.stringify(visitedRegionIds.ids)); } else { const storedVisited = storage.get('visitedRegions', StoreType.STRING) as string; setRegionsVisited(storedVisited ? JSON.parse(storedVisited) : []); } }, [visitedRegionIds]); useEffect(() => { if (visitedCountryIds) { setCountriesVisited(visitedCountryIds.ids); storage.set('visitedCountries', JSON.stringify(visitedCountryIds.ids)); } else { const storedVisited = storage.get('visitedCountries', StoreType.STRING) as string; setCountriesVisited(storedVisited ? JSON.parse(storedVisited) : []); } }, [visitedCountryIds]); useEffect(() => { if (visitedDareIds) { setDareVisited(visitedDareIds.ids); storage.set('visitedDares', JSON.stringify(visitedDareIds.ids)); } else { const storedVisited = storage.get('visitedDares', StoreType.STRING) as string; setDareVisited(storedVisited ? JSON.parse(storedVisited) : []); } }, [visitedDareIds]); useEffect(() => { if (visitedSeriesIds && token) { setSeriesVisited(visitedSeriesIds.ids); storage.set('visitedSeries', JSON.stringify(visitedSeriesIds.ids)); } else { const storedVisited = storage.get('visitedSeries', StoreType.STRING) as string; setSeriesVisited(storedVisited ? JSON.parse(storedVisited) : []); } }, [visitedSeriesIds]); useEffect(() => { if (regionsVisited && regionsVisited.length) { setRegionsVisitedFilter(generateFilter(regionsVisited)); } else { setRegionsVisitedFilter(['==', 'id', -1]); } }, [regionsVisited]); useEffect(() => { if (countriesVisited && countriesVisited.length) { setCountriesVisitedFilter(generateFilter(countriesVisited)); } else { setCountriesVisitedFilter(['==', 'id', -1]); } }, [countriesVisited]); useEffect(() => { if (dareVisited && dareVisited.length) { setDareVisitedFilter(generateFilter(dareVisited)); } else { setDareVisitedFilter(['==', 'id', -1]); } }, [dareVisited]); useEffect(() => { if (!seriesFilter.visible) { setSeriesVisitedFilter(generateFilter([])); setSeriesNotVisitedFilter(generateFilter([])); return; } if (seriesFilter.applied) { if (seriesVisited?.length) { setSeriesVisitedFilter([ 'all', ['any', ...seriesVisited.map((id) => ['==', 'id', id])], ['any', ...seriesFilter.groups.map((groupId: number) => ['==', 'series_id', groupId])] ]); setSeriesNotVisitedFilter([ 'all', ['all', ...seriesVisited.map((id) => ['!=', 'id', id])], ['any', ...seriesFilter.groups.map((groupId: number) => ['==', 'series_id', groupId])] ]); } else { setSeriesNotVisitedFilter([ 'any', ...seriesFilter.groups.map((groupId: number) => ['==', 'series_id', groupId]) ]); } } else { setSeriesVisitedFilter(['any', ...seriesVisited.map((id) => ['==', 'id', id])]); setSeriesNotVisitedFilter(['all', ...seriesVisited.map((id) => ['!=', 'id', id])]); } }, [seriesVisited, seriesFilter]); useEffect(() => { if (route.params?.lon && route.params?.lat) { setMarkerCoords([route.params.lon, route.params.lat]); const timeoutId = setTimeout(() => { if (cameraRef.current) { cameraRef.current.setCamera({ centerCoordinate: [route.params?.lon, route.params?.lat], zoomLevel: 15, animationDuration: 800 }); } else { console.warn('Camera ref is not available.'); } }, 800); return () => clearTimeout(timeoutId); } if (route.params?.id && route.params?.type && db1 && db2 && db3) { handleFindRegion(route.params?.id, route.params?.type); } }, [route, db1, db2, db3]); useFocusEffect( useCallback(() => { if (token) { refetch(); } }, []) ); useEffect(() => { if (refreshInterval > 0 && showNomads) { const intervalId = setInterval(() => { refetchUsersLocation(); }, refreshInterval); return () => clearInterval(intervalId); } }, [refreshInterval, showNomads]); useEffect(() => { (async () => { let { status } = await Location.getForegroundPermissionsAsync(); const isServicesEnabled = await Location.hasServicesEnabledAsync(); if (locationSettings && locationSettings.sharing_refresh_interval) { setRefreshInterval(locationSettings.sharing_refresh_interval * 1000); } if ( status !== 'granted' || !token || (locationSettings && locationSettings.sharing === 0) || !isServicesEnabled ) { setShowNomads(false); storage.set('showNomads', false); await stopBackgroundLocationUpdates(); return; } const bgStatus = await Location.getBackgroundPermissionsAsync(); if (bgStatus.status !== 'granted') { const { status } = await Location.requestBackgroundPermissionsAsync(); if (status === Location.PermissionStatus.GRANTED) { await startBackgroundLocationUpdates(); } else { await stopBackgroundLocationUpdates(); } } else { // await startBackgroundLocationUpdates(); await restartBackgroundLocationUpdates(); } try { let currentLocation = await Location.getCurrentPositionAsync({ accuracy: Location.Accuracy.Balanced }); setLocation(currentLocation.coords); if (locationSettings && locationSettings.sharing === 1 && token) { updateLocation({ token, lat: currentLocation.coords.latitude, lng: currentLocation.coords.longitude }); showNomads && refetchUsersLocation(); } } catch (error) { console.error('Error fetching user location:', error); } })(); }, [locationSettings]); useEffect(() => { const currentYear = moment().year(); let yearSelector: { label: string; value: number }[] = [{ label: 'visited', value: 1 }]; for (let i = currentYear; i >= 1951; i--) { yearSelector.push({ label: i.toString(), value: i }); } handleModalStateChange({ years: yearSelector }); }, []); useFocusEffect( useCallback(() => { navigation.getParent()?.setOptions({ tabBarStyle: { display: regionPopupVisible ? 'none' : 'flex', position: 'absolute', ...Platform.select({ android: { height: 58 } }) } }); }, [regionPopupVisible, navigation]) ); const mapRef = useRef(null); const cameraRef = useRef(null); const shapeSourceRef = useRef(null); useEffect(() => { if (userInfo) { setUserInfoData(JSON.parse(userInfo)); } }, [userInfo]); const handlePress = () => { if (isExpanded) { setSearchInput(''); } setIsExpanded((prev) => !prev); width.value = withTiming(isExpanded ? 48 : usableWidth, { duration: 300, easing: Easing.inOut(Easing.ease) }); }; const animatedStyle = useAnimatedStyle(() => { return { width: width.value }; }); const loadInitialRegion = () => { try { const savedInitialRegion = storage.get('initialRegion', StoreType.STRING) as string; if (savedInitialRegion) { const region = JSON.parse(savedInitialRegion); setInitialRegion(region); } } catch (e) { console.error('Failed to load saved initial region:', e); } }; useEffect(() => { loadInitialRegion(); }, []); useEffect(() => { if (initialRegion && !route.params?.id) { const timeoutId = setTimeout(() => { if (cameraRef.current) { cameraRef.current.setCamera({ centerCoordinate: [initialRegion.longitude, initialRegion.latitude], zoomLevel: Math.log2(360 / initialRegion.latitudeDelta), animationDuration: 500 }); } else { console.warn('Camera ref is not available.'); } }, 500); return () => clearTimeout(timeoutId); } }, [initialRegion]); const handleMapChange = async () => { if (!mapRef.current) return; if (hideTimer.current) clearTimeout(hideTimer.current); setIsZooming(true); const currentZoom = await mapRef.current.getZoom(); const currentCenter = await mapRef.current.getCenter(); setZoom(currentZoom); setCenter(currentCenter); }; const onMapPress = async (event: any) => { if (!mapRef.current) return; if (selectedMarker || selectedUser) { closeCallout(); return; } if (type === 'blank') return; try { const { screenPointX, screenPointY } = event.properties; const { features } = await mapRef.current.queryRenderedFeaturesAtPoint( [screenPointX, screenPointY], undefined, ['regions', 'countries', 'dare'] ); if (features?.length) { const region = features[0]; if (selectedRegion === region.properties?.id) return; let db = type === 'regions' ? db1 : type === 'countries' ? db3 : db2; let tableName = type === 'dare' ? 'places' : type; let foundRegion = region.properties?.id; setSelectedRegion(region.properties?.id); await getData(db, foundRegion, tableName, handleRegionData) .then(() => { setRegionPopupVisible(true); }) .catch((error) => { console.error('Error fetching data', error); refreshDatabases(); }); if (tableName === 'regions') { token ? await mutateUserData( { region_id: +foundRegion, token: String(token) }, { onSuccess: (data) => { setUserData({ type: 'nm', id: +foundRegion, ...data }); } } ) : setUserData({ type: 'nm', id: +foundRegion }); if (regionsList && regionsList.data) { const region = regionsList.data.find((region) => region.id === +foundRegion); if (region) { const bounds = turf.bbox(region.bbox); cameraRef.current?.fitBounds( [bounds[2], bounds[3]], [bounds[0], bounds[1]], [10, 10, 50, 10], 1000 ); } } } else if (tableName === 'countries') { token ? await mutateCountriesData( { id: +foundRegion, token }, { onSuccess: (data) => { setUserData({ type: 'countries', id: +foundRegion, ...data.data }); } } ) : setUserData({ type: 'countries', id: +foundRegion }); if (countriesList && countriesList.data) { const region = countriesList.data.find((region) => region.id === +foundRegion); if (region) { const bounds = turf.bbox(region.bbox); cameraRef.current?.fitBounds( [bounds[2], bounds[3]], [bounds[0], bounds[1]], [10, 10, 50, 10], 1000 ); } } } else { token ? await mutateUserDataDare( { dare_id: +foundRegion, token: String(token) }, { onSuccess: (data) => { setUserData({ type: 'dare', id: +foundRegion, ...data }); } } ) : setUserData({ type: 'dare', id: +foundRegion }); if (dareList && dareList.data) { const region = dareList.data.find((region) => region.id === +foundRegion); if (region) { const bounds = turf.bbox(region.bbox); cameraRef.current?.fitBounds( [bounds[2], bounds[3]], [bounds[0], bounds[1]], [10, 10, 50, 10], 1000 ); } } } } else { handleClosePopup(); } } catch (error) { console.error('Error onMapPress features:', error); } }; const handleRegionDidChange = async (feature: GeoJSON.Feature) => { hideTimer.current = setTimeout(() => { setIsZooming(false); }, 2000); if (!feature) return; const { zoomLevel } = feature.properties; const { coordinates } = feature.geometry; if (!zoomLevel || !coordinates) return; const latitudeDelta = 360 / 2 ** zoomLevel; const longitudeDelta = latitudeDelta; const region = { latitude: coordinates[1], longitude: coordinates[0], latitudeDelta, longitudeDelta }; storage.set('initialRegion', JSON.stringify(region)); }; const handleClosePopup = async () => { setSelectedRegion(null); setRegionPopupVisible(false); setRegionData(null); }; const handleGetLocation = async () => { setIsLocationLoading(true); try { let { status, canAskAgain } = await Location.getForegroundPermissionsAsync(); const isServicesEnabled = await Location.hasServicesEnabledAsync(); if (status === 'granted' && isServicesEnabled) { const bgStatus = await Location.getBackgroundPermissionsAsync(); if (bgStatus.status !== 'granted') { const { status } = await Location.requestBackgroundPermissionsAsync(); if (status === Location.PermissionStatus.GRANTED) { await startBackgroundLocationUpdates(); } else { await stopBackgroundLocationUpdates(); } } else { await startBackgroundLocationUpdates(); } 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) { cameraRef.current?.flyTo( [currentLocation.coords.longitude, currentLocation.coords.latitude], 1000 ); } if (locationSettings && locationSettings.sharing === 1 && token) { updateLocation({ token, lat: currentLocation.coords.latitude, lng: currentLocation.coords.longitude }); showNomads && refetchUsersLocation(); } handleClosePopup(); } 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 handleOpenEditModal = () => { handleModalStateChange({ selectedFirstYear: userData?.first_visit_year, selectedLastYear: userData?.last_visit_year, selectedQuality: qualityOptions.find((quality) => quality.id === userData?.best_visit_quality) || qualityOptions[2], selectedNoOfVisits: userData?.no_of_visits || 1, id: regionData?.id }); setIsEditModalVisible(true); }; const handleOpenEditSlowModal = () => { setIsEditSlowModalVisible(true); }; const handleSearch = async () => { setSearch(searchInput); setSearchVisible(true); }; const handleCloseModal = () => { setSearchInput(''); setSearchVisible(false); handlePress(); }; const handleRegionData = async (regionData: any, avatars: string[]) => { if (!regionData) { await refreshDatabases(); } setRegionData(regionData); setUserAvatars(avatars); }; const handleFindRegion = async (id: number, type: 'regions' | 'countries' | 'places') => { setType(type === 'places' ? 'dare' : type); if (!db1 || !db2 || !db3) { return; } const db = type === 'regions' ? db1 : type === 'countries' ? db3 : db2; if (id) { setSelectedRegion(id); await getData(db, id, type, handleRegionData) .then(() => { setRegionPopupVisible(true); }) .catch((error) => { console.error('Error fetching data', error); refreshDatabases(); }); if (type === 'regions') { token ? await mutateUserData( { region_id: id, token: String(token) }, { onSuccess: (data) => { setUserData({ type: 'nm', id, ...data }); } } ) : setUserData({ type: 'nm', id }); if (regionsList && regionsList.data) { const region = regionsList.data.find((region) => region.id === +id); if (region) { const bounds = turf.bbox(region.bbox); cameraRef.current?.fitBounds( [bounds[2], bounds[3]], [bounds[0], bounds[1]], [10, 10, 50, 10], 1000 ); } } } else if (type === 'countries') { token ? await mutateCountriesData( { id, token }, { onSuccess: (data) => { setUserData({ type: 'countries', id, ...data.data }); } } ) : setUserData({ type: 'countries', id }); if (countriesList && countriesList.data) { const region = countriesList.data.find((region) => region.id === +id); if (region) { const bounds = turf.bbox(region.bbox); cameraRef.current?.fitBounds( [bounds[2], bounds[3]], [bounds[0], bounds[1]], [10, 10, 50, 10], 1000 ); } } } else { token ? await mutateUserDataDare( { dare_id: +id, token: String(token) }, { onSuccess: (data) => { setUserData({ type: 'dare', id: +id, ...data }); } } ) : setUserData({ type: 'dare', id: +id }); if (dareList && dareList.data) { const region = dareList.data.find((region) => region.id === +id); if (region) { const bounds = turf.bbox(region.bbox); cameraRef.current?.fitBounds( [bounds[2], bounds[3]], [bounds[0], bounds[1]], [10, 10, 50, 10], 1000 ); } } } } else { handleClosePopup(); } }; const handleMarkerPress = async (event: any) => { const { features } = event; if (features?.length) { const selectedFeature = features[0]; const { coordinates } = selectedFeature.geometry; const visited = seriesVisited.includes(selectedFeature.properties.id) ? 1 : 0; const icon = images[selectedFeature.properties.series_id]; const { name, description, series_name, series_id, id } = selectedFeature.properties; if (coordinates) { setSelectedMarker({ coordinates, name, icon, description, series_name, visited, series_id, id }); setSelectedUser(null); } } }; const closeCallout = () => { setSelectedMarker(null); setSelectedUser(null); }; const toggleSeries = useCallback( async (item: any) => { if (!token) { setIsWarningModalVisible(true); return; } const itemData = { token, series_id: item.series_id, item_id: item.id, checked: (item.visited === 0 ? 1 : 0) as 0 | 1, double: 0 as 0 | 1 }; try { updateSeriesItem(itemData); if (item.visited === 1) { setSeriesVisited((current) => current.filter((id) => id !== item.id)); setSelectedMarker((current: any) => ({ ...current, visited: 0 })); } else { setSeriesVisited((current) => [...current, item.id]); setSelectedMarker((current: any) => ({ ...current, visited: 1 })); } } catch (error) { console.error('Failed to update series state', error); } }, [token, updateSeriesItem] ); const handleModalStateChange = (updates: { [key: string]: any }) => { setModalState((prevState) => ({ ...prevState, ...updates })); }; const handleUserPress = (event: any) => { const selectedFeature = event.features[0]; const { coordinates } = selectedFeature.geometry; const { avatar, first_name, last_name, flag, id, last_seen } = selectedFeature.properties; if (selectedFeature) { setSelectedUser({ coordinates, avatar: avatar ? { uri: API_HOST + avatar } : logo, first_name, last_name, flag: { uri: API_HOST + flag }, id, last_seen }); setSelectedMarker(null); } }; return ( setDidFinishLoadingStyle(true)} > {/* { try { if (processedImages.current.has(image)) { return; } processedImages.current.add(image); setImages((prevImages: any) => ({ ...prevImages, [image]: defaultSeriesIcon })); } catch (error) { console.error('Error in onImageMissing:', error); } }} > */} {markerCoords && ( )} {type === 'regions' && ( <> )} {type === 'countries' && ( <> )} {type === 'dare' && ( <> )} {selectedRegion && type && ( <> )} {seriesFilter.status !== 1 ? (() => { try { return ( ); } catch (error) { console.warn('SymbolLayer render error:', error); return null; } })() : null} {seriesFilter.status !== 0 ? (() => { try { return ( ); } catch (error) { console.warn('SymbolLayer render error:', error); return null; } })() : null} {nomads && showNomads && ( { const feature = event.features[0]; const isCluster = feature.properties?.cluster; if (isCluster) { const clusterCoordinates = (feature.geometry as GeoJSON.Point).coordinates; const zoom = await shapeSourceRef.current?.getClusterExpansionZoom( feature as GeoJSON.Feature ); const newZoom = zoom ?? 2; cameraRef.current?.setCamera({ centerCoordinate: clusterCoordinates, zoomLevel: newZoom, animationDuration: 500, animationMode: 'flyTo' }); return; } else { handleUserPress(event); } }} cluster={true} clusterRadius={50} > )} {selectedUser && } {selectedMarker && ( )} {location && ( { const currentZoom = await mapRef.current?.getZoom(); const newZoom = (currentZoom || 0) + 2; cameraRef.current?.setCamera({ centerCoordinate: [location.longitude, location.latitude], zoomLevel: newZoom, animationDuration: 500, animationMode: 'flyTo' }); }} > {/* to do custom user location */} )} {center ? ( ) : null} {regionPopupVisible && regionData ? ( <> Close {isLocationLoading ? ( ) : ( )} { if (!token) { setIsWarningModalVisible(true); return; } handleUpdateNM(id, first, last, visits, quality); const updatedIds = regionsVisited.includes(id) ? regionsVisited.filter((visitedId) => visitedId !== id) : [...regionsVisited, id]; setRegionsVisited(updatedIds); refetchVisitedCountries(); }} updateDare={(id, visits) => { if (!token) { setIsWarningModalVisible(true); return; } handleUpdateDare(id, visits); const updatedIds = dareVisited.includes(id) ? dareVisited.filter((visitedId) => visitedId !== id) : [...dareVisited, id]; setDareVisited(updatedIds); }} disabled={!token || !isConnected} updateSlow={(id, v, s11, s31, s101) => { if (!token) { setIsWarningModalVisible(true); return; } handleUpdateSlow(id, v, s11, s31, s101); const updatedIds = countriesVisited.includes(id) ? countriesVisited.filter((visitedId) => visitedId !== id) : [...countriesVisited, id]; setCountriesVisited(updatedIds); }} openEditSlowModal={handleOpenEditSlowModal} /> ) : ( <> {!isExpanded ? ( navigation.navigate(NAVIGATION_PAGES.PROFILE_TAB)} > {token ? ( userInfoData?.avatar ? ( ) ) : ( )} ) : null} {isExpanded ? ( <> setSearchInput(text)} onSubmitEditing={handleSearch} /> ) : ( )} { try { setIsFilterVisible('regions'); closeCallout(); } catch (error) { console.error('Error opening filter:', error); } }} icon={TravelsIcon} text="Travels" active={type !== 'blank'} /> { try { setIsFilterVisible('series'); closeCallout(); } catch (error) { console.error('Error opening filter:', error); } }} icon={SeriesIcon} text="Series" active={seriesFilter.visible} /> {token ? ( { try { setIsFilterVisible('nomads'); closeCallout(); } catch (error) { console.error('Error opening filter:', error); } }} icon={NomadsIcon} text="Nomads" active={showNomads} > {usersOnMapCount && usersOnMapCount?.count > 0 ? ( ) : null} ) : null} {isLocationLoading ? ( ) : ( )} )} setIsWarningModalVisible(false)} /> setIsEditModalVisible(false)} modalState={modalState} updateModalState={handleModalStateChange} updateNM={handleUpdateNM} /> setIsEditSlowModalVisible(false)} item={{ ...userData, country_id: regionData?.id }} updateSlow={(id, v, s11, s31, s101) => handleUpdateSlow(id, v, s11, s31, s101)} /> 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?" /> ); }; export default MapScreen;