mirror of
https://github.com/myfatemi04/wheelshare-frontend.git
synced 2025-04-21 11:20:17 -04:00
add location estimation (w/ optimal path!)
This commit is contained in:
parent
6bcea029d5
commit
fa5a9df5da
|
@ -24,6 +24,8 @@ function GroupName({ group }: { group: IEvent['group'] }) {
|
||||||
return <UILink href={`/groups/${group.id}`}>{group.name}</UILink>;
|
return <UILink href={`/groups/${group.id}`}>{group.name}</UILink>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const NOT_LOADED = {};
|
||||||
|
|
||||||
export default function Event({
|
export default function Event({
|
||||||
id,
|
id,
|
||||||
initial,
|
initial,
|
||||||
|
@ -32,10 +34,11 @@ export default function Event({
|
||||||
initial?: IEvent;
|
initial?: IEvent;
|
||||||
}) {
|
}) {
|
||||||
const [event, setEvent] = useState<IEvent | null>(initial || null);
|
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 [interested, setInterested] = useState(false);
|
||||||
const [updating, setUpdating] = 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 [hasCarpool, setHasCarpool] = useState(false);
|
||||||
const toggleInterested = useCallback(() => setInterested((i) => !i), []);
|
const toggleInterested = useCallback(() => setInterested((i) => !i), []);
|
||||||
const toggleInterestedThrottled = useThrottle(toggleInterested, 500);
|
const toggleInterestedThrottled = useThrottle(toggleInterested, 500);
|
||||||
|
@ -65,7 +68,7 @@ export default function Event({
|
||||||
useEffect(refresh, [refresh]);
|
useEffect(refresh, [refresh]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (signups === null) {
|
if (signups === NOT_LOADED) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,16 +83,16 @@ export default function Event({
|
||||||
};
|
};
|
||||||
|
|
||||||
const addOrUpdateSignup = () => {
|
const addOrUpdateSignup = () => {
|
||||||
if (!prev.interested || prev.placeId !== placeId) {
|
if (!prev.interested || prev.placeId !== myPlaceId) {
|
||||||
console.log('Adding or updating signup.', prev, {
|
console.log('Adding or updating signup.', prev, {
|
||||||
interested,
|
interested,
|
||||||
placeId,
|
placeId: myPlaceId,
|
||||||
eventId: id,
|
eventId: id,
|
||||||
signups,
|
signups,
|
||||||
});
|
});
|
||||||
addOrUpdateEventSignup(id, placeId)
|
addOrUpdateEventSignup(id, myPlaceId)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
prev.placeId = placeId;
|
prev.placeId = myPlaceId;
|
||||||
prev.eventId = id;
|
prev.eventId = id;
|
||||||
prev.interested = true;
|
prev.interested = true;
|
||||||
})
|
})
|
||||||
|
@ -104,7 +107,7 @@ export default function Event({
|
||||||
} else {
|
} else {
|
||||||
addOrUpdateSignup();
|
addOrUpdateSignup();
|
||||||
}
|
}
|
||||||
}, [id, interested, placeId, signups, updating]);
|
}, [id, interested, myPlaceId, signups, updating]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getEventSignups(id)
|
getEventSignups(id)
|
||||||
|
@ -118,7 +121,11 @@ export default function Event({
|
||||||
existingSignup.current.interested = true;
|
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);
|
.catch(console.error);
|
||||||
}, [id, me?.id]);
|
}, [id, me?.id]);
|
||||||
|
@ -141,6 +148,7 @@ export default function Event({
|
||||||
signups,
|
signups,
|
||||||
hasCarpool,
|
hasCarpool,
|
||||||
setHasCarpool,
|
setHasCarpool,
|
||||||
|
myPlaceId,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<UISecondaryBox>
|
<UISecondaryBox>
|
||||||
|
@ -166,14 +174,12 @@ export default function Event({
|
||||||
onSelected={(_address, placeID) => {
|
onSelected={(_address, placeID) => {
|
||||||
setPlaceId(placeID);
|
setPlaceId(placeID);
|
||||||
}}
|
}}
|
||||||
style={placeId != null ? { border: '2px solid ' + green } : {}}
|
style={myPlaceId != null ? { border: '2px solid ' + green } : {}}
|
||||||
placeId={placeId}
|
placeId={myPlaceId}
|
||||||
/>
|
/>
|
||||||
<br />
|
<br />
|
||||||
<EventCarpools />
|
<EventCarpools />
|
||||||
{signups !== null && (
|
{signups !== null && <EventSignups myPlaceId={myPlaceId} />}
|
||||||
<EventSignups myPlaceId={placeId} signups={signups} />
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</UISecondaryBox>
|
</UISecondaryBox>
|
||||||
|
|
|
@ -4,6 +4,7 @@ import EmojiPeopleIcon from '@material-ui/icons/EmojiPeople';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useCallback, useContext, useMemo, useState } from 'react';
|
import { useCallback, useContext, useMemo, useState } from 'react';
|
||||||
import { lightgrey } from '../../lib/colors';
|
import { lightgrey } from '../../lib/colors';
|
||||||
|
import furthestPoint from '../../lib/furthestpoint';
|
||||||
import {
|
import {
|
||||||
useCancelCarpoolRequest,
|
useCancelCarpoolRequest,
|
||||||
useInvitationState,
|
useInvitationState,
|
||||||
|
@ -15,6 +16,8 @@ import { IEvent } from '../types';
|
||||||
import UIButton from '../UI/UIButton';
|
import UIButton from '../UI/UIButton';
|
||||||
import UILink from '../UI/UILink';
|
import UILink from '../UI/UILink';
|
||||||
import EventContext from './EventContext';
|
import EventContext from './EventContext';
|
||||||
|
import estimateOptimalPath, { Location } from '../../lib/estimateoptimalpath';
|
||||||
|
import usePlace from '../usePlace';
|
||||||
|
|
||||||
function CarpoolRow({
|
function CarpoolRow({
|
||||||
carpool,
|
carpool,
|
||||||
|
@ -29,6 +32,8 @@ function CarpoolRow({
|
||||||
const cancelCarpoolRequest = useCancelCarpoolRequest();
|
const cancelCarpoolRequest = useCancelCarpoolRequest();
|
||||||
const sendCarpoolRequest = useSendCarpoolRequest();
|
const sendCarpoolRequest = useSendCarpoolRequest();
|
||||||
|
|
||||||
|
const { signups } = useContext(EventContext);
|
||||||
|
|
||||||
const sendButton = useCallback(() => {
|
const sendButton = useCallback(() => {
|
||||||
sendCarpoolRequest(carpool.id);
|
sendCarpoolRequest(carpool.id);
|
||||||
}, [sendCarpoolRequest, carpool.id]);
|
}, [sendCarpoolRequest, carpool.id]);
|
||||||
|
@ -37,6 +42,61 @@ function CarpoolRow({
|
||||||
cancelCarpoolRequest(carpool.id);
|
cancelCarpoolRequest(carpool.id);
|
||||||
}, [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 (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
@ -58,7 +118,7 @@ function CarpoolRow({
|
||||||
window.location.href = '/carpools/' + carpool.id;
|
window.location.href = '/carpools/' + carpool.id;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{carpool.name}
|
{carpool.name} {extraDistance !== null && '+ ' + extraDistance}
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
|
@ -132,7 +192,7 @@ export default function Carpools() {
|
||||||
const tentativeInviteNames = useMemo(() => {
|
const tentativeInviteNames = useMemo(() => {
|
||||||
if (!signups) return [];
|
if (!signups) return [];
|
||||||
const names = tentativeInvites.map((id) => {
|
const names = tentativeInvites.map((id) => {
|
||||||
const signup = signups.find((s) => s.user.id === id);
|
const signup = signups[id];
|
||||||
return signup?.user.name;
|
return signup?.user.name;
|
||||||
});
|
});
|
||||||
const nonNull = names.filter((n) => n != null);
|
const nonNull = names.filter((n) => n != null);
|
||||||
|
|
|
@ -8,7 +8,7 @@ const EventContext = createContext({
|
||||||
},
|
},
|
||||||
event: null! as IEvent,
|
event: null! as IEvent,
|
||||||
default: true,
|
default: true,
|
||||||
signups: null as IEventSignup[] | null,
|
signups: {} as Record<string, IEventSignup>,
|
||||||
addTentativeInvite: (id: number) => {
|
addTentativeInvite: (id: number) => {
|
||||||
console.error('not implemented: addTentativeInvite');
|
console.error('not implemented: addTentativeInvite');
|
||||||
},
|
},
|
||||||
|
@ -20,6 +20,7 @@ const EventContext = createContext({
|
||||||
setHasCarpool: (has: boolean) => {
|
setHasCarpool: (has: boolean) => {
|
||||||
console.error('not implemented: setHasCarpool');
|
console.error('not implemented: setHasCarpool');
|
||||||
},
|
},
|
||||||
|
myPlaceId: null as string | null,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default EventContext;
|
export default EventContext;
|
||||||
|
|
|
@ -110,13 +110,11 @@ function EventSignup({
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function EventSignups({
|
export default function EventSignups({
|
||||||
signups,
|
|
||||||
myPlaceId,
|
myPlaceId,
|
||||||
}: {
|
}: {
|
||||||
signups: IEventSignup[];
|
|
||||||
myPlaceId: string | null;
|
myPlaceId: string | null;
|
||||||
}) {
|
}) {
|
||||||
const { event } = useContext(EventContext);
|
const { event, signups } = useContext(EventContext);
|
||||||
const carpools = event.carpools;
|
const carpools = event.carpools;
|
||||||
const myPlaceDetails = usePlace(myPlaceId);
|
const myPlaceDetails = usePlace(myPlaceId);
|
||||||
|
|
||||||
|
@ -125,7 +123,9 @@ export default function EventSignups({
|
||||||
const members = carpools.map((c) => c.members);
|
const members = carpools.map((c) => c.members);
|
||||||
const allMembers = members.reduce((a, b) => a.concat(b), []);
|
const allMembers = members.reduce((a, b) => a.concat(b), []);
|
||||||
const allMembersIds = allMembers.map((m) => m.id);
|
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]);
|
}, [signups, carpools]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import latlongdist from './latlongdist';
|
import getDistance from './getdistance';
|
||||||
|
|
||||||
export type Location = {
|
export type Location = {
|
||||||
latitude: number;
|
latitude: number;
|
||||||
|
@ -11,21 +11,6 @@ export type Path = {
|
||||||
waypoints: Location[];
|
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): {
|
export default function estimateOptimalPath(path: Path): {
|
||||||
path: Path;
|
path: Path;
|
||||||
distance: number;
|
distance: number;
|
||||||
|
|
20
src/lib/furthestpoint.ts
Normal file
20
src/lib/furthestpoint.ts
Normal file
|
@ -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 };
|
||||||
|
}
|
|
@ -8,18 +8,26 @@ export type PlaceDetails = {
|
||||||
longitude: number;
|
longitude: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const cache = new Map<string, PlaceDetails>();
|
||||||
|
|
||||||
export default async function getPlaceDetails(placeId: string) {
|
export default async function getPlaceDetails(placeId: string) {
|
||||||
|
if (cache.has(placeId)) {
|
||||||
|
return cache.get(placeId)!;
|
||||||
|
}
|
||||||
|
|
||||||
return new Promise<PlaceDetails>((resolve, reject) => {
|
return new Promise<PlaceDetails>((resolve, reject) => {
|
||||||
places.getDetails(
|
places.getDetails(
|
||||||
{ placeId, fields: ['name', 'formatted_address', 'geometry'] },
|
{ placeId, fields: ['name', 'formatted_address', 'geometry'] },
|
||||||
(result, status) => {
|
(result, status) => {
|
||||||
if (result || status === 'OK') {
|
if (result || status === 'OK') {
|
||||||
resolve({
|
const place = {
|
||||||
name: result.name,
|
name: result.name,
|
||||||
formattedAddress: result.formatted_address!,
|
formattedAddress: result.formatted_address!,
|
||||||
latitude: result.geometry!.location.lat(),
|
latitude: result.geometry!.location.lat(),
|
||||||
longitude: result.geometry!.location.lng(),
|
longitude: result.geometry!.location.lng(),
|
||||||
});
|
};
|
||||||
|
cache.set(placeId, place);
|
||||||
|
resolve(place);
|
||||||
} else {
|
} else {
|
||||||
reject(new Error('Unexpected Places status ' + status));
|
reject(new Error('Unexpected Places status ' + status));
|
||||||
}
|
}
|
||||||
|
|
17
src/lib/getdistance.ts
Normal file
17
src/lib/getdistance.ts
Normal file
|
@ -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;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user