javascriptreactjsreact-nativeexporeact-location

How to create a compass that points to specific coordinates (React-Native)


Here is what I have for now:

import {
  Alert,
  Animated,
  Easing,
  Linking,
  StyleSheet,
  Text,
  View,
} from "react-native";
import React, { useEffect, useState } from "react";

import * as Location from "expo-location";
import * as geolib from "geolib";

import { COLORS } from "../../assets/Colors/Colors";

export default function DateFinder() {
  const [hasForegroundPermissions, setHasForegroundPermissions] =
    useState(null);
  const [userLocation, setUserLocation] = useState(null);
  const [userHeading, setUserHeading] = useState(null);
  const [angle, setAngle] = useState(0);

  useEffect(() => {
    const AccessLocation = async () => {
      function appSettings() {
        console.warn("Open settigs pressed");
        if (Platform.OS === "ios") {
          Linking.openURL("app-settings:");
        } else RNAndroidOpenSettings.appDetailsSettings();
      }

      const appSettingsALert = () => {
        Alert.alert(
          "Allow Wassupp to Use your Location",
          "Open your app settings to allow Wassupp to access your current position. Without it, you won't be able to use the love compass",
          [
            {
              text: "Cancel",
              onPress: () => console.warn("Cancel pressed"),
            },
            { text: "Open settings", onPress: appSettings },
          ]
        );
      };

      const foregroundPermissions =
        await Location.requestForegroundPermissionsAsync();
      if (
        foregroundPermissions.canAskAgain == false ||
        foregroundPermissions.status == "denied"
      ) {
        appSettingsALert();
      }
      setHasForegroundPermissions(foregroundPermissions.status === "granted");
      if (foregroundPermissions.status == "granted") {
        const location = await Location.watchPositionAsync(
          {
            accuracy: Location.Accuracy.BestForNavigation,
            activityType: Location.ActivityType.Fitness,
            distanceInterval: 0,
          },
          (location) => {
            setUserLocation(location);
          }
        );
        const heading = await Location.watchHeadingAsync((heading) => {
          setUserHeading(heading.trueHeading);
        });
      }
    };

    AccessLocation().catch(console.error);
  }, []);

  useEffect(() => {
    if (userLocation != null) {
      setAngle(getBearing() - userHeading);
      rotateImage(angle);
    }
  }, [userLocation]);

  const textPosition = JSON.stringify(userLocation);

  const getBearing = () => {
    const bearing = geolib.getGreatCircleBearing(
      {
        latitude: userLocation.coords.latitude,
        longitude: userLocation.coords.longitude,
      },
      {
        latitude: 45.47200370608976,
        longitude: -73.86246549592089,
      }
    );
    return bearing;
  };

  const rotation = new Animated.Value(0);
  console.warn(angle);

  const rotateImage = (angle) => {
    Animated.timing(rotation, {
      toValue: angle,
      duration: 1000,
      easing: Easing.bounce,
      useNativeDriver: true,
    }).start();
  };

  //console.warn(rotation);

  return (
    <View style={styles.background}>
      <Text>{textPosition}</Text>
      <Animated.Image
        source={require("../../assets/Compass/Arrow_up.png")}
        style={[styles.image, { transform: [{ rotate: `${angle}deg` }] }]}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  background: {
    backgroundColor: COLORS.background_Pale,
    flex: 1,
    // justifyContent: "flex-start",
    //alignItems: "center",
  },
  image: {
    flex: 1,
    // height: null,
    // width: null,
    //alignItems: "center",
  },
  scrollView: {
    backgroundColor: COLORS.background_Pale,
  },
});

I think that the math I'm doing must be wrong because the arrow is pointing random directions spinning like crazy and not going to the coordinate I gave it. Also, I can't seem to use the rotateImage function in a way that rotation would be animated and i'd be able to use it to animate the image/compass. If anyone could help me out i'd really appreciate it I've been stuck on this for literally weeks.


