Browse Source

Merge branch 'notifications' of SashaGoncharov19/nomadmania-app into dev

Viktoriia 1 year ago
parent
commit
271de4e393

+ 1 - 1
app.config.ts

@@ -21,7 +21,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
   // Should be updated after every production release (deploy to AppStore/PlayMarket)
   version: '1.0.0',
   // Should be updated after every dependency change
-  runtimeVersion: '1.3',
+  runtimeVersion: '1.4',
   orientation: 'portrait',
   icon: './assets/icon.png',
   userInterfaceStyle: 'light',

+ 4 - 2
package.json

@@ -20,6 +20,7 @@
     "@react-navigation/native": "^6.1.9",
     "@react-navigation/native-stack": "^6.9.17",
     "@react-navigation/stack": "^6.3.20",
+    "@shopify/flash-list": "1.4.3",
     "@tanstack/react-query": "^5.8.3",
     "@turf/turf": "^6.5.0",
     "axios": "^1.6.1",
@@ -30,6 +31,7 @@
     "expo-image": "~1.3.5",
     "expo-image-picker": "~14.3.2",
     "expo-location": "~16.1.0",
+    "expo-notifications": "~0.20.1",
     "expo-splash-screen": "~0.20.5",
     "expo-sqlite": "~11.3.3",
     "expo-status-bar": "~1.6.0",
@@ -46,6 +48,7 @@
     "react-native-gesture-handler": "~2.12.0",
     "react-native-image-viewing": "^0.2.2",
     "react-native-keyboard-aware-scroll-view": "^0.9.5",
+    "react-native-map-clustering": "^3.4.2",
     "react-native-maps": "1.7.1",
     "react-native-mmkv": "^2.11.0",
     "react-native-modal": "^13.0.1",
@@ -59,8 +62,7 @@
     "react-native-svg": "13.9.0",
     "react-native-tab-view": "^3.5.2",
     "yup": "^1.3.3",
