Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Deploy Website to GitHub Pages

on:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: "pages"
cancel-in-progress: false

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20.x'
cache: 'npm'

- name: Install root dependencies
run: npm ci

- name: Build gtfs-sqljs package
run: npm run build

- name: Install website dependencies
working-directory: ./website
run: npm ci

- name: Build website
working-directory: ./website
run: npm run build

- name: Setup Pages
uses: actions/configure-pages@v4

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: './website/dist'

deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
77 changes: 77 additions & 0 deletions website/demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo - gtfs-sqljs</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<nav class="navbar">
<div class="container">
<h1 class="logo">gtfs-sqljs</h1>
<ul class="nav-links">
<li><a href="/">Documentation</a></li>
<li><a href="/demo.html" class="active">Demo</a></li>
<li><a href="https://github.com/sysdevrun/gtfs-sqljs" target="_blank">GitHub</a></li>
</ul>
</div>
</nav>

<main class="container">
<section class="demo-header">
<h1>Interactive Demo</h1>
<p>Exploring Car Jaune GTFS data (Martinique, France)</p>
</section>

<div id="loading" class="loading">
<div class="spinner"></div>
<p>Loading GTFS data...</p>
</div>

<div id="error" class="error" style="display: none;">
<p>Error loading GTFS data. Please refresh the page.</p>
</div>

<div id="demo-content" style="display: none;">
<section class="demo-section">
<h2>Calendar Information</h2>
<div id="calendar-info" class="calendar-info">
<p>Loading...</p>
</div>
</section>

<section class="demo-section">
<h2>Routes</h2>
<p class="help-text">Click on a route to see its trips</p>
<div id="routes-list" class="routes-list">
<p>Loading routes...</p>
</div>
</section>

<section class="demo-section" id="trips-section" style="display: none;">
<h2>Trips for <span id="selected-route-name"></span></h2>
<p class="help-text">Click on a trip to see stop times</p>
<div id="trips-list" class="trips-list">
<p>Loading trips...</p>
</div>
</section>

<section class="demo-section" id="stop-times-section" style="display: none;">
<h2>Stop Times for <span id="selected-trip-name"></span></h2>
<div id="stop-times-list" class="stop-times-list">
<p>Loading stop times...</p>
</div>
</section>
</div>
</main>

<footer>
<div class="container">
<p>&copy; 2024 Théophile Helleboid / SysDevRun. Licensed under MIT.</p>
</div>
</footer>

<script type="module" src="/demo.ts"></script>
</body>
</html>
247 changes: 247 additions & 0 deletions website/demo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
import initSqlJs from 'sql.js';
import { GtfsSqlJs } from 'gtfs-sqljs';
import type { Route, Trip, StopTime, Calendar } from 'gtfs-sqljs';

let gtfs: GtfsSqlJs;

// Initialize the demo
async function init() {
const loadingEl = document.getElementById('loading')!;
const errorEl = document.getElementById('error')!;
const contentEl = document.getElementById('demo-content')!;

try {
// Initialize SQL.js with CDN WASM file
const SQL = await initSqlJs({
locateFile: (filename) => `https://sql.js.org/dist/${filename}`
});

// Load GTFS data
gtfs = await GtfsSqlJs.fromZip('/car-jaune.zip', { SQL });

// Hide loading, show content
loadingEl.style.display = 'none';
contentEl.style.display = 'block';

// Render initial data
renderCalendarInfo();
renderRoutes();
} catch (error) {
console.error('Error loading GTFS data:', error);
loadingEl.style.display = 'none';
errorEl.style.display = 'block';
}
}

// 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();

if (calendars.length === 0) {
calendarInfoEl.innerHTML = '<p>No calendar data available</p>';
return;
}

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) =>
`<div class="day ${values[i] ? 'active' : 'inactive'}">${day}</div>`
).join('');

return `
<div class="calendar-card">
<h3>${cal.service_id}</h3>
<div class="calendar-days">${daysHtml}</div>
<p style="font-size: 0.875rem; color: var(--text-secondary); margin-top: 0.5rem;">
${formatDate(cal.start_date)} - ${formatDate(cal.end_date)}
</p>
</div>
`;
}).join('');