Solution

  • Updated version of the code that works:

    import {
      Alert,
      Animated,
      Easing,
      FlatList,
      Linking,
      StyleSheet,
      Text,
      TouchableOpacity,
      View,
    } from "react-native";
    import React, { useEffect, memo, useRef, useState } from "react";
    
    import * as Location from "expo-location";
    import * as geolib from "geolib";
    
    import { COLORS } from "../../assets/Colors/Colors";
    
    export default function DateFinder() {
      const [hasForegroundPermissions, setHasForegroundPermissions] =
        useState(null);
      const [userLocation, setUserLocation] = useState(null);
      const [userHeading, setUserHeading] = useState(null);
      const [angle, setAngle] = useState(0);
      const rotation = useRef(new Animated.Value(0)).current;
      const [selectedId, setSelectedId] = useState();
    
      useEffect(() => {
        const AccessLocation = async () => {
          function appSettings() {
            console.warn("Open settigs pressed");
            if (Platform.OS === "ios") {
              Linking.openURL("app-settings:");
            } else RNAndroidOpenSettings.appDetailsSettings();
          }
    
          const appSettingsALert = () => {
            Alert.alert(
              "Allow Wassupp to Use your Location",
              "Open your app settings to allow Wassupp to access your current position. Without it, you won't be able to use the love compass",
              [
                {
                  text: "Cancel",
                  onPress: () => console.warn("Cancel pressed"),
                },
                { text: "Open settings", onPress: appSettings },
              ]
            );
          };
    
          const foregroundPermissions =
            await Location.requestForegroundPermissionsAsync();
          if (
            foregroundPermissions.canAskAgain == false ||
            foregroundPermissions.status == "denied"
          ) {
            appSettingsALert();
          }
          setHasForegroundPermissions(foregroundPermissions.status === "granted");
          if (foregroundPermissions.status == "granted") {
            const location = await Location.watchPositionAsync(
              {
                accuracy: Location.Accuracy.BestForNavigation,
                distanceInterval: 0,
              },
              (location) => {
                setUserLocation(location);
              }
            );
            const heading = await Location.watchHeadingAsync((heading) => {
              setUserHeading(heading.trueHeading);
            });
          }
        };
    
        AccessLocation().catch(console.error);
      }, []);
    
      useEffect(() => {
        const rotateImage = (angle) => {
          Animated.timing(rotation, {
            toValue: angle,
            duration: 300,
            easing: Easing.linear,
            useNativeDriver: true,
          }).start();
        };
    
        const getBearing = () => {
          const bearing = geolib.getGreatCircleBearing(
            {
              latitude: userLocation.coords.latitude,
              longitude: userLocation.coords.longitude,
            },
            {
              latitude: 45.472748,
              longitude: -73.862076,
            }
          );
          return bearing;
        };
    
        const checkHeading = setTimeout(() => {
          if (userLocation) {
            let newAngle = getBearing() - userHeading;
            let delta = newAngle - angle;
            while (delta > 180 || delta < -180) {
              if (delta > 180) {
                newAngle -= 360;
              } else if (delta < -180) {
                newAngle += 360;
              }
              delta = newAngle - angle;
            }
            if (delta > 5 || delta < -5) {
              setAngle(newAngle);
              rotateImage(newAngle);
            }
          }
        }, 0);
    
        return () => clearTimeout(checkHeading);
      }, [userHeading]);
    
      const textPosition = JSON.stringify(userLocation);
    
      const DATA = [
        {
          id: "bd7acbea-c1b1-46c2-aed5-3ad53abb28ba",
          title: "First Item",
        },
        {
          id: "3ac68afc-c605-48d3-a4f8-fbd91aa97f63",
          title: "Second Item",
        },
        {
          id: "58694a0f-3da1-471f-bd96-145571e29d72",
          title: "Third Item",
        },
      ];
    
      const Item = ({ item, onPress, backgroundColor, textColor }) => (
        <TouchableOpacity
          onPressIn={onPress}
          style={[styles.item, { backgroundColor }]}
        >
          <Text style={[styles.title, { color: textColor }]}>{item.title}</Text>
        </TouchableOpacity>
      );
    
      const renderItem = ({ item }) => {
        const backgroundColor = item.id === selectedId ? "#6e3b6e" : "#f9c2ff";
        const color = item.id === selectedId ? "white" : "black";
    
        return (
          <Item
            item={item}
            onPress={() => {
              setSelectedId(item.id);
              console.warn("bob");
            }}
            backgroundColor={backgroundColor}
            textColor={color}
          />
        );
      };
    
      return (
        <View style={styles.background}>
          <Text>{textPosition}</Text>
          <Animated.Image
            source={require("../../assets/Compass/Arrow_up.png")}
            style={[
              styles.image,
              {
                transform: [
                  {
                    rotate: rotation.interpolate({
                      inputRange: [0, 360],
                      outputRange: ["0deg", "360deg"],
                      //extrapolate: "clamp",
                    }),
                  },
                ],
              },
            ]}
          />
          <FlatList
            data={DATA}
            extraData={selectedId}
            horizontal={true}
            keyExtractor={(item) => item.id}
            renderItem={renderItem}
            style={styles.flatList}
          ></FlatList>
        </View>
      );
    }
    
    const styles = StyleSheet.create({
      background: {
        backgroundColor: COLORS.background_Pale,
        flex: 1,
        // justifyContent: "flex-start",
        //alignItems: "center",
      },
      image: {
        flex: 1,
        // height: null,
        // width: null,
        //alignItems: "center",
      },
      flatList: {
        backgroundColor: COLORS.background_Pale,
      },
    });