From fa5a9df5da37c05f8c56bb5c054f28bad618f6a3 Mon Sep 17 00:00:00 2001
From: Michael Fatemi <myfatemi04@gmail.com>
Date: Wed, 14 Jul 2021 12:50:23 -0400
Subject: [PATCH] add location estimation (w/ optimal path!)

---
 src/components/Event/Event.tsx         | 34 ++++++++------
 src/components/Event/EventCarpools.tsx | 64 +++++++++++++++++++++++++-
 src/components/Event/EventContext.tsx  |  3 +-
 src/components/Event/EventSignups.tsx  |  8 ++--
 src/lib/estimateoptimalpath.ts         | 17 +------
 src/lib/furthestpoint.ts               | 20 ++++++++
 src/lib/getPlaceDetails.ts             | 12 ++++-
 src/lib/getdistance.ts                 | 17 +++++++
 8 files changed, 136 insertions(+), 39 deletions(-)
 create mode 100644 src/lib/furthestpoint.ts
 create mode 100644 src/lib/getdistance.ts

diff --git a/src/components/Event/Event.tsx b/src/components/Event/Event.tsx
index 38e4553..99d11db 100644
--- a/src/components/Event/Event.tsx
+++ b/src/components/Event/Event.tsx
@@ -24,6 +24,8 @@ function GroupName({ group }: { group: IEvent['group'] }) {
 	return <UILink href={`/groups/${group.id}`}>{group.name}</UILink>;
 }
 
