From c78d03a6a1658219400b25187881128dfeb40d91 Mon Sep 17 00:00:00 2001 From: Michael Fatemi Date: Fri, 16 Jul 2021 17:32:29 -0400 Subject: [PATCH] add route optimization component --- src/components/Carpool/Carpool.tsx | 5 ++ .../Carpool/CarpoolRouteEstimator.tsx | 89 +++++++++++++++++++ src/components/Carpool/Members.tsx | 12 +++ .../Carpool/useIsLocalUserMember.ts | 18 ++++ src/components/Event/EventSignups.tsx | 2 - src/components/Group/Group.tsx | 2 +- src/components/api.ts | 9 ++ src/components/types.ts | 37 +++++--- src/components/useOptimalPath.ts | 63 ++++++++----- src/lib/estimateoptimalpath.ts | 28 +++--- src/lib/furthestpoint.ts | 6 +- 11 files changed, 217 insertions(+), 54 deletions(-) create mode 100644 src/components/Carpool/CarpoolRouteEstimator.tsx create mode 100644 src/components/Carpool/Members.tsx create mode 100644 src/components/Carpool/useIsLocalUserMember.ts diff --git a/src/components/Carpool/Carpool.tsx b/src/components/Carpool/Carpool.tsx index fabea95..b4d614f 100644 --- a/src/components/Carpool/Carpool.tsx +++ b/src/components/Carpool/Carpool.tsx @@ -10,8 +10,10 @@ import UILink from '../UI/UILink'; import UISecondaryBox from '../UI/UISecondaryBox'; import useImmutable from '../useImmutable'; import CarpoolDetails from './CarpoolDetails'; +import CarpoolRouteEstimator from './CarpoolRouteEstimator'; import CarpoolTopButtons from './CarpoolTopButtons'; import MemberList from './MemberList'; +import Members from './Members'; type CarpoolState = { id: number; @@ -116,6 +118,9 @@ export default function Carpool({ id }: { id: number }) { + + + diff --git a/src/components/Carpool/CarpoolRouteEstimator.tsx b/src/components/Carpool/CarpoolRouteEstimator.tsx new file mode 100644 index 0000000..f2c56bf --- /dev/null +++ b/src/components/Carpool/CarpoolRouteEstimator.tsx @@ -0,0 +1,89 @@ +import { useMemo, useState } from 'react'; +import { useEffect } from 'react'; +import { useContext } from 'react'; +import { Location } from '../../lib/estimateoptimalpath'; +import getDistance from '../../lib/getdistance'; +import { getEventSignupsBulk } from '../api'; +import { IEventSignupComplete, IEventSignup } from '../types'; +import useOptimalPath from '../useOptimalPath'; +import { CarpoolContext } from './Carpool'; + +function useSignups(eventId: number, userIds: number[]) { + // Fetchs bulk signups from the API for the given event and user ids + // and returns a memoized result. + + const [signups, setSignups] = useState([]); + + useEffect(() => { + getEventSignupsBulk(eventId, userIds).then((signups) => { + setSignups(signups); + }); + }, [eventId, userIds]); + + return signups; +} + +export default function CarpoolRouteEstimator() { + const { carpool } = useContext(CarpoolContext); + const { members } = carpool; + + const memberIds = useMemo( + () => members.map((member) => member.id), + [members] + ); + + const signups = useSignups(carpool.event.id, memberIds); + + const signupsWithLocation = useMemo( + () => + signups.filter( + (signup) => signup.latitude !== null + ) as IEventSignupComplete[], + [signups] + ); + + const path = useOptimalPath(signupsWithLocation, carpool.event); + + return ( +
+

Route Optimization

+ {path ? ( +
+ Best route: {path.distance.toFixed(1)} miles +
+ {(() => { + const driver = path.path.from; + const waypoints = path.path.waypoints; + + let previousLocation: Location = driver; + + return ( + <> + Driver: {driver.user.name} + {waypoints.map((waypoint, index) => { + const distance = getDistance(previousLocation, waypoint); + previousLocation = waypoint; + return ( + + Passenger #{index + 1}: {waypoint.user.name} ( + {distance.toFixed(1)} miles) + + ); + })} + + Destination: {carpool.event.name} ( + {getDistance(carpool.event, previousLocation).toFixed(1)}{' '} + miles) + + + ); + })()} +
+ ) : ( + 'No valid paths are available.' + )} +
+ ); +} diff --git a/src/components/Carpool/Members.tsx b/src/components/Carpool/Members.tsx new file mode 100644 index 0000000..32d4206 --- /dev/null +++ b/src/components/Carpool/Members.tsx @@ -0,0 +1,12 @@ +import { ReactNode } from 'react'; +import useIsLocalUserMember from './useIsLocalUserMember'; + +export default function Members({ children }: { children: ReactNode }) { + const isMember = useIsLocalUserMember(); + + if (isMember) { + return <>{children}; + } else { + return null; + } +} diff --git a/src/components/Carpool/useIsLocalUserMember.ts b/src/components/Carpool/useIsLocalUserMember.ts new file mode 100644 index 0000000..002af4c --- /dev/null +++ b/src/components/Carpool/useIsLocalUserMember.ts @@ -0,0 +1,18 @@ +import { useContext, useDebugValue, useMemo } from 'react'; +import { useMe } from '../hooks'; +import { CarpoolContext } from './Carpool'; + +export default function useIsLocalUserMember() { + const me = useMe(); + const { carpool } = useContext(CarpoolContext); + const members = carpool.members; + + const isMember = useMemo( + () => members.some(({ id }) => id === me?.id), + [me?.id, members] + ); + + useDebugValue(isMember); + + return isMember; +} diff --git a/src/components/Event/EventSignups.tsx b/src/components/Event/EventSignups.tsx index de5e512..0489375 100644 --- a/src/components/Event/EventSignups.tsx +++ b/src/components/Event/EventSignups.tsx @@ -93,8 +93,6 @@ export default function EventSignups() { .map((id) => signups[id]); }, [signups, carpools]); - console.log(signups); - return (

People without a carpool

diff --git a/src/components/Group/Group.tsx b/src/components/Group/Group.tsx index 009d5cd..642984c 100644 --- a/src/components/Group/Group.tsx +++ b/src/components/Group/Group.tsx @@ -54,7 +54,7 @@ export default function Group({ id }: { id: number }) {

- {group.events.length > 0 ? ( + {group.events?.length > 0 ? ( ) : ( diff --git a/src/components/api.ts b/src/components/api.ts index f64ca0e..f46f6ae 100644 --- a/src/components/api.ts +++ b/src/components/api.ts @@ -53,6 +53,15 @@ async function get(path: string) { // } } +export async function getEventSignupsBulk( + eventId: number, + userIds: number[] +): Promise { + return await get( + `/events/${eventId}/signups_bulk?userIds=${userIds.join(',')}` + ); +} + export async function getEventSignups( eventId: number ): Promise { diff --git a/src/components/types.ts b/src/components/types.ts index e6a05eb..c770d33 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -87,24 +87,33 @@ export type IEvent = { longitude: number; }; -/** - * Model EventSignup - */ - -export type IEventSignup = { +export type IEventSignupComplete = { user: { id: number; name: string; }; -} & ( - | { placeId: null; formattedAddress: null; latitude: null; longitude: null } - | { - placeId: string; - formattedAddress: string; - latitude: number; - longitude: number; - } -); + placeId: string; + formattedAddress: string; + latitude: number; + longitude: number; +}; + +export type IEventSignupIncomplete = { + user: { + id: number; + name: string; + }; + placeId: null; + formattedAddress: null; + latitude: null; + longitude: null; +}; + +/** + * Model EventSignup + */ + +export type IEventSignup = IEventSignupComplete | IEventSignupIncomplete; export type IInvitation = { user: { diff --git a/src/components/useOptimalPath.ts b/src/components/useOptimalPath.ts index b51ee5c..aa49bee 100644 --- a/src/components/useOptimalPath.ts +++ b/src/components/useOptimalPath.ts @@ -1,32 +1,47 @@ -import { useMemo } from 'react'; -import estimateOptimalPath, { Location } from '../lib/estimateoptimalpath'; -import furthestPoint from '../lib/furthestpoint'; +import { useDebugValue, useMemo } from 'react'; +import estimateOptimalPath, { + Location, + Path, +} from '../lib/estimateoptimalpath'; -export default function useOptimalPath( - memberLocations: Location[], - destination: Location +export default function useOptimalPath( + members: M[], + destination: D ) { - return useMemo(() => { - if (memberLocations.length === 0) { + const path = useMemo(() => { + if (members.length === 0) { return null; } - // O(n) - const { maxLocation: driverLocation } = furthestPoint( - memberLocations, - destination - ); + // O(n^2) + const path = members.reduce((prev, driver) => { + // O(n) + const passengerLocations = members.filter( + (location) => location !== driver + ); - // O(n) - const passengerLocations = memberLocations.filter( - (location) => location !== driverLocation - ); + // O(n) + const path = estimateOptimalPath({ + from: driver, + waypoints: passengerLocations, + to: destination, + }); - // O(n) - return estimateOptimalPath({ - from: driverLocation!, - waypoints: passengerLocations, - to: destination, - }); - }, [destination, memberLocations]); + if (prev == null) { + return path; + } + + if (prev.distance > path.distance) { + return path; + } + + return prev; + }, null! as { path: Path; distance: number }); + + return path; + }, [destination, members]); + + useDebugValue(path); + + return path; } diff --git a/src/lib/estimateoptimalpath.ts b/src/lib/estimateoptimalpath.ts index c3f4ec1..50b8f5c 100644 --- a/src/lib/estimateoptimalpath.ts +++ b/src/lib/estimateoptimalpath.ts @@ -5,14 +5,19 @@ export type Location = { longitude: number; }; -export type Path = { - from: Location; - to: Location; - waypoints: Location[]; +export type Path = { + from: M; + waypoints: M[]; + to: D; }; -export default function estimateOptimalPath(path: Path): { - path: Path; +export default function estimateOptimalPath< + M extends Location, + D extends Location +>( + path: Path +): { + path: Path; distance: number; } { const { from, to, waypoints } = path; @@ -23,7 +28,7 @@ export default function estimateOptimalPath(path: Path): { for (let waypoint of waypoints) { // Iterate over all possible insertion points for the waypoint let minDistance = Infinity; - let insertionPoint = 1; + let insertionPoint = 0; for (let i = 0; i < sequence.length - 1; i++) { const [start, end] = sequence.slice(i, i + 2); @@ -35,17 +40,20 @@ export default function estimateOptimalPath(path: Path): { } sequence = sequence - .slice(0, insertionPoint) + .slice(0, insertionPoint + 1) .concat([waypoint]) - .concat(sequence.slice(insertionPoint)); + .concat(sequence.slice(insertionPoint + 1)); } const newWaypoints = sequence.slice(1, sequence.length - 1); + + console.log({ sequence, path }); + return { path: { from, to, - waypoints: newWaypoints, + waypoints: newWaypoints as M[], }, distance: getDistance(from, ...sequence, to), }; diff --git a/src/lib/furthestpoint.ts b/src/lib/furthestpoint.ts index a68ee75..1facd0b 100644 --- a/src/lib/furthestpoint.ts +++ b/src/lib/furthestpoint.ts @@ -1,12 +1,12 @@ import { Location } from './estimateoptimalpath'; import getDistance from './getdistance'; -export default function furthestPoint( - locations: Location[], +export default function furthestPoint( + locations: M[], destination: Location ) { let maxDistance = 0; - let maxLocation = null; + let maxLocation: M | null = null; for (let i = 0; i < locations.length; i++) { let distance = getDistance(locations[i], destination);