123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162 |
- 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<ScaleBarProps> = ({ 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 (
- <Animated.View style={[styles.container, { bottom }, animatedStyle]}>
- <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>
- </Animated.View>
- );
- };
- 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;
|