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