Skip to content

fix(gtfs): support GTFS transfer types 4 and 5 #311

@munterfi

Description

@munterfi

Description

The application currently experiences a fatal crash during the startup process when parsing a GTFS feed that includes transfers.txt entries with a transfer_type of 4 or 5. These transfer types, while part of the official GTFS specification, are not yet implemented in the TransferType enum, leading to an IllegalArgumentException.

This issue was identified when using the Swiss GTFS data, which utilizes these transfer types.

Reference

According to the official GTFS documentation, the definitions for these transfer types are:

  • 4 - In-seat transfer: Passengers can transfer from one trip to another by staying on board the same vehicle.
  • 5 - In-seat transfer not allowed: Passengers must alight from the vehicle and re-board to transfer between sequential trips.

Problem

The current implementation of the TransferType enum only supports types 0 through 3. When the parser encounters a value of 4 or 5, it is unable to map it to an existing enum constant and throws an exception, which in turn prevents the application from starting.

package org.naviqore.gtfs.schedule.type;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

/**
 * Defines the types of transfers between routes as specified in the GTFS feed standards.
 * <p>
 * For more information on transfer types, see <a
 * href="https://support.google.com/transitpartners/answer/6377424?hl=en">GTFS Transfers</a>.
 *
 * @author munterfi
 */
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
@Getter
public enum TransferType {
    RECOMMENDED(0, "Recommended transfer point between two routes."),
    TIMED(1, "Timed transfer between two routes. The departing vehicle is expected to wait for the arriving one."),
    MINIMUM_TIME(2, "Transfer requires a minimum amount of time between arrival and departure to ensure a connection."),
    NOT_POSSIBLE(3, "Transfer is not possible between routes at this location.");

    private final int code;
    private final String description;

    public static TransferType parse(String code) {
        return parse(Integer.parseInt(code));
    }

    public static TransferType parse(int code) {
        for (TransferType type : TransferType.values()) {
            if (type.code == code) {
                return type;
            }
        }
        throw new IllegalArgumentException("No transfer type with code " + code + " found");
    }
}

Proposed Solution

To resolve this, the TransferType enum should be updated to include the IN_SEAT_TRANSFER (4) and IN_SEAT_TRANSFER_NOT_ALLOWED (5) types. Additionally, the application logic that handles transfers will need to be reviewed and potentially adjusted to correctly interpret and utilize these new transfer types.

Reproducible Example

❯ docker run -p 8080:8080 -e GTFS_STATIC_URI=https://data.opentransportdata.swiss/dataset/timetable-2025-gtfs2020/permalink ghcr.io/naviqore/public-transit-service:latest

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.4.5)

