Skip to content

Commit

Permalink
Merge pull request #109 from OneBusAway/feat/trip-plan-logic-implemen…
Browse files Browse the repository at this point in the history
…ation

Feat/trip-plan-logic-implemenation
  • Loading branch information
aaronbrethorst authored Nov 13, 2024
2 parents cc6e658 + 434893f commit e727c38
Show file tree
Hide file tree
Showing 19 changed files with 893 additions and 48 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,9 @@ See `.env.example` for an example of the required keys and values.

### Geocoding

- `PRIVATE_OBA_GEOCODER_API_KEY` - string: Your Geocoder service's API key. Leave this blank if you don't have one.
- `PRIVATE_OBA_GEOCODER_API_KEY` - string: Your Geocoder service's API key. Ensure that the Geocoder and Places API permissions are enabled. Leave this blank if you don't have one.
- `PRIVATE_OBA_GEOCODER_PROVIDER` - string: Your Geocoder service. We currently only support the Google Places SDK (value: "google").


### Trip Planner

- `PUBLIC_OTP_SERVER_URL` - string: Your OpenTripPlanner 1.x-compatible trip planner server URL. Leave this blank if you don't have one.
Expand Down
21 changes: 21 additions & 0 deletions src/components/map/MapView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import { faBus } from '@fortawesome/free-solid-svg-icons';
import { RouteType, routePriorities, prioritizedRouteTypeForDisplay } from '$config/routeConfig';
import { isMapLoaded } from '$src/stores/mapStore';
export let selectedTrip = null;
export let selectedRoute = null;
Expand All @@ -20,6 +21,8 @@
export let showAllStops = true;
export let stop = null;
export let mapProvider = null;
let isTripPlanMoodActive = false;
let selectedStopID = null;
const dispatch = createEventDispatcher();
Expand Down Expand Up @@ -163,6 +166,17 @@
allStops.forEach((s) => addMarker(s));
}
// TODO: prevent fetch stops-for-location if the trip planner mode is on - we should do this after merge.
$: {
if (isTripPlanMoodActive) {
clearAllMarkers();
} else {
if (!selectedRoute || !showRoute) {
allStops.forEach((s) => addMarker(s));
}
}
}
function addMarker(s, routeReference) {
if (!mapInstance) {
console.error('Map not initialized yet');
Expand Down Expand Up @@ -216,8 +230,15 @@
onMount(async () => {
await initMap();
isMapLoaded.set(true);
if (browser) {
const darkMode = document.documentElement.classList.contains('dark');
window.addEventListener('planTripTabClicked', () => {
isTripPlanMoodActive = true;
});
window.addEventListener('tabSwitched', () => {
isTripPlanMoodActive = false;
});
const event = new CustomEvent('themeChange', { detail: { darkMode } });
window.dispatchEvent(event);
}
Expand Down
49 changes: 32 additions & 17 deletions src/components/search/SearchPane.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -9,44 +9,41 @@
import { clearVehicleMarkersMap, fetchAndUpdateVehicles } from '$lib/vehicleUtils';
import { calculateMidpoint } from '$lib/mathUtils';
import { Tabs, TabItem } from 'flowbite-svelte';
import { PUBLIC_OTP_SERVER_URL } from '$env/static/public';
import TripPlan from '$components/trip-planner/TripPlan.svelte';
import { isMapLoaded } from '$src/stores/mapStore';
const dispatch = createEventDispatcher();
export let cssClasses = '';
export let mapProvider = null;
let routes = null;
let stops = null;
let location = null;
let query = null;
let polylines = [];
let currentIntervalId = null;
export let mapProvider = null;
let mapLoaded = false;
function handleLocationClick(location) {
clearResults();
const lat = location.geometry.location.lat;
const lng = location.geometry.location.lng;
mapProvider.panTo(lat, lng);
mapProvider.setZoom(20);
dispatch('locationSelected', { location });
}
function handleStopClick(stop) {
clearResults();
mapProvider.panTo(stop.lat, stop.lon);
mapProvider.setZoom(20);
dispatch('stopSelected', { stop });
}
async function handleRouteClick(route) {
clearResults();
const response = await fetch(`/api/oba/stops-for-route/${route.id}`);
const stopsForRoute = await response.json();
const stops = stopsForRoute.data.references.stops;
Expand All @@ -55,18 +52,15 @@
for (const polylineData of polylinesData) {
const shape = polylineData.points;
let polyline;
polyline = mapProvider.createPolyline(shape);
polylines.push(polyline);
}
await showStopsOnRoute(stops);
currentIntervalId = await fetchAndUpdateVehicles(route.id, mapProvider);
const midpoint = calculateMidpoint(stopsForRoute.data.references.stops);
mapProvider.panTo(midpoint.lat, midpoint.lng);
mapProvider.setZoom(12);
dispatch('routeSelected', { route, stopsForRoute, stops, polylines, currentIntervalId });
}
Expand Down Expand Up @@ -101,16 +95,34 @@
clearInterval(currentIntervalId);
}
function handleTripPlan(event) {
dispatch('tripPlanned', event.detail);
}
function handlePlanTripTabClick() {
const event = new CustomEvent('planTripTabClicked');
window.dispatchEvent(event);
}
function handleTabSwitch() {
const event = new CustomEvent('tabSwitched');
window.dispatchEvent(event);
}
onMount(() => {
isMapLoaded.subscribe((value) => {
mapLoaded = value;
});
window.addEventListener('routeSelectedFromModal', (event) => {
handleRouteClick(event.detail.route);
});
});
</script>

<div class={`modal-pane flex flex-col justify-between md:w-96 ${cssClasses}`}>
<Tabs tabStyle="underline" contentClass="pt-2 pb-4 bg-gray-50 rounded-lg dark:bg-gray-800">
<TabItem open title="Stops and Stations">
<Tabs tabStyle="underline" contentClass="pt-2 pb-4 bg-gray-50 rounded-lg dark:bg-black">
<TabItem open title="Stops and Stations" on:click={handleTabSwitch}>
<SearchField value={query} on:searchResults={handleSearchResults} />

{#if query}
Expand Down Expand Up @@ -158,7 +170,7 @@
<div class="mt-0 sm:mt-0">
<button
type="button"
class="text-sm font-medium text-green-600 underline hover:text-green-400 focus:outline-none"
class="mt-3 text-sm font-medium text-green-600 underline hover:text-green-400 focus:outline-none"
on:click={handleViewAllRoutes}
>
{$t('search.click_here')}
Expand All @@ -168,8 +180,11 @@
>
</div>
</TabItem>
<TabItem title="Plan a Trip">
plan a trip UI goes here!
</TabItem>

{#if PUBLIC_OTP_SERVER_URL}
<TabItem title="Plan a Trip" on:click={handlePlanTripTabClick} disabled={!mapLoaded}>
<TripPlan {mapProvider} on:tripPlanned={handleTripPlan} />
</TabItem>
{/if}
</Tabs>
</div>
27 changes: 27 additions & 0 deletions src/components/trip-planner/ItineraryDetails.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<script>
import LegDetails from './LegDetails.svelte';
import { formatTime } from '$lib/formatters';
export let itinerary;
export let expandedSteps;
export let toggleSteps;
</script>

<div class="mb-4 flex items-center justify-between rounded-lg bg-gray-100 p-4 shadow-md">
<div class="text-center">
<p class="text-sm font-semibold text-gray-500">Duration</p>
<p class="text-lg font-bold text-gray-700">{Math.round(itinerary.duration / 60)} min</p>
</div>
<div class="text-center">
<p class="text-sm font-semibold text-gray-500">Start Time</p>
<p class="text-lg font-bold text-gray-700">{formatTime(itinerary.startTime)}</p>
</div>
<div class="text-center">
<p class="text-sm font-semibold text-gray-500">End Time</p>
<p class="text-lg font-bold text-gray-700">{formatTime(itinerary.endTime)}</p>
</div>
</div>
<div class="space-y-4">
{#each itinerary.legs as leg, index}
<LegDetails {leg} {index} {expandedSteps} {toggleSteps} />
{/each}
</div>
14 changes: 14 additions & 0 deletions src/components/trip-planner/ItineraryTab.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script>
export let index;
export let activeTab;
export let setActiveTab;
</script>

<button
class={`-mb-px px-4 py-2 font-semibold ${
activeTab === index ? 'border-b-2 border-green-500 text-green-500' : 'text-gray-500'
}`}
on:click={() => setActiveTab(index)}
>
Itinerary {index + 1}
</button>
134 changes: 134 additions & 0 deletions src/components/trip-planner/LegDetails.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
<script>
import { formatTime } from '$lib/formatters';
import {
faWalking,
faBus,
faTrain,
faChevronDown,
faChevronUp,
faFerry,
faTrainSubway,
faRulerCombined,
faClock,
faArrowRight,
faArrowAltCircleRight
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
export let leg;
export let index;
export let expandedSteps;
export let toggleSteps;
let icon, iconColor;
let isWalking = leg.mode === 'WALK';
// TODO: Add more icons for different modes of transport
switch (leg.mode) {
case 'WALK':
icon = faWalking;
iconColor = 'text-blue-600';
break;
case 'BUS':
icon = faBus;
iconColor = 'text-green-600';
break;
case 'TRAIN':
icon = faTrain;
iconColor = 'text-red-600';
break;
case 'RAIL':
icon = faTrain;
iconColor = 'text-red-600';
break;
case 'FERRY':
icon = faFerry;
iconColor = 'text-blue-700';
break;
case 'LIGHT_RAIL':
icon = faTrainSubway;
iconColor = 'text-red-600';
break;
default:
icon = null;
}
</script>

<div class="relative flex items-start pb-8">
<div
class="absolute left-5 top-5 border-l-4 border-green-400 {isWalking
? 'border-dotted'
: 'border-gray-300'} h-full"
></div>
<div
class="relative z-10 flex h-12 w-12 items-center justify-center rounded-full bg-gray-200 shadow-md dark:bg-white"
>
{#if icon}
<FontAwesomeIcon {icon} class={iconColor + ' text-xl'} />
{/if}
</div>

<div class="ml-3 mt-3 flex-1">
<div class="mb-2 flex items-center justify-between">
<div class="text-md font-semibold text-gray-800 dark:text-white">{leg.from.name}</div>
</div>

<div class="mt-3 flex space-x-4 text-sm text-gray-600 dark:text-gray-100">
<div class="flex items-center">
<FontAwesomeIcon icon={faClock} class="mr-1 text-blue-500" />
<span class="text-md">Start:</span>
<div class="ml-1 flex items-baseline">
<span class="text-md font-semibold">{formatTime(leg.startTime).slice(0, -3)}</span>
<span class="ml-1 text-xs">{formatTime(leg.startTime).slice(-2)}</span>
</div>
</div>
<div class="flex items-center">
<FontAwesomeIcon icon={faClock} class="mr-1 text-red-500" />
<span class="text-md">End:</span>
<div class="ml-1 flex items-baseline">
<span class="text-md font-semibold">{formatTime(leg.endTime).slice(0, -3)}</span>
<span class="ml-1 text-xs">{formatTime(leg.endTime).slice(-2)}</span>
</div>
</div>
</div>

<div class="mt-4 space-y-4 text-sm text-gray-600 dark:text-gray-100">
<div class="mb-2 flex items-center">
<FontAwesomeIcon icon={faArrowRight} class="mr-2 text-green-500" />
<span class="font-medium">{leg.to.name}</span>
</div>
<div class="mb-2 flex items-center">
<FontAwesomeIcon icon={faRulerCombined} class="mr-2 text-gray-400" />
<span>Distance: {Math.round(leg.distance)} meters</span>
</div>
<div class="mb-4 flex items-center">
<FontAwesomeIcon icon={faClock} class="mr-2 text-gray-400" />
<span>Duration: {Math.round(leg.duration / 60)} minutes</span>
</div>
</div>

{#if isWalking}
<button class="mt-4 flex items-center text-blue-500" on:click={() => toggleSteps(index)}>
<FontAwesomeIcon icon={expandedSteps[index] ? faChevronUp : faChevronDown} class="mr-2" />
{expandedSteps[index] ? 'Hide Steps' : 'Show Steps'}
</button>

{#if expandedSteps[index]}
<div class="mt-4 space-y-2">
{#each leg.steps as step}
<div class="text-sm">
<div class="font-semibold">{step.relativeDirection} on {step.streetName}</div>
<div class="mb-2 flex items-center">
<FontAwesomeIcon icon={faRulerCombined} class="mr-2 text-gray-400" />
<span>Distance: {Math.round(step.distance)} meters</span>
</div>
<div class="flex items-center">
<FontAwesomeIcon icon={faArrowAltCircleRight} class="mr-2 text-gray-400" />
<span>{step.absoluteDirection}</span>
</div>
</div>
{/each}
</div>
{/if}
{/if}
</div>
</div>
Loading

0 comments on commit e727c38

Please sign in to comment.