index.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import React, { useEffect } from 'react';
  2. import { StyleSheet, Text, Dimensions, View } from 'react-native';
  3. import Animated, { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated';
  4. import { Colors } from 'src/theme';
  5. const FEET_PER_METER = 3.28084;
  6. const FEET_PER_MILES = 5280;
  7. const SCALE_SCREEN_RATIO = 0.25;
  8. const TILE_SIZE_METERS_AT_0_ZOOM = 156543.034;
  9. const SCALE_STEPS_IN_METERS = [
  10. 2, 5, 10, 20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000, 50000, 100000, 200000, 500000,
  11. 1000000, 2000000, 3000000
  12. ];
  13. const SCALE_STEPS_IN_FEET = [
  14. 5,
  15. 10,
  16. 20,
  17. 50,
  18. 100,
  19. 200,
  20. 500,
  21. 1000,
  22. 2000,
  23. 1 * FEET_PER_MILES,
  24. 2 * FEET_PER_MILES,
  25. 5 * FEET_PER_MILES,
  26. 10 * FEET_PER_MILES,
  27. 20 * FEET_PER_MILES,
  28. 50 * FEET_PER_MILES,
  29. 100 * FEET_PER_MILES,
  30. 200 * FEET_PER_MILES,
  31. 500 * FEET_PER_MILES,
  32. 1000 * FEET_PER_MILES,
  33. 2000 * FEET_PER_MILES
  34. ];
  35. interface ScaleBarProps {
  36. zoom: number;
  37. latitude: number;
  38. isVisible: boolean;
  39. bottom?: any;
  40. }
  41. const ScaleBar: React.FC<ScaleBarProps> = ({ zoom, latitude, isVisible, bottom = '21%' }) => {
  42. const metricWidth = useSharedValue(0);
  43. const imperialWidth = useSharedValue(0);
  44. const metricText = useSharedValue('');
  45. const imperialText = useSharedValue('');
  46. const screenWidth = Dimensions.get('window').width;
  47. const opacity = useSharedValue(isVisible ? 1 : 0);
  48. useEffect(() => {
  49. if (isVisible) {
  50. opacity.value = withTiming(1, { duration: 100 });
  51. } else {
  52. opacity.value = withTiming(0, { duration: 1500 });
  53. }
  54. }, [isVisible]);
  55. const animatedStyle = useAnimatedStyle(() => ({
  56. opacity: opacity.value
  57. }));
  58. const getResolutionFromZoomAndLatitude = (zoom: number, latitude: number) =>
  59. (TILE_SIZE_METERS_AT_0_ZOOM * Math.cos((latitude * Math.PI) / 180)) / Math.pow(2, zoom);
  60. const getBestStepFromResolution = (
  61. resolution: number,
  62. steps: number[],
  63. multiplier: number = 1
  64. ) => {
  65. return steps.reduce((bestStep, currentStep) => {
  66. const scaleSize = (2 * currentStep * multiplier) / resolution;
  67. return scaleSize / screenWidth < SCALE_SCREEN_RATIO ? currentStep : bestStep;
  68. });
  69. };
  70. const calculateScale = (zoom: number, latitude: number) => {
  71. const resolution = getResolutionFromZoomAndLatitude(zoom, latitude);
  72. const metricStep = getBestStepFromResolution(resolution, SCALE_STEPS_IN_METERS);
  73. const imperialStep = getBestStepFromResolution(resolution, SCALE_STEPS_IN_FEET, 0.4);
  74. const metricScaleSize = (2 * metricStep) / resolution;
  75. const imperialScaleSize = (2 * imperialStep) / resolution / FEET_PER_METER;
  76. metricText.value =
  77. metricStep >= 1000 ? `${(metricStep / 1000).toFixed(0)} km` : `${metricStep} m`;
  78. imperialText.value =
  79. imperialStep >= FEET_PER_MILES
  80. ? `${(imperialStep / FEET_PER_MILES).toFixed(0)} mi`
  81. : `${Math.round(imperialStep)} ft`;
  82. return { metricScaleSize, imperialScaleSize };
  83. };
  84. useEffect(() => {
  85. const { metricScaleSize, imperialScaleSize } = calculateScale(zoom, latitude);
  86. metricWidth.value = withTiming(metricScaleSize, { duration: 200 });
  87. imperialWidth.value = withTiming(imperialScaleSize, { duration: 300 });
  88. }, [zoom, latitude]);
  89. const animatedMetricLineStyle = useAnimatedStyle(() => ({
  90. left: metricWidth.value
  91. }));
  92. const animatedImperialLineStyle = useAnimatedStyle(() => ({
  93. left: imperialWidth.value
  94. }));
  95. const animatedCombinedStyle = useAnimatedStyle(() => ({
  96. width: Math.max(metricWidth.value, imperialWidth.value)
  97. }));
  98. if (!metricText.value || !imperialText.value) return null;
  99. return (
  100. <Animated.View style={[styles.container, { bottom }, animatedStyle]}>
  101. <Text style={[styles.text, { bottom: 0 }]}>{metricText.value}</Text>
  102. <Animated.View style={[styles.scaleBar, animatedCombinedStyle]}>
  103. <Animated.View style={[styles.verticalTick, { bottom: 0 }, animatedMetricLineStyle]} />
  104. <Animated.View style={[styles.verticalTick, { top: 0 }, animatedImperialLineStyle]} />
  105. </Animated.View>
  106. <Text style={[styles.text, { top: 0 }]}>{imperialText.value}</Text>
  107. </Animated.View>
  108. );
  109. };
  110. const styles = StyleSheet.create({
  111. container: {
  112. position: 'absolute',
  113. left: 16,
  114. alignItems: 'center'
  115. },
  116. scaleBar: {
  117. height: 1,
  118. backgroundColor: Colors.DARK_BLUE,
  119. position: 'relative',
  120. width: '100%'
  121. },
  122. verticalTick: {
  123. position: 'absolute',
  124. width: 1,
  125. height: 12,
  126. backgroundColor: Colors.DARK_BLUE
  127. },
  128. text: {
  129. fontSize: 10,
  130. fontWeight: '500',
  131. textAlign: 'center',
  132. color: Colors.DARK_BLUE,
  133. marginVertical: 4,
  134. position: 'absolute',
  135. left: 0
  136. }
  137. });
  138. export default ScaleBar;