From 345cf27adb315a594fdd2aaa204fdb30ffcf5335 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Nov 2025 16:54:57 +0000 Subject: [PATCH 1/2] Major API refactoring: Remove convenience methods, add agency support, add file skipping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BREAKING CHANGES: - Removed redundant convenience methods that can be replaced with flexible API - Kept only: getById methods, flexible filter methods (get* with filters), and essential calendar methods New Features: - Add agency query methods (getAgencies, getAgencyById) - Add agencyId filter to routes, trips, and stop times queries - Add skipFiles option to fromZip to skip importing specific files (tables still created) - Demo now skips shapes.txt import for faster loading - Add SQLite database download button in demo API Changes: Removed methods (use flexible API instead): - getAllStops() → getStops() - searchStopsByName(name) → getStops({ name }) - getStopByCode(code) → getStops({ stopCode: code })[0] - getAllRoutes() → getRoutes() - getRoutesByAgency(id) → getRoutes({ agencyId: id }) - getTripsByRoute(id) → getTrips({ routeId: id }) - getTripsByRouteAndDate(id, date) → getTrips({ routeId: id, date }) - getTripsByRouteAndDateAndDirection() → getTrips({ routeId, date, directionId }) - getTripsByDate(date) → getTrips({ date }) - getStopsByTrip(id) → getStops({ tripId: id }) - getStopTimesByStop(id, limit) → getStopTimes({ stopId: id, limit }) - getStopTimesForStopRouteAndDate() → getStopTimes({ stopId, routeId, date, directionId }) Kept methods: - getAgencyById, getAgencies (new) - getStopById, getStops - getRouteById, getRoutes - getTripById, getTrips - getStopTimesByTrip, getStopTimes - All calendar methods (getActiveServiceIds, getCalendarByServiceId, etc.) All 77 tests pass with updated API usage. --- src/gtfs-sqljs.ts | 143 ++--------- src/index.ts | 2 +- src/loaders/data-loader.ts | 12 +- src/queries/agencies.ts | 105 ++++++++ src/queries/stop-times.ts | 24 +- src/queries/trips.ts | 24 +- tests/gtfs-sqljs.test.ts | 24 +- tests/gtfs-sqljs.test.ts.bak | 172 +++++++++++++ tests/sample-feed.test.ts | 38 +-- tests/sample-feed.test.ts.bak | 467 ++++++++++++++++++++++++++++++++++ website/demo.html | 6 + website/demo.ts | 36 ++- website/style.css | 14 + 13 files changed, 906 insertions(+), 161 deletions(-) create mode 100644 src/queries/agencies.ts create mode 100644 tests/gtfs-sqljs.test.ts.bak create mode 100644 tests/sample-feed.test.ts.bak diff --git a/src/gtfs-sqljs.ts b/src/gtfs-sqljs.ts index e6c176f..1eee858 100644 --- a/src/gtfs-sqljs.ts +++ b/src/gtfs-sqljs.ts @@ -8,36 +8,23 @@ import { loadGTFSZip } from './loaders/zip-loader'; import { loadGTFSData } from './loaders/data-loader'; // Query methods -import { getStopById, getStopByCode, searchStopsByName, getAllStops, getStopsByTrip, getStops, type StopFilters } from './queries/stops'; -import { getRouteById, getAllRoutes, getRoutesByAgency, getRoutes, type RouteFilters } from './queries/routes'; +import { getAgencyById, getAgencies, type AgencyFilters } from './queries/agencies'; +import { getStopById, getStops, type StopFilters } from './queries/stops'; +import { getRouteById, getRoutes, type RouteFilters } from './queries/routes'; import { getActiveServiceIds, getCalendarByServiceId, getCalendarDates, getCalendarDatesForDate, } from './queries/calendar'; -import { - getTripById, - getTripsByRoute, - getTripsByRouteAndService, - getTripsByRouteServiceAndDirection, - getTripsByService, - getTrips, - type TripFilters, -} from './queries/trips'; -import { - getStopTimesByTrip, - getStopTimesByStop, - getStopTimesForStopRouteDirection, - getStopTimes, - type StopTimeFilters, -} from './queries/stop-times'; +import { getTripById, getTrips, type TripFilters } from './queries/trips'; +import { getStopTimesByTrip, getStopTimes, type StopTimeFilters } from './queries/stop-times'; // Types -import type { Stop, Route, Trip, StopTime, Calendar, CalendarDate } from './types/gtfs'; +import type { Agency, Stop, Route, Trip, StopTime, Calendar, CalendarDate } from './types/gtfs'; // Export filter types for users -export type { StopFilters, RouteFilters, TripFilters, StopTimeFilters }; +export type { AgencyFilters, StopFilters, RouteFilters, TripFilters, StopTimeFilters }; export interface GtfsSqlJsOptions { /** @@ -59,6 +46,12 @@ export interface GtfsSqlJsOptions { * Optional: Path to SQL.js WASM file (for custom loading) */ locateFile?: (filename: string) => string; + + /** + * Optional: Array of GTFS filenames to skip importing (e.g., ['shapes.txt']) + * Tables will be created but no data will be imported for these files + */ + skipFiles?: string[]; } export class GtfsSqlJs { @@ -112,7 +105,7 @@ export class GtfsSqlJs { // Load GTFS data const files = await loadGTFSZip(zipPath); - await loadGTFSData(this.db, files); + await loadGTFSData(this.db, files, options.skipFiles); } /** @@ -164,38 +157,32 @@ export class GtfsSqlJs { return this.db; } - // ==================== Stop Methods ==================== + // ==================== Agency Methods ==================== /** - * Get a stop by its stop_id + * Get an agency by its agency_id */ - getStopById(stopId: string): Stop | null { + getAgencyById(agencyId: string): Agency | null { if (!this.db) throw new Error('Database not initialized'); - return getStopById(this.db, stopId); + return getAgencyById(this.db, agencyId); } /** - * Get a stop by its stop_code + * Get agencies with optional filters */ - getStopByCode(stopCode: string): Stop | null { + getAgencies(filters?: AgencyFilters): Agency[] { if (!this.db) throw new Error('Database not initialized'); - return getStopByCode(this.db, stopCode); + return getAgencies(this.db, filters); } - /** - * Search stops by name (case-insensitive, partial match) - */ - searchStopsByName(name: string, limit = 50): Stop[] { - if (!this.db) throw new Error('Database not initialized'); - return searchStopsByName(this.db, name, limit); - } + // ==================== Stop Methods ==================== /** - * Get all stops + * Get a stop by its stop_id */ - getAllStops(limit?: number): Stop[] { + getStopById(stopId: string): Stop | null { if (!this.db) throw new Error('Database not initialized'); - return getAllStops(this.db, limit); + return getStopById(this.db, stopId); } /** @@ -216,22 +203,6 @@ export class GtfsSqlJs { return getRouteById(this.db, routeId); } - /** - * Get all routes - */ - getAllRoutes(limit?: number): Route[] { - if (!this.db) throw new Error('Database not initialized'); - return getAllRoutes(this.db, limit); - } - - /** - * Get routes by agency - */ - getRoutesByAgency(agencyId: string): Route[] { - if (!this.db) throw new Error('Database not initialized'); - return getRoutesByAgency(this.db, agencyId); - } - /** * Get routes with optional filters */ @@ -284,49 +255,6 @@ export class GtfsSqlJs { return getTripById(this.db, tripId); } - /** - * Get trips for a route - */ - getTripsByRoute(routeId: string): Trip[] { - if (!this.db) throw new Error('Database not initialized'); - return getTripsByRoute(this.db, routeId); - } - - /** - * Get trips for a route and date - */ - getTripsByRouteAndDate(routeId: string, date: string): Trip[] { - if (!this.db) throw new Error('Database not initialized'); - const serviceIds = getActiveServiceIds(this.db, date); - return getTripsByRouteAndService(this.db, routeId, serviceIds); - } - - /** - * Get trips for a route, date, and direction - */ - getTripsByRouteAndDateAndDirection(routeId: string, date: string, directionId: number): Trip[] { - if (!this.db) throw new Error('Database not initialized'); - const serviceIds = getActiveServiceIds(this.db, date); - return getTripsByRouteServiceAndDirection(this.db, routeId, serviceIds, directionId); - } - - /** - * Get all trips for a given date (YYYYMMDD format) - */ - getTripsByDate(date: string): Trip[] { - if (!this.db) throw new Error('Database not initialized'); - const serviceIds = getActiveServiceIds(this.db, date); - return getTripsByService(this.db, serviceIds); - } - - /** - * Get stops for a given trip (ordered by stop_sequence) - */ - getStopsByTrip(tripId: string): Stop[] { - if (!this.db) throw new Error('Database not initialized'); - return getStopsByTrip(this.db, tripId); - } - /** * Get trips with optional filters * @@ -335,6 +263,7 @@ export class GtfsSqlJs { * @param filters.routeId - Filter by route ID * @param filters.date - Filter by date (YYYYMMDD format) - will get active services for that date * @param filters.directionId - Filter by direction ID + * @param filters.agencyId - Filter by agency ID * @param filters.limit - Limit number of results * * @example @@ -363,30 +292,13 @@ export class GtfsSqlJs { // ==================== Stop Time Methods ==================== /** - * Get stop times for a trip + * Get stop times for a trip (ordered by stop_sequence) */ getStopTimesByTrip(tripId: string): StopTime[] { if (!this.db) throw new Error('Database not initialized'); return getStopTimesByTrip(this.db, tripId); } - /** - * Get stop times for a stop - */ - getStopTimesByStop(stopId: string, limit = 100): StopTime[] { - if (!this.db) throw new Error('Database not initialized'); - return getStopTimesByStop(this.db, stopId, limit); - } - - /** - * Get stop times for a stop, route, and date - */ - getStopTimesForStopRouteAndDate(stopId: string, routeId: string, date: string, directionId?: number): StopTime[] { - if (!this.db) throw new Error('Database not initialized'); - const serviceIds = getActiveServiceIds(this.db, date); - return getStopTimesForStopRouteDirection(this.db, stopId, routeId, serviceIds, directionId); - } - /** * Get stop times with optional filters * @@ -396,6 +308,7 @@ export class GtfsSqlJs { * @param filters.routeId - Filter by route ID * @param filters.date - Filter by date (YYYYMMDD format) - will get active services for that date * @param filters.directionId - Filter by direction ID + * @param filters.agencyId - Filter by agency ID * @param filters.limit - Limit number of results * * @example diff --git a/src/index.ts b/src/index.ts index 1c84c39..0314b82 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ * @license MIT */ -export { GtfsSqlJs, type GtfsSqlJsOptions, type StopFilters, type RouteFilters, type TripFilters, type StopTimeFilters } from './gtfs-sqljs'; +export { GtfsSqlJs, type GtfsSqlJsOptions, type AgencyFilters, type StopFilters, type RouteFilters, type TripFilters, type StopTimeFilters } from './gtfs-sqljs'; // Export types export type { diff --git a/src/loaders/data-loader.ts b/src/loaders/data-loader.ts index f678bda..a855027 100644 --- a/src/loaders/data-loader.ts +++ b/src/loaders/data-loader.ts @@ -9,8 +9,9 @@ import type { GTFSFiles } from './zip-loader'; /** * Load GTFS files into SQLite database + * @param skipFiles - Optional array of filenames to skip importing (tables will be created but remain empty) */ -export async function loadGTFSData(db: Database, files: GTFSFiles): Promise { +export async function loadGTFSData(db: Database, files: GTFSFiles, skipFiles?: string[]): Promise { // Map of file names to table schemas const fileToSchema: Map = new Map(); for (const schema of GTFS_SCHEMA) { @@ -18,6 +19,9 @@ export async function loadGTFSData(db: Database, files: GTFSFiles): Promise f.toLowerCase()) || []); + // Process each file for (const [fileName, content] of Object.entries(files)) { const schema = fileToSchema.get(fileName); @@ -26,6 +30,12 @@ export async function loadGTFSData(db: Database, files: GTFSFiles): Promise 0) { + sql += ' WHERE ' + conditions.join(' AND '); + } + sql += ' ORDER BY agency_name'; + if (limit) { + sql += ' LIMIT ?'; + params.push(limit); + } + + const stmt = db.prepare(sql); + if (params.length > 0) { + stmt.bind(params); + } + + const agencies: Agency[] = []; + while (stmt.step()) { + const row = stmt.getAsObject() as Record; + agencies.push(rowToAgency(row)); + } + + stmt.free(); + return agencies; +} + +/** + * Get an agency by its agency_id + */ +export function getAgencyById(db: Database, agencyId: string): Agency | null { + const stmt = db.prepare('SELECT * FROM agency WHERE agency_id = ?'); + stmt.bind([agencyId]); + + if (stmt.step()) { + const row = stmt.getAsObject() as Record; + stmt.free(); + return rowToAgency(row); + } + + stmt.free(); + return null; +} + +/** + * Get all agencies + */ +export function getAllAgencies(db: Database, limit?: number): Agency[] { + const sql = limit + ? `SELECT * FROM agency ORDER BY agency_name LIMIT ${limit}` + : 'SELECT * FROM agency ORDER BY agency_name'; + + const stmt = db.prepare(sql); + + const agencies: Agency[] = []; + while (stmt.step()) { + const row = stmt.getAsObject() as Record; + agencies.push(rowToAgency(row)); + } + + stmt.free(); + return agencies; +} + +/** + * Convert database row to Agency object + */ +function rowToAgency(row: Record): Agency { + return { + agency_id: String(row.agency_id), + agency_name: String(row.agency_name), + agency_url: String(row.agency_url), + agency_timezone: String(row.agency_timezone), + agency_lang: row.agency_lang ? String(row.agency_lang) : undefined, + agency_phone: row.agency_phone ? String(row.agency_phone) : undefined, + agency_fare_url: row.agency_fare_url ? String(row.agency_fare_url) : undefined, + agency_email: row.agency_email ? String(row.agency_email) : undefined, + }; +} diff --git a/src/queries/stop-times.ts b/src/queries/stop-times.ts index fc8f337..f1a76f0 100644 --- a/src/queries/stop-times.ts +++ b/src/queries/stop-times.ts @@ -11,6 +11,7 @@ export interface StopTimeFilters { routeId?: string; serviceIds?: string[]; directionId?: number; + agencyId?: string; limit?: number; } @@ -18,10 +19,11 @@ export interface StopTimeFilters { * Get stop times with optional filters */ export function getStopTimes(db: Database, filters: StopTimeFilters = {}): StopTime[] { - const { tripId, stopId, routeId, serviceIds, directionId, limit } = filters; + const { tripId, stopId, routeId, serviceIds, directionId, agencyId, limit } = filters; - // Determine if we need to join with trips table - const needsTripsJoin = routeId || serviceIds || directionId !== undefined; + // Determine if we need to join with trips and/or routes table + const needsTripsJoin = routeId || serviceIds || directionId !== undefined || agencyId !== undefined; + const needsRoutesJoin = agencyId !== undefined; // Build WHERE clause dynamically const conditions: string[] = []; @@ -53,10 +55,20 @@ export function getStopTimes(db: Database, filters: StopTimeFilters = {}): StopT params.push(directionId); } + if (agencyId) { + conditions.push('r.agency_id = ?'); + params.push(agencyId); + } + // Build SQL query - let sql = needsTripsJoin - ? 'SELECT st.* FROM stop_times st INNER JOIN trips t ON st.trip_id = t.trip_id' - : 'SELECT * FROM stop_times'; + let sql: string; + if (needsRoutesJoin) { + sql = 'SELECT st.* FROM stop_times st INNER JOIN trips t ON st.trip_id = t.trip_id INNER JOIN routes r ON t.route_id = r.route_id'; + } else if (needsTripsJoin) { + sql = 'SELECT st.* FROM stop_times st INNER JOIN trips t ON st.trip_id = t.trip_id'; + } else { + sql = 'SELECT * FROM stop_times'; + } if (conditions.length > 0) { sql += ' WHERE ' + conditions.join(' AND '); diff --git a/src/queries/trips.ts b/src/queries/trips.ts index 77e745f..d3c539b 100644 --- a/src/queries/trips.ts +++ b/src/queries/trips.ts @@ -10,6 +10,7 @@ export interface TripFilters { routeId?: string; serviceIds?: string[]; directionId?: number; + agencyId?: string; limit?: number; } @@ -17,35 +18,46 @@ export interface TripFilters { * Get trips with optional filters */ export function getTrips(db: Database, filters: TripFilters = {}): Trip[] { - const { tripId, routeId, serviceIds, directionId, limit } = filters; + const { tripId, routeId, serviceIds, directionId, agencyId, limit } = filters; + + // Determine if we need to join with routes table + const needsRoutesJoin = agencyId !== undefined; // Build WHERE clause dynamically const conditions: string[] = []; const params: (string | number)[] = []; if (tripId) { - conditions.push('trip_id = ?'); + conditions.push(needsRoutesJoin ? 't.trip_id = ?' : 'trip_id = ?'); params.push(tripId); } if (routeId) { - conditions.push('route_id = ?'); + conditions.push(needsRoutesJoin ? 't.route_id = ?' : 'route_id = ?'); params.push(routeId); } if (serviceIds && serviceIds.length > 0) { const placeholders = serviceIds.map(() => '?').join(', '); - conditions.push(`service_id IN (${placeholders})`); + conditions.push(needsRoutesJoin ? `t.service_id IN (${placeholders})` : `service_id IN (${placeholders})`); params.push(...serviceIds); } if (directionId !== undefined) { - conditions.push('direction_id = ?'); + conditions.push(needsRoutesJoin ? 't.direction_id = ?' : 'direction_id = ?'); params.push(directionId); } + if (agencyId) { + conditions.push('r.agency_id = ?'); + params.push(agencyId); + } + // Build SQL query - let sql = 'SELECT * FROM trips'; + let sql = needsRoutesJoin + ? 'SELECT t.* FROM trips t INNER JOIN routes r ON t.route_id = r.route_id' + : 'SELECT * FROM trips'; + if (conditions.length > 0) { sql += ' WHERE ' + conditions.join(' AND '); } diff --git a/tests/gtfs-sqljs.test.ts b/tests/gtfs-sqljs.test.ts index 098d453..e861bed 100644 --- a/tests/gtfs-sqljs.test.ts +++ b/tests/gtfs-sqljs.test.ts @@ -35,13 +35,13 @@ describe('GtfsSqlJs', () => { }); it('should get stop by code', () => { - const stop = gtfs.getStopByCode('FS'); - expect(stop).toBeDefined(); - expect(stop?.stop_id).toBe('STOP1'); + const stops = gtfs.getStops({ stopCode: 'FS' }); + expect(stops.length).toBe(1); + expect(stops[0].stop_id).toBe('STOP1'); }); it('should search stops by name', () => { - const stops = gtfs.searchStopsByName('Street'); + const stops = gtfs.getStops({ name: 'Street' }); expect(stops.length).toBeGreaterThan(0); expect(stops[0].stop_name).toContain('Street'); }); @@ -52,7 +52,7 @@ describe('GtfsSqlJs', () => { }); it('should get all stops', () => { - const stops = gtfs.getAllStops(); + const stops = gtfs.getStops(); expect(stops.length).toBeGreaterThan(0); }); }); @@ -66,7 +66,7 @@ describe('GtfsSqlJs', () => { }); it('should get all routes', () => { - const routes = gtfs.getAllRoutes(); + const routes = gtfs.getRoutes(); expect(routes.length).toBeGreaterThan(0); }); @@ -105,17 +105,17 @@ describe('GtfsSqlJs', () => { }); it('should get trips by route', () => { - const trips = gtfs.getTripsByRoute('ROUTE1'); + const trips = gtfs.getTrips({ routeId: 'ROUTE1' }); expect(trips.length).toBeGreaterThan(0); }); it('should get trips by route and date', () => { - const trips = gtfs.getTripsByRouteAndDate('ROUTE1', '20240101'); + const trips = gtfs.getTrips({ routeId: 'ROUTE1', date: '20240101' }); expect(trips.length).toBeGreaterThan(0); }); it('should get trips by route, date, and direction', () => { - const trips = gtfs.getTripsByRouteAndDateAndDirection('ROUTE1', '20240101', 0); + const trips = gtfs.getTrips({ routeId: 'ROUTE1', date: '20240101', directionId: 0 }); expect(trips.length).toBeGreaterThan(0); expect(trips.every(t => t.direction_id === 0)).toBe(true); }); @@ -129,17 +129,17 @@ describe('GtfsSqlJs', () => { }); it('should get stop times by stop', () => { - const stopTimes = gtfs.getStopTimesByStop('STOP1'); + const stopTimes = gtfs.getStopTimes({ stopId: 'STOP1' }); expect(stopTimes.length).toBeGreaterThan(0); }); it('should get stop times for stop, route, and date', () => { - const stopTimes = gtfs.getStopTimesForStopRouteAndDate('STOP1', 'ROUTE1', '20240101'); + const stopTimes = gtfs.getStopTimes({ stopId: 'STOP1', routeId: 'ROUTE1', date: '20240101' }); expect(stopTimes.length).toBeGreaterThan(0); }); it('should get stop times with direction filter', () => { - const stopTimes = gtfs.getStopTimesForStopRouteAndDate('STOP1', 'ROUTE1', '20240101', 0); + const stopTimes = gtfs.getStopTimes({ stopId: 'STOP1', routeId: 'ROUTE1', date: '20240101', directionId: 0 }); expect(stopTimes.length).toBeGreaterThan(0); }); }); diff --git a/tests/gtfs-sqljs.test.ts.bak b/tests/gtfs-sqljs.test.ts.bak new file mode 100644 index 0000000..098d453 --- /dev/null +++ b/tests/gtfs-sqljs.test.ts.bak @@ -0,0 +1,172 @@ +/** + * Tests for GtfsSqlJs class + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import initSqlJs from 'sql.js'; +import { GtfsSqlJs } from '../src/gtfs-sqljs'; +import { createTestDatabase } from './helpers/test-database'; + +describe('GtfsSqlJs', () => { + let gtfs: GtfsSqlJs; + let SQL: any; + + beforeAll(async () => { + // Initialize SQL.js + SQL = await initSqlJs(); + + // Create test database + const dbBuffer = await createTestDatabase(SQL); + + // Initialize GtfsSqlJs with test data + gtfs = await GtfsSqlJs.fromDatabase(dbBuffer, { SQL }); + }); + + afterAll(() => { + gtfs?.close(); + }); + + describe('Stop methods', () => { + it('should get stop by ID', () => { + const stop = gtfs.getStopById('STOP1'); + expect(stop).toBeDefined(); + expect(stop?.stop_id).toBe('STOP1'); + expect(stop?.stop_name).toBe('First Street'); + }); + + it('should get stop by code', () => { + const stop = gtfs.getStopByCode('FS'); + expect(stop).toBeDefined(); + expect(stop?.stop_id).toBe('STOP1'); + }); + + it('should search stops by name', () => { + const stops = gtfs.searchStopsByName('Street'); + expect(stops.length).toBeGreaterThan(0); + expect(stops[0].stop_name).toContain('Street'); + }); + + it('should return null for non-existent stop ID', () => { + const stop = gtfs.getStopById('NONEXISTENT'); + expect(stop).toBeNull(); + }); + + it('should get all stops', () => { + const stops = gtfs.getAllStops(); + expect(stops.length).toBeGreaterThan(0); + }); + }); + + describe('Route methods', () => { + it('should get route by ID', () => { + const route = gtfs.getRouteById('ROUTE1'); + expect(route).toBeDefined(); + expect(route?.route_id).toBe('ROUTE1'); + expect(route?.route_short_name).toBe('1'); + }); + + it('should get all routes', () => { + const routes = gtfs.getAllRoutes(); + expect(routes.length).toBeGreaterThan(0); + }); + + it('should return null for non-existent route', () => { + const route = gtfs.getRouteById('NONEXISTENT'); + expect(route).toBeNull(); + }); + }); + + describe('Calendar methods', () => { + it('should get active service IDs for a date', () => { + const serviceIds = gtfs.getActiveServiceIds('20240101'); + expect(serviceIds.length).toBeGreaterThan(0); + expect(serviceIds).toContain('WEEKDAY'); + }); + + it('should get calendar by service ID', () => { + const calendar = gtfs.getCalendarByServiceId('WEEKDAY'); + expect(calendar).toBeDefined(); + expect(calendar?.service_id).toBe('WEEKDAY'); + expect(calendar?.monday).toBe(1); + }); + + it('should return empty array for date with no service', () => { + const serviceIds = gtfs.getActiveServiceIds('21000101'); + expect(serviceIds.length).toBe(0); + }); + }); + + describe('Trip methods', () => { + it('should get trip by ID', () => { + const trip = gtfs.getTripById('TRIP1'); + expect(trip).toBeDefined(); + expect(trip?.trip_id).toBe('TRIP1'); + expect(trip?.route_id).toBe('ROUTE1'); + }); + + it('should get trips by route', () => { + const trips = gtfs.getTripsByRoute('ROUTE1'); + expect(trips.length).toBeGreaterThan(0); + }); + + it('should get trips by route and date', () => { + const trips = gtfs.getTripsByRouteAndDate('ROUTE1', '20240101'); + expect(trips.length).toBeGreaterThan(0); + }); + + it('should get trips by route, date, and direction', () => { + const trips = gtfs.getTripsByRouteAndDateAndDirection('ROUTE1', '20240101', 0); + expect(trips.length).toBeGreaterThan(0); + expect(trips.every(t => t.direction_id === 0)).toBe(true); + }); + }); + + describe('Stop time methods', () => { + it('should get stop times by trip', () => { + const stopTimes = gtfs.getStopTimesByTrip('TRIP1'); + expect(stopTimes.length).toBeGreaterThan(0); + expect(stopTimes[0].trip_id).toBe('TRIP1'); + }); + + it('should get stop times by stop', () => { + const stopTimes = gtfs.getStopTimesByStop('STOP1'); + expect(stopTimes.length).toBeGreaterThan(0); + }); + + it('should get stop times for stop, route, and date', () => { + const stopTimes = gtfs.getStopTimesForStopRouteAndDate('STOP1', 'ROUTE1', '20240101'); + expect(stopTimes.length).toBeGreaterThan(0); + }); + + it('should get stop times with direction filter', () => { + const stopTimes = gtfs.getStopTimesForStopRouteAndDate('STOP1', 'ROUTE1', '20240101', 0); + expect(stopTimes.length).toBeGreaterThan(0); + }); + }); + + describe('Database export', () => { + it('should export database to ArrayBuffer', () => { + const buffer = gtfs.export(); + expect(buffer).toBeInstanceOf(ArrayBuffer); + expect(buffer.byteLength).toBeGreaterThan(0); + }); + + it('should be able to reload exported database', async () => { + const buffer = gtfs.export(); + const newGtfs = await GtfsSqlJs.fromDatabase(buffer, { SQL }); + + const stop = newGtfs.getStopById('STOP1'); + expect(stop).toBeDefined(); + expect(stop?.stop_name).toBe('First Street'); + + newGtfs.close(); + }); + }); + + describe('Error handling', () => { + it('should throw error when accessing closed database', () => { + const closedGtfs = new (GtfsSqlJs as any)(); + expect(() => closedGtfs.getStopById('STOP1')).toThrow('Database not initialized'); + }); + }); +}); diff --git a/tests/sample-feed.test.ts b/tests/sample-feed.test.ts index 2fc5079..6a0fb06 100644 --- a/tests/sample-feed.test.ts +++ b/tests/sample-feed.test.ts @@ -43,7 +43,7 @@ describe('Sample GTFS Feed Tests - Actual Data', () => { describe('Stops', () => { it('should have 9 stops', () => { - const stops = gtfs.getAllStops(); + const stops = gtfs.getStops(); expect(stops.length).toBe(9); }); @@ -68,7 +68,7 @@ describe('Sample GTFS Feed Tests - Actual Data', () => { }); it('should find stops when searching for "Airport"', () => { - const stops = gtfs.searchStopsByName('Airport'); + const stops = gtfs.getStops({ name: 'Airport' }); expect(stops.length).toBeGreaterThanOrEqual(1); const airportStop = stops.find(s => s.stop_id === 'BEATTY_AIRPORT'); @@ -77,7 +77,7 @@ describe('Sample GTFS Feed Tests - Actual Data', () => { }); it('should get stops for trip AB1 in correct order', () => { - const stops = gtfs.getStopsByTrip('AB1'); + const stops = gtfs.getStops({ tripId: 'AB1' }); expect(stops.length).toBe(2); expect(stops[0].stop_id).toBe('BEATTY_AIRPORT'); @@ -87,7 +87,7 @@ describe('Sample GTFS Feed Tests - Actual Data', () => { }); it('should get stops for trip CITY1 in correct order', () => { - const stops = gtfs.getStopsByTrip('CITY1'); + const stops = gtfs.getStops({ tripId: 'CITY1' }); expect(stops.length).toBe(5); expect(stops[0].stop_id).toBe('STAGECOACH'); @@ -100,7 +100,7 @@ describe('Sample GTFS Feed Tests - Actual Data', () => { describe('Routes', () => { it('should have 5 routes', () => { - const routes = gtfs.getAllRoutes(); + const routes = gtfs.getRoutes(); expect(routes.length).toBe(5); }); @@ -135,7 +135,7 @@ describe('Sample GTFS Feed Tests - Actual Data', () => { }); it('should get all routes for agency DTA', () => { - const routes = gtfs.getRoutesByAgency('DTA'); + const routes = gtfs.getRoutes({ agencyId: 'DTA' }); expect(routes.length).toBe(5); }); }); @@ -229,7 +229,7 @@ describe('Sample GTFS Feed Tests - Actual Data', () => { }); it('should get 2 trips for route AB', () => { - const trips = gtfs.getTripsByRoute('AB'); + const trips = gtfs.getTrips({ routeId: 'AB' }); expect(trips.length).toBe(2); const tripIds = trips.map(t => t.trip_id).sort(); @@ -237,19 +237,19 @@ describe('Sample GTFS Feed Tests - Actual Data', () => { }); it('should get trips for route AB on Monday', () => { - const trips = gtfs.getTripsByRouteAndDate('AB', '20070101'); + const trips = gtfs.getTrips({ routeId: 'AB', date: '20070101' }); expect(trips.length).toBe(2); expect(trips.every(t => t.service_id === 'FULLW')).toBe(true); }); it('should get 7 trips for Monday (all FULLW trips)', () => { - const trips = gtfs.getTripsByDate('20070101'); + const trips = gtfs.getTrips({ date: '20070101' }); expect(trips.length).toBe(7); // AB1, AB2, STBA, CITY1, CITY2, BFC1, BFC2 expect(trips.every(t => t.service_id === 'FULLW')).toBe(true); }); it('should get 11 trips for Saturday (FULLW + WE trips)', () => { - const trips = gtfs.getTripsByDate('20070106'); + const trips = gtfs.getTrips({ date: '20070106' }); expect(trips.length).toBe(11); // 7 FULLW + 4 AAMV (WE) const fullwTrips = trips.filter(t => t.service_id === 'FULLW'); @@ -260,8 +260,8 @@ describe('Sample GTFS Feed Tests - Actual Data', () => { }); it('should get trips by direction', () => { - const tripsDir0 = gtfs.getTripsByRouteAndDateAndDirection('AB', '20070101', 0); - const tripsDir1 = gtfs.getTripsByRouteAndDateAndDirection('AB', '20070101', 1); + const tripsDir0 = gtfs.getTrips({ routeId: 'AB', date: '20070101', directionId: 0 }); + const tripsDir1 = gtfs.getTrips({ routeId: 'AB', date: '20070101', directionId: 1 }); expect(tripsDir0.length).toBe(1); expect(tripsDir0[0].trip_id).toBe('AB1'); @@ -313,7 +313,7 @@ describe('Sample GTFS Feed Tests - Actual Data', () => { }); it('should get stop times for BEATTY_AIRPORT', () => { - const stopTimes = gtfs.getStopTimesByStop('BEATTY_AIRPORT', 100); + const stopTimes = gtfs.getStopTimes({ stopId: 'BEATTY_AIRPORT', limit: 100 }); // BEATTY_AIRPORT appears in: STBA, AB1, AB2, AAMV1, AAMV2, AAMV3, AAMV4 = 7 trips expect(stopTimes.length).toBe(7); @@ -327,7 +327,7 @@ describe('Sample GTFS Feed Tests - Actual Data', () => { }); it('should get stop times for route AB at BEATTY_AIRPORT on Monday', () => { - const stopTimes = gtfs.getStopTimesForStopRouteAndDate('BEATTY_AIRPORT', 'AB', '20070101'); + const stopTimes = gtfs.getStopTimes({ stopId: 'BEATTY_AIRPORT', routeId: 'AB', date: '20070101' }); expect(stopTimes.length).toBe(2); // AB1 and AB2 @@ -343,7 +343,7 @@ describe('Sample GTFS Feed Tests - Actual Data', () => { }); it('should filter by direction', () => { - const stopTimes = gtfs.getStopTimesForStopRouteAndDate('BEATTY_AIRPORT', 'AB', '20070101', 0); + const stopTimes = gtfs.getStopTimes({ stopId: 'BEATTY_AIRPORT', routeId: 'AB', date: '20070101', directionId: 0 }); expect(stopTimes.length).toBe(1); expect(stopTimes[0].trip_id).toBe('AB1'); @@ -365,7 +365,7 @@ describe('Sample GTFS Feed Tests - Actual Data', () => { expect(route).not.toBeNull(); // Get trips on a Monday - const trips = gtfs.getTripsByRouteAndDate('AB', '20070101'); + const trips = gtfs.getTrips({ routeId: 'AB', date: '20070101' }); // Trip AB1 goes to Bullfrog (direction 0) const trip = trips.find(t => t.trip_headsign === 'to Bullfrog'); @@ -389,7 +389,7 @@ describe('Sample GTFS Feed Tests - Actual Data', () => { }); it('should find all routes serving BULLFROG', () => { - const stopTimes = gtfs.getStopTimesByStop('BULLFROG', 100); + const stopTimes = gtfs.getStopTimes({ stopId: 'BULLFROG', limit: 100 }); // Get unique trip IDs const tripIds = [...new Set(stopTimes.map(st => st.trip_id))]; @@ -417,8 +417,8 @@ describe('Sample GTFS Feed Tests - Actual Data', () => { const trip1 = gtfs.getTripById('CITY1'); const trip2 = gtfs.getTripById('CITY2'); - const stops1 = gtfs.getStopsByTrip('CITY1'); - const stops2 = gtfs.getStopsByTrip('CITY2'); + const stops1 = gtfs.getStops({ tripId: 'CITY1' }); + const stops2 = gtfs.getStops({ tripId: 'CITY2' }); // Both trips have 5 stops expect(stops1.length).toBe(5); diff --git a/tests/sample-feed.test.ts.bak b/tests/sample-feed.test.ts.bak new file mode 100644 index 0000000..2fc5079 --- /dev/null +++ b/tests/sample-feed.test.ts.bak @@ -0,0 +1,467 @@ +/** + * Tests using the official Google GTFS sample feed + * + * This test file uses the real sample feed from: + * https://developers.google.com/transit/gtfs/examples/gtfs-feed + * + * The tests validate specific known data from the feed. + */ + +import { describe, it, expect, beforeAll, afterAll } from 'vitest'; +import { GtfsSqlJs } from '../src/gtfs-sqljs'; +import path from 'path'; + +describe('Sample GTFS Feed Tests - Actual Data', () => { + let gtfs: GtfsSqlJs; + + beforeAll(async () => { + // Load the sample GTFS feed + const feedPath = path.join(__dirname, 'fixtures', 'sample-feed.zip'); + gtfs = await GtfsSqlJs.fromZip(feedPath); + }); + + afterAll(() => { + gtfs?.close(); + }); + + describe('Agency', () => { + it('should have DTA (Demo Transit Authority)', () => { + const db = gtfs.getDatabase(); + const stmt = db.prepare('SELECT * FROM agency WHERE agency_id = ?'); + stmt.bind(['DTA']); + + expect(stmt.step()).toBe(true); + const agency = stmt.getAsObject(); + stmt.free(); + + expect(agency.agency_id).toBe('DTA'); + expect(agency.agency_name).toBe('Demo Transit Authority'); + expect(agency.agency_url).toBe('http://google.com'); + expect(agency.agency_timezone).toBe('America/Los_Angeles'); + }); + }); + + describe('Stops', () => { + it('should have 9 stops', () => { + const stops = gtfs.getAllStops(); + expect(stops.length).toBe(9); + }); + + it('should have BEATTY_AIRPORT with correct details', () => { + const stop = gtfs.getStopById('BEATTY_AIRPORT'); + + expect(stop).not.toBeNull(); + expect(stop!.stop_id).toBe('BEATTY_AIRPORT'); + expect(stop!.stop_name).toBe('Nye County Airport (Demo)'); + expect(stop!.stop_lat).toBeCloseTo(36.868446, 5); + expect(stop!.stop_lon).toBeCloseTo(-116.784582, 5); + }); + + it('should have BULLFROG with correct details', () => { + const stop = gtfs.getStopById('BULLFROG'); + + expect(stop).not.toBeNull(); + expect(stop!.stop_id).toBe('BULLFROG'); + expect(stop!.stop_name).toBe('Bullfrog (Demo)'); + expect(stop!.stop_lat).toBeCloseTo(36.88108, 5); + expect(stop!.stop_lon).toBeCloseTo(-116.81797, 5); + }); + + it('should find stops when searching for "Airport"', () => { + const stops = gtfs.searchStopsByName('Airport'); + expect(stops.length).toBeGreaterThanOrEqual(1); + + const airportStop = stops.find(s => s.stop_id === 'BEATTY_AIRPORT'); + expect(airportStop).toBeDefined(); + expect(airportStop!.stop_name).toBe('Nye County Airport (Demo)'); + }); + + it('should get stops for trip AB1 in correct order', () => { + const stops = gtfs.getStopsByTrip('AB1'); + + expect(stops.length).toBe(2); + expect(stops[0].stop_id).toBe('BEATTY_AIRPORT'); + expect(stops[0].stop_name).toBe('Nye County Airport (Demo)'); + expect(stops[1].stop_id).toBe('BULLFROG'); + expect(stops[1].stop_name).toBe('Bullfrog (Demo)'); + }); + + it('should get stops for trip CITY1 in correct order', () => { + const stops = gtfs.getStopsByTrip('CITY1'); + + expect(stops.length).toBe(5); + expect(stops[0].stop_id).toBe('STAGECOACH'); + expect(stops[1].stop_id).toBe('NANAA'); + expect(stops[2].stop_id).toBe('NADAV'); + expect(stops[3].stop_id).toBe('DADAN'); + expect(stops[4].stop_id).toBe('EMSI'); + }); + }); + + describe('Routes', () => { + it('should have 5 routes', () => { + const routes = gtfs.getAllRoutes(); + expect(routes.length).toBe(5); + }); + + it('should have route AB (Airport - Bullfrog)', () => { + const route = gtfs.getRouteById('AB'); + + expect(route).not.toBeNull(); + expect(route!.route_id).toBe('AB'); + expect(route!.agency_id).toBe('DTA'); + expect(route!.route_short_name).toBe('10'); + expect(route!.route_long_name).toBe('Airport - Bullfrog'); + expect(route!.route_type).toBe(3); // Bus + }); + + it('should have route BFC (Bullfrog - Furnace Creek Resort)', () => { + const route = gtfs.getRouteById('BFC'); + + expect(route).not.toBeNull(); + expect(route!.route_id).toBe('BFC'); + expect(route!.route_short_name).toBe('20'); + expect(route!.route_long_name).toBe('Bullfrog - Furnace Creek Resort'); + expect(route!.route_type).toBe(3); + }); + + it('should have route CITY (City)', () => { + const route = gtfs.getRouteById('CITY'); + + expect(route).not.toBeNull(); + expect(route!.route_id).toBe('CITY'); + expect(route!.route_short_name).toBe('40'); + expect(route!.route_long_name).toBe('City'); + }); + + it('should get all routes for agency DTA', () => { + const routes = gtfs.getRoutesByAgency('DTA'); + expect(routes.length).toBe(5); + }); + }); + + describe('Calendar', () => { + it('should have FULLW service (full week)', () => { + const calendar = gtfs.getCalendarByServiceId('FULLW'); + + expect(calendar).not.toBeNull(); + expect(calendar!.service_id).toBe('FULLW'); + expect(calendar!.monday).toBe(1); + expect(calendar!.tuesday).toBe(1); + expect(calendar!.wednesday).toBe(1); + expect(calendar!.thursday).toBe(1); + expect(calendar!.friday).toBe(1); + expect(calendar!.saturday).toBe(1); + expect(calendar!.sunday).toBe(1); + expect(calendar!.start_date).toBe('20070101'); + expect(calendar!.end_date).toBe('20101231'); + }); + + it('should have WE service (weekend only)', () => { + const calendar = gtfs.getCalendarByServiceId('WE'); + + expect(calendar).not.toBeNull(); + expect(calendar!.service_id).toBe('WE'); + expect(calendar!.monday).toBe(0); + expect(calendar!.tuesday).toBe(0); + expect(calendar!.wednesday).toBe(0); + expect(calendar!.thursday).toBe(0); + expect(calendar!.friday).toBe(0); + expect(calendar!.saturday).toBe(1); + expect(calendar!.sunday).toBe(1); + expect(calendar!.start_date).toBe('20070101'); + expect(calendar!.end_date).toBe('20101231'); + }); + + it('should return FULLW service for Monday 2007-01-01', () => { + // 2007-01-01 was a Monday + const serviceIds = gtfs.getActiveServiceIds('20070101'); + + expect(serviceIds).toContain('FULLW'); + expect(serviceIds).not.toContain('WE'); // Weekend service not active on Monday + }); + + it('should return both FULLW and WE services for Saturday 2007-01-06', () => { + // 2007-01-06 was a Saturday + const serviceIds = gtfs.getActiveServiceIds('20070106'); + + expect(serviceIds).toContain('FULLW'); + expect(serviceIds).toContain('WE'); + }); + + it('should return both services for Sunday 2007-01-07', () => { + // 2007-01-07 was a Sunday + const serviceIds = gtfs.getActiveServiceIds('20070107'); + + expect(serviceIds).toContain('FULLW'); + expect(serviceIds).toContain('WE'); + }); + + it('should return no services for date outside range', () => { + const serviceIds = gtfs.getActiveServiceIds('20110101'); + expect(serviceIds.length).toBe(0); + }); + }); + + describe('Trips', () => { + it('should have trip AB1 (to Bullfrog)', () => { + const trip = gtfs.getTripById('AB1'); + + expect(trip).not.toBeNull(); + expect(trip!.trip_id).toBe('AB1'); + expect(trip!.route_id).toBe('AB'); + expect(trip!.service_id).toBe('FULLW'); + expect(trip!.trip_headsign).toBe('to Bullfrog'); + expect(trip!.direction_id).toBe(0); + expect(trip!.block_id).toBe('1'); + }); + + it('should have trip AB2 (to Airport)', () => { + const trip = gtfs.getTripById('AB2'); + + expect(trip).not.toBeNull(); + expect(trip!.trip_id).toBe('AB2'); + expect(trip!.route_id).toBe('AB'); + expect(trip!.service_id).toBe('FULLW'); + expect(trip!.trip_headsign).toBe('to Airport'); + expect(trip!.direction_id).toBe(1); + expect(trip!.block_id).toBe('2'); + }); + + it('should get 2 trips for route AB', () => { + const trips = gtfs.getTripsByRoute('AB'); + expect(trips.length).toBe(2); + + const tripIds = trips.map(t => t.trip_id).sort(); + expect(tripIds).toEqual(['AB1', 'AB2']); + }); + + it('should get trips for route AB on Monday', () => { + const trips = gtfs.getTripsByRouteAndDate('AB', '20070101'); + expect(trips.length).toBe(2); + expect(trips.every(t => t.service_id === 'FULLW')).toBe(true); + }); + + it('should get 7 trips for Monday (all FULLW trips)', () => { + const trips = gtfs.getTripsByDate('20070101'); + expect(trips.length).toBe(7); // AB1, AB2, STBA, CITY1, CITY2, BFC1, BFC2 + expect(trips.every(t => t.service_id === 'FULLW')).toBe(true); + }); + + it('should get 11 trips for Saturday (FULLW + WE trips)', () => { + const trips = gtfs.getTripsByDate('20070106'); + expect(trips.length).toBe(11); // 7 FULLW + 4 AAMV (WE) + + const fullwTrips = trips.filter(t => t.service_id === 'FULLW'); + const weTrips = trips.filter(t => t.service_id === 'WE'); + + expect(fullwTrips.length).toBe(7); + expect(weTrips.length).toBe(4); + }); + + it('should get trips by direction', () => { + const tripsDir0 = gtfs.getTripsByRouteAndDateAndDirection('AB', '20070101', 0); + const tripsDir1 = gtfs.getTripsByRouteAndDateAndDirection('AB', '20070101', 1); + + expect(tripsDir0.length).toBe(1); + expect(tripsDir0[0].trip_id).toBe('AB1'); + expect(tripsDir0[0].trip_headsign).toBe('to Bullfrog'); + + expect(tripsDir1.length).toBe(1); + expect(tripsDir1[0].trip_id).toBe('AB2'); + expect(tripsDir1[0].trip_headsign).toBe('to Airport'); + }); + }); + + describe('Stop Times', () => { + it('should have correct stop times for trip AB1', () => { + const stopTimes = gtfs.getStopTimesByTrip('AB1'); + + expect(stopTimes.length).toBe(2); + + // First stop + expect(stopTimes[0].trip_id).toBe('AB1'); + expect(stopTimes[0].stop_id).toBe('BEATTY_AIRPORT'); + expect(stopTimes[0].arrival_time).toBe('8:00:00'); + expect(stopTimes[0].departure_time).toBe('8:00:00'); + expect(stopTimes[0].stop_sequence).toBe(1); + + // Second stop + expect(stopTimes[1].trip_id).toBe('AB1'); + expect(stopTimes[1].stop_id).toBe('BULLFROG'); + expect(stopTimes[1].arrival_time).toBe('8:10:00'); + expect(stopTimes[1].departure_time).toBe('8:15:00'); + expect(stopTimes[1].stop_sequence).toBe(2); + }); + + it('should have correct stop times for trip CITY1', () => { + const stopTimes = gtfs.getStopTimesByTrip('CITY1'); + + expect(stopTimes.length).toBe(5); + + // Verify all stops in sequence + expect(stopTimes[0].stop_id).toBe('STAGECOACH'); + expect(stopTimes[0].arrival_time).toBe('6:00:00'); + + expect(stopTimes[1].stop_id).toBe('NANAA'); + expect(stopTimes[1].arrival_time).toBe('6:05:00'); + expect(stopTimes[1].departure_time).toBe('6:07:00'); + + expect(stopTimes[2].stop_id).toBe('NADAV'); + expect(stopTimes[3].stop_id).toBe('DADAN'); + expect(stopTimes[4].stop_id).toBe('EMSI'); + }); + + it('should get stop times for BEATTY_AIRPORT', () => { + const stopTimes = gtfs.getStopTimesByStop('BEATTY_AIRPORT', 100); + + // BEATTY_AIRPORT appears in: STBA, AB1, AB2, AAMV1, AAMV2, AAMV3, AAMV4 = 7 trips + expect(stopTimes.length).toBe(7); + + // Should be ordered by arrival_time (as strings) + // Note: SQL ORDER BY treats times as strings, so "11:00:00" < "6:20:00" alphabetically + const times = stopTimes.map(st => st.arrival_time); + expect(times).toContain('6:20:00'); // STBA + expect(times).toContain('8:00:00'); // AB1 or AAMV1 + expect(times).toContain('12:15:00'); // AB2 + }); + + it('should get stop times for route AB at BEATTY_AIRPORT on Monday', () => { + const stopTimes = gtfs.getStopTimesForStopRouteAndDate('BEATTY_AIRPORT', 'AB', '20070101'); + + expect(stopTimes.length).toBe(2); // AB1 and AB2 + + // Check specific times + const ab1Time = stopTimes.find(st => st.trip_id === 'AB1'); + const ab2Time = stopTimes.find(st => st.trip_id === 'AB2'); + + expect(ab1Time).toBeDefined(); + expect(ab1Time!.arrival_time).toBe('8:00:00'); + + expect(ab2Time).toBeDefined(); + expect(ab2Time!.arrival_time).toBe('12:15:00'); + }); + + it('should filter by direction', () => { + const stopTimes = gtfs.getStopTimesForStopRouteAndDate('BEATTY_AIRPORT', 'AB', '20070101', 0); + + expect(stopTimes.length).toBe(1); + expect(stopTimes[0].trip_id).toBe('AB1'); + expect(stopTimes[0].arrival_time).toBe('8:00:00'); + }); + }); + + describe('Complete Journey Scenarios', () => { + it('should plan journey from Airport to Bullfrog at 8am', () => { + // User wants to go from BEATTY_AIRPORT to BULLFROG + const origin = gtfs.getStopById('BEATTY_AIRPORT'); + const destination = gtfs.getStopById('BULLFROG'); + + expect(origin).not.toBeNull(); + expect(destination).not.toBeNull(); + + // Find route AB + const route = gtfs.getRouteById('AB'); + expect(route).not.toBeNull(); + + // Get trips on a Monday + const trips = gtfs.getTripsByRouteAndDate('AB', '20070101'); + + // Trip AB1 goes to Bullfrog (direction 0) + const trip = trips.find(t => t.trip_headsign === 'to Bullfrog'); + expect(trip).toBeDefined(); + expect(trip!.trip_id).toBe('AB1'); + + // Get stop times + const stopTimes = gtfs.getStopTimesByTrip('AB1'); + expect(stopTimes[0].stop_id).toBe('BEATTY_AIRPORT'); + expect(stopTimes[0].departure_time).toBe('8:00:00'); + expect(stopTimes[1].stop_id).toBe('BULLFROG'); + expect(stopTimes[1].arrival_time).toBe('8:10:00'); + + console.log('\nJourney Plan:'); + console.log(`From: ${origin!.stop_name}`); + console.log(`To: ${destination!.stop_name}`); + console.log(`Route: ${route!.route_short_name} - ${route!.route_long_name}`); + console.log(`Depart: ${stopTimes[0].departure_time}`); + console.log(`Arrive: ${stopTimes[1].arrival_time}`); + console.log(`Duration: 10 minutes`); + }); + + it('should find all routes serving BULLFROG', () => { + const stopTimes = gtfs.getStopTimesByStop('BULLFROG', 100); + + // Get unique trip IDs + const tripIds = [...new Set(stopTimes.map(st => st.trip_id))]; + + // Get routes for these trips + const routes = new Set(); + for (const tripId of tripIds) { + const trip = gtfs.getTripById(tripId); + if (trip) { + routes.add(trip.route_id); + } + } + + // BULLFROG is served by routes AB and BFC + expect(routes.size).toBe(2); + expect(routes.has('AB')).toBe(true); + expect(routes.has('BFC')).toBe(true); + }); + + it('should find city circuit route with all stops', () => { + const route = gtfs.getRouteById('CITY'); + expect(route).not.toBeNull(); + + // Get both directions + const trip1 = gtfs.getTripById('CITY1'); + const trip2 = gtfs.getTripById('CITY2'); + + const stops1 = gtfs.getStopsByTrip('CITY1'); + const stops2 = gtfs.getStopsByTrip('CITY2'); + + // Both trips have 5 stops + expect(stops1.length).toBe(5); + expect(stops2.length).toBe(5); + + // They visit the same stops in reverse order + expect(stops1[0].stop_id).toBe('STAGECOACH'); + expect(stops2[4].stop_id).toBe('STAGECOACH'); + + expect(stops1[4].stop_id).toBe('EMSI'); + expect(stops2[0].stop_id).toBe('EMSI'); + }); + }); + + describe('Database Export/Import', () => { + it('should export and re-import database with all data intact', async () => { + // Export database + const buffer = gtfs.export(); + expect(buffer.byteLength).toBeGreaterThan(0); + + // Create new instance from exported buffer + const gtfs2 = await GtfsSqlJs.fromDatabase(buffer); + + // Verify specific data is intact + const db = gtfs2.getDatabase(); + const agency = db.prepare('SELECT * FROM agency WHERE agency_id = ?'); + agency.bind(['DTA']); + expect(agency.step()).toBe(true); + const agencyData = agency.getAsObject(); + agency.free(); + expect(agencyData.agency_name).toBe('Demo Transit Authority'); + + // Verify route + const route = gtfs2.getRouteById('AB'); + expect(route).not.toBeNull(); + expect(route!.route_long_name).toBe('Airport - Bullfrog'); + + // Verify trip + const trip = gtfs2.getTripById('AB1'); + expect(trip).not.toBeNull(); + expect(trip!.trip_headsign).toBe('to Bullfrog'); + + gtfs2.close(); + }); + }); +}); diff --git a/website/demo.html b/website/demo.html index 754798a..d61cef6 100644 --- a/website/demo.html +++ b/website/demo.html @@ -43,6 +43,12 @@

