浏览代码

scale bar for test + fixes

Viktoriia 6 月之前
父节点
当前提交
dd60d9246d

+ 142 - 0
src/components/ScaleBar/index.tsx

@@ -0,0 +1,142 @@
+import React, { useEffect, useState } from 'react';
+import { StyleSheet, View, Text, Dimensions } from 'react-native';
+import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated';
+import { Colors } from 'src/theme';
+
+const FEET_PER_METER = 3.28084;
+const FEET_PER_MILES = 5280;
+const SCALE_SCREEN_RATIO = 0.3;
+const TILE_SIZE_METERS_AT_0_ZOOM = 156543.034;
+
+const SCALE_STEPS_IN_METERS = [
+  1, 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000, 200000, 500000,
+  1000000, 2000000, 3000000
+];
+
+const SCALE_STEPS_IN_FEET = [
+  5,
+  10,
+  20,
+  50,
+  100,
+  200,
+  500,
+  1000,
+  2000,
+  1 * FEET_PER_MILES,
+  2 * FEET_PER_MILES,
+  5 * FEET_PER_MILES,
+  10 * FEET_PER_MILES,
+  20 * FEET_PER_MILES,
+  50 * FEET_PER_MILES,
+  100 * FEET_PER_MILES,
+  200 * FEET_PER_MILES,
+  500 * FEET_PER_MILES,
+  1000 * FEET_PER_MILES,
+  2000 * FEET_PER_MILES
+];
+
+interface ScaleBarProps {
+  zoom: number;
+  latitude: number;
+  bottom?: any;
+}
+
+const ScaleBar: React.FC<ScaleBarProps> = ({ zoom, latitude, bottom = '25%' }) => {
+  const metricWidth = useSharedValue(0);
+  const imperialWidth = useSharedValue(0);
+  const metricText = useSharedValue('');
+  const imperialText = useSharedValue('');
+  const screenWidth = Dimensions.get('window').width;
+
+  const getResolutionFromZoomAndLatitude = (zoom: number, latitude: number) =>
+    (TILE_SIZE_METERS_AT_0_ZOOM * Math.cos((latitude * Math.PI) / 180)) / Math.pow(2, zoom);
+
+  const getBestStepFromResolution = (resolution: number, steps: number[]) => {
+    return steps.reduce((bestStep, currentStep) => {
+      const scaleSize = (2 * currentStep) / resolution;
+      return scaleSize / screenWidth < SCALE_SCREEN_RATIO ? currentStep : bestStep;
+    });
+  };
+
+  const calculateScale = (zoom: number, latitude: number) => {
+    const resolution = getResolutionFromZoomAndLatitude(zoom, latitude);
+
+    const metricStep = getBestStepFromResolution(resolution, SCALE_STEPS_IN_METERS);
+    const imperialStep = getBestStepFromResolution(resolution, SCALE_STEPS_IN_FEET);
+
+    const metricScaleSize = (2 * metricStep) / resolution;
+    const imperialScaleSize = (2 * imperialStep) / resolution / FEET_PER_METER;
+
+    metricText.value =
+      metricStep >= 1000 ? `${(metricStep / 1000).toFixed(0)} km` : `${metricStep} m`;
+    imperialText.value =
+      imperialStep >= FEET_PER_MILES
+        ? `${(imperialStep / FEET_PER_MILES).toFixed(0)} mi`
+        : `${Math.round(imperialStep)} ft`;
+
+    return { metricScaleSize, imperialScaleSize };
+  };
+
+  useEffect(() => {
+    const { metricScaleSize, imperialScaleSize } = calculateScale(zoom, latitude);
+    metricWidth.value = withTiming(metricScaleSize, { duration: 200 });
+    imperialWidth.value = withTiming(imperialScaleSize, { duration: 300 });
+  }, [zoom, latitude]);
+
+  const animatedMetricLineStyle = useAnimatedStyle(() => ({
+    right: metricWidth.value
+  }));
+
+  const animatedImperialLineStyle = useAnimatedStyle(() => ({
+    right: imperialWidth.value
+  }));
+
+  const animatedCombinedStyle = useAnimatedStyle(() => ({
+    width: Math.max(metricWidth.value, imperialWidth.value)
+  }));
+
+  return (
+    <View style={[styles.container, { bottom }]}>
+      <Text style={[styles.text, { bottom: 0 }]}>{metricText.value}</Text>
+
+      <Animated.View style={[styles.scaleBar, animatedCombinedStyle]}>
+        <Animated.View style={[styles.verticalTick, { bottom: 0 }, animatedMetricLineStyle]} />
+        <Animated.View style={[styles.verticalTick, { top: 0 }, animatedImperialLineStyle]} />
+      </Animated.View>
+
+      <Text style={[styles.text, { top: 0 }]}>{imperialText.value}</Text>
+    </View>
+  );
+};
+
+const styles = StyleSheet.create({
+  container: {
+    position: 'absolute',
+    right: 10,
+    alignItems: 'center'
+  },
+  scaleBar: {
+    height: 1,
+    backgroundColor: Colors.DARK_BLUE,
+    position: 'relative',
+    width: '100%'
+  },
+  verticalTick: {
+    position: 'absolute',
+    width: 1,
+    height: 12,
+    backgroundColor: Colors.DARK_BLUE
+  },
+  text: {
+    fontSize: 10,
+    fontWeight: '500',
+    textAlign: 'center',
+    color: Colors.TEXT_GRAY,
+    marginVertical: 4,
+    position: 'absolute',
+    right: 0
+  }
+});
+
+export default ScaleBar;

