Skip to content

Commit

Permalink
trip planner
Browse files Browse the repository at this point in the history
good enough
  • Loading branch information
NishilJ committed Nov 15, 2024
2 parents 6a64bc3 + b258407 commit 8b78c6b
Show file tree
Hide file tree
Showing 3 changed files with 290 additions and 19 deletions.
27 changes: 19 additions & 8 deletions src/components/TransitBar.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import React from 'react';
import {Flex, Item, ActionGroup} from '@adobe/react-spectrum';
import { Flex, ActionGroup, Item } from '@adobe/react-spectrum';

// Nishil
// Implements the three transit use case buttons
const TransitBar: React.FC = () => {
interface TransitBarProps {
onSelect: (selectedView: string) => void; // Define the expected prop type
}

const TransitBar: React.FC<TransitBarProps> = ({ onSelect }) => {
return (
<Flex margin="auto" alignItems="center" justifyContent="center" height="100%">
<ActionGroup selectionMode="single" width="fit-content" defaultSelectedKeys={['planner']}>
<Item key="departures">Departures</Item>
<Item key="planner">Trip Planner</Item>
<Item key="routes">Routes</Item>
<ActionGroup
selectionMode="single"
width="fit-content"
defaultSelectedKeys={['planner']}
onSelectionChange={(key) => {
if (key) {
onSelect(key.toString()); // Pass the selected key to onSelect
}
}}
>
<Item href="/departures" key="departures">Departures</Item>
<Item href="/plan" key="planner">Trip Planner</Item>
<Item href="/routes" key="routes">Routes</Item>
</ActionGroup>
</Flex>
);
Expand Down
254 changes: 254 additions & 0 deletions src/components/TripPlanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
// Worked on by Yoel and Nishil
import React, {useState} from "react";
import {
Flex,
ComboBox,
Item,
View,
Button,
useAsyncList,
ListBox,
Text,
Tabs,
TabList,
TabPanels, Content, Heading, Divider
} from '@adobe/react-spectrum';
import {wait} from "@testing-library/user-event/dist/utils";

interface Place {
type: string,
name: string,
id: string,
lat: number,
lon: number;
}

interface Trip {
id: string;
duration: number; // Duration in seconds
startTime: string; // ISO timestamp of start time
endTime: string; // ISO timestamp of end time
transfers: number; // Number of transfers
legs: Leg[]; // List of legs

}

interface Leg {
mode: string; // e.g., "BUS", "TRAIN", "WALK"
from: Place; // Starting location details
to: Place; // Destination location details
duration: number; // Duration in seconds
startTime: string; // ISO timestamp of start time
endTime: string; // ISO timestamp of end time
distance: number; // Distance in meters (optional if unknown)
headsign: string; // Vehicle headsign
tripId: string; // Unique ID for the trip
routeShortName: string; // Short name for the route (e.g., "Bus 232")
routeColor: string; // Color of the route, in hex
routeTextColor: string; // Text color for route, in hex
intermediateStops: Place[]; // List of intermediate stops
}

const usePlaceSuggestions = () => {
return useAsyncList<Place>({
async load({signal, filterText}) {
if (!filterText) return {items: []}; // Return empty if no input
let response = await fetch(`http://motis.metroll.live/api/v1/geocode?text=${filterText}`, {signal});
let data = await response.json();
let places = data.map((pl: Place) => ({
type: pl.type,
name: pl.name,
id: pl.id || null, // If no id, set to null
lat: pl.lat,
lon: pl.lon,
}));
return {items: places.slice(0, 5)};
}
});
};

const getTripSuggestions = async (fromPlace: Place, toPlace: Place)=> {
if (!fromPlace || !toPlace) return { items: [] };
const currentTime = new Date().toISOString();
let fromPlaceId = fromPlace.id;
let toPlaceId = toPlace.id;
if (fromPlace.type === "PLACE" || fromPlace.type === "ADDRESS")
fromPlaceId = `${fromPlace.lat},${fromPlace.lon}`;
if (toPlace.type === "PLACE" || toPlace.type === "ADDRESS")
toPlaceId = `${toPlace.lat},${toPlace.lon}`;
const response = await fetch(`http://motis.metroll.live/api/v1/plan?time=${currentTime}&fromPlace=${fromPlaceId}&toPlace=${toPlaceId}&arriveBy=false`);
const data = await response.json();
let trips = data.itineraries.map((trip: Trip) => ({
...trip,
id: `${trip.startTime}-${trip.endTime}-${trip.transfers}`,
legs: trip.legs.map((leg: Leg) => ({
...leg,
routeShortName: leg.mode === "WALK" ? "Walk" : leg.routeShortName
}))

}));
return {items: trips.slice(0, 5)};
};

const formatDuration = (seconds: number): string => {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
let formattedDuration = "";

if (hours > 0) {
formattedDuration += `${hours} hour${hours > 1 ? 's' : ''}`;
}

if (minutes > 0) {
if (hours > 0) formattedDuration += " ";
formattedDuration += `${minutes} minute${minutes > 1 ? 's' : ''}`;
}

return formattedDuration || "0 minutes";
};


const TripPlanner: React.FC = () => {
const [fromPlace, setFromPlace] = useState<Place | null>(null);
const [toPlace, setToPlace] = useState<Place | null>(null);
const [trips, setTrips] = useState<Trip[]>([]);
const [showTrips, setShowTrips] = useState(false);
const [tripDetails, setTripDetails] = useState<Trip | null>(null);
const [showTripDetails, setShowTripDetails] = useState(false);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = React.useState(false);

const fromPlaceSuggestions = usePlaceSuggestions();
const toPlaceSuggestions = usePlaceSuggestions();

const HandlePlanTripButton = async () => {
setShowTrips(false);
setShowTripDetails(false);
if (fromPlace && toPlace) {
if (fromPlace.id === toPlace.id) {
setError("Origin and destination cannot be the same.");
}
else {
setIsLoading(true);
await wait(1000);
const result = await getTripSuggestions(fromPlace, toPlace);
if (result.items.length === 0) {
setError("No trips found.");
} else {
setError(null);
setTrips(result.items);

}
}
} else {
setError("Please fill in both the origin and destination fields.");
}
setIsLoading(false);
setShowTrips(true);
};

return (
<Flex direction="row" gap="size-200" justifyContent="center">
<Flex direction="column" justifyContent="space-between" gap="size-200">
<View backgroundColor="gray-50" padding="size-300" marginX="auto" borderRadius="medium">
<Flex gap="size-100" direction="column" width="size-6000">
{/* Start Location ComboBox */}
<ComboBox
label="Origin"
placeholder="Enter a stop, place, or address"
items={fromPlaceSuggestions.items}
inputValue={fromPlaceSuggestions.filterText}
onInputChange={fromPlaceSuggestions.setFilterText}
onSelectionChange={(key) => {
const selectedItem = fromPlaceSuggestions.items.find(item => item.id === key);
setFromPlace(selectedItem || null);
}}
loadingState={fromPlaceSuggestions.loadingState}
direction="bottom"
shouldFlip={true}
menuTrigger="input"
width="100%"

>
{(item) => <Item key={item.id}>{item.name}</Item>}
</ComboBox>
{/* End Location ComboBox */}
<ComboBox
label="Destination"
placeholder="Enter a stop, place, or address"
items={toPlaceSuggestions.items}
inputValue={toPlaceSuggestions.filterText}
onInputChange={toPlaceSuggestions.setFilterText}
onSelectionChange={(key) => {
const selectedItem = toPlaceSuggestions.items.find(item => item.id === key);
setToPlace(selectedItem || null);
}}
loadingState={toPlaceSuggestions.loadingState}
direction="bottom"
shouldFlip={true}
menuTrigger="input"
width="100%"
>
{(item) => <Item key={item.id}>{item.name}</Item>}
</ComboBox>
{/* Plan Trip Button */}
<Button variant="accent" isPending={isLoading} onPress={HandlePlanTripButton}>
Plan Trip
</Button>
</Flex>
</View>

{/* Display Trip Suggestions */}
{showTrips && (
<View backgroundColor="gray-50" padding="size-300" width="size-6000" marginX="auto" borderRadius="medium">
{error ? (
<Text>{error}</Text>
) : (
<ListBox aria-label="Trip Suggestions" items={trips} selectionMode="single"
onSelectionChange={(key) => {
let selectedKey = Array.from(key)[0];
const selectedTrip = trips.find(trip => trip.id === selectedKey);
setTripDetails(selectedTrip || null);
setShowTripDetails(true);

}}

>
{(item) => (
<Item key={item.id}>
<Text justifySelf="start"> {item.legs.map(leg => leg.routeShortName).join(' • ')}</Text>
<Text justifySelf="end">{item.duration / 60} min </Text>
</Item>
)}
</ListBox>
)}
</View>
)}
</Flex>
<Flex direction="column" justifyContent="center">
{/* Display Trip Details */}
{showTripDetails && (
<View backgroundColor="gray-100" padding="size-300" borderRadius="medium" width="size-6000" marginTop="size-300" maxHeight="size-3600" overflow="auto">
<Content>
<Heading level={4}>{fromPlace?.name} - {toPlace?.name}</Heading>
<Text><strong>Duration:</strong> {formatDuration(Number(tripDetails?.duration))}</Text>
<Divider marginY="size-150" />
{tripDetails?.legs.map((leg: Leg, legIndex: number) => (
<View marginBottom="size-200">
<Text>
<strong>{tripDetails.legs.length > 1 && (legIndex === 0 ? "START: " : legIndex === tripDetails.legs.length - 1 ? " END: " : "")}{leg.routeShortName}</strong>
<br/>{new Date(leg.startTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} to {new Date(leg.endTime).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
<br/>{leg.from.name} to {leg.to.name}
</Text>
<Divider size="S" marginTop="size-150" />
</View>
))}
</Content>
</View>
)};
</Flex>
</Flex>
);
};

export default TripPlanner;
28 changes: 17 additions & 11 deletions src/pages/HomePage.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,43 @@
import React from 'react';
import React, { useState } from 'react';
import TopNavBar from '../components/TopNavBar';
import {Provider, defaultTheme, Grid, View} from "@adobe/react-spectrum";
import { Provider, defaultTheme, Grid, View } from '@adobe/react-spectrum';
import TransitBar from "../components/TransitBar";
import TripPlanner from '../components/TripPlanner';

// Nishil
// Arranges home page layout using react-spectrum and grid
const HomePage: React.FC = () => {
const [selectedView, setSelectedView] = useState('planner');

return (
<Provider theme={defaultTheme}>
<Grid
areas={[
'header header',
'subheader subheader',
'sidebar content',
'header header',
'subheader subheader',
'content content',
'footer footer'
]}
columns={['1fr', '2fr']}
columns={['1fr', '1fr']}
rows={['size-1000', "size-1000", '1fr', 'size-1000']}
height="100vh"
gap="size-0"
>
<View colorVersion={6} borderWidth="thin" backgroundColor="orange-500" gridArea="header">
<View colorVersion={6} borderWidth="thin" backgroundColor="orange-500" gridArea="header">
<TopNavBar />
</View>
<View colorVersion={6} borderWidth="thin" backgroundColor="orange-500" gridArea="subheader">
<TransitBar />
<View colorVersion={6} borderWidth="thin" backgroundColor="orange-500" gridArea="subheader">
<TransitBar onSelect={setSelectedView} />
</View>
<View colorVersion={6} borderWidth="thin" backgroundColor="orange-500" gridArea="content">
{selectedView === 'planner' && <TripPlanner />}
</View>
<View colorVersion={6} borderWidth="thin" backgroundColor="orange-500" gridArea="sidebar" />
<View colorVersion={6} borderWidth="thin" backgroundColor="orange-500" gridArea="content" />
<View colorVersion={6} borderWidth="thin" backgroundColor="orange-500" gridArea="footer" />
</Grid>
</Provider>
);
}

export default HomePage;


0 comments on commit 8b78c6b

Please sign in to comment.