import { Dimensions, Linking, Platform, Text, TextInput, TouchableOpacity, View, Image, StatusBar, ActivityIndicator, ScrollView } from 'react-native'; import React, { useEffect, useRef, useState, useCallback, useMemo } 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 SearchIcon from 'assets/icons/search.svg'; import LocationIcon from 'assets/icons/location.svg'; import CloseSvg from 'assets/icons/close.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, { configureReanimatedLogger, 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'; 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/legacy'; import moment from 'moment'; import { usePostGetVisitedCountriesIdsQuery, usePostGetVisitedDareIdsQuery, usePostGetVisitedRegionsIdsQuery, usePostGetVisitedSeriesIdsQuery } from '@api/maps'; import FilterModal, { FilterModalRef } from './FilterModal'; import { useGetListDareQuery } from '@api/myDARE'; import { useGetIconsQuery, usePostSetToggleItem } from '@api/series'; import MarkerItem from './MarkerItem'; import { usePostGetSettingsQuery, usePostGetUsersCountQuery, usePostGetUsersLocationFilteredMutation, 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 { stopBackgroundLocationUpdates } from 'src/utils/backgroundLocation'; import { SheetManager } from 'react-native-actions-sheet'; import MultipleSeriesModal from './MultipleSeriesModal'; import { useSubscription } from 'src/screens/OfflineMapsScreen/useSubscription'; import { point } from '@turf/turf'; import { booleanPointInPolygon } from '@turf/turf'; import { area } from '@turf/turf'; configureReanimatedLogger({ strict: false }); const clusteredUsersIcon = require('assets/icons/icon-clustered-users.png'); const logo = require('assets/logo-world.png'); MapLibreRN.Logger.setLogLevel('error'); const isValidNumber = (val: any) => typeof val === 'number' && !isNaN(val) && isFinite(val); const sanitizeNomadsGeoJSON = ( collection: GeoJSON.FeatureCollection ): GeoJSON.FeatureCollection => { const validFeatures = collection.features.filter((feature) => { if (!feature || !feature.geometry || !feature.properties) return false; const geom = feature.geometry; if (geom.type !== 'Point') return false; const coords = (geom as GeoJSON.Point).coordinates; if (!Array.isArray(coords) || coords.length < 2) return false; const [lng, lat] = coords; if (lng == null || lat == null) return false; if (!isValidNumber(lng) || !isValidNumber(lat)) return false; if (lng < -180 || lng > 180 || lat < -90 || lat > 90) return false; return true; }); return { type: 'FeatureCollection', features: validFeatures }; }; const generateFilter = (ids: number[]) => { const validIds = ids?.filter(isValidNumber) || []; return validIds.length ? ['any', ...validIds.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: 5, 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: 5, 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 SERIES_ICON_SIZE = [ 'interpolate', ['linear'], ['zoom'], 3, 0.3, 4, 0.36, 5, 0.43, 6, 0.5, 7, 0.57, 8, 0.63, 9, 0.7, 10, 0.77, 11, 0.84, 12, 0.91, 13, 0.98, 14, 1.05, 15, 1.12, 16, 1.18, 17, 1.25 ] as unknown as number; const SERIES_CIRCLE_RADIUS = [ 'interpolate', ['linear'], ['zoom'], 3, 5, 4, 6, 5, 7, 6, 8, 7, 9, 8, 10, 9, 11, 10, 12, 11, 13, 12, 14, 13, 16, 14, 17, 15, 18, 16, 19, 17, 20 ] as unknown as number; const SERIES_CIRCLE_STROKE = [ 'interpolate', ['linear'], ['zoom'], 3, 1, 7, 1.5, 13, 2, 17, 2.5 ] as unknown as number; const SERIES_TEXT_SIZE = [ 'interpolate', ['linear'], ['zoom'], 3, 6, 4, 7, 5, 8, 6, 9, 7, 10, 8, 10, 9, 11, 10, 11, 11, 12, 12, 12, 13, 13, 14, 13, 15, 14, 16, 14, 17, 15 ] as unknown as number; const processedSeriesShares = new Set(); 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 [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 [usersCount, setUsersCount] = useState(0); 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, labelIds: [] }); const [regionsFilter, setRegionsFilter] = useState({ visitedLabel: 'by', year: moment().year() }); const [nomadsFilter, setNomadsFilter] = useState({ friends: 0, trusted: 0, countries: [] }); 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 [forceRefetch, setForceRefetch] = useState(0); const { data: visitedRegionIds, refetch: refetchVisitedRegions } = usePostGetVisitedRegionsIdsQuery( token, regionsFilter.visitedLabel, regionsFilter.year, +userId, type === 'regions' && !!userId && isConnected, forceRefetch ); const { data: visitedCountryIds, refetch: refetchVisitedCountries } = usePostGetVisitedCountriesIdsQuery( token, regionsFilter.visitedLabel, regionsFilter.year, +userId, type === 'countries' && !!userId && isConnected, forceRefetch ); 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 filterModalRef = useRef(null); const [isLocationLoading, setIsLocationLoading] = useState(false); const [premiumModalVisible, setPremiumModalVisible] = 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 { mutateAsync: getLocationFiltered } = usePostGetUsersLocationFilteredMutation(); 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 [renderCamera, setRenderCamera] = useState(Platform.OS === 'ios'); const isAnimatingRef = useRef(false); const animationTimeoutRef = useRef(null); const handledSeriesShareRef = useRef(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); const { isPremium, loading } = useSubscription(); useEffect(() => { if (netInfo && netInfo.isConnected !== null) { setIsConnected(netInfo.isConnected); } }, [netInfo]); useEffect(() => { if (Platform.OS === 'android') { MapLibreRN.setConnected(isConnected); } }, [isConnected]) useEffect(() => { if (showNomads) { refetchUsersLocation(); } }, [showNomads]); useEffect(() => { if (usersLocation && usersLocation.geojson && showNomads) { const rawFiltered: GeoJSON.FeatureCollection = { type: 'FeatureCollection', features: usersLocation.geojson.features.filter( (feature: GeoJSON.Feature) => feature.properties?.id !== +userId ) }; const filteredNomads = sanitizeNomadsGeoJSON(rawFiltered); if (!nomads || filteredNomads.features.length !== nomads.features.length) { setNomads(filteredNomads); } if (usersOnMapCount && usersOnMapCount.count) { setUsersCount(usersOnMapCount.count); } } }, [usersLocation, showNomads, refreshInterval]); useEffect(() => { if ( usersOnMapCount && usersOnMapCount.count && !nomadsFilter.friends && !nomadsFilter.countries?.length ) { setUsersCount(usersOnMapCount.count); } }, [usersOnMapCount]); 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; const storageShowNomads = storage.get('showNomads', StoreType.BOOLEAN) as boolean; 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() }); setShowNomads(storageShowNomads ?? false); setNomadsFilter({ friends: filterSettings.nomadsFilter ? filterSettings.nomadsFilter.friends : 0, trusted: filterSettings.nomadsFilter ? filterSettings.nomadsFilter.trusted : 0, countries: filterSettings.nomadsFilter?.countries ?? [] }); setSeriesFilter(filterSettings.seriesFilter); } }, []); useFocusEffect( useCallback(() => { if (token) { setForceRefetch((prev) => prev + 1); refetchVisitedCountries(); refetchVisitedDare(); } if (userData && type === 'regions') { handleClosePopup(); } }, [navigation, token]) ); 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 (loading) return; if ( !isPremium || !token || showNomads || (!nomadsFilter.friends && !nomadsFilter.countries?.length) ) return; const countriesFilter = nomadsFilter.countries ? nomadsFilter.countries.map((country: any) => country.country) : []; getLocationFiltered( { token, friends: nomadsFilter.friends, trusted: nomadsFilter.trusted, countries: nomadsFilter.countries ? JSON.stringify(countriesFilter) : undefined }, { onSuccess: (data: any) => { if (data && data?.geojson) { setUsersCount(data.geojson.features?.length); const rawFiltered: GeoJSON.FeatureCollection = { type: 'FeatureCollection', features: data.geojson.features.filter( (feature: GeoJSON.Feature) => feature.properties?.id !== +userId ) }; const filteredNomads = sanitizeNomadsGeoJSON(rawFiltered); if (!nomads || JSON.stringify(filteredNomads) !== JSON.stringify(nomads)) { setNomads(filteredNomads); } } else { setNomads(null); } }, onError: (error) => { console.error('Error fetching filtered users location:', error); } } ); }, [nomadsFilter, loading]); // const isMustModeEnabled = useMemo( // () => seriesFilter.visible && seriesFilter.groups?.includes(-1), // [seriesFilter] // ); // const isGroupEnabled = useCallback( // (groupId: number) => { // if (!seriesFilter.visible) return false; // if (!seriesFilter.groups?.length) return true; // return seriesFilter.groups.includes(groupId); // }, // [seriesFilter] // ); // const seriesGroups = useMemo(() => { // if (!seriesIcons?.data) return []; // return seriesIcons.data.map((icon) => ({ // id: icon.id // })); // }, [seriesIcons]); const seriesTextField = useMemo(() => { const ids = seriesFilter.labelIds; if (!ids) return ['step', ['zoom'], '', 9, ['get', 'name']] as unknown as string; if (ids.length === 0) return ''; return [ 'step', ['zoom'], '', 9, ['case', ['in', ['get', 'series_id'], ['literal', ids]], ['get', 'name'], ''] ] as unknown as string; }, [seriesFilter.labelIds]); useEffect(() => { if (!seriesFilter.visible) { setSeriesVisitedFilter(generateFilter([])); setSeriesNotVisitedFilter(generateFilter([])); return; } if (seriesFilter.applied) { if (seriesVisited?.length) { setSeriesVisitedFilter([ 'all', ['any', ...seriesVisited.filter(isValidNumber).map((id) => ['==', 'id', id])], [ 'any', ...seriesFilter.groups.filter(isValidNumber).map((groupId: number) => { if (groupId === -1) { return ['==', 'must', 1]; } return ['==', 'series_id', groupId]; }) ] ]); setSeriesNotVisitedFilter([ 'all', ['all', ...seriesVisited.filter(isValidNumber).map((id) => ['!=', 'id', id])], [ 'any', ...seriesFilter.groups.filter(isValidNumber).map((groupId: number) => { if (groupId === -1) { return ['==', 'must', 1]; } return ['==', 'series_id', groupId]; }) ] ]); } else { setSeriesNotVisitedFilter([ 'any', ...seriesFilter.groups.filter(isValidNumber).map((groupId: number) => { if (groupId === -1) { return ['==', 'must', 1]; } return ['==', 'series_id', groupId]; }) ]); } } else { setSeriesVisitedFilter( seriesVisited.length ? ['any', ...seriesVisited.filter(isValidNumber).map((id) => ['==', 'id', id])] : generateFilter([]) ); setSeriesNotVisitedFilter( seriesVisited.length ? ['all', ...seriesVisited.filter(isValidNumber).map((id) => ['!=', 'id', id])] : ['all'] ); } }, [seriesVisited, seriesFilter]); useEffect(() => { if (isValidNumber(route.params?.lon) && isValidNumber(route.params?.lat)) { setMarkerCoords([route.params.lon, route.params.lat]); const timeoutId = setTimeout(() => { cameraController.setCamera({ centerCoordinate: [route.params?.lon, route.params?.lat], zoomLevel: 15, animationDuration: 800, animationMode: 'flyTo' }); }, 800); return () => clearTimeout(timeoutId); } if (route.params?.seriesItemShare) { const itemToFind = route.params.seriesItemShare; const shareId = itemToFind._timestamp || itemToFind.item_id; if (!processedSeriesShares.has(shareId)) { processedSeriesShares.add(shareId); handledSeriesShareRef.current = shareId; navigation.setParams({ seriesItemShare: undefined }); handleFindSeries(itemToFind); } } else 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(); } return () => { if (handledSeriesShareRef.current !== null) { setSelectedMarker(null); } }; }, []) ); 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 requestBackgroundPermissionSafe(); // 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); const androidCancelRef = useRef void)>(null); const setCameraAndroid = (config: any) => { androidCancelRef.current?.(); androidCancelRef.current = null; setRenderCamera(true); let frame = 0; let cancelled = false; const startedAt = Date.now(); const MAX_WAIT_MS = 1500; const cleanup = () => { cancelled = true; if (frame) cancelAnimationFrame(frame); isAnimatingRef.current = false; setRenderCamera(false); }; androidCancelRef.current = cleanup; const trySetCamera = () => { if (cancelled) return; if (cameraRef.current) { cameraRef.current.setCamera(config); const duration = (config.animationDuration || 1000) + 200; animationTimeoutRef.current && clearTimeout(animationTimeoutRef.current); animationTimeoutRef.current = setTimeout(() => { if (!cancelled) cleanup(); }, duration); return; } if (Date.now() - startedAt > MAX_WAIT_MS) { cleanup(); return; } frame = requestAnimationFrame(trySetCamera); }; frame = requestAnimationFrame(trySetCamera); }; const cameraController = { setCamera: useCallback((config: any) => { isAnimatingRef.current = true; if (animationTimeoutRef.current) { clearTimeout(animationTimeoutRef.current); } if (Platform.OS === 'android') { setRenderCamera(true); setCameraAndroid(config); return; } 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); } }, []), 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); } }, []), moveTo: useCallback((coordinates: number[], duration: number = 0) => { isAnimatingRef.current = true; if (animationTimeoutRef.current) { clearTimeout(animationTimeoutRef.current); } if (Platform.OS === 'android') { setRenderCamera(true); requestAnimationFrame(() => { cameraRef.current?.moveTo(coordinates, duration); }); animationTimeoutRef.current = setTimeout(() => { isAnimatingRef.current = false; setRenderCamera(false); }, duration + 200); } else { cameraRef.current?.moveTo(coordinates, duration); animationTimeoutRef.current = setTimeout(() => { isAnimatingRef.current = false; }, duration + 100); } }, []), zoomTo: useCallback((zoomLevel: number, duration: number = 1000) => { isAnimatingRef.current = true; if (animationTimeoutRef.current) { clearTimeout(animationTimeoutRef.current); } if (Platform.OS === 'android') { setRenderCamera(true); requestAnimationFrame(() => { cameraRef.current?.zoomTo(zoomLevel, duration); }); animationTimeoutRef.current = setTimeout(() => { isAnimatingRef.current = false; setRenderCamera(false); }, duration + 200); } else { cameraRef.current?.zoomTo(zoomLevel, duration); animationTimeoutRef.current = setTimeout(() => { isAnimatingRef.current = false; }, duration + 100); } }, []) }; useEffect(() => { return () => { androidCancelRef.current?.(); if (animationTimeoutRef.current) clearTimeout(animationTimeoutRef.current); }; }, []); useEffect(() => { if (userInfo) { setUserInfoData(JSON.parse(userInfo)); } }, [userInfo]); const requestBackgroundPermissionSafe = async () => { await new Promise((resolve) => setTimeout(resolve, 300)); return await Location.requestBackgroundPermissionsAsync(); }; 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 && isValidNumber(initialRegion.longitude) && isValidNumber(initialRegion.latitude) ) { const timeoutId = setTimeout(() => { cameraController.setCamera({ centerCoordinate: [initialRegion.longitude, initialRegion.latitude], zoomLevel: isValidNumber(initialRegion.latitudeDelta) && initialRegion.latitudeDelta > 0 ? Math.log2(360 / initialRegion.latitudeDelta) : 0, animationDuration: 500, animationMode: 'flyTo' }); }, 500); return () => clearTimeout(timeoutId); } }, [initialRegion]); const handleMapChange = async () => { if (!mapRef.current || isAnimatingRef.current) return; if (hideTimer.current) clearTimeout(hideTimer.current); setIsZooming(true); const newZoom = await mapRef.current.getZoom(); const newCenter = await mapRef.current.getCenter(); setZoom((prevZoom) => { if (prevZoom !== newZoom) return newZoom; return prevZoom; }); setCenter((prevCenter) => { if (!prevCenter || prevCenter[0] !== newCenter[0] || prevCenter[0] !== newCenter[0]) { return newCenter; } return prevCenter; }); }; useEffect(() => { return () => { if (animationTimeoutRef.current) { clearTimeout(animationTimeoutRef.current); } }; }, []); const onMapPress = async (event: any) => { if (!mapRef.current) return; // const { screenPointX, screenPointY } = event.properties; // const seriesLayerIds = seriesGroups.flatMap((g) => [ // `series-${g.id}-unvisited`, // `series-${g.id}-must-icon`, // `series-${g.id}-visited-normal`, // `series-${g.id}-visited-must` // ]); // const { features: seriesFeatures } = await mapRef.current.queryRenderedFeaturesAtPoint( // [screenPointX, screenPointY], // undefined, // seriesLayerIds // ); // if (seriesFeatures?.length) { // handleMarkerPress({ features: seriesFeatures }); // return; // } if (selectedMarker || selectedUser) { closeCallout(); return; } if (type === 'blank') return; try { const { screenPointX, screenPointY } = event.properties; const [lng, lat] = event.geometry?.coordinates; const { features } = await mapRef.current.queryRenderedFeaturesAtPoint( [screenPointX, screenPointY], undefined, ['regions', 'countries', 'dare'] ); if (features?.length) { let region = features[0]; try { if (typeof lng === 'number' && typeof lat === 'number') { const p = point([lng, lat]); const inside = features .filter( (f) => f?.geometry?.type === 'Polygon' || f?.geometry?.type === 'MultiPolygon' ) .filter((f) => booleanPointInPolygon(p, f as any)); const best = inside.slice().sort((a, b) => area(a as any) - area(b as any))[0]; if (best) { region = best; } } } catch (e) { console.warn('region detection failed', e); } 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); if (region.properties?.alt_id) { setSelectedRegion(region.properties?.alt_id); db = db1; tableName = 'regions'; } await getData(db, foundRegion, tableName, handleRegionData) .then(() => { setRegionPopupVisible(true); }) .catch((error) => { console.error('Error fetching data', error); refreshDatabases(); }); if (tableName === 'regions') { const isVisitedLocal = regionsVisited.includes(+foundRegion); setUserData({ type: 'nm', id: +foundRegion, visited: isVisitedLocal }); if (token) { await mutateUserData( { region_id: +foundRegion, token: String(token) }, { onSuccess: (data) => { setUserData({ type: 'nm', id: +foundRegion, ...data }); } } ); } else { setUserData({ type: 'nm', id: +foundRegion }); } if (regionsList && regionsList.data) { const region = regionsList.data.find((region) => region.id === +foundRegion); if (region && region.bbox) { const bounds = turf.bbox(region.bbox); if (bounds.every(isValidNumber)) { cameraController.fitBounds( [bounds[2], bounds[3]], [bounds[0], bounds[1]], [10, 10, 50, 10], 1000 ); } } } } else if (tableName === 'countries') { const isVisitedLocal = countriesVisited.includes(+foundRegion); setUserData({ type: 'countries', id: +foundRegion, visited: isVisitedLocal }); if (token) { await mutateCountriesData( { id: +foundRegion, token }, { onSuccess: (data) => { setUserData({ type: 'countries', id: +foundRegion, ...data.data }); } } ); } else { setUserData({ type: 'countries', id: +foundRegion }); } if (countriesList && countriesList.data) { const region = countriesList.data.find((region) => region.id === +foundRegion); if (region && region.bbox) { const bounds = turf.bbox(region.bbox); if (bounds.every(isValidNumber)) { cameraController.fitBounds( [bounds[2], bounds[3]], [bounds[0], bounds[1]], [10, 10, 50, 10], 1000 ); } } } } else { const isVisitedLocal = dareVisited.includes(+foundRegion); setUserData({ type: 'dare', id: +foundRegion, visited: isVisitedLocal }); if (token) { await mutateUserDataDare( { dare_id: +foundRegion, token: String(token) }, { onSuccess: (data) => { setUserData({ type: 'dare', id: +foundRegion, ...data }); } } ); } else { setUserData({ type: 'dare', id: +foundRegion }); } if (dareList && dareList.data) { const region = dareList.data.find((region) => region.id === +foundRegion); if (region && region.bbox) { const bounds = turf.bbox(region.bbox); if (bounds.every(isValidNumber)) { cameraController.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 requestBackgroundPermissionSafe(); // 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: Platform.OS === 'ios' ? Location.Accuracy.Balanced : Location.Accuracy.Low }); setLocation(currentLocation.coords); if (currentLocation.coords) { cameraController.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) { await getLocation(); } else if (!canAskAgain || !isServicesEnabled) { setOpenSettingsVisible(true); } }; const handleOpenEditModal = () => { navigation.navigate(NAVIGATION_PAGES.TRIPS_2025, { region: { id: regionData.id, name: regionData.name } }); }; 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') { const isVisitedLocal = regionsVisited.includes(+id); setUserData({ type: 'nm', id, visited: isVisitedLocal }); if (token) { await mutateUserData( { region_id: id, token: String(token) }, { onSuccess: (data) => { setUserData({ type: 'nm', id, ...data }); } } ); } else { setUserData({ type: 'nm', id }); } if (regionsList && regionsList.data) { const region = regionsList.data.find((region) => region.id === +id); if (region && region.bbox) { const bounds = turf.bbox(region.bbox); if (bounds.every(isValidNumber)) { cameraController.fitBounds( [bounds[2], bounds[3]], [bounds[0], bounds[1]], [10, 10, 50, 10], 1000 ); } } } } else if (type === 'countries') { const isVisitedLocal = countriesVisited.includes(+id); setUserData({ type: 'countries', id, visited: isVisitedLocal }); if (token) { await mutateCountriesData( { id, token }, { onSuccess: (data) => { setUserData({ type: 'countries', id, ...data.data }); } } ); } else { setUserData({ type: 'countries', id }); } if (countriesList && countriesList.data) { const region = countriesList.data.find((region) => region.id === +id); if (region && region.bbox) { const bounds = turf.bbox(region.bbox); if (bounds.every(isValidNumber)) { cameraController.fitBounds( [bounds[2], bounds[3]], [bounds[0], bounds[1]], [10, 10, 50, 10], 1000 ); } } } } else { const isVisitedLocal = dareVisited.includes(+id); setUserData({ type: 'dare', id: +id, visited: isVisitedLocal }); if (token) { await mutateUserDataDare( { dare_id: +id, token: String(token) }, { onSuccess: (data) => { setUserData({ type: 'dare', id: +id, ...data }); } } ); } else { setUserData({ type: 'dare', id: +id }); } if (dareList && dareList.data) { const region = dareList.data.find((region) => region.id === +id); if (region && region.bbox) { const bounds = turf.bbox(region.bbox); if (bounds.every(isValidNumber)) { cameraController.fitBounds( [bounds[2], bounds[3]], [bounds[0], bounds[1]], [10, 10, 50, 10], 1000 ); } } } } } else { handleClosePopup(); } }; const handleFindSeries = async (seriesItem: any) => { let coordinates = null; try { if (seriesItem.location_geojson) { const geojsonObj = JSON.parse(seriesItem.location_geojson); if (geojsonObj.type === 'Point') { const parsedCoords = geojsonObj.coordinates; if (parsedCoords && parsedCoords.length === 2) { coordinates = [parsedCoords[1], parsedCoords[0]]; } } else if ( geojsonObj.type === 'Polygon' || geojsonObj.type === 'MultiPolygon' || geojsonObj.type === 'GeometryCollection' ) { try { const center = turf.centroid(geojsonObj); if (center && center.geometry && center.geometry.coordinates) { const parsedCoords = center.geometry.coordinates; coordinates = [parsedCoords[1], parsedCoords[0]]; } } catch (err) { console.error('Error finding centroid for series', err); let firstPoint; if (geojsonObj.type === 'Polygon' && geojsonObj.coordinates?.[0]?.[0]) { firstPoint = geojsonObj.coordinates[0][0]; } else if (geojsonObj.type === 'MultiPolygon' && geojsonObj.coordinates?.[0]?.[0]?.[0]) { firstPoint = geojsonObj.coordinates[0][0][0]; } else if (geojsonObj.type === 'GeometryCollection' && geojsonObj.geometries?.[0]) { const firstGeom = geojsonObj.geometries[0]; if (firstGeom.type === 'Point' && firstGeom.coordinates) { firstPoint = firstGeom.coordinates; } else if (firstGeom.type === 'Polygon' && firstGeom.coordinates?.[0]?.[0]) { firstPoint = firstGeom.coordinates[0][0]; } } if (firstPoint && firstPoint.length >= 2) { coordinates = [firstPoint[1], firstPoint[0]]; } } } } } catch (e) { console.error('Error parsing coordinates for series', e); } if (coordinates) { setTimeout(() => { cameraController.setCamera({ centerCoordinate: coordinates, zoomLevel: 10, animationDuration: 1000, animationMode: 'flyTo' }); }, 800); handledSeriesShareRef.current = seriesItem.item_id; setSelectedMarker({ coordinates, name: seriesItem.item_name, icon: seriesItem.series_icon ? { uri: API_HOST + seriesItem.series_icon } : null, description: seriesItem.series_name, series_name: seriesItem.series_name, visited: seriesItem.visited, series_id: seriesItem.series_id, id: seriesItem.item_id, avatars: null, no_visited: 0 }); setSelectedUser(null); setTimeout(async () => { try { if (!mapRef.current) return; const pointInView = await mapRef.current.getPointInView(coordinates); if (pointInView) { const { features } = await mapRef.current.queryRenderedFeaturesAtPoint( pointInView, undefined, [] ); if (features && features.length > 0) { const feature = features.find( (f: any) => f.properties && f.properties.id === seriesItem.item_id ); if (feature) { feature.properties.override_visited = seriesItem.visited; handleMarkerPress({ features: [feature] }); } } } } catch (e) { console.log('Failed to query map features for series', e); } }, 1500); } }; const handleMarkerPress = async (event: any) => { const { features } = event; if (features?.length) { const uniqueFeatures = features ? Array.from(new Map(features.map((f: any) => [f.properties.id, f])).values()) : []; if (uniqueFeatures.length > 1) { const markers = uniqueFeatures .map((f: any) => { const markerCoordinates = f.geometry.coordinates; if (!markerCoordinates) return null; return { coordinates: markerCoordinates, name: f.properties.name, icon: images[f.properties.series_id], description: f.properties.description, series_name: f.properties.series_name, visited: f.properties.override_visited !== undefined ? f.properties.override_visited : (seriesVisited.includes(f.properties.id) ? 1 : 0), series_id: f.properties.series_id, id: f.properties.id, avatars: f.properties.avatars, no_visited: f.properties.no_visited }; }) .filter(Boolean) .sort((a: any, b: any) => a.visited - b.visited); SheetManager.show('multiple-series-modal', { payload: { markers, token, toggleSeries, setSelectedMarker, setIsWarningModalVisible, setPremiumModalVisible } as any }); setSelectedUser(null); return; } const selectedFeature = features[0]; const { coordinates } = selectedFeature.geometry; const { name, description, series_name, series_id, id, avatars, no_visited, override_visited } = selectedFeature.properties; const visited = override_visited !== undefined ? override_visited : (seriesVisited.includes(id) ? 1 : 0); const icon = images[series_id]; if (coordinates) { setSelectedMarker((prev: any) => { const isSameMarker = prev && prev.id === id; const currentVisited = isSameMarker ? prev.visited : visited; return { coordinates, name, icon: icon || (isSameMarker ? prev.icon : null), description, series_name, visited: currentVisited, series_id, id, avatars, no_visited }; }); 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 && current.id === item.id ? { ...current, visited: 0 } : current ); } else { setSeriesVisited((current) => [...current, item.id]); setSelectedMarker((current: any) => current && current.id === item.id ? { ...current, visited: 1 } : current ); } } 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 && ( )} <> <> <> {selectedRegion && type && ( <> )} {/* {didFinishLoadingStyle && seriesGroups?.length > 0 && seriesGroups.map((group) => { const visible = isGroupEnabled(group.id); const mustVisible = seriesFilter.visible && (visible || isMustModeEnabled); return ( {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} ); })} */} {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} { 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; cameraController.setCamera({ centerCoordinate: clusterCoordinates, zoomLevel: newZoom, animationDuration: 500, animationMode: 'flyTo' }); return; } else { handleUserPress(event); } }} cluster={true} clusterRadius={50} > {selectedUser && } {selectedMarker && ( )} {(renderCamera || Platform.OS === 'ios') && ( )} {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' }); }} > {/* 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} onAddVisitSuccess={(id, isFuture) => { if (!isFuture && !regionsVisited.includes(id)) { setRegionsVisited([...regionsVisited, id]); } refetchVisitedCountries(); }} /> ) : ( <> {!isExpanded ? ( navigation.navigate(NAVIGATION_PAGES.PROFILE_TAB)} > {token ? ( userInfoData?.avatar ? ( ) ) : ( )} ) : null} {isExpanded ? ( <> setSearchInput(text)} onSubmitEditing={handleSearch} /> ) : ( )} { try { filterModalRef.current?.openFilter('regions'); closeCallout(); } catch (error) { console.error('Error opening filter:', error); } }} icon={TravelsIcon} text={ tilesType.value === 0 || tilesType.value === 1 ? token ? `${tilesType.value === 0 ? 'NM' : 'UN'} ${regionsFilter.visitedLabel} ${regionsFilter.year}` : `${tilesType.value === 0 ? 'NM' : 'UN'}` : tilesType.value === 2 ? 'DARE' : 'Travels' } active={type !== 'blank'} /> { try { filterModalRef.current?.openFilter('series'); closeCallout(); } catch (error) { console.error('Error opening filter:', error); } }} icon={SeriesIcon} text="Series" active={seriesFilter.visible} /> {token ? ( { try { filterModalRef.current?.openFilter('nomads'); closeCallout(); } catch (error) { console.error('Error opening filter:', error); } }} icon={NomadsIcon} text="Nomads" active={showNomads || nomadsFilter.friends || nomadsFilter.countries?.length} > {usersCount && usersCount > 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?" /> {}} onClose={() => setPremiumModalVisible(false)} title={'Premium Feature'} /> ); }; export default MapScreen;