+ 1 - 0
src/components/index.ts

@@ -21,3 +21,4 @@ export * from './ErrorModal';
 export * from './BlinkingDot';
 export * from './MessagesDot';
 export * from './MapButton';
+export * from './ScaleBar';

+ 16 - 2
src/screens/InAppScreens/MapScreen/UserItem/index.tsx

@@ -64,7 +64,14 @@ const UserItem = ({ marker }: { marker: any }) => {
                 />
               </View>
               <View style={styles.calloutTextContainer}>
-                <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
+                <View
+                  style={{
+                    flexDirection: 'row',
+                    alignItems: 'center',
+                    gap: 8,
+                    paddingHorizontal: 12
+                  }}
+                >
                   <Image
                     key={marker.flag.uri}
                     source={{ uri: marker.flag.uri, cache: 'force-cache' }}
@@ -116,7 +123,14 @@ const UserItem = ({ marker }: { marker: any }) => {
                 />
               </View>
               <View style={styles.calloutTextContainer}>
-                <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
+                <View
+                  style={{
+                    flexDirection: 'row',
+                    alignItems: 'center',
+                    gap: 8,
+                    paddingHorizontal: 12
+                  }}
+                >
                   <Image
                     key={marker.flag.uri}
                     source={{ uri: marker.flag.uri, cache: 'force-cache' }}

+ 23 - 2
src/screens/InAppScreens/MapScreen/index.tsx

@@ -81,6 +81,8 @@ import NomadsIcon from 'assets/icons/bottom-navigation/travellers.svg';
 import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
 import MapButton from 'src/components/MapButton';
 import { useAvatarStore } from 'src/stores/avatarVersionStore';
+import _ from 'lodash';
+import ScaleBar from 'src/components/ScaleBar';
 
 const defaultUserAvatar = require('assets/icon-user-share-location-solid.png');
 const logo = require('assets/logo-ua.png');
@@ -390,6 +392,8 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
     !!token && showNomads && Boolean(location) && isConnected
   );
   const [selectedUser, setSelectedUser] = useState<any>(null);
+  const [zoom, setZoom] = useState(0);
+  const [center, setCenter] = useState<number[] | null>(null);
 
   const isSmallScreen = Dimensions.get('window').width < 383;
   const processedImages = useRef(new Set<string>());
@@ -726,6 +730,16 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
     }
   }, [initialRegion]);
 
