123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498 |
- 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 { getOnlineStatus, 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 NetInfo from '@react-native-community/netinfo';
- import { getFirstDatabase, getSecondDatabase } 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';
- 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<MapScreenProps> = ({ navigation }) => {
- const userId = storage.get('uid', StoreType.STRING);
- const token = storage.get('token', StoreType.STRING);
- const isOnline = getOnlineStatus();
- const { mutateAsync } = fetchSeriesData();
- const visitedTiles = `${MAP_HOST}/tiles_nm/user_visited/${userId}`;
- const mapRef = useRef<MapView>(null);
- const [isConnected, setIsConnected] = useState<boolean | null>(true);
- const [selectedRegion, setSelectedRegion] = useState<FeatureCollection | null>(null);
- const [regionPopupVisible, setRegionPopupVisible] = useState<boolean | null>(false);
- const [regionData, setRegionData] = useState<Region | null>(null);
- const [userAvatars, setUserAvatars] = useState<string[]>([]);
- const [location, setLocation] = useState<Location.LocationObjectCoords | null>(null);
- const [askLocationVisible, setAskLocationVisible] = useState<boolean>(false);
- const [openSettingsVisible, setOpenSettingsVisible] = useState<boolean>(false);
- const [isWarningModalVisible, setIsWarningModalVisible] = useState<boolean>(false);
- const [markers, setMarkers] = useState<MarkerData[]>([]);
- const [clusters, setClusters] = useState<ClusterData | null>(null);
- const [series, setSeries] = useState<Series[] | null>(null);
- const [processedMarkers, setProcessedMarkers] = useState<ItemSeries[]>([]);
- const cancelTokenRef = useRef(false);
- const currentTokenRef = useRef(0);
- const strokeWidthAnim = useRef(new Animated.Value(2)).current;
- 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(isOnline as boolean);
- });
- return () => unsubscribe();
- }, [isOnline]);
- 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 (!isOnline) 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
- );
- isOnline && 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 <MarkerItem marker={marker} iconUrl={iconUrl} key={idx} />;
- })}
- {clusters &&
- Object.entries(clusters).map(([clusterId, data], idx) => (
- <ClusterItem clusterId={clusterId} data={data} key={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') {
- 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) => (
- <UrlTile
- urlTemplate={`${url}/{z}/{x}/{y}`}
- maximumZ={15}
- maximumNativeZ={13}
- tileCachePath={`${cacheDir}`}
- shouldReplaceMapContent
- minimumZ={0}
- offlineMode={!isConnected}
- opacity={opacity}
- zIndex={zIndex}
- />
- );
- function renderGeoJSON() {
- if (!selectedRegion) return null;
- return (
- <Geojson
- geojson={selectedRegion as any}
- fillColor="rgba(57, 115, 172, 0.2)"
- strokeColor="#3973ac"
- strokeWidth={Platform.OS == 'android' ? 3 : 2}
- zIndex={3}
- />
- );
- }
- 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 (
- <View style={styles.container}>
- <MapView
- ref={mapRef}
- showsMyLocationButton={false}
- showsCompass={false}
- zoomControlEnabled={false}
- onPress={handleMapPress}
- style={styles.map}
- mapType={Platform.OS == 'android' ? 'none' : 'standard'}
- maxZoomLevel={15}
- minZoomLevel={0}
- onRegionChangeComplete={findFeaturesInVisibleMapArea}
- >
- {renderedGeoJSON}
- {renderMapTiles(tilesBaseURL, localTileDir, 1)}
- {renderMapTiles(gridUrl, localGridDir, 2)}
- {userId && renderMapTiles(visitedTiles, localVisitedDir, 2, 0.5)}
- {renderMapTiles(dareTiles, localDareDir, 2, 0.5)}
- {location && (
- <AnimatedMarker coordinate={location} anchor={{ x: 0.5, y: 0.5 }}>
- <Animated.View style={[styles.location, { borderWidth: strokeWidthAnim }]} />
- </AnimatedMarker>
- )}
- {markers && renderMarkers()}
- </MapView>
- <LocationPopup
- visible={askLocationVisible}
- onClose={() => 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."
- />
- <LocationPopup
- visible={openSettingsVisible}
- onClose={() => 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 ? (
- <>
- <TouchableOpacity
- style={[styles.cornerButton, styles.topLeftButton, styles.closeLeftButton]}
- onPress={handleClosePopup}
- >
- <CloseSvg fill="white" width={13} height={13} />
- <Text style={styles.textClose}>Close</Text>
- </TouchableOpacity>
- <TouchableOpacity
- onPress={handleGetLocation}
- style={[styles.cornerButton, styles.topRightButton, styles.bottomButton]}
- >
- <LocationIcon />
- </TouchableOpacity>
- <RegionPopup
- region={regionData}
- userAvatars={userAvatars}
- onMarkVisited={() => {
- if (!token) {
- setIsWarningModalVisible(true);
- } else {
- console.log('Mark as visited');
- }
- }}
- />
- </>
- ) : (
- <>
- <TouchableOpacity style={[styles.cornerButton, styles.topLeftButton]}>
- <MenuIcon />
- </TouchableOpacity>
- <TouchableOpacity style={[styles.cornerButton, styles.topRightButton]}>
- <SearchIcon fill={'#0F3F4F'} />
- </TouchableOpacity>
- <TouchableOpacity
- style={[styles.cornerButton, styles.bottomButton, styles.bottomLeftButton]}
- >
- <RadarIcon />
- </TouchableOpacity>
- <TouchableOpacity
- onPress={handleGetLocation}
- style={[styles.cornerButton, styles.bottomButton, styles.bottomRightButton]}
- >
- <LocationIcon />
- </TouchableOpacity>
- </>
- )}
- <WarningModal
- type={'unauthorized'}
- isVisible={isWarningModalVisible}
- onClose={() => setIsWarningModalVisible(false)}
- />
- </View>
- );
- };
- export default MapScreen;
|