|
@@ -2,12 +2,16 @@ import {
|
|
|
View,
|
|
|
Platform,
|
|
|
TouchableOpacity,
|
|
|
- Text
|
|
|
+ Text,
|
|
|
+ Linking,
|
|
|
+ Animated,
|
|
|
} from 'react-native';
|
|
|
import React, { useEffect, useState, useRef, useMemo } from 'react';
|
|
|
-import MapView, { UrlTile, Geojson } from 'react-native-maps';
|
|
|
+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';
|
|
@@ -20,12 +24,36 @@ import dareRegions from '../../../../assets/geojson/mqp.json';
|
|
|
|
|
|
import NetInfo from "@react-native-community/netinfo";
|
|
|
import { getFirstDatabase, getSecondDatabase } from '../../../db';
|
|
|
-import RegionPopup from '../../../components/RegionPopup';
|
|
|
+import { RegionPopup, LocationPopup } from '../../../components';
|
|
|
|
|
|
-import { BottomTabNavigationProp } from '@react-navigation/bottom-tabs';
|
|
|
import { styles } from './style';
|
|
|
-import { findRegionInDataset, calculateMapRegion } from '../../../utils/mapHelpers';
|
|
|
+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';
|
|
|
+
|
|
|
+let userId: string | null = '';
|
|
|
+let token: string | null = '';
|
|
|
+storageGet('id').then((data) => (userId = data));
|
|
|
+storageGet('token').then((data) => (token = data));
|
|
|
|
|
|
const tilesBaseURL = 'https://maps.nomadmania.com/tiles_osm';
|
|
|
const localTileDir = `${FileSystem.cacheDirectory}tiles`;
|
|
@@ -33,36 +61,57 @@ const localTileDir = `${FileSystem.cacheDirectory}tiles`;
|
|
|
const gridUrl = 'https://maps.nomadmania.com/tiles_nm/grid';
|
|
|
const localGridDir = `${FileSystem.cacheDirectory}tiles/grid`;
|
|
|
|
|
|
-const visitedTiles = 'https://maps.nomadmania.com/tiles_nm/user_visited/51363';
|
|
|
+const visitedTiles = `https://maps.nomadmania.com/tiles_nm/user_visited/${userId}`;
|
|
|
const localVisitedDir = `${FileSystem.cacheDirectory}tiles/user_visited`;
|
|
|
|
|
|
const dareTiles = 'https://maps.nomadmania.com/tiles_nm/regions_mqp';
|
|
|
const localDareDir = `${FileSystem.cacheDirectory}tiles/regions_mqp`;
|
|
|
|
|
|
-interface Region {
|
|
|
- id: number;
|
|
|
- name: string;
|
|
|
- region_photos: string;
|
|
|
- visitors_count: number;
|
|
|
-}
|
|
|
-
|
|
|
-interface MapScreenProps {
|
|
|
- navigation: BottomTabNavigationProp<any>;
|
|
|
-}
|
|
|
+const AnimatedMarker = Animated.createAnimatedComponent(Marker);
|
|
|
|
|
|
const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
const mapRef = useRef<MapView>(null);
|
|
|
|
|
|
const [isConnected, setIsConnected] = useState<boolean | null>(true);
|
|
|
- const [selectedRegion, setSelectedRegion] = useState(null);
|
|
|
- const [popupVisible, setPopupVisible] = useState<boolean | null>(false);
|
|
|
+ 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 [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: popupVisible ? 'none' : 'flex',
|
|
|
+ display: regionPopupVisible ? 'none' : 'flex',
|
|
|
position: 'absolute',
|
|
|
...Platform.select({
|
|
|
android: {
|
|
@@ -71,7 +120,137 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
}),
|
|
|
}
|
|
|
});
|
|
|
- }, [popupVisible, navigation]);
|
|
|
+ }, [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 <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);
|
|
@@ -79,6 +258,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
};
|
|
|
|
|
|
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([]);
|
|
@@ -112,7 +292,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
|
|
|
await getData(db, id, tableName, handleRegionData)
|
|
|
.then(() => {
|
|
|
- setPopupVisible(true);
|
|
|
+ setRegionPopupVisible(true);
|
|
|
})
|
|
|
.catch(error => {
|
|
|
console.error("Error fetching data", error);
|
|
@@ -122,19 +302,20 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
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();
|
|
|
}
|
|
|
};
|
|
|
|
|
|
- useEffect(() => {
|
|
|
- const unsubscribe = NetInfo.addEventListener(state => {
|
|
|
- setIsConnected(state.isConnected);
|
|
|
- });
|
|
|
-
|
|
|
- return () => unsubscribe();
|
|
|
- }, []);
|
|
|
-
|
|
|
const renderMapTiles = (
|
|
|
url: string,
|
|
|
cacheDir: string,
|
|
@@ -159,7 +340,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
|
|
|
return (
|
|
|
<Geojson
|
|
|
- geojson={selectedRegion}
|
|
|
+ geojson={selectedRegion as any}
|
|
|
fillColor="rgba(57, 115, 172, 0.2)"
|
|
|
strokeColor="#3973ac"
|
|
|
strokeWidth={Platform.OS == 'android' ? 3 : 2}
|
|
@@ -168,9 +349,21 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
);
|
|
|
};
|
|
|
|
|
|
- const handleClosePopup = () => {
|
|
|
- setPopupVisible(false);
|
|
|
+ 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]);
|
|
@@ -185,18 +378,40 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
onPress={handleMapPress}
|
|
|
style={styles.map}
|
|
|
mapType={Platform.OS == 'android' ? 'none' : 'standard'}
|
|
|
- offlineMode={!isConnected}
|
|
|
maxZoomLevel={15}
|
|
|
minZoomLevel={0}
|
|
|
+ onRegionChangeComplete={findFeaturesInVisibleMapArea}
|
|
|
>
|
|
|
{renderedGeoJSON}
|
|
|
{renderMapTiles(tilesBaseURL, localTileDir, 1)}
|
|
|
{renderMapTiles(gridUrl, localGridDir, 2)}
|
|
|
- {renderMapTiles(visitedTiles, localVisitedDir, 2, 0.5)}
|
|
|
+ {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>
|
|
|
|
|
|
- {popupVisible && regionData ? (
|
|
|
+ <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 ]}
|
|
@@ -210,7 +425,10 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
<Text style={styles.textClose}>Close</Text>
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
- <TouchableOpacity style={[ styles.cornerButton, styles.topRightButton, styles.bottomButton ]}>
|
|
|
+ <TouchableOpacity
|
|
|
+ onPress={handleGetLocation}
|
|
|
+ style={[ styles.cornerButton, styles.topRightButton, styles.bottomButton ]}
|
|
|
+ >
|
|
|
<LocationIcon />
|
|
|
</TouchableOpacity>
|
|
|
|
|
@@ -234,7 +452,10 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
<RadarIcon />
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
- <TouchableOpacity style={[styles.cornerButton, styles.bottomButton, styles.bottomRightButton]}>
|
|
|
+ <TouchableOpacity
|
|
|
+ onPress={handleGetLocation}
|
|
|
+ style={[styles.cornerButton, styles.bottomButton, styles.bottomRightButton]}
|
|
|
+ >
|
|
|
<LocationIcon />
|
|
|
</TouchableOpacity>
|
|
|
</>
|