-    "zustand": "^4.4.7",
-    "@shopify/flash-list": "1.4.3"
+    "zustand": "^4.4.7"
   },
   "devDependencies": {
     "@babel/core": "^7.20.0",

+ 9 - 4
src/components/WarningModal/index.tsx

@@ -17,7 +17,8 @@ export const WarningModal = ({
   type,
   message,
   title,
-  action
+  action,
+  onModalHide
 }: {
   isVisible: boolean;
   onClose: () => void;
@@ -25,6 +26,7 @@ export const WarningModal = ({
   message?: string;
   title?: string;
   action?: () => void;
+  onModalHide?: () => void;
 }) => {
   const navigation = useNavigation();
 
@@ -97,9 +99,12 @@ export const WarningModal = ({
           text: 'OK',
           textColor: Colors.WHITE,
           color: Colors.ORANGE,
-          action: onClose,
+          action: () => {
+            onClose;
+            action && action();
+          },
           borderColor: Colors.ORANGE
-        },
+        }
       ]
     }
   };
@@ -107,7 +112,7 @@ export const WarningModal = ({
   const modalContent = content[type as keyof typeof content] || {};
 
   return (
-    <Modal isVisible={isVisible}>
+    <Modal isVisible={isVisible} onModalHide={onModalHide}>
       <View style={styles.centeredView}>
         <View style={styles.modalView}>
           <View style={{ alignSelf: 'flex-end' }}>

+ 6 - 3
src/screens/InAppScreens/MapScreen/ClusterItem/index.tsx

@@ -2,13 +2,16 @@ import { View, Text } from 'react-native';
 import { Marker } from 'react-native-maps';
 import { styles } from './styles';
 
-const ClusterItem = ({ clusterId, data }: { clusterId: string, data: { center: number[], size: number } }) => {
+const ClusterItem = ({ cluster }: { cluster: any }) => {
   return (
     <Marker
-      coordinate={{ latitude: data.center[1], longitude: data.center[0] }}
+      coordinate={{
+        latitude: cluster.geometry.coordinates[1],
+        longitude: cluster.geometry.coordinates[0]
+      }}
     >
       <View style={styles.clusterContainer}>
-        <Text style={styles.text}>{data.size}</Text>
+        <Text style={styles.text}>{cluster.properties.point_count}</Text>
       </View>
     </Marker>
   );

+ 57 - 15
src/screens/InAppScreens/MapScreen/MarkerItem/index.tsx

@@ -1,22 +1,64 @@
-import { View, Image } from 'react-native';
-import { Marker } from 'react-native-maps';
+import { View, Image, Text } from 'react-native';
+import { Marker, Callout, CalloutSubview } from 'react-native-maps';
 import { styles } from './styles';
-import { MarkerData } from '../../../../types/map';
+import { ItemSeries } from '../../../../types/map';
+import { Colors } from 'src/theme';
+import CheckSvg from 'assets/icons/mark.svg';
 
-const MarkerItem = ({ marker, iconUrl }: { marker: MarkerData, iconUrl: string }) => {
+const MarkerItem = ({
+  marker,
+  iconUrl,
+  coordinate,
+  seriesName,
+  toggleSeries
+}: {
+  marker: ItemSeries;
+  iconUrl: string;
+  coordinate?: any;
+  seriesName: string;
+  toggleSeries: (item: any) => void;
+}) => {
   return (
-    <Marker
-      coordinate={{ latitude: marker.geometry.coordinates[1], longitude: marker.geometry.coordinates[0] }}
-      title={marker.properties.name}
-      tracksViewChanges={false}
-    >
-      <View style={styles.markerContainer}>
-        <Image
-          source={{ uri: iconUrl }}
-          style={styles.icon}
-          resizeMode="contain"
-        />
+    <Marker coordinate={coordinate} tracksViewChanges={false}>
+      <View
+        style={[styles.markerContainer, marker.visited === 1 && { backgroundColor: Colors.ORANGE }]}
+      >
+        <Image source={{ uri: iconUrl }} style={styles.icon} resizeMode="contain" />
       </View>
+
+      <Callout tooltip style={styles.customView}>
+        <View style={styles.calloutContainer}>
+          <View style={styles.calloutImageContainer}>
+            <Image source={{ uri: iconUrl }} style={styles.calloutImage} resizeMode="contain" />
+          </View>
+          <View style={styles.calloutTextContainer}>
+            <Text style={styles.seriesName}>{seriesName}</Text>
+            <Text style={styles.markerName}>{marker.name}</Text>
+          </View>
+          <CalloutSubview
+            style={[
+              styles.calloutButton,
+              marker.visited === 1 && {
+                backgroundColor: Colors.WHITE,
+                borderWidth: 1,
+                borderColor: Colors.BORDER_LIGHT
+              }
+            ]}
+            onPress={() => toggleSeries(marker)}
+          >
+            {marker?.visited === 1 ? (
+              <View style={styles.completedContainer}>
+                <CheckSvg width={14} height={14} fill={Colors.DARK_BLUE} />
+                <Text style={[styles.calloutButtonText, { color: Colors.DARK_BLUE }]}>
+                  Completed
+                </Text>
+              </View>
+            ) : (
+              <Text style={styles.calloutButtonText}>Mark Completed</Text>
+            )}
+          </CalloutSubview>
+        </View>
+      </Callout>
     </Marker>
   );
 };

+ 62 - 0
src/screens/InAppScreens/MapScreen/MarkerItem/styles.tsx

@@ -17,4 +17,66 @@ export const styles = StyleSheet.create({
     height: 20,
     zIndex: 5,
   },
+  customView: {
+    width: 200,
+    backgroundColor: 'white',
+    borderRadius: 8,
+    shadowColor: '#212529',
+    shadowOffset: { width: 0, height: 4 },
+    shadowOpacity: 0.12,
+    shadowRadius: 8,
+    elevation: 5,
+  },
+  calloutContainer: {
+    alignItems: 'center',
+    paddingVertical: 15,
+    paddingHorizontal: 10
+  },
+  calloutImageContainer: {
+    width: 38,
+    height: 38,
+    justifyContent: 'center',
+    alignItems: 'center',
+    backgroundColor: Colors.WHITE,
+    borderRadius: 19,
+    borderWidth: 2,
+    borderColor: Colors.TEXT_GRAY,
+    marginTop: -34,
+  },
+  calloutImage: {
+    width: 28,
+    height: 28,
+  },
+  calloutTextContainer: {
+    flex: 1,
+    gap: 4,
+    alignItems: 'center',
+    marginVertical: 10
+  },
+  seriesName: {
+    fontSize: 13,
+    fontWeight: 'bold',
+    color: Colors.DARK_BLUE,
+    textAlign: 'center'
+  },
+  markerName: {
+    fontSize: 12,
+    color: Colors.DARK_BLUE,
+    textAlign: 'center'
+  },
+  calloutButton: {
+    paddingHorizontal: 12,
+    paddingVertical: 6,
+    backgroundColor: Colors.ORANGE,
+    borderRadius: 6,
+    height: 30,
+    alignItems: 'center',
+    justifyContent: 'center',
+  },
+  calloutButtonText: {
+    color: 'white',
+    fontSize: 12,
+    fontWeight: 'bold',
+  },
+  completedContainer: { flexDirection: 'row', alignItems: 'center', gap: 6 }
 });

+ 71 - 50
src/screens/InAppScreens/MapScreen/index.tsx

@@ -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}

+ 112 - 3
src/screens/InAppScreens/ProfileScreen/Settings/index.tsx

@@ -1,6 +1,8 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
+import { View, Text, Switch, Platform, Linking } from 'react-native';
+import * as Notifications from 'expo-notifications';
 
-import { Header, PageWrapper, MenuButton } from '../../../../components';
+import { Header, PageWrapper, MenuButton, WarningModal } from '../../../../components';
 import { NAVIGATION_PAGES } from '../../../../types';
 import { Colors } from '../../../../theme';
 
@@ -15,7 +17,7 @@ import ExitIcon from '../../../../../assets/icons/exit.svg';
 import UserXMark from '../../../../../assets/icons/user-xmark.svg';
 
 import type { MenuButtonType } from '../../../../types/components';
-import { storage } from 'src/storage';
+import { StoreType, storage } from 'src/storage';
 import { CommonActions } from '@react-navigation/native';
 
 const buttons: MenuButtonType[] = [
@@ -74,6 +76,69 @@ const buttons: MenuButtonType[] = [
 ];
 
 const Settings = () => {
+  const [isSubscribed, setIsSubscribed] = useState(false);
+  const [warningVisible, setWarningVisible] = useState(false);
+  const [askPermission, setAskPermission] = useState(false);
+  const [shouldOpenWarningModal, setShouldOpenWarningModal] = useState(false);
+
+  useEffect(() => {
+    const subscribed = (storage.get('subscribed', StoreType.BOOLEAN) as boolean) ?? false;
+    setIsSubscribed(subscribed);
+  }, []);
+
+  const handleSubscribe = async () => {
+    const token = await registerForPushNotificationsAsync();
+    if (token) {
+      console.log(token);
+      storage.set('subscribed', true);
+      setIsSubscribed(true);
+
+      Notifications.addNotificationReceivedListener((notification) => {
+        console.log('notification', notification);
+      });
+
+      Notifications.addNotificationResponseReceivedListener((response) => {
+        const data = response.notification.request.content.data;
+        console.log('data', data);
+      });
+    }
+  };
+
+  const toggleSwitch = async () => {
+    if (isSubscribed) {
+      storage.set('subscribed', false);
+      setIsSubscribed(false);
+    } else {
+      setAskPermission(true);
+    }
+  };
+
+  async function registerForPushNotificationsAsync() {
+    let token;
+    const { status: existingStatus } = await Notifications.getPermissionsAsync();
+    let finalStatus = existingStatus;
+    if (existingStatus !== 'granted') {
+      const { status } = await Notifications.requestPermissionsAsync();
+      finalStatus = status;
+    }
+    if (finalStatus !== 'granted') {
+      setWarningVisible(true);
+      return null;
+    }
+    token = (await Notifications.getExpoPushTokenAsync()).data;
+
+    if (Platform.OS === 'android') {
+      Notifications.setNotificationChannelAsync('default', {
+        name: 'default',
+        importance: Notifications.AndroidImportance.MAX,
+        vibrationPattern: [0, 250, 250, 250],
+        lightColor: '#FF231F7C'
+      });
+    }
+
+    return token;
+  }
+
   return (
     <PageWrapper>
       <Header label={'Settings'} />
@@ -86,6 +151,50 @@ const Settings = () => {
           buttonFn={button.buttonFn}
         />
       ))}
+      <View
+        style={{
+          display: 'flex',
+          flexDirection: 'row',
+          justifyContent: 'space-between',
+          marginTop: 20
+        }}
+      >
+        <Text style={{ color: Colors.DARK_BLUE, fontSize: 16, fontWeight: 'bold' }}>
+          Notifications
+        </Text>
+        <Switch
+          trackColor={{ false: Colors.LIGHT_GRAY, true: Colors.DARK_BLUE }}
+          thumbColor={Colors.WHITE}
+          onValueChange={toggleSwitch}
+          value={isSubscribed}
+        />
+      </View>
+      <WarningModal
+        isVisible={askPermission}
+        onClose={() => setAskPermission(false)}
+        onModalHide={() => {
+          if (shouldOpenWarningModal) {
+            setShouldOpenWarningModal(false);
+            handleSubscribe();
+          }
+        }}
+        type="success"
+        action={() => {
+          setAskPermission(false);
+          setShouldOpenWarningModal(true);
+        }}
+        message="To use this feature we need your permission to access your notifications. If you press OK your system will ask you to confirm permission to receive notifications from NomadMania."
+      />
+      <WarningModal
+        isVisible={warningVisible}
+        onClose={() => setWarningVisible(false)}
+        type="success"
+        action={() => {
+          Platform.OS === 'ios' ? Linking.openURL('app-settings:') : Linking.openSettings();
+          setWarningVisible(false);
+        }}
+        message="NomadMania app needs notification permissions to function properly. Open settings?"
+      />
     </PageWrapper>
   );
 };

+ 0 - 23
src/types/map/index.ts

@@ -28,29 +28,6 @@ export interface ItemSeries {
   visited?: 0 | 1;
 }
 
-export interface MarkerData {
-  properties: {
-    dbscan?: string;
-    cluster?: number,
-    id: number;
-    series_id: number;
-    name: string;
-    pointJSON: any;
-    polygonJSON: string;
-    region: number;
-    visited?: 0 | 1;
-  };
-  geometry: {
-    coordinates: [number, number];
-    type: string;
-  };
-  type: string;
-  }
-  
-  export interface ClusterData {
-    [key: string]: { center: number[], size: number }
-  }
-
   export type FeatureCollection = {
     type: "FeatureCollection";
     features: Feature[];

+ 1 - 42
src/utils/mapHelpers.ts

@@ -1,6 +1,6 @@
 import * as turf from '@turf/turf';
 import { Feature } from '@turf/turf';
-import { ItemSeries, ClusterData, RegionData } from '../types/map';
+import { ItemSeries, RegionData } from '../types/map';
 
 export const findRegionInDataset = (dataset: RegionData, point: turf.helpers.Position | turf.helpers.Point | turf.helpers.Feature<turf.helpers.Point, turf.helpers.Properties>): Feature | undefined => {
   return dataset.features.find((region: any) => {
@@ -34,47 +34,6 @@ export const calculateMapRegion = (bounds: turf.BBox): any => {
   };
 };
 
-export const clusterMarkers = (markers: ItemSeries[], currentZoom: number, setClusters: React.Dispatch<React.SetStateAction<ClusterData | null>>) => {
-  if (!markers?.length) return [];
-  
-  const points = turf.featureCollection(markers.map(marker =>
-    turf.point([+marker.pointJSON[1], +marker.pointJSON[0]], { ...marker })
-  ));
-
-  let distance = 0;
-  switch (true) {
-    case (currentZoom < 7):
-      distance = 280;
-      break;
-    case (currentZoom < 9):
-      distance = 100;
-      break;
-    case (currentZoom < 13):
-      distance = 35;
-      break;
-    default:
-      distance = 0;
-  }
-  const maxDistance = Math.max(0.1, distance * Math.pow(0.5, currentZoom / 2));
-
-  const clustered = turf.clustersDbscan(points, maxDistance, { minPoints: 11 });
-
-  const clustersData: { [key: string]: { center: number[], size: number } } = {};
-  turf.clusterEach(clustered, 'cluster', (cluster, clusterValue) => {
-    const clusterCenter = turf.center(cluster as turf.AllGeoJSON);
-    const clusterSize = cluster?.features.length ?? 0;
-    
-    clustersData[clusterValue] = {
-      center: clusterCenter.geometry.coordinates,
-      size: clusterSize
-    };
-  });
-
-  setClusters(clustersData);
-
-  return clustered.features;
-};
-
 const isBBoxOverlap = (bbox1: number[], bbox2: number[]) => {
   return bbox1[0] <= bbox2[2] &&
          bbox1[2] >= bbox2[0] &&