import { View, Platform, TouchableOpacity, Text, Linking, Animated, } from 'react-native'; import React, { useEffect, useState, useRef, useMemo } from 'react'; import MapView, { UrlTile, Geojson, Marker } from 'react-native-maps'; import * as turf from '@turf/turf'; import * as FileSystem from 'expo-file-system'; import * as Location from 'expo-location'; import { storageGet } 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 NetInfo from "@react-native-community/netinfo"; import { getFirstDatabase, getSecondDatabase } from '../../../db'; import { RegionPopup, LocationPopup } from '../../../components'; import { styles } from './style'; import { findRegionInDataset, calculateMapRegion, clusterMarkers, filterCandidates, processMarkerData, processIconUrl, filterCandidatesMarkers } from '../../../utils/mapHelpers'; import { getData } from '../../../modules/map/regionData'; import { fetchSeriesData } from '../../../modules/map/series/queries/use-post-get-series'; import MarkerItem from './MarkerItem'; import ClusterItem from './ClusterItem'; import { Region, Series, ItemSeries, MarkerData, ClusterData, MapScreenProps, FeatureCollection } from '../../../types/map'; const MAP_HOST ='https://maps.nomadmania.eu'; const tilesBaseURL = `${MAP_HOST}/tiles_osm`; const localTileDir = `${FileSystem.cacheDirectory}tiles`; 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, setUserId] = useState(''); const [token, setToken] = useState(''); 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 [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(() => { const fetchData = async () => { const fetchedUserId = await storageGet('uid'); const fetchedToken = await storageGet('token'); setUserId(fetchedUserId); setToken(fetchedToken); }; fetchData(); }, []); 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(() => { const unsubscribe = NetInfo.addEventListener(state => { setIsConnected(state.isConnected); }); return () => unsubscribe(); }, []); 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; }) => { 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); const candidatesMarkers = await fetchSeriesData(token, JSON.stringify(regionIds)); if (thisToken !== currentTokenRef.current) return; setSeries(candidatesMarkers.series); const markersVisible = filterCandidatesMarkers(candidatesMarkers.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); }); const bounds = turf.bbox(foundRegion); const region = calculateMapRegion(bounds); mapRef.current?.animateToRegion(region, 1000); if(tableName === 'regions') { const seriesData = await fetchSeriesData(token, JSON.stringify([id])); setSeries(seriesData.series); const allMarkers = seriesData.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 console.log('Mark as visited')} /> ) : ( <> )} ); } export default MapScreen;