From cde6bd8e08878203ad32f0b8603de3a3435e177f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Nov 2025 15:26:29 +0000 Subject: [PATCH 1/3] Add GitHub Pages link to README - Add Documentation & Demo section with link to live site - Include description encouraging users to try the interactive demo --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 083e268..50a32c0 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,12 @@ A TypeScript library for loading GTFS (General Transit Feed Specification) data - Email: contact@sys-dev-run.fr - Website: https://www.sys-dev-run.fr/ +## Documentation & Demo + +📚 **[View Documentation and Interactive Demo](https://sysdevrun.github.io/gtfs-sqljs/)** + +Try the live demo to explore GTFS data, view routes with colors, and see trip schedules in action! + ## Features - ✅ Load GTFS data from ZIP files (URL or local path) From f0cb61ce7bfa254e4c59bf2ef711a7e5da7a9a10 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Nov 2025 15:37:20 +0000 Subject: [PATCH 2/3] Add flexible filter-based query API Major API improvement: Add flexible parameter object-based query methods that allow combining multiple filters instead of requiring separate methods for each parameter combination. New methods: - getStops(filters?) - Filter by stopId, stopCode, name, tripId, limit - getRoutes(filters?) - Filter by routeId, agencyId, limit - getTrips(filters?) - Filter by tripId, routeId, date, directionId, limit - getStopTimes(filters?) - Filter by tripId, stopId, routeId, date, directionId, limit Benefits: - More flexible and composable API - Easier to add new filter options without API changes - Cleaner code with fewer method overloads - Better TypeScript support with exported filter types All existing methods remain available for backward compatibility. Updated: - Query methods in src/queries/ with flexible filter implementations - Main GtfsSqlJs class with new public API methods - README.md with comprehensive documentation of new API - Website documentation highlighting the flexible approach - Exported filter types (StopFilters, RouteFilters, TripFilters, StopTimeFilters) All 77 tests pass successfully. --- README.md | 84 ++++++++++++++++++++++++++++++++- src/gtfs-sqljs.ts | 98 ++++++++++++++++++++++++++++++++++++++- src/index.ts | 2 +- src/queries/routes.ts | 52 +++++++++++++++++++++ src/queries/stop-times.ts | 80 ++++++++++++++++++++++++++++++++ src/queries/stops.ts | 64 +++++++++++++++++++++++++ src/queries/trips.ts | 64 +++++++++++++++++++++++++ website/index.html | 15 ++++++ 8 files changed, 454 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 50a32c0..084da68 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,71 @@ const gtfs = await GtfsSqlJs.fromDatabase(dbBuffer); ### Querying Data +The library provides two ways to query GTFS data: +1. **Flexible filter-based methods** (recommended) - Pass an object with optional filters +2. **Convenience methods** - Direct methods for common use cases + +#### Flexible Filter-Based Queries (Recommended) + +The new flexible API allows you to pass multiple optional filters in a single method call: + +```typescript +// Get stops - combine any filters +const stops = gtfs.getStops({ + name: 'Station', // Search by name + limit: 10 // Limit results +}); + +// Get routes - with or without filters +const allRoutes = gtfs.getRoutes(); +const agencyRoutes = gtfs.getRoutes({ agencyId: 'AGENCY_1' }); + +// Get trips - combine multiple filters +const trips = gtfs.getTrips({ + routeId: 'ROUTE_1', // Filter by route + date: '20240115', // Filter by date (gets active services) + directionId: 0, // Filter by direction + limit: 50 // Limit results +}); + +// Get stop times - flexible filtering +const stopTimes = gtfs.getStopTimes({ + stopId: 'STOP_123', // At a specific stop + routeId: 'ROUTE_1', // For a specific route + date: '20240115', // On a specific date + directionId: 0 // In a specific direction +}); +``` + +**Available Filter Options:** + +- `getStops(filters?)`: + - `stopId`: string - Filter by stop ID + - `stopCode`: string - Filter by stop code + - `name`: string - Search by stop name (partial match) + - `tripId`: string - Get stops for a trip + - `limit`: number - Limit results + +- `getRoutes(filters?)`: + - `routeId`: string - Filter by route ID + - `agencyId`: string - Filter by agency + - `limit`: number - Limit results + +- `getTrips(filters?)`: + - `tripId`: string - Filter by trip ID + - `routeId`: string - Filter by route + - `date`: string - Filter by date (YYYYMMDD format) + - `directionId`: number - Filter by direction + - `limit`: number - Limit results + +- `getStopTimes(filters?)`: + - `tripId`: string - Filter by trip + - `stopId`: string - Filter by stop + - `routeId`: string - Filter by route + - `date`: string - Filter by date (YYYYMMDD format) + - `directionId`: number - Filter by direction + - `limit`: number - Limit results + #### Get Stop Information ```typescript @@ -319,11 +384,18 @@ example(); ### Instance Methods +#### 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) + #### Stop 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 @@ -341,6 +413,7 @@ example(); - `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 @@ -354,12 +427,19 @@ example(); ## TypeScript Support -This library is written in TypeScript and provides full type definitions for all GTFS entities: +This library is written in TypeScript and provides full type definitions for all GTFS entities and filter options: ```typescript -import type { Stop, Route, Trip, StopTime } from 'gtfs-sqljs'; +import type { Stop, Route, Trip, StopTime, TripFilters, StopTimeFilters } from 'gtfs-sqljs'; const stop: Stop = gtfs.getStopById('STOP_123')!; + +// Use filter types for better type safety +const filters: TripFilters = { + routeId: 'ROUTE_1', + directionId: 0 +}; +const trips = gtfs.getTrips(filters); ``` ## GTFS Specification diff --git a/src/gtfs-sqljs.ts b/src/gtfs-sqljs.ts index 74e9474..e6c176f 100644 --- a/src/gtfs-sqljs.ts +++ b/src/gtfs-sqljs.ts @@ -8,8 +8,8 @@ import { loadGTFSZip } from './loaders/zip-loader'; import { loadGTFSData } from './loaders/data-loader'; // Query methods -import { getStopById, getStopByCode, searchStopsByName, getAllStops, getStopsByTrip } from './queries/stops'; -import { getRouteById, getAllRoutes, getRoutesByAgency } from './queries/routes'; +import { getStopById, getStopByCode, searchStopsByName, getAllStops, getStopsByTrip, getStops, type StopFilters } from './queries/stops'; +import { getRouteById, getAllRoutes, getRoutesByAgency, getRoutes, type RouteFilters } from './queries/routes'; import { getActiveServiceIds, getCalendarByServiceId, @@ -22,16 +22,23 @@ import { getTripsByRouteAndService, getTripsByRouteServiceAndDirection, getTripsByService, + getTrips, + type TripFilters, } from './queries/trips'; import { getStopTimesByTrip, getStopTimesByStop, getStopTimesForStopRouteDirection, + getStopTimes, + type StopTimeFilters, } from './queries/stop-times'; // Types import type { Stop, Route, Trip, StopTime, Calendar, CalendarDate } from './types/gtfs'; +// Export filter types for users +export type { StopFilters, RouteFilters, TripFilters, StopTimeFilters }; + export interface GtfsSqlJsOptions { /** * Path or URL to GTFS ZIP file @@ -191,6 +198,14 @@ export class GtfsSqlJs { return getAllStops(this.db, limit); } + /** + * Get stops with optional filters + */ + getStops(filters?: StopFilters): Stop[] { + if (!this.db) throw new Error('Database not initialized'); + return getStops(this.db, filters); + } + // ==================== Route Methods ==================== /** @@ -217,6 +232,14 @@ export class GtfsSqlJs { return getRoutesByAgency(this.db, agencyId); } + /** + * Get routes with optional filters + */ + getRoutes(filters?: RouteFilters): Route[] { + if (!this.db) throw new Error('Database not initialized'); + return getRoutes(this.db, filters); + } + // ==================== Calendar Methods ==================== /** @@ -304,6 +327,39 @@ export class GtfsSqlJs { return getStopsByTrip(this.db, tripId); } + /** + * Get trips with optional filters + * + * @param filters - Optional filters + * @param filters.tripId - Filter by trip ID + * @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.limit - Limit number of results + * + * @example + * // Get all trips for a route on a specific date + * const trips = gtfs.getTrips({ routeId: 'ROUTE_1', date: '20240115' }); + * + * @example + * // Get all trips for a route going in one direction + * const trips = gtfs.getTrips({ routeId: 'ROUTE_1', directionId: 0 }); + */ + getTrips(filters?: TripFilters & { date?: string }): Trip[] { + if (!this.db) throw new Error('Database not initialized'); + + // Handle date parameter by converting it to serviceIds + const { date, ...restFilters } = filters || {}; + const finalFilters = { ...restFilters }; + + if (date) { + const serviceIds = getActiveServiceIds(this.db, date); + finalFilters.serviceIds = serviceIds; + } + + return getTrips(this.db, finalFilters); + } + // ==================== Stop Time Methods ==================== /** @@ -330,4 +386,42 @@ export class GtfsSqlJs { const serviceIds = getActiveServiceIds(this.db, date); return getStopTimesForStopRouteDirection(this.db, stopId, routeId, serviceIds, directionId); } + + /** + * Get stop times with optional filters + * + * @param filters - Optional filters + * @param filters.tripId - Filter by trip ID + * @param filters.stopId - Filter by stop ID + * @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.limit - Limit number of results + * + * @example + * // Get stop times for a specific trip + * const stopTimes = gtfs.getStopTimes({ tripId: 'TRIP_123' }); + * + * @example + * // Get stop times at a stop for a specific route on a date + * const stopTimes = gtfs.getStopTimes({ + * stopId: 'STOP_123', + * routeId: 'ROUTE_1', + * date: '20240115' + * }); + */ + getStopTimes(filters?: StopTimeFilters & { date?: string }): StopTime[] { + if (!this.db) throw new Error('Database not initialized'); + + // Handle date parameter by converting it to serviceIds + const { date, ...restFilters } = filters || {}; + const finalFilters = { ...restFilters }; + + if (date) { + const serviceIds = getActiveServiceIds(this.db, date); + finalFilters.serviceIds = serviceIds; + } + + return getStopTimes(this.db, finalFilters); + } } diff --git a/src/index.ts b/src/index.ts index 2ae7df5..1c84c39 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ * @license MIT */ -export { GtfsSqlJs, type GtfsSqlJsOptions } from './gtfs-sqljs'; +export { GtfsSqlJs, type GtfsSqlJsOptions, type StopFilters, type RouteFilters, type TripFilters, type StopTimeFilters } from './gtfs-sqljs'; // Export types export type { diff --git a/src/queries/routes.ts b/src/queries/routes.ts index 4868bbb..e4d5bec 100644 --- a/src/queries/routes.ts +++ b/src/queries/routes.ts @@ -5,6 +5,58 @@ import type { Database } from 'sql.js'; import type { Route } from '../types/gtfs'; +export interface RouteFilters { + routeId?: string; + agencyId?: string; + limit?: number; +} + +/** + * Get routes with optional filters + */ +export function getRoutes(db: Database, filters: RouteFilters = {}): Route[] { + const { routeId, agencyId, limit } = filters; + + // Build WHERE clause dynamically + const conditions: string[] = []; + const params: (string | number)[] = []; + + if (routeId) { + conditions.push('route_id = ?'); + params.push(routeId); + } + + if (agencyId) { + conditions.push('agency_id = ?'); + params.push(agencyId); + } + + // Build SQL query + let sql = 'SELECT * FROM routes'; + if (conditions.length > 0) { + sql += ' WHERE ' + conditions.join(' AND '); + } + sql += ' ORDER BY route_short_name, route_long_name'; + if (limit) { + sql += ' LIMIT ?'; + params.push(limit); + } + + const stmt = db.prepare(sql); + if (params.length > 0) { + stmt.bind(params); + } + + const routes: Route[] = []; + while (stmt.step()) { + const row = stmt.getAsObject() as Record; + routes.push(rowToRoute(row)); + } + + stmt.free(); + return routes; +} + /** * Get a route by its route_id */ diff --git a/src/queries/stop-times.ts b/src/queries/stop-times.ts index 357adfc..fc8f337 100644 --- a/src/queries/stop-times.ts +++ b/src/queries/stop-times.ts @@ -5,6 +5,86 @@ import type { Database } from 'sql.js'; import type { StopTime } from '../types/gtfs'; +export interface StopTimeFilters { + tripId?: string; + stopId?: string; + routeId?: string; + serviceIds?: string[]; + directionId?: number; + limit?: number; +} + +/** + * Get stop times with optional filters + */ +export function getStopTimes(db: Database, filters: StopTimeFilters = {}): StopTime[] { + const { tripId, stopId, routeId, serviceIds, directionId, limit } = filters; + + // Determine if we need to join with trips table + const needsTripsJoin = routeId || serviceIds || directionId !== undefined; + + // Build WHERE clause dynamically + const conditions: string[] = []; + const params: (string | number)[] = []; + + if (tripId) { + conditions.push(needsTripsJoin ? 'st.trip_id = ?' : 'trip_id = ?'); + params.push(tripId); + } + + if (stopId) { + conditions.push(needsTripsJoin ? 'st.stop_id = ?' : 'stop_id = ?'); + params.push(stopId); + } + + if (routeId) { + conditions.push('t.route_id = ?'); + params.push(routeId); + } + + if (serviceIds && serviceIds.length > 0) { + const placeholders = serviceIds.map(() => '?').join(', '); + conditions.push(`t.service_id IN (${placeholders})`); + params.push(...serviceIds); + } + + if (directionId !== undefined) { + conditions.push('t.direction_id = ?'); + params.push(directionId); + } + + // 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'; + + if (conditions.length > 0) { + sql += ' WHERE ' + conditions.join(' AND '); + } + + // Order by stop_sequence if filtering by trip, otherwise by arrival_time + sql += tripId ? ' ORDER BY stop_sequence' : ' ORDER BY arrival_time'; + + if (limit) { + sql += ' LIMIT ?'; + params.push(limit); + } + + const stmt = db.prepare(sql); + if (params.length > 0) { + stmt.bind(params); + } + + const stopTimes: StopTime[] = []; + while (stmt.step()) { + const row = stmt.getAsObject() as Record; + stopTimes.push(rowToStopTime(row)); + } + + stmt.free(); + return stopTimes; +} + /** * Get stop times for a trip */ diff --git a/src/queries/stops.ts b/src/queries/stops.ts index e76b3de..f58da3e 100644 --- a/src/queries/stops.ts +++ b/src/queries/stops.ts @@ -5,6 +5,70 @@ import type { Database } from 'sql.js'; import type { Stop } from '../types/gtfs'; +export interface StopFilters { + stopId?: string; + stopCode?: string; + name?: string; + tripId?: string; + limit?: number; +} + +/** + * Get stops with optional filters + */ +export function getStops(db: Database, filters: StopFilters = {}): Stop[] { + const { stopId, stopCode, name, tripId, limit } = filters; + + // Handle special case: get stops by trip (requires JOIN) + if (tripId) { + return getStopsByTrip(db, tripId); + } + + // Build WHERE clause dynamically + const conditions: string[] = []; + const params: (string | number)[] = []; + + if (stopId) { + conditions.push('stop_id = ?'); + params.push(stopId); + } + + if (stopCode) { + conditions.push('stop_code = ?'); + params.push(stopCode); + } + + if (name) { + conditions.push('stop_name LIKE ?'); + params.push(`%${name}%`); + } + + // Build SQL query + let sql = 'SELECT * FROM stops'; + if (conditions.length > 0) { + sql += ' WHERE ' + conditions.join(' AND '); + } + sql += ' ORDER BY stop_name'; + if (limit) { + sql += ' LIMIT ?'; + params.push(limit); + } + + const stmt = db.prepare(sql); + if (params.length > 0) { + stmt.bind(params); + } + + const stops: Stop[] = []; + while (stmt.step()) { + const row = stmt.getAsObject() as Record; + stops.push(rowToStop(row)); + } + + stmt.free(); + return stops; +} + /** * Get a stop by its stop_id */ diff --git a/src/queries/trips.ts b/src/queries/trips.ts index 93485f0..77e745f 100644 --- a/src/queries/trips.ts +++ b/src/queries/trips.ts @@ -5,6 +5,70 @@ import type { Database } from 'sql.js'; import type { Trip } from '../types/gtfs'; +export interface TripFilters { + tripId?: string; + routeId?: string; + serviceIds?: string[]; + directionId?: number; + limit?: number; +} + +/** + * Get trips with optional filters + */ +export function getTrips(db: Database, filters: TripFilters = {}): Trip[] { + const { tripId, routeId, serviceIds, directionId, limit } = filters; + + // Build WHERE clause dynamically + const conditions: string[] = []; + const params: (string | number)[] = []; + + if (tripId) { + conditions.push('trip_id = ?'); + params.push(tripId); + } + + if (routeId) { + conditions.push('route_id = ?'); + params.push(routeId); + } + + if (serviceIds && serviceIds.length > 0) { + const placeholders = serviceIds.map(() => '?').join(', '); + conditions.push(`service_id IN (${placeholders})`); + params.push(...serviceIds); + } + + if (directionId !== undefined) { + conditions.push('direction_id = ?'); + params.push(directionId); + } + + // Build SQL query + let sql = 'SELECT * FROM trips'; + if (conditions.length > 0) { + sql += ' WHERE ' + conditions.join(' AND '); + } + if (limit) { + sql += ' LIMIT ?'; + params.push(limit); + } + + const stmt = db.prepare(sql); + if (params.length > 0) { + stmt.bind(params); + } + + const trips: Trip[] = []; + while (stmt.step()) { + const row = stmt.getAsObject() as Record; + trips.push(rowToTrip(row)); + } + + stmt.free(); + return trips; +} + /** * Get a trip by its trip_id */ diff --git a/website/index.html b/website/index.html index b18171c..33753b6 100644 --- a/website/index.html +++ b/website/index.html @@ -86,6 +86,21 @@