2025-08-14T15:15:23.848Z  INFO 1 --- [           main] org.naviqore.app.Application             : Starting Application using Java 21 with PID 1 (/app/app.jar started by root in /app)
2025-08-14T15:15:23.849Z  INFO 1 --- [           main] org.naviqore.app.Application             : No active profile set, falling back to 1 default profile: "default"
2025-08-14T15:15:25.041Z  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8080 (http)
2025-08-14T15:15:25.054Z  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2025-08-14T15:15:25.054Z  INFO 1 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.40]
2025-08-14T15:15:25.079Z  INFO 1 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2025-08-14T15:15:25.080Z  INFO 1 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1181 ms
2025-08-14T15:15:25.263Z  INFO 1 --- [           main] o.n.a.s.PublicTransitSpringService       : Initializing public transit spring service
2025-08-14T15:15:25.428Z  INFO 1 --- [           main] o.naviqore.utils.network.FileDownloader  : Downloading file: https://data.opentransportdata.swiss/dataset/timetable-2025-gtfs2020/permalink
2025-08-14T15:15:41.274Z  INFO 1 --- [           main] o.naviqore.utils.network.FileDownloader  : Dataset downloaded successfully to: /tmp/tmp_gtfs_2065902117131781254/gtfs.zip
2025-08-14T15:15:41.288Z  INFO 1 --- [           main] o.n.gtfs.schedule.GtfsScheduleReader     : Reading GTFS from ZIP file: /tmp/tmp_gtfs_2065902117131781254/gtfs.zip
2025-08-14T15:15:41.289Z  INFO 1 --- [           main] o.n.gtfs.schedule.GtfsScheduleReader     : Reading GTFS file from ZIP: agency.txt
2025-08-14T15:15:41.320Z  INFO 1 --- [           main] o.n.gtfs.schedule.GtfsScheduleReader     : Reading GTFS file from ZIP: calendar.txt
2025-08-14T15:15:41.519Z  INFO 1 --- [           main] o.n.gtfs.schedule.GtfsScheduleReader     : Reading GTFS file from ZIP: calendar_dates.txt
2025-08-14T15:15:49.689Z  INFO 1 --- [           main] o.n.gtfs.schedule.GtfsScheduleReader     : Reading GTFS file from ZIP: stops.txt
2025-08-14T15:15:49.923Z  INFO 1 --- [           main] o.n.gtfs.schedule.GtfsScheduleReader     : Reading GTFS file from ZIP: routes.txt
2025-08-14T15:15:49.935Z  INFO 1 --- [           main] o.n.gtfs.schedule.GtfsScheduleReader     : Reading GTFS file from ZIP: trips.txt
2025-08-14T15:15:52.840Z  INFO 1 --- [           main] o.n.gtfs.schedule.GtfsScheduleReader     : Reading GTFS file from ZIP: stop_times.txt
2025-08-14T15:16:26.067Z  INFO 1 --- [           main] o.n.gtfs.schedule.GtfsScheduleReader     : Reading GTFS file from ZIP: transfers.txt
2025-08-14T15:16:26.105Z  WARN 1 --- [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'routingController' defined in URL [jar:nested:/app/app.jar/!BOOT-INF/classes/!/org/naviqore/app/controller/RoutingController.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'publicTransitSpringService' defined in URL [jar:nested:/app/app.jar/!BOOT-INF/classes/!/org/naviqore/app/service/PublicTransitSpringService.class]: Failed to instantiate [org.naviqore.app.service.PublicTransitSpringService]: Constructor threw exception
2025-08-14T15:16:26.109Z  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Stopping service [Tomcat]
2025-08-14T15:16:26.125Z  INFO 1 --- [           main] .s.b.a.l.ConditionEvaluationReportLogger :

Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2025-08-14T15:16:26.144Z ERROR 1 --- [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'routingController' defined in URL [jar:nested:/app/app.jar/!BOOT-INF/classes/!/org/naviqore/app/controller/RoutingController.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'publicTransitSpringService' defined in URL [jar:nested:/app/app.jar/!BOOT-INF/classes/!/org/naviqore/app/service/PublicTransitSpringService.class]: Failed to instantiate [org.naviqore.app.service.PublicTransitSpringService]: Constructor threw exception
        at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:804) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1387) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1224) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:569) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:371) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.instantiateSingleton(DefaultListableBeanFactory.java:1221) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingleton(DefaultListableBeanFactory.java:1187) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:1122) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:987) ~[spring-context-6.2.6.jar!/:6.2.6]
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:627) ~[spring-context-6.2.6.jar!/:6.2.6]
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.4.5.jar!/:3.4.5]
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:753) ~[spring-boot-3.4.5.jar!/:3.4.5]
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439) ~[spring-boot-3.4.5.jar!/:3.4.5]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:318) ~[spring-boot-3.4.5.jar!/:3.4.5]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1362) ~[spring-boot-3.4.5.jar!/:3.4.5]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1351) ~[spring-boot-3.4.5.jar!/:3.4.5]
        at org.naviqore.app.Application.main(Application.java:12) ~[!/:na]
        at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:580) ~[na:na]
        at org.springframework.boot.loader.launch.Launcher.launch(Launcher.java:102) ~[app.jar:na]
        at org.springframework.boot.loader.launch.Launcher.launch(Launcher.java:64) ~[app.jar:na]
        at org.springframework.boot.loader.launch.JarLauncher.main(JarLauncher.java:40) ~[app.jar:na]
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'publicTransitSpringService' defined in URL [jar:nested:/app/app.jar/!BOOT-INF/classes/!/org/naviqore/app/service/PublicTransitSpringService.class]: Failed to instantiate [org.naviqore.app.service.PublicTransitSpringService]: Constructor threw exception
        at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:321) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:309) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1387) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1224) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:569) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:529) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:339) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:371) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:337) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1739) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1627) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:913) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791) ~[spring-beans-6.2.6.jar!/:6.2.6]
        ... 26 common frames omitted
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.naviqore.app.service.PublicTransitSpringService]: Constructor threw exception
        at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:222) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:145) ~[spring-beans-6.2.6.jar!/:6.2.6]
        at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:318) ~[spring-beans-6.2.6.jar!/:6.2.6]
        ... 40 common frames omitted
