Skip to content

Commit

Permalink
Merge pull request #127 from OneBusAway/feature/schedule-visualization
Browse files Browse the repository at this point in the history
Feature/schedule-visualization
  • Loading branch information
aaronbrethorst authored Nov 23, 2024
2 parents d953ff9 + f837084 commit e8f0216
Show file tree
Hide file tree
Showing 17 changed files with 538 additions and 22 deletions.
2 changes: 1 addition & 1 deletion src/components/ArrivalDeparture.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
};
} else if (scheduledDiff <= 0) {
return {
status: 'on time',
status: `${$t('status.on_time')}`,
text: `${$t('status.arrives_on_time')}`,
color: 'text-green-500'
};
Expand Down
10 changes: 2 additions & 8 deletions src/components/oba/TripDetailsPane.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { onMount, onDestroy } from 'svelte';
import { faBus, faPersonWalking } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
import { convertUnixToTime } from '$lib/formatters';
export let stop;
export let tripId;
Expand All @@ -14,13 +15,6 @@
let interval;
let busPosition = 0;
function formatTime(seconds) {
if (!seconds) return '';
const date = new Date(seconds * 1000);
const utcDate = new Date(date.toUTCString().slice(0, -4));
return utcDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
function calculateBusPosition() {
if (tripDetails && tripDetails.status && tripDetails.status.position) {
const { lat, lon } = tripDetails.status.position;
Expand Down Expand Up @@ -118,7 +112,7 @@
<div class="text-md font-semibold text-[#000000] dark:text-white">
{stopInfo[tripStop.stopId] ? stopInfo[tripStop.stopId].name : tripStop.stopId}
</div>
<div class="text-sm text-[#86858B]">{formatTime(tripStop.arrivalTime)}</div>
<div class="text-sm text-[#86858B]">{convertUnixToTime(tripStop.arrivalTime)}</div>
</div>
</div>
{/each}
Expand Down
114 changes: 114 additions & 0 deletions src/components/schedule-for-stop/ScheduleAccordionItem.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<script>
import { createEventDispatcher } from 'svelte';
import { AccordionItem } from 'flowbite-svelte';
import { t } from 'svelte-i18n';
export let schedule;
export let expanded;
const dispatch = createEventDispatcher();
function handleToggle() {
dispatch('toggle');
}
function formatHour(hour) {
const hourInt = +hour;
if (hourInt === 0) return '12';
if (hourInt > 12) return hourInt - 12;
return hourInt;
}
function renderScheduleTable(schedule) {
const stopTimes = Object.entries(schedule.stopTimes);
const amTimes = stopTimes.filter(([hour]) => +hour < 12);
const pmTimes = stopTimes.filter(([hour]) => +hour >= 12);
return {
amTimes,
pmTimes
};
}
function extractMinutes(arrivalTime) {
return arrivalTime.replace(/[AP]M/, '').split(':')[1];
}
</script>

<AccordionItem open={expanded} on:click={handleToggle}>
<span slot="header" class="text-lg font-semibold text-gray-800">
{schedule.tripHeadsign}
</span>
<div class="overflow-x-auto">
<table class="mt-4 w-full table-auto rounded-lg border border-gray-200 shadow-lg">
<thead class="bg-gray-100 text-gray-800">
<tr>
<th class="cursor-pointer px-6 py-3 text-left">{$t('schedule_for_stop.hour')}</th>
<th class="cursor-pointer px-6 py-3 text-left">{$t('schedule_for_stop.minutes')}</th>
</tr>
</thead>
<tbody>
<tr class="bg-gray-50 hover:bg-gray-100">
<td colspan="2" class="px-6 py-3 font-semibold text-gray-700">AM</td>
</tr>
{#if renderScheduleTable(schedule).amTimes.length === 0}
<tr>
<td colspan="2" class="border px-6 py-3 text-center text-gray-500">
{$t('schedule_for_stop.no_am_schedules_available')}
</td>
</tr>
{:else}
{#each renderScheduleTable(schedule).amTimes as [hour, times]}
<tr class="hover:bg-gray-100">
<td
class="border px-6 py-3 text-center text-lg font-semibold"
title="Full Time: {hour}:{extractMinutes(times[0].arrivalTime)}"
>
{formatHour(hour)} <span class="text-sm text-gray-600">AM</span>
</td>
<td class="border px-6 py-3 text-lg">
{#each times as stopTime, index (index)}
<span>
{extractMinutes(stopTime.arrivalTime)}
{index < times.length - 1 ? ', ' : ''}
</span>
{/each}
</td>
</tr>
{/each}
{/if}

<tr class="bg-gray-50 hover:bg-gray-100">
<td colspan="2" class="px-6 py-3 font-semibold text-gray-700">PM</td>
</tr>
{#if renderScheduleTable(schedule).pmTimes.length === 0}
<tr>
<td colspan="2" class="border px-6 py-3 text-center text-gray-500">
{$t('schedule_for_stop.no_pm_schedules_available')}
</td>
</tr>
{:else}
{#each renderScheduleTable(schedule).pmTimes as [hour, times]}
<tr class="hover:bg-gray-100">
<td
class="border px-6 py-3 text-center text-lg font-semibold"
title="Full Time: {hour}:{extractMinutes(times[0].arrivalTime)}"
>
{formatHour(hour)} <span class="text-sm text-gray-600">PM</span>
</td>
<td class="border px-6 py-3 text-lg">
{#each times as stopTime, index (index)}
<span>
{extractMinutes(stopTime.arrivalTime)}
{index < times.length - 1 ? ', ' : ''}
</span>
{/each}
</td>
</tr>
{/each}
{/if}
</tbody>
</table>
</div>
</AccordionItem>
35 changes: 35 additions & 0 deletions src/components/schedule-for-stop/StopDetailsHeader.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<script>
import { faBus, faMapMarkerAlt, faArrowRight } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
import { t } from 'svelte-i18n';
export let stopName;
export let stopId;
export let stopDirection;
</script>

<div class="mb-6 rounded-lg bg-white p-6 text-center shadow-lg">
<h1 class="flex items-center justify-center gap-2 text-3xl font-bold text-green-700">
<FontAwesomeIcon icon={faBus} />
{$t('schedule_for_stop.stop_details')}
</h1>
<p class="mt-2 flex items-center justify-center gap-2 text-lg text-gray-700">
<FontAwesomeIcon icon={faMapMarkerAlt} />
<strong>{$t('schedule_for_stop.stop_name')}:</strong>
{stopName} | <strong>{$t('schedule_for_stop.stop_id')}:</strong>
{stopId}
</p>
<p class="mt-2 flex items-center justify-center gap-2 text-lg text-gray-700">
<FontAwesomeIcon icon={faArrowRight} />
<strong>{$t('schedule_for_stop.direction')}:</strong>
{stopDirection}
</p>
<div class="mt-4 flex justify-center">
<a
href={`/stops/${stopId}`}
class="flex items-center justify-center gap-2 rounded-lg bg-green-500 px-3 py-1 text-white shadow hover:bg-green-600"
>
<FontAwesomeIcon icon={faMapMarkerAlt} />
{$t('schedule_for_stop.click_for_realtime')}
</a>
</div>
</div>
11 changes: 10 additions & 1 deletion src/components/stops/StopPane.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,21 @@
{#if arrivalsAndDepartures}
<div class="space-y-4">
<div>
<div class="flex flex-col gap-y-1 rounded-lg bg-[#1C1C1E] bg-opacity-80 p-4">
<div class="relative flex flex-col gap-y-1 rounded-lg bg-[#1C1C1E] bg-opacity-80 p-4">
<h1 class="h1 mb-0 text-white">{stop.name}</h1>
<h2 class="h2 mb-0 text-white">{$t('stop')} #{stop.id}</h2>
{#if routeShortNames()}
<h2 class="h2 mb-0 text-white">{$t('routes')}: {routeShortNames().join(', ')}</h2>
{/if}
<div class="mt-auto flex justify-end">
<a
href={`/stops/${stop.id}/schedule`}
class="inline-block rounded-lg border border-green-500 bg-green-500 px-3 py-1 text-sm font-medium text-white shadow-md transition duration-200 ease-in-out hover:bg-green-600"
target="_blank"
>
{$t('schedule_for_stop.view_schedule')}
</a>
</div>
</div>
</div>
{#if arrivalsAndDepartures.arrivalsAndDepartures.length === 0}
Expand Down
7 changes: 7 additions & 0 deletions src/lib/formatters.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,10 @@ export function formatTime(dateString) {
hour12: true
});
}

export function convertUnixToTime(seconds) {
if (!seconds) return '';
const date = new Date(seconds * 1000);
const utcDate = new Date(date.toUTCString().slice(0, -4));
return utcDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
30 changes: 18 additions & 12 deletions src/lib/i18n.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
import { init, register, getLocaleFromNavigator } from 'svelte-i18n';

register('en', () => import('./../locales/en.json')); // English
register('es', () => import('./../locales/es.json')); // Spanish
register('pl', () => import('./../locales/pl.json')); // Polish
register('vi', () => import('./../locales/vi.json')); // Vietnamese
register('tl', () => import('./../locales/tl.json')); // Tagalog
register('so', () => import('./../locales/so.json')); // Somali
register('am', () => import('./../locales/am.json')); // Amharic
register('ar', () => import('./../locales/ar.json')); // Arabic
async function setup() {
register('en', () => import('./../locales/en.json')); // English
register('es', () => import('./../locales/es.json')); // Spanish
register('pl', () => import('./../locales/pl.json')); // Polish
register('vi', () => import('./../locales/vi.json')); // Vietnamese
register('tl', () => import('./../locales/tl.json')); // Tagalog
register('so', () => import('./../locales/so.json')); // Somali
register('am', () => import('./../locales/am.json')); // Amharic
register('ar', () => import('./../locales/ar.json')); // Arabic

init({
fallbackLocale: 'en',
initialLocale: getLocaleFromNavigator()
});
return await Promise.allSettled([
init({
fallbackLocale: 'en',
initialLocale: getLocaleFromNavigator()
})
]);
}

await setup();
18 changes: 18 additions & 0 deletions src/locales/am.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,23 @@
"data_updated": "የተሻሻለ ውሂብ",
"no_real_time_data": "ለዚህ ተሽከርካሪ በእሁድኛ ጊዜ ውሂብ አናስተውላም።",
"next_stop": "ቀጣይ ማቆሚያ:"
},
"schedule_for_stop": {
"view_schedule": "ጊዜ ሰንጠረዥን ይመልከቱ",
"no_am_schedules_available": "ምንም AM ጊዜ ሰንጠረዦች አልተገኙም",
"no_pm_schedules_available": "ምንም PM ጊዜ ሰንጠረዦች አልተገኙም",
"minutes": "ደቂቃዎች",
"hour": "ሰዓት",
"selected_date": "የተመረጠ ቀን",
"show_all_routes": "ሁሉንም መንገዶች አሳይ",
"collapse_all_routes": "ሁሉንም መንገዶች አጠፋ",
"route_schedules": "የመንገድ ጊዜ ሰንጠረዦች",
"no_schedules_available": "ምንም ጊዜ ሰንጠረዦች ለተመረጠው ቀን አልተገኙም።",
"stop_details": "የማቆሚያ ዝርዝሮች",
"select_date": "ቀን ይምረጡ",
"stop_id": "የማቆሚያ መታወቂያ",
"direction": "አቅጣጫ",
"click_for_realtime": "ለትክክለኛ ጊዜ መረጃ እዚህ ጠቅ ያድርጉ",
"stop_name": "የማቆሚያ ስም"
}
}
18 changes: 18 additions & 0 deletions src/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,23 @@
"data_updated": "تم تحديث البيانات",
"no_real_time_data": "لا توجد بيانات في الوقت الحقيقي لهذه المركبة الآن.",
"next_stop": "المحطة التالية:"
},
"schedule_for_stop": {
"view_schedule": "عرض الجدول",
"no_am_schedules_available": "لا توجد جداول صباحية متاحة",
"no_pm_schedules_available": "لا توجد جداول مسائية متاحة",
"minutes": "دقائق",
"hour": "ساعة",
"selected_date": "التاريخ المحدد",
"show_all_routes": "عرض جميع المسارات",
"collapse_all_routes": "إخفاء جميع المسارات",
"route_schedules": "جداول المسارات",
"no_schedules_available": "لا توجد جداول متاحة للتاريخ المحدد.",
"stop_details": "تفاصيل المحطة",
"select_date": "اختر التاريخ",
"stop_id": "رقم المحطة",
"direction": "الاتجاه",
"click_for_realtime": "انقر هنا لمعلومات الوقت الفعلي",
"stop_name": "اسم المحطة"
}
}
18 changes: 18 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,23 @@
"data_updated": "Data updated",
"no_real_time_data": "We don't have real-time data for this vehicle now.",
"next_stop": "Next stop:"
},
"schedule_for_stop": {
"view_schedule": "View Schedule",
"no_am_schedules_available": "No AM schedules available",
"no_pm_schedules_available": "No PM schedules available",
"minutes": "Minutes",
"hour": "Hour",
"selected_date": "Selected date",
"show_all_routes": "Show All Routes",
"collapse_all_routes": "Collapse All Routes",
"route_schedules": "Route Schedules",
"no_schedules_available": "No schedules available for the selected date.",
"stop_details": "Stop Details",
"select_date": "Select Date",
"stop_id": "Stop ID",
"direction": "Direction",
"click_for_realtime": "Click here for real-time info",
"stop_name": "Stop Name"
}
}
18 changes: 18 additions & 0 deletions src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,23 @@
"data_updated": "Datos actualizados",
"no_real_time_data": "Actualmente no tenemos datos en tiempo real para este vehículo.",
"next_stop": "Próxima parada:"
},
"schedule_for_stop": {
"view_schedule": "Ver horario",
"no_am_schedules_available": "No hay horarios AM disponibles",
"no_pm_schedules_available": "No hay horarios PM disponibles",
"minutes": "Minutos",
"hour": "Hora",
"selected_date": "Fecha seleccionada",
"show_all_routes": "Mostrar todas las rutas",
"collapse_all_routes": "Colapsar todas las rutas",
"route_schedules": "Horarios de rutas",
"no_schedules_available": "No hay horarios disponibles para la fecha seleccionada.",
"stop_details": "Detalles de la parada",
"select_date": "Seleccionar fecha",
"stop_id": "ID de la parada",
"direction": "Dirección",
"click_for_realtime": "Haga clic aquí para información en tiempo real",
"stop_name": "Nombre de la parada"
}
}
18 changes: 18 additions & 0 deletions src/locales/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,23 @@
"data_updated": "Dane zaktualizowane",
"no_real_time_data": "Obecnie nie mamy danych w czasie rzeczywistym dla tego pojazdu.",
"next_stop": "Następny przystanek:"
},
"schedule_for_stop": {
"view_schedule": "Zobacz rozkład",
"no_am_schedules_available": "Brak dostępnych rozkładów AM",
"no_pm_schedules_available": "Brak dostępnych rozkładów PM",
"minutes": "Minuty",
"hour": "Godzina",
"selected_date": "Wybrana data",
"show_all_routes": "Pokaż wszystkie trasy",
"collapse_all_routes": "Zwiń wszystkie trasy",
"route_schedules": "Rozkłady tras",
"no_schedules_available": "Brak dostępnych rozkładów na wybraną datę.",
"stop_details": "Szczegóły przystanku",
"select_date": "Wybierz datę",
"stop_id": "ID przystanku",
"direction": "Kierunek",
"click_for_realtime": "Kliknij tutaj, aby zobaczyć informacje w czasie rzeczywistym",
"stop_name": "Nazwa przystanku"
}
}
Loading

0 comments on commit e8f0216

Please sign in to comment.