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 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 }) {
|
|||
</UILink>
|
||||
<CarpoolTopButtons />
|
||||
<CarpoolDetails />
|
||||
<Members>
|
||||
<CarpoolRouteEstimator />
|
||||
</Members>
|
||||
<MemberList />
|
||||
</UISecondaryBox>
|
||||
</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]);
|
||||
}, [signups, carpools]);
|
||||
|
||||
console.log(signups);
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
<h3 style={{ marginBlockEnd: '0' }}>People without a carpool</h3>
|
||||
|
|
|
@ -54,7 +54,7 @@ export default function Group({ id }: { id: number }) {
|
|||
</div>
|
||||
<br />
|
||||
|
||||
{group.events.length > 0 ? (
|
||||
{group.events?.length > 0 ? (
|
||||
<EventStream events={group.events} />
|
||||
) : (
|
||||
<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(
|
||||
eventId: number
|
||||
): Promise<IEventSignup[]> {
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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<M extends Location, D extends Location>(
|
||||
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<M, D>({
|
||||
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<M, D>; distance: number });
|
||||
|
||||
return path;
|
||||
}, [destination, members]);
|
||||
|
||||
useDebugValue(path);
|
||||
|
||||
return path;
|
||||
}
|
||||
|
|
|
@ -5,14 +5,19 @@ export type Location = {
|
|||
longitude: number;
|
||||
};
|
||||
|
||||
export type Path = {
|
||||
from: Location;
|
||||
to: Location;
|
||||
waypoints: Location[];
|
||||
export type Path<M extends Location, D extends Location> = {
|
||||
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<M, D>
|
||||
): {
|
||||
path: Path<M, D>;
|
||||
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),
|
||||
};
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { Location } from './estimateoptimalpath';
|
||||
import getDistance from './getdistance';
|
||||
|
||||
export default function furthestPoint(
|
||||
locations: Location[],
|
||||
export default function furthestPoint<M extends Location>(
|
||||
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);
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user