+const NOT_LOADED = {};
+
 export default function Event({
 	id,
 	initial,
@@ -32,10 +34,11 @@ export default function Event({
 	initial?: IEvent;
 }) {
 	const [event, setEvent] = useState<IEvent | null>(initial || null);
-	const [placeId, setPlaceId] = useState<string | null>(null);
+	const [myPlaceId, setPlaceId] = useState<string | null>(null);
 	const [interested, setInterested] = useState(false);
 	const [updating, setUpdating] = useState(false);
-	const [signups, setSignups] = useState<IEventSignup[] | null>(null);
+	const [signups, setSignups] =
+		useState<Record<string, IEventSignup>>(NOT_LOADED);
 	const [hasCarpool, setHasCarpool] = useState(false);
 	const toggleInterested = useCallback(() => setInterested((i) => !i), []);
 	const toggleInterestedThrottled = useThrottle(toggleInterested, 500);
@@ -65,7 +68,7 @@ export default function Event({
 	useEffect(refresh, [refresh]);
 
 	useEffect(() => {
-		if (signups === null) {
+		if (signups === NOT_LOADED) {
 			return;
 		}
 
@@ -80,16 +83,16 @@ export default function Event({
 		};
 
 		const addOrUpdateSignup = () => {
-			if (!prev.interested || prev.placeId !== placeId) {
+			if (!prev.interested || prev.placeId !== myPlaceId) {
 				console.log('Adding or updating signup.', prev, {
 					interested,
-					placeId,
+					placeId: myPlaceId,
 					eventId: id,
 					signups,
 				});
-				addOrUpdateEventSignup(id, placeId)
+				addOrUpdateEventSignup(id, myPlaceId)
 					.then(() => {
-						prev.placeId = placeId;
+						prev.placeId = myPlaceId;
 						prev.eventId = id;
 						prev.interested = true;
 					})
@@ -104,7 +107,7 @@ export default function Event({
 		} else {
 			addOrUpdateSignup();
 		}
-	}, [id, interested, placeId, signups, updating]);
+	}, [id, interested, myPlaceId, signups, updating]);
 
 	useEffect(() => {
 		getEventSignups(id)
@@ -118,7 +121,11 @@ export default function Event({
 						existingSignup.current.interested = true;
 					}
 				}
-				setSignups(signups);
+				const signupMap: Record<string, IEventSignup> = {};
+				for (let signup of signups) {
+					signupMap[signup.user.id] = signup;
+				}
+				setSignups(signupMap);
 			})
 			.catch(console.error);
 	}, [id, me?.id]);
@@ -141,6 +148,7 @@ export default function Event({
 				signups,
 				hasCarpool,
 				setHasCarpool,
+				myPlaceId,
 			}}
 		>
 			<UISecondaryBox>
@@ -166,14 +174,12 @@ export default function Event({
 							onSelected={(_address, placeID) => {
 								setPlaceId(placeID);
 							}}
-							style={placeId != null ? { border: '2px solid ' + green } : {}}
-							placeId={placeId}
+							style={myPlaceId != null ? { border: '2px solid ' + green } : {}}
+							placeId={myPlaceId}
 						/>
 						<br />
 						<EventCarpools />
-						{signups !== null && (
-							<EventSignups myPlaceId={placeId} signups={signups} />
-						)}
+						{signups !== null && <EventSignups myPlaceId={myPlaceId} />}
 					</>
 				)}
 			</UISecondaryBox>
diff --git a/src/components/Event/EventCarpools.tsx b/src/components/Event/EventCarpools.tsx
index a08e170..009b313 100644
--- a/src/components/Event/EventCarpools.tsx
+++ b/src/components/Event/EventCarpools.tsx
@@ -4,6 +4,7 @@ import EmojiPeopleIcon from '@material-ui/icons/EmojiPeople';
 import { useEffect } from 'react';
 import { useCallback, useContext, useMemo, useState } from 'react';
 import { lightgrey } from '../../lib/colors';
+import furthestPoint from '../../lib/furthestpoint';
 import {
 	useCancelCarpoolRequest,
 	useInvitationState,
@@ -15,6 +16,8 @@ import { IEvent } from '../types';
 import UIButton from '../UI/UIButton';
 import UILink from '../UI/UILink';
 import EventContext from './EventContext';
+import estimateOptimalPath, { Location } from '../../lib/estimateoptimalpath';
+import usePlace from '../usePlace';
 
 function CarpoolRow({
 	carpool,
@@ -29,6 +32,8 @@ function CarpoolRow({
 	const cancelCarpoolRequest = useCancelCarpoolRequest();
 	const sendCarpoolRequest = useSendCarpoolRequest();
 
+	const { signups } = useContext(EventContext);
+
 	const sendButton = useCallback(() => {
 		sendCarpoolRequest(carpool.id);
 	}, [sendCarpoolRequest, carpool.id]);
@@ -37,6 +42,61 @@ function CarpoolRow({
 		cancelCarpoolRequest(carpool.id);
 	}, [cancelCarpoolRequest, carpool.id]);
 
+	const {
+		event: { latitude, longitude },
+		myPlaceId,
+	} = useContext(EventContext);
+
+	const myLocation = usePlace(myPlaceId);
+
+	const extraDistance = useMemo(() => {
+		if (!myLocation) {
+			console.log('!myLocation');
+			return null;
+		}
+
+		// Calculates the minimum distance if I'm in the carpool
+		// and subtracts the distance if I'm not in the carpool
+
+		const memberLocations = carpool.members
+			.map((member) => {
+				const signup = signups[member.id];
+				if (!signup) {
+					return null;
+				}
+				return {
+					latitude: signup.latitude,
+					longitude: signup.longitude,
+				};
+			})
+			.filter(Boolean) as Location[];
+
+		const { maxLocation: driverLocation } = furthestPoint(memberLocations, {
+			latitude,
+			longitude,
+		});
+
+		const passengerLocations = memberLocations.filter(
+			(location) => location !== driverLocation
+		);
+
+		const { distance: distanceInCarpool } = estimateOptimalPath({
+			from: driverLocation,
+			waypoints: [...passengerLocations, myLocation],
+			to: { latitude, longitude },
+		});
+
+		const { distance: distanceNotInCarpool } = estimateOptimalPath({
+			from: driverLocation,
+			waypoints: passengerLocations,
+			to: { latitude, longitude },
+		});
+
+		return distanceInCarpool - distanceNotInCarpool;
+	}, [carpool.members, latitude, longitude, myLocation, signups]);
+
+	console.log(carpool.id, extraDistance);
+
 	return (
 		<div
 			style={{
@@ -58,7 +118,7 @@ function CarpoolRow({
 					window.location.href = '/carpools/' + carpool.id;
 				}}
 			>
-				{carpool.name}
+				{carpool.name} {extraDistance !== null && '+ ' + extraDistance}
 			</span>
 			<br />
 			<br />
@@ -132,7 +192,7 @@ export default function Carpools() {
 	const tentativeInviteNames = useMemo(() => {
 		if (!signups) return [];
 		const names = tentativeInvites.map((id) => {
-			const signup = signups.find((s) => s.user.id === id);
+			const signup = signups[id];
 			return signup?.user.name;
 		});
 		const nonNull = names.filter((n) => n != null);
diff --git a/src/components/Event/EventContext.tsx b/src/components/Event/EventContext.tsx
index 1da0402..c946df3 100644
--- a/src/components/Event/EventContext.tsx
+++ b/src/components/Event/EventContext.tsx
@@ -8,7 +8,7 @@ const EventContext = createContext({
 	},
 	event: null! as IEvent,
 	default: true,
-	signups: null as IEventSignup[] | null,
+	signups: {} as Record<string, IEventSignup>,
 	addTentativeInvite: (id: number) => {
 		console.error('not implemented: addTentativeInvite');
 	},
@@ -20,6 +20,7 @@ const EventContext = createContext({
 	setHasCarpool: (has: boolean) => {
 		console.error('not implemented: setHasCarpool');
 	},
+	myPlaceId: null as string | null,
 });
 
 export default EventContext;
diff --git a/src/components/Event/EventSignups.tsx b/src/components/Event/EventSignups.tsx
index f847ada..a7ec3b6 100644
--- a/src/components/Event/EventSignups.tsx
+++ b/src/components/Event/EventSignups.tsx
@@ -110,13 +110,11 @@ function EventSignup({
 }
 
 export default function EventSignups({
-	signups,
 	myPlaceId,
 }: {
-	signups: IEventSignup[];
 	myPlaceId: string | null;
 }) {
-	const { event } = useContext(EventContext);
+	const { event, signups } = useContext(EventContext);
 	const carpools = event.carpools;
 	const myPlaceDetails = usePlace(myPlaceId);
 
@@ -125,7 +123,9 @@ export default function EventSignups({
 		const members = carpools.map((c) => c.members);
 		const allMembers = members.reduce((a, b) => a.concat(b), []);
 		const allMembersIds = allMembers.map((m) => m.id);
-		return signups.filter((s) => !allMembersIds.includes(s.user.id));
+		return Object.keys(signups)
+			.filter((id) => !allMembersIds.includes(+id))
+			.map((id) => signups[id]);
 	}, [signups, carpools]);
 
 	return (
diff --git a/src/lib/estimateoptimalpath.ts b/src/lib/estimateoptimalpath.ts
index b7b7e9d..c3f4ec1 100644
--- a/src/lib/estimateoptimalpath.ts
+++ b/src/lib/estimateoptimalpath.ts
@@ -1,4 +1,4 @@
-import latlongdist from './latlongdist';
+import getDistance from './getdistance';
 
 export type Location = {
 	latitude: number;
@@ -11,21 +11,6 @@ export type Path = {
 	waypoints: Location[];
 };
 
-function getDistance(...locations: Location[]): number {
-	let distance = 0;
-	for (let i = 0; i < locations.length - 1; i++) {
-		const from = locations[i];
-		const to = locations[i + 1];
-		distance += latlongdist(
-			from.latitude,
-			from.longitude,
-			to.latitude,
-			to.longitude
-		);
-	}
-	return distance;
-}
-
 export default function estimateOptimalPath(path: Path): {
 	path: Path;
 	distance: number;
diff --git a/src/lib/furthestpoint.ts b/src/lib/furthestpoint.ts
new file mode 100644
index 0000000..e97ab8b
--- /dev/null
+++ b/src/lib/furthestpoint.ts
@@ -0,0 +1,20 @@
+import { Location } from './estimateoptimalpath';
+import getDistance from './getdistance';
+
+export default function furthestPoint(
+	locations: Location[],
+	destination: Location
+) {
+	let maxDistance = 0;
+	let maxLocation = { latitude: 0, longitude: 0 };
+	for (let i = 0; i < locations.length; i++) {
+		let distance = getDistance(locations[i], destination);
+
+		if (distance > maxDistance) {
+			maxDistance = distance;
+			maxLocation = locations[i];
+		}
+	}
+
+	return { maxDistance, maxLocation };
+}
diff --git a/src/lib/getPlaceDetails.ts b/src/lib/getPlaceDetails.ts
index 9bb630a..d6275e2 100644
--- a/src/lib/getPlaceDetails.ts
+++ b/src/lib/getPlaceDetails.ts
@@ -8,18 +8,26 @@ export type PlaceDetails = {
 	longitude: number;
 };
 
+const cache = new Map<string, PlaceDetails>();
+
 export default async function getPlaceDetails(placeId: string) {
+	if (cache.has(placeId)) {
+		return cache.get(placeId)!;
+	}
+
 	return new Promise<PlaceDetails>((resolve, reject) => {
 		places.getDetails(
 			{ placeId, fields: ['name', 'formatted_address', 'geometry'] },
 			(result, status) => {
 				if (result || status === 'OK') {
-					resolve({
+					const place = {
 						name: result.name,
 						formattedAddress: result.formatted_address!,
 						latitude: result.geometry!.location.lat(),
 						longitude: result.geometry!.location.lng(),
-					});
+					};
+					cache.set(placeId, place);
+					resolve(place);
 				} else {
 					reject(new Error('Unexpected Places status ' + status));
 				}
diff --git a/src/lib/getdistance.ts b/src/lib/getdistance.ts
new file mode 100644
index 0000000..d051038
--- /dev/null
+++ b/src/lib/getdistance.ts
@@ -0,0 +1,17 @@
+import { Location } from './estimateoptimalpath';
+import latlongdist from './latlongdist';
+
+export default function getDistance(...locations: Location[]): number {
+	let distance = 0;
+	for (let i = 0; i < locations.length - 1; i++) {
+		const from = locations[i];
+		const to = locations[i + 1];
+		distance += latlongdist(
+			from.latitude,
+			from.longitude,
+			to.latitude,
+			to.longitude
+		);
+	}
+	return distance;
+}