Interactive Demo
-Exploring Car Jaune GTFS data (Martinique, France)
+Exploring Car Jaune GTFS data (La Réunion, France)
+Operated by Transdev Outre-Mer and GIE Activ'
@@ -35,15 +36,18 @@
-
Interactive Demo
Calendar Information
-
+
diff --git a/website/demo.ts b/website/demo.ts
index 37d3997..45b2a11 100644
--- a/website/demo.ts
+++ b/website/demo.ts
@@ -1,8 +1,9 @@
import initSqlJs from 'sql.js';
import { GtfsSqlJs } from 'gtfs-sqljs';
-import type { Route, Trip, StopTime, Calendar } from 'gtfs-sqljs';
+import type { Route, Trip, StopTime } from 'gtfs-sqljs';
let gtfs: GtfsSqlJs;
+let selectedDate: string;
// Initialize the demo
async function init() {
@@ -23,8 +24,11 @@ async function init() {
loadingEl.style.display = 'none';
contentEl.style.display = 'block';
+ // Initialize date picker with today's date
+ initDatePicker();
+
// Render initial data
- renderCalendarInfo();
+ renderActiveCalendars();
renderRoutes();
} catch (error) {
console.error('Error loading GTFS data:', error);
@@ -33,64 +37,55 @@ async function init() {
}
}
-// Render calendar information
-function renderCalendarInfo() {
- const calendarInfoEl = document.getElementById('calendar-info')!;
-
- const db = gtfs.getDatabase();
- const stmt = db.prepare('SELECT * FROM calendar');
-
- const calendars: Calendar[] = [];
- while (stmt.step()) {
- const row = stmt.getAsObject();
- calendars.push({
- service_id: String(row.service_id),
- monday: Number(row.monday),
- tuesday: Number(row.tuesday),
- wednesday: Number(row.wednesday),
- thursday: Number(row.thursday),
- friday: Number(row.friday),
- saturday: Number(row.saturday),
- sunday: Number(row.sunday),
- start_date: String(row.start_date),
- end_date: String(row.end_date),
- });
- }
- stmt.free();
+// Initialize date picker
+function initDatePicker() {
+ const dateInput = document.getElementById('date-input') as HTMLInputElement;
+
+ // Set to today's date
+ const today = new Date();
+ const year = today.getFullYear();
+ const month = String(today.getMonth() + 1).padStart(2, '0');
+ const day = String(today.getDate()).padStart(2, '0');
+ const todayStr = `${year}-${month}-${day}`;
+
+ dateInput.value = todayStr;
+ selectedDate = `${year}${month}${day}`; // YYYYMMDD format for GTFS
+
+ // Listen for date changes
+ dateInput.addEventListener('change', (e) => {
+ const target = e.target as HTMLInputElement;
+ const [y, m, d] = target.value.split('-');
+ selectedDate = `${y}${m}${d}`;
+ renderActiveCalendars();
+
+ // Reset trips section
+ document.getElementById('trips-section')!.style.display = 'none';
+ document.getElementById('stop-times-section')!.style.display = 'none';
+ });
+}
- if (calendars.length === 0) {
- calendarInfoEl.innerHTML = '();
+
+ trips.forEach(trip => {
const headsign = trip.trip_headsign || 'No headsign';
- const direction = trip.direction_id !== undefined ? `Direction ${trip.direction_id}` : '';
- const service = trip.service_id;
+ const directionId = trip.direction_id ?? 0;
+ const key = `${headsign}|${directionId}`;
+
+ if (!groupMap.has(key)) {
+ groupMap.set(key, {
+ headsign,
+ directionId,
+ trips: []
+ });
+ }
+
+ groupMap.get(key)!.trips.push(trip);
+ });
+
+ // Sort groups by direction, then by headsign
+ const groups = Array.from(groupMap.values()).sort((a, b) => {
+ if (a.directionId !== b.directionId) {
+ return a.directionId - b.directionId;
+ }
+ return a.headsign.localeCompare(b.headsign);
+ });
+
+ // Render grouped trips
+ const html = groups.map(group => {
+ const directionLabel = `Direction ${group.directionId}`;
+ const tripCount = group.trips.length;
return `
-
Select Date
+
+
+
+ Loading...
Routes
-Click on a route to see its trips
+Click on a route to see its trips for the selected date
Loading routes...
No calendar data available
'; - return; - } +// Render active calendars for selected date +function renderActiveCalendars() { + const activeCalendarsEl = document.getElementById('active-calendars')!; - const html = calendars.map(cal => { - const days = ['M', 'T', 'W', 'T', 'F', 'S', 'S']; - const values = [ - cal.monday, - cal.tuesday, - cal.wednesday, - cal.thursday, - cal.friday, - cal.saturday, - cal.sunday - ]; - - const daysHtml = days.map((day, i) => - `${day}
`
- ).join('');
+ try {
+ const serviceIds = gtfs.getActiveServiceIds(selectedDate);
- return `
-
-
- `;
- }).join('');
+ if (serviceIds.length === 0) {
+ activeCalendarsEl.innerHTML = '${cal.service_id}
-${daysHtml}
- - ${formatDate(cal.start_date)} - ${formatDate(cal.end_date)} -
-No active service calendars for this date
'; + return; + } - calendarInfoEl.innerHTML = html; + const html = ` ++ Active calendars: ${serviceIds.join(', ')} +
+ `; + activeCalendarsEl.innerHTML = html; + } catch (error) { + console.error('Error getting active calendars:', error); + activeCalendarsEl.innerHTML = 'Error loading calendar information
'; + } } // Render routes list @@ -131,33 +126,74 @@ function renderRoutes() { // Hide stop times section stopTimesSectionEl.style.display = 'none'; - // Get trips for this route - const trips = gtfs.getTripsByRoute(routeId); + // Get trips for this route on the selected date + const trips = gtfs.getTrips({ + routeId: routeId, + date: selectedDate + }); if (trips.length === 0) { - tripsListEl.innerHTML = 'No trips found for this route
'; + tripsListEl.innerHTML = 'No trips found for this route on the selected date
'; tripsSectionEl.style.display = 'block'; selectedRouteNameEl.textContent = routeName; return; } - // Render trips - const html = trips.map(trip => { + // Group trips by headsign and direction + interface TripGroup { + headsign: string; + directionId: number; + trips: Trip[]; + } + + const groupMap = new Map
-
-
${escapeHtml(headsign)}
-
- Trip ID: ${escapeHtml(trip.trip_id)}
- ${direction ? `• ${direction}` : ''}
- • Service: ${escapeHtml(service)}
-
+
+
`;
}).join('');
@@ -210,17 +246,6 @@ function renderRoutes() {
// Utility functions
-function formatDate(dateStr: string): string {
- // YYYYMMDD -> DD/MM/YYYY
- if (dateStr.length === 8) {
- const year = dateStr.substring(0, 4);
- const month = dateStr.substring(4, 6);
- const day = dateStr.substring(6, 8);
- return `${day}/${month}/${year}`;
- }
- return dateStr;
-}
-
function escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
diff --git a/website/style.css b/website/style.css
index 7e05a0f..61225ab 100644
--- a/website/style.css
+++ b/website/style.css
@@ -203,6 +203,12 @@ footer {
margin-bottom: 2rem;
}
+.operator-info {
+ color: var(--text-secondary);
+ font-size: 0.95rem;
+ margin-top: 0.5rem;
+}
+
.loading {
display: flex;
flex-direction: column;
@@ -248,6 +254,36 @@ footer {
margin-bottom: 1rem;
}
+.date-selector {
+ margin-bottom: 1.5rem;
+}
+
+.date-selector input[type="date"] {
+ padding: 0.75rem 1rem;
+ border: 2px solid var(--border);
+ border-radius: 0.5rem;
+ font-size: 1rem;
+ font-family: inherit;
+ width: 100%;
+ max-width: 300px;
+ transition: border-color 0.2s;
+}
+
+.date-selector input[type="date"]:focus {
+ outline: none;
+ border-color: var(--primary-color);
+}
+
+.active-calendars {
+ margin-top: 1rem;
+}
+
+.info-text {
+ color: var(--text-secondary);
+ font-size: 0.95rem;
+ line-height: 1.6;
+}
+
.calendar-info {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
@@ -327,14 +363,42 @@ footer {
.trips-list {
display: grid;
- gap: 0.5rem;
+ gap: 1.5rem;
}
-.trip-card {
+.trip-group {
background: white;
- padding: 1rem;
border-radius: 0.5rem;
border: 1px solid var(--border);
+ overflow: hidden;
+}
+
+.trip-group-header {
+ background: var(--surface);
+ padding: 1rem;
+ border-bottom: 1px solid var(--border);
+}
+
+.trip-headsign {
+ font-weight: 600;
+ font-size: 1.1rem;
+ margin-bottom: 0.25rem;
+}
+
+.trip-meta {
+ font-size: 0.875rem;
+ color: var(--text-secondary);
+}
+
+.trip-items {
+ display: grid;
+ gap: 0;
+}
+
+.trip-card {
+ background: white;
+ padding: 1rem;
+ border-bottom: 1px solid var(--border);
cursor: pointer;
transition: all 0.2s;
display: flex;
@@ -342,21 +406,24 @@ footer {
align-items: center;
}
+.trip-card:last-child {
+ border-bottom: none;
+}
+
.trip-card:hover {
- border-color: var(--primary-color);
- box-shadow: var(--shadow);
+ background: var(--surface);
}
.trip-info {
flex: 1;
}
-.trip-headsign {
- font-weight: 600;
+.trip-id {
+ font-size: 0.95rem;
margin-bottom: 0.25rem;
}
-.trip-meta {
+.trip-service {
font-size: 0.875rem;
color: var(--text-secondary);
}
+
+ ${escapeHtml(group.headsign)}
+ ${directionLabel} • ${tripCount} trip${tripCount > 1 ? 's' : ''}
+
+ ${group.trips.map(trip => `
+
-
+
+ `).join('')}
+
+ Trip: ${escapeHtml(trip.trip_id)}
+ Service: ${escapeHtml(trip.service_id)}
+ →
+ →