+  const handleMapChange = async () => {
+    if (!mapRef.current) return;
+
+    const currentZoom = await mapRef.current.getZoom();
+    const currentCenter = await mapRef.current.getCenter();
+
+    setZoom(currentZoom);
+    setCenter(currentCenter);
+  };
+
   const onMapPress = async (event: any) => {
     if (!mapRef.current) return;
     if (selectedMarker || selectedUser) {
@@ -840,6 +854,7 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
   };
 
   const handleRegionDidChange = async (feature: GeoJSON.Feature<GeoJSON.Point, any>) => {
+    handleMapChange();
     if (!feature) return;
     const { zoomLevel } = feature.properties;
     const { coordinates } = feature.geometry;
@@ -1148,6 +1163,8 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
         attributionEnabled={false}
         onPress={onMapPress}
         onRegionDidChange={handleRegionDidChange}
+        onRegionIsChanging={handleMapChange}
+        onRegionWillChange={_.debounce(handleMapChange, 200)}
       >
         <MapLibreGL.Images
           images={images}
@@ -1456,6 +1473,10 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
         )}
       </MapLibreGL.MapView>
 
+      {center ? (
+        <ScaleBar zoom={zoom} latitude={center[1]} />
+      ) : null}
+
       {regionPopupVisible && regionData ? (
         <>
           <TouchableOpacity
@@ -1612,7 +1633,7 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
                 text="Series"
                 active={seriesFilter.visible}
               />
-              {isFeatureActive && isFeatureActive.active ? (
+              {/* {isFeatureActive && isFeatureActive.active ? ( */}
                 <MapButton
                   onPress={() => {
                     setIsFilterVisible('nomads');
@@ -1622,7 +1643,7 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
                   text="Nomads"
                   active={showNomads}
                 />
-              ) : null}
+              {/* ) : null} */}
             </ScrollView>
           </View>
 

+ 20 - 0
src/screens/InAppScreens/ProfileScreen/UsersMap/index.tsx

@@ -45,6 +45,8 @@ import moment from 'moment';
 
 import TravelsIcon from 'assets/icons/bottom-navigation/globe-solid.svg';
 import MapButton from 'src/components/MapButton';
+import ScaleBar from 'src/components/ScaleBar';
+import _ from 'lodash';
 
 MapLibreGL.setAccessToken(null);
 
@@ -175,6 +177,9 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
   const { data: searchData } = useGetUniversalSearch(search, search.length > 0);
   const [isLocationLoading, setIsLocationLoading] = useState(false);
 
+  const [zoom, setZoom] = useState(0);
+  const [center, setCenter] = useState<number[] | null>(null);
+
   const { data: visitedRegionIds } = usePostGetVisitedRegionsIdsQuery(
     token,
     regionsFilter.visitedLabel,
@@ -219,6 +224,16 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
     }
   }, [visitedDareIds]);
 
+  const handleMapChange = async () => {
+    if (!mapRef.current) return;
+
+    const currentZoom = await mapRef.current.getZoom();
+    const currentCenter = await mapRef.current.getCenter();
+
+    setZoom(currentZoom);
+    setCenter(currentCenter);
+  };
+
   const handleGetLocation = async () => {
     setIsLocationLoading(true);
     try {
@@ -327,6 +342,9 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
         styleJSON={VECTOR_MAP_HOST + '/nomadmania-maps.json'}
         rotateEnabled={false}
         attributionEnabled={false}
+        onRegionDidChange={handleMapChange}
+        onRegionIsChanging={handleMapChange}
+        onRegionWillChange={_.debounce(handleMapChange, 200)}
       >
         {type === 'regions' && (
           <>
@@ -428,6 +446,8 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
         )}
       </MapLibreGL.MapView>
 
+      {center ? <ScaleBar zoom={zoom} latitude={center[1]} bottom={'15%'} /> : null}
+
       {!isExpanded ? (
         <TouchableOpacity
           style={[styles.cornerButton, styles.topRightButton]}