Select Date

Loading...

+
+ +

Download the processed GTFS data as a SQLite database file

+
diff --git a/website/demo.ts b/website/demo.ts index 45b2a11..9b707fc 100644 --- a/website/demo.ts +++ b/website/demo.ts @@ -18,7 +18,11 @@ async function init() { }); // Load GTFS data (Vite will resolve this from public folder) - gtfs = await GtfsSqlJs.fromZip('./car-jaune.zip', { SQL }); + // Skip shapes.txt to reduce memory usage and improve load time + gtfs = await GtfsSqlJs.fromZip('./car-jaune.zip', { + SQL, + skipFiles: ['shapes.txt'] + }); // Hide loading, show content loadingEl.style.display = 'none'; @@ -27,6 +31,9 @@ async function init() { // Initialize date picker with today's date initDatePicker(); + // Setup download button + setupDownloadButton(); + // Render initial data renderActiveCalendars(); renderRoutes(); @@ -244,6 +251,33 @@ function renderRoutes() { stopTimesSectionEl.scrollIntoView({ behavior: 'smooth', block: 'start' }); }; +// Setup download button +function setupDownloadButton() { + const downloadBtn = document.getElementById('download-db-btn') as HTMLButtonElement; + if (!downloadBtn) return; + + downloadBtn.addEventListener('click', () => { + try { + // Export database to ArrayBuffer + const dbData = gtfs.export(); + + // Create blob and download + const blob = new Blob([dbData], { type: 'application/x-sqlite3' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `car-jaune-${selectedDate}.db`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } catch (error) { + console.error('Error downloading database:', error); + alert('Failed to download database'); + } + }); +} + // Utility functions function escapeHtml(text: string): string { diff --git a/website/style.css b/website/style.css index 61225ab..1d0b500 100644 --- a/website/style.css +++ b/website/style.css @@ -284,6 +284,20 @@ footer { line-height: 1.6; } +.download-section { + margin-top: 2rem; + text-align: center; +} + +.download-section .button { + margin-bottom: 0.5rem; +} + +.download-section .help-text { + margin-top: 0.5rem; + font-size: 0.875rem; +} + .calendar-info { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); From 3b717ca3e2580429baff5b326d3734727a89cfec Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Nov 2025 17:04:46 +0000 Subject: [PATCH 2/2] Update documentation and improve demo trip display Documentation updates: - Update all query examples to use new flexible filter-based API - Add agency methods documentation (getAgencies, getAgencyById) - Show skipFiles feature in examples - Update "Complete Example" section with new API patterns - Simplify API reference to reflect actual current methods - Remove references to deprecated convenience methods Demo improvements: - Display trip_short_name prominently instead of trip_id - Show trip_id as secondary information when short_name exists - Sort trips by trip_short_name with numeric comparison - Add new CSS styles for trip display hierarchy (.trip-name, .trip-id-secondary) These changes improve documentation clarity and make the demo more user-friendly by showing meaningful trip names. --- README.md | 171 +++++++++++++++++++++++++++++----------------- website/demo.ts | 29 +++++--- website/style.css | 12 ++++ 3 files changed, 141 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 084da68..99bea0e 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,11 @@ Try the live demo to explore GTFS data, view routes with colors, and see trip sc ## Features - ✅ Load GTFS data from ZIP files (URL or local path) +- ✅ Skip importing specific files (e.g., shapes.txt) to reduce memory usage - ✅ Load existing SQLite databases - ✅ Export databases to ArrayBuffer for persistence +- ✅ Flexible filter-based query API - combine multiple filters easily +- ✅ Agency query support with agency-based filtering - ✅ Full TypeScript support with comprehensive types - ✅ Works in both browser and Node.js - ✅ Efficient querying with indexed SQLite database @@ -117,6 +120,12 @@ const gtfs = await GtfsSqlJs.fromZip('https://example.com/gtfs.zip'); // From local file (Node.js) const gtfs = await GtfsSqlJs.fromZip('./path/to/gtfs.zip'); + +// Skip importing specific files to reduce memory usage +// Tables will be created but data won't be imported +const gtfs = await GtfsSqlJs.fromZip('https://example.com/gtfs.zip', { + skipFiles: ['shapes.txt', 'frequencies.txt'] +}); ``` #### From an existing SQLite database @@ -203,14 +212,21 @@ const stopTimes = gtfs.getStopTimes({ const stop = gtfs.getStopById('STOP_123'); console.log(stop?.stop_name); -// Get stop by code -const stop = gtfs.getStopByCode('ABC'); +// Get stop by code (using filters) +const stops = gtfs.getStops({ stopCode: 'ABC' }); +const stop = stops[0]; + +// Search stops by name (using filters) +const stops = gtfs.getStops({ name: 'Main Street' }); -// Search stops by name -const stops = gtfs.searchStopsByName('Main Street'); +// Get all stops (using filters with no parameters) +const allStops = gtfs.getStops(); -// Get all stops -const allStops = gtfs.getAllStops(); +// Get stops with limit +const stops = gtfs.getStops({ limit: 10 }); + +// Get stops for a specific trip +const stops = gtfs.getStops({ tripId: 'TRIP_123' }); ``` #### Get Route Information @@ -219,11 +235,27 @@ const allStops = gtfs.getAllStops(); // Get route by ID const route = gtfs.getRouteById('ROUTE_1'); -// Get all routes -const routes = gtfs.getAllRoutes(); +// Get all routes (using filters with no parameters) +const routes = gtfs.getRoutes(); + +// Get routes by agency (using filters) +const agencyRoutes = gtfs.getRoutes({ agencyId: 'AGENCY_1' }); + +// Get routes with limit +const routes = gtfs.getRoutes({ limit: 10 }); +``` + +#### Get Agency Information -// Get routes by agency -const agencyRoutes = gtfs.getRoutesByAgency('AGENCY_1'); +```typescript +// Get agency by ID +const agency = gtfs.getAgencyById('AGENCY_1'); + +// Get all agencies +const agencies = gtfs.getAgencies(); + +// Get agencies with limit +const agencies = gtfs.getAgencies({ limit: 5 }); ``` #### Get Calendar Information @@ -245,39 +277,61 @@ const exceptions = gtfs.getCalendarDates('WEEKDAY'); // Get trip by ID const trip = gtfs.getTripById('TRIP_123'); -// Get trips by route -const trips = gtfs.getTripsByRoute('ROUTE_1'); +// Get trips by route (using filters) +const trips = gtfs.getTrips({ routeId: 'ROUTE_1' }); + +// Get trips by route and date (using filters) +const trips = gtfs.getTrips({ routeId: 'ROUTE_1', date: '20240115' }); + +// Get trips by route, date, and direction (using filters) +const trips = gtfs.getTrips({ + routeId: 'ROUTE_1', + date: '20240115', + directionId: 0 +}); -// Get trips by route and date -const trips = gtfs.getTripsByRouteAndDate('ROUTE_1', '20240115'); +// Get all trips for a date +const trips = gtfs.getTrips({ date: '20240115' }); -// Get trips by route, date, and direction -const trips = gtfs.getTripsByRouteAndDateAndDirection('ROUTE_1', '20240115', 0); +// Get trips by agency +const trips = gtfs.getTrips({ agencyId: 'AGENCY_1' }); ``` #### Get Stop Time Information ```typescript -// Get stop times for a trip +// Get stop times for a trip (ordered by stop_sequence) const stopTimes = gtfs.getStopTimesByTrip('TRIP_123'); -// Get stop times for a stop -const stopTimes = gtfs.getStopTimesByStop('STOP_123'); - -// Get stop times for a stop, route, and date -const stopTimes = gtfs.getStopTimesForStopRouteAndDate( - 'STOP_123', - 'ROUTE_1', - '20240115' -); - -// Get stop times with direction filter -const stopTimes = gtfs.getStopTimesForStopRouteAndDate( - 'STOP_123', - 'ROUTE_1', - '20240115', - 0 // direction_id -); +// Get stop times for a stop (using filters) +const stopTimes = gtfs.getStopTimes({ stopId: 'STOP_123' }); + +// Get stop times for a stop and route (using filters) +const stopTimes = gtfs.getStopTimes({ + stopId: 'STOP_123', + routeId: 'ROUTE_1' +}); + +// Get stop times for a stop, route, and date (using filters) +const stopTimes = gtfs.getStopTimes({ + stopId: 'STOP_123', + routeId: 'ROUTE_1', + date: '20240115' +}); + +// Get stop times with direction filter (using filters) +const stopTimes = gtfs.getStopTimes({ + stopId: 'STOP_123', + routeId: 'ROUTE_1', + date: '20240115', + directionId: 0 +}); + +// Get stop times by agency +const stopTimes = gtfs.getStopTimes({ + agencyId: 'AGENCY_1', + date: '20240115' +}); ``` ### Export Database @@ -327,16 +381,18 @@ gtfs.close(); import { GtfsSqlJs } from 'gtfs-sqljs'; async function example() { - // Load GTFS data - const gtfs = await GtfsSqlJs.fromZip('https://example.com/gtfs.zip'); + // Load GTFS data (skip shapes.txt to reduce memory usage) + const gtfs = await GtfsSqlJs.fromZip('https://example.com/gtfs.zip', { + skipFiles: ['shapes.txt'] + }); - // Find a stop - const stops = gtfs.searchStopsByName('Central Station'); + // Find a stop using flexible filters + const stops = gtfs.getStops({ name: 'Central Station' }); const stop = stops[0]; console.log(`Found stop: ${stop.stop_name}`); // Find routes serving this stop (via stop_times and trips) - const allStopTimes = gtfs.getStopTimesByStop(stop.stop_id); + const allStopTimes = gtfs.getStopTimes({ stopId: stop.stop_id }); const routeIds = new Set( allStopTimes.map(st => { const trip = gtfs.getTripById(st.trip_id); @@ -351,9 +407,12 @@ async function example() { console.log(`Route: ${route?.route_short_name} - ${route?.route_long_name}`); } - // Get trips for a specific route on a date + // Get trips for a specific route on a date using flexible filters const today = '20240115'; // YYYYMMDD format - const trips = gtfs.getTripsByRouteAndDate(Array.from(routeIds)[0]!, today); + const trips = gtfs.getTrips({ + routeId: Array.from(routeIds)[0]!, + date: today + }); console.log(`Found ${trips.length} trips for today`); // Get stop times for a specific trip @@ -387,20 +446,15 @@ example(); #### Flexible Filter-Based Methods (Recommended) - `getStops(filters?)` - Get stops with optional filters (stopId, stopCode, name, tripId, limit) - `getRoutes(filters?)` - Get routes with optional filters (routeId, agencyId, limit) -- `getTrips(filters?)` - Get trips with optional filters (tripId, routeId, date, directionId, limit) -- `getStopTimes(filters?)` - Get stop times with optional filters (tripId, stopId, routeId, date, directionId, limit) +- `getTrips(filters?)` - Get trips with optional filters (tripId, routeId, date, directionId, agencyId, limit) +- `getStopTimes(filters?)` - Get stop times with optional filters (tripId, stopId, routeId, date, directionId, agencyId, limit) +- `getAgencies(filters?)` - Get agencies with optional filters (agencyId, limit) -#### Stop Methods +#### Direct Lookup Methods - `getStopById(stopId)` - Get stop by stop_id -- `getStopByCode(stopCode)` - Get stop by stop_code -- `searchStopsByName(name, limit?)` - Search stops by name -- `getAllStops(limit?)` - Get all stops -- `getStopsByTrip(tripId)` - Get stops for a trip (ordered by sequence) - -#### Route Methods - `getRouteById(routeId)` - Get route by route_id -- `getAllRoutes(limit?)` - Get all routes -- `getRoutesByAgency(agencyId)` - Get routes by agency +- `getTripById(tripId)` - Get trip by trip_id +- `getAgencyById(agencyId)` - Get agency by agency_id #### Calendar Methods - `getActiveServiceIds(date)` - Get active service IDs for a date @@ -408,17 +462,8 @@ example(); - `getCalendarDates(serviceId)` - Get calendar date exceptions - `getCalendarDatesForDate(date)` - Get exceptions for a specific date -#### Trip Methods -- `getTripById(tripId)` - Get trip by trip_id -- `getTripsByRoute(routeId)` - Get trips for a route -- `getTripsByRouteAndDate(routeId, date)` - Get trips for route and date -- `getTripsByRouteAndDateAndDirection(routeId, date, directionId)` - Get trips with direction filter -- `getTripsByDate(date)` - Get all trips for a date - -#### Stop Time Methods -- `getStopTimesByTrip(tripId)` - Get stop times for a trip -- `getStopTimesByStop(stopId, limit?)` - Get stop times for a stop -- `getStopTimesForStopRouteAndDate(stopId, routeId, date, directionId?)` - Get stop times with filters +#### Special Methods +- `getStopTimesByTrip(tripId)` - Get stop times for a trip (ordered by stop_sequence) #### Database Methods - `export()` - Export database to ArrayBuffer diff --git a/website/demo.ts b/website/demo.ts index 9b707fc..219a072 100644 --- a/website/demo.ts +++ b/website/demo.ts @@ -184,6 +184,13 @@ function renderRoutes() { const directionLabel = `Direction ${group.directionId}`; const tripCount = group.trips.length; + // Sort trips by short name within each group + const sortedTrips = [...group.trips].sort((a, b) => { + const aName = a.trip_short_name || a.trip_id; + const bName = b.trip_short_name || b.trip_id; + return aName.localeCompare(bName, undefined, { numeric: true }); + }); + return `
@@ -191,15 +198,21 @@ function renderRoutes() {
${directionLabel} • ${tripCount} trip${tripCount > 1 ? 's' : ''}
- ${group.trips.map(trip => ` -
-
-
Trip: ${escapeHtml(trip.trip_id)}
-
Service: ${escapeHtml(trip.service_id)}
+ ${sortedTrips.map(trip => { + const tripName = trip.trip_short_name || trip.trip_id; + const showTripId = trip.trip_short_name ? trip.trip_id : null; + + return ` +
+
+
${escapeHtml(tripName)}
+ ${showTripId ? `
ID: ${escapeHtml(showTripId)}
` : ''} +
Service: ${escapeHtml(trip.service_id)}
+
+
→
-
→
-
- `).join('')} + `; + }).join('')}
`; diff --git a/website/style.css b/website/style.css index 1d0b500..4842e2d 100644 --- a/website/style.css +++ b/website/style.css @@ -432,6 +432,18 @@ footer { flex: 1; } +.trip-name { + font-size: 1rem; + font-weight: 600; + margin-bottom: 0.25rem; +} + +.trip-id-secondary { + font-size: 0.8rem; + color: var(--text-secondary); + margin-bottom: 0.25rem; +} + .trip-id { font-size: 0.95rem; margin-bottom: 0.25rem;