Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
edbc331
add netexMode and netexSubmode to gtfs graphql trip interface
tkalvas May 28, 2025
7085af9
formatting of schema.graphqls
tkalvas May 28, 2025
29be6f3
netex mode/submode in gtfs query cleanup
tkalvas Jun 5, 2025
40ea82e
generator library version update
tkalvas Jun 5, 2025
93111a7
csv file backed gtfs/netex source to replacementMode in Trip
tkalvas Jun 17, 2025
9d8c619
for some reason my yarn always changes this package.json
tkalvas Jun 17, 2025
7a1b78c
remove unused TransmodelSubmodeMapper
tkalvas Jun 17, 2025
73e3b26
fix merge
tkalvas Jun 17, 2025
e1b3b2c
fix mapping logic and better name for map function in service
tkalvas Jun 17, 2025
809b2dc
configure gtfs/netex submode mapping in a csv file
tkalvas Jun 25, 2025
cba5329
check submode mapping csv has all required columns
tkalvas Jun 25, 2025
7b4819c
allow only one submode mapping config file, and make its name implicit
tkalvas Jun 27, 2025
770ed57
store submode mapping in TimetableRepository, not Graph
tkalvas Jun 27, 2025
cee387f
import and test cleanup
tkalvas Jun 27, 2025
5225f2b
further test cleanup
tkalvas Jun 27, 2025
986a2fd
add Trip.originalMode
tkalvas Jun 27, 2025
5fb3eb5
javadoc for SubmodeMappingService
tkalvas Jun 27, 2025
f64dd21
more javadoc
tkalvas Jun 27, 2025
fcbd9d0
link all javadoc to the full explanation in SubmodeMappingService
tkalvas Jun 27, 2025
e4fa718
iterate through all trips of a route at netex load time to see if gtf…
tkalvas Jul 17, 2025
2b8d505
version according to Joels plan: emulate GTFS source for GTFS query, …
tkalvas Jul 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.opentripplanner.apis.gtfs;

import graphql.schema.GraphQLSchema;
import org.opentripplanner.model.impl.SubmodeMappingService;
import org.opentripplanner.routing.api.RoutingService;
import org.opentripplanner.routing.api.request.RouteRequest;
import org.opentripplanner.routing.fares.FareService;
Expand All @@ -20,7 +21,8 @@ public record GraphQLRequestContext(
RealtimeVehicleService realTimeVehicleService,
GraphQLSchema schema,
GraphFinder graphFinder,
RouteRequest defaultRouteRequest
RouteRequest defaultRouteRequest,
SubmodeMappingService submodeMappingService
) {
public static GraphQLRequestContext ofServerContext(OtpServerRequestContext context) {
return new GraphQLRequestContext(
Expand All @@ -32,7 +34,8 @@ public static GraphQLRequestContext ofServerContext(OtpServerRequestContext cont
context.realtimeVehicleService(),
context.schema(),
context.graphFinder(),
context.defaultRouteRequest()
context.defaultRouteRequest(),
context.submodeMappingService()
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,13 @@ public DataFetcher<String> longName() {

@Override
public DataFetcher<GraphQLTransitMode> mode() {
return environment -> TransitModeMapper.map(getSource(environment).getMode());
return environment -> {
var route = getSource(environment);
if (route.getGtfsReplacementMode() != null) {
return TransitModeMapper.map(route.getGtfsReplacementMode());
}
return TransitModeMapper.map(route.getMode());
};
}

@Override
Expand Down Expand Up @@ -223,7 +229,13 @@ public DataFetcher<Iterable<Trip>> trips() {

@Override
public DataFetcher<Integer> type() {
return environment -> getSource(environment).getGtfsType();
return environment -> {
var route = getSource(environment);
if (route.getGtfsReplacementType() != null) {
return route.getGtfsReplacementType();
}
return route.getGtfsType();
};
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@
import org.opentripplanner.apis.gtfs.generated.GraphQLTypes;
import org.opentripplanner.apis.gtfs.generated.GraphQLTypes.GraphQLBikesAllowed;
import org.opentripplanner.apis.gtfs.mapping.BikesAllowedMapper;
import org.opentripplanner.apis.gtfs.mapping.TransitModeMapper;
import org.opentripplanner.apis.gtfs.model.TripOccupancy;
import org.opentripplanner.apis.support.SemanticHash;
import org.opentripplanner.model.Timetable;
import org.opentripplanner.model.TripTimeOnDate;
import org.opentripplanner.model.impl.SubmodeMappingService;
import org.opentripplanner.routing.alertpatch.EntitySelector;
import org.opentripplanner.routing.alertpatch.TransitAlert;
import org.opentripplanner.routing.services.TransitAlertService;
Expand Down Expand Up @@ -426,4 +428,8 @@ private static Optional<LocalDate> getOptionalServiceDateArgument(
private Trip getSource(DataFetchingEnvironment environment) {
return environment.getSource();
}

private SubmodeMappingService getSubmodeMappingService(DataFetchingEnvironment environment) {
return environment.<GraphQLRequestContext>getContext().submodeMappingService();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import static org.opentripplanner.datastore.api.FileType.NETEX;
import static org.opentripplanner.datastore.api.FileType.OSM;
import static org.opentripplanner.datastore.api.FileType.REPORT;
import static org.opentripplanner.datastore.api.FileType.SUBMODE;
import static org.opentripplanner.datastore.api.FileType.UNKNOWN;

import com.google.common.collect.ArrayListMultimap;
Expand Down Expand Up @@ -102,6 +103,7 @@ public void open() {
addAll(findMultipleCompositeSources(config.gtfsFiles(), GTFS));
addAll(findMultipleCompositeSources(config.netexFiles(), NETEX));
addAll(findMultipleSources(config.emissionFiles(), EMISSION));
addAll(findMultipleSources(null, SUBMODE));

streetGraph = findSingleSource(config.streetGraph(), STREET_GRAPH_FILENAME, GRAPH);
graph = findSingleSource(config.graph(), GRAPH_FILENAME, GRAPH);
Expand Down Expand Up @@ -207,7 +209,7 @@ private CompositeDataSource findCompositeSource(
}
}

private List<DataSource> findMultipleSources(Collection<URI> uris, FileType type) {
private List<DataSource> findMultipleSources(@Nullable Collection<URI> uris, FileType type) {
if (uris == null || uris.isEmpty()) {
return localRepository.listExistingSources(type);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public enum FileType {
EMISSION("🌿", "Emission data"),
GRAPH("🌐", "OTP Graph file"),
REPORT("📈", "Issue report"),
SUBMODE("🚝", "Submode mapping"),
UNKNOWN("❓", "Unknown file");

private final String icon;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import static org.opentripplanner.datastore.api.FileType.NETEX;
import static org.opentripplanner.datastore.api.FileType.OSM;
import static org.opentripplanner.datastore.api.FileType.REPORT;
import static org.opentripplanner.datastore.api.FileType.SUBMODE;
import static org.opentripplanner.datastore.api.FileType.UNKNOWN;
import static org.opentripplanner.datastore.base.LocalDataSourceRepository.isCurrentDir;

Expand All @@ -35,6 +36,7 @@ public class FileDataSourceRepository implements LocalDataSourceRepository {

private final Pattern GRAPH_PATTERN = Pattern.compile("(?i)(street)?graph.*\\.obj");
private final Pattern EMISSION_PATTERN = Pattern.compile("(?i)(emission).*\\.(txt|csv)");
private final String SUBMODE_FILENAME = "submode-mapping.csv";

private final File baseDir;
private final Pattern gtfsLocalFilePattern;
Expand Down Expand Up @@ -192,6 +194,9 @@ private FileType resolveFileType(File file) {
if (EMISSION_PATTERN.matcher(name).find()) {
return EMISSION;
}
if (SUBMODE_FILENAME.equals(name)) {
return SUBMODE;
}
if (GRAPH_PATTERN.matcher(name).find()) {
return GRAPH;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ public static GraphBuilder create(
graphBuilder.addModule(factory.osmModule());
}

// Submode mapping has to be initialized before processing timetables
graphBuilder.addModule(factory.submodeMappingModule());

if (hasGtfs) {
graphBuilder.addModule(factory.gtfsModule());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static org.opentripplanner.datastore.api.FileType.GTFS;
import static org.opentripplanner.datastore.api.FileType.NETEX;
import static org.opentripplanner.datastore.api.FileType.OSM;
import static org.opentripplanner.datastore.api.FileType.SUBMODE;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
Expand Down Expand Up @@ -85,6 +86,7 @@ public GraphBuilderDataSources(
include(cli.doBuildStreet(), DEM);
include(cli.doBuildTransit(), GTFS);
include(cli.doBuildTransit(), NETEX);
include(cli.doBuildTransit(), SUBMODE);

selectFilesToImport();

Expand Down Expand Up @@ -130,6 +132,13 @@ public Iterable<ConfiguredDataSource<EmissionFeedParameters>> getEmissionConfigu
return ofStream(EMISSION).map(this::mapEmissionFeed).toList();
}

public Optional<DataSource> getSubmodeMappingDataSource() {
var dataSources = inputData.get(SUBMODE);
// There is either zero or one datasources of type SUBMODE. The inputData mechanism allows
// multiple datasources, because that is useful for the other types.
return dataSources.stream().findFirst();
}

/**
* Returns the optional data source for the stop consolidation configuration.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.opentripplanner.graph_builder.module.ned.ElevationModule;
import org.opentripplanner.graph_builder.module.osm.OsmModule;
import org.opentripplanner.gtfs.graphbuilder.GtfsModule;
import org.opentripplanner.model.impl.SubmodeMappingModule;
import org.opentripplanner.netex.NetexModule;
import org.opentripplanner.routing.fares.FareServiceFactory;
import org.opentripplanner.routing.graph.Graph;
Expand Down Expand Up @@ -64,6 +65,7 @@ public interface GraphBuilderFactory {
OsmModule osmModule();
PruneIslands pruneIslands();
StreetLinkerModule streetLinkerModule();
SubmodeMappingModule submodeMappingModule();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is nullable? If so, this should be marked as such and moved downwards a bit.

Copy link
Contributor Author

@tkalvas tkalvas Jun 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not. I think it's better and it is definitely easier to instantiate the module and have the service contain an empty map, than try to make sure null handling is correct everywhere.

And it's not even an empty map, because there is a default mapping.

TimeZoneAdjusterModule timeZoneAdjusterModule();
TripPatternNamer tripPatternNamer();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import org.opentripplanner.graph_builder.services.ned.ElevationGridCoverageFactory;
import org.opentripplanner.gtfs.graphbuilder.GtfsBundle;
import org.opentripplanner.gtfs.graphbuilder.GtfsModule;
import org.opentripplanner.model.impl.SubmodeMappingModule;
import org.opentripplanner.netex.NetexModule;
import org.opentripplanner.netex.configure.NetexConfigure;
import org.opentripplanner.osm.DefaultOsmProvider;
Expand Down Expand Up @@ -298,6 +299,15 @@ static DataImportIssueSummary providesDataImportIssueSummary(DataImportIssueStor
return new DataImportIssueSummary(issueStore.listIssues());
}

@Provides
@Singleton
static SubmodeMappingModule provideSubmodeMappingModule(
GraphBuilderDataSources graphBuilderDataSources,
TimetableRepository timetableRepository
) {
return new SubmodeMappingModule(graphBuilderDataSources, timetableRepository);
}

@Provides
@Singleton
static TurnRestrictionModule provideTurnRestrictionModule(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.opentripplanner.model;

import java.util.Arrays;

public enum FeedType {
GTFS("GTFS"),
NETEX("NeTEx");

private final String value;

FeedType(String value) {
this.value = value;
}

public String getValue() {
return value;
}

public static FeedType of(String value) {
return Arrays.stream(FeedType.values())
.filter(ft -> ft.getValue().equals(value))
.findFirst()
.orElse(null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.opentripplanner.model.impl;

import org.opentripplanner.model.FeedType;

/**
* The key part of a row in submode-mapping.csv, consisting of a feed type (GTFS or NeTEx)
* and a label, which is Route.type in GTFS and Trip.submode in NeTEx.
*
* @see SubmodeMappingService
*/
public record SubmodeMappingMatcher(FeedType inputFeedType, String inputLabel) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package org.opentripplanner.model.impl;

import com.csvreader.CsvReader;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does this dependency come from? Should we explicitly import it in pom (if it is not already)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Application/pom.xml

        <dependency>
            <groupId>net.sourceforge.javacsv</groupId>
            <artifactId>javacsv</artifactId>
            <version>2.0</version>
        </dependency>

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.Nullable;
import org.opentripplanner.datastore.api.DataSource;
import org.opentripplanner.framework.application.OtpAppException;
import org.opentripplanner.graph_builder.GraphBuilderDataSources;
import org.opentripplanner.graph_builder.model.GraphBuilderModule;
import org.opentripplanner.model.FeedType;
import org.opentripplanner.transit.model.basic.TransitMode;
import org.opentripplanner.transit.service.TimetableRepository;

/**
* Part of infra to map GTFS and NeTEx Trip.replacementMode similarly.
*
* @see SubmodeMappingService
*/
public class SubmodeMappingModule implements GraphBuilderModule {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Javadoc.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, it feels a bit weird that this is both a dagger module and a GraphBuilderModule.


private static final String INPUT_FEED_TYPE = "Input feed type";
private static final String INPUT_LABEL = "Input label";
private static final String NETEX_SUBMODE = "NeTEx submode";
private static final String REPLACEMENT_MODE = "Replacement mode";
private static final String ORIGINAL_MODE = "Original mode";
private static final String GTFS_REPLACEMENT_MODE = "GTFS replacement mode";
private static final String GTFS_REPLACEMENT_TYPE = "GTFS replacement type";
private static final String[] MANDATORY = { INPUT_FEED_TYPE, INPUT_LABEL };

private final TimetableRepository timetableRepository;

@Nullable
private final DataSource dataSource;

public SubmodeMappingModule(
GraphBuilderDataSources graphBuilderDataSources,
TimetableRepository timetableRepository
) {
this.timetableRepository = timetableRepository;
this.dataSource = graphBuilderDataSources.getSubmodeMappingDataSource().orElse(null);
}

private boolean isEmpty(@Nullable String string) {
return string == null || string.isEmpty();
}

private Map<SubmodeMappingMatcher, SubmodeMappingRow> read(DataSource dataSource) {
var map = new HashMap<SubmodeMappingMatcher, SubmodeMappingRow>();
try {
var reader = new CsvReader(dataSource.asInputStream(), StandardCharsets.UTF_8);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to check dataSource.exists() here before we use it?

reader.readHeaders();
var headers = reader.getHeaders();
for (var header : MANDATORY) {
if (Arrays.stream(headers).noneMatch(h -> h.equals(header))) {
throw new OtpAppException("submode mapping header not found: " + header);
}
}
while (reader.readRecord()) {
var inputFeedType = FeedType.of(reader.get(INPUT_FEED_TYPE));
if (inputFeedType == null) {
throw new OtpAppException(
"not a valid submode mapping feed type: " + reader.get(INPUT_FEED_TYPE)
);
}
var inputLabel = reader.get(INPUT_LABEL);
var netexSubmode = reader.get(NETEX_SUBMODE);
var replacementMode = isEmpty(reader.get(REPLACEMENT_MODE))
? null
: TransitMode.valueOf(reader.get(REPLACEMENT_MODE));
var originalMode = isEmpty(reader.get(ORIGINAL_MODE))
? null
: TransitMode.valueOf(reader.get(ORIGINAL_MODE));
var gtfsReplacementMode = isEmpty(reader.get(GTFS_REPLACEMENT_MODE))
? null
: TransitMode.valueOf(reader.get(GTFS_REPLACEMENT_MODE));
var gtfsReplacementType = isEmpty(reader.get(GTFS_REPLACEMENT_TYPE))
? null
: Integer.parseInt(reader.get(GTFS_REPLACEMENT_TYPE));
var matcher = new SubmodeMappingMatcher(inputFeedType, inputLabel);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the inputFeedType be an enum?

var row = new SubmodeMappingRow(
netexSubmode,
replacementMode,
originalMode,
gtfsReplacementMode,
gtfsReplacementType
);
map.put(matcher, row);
}
} catch (IOException ioe) {
throw new OtpAppException("cannot read submode mapping config file", ioe);
}
return map;
}

private Map<SubmodeMappingMatcher, SubmodeMappingRow> useDefaultMapping() {
var map = new HashMap<SubmodeMappingMatcher, SubmodeMappingRow>();
map.put(
new SubmodeMappingMatcher(FeedType.GTFS, "714"),
new SubmodeMappingRow("railReplacementBus", null, TransitMode.RAIL, null, null)
);
map.put(
new SubmodeMappingMatcher(FeedType.NETEX, "railReplacementBus"),
new SubmodeMappingRow("railReplacementBus", TransitMode.BUS, null, TransitMode.BUS, 714)
);
return map;
}

@Override
public void buildGraph() {
if (dataSource != null) {
timetableRepository.setSubmodeMapping(read(dataSource));
} else {
timetableRepository.setSubmodeMapping(useDefaultMapping());
}
}

@Override
public void checkInputs() {
if (dataSource != null && !dataSource.exists()) {
throw new RuntimeException(
"Submode mapping file " + dataSource.path() + " does not exist or cannot be read."
);
}
}
}
Loading
Loading