Caused by: java.lang.IllegalArgumentException: No transfer type with code 4 found
        at org.naviqore.gtfs.schedule.type.TransferType.parse(TransferType.java:36) ~[naviqore-gtfs-2.1.0.jar!/:na]
        at org.naviqore.gtfs.schedule.type.TransferType.parse(TransferType.java:27) ~[naviqore-gtfs-2.1.0.jar!/:na]
        at org.naviqore.gtfs.schedule.GtfsScheduleParser.parseTransfers(GtfsScheduleParser.java:116) ~[naviqore-gtfs-2.1.0.jar!/:na]
        at org.naviqore.gtfs.schedule.GtfsScheduleParser.parse(GtfsScheduleParser.java:40) ~[naviqore-gtfs-2.1.0.jar!/:na]
        at org.naviqore.gtfs.schedule.GtfsScheduleReader.lambda$readCsvRecords$0(GtfsScheduleReader.java:110) ~[naviqore-gtfs-2.1.0.jar!/:na]
        at java.base/java.lang.Iterable.forEach(Iterable.java:75) ~[na:na]
        at org.naviqore.gtfs.schedule.GtfsScheduleReader.readCsvRecords(GtfsScheduleReader.java:110) ~[naviqore-gtfs-2.1.0.jar!/:na]
        at org.naviqore.gtfs.schedule.GtfsScheduleReader.readFromZip(GtfsScheduleReader.java:70) ~[naviqore-gtfs-2.1.0.jar!/:na]
        at org.naviqore.gtfs.schedule.GtfsScheduleReader.read(GtfsScheduleReader.java:139) ~[naviqore-gtfs-2.1.0.jar!/:na]
        at org.naviqore.app.infrastructure.GtfsScheduleUrl.get(GtfsScheduleUrl.java:27) ~[!/:na]
        at org.naviqore.service.gtfs.raptor.GtfsRaptorServiceInitializer.<init>(GtfsRaptorServiceInitializer.java:40) ~[naviqore-public-transit-service-2.1.0.jar!/:na]
        at org.naviqore.service.PublicTransitServiceFactory.create(PublicTransitServiceFactory.java:15) ~[naviqore-public-transit-service-2.1.0.jar!/:na]
        at org.naviqore.app.service.PublicTransitSpringService.createDelegate(PublicTransitSpringService.java:49) ~[!/:na]
        at org.naviqore.app.service.PublicTransitSpringService.<init>(PublicTransitSpringService.java:32) ~[!/:na]
        at java.base/jdk.internal.reflect.DirectConstructorHandleAccessor.newInstance(DirectConstructorHandleAccessor.java:62) ~[na:na]
        at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:502) ~[na:na]
        at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:486) ~[na:na]
        at org.springframework.beans.BeanUtils.instantiateClass(BeanUtils.java:209) ~[spring-beans-6.2.6.jar!/:6.2.6]
        ... 42 common frames omitted

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions