add location estimation (w/ optimal path!)

This commit is contained in:
Michael Fatemi 2021-07-14 12:50:23 -04:00
parent 6bcea029d5
commit fa5a9df5da
8 changed files with 136 additions and 39 deletions

View File

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

View File

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

View File

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

View File

@ -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 (

View File

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

View File

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