import React, { useEffect } from 'react'; import { StyleSheet, Text, Dimensions, View } 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.25; const TILE_SIZE_METERS_AT_0_ZOOM = 156543.034; const SCALE_STEPS_IN_METERS = [ 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; isVisible: boolean; bottom?: any; } const ScaleBar: React.FC = ({ zoom, latitude, isVisible, bottom = '21%' }) => { const metricWidth = useSharedValue(0); const imperialWidth = useSharedValue(0); const metricText = useSharedValue(''); const imperialText = useSharedValue(''); const screenWidth = Dimensions.get('window').width; const opacity = useSharedValue(isVisible ? 1 : 0); useEffect(() => { if (isVisible) { opacity.value = withTiming(1, { duration: 100 }); } else { opacity.value = withTiming(0, { duration: 1500 }); } }, [isVisible]); const animatedStyle = useAnimatedStyle(() => ({ opacity: opacity.value })); 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[], multiplier: number = 1 ) => { return steps.reduce((bestStep, currentStep) => { const scaleSize = (2 * currentStep * multiplier) / 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, 0.4); 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(() => ({ left: metricWidth.value })); const animatedImperialLineStyle = useAnimatedStyle(() => ({ left: imperialWidth.value })); const animatedCombinedStyle = useAnimatedStyle(() => ({ width: Math.max(metricWidth.value, imperialWidth.value) })); if (!metricText.value || !imperialText.value) return null; return ( {metricText.value} {imperialText.value} ); }; const styles = StyleSheet.create({ container: { position: 'absolute', left: 16, 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.DARK_BLUE, marginVertical: 4, position: 'absolute', left: 0 } }); export default ScaleBar;