|
@@ -1,5 +1,5 @@
|
|
|
-import { Animated, Linking, Platform, Text, TouchableOpacity, View } from 'react-native';
|
|
|
-import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
+import { Animated, Linking, Platform, Text, TouchableOpacity, View, Image } from 'react-native';
|
|
|
+import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react';
|
|
|
import MapView, { Geojson, Marker, UrlTile } from 'react-native-maps';
|
|
|
import * as turf from '@turf/turf';
|
|
|
import * as FileSystem from 'expo-file-system';
|
|
@@ -21,7 +21,6 @@ import { LocationPopup, RegionPopup, WarningModal } from '../../../components';
|
|
|
import { styles } from './style';
|
|
|
import {
|
|
|
calculateMapRegion,
|
|
|
- clusterMarkers,
|
|
|
filterCandidates,
|
|
|
filterCandidatesMarkers,
|
|
|
findRegionInDataset,
|
|
@@ -29,20 +28,13 @@ import {
|
|
|
processMarkerData
|
|
|
} from '../../../utils/mapHelpers';
|
|
|
import { getData } from '../../../modules/map/regionData';
|
|
|
-import { fetchSeriesData } from '@api/series';
|
|
|
+import { fetchSeriesData, usePostSetToggleItem } from '@api/series';
|
|
|
import MarkerItem from './MarkerItem';
|
|
|
import ClusterItem from './ClusterItem';
|
|
|
-import {
|
|
|
- ClusterData,
|
|
|
- FeatureCollection,
|
|
|
- ItemSeries,
|
|
|
- MapScreenProps,
|
|
|
- MarkerData,
|
|
|
- Region,
|
|
|
- Series
|
|
|
-} from '../../../types/map';
|
|
|
+import { FeatureCollection, ItemSeries, MapScreenProps, Region, Series } from '../../../types/map';
|
|
|
import { MAP_HOST } from 'src/constants';
|
|
|
import { useConnection } from 'src/contexts/ConnectionContext';
|
|
|
+import ClusteredMapView from 'react-native-map-clustering';
|
|
|
|
|
|
const tilesBaseURL = `${MAP_HOST}/tiles_osm`;
|
|
|
const localTileDir = `${FileSystem.cacheDirectory}tiles/background`;
|
|
@@ -59,13 +51,12 @@ 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 token = storage.get('token', StoreType.STRING) as string;
|
|
|
const netInfo = useConnection();
|
|
|
|
|
|
const { mutateAsync } = fetchSeriesData();
|
|
|
-
|
|
|
+ const { mutate: updateSeriesItem } = usePostSetToggleItem();
|
|
|
const visitedTiles = `${MAP_HOST}/tiles_nm/user_visited/${userId}`;
|
|
|
-
|
|
|
const mapRef = useRef<MapView>(null);
|
|
|
|
|
|
const [isConnected, setIsConnected] = useState<boolean | null>(true);
|
|
@@ -78,10 +69,10 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
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 [markers, setMarkers] = useState<ItemSeries[]>([]);
|
|
|
const [series, setSeries] = useState<Series[] | null>(null);
|
|
|
const [processedMarkers, setProcessedMarkers] = useState<ItemSeries[]>([]);
|
|
|
+ const [zoomLevel, setZoomLevel] = useState<number>(0);
|
|
|
|
|
|
const cancelTokenRef = useRef(false);
|
|
|
const currentTokenRef = useRef(0);
|
|
@@ -156,11 +147,10 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
}) => {
|
|
|
if (!isConnected) return;
|
|
|
const currentZoom = Math.log2(360 / visibleMapArea.latitudeDelta);
|
|
|
+ setZoomLevel(currentZoom);
|
|
|
|
|
|
if (cancelTokenRef.current) {
|
|
|
- const clusteredMarkers = clusterMarkers(processedMarkers, currentZoom, setClusters);
|
|
|
- setMarkers(clusteredMarkers as MarkerData[]);
|
|
|
-
|
|
|
+ setMarkers(processedMarkers);
|
|
|
return;
|
|
|
}
|
|
|
const thisToken = ++currentTokenRef.current;
|
|
@@ -199,37 +189,12 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
|
|
|
const markersVisible = filterCandidatesMarkers(data.items, visibleAreaPolygon);
|
|
|
const allMarkers = markersVisible.map(processMarkerData);
|
|
|
- const clusteredMarkers = clusterMarkers(allMarkers, currentZoom, setClusters);
|
|
|
-
|
|
|
- setMarkers(clusteredMarkers as MarkerData[]);
|
|
|
+ setMarkers(allMarkers);
|
|
|
}
|
|
|
}
|
|
|
));
|
|
|
};
|
|
|
|
|
|
- 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();
|
|
|
|
|
@@ -276,8 +241,10 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
};
|
|
|
|
|
|
const handleMapPress = async (event: {
|
|
|
- nativeEvent: { coordinate: { latitude: any; longitude: any } };
|
|
|
+ nativeEvent: { coordinate: { latitude: any; longitude: any }; action?: string };
|
|
|
}) => {
|
|
|
+ if (event.nativeEvent?.action === 'marker-press') return;
|
|
|
+
|
|
|
cancelTokenRef.current = true;
|
|
|
const { latitude, longitude } = event.nativeEvent.coordinate;
|
|
|
const point = turf.point([longitude, latitude]);
|
|
@@ -393,9 +360,60 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
|
|
|
const renderedGeoJSON = useMemo(() => renderGeoJSON(), [selectedRegion]);
|
|
|
|
|
|
+ const toggleSeries = useCallback(
|
|
|
+ async (item: any) => {
|
|
|
+ setMarkers((currentMarkers) =>
|
|
|
+ currentMarkers.map((marker) =>
|
|
|
+ marker.id === item.id ? { ...marker, visited: Number(!marker.visited) as 0 | 1 } : marker
|
|
|
+ )
|
|
|
+ );
|
|
|
+
|
|
|
+ const itemData = {
|
|
|
+ token: token,
|
|
|
+ series_id: item.series_id,
|
|
|
+ item_id: item.id,
|
|
|
+ checked: (!item.visited ? 1 : 0) as 0 | 1,
|
|
|
+ double: 0 as 0 | 1
|
|
|
+ };
|
|
|
+
|
|
|
+ try {
|
|
|
+ updateSeriesItem(itemData);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Failed to update series state', error);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ [token, updateSeriesItem]
|
|
|
+ );
|
|
|
+
|
|
|
+ const renderMarkers = () => {
|
|
|
+ return markers.map((marker, idx) => {
|
|
|
+ const coordinate = { latitude: marker.pointJSON[0], longitude: marker.pointJSON[1] };
|
|
|
+ const markerSeries = series?.find((s) => s.id === marker.series_id);
|
|
|
+ const iconUrl = markerSeries ? processIconUrl(markerSeries.icon) : 'default_icon_url';
|
|
|
+ const seriesName = markerSeries ? markerSeries.name : 'Unknown';
|
|
|
+
|
|
|
+ return (
|
|
|
+ <MarkerItem
|
|
|
+ marker={marker}
|
|
|
+ iconUrl={iconUrl}
|
|
|
+ key={`${idx} - ${marker.id}`}
|
|
|
+ coordinate={coordinate}
|
|
|
+ seriesName={seriesName}
|
|
|
+ toggleSeries={toggleSeries}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
return (
|
|
|
<View style={styles.container}>
|
|
|
- <MapView
|
|
|
+ <ClusteredMapView
|
|
|
+ initialRegion={{
|
|
|
+ latitude: 0,
|
|
|
+ longitude: 0,
|
|
|
+ latitudeDelta: 180,
|
|
|
+ longitudeDelta: 180
|
|
|
+ }}
|
|
|
ref={mapRef}
|
|
|
showsMyLocationButton={false}
|
|
|
showsCompass={false}
|
|
@@ -406,6 +424,9 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
maxZoomLevel={15}
|
|
|
minZoomLevel={0}
|
|
|
onRegionChangeComplete={findFeaturesInVisibleMapArea}
|
|
|
+ minPoints={zoomLevel < 7 ? 0 : 12}
|
|
|
+ tracksViewChanges={false}
|
|
|
+ renderCluster={(cluster) => <ClusterItem key={cluster.id} cluster={cluster} />}
|
|
|
>
|
|
|
{renderedGeoJSON}
|
|
|
{renderMapTiles(tilesBaseURL, localTileDir, 1)}
|
|
@@ -418,7 +439,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
</AnimatedMarker>
|
|
|
)}
|
|
|
{markers && renderMarkers()}
|
|
|
- </MapView>
|
|
|
+ </ClusteredMapView>
|
|
|
|
|
|
<LocationPopup
|
|
|
visible={askLocationVisible}
|