add route optimization component

This commit is contained in:
Michael Fatemi 2021-07-16 17:32:29 -04:00
parent dadb6e9bb3
commit c78d03a6a1
11 changed files with 217 additions and 54 deletions

View File

@ -10,8 +10,10 @@ import UILink from '../UI/UILink';
import UISecondaryBox from '../UI/UISecondaryBox'; import UISecondaryBox from '../UI/UISecondaryBox';
import useImmutable from '../useImmutable'; import useImmutable from '../useImmutable';
import CarpoolDetails from './CarpoolDetails'; import CarpoolDetails from './CarpoolDetails';
import CarpoolRouteEstimator from './CarpoolRouteEstimator';
import CarpoolTopButtons from './CarpoolTopButtons'; import CarpoolTopButtons from './CarpoolTopButtons';
import MemberList from './MemberList'; import MemberList from './MemberList';
import Members from './Members';
type CarpoolState = { type CarpoolState = {
id: number; id: number;
@ -116,6 +118,9 @@ export default function Carpool({ id }: { id: number }) {
</UILink> </UILink>
<CarpoolTopButtons /> <CarpoolTopButtons />
<CarpoolDetails /> <CarpoolDetails />
<Members>
<CarpoolRouteEstimator />
</Members>
<MemberList /> <MemberList />
</UISecondaryBox> </UISecondaryBox>
</CarpoolContext.Provider> </CarpoolContext.Provider>

View File

@ -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<IEventSignup[]>([]);
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 (
<div
style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}
>
<h2>Route Optimization</h2>
{path ? (
<div style={{ display: 'flex', flexDirection: 'column' }}>
<span>Best route: {path.distance.toFixed(1)} miles</span>
<br />
{(() => {
const driver = path.path.from;
const waypoints = path.path.waypoints;
let previousLocation: Location = driver;
return (
<>
<span>Driver: {driver.user.name}</span>
{waypoints.map((waypoint, index) => {
const distance = getDistance(previousLocation, waypoint);
previousLocation = waypoint;
return (
<span key={waypoint.user.id}>
Passenger #{index + 1}: {waypoint.user.name} (
{distance.toFixed(1)} miles)
</span>
);
})}
<span>
Destination: {carpool.event.name} (
{getDistance(carpool.event, previousLocation).toFixed(1)}{' '}
miles)
</span>
</>
);
})()}
</div>
) : (
'No valid paths are available.'
)}
</div>
);
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -93,8 +93,6 @@ export default function EventSignups() {
.map((id) => signups[id]); .map((id) => signups[id]);
}, [signups, carpools]); }, [signups, carpools]);
console.log(signups);
return ( return (
<div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
<h3 style={{ marginBlockEnd: '0' }}>People without a carpool</h3> <h3 style={{ marginBlockEnd: '0' }}>People without a carpool</h3>

View File

@ -54,7 +54,7 @@ export default function Group({ id }: { id: number }) {
</div> </div>
<br /> <br />
{group.events.length > 0 ? ( {group.events?.length > 0 ? (
<EventStream events={group.events} /> <EventStream events={group.events} />
) : ( ) : (
<span> <span>

View File

@ -53,6 +53,15 @@ async function get(path: string) {
// } // }
} }
export async function getEventSignupsBulk(
eventId: number,
userIds: number[]
): Promise<IEventSignup[]> {
return await get(
`/events/${eventId}/signups_bulk?userIds=${userIds.join(',')}`
);
}
export async function getEventSignups( export async function getEventSignups(
eventId: number eventId: number
): Promise<IEventSignup[]> { ): Promise<IEventSignup[]> {

View File

@ -87,24 +87,33 @@ export type IEvent = {
longitude: number; longitude: number;
}; };
/** export type IEventSignupComplete = {
* Model EventSignup
*/
export type IEventSignup = {
user: { user: {
id: number; id: number;
name: string; name: string;
}; };
} & ( placeId: string;
| { placeId: null; formattedAddress: null; latitude: null; longitude: null } formattedAddress: string;
| { latitude: number;
placeId: string; longitude: number;
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 = { export type IInvitation = {
user: { user: {

View File

@ -1,32 +1,47 @@
import { useMemo } from 'react'; import { useDebugValue, useMemo } from 'react';
import estimateOptimalPath, { Location } from '../lib/estimateoptimalpath'; import estimateOptimalPath, {
import furthestPoint from '../lib/furthestpoint'; Location,
Path,
} from '../lib/estimateoptimalpath';
export default function useOptimalPath( export default function useOptimalPath<M extends Location, D extends Location>(
memberLocations: Location[], members: M[],
destination: Location destination: D
) { ) {
return useMemo(() => { const path = useMemo(() => {
if (memberLocations.length === 0) { if (members.length === 0) {
return null; return null;
} }
// O(n) // O(n^2)
const { maxLocation: driverLocation } = furthestPoint( const path = members.reduce((prev, driver) => {
memberLocations, // O(n)
destination const passengerLocations = members.filter(
); (location) => location !== driver
);
// O(n) // O(n)
const passengerLocations = memberLocations.filter( const path = estimateOptimalPath<M, D>({
(location) => location !== driverLocation from: driver,
); waypoints: passengerLocations,
to: destination,
});
// O(n) if (prev == null) {
return estimateOptimalPath({ return path;
from: driverLocation!, }
waypoints: passengerLocations,
to: destination, if (prev.distance > path.distance) {
}); return path;
}, [destination, memberLocations]); }
return prev;
}, null! as { path: Path<M, D>; distance: number });
return path;
}, [destination, members]);
useDebugValue(path);
return path;
} }

View File

@ -5,14 +5,19 @@ export type Location = {
longitude: number; longitude: number;
}; };
export type Path = { export type Path<M extends Location, D extends Location> = {
from: Location; from: M;
to: Location; waypoints: M[];
waypoints: Location[]; to: D;
}; };
export default function estimateOptimalPath(path: Path): { export default function estimateOptimalPath<
path: Path; M extends Location,
D extends Location
>(
path: Path<M, D>
): {
path: Path<M, D>;
distance: number; distance: number;
} { } {
const { from, to, waypoints } = path; const { from, to, waypoints } = path;
@ -23,7 +28,7 @@ export default function estimateOptimalPath(path: Path): {
for (let waypoint of waypoints) { for (let waypoint of waypoints) {
// Iterate over all possible insertion points for the waypoint // Iterate over all possible insertion points for the waypoint
let minDistance = Infinity; let minDistance = Infinity;
let insertionPoint = 1; let insertionPoint = 0;
for (let i = 0; i < sequence.length - 1; i++) { for (let i = 0; i < sequence.length - 1; i++) {
const [start, end] = sequence.slice(i, i + 2); const [start, end] = sequence.slice(i, i + 2);
@ -35,17 +40,20 @@ export default function estimateOptimalPath(path: Path): {
} }
sequence = sequence sequence = sequence
.slice(0, insertionPoint) .slice(0, insertionPoint + 1)
.concat([waypoint]) .concat([waypoint])
.concat(sequence.slice(insertionPoint)); .concat(sequence.slice(insertionPoint + 1));
} }
const newWaypoints = sequence.slice(1, sequence.length - 1); const newWaypoints = sequence.slice(1, sequence.length - 1);
console.log({ sequence, path });
return { return {
path: { path: {
from, from,
to, to,
waypoints: newWaypoints, waypoints: newWaypoints as M[],
}, },
distance: getDistance(from, ...sequence, to), distance: getDistance(from, ...sequence, to),
}; };

View File

@ -1,12 +1,12 @@
import { Location } from './estimateoptimalpath'; import { Location } from './estimateoptimalpath';
import getDistance from './getdistance'; import getDistance from './getdistance';
export default function furthestPoint( export default function furthestPoint<M extends Location>(
locations: Location[], locations: M[],
destination: Location destination: Location
) { ) {
let maxDistance = 0; let maxDistance = 0;
let maxLocation = null; let maxLocation: M | null = null;
for (let i = 0; i < locations.length; i++) { for (let i = 0; i < locations.length; i++) {
let distance = getDistance(locations[i], destination); let distance = getDistance(locations[i], destination);