mirror of
https://github.com/myfatemi04/wheelshare-frontend.git
synced 2025-04-16 00:50:18 -04:00
add route optimization component
This commit is contained in:
parent
dadb6e9bb3
commit
c78d03a6a1
|
@ -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>
|
||||||
|
|
89
src/components/Carpool/CarpoolRouteEstimator.tsx
Normal file
89
src/components/Carpool/CarpoolRouteEstimator.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
12
src/components/Carpool/Members.tsx
Normal file
12
src/components/Carpool/Members.tsx
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
18
src/components/Carpool/useIsLocalUserMember.ts
Normal file
18
src/components/Carpool/useIsLocalUserMember.ts
Normal 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;
|
||||||
|
}
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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[]> {
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user