Quick Start

API Methods

+

⭐ Flexible Filter-Based Methods (Recommended)

+

Pass optional filter objects to combine multiple criteria:

+
    +
  • getStops(filters?) - Filter by: stopId, stopCode, name, tripId, limit
  • +
  • getRoutes(filters?) - Filter by: routeId, agencyId, limit
  • +
  • getTrips(filters?) - Filter by: tripId, routeId, date, directionId, limit
  • +
  • getStopTimes(filters?) - Filter by: tripId, stopId, routeId, date, directionId, limit
  • +
+
// Example: Get trips for a route on a specific date going one direction
+const trips = gtfs.getTrips({
+  routeId: 'ROUTE_1',
+  date: '20240115',
+  directionId: 0
+});
+

Stop Methods

  • getStopById(stopId) - Get stop by ID
  • From 523bcb5985ee79472f77fafb3d7cf074c0d4c9ea Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Nov 2025 16:20:33 +0000 Subject: [PATCH 3/3] Fix ZIP loader to handle relative paths in browser The demo was failing because the ZIP loader tried to use Node.js fs module for relative paths like './car-jaune.zip' even in browser environments. Changes: - Detect browser vs Node.js environment using process.versions.node check - In browser: use fetch() for both absolute URLs and relative paths - In Node.js: use fs for file paths, fetch for URLs - Properly handle relative URLs in browser (e.g., ./car-jaune.zip) This fixes the error: 'Cannot read properties of undefined (reading readFile)' All 77 tests still pass. --- src/loaders/zip-loader.ts | 30 +++++++++++++++++++++++------- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/loaders/zip-loader.ts b/src/loaders/zip-loader.ts index d0709a7..7eb0674 100644 --- a/src/loaders/zip-loader.ts +++ b/src/loaders/zip-loader.ts @@ -50,13 +50,30 @@ export async function loadGTFSZip(source: string | ArrayBuffer | Uint8Array): Pr * Fetch ZIP file from URL or file path */ async function fetchZip(source: string): Promise { - // Check if source is a URL or file path + // Check if source is a URL const isUrl = source.startsWith('http://') || source.startsWith('https://'); - // If it's a file path, try to use fs first - if (!isUrl) { + // For URLs, always use fetch + if (isUrl) { + if (typeof fetch !== 'undefined') { + const response = await fetch(source); + if (!response.ok) { + throw new Error(`Failed to fetch GTFS ZIP: ${response.status} ${response.statusText}`); + } + return await response.arrayBuffer(); + } + throw new Error('fetch is not available to load URL'); + } + + // For non-URLs, try Node.js fs first, fall back to fetch for browser + // Check if we're in Node.js environment + const isNode = typeof process !== 'undefined' && + process.versions != null && + process.versions.node != null; + + if (isNode) { + // In Node.js, treat as file path try { - // Dynamic import for Node.js fs module const fs = await import('fs'); const buffer = await fs.promises.readFile(source); return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); @@ -65,9 +82,8 @@ async function fetchZip(source: string): Promise { } } - // For URLs, use fetch + // In browser, treat as relative URL and use fetch if (typeof fetch !== 'undefined') { - // Browser or Node.js with fetch support const response = await fetch(source); if (!response.ok) { throw new Error(`Failed to fetch GTFS ZIP: ${response.status} ${response.statusText}`); @@ -75,7 +91,7 @@ async function fetchZip(source: string): Promise { return await response.arrayBuffer(); } - throw new Error('No method available to fetch ZIP file from URL'); + throw new Error('No method available to load ZIP file'); } /**