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

560 Views Asked by At

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.

1

There are 1 best solutions below

0
On BEST ANSWER

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,
  },
});