calendarInfoEl.innerHTML = html;
}

// Render routes list
function renderRoutes() {
const routesListEl = document.getElementById('routes-list')!;
const routes = gtfs.getAllRoutes();

if (routes.length === 0) {
routesListEl.innerHTML = '<p>No routes found</p>';
return;
}

const html = routes.map(route => {
const bgColor = route.route_color ? `#${route.route_color}` : '#64748b';
const textColor = route.route_text_color ? `#${route.route_text_color}` : getContrastColor(bgColor);

return `
<div class="route-card"
data-route-id="${route.route_id}"
style="background-color: ${bgColor}; color: ${textColor};"
onclick="showTrips('${route.route_id}', '${escapeHtml(route.route_short_name)} - ${escapeHtml(route.route_long_name)}')">
<div class="route-number">${escapeHtml(route.route_short_name)}</div>
<div class="route-name">${escapeHtml(route.route_long_name)}</div>
</div>
`;
}).join('');

routesListEl.innerHTML = html;
}

// Show trips for a route
(window as any).showTrips = function(routeId: string, routeName: string) {
const tripsSectionEl = document.getElementById('trips-section')!;
const tripsListEl = document.getElementById('trips-list')!;
const selectedRouteNameEl = document.getElementById('selected-route-name')!;
const stopTimesSectionEl = document.getElementById('stop-times-section')!;

// Hide stop times section
stopTimesSectionEl.style.display = 'none';

// Get trips for this route
const trips = gtfs.getTripsByRoute(routeId);

if (trips.length === 0) {
tripsListEl.innerHTML = '<p>No trips found for this route</p>';
tripsSectionEl.style.display = 'block';
selectedRouteNameEl.textContent = routeName;
return;
}

// Render trips
const html = trips.map(trip => {
const headsign = trip.trip_headsign || 'No headsign';
const direction = trip.direction_id !== undefined ? `Direction ${trip.direction_id}` : '';
const service = trip.service_id;

return `
<div class="trip-card" onclick="showStopTimes('${trip.trip_id}', '${escapeHtml(headsign)}')">
<div class="trip-info">
<div class="trip-headsign">${escapeHtml(headsign)}</div>
<div class="trip-meta">
Trip ID: ${escapeHtml(trip.trip_id)}
${direction ? `• ${direction}` : ''}
• Service: ${escapeHtml(service)}
</div>
</div>
<div>→</div>
</div>
`;
}).join('');

tripsListEl.innerHTML = html;
tripsSectionEl.style.display = 'block';
selectedRouteNameEl.textContent = routeName;

// Scroll to trips section
tripsSectionEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
};

// Show stop times for a trip
(window as any).showStopTimes = function(tripId: string, tripName: string) {
const stopTimesSectionEl = document.getElementById('stop-times-section')!;
const stopTimesListEl = document.getElementById('stop-times-list')!;
const selectedTripNameEl = document.getElementById('selected-trip-name')!;

// Get stop times for this trip
const stopTimes = gtfs.getStopTimesByTrip(tripId);

if (stopTimes.length === 0) {
stopTimesListEl.innerHTML = '<p>No stop times found for this trip</p>';
stopTimesSectionEl.style.display = 'block';
selectedTripNameEl.textContent = tripName;
return;
}

// Render stop times with stop names
const html = stopTimes.map(st => {
const stop = gtfs.getStopById(st.stop_id);
const stopName = stop ? stop.stop_name : st.stop_id;

return `
<div class="stop-time-item">
<div class="sequence">#${st.stop_sequence}</div>
<div class="time">${st.arrival_time}</div>
<div class="stop-name">${escapeHtml(stopName)}</div>
</div>
`;
}).join('');

stopTimesListEl.innerHTML = html;
stopTimesSectionEl.style.display = 'block';
selectedTripNameEl.textContent = tripName;

// Scroll to stop times section
stopTimesSectionEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
};

// 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;
return div.innerHTML;
}

function getContrastColor(hexColor: string): string {
// Remove # if present
const hex = hexColor.replace('#', '');

// Convert to RGB
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);

// Calculate luminance
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;

// Return black or white based on luminance
return luminance > 0.5 ? '#000000' : '#ffffff';
}

// Start the demo
init();
Loading