import { Animated, Linking, Platform, Text, TouchableOpacity, View } from 'react-native'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import MapView, { Geojson, Marker, UrlTile } from 'react-native-maps'; import * as turf from '@turf/turf'; import * as FileSystem from 'expo-file-system'; import * as Location from 'expo-location'; import { storage, StoreType } from '../../../storage'; import MenuIcon from '../../../../assets/icons/menu.svg'; import SearchIcon from '../../../../assets/icons/search.svg'; import RadarIcon from '../../../../assets/icons/radar.svg'; import LocationIcon from '../../../../assets/icons/location.svg'; import CloseSvg from '../../../../assets/icons/close.svg'; import regions from '../../../../assets/geojson/nm2022.json'; import dareRegions from '../../../../assets/geojson/mqp.json'; import { getFirstDatabase, getSecondDatabase, refreshDatabases } from '../../../db'; import { LocationPopup, RegionPopup, WarningModal } from '../../../components'; import { styles } from './style'; import { calculateMapRegion, clusterMarkers, filterCandidates, filterCandidatesMarkers, findRegionInDataset, processIconUrl, processMarkerData } from '../../../utils/mapHelpers'; import { getData } from '../../../modules/map/regionData'; import { fetchSeriesData } from '@api/series'; import MarkerItem from './MarkerItem'; import ClusterItem from './ClusterItem'; import { ClusterData, FeatureCollection, ItemSeries, MapScreenProps, MarkerData, Region, Series } from '../../../types/map'; import { MAP_HOST } from 'src/constants'; import { useConnection } from 'src/contexts/ConnectionContext'; const tilesBaseURL = `${MAP_HOST}/tiles_osm`; const localTileDir = `${FileSystem.cacheDirectory}tiles/background`; const gridUrl = `${MAP_HOST}/tiles_nm/grid`; const localGridDir = `${FileSystem.cacheDirectory}tiles/grid`; const localVisitedDir = `${FileSystem.cacheDirectory}tiles/user_visited`; const dareTiles = `${MAP_HOST}/tiles_nm/regions_mqp`; const localDareDir = `${FileSystem.cacheDirectory}tiles/regions_mqp`; const AnimatedMarker = Animated.createAnimatedComponent(Marker); const MapScreen: React.FC = ({ navigation }) => { const userId = storage.get('uid', StoreType.STRING); const token = storage.get('token', StoreType.STRING); const netInfo = useConnection(); const { mutateAsync } = fetchSeriesData(); const visitedTiles = `${MAP_HOST}/tiles_nm/user_visited/${userId}`; const mapRef = useRef(null); const [isConnected, setIsConnected] = useState(true); const [selectedRegion, setSelectedRegion] = useState(null); const [regionPopupVisible, setRegionPopupVisible] = useState(false); const [regionData, setRegionData] = useState(null); const [userAvatars, setUserAvatars] = useState([]); const [location, setLocation] = useState(null); const [askLocationVisible, setAskLocationVisible] = useState(false); const [openSettingsVisible, setOpenSettingsVisible] = useState(false); const [isWarningModalVisible, setIsWarningModalVisible] = useState(false); const [markers, setMarkers] = useState([]); const [clusters, setClusters] = useState(null); const [series, setSeries] = useState(null); const [processedMarkers, setProcessedMarkers] = useState([]); const cancelTokenRef = useRef(false); const currentTokenRef = useRef(0); const strokeWidthAnim = useRef(new Animated.Value(2)).current; useEffect(() => { if (netInfo?.isInternetReachable) { setIsConnected(true); } else { setIsConnected(false); } }, [netInfo?.isInternetReachable]); useEffect(() => { Animated.loop( Animated.sequence([ Animated.timing(strokeWidthAnim, { toValue: 3, duration: 700, useNativeDriver: false }), Animated.timing(strokeWidthAnim, { toValue: 2, duration: 700, useNativeDriver: false }) ]) ).start(); }, [strokeWidthAnim]); useEffect(() => { navigation.setOptions({ tabBarStyle: { display: regionPopupVisible ? 'none' : 'flex', position: 'absolute', ...Platform.select({ android: { height: 58 } }) } }); }, [regionPopupVisible, navigation]); useEffect(() => { (async () => { let { status } = await Location.getForegroundPermissionsAsync(); if (status !== 'granted') { return; } let currentLocation = await Location.getCurrentPositionAsync({}); setLocation(currentLocation.coords); mapRef.current?.animateToRegion( { latitude: currentLocation.coords.latitude, longitude: currentLocation.coords.longitude, latitudeDelta: 5, longitudeDelta: 5 }, 1000 ); })(); }, []); const findFeaturesInVisibleMapArea = async (visibleMapArea: { latitude?: any; longitude?: any; latitudeDelta: any; longitudeDelta?: any; }) => { if (!isConnected) return; const currentZoom = Math.log2(360 / visibleMapArea.latitudeDelta); if (cancelTokenRef.current) { const clusteredMarkers = clusterMarkers(processedMarkers, currentZoom, setClusters); setMarkers(clusteredMarkers as MarkerData[]); return; } const thisToken = ++currentTokenRef.current; if (!regions || !dareRegions) return; if (currentZoom < 7) { setMarkers([]); return; } const { latitude, longitude, latitudeDelta, longitudeDelta } = visibleMapArea; const bbox: turf.BBox = [ longitude - longitudeDelta / 2, latitude - latitudeDelta / 2, longitude + longitudeDelta / 2, latitude + latitudeDelta / 2 ]; const visibleAreaPolygon = turf.bboxPolygon(bbox); const regionsFound = filterCandidates(regions, bbox); // const daresFound = filterCandidates(dareRegions, bbox); const regionIds = regionsFound.map( (region: { properties: { id: any } }) => region.properties.id ); isConnected && await mutateAsync( { regions: JSON.stringify(regionIds), token: String(token) }, { onSuccess: (data) => { if (thisToken !== currentTokenRef.current) return; setSeries(data.series); const markersVisible = filterCandidatesMarkers(data.items, visibleAreaPolygon); const allMarkers = markersVisible.map(processMarkerData); const clusteredMarkers = clusterMarkers(allMarkers, currentZoom, setClusters); setMarkers(clusteredMarkers as MarkerData[]); } } ); }; const renderMarkers = () => { if (!markers.length) return null; const singleMarkers = markers.filter((feature) => { return feature.properties.dbscan !== 'core'; }); return ( <> {singleMarkers.map((marker, idx) => { const markerSeries = series?.find((s) => s.id === marker.properties.series_id); const iconUrl = markerSeries ? processIconUrl(markerSeries.icon) : 'default_icon_url'; return ; })} {clusters && Object.entries(clusters).map(([clusterId, data], idx) => ( ))} ); }; const handleGetLocation = async () => { let { status, canAskAgain } = await Location.getForegroundPermissionsAsync(); if (status === 'granted') { getLocation(); } else if (!canAskAgain) { setOpenSettingsVisible(true); } else { setAskLocationVisible(true); } }; const getLocation = async () => { let currentLocation = await Location.getCurrentPositionAsync(); setLocation(currentLocation.coords); mapRef.current?.animateToRegion( { latitude: currentLocation.coords.latitude, longitude: currentLocation.coords.longitude, latitudeDelta: 5, longitudeDelta: 5 }, 800 ); handleClosePopup(); }; const handleAcceptPermission = async () => { setAskLocationVisible(false); let { status, canAskAgain } = await Location.requestForegroundPermissionsAsync(); if (status === 'granted') { getLocation(); } else if (!canAskAgain) { setOpenSettingsVisible(true); } }; const handleRegionData = (regionData: Region, avatars: string[]) => { setRegionData(regionData); setUserAvatars(avatars); }; const handleMapPress = async (event: { nativeEvent: { coordinate: { latitude: any; longitude: any } }; }) => { cancelTokenRef.current = true; const { latitude, longitude } = event.nativeEvent.coordinate; const point = turf.point([longitude, latitude]); setUserAvatars([]); let db = getSecondDatabase(); let tableName = 'places'; let foundRegion = findRegionInDataset(dareRegions, point); if (!foundRegion) { foundRegion = findRegionInDataset(regions, point); db = getFirstDatabase(); tableName = 'regions'; } if (foundRegion) { const id = foundRegion.properties?.id; setSelectedRegion({ type: 'FeatureCollection', features: [ { geometry: foundRegion.geometry, properties: { ...foundRegion.properties, fill: 'rgba(57, 115, 172, 0.2)', stroke: '#3973AC' }, type: 'Feature' } ] }); await getData(db, id, tableName, handleRegionData) .then(() => { setRegionPopupVisible(true); }) .catch((error) => { console.error('Error fetching data', error); refreshDatabases(); }); const bounds = turf.bbox(foundRegion); const region = calculateMapRegion(bounds); mapRef.current?.animateToRegion(region, 1000); if (tableName === 'regions') { await mutateAsync( { regions: JSON.stringify([id]), token: String(token) }, { onSuccess: (data) => { setSeries(data.series); const allMarkers = data.items.map(processMarkerData); setProcessedMarkers(allMarkers); } } ); } else { setProcessedMarkers([]); } } else { handleClosePopup(); } }; const renderMapTiles = (url: string, cacheDir: string, zIndex: number, opacity = 1) => ( ); function renderGeoJSON() { if (!selectedRegion) return null; return ( ); } const handleClosePopup = async () => { cancelTokenRef.current = false; setRegionPopupVisible(false); setMarkers([]); setSelectedRegion(null); const boundaries = await mapRef.current?.getMapBoundaries(); const { northEast, southWest } = boundaries || {}; const latitudeDelta = (northEast?.latitude ?? 0) - (southWest?.latitude ?? 0); const longitudeDelta = (northEast?.longitude ?? 0) - (southWest?.longitude ?? 0); const latitude = (southWest?.latitude ?? 0) + latitudeDelta / 2; const longitude = (southWest?.longitude ?? 0) + longitudeDelta / 2; findFeaturesInVisibleMapArea({ latitude, longitude, latitudeDelta, longitudeDelta }); }; const renderedGeoJSON = useMemo(() => renderGeoJSON(), [selectedRegion]); return ( {renderedGeoJSON} {renderMapTiles(tilesBaseURL, localTileDir, 1)} {renderMapTiles(gridUrl, localGridDir, 2)} {userId && renderMapTiles(visitedTiles, localVisitedDir, 2, 0.5)} {renderMapTiles(dareTiles, localDareDir, 2, 0.5)} {location && ( )} {markers && renderMarkers()} setAskLocationVisible(false)} onAccept={handleAcceptPermission} modalText="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)} onAccept={() => Platform.OS === 'ios' ? Linking.openURL('app-settings:') : Linking.openSettings() } modalText="NomadMania app needs location permissions to function properly. Open settings?" /> {regionPopupVisible && regionData ? ( <> Close { if (!token) { setIsWarningModalVisible(true); } else { console.log('Mark as visited'); } }} /> ) : ( <> )} setIsWarningModalVisible(false)} /> ); }; export default MapScreen;