From 7b9d05f84b762d5fe8bc5c44d8b7aa8f38e0daf1 Mon Sep 17 00:00:00 2001 From: Keannu Bernasol Date: Sat, 9 Sep 2023 20:45:29 +0800 Subject: [PATCH] Added Haversine Formula calculation to get the radius of circles for study groups required for rendering --- .../CustomMapCallout/CustomMapCallout.tsx | 4 +- .../ParseStudyGroupList.tsx | 125 ++++++++++ src/interfaces/Interfaces.tsx | 29 ++- src/routes/Home/Home.tsx | 226 ++++++------------ 4 files changed, 225 insertions(+), 159 deletions(-) create mode 100644 src/components/ParseStudyGroupList/ParseStudyGroupList.tsx diff --git a/src/components/CustomMapCallout/CustomMapCallout.tsx b/src/components/CustomMapCallout/CustomMapCallout.tsx index 26cea15..23661e3 100644 --- a/src/components/CustomMapCallout/CustomMapCallout.tsx +++ b/src/components/CustomMapCallout/CustomMapCallout.tsx @@ -1,12 +1,12 @@ import { Callout } from "react-native-maps"; -import { LocationType } from "../../interfaces/Interfaces"; +import { RawLocationType } from "../../interfaces/Interfaces"; import styles from "../../styles"; import { Text } from "react-native"; // Map popup for user's location type props = { - location: LocationType; + location: RawLocationType; studying: boolean; subject?: string; }; diff --git a/src/components/ParseStudyGroupList/ParseStudyGroupList.tsx b/src/components/ParseStudyGroupList/ParseStudyGroupList.tsx new file mode 100644 index 0000000..492b649 --- /dev/null +++ b/src/components/ParseStudyGroupList/ParseStudyGroupList.tsx @@ -0,0 +1,125 @@ +import * as React from "react"; +import { View, Text } from "react-native"; +import { + StudentStatusFilterType, + LocationType, + subjectUserMapType, + StudentStatusListType, + StudentStatusFilterTypeFlattened, +} from "../../interfaces/Interfaces"; +import { Double, Float } from "react-native/Libraries/Types/CodegenTypes"; + +export default function ParseStudyGroupList(data: any) { + let result: any[] = []; + // Circle generation for students in a study group + // We first flatten the data to remove nested entries + console.log("Initial Data:", data); + let flattened_data = data + .filter((item: StudentStatusFilterType) => item.study_group !== "") + .map((item: StudentStatusFilterType) => ({ + active: item.active, + distance: item.distance, + landmark: item.landmark, + latitude: item.location.latitude, + longitude: item.location.longitude, + study_group: item.study_group, + subject: item.subject, + user: item.user, + weight: 1, + })); + console.log("Filtered Data:", flattened_data); + + // We get each unique subject + let unique_subjects = [ + ...new Set( + flattened_data.map((item: StudentStatusFilterType) => item.subject) + ), + ]; + + // Then append all entries belonging to that subject to its own array + unique_subjects.forEach((subject, index: number) => { + index++; + let filteredData = flattened_data.filter( + (item: StudentStatusFilterTypeFlattened) => item.subject === subject + ); + console.log("Subject #", index, "-", filteredData[0].subject, filteredData); + // We get the circle's center by averaging all the points + // Calculate the average latitude and longitude + const totalLat = filteredData.reduce( + (sum: Double, point: LocationType) => sum + point.latitude, + 0 + ); + const totalLng = filteredData.reduce( + (sum: Double, point: LocationType) => sum + point.longitude, + 0 + ); + + const avgLat = totalLat / filteredData.length; + const avgLng = totalLng / filteredData.length; + + console.log("Center Latitude:", avgLat); + console.log("Center Longitude:", avgLng); + + // We now calculate the radius of the circle using the Haversine Distance Formula + + function haversineDistance( + lat1: number, + lon1: number, + lat2: number, + lon2: number + ) { + function toRad(x: number) { + return (x * Math.PI) / 180; + } + + var R = 6371; // km + var x1 = lat2 - lat1; + var dLat = toRad(x1); + var x2 = lon2 - lon1; + var dLon = toRad(x2); + var a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(toRad(lat1)) * + Math.cos(toRad(lat2)) * + Math.sin(dLon / 2) * + Math.sin(dLon / 2); + var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + var d = R * c; + return d; + } + + let circle_radius = + Math.max( + ...filteredData.map((item: StudentStatusFilterTypeFlattened) => + haversineDistance(avgLat, avgLng, item.latitude, item.longitude) + ) + ) * 1000; + console.log("Radius:", circle_radius); + + // We now build the object + const subjectUserMap: subjectUserMapType = { + subject: "", + users: [], + latitude: 0, + longitude: 0, + radius: 0, + }; + filteredData.forEach((item: StudentStatusFilterType) => { + if (!subjectUserMap["users"]) { + subjectUserMap["users"] = []; + } + subjectUserMap["subject"] = item.subject; + subjectUserMap["latitude"] = avgLat; + subjectUserMap["longitude"] = avgLng; + subjectUserMap["radius"] = circle_radius; + subjectUserMap["users"].push(item.user); + }); + console.log(subjectUserMap); + + result = result.concat([subjectUserMap]); + }); + + // console.log("Final Result:", result); + + return result; +} diff --git a/src/interfaces/Interfaces.tsx b/src/interfaces/Interfaces.tsx index a21360e..ac42a41 100644 --- a/src/interfaces/Interfaces.tsx +++ b/src/interfaces/Interfaces.tsx @@ -124,7 +124,7 @@ export interface PatchUserInfoType { avatar?: string; } -interface Location { +export interface LocationType { latitude: Float; longitude: Float; } @@ -132,7 +132,7 @@ interface Location { export interface StudentStatusType { user?: string; subject?: string; - location?: Location; + location?: LocationType; landmark?: string | null; active?: boolean; } @@ -141,10 +141,23 @@ export interface StudentStatusFilterType { active: boolean; distance: number; landmark: string | null; - location: Location; + location: LocationType; study_group?: string; subject: string; user: string; + weight?: number; +} + +export interface StudentStatusFilterTypeFlattened { + active: boolean; + distance: number; + landmark: string | null; + latitude: Float; + longitude: Float; + study_group?: string; + subject: string; + user: string; + weight?: number; } export type StudentStatusReturnType = [boolean, StudentStatusType]; @@ -152,7 +165,7 @@ export type StudentStatusReturnType = [boolean, StudentStatusType]; export type StudentStatusListType = Array; export type StudentStatusListReturnType = [boolean, StudentStatusListType]; -export type LocationType = Location.LocationObject; +export type RawLocationType = Location.LocationObject; export interface UserInfoType { first_name: string; @@ -172,3 +185,11 @@ export interface UserInfoType { } export type UserInfoReturnType = [boolean, UserInfoType]; + +export type subjectUserMapType = { + subject: string; + users: string[]; + latitude: Float; + longitude: Float; + radius: Float; +}; diff --git a/src/routes/Home/Home.tsx b/src/routes/Home/Home.tsx index e1fc2d8..cf8332a 100644 --- a/src/routes/Home/Home.tsx +++ b/src/routes/Home/Home.tsx @@ -17,10 +17,11 @@ import Button from "../../components/Button/Button"; import { RootDrawerParamList, StudentStatusReturnType, - LocationType, + RawLocationType, StudentStatusType, StudentStatusListReturnType, StudentStatusListType, + subjectUserMapType, } from "../../interfaces/Interfaces"; import { useNavigation } from "@react-navigation/native"; import { @@ -33,12 +34,14 @@ import { import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useToast } from "react-native-toast-notifications"; import CustomMapCallout from "../../components/CustomMapCallout/CustomMapCallout"; +import ParseStudentStatusList from "../../components/ParseStudyGroupList/ParseStudyGroupList"; +import React from "react"; export default function Home() { // Switch this condition to see the main map when debugging const map_debug = true; const navigation = useNavigation(); - const [location, setLocation] = useState(null); + const [location, setLocation] = useState(null); const [dist, setDist] = useState(null); const [feedback, setFeedback] = useState( "To continue, please allow Stud-E permission to location services" @@ -97,7 +100,7 @@ export default function Home() { requestLocation(); }, []); - async function GetDistanceRoundedOff(location: LocationType) { + async function GetDistanceRoundedOff(location: RawLocationType) { let dist = GetDistance( location.coords.latitude, location.coords.longitude, @@ -176,8 +179,8 @@ export default function Home() { }, }); - // Can probably just get the max distance from the array and use it as radius const [student_statuses, setStudentStatuses] = useState([]); + const [study_groups, setStudyGroups] = useState([]); // Student Status List const StudentStatusList = useQuery({ enabled: studying, @@ -191,93 +194,7 @@ export default function Home() { }, onSuccess: (data: StudentStatusListReturnType) => { if (data[1]) { - // Circle generation for students in a study group - // We first flatten the data to remove nested entries - let flattened_data = data[1].map((item) => ({ - active: item.active, - distance: item.distance, - landmark: item.landmark, - latitude: item.location.latitude, - longitude: item.location.longitude, - study_group: "", - subject: item.subject, - user: item.user, - weight: 1, - })); - // Dummy data - flattened_data.push({ - active: true, - distance: 50, - landmark: "", - latitude: 8.498837, - longitude: 124.595422, - study_group: "", - subject: "Introduction to Computing", - user: "Dummy", - weight: 1, - }); - // We get each unique subject - let unique_subjects = [ - ...new Set(flattened_data.map((item) => item.subject)), - ]; - let result: any[] = []; - // Then append all entries belonging to that subject to its own array - unique_subjects.forEach((subject, index: number) => { - index++; - let filteredData = flattened_data.filter( - (item) => item.subject === subject && item.study_group === "" - ); - /*console.log( - "Subject #", - index, - "-", - filteredData[0].subject, - filteredData - );*/ - // We get the circle radius based on the furthest point - let circle_radius = Math.max( - ...filteredData.map((item) => item.distance) - ); - console.log( - "Radius of circle:", - Math.max(...filteredData.map((item) => item.distance)) - ); - // We get the circle's center by averaging all the points - // Calculate the average latitude and longitude - const totalLat = filteredData.reduce( - (sum, point) => sum + point.latitude, - 0 - ); - const totalLng = filteredData.reduce( - (sum, point) => sum + point.longitude, - 0 - ); - - const avgLat = totalLat / filteredData.length; - const avgLng = totalLng / filteredData.length; - - console.log("Center Latitude:", avgLat); - console.log("Center Longitude:", avgLng); - // We now build the object - const subjectUserMap: any = {}; - filteredData.forEach((item) => { - if (!subjectUserMap["users"]) { - subjectUserMap["users"] = []; - } - subjectUserMap["subject"] = item.subject; - subjectUserMap["latitude"] = avgLat; - subjectUserMap["longitude"] = avgLng; - subjectUserMap["radius"] = circle_radius; - subjectUserMap["users"].push(item.user); - }); - console.log(subjectUserMap); - - result = result.concat([subjectUserMap]); - }); - - console.log("Final Result:", result); - - setStudentStatuses(result); + setStudyGroups(ParseStudentStatusList(data[1])); } }, onError: (error: Error) => { @@ -322,73 +239,76 @@ export default function Home() { }} loadingBackgroundColor={colors.secondary_2} > - {student_statuses.map((student_status: any, index: number) => { - const randomColorWithOpacity = `rgba(${Math.floor( - Math.random() * 256 - )}, ${Math.floor(Math.random() * 256)}, ${Math.floor( - Math.random() * 256 - )}, 0.7)`; + {study_groups.map( + (student_status: subjectUserMapType, index: number) => { + const randomColorWithOpacity = `rgba(${Math.floor( + Math.random() * 256 + )}, ${Math.floor(Math.random() * 256)}, ${Math.floor( + Math.random() * 256 + )}, 0.7)`; - return ( - <> - { - toast.hideAll(); - toast.show( - - - Subject: {student_status.subject} - - - Students Studying: {student_status.users.length} - - - , - { - type: "normal", - placement: "top", - duration: 2000, - animationType: "slide-in", - style: { - backgroundColor: colors.secondary_2, - borderWidth: 1, - borderColor: colors.primary_1, - }, - } - ); - }} - /> - - - ); - })} + + Students Studying: {student_status.users.length} + + + , + { + type: "normal", + placement: "top", + duration: 2000, + animationType: "slide-in", + style: { + backgroundColor: colors.secondary_2, + borderWidth: 1, + borderColor: colors.primary_1, + }, + } + ); + }} + /> + + + ); + } + )}