From 07ecda4cb0374a011acd3881d522cf7d6b1d20c5 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Thu, 15 Aug 2024 13:14:34 +0100 Subject: [PATCH 01/40] refactor(Initial fares v2 work): --- src/main/java/com/conveyal/gtfs/GTFSFeed.java | 42 + .../gtfs/graphql/GraphQLGtfsSchema.java | 289 ++++++ .../conveyal/gtfs/loader/EntityPopulator.java | 88 ++ .../java/com/conveyal/gtfs/loader/Feed.java | 18 + .../conveyal/gtfs/loader/FeedLoadResult.java | 22 + .../gtfs/loader/JdbcGTFSFeedConverter.java | 12 + .../gtfs/loader/JdbcGtfsExporter.java | 13 +- .../conveyal/gtfs/loader/JdbcGtfsLoader.java | 10 + .../gtfs/loader/JdbcGtfsSnapshotter.java | 11 + .../java/com/conveyal/gtfs/loader/Table.java | 127 ++- .../java/com/conveyal/gtfs/model/Area.java | 87 ++ .../java/com/conveyal/gtfs/model/Entity.java | 13 + .../com/conveyal/gtfs/model/FareLegRule.java | 138 +++ .../com/conveyal/gtfs/model/FareMedia.java | 97 ++ .../com/conveyal/gtfs/model/FareProduct.java | 115 +++ .../conveyal/gtfs/model/FareTransferRule.java | 128 +++ .../java/com/conveyal/gtfs/model/Network.java | 86 ++ .../java/com/conveyal/gtfs/model/Route.java | 11 +- .../com/conveyal/gtfs/model/RouteNetwork.java | 88 ++ .../com/conveyal/gtfs/model/StopArea.java | 93 ++ .../com/conveyal/gtfs/model/TimeFrame.java | 110 +++ .../com/conveyal/gtfs/GTFSFaresV2Test.java | 275 ++++++ .../java/com/conveyal/gtfs/GTFSFeedTest.java | 164 ++-- src/test/java/com/conveyal/gtfs/GTFSTest.java | 170 +++- .../java/com/conveyal/gtfs/TestUtils.java | 91 +- .../java/com/conveyal/gtfs/dto/RouteDTO.java | 2 + .../fake-agency-with-fares-v2/agency.txt | 2 + .../fake-agency-with-fares-v2/areas.txt | 40 + .../fake-agency-with-fares-v2/calendar.txt | 2 + .../calendar_dates.txt | 3 + .../fare_leg_rules.txt | 697 ++++++++++++++ .../fake-agency-with-fares-v2/fare_media.txt | 5 + .../fare_products.txt | 97 ++ .../fare_transfer_rules.txt | 38 + .../fake-agency-with-fares-v2/feed_info.txt | 2 + .../fake-agency-with-fares-v2/networks.txt | 2 + .../route_networks.txt | 2 + .../fake-agency-with-fares-v2/routes.txt | 2 + .../fake-agency-with-fares-v2/shapes.txt | 8 + .../fake-agency-with-fares-v2/stop_areas.txt | 848 ++++++++++++++++++ .../fake-agency-with-fares-v2/stop_times.txt | 7 + .../fake-agency-with-fares-v2/stops.txt | 5 + .../fake-agency-with-fares-v2/timeframes.txt | 8 + .../fake-agency-with-fares-v2/trips.txt | 4 + src/test/resources/graphql/feedAreas.txt | 19 + .../resources/graphql/feedFareLegRules.txt | 51 ++ src/test/resources/graphql/feedFareMedias.txt | 11 + .../resources/graphql/feedFareProducts.txt | 18 + .../graphql/feedFareTransferRules.txt | 48 + src/test/resources/graphql/feedNetworks.txt | 9 + .../resources/graphql/feedRouteNetworks.txt | 9 + src/test/resources/graphql/feedStopAreas.txt | 14 + src/test/resources/graphql/feedTimeFrames.txt | 12 + .../gtfs/GTFSFaresV2Test/canFetchAreas-0.json | 210 +++++ .../canFetchFareLegRules-0.json | 416 +++++++++ .../GTFSFaresV2Test/canFetchFareMedias-0.json | 28 + .../canFetchFareProducts-0.json | 128 +++ .../canFetchFareTransferRules-0.json | 662 ++++++++++++++ .../GTFSFaresV2Test/canFetchNetworks-0.json | 11 + .../canFetchRouteNetworks-0.json | 11 + .../GTFSFaresV2Test/canFetchStopAreas-0.json | 408 +++++++++ .../GTFSFaresV2Test/canFetchTimeFrames-0.json | 50 ++ 62 files changed, 6046 insertions(+), 141 deletions(-) create mode 100644 src/main/java/com/conveyal/gtfs/model/Area.java create mode 100644 src/main/java/com/conveyal/gtfs/model/FareLegRule.java create mode 100644 src/main/java/com/conveyal/gtfs/model/FareMedia.java create mode 100644 src/main/java/com/conveyal/gtfs/model/FareProduct.java create mode 100644 src/main/java/com/conveyal/gtfs/model/FareTransferRule.java create mode 100644 src/main/java/com/conveyal/gtfs/model/Network.java create mode 100644 src/main/java/com/conveyal/gtfs/model/RouteNetwork.java create mode 100644 src/main/java/com/conveyal/gtfs/model/StopArea.java create mode 100644 src/main/java/com/conveyal/gtfs/model/TimeFrame.java create mode 100644 src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java create mode 100644 src/test/resources/fake-agency-with-fares-v2/agency.txt create mode 100644 src/test/resources/fake-agency-with-fares-v2/areas.txt create mode 100644 src/test/resources/fake-agency-with-fares-v2/calendar.txt create mode 100644 src/test/resources/fake-agency-with-fares-v2/calendar_dates.txt create mode 100644 src/test/resources/fake-agency-with-fares-v2/fare_leg_rules.txt create mode 100644 src/test/resources/fake-agency-with-fares-v2/fare_media.txt create mode 100644 src/test/resources/fake-agency-with-fares-v2/fare_products.txt create mode 100644 src/test/resources/fake-agency-with-fares-v2/fare_transfer_rules.txt create mode 100644 src/test/resources/fake-agency-with-fares-v2/feed_info.txt create mode 100644 src/test/resources/fake-agency-with-fares-v2/networks.txt create mode 100644 src/test/resources/fake-agency-with-fares-v2/route_networks.txt create mode 100644 src/test/resources/fake-agency-with-fares-v2/routes.txt create mode 100644 src/test/resources/fake-agency-with-fares-v2/shapes.txt create mode 100644 src/test/resources/fake-agency-with-fares-v2/stop_areas.txt create mode 100644 src/test/resources/fake-agency-with-fares-v2/stop_times.txt create mode 100644 src/test/resources/fake-agency-with-fares-v2/stops.txt create mode 100644 src/test/resources/fake-agency-with-fares-v2/timeframes.txt create mode 100644 src/test/resources/fake-agency-with-fares-v2/trips.txt create mode 100644 src/test/resources/graphql/feedAreas.txt create mode 100644 src/test/resources/graphql/feedFareLegRules.txt create mode 100644 src/test/resources/graphql/feedFareMedias.txt create mode 100644 src/test/resources/graphql/feedFareProducts.txt create mode 100644 src/test/resources/graphql/feedFareTransferRules.txt create mode 100644 src/test/resources/graphql/feedNetworks.txt create mode 100644 src/test/resources/graphql/feedRouteNetworks.txt create mode 100644 src/test/resources/graphql/feedStopAreas.txt create mode 100644 src/test/resources/graphql/feedTimeFrames.txt create mode 100644 src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchAreas-0.json create mode 100644 src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareLegRules-0.json create mode 100644 src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareMedias-0.json create mode 100644 src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareProducts-0.json create mode 100644 src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareTransferRules-0.json create mode 100644 src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchNetworks-0.json create mode 100644 src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchRouteNetworks-0.json create mode 100644 src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchStopAreas-0.json create mode 100644 src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchTimeFrames-0.json diff --git a/src/main/java/com/conveyal/gtfs/GTFSFeed.java b/src/main/java/com/conveyal/gtfs/GTFSFeed.java index fe182a8e8..b468e9afb 100644 --- a/src/main/java/com/conveyal/gtfs/GTFSFeed.java +++ b/src/main/java/com/conveyal/gtfs/GTFSFeed.java @@ -110,6 +110,16 @@ public class GTFSFeed implements Cloneable, Closeable { /* A place to store an event bus that is passed through constructor. */ public transient EventBus eventBus; + public final Map areas; + public final Map stop_areas; + public final Map fare_products; + public final Map fare_medias; + public final Map time_frames; + public final Map fare_leg_rules; + public final Map fare_transfer_rules; + public final Map networks; + public final Map route_networks; + /** * The order in which we load the tables is important for two reasons. * 1. We must load feed_info first so we know the feed ID before loading any other entities. This could be relaxed @@ -177,6 +187,18 @@ else if (feedId == null || feedId.isEmpty()) { new Trip.Loader(this).loadTable(zip); new Frequency.Loader(this).loadTable(zip); new StopTime.Loader(this).loadTable(zip); // comment out this line for quick testing using NL feed + + // Fares v2. + new Area.Loader(this).loadTable(zip); + new StopArea.Loader(this).loadTable(zip); + new TimeFrame.Loader(this).loadTable(zip); + new Network.Loader(this).loadTable(zip); + new RouteNetwork.Loader(this).loadTable(zip); + new FareMedia.Loader(this).loadTable(zip); + new FareProduct.Loader(this).loadTable(zip); + new FareLegRule.Loader(this).loadTable(zip); + new FareTransferRule.Loader(this).loadTable(zip); + LOG.info("{} errors", errors.size()); for (GTFSError error : errors) { LOG.info("{}", error); @@ -219,6 +241,17 @@ public void toFile (String file) { new StopTime.Writer(this).writeTable(zip); new Pattern.Writer(this).writeTable(zip); + // Fares v2. + new Area.Writer(this).writeTable(zip); + new StopArea.Writer(this).writeTable(zip); + new TimeFrame.Writer(this).writeTable(zip); + new Network.Writer(this).writeTable(zip); + new RouteNetwork.Writer(this).writeTable(zip); + new FareMedia.Writer(this).writeTable(zip); + new FareProduct.Writer(this).writeTable(zip); + new FareLegRule.Writer(this).writeTable(zip); + new FareTransferRule.Writer(this).writeTable(zip); + zip.close(); LOG.info("GTFS file written"); @@ -608,8 +641,15 @@ private GTFSFeed (DB db) { this.db = db; agency = db.getTreeMap("agency"); + areas = db.getTreeMap("area"); + fare_leg_rules = db.getTreeMap("fare_leg_rules"); + fare_medias = db.getTreeMap("fare_medias"); + fare_products = db.getTreeMap("fare_products"); + fare_transfer_rules = db.getTreeMap("fare_transfer_rules"); feedInfo = db.getTreeMap("feed_info"); + networks = db.getTreeMap("networks"); routes = db.getTreeMap("routes"); + route_networks = db.getTreeMap("route_networks"); trips = db.getTreeMap("trips"); stop_times = db.getTreeMap("stop_times"); frequencies = db.getTreeSet("frequencies"); @@ -618,6 +658,8 @@ private GTFSFeed (DB db) { fares = db.getTreeMap("fares"); services = db.getTreeMap("services"); shape_points = db.getTreeMap("shape_points"); + stop_areas = db.getTreeMap("stop_areas"); + time_frames = db.getTreeMap("time_frames"); translations = db.getTreeMap("translations"); attributions = db.getTreeMap("attributions"); diff --git a/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsSchema.java b/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsSchema.java index 6c4032dc2..ac5b10531 100644 --- a/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsSchema.java +++ b/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsSchema.java @@ -9,6 +9,16 @@ import com.conveyal.gtfs.graphql.fetchers.RowCountFetcher; import com.conveyal.gtfs.graphql.fetchers.SQLColumnFetcher; import com.conveyal.gtfs.graphql.fetchers.SourceObjectFetcher; +import com.conveyal.gtfs.model.Area; +import com.conveyal.gtfs.model.FareLegRule; +import com.conveyal.gtfs.model.FareMedia; +import com.conveyal.gtfs.model.FareProduct; +import com.conveyal.gtfs.model.FareTransferRule; +import com.conveyal.gtfs.model.Network; +import com.conveyal.gtfs.model.Route; +import com.conveyal.gtfs.model.RouteNetwork; +import com.conveyal.gtfs.model.StopArea; +import com.conveyal.gtfs.model.TimeFrame; import graphql.schema.Coercing; import graphql.schema.GraphQLList; import graphql.schema.GraphQLObjectType; @@ -622,6 +632,193 @@ public class GraphQLGtfsSchema { .build()) .build(); + public static final GraphQLObjectType stopAreaType = newObject().name("stopArea") + .description("A GTFS stop area object") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field(StopArea.AREA_ID_COLUMN_NAME)) + .field(MapFetcher.field(StopArea.STOP_ID_COLUMN_NAME)) + .field(newFieldDefinition() + .name("stops") + .type(new GraphQLList(stopType)) + .dataFetcher(new JDBCFetcher("stops", StopArea.STOP_ID_COLUMN_NAME)) + .build()) + .build(); + + public static final GraphQLObjectType areaType = newObject().name("area") + .description("A GTFS area object") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field(Area.AREA_ID_COLUMN_NAME)) + .field(MapFetcher.field(Area.AREA_NAME_COLUMN_NAME)) + .field(newFieldDefinition() + .name("stopAreas") + .type(new GraphQLList(stopAreaType)) + .dataFetcher(new JDBCFetcher(StopArea.TABLE_NAME, Area.AREA_ID_COLUMN_NAME)) + .build()) + .build(); + + public static final GraphQLObjectType fareMediaType = newObject().name("fareMedia") + .description("A GTFS fare media object") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field(FareMedia.FARE_MEDIA_ID_COLUMN_NAME)) + .field(MapFetcher.field(FareMedia.FARE_MEDIA_NAME_COLUMN_NAME)) + .field(MapFetcher.field(FareMedia.FARE_MEDIA_TYPE_COLUMN_NAME)) + .build(); + + public static final GraphQLObjectType fareProductType = newObject().name("fareProduct") + .description("A GTFS fare product object") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field(FareProduct.FARE_PRODUCT_ID_COLUMN_NAME)) + .field(MapFetcher.field(FareProduct.FARE_PRODUCT_NAME_COLUMN_NAME)) + .field(MapFetcher.field(FareProduct.FARE_MEDIA_ID_COLUMN_NAME)) + .field(MapFetcher.field(FareProduct.AMOUNT_COLUMN_NAME)) + .field(MapFetcher.field(FareProduct.CURRENCY_COLUMN_NAME)) + .field(newFieldDefinition() + .name("fareMedia") + .type(new GraphQLList(fareMediaType)) + .dataFetcher(new JDBCFetcher(FareMedia.TABLE_NAME, FareProduct.FARE_MEDIA_ID_COLUMN_NAME)) + .build()) + .build(); + + public static final GraphQLObjectType timeFrameType = newObject().name("timeFrame") + .description("A GTFS time frame object") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field(TimeFrame.TIME_FRAME_GROUP_ID_COLUMN_NAME)) + .field(MapFetcher.field(TimeFrame.START_TIME_COLUMN_NAME)) + .field(MapFetcher.field(TimeFrame.END_TIME_COLUMN_NAME)) + .field(MapFetcher.field(TimeFrame.SERVICE_ID_COLUMN_NAME)) + .build(); + + public static final GraphQLObjectType networkType = newObject().name("network") + .description("A GTFS network object") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field(Network.NETWORK_ID_COLUMN_NAME)) + .field(MapFetcher.field(Network.NETWORK_NAME_COLUMN_NAME)) + .build(); + + public static final GraphQLObjectType fareLegRuleType = newObject().name("fareLegRule") + .description("A GTFS fare leg rule object") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field(FareLegRule.LEG_GROUP_ID_COLUMN_NAME)) + .field(MapFetcher.field(FareLegRule.NETWORK_ID_COLUMN_NAME)) + .field(MapFetcher.field(FareLegRule.FROM_AREA_ID_COLUMN_NAME)) + .field(MapFetcher.field(FareLegRule.TO_AREA_ID_COLUMN_NAME)) + .field(MapFetcher.field(FareLegRule.FROM_TIMEFRAME_GROUP_ID_COLUMN_NAME)) + .field(MapFetcher.field(FareLegRule.TO_TIMEFRAME_GROUP_ID_COLUMN_NAME)) + .field(MapFetcher.field(FareLegRule.FARE_PRODUCT_ID_COLUMN_NAME)) + .field(MapFetcher.field(FareLegRule.RULE_PRIORITY_COLUMN_NAME)) + // Will return either routes or networks, not both. + .field(newFieldDefinition() + .name("routes") + .type(new GraphQLList(routeType)) + .dataFetcher(new JDBCFetcher(Route.TABLE_NAME, FareLegRule.NETWORK_ID_COLUMN_NAME)) + .build()) + .field(newFieldDefinition() + .name("networks") + .type(new GraphQLList(networkType)) + .dataFetcher(new JDBCFetcher(Network.TABLE_NAME, Network.NETWORK_ID_COLUMN_NAME)) + .build()) + .field(newFieldDefinition() + .name("fareProducts") + .type(new GraphQLList(fareProductType)) + .dataFetcher(new JDBCFetcher(FareProduct.TABLE_NAME, FareLegRule.FARE_PRODUCT_ID_COLUMN_NAME)) + .build()) + // fromTimeFrame and toTimeFrame may return multiple time frames. + .field(newFieldDefinition() + .name("fromTimeFrame") + .type(new GraphQLList(timeFrameType)) + .dataFetcher(new JDBCFetcher( + TimeFrame.TABLE_NAME, + FareLegRule.FROM_TIMEFRAME_GROUP_ID_COLUMN_NAME, + null, + false, + TimeFrame.TIME_FRAME_GROUP_ID_COLUMN_NAME) + ) + .build()) + .field(newFieldDefinition() + .name("toTimeFrame") + .type(new GraphQLList(timeFrameType)) + .dataFetcher(new JDBCFetcher( + TimeFrame.TABLE_NAME, + FareLegRule.TO_TIMEFRAME_GROUP_ID_COLUMN_NAME, + null, + false, + TimeFrame.TIME_FRAME_GROUP_ID_COLUMN_NAME) + ) + .build()) + .field(newFieldDefinition() + .name("toArea") + .type(new GraphQLList(areaType)) + .dataFetcher(new JDBCFetcher( + Area.TABLE_NAME, + FareLegRule.TO_AREA_ID_COLUMN_NAME, + null, + false, + Area.AREA_ID_COLUMN_NAME) + ) + .build()) + .field(newFieldDefinition() + .name("fromArea") + .type(new GraphQLList(areaType)) + .dataFetcher(new JDBCFetcher( + Area.TABLE_NAME, + FareLegRule.FROM_AREA_ID_COLUMN_NAME, + null, + false, + Area.AREA_ID_COLUMN_NAME) + ) + .build()) + .build(); + + public static final GraphQLObjectType fareTransferRuleType = newObject().name("fareTransferRule") + .description("A GTFS fare transfer rule object") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field(FareTransferRule.FROM_LEG_GROUP_ID_COLUMN_NAME)) + .field(MapFetcher.field(FareTransferRule.TO_LEG_GROUP_ID_COLUMN_NAME)) + .field(MapFetcher.field(FareTransferRule.TRANSFER_COUNT_COLUMN_NAME)) + .field(MapFetcher.field(FareTransferRule.DURATION_LIMIT_COLUMN_NAME)) + .field(MapFetcher.field(FareTransferRule.DURATION_LIMIT_TYPE_COLUMN_NAME)) + .field(MapFetcher.field(FareTransferRule.FARE_PRODUCT_ID_COLUMN_NAME)) + .field(newFieldDefinition() + .name("fareProducts") + .type(new GraphQLList(fareProductType)) + .dataFetcher(new JDBCFetcher(FareProduct.TABLE_NAME, FareProduct.FARE_PRODUCT_ID_COLUMN_NAME)) + .build()) + .field(newFieldDefinition() + .name("fromFareLegRule") + .type(new GraphQLList(fareLegRuleType)) + .dataFetcher(new JDBCFetcher( + FareLegRule.TABLE_NAME, + FareTransferRule.FROM_LEG_GROUP_ID_COLUMN_NAME, + null, + false, + FareLegRule.LEG_GROUP_ID_COLUMN_NAME) + ) + .build()) + .field(newFieldDefinition() + .name("toFareLegRule") + .type(new GraphQLList(fareLegRuleType)) + .dataFetcher(new JDBCFetcher( + FareLegRule.TABLE_NAME, + FareTransferRule.TO_LEG_GROUP_ID_COLUMN_NAME, + null, + false, + FareLegRule.LEG_GROUP_ID_COLUMN_NAME) + ) + .build()) + .build(); + + public static final GraphQLObjectType routeNetworkType = newObject().name("routeNetwork") + .description("A GTFS route network object") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field(RouteNetwork.NETWORK_ID_COLUMN_NAME)) + .field(MapFetcher.field(RouteNetwork.ROUTE_ID_COLUMN_NAME)) + .field(newFieldDefinition() + .name("networks") + .type(new GraphQLList(networkType)) + .dataFetcher(new JDBCFetcher(Network.TABLE_NAME, RouteNetwork.NETWORK_ID_COLUMN_NAME)) + .build()) + .build(); + /** * The GraphQL API type representing entries in the top-level table listing all the feeds imported into a gtfs-api * database, and with sub-fields for each table of GTFS entities within a single feed. @@ -823,6 +1020,98 @@ public class GraphQLGtfsSchema { .dataFetcher(new JDBCFetcher("translations")) .build() ) + .field(newFieldDefinition() + .name("area") + .type(new GraphQLList(GraphQLGtfsSchema.areaType)) + .argument(stringArg("namespace")) + .argument(multiStringArg("area_id")) + .argument(intArg(ID_ARG)) + .argument(intArg(LIMIT_ARG)) + .argument(intArg(OFFSET_ARG)) + .dataFetcher(new JDBCFetcher("areas")) + .build() + ) + .field(newFieldDefinition() + .name("stopArea") + .type(new GraphQLList(GraphQLGtfsSchema.stopAreaType)) + .argument(stringArg("namespace")) + .argument(multiStringArg("area_id")) + .argument(intArg(ID_ARG)) + .argument(intArg(LIMIT_ARG)) + .argument(intArg(OFFSET_ARG)) + .dataFetcher(new JDBCFetcher("stop_areas")) + .build() + ) + .field(newFieldDefinition() + .name("fareTransferRule") + .type(new GraphQLList(GraphQLGtfsSchema.fareTransferRuleType)) + .argument(stringArg("namespace")) + .argument(intArg(ID_ARG)) + .argument(intArg(LIMIT_ARG)) + .argument(intArg(OFFSET_ARG)) + .dataFetcher(new JDBCFetcher("fare_transfer_rules")) + .build() + ) + .field(newFieldDefinition() + .name("fareProduct") + .type(new GraphQLList(GraphQLGtfsSchema.fareProductType)) + .argument(stringArg("namespace")) + .argument(intArg(ID_ARG)) + .argument(intArg(LIMIT_ARG)) + .argument(intArg(OFFSET_ARG)) + .dataFetcher(new JDBCFetcher("fare_products")) + .build() + ) + .field(newFieldDefinition() + .name("fareMedia") + .type(new GraphQLList(GraphQLGtfsSchema.fareMediaType)) + .argument(stringArg("namespace")) + .argument(intArg(ID_ARG)) + .argument(intArg(LIMIT_ARG)) + .argument(intArg(OFFSET_ARG)) + .dataFetcher(new JDBCFetcher("fare_media")) + .build() + ) + .field(newFieldDefinition() + .name("fareLegRule") + .type(new GraphQLList(GraphQLGtfsSchema.fareLegRuleType)) + .argument(stringArg("namespace")) + .argument(intArg(ID_ARG)) + .argument(intArg(LIMIT_ARG)) + .argument(intArg(OFFSET_ARG)) + .dataFetcher(new JDBCFetcher("fare_leg_rules")) + .build() + ) + .field(newFieldDefinition() + .name("timeFrame") + .type(new GraphQLList(GraphQLGtfsSchema.timeFrameType)) + .argument(stringArg("namespace")) + .argument(intArg(ID_ARG)) + .argument(intArg(LIMIT_ARG)) + .argument(intArg(OFFSET_ARG)) + .dataFetcher(new JDBCFetcher(TimeFrame.TABLE_NAME)) + .build() + ) + .field(newFieldDefinition() + .name("network") + .type(new GraphQLList(GraphQLGtfsSchema.networkType)) + .argument(stringArg("namespace")) + .argument(intArg(ID_ARG)) + .argument(intArg(LIMIT_ARG)) + .argument(intArg(OFFSET_ARG)) + .dataFetcher(new JDBCFetcher(Network.TABLE_NAME)) + .build() + ) + .field(newFieldDefinition() + .name("routeNetwork") + .type(new GraphQLList(GraphQLGtfsSchema.routeNetworkType)) + .argument(stringArg("namespace")) + .argument(intArg(ID_ARG)) + .argument(intArg(LIMIT_ARG)) + .argument(intArg(OFFSET_ARG)) + .dataFetcher(new JDBCFetcher(RouteNetwork.TABLE_NAME)) + .build() + ) .build(); /** diff --git a/src/main/java/com/conveyal/gtfs/loader/EntityPopulator.java b/src/main/java/com/conveyal/gtfs/loader/EntityPopulator.java index c5c59d900..ca82d69e3 100644 --- a/src/main/java/com/conveyal/gtfs/loader/EntityPopulator.java +++ b/src/main/java/com/conveyal/gtfs/loader/EntityPopulator.java @@ -1,18 +1,27 @@ package com.conveyal.gtfs.loader; import com.conveyal.gtfs.model.Agency; +import com.conveyal.gtfs.model.Area; import com.conveyal.gtfs.model.Calendar; import com.conveyal.gtfs.model.CalendarDate; import com.conveyal.gtfs.model.Entity; import com.conveyal.gtfs.model.FareAttribute; +import com.conveyal.gtfs.model.FareLegRule; +import com.conveyal.gtfs.model.FareMedia; +import com.conveyal.gtfs.model.FareProduct; +import com.conveyal.gtfs.model.FareTransferRule; import com.conveyal.gtfs.model.Frequency; +import com.conveyal.gtfs.model.Network; import com.conveyal.gtfs.model.Pattern; import com.conveyal.gtfs.model.PatternStop; import com.conveyal.gtfs.model.Route; +import com.conveyal.gtfs.model.RouteNetwork; import com.conveyal.gtfs.model.ScheduleException; import com.conveyal.gtfs.model.ShapePoint; import com.conveyal.gtfs.model.Stop; +import com.conveyal.gtfs.model.StopArea; import com.conveyal.gtfs.model.StopTime; +import com.conveyal.gtfs.model.TimeFrame; import com.conveyal.gtfs.model.Trip; import gnu.trove.map.TObjectIntMap; import org.slf4j.Logger; @@ -230,6 +239,85 @@ public interface EntityPopulator { return stopTime; }; + EntityPopulator AREA = (result, columnForName) -> { + Area area = new Area(); + area.area_id = getStringIfPresent(result, Area.AREA_ID_COLUMN_NAME, columnForName); + area.area_name = getStringIfPresent(result, Area.AREA_NAME_COLUMN_NAME, columnForName); + return area; + }; + + EntityPopulator STOP_AREA = (result, columnForName) -> { + StopArea stopArea = new StopArea(); + stopArea.area_id = getStringIfPresent(result, StopArea.AREA_ID_COLUMN_NAME, columnForName); + stopArea.stop_id = getStringIfPresent(result, StopArea.STOP_ID_COLUMN_NAME, columnForName); + return stopArea; + }; + + EntityPopulator FARE_MEDIA = (result, columnForName) -> { + FareMedia fareMedia = new FareMedia(); + fareMedia.fare_media_id = getStringIfPresent(result, FareMedia.FARE_MEDIA_ID_COLUMN_NAME, columnForName); + fareMedia.fare_media_name = getStringIfPresent(result, FareMedia.FARE_MEDIA_NAME_COLUMN_NAME, columnForName); + fareMedia.fare_media_type = getIntIfPresent(result, FareMedia.FARE_MEDIA_TYPE_COLUMN_NAME, columnForName); + return fareMedia; + }; + + EntityPopulator FARE_PRODUCT = (result, columnForName) -> { + FareProduct fareProduct = new FareProduct(); + fareProduct.fare_product_id = getStringIfPresent(result, FareProduct.FARE_PRODUCT_ID_COLUMN_NAME, columnForName); + fareProduct.fare_product_name = getStringIfPresent(result, FareProduct.FARE_PRODUCT_NAME_COLUMN_NAME, columnForName); + fareProduct.fare_media_id = getStringIfPresent(result, FareProduct.FARE_MEDIA_ID_COLUMN_NAME, columnForName); + fareProduct.amount = getDoubleIfPresent(result, FareProduct.AMOUNT_COLUMN_NAME, columnForName); + fareProduct.currency = getStringIfPresent(result, FareProduct.CURRENCY_COLUMN_NAME, columnForName); + return fareProduct; + }; + + EntityPopulator TIME_FRAME = (result, columnForName) -> { + TimeFrame timeFrame = new TimeFrame(); + timeFrame.timeframe_group_id = getStringIfPresent(result, TimeFrame.TIME_FRAME_GROUP_ID_COLUMN_NAME, columnForName); + timeFrame.start_time = getIntIfPresent(result, TimeFrame.START_TIME_COLUMN_NAME, columnForName); + timeFrame.end_time = getIntIfPresent(result, TimeFrame.END_TIME_COLUMN_NAME, columnForName); + timeFrame.service_id = getStringIfPresent(result, FareProduct.FARE_MEDIA_ID_COLUMN_NAME, columnForName); + return timeFrame; + }; + + EntityPopulator FARE_LEG_RULE = (result, columnForName) -> { + FareLegRule fareLegRule = new FareLegRule(); + fareLegRule.leg_group_id = getStringIfPresent(result, FareLegRule.LEG_GROUP_ID_COLUMN_NAME, columnForName); + fareLegRule.from_timeframe_group_id = getStringIfPresent(result, FareLegRule.FROM_AREA_ID_COLUMN_NAME, columnForName); + fareLegRule.to_timeframe_group_id = getStringIfPresent(result, FareLegRule.TO_AREA_ID_COLUMN_NAME, columnForName); + fareLegRule.from_timeframe_group_id = getStringIfPresent(result, FareLegRule.FROM_TIMEFRAME_GROUP_ID_COLUMN_NAME, columnForName); + fareLegRule.to_timeframe_group_id = getStringIfPresent(result, FareLegRule.TO_TIMEFRAME_GROUP_ID_COLUMN_NAME, columnForName); + fareLegRule.fare_product_id = getStringIfPresent(result, FareLegRule.FARE_PRODUCT_ID_COLUMN_NAME, columnForName); + fareLegRule.rule_priority = getIntIfPresent(result, FareLegRule.RULE_PRIORITY_COLUMN_NAME, columnForName); + return fareLegRule; + }; + + EntityPopulator FARE_TRANSFER_RULE = (result, columnForName) -> { + FareTransferRule fareTransferRule = new FareTransferRule(); + fareTransferRule.from_leg_group_id = getStringIfPresent(result, FareTransferRule.FROM_LEG_GROUP_ID_COLUMN_NAME, columnForName); + fareTransferRule.to_leg_group_id = getStringIfPresent(result, FareTransferRule.TO_LEG_GROUP_ID_COLUMN_NAME, columnForName); + fareTransferRule.transfer_count = getIntIfPresent(result, FareTransferRule.TRANSFER_COUNT_COLUMN_NAME, columnForName); + fareTransferRule.duration_limit = getIntIfPresent(result, FareTransferRule.DURATION_LIMIT_COLUMN_NAME, columnForName); + fareTransferRule.duration_limit_type = getIntIfPresent(result, FareTransferRule.DURATION_LIMIT_TYPE_COLUMN_NAME, columnForName); + fareTransferRule.fare_transfer_type = getIntIfPresent(result, FareTransferRule.FARE_TRANSFER_TYPE_COLUMN_NAME, columnForName); + fareTransferRule.fare_product_id = getStringIfPresent(result, FareTransferRule.FARE_PRODUCT_ID_COLUMN_NAME, columnForName); + return fareTransferRule; + }; + + EntityPopulator NETWORK = (result, columnForName) -> { + Network network = new Network(); + network.network_id = getStringIfPresent(result, Network.NETWORK_ID_COLUMN_NAME, columnForName); + network.network_name = getStringIfPresent(result, Network.NETWORK_NAME_COLUMN_NAME, columnForName); + return network; + }; + + EntityPopulator ROUTE_NETWORK = (result, columnForName) -> { + RouteNetwork routeNetwork = new RouteNetwork(); + routeNetwork.network_id = getStringIfPresent(result, RouteNetwork.NETWORK_ID_COLUMN_NAME, columnForName); + routeNetwork.route_id = getStringIfPresent(result, RouteNetwork.ROUTE_ID_COLUMN_NAME, columnForName); + return routeNetwork; + }; + // The reason we're passing in the columnForName map is that resultSet.getX(columnName) throws an exception // when the column is not present. // Exceptions should only be used in exceptional circumstances (ones that should be logged as errors). diff --git a/src/main/java/com/conveyal/gtfs/loader/Feed.java b/src/main/java/com/conveyal/gtfs/loader/Feed.java index 1430b5751..0711429e8 100644 --- a/src/main/java/com/conveyal/gtfs/loader/Feed.java +++ b/src/main/java/com/conveyal/gtfs/loader/Feed.java @@ -40,6 +40,15 @@ public class Feed { public final TableReader trips; public final TableReader stopTimes; public final TableReader patterns; + public final TableReader areas; + public final TableReader stopAreas; + public final TableReader fareMedia; + public final TableReader fareProducts; + public final TableReader timeFrames; + public final TableReader fareLegRules; + public final TableReader fareTransferRules; + public final TableReader networks; + public final TableReader routeNetworks; /** * Create a feed that reads tables over a JDBC connection. The connection should already be set to the right @@ -61,6 +70,15 @@ public Feed (DataSource dataSource, String databaseSchemaPrefix) { trips = new JDBCTableReader(Table.TRIPS, dataSource, databaseSchemaPrefix, EntityPopulator.TRIP); stopTimes = new JDBCTableReader(Table.STOP_TIMES, dataSource, databaseSchemaPrefix, EntityPopulator.STOP_TIME); patterns = new JDBCTableReader(Table.PATTERNS, dataSource, databaseSchemaPrefix, EntityPopulator.PATTERN); + areas = new JDBCTableReader(Table.AREAS, dataSource, databaseSchemaPrefix, EntityPopulator.AREA); + stopAreas = new JDBCTableReader(Table.STOP_AREAS, dataSource, databaseSchemaPrefix, EntityPopulator.STOP_AREA); + fareMedia = new JDBCTableReader(Table.FARE_MEDIAS, dataSource, databaseSchemaPrefix, EntityPopulator.FARE_MEDIA); + fareProducts = new JDBCTableReader(Table.FARE_PRODUCTS, dataSource, databaseSchemaPrefix, EntityPopulator.FARE_PRODUCT); + timeFrames = new JDBCTableReader(Table.TIME_FRAMES, dataSource, databaseSchemaPrefix, EntityPopulator.TIME_FRAME); + fareLegRules = new JDBCTableReader(Table.FARE_LEG_RULES, dataSource, databaseSchemaPrefix, EntityPopulator.FARE_LEG_RULE); + fareTransferRules = new JDBCTableReader(Table.FARE_TRANSFER_RULES, dataSource, databaseSchemaPrefix, EntityPopulator.FARE_TRANSFER_RULE); + networks = new JDBCTableReader(Table.NETWORKS, dataSource, databaseSchemaPrefix, EntityPopulator.NETWORK); + routeNetworks = new JDBCTableReader(Table.ROUTE_NETWORKS, dataSource, databaseSchemaPrefix, EntityPopulator.ROUTE_NETWORK); } /** diff --git a/src/main/java/com/conveyal/gtfs/loader/FeedLoadResult.java b/src/main/java/com/conveyal/gtfs/loader/FeedLoadResult.java index 301ed0d6e..4da375f53 100644 --- a/src/main/java/com/conveyal/gtfs/loader/FeedLoadResult.java +++ b/src/main/java/com/conveyal/gtfs/loader/FeedLoadResult.java @@ -38,6 +38,17 @@ public class FeedLoadResult implements Serializable { public TableLoadResult translations; public TableLoadResult attributions; + // Fares v2. + public TableLoadResult areas; + public TableLoadResult stopAreas; + public TableLoadResult fareMedias; + public TableLoadResult fareProducts; + public TableLoadResult timeFrames; + public TableLoadResult fareLegRules; + public TableLoadResult fareTransferRules; + public TableLoadResult networks; + public TableLoadResult routeNetworks; + public long loadTimeMillis; public long completionTime; @@ -64,5 +75,16 @@ public FeedLoadResult (boolean constructTableResults) { trips = new TableLoadResult(); translations = new TableLoadResult(); attributions = new TableLoadResult(); + + // Fares v2. + areas = new TableLoadResult(); + stopAreas = new TableLoadResult(); + fareMedias = new TableLoadResult(); + fareProducts = new TableLoadResult(); + timeFrames = new TableLoadResult(); + fareLegRules = new TableLoadResult(); + fareTransferRules = new TableLoadResult(); + networks = new TableLoadResult(); + routeNetworks = new TableLoadResult(); } } diff --git a/src/main/java/com/conveyal/gtfs/loader/JdbcGTFSFeedConverter.java b/src/main/java/com/conveyal/gtfs/loader/JdbcGTFSFeedConverter.java index 01990b24a..0734dae37 100644 --- a/src/main/java/com/conveyal/gtfs/loader/JdbcGTFSFeedConverter.java +++ b/src/main/java/com/conveyal/gtfs/loader/JdbcGTFSFeedConverter.java @@ -122,6 +122,18 @@ public FeedLoadResult loadTables () { copyEntityToSql(gtfsFeed.trips.values(), Table.TRIPS); // refs routes copyEntityToSql(frequencies, Table.FREQUENCIES); // refs trips copyEntityToSql(gtfsFeed.stop_times.values(), Table.STOP_TIMES); + + // Fares v2. + copyEntityToSql(gtfsFeed.areas.values(), Table.AREAS); + copyEntityToSql(gtfsFeed.stop_areas.values(), Table.STOP_AREAS); + copyEntityToSql(gtfsFeed.fare_medias.values(), Table.FARE_MEDIAS); + copyEntityToSql(gtfsFeed.fare_products.values(), Table.FARE_PRODUCTS); + copyEntityToSql(gtfsFeed.time_frames.values(), Table.TIME_FRAMES); + copyEntityToSql(gtfsFeed.fare_leg_rules.values(), Table.FARE_LEG_RULES); + copyEntityToSql(gtfsFeed.fare_transfer_rules.values(), Table.FARE_TRANSFER_RULES); + copyEntityToSql(gtfsFeed.networks.values(), Table.NETWORKS); + copyEntityToSql(gtfsFeed.route_networks.values(), Table.ROUTE_NETWORKS); + // result.errorCount = errorStorage.getErrorCount(); // This will commit and close the single connection that has been shared between all preceding load steps. errorStorage.commitAndClose(); diff --git a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java index 78e6591d7..86172a72f 100644 --- a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java +++ b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java @@ -14,7 +14,6 @@ import javax.sql.DataSource; import java.io.File; -import java.io.FileOutputStream; import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; @@ -29,10 +28,8 @@ import java.time.LocalDate; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -347,6 +344,16 @@ public FeedLoadResult exportTables() { result.trips = export(Table.TRIPS, connection); } + result.areas = export(Table.AREAS, connection); + result.stopAreas = export(Table.STOP_AREAS, connection); + result.fareMedias = export(Table.FARE_MEDIAS, connection); + result.fareProducts = export(Table.FARE_PRODUCTS, connection); + result.timeFrames = export(Table.TIME_FRAMES, connection); + result.fareLegRules = export(Table.FARE_LEG_RULES, connection); + result.fareTransferRules = export(Table.FARE_TRANSFER_RULES, connection); + result.networks = export(Table.NETWORKS, connection); + result.routeNetworks = export(Table.ROUTE_NETWORKS, connection); + exportProprietaryFiles(result); zipOutputStream.close(); diff --git a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsLoader.java b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsLoader.java index 25ffc0340..299af9484 100644 --- a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsLoader.java +++ b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsLoader.java @@ -156,12 +156,21 @@ public FeedLoadResult loadTables() { result.agency = load(Table.AGENCY); result.calendar = load(Table.CALENDAR); result.calendarDates = load(Table.CALENDAR_DATES); + result.timeFrames = load(Table.TIME_FRAMES); result.routes = load(Table.ROUTES); result.fareAttributes = load(Table.FARE_ATTRIBUTES); + result.fareMedias = load(Table.FARE_MEDIAS); + result.fareProducts = load(Table.FARE_PRODUCTS); + result.networks = load(Table.NETWORKS); + result.routeNetworks = load(Table.ROUTE_NETWORKS); // refs networks. + result.areas = load(Table.AREAS); + result.fareLegRules = load(Table.FARE_LEG_RULES); // ref areas + result.fareTransferRules = load(Table.FARE_TRANSFER_RULES); result.feedInfo = load(Table.FEED_INFO); result.shapes = load(Table.SHAPES); result.patterns = load(Table.PATTERNS); // refs shapes and routes. result.stops = load(Table.STOPS); + result.stopAreas = load(Table.STOP_AREAS); result.fareRules = load(Table.FARE_RULES); result.trips = load(Table.TRIPS); // refs routes result.transfers = load(Table.TRANSFERS); // refs trips. @@ -169,6 +178,7 @@ public FeedLoadResult loadTables() { result.stopTimes = load(Table.STOP_TIMES); result.translations = load(Table.TRANSLATIONS); result.attributions = load(Table.ATTRIBUTIONS); + result.errorCount = errorStorage.getErrorCount(); // This will commit and close the single connection that has been shared between all preceding load steps. errorStorage.commitAndClose(); diff --git a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsSnapshotter.java b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsSnapshotter.java index 67a9c9ebc..0f3bb12a0 100644 --- a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsSnapshotter.java +++ b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsSnapshotter.java @@ -115,6 +115,17 @@ public SnapshotResult copyTables() { result.trips = copy(Table.TRIPS, true); result.attributions = copy(Table.ATTRIBUTIONS, true); result.translations = copy(Table.TRANSLATIONS, true); + // Fares v2. + result.areas = copy(Table.AREAS, true); + result.stopAreas = copy(Table.STOP_AREAS, true); + result.fareMedias = copy(Table.FARE_MEDIAS, true); + result.fareProducts = copy(Table.FARE_PRODUCTS, true); + result.timeFrames = copy(Table.TIME_FRAMES, true); + result.fareLegRules = copy(Table.FARE_LEG_RULES, true); + result.fareTransferRules = copy(Table.FARE_TRANSFER_RULES, true); + result.networks = copy(Table.NETWORKS, true); + result.routeNetworks = copy(Table.ROUTE_NETWORKS, true); + result.completionTime = System.currentTimeMillis(); result.loadTimeMillis = result.completionTime - startTime; LOG.info("Copying tables took {} sec", (result.loadTimeMillis) / 1000); diff --git a/src/main/java/com/conveyal/gtfs/loader/Table.java b/src/main/java/com/conveyal/gtfs/loader/Table.java index 1ac7a0030..c131ef5c6 100644 --- a/src/main/java/com/conveyal/gtfs/loader/Table.java +++ b/src/main/java/com/conveyal/gtfs/loader/Table.java @@ -10,21 +10,30 @@ import com.conveyal.gtfs.loader.conditions.ForeignRefExistsCheck; import com.conveyal.gtfs.loader.conditions.ReferenceFieldShouldBeProvidedCheck; import com.conveyal.gtfs.model.Agency; +import com.conveyal.gtfs.model.Area; import com.conveyal.gtfs.model.Attribution; import com.conveyal.gtfs.model.Calendar; import com.conveyal.gtfs.model.CalendarDate; import com.conveyal.gtfs.model.Entity; import com.conveyal.gtfs.model.FareAttribute; +import com.conveyal.gtfs.model.FareLegRule; +import com.conveyal.gtfs.model.FareMedia; +import com.conveyal.gtfs.model.FareProduct; import com.conveyal.gtfs.model.FareRule; +import com.conveyal.gtfs.model.FareTransferRule; import com.conveyal.gtfs.model.FeedInfo; import com.conveyal.gtfs.model.Frequency; +import com.conveyal.gtfs.model.Network; import com.conveyal.gtfs.model.Pattern; import com.conveyal.gtfs.model.PatternStop; import com.conveyal.gtfs.model.Route; +import com.conveyal.gtfs.model.RouteNetwork; import com.conveyal.gtfs.model.ScheduleException; import com.conveyal.gtfs.model.ShapePoint; import com.conveyal.gtfs.model.Stop; +import com.conveyal.gtfs.model.StopArea; import com.conveyal.gtfs.model.StopTime; +import com.conveyal.gtfs.model.TimeFrame; import com.conveyal.gtfs.model.Transfer; import com.conveyal.gtfs.model.Translation; import com.conveyal.gtfs.model.Trip; @@ -133,6 +142,7 @@ public Table (String name, Class entityClass, Requirement requ .addPrimaryKey() .addPrimaryKeyNames("agency_id"); + // The GTFS spec says this table is required, but in practice it is not required if calendar_dates is present. public static final Table CALENDAR = new Table("calendar", Calendar.class, OPTIONAL, new StringField("service_id", REQUIRED), @@ -213,7 +223,7 @@ public Table (String name, Class entityClass, Requirement requ new StringField("route_short_name", OPTIONAL), // one of short or long must be provided new StringField("route_long_name", OPTIONAL), new StringField("route_desc", OPTIONAL), - // Max route type according to the GTFS spec is 7; however, there is a GTFS proposal that could see this + // Max route type according to the GTFS spec is 7; however, there is a GTFS proposal that could see this // max value grow to around 1800: https://groups.google.com/forum/#!msg/gtfs-changes/keT5rTPS7Y0/71uMz2l6ke0J new IntegerField("route_type", REQUIRED, 1800), new URLField("route_url", OPTIONAL), @@ -228,7 +238,8 @@ public Table (String name, Class entityClass, Requirement requ // Status values are In progress (0), Pending approval (1), and Approved (2). new ShortField("status", EDITOR, 2), new ShortField("continuous_pickup", OPTIONAL,3), - new ShortField("continuous_drop_off", OPTIONAL,3) + new ShortField("continuous_drop_off", OPTIONAL,3), + new StringField("network_id", OPTIONAL) ).addPrimaryKey() .addPrimaryKeyNames("route_id"); @@ -287,6 +298,109 @@ public Table (String name, Class entityClass, Requirement requ .addPrimaryKey() .addPrimaryKeyNames("stop_id"); + public static final Table AREAS = new Table(Area.TABLE_NAME, Area.class, OPTIONAL, + new StringField(Area.AREA_ID_COLUMN_NAME, REQUIRED), + new StringField(Area.AREA_NAME_COLUMN_NAME, OPTIONAL) + ) + .restrictDelete() + .addPrimaryKeyNames(Area.AREA_ID_COLUMN_NAME); + + public static final Table STOP_AREAS = new Table(StopArea.TABLE_NAME, StopArea.class, OPTIONAL, + new StringField(StopArea.AREA_ID_COLUMN_NAME, REQUIRED).isReferenceTo(AREAS), + new StringField(StopArea.STOP_ID_COLUMN_NAME, REQUIRED).isReferenceTo(STOPS) + ) + .keyFieldIsNotUnique() + .addPrimaryKeyNames(StopArea.AREA_ID_COLUMN_NAME, StopArea.STOP_ID_COLUMN_NAME); + + public static final Table FARE_MEDIAS = new Table(FareMedia.TABLE_NAME, FareMedia.class, OPTIONAL, + new StringField(FareMedia.FARE_MEDIA_ID_COLUMN_NAME, REQUIRED), + new StringField(FareMedia.FARE_MEDIA_NAME_COLUMN_NAME, OPTIONAL), + new IntegerField(FareMedia.FARE_MEDIA_TYPE_COLUMN_NAME, REQUIRED) + ) + .restrictDelete() + .addPrimaryKeyNames(FareMedia.FARE_MEDIA_ID_COLUMN_NAME); + + public static final Table FARE_PRODUCTS = new Table(FareProduct.TABLE_NAME, FareProduct.class, OPTIONAL, + new StringField(FareProduct.FARE_PRODUCT_ID_COLUMN_NAME, REQUIRED), + new StringField(FareProduct.FARE_PRODUCT_NAME_COLUMN_NAME, OPTIONAL), + new StringField(FareProduct.FARE_MEDIA_ID_COLUMN_NAME, OPTIONAL).isReferenceTo(FARE_MEDIAS), + new DoubleField(FareProduct.AMOUNT_COLUMN_NAME, REQUIRED, 0.0, Double.MAX_VALUE, 2), + new StringField(FareProduct.CURRENCY_COLUMN_NAME, REQUIRED) + ) + .restrictDelete() + .keyFieldIsNotUnique() + .addPrimaryKeyNames(FareProduct.FARE_PRODUCT_ID_COLUMN_NAME, FareProduct.FARE_MEDIA_ID_COLUMN_NAME); + + public static final Table TIME_FRAMES = new Table(TimeFrame.TABLE_NAME, TimeFrame.class, OPTIONAL, + new StringField(TimeFrame.TIME_FRAME_GROUP_ID_COLUMN_NAME, REQUIRED), + new TimeField(TimeFrame.START_TIME_COLUMN_NAME, OPTIONAL), + new TimeField(TimeFrame.END_TIME_COLUMN_NAME, OPTIONAL), + new StringField(TimeFrame.SERVICE_ID_COLUMN_NAME, OPTIONAL).isReferenceTo(CALENDAR).isReferenceTo(CALENDAR_DATES) + ) + .restrictDelete() + .keyFieldIsNotUnique() + .addPrimaryKeyNames( + TimeFrame.TIME_FRAME_GROUP_ID_COLUMN_NAME, + TimeFrame.START_TIME_COLUMN_NAME, + TimeFrame.END_TIME_COLUMN_NAME, + TimeFrame.SERVICE_ID_COLUMN_NAME + ); + + public static final Table FARE_LEG_RULES = new Table(FareLegRule.TABLE_NAME, FareLegRule.class, OPTIONAL, + new StringField(FareLegRule.LEG_GROUP_ID_COLUMN_NAME, OPTIONAL), + new StringField(FareLegRule.NETWORK_ID_COLUMN_NAME, OPTIONAL), + new StringField(FareLegRule.FROM_AREA_ID_COLUMN_NAME, OPTIONAL).isReferenceTo(AREAS), + new StringField(FareLegRule.TO_AREA_ID_COLUMN_NAME, OPTIONAL).isReferenceTo(AREAS), + new StringField(FareLegRule.FROM_TIMEFRAME_GROUP_ID_COLUMN_NAME, OPTIONAL).isReferenceTo(TIME_FRAMES), + new StringField(FareLegRule.TO_TIMEFRAME_GROUP_ID_COLUMN_NAME, OPTIONAL).isReferenceTo(TIME_FRAMES), + new StringField(FareLegRule.FARE_PRODUCT_ID_COLUMN_NAME, REQUIRED).isReferenceTo(FARE_PRODUCTS), + new IntegerField(FareLegRule.RULE_PRIORITY_COLUMN_NAME, OPTIONAL) + ) + .restrictDelete() + .keyFieldIsNotUnique() + .addPrimaryKeyNames( + FareLegRule.NETWORK_ID_COLUMN_NAME, + FareLegRule.FROM_AREA_ID_COLUMN_NAME, + FareLegRule.TO_AREA_ID_COLUMN_NAME, + FareLegRule.FROM_TIMEFRAME_GROUP_ID_COLUMN_NAME, + FareLegRule.TO_TIMEFRAME_GROUP_ID_COLUMN_NAME, + FareLegRule.FARE_PRODUCT_ID_COLUMN_NAME + ); + + public static final Table FARE_TRANSFER_RULES = new Table(FareTransferRule.TABLE_NAME, FareTransferRule.class, OPTIONAL, + new StringField(FareTransferRule.FROM_LEG_GROUP_ID_COLUMN_NAME, OPTIONAL).isReferenceTo(FARE_LEG_RULES), + new StringField(FareTransferRule.TO_LEG_GROUP_ID_COLUMN_NAME, OPTIONAL).isReferenceTo(FARE_LEG_RULES), + new IntegerField(FareTransferRule.TRANSFER_COUNT_COLUMN_NAME, OPTIONAL, -1, Integer.MAX_VALUE), + new IntegerField(FareTransferRule.DURATION_LIMIT_COLUMN_NAME, OPTIONAL), + new IntegerField(FareTransferRule.DURATION_LIMIT_TYPE_COLUMN_NAME, OPTIONAL), + new IntegerField(FareTransferRule.FARE_TRANSFER_TYPE_COLUMN_NAME, REQUIRED), + new StringField(FareTransferRule.FARE_PRODUCT_ID_COLUMN_NAME, OPTIONAL).isReferenceTo(FARE_PRODUCTS) + ) + .restrictDelete() + .keyFieldIsNotUnique() + .addPrimaryKeyNames( + FareTransferRule.FROM_LEG_GROUP_ID_COLUMN_NAME, + FareTransferRule.TO_LEG_GROUP_ID_COLUMN_NAME, + FareTransferRule.FARE_PRODUCT_ID_COLUMN_NAME, + FareTransferRule.TRANSFER_COUNT_COLUMN_NAME, + FareTransferRule.DURATION_LIMIT_COLUMN_NAME + ); + + public static final Table NETWORKS = new Table(Network.TABLE_NAME, Network.class, OPTIONAL, + new StringField(Network.NETWORK_ID_COLUMN_NAME, REQUIRED), + new StringField(Network.NETWORK_NAME_COLUMN_NAME, OPTIONAL) + ) + .restrictDelete() + .addPrimaryKeyNames(Network.NETWORK_ID_COLUMN_NAME); + + public static final Table ROUTE_NETWORKS = new Table(RouteNetwork.TABLE_NAME, RouteNetwork.class, OPTIONAL, + new StringField(RouteNetwork.NETWORK_ID_COLUMN_NAME, REQUIRED).isReferenceTo(NETWORKS), + new StringField(RouteNetwork.ROUTE_ID_COLUMN_NAME, REQUIRED).isReferenceTo(ROUTES) + ) + .keyFieldIsNotUnique() + .addPrimaryKeyNames(RouteNetwork.ROUTE_ID_COLUMN_NAME); + + // GTFS reference: https://developers.google.com/transit/gtfs/reference#fare_rulestxt public static final Table FARE_RULES = new Table("fare_rules", FareRule.class, OPTIONAL, new StringField("fare_id", REQUIRED).isReferenceTo(FARE_ATTRIBUTES), @@ -437,12 +551,21 @@ public Table (String name, Class entityClass, Requirement requ CALENDAR, SCHEDULE_EXCEPTIONS, CALENDAR_DATES, + TIME_FRAMES, FARE_ATTRIBUTES, + FARE_MEDIAS, + FARE_PRODUCTS, + NETWORKS, + FARE_LEG_RULES, + FARE_TRANSFER_RULES, FEED_INFO, ROUTES, + ROUTE_NETWORKS, PATTERNS, SHAPES, STOPS, + AREAS, + STOP_AREAS, FARE_RULES, PATTERN_STOP, TRANSFERS, diff --git a/src/main/java/com/conveyal/gtfs/model/Area.java b/src/main/java/com/conveyal/gtfs/model/Area.java new file mode 100644 index 000000000..983864eb1 --- /dev/null +++ b/src/main/java/com/conveyal/gtfs/model/Area.java @@ -0,0 +1,87 @@ +package com.conveyal.gtfs.model; + +import com.conveyal.gtfs.GTFSFeed; + +import java.io.IOException; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Iterator; + +public class Area extends Entity { + + private static final long serialVersionUID = -2825890165823575940L; + public String area_id; + public String area_name; + public String feed_id; + + public static final String TABLE_NAME = "areas"; + public static final String AREA_ID_COLUMN_NAME = "area_id"; + public static final String AREA_NAME_COLUMN_NAME = "area_name"; + + + @Override + public String getId () { + return area_id; + } + + /** + * Sets the parameters for a prepared statement following the parameter order defined in + * {@link com.conveyal.gtfs.loader.Table#AREAS}. JDBC prepared statement parameters use a one-based index. + */ + @Override + public void setStatementParameters(PreparedStatement statement, boolean setDefaultId) throws SQLException { + int oneBasedIndex = 1; + if (!setDefaultId) statement.setInt(oneBasedIndex++, id); + statement.setString(oneBasedIndex++, area_id); + statement.setString(oneBasedIndex, area_name); + } + + public static class Loader extends Entity.Loader { + + public Loader(GTFSFeed feed) { + super(feed, TABLE_NAME); + } + + @Override + protected boolean isRequired() { + return false; + } + + @Override + public void loadOneRow() throws IOException { + Area area = new Area(); + area.id = row + 1; // offset line number by 1 to account for 0-based row index + area.area_id = getStringField(AREA_ID_COLUMN_NAME, true); + area.area_name = getStringField(AREA_NAME_COLUMN_NAME, false); + area.feed = feed; + area.feed_id = feed.feedId; + feed.areas.put(area.getId(), area); + } + + } + + public static class Writer extends Entity.Writer { + public Writer(GTFSFeed feed) { + super(feed, TABLE_NAME); + } + + @Override + public void writeHeaders() throws IOException { + writer.writeRecord(new String[] {AREA_ID_COLUMN_NAME, AREA_NAME_COLUMN_NAME}); + } + + @Override + public void writeOneRow(Area area) throws IOException { + writeStringField(area.area_id); + writeStringField(area.area_name); + endRecord(); + } + + @Override + public Iterator iterator() { + return this.feed.areas.values().iterator(); + } + } + +} + diff --git a/src/main/java/com/conveyal/gtfs/model/Entity.java b/src/main/java/com/conveyal/gtfs/model/Entity.java index 29657b6e6..b4ada462c 100644 --- a/src/main/java/com/conveyal/gtfs/model/Entity.java +++ b/src/main/java/com/conveyal/gtfs/model/Entity.java @@ -36,11 +36,13 @@ import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; +import java.util.Arrays; import java.util.Enumeration; import java.util.Iterator; import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; @@ -498,4 +500,15 @@ public static final String human (long n) { if (n >= 1000) return String.format("%.1fk", n/1000.0); else return String.format("%d", n); } + + /** + * Creates a primary key from the provided fields. It is acceptable for a field that makes up the primary key to be + * optional! In this case the null value is represented with "empty". + */ + protected static String createPrimaryKey(Object... fields) { + return Arrays + .stream(fields) + .map(id -> id == null ? "empty" : id.toString()) + .collect(Collectors.joining("_")); + } } diff --git a/src/main/java/com/conveyal/gtfs/model/FareLegRule.java b/src/main/java/com/conveyal/gtfs/model/FareLegRule.java new file mode 100644 index 000000000..4b3d75ae3 --- /dev/null +++ b/src/main/java/com/conveyal/gtfs/model/FareLegRule.java @@ -0,0 +1,138 @@ +package com.conveyal.gtfs.model; + +import com.conveyal.gtfs.GTFSFeed; + +import java.io.IOException; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Iterator; + +public class FareLegRule extends Entity { + + private static final long serialVersionUID = -795847376633855940L; + + public String leg_group_id; + public String network_id; + public String from_area_id; + public String to_area_id; + public String from_timeframe_group_id; + public String to_timeframe_group_id; + public String fare_product_id; + public int rule_priority = INT_MISSING; + + public String feed_id; + + public static final String TABLE_NAME = "fare_leg_rules"; + public static final String LEG_GROUP_ID_COLUMN_NAME = "leg_group_id"; + public static final String NETWORK_ID_COLUMN_NAME = "network_id"; + public static final String FROM_AREA_ID_COLUMN_NAME = "from_area_id"; + public static final String TO_AREA_ID_COLUMN_NAME = "to_area_id"; + public static final String FROM_TIMEFRAME_GROUP_ID_COLUMN_NAME = "from_timeframe_group_id"; + public static final String TO_TIMEFRAME_GROUP_ID_COLUMN_NAME = "to_timeframe_group_id"; + public static final String FARE_PRODUCT_ID_COLUMN_NAME = "fare_product_id"; + public static final String RULE_PRIORITY_COLUMN_NAME = "rule_priority"; + + @Override + public String getId () { + return createPrimaryKey( + network_id, + from_area_id, + to_area_id, + from_timeframe_group_id, + to_timeframe_group_id, + fare_product_id + ); + } + + /** + * Sets the parameters for a prepared statement following the parameter order defined in + * {@link com.conveyal.gtfs.loader.Table#FARE_LEG_RULES}. JDBC prepared statement parameters use a one-based index. + */ + @Override + public void setStatementParameters(PreparedStatement statement, boolean setDefaultId) throws SQLException { + int oneBasedIndex = 1; + if (!setDefaultId) statement.setInt(oneBasedIndex++, id); + statement.setString(oneBasedIndex++, leg_group_id); + statement.setString(oneBasedIndex++, network_id); + statement.setString(oneBasedIndex++, from_area_id); + statement.setString(oneBasedIndex++, to_area_id); + statement.setString(oneBasedIndex++, from_timeframe_group_id); + statement.setString(oneBasedIndex++, to_timeframe_group_id); + statement.setString(oneBasedIndex++, fare_product_id); + setIntParameter(statement, oneBasedIndex, rule_priority); + } + + public static class Loader extends Entity.Loader { + + public Loader(GTFSFeed feed) { + super(feed, TABLE_NAME); + } + + @Override + protected boolean isRequired() { + return false; + } + + @Override + public void loadOneRow() throws IOException { + FareLegRule fareLegRule = new FareLegRule(); + fareLegRule.id = row + 1; // offset line number by 1 to account for 0-based row index + fareLegRule.leg_group_id = getStringField(LEG_GROUP_ID_COLUMN_NAME, false); + fareLegRule.network_id = getStringField(NETWORK_ID_COLUMN_NAME, false); + fareLegRule.from_area_id = getStringField(FROM_AREA_ID_COLUMN_NAME, false); + fareLegRule.to_area_id = getStringField(TO_AREA_ID_COLUMN_NAME, false); + fareLegRule.from_timeframe_group_id = getStringField(FROM_TIMEFRAME_GROUP_ID_COLUMN_NAME, false); + fareLegRule.to_timeframe_group_id = getStringField(TO_TIMEFRAME_GROUP_ID_COLUMN_NAME, false); + fareLegRule.fare_product_id = getStringField(FARE_PRODUCT_ID_COLUMN_NAME, true); + fareLegRule.rule_priority = getIntField(RULE_PRIORITY_COLUMN_NAME, false, 0, Integer.MAX_VALUE); + if (fareLegRule.rule_priority == INT_MISSING) { + // An empty value for rule_priority is treated as zero. + fareLegRule.rule_priority = 0; + } + fareLegRule.feed = feed; + fareLegRule.feed_id = feed.feedId; + feed.fare_leg_rules.put(fareLegRule.getId(), fareLegRule); + } + + } + + public static class Writer extends Entity.Writer { + public Writer(GTFSFeed feed) { + super(feed, TABLE_NAME); + } + + @Override + public void writeHeaders() throws IOException { + writer.writeRecord(new String[] { + LEG_GROUP_ID_COLUMN_NAME, + NETWORK_ID_COLUMN_NAME, + FROM_AREA_ID_COLUMN_NAME, + TO_AREA_ID_COLUMN_NAME, + FROM_TIMEFRAME_GROUP_ID_COLUMN_NAME, + TO_TIMEFRAME_GROUP_ID_COLUMN_NAME, + FARE_PRODUCT_ID_COLUMN_NAME, + RULE_PRIORITY_COLUMN_NAME + }); + } + + @Override + public void writeOneRow(FareLegRule fareLegRule) throws IOException { + writeStringField(fareLegRule.leg_group_id); + writeStringField(fareLegRule.network_id); + writeStringField(fareLegRule.from_area_id); + writeStringField(fareLegRule.to_area_id); + writeStringField(fareLegRule.from_timeframe_group_id); + writeStringField(fareLegRule.to_timeframe_group_id); + writeStringField(fareLegRule.fare_product_id); + writeIntField(fareLegRule.rule_priority); + endRecord(); + } + + @Override + public Iterator iterator() { + return this.feed.fare_leg_rules.values().iterator(); + } + } +} + + diff --git a/src/main/java/com/conveyal/gtfs/model/FareMedia.java b/src/main/java/com/conveyal/gtfs/model/FareMedia.java new file mode 100644 index 000000000..d484a4ce3 --- /dev/null +++ b/src/main/java/com/conveyal/gtfs/model/FareMedia.java @@ -0,0 +1,97 @@ +package com.conveyal.gtfs.model; + +import com.conveyal.gtfs.GTFSFeed; + +import java.io.IOException; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Iterator; + +public class FareMedia extends Entity { + + private static final long serialVersionUID = -4968771968571945940L; + + public String fare_media_id; + public String fare_media_name; + public int fare_media_type; + public String feed_id; + + public static final String TABLE_NAME = "fare_media"; + public static final String FARE_MEDIA_ID_COLUMN_NAME = "fare_media_id"; + public static final String FARE_MEDIA_NAME_COLUMN_NAME = "fare_media_name"; + public static final String FARE_MEDIA_TYPE_COLUMN_NAME = "fare_media_type"; + + @Override + public String getId () { + return fare_media_id; + } + + /** + * Sets the parameters for a prepared statement following the parameter order defined in + * {@link com.conveyal.gtfs.loader.Table#FARE_MEDIAS}. JDBC prepared statement parameters use a one-based index. + */ + @Override + public void setStatementParameters(PreparedStatement statement, boolean setDefaultId) throws SQLException { + int oneBasedIndex = 1; + if (!setDefaultId) statement.setInt(oneBasedIndex++, id); + statement.setString(oneBasedIndex++, fare_media_id); + statement.setString(oneBasedIndex++, fare_media_name); + setIntParameter(statement, oneBasedIndex, fare_media_type); + } + + public static class Loader extends Entity.Loader { + + public Loader(GTFSFeed feed) { + super(feed, TABLE_NAME); + } + + @Override + protected boolean isRequired() { + return false; + } + + @Override + public void loadOneRow() throws IOException { + FareMedia fareMedia = new FareMedia(); + fareMedia.id = row + 1; // offset line number by 1 to account for 0-based row index + fareMedia.fare_media_id = getStringField(FARE_MEDIA_ID_COLUMN_NAME, true); + fareMedia.fare_media_name = getStringField(FARE_MEDIA_NAME_COLUMN_NAME, false); + fareMedia.fare_media_type = getIntField(FARE_MEDIA_TYPE_COLUMN_NAME, true, 0, 4); + fareMedia.feed = feed; + fareMedia.feed_id = feed.feedId; + feed.fare_medias.put(fareMedia.getId(), fareMedia); + } + + } + + public static class Writer extends Entity.Writer { + public Writer(GTFSFeed feed) { + super(feed, TABLE_NAME); + } + + @Override + public void writeHeaders() throws IOException { + writer.writeRecord(new String[] { + FARE_MEDIA_ID_COLUMN_NAME, + FARE_MEDIA_NAME_COLUMN_NAME, + FARE_MEDIA_TYPE_COLUMN_NAME + }); + } + + @Override + public void writeOneRow(FareMedia fareMedia) throws IOException { + writeStringField(fareMedia.fare_media_id); + writeStringField(fareMedia.fare_media_name); + writeIntField(fareMedia.fare_media_type); + endRecord(); + } + + @Override + public Iterator iterator() { + return this.feed.fare_medias.values().iterator(); + } + } +} + + + diff --git a/src/main/java/com/conveyal/gtfs/model/FareProduct.java b/src/main/java/com/conveyal/gtfs/model/FareProduct.java new file mode 100644 index 000000000..e7f3599d5 --- /dev/null +++ b/src/main/java/com/conveyal/gtfs/model/FareProduct.java @@ -0,0 +1,115 @@ +package com.conveyal.gtfs.model; + +import com.conveyal.gtfs.GTFSFeed; + +import java.io.IOException; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.Iterator; +import java.util.stream.Collectors; + +public class FareProduct extends Entity { + + private static final long serialVersionUID = -5678890165823575940L; + + public String fare_product_id; + public String fare_product_name; + public String fare_media_id; + public double amount; + public String currency; + public String feed_id; + + public static final String TABLE_NAME = "fare_products"; + public static final String FARE_PRODUCT_ID_COLUMN_NAME = "fare_product_id"; + public static final String FARE_PRODUCT_NAME_COLUMN_NAME = "fare_product_name"; + public static final String FARE_MEDIA_ID_COLUMN_NAME = "fare_media_id"; + public static final String AMOUNT_COLUMN_NAME = "amount"; + public static final String CURRENCY_COLUMN_NAME = "currency"; + + + @Override + public String getId () { + return createPrimaryKey(fare_product_id, fare_media_id); + } + + /** + * Sets the parameters for a prepared statement following the parameter order defined in + * {@link com.conveyal.gtfs.loader.Table#FARE_PRODUCTS}. JDBC prepared statement parameters use a one-based index. + */ + @Override + public void setStatementParameters(PreparedStatement statement, boolean setDefaultId) throws SQLException { + int oneBasedIndex = 1; + if (!setDefaultId) statement.setInt(oneBasedIndex++, id); + statement.setString(oneBasedIndex++, fare_product_id); + statement.setString(oneBasedIndex++, fare_product_name); + statement.setString(oneBasedIndex++, fare_media_id); + setDoubleParameter(statement, oneBasedIndex++, amount); + statement.setString(oneBasedIndex, currency); + } + + public static class Loader extends Entity.Loader { + + public Loader(GTFSFeed feed) { + super(feed, TABLE_NAME); + } + + @Override + protected boolean isRequired() { + return false; + } + + @Override + public void loadOneRow() throws IOException { + FareProduct fareProduct = new FareProduct(); + fareProduct.id = row + 1; // offset line number by 1 to account for 0-based row index + fareProduct.fare_product_id = getStringField(FARE_PRODUCT_ID_COLUMN_NAME, true); + fareProduct.fare_product_name = getStringField(FARE_PRODUCT_NAME_COLUMN_NAME, false); + fareProduct.fare_media_id = getStringField(FARE_MEDIA_ID_COLUMN_NAME, false); + fareProduct.amount = getDoubleField(AMOUNT_COLUMN_NAME, true, 0.0, Double.MAX_VALUE); + fareProduct.currency = getStringField(CURRENCY_COLUMN_NAME, true); + fareProduct.feed = feed; + fareProduct.feed_id = feed.feedId; + feed.fare_products.put(fareProduct.getId(), fareProduct); + + /* + Check referential integrity without storing references. + */ + getRefField(FARE_MEDIA_ID_COLUMN_NAME, false, feed.fare_medias); + } + + } + + public static class Writer extends Entity.Writer { + public Writer(GTFSFeed feed) { + super(feed, TABLE_NAME); + } + + @Override + public void writeHeaders() throws IOException { + writer.writeRecord(new String[] { + FARE_PRODUCT_ID_COLUMN_NAME, + FARE_PRODUCT_NAME_COLUMN_NAME, + FARE_MEDIA_ID_COLUMN_NAME, + AMOUNT_COLUMN_NAME,CURRENCY_COLUMN_NAME + }); + } + + @Override + public void writeOneRow(FareProduct fareProduct) throws IOException { + writeStringField(fareProduct.fare_product_id); + writeStringField(fareProduct.fare_product_name); + writeStringField(fareProduct.fare_media_id); + writeDoubleField(fareProduct.amount); + writeStringField(fareProduct.currency); + endRecord(); + } + + @Override + public Iterator iterator() { + return this.feed.fare_products.values().iterator(); + } + } +} + + diff --git a/src/main/java/com/conveyal/gtfs/model/FareTransferRule.java b/src/main/java/com/conveyal/gtfs/model/FareTransferRule.java new file mode 100644 index 000000000..bbffb21e8 --- /dev/null +++ b/src/main/java/com/conveyal/gtfs/model/FareTransferRule.java @@ -0,0 +1,128 @@ +package com.conveyal.gtfs.model; + +import com.conveyal.gtfs.GTFSFeed; + +import java.io.IOException; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Iterator; + +public class FareTransferRule extends Entity { + + private static final long serialVersionUID = -958672649111736468L; + + public String from_leg_group_id; + public String to_leg_group_id; + public int transfer_count = INT_MISSING; + public int duration_limit; + public int duration_limit_type; + public int fare_transfer_type; + public String fare_product_id; + + public String feed_id; + + public static final String TABLE_NAME = "fare_transfer_rules"; + public static final String FROM_LEG_GROUP_ID_COLUMN_NAME = "from_leg_group_id"; + public static final String TO_LEG_GROUP_ID_COLUMN_NAME = "to_leg_group_id"; + public static final String TRANSFER_COUNT_COLUMN_NAME = "transfer_count"; + public static final String DURATION_LIMIT_COLUMN_NAME = "duration_limit"; + public static final String DURATION_LIMIT_TYPE_COLUMN_NAME = "duration_limit_type"; + public static final String FARE_TRANSFER_TYPE_COLUMN_NAME = "fare_transfer_type"; + public static final String FARE_PRODUCT_ID_COLUMN_NAME = "fare_product_id"; + + @Override + public String getId () { + return createPrimaryKey( + from_leg_group_id, + to_leg_group_id, + fare_product_id, + transfer_count, + duration_limit + ); + } + + /** + * Sets the parameters for a prepared statement following the parameter order defined in + * {@link com.conveyal.gtfs.loader.Table#FARE_TRANSFER_RULES}. JDBC prepared statement parameters use a one-based index. + */ + @Override + public void setStatementParameters(PreparedStatement statement, boolean setDefaultId) throws SQLException { + int oneBasedIndex = 1; + if (!setDefaultId) statement.setInt(oneBasedIndex++, id); + statement.setString(oneBasedIndex++, from_leg_group_id); + statement.setString(oneBasedIndex++, to_leg_group_id); + setIntParameter(statement, oneBasedIndex++, transfer_count); + setIntParameter(statement, oneBasedIndex++, duration_limit); + setIntParameter(statement, oneBasedIndex++, duration_limit_type); + setIntParameter(statement, oneBasedIndex++, fare_transfer_type); + statement.setString(oneBasedIndex, fare_product_id); + } + + public static class Loader extends Entity.Loader { + + public Loader(GTFSFeed feed) { + super(feed, TABLE_NAME); + } + + @Override + protected boolean isRequired() { + return false; + } + + @Override + public void loadOneRow() throws IOException { + FareTransferRule fareTransferRule = new FareTransferRule(); + fareTransferRule.id = row + 1; // offset line number by 1 to account for 0-based row index + fareTransferRule.from_leg_group_id = getStringField(FROM_LEG_GROUP_ID_COLUMN_NAME, false); + fareTransferRule.to_leg_group_id = getStringField(TO_LEG_GROUP_ID_COLUMN_NAME, false); + fareTransferRule.transfer_count = getIntField(TRANSFER_COUNT_COLUMN_NAME, false, -1, Integer.MAX_VALUE, INT_MISSING); + fareTransferRule.duration_limit = getIntField(DURATION_LIMIT_COLUMN_NAME, false, 0, Integer.MAX_VALUE); + fareTransferRule.duration_limit_type = getIntField(DURATION_LIMIT_TYPE_COLUMN_NAME, false, 0, 3); + fareTransferRule.fare_transfer_type = getIntField(FARE_TRANSFER_TYPE_COLUMN_NAME, true, 0, 2); + fareTransferRule.fare_product_id = getStringField(FARE_PRODUCT_ID_COLUMN_NAME, false); + fareTransferRule.feed = feed; + fareTransferRule.feed_id = feed.feedId; + feed.fare_transfer_rules.put(fareTransferRule.getId(), fareTransferRule); + } + + } + + public static class Writer extends Entity.Writer { + public Writer(GTFSFeed feed) { + super(feed, TABLE_NAME); + } + + @Override + public void writeHeaders() throws IOException { + writer.writeRecord(new String[] { + FROM_LEG_GROUP_ID_COLUMN_NAME, + TO_LEG_GROUP_ID_COLUMN_NAME, + TRANSFER_COUNT_COLUMN_NAME, + DURATION_LIMIT_COLUMN_NAME, + DURATION_LIMIT_TYPE_COLUMN_NAME, + FARE_TRANSFER_TYPE_COLUMN_NAME, + FARE_PRODUCT_ID_COLUMN_NAME, + }); + } + + @Override + public void writeOneRow(FareTransferRule fareTransferRule) throws IOException { + writeStringField(fareTransferRule.from_leg_group_id); + writeStringField(fareTransferRule.to_leg_group_id); + writeIntField(fareTransferRule.transfer_count); + writeIntField(fareTransferRule.duration_limit); + writeIntField(fareTransferRule.duration_limit_type); + writeIntField(fareTransferRule.fare_transfer_type); + writeStringField(fareTransferRule.fare_product_id); + endRecord(); + } + + @Override + public Iterator iterator() { + return this.feed.fare_transfer_rules.values().iterator(); + } + } +} + + + diff --git a/src/main/java/com/conveyal/gtfs/model/Network.java b/src/main/java/com/conveyal/gtfs/model/Network.java new file mode 100644 index 000000000..004a0681a --- /dev/null +++ b/src/main/java/com/conveyal/gtfs/model/Network.java @@ -0,0 +1,86 @@ +package com.conveyal.gtfs.model; + +import com.conveyal.gtfs.GTFSFeed; + +import java.io.IOException; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Iterator; + +public class Network extends Entity { + + private static final long serialVersionUID = -4739475958736362940L; + public String network_id; + public String network_name; + public String feed_id; + + public static final String TABLE_NAME = "networks"; + public static final String NETWORK_ID_COLUMN_NAME = "network_id"; + public static final String NETWORK_NAME_COLUMN_NAME = "network_name"; + + @Override + public String getId () { + return network_id; + } + + /** + * Sets the parameters for a prepared statement following the parameter order defined in + * {@link com.conveyal.gtfs.loader.Table#NETWORKS}. JDBC prepared statement parameters use a one-based index. + */ + @Override + public void setStatementParameters(PreparedStatement statement, boolean setDefaultId) throws SQLException { + int oneBasedIndex = 1; + if (!setDefaultId) statement.setInt(oneBasedIndex++, id); + statement.setString(oneBasedIndex++, network_id); + statement.setString(oneBasedIndex, network_name); + } + + public static class Loader extends Entity.Loader { + + public Loader(GTFSFeed feed) { + super(feed, TABLE_NAME); + } + + @Override + protected boolean isRequired() { + return false; + } + + @Override + public void loadOneRow() throws IOException { + Network network = new Network(); + network.id = row + 1; // offset line number by 1 to account for 0-based row index + network.network_id = getStringField(NETWORK_ID_COLUMN_NAME, true); + network.network_name = getStringField(NETWORK_NAME_COLUMN_NAME, false); + network.feed = feed; + network.feed_id = feed.feedId; + feed.networks.put(network.getId(), network); + } + + } + + public static class Writer extends Entity.Writer { + public Writer(GTFSFeed feed) { + super(feed, TABLE_NAME); + } + + @Override + public void writeHeaders() throws IOException { + writer.writeRecord(new String[] {NETWORK_ID_COLUMN_NAME, NETWORK_NAME_COLUMN_NAME}); + } + + @Override + public void writeOneRow(Network network) throws IOException { + writeStringField(network.network_id); + writeStringField(network.network_name); + endRecord(); + } + + @Override + public Iterator iterator() { + return this.feed.networks.values().iterator(); + } + } + +} + diff --git a/src/main/java/com/conveyal/gtfs/model/Route.java b/src/main/java/com/conveyal/gtfs/model/Route.java index 31288cafa..a55ea29fc 100644 --- a/src/main/java/com/conveyal/gtfs/model/Route.java +++ b/src/main/java/com/conveyal/gtfs/model/Route.java @@ -36,6 +36,9 @@ public class Route extends Entity { // implements Entity.Factory public String feed_id; public int continuous_pickup = INT_MISSING; public int continuous_drop_off = INT_MISSING; + public String network_id; + + public static final String TABLE_NAME = "routes"; @Override public String getId () { @@ -68,12 +71,13 @@ public void setStatementParameters(PreparedStatement statement, boolean setDefau setIntParameter(statement, oneBasedIndex++, 0); setIntParameter(statement, oneBasedIndex++, continuous_pickup); setIntParameter(statement, oneBasedIndex++, continuous_drop_off); + statement.setString(oneBasedIndex, network_id); } public static class Loader extends Entity.Loader { public Loader(GTFSFeed feed) { - super(feed, "routes"); + super(feed, TABLE_NAME); } @Override @@ -110,6 +114,7 @@ public void loadOneRow() throws IOException { r.route_branding_url = getUrlField("route_branding_url", false); r.continuous_pickup = getIntField("continuous_pickup", false, 0, 3, INT_MISSING); r.continuous_drop_off = getIntField("continuous_drop_off", false, 0, 3, INT_MISSING); + r.network_id = getStringField("network_id", false); r.feed = feed; r.feed_id = feed.feedId; // Attempting to put a null key or value will cause an NPE in BTreeMap @@ -120,7 +125,7 @@ public void loadOneRow() throws IOException { public static class Writer extends Entity.Writer { public Writer (GTFSFeed feed) { - super(feed, "routes"); + super(feed, TABLE_NAME); } @Override @@ -138,6 +143,7 @@ public void writeHeaders() throws IOException { writeStringField("route_sort_order"); writeStringField("continuous_pickup"); writeStringField("continuous_drop_off"); + writeStringField("network_id"); endRecord(); } @@ -156,6 +162,7 @@ public void writeOneRow(Route r) throws IOException { writeIntField(r.route_sort_order); writeIntField(r.continuous_pickup); writeIntField(r.continuous_drop_off); + writeStringField(r.network_id); endRecord(); } diff --git a/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java b/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java new file mode 100644 index 000000000..c39cd1af5 --- /dev/null +++ b/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java @@ -0,0 +1,88 @@ +package com.conveyal.gtfs.model; + +import com.conveyal.gtfs.GTFSFeed; + +import java.io.IOException; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Iterator; + +public class RouteNetwork extends Entity { + + private static final long serialVersionUID = -4739475958736362940L; + public String network_id; + public String route_id; + public String feed_id; + + public static final String TABLE_NAME = "route_networks"; + public static final String NETWORK_ID_COLUMN_NAME = "network_id"; + public static final String ROUTE_ID_COLUMN_NAME = "route_id"; + + @Override + public String getId () { + return route_id; + } + + /** + * Sets the parameters for a prepared statement following the parameter order defined in + * {@link com.conveyal.gtfs.loader.Table#ROUTE_NETWORKS}. JDBC prepared statement parameters use a one-based index. + */ + @Override + public void setStatementParameters(PreparedStatement statement, boolean setDefaultId) throws SQLException { + int oneBasedIndex = 1; + if (!setDefaultId) statement.setInt(oneBasedIndex++, id); + statement.setString(oneBasedIndex++, network_id); + statement.setString(oneBasedIndex, route_id); + } + + public static class Loader extends Entity.Loader { + + public Loader(GTFSFeed feed) { + super(feed, TABLE_NAME); + } + + @Override + protected boolean isRequired() { + return false; + } + + @Override + public void loadOneRow() throws IOException { + RouteNetwork routeNetwork = new RouteNetwork(); + routeNetwork.id = row + 1; // offset line number by 1 to account for 0-based row index + routeNetwork.network_id = getStringField(NETWORK_ID_COLUMN_NAME, true); + routeNetwork.route_id = getStringField(ROUTE_ID_COLUMN_NAME, true); + routeNetwork.feed = feed; + routeNetwork.feed_id = feed.feedId; + feed.route_networks.put(routeNetwork.getId(), routeNetwork); + getRefField(NETWORK_ID_COLUMN_NAME, true, feed.networks); + getRefField(ROUTE_ID_COLUMN_NAME, true, feed.routes); + } + + } + + public static class Writer extends Entity.Writer { + public Writer(GTFSFeed feed) { + super(feed, TABLE_NAME); + } + + @Override + public void writeHeaders() throws IOException { + writer.writeRecord(new String[] {NETWORK_ID_COLUMN_NAME, ROUTE_ID_COLUMN_NAME}); + } + + @Override + public void writeOneRow(RouteNetwork routeNetwork) throws IOException { + writeStringField(routeNetwork.network_id); + writeStringField(routeNetwork.route_id); + endRecord(); + } + + @Override + public Iterator iterator() { + return this.feed.route_networks.values().iterator(); + } + } + +} + diff --git a/src/main/java/com/conveyal/gtfs/model/StopArea.java b/src/main/java/com/conveyal/gtfs/model/StopArea.java new file mode 100644 index 000000000..8eae495e8 --- /dev/null +++ b/src/main/java/com/conveyal/gtfs/model/StopArea.java @@ -0,0 +1,93 @@ +package com.conveyal.gtfs.model; + +import com.conveyal.gtfs.GTFSFeed; + +import java.io.IOException; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Iterator; + +public class StopArea extends Entity { + + private static final long serialVersionUID = -2825890165823575940L; + public String area_id; + public String stop_id; + public String feed_id; + + public static final String TABLE_NAME = "stop_areas"; + public static final String AREA_ID_COLUMN_NAME = "area_id"; + public static final String STOP_ID_COLUMN_NAME = "stop_id"; + + @Override + public String getId () { + return createPrimaryKey(area_id, stop_id); + } + + /** + * Sets the parameters for a prepared statement following the parameter order defined in + * {@link com.conveyal.gtfs.loader.Table#STOP_AREAS}. JDBC prepared statement parameters use a one-based index. + */ + @Override + public void setStatementParameters(PreparedStatement statement, boolean setDefaultId) throws SQLException { + int oneBasedIndex = 1; + if (!setDefaultId) statement.setInt(oneBasedIndex++, id); + statement.setString(oneBasedIndex++, area_id); + statement.setString(oneBasedIndex, stop_id); + } + + public static class Loader extends Entity.Loader { + + public Loader(GTFSFeed feed) { + super(feed, TABLE_NAME); + } + + @Override + protected boolean isRequired() { + return false; + } + + @Override + public void loadOneRow() throws IOException { + StopArea stopArea = new StopArea(); + stopArea.id = row + 1; // offset line number by 1 to account for 0-based row index + stopArea.area_id = getStringField(AREA_ID_COLUMN_NAME, true); + stopArea.stop_id = getStringField(STOP_ID_COLUMN_NAME, true); + stopArea.feed = feed; + stopArea.feed_id = feed.feedId; + feed.stop_areas.put(stopArea.getId(), stopArea); + + /* + Check referential integrity without storing references. + */ + getRefField(AREA_ID_COLUMN_NAME, true, feed.areas); + getRefField(STOP_ID_COLUMN_NAME, true, feed.stops); + + } + + } + + public static class Writer extends Entity.Writer { + public Writer(GTFSFeed feed) { + super(feed, TABLE_NAME); + } + + @Override + public void writeHeaders() throws IOException { + writer.writeRecord(new String[] {AREA_ID_COLUMN_NAME, STOP_ID_COLUMN_NAME}); + } + + @Override + public void writeOneRow(StopArea stopArea) throws IOException { + writeStringField(stopArea.area_id); + writeStringField(stopArea.stop_id); + endRecord(); + } + + @Override + public Iterator iterator() { + return this.feed.stop_areas.values().iterator(); + } + } +} + + diff --git a/src/main/java/com/conveyal/gtfs/model/TimeFrame.java b/src/main/java/com/conveyal/gtfs/model/TimeFrame.java new file mode 100644 index 000000000..d065507f3 --- /dev/null +++ b/src/main/java/com/conveyal/gtfs/model/TimeFrame.java @@ -0,0 +1,110 @@ +package com.conveyal.gtfs.model; + +import com.conveyal.gtfs.GTFSFeed; + +import java.io.IOException; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.Iterator; + +public class TimeFrame extends Entity { + + private static final long serialVersionUID = -194783727784855940L; + + public String timeframe_group_id; + public int start_time = INT_MISSING; + public int end_time = INT_MISSING; + public String service_id; + public String feed_id; + + public static final String TABLE_NAME = "timeframes"; + public static final String TIME_FRAME_GROUP_ID_COLUMN_NAME = "timeframe_group_id"; + public static final String START_TIME_COLUMN_NAME = "start_time"; + public static final String END_TIME_COLUMN_NAME = "end_time"; + public static final String SERVICE_ID_COLUMN_NAME = "service_id"; + + @Override + public String getId () { + return createPrimaryKey(timeframe_group_id, start_time, end_time, service_id); + } + + /** + * Sets the parameters for a prepared statement following the parameter order defined in + * {@link com.conveyal.gtfs.loader.Table#TIME_FRAMES}. JDBC prepared statement parameters use a one-based index. + */ + @Override + public void setStatementParameters(PreparedStatement statement, boolean setDefaultId) throws SQLException { + int oneBasedIndex = 1; + if (!setDefaultId) statement.setInt(oneBasedIndex++, id); + statement.setString(oneBasedIndex++, timeframe_group_id); + setIntParameter(statement, oneBasedIndex++, start_time); + setIntParameter(statement, oneBasedIndex++, end_time); + statement.setString(oneBasedIndex, service_id); + } + + public static class Loader extends Entity.Loader { + + public Loader(GTFSFeed feed) { + super(feed, TABLE_NAME); + } + + @Override + protected boolean isRequired() { + return false; + } + + @Override + public void loadOneRow() throws IOException { + TimeFrame timeFrame = new TimeFrame(); + timeFrame.id = row + 1; // offset line number by 1 to account for 0-based row index + timeFrame.timeframe_group_id = getStringField(TIME_FRAME_GROUP_ID_COLUMN_NAME, true); + timeFrame.start_time = getTimeField(START_TIME_COLUMN_NAME, false); + if (timeFrame.start_time == INT_MISSING) { + // An empty value is considered the start of the day (00:00:00). + timeFrame.start_time = 0; + } + timeFrame.end_time = getTimeField(END_TIME_COLUMN_NAME, false); + if (timeFrame.end_time == INT_MISSING) { + // An empty value is considered the end of the day (24:00:00). + timeFrame.end_time = 86400; + } + timeFrame.service_id = getStringField(SERVICE_ID_COLUMN_NAME, true); + timeFrame.feed = feed; + timeFrame.feed_id = feed.feedId; + feed.time_frames.put(timeFrame.getId(), timeFrame); + } + + } + + public static class Writer extends Entity.Writer { + public Writer(GTFSFeed feed) { + super(feed, TABLE_NAME); + } + + @Override + public void writeHeaders() throws IOException { + writer.writeRecord(new String[] { + TIME_FRAME_GROUP_ID_COLUMN_NAME, + START_TIME_COLUMN_NAME, + END_TIME_COLUMN_NAME, + SERVICE_ID_COLUMN_NAME + }); + } + + @Override + public void writeOneRow(TimeFrame timeFrame) throws IOException { + writeStringField(timeFrame.timeframe_group_id); + writeTimeField(timeFrame.start_time); + writeTimeField(timeFrame.end_time); + writeStringField(timeFrame.service_id); + endRecord(); + } + + @Override + public Iterator iterator() { + return this.feed.time_frames.values().iterator(); + } + } +} + + diff --git a/src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java b/src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java new file mode 100644 index 000000000..93a636157 --- /dev/null +++ b/src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java @@ -0,0 +1,275 @@ +package com.conveyal.gtfs; + +import com.conveyal.gtfs.graphql.GTFSGraphQL; +import com.conveyal.gtfs.loader.FeedLoadResult; +import com.csvreader.CsvReader; +import graphql.ExecutionInput; +import org.apache.commons.io.IOUtils; +import org.apache.commons.io.input.BOMInputStream; +import org.hamcrest.MatcherAssert; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import javax.sql.DataSource; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import static com.conveyal.gtfs.GTFS.load; +import static com.conveyal.gtfs.GTFS.validate; +import static com.conveyal.gtfs.TestUtils.checkFileTestCases; +import static com.conveyal.gtfs.TestUtils.getResourceFileName; +import static com.zenika.snapshotmatcher.SnapshotMatcher.matchesSnapshot; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.jupiter.api.Assertions.assertTimeout; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GTFSFaresV2Test { + + private static String simpleGtfsZipFileName; + public static String testDBName; + private static DataSource testDataSource; + private static String testNamespace; + private static final int TEST_TIMEOUT = 5000; + + + @BeforeAll + public static void setUpClass() throws IOException { + String folderName = "fake-agency-with-fares-v2"; + simpleGtfsZipFileName = TestUtils.zipFolderFiles(folderName, true); + // create a new database + testDBName = TestUtils.generateNewDB(); + String dbConnectionUrl = String.format("jdbc:postgresql://localhost/%s", testDBName); + testDataSource = TestUtils.createTestDataSource(dbConnectionUrl); + // zip up test folder into temp zip file + String zipFileName = TestUtils.zipFolderFiles(folderName, true); + // load feed into db + FeedLoadResult feedLoadResult = load(zipFileName, testDataSource); + testNamespace = feedLoadResult.uniqueIdentifier; + // validate feed to create additional tables + validate(testNamespace, testDataSource); + } + + @AfterAll + public static void tearDownClass() { + TestUtils.dropDB(testDBName); + } + + /** Tests that the graphQL schema can initialize. */ + @Test + void canInitialize() { + assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { + GTFSGraphQL.initialize(testDataSource); + GTFSGraphQL.getGraphQl(); + }); + } + + @Test + void canFetchAreas() { + assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { + MatcherAssert.assertThat(queryGraphQL("feedAreas.txt"), matchesSnapshot()); + }); + } + + @Test + void canFetchStopAreas() { + assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { + MatcherAssert.assertThat(queryGraphQL("feedStopAreas.txt"), matchesSnapshot()); + }); + } + + @Test + void canFetchFareTransferRules() { + assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { + MatcherAssert.assertThat(queryGraphQL("feedFareTransferRules.txt"), matchesSnapshot()); + }); + } + + @Test + void canFetchFareProducts() { + assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { + MatcherAssert.assertThat(queryGraphQL("feedFareProducts.txt"), matchesSnapshot()); + }); + } + + @Test + void canFetchFareMedias() { + assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { + MatcherAssert.assertThat(queryGraphQL("feedFareMedias.txt"), matchesSnapshot()); + }); + } + + @Test + void canFetchFareLegRules() { + assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { + MatcherAssert.assertThat(queryGraphQL("feedFareLegRules.txt"), matchesSnapshot()); + }); + } + + @Test + void canFetchTimeFrames() { + assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { + MatcherAssert.assertThat(queryGraphQL("feedTimeFrames.txt"), matchesSnapshot()); + }); + } + + @Test + void canFetchNetworks() { + assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { + MatcherAssert.assertThat(queryGraphQL("feedNetworks.txt"), matchesSnapshot()); + }); + } + + @Test + void canFetchRouteNetworks() { + assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { + MatcherAssert.assertThat(queryGraphQL("feedRouteNetworks.txt"), matchesSnapshot()); + }); + } + + /** + * Make sure a round-trip of loading fares v2 data and then writing this to another zip file can be performed. + */ + @Test + void canDoRoundTripLoadAndWriteToZipFile() throws IOException { + // create a temp file for this test + File outZip = File.createTempFile("fares-v2-output", ".zip"); + + // delete file to make sure we can assert that this program created the file + outZip.delete(); + + GTFSFeed feed = GTFSFeed.fromFile(simpleGtfsZipFileName); + feed.toFile(outZip.getAbsolutePath()); + feed.close(); + assertTrue(outZip.exists()); + + // assert that rows of data were written to files within the zipfile + ZipFile zip = new ZipFile(outZip); + + TestUtils.FileTestCase[] fileTestCases = { + new TestUtils.FileTestCase( + "areas.txt", + new TestUtils.DataExpectation[] { + new TestUtils.DataExpectation("area_id", "area_bl"), + new TestUtils.DataExpectation("area_name", "Blue Line") + } + ), + new TestUtils.FileTestCase( + "fare_leg_rules.txt", + new TestUtils.DataExpectation[] { + new TestUtils.DataExpectation("leg_group_id", "leg_airport_rapid_transit_quick_subway"), + new TestUtils.DataExpectation("network_id", "rapid_transit"), + new TestUtils.DataExpectation("from_area_id", "area_bl_airport") + } + ), + new TestUtils.FileTestCase( + "fare_media.txt", + new TestUtils.DataExpectation[] { + new TestUtils.DataExpectation("fare_media_id", "cash"), + new TestUtils.DataExpectation("fare_media_name", "Cash"), + new TestUtils.DataExpectation("fare_media_type", "0") + } + ), + new TestUtils.FileTestCase( + "fare_products.txt", + new TestUtils.DataExpectation[] { + new TestUtils.DataExpectation("fare_product_id", "prod_boat_zone_1"), + new TestUtils.DataExpectation("fare_product_name", "Ferry Zone 1 one-way fare"), + new TestUtils.DataExpectation("fare_media_id", "cash"), + new TestUtils.DataExpectation("amount", "6.5000000"), + new TestUtils.DataExpectation("currency", "USD") + } + ), + new TestUtils.FileTestCase( + "fare_transfer_rules.txt", + new TestUtils.DataExpectation[] { + new TestUtils.DataExpectation("from_leg_group_id", "leg_airport_rapid_transit_quick_subway"), + new TestUtils.DataExpectation("to_leg_group_id", "leg_local_bus_quick_subway"), + new TestUtils.DataExpectation("transfer_count", ""), + new TestUtils.DataExpectation("duration_limit", "7200"), + new TestUtils.DataExpectation("duration_limit_type", "1"), + new TestUtils.DataExpectation("fare_transfer_type", "0"), + new TestUtils.DataExpectation("fare_product_id", "prod_rapid_transit_quick_subway") + } + ), + new TestUtils.FileTestCase( + "networks.txt", + new TestUtils.DataExpectation[] { + new TestUtils.DataExpectation("network_id", "1"), + new TestUtils.DataExpectation("network_name", "Forbidden because network id is defined in routes") + } + ), + new TestUtils.FileTestCase( + "route_networks.txt", + new TestUtils.DataExpectation[] { + new TestUtils.DataExpectation("network_id", "1"), + new TestUtils.DataExpectation("route_id", "1") + } + ), + new TestUtils.FileTestCase( + "stop_areas.txt", + new TestUtils.DataExpectation[] { + new TestUtils.DataExpectation("stop_id", "4u6g"), + new TestUtils.DataExpectation("area_id", "area_route_426_downtown") + } + ), + new TestUtils.FileTestCase( + "timeframes.txt", + new TestUtils.DataExpectation[] { + new TestUtils.DataExpectation("timeframe_group_id", "timeframe_sumner_tunnel_closure"), + new TestUtils.DataExpectation("start_time", "00:00:00"), + new TestUtils.DataExpectation("end_time", "02:30:00"), + new TestUtils.DataExpectation("service_id", "04100312-8fe1-46a5-a9f2-556f39478f57") + } + ) + }; + checkFileTestCases(zip, fileTestCases); + } + + /** + * Helper method to make a query with default variables. + * + * @param queryFilename the filename that should be used to generate the GraphQL query. This file must be present + * in the `src/test/resources/graphql` folder + */ + private Map queryGraphQL(String queryFileName) throws IOException { + Map variables = new HashMap<>(); + variables.put("namespace", testNamespace); + return queryGraphQL(queryFileName, variables, testDataSource); + } + + /** + * Helper method to execute a GraphQL query and return the result. + * + * @param queryFilename the filename that should be used to generate the GraphQL query. This file must be present + * in the `src/test/resources/graphql` folder + * @param variables a Map of input variables to the graphql query about to be executed + * @param dataSource the datasource to use when initializing GraphQL + */ + private Map queryGraphQL( + String queryFilename, + Map variables, + DataSource dataSource + ) throws IOException { + GTFSGraphQL.initialize(dataSource); + FileInputStream inputStream = new FileInputStream( + getResourceFileName(String.format("graphql/%s", queryFilename)) + ); + ExecutionInput executionInput = ExecutionInput.newExecutionInput() + .query(IOUtils.toString(inputStream)) + .variables(variables) + .build(); + return GTFSGraphQL.getGraphQl().execute(executionInput).toSpecification(); + } +} diff --git a/src/test/java/com/conveyal/gtfs/GTFSFeedTest.java b/src/test/java/com/conveyal/gtfs/GTFSFeedTest.java index 7afd7189b..d842b3bcc 100644 --- a/src/test/java/com/conveyal/gtfs/GTFSFeedTest.java +++ b/src/test/java/com/conveyal/gtfs/GTFSFeedTest.java @@ -1,24 +1,17 @@ package com.conveyal.gtfs; import com.conveyal.gtfs.model.StopTime; -import com.csvreader.CsvReader; -import org.apache.commons.io.input.BOMInputStream; import org.hamcrest.comparator.ComparatorMatcherBuilder; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import static com.conveyal.gtfs.TestUtils.checkFileTestCases; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.number.IsCloseTo.closeTo; @@ -26,30 +19,8 @@ * Test suite for the GTFSFeed class. */ public class GTFSFeedTest { - - private static final Logger LOG = LoggerFactory.getLogger(GTFSFeedTest.class); private static String simpleGtfsZipFileName; - private static class FileTestCase { - public String filename; - public DataExpectation[] expectedColumnData; - - public FileTestCase(String filename, DataExpectation[] expectedColumnData) { - this.filename = filename; - this.expectedColumnData = expectedColumnData; - } - } - - private static class DataExpectation { - public String columnName; - public String expectedValue; - - public DataExpectation(String columnName, String expectedValue) { - this.columnName = columnName; - this.expectedValue = expectedValue; - } - } - @BeforeAll public static void setUpClass() { //executed only once, before the first test @@ -65,7 +36,7 @@ public static void setUpClass() { * Make sure a round-trip of loading a GTFS zip file and then writing another zip file can be performed. */ @Test - public void canDoRoundTripLoadAndWriteToZipFile() throws IOException { + void canDoRoundTripLoadAndWriteToZipFile() throws IOException { // create a temp file for this test File outZip = File.createTempFile("fake-agency-output", ".zip"); @@ -80,119 +51,84 @@ public void canDoRoundTripLoadAndWriteToZipFile() throws IOException { // assert that rows of data were written to files within the zipfile ZipFile zip = new ZipFile(outZip); - FileTestCase[] fileTestCases = { + TestUtils.FileTestCase[] fileTestCases = { // agency.txt - new FileTestCase( + new TestUtils.FileTestCase( "agency.txt", - new DataExpectation[]{ - new DataExpectation("agency_id", "1"), - new DataExpectation("agency_name", "Fake Transit") + new TestUtils.DataExpectation[] { + new TestUtils.DataExpectation("agency_id", "1"), + new TestUtils.DataExpectation("agency_name", "Fake Transit") } ), - new FileTestCase( + new TestUtils.FileTestCase( "calendar.txt", - new DataExpectation[]{ - new DataExpectation("service_id", "04100312-8fe1-46a5-a9f2-556f39478f57"), - new DataExpectation("start_date", "20170915"), - new DataExpectation("end_date", "20170917") + new TestUtils.DataExpectation[] { + new TestUtils.DataExpectation("service_id", "04100312-8fe1-46a5-a9f2-556f39478f57"), + new TestUtils.DataExpectation("start_date", "20170915"), + new TestUtils.DataExpectation("end_date", "20170917") } ), - new FileTestCase( + new TestUtils.FileTestCase( "calendar_dates.txt", - new DataExpectation[]{ - new DataExpectation("service_id", "calendar-date-service"), - new DataExpectation("date", "20170917"), - new DataExpectation("exception_type", "1") + new TestUtils.DataExpectation[] { + new TestUtils.DataExpectation("service_id", "calendar-date-service"), + new TestUtils.DataExpectation("date", "20170917"), + new TestUtils.DataExpectation("exception_type", "1") } ), - new FileTestCase( + new TestUtils.FileTestCase( "routes.txt", - new DataExpectation[]{ - new DataExpectation("agency_id", "1"), - new DataExpectation("route_id", "1"), - new DataExpectation("route_long_name", "Route 1") + new TestUtils.DataExpectation[] { + new TestUtils.DataExpectation("agency_id", "1"), + new TestUtils.DataExpectation("route_id", "1"), + new TestUtils.DataExpectation("route_long_name", "Route 1") } ), - new FileTestCase( + new TestUtils.FileTestCase( "shapes.txt", - new DataExpectation[]{ - new DataExpectation("shape_id", "5820f377-f947-4728-ac29-ac0102cbc34e"), - new DataExpectation("shape_pt_lat", "37.0612132"), - new DataExpectation("shape_pt_lon", "-122.0074332") + new TestUtils.DataExpectation[] { + new TestUtils.DataExpectation("shape_id", "5820f377-f947-4728-ac29-ac0102cbc34e"), + new TestUtils.DataExpectation("shape_pt_lat", "37.0612132"), + new TestUtils.DataExpectation("shape_pt_lon", "-122.0074332") } ), - new FileTestCase( + new TestUtils.FileTestCase( "stop_times.txt", - new DataExpectation[]{ - new DataExpectation("trip_id", "a30277f8-e50a-4a85-9141-b1e0da9d429d"), - new DataExpectation("departure_time", "07:00:00"), - new DataExpectation("stop_id", "4u6g") + new TestUtils.DataExpectation[] { + new TestUtils.DataExpectation("trip_id", "a30277f8-e50a-4a85-9141-b1e0da9d429d"), + new TestUtils.DataExpectation("departure_time", "07:00:00"), + new TestUtils.DataExpectation("stop_id", "4u6g") } ), - new FileTestCase( + new TestUtils.FileTestCase( "trips.txt", - new DataExpectation[]{ - new DataExpectation("route_id", "1"), - new DataExpectation("trip_id", "a30277f8-e50a-4a85-9141-b1e0da9d429d"), - new DataExpectation("service_id", "04100312-8fe1-46a5-a9f2-556f39478f57") + new TestUtils.DataExpectation[] { + new TestUtils.DataExpectation("route_id", "1"), + new TestUtils.DataExpectation("trip_id", "a30277f8-e50a-4a85-9141-b1e0da9d429d"), + new TestUtils.DataExpectation("service_id", "04100312-8fe1-46a5-a9f2-556f39478f57") } ), - new FileTestCase( + new TestUtils.FileTestCase( "datatools_patterns.txt", - new DataExpectation[]{ - new DataExpectation("pattern_id", "1"), - new DataExpectation("route_id", "1"), - new DataExpectation("name", "2 stops from Butler Ln to Scotts Valley Dr & Victor Sq (1 trips)"), - new DataExpectation("direction_id", "0"), - new DataExpectation("shape_id", "5820f377-f947-4728-ac29-ac0102cbc34e") + new TestUtils.DataExpectation[] { + new TestUtils.DataExpectation("pattern_id", "1"), + new TestUtils.DataExpectation("route_id", "1"), + new TestUtils.DataExpectation("name", "2 stops from Butler Ln to Scotts Valley Dr & Victor Sq (1 trips)"), + new TestUtils.DataExpectation("direction_id", "0"), + new TestUtils.DataExpectation("shape_id", "5820f377-f947-4728-ac29-ac0102cbc34e") } ) }; - - // look through all written files in the zipfile - for (FileTestCase fileTestCase: fileTestCases) { - ZipEntry entry = zip.getEntry(fileTestCase.filename); - - // make sure the file exists within the zipfile - assertThat(entry, notNullValue()); - - // create csv reader for file - InputStream zis = zip.getInputStream(entry); - InputStream bis = new BOMInputStream(zis); - CsvReader reader = new CsvReader(bis, ',', Charset.forName("UTF8")); - - // make sure the file has headers - boolean hasHeaders = reader.readHeaders(); - assertThat(hasHeaders, is(true)); - - // make sure that the a record matching the expected row exists in this table - boolean recordFound = false; - while (reader.readRecord() && !recordFound) { - boolean allExpectationsMetForThisRecord = true; - for (DataExpectation dataExpectation : fileTestCase.expectedColumnData) { - if(!reader.get(dataExpectation.columnName).equals(dataExpectation.expectedValue)) { - allExpectationsMetForThisRecord = false; - break; - } - } - if (allExpectationsMetForThisRecord) { - recordFound = true; - } - } - assertThat( - String.format("Data Expectation record not found in %s", fileTestCase.filename), - recordFound, - is(true) - ); - } + checkFileTestCases(zip, fileTestCases); } /** * Make sure that a GTFS feed with interpolated stop times have calculated times after feed processing + * * @throws GTFSFeed.FirstAndLastStopsDoNotHaveTimes */ @Test - public void canGetInterpolatedTimes() throws GTFSFeed.FirstAndLastStopsDoNotHaveTimes, IOException { + void canGetInterpolatedTimes() throws GTFSFeed.FirstAndLastStopsDoNotHaveTimes, IOException { String tripId = "a30277f8-e50a-4a85-9141-b1e0da9d429d"; String gtfsZipFileName = TestUtils.zipFolderFiles("fake-agency-interpolated-stop-times", true); @@ -241,7 +177,7 @@ public void canGetInterpolatedTimes() throws GTFSFeed.FirstAndLastStopsDoNotHave * Make sure a spatial index of stops can be calculated */ @Test - public void canGetSpatialIndex() { + void canGetSpatialIndex() { GTFSFeed feed = GTFSFeed.fromFile(simpleGtfsZipFileName); assertThat( feed.getSpatialIndex().size(), @@ -254,7 +190,7 @@ public void canGetSpatialIndex() { * Make sure trip speed can be calculated using trip's shape. */ @Test - public void canGetTripSpeedUsingShape() { + void canGetTripSpeedUsingShape() { GTFSFeed feed = GTFSFeed.fromFile(simpleGtfsZipFileName); assertThat( feed.getTripSpeed("a30277f8-e50a-4a85-9141-b1e0da9d429d"), @@ -266,7 +202,7 @@ public void canGetTripSpeedUsingShape() { * Make sure trip speed can be calculated using trip's shape. */ @Test - public void canGetTripSpeedUsingStraightLine() { + void canGetTripSpeedUsingStraightLine() { GTFSFeed feed = GTFSFeed.fromFile(simpleGtfsZipFileName); assertThat( feed.getTripSpeed("a30277f8-e50a-4a85-9141-b1e0da9d429d", true), diff --git a/src/test/java/com/conveyal/gtfs/GTFSTest.java b/src/test/java/com/conveyal/gtfs/GTFSTest.java index 5eacb02a9..2b0d82da0 100644 --- a/src/test/java/com/conveyal/gtfs/GTFSTest.java +++ b/src/test/java/com/conveyal/gtfs/GTFSTest.java @@ -6,6 +6,7 @@ import com.conveyal.gtfs.loader.JdbcGtfsExporter; import com.conveyal.gtfs.loader.SnapshotResult; import com.conveyal.gtfs.loader.Table; +import com.conveyal.gtfs.loader.TableLoadResult; import com.conveyal.gtfs.storage.ErrorExpectation; import com.conveyal.gtfs.storage.ExpectedFieldType; import com.conveyal.gtfs.storage.PersistenceExpectation; @@ -41,15 +42,20 @@ import java.util.Arrays; import java.util.Collection; import java.util.Iterator; +import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import static com.conveyal.gtfs.TestUtils.getResourceFileName; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.core.IsNull.nullValue; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; /** @@ -300,6 +306,128 @@ public void canLoadAndExportSimpleAgencyInSubDirectory() throws IOException { ); } + + /** + * Tests whether "fake-agency-with-fares-v2" GTFS can be placed in a zipped subdirectory and loaded/exported + * successfully. + */ + @Test + void canLoadAndExportFaresV2Feed() throws IOException { + String resourceFolder = TestUtils.getResourceFileName("fake-agency-with-fares-v2"); + String zipFileName = TestUtils.zipFolderFiles(resourceFolder, false); + ErrorExpectation[] errorExpectations = ErrorExpectation.list( + new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), + new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), + new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), + new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), + new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), + new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), + new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), + new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), + new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), + new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), + new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), + new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), + new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), + new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), + new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), + new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), + new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), + new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), + new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), + new ErrorExpectation(NewGTFSErrorType.FEED_TRAVEL_TIMES_ROUNDED), + new ErrorExpectation(NewGTFSErrorType.DATE_NO_SERVICE) + ); + assertTrue( + runIntegrationTestOnZipFile(zipFileName, nullValue(), faresV2PersistenceExpectations, errorExpectations) + ); + } + + /** + * Persistence expectations for use with the GTFS contained within the "MBTA_GTFS_Fares_v2.zip" feed. + */ + private final PersistenceExpectation[] faresV2PersistenceExpectations = new PersistenceExpectation[]{ + new PersistenceExpectation( + "areas", + new RecordExpectation[]{ + new RecordExpectation("area_id", "area_bl"), + new RecordExpectation("area_name", "Blue Line") + } + ), + new PersistenceExpectation( + "fare_leg_rules", + new RecordExpectation[]{ + new RecordExpectation("leg_group_id", "leg_commuter_rail_cash"), + new RecordExpectation("network_id", "commuter_rail"), + new RecordExpectation("from_area_id", "area_commuter_rail_sumner_tunnel_zone_1a"), + new RecordExpectation("to_area_id", "area_commuter_rail_sumner_tunnel_zone_1a"), + new RecordExpectation("fare_product_id", "prod_cr_zone_1a"), + new RecordExpectation("from_timeframe_group_id", "timeframe_sumner_tunnel_closure"), + new RecordExpectation("to_timeframe_group_id", null), + new RecordExpectation("transfer_only", null) + } + ), + new PersistenceExpectation( + "fare_media", + new RecordExpectation[]{ + new RecordExpectation("fare_media_id", "credit_debit"), + new RecordExpectation("fare_media_name", "Credit/debit card"), + new RecordExpectation("fare_media_type", "0"), + } + ), + new PersistenceExpectation( + "fare_products", + new RecordExpectation[]{ + new RecordExpectation("fare_product_id", "prod_cr_inter_4"), + new RecordExpectation("fare_product_name", "Commuter Rail Interzone 4 one-way fare"), + new RecordExpectation("fare_media_id", "credit_debit"), + new RecordExpectation("amount", "4.25"), + new RecordExpectation("currency", "USD"), + } + ), + new PersistenceExpectation( + "fare_transfer_rules", + new RecordExpectation[]{ + new RecordExpectation("from_leg_group_id", "leg_airport_rapid_transit_quick_subway"), + new RecordExpectation("to_leg_group_id", "leg_local_bus_quick_subway"), + new RecordExpectation("transfer_count", null), + new RecordExpectation("duration_limit", "7200"), + new RecordExpectation("duration_limit_type", "1"), + new RecordExpectation("fare_transfer_type", "0"), + new RecordExpectation("fare_product_id", null) + } + ), + new PersistenceExpectation( + "networks", + new RecordExpectation[]{ + new RecordExpectation("network_id", "1"), + new RecordExpectation("network_name", "Forbidden because network id is defined in routes") + } + ), + new PersistenceExpectation( + "route_networks", + new RecordExpectation[]{ + new RecordExpectation("network_id", "1"), + new RecordExpectation("route_id", "1") + } + ), + new PersistenceExpectation( + "stop_areas", + new RecordExpectation[]{ + new RecordExpectation("stop_id", "4u6g"), + new RecordExpectation("area_id", "area_route_426_downtown") + } + ), + new PersistenceExpectation( + "timeframes", + new RecordExpectation[]{ + new RecordExpectation("timeframe_group_id", "timeframe_regular"), + new RecordExpectation("service_id", "04100312-8fe1-46a5-a9f2-556f39478f57") + } + ) + }; + + /** * Tests whether the simple gtfs can be loaded and exported if it has only calendar_dates.txt */ @@ -834,20 +962,29 @@ private boolean runIntegrationTestOnZipFile( } private void assertThatLoadIsErrorFree(FeedLoadResult loadResult) { - assertThat(loadResult.fatalException, is(nullValue())); - assertThat(loadResult.agency.fatalException, is(nullValue())); - assertThat(loadResult.calendar.fatalException, is(nullValue())); - assertThat(loadResult.calendarDates.fatalException, is(nullValue())); - assertThat(loadResult.fareAttributes.fatalException, is(nullValue())); - assertThat(loadResult.fareRules.fatalException, is(nullValue())); - assertThat(loadResult.feedInfo.fatalException, is(nullValue())); - assertThat(loadResult.frequencies.fatalException, is(nullValue())); - assertThat(loadResult.routes.fatalException, is(nullValue())); - assertThat(loadResult.shapes.fatalException, is(nullValue())); - assertThat(loadResult.stops.fatalException, is(nullValue())); - assertThat(loadResult.stopTimes.fatalException, is(nullValue())); - assertThat(loadResult.transfers.fatalException, is(nullValue())); - assertThat(loadResult.trips.fatalException, is(nullValue())); + assertNull(loadResult.fatalException); + assertNull(loadResult.agency.fatalException); + assertNull(loadResult.calendar.fatalException); + assertNull(loadResult.calendarDates.fatalException); + assertNull(loadResult.fareAttributes.fatalException); + assertNull(loadResult.fareRules.fatalException); + assertNull(loadResult.feedInfo.fatalException); + assertNull(loadResult.frequencies.fatalException); + assertNull(loadResult.routes.fatalException); + assertNull(loadResult.shapes.fatalException); + assertNull(loadResult.stops.fatalException); + assertNull(loadResult.stopTimes.fatalException); + assertNull(loadResult.transfers.fatalException); + assertNull(loadResult.trips.fatalException); + assertNull(loadResult.areas.fatalException); + assertNull(loadResult.fareLegRules.fatalException); + assertNull(loadResult.fareMedias.fatalException); + assertNull(loadResult.fareProducts.fatalException); + assertNull(loadResult.fareTransferRules.fatalException); + assertNull(loadResult.networks.fatalException); + assertNull(loadResult.routeNetworks.fatalException); + assertNull(loadResult.stopAreas.fatalException); + assertNull(loadResult.timeFrames.fatalException); } private void assertThatSnapshotIsErrorFree(SnapshotResult snapshotResult) { @@ -858,11 +995,6 @@ private void assertThatSnapshotIsErrorFree(SnapshotResult snapshotResult) { /** * Helper function to export a GTFS from the database to a temporary zip file. */ -// private File exportGtfs(String namespace, DataSource dataSource, boolean fromEditor) throws IOException { -// File tempFile = File.createTempFile("snapshot", ".zip"); -// GTFS.export(namespace, tempFile.getAbsolutePath(), dataSource, fromEditor, false); -// return tempFile; -// } private File exportGtfs(String namespace, DataSource dataSource, boolean fromEditor, boolean publishProprietaryFiles) throws IOException { File tempFile = File.createTempFile("snapshot", ".zip"); GTFS.export(namespace, tempFile.getAbsolutePath(), dataSource, fromEditor, publishProprietaryFiles); diff --git a/src/test/java/com/conveyal/gtfs/TestUtils.java b/src/test/java/com/conveyal/gtfs/TestUtils.java index ca06de25d..a346c437a 100644 --- a/src/test/java/com/conveyal/gtfs/TestUtils.java +++ b/src/test/java/com/conveyal/gtfs/TestUtils.java @@ -1,7 +1,24 @@ package com.conveyal.gtfs; import com.conveyal.gtfs.error.NewGTFSErrorType; +import com.conveyal.gtfs.loader.FeedLoadResult; +import com.conveyal.gtfs.loader.SnapshotResult; +import com.conveyal.gtfs.loader.Table; +import com.conveyal.gtfs.storage.ErrorExpectation; +import com.conveyal.gtfs.storage.ExpectedFieldType; +import com.conveyal.gtfs.storage.PersistenceExpectation; +import com.conveyal.gtfs.storage.RecordExpectation; +import com.conveyal.gtfs.util.InvalidNamespaceException; +import com.conveyal.gtfs.validator.FeedValidatorCreator; +import com.conveyal.gtfs.validator.ValidationResult; +import com.csvreader.CsvReader; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; import org.apache.commons.io.IOUtils; +import org.apache.commons.io.input.BOMInputStream; +import org.hamcrest.Matcher; +import org.hamcrest.comparator.ComparatorMatcherBuilder; +import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -10,16 +27,27 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; import static com.conveyal.gtfs.util.Util.randomIdString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.core.IsNull.nullValue; +import static org.junit.jupiter.api.Assertions.assertTrue; public class TestUtils { @@ -28,6 +56,8 @@ public class TestUtils { public static final String PG_TEST_USER = "postgres"; public static final String PG_TEST_PASSWORD = "postgres"; + private static final String JDBC_URL = "jdbc:postgresql://localhost"; + /** * Forcefully drops a database even if other users are connected to it. * @@ -42,7 +72,7 @@ public static void dropDB(String dbName) { )); // drop the db if(executeAndClose(String.format("DROP DATABASE %s", dbName))) { - LOG.error(String.format("Successfully dropped database: %s", dbName)); + LOG.info(String.format("Successfully dropped database: %s", dbName)); } else { LOG.error(String.format("Failed to drop database: %s", dbName)); } @@ -211,5 +241,64 @@ public static void checkFeedHasExpectedNumberOfErrors( assertThatSqlCountQueryYieldsExpectedCount(testDataSource, sql, expectedNumberOfErrors); } + public static class FileTestCase { + public String filename; + public TestUtils.DataExpectation[] expectedColumnData; + + public FileTestCase(String filename, TestUtils.DataExpectation[] expectedColumnData) { + this.filename = filename; + this.expectedColumnData = expectedColumnData; + } + } + + public static class DataExpectation { + public String columnName; + public String expectedValue; + public DataExpectation(String columnName, String expectedValue) { + this.columnName = columnName; + this.expectedValue = expectedValue; + } + } + + /** + * Look through all written files and confirm that the record matching the expected row exists in appropriate file. + */ + public static void checkFileTestCases(ZipFile zip, FileTestCase[] fileTestCases) throws IOException { + // Look through all written files in the zip file. + for (TestUtils.FileTestCase fileTestCase : fileTestCases) { + ZipEntry entry = zip.getEntry(fileTestCase.filename); + + // make sure the file exists within the zip file. + assertThat(entry, notNullValue()); + + // create csv reader for file + InputStream zis = zip.getInputStream(entry); + InputStream bis = new BOMInputStream(zis); + CsvReader reader = new CsvReader(bis, ',', StandardCharsets.UTF_8); + + // make sure the file has headers + boolean hasHeaders = reader.readHeaders(); + assertTrue(hasHeaders); + + // make sure that the record matching the expected row exists in this table. + boolean recordFound = false; + while (reader.readRecord() && !recordFound) { + boolean allExpectationsMetForThisRecord = true; + for (TestUtils.DataExpectation dataExpectation : fileTestCase.expectedColumnData) { + if (!reader.get(dataExpectation.columnName).equals(dataExpectation.expectedValue)) { + allExpectationsMetForThisRecord = false; + break; + } + } + if (allExpectationsMetForThisRecord) { + recordFound = true; + } + } + assertTrue( + recordFound, + String.format("Data Expectation record not found in %s", fileTestCase.filename) + ); + } + } } diff --git a/src/test/java/com/conveyal/gtfs/dto/RouteDTO.java b/src/test/java/com/conveyal/gtfs/dto/RouteDTO.java index ec15aca45..6b8cf8444 100644 --- a/src/test/java/com/conveyal/gtfs/dto/RouteDTO.java +++ b/src/test/java/com/conveyal/gtfs/dto/RouteDTO.java @@ -26,4 +26,6 @@ public class RouteDTO { public Integer status; public int continuous_pickup; public int continuous_drop_off; + public String network_id; + } diff --git a/src/test/resources/fake-agency-with-fares-v2/agency.txt b/src/test/resources/fake-agency-with-fares-v2/agency.txt new file mode 100644 index 000000000..5cb6afa42 --- /dev/null +++ b/src/test/resources/fake-agency-with-fares-v2/agency.txt @@ -0,0 +1,2 @@ +agency_id,agency_name,agency_url,agency_lang,agency_phone,agency_email,agency_timezone,agency_fare_url,agency_branding_url +1,Fake Transit,http://www.fake-agency.com,,,,America/Los_Angeles,, diff --git a/src/test/resources/fake-agency-with-fares-v2/areas.txt b/src/test/resources/fake-agency-with-fares-v2/areas.txt new file mode 100644 index 000000000..73b6734b6 --- /dev/null +++ b/src/test/resources/fake-agency-with-fares-v2/areas.txt @@ -0,0 +1,40 @@ +area_id,area_name +area_bl,Blue Line +area_bl_airport,Blue Line - Airport Station +area_cf_zone_buzzards,CapeFLYER - Wareham/Buzzards Bay/Bourne +area_cf_zone_hyannis,CapeFLYER - Hyannis +area_commuter_rail_porter_zone_1a,Commuter Rail Zone 1A +area_commuter_rail_sumner_tunnel_zone_1a,Commuter Rail Zone 1A +area_commuter_rail_zone_1,Commuter Rail Zone 1 +area_commuter_rail_zone_10,Commuter Rail Zone 10 +area_commuter_rail_zone_1a,Commuter Rail Zone 1A +area_commuter_rail_zone_2,Commuter Rail Zone 2 +area_commuter_rail_zone_3,Commuter Rail Zone 3 +area_commuter_rail_zone_4,Commuter Rail Zone 4 +area_commuter_rail_zone_5,Commuter Rail Zone 5 +area_commuter_rail_zone_6,Commuter Rail Zone 6 +area_commuter_rail_zone_7,Commuter Rail Zone 7 +area_commuter_rail_zone_8,Commuter Rail Zone 8 +area_commuter_rail_zone_9,Commuter Rail Zone 9 +area_fairmount_line_zone_1a,Commuter Rail Zone 1A - Fairmount Line +area_gl_govt_ctr,Green Line - Government Center +area_green_b_west_of_kenmore,Green Line B - West of Kenmore +area_green_c_west_of_kenmore,Green Line C - West of Kenmore +area_green_e_west_of_symphony,Green Line E - West of Symphony +area_m_ashmont_mattapan,Mattapan Trolley - Ashmont and Mattapan +area_ol_state,Orange Line - State +area_red_south_station,Red Line - South Station +area_route_354_downtown,Route 354 - Downtown +area_route_354_outside_downtown,Route 354 - Outside Downtown +area_route_426_downtown,Route 426 - Downtown +area_route_426_outside_downtown,Route 426 - Outside Downtown +area_route_450_downtown,Route 450 - Downtown +area_route_450_outside_downtown,Route 450 - Outside Downtown +area_sl3_north_of_airport,Silver Line - North of Airport Station +area_sl_airport,Silver Line - Airport Station +area_sl_courthouse,Silver Line - Courthouse +area_sl_logan_terminal,Silver Line - Airport Terminals +area_sl_silver_line_way,Silver Line - Silver Line Way +area_sl_south_station,Silver Line - South Station +area_sl_world_trade_center,Silver Line - World Trade Center +area_ss_commuter_rail_zone_1a,Commuter Rail Zone 1A - South Station diff --git a/src/test/resources/fake-agency-with-fares-v2/calendar.txt b/src/test/resources/fake-agency-with-fares-v2/calendar.txt new file mode 100644 index 000000000..1bf13678e --- /dev/null +++ b/src/test/resources/fake-agency-with-fares-v2/calendar.txt @@ -0,0 +1,2 @@ +service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date +04100312-8fe1-46a5-a9f2-556f39478f57,1,1,1,1,1,1,1,20170915,20170917 \ No newline at end of file diff --git a/src/test/resources/fake-agency-with-fares-v2/calendar_dates.txt b/src/test/resources/fake-agency-with-fares-v2/calendar_dates.txt new file mode 100644 index 000000000..5d0a31806 --- /dev/null +++ b/src/test/resources/fake-agency-with-fares-v2/calendar_dates.txt @@ -0,0 +1,3 @@ +service_id,date,exception_type +04100312-8fe1-46a5-a9f2-556f39478f57,20170916,2 +calendar-date-service,20170917,1 \ No newline at end of file diff --git a/src/test/resources/fake-agency-with-fares-v2/fare_leg_rules.txt b/src/test/resources/fake-agency-with-fares-v2/fare_leg_rules.txt new file mode 100644 index 000000000..37b60d472 --- /dev/null +++ b/src/test/resources/fake-agency-with-fares-v2/fare_leg_rules.txt @@ -0,0 +1,697 @@ +leg_group_id,network_id,from_area_id,to_area_id,fare_product_id,from_timeframe_group_id,to_timeframe_group_id,transfer_only +leg_airport_rapid_transit_quick_subway,rapid_transit,area_bl_airport,,prod_rapid_transit_quick_subway,timeframe_regular,, +leg_cape_buzzards_hyannis_cash,cape_flyer,area_cf_zone_buzzards,area_cf_zone_hyannis,prod_cape_buzzards_hyannis_fare,,, +leg_cape_buzzards_hyannis_cash,cape_flyer,area_cf_zone_hyannis,area_cf_zone_buzzards,prod_cape_buzzards_hyannis_fare,,, +leg_cape_buzzards_hyannis_cash,cape_flyer,area_cf_zone_hyannis,area_commuter_rail_zone_8,prod_cape_buzzards_hyannis_fare,,, +leg_cape_buzzards_hyannis_cash,cape_flyer,area_commuter_rail_zone_8,area_cf_zone_hyannis,prod_cape_buzzards_hyannis_fare,,, +leg_cape_sbb_buzzards_cash,cape_flyer,area_cf_zone_buzzards,area_commuter_rail_zone_1a,prod_cape_sbb_buzzards_fare,,, +leg_cape_sbb_buzzards_cash,cape_flyer,area_cf_zone_buzzards,area_commuter_rail_zone_2,prod_cape_sbb_buzzards_fare,,, +leg_cape_sbb_buzzards_cash,cape_flyer,area_cf_zone_buzzards,area_commuter_rail_zone_4,prod_cape_sbb_buzzards_fare,,, +leg_cape_sbb_buzzards_cash,cape_flyer,area_commuter_rail_zone_1a,area_cf_zone_buzzards,prod_cape_sbb_buzzards_fare,,, +leg_cape_sbb_buzzards_cash,cape_flyer,area_commuter_rail_zone_2,area_cf_zone_buzzards,prod_cape_sbb_buzzards_fare,,, +leg_cape_sbb_buzzards_cash,cape_flyer,area_commuter_rail_zone_4,area_cf_zone_buzzards,prod_cape_sbb_buzzards_fare,,, +leg_cape_sbb_hyannis_cash,cape_flyer,area_cf_zone_hyannis,area_commuter_rail_zone_1a,prod_cape_sbb_hyannis_fare,,, +leg_cape_sbb_hyannis_cash,cape_flyer,area_cf_zone_hyannis,area_commuter_rail_zone_2,prod_cape_sbb_hyannis_fare,,, +leg_cape_sbb_hyannis_cash,cape_flyer,area_cf_zone_hyannis,area_commuter_rail_zone_4,prod_cape_sbb_hyannis_fare,,, +leg_cape_sbb_hyannis_cash,cape_flyer,area_commuter_rail_zone_1a,area_cf_zone_hyannis,prod_cape_sbb_hyannis_fare,,, +leg_cape_sbb_hyannis_cash,cape_flyer,area_commuter_rail_zone_2,area_cf_zone_hyannis,prod_cape_sbb_hyannis_fare,,, +leg_cape_sbb_hyannis_cash,cape_flyer,area_commuter_rail_zone_4,area_cf_zone_hyannis,prod_cape_sbb_hyannis_fare,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_sumner_tunnel_zone_1a,area_commuter_rail_sumner_tunnel_zone_1a,prod_cr_zone_1a,timeframe_sumner_tunnel_closure,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_sumner_tunnel_zone_1a,area_commuter_rail_zone_1a,prod_cr_zone_1a,timeframe_sumner_tunnel_closure,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_1,prod_cr_inter_1,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_10,prod_cr_inter_10,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_1a,prod_cr_zone_1,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_2,prod_cr_inter_2,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_3,prod_cr_inter_3,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_4,prod_cr_inter_4,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_5,prod_cr_inter_5,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_6,prod_cr_inter_6,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_7,prod_cr_inter_7,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_8,prod_cr_inter_8,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_9,prod_cr_inter_9,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_fairmount_line_zone_1a,prod_cr_zone_1,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1,area_ss_commuter_rail_zone_1a,prod_cr_zone_1,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_1,prod_cr_inter_10,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_10,prod_cr_inter_1,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_1a,prod_cr_zone_10,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_2,prod_cr_inter_9,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_3,prod_cr_inter_8,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_4,prod_cr_inter_7,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_5,prod_cr_inter_6,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_6,prod_cr_inter_5,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_7,prod_cr_inter_4,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_8,prod_cr_inter_3,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_9,prod_cr_inter_2,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_fairmount_line_zone_1a,prod_cr_zone_10,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_10,area_ss_commuter_rail_zone_1a,prod_cr_zone_10,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_sumner_tunnel_zone_1a,prod_cr_zone_1a,timeframe_sumner_tunnel_closure,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_1,prod_cr_zone_1,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_10,prod_cr_zone_10,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_1a,prod_cr_zone_1a,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_2,prod_cr_zone_2,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_3,prod_cr_zone_3,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_4,prod_cr_zone_4,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_5,prod_cr_zone_5,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_6,prod_cr_zone_6,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_7,prod_cr_zone_7,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_8,prod_cr_zone_8,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_9,prod_cr_zone_9,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_fairmount_line_zone_1a,prod_cr_zone_1a,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_1a,area_ss_commuter_rail_zone_1a,prod_cr_zone_1a,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_1,prod_cr_inter_2,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_10,prod_cr_inter_9,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_1a,prod_cr_zone_2,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_2,prod_cr_inter_1,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_3,prod_cr_inter_2,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_4,prod_cr_inter_3,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_5,prod_cr_inter_4,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_6,prod_cr_inter_5,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_7,prod_cr_inter_6,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_8,prod_cr_inter_7,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_9,prod_cr_inter_8,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_fairmount_line_zone_1a,prod_cr_zone_2,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_2,area_ss_commuter_rail_zone_1a,prod_cr_zone_2,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_1,prod_cr_inter_3,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_10,prod_cr_inter_8,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_1a,prod_cr_zone_3,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_2,prod_cr_inter_2,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_3,prod_cr_inter_1,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_4,prod_cr_inter_2,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_5,prod_cr_inter_3,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_6,prod_cr_inter_4,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_7,prod_cr_inter_5,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_8,prod_cr_inter_6,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_9,prod_cr_inter_7,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_fairmount_line_zone_1a,prod_cr_zone_3,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_3,area_ss_commuter_rail_zone_1a,prod_cr_zone_3,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_1,prod_cr_inter_4,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_10,prod_cr_inter_7,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_1a,prod_cr_zone_4,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_2,prod_cr_inter_3,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_3,prod_cr_inter_2,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_4,prod_cr_inter_1,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_5,prod_cr_inter_2,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_6,prod_cr_inter_3,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_7,prod_cr_inter_4,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_8,prod_cr_inter_5,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_9,prod_cr_inter_6,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_fairmount_line_zone_1a,prod_cr_zone_4,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_4,area_ss_commuter_rail_zone_1a,prod_cr_zone_4,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_1,prod_cr_inter_5,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_10,prod_cr_inter_6,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_1a,prod_cr_zone_5,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_2,prod_cr_inter_4,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_3,prod_cr_inter_3,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_4,prod_cr_inter_2,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_5,prod_cr_inter_1,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_6,prod_cr_inter_2,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_7,prod_cr_inter_3,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_8,prod_cr_inter_4,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_9,prod_cr_inter_5,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_fairmount_line_zone_1a,prod_cr_zone_5,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_5,area_ss_commuter_rail_zone_1a,prod_cr_zone_5,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_1,prod_cr_inter_6,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_10,prod_cr_inter_5,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_1a,prod_cr_zone_6,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_2,prod_cr_inter_5,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_3,prod_cr_inter_4,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_4,prod_cr_inter_3,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_5,prod_cr_inter_2,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_6,prod_cr_inter_1,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_7,prod_cr_inter_2,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_8,prod_cr_inter_3,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_9,prod_cr_inter_4,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_fairmount_line_zone_1a,prod_cr_zone_6,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_6,area_ss_commuter_rail_zone_1a,prod_cr_zone_6,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_1,prod_cr_inter_7,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_10,prod_cr_inter_4,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_1a,prod_cr_zone_7,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_2,prod_cr_inter_6,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_3,prod_cr_inter_5,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_4,prod_cr_inter_4,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_5,prod_cr_inter_3,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_6,prod_cr_inter_2,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_7,prod_cr_inter_1,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_8,prod_cr_inter_2,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_9,prod_cr_inter_3,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_fairmount_line_zone_1a,prod_cr_zone_7,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_7,area_ss_commuter_rail_zone_1a,prod_cr_zone_7,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_1,prod_cr_inter_8,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_10,prod_cr_inter_3,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_1a,prod_cr_zone_8,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_2,prod_cr_inter_7,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_3,prod_cr_inter_6,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_4,prod_cr_inter_5,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_5,prod_cr_inter_4,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_6,prod_cr_inter_3,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_7,prod_cr_inter_2,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_8,prod_cr_inter_1,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_9,prod_cr_inter_2,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_fairmount_line_zone_1a,prod_cr_zone_8,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_8,area_ss_commuter_rail_zone_1a,prod_cr_zone_8,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_1,prod_cr_inter_9,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_10,prod_cr_inter_2,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_1a,prod_cr_zone_9,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_2,prod_cr_inter_8,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_3,prod_cr_inter_7,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_4,prod_cr_inter_6,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_5,prod_cr_inter_5,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_6,prod_cr_inter_4,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_7,prod_cr_inter_3,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_8,prod_cr_inter_2,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_9,prod_cr_inter_1,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_fairmount_line_zone_1a,prod_cr_zone_9,,, +leg_commuter_rail_cash,commuter_rail,area_commuter_rail_zone_9,area_ss_commuter_rail_zone_1a,prod_cr_zone_9,,, +leg_commuter_rail_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_1,prod_cr_zone_1,,, +leg_commuter_rail_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_10,prod_cr_zone_10,,, +leg_commuter_rail_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_1a,prod_cr_zone_1a,,, +leg_commuter_rail_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_2,prod_cr_zone_2,,, +leg_commuter_rail_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_3,prod_cr_zone_3,,, +leg_commuter_rail_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_4,prod_cr_zone_4,,, +leg_commuter_rail_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_5,prod_cr_zone_5,,, +leg_commuter_rail_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_6,prod_cr_zone_6,,, +leg_commuter_rail_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_7,prod_cr_zone_7,,, +leg_commuter_rail_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_8,prod_cr_zone_8,,, +leg_commuter_rail_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_9,prod_cr_zone_9,,, +leg_commuter_rail_free,commuter_rail,area_commuter_rail_porter_zone_1a,area_commuter_rail_zone_1a,prod_free_fare,timeframe_alewife_kendall_surge,, +leg_commuter_rail_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_porter_zone_1a,prod_free_fare,timeframe_alewife_kendall_surge,, +leg_express_bus_cash,express_bus,area_route_354_downtown,area_route_354_outside_downtown,prod_express_bus,,, +leg_express_bus_cash,express_bus,area_route_354_outside_downtown,area_route_354_downtown,prod_express_bus,,, +leg_express_bus_cash,express_bus,area_route_426_downtown,area_route_426_outside_downtown,prod_express_bus,,, +leg_express_bus_cash,express_bus,area_route_426_outside_downtown,area_route_426_downtown,prod_express_bus,,, +leg_express_bus_cash,express_bus,area_route_450_downtown,area_route_450_outside_downtown,prod_express_bus,,, +leg_express_bus_cash,express_bus,area_route_450_outside_downtown,area_route_450_downtown,prod_express_bus,,, +leg_express_bus_cash,express_bus_special,,,prod_express_bus,,, +leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_1,prod_cr_zone_1,,, +leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_10,prod_cr_zone_10,,, +leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_1a,prod_cr_zone_1a,,, +leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_2,prod_cr_zone_2,,, +leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_3,prod_cr_zone_3,,, +leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_4,prod_cr_zone_4,,, +leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_5,prod_cr_zone_5,,, +leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_6,prod_cr_zone_6,,, +leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_7,prod_cr_zone_7,,, +leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_8,prod_cr_zone_8,,, +leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_9,prod_cr_zone_9,,, +leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_fairmount_line_zone_1a,prod_cr_zone_1a,,, +leg_fairmount_line_cash,commuter_rail,area_fairmount_line_zone_1a,area_ss_commuter_rail_zone_1a,prod_cr_zone_1a,,, +leg_ferry_east_boston_free,ferry_east_boston,,,prod_free_fare,timeframe_sumner_tunnel_closure,, +leg_ferry_east_boston_free,ferry_east_boston,,,prod_boat_zone_1a,timeframe_regular,, +leg_ferry_f1_cash,ferry_f1,,,prod_ferry_f1,,, +leg_ferry_f4_cash,ferry_f4,,,prod_ferry_f4,,, +leg_ferry_f6_cash,ferry_f6,,,prod_boat_zone_1a,timeframe_sumner_tunnel_closure,, +leg_ferry_f6_cash,ferry_f6,,,prod_boat_zone_1,timeframe_regular,, +leg_ferry_lynn_cash,ferry_lynn,,,prod_boat_zone_1a,timeframe_sumner_tunnel_closure,, +leg_ferry_lynn_cash,ferry_lynn,,,prod_boat_zone_2,timeframe_regular,, +leg_foxboro_event_cash,cr_foxboro,,,prod_foxboro_event_fare,,, +leg_local_bus_cash,express_bus,area_route_354_downtown,area_route_354_downtown,prod_local_bus,,, +leg_local_bus_cash,express_bus,area_route_354_outside_downtown,area_route_354_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,express_bus,area_route_426_downtown,area_route_426_downtown,prod_local_bus,,, +leg_local_bus_cash,express_bus,area_route_426_outside_downtown,area_route_426_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,express_bus,area_route_450_downtown,area_route_450_downtown,prod_local_bus,,, +leg_local_bus_cash,express_bus,area_route_450_outside_downtown,area_route_450_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_354_downtown,area_route_354_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_354_downtown,area_route_354_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_354_downtown,area_route_426_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_354_downtown,area_route_426_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_354_downtown,area_route_450_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_354_downtown,area_route_450_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_354_downtown,,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_354_outside_downtown,area_route_354_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_354_outside_downtown,area_route_354_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_354_outside_downtown,area_route_426_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_354_outside_downtown,area_route_426_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_354_outside_downtown,area_route_450_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_354_outside_downtown,area_route_450_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_354_outside_downtown,,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_426_downtown,area_route_354_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_426_downtown,area_route_354_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_426_downtown,area_route_426_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_426_downtown,area_route_426_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_426_downtown,area_route_450_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_426_downtown,area_route_450_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_426_downtown,,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_426_outside_downtown,area_route_354_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_426_outside_downtown,area_route_354_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_426_outside_downtown,area_route_426_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_426_outside_downtown,area_route_426_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_426_outside_downtown,area_route_450_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_426_outside_downtown,area_route_450_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_426_outside_downtown,,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_450_downtown,area_route_354_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_450_downtown,area_route_354_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_450_downtown,area_route_426_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_450_downtown,area_route_426_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_450_downtown,area_route_450_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_450_downtown,area_route_450_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_450_downtown,,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_450_outside_downtown,area_route_354_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_450_outside_downtown,area_route_354_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_450_outside_downtown,area_route_426_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_450_outside_downtown,area_route_426_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_450_outside_downtown,area_route_450_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_450_outside_downtown,area_route_450_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,area_route_450_outside_downtown,,prod_local_bus,,, +leg_local_bus_cash,local_bus,,area_route_354_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,,area_route_354_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,,area_route_426_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,,area_route_426_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,,area_route_450_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,,area_route_450_outside_downtown,prod_local_bus,,, +leg_local_bus_cash,local_bus,,,prod_local_bus,,, +leg_local_bus_free,local_bus_free,area_route_354_downtown,area_route_354_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_354_downtown,area_route_354_outside_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_354_downtown,area_route_426_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_354_downtown,area_route_426_outside_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_354_downtown,area_route_450_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_354_downtown,area_route_450_outside_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_354_downtown,,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_354_outside_downtown,area_route_354_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_354_outside_downtown,area_route_354_outside_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_354_outside_downtown,area_route_426_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_354_outside_downtown,area_route_426_outside_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_354_outside_downtown,area_route_450_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_354_outside_downtown,area_route_450_outside_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_354_outside_downtown,,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_426_downtown,area_route_354_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_426_downtown,area_route_354_outside_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_426_downtown,area_route_426_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_426_downtown,area_route_426_outside_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_426_downtown,area_route_450_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_426_downtown,area_route_450_outside_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_426_downtown,,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_426_outside_downtown,area_route_354_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_426_outside_downtown,area_route_354_outside_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_426_outside_downtown,area_route_426_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_426_outside_downtown,area_route_426_outside_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_426_outside_downtown,area_route_450_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_426_outside_downtown,area_route_450_outside_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_426_outside_downtown,,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_450_downtown,area_route_354_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_450_downtown,area_route_354_outside_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_450_downtown,area_route_426_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_450_downtown,area_route_426_outside_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_450_downtown,area_route_450_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_450_downtown,area_route_450_outside_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_450_downtown,,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_450_outside_downtown,area_route_354_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_450_outside_downtown,area_route_354_outside_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_450_outside_downtown,area_route_426_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_450_outside_downtown,area_route_426_outside_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_450_outside_downtown,area_route_450_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_450_outside_downtown,area_route_450_outside_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,area_route_450_outside_downtown,,prod_free_fare,,, +leg_local_bus_free,local_bus_free,,area_route_354_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,,area_route_354_outside_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,,area_route_426_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,,area_route_426_outside_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,,area_route_450_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,,area_route_450_outside_downtown,prod_free_fare,,, +leg_local_bus_free,local_bus_free,,,prod_free_fare,,, +leg_local_bus_quick_subway,express_bus,area_route_354_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,express_bus,area_route_354_outside_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,express_bus,area_route_426_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,express_bus,area_route_426_outside_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,express_bus,area_route_450_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,express_bus,area_route_450_outside_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_354_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_354_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_354_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_354_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_354_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_354_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_354_downtown,,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_354_outside_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_354_outside_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_354_outside_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_354_outside_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_354_outside_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_354_outside_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_354_outside_downtown,,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_426_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_426_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_426_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_426_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_426_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_426_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_426_downtown,,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_426_outside_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_426_outside_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_426_outside_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_426_outside_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_426_outside_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_426_outside_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_426_outside_downtown,,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_450_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_450_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_450_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_450_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_450_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_450_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_450_downtown,,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_450_outside_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_450_outside_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_450_outside_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_450_outside_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_450_outside_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_450_outside_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,area_route_450_outside_downtown,,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,,area_route_354_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,,area_route_426_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,,area_route_450_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,, +leg_local_bus_quick_subway,local_bus,,,prod_rapid_transit_quick_subway,,, +leg_local_bus_restricted_cash,local_bus_restricted,,,prod_local_bus,,, +leg_mattapan_rapid_transit_cash,m_rapid_transit,,,prod_rapid_transit_cash,,, +leg_mattapan_rapid_transit_quick_subway,m_rapid_transit,area_m_ashmont_mattapan,,prod_rapid_transit_quick_subway,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_354_downtown,area_route_354_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_354_downtown,area_route_354_outside_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_354_downtown,area_route_426_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_354_downtown,area_route_426_outside_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_354_downtown,area_route_450_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_354_downtown,area_route_450_outside_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_354_downtown,,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_354_outside_downtown,area_route_354_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_354_outside_downtown,area_route_354_outside_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_354_outside_downtown,area_route_426_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_354_outside_downtown,area_route_426_outside_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_354_outside_downtown,area_route_450_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_354_outside_downtown,area_route_450_outside_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_354_outside_downtown,,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_426_downtown,area_route_354_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_426_downtown,area_route_354_outside_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_426_downtown,area_route_426_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_426_downtown,area_route_426_outside_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_426_downtown,area_route_450_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_426_downtown,area_route_450_outside_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_426_downtown,,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_426_outside_downtown,area_route_354_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_426_outside_downtown,area_route_354_outside_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_426_outside_downtown,area_route_426_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_426_outside_downtown,area_route_426_outside_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_426_outside_downtown,area_route_450_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_426_outside_downtown,area_route_450_outside_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_426_outside_downtown,,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_450_downtown,area_route_354_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_450_downtown,area_route_354_outside_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_450_downtown,area_route_426_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_450_downtown,area_route_426_outside_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_450_downtown,area_route_450_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_450_downtown,area_route_450_outside_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_450_downtown,,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_450_outside_downtown,area_route_354_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_450_outside_downtown,area_route_354_outside_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_450_outside_downtown,area_route_426_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_450_outside_downtown,area_route_426_outside_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_450_outside_downtown,area_route_450_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_450_outside_downtown,area_route_450_outside_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,area_route_450_outside_downtown,,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,,area_route_354_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,,area_route_354_outside_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,,area_route_426_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,,area_route_426_outside_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,,area_route_450_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,,area_route_450_outside_downtown,prod_free_fare,,, +leg_rail_replacement_free,rail_replacement_bus,,,prod_free_fare,,, +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_downtown,,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_outside_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_outside_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_outside_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_outside_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_outside_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_outside_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_354_outside_downtown,,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_downtown,,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_outside_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_outside_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_outside_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_outside_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_outside_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_outside_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_426_outside_downtown,,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_downtown,,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_outside_downtown,area_route_354_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_outside_downtown,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_outside_downtown,area_route_426_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_outside_downtown,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_outside_downtown,area_route_450_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_outside_downtown,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,area_route_450_outside_downtown,,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,,area_route_354_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,,area_route_354_outside_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,,area_route_426_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,,area_route_426_outside_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,,area_route_450_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,,area_route_450_outside_downtown,prod_rapid_transit_quick_subway,,,1 +leg_rail_replacement_quick_subway,rail_replacement_bus,,,prod_rapid_transit_quick_subway,,,1 +leg_rapid_transit_cash,rapid_transit,area_green_b_west_of_kenmore,,prod_rapid_transit_cash,,, +leg_rapid_transit_cash,rapid_transit,area_green_c_west_of_kenmore,,prod_rapid_transit_cash,,, +leg_rapid_transit_cash,rapid_transit,area_green_e_west_of_symphony,,prod_rapid_transit_cash,,, +leg_rapid_transit_cash,rapid_transit,,,prod_rapid_transit_cash,,,1 +leg_rapid_transit_free,rapid_transit,area_bl,,prod_free_fare,timeframe_sumner_tunnel_closure,, +leg_rapid_transit_free,rapid_transit,area_bl_airport,,prod_free_fare,timeframe_sumner_tunnel_closure,, +leg_rapid_transit_free,rapid_transit,area_gl_govt_ctr,,prod_free_fare,timeframe_sumner_tunnel_closure,, +leg_rapid_transit_free,rapid_transit,area_ol_state,,prod_free_fare,timeframe_sumner_tunnel_closure,, +leg_rapid_transit_free,rapid_transit,,,prod_free_fare,,,1 +leg_rapid_transit_quick_subway,rapid_transit,area_bl,,prod_rapid_transit_quick_subway,timeframe_regular,, +leg_rapid_transit_quick_subway,rapid_transit,area_gl_govt_ctr,,prod_rapid_transit_quick_subway,timeframe_regular,, +leg_rapid_transit_quick_subway,rapid_transit,area_ol_state,,prod_rapid_transit_quick_subway,timeframe_regular,, +leg_rapid_transit_quick_subway,rapid_transit,,,prod_rapid_transit_quick_subway,timeframe_regular,, +leg_sl_rapid_transit_cash,sl_rapid_transit,area_sl3_north_of_airport,area_sl3_north_of_airport,prod_rapid_transit_cash,timeframe_regular,, +leg_sl_rapid_transit_cash,sl_rapid_transit,area_sl3_north_of_airport,area_sl_airport,prod_rapid_transit_cash,timeframe_regular,, +leg_sl_rapid_transit_cash,sl_rapid_transit,area_sl3_north_of_airport,,prod_rapid_transit_cash,timeframe_regular,, +leg_sl_rapid_transit_cash,sl_rapid_transit,area_sl_silver_line_way,area_sl3_north_of_airport,prod_rapid_transit_cash,timeframe_regular,, +leg_sl_rapid_transit_cash,sl_rapid_transit,area_sl_silver_line_way,area_sl_airport,prod_rapid_transit_cash,timeframe_regular,, +leg_sl_rapid_transit_cash,sl_rapid_transit,area_sl_silver_line_way,,prod_rapid_transit_cash,,, +leg_sl_rapid_transit_cash,sl_rapid_transit,area_sl_south_station,area_sl3_north_of_airport,prod_rapid_transit_cash,,,1 +leg_sl_rapid_transit_cash,sl_rapid_transit,area_sl_south_station,area_sl_airport,prod_rapid_transit_cash,,,1 +leg_sl_rapid_transit_cash,sl_rapid_transit,area_sl_south_station,,prod_rapid_transit_cash,,,1 +leg_sl_rapid_transit_cash,sl_rapid_transit,,,prod_rapid_transit_cash,,, +leg_sl_rapid_transit_free,sl_rapid_transit,area_sl3_north_of_airport,area_sl3_north_of_airport,prod_free_fare,timeframe_sumner_tunnel_closure,, +leg_sl_rapid_transit_free,sl_rapid_transit,area_sl3_north_of_airport,area_sl_airport,prod_free_fare,timeframe_sumner_tunnel_closure,, +leg_sl_rapid_transit_free,sl_rapid_transit,area_sl3_north_of_airport,,prod_free_fare,timeframe_sumner_tunnel_closure,, +leg_sl_rapid_transit_free,sl_rapid_transit,area_sl_airport,area_sl3_north_of_airport,prod_free_fare,timeframe_sumner_tunnel_closure,, +leg_sl_rapid_transit_free,sl_rapid_transit,area_sl_airport,,prod_free_fare,timeframe_sumner_tunnel_closure,, +leg_sl_rapid_transit_free,sl_rapid_transit,area_sl_logan_terminal,,prod_free_fare,,, +leg_sl_rapid_transit_free,sl_rapid_transit,area_sl_silver_line_way,area_sl3_north_of_airport,prod_free_fare,timeframe_sumner_tunnel_closure,, +leg_sl_rapid_transit_free,sl_rapid_transit,area_sl_silver_line_way,area_sl_airport,prod_free_fare,timeframe_sumner_tunnel_closure,, +leg_sl_rapid_transit_quick_subway,sl_rapid_transit,area_sl_airport,area_sl3_north_of_airport,prod_rapid_transit_quick_subway,timeframe_regular,, +leg_sl_rapid_transit_quick_subway,sl_rapid_transit,area_sl_airport,,prod_rapid_transit_quick_subway,timeframe_regular,, +leg_sl_rapid_transit_quick_subway,sl_rapid_transit,area_sl_courthouse,area_sl3_north_of_airport,prod_rapid_transit_quick_subway,,, +leg_sl_rapid_transit_quick_subway,sl_rapid_transit,area_sl_courthouse,area_sl_airport,prod_rapid_transit_quick_subway,,, +leg_sl_rapid_transit_quick_subway,sl_rapid_transit,area_sl_courthouse,,prod_rapid_transit_quick_subway,,, +leg_sl_rapid_transit_quick_subway,sl_rapid_transit,area_sl_south_station,area_sl3_north_of_airport,prod_rapid_transit_quick_subway,,, +leg_sl_rapid_transit_quick_subway,sl_rapid_transit,area_sl_south_station,area_sl_airport,prod_rapid_transit_quick_subway,,, +leg_sl_rapid_transit_quick_subway,sl_rapid_transit,area_sl_south_station,,prod_rapid_transit_quick_subway,,, +leg_sl_rapid_transit_quick_subway,sl_rapid_transit,area_sl_world_trade_center,area_sl3_north_of_airport,prod_rapid_transit_quick_subway,,, +leg_sl_rapid_transit_quick_subway,sl_rapid_transit,area_sl_world_trade_center,area_sl_airport,prod_rapid_transit_quick_subway,,, +leg_sl_rapid_transit_quick_subway,sl_rapid_transit,area_sl_world_trade_center,,prod_rapid_transit_quick_subway,,, +leg_ss_fairmount_line_zone_1a_cash,commuter_rail,area_ss_commuter_rail_zone_1a,area_fairmount_line_zone_1a,prod_cr_zone_1a,,, +leg_ss_rapid_transit_cash,rapid_transit,area_red_south_station,,prod_rapid_transit_cash,,,1 +leg_ss_rapid_transit_free,rapid_transit,area_red_south_station,,prod_free_fare,,,1 +leg_ss_rapid_transit_quick_subway,rapid_transit,area_red_south_station,,prod_rapid_transit_quick_subway,,, +leg_systemwide_free,cape_flyer,area_cf_zone_buzzards,area_cf_zone_hyannis,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,cape_flyer,area_cf_zone_buzzards,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,cape_flyer,area_cf_zone_buzzards,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,cape_flyer,area_cf_zone_buzzards,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,cape_flyer,area_cf_zone_hyannis,area_cf_zone_buzzards,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,cape_flyer,area_cf_zone_hyannis,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,cape_flyer,area_cf_zone_hyannis,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,cape_flyer,area_cf_zone_hyannis,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,cape_flyer,area_cf_zone_hyannis,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,cape_flyer,area_commuter_rail_zone_1a,area_cf_zone_buzzards,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,cape_flyer,area_commuter_rail_zone_1a,area_cf_zone_hyannis,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,cape_flyer,area_commuter_rail_zone_2,area_cf_zone_buzzards,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,cape_flyer,area_commuter_rail_zone_2,area_cf_zone_hyannis,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,cape_flyer,area_commuter_rail_zone_4,area_cf_zone_buzzards,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,cape_flyer,area_commuter_rail_zone_4,area_cf_zone_hyannis,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,cape_flyer,area_commuter_rail_zone_8,area_cf_zone_hyannis,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_10,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_1a,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_2,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_3,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_4,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_5,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_6,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_7,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_8,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_commuter_rail_zone_9,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_fairmount_line_zone_1a,area_ss_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_1,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_10,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_2,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_3,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_4,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_5,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_6,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_7,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_8,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_commuter_rail_zone_9,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,commuter_rail,area_ss_commuter_rail_zone_1a,area_fairmount_line_zone_1a,prod_free_fare,timeframe_systemwide_free,, +leg_systemwide_free,cr_foxboro,,,prod_free_fare,timeframe_systemwide_free,, diff --git a/src/test/resources/fake-agency-with-fares-v2/fare_media.txt b/src/test/resources/fake-agency-with-fares-v2/fare_media.txt new file mode 100644 index 000000000..a695de634 --- /dev/null +++ b/src/test/resources/fake-agency-with-fares-v2/fare_media.txt @@ -0,0 +1,5 @@ +fare_media_id,fare_media_name,fare_media_type +cash,Cash,0 +credit_debit,Credit/debit card,0 +charlieticket,CharlieTicket,1 +mticket,mTicket app,4 diff --git a/src/test/resources/fake-agency-with-fares-v2/fare_products.txt b/src/test/resources/fake-agency-with-fares-v2/fare_products.txt new file mode 100644 index 000000000..1138f4591 --- /dev/null +++ b/src/test/resources/fake-agency-with-fares-v2/fare_products.txt @@ -0,0 +1,97 @@ +fare_product_id,fare_product_name,fare_media_id,amount,currency +prod_boat_zone_1,Ferry Zone 1 one-way fare,cash,6.50,USD +prod_boat_zone_1,Ferry Zone 1 one-way fare,credit_debit,6.50,USD +prod_boat_zone_1,Ferry Zone 1 one-way fare,mticket,6.50,USD +prod_boat_zone_1a,Ferry Zone 1A one-way fare,cash,2.40,USD +prod_boat_zone_1a,Ferry Zone 1A one-way fare,credit_debit,2.40,USD +prod_boat_zone_1a,Ferry Zone 1A one-way fare,mticket,2.40,USD +prod_boat_zone_2,Ferry Zone 2 one-way fare,cash,7.00,USD +prod_boat_zone_2,Ferry Zone 2 one-way fare,credit_debit,7.00,USD +prod_boat_zone_2,Ferry Zone 2 one-way fare,mticket,7.00,USD +prod_cape_buzzards_hyannis_fare,CapeFLYER Middleborough/Lakeville one-way fare,cash,5.00,USD +prod_cape_buzzards_hyannis_fare,CapeFLYER Middleborough/Lakeville one-way fare,credit_debit,5.00,USD +prod_cape_buzzards_hyannis_fare,CapeFLYER Middleborough/Lakeville one-way fare,mticket,5.00,USD +prod_cape_sbb_buzzards_fare,CapeFLYER Bourne one-way fare,cash,20.00,USD +prod_cape_sbb_buzzards_fare,CapeFLYER Bourne one-way fare,credit_debit,20.00,USD +prod_cape_sbb_buzzards_fare,CapeFLYER Bourne one-way fare,mticket,20.00,USD +prod_cape_sbb_hyannis_fare,CapeFLYER Hyannis one-way fare,cash,22.00,USD +prod_cape_sbb_hyannis_fare,CapeFLYER Hyannis one-way fare,credit_debit,22.00,USD +prod_cape_sbb_hyannis_fare,CapeFLYER Hyannis one-way fare,mticket,22.00,USD +prod_cr_inter_1,Commuter Rail Interzone 1 one-way fare,cash,2.75,USD +prod_cr_inter_1,Commuter Rail Interzone 1 one-way fare,credit_debit,2.75,USD +prod_cr_inter_1,Commuter Rail Interzone 1 one-way fare,mticket,2.75,USD +prod_cr_inter_10,Commuter Rail Interzone 10 one-way fare,cash,7.25,USD +prod_cr_inter_10,Commuter Rail Interzone 10 one-way fare,credit_debit,7.25,USD +prod_cr_inter_10,Commuter Rail Interzone 10 one-way fare,mticket,7.25,USD +prod_cr_inter_2,Commuter Rail Interzone 2 one-way fare,cash,3.25,USD +prod_cr_inter_2,Commuter Rail Interzone 2 one-way fare,credit_debit,3.25,USD +prod_cr_inter_2,Commuter Rail Interzone 2 one-way fare,mticket,3.25,USD +prod_cr_inter_3,Commuter Rail Interzone 3 one-way fare,cash,3.50,USD +prod_cr_inter_3,Commuter Rail Interzone 3 one-way fare,credit_debit,3.50,USD +prod_cr_inter_3,Commuter Rail Interzone 3 one-way fare,mticket,3.50,USD +prod_cr_inter_4,Commuter Rail Interzone 4 one-way fare,cash,4.25,USD +prod_cr_inter_4,Commuter Rail Interzone 4 one-way fare,credit_debit,4.25,USD +prod_cr_inter_4,Commuter Rail Interzone 4 one-way fare,mticket,4.25,USD +prod_cr_inter_5,Commuter Rail Interzone 5 one-way fare,cash,4.75,USD +prod_cr_inter_5,Commuter Rail Interzone 5 one-way fare,credit_debit,4.75,USD +prod_cr_inter_5,Commuter Rail Interzone 5 one-way fare,mticket,4.75,USD +prod_cr_inter_6,Commuter Rail Interzone 6 one-way fare,cash,5.25,USD +prod_cr_inter_6,Commuter Rail Interzone 6 one-way fare,credit_debit,5.25,USD +prod_cr_inter_6,Commuter Rail Interzone 6 one-way fare,mticket,5.25,USD +prod_cr_inter_7,Commuter Rail Interzone 7 one-way fare,cash,5.75,USD +prod_cr_inter_7,Commuter Rail Interzone 7 one-way fare,credit_debit,5.75,USD +prod_cr_inter_7,Commuter Rail Interzone 7 one-way fare,mticket,5.75,USD +prod_cr_inter_8,Commuter Rail Interzone 8 one-way fare,cash,6.25,USD +prod_cr_inter_8,Commuter Rail Interzone 8 one-way fare,credit_debit,6.25,USD +prod_cr_inter_8,Commuter Rail Interzone 8 one-way fare,mticket,6.25,USD +prod_cr_inter_9,Commuter Rail Interzone 9 one-way fare,cash,6.75,USD +prod_cr_inter_9,Commuter Rail Interzone 9 one-way fare,credit_debit,6.75,USD +prod_cr_inter_9,Commuter Rail Interzone 9 one-way fare,mticket,6.75,USD +prod_cr_zone_1,Commuter Rail Zone 1 one-way fare,cash,6.50,USD +prod_cr_zone_1,Commuter Rail Zone 1 one-way fare,credit_debit,6.50,USD +prod_cr_zone_1,Commuter Rail Zone 1 one-way fare,mticket,6.50,USD +prod_cr_zone_10,Commuter Rail Zone 10 one-way fare,cash,13.25,USD +prod_cr_zone_10,Commuter Rail Zone 10 one-way fare,credit_debit,13.25,USD +prod_cr_zone_10,Commuter Rail Zone 10 one-way fare,mticket,13.25,USD +prod_cr_zone_1a,Commuter Rail Zone 1A one-way fare,cash,2.40,USD +prod_cr_zone_1a,Commuter Rail Zone 1A one-way fare,credit_debit,2.40,USD +prod_cr_zone_1a,Commuter Rail Zone 1A one-way fare,mticket,2.40,USD +prod_cr_zone_2,Commuter Rail Zone 2 one-way fare,cash,7.00,USD +prod_cr_zone_2,Commuter Rail Zone 2 one-way fare,credit_debit,7.00,USD +prod_cr_zone_2,Commuter Rail Zone 2 one-way fare,mticket,7.00,USD +prod_cr_zone_3,Commuter Rail Zone 3 one-way fare,cash,8.00,USD +prod_cr_zone_3,Commuter Rail Zone 3 one-way fare,credit_debit,8.00,USD +prod_cr_zone_3,Commuter Rail Zone 3 one-way fare,mticket,8.00,USD +prod_cr_zone_4,Commuter Rail Zone 4 one-way fare,cash,8.75,USD +prod_cr_zone_4,Commuter Rail Zone 4 one-way fare,credit_debit,8.75,USD +prod_cr_zone_4,Commuter Rail Zone 4 one-way fare,mticket,8.75,USD +prod_cr_zone_5,Commuter Rail Zone 5 one-way fare,cash,9.75,USD +prod_cr_zone_5,Commuter Rail Zone 5 one-way fare,credit_debit,9.75,USD +prod_cr_zone_5,Commuter Rail Zone 5 one-way fare,mticket,9.75,USD +prod_cr_zone_6,Commuter Rail Zone 6 one-way fare,cash,10.50,USD +prod_cr_zone_6,Commuter Rail Zone 6 one-way fare,credit_debit,10.50,USD +prod_cr_zone_6,Commuter Rail Zone 6 one-way fare,mticket,10.50,USD +prod_cr_zone_7,Commuter Rail Zone 7 one-way fare,cash,11.00,USD +prod_cr_zone_7,Commuter Rail Zone 7 one-way fare,credit_debit,11.00,USD +prod_cr_zone_7,Commuter Rail Zone 7 one-way fare,mticket,11.00,USD +prod_cr_zone_8,Commuter Rail Zone 8 one-way fare,cash,12.25,USD +prod_cr_zone_8,Commuter Rail Zone 8 one-way fare,credit_debit,12.25,USD +prod_cr_zone_8,Commuter Rail Zone 8 one-way fare,mticket,12.25,USD +prod_cr_zone_9,Commuter Rail Zone 9 one-way fare,cash,12.75,USD +prod_cr_zone_9,Commuter Rail Zone 9 one-way fare,credit_debit,12.75,USD +prod_cr_zone_9,Commuter Rail Zone 9 one-way fare,mticket,12.75,USD +prod_express_bus,Express Bus cash fare,cash,4.25,USD +prod_ferry_east_boston,East Boston Ferry one-way fare,mticket,2.40,USD +prod_ferry_f1,Hingham/Hull Ferry one-way fare,cash,9.75,USD +prod_ferry_f1,Hingham/Hull Ferry one-way fare,credit_debit,9.75,USD +prod_ferry_f1,Hingham/Hull Ferry one-way fare,mticket,9.75,USD +prod_ferry_f4,Charlestown Ferry one-way fare,cash,3.70,USD +prod_ferry_f4,Charlestown Ferry one-way fare,credit_debit,3.70,USD +prod_ferry_f4,Charlestown Ferry one-way fare,mticket,3.70,USD +prod_foxboro_event_fare,Foxboro Event Service round-trip fare,cash,20.00,USD +prod_foxboro_event_fare,Foxboro Event Service round-trip fare,credit_debit,20.00,USD +prod_foxboro_event_fare,Foxboro Event Service round-trip fare,mticket,20.00,USD +prod_free_fare,Free fare,,0.00,USD +prod_local_bus,Local Bus cash fare,cash,1.70,USD +prod_rapid_transit_cash,Subway cash fare,cash,2.40,USD +prod_rapid_transit_quick_subway,Subway Quick Ticket,charlieticket,2.40,USD diff --git a/src/test/resources/fake-agency-with-fares-v2/fare_transfer_rules.txt b/src/test/resources/fake-agency-with-fares-v2/fare_transfer_rules.txt new file mode 100644 index 000000000..0286fa015 --- /dev/null +++ b/src/test/resources/fake-agency-with-fares-v2/fare_transfer_rules.txt @@ -0,0 +1,38 @@ +from_leg_group_id,to_leg_group_id,transfer_count,duration_limit,duration_limit_type,fare_transfer_type,fare_product_id,filter_fare_product_id,fare_media_behavior,fare_product_behavior +leg_airport_rapid_transit_quick_subway,leg_local_bus_quick_subway,,7200,1,0,prod_rapid_transit_quick_subway,prod_rapid_transit_quick_subway,0,1 +leg_airport_rapid_transit_quick_subway,leg_rapid_transit_quick_subway,,,,0,prod_rapid_transit_quick_subway,prod_rapid_transit_quick_subway,0,1 +leg_mattapan_rapid_transit_quick_subway,leg_local_bus_quick_subway,,7200,1,0,prod_rapid_transit_quick_subway,prod_rapid_transit_quick_subway,0,1 +leg_mattapan_rapid_transit_quick_subway,leg_rail_replacement_quick_subway,,,,0,,prod_rapid_transit_quick_subway,0,1 +leg_mattapan_rapid_transit_quick_subway,leg_rapid_transit_quick_subway,,7200,1,0,,prod_rapid_transit_quick_subway,0,1 +leg_mattapan_rapid_transit_quick_subway,leg_ss_rapid_transit_quick_subway,,7200,1,0,,prod_rapid_transit_quick_subway,0,1 +leg_rail_replacement_quick_subway,leg_airport_rapid_transit_quick_subway,,,,0,,prod_rapid_transit_quick_subway,0,1 +leg_rail_replacement_quick_subway,leg_local_bus_quick_subway,,7200,1,0,,prod_rapid_transit_quick_subway,0,1 +leg_rail_replacement_quick_subway,leg_mattapan_rapid_transit_quick_subway,,,,0,,prod_rapid_transit_quick_subway,0,1 +leg_rail_replacement_quick_subway,leg_rail_replacement_quick_subway,-1,,,0,,prod_rapid_transit_quick_subway,0,1 +leg_rail_replacement_quick_subway,leg_rapid_transit_quick_subway,,,,0,,prod_rapid_transit_quick_subway,0,1 +leg_rail_replacement_quick_subway,leg_sl_rapid_transit_quick_subway,,,,0,,prod_rapid_transit_quick_subway,0,1 +leg_rail_replacement_quick_subway,leg_ss_rapid_transit_quick_subway,,,,0,,prod_rapid_transit_quick_subway,0,1 +leg_rapid_transit_cash,leg_rapid_transit_cash,-1,,,0,,prod_rapid_transit_cash,0,1 +leg_rapid_transit_cash,leg_sl_rapid_transit_cash,,,,0,,prod_rapid_transit_cash,0,1 +leg_rapid_transit_free,leg_rapid_transit_free,-1,,,0,,prod_free_fare,0,1 +leg_rapid_transit_free,leg_sl_rapid_transit_free,,,,0,,prod_free_fare,0,1 +leg_rapid_transit_quick_subway,leg_airport_rapid_transit_quick_subway,,7200,1,0,,prod_rapid_transit_quick_subway,0,1 +leg_rapid_transit_quick_subway,leg_local_bus_quick_subway,,7200,1,0,,prod_rapid_transit_quick_subway,0,1 +leg_rapid_transit_quick_subway,leg_mattapan_rapid_transit_quick_subway,,7200,1,0,,prod_rapid_transit_quick_subway,0,1 +leg_rapid_transit_quick_subway,leg_rail_replacement_quick_subway,,,,0,,prod_rapid_transit_quick_subway,0,1 +leg_rapid_transit_quick_subway,leg_rapid_transit_quick_subway,-1,,,0,,prod_rapid_transit_quick_subway,0,1 +leg_rapid_transit_quick_subway,leg_sl_rapid_transit_quick_subway,,,,0,,prod_rapid_transit_quick_subway,0,1 +leg_rapid_transit_quick_subway,leg_ss_rapid_transit_quick_subway,,7200,1,0,,prod_rapid_transit_quick_subway,0,1 +leg_sl_rapid_transit_cash,leg_ss_rapid_transit_cash,,,,0,,prod_rapid_transit_cash,0,1 +leg_sl_rapid_transit_free,leg_ss_rapid_transit_free,,,,0,,prod_free_fare,0,1 +leg_sl_rapid_transit_quick_subway,leg_airport_rapid_transit_quick_subway,,7200,1,0,,prod_rapid_transit_quick_subway,0,1 +leg_sl_rapid_transit_quick_subway,leg_local_bus_quick_subway,,7200,1,0,,prod_rapid_transit_quick_subway,0,1 +leg_sl_rapid_transit_quick_subway,leg_mattapan_rapid_transit_quick_subway,,7200,1,0,,prod_rapid_transit_quick_subway,0,1 +leg_sl_rapid_transit_quick_subway,leg_rail_replacement_quick_subway,,,,0,,prod_rapid_transit_quick_subway,0,1 +leg_sl_rapid_transit_quick_subway,leg_ss_rapid_transit_quick_subway,,,,0,,prod_rapid_transit_quick_subway,0,1 +leg_ss_rapid_transit_cash,leg_rapid_transit_cash,,,,0,,prod_rapid_transit_cash,0,1 +leg_ss_rapid_transit_free,leg_rapid_transit_free,,,,0,,prod_free_fare,0,1 +leg_ss_rapid_transit_quick_subway,leg_local_bus_quick_subway,,7200,1,0,,prod_rapid_transit_quick_subway,0,1 +leg_ss_rapid_transit_quick_subway,leg_mattapan_rapid_transit_quick_subway,,7200,1,0,,prod_rapid_transit_quick_subway,0,1 +leg_ss_rapid_transit_quick_subway,leg_rail_replacement_quick_subway,,,,0,,prod_rapid_transit_quick_subway,0,1 +leg_ss_rapid_transit_quick_subway,leg_rapid_transit_quick_subway,,,,0,,prod_rapid_transit_quick_subway,0,1 diff --git a/src/test/resources/fake-agency-with-fares-v2/feed_info.txt b/src/test/resources/fake-agency-with-fares-v2/feed_info.txt new file mode 100644 index 000000000..ceac60810 --- /dev/null +++ b/src/test/resources/fake-agency-with-fares-v2/feed_info.txt @@ -0,0 +1,2 @@ +feed_id,feed_publisher_name,feed_publisher_url,feed_lang,feed_version +fake_transit,Conveyal,http://www.conveyal.com,en,1.0 \ No newline at end of file diff --git a/src/test/resources/fake-agency-with-fares-v2/networks.txt b/src/test/resources/fake-agency-with-fares-v2/networks.txt new file mode 100644 index 000000000..47a67eaad --- /dev/null +++ b/src/test/resources/fake-agency-with-fares-v2/networks.txt @@ -0,0 +1,2 @@ +network_id,network_name +1,Forbidden because network id is defined in routes \ No newline at end of file diff --git a/src/test/resources/fake-agency-with-fares-v2/route_networks.txt b/src/test/resources/fake-agency-with-fares-v2/route_networks.txt new file mode 100644 index 000000000..d2f8f9a9d --- /dev/null +++ b/src/test/resources/fake-agency-with-fares-v2/route_networks.txt @@ -0,0 +1,2 @@ +network_id,route_id +1,1 \ No newline at end of file diff --git a/src/test/resources/fake-agency-with-fares-v2/routes.txt b/src/test/resources/fake-agency-with-fares-v2/routes.txt new file mode 100644 index 000000000..dd6a30eaf --- /dev/null +++ b/src/test/resources/fake-agency-with-fares-v2/routes.txt @@ -0,0 +1,2 @@ +route_id,agency_id,route_short_name,route_long_name,route_desc,route_type,route_url,route_color,route_text_color,route_sort_order,route_fare_class,line_id,listed_route,network_id +1,1,,RL,RT,1,https://www.mbta.com/schedules/Red,DA291C,FFFFFF,10010,Rapid Transit,line-Red,,rapid_transit \ No newline at end of file diff --git a/src/test/resources/fake-agency-with-fares-v2/shapes.txt b/src/test/resources/fake-agency-with-fares-v2/shapes.txt new file mode 100644 index 000000000..3f2e3fd13 --- /dev/null +++ b/src/test/resources/fake-agency-with-fares-v2/shapes.txt @@ -0,0 +1,8 @@ +shape_id,shape_pt_lat,shape_pt_lon,shape_pt_sequence,shape_dist_traveled +5820f377-f947-4728-ac29-ac0102cbc34e,37.0612132,-122.0074332,1,0.0000000 +5820f377-f947-4728-ac29-ac0102cbc34e,37.0611720,-122.0075000,2,7.4997067 +5820f377-f947-4728-ac29-ac0102cbc34e,37.0613590,-122.0076830,3,33.8739075 +5820f377-f947-4728-ac29-ac0102cbc34e,37.0608780,-122.0082780,4,109.0402932 +5820f377-f947-4728-ac29-ac0102cbc34e,37.0603590,-122.0088280,5,184.6078298 +5820f377-f947-4728-ac29-ac0102cbc34e,37.0597610,-122.0093540,6,265.8053023 +5820f377-f947-4728-ac29-ac0102cbc34e,37.0590660,-122.0099190,7,357.8617018 diff --git a/src/test/resources/fake-agency-with-fares-v2/stop_areas.txt b/src/test/resources/fake-agency-with-fares-v2/stop_areas.txt new file mode 100644 index 000000000..85cbda304 --- /dev/null +++ b/src/test/resources/fake-agency-with-fares-v2/stop_areas.txt @@ -0,0 +1,848 @@ +stop_id,area_id +4u6g,area_route_426_downtown +4u6g,area_route_450_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_sl_logan_terminal +4u6g,area_sl_logan_terminal +4u6g,area_sl_logan_terminal +4u6g,area_sl_logan_terminal +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_sl_logan_terminal +4u6g,area_route_426_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_downtown +4u6g,area_route_354_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_ol_state +4u6g,area_ol_state +4u6g,area_bl +4u6g,area_bl +4u6g,area_bl +4u6g,area_bl +4u6g,area_bl +4u6g,area_bl +4u6g,area_bl +4u6g,area_bl +4u6g,area_bl +4u6g,area_bl_airport +4u6g,area_bl_airport +4u6g,area_bl +4u6g,area_bl +4u6g,area_bl +4u6g,area_bl +4u6g,area_bl +4u6g,area_bl +4u6g,area_bl +4u6g,area_bl +4u6g,area_bl +4u6g,area_bl +4u6g,area_bl +4u6g,area_bl +4u6g,area_red_south_station +4u6g,area_red_south_station +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_green_b_west_of_kenmore +4u6g,area_gl_govt_ctr +4u6g,area_gl_govt_ctr +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_c_west_of_kenmore +4u6g,area_green_e_west_of_symphony +4u6g,area_green_e_west_of_symphony +4u6g,area_green_e_west_of_symphony +4u6g,area_green_e_west_of_symphony +4u6g,area_green_e_west_of_symphony +4u6g,area_green_e_west_of_symphony +4u6g,area_green_e_west_of_symphony +4u6g,area_green_e_west_of_symphony +4u6g,area_green_e_west_of_symphony +4u6g,area_green_e_west_of_symphony +4u6g,area_green_e_west_of_symphony +4u6g,area_green_e_west_of_symphony +4u6g,area_green_e_west_of_symphony +4u6g,area_green_e_west_of_symphony +4u6g,area_green_e_west_of_symphony +4u6g,area_green_e_west_of_symphony +4u6g,area_green_e_west_of_symphony +4u6g,area_m_ashmont_mattapan +4u6g,area_m_ashmont_mattapan +4u6g,area_m_ashmont_mattapan +4u6g,area_bl +4u6g,area_sl_airport +4u6g,area_sl_airport +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_450_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_route_426_outside_downtown +4u6g,area_sl_south_station +4u6g,area_sl_courthouse +4u6g,area_sl_world_trade_center +4u6g,area_sl_silver_line_way +4u6g,area_sl_world_trade_center +4u6g,area_sl_courthouse +4u6g,area_sl_south_station +4u6g,area_sl_silver_line_way +4u6g,area_sl3_north_of_airport +4u6g,area_sl3_north_of_airport +4u6g,area_sl3_north_of_airport +4u6g,area_sl3_north_of_airport +4u6g,area_sl3_north_of_airport +4u6g,area_sl3_north_of_airport +4u6g,area_sl3_north_of_airport +4u6g,area_sl3_north_of_airport +4u6g,area_route_426_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_route_426_downtown +4u6g,area_route_450_downtown +4u6g,area_route_354_outside_downtown +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_cf_zone_buzzards +4u6g,area_cf_zone_buzzards +4u6g,area_cf_zone_buzzards +4u6g,area_cf_zone_hyannis +4u6g,area_commuter_rail_zone_2 +4u6g,area_fairmount_line_zone_1a +4u6g,area_fairmount_line_zone_1a +4u6g,area_fairmount_line_zone_1a +4u6g,area_fairmount_line_zone_1a +4u6g,area_fairmount_line_zone_1a +4u6g,area_fairmount_line_zone_1a +4u6g,area_fairmount_line_zone_1a +4u6g,area_fairmount_line_zone_1a +4u6g,area_fairmount_line_zone_1a +4u6g,area_fairmount_line_zone_1a +4u6g,area_fairmount_line_zone_1a +4u6g,area_fairmount_line_zone_1a +4u6g,area_fairmount_line_zone_1a +4u6g,area_fairmount_line_zone_1a +4u6g,area_fairmount_line_zone_1a +4u6g,area_fairmount_line_zone_1a +4u6g,area_fairmount_line_zone_1a +4u6g,area_fairmount_line_zone_1a +4u6g,area_fairmount_line_zone_1a +4u6g,area_fairmount_line_zone_1a +4u6g,area_fairmount_line_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_5 +4u6g,area_commuter_rail_zone_6 +4u6g,area_commuter_rail_zone_6 +4u6g,area_commuter_rail_porter_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_porter_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_porter_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_5 +4u6g,area_commuter_rail_zone_5 +4u6g,area_commuter_rail_zone_5 +4u6g,area_commuter_rail_zone_5 +4u6g,area_commuter_rail_zone_5 +4u6g,area_commuter_rail_zone_5 +4u6g,area_commuter_rail_zone_6 +4u6g,area_commuter_rail_zone_6 +4u6g,area_commuter_rail_zone_6 +4u6g,area_commuter_rail_zone_7 +4u6g,area_commuter_rail_zone_7 +4u6g,area_commuter_rail_zone_7 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_sumner_tunnel_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_5 +4u6g,area_commuter_rail_zone_6 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_5 +4u6g,area_commuter_rail_zone_6 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_10 +4u6g,area_commuter_rail_zone_9 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_7 +4u6g,area_commuter_rail_zone_7 +4u6g,area_commuter_rail_zone_7 +4u6g,area_commuter_rail_zone_7 +4u6g,area_commuter_rail_zone_7 +4u6g,area_commuter_rail_zone_7 +4u6g,area_commuter_rail_zone_6 +4u6g,area_commuter_rail_zone_6 +4u6g,area_commuter_rail_zone_6 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_ss_commuter_rail_zone_1a +4u6g,area_ss_commuter_rail_zone_1a +4u6g,area_ss_commuter_rail_zone_1a +4u6g,area_ss_commuter_rail_zone_1a +4u6g,area_ss_commuter_rail_zone_1a +4u6g,area_ss_commuter_rail_zone_1a +4u6g,area_ss_commuter_rail_zone_1a +4u6g,area_ss_commuter_rail_zone_1a +4u6g,area_ss_commuter_rail_zone_1a +4u6g,area_ss_commuter_rail_zone_1a +4u6g,area_ss_commuter_rail_zone_1a +4u6g,area_ss_commuter_rail_zone_1a +4u6g,area_ss_commuter_rail_zone_1a +4u6g,area_ss_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_5 +4u6g,area_commuter_rail_zone_5 +4u6g,area_commuter_rail_zone_5 +4u6g,area_commuter_rail_zone_6 +4u6g,area_commuter_rail_zone_6 +4u6g,area_commuter_rail_zone_6 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_5 +4u6g,area_commuter_rail_zone_6 +4u6g,area_commuter_rail_zone_7 +4u6g,area_commuter_rail_zone_7 +4u6g,area_commuter_rail_zone_7 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_5 +4u6g,area_commuter_rail_zone_5 +4u6g,area_commuter_rail_zone_5 +4u6g,area_commuter_rail_zone_6 +4u6g,area_commuter_rail_zone_6 +4u6g,area_commuter_rail_zone_6 +4u6g,area_commuter_rail_zone_6 +4u6g,area_commuter_rail_zone_6 +4u6g,area_commuter_rail_zone_6 +4u6g,area_commuter_rail_zone_7 +4u6g,area_commuter_rail_zone_7 +4u6g,area_commuter_rail_zone_7 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_8 +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1a +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_1 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_2 +4u6g,area_commuter_rail_zone_3 +4u6g,area_commuter_rail_zone_4 +4u6g,area_commuter_rail_zone_5 +4u6g,area_commuter_rail_zone_6 +4u6g,area_commuter_rail_zone_7 +4u6g,area_commuter_rail_zone_7 +4u6g,area_commuter_rail_zone_7 +4u6g,area_commuter_rail_zone_7 +4u6g,area_commuter_rail_zone_7 +4u6g,area_commuter_rail_zone_7 \ No newline at end of file diff --git a/src/test/resources/fake-agency-with-fares-v2/stop_times.txt b/src/test/resources/fake-agency-with-fares-v2/stop_times.txt new file mode 100644 index 000000000..1cfefaa49 --- /dev/null +++ b/src/test/resources/fake-agency-with-fares-v2/stop_times.txt @@ -0,0 +1,7 @@ +trip_id,arrival_time,departure_time,stop_id,stop_sequence,stop_headsign,pickup_type,drop_off_type,shape_dist_traveled,timepoint +a30277f8-e50a-4a85-9141-b1e0da9d429d,07:00:00,07:00:00,4u6g,1,Test stop headsign,0,0,0.0000000, +a30277f8-e50a-4a85-9141-b1e0da9d429d,07:01:00,07:01:00,johv,2,Test stop headsign 2,0,0,341.4491961, +frequency-trip,08:00:00,08:00:00,4u6g,1,Test stop headsign frequency trip,0,0,0.0000000, +frequency-trip,08:29:00,08:29:00,1234,2,Test stop headsign frequency trip 2,0,0,341.4491961, +calendar-date-trip,08:00:00,08:00:00,4u6g,1,Test stop headsign calendar date trip,0,0,0.0000000, +calendar-date-trip,08:29:00,08:29:00,1234,2,Test stop headsign calendar date trip 2,0,0,341.4491961, \ No newline at end of file diff --git a/src/test/resources/fake-agency-with-fares-v2/stops.txt b/src/test/resources/fake-agency-with-fares-v2/stops.txt new file mode 100644 index 000000000..ba368047e --- /dev/null +++ b/src/test/resources/fake-agency-with-fares-v2/stops.txt @@ -0,0 +1,5 @@ +stop_id,stop_code,stop_name,stop_desc,stop_lat,stop_lon,zone_id,stop_url,location_type,parent_station,stop_timezone,wheelchair_boarding +4u6g,,Butler Ln,,37.0612132,-122.0074332,,,0,,, +johv,,Scotts Valley Dr & Victor Sq,,37.0590172,-122.0096058,,,0,,, +123,,Parent Station,,37.0666,-122.0777,,,1,,, +1234,,Child Stop,,37.06662,-122.07772,,,0,123,, \ No newline at end of file diff --git a/src/test/resources/fake-agency-with-fares-v2/timeframes.txt b/src/test/resources/fake-agency-with-fares-v2/timeframes.txt new file mode 100644 index 000000000..52667abe1 --- /dev/null +++ b/src/test/resources/fake-agency-with-fares-v2/timeframes.txt @@ -0,0 +1,8 @@ +timeframe_group_id,start_time,end_time,service_id +timeframe_regular,,,04100312-8fe1-46a5-a9f2-556f39478f57 +timeframe_systemwide_free,,,04100312-8fe1-46a5-a9f2-556f39478f57 +timeframe_sumner_tunnel_closure,,,04100312-8fe1-46a5-a9f2-556f39478f57 +timeframe_sumner_tunnel_closure,00:00:00,02:30:00,04100312-8fe1-46a5-a9f2-556f39478f57 +timeframe_regular,02:30:00,24:00:00,04100312-8fe1-46a5-a9f2-556f39478f57 +timeframe_alewife_kendall_surge,,,04100312-8fe1-46a5-a9f2-556f39478f57 +timeframe_alewife_kendall_surge,00:00:00,02:30:00,04100312-8fe1-46a5-a9f2-556f39478f57 diff --git a/src/test/resources/fake-agency-with-fares-v2/trips.txt b/src/test/resources/fake-agency-with-fares-v2/trips.txt new file mode 100644 index 000000000..982c01e0f --- /dev/null +++ b/src/test/resources/fake-agency-with-fares-v2/trips.txt @@ -0,0 +1,4 @@ +route_id,trip_id,trip_headsign,trip_short_name,direction_id,block_id,shape_id,bikes_allowed,wheelchair_accessible,service_id +1,a30277f8-e50a-4a85-9141-b1e0da9d429d,,,0,,5820f377-f947-4728-ac29-ac0102cbc34e,0,0,04100312-8fe1-46a5-a9f2-556f39478f57 +1,frequency-trip,,,0,,5820f377-f947-4728-ac29-ac0102cbc34e,0,0,04100312-8fe1-46a5-a9f2-556f39478f57 +1,calendar-date-trip,,,0,,5820f377-f947-4728-ac29-ac0102cbc34e,0,0,calendar-date-service \ No newline at end of file diff --git a/src/test/resources/graphql/feedAreas.txt b/src/test/resources/graphql/feedAreas.txt new file mode 100644 index 000000000..0cbe237a4 --- /dev/null +++ b/src/test/resources/graphql/feedAreas.txt @@ -0,0 +1,19 @@ +query ($namespace: String) { + feed(namespace: $namespace) { + feed_version + area(limit: 2) { + area_id + area_name + id + stopAreas { + area_id + stop_id + id + stops { + stop_id + stop_name + } + } + } + } +} \ No newline at end of file diff --git a/src/test/resources/graphql/feedFareLegRules.txt b/src/test/resources/graphql/feedFareLegRules.txt new file mode 100644 index 000000000..021c9f754 --- /dev/null +++ b/src/test/resources/graphql/feedFareLegRules.txt @@ -0,0 +1,51 @@ +query ($namespace: String) { + feed(namespace: $namespace) { + feed_version + fareLegRule(limit: 10) { + leg_group_id + network_id + from_area_id + to_area_id + from_timeframe_group_id + to_timeframe_group_id + fare_product_id + rule_priority + id + routes { + route_id + route_long_name + } + networks { + network_id + network_name + } + fareProducts { + fare_product_id + fare_product_name + fare_media_id + amount + currency + } + fromTimeFrame { + timeframe_group_id + start_time + end_time + service_id + } + toTimeFrame { + timeframe_group_id + start_time + end_time + service_id + } + toArea { + area_id + area_name + } + fromArea { + area_id + area_name + } + } + } +} \ No newline at end of file diff --git a/src/test/resources/graphql/feedFareMedias.txt b/src/test/resources/graphql/feedFareMedias.txt new file mode 100644 index 000000000..d35b2b057 --- /dev/null +++ b/src/test/resources/graphql/feedFareMedias.txt @@ -0,0 +1,11 @@ +query ($namespace: String) { + feed(namespace: $namespace) { + feed_version + fareMedia { + fare_media_id + fare_media_name + fare_media_type + id + } + } +} \ No newline at end of file diff --git a/src/test/resources/graphql/feedFareProducts.txt b/src/test/resources/graphql/feedFareProducts.txt new file mode 100644 index 000000000..bb550d475 --- /dev/null +++ b/src/test/resources/graphql/feedFareProducts.txt @@ -0,0 +1,18 @@ +query ($namespace: String) { + feed(namespace: $namespace) { + feed_version + fareProduct(limit: 10) { + fare_product_id + fare_product_name + fare_media_id + amount + currency + fareMedia { + fare_media_id + fare_media_name + fare_media_type + id + } + } + } +} \ No newline at end of file diff --git a/src/test/resources/graphql/feedFareTransferRules.txt b/src/test/resources/graphql/feedFareTransferRules.txt new file mode 100644 index 000000000..b407b7026 --- /dev/null +++ b/src/test/resources/graphql/feedFareTransferRules.txt @@ -0,0 +1,48 @@ +query ($namespace: String) { + feed(namespace: $namespace) { + feed_version + fareTransferRule (limit: 2) { + from_leg_group_id + to_leg_group_id + transfer_count + duration_limit + duration_limit_type + fare_product_id + fareProducts { + fare_product_id + fare_product_name + fare_media_id + amount + currency + fareMedia { + fare_media_id + fare_media_name + fare_media_type + id + } + } + fromFareLegRule { + leg_group_id + network_id + from_area_id + to_area_id + from_timeframe_group_id + to_timeframe_group_id + fare_product_id + rule_priority + id + } + toFareLegRule { + leg_group_id + network_id + from_area_id + to_area_id + from_timeframe_group_id + to_timeframe_group_id + fare_product_id + rule_priority + id + } + } + } +} \ No newline at end of file diff --git a/src/test/resources/graphql/feedNetworks.txt b/src/test/resources/graphql/feedNetworks.txt new file mode 100644 index 000000000..b0ebdeecc --- /dev/null +++ b/src/test/resources/graphql/feedNetworks.txt @@ -0,0 +1,9 @@ +query ($namespace: String) { + feed(namespace: $namespace) { + feed_version + network { + network_id + network_name + } + } +} \ No newline at end of file diff --git a/src/test/resources/graphql/feedRouteNetworks.txt b/src/test/resources/graphql/feedRouteNetworks.txt new file mode 100644 index 000000000..24a303848 --- /dev/null +++ b/src/test/resources/graphql/feedRouteNetworks.txt @@ -0,0 +1,9 @@ +query ($namespace: String) { + feed(namespace: $namespace) { + feed_version + routeNetwork { + network_id + route_id + } + } +} \ No newline at end of file diff --git a/src/test/resources/graphql/feedStopAreas.txt b/src/test/resources/graphql/feedStopAreas.txt new file mode 100644 index 000000000..91999bfd7 --- /dev/null +++ b/src/test/resources/graphql/feedStopAreas.txt @@ -0,0 +1,14 @@ +query ($namespace: String) { + feed(namespace: $namespace) { + feed_version + stopArea { + area_id + stop_id + id + stops { + stop_id + stop_name + } + } + } +} \ No newline at end of file diff --git a/src/test/resources/graphql/feedTimeFrames.txt b/src/test/resources/graphql/feedTimeFrames.txt new file mode 100644 index 000000000..6a259e893 --- /dev/null +++ b/src/test/resources/graphql/feedTimeFrames.txt @@ -0,0 +1,12 @@ +query ($namespace: String) { + feed(namespace: $namespace) { + feed_version + timeFrame { + timeframe_group_id + start_time + end_time + service_id + id + } + } +} \ No newline at end of file diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchAreas-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchAreas-0.json new file mode 100644 index 000000000..a40f5d459 --- /dev/null +++ b/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchAreas-0.json @@ -0,0 +1,210 @@ +{ + "data" : { + "feed" : { + "area" : [ { + "area_id" : "area_bl", + "area_name" : "Blue Line", + "id" : 2, + "stopAreas" : [ { + "area_id" : "area_bl", + "id" : 229, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_bl", + "id" : 230, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_bl", + "id" : 231, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_bl", + "id" : 232, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_bl", + "id" : 233, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_bl", + "id" : 234, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_bl", + "id" : 235, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_bl", + "id" : 236, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_bl", + "id" : 237, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_bl", + "id" : 240, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_bl", + "id" : 241, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_bl", + "id" : 242, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_bl", + "id" : 243, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_bl", + "id" : 244, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_bl", + "id" : 245, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_bl", + "id" : 246, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_bl", + "id" : 247, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_bl", + "id" : 248, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_bl", + "id" : 249, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_bl", + "id" : 250, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_bl", + "id" : 251, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_bl", + "id" : 330, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + } ] + }, { + "area_id" : "area_bl_airport", + "area_name" : "Blue Line - Airport Station", + "id" : 3, + "stopAreas" : [ { + "area_id" : "area_bl_airport", + "id" : 238, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_bl_airport", + "id" : 239, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + } ] + } ], + "feed_version" : "1.0" + } + } +} \ No newline at end of file diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareLegRules-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareLegRules-0.json new file mode 100644 index 000000000..0403cdb2c --- /dev/null +++ b/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareLegRules-0.json @@ -0,0 +1,416 @@ +{ + "data" : { + "feed" : { + "fareLegRule" : [ { + "fareProducts" : [ { + "amount" : "2.4", + "currency" : "USD", + "fare_media_id" : "charlieticket", + "fare_product_id" : "prod_rapid_transit_quick_subway", + "fare_product_name" : "Subway Quick Ticket" + } ], + "fare_product_id" : "prod_rapid_transit_quick_subway", + "fromArea" : [ { + "area_id" : "area_bl_airport", + "area_name" : "Blue Line - Airport Station" + } ], + "fromTimeFrame" : [ { + "end_time" : null, + "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", + "start_time" : null, + "timeframe_group_id" : "timeframe_regular" + }, { + "end_time" : "86400", + "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", + "start_time" : "9000", + "timeframe_group_id" : "timeframe_regular" + } ], + "from_area_id" : "area_bl_airport", + "from_timeframe_group_id" : "timeframe_regular", + "id" : 2, + "leg_group_id" : "leg_airport_rapid_transit_quick_subway", + "network_id" : "rapid_transit", + "networks" : [ ], + "routes" : [ { + "route_id" : "1", + "route_long_name" : "RL" + } ], + "rule_priority" : null, + "toArea" : [ ], + "toTimeFrame" : [ ], + "to_area_id" : null, + "to_timeframe_group_id" : null + }, { + "fareProducts" : [ { + "amount" : "5.0", + "currency" : "USD", + "fare_media_id" : "cash", + "fare_product_id" : "prod_cape_buzzards_hyannis_fare", + "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" + }, { + "amount" : "5.0", + "currency" : "USD", + "fare_media_id" : "credit_debit", + "fare_product_id" : "prod_cape_buzzards_hyannis_fare", + "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" + }, { + "amount" : "5.0", + "currency" : "USD", + "fare_media_id" : "mticket", + "fare_product_id" : "prod_cape_buzzards_hyannis_fare", + "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" + } ], + "fare_product_id" : "prod_cape_buzzards_hyannis_fare", + "fromArea" : [ { + "area_id" : "area_cf_zone_buzzards", + "area_name" : "CapeFLYER - Wareham/Buzzards Bay/Bourne" + } ], + "fromTimeFrame" : [ ], + "from_area_id" : "area_cf_zone_buzzards", + "from_timeframe_group_id" : null, + "id" : 3, + "leg_group_id" : "leg_cape_buzzards_hyannis_cash", + "network_id" : "cape_flyer", + "networks" : [ ], + "routes" : [ ], + "rule_priority" : null, + "toArea" : [ { + "area_id" : "area_cf_zone_hyannis", + "area_name" : "CapeFLYER - Hyannis" + } ], + "toTimeFrame" : [ ], + "to_area_id" : "area_cf_zone_hyannis", + "to_timeframe_group_id" : null + }, { + "fareProducts" : [ { + "amount" : "5.0", + "currency" : "USD", + "fare_media_id" : "cash", + "fare_product_id" : "prod_cape_buzzards_hyannis_fare", + "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" + }, { + "amount" : "5.0", + "currency" : "USD", + "fare_media_id" : "credit_debit", + "fare_product_id" : "prod_cape_buzzards_hyannis_fare", + "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" + }, { + "amount" : "5.0", + "currency" : "USD", + "fare_media_id" : "mticket", + "fare_product_id" : "prod_cape_buzzards_hyannis_fare", + "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" + } ], + "fare_product_id" : "prod_cape_buzzards_hyannis_fare", + "fromArea" : [ { + "area_id" : "area_cf_zone_hyannis", + "area_name" : "CapeFLYER - Hyannis" + } ], + "fromTimeFrame" : [ ], + "from_area_id" : "area_cf_zone_hyannis", + "from_timeframe_group_id" : null, + "id" : 4, + "leg_group_id" : "leg_cape_buzzards_hyannis_cash", + "network_id" : "cape_flyer", + "networks" : [ ], + "routes" : [ ], + "rule_priority" : null, + "toArea" : [ { + "area_id" : "area_cf_zone_buzzards", + "area_name" : "CapeFLYER - Wareham/Buzzards Bay/Bourne" + } ], + "toTimeFrame" : [ ], + "to_area_id" : "area_cf_zone_buzzards", + "to_timeframe_group_id" : null + }, { + "fareProducts" : [ { + "amount" : "5.0", + "currency" : "USD", + "fare_media_id" : "cash", + "fare_product_id" : "prod_cape_buzzards_hyannis_fare", + "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" + }, { + "amount" : "5.0", + "currency" : "USD", + "fare_media_id" : "credit_debit", + "fare_product_id" : "prod_cape_buzzards_hyannis_fare", + "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" + }, { + "amount" : "5.0", + "currency" : "USD", + "fare_media_id" : "mticket", + "fare_product_id" : "prod_cape_buzzards_hyannis_fare", + "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" + } ], + "fare_product_id" : "prod_cape_buzzards_hyannis_fare", + "fromArea" : [ { + "area_id" : "area_cf_zone_hyannis", + "area_name" : "CapeFLYER - Hyannis" + } ], + "fromTimeFrame" : [ ], + "from_area_id" : "area_cf_zone_hyannis", + "from_timeframe_group_id" : null, + "id" : 5, + "leg_group_id" : "leg_cape_buzzards_hyannis_cash", + "network_id" : "cape_flyer", + "networks" : [ ], + "routes" : [ ], + "rule_priority" : null, + "toArea" : [ { + "area_id" : "area_commuter_rail_zone_8", + "area_name" : "Commuter Rail Zone 8" + } ], + "toTimeFrame" : [ ], + "to_area_id" : "area_commuter_rail_zone_8", + "to_timeframe_group_id" : null + }, { + "fareProducts" : [ { + "amount" : "5.0", + "currency" : "USD", + "fare_media_id" : "cash", + "fare_product_id" : "prod_cape_buzzards_hyannis_fare", + "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" + }, { + "amount" : "5.0", + "currency" : "USD", + "fare_media_id" : "credit_debit", + "fare_product_id" : "prod_cape_buzzards_hyannis_fare", + "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" + }, { + "amount" : "5.0", + "currency" : "USD", + "fare_media_id" : "mticket", + "fare_product_id" : "prod_cape_buzzards_hyannis_fare", + "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" + } ], + "fare_product_id" : "prod_cape_buzzards_hyannis_fare", + "fromArea" : [ { + "area_id" : "area_commuter_rail_zone_8", + "area_name" : "Commuter Rail Zone 8" + } ], + "fromTimeFrame" : [ ], + "from_area_id" : "area_commuter_rail_zone_8", + "from_timeframe_group_id" : null, + "id" : 6, + "leg_group_id" : "leg_cape_buzzards_hyannis_cash", + "network_id" : "cape_flyer", + "networks" : [ ], + "routes" : [ ], + "rule_priority" : null, + "toArea" : [ { + "area_id" : "area_cf_zone_hyannis", + "area_name" : "CapeFLYER - Hyannis" + } ], + "toTimeFrame" : [ ], + "to_area_id" : "area_cf_zone_hyannis", + "to_timeframe_group_id" : null + }, { + "fareProducts" : [ { + "amount" : "20.0", + "currency" : "USD", + "fare_media_id" : "cash", + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fare_product_name" : "CapeFLYER Bourne one-way fare" + }, { + "amount" : "20.0", + "currency" : "USD", + "fare_media_id" : "credit_debit", + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fare_product_name" : "CapeFLYER Bourne one-way fare" + }, { + "amount" : "20.0", + "currency" : "USD", + "fare_media_id" : "mticket", + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fare_product_name" : "CapeFLYER Bourne one-way fare" + } ], + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fromArea" : [ { + "area_id" : "area_cf_zone_buzzards", + "area_name" : "CapeFLYER - Wareham/Buzzards Bay/Bourne" + } ], + "fromTimeFrame" : [ ], + "from_area_id" : "area_cf_zone_buzzards", + "from_timeframe_group_id" : null, + "id" : 7, + "leg_group_id" : "leg_cape_sbb_buzzards_cash", + "network_id" : "cape_flyer", + "networks" : [ ], + "routes" : [ ], + "rule_priority" : null, + "toArea" : [ { + "area_id" : "area_commuter_rail_zone_1a", + "area_name" : "Commuter Rail Zone 1A" + } ], + "toTimeFrame" : [ ], + "to_area_id" : "area_commuter_rail_zone_1a", + "to_timeframe_group_id" : null + }, { + "fareProducts" : [ { + "amount" : "20.0", + "currency" : "USD", + "fare_media_id" : "cash", + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fare_product_name" : "CapeFLYER Bourne one-way fare" + }, { + "amount" : "20.0", + "currency" : "USD", + "fare_media_id" : "credit_debit", + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fare_product_name" : "CapeFLYER Bourne one-way fare" + }, { + "amount" : "20.0", + "currency" : "USD", + "fare_media_id" : "mticket", + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fare_product_name" : "CapeFLYER Bourne one-way fare" + } ], + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fromArea" : [ { + "area_id" : "area_cf_zone_buzzards", + "area_name" : "CapeFLYER - Wareham/Buzzards Bay/Bourne" + } ], + "fromTimeFrame" : [ ], + "from_area_id" : "area_cf_zone_buzzards", + "from_timeframe_group_id" : null, + "id" : 8, + "leg_group_id" : "leg_cape_sbb_buzzards_cash", + "network_id" : "cape_flyer", + "networks" : [ ], + "routes" : [ ], + "rule_priority" : null, + "toArea" : [ { + "area_id" : "area_commuter_rail_zone_2", + "area_name" : "Commuter Rail Zone 2" + } ], + "toTimeFrame" : [ ], + "to_area_id" : "area_commuter_rail_zone_2", + "to_timeframe_group_id" : null + }, { + "fareProducts" : [ { + "amount" : "20.0", + "currency" : "USD", + "fare_media_id" : "cash", + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fare_product_name" : "CapeFLYER Bourne one-way fare" + }, { + "amount" : "20.0", + "currency" : "USD", + "fare_media_id" : "credit_debit", + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fare_product_name" : "CapeFLYER Bourne one-way fare" + }, { + "amount" : "20.0", + "currency" : "USD", + "fare_media_id" : "mticket", + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fare_product_name" : "CapeFLYER Bourne one-way fare" + } ], + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fromArea" : [ { + "area_id" : "area_cf_zone_buzzards", + "area_name" : "CapeFLYER - Wareham/Buzzards Bay/Bourne" + } ], + "fromTimeFrame" : [ ], + "from_area_id" : "area_cf_zone_buzzards", + "from_timeframe_group_id" : null, + "id" : 9, + "leg_group_id" : "leg_cape_sbb_buzzards_cash", + "network_id" : "cape_flyer", + "networks" : [ ], + "routes" : [ ], + "rule_priority" : null, + "toArea" : [ { + "area_id" : "area_commuter_rail_zone_4", + "area_name" : "Commuter Rail Zone 4" + } ], + "toTimeFrame" : [ ], + "to_area_id" : "area_commuter_rail_zone_4", + "to_timeframe_group_id" : null + }, { + "fareProducts" : [ { + "amount" : "20.0", + "currency" : "USD", + "fare_media_id" : "cash", + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fare_product_name" : "CapeFLYER Bourne one-way fare" + }, { + "amount" : "20.0", + "currency" : "USD", + "fare_media_id" : "credit_debit", + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fare_product_name" : "CapeFLYER Bourne one-way fare" + }, { + "amount" : "20.0", + "currency" : "USD", + "fare_media_id" : "mticket", + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fare_product_name" : "CapeFLYER Bourne one-way fare" + } ], + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fromArea" : [ { + "area_id" : "area_commuter_rail_zone_1a", + "area_name" : "Commuter Rail Zone 1A" + } ], + "fromTimeFrame" : [ ], + "from_area_id" : "area_commuter_rail_zone_1a", + "from_timeframe_group_id" : null, + "id" : 10, + "leg_group_id" : "leg_cape_sbb_buzzards_cash", + "network_id" : "cape_flyer", + "networks" : [ ], + "routes" : [ ], + "rule_priority" : null, + "toArea" : [ { + "area_id" : "area_cf_zone_buzzards", + "area_name" : "CapeFLYER - Wareham/Buzzards Bay/Bourne" + } ], + "toTimeFrame" : [ ], + "to_area_id" : "area_cf_zone_buzzards", + "to_timeframe_group_id" : null + }, { + "fareProducts" : [ { + "amount" : "20.0", + "currency" : "USD", + "fare_media_id" : "cash", + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fare_product_name" : "CapeFLYER Bourne one-way fare" + }, { + "amount" : "20.0", + "currency" : "USD", + "fare_media_id" : "credit_debit", + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fare_product_name" : "CapeFLYER Bourne one-way fare" + }, { + "amount" : "20.0", + "currency" : "USD", + "fare_media_id" : "mticket", + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fare_product_name" : "CapeFLYER Bourne one-way fare" + } ], + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fromArea" : [ { + "area_id" : "area_commuter_rail_zone_2", + "area_name" : "Commuter Rail Zone 2" + } ], + "fromTimeFrame" : [ ], + "from_area_id" : "area_commuter_rail_zone_2", + "from_timeframe_group_id" : null, + "id" : 11, + "leg_group_id" : "leg_cape_sbb_buzzards_cash", + "network_id" : "cape_flyer", + "networks" : [ ], + "routes" : [ ], + "rule_priority" : null, + "toArea" : [ { + "area_id" : "area_cf_zone_buzzards", + "area_name" : "CapeFLYER - Wareham/Buzzards Bay/Bourne" + } ], + "toTimeFrame" : [ ], + "to_area_id" : "area_cf_zone_buzzards", + "to_timeframe_group_id" : null + } ], + "feed_version" : "1.0" + } + } +} \ No newline at end of file diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareMedias-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareMedias-0.json new file mode 100644 index 000000000..86d7c8d3d --- /dev/null +++ b/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareMedias-0.json @@ -0,0 +1,28 @@ +{ + "data" : { + "feed" : { + "fareMedia" : [ { + "fare_media_id" : "cash", + "fare_media_name" : "Cash", + "fare_media_type" : "0", + "id" : 2 + }, { + "fare_media_id" : "credit_debit", + "fare_media_name" : "Credit/debit card", + "fare_media_type" : "0", + "id" : 3 + }, { + "fare_media_id" : "charlieticket", + "fare_media_name" : "CharlieTicket", + "fare_media_type" : "1", + "id" : 4 + }, { + "fare_media_id" : "mticket", + "fare_media_name" : "mTicket app", + "fare_media_type" : "4", + "id" : 5 + } ], + "feed_version" : "1.0" + } + } +} \ No newline at end of file diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareProducts-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareProducts-0.json new file mode 100644 index 000000000..aeb5b0f9a --- /dev/null +++ b/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareProducts-0.json @@ -0,0 +1,128 @@ +{ + "data" : { + "feed" : { + "fareProduct" : [ { + "amount" : "6.5", + "currency" : "USD", + "fareMedia" : [ { + "fare_media_id" : "cash", + "fare_media_name" : "Cash", + "fare_media_type" : "0", + "id" : 2 + } ], + "fare_media_id" : "cash", + "fare_product_id" : "prod_boat_zone_1", + "fare_product_name" : "Ferry Zone 1 one-way fare" + }, { + "amount" : "6.5", + "currency" : "USD", + "fareMedia" : [ { + "fare_media_id" : "credit_debit", + "fare_media_name" : "Credit/debit card", + "fare_media_type" : "0", + "id" : 3 + } ], + "fare_media_id" : "credit_debit", + "fare_product_id" : "prod_boat_zone_1", + "fare_product_name" : "Ferry Zone 1 one-way fare" + }, { + "amount" : "6.5", + "currency" : "USD", + "fareMedia" : [ { + "fare_media_id" : "mticket", + "fare_media_name" : "mTicket app", + "fare_media_type" : "4", + "id" : 5 + } ], + "fare_media_id" : "mticket", + "fare_product_id" : "prod_boat_zone_1", + "fare_product_name" : "Ferry Zone 1 one-way fare" + }, { + "amount" : "2.4", + "currency" : "USD", + "fareMedia" : [ { + "fare_media_id" : "cash", + "fare_media_name" : "Cash", + "fare_media_type" : "0", + "id" : 2 + } ], + "fare_media_id" : "cash", + "fare_product_id" : "prod_boat_zone_1a", + "fare_product_name" : "Ferry Zone 1A one-way fare" + }, { + "amount" : "2.4", + "currency" : "USD", + "fareMedia" : [ { + "fare_media_id" : "credit_debit", + "fare_media_name" : "Credit/debit card", + "fare_media_type" : "0", + "id" : 3 + } ], + "fare_media_id" : "credit_debit", + "fare_product_id" : "prod_boat_zone_1a", + "fare_product_name" : "Ferry Zone 1A one-way fare" + }, { + "amount" : "2.4", + "currency" : "USD", + "fareMedia" : [ { + "fare_media_id" : "mticket", + "fare_media_name" : "mTicket app", + "fare_media_type" : "4", + "id" : 5 + } ], + "fare_media_id" : "mticket", + "fare_product_id" : "prod_boat_zone_1a", + "fare_product_name" : "Ferry Zone 1A one-way fare" + }, { + "amount" : "7.0", + "currency" : "USD", + "fareMedia" : [ { + "fare_media_id" : "cash", + "fare_media_name" : "Cash", + "fare_media_type" : "0", + "id" : 2 + } ], + "fare_media_id" : "cash", + "fare_product_id" : "prod_boat_zone_2", + "fare_product_name" : "Ferry Zone 2 one-way fare" + }, { + "amount" : "7.0", + "currency" : "USD", + "fareMedia" : [ { + "fare_media_id" : "credit_debit", + "fare_media_name" : "Credit/debit card", + "fare_media_type" : "0", + "id" : 3 + } ], + "fare_media_id" : "credit_debit", + "fare_product_id" : "prod_boat_zone_2", + "fare_product_name" : "Ferry Zone 2 one-way fare" + }, { + "amount" : "7.0", + "currency" : "USD", + "fareMedia" : [ { + "fare_media_id" : "mticket", + "fare_media_name" : "mTicket app", + "fare_media_type" : "4", + "id" : 5 + } ], + "fare_media_id" : "mticket", + "fare_product_id" : "prod_boat_zone_2", + "fare_product_name" : "Ferry Zone 2 one-way fare" + }, { + "amount" : "5.0", + "currency" : "USD", + "fareMedia" : [ { + "fare_media_id" : "cash", + "fare_media_name" : "Cash", + "fare_media_type" : "0", + "id" : 2 + } ], + "fare_media_id" : "cash", + "fare_product_id" : "prod_cape_buzzards_hyannis_fare", + "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" + } ], + "feed_version" : "1.0" + } + } +} \ No newline at end of file diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareTransferRules-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareTransferRules-0.json new file mode 100644 index 000000000..964d368c6 --- /dev/null +++ b/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareTransferRules-0.json @@ -0,0 +1,662 @@ +{ + "data" : { + "feed" : { + "fareTransferRule" : [ { + "duration_limit" : "7200", + "duration_limit_type" : "1", + "fareProducts" : [ { + "amount" : "2.4", + "currency" : "USD", + "fareMedia" : [ { + "fare_media_id" : "charlieticket", + "fare_media_name" : "CharlieTicket", + "fare_media_type" : "1", + "id" : 4 + } ], + "fare_media_id" : "charlieticket", + "fare_product_id" : "prod_rapid_transit_quick_subway", + "fare_product_name" : "Subway Quick Ticket" + } ], + "fare_product_id" : "prod_rapid_transit_quick_subway", + "fromFareLegRule" : [ { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_bl_airport", + "from_timeframe_group_id" : "timeframe_regular", + "id" : 2, + "leg_group_id" : "leg_airport_rapid_transit_quick_subway", + "network_id" : "rapid_transit", + "rule_priority" : null, + "to_area_id" : null, + "to_timeframe_group_id" : null + } ], + "from_leg_group_id" : "leg_airport_rapid_transit_quick_subway", + "toFareLegRule" : [ { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_354_downtown", + "from_timeframe_group_id" : null, + "id" : 311, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "express_bus", + "rule_priority" : null, + "to_area_id" : "area_route_354_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_354_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 312, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "express_bus", + "rule_priority" : null, + "to_area_id" : "area_route_354_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_426_downtown", + "from_timeframe_group_id" : null, + "id" : 313, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "express_bus", + "rule_priority" : null, + "to_area_id" : "area_route_426_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_426_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 314, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "express_bus", + "rule_priority" : null, + "to_area_id" : "area_route_426_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_450_downtown", + "from_timeframe_group_id" : null, + "id" : 315, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "express_bus", + "rule_priority" : null, + "to_area_id" : "area_route_450_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_450_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 316, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "express_bus", + "rule_priority" : null, + "to_area_id" : "area_route_450_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_354_downtown", + "from_timeframe_group_id" : null, + "id" : 317, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_354_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_354_downtown", + "from_timeframe_group_id" : null, + "id" : 318, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_354_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_354_downtown", + "from_timeframe_group_id" : null, + "id" : 319, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_426_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_354_downtown", + "from_timeframe_group_id" : null, + "id" : 320, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_426_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_354_downtown", + "from_timeframe_group_id" : null, + "id" : 321, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_450_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_354_downtown", + "from_timeframe_group_id" : null, + "id" : 322, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_450_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_354_downtown", + "from_timeframe_group_id" : null, + "id" : 323, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : null, + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_354_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 324, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_354_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_354_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 325, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_354_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_354_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 326, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_426_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_354_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 327, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_426_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_354_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 328, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_450_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_354_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 329, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_450_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_354_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 330, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : null, + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_426_downtown", + "from_timeframe_group_id" : null, + "id" : 331, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_354_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_426_downtown", + "from_timeframe_group_id" : null, + "id" : 332, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_354_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_426_downtown", + "from_timeframe_group_id" : null, + "id" : 333, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_426_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_426_downtown", + "from_timeframe_group_id" : null, + "id" : 334, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_426_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_426_downtown", + "from_timeframe_group_id" : null, + "id" : 335, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_450_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_426_downtown", + "from_timeframe_group_id" : null, + "id" : 336, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_450_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_426_downtown", + "from_timeframe_group_id" : null, + "id" : 337, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : null, + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_426_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 338, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_354_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_426_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 339, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_354_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_426_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 340, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_426_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_426_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 341, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_426_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_426_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 342, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_450_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_426_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 343, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_450_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_426_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 344, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : null, + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_450_downtown", + "from_timeframe_group_id" : null, + "id" : 345, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_354_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_450_downtown", + "from_timeframe_group_id" : null, + "id" : 346, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_354_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_450_downtown", + "from_timeframe_group_id" : null, + "id" : 347, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_426_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_450_downtown", + "from_timeframe_group_id" : null, + "id" : 348, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_426_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_450_downtown", + "from_timeframe_group_id" : null, + "id" : 349, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_450_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_450_downtown", + "from_timeframe_group_id" : null, + "id" : 350, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_450_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_450_downtown", + "from_timeframe_group_id" : null, + "id" : 351, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : null, + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_450_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 352, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_354_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_450_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 353, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_354_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_450_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 354, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_426_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_450_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 355, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_426_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_450_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 356, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_450_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_450_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 357, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_450_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_route_450_outside_downtown", + "from_timeframe_group_id" : null, + "id" : 358, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : null, + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : null, + "from_timeframe_group_id" : null, + "id" : 359, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_354_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : null, + "from_timeframe_group_id" : null, + "id" : 360, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_354_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : null, + "from_timeframe_group_id" : null, + "id" : 361, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_426_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : null, + "from_timeframe_group_id" : null, + "id" : 362, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_426_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : null, + "from_timeframe_group_id" : null, + "id" : 363, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_450_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : null, + "from_timeframe_group_id" : null, + "id" : 364, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : "area_route_450_outside_downtown", + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : null, + "from_timeframe_group_id" : null, + "id" : 365, + "leg_group_id" : "leg_local_bus_quick_subway", + "network_id" : "local_bus", + "rule_priority" : null, + "to_area_id" : null, + "to_timeframe_group_id" : null + } ], + "to_leg_group_id" : "leg_local_bus_quick_subway", + "transfer_count" : null + }, { + "duration_limit" : null, + "duration_limit_type" : null, + "fareProducts" : [ { + "amount" : "2.4", + "currency" : "USD", + "fareMedia" : [ { + "fare_media_id" : "charlieticket", + "fare_media_name" : "CharlieTicket", + "fare_media_type" : "1", + "id" : 4 + } ], + "fare_media_id" : "charlieticket", + "fare_product_id" : "prod_rapid_transit_quick_subway", + "fare_product_name" : "Subway Quick Ticket" + } ], + "fare_product_id" : "prod_rapid_transit_quick_subway", + "fromFareLegRule" : [ { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_bl_airport", + "from_timeframe_group_id" : "timeframe_regular", + "id" : 2, + "leg_group_id" : "leg_airport_rapid_transit_quick_subway", + "network_id" : "rapid_transit", + "rule_priority" : null, + "to_area_id" : null, + "to_timeframe_group_id" : null + } ], + "from_leg_group_id" : "leg_airport_rapid_transit_quick_subway", + "toFareLegRule" : [ { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_bl", + "from_timeframe_group_id" : "timeframe_regular", + "id" : 476, + "leg_group_id" : "leg_rapid_transit_quick_subway", + "network_id" : "rapid_transit", + "rule_priority" : null, + "to_area_id" : null, + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_gl_govt_ctr", + "from_timeframe_group_id" : "timeframe_regular", + "id" : 477, + "leg_group_id" : "leg_rapid_transit_quick_subway", + "network_id" : "rapid_transit", + "rule_priority" : null, + "to_area_id" : null, + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : "area_ol_state", + "from_timeframe_group_id" : "timeframe_regular", + "id" : 478, + "leg_group_id" : "leg_rapid_transit_quick_subway", + "network_id" : "rapid_transit", + "rule_priority" : null, + "to_area_id" : null, + "to_timeframe_group_id" : null + }, { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "from_area_id" : null, + "from_timeframe_group_id" : "timeframe_regular", + "id" : 479, + "leg_group_id" : "leg_rapid_transit_quick_subway", + "network_id" : "rapid_transit", + "rule_priority" : null, + "to_area_id" : null, + "to_timeframe_group_id" : null + } ], + "to_leg_group_id" : "leg_rapid_transit_quick_subway", + "transfer_count" : null + } ], + "feed_version" : "1.0" + } + } +} \ No newline at end of file diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchNetworks-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchNetworks-0.json new file mode 100644 index 000000000..0b3fa282d --- /dev/null +++ b/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchNetworks-0.json @@ -0,0 +1,11 @@ +{ + "data" : { + "feed" : { + "feed_version" : "1.0", + "network" : [ { + "network_id" : "1", + "network_name" : "Forbidden because network id is defined in routes" + } ] + } + } +} \ No newline at end of file diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchRouteNetworks-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchRouteNetworks-0.json new file mode 100644 index 000000000..1a234d07e --- /dev/null +++ b/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchRouteNetworks-0.json @@ -0,0 +1,11 @@ +{ + "data" : { + "feed" : { + "feed_version" : "1.0", + "routeNetwork" : [ { + "network_id" : "1", + "route_id" : "1" + } ] + } + } +} \ No newline at end of file diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchStopAreas-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchStopAreas-0.json new file mode 100644 index 000000000..a9fab4197 --- /dev/null +++ b/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchStopAreas-0.json @@ -0,0 +1,408 @@ +{ + "data" : { + "feed" : { + "feed_version" : "1.0", + "stopArea" : [ { + "area_id" : "area_route_426_downtown", + "id" : 2, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_450_downtown", + "id" : 3, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_450_outside_downtown", + "id" : 4, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_450_outside_downtown", + "id" : 5, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_450_outside_downtown", + "id" : 6, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_450_outside_downtown", + "id" : 7, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_450_outside_downtown", + "id" : 8, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_450_outside_downtown", + "id" : 9, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_450_outside_downtown", + "id" : 10, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_426_outside_downtown", + "id" : 11, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_450_outside_downtown", + "id" : 12, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_426_outside_downtown", + "id" : 13, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_426_outside_downtown", + "id" : 14, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_450_outside_downtown", + "id" : 15, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_426_outside_downtown", + "id" : 16, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_354_outside_downtown", + "id" : 17, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_354_outside_downtown", + "id" : 18, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_354_outside_downtown", + "id" : 19, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_354_outside_downtown", + "id" : 20, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_354_outside_downtown", + "id" : 21, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_354_outside_downtown", + "id" : 22, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_354_outside_downtown", + "id" : 23, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_354_outside_downtown", + "id" : 24, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_354_outside_downtown", + "id" : 25, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_354_outside_downtown", + "id" : 26, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_354_outside_downtown", + "id" : 27, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_354_outside_downtown", + "id" : 28, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_354_outside_downtown", + "id" : 29, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_354_outside_downtown", + "id" : 30, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_354_outside_downtown", + "id" : 31, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_354_outside_downtown", + "id" : 32, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_354_outside_downtown", + "id" : 33, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_green_b_west_of_kenmore", + "id" : 34, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_green_b_west_of_kenmore", + "id" : 35, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_green_b_west_of_kenmore", + "id" : 36, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_green_b_west_of_kenmore", + "id" : 37, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_sl_logan_terminal", + "id" : 38, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_sl_logan_terminal", + "id" : 39, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_sl_logan_terminal", + "id" : 40, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_sl_logan_terminal", + "id" : 41, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_426_outside_downtown", + "id" : 42, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_426_outside_downtown", + "id" : 43, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_426_outside_downtown", + "id" : 44, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_426_outside_downtown", + "id" : 45, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_354_outside_downtown", + "id" : 46, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_354_outside_downtown", + "id" : 47, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_426_outside_downtown", + "id" : 48, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_450_outside_downtown", + "id" : 49, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_426_outside_downtown", + "id" : 50, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + }, { + "area_id" : "area_route_450_outside_downtown", + "id" : 51, + "stop_id" : "4u6g", + "stops" : [ { + "stop_id" : "4u6g", + "stop_name" : "Butler Ln" + } ] + } ] + } + } +} \ No newline at end of file diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchTimeFrames-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchTimeFrames-0.json new file mode 100644 index 000000000..66b4bbc81 --- /dev/null +++ b/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchTimeFrames-0.json @@ -0,0 +1,50 @@ +{ + "data" : { + "feed" : { + "feed_version" : "1.0", + "timeFrame" : [ { + "end_time" : null, + "id" : 2, + "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", + "start_time" : null, + "timeframe_group_id" : "timeframe_regular" + }, { + "end_time" : null, + "id" : 3, + "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", + "start_time" : null, + "timeframe_group_id" : "timeframe_systemwide_free" + }, { + "end_time" : null, + "id" : 4, + "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", + "start_time" : null, + "timeframe_group_id" : "timeframe_sumner_tunnel_closure" + }, { + "end_time" : "9000", + "id" : 5, + "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", + "start_time" : "0", + "timeframe_group_id" : "timeframe_sumner_tunnel_closure" + }, { + "end_time" : "86400", + "id" : 6, + "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", + "start_time" : "9000", + "timeframe_group_id" : "timeframe_regular" + }, { + "end_time" : null, + "id" : 7, + "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", + "start_time" : null, + "timeframe_group_id" : "timeframe_alewife_kendall_surge" + }, { + "end_time" : "9000", + "id" : 8, + "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", + "start_time" : "0", + "timeframe_group_id" : "timeframe_alewife_kendall_surge" + } ] + } + } +} \ No newline at end of file From 1d57cd6a1cf36243fa6347066b81a7e0e7292fcd Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Thu, 15 Aug 2024 14:16:55 +0100 Subject: [PATCH 02/40] refactor(GTFSTest): Addressed failing test --- src/test/java/com/conveyal/gtfs/GTFSTest.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/test/java/com/conveyal/gtfs/GTFSTest.java b/src/test/java/com/conveyal/gtfs/GTFSTest.java index 2b0d82da0..20c2f7c69 100644 --- a/src/test/java/com/conveyal/gtfs/GTFSTest.java +++ b/src/test/java/com/conveyal/gtfs/GTFSTest.java @@ -357,12 +357,12 @@ void canLoadAndExportFaresV2Feed() throws IOException { new PersistenceExpectation( "fare_leg_rules", new RecordExpectation[]{ - new RecordExpectation("leg_group_id", "leg_commuter_rail_cash"), - new RecordExpectation("network_id", "commuter_rail"), - new RecordExpectation("from_area_id", "area_commuter_rail_sumner_tunnel_zone_1a"), - new RecordExpectation("to_area_id", "area_commuter_rail_sumner_tunnel_zone_1a"), - new RecordExpectation("fare_product_id", "prod_cr_zone_1a"), - new RecordExpectation("from_timeframe_group_id", "timeframe_sumner_tunnel_closure"), + new RecordExpectation("leg_group_id", "leg_airport_rapid_transit_quick_subway"), + new RecordExpectation("network_id", "rapid_transit"), + new RecordExpectation("from_area_id", "area_bl_airport"), + new RecordExpectation("to_area_id", null), + new RecordExpectation("fare_product_id", "prod_rapid_transit_quick_subway"), + new RecordExpectation("from_timeframe_group_id", "timeframe_regular"), new RecordExpectation("to_timeframe_group_id", null), new RecordExpectation("transfer_only", null) } @@ -388,13 +388,13 @@ void canLoadAndExportFaresV2Feed() throws IOException { new PersistenceExpectation( "fare_transfer_rules", new RecordExpectation[]{ - new RecordExpectation("from_leg_group_id", "leg_airport_rapid_transit_quick_subway"), + new RecordExpectation("from_leg_group_id", "leg_mattapan_rapid_transit_quick_subway"), new RecordExpectation("to_leg_group_id", "leg_local_bus_quick_subway"), new RecordExpectation("transfer_count", null), new RecordExpectation("duration_limit", "7200"), new RecordExpectation("duration_limit_type", "1"), new RecordExpectation("fare_transfer_type", "0"), - new RecordExpectation("fare_product_id", null) + new RecordExpectation("fare_product_id", "prod_rapid_transit_quick_subway") } ), new PersistenceExpectation( From 208ea3c058f74ee8f114aa5ad6b640421d15bf65 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Fri, 16 Aug 2024 13:22:27 +0100 Subject: [PATCH 03/40] refactor(Various new tests): --- .../com/conveyal/gtfs/GTFSFaresV2Test.java | 583 ++++++++++++++---- .../java/com/conveyal/gtfs/TestUtils.java | 62 +- .../java/com/conveyal/gtfs/dto/AreaDTO.java | 14 + .../com/conveyal/gtfs/dto/FareLegRuleDTO.java | 20 + .../com/conveyal/gtfs/dto/FareMediaDTO.java | 15 + .../com/conveyal/gtfs/dto/FareProductDTO.java | 17 + .../gtfs/dto/FareTransferRuleDTO.java | 19 + .../com/conveyal/gtfs/dto/NetworkDTO.java | 14 + .../conveyal/gtfs/dto/RouteNetworkDTO.java | 14 + .../com/conveyal/gtfs/dto/StopAreaDTO.java | 14 + .../com/conveyal/gtfs/dto/TimeFrameDTO.java | 16 + .../gtfs/graphql/GTFSGraphQLTest.java | 93 +++ .../GTFSFaresV2Test/canFetchTimeFrames-0.json | 50 -- .../GTFSGraphQLTest}/canFetchAreas-0.json | 0 .../canFetchFareLegRules-0.json | 0 .../canFetchFareMedias-0.json | 0 .../canFetchFareProducts-0.json | 0 .../canFetchFareTransferRules-0.json | 0 .../GTFSGraphQLTest}/canFetchNetworks-0.json | 0 .../canFetchRouteNetworks-0.json | 0 .../GTFSGraphQLTest}/canFetchStopAreas-0.json | 0 21 files changed, 738 insertions(+), 193 deletions(-) create mode 100644 src/test/java/com/conveyal/gtfs/dto/AreaDTO.java create mode 100644 src/test/java/com/conveyal/gtfs/dto/FareLegRuleDTO.java create mode 100644 src/test/java/com/conveyal/gtfs/dto/FareMediaDTO.java create mode 100644 src/test/java/com/conveyal/gtfs/dto/FareProductDTO.java create mode 100644 src/test/java/com/conveyal/gtfs/dto/FareTransferRuleDTO.java create mode 100644 src/test/java/com/conveyal/gtfs/dto/NetworkDTO.java create mode 100644 src/test/java/com/conveyal/gtfs/dto/RouteNetworkDTO.java create mode 100644 src/test/java/com/conveyal/gtfs/dto/StopAreaDTO.java create mode 100644 src/test/java/com/conveyal/gtfs/dto/TimeFrameDTO.java delete mode 100644 src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchTimeFrames-0.json rename src/test/resources/snapshots/com/conveyal/gtfs/{GTFSFaresV2Test => graphql/GTFSGraphQLTest}/canFetchAreas-0.json (100%) rename src/test/resources/snapshots/com/conveyal/gtfs/{GTFSFaresV2Test => graphql/GTFSGraphQLTest}/canFetchFareLegRules-0.json (100%) rename src/test/resources/snapshots/com/conveyal/gtfs/{GTFSFaresV2Test => graphql/GTFSGraphQLTest}/canFetchFareMedias-0.json (100%) rename src/test/resources/snapshots/com/conveyal/gtfs/{GTFSFaresV2Test => graphql/GTFSGraphQLTest}/canFetchFareProducts-0.json (100%) rename src/test/resources/snapshots/com/conveyal/gtfs/{GTFSFaresV2Test => graphql/GTFSGraphQLTest}/canFetchFareTransferRules-0.json (100%) rename src/test/resources/snapshots/com/conveyal/gtfs/{GTFSFaresV2Test => graphql/GTFSGraphQLTest}/canFetchNetworks-0.json (100%) rename src/test/resources/snapshots/com/conveyal/gtfs/{GTFSFaresV2Test => graphql/GTFSGraphQLTest}/canFetchRouteNetworks-0.json (100%) rename src/test/resources/snapshots/com/conveyal/gtfs/{GTFSFaresV2Test => graphql/GTFSGraphQLTest}/canFetchStopAreas-0.json (100%) diff --git a/src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java b/src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java index 93a636157..7d2b5d7ff 100644 --- a/src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java +++ b/src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java @@ -1,143 +1,103 @@ package com.conveyal.gtfs; +import com.conveyal.gtfs.dto.AreaDTO; +import com.conveyal.gtfs.dto.FareLegRuleDTO; +import com.conveyal.gtfs.dto.FareMediaDTO; +import com.conveyal.gtfs.dto.FareProductDTO; +import com.conveyal.gtfs.dto.FareTransferRuleDTO; +import com.conveyal.gtfs.dto.NetworkDTO; +import com.conveyal.gtfs.dto.RouteNetworkDTO; +import com.conveyal.gtfs.dto.StopAreaDTO; +import com.conveyal.gtfs.dto.TimeFrameDTO; import com.conveyal.gtfs.graphql.GTFSGraphQL; import com.conveyal.gtfs.loader.FeedLoadResult; -import com.csvreader.CsvReader; -import graphql.ExecutionInput; -import org.apache.commons.io.IOUtils; -import org.apache.commons.io.input.BOMInputStream; -import org.hamcrest.MatcherAssert; +import com.conveyal.gtfs.loader.JdbcTableWriter; +import com.conveyal.gtfs.loader.Table; +import com.conveyal.gtfs.model.Area; +import com.conveyal.gtfs.model.FareLegRule; +import com.conveyal.gtfs.model.FareMedia; +import com.conveyal.gtfs.model.FareProduct; +import com.conveyal.gtfs.model.FareTransferRule; +import com.conveyal.gtfs.model.Network; +import com.conveyal.gtfs.model.RouteNetwork; +import com.conveyal.gtfs.model.StopArea; +import com.conveyal.gtfs.model.TimeFrame; +import com.conveyal.gtfs.util.InvalidNamespaceException; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.sql.DataSource; import java.io.File; -import java.io.FileInputStream; import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; +import java.sql.ResultSet; +import java.sql.SQLException; import java.time.Duration; -import java.util.HashMap; -import java.util.Map; -import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import static com.conveyal.gtfs.GTFS.load; +import static com.conveyal.gtfs.GTFS.makeSnapshot; import static com.conveyal.gtfs.GTFS.validate; +import static com.conveyal.gtfs.TestUtils.assertResultValue; +import static com.conveyal.gtfs.TestUtils.assertThatSqlQueryYieldsZeroRows; import static com.conveyal.gtfs.TestUtils.checkFileTestCases; -import static com.conveyal.gtfs.TestUtils.getResourceFileName; -import static com.zenika.snapshotmatcher.SnapshotMatcher.matchesSnapshot; -import static org.hamcrest.CoreMatchers.is; -import static org.hamcrest.CoreMatchers.notNullValue; -import static org.hamcrest.MatcherAssert.assertThat; +import static com.conveyal.gtfs.TestUtils.getColumnsForId; +import static com.conveyal.gtfs.TestUtils.getResultSetForId; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTimeout; import static org.junit.jupiter.api.Assertions.assertTrue; public class GTFSFaresV2Test { - private static String simpleGtfsZipFileName; - public static String testDBName; - private static DataSource testDataSource; - private static String testNamespace; + private static final Logger LOG = LoggerFactory.getLogger(GTFSFaresV2Test.class); + private static String faresZipFileName; + public static String faresDBName; + private static DataSource faresDataSource; + private static String faresNamespace; private static final int TEST_TIMEOUT = 5000; + private static final ObjectMapper mapper = new ObjectMapper(); + + private static JdbcTableWriter createTestTableWriter (Table table) throws InvalidNamespaceException { + return new JdbcTableWriter(table, faresDataSource, faresNamespace); + } @BeforeAll public static void setUpClass() throws IOException { String folderName = "fake-agency-with-fares-v2"; - simpleGtfsZipFileName = TestUtils.zipFolderFiles(folderName, true); + faresZipFileName = TestUtils.zipFolderFiles(folderName, true); // create a new database - testDBName = TestUtils.generateNewDB(); - String dbConnectionUrl = String.format("jdbc:postgresql://localhost/%s", testDBName); - testDataSource = TestUtils.createTestDataSource(dbConnectionUrl); - // zip up test folder into temp zip file - String zipFileName = TestUtils.zipFolderFiles(folderName, true); + faresDBName = TestUtils.generateNewDB(); + String dbConnectionUrl = String.format("jdbc:postgresql://localhost/%s", faresDBName); + faresDataSource = TestUtils.createTestDataSource(dbConnectionUrl); // load feed into db - FeedLoadResult feedLoadResult = load(zipFileName, testDataSource); - testNamespace = feedLoadResult.uniqueIdentifier; + FeedLoadResult feedLoadResult = load(faresZipFileName, faresDataSource); + faresNamespace = feedLoadResult.uniqueIdentifier; // validate feed to create additional tables - validate(testNamespace, testDataSource); + validate(faresNamespace, faresDataSource); + // Create an empty snapshot to create a new namespace and all the tables + FeedLoadResult result = makeSnapshot(null, faresDataSource, false); + faresNamespace = result.uniqueIdentifier; } @AfterAll public static void tearDownClass() { - TestUtils.dropDB(testDBName); + TestUtils.dropDB(faresDBName); } /** Tests that the graphQL schema can initialize. */ @Test void canInitialize() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { - GTFSGraphQL.initialize(testDataSource); + GTFSGraphQL.initialize(faresDataSource); GTFSGraphQL.getGraphQl(); }); } - @Test - void canFetchAreas() { - assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { - MatcherAssert.assertThat(queryGraphQL("feedAreas.txt"), matchesSnapshot()); - }); - } - - @Test - void canFetchStopAreas() { - assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { - MatcherAssert.assertThat(queryGraphQL("feedStopAreas.txt"), matchesSnapshot()); - }); - } - - @Test - void canFetchFareTransferRules() { - assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { - MatcherAssert.assertThat(queryGraphQL("feedFareTransferRules.txt"), matchesSnapshot()); - }); - } - - @Test - void canFetchFareProducts() { - assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { - MatcherAssert.assertThat(queryGraphQL("feedFareProducts.txt"), matchesSnapshot()); - }); - } - - @Test - void canFetchFareMedias() { - assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { - MatcherAssert.assertThat(queryGraphQL("feedFareMedias.txt"), matchesSnapshot()); - }); - } - - @Test - void canFetchFareLegRules() { - assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { - MatcherAssert.assertThat(queryGraphQL("feedFareLegRules.txt"), matchesSnapshot()); - }); - } - - @Test - void canFetchTimeFrames() { - assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { - MatcherAssert.assertThat(queryGraphQL("feedTimeFrames.txt"), matchesSnapshot()); - }); - } - - @Test - void canFetchNetworks() { - assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { - MatcherAssert.assertThat(queryGraphQL("feedNetworks.txt"), matchesSnapshot()); - }); - } - - @Test - void canFetchRouteNetworks() { - assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { - MatcherAssert.assertThat(queryGraphQL("feedRouteNetworks.txt"), matchesSnapshot()); - }); - } - /** * Make sure a round-trip of loading fares v2 data and then writing this to another zip file can be performed. */ @@ -149,7 +109,7 @@ void canDoRoundTripLoadAndWriteToZipFile() throws IOException { // delete file to make sure we can assert that this program created the file outZip.delete(); - GTFSFeed feed = GTFSFeed.fromFile(simpleGtfsZipFileName); + GTFSFeed feed = GTFSFeed.fromFile(faresZipFileName); feed.toFile(outZip.getAbsolutePath()); feed.close(); assertTrue(outZip.exists()); @@ -237,39 +197,414 @@ void canDoRoundTripLoadAndWriteToZipFile() throws IOException { checkFileTestCases(zip, fileTestCases); } + @Test + void canCreateUpdateAndDeleteAreas() throws IOException, SQLException, InvalidNamespaceException { + // Create. + String areaId = "area-id-1"; + AreaDTO createdArea = createArea(areaId); + assertEquals(createdArea.area_id, areaId); + + // Update. + String updatedAreaId = "area-id-2"; + createdArea.area_id = updatedAreaId; + JdbcTableWriter updateTableWriter = createTestTableWriter(Table.AREAS); + String updateOutput = updateTableWriter.update( + createdArea.id, + mapper.writeValueAsString(createdArea), + true + ); + AreaDTO updatedAreaDTO = mapper.readValue(updateOutput, AreaDTO.class); + assertEquals(updatedAreaDTO.area_id, updatedAreaId); + + ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, updatedAreaDTO.id, Table.AREAS); + while (resultSet.next()) { + assertResultValue(resultSet, Area.AREA_ID_COLUMN_NAME, equalTo(createdArea.area_id)); + assertResultValue(resultSet, Area.AREA_NAME_COLUMN_NAME,equalTo(createdArea.area_name)); + } + + // Delete. + deleteRecord(Table.AREAS, createdArea.id); + } + + @Test + void canCreateUpdateAndDeleteStopAreas() throws IOException, SQLException, InvalidNamespaceException { + // Create. + String areaId = "area-id-1"; + StopAreaDTO createdStopArea = createStopArea(areaId); + assertEquals(createdStopArea.area_id, areaId); + + // Update. + String updatedAreaId = "area-id-2"; + createdStopArea.area_id = updatedAreaId; + JdbcTableWriter updateTableWriter = createTestTableWriter(Table.STOP_AREAS); + String updateOutput = updateTableWriter.update( + createdStopArea.id, + mapper.writeValueAsString(createdStopArea), + true + ); + StopAreaDTO updatedStopAreaDTO = mapper.readValue(updateOutput, StopAreaDTO.class); + assertEquals(updatedStopAreaDTO.area_id, updatedAreaId); + + ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, updatedStopAreaDTO.id, Table.STOP_AREAS); + while (resultSet.next()) { + assertResultValue(resultSet, StopArea.AREA_ID_COLUMN_NAME, equalTo(createdStopArea.area_id)); + assertResultValue(resultSet, StopArea.STOP_ID_COLUMN_NAME,equalTo(createdStopArea.stop_id)); + } + + // Delete. + deleteRecord(Table.STOP_AREAS, createdStopArea.id); + } + + @Test + void canCreateUpdateAndDeleteTimeFrames() throws IOException, SQLException, InvalidNamespaceException { + // Create. + String timeFrameGroupId = "time-frame-group-id-1"; + TimeFrameDTO createdTimeFrame = createTimeFrame(timeFrameGroupId); + assertEquals(createdTimeFrame.timeframe_group_id, timeFrameGroupId); + + // Update. + String updatedTimeFrameGroupId = "time-frame-group-id-2"; + createdTimeFrame.timeframe_group_id = updatedTimeFrameGroupId; + JdbcTableWriter updateTableWriter = createTestTableWriter(Table.TIME_FRAMES); + String updateOutput = updateTableWriter.update( + createdTimeFrame.id, + mapper.writeValueAsString(createdTimeFrame), + true + ); + TimeFrameDTO updatedTimeFrameDTO = mapper.readValue(updateOutput, TimeFrameDTO.class); + assertEquals(updatedTimeFrameDTO.timeframe_group_id, updatedTimeFrameGroupId); + + ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, updatedTimeFrameDTO.id, Table.TIME_FRAMES); + while (resultSet.next()) { + assertResultValue(resultSet, TimeFrame.TIME_FRAME_GROUP_ID_COLUMN_NAME, equalTo(createdTimeFrame.timeframe_group_id)); + assertResultValue(resultSet, TimeFrame.START_TIME_COLUMN_NAME, equalTo(createdTimeFrame.start_time)); + assertResultValue(resultSet, TimeFrame.END_TIME_COLUMN_NAME, equalTo(createdTimeFrame.end_time)); + assertResultValue(resultSet, TimeFrame.SERVICE_ID_COLUMN_NAME, equalTo(createdTimeFrame.service_id)); + } + + // Delete. + deleteRecord(Table.TIME_FRAMES, createdTimeFrame.id); + } + + @Test + void canCreateUpdateAndDeleteNetworks() throws IOException, SQLException, InvalidNamespaceException { + // Create. + String networkId = "network-id-1"; + NetworkDTO createdNetwork = createNetwork(networkId); + assertEquals(createdNetwork.network_id, networkId); + + // Update. + String updatedNetworkId = "network-id-2"; + createdNetwork.network_id = updatedNetworkId; + JdbcTableWriter updateTableWriter = createTestTableWriter(Table.NETWORKS); + String updateOutput = updateTableWriter.update( + createdNetwork.id, + mapper.writeValueAsString(createdNetwork), + true + ); + NetworkDTO updatedNetworkDTO = mapper.readValue(updateOutput, NetworkDTO.class); + assertEquals(updatedNetworkDTO.network_id, updatedNetworkId); + + ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, updatedNetworkDTO.id, Table.NETWORKS); + while (resultSet.next()) { + assertResultValue(resultSet, Network.NETWORK_ID_COLUMN_NAME, equalTo(createdNetwork.network_id)); + assertResultValue(resultSet, Network.NETWORK_NAME_COLUMN_NAME, equalTo(createdNetwork.network_name)); + } + + // Delete. + deleteRecord(Table.NETWORKS, createdNetwork.id); + } + + @Test + void canCreateUpdateAndDeleteRouteNetworks() throws IOException, SQLException, InvalidNamespaceException { + // Create. + String networkId = "network-id-1"; + RouteNetworkDTO createdRouteNetwork = createRouteNetwork(networkId); + assertEquals(createdRouteNetwork.network_id, networkId); + + // Update. + String updatedRouteNetworkId = "network-id-2"; + createdRouteNetwork.network_id = updatedRouteNetworkId; + JdbcTableWriter updateTableWriter = createTestTableWriter(Table.ROUTE_NETWORKS); + String updateOutput = updateTableWriter.update( + createdRouteNetwork.id, + mapper.writeValueAsString(createdRouteNetwork), + true + ); + RouteNetworkDTO updatedNetworkDTO = mapper.readValue(updateOutput, RouteNetworkDTO.class); + assertEquals(updatedNetworkDTO.network_id, updatedRouteNetworkId); + + ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, updatedNetworkDTO.id, Table.ROUTE_NETWORKS); + while (resultSet.next()) { + assertResultValue(resultSet, RouteNetwork.NETWORK_ID_COLUMN_NAME, equalTo(createdRouteNetwork.network_id)); + assertResultValue(resultSet, RouteNetwork.ROUTE_ID_COLUMN_NAME, equalTo(createdRouteNetwork.route_id)); + } + + // Delete. + deleteRecord(Table.ROUTE_NETWORKS, createdRouteNetwork.id); + } + + @Test + void canCreateUpdateAndDeleteFareLegRules() throws IOException, SQLException, InvalidNamespaceException { + // Create. + String legGroupId = "leg-group-id-1"; + FareLegRuleDTO createdFareLegRule = createFareLegRule(legGroupId); + assertEquals(createdFareLegRule.leg_group_id, legGroupId); + + // Update. + String updatedLegGroupId = "leg-group-id-2"; + createdFareLegRule.leg_group_id = updatedLegGroupId; + JdbcTableWriter updateTableWriter = createTestTableWriter(Table.FARE_LEG_RULES); + String updateOutput = updateTableWriter.update( + createdFareLegRule.id, + mapper.writeValueAsString(createdFareLegRule), + true + ); + FareLegRuleDTO updatedFareLegRuleDTO = mapper.readValue(updateOutput, FareLegRuleDTO.class); + assertEquals(updatedFareLegRuleDTO.leg_group_id, updatedLegGroupId); + + ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, updatedFareLegRuleDTO.id, Table.FARE_LEG_RULES); + while (resultSet.next()) { + assertResultValue(resultSet, FareLegRule.LEG_GROUP_ID_COLUMN_NAME, equalTo(createdFareLegRule.leg_group_id)); + assertResultValue(resultSet, FareLegRule.NETWORK_ID_COLUMN_NAME, equalTo(createdFareLegRule.network_id)); + assertResultValue(resultSet, FareLegRule.FROM_AREA_ID_COLUMN_NAME, equalTo(createdFareLegRule.from_area_id)); + assertResultValue(resultSet, FareLegRule.TO_AREA_ID_COLUMN_NAME, equalTo(createdFareLegRule.to_area_id)); + assertResultValue(resultSet, FareLegRule.FROM_TIMEFRAME_GROUP_ID_COLUMN_NAME, equalTo(createdFareLegRule.from_timeframe_group_id)); + assertResultValue(resultSet, FareLegRule.FARE_PRODUCT_ID_COLUMN_NAME, equalTo(createdFareLegRule.fare_product_id)); + assertResultValue(resultSet, FareLegRule.RULE_PRIORITY_COLUMN_NAME, equalTo(createdFareLegRule.rule_priority)); + } + + // Delete. + deleteRecord(Table.FARE_LEG_RULES, createdFareLegRule.id); + } + + @Test + void canCreateUpdateAndDeleteFareMedia() throws IOException, SQLException, InvalidNamespaceException { + // Create. + String fareMediaId = "fare-media-id-1"; + FareMediaDTO createdFareMedia = createFareMedia(fareMediaId); + assertEquals(createdFareMedia.fare_media_id, fareMediaId); + + // Update. + String updatedFareMediaId = "fare-media-id-2"; + createdFareMedia.fare_media_id = updatedFareMediaId; + JdbcTableWriter updateTableWriter = createTestTableWriter(Table.FARE_MEDIAS); + String updateOutput = updateTableWriter.update( + createdFareMedia.id, + mapper.writeValueAsString(createdFareMedia), + true + ); + FareMediaDTO updatedFareMediaDTO = mapper.readValue(updateOutput, FareMediaDTO.class); + assertEquals(updatedFareMediaDTO.fare_media_id, updatedFareMediaId); + + ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, updatedFareMediaDTO.id, Table.FARE_MEDIAS); + while (resultSet.next()) { + assertResultValue(resultSet, FareMedia.FARE_MEDIA_ID_COLUMN_NAME, equalTo(createdFareMedia.fare_media_id)); + assertResultValue(resultSet, FareMedia.FARE_MEDIA_NAME_COLUMN_NAME, equalTo(createdFareMedia.fare_media_name)); + assertResultValue(resultSet, FareMedia.FARE_MEDIA_TYPE_COLUMN_NAME, equalTo(createdFareMedia.fare_media_type)); + } + + // Delete. + deleteRecord(Table.FARE_MEDIAS, createdFareMedia.id); + } + + @Test + void canCreateUpdateAndDeleteFareProduct() throws IOException, SQLException, InvalidNamespaceException { + // Create. + String fareProductId = "fare-product-id-1"; + FareProductDTO createdFareProduct = createFareProduct(fareProductId); + assertEquals(createdFareProduct.fare_product_id, fareProductId); + + // Update. + String updatedFareProductId = "fare-product-id-2"; + createdFareProduct.fare_product_id = updatedFareProductId; + JdbcTableWriter updateTableWriter = createTestTableWriter(Table.FARE_PRODUCTS); + String updateOutput = updateTableWriter.update( + createdFareProduct.id, + mapper.writeValueAsString(createdFareProduct), + true + ); + FareProductDTO fareProductDTO = mapper.readValue(updateOutput, FareProductDTO.class); + assertEquals(fareProductDTO.fare_product_id, updatedFareProductId); + + ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, fareProductDTO.id, Table.FARE_PRODUCTS); + while (resultSet.next()) { + assertResultValue(resultSet, FareProduct.FARE_PRODUCT_ID_COLUMN_NAME, equalTo(createdFareProduct.fare_product_id)); + assertResultValue(resultSet, FareProduct.FARE_PRODUCT_NAME_COLUMN_NAME, equalTo(createdFareProduct.fare_product_name)); + assertResultValue(resultSet, FareProduct.FARE_MEDIA_ID_COLUMN_NAME, equalTo(createdFareProduct.fare_media_id)); + } + + // Delete. + deleteRecord(Table.FARE_PRODUCTS, createdFareProduct.id); + } + + @Test + void canCreateUpdateAndDeleteFareTransferRule() throws IOException, SQLException, InvalidNamespaceException { + // Create. + String fromLegGroupId = "from-leg-group-id-1"; + String toLegGroupId = "to-leg-group-id-1"; + FareTransferRuleDTO fareTransferRules = createFareTransferRules(fromLegGroupId, toLegGroupId); + assertEquals(fareTransferRules.from_leg_group_id, fromLegGroupId); + assertEquals(fareTransferRules.to_leg_group_id, toLegGroupId); + + // Update. + String updatedFromLegGroupId = "from-leg-group-id-2"; + String updatedToLegGroupId = "to-leg-group-id-2"; + fareTransferRules.from_leg_group_id = updatedFromLegGroupId; + fareTransferRules.to_leg_group_id = updatedToLegGroupId; + JdbcTableWriter updateTableWriter = createTestTableWriter(Table.FARE_TRANSFER_RULES); + String updateOutput = updateTableWriter.update( + fareTransferRules.id, + mapper.writeValueAsString(fareTransferRules), + true + ); + FareTransferRuleDTO fareTransferRuleDTO = mapper.readValue(updateOutput, FareTransferRuleDTO.class); + assertEquals(fareTransferRuleDTO.from_leg_group_id, updatedFromLegGroupId); + assertEquals(fareTransferRuleDTO.to_leg_group_id, updatedToLegGroupId); + + ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, fareTransferRuleDTO.id, Table.FARE_TRANSFER_RULES); + while (resultSet.next()) { + assertResultValue(resultSet, FareTransferRule.FROM_LEG_GROUP_ID_COLUMN_NAME, equalTo(fareTransferRules.from_leg_group_id)); + assertResultValue(resultSet, FareTransferRule.TO_LEG_GROUP_ID_COLUMN_NAME, equalTo(fareTransferRules.to_leg_group_id)); + assertResultValue(resultSet, FareTransferRule.TRANSFER_COUNT_COLUMN_NAME, equalTo(fareTransferRules.transfer_count)); + assertResultValue(resultSet, FareTransferRule.DURATION_LIMIT_COLUMN_NAME, equalTo(fareTransferRules.duration_limit)); + assertResultValue(resultSet, FareTransferRule.FARE_TRANSFER_TYPE_COLUMN_NAME, equalTo(fareTransferRules.fare_transfer_type)); + assertResultValue(resultSet, FareTransferRule.FARE_PRODUCT_ID_COLUMN_NAME, equalTo(fareTransferRules.fare_product_id)); + } + + // Delete. + deleteRecord(Table.FARE_TRANSFER_RULES, fareTransferRules.id); + } + + private void deleteRecord(Table table, Integer id) throws InvalidNamespaceException, SQLException { + JdbcTableWriter deleteTableWriter = createTestTableWriter(table); + int deleteOutput = deleteTableWriter.delete(id, true); + LOG.info("deleted {} records from {}", deleteOutput, table.name); + assertThatSqlQueryYieldsZeroRows(faresDataSource, getColumnsForId(faresNamespace, id, table)); + } + /** - * Helper method to make a query with default variables. - * - * @param queryFilename the filename that should be used to generate the GraphQL query. This file must be present - * in the `src/test/resources/graphql` folder + * Create and store an area for testing. */ - private Map queryGraphQL(String queryFileName) throws IOException { - Map variables = new HashMap<>(); - variables.put("namespace", testNamespace); - return queryGraphQL(queryFileName, variables, testDataSource); + private static AreaDTO createArea(String areaId) throws InvalidNamespaceException, IOException, SQLException { + AreaDTO input = new AreaDTO(); + input.area_id = areaId; + input.area_name = "area-name"; + JdbcTableWriter createTableWriter = createTestTableWriter(Table.AREAS); + String output = createTableWriter.create(mapper.writeValueAsString(input), true); + return mapper.readValue(output, AreaDTO.class); } /** - * Helper method to execute a GraphQL query and return the result. - * - * @param queryFilename the filename that should be used to generate the GraphQL query. This file must be present - * in the `src/test/resources/graphql` folder - * @param variables a Map of input variables to the graphql query about to be executed - * @param dataSource the datasource to use when initializing GraphQL + * Create and store a stop area for testing. */ - private Map queryGraphQL( - String queryFilename, - Map variables, - DataSource dataSource - ) throws IOException { - GTFSGraphQL.initialize(dataSource); - FileInputStream inputStream = new FileInputStream( - getResourceFileName(String.format("graphql/%s", queryFilename)) - ); - ExecutionInput executionInput = ExecutionInput.newExecutionInput() - .query(IOUtils.toString(inputStream)) - .variables(variables) - .build(); - return GTFSGraphQL.getGraphQl().execute(executionInput).toSpecification(); + private static StopAreaDTO createStopArea(String areaId) throws InvalidNamespaceException, IOException, SQLException { + StopAreaDTO input = new StopAreaDTO(); + input.area_id = areaId; + input.stop_id = "stop-id-1"; + JdbcTableWriter createTableWriter = createTestTableWriter(Table.STOP_AREAS); + String output = createTableWriter.create(mapper.writeValueAsString(input), true); + return mapper.readValue(output, StopAreaDTO.class); + } + + /** + * Create and store a time frame for testing. + */ + private static TimeFrameDTO createTimeFrame(String timeframeGroupId) throws InvalidNamespaceException, IOException, SQLException { + TimeFrameDTO input = new TimeFrameDTO(); + input.timeframe_group_id = timeframeGroupId; + input.start_time = 0; + input.end_time = 2600; + input.service_id = "service-id-1"; + JdbcTableWriter createTableWriter = createTestTableWriter(Table.TIME_FRAMES); + String output = createTableWriter.create(mapper.writeValueAsString(input), true); + return mapper.readValue(output, TimeFrameDTO.class); + } + + /** + * Create and store a network for testing. + */ + private static NetworkDTO createNetwork(String networkId) throws InvalidNamespaceException, IOException, SQLException { + NetworkDTO input = new NetworkDTO(); + input.network_id = networkId; + input.network_name = "network-name-1"; + JdbcTableWriter createTableWriter = createTestTableWriter(Table.NETWORKS); + String output = createTableWriter.create(mapper.writeValueAsString(input), true); + return mapper.readValue(output, NetworkDTO.class); + } + + /** + * Create and store a route network for testing. + */ + private static RouteNetworkDTO createRouteNetwork(String networkId) throws InvalidNamespaceException, IOException, SQLException { + RouteNetworkDTO input = new RouteNetworkDTO(); + input.network_id = networkId; + input.route_id = "route-id-1"; + JdbcTableWriter createTableWriter = createTestTableWriter(Table.ROUTE_NETWORKS); + String output = createTableWriter.create(mapper.writeValueAsString(input), true); + return mapper.readValue(output, RouteNetworkDTO.class); + } + + /** + * Create and store a fare leg rule for testing. + */ + private static FareLegRuleDTO createFareLegRule(String legGroupId) throws InvalidNamespaceException, IOException, SQLException { + FareLegRuleDTO input = new FareLegRuleDTO(); + input.leg_group_id = legGroupId; + input.network_id = "network-id-1"; + input.from_area_id = "from-area-id-1"; + input.to_area_id = "to-area-id-1"; + input.from_timeframe_group_id = "from-timeframe-group-id-1"; + input.to_timeframe_group_id = "to-timeframe-group-id-1"; + input.fare_product_id = "fare-product-id-1"; + input.rule_priority = 1; + JdbcTableWriter createTableWriter = createTestTableWriter(Table.FARE_LEG_RULES); + String output = createTableWriter.create(mapper.writeValueAsString(input), true); + return mapper.readValue(output, FareLegRuleDTO.class); + } + + /** + * Create and store a fare media for testing. + */ + private static FareMediaDTO createFareMedia(String fareMediaId) throws InvalidNamespaceException, IOException, SQLException { + FareMediaDTO input = new FareMediaDTO(); + input.fare_media_id = fareMediaId; + input.fare_media_name = "fare-media-name"; + input.fare_media_type = 1; + JdbcTableWriter createTableWriter = createTestTableWriter(Table.FARE_MEDIAS); + String output = createTableWriter.create(mapper.writeValueAsString(input), true); + return mapper.readValue(output, FareMediaDTO.class); + } + + /** + * Create and store a fare product for testing. + */ + private static FareProductDTO createFareProduct(String fareProductId) throws InvalidNamespaceException, IOException, SQLException { + FareProductDTO input = new FareProductDTO(); + input.fare_product_id = fareProductId; + input.fare_product_name = "fare-product-name"; + input.fare_media_id = "fare-media-id"; + input.amount = 6.25; + input.currency = "USD"; + JdbcTableWriter createTableWriter = createTestTableWriter(Table.FARE_PRODUCTS); + String output = createTableWriter.create(mapper.writeValueAsString(input), true); + return mapper.readValue(output, FareProductDTO.class); + } + + /** + * Create and store a fare transfer rules for testing. + */ + private static FareTransferRuleDTO createFareTransferRules(String fromLegGroupId, String toLegGroupId) throws InvalidNamespaceException, IOException, SQLException { + FareTransferRuleDTO input = new FareTransferRuleDTO(); + input.from_leg_group_id = fromLegGroupId; + input.to_leg_group_id = toLegGroupId; + input.transfer_count = -1; + input.duration_limit = 1; + input.duration_limit_type = 1; + input.fare_transfer_type = 2; + input.fare_product_id = "fare-product-id-1"; + JdbcTableWriter createTableWriter = createTestTableWriter(Table.FARE_TRANSFER_RULES); + String output = createTableWriter.create(mapper.writeValueAsString(input), true); + return mapper.readValue(output, FareTransferRuleDTO.class); } } diff --git a/src/test/java/com/conveyal/gtfs/TestUtils.java b/src/test/java/com/conveyal/gtfs/TestUtils.java index a346c437a..a378f5492 100644 --- a/src/test/java/com/conveyal/gtfs/TestUtils.java +++ b/src/test/java/com/conveyal/gtfs/TestUtils.java @@ -1,24 +1,11 @@ package com.conveyal.gtfs; import com.conveyal.gtfs.error.NewGTFSErrorType; -import com.conveyal.gtfs.loader.FeedLoadResult; -import com.conveyal.gtfs.loader.SnapshotResult; import com.conveyal.gtfs.loader.Table; -import com.conveyal.gtfs.storage.ErrorExpectation; -import com.conveyal.gtfs.storage.ExpectedFieldType; -import com.conveyal.gtfs.storage.PersistenceExpectation; -import com.conveyal.gtfs.storage.RecordExpectation; -import com.conveyal.gtfs.util.InvalidNamespaceException; -import com.conveyal.gtfs.validator.FeedValidatorCreator; -import com.conveyal.gtfs.validator.ValidationResult; import com.csvreader.CsvReader; -import com.google.common.collect.ArrayListMultimap; -import com.google.common.collect.Multimap; import org.apache.commons.io.IOUtils; import org.apache.commons.io.input.BOMInputStream; import org.hamcrest.Matcher; -import org.hamcrest.comparator.ComparatorMatcherBuilder; -import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,25 +15,19 @@ import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; import static com.conveyal.gtfs.util.Util.randomIdString; -import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.core.IsNull.nullValue; import static org.junit.jupiter.api.Assertions.assertTrue; public class TestUtils { @@ -301,4 +282,47 @@ public static void checkFileTestCases(ZipFile zip, FileTestCase[] fileTestCases) ); } } + + + /** + * Asserts that a given value for the specified field in result set matches provided matcher. + */ + public static void assertResultValue(ResultSet resultSet, String field, Matcher matcher) throws SQLException { + assertThat(resultSet.getObject(field), matcher); + } + + /** + * Executes SQL query for the specified ID and columns and returns the resulting result set. + */ + public static ResultSet getResultSetForId(DataSource dataSource, String namespace, int id, Table table, String... columns) throws SQLException { + String sql = getColumnsForId(namespace, id, table, columns); + return dataSource.getConnection().prepareStatement(sql).executeQuery(); + } + + /** + * Constructs SQL query for the specified ID and columns and returns the resulting result set. + */ + public static String getColumnsForId(String namespace, int id, Table table, String... columns) { + String sql = String.format( + "select %s from %s.%s where id=%d", + columns.length > 0 ? String.join(", ", columns) : "*", + namespace, + table.name, + id + ); + LOG.info(sql); + return sql; + } + + public static void assertThatSqlQueryYieldsRowCount(DataSource dataSource, String sql, int expectedRowCount) throws SQLException { + LOG.info(sql); + int recordCount = 0; + ResultSet rs = dataSource.getConnection().prepareStatement(sql).executeQuery(); + while (rs.next()) recordCount++; + assertThat("Records matching query should equal expected count.", recordCount, equalTo(expectedRowCount)); + } + + public static void assertThatSqlQueryYieldsZeroRows(DataSource dataSource, String sql) throws SQLException { + assertThatSqlQueryYieldsRowCount(dataSource, sql, 0); + } } diff --git a/src/test/java/com/conveyal/gtfs/dto/AreaDTO.java b/src/test/java/com/conveyal/gtfs/dto/AreaDTO.java new file mode 100644 index 000000000..0ea3b0e52 --- /dev/null +++ b/src/test/java/com/conveyal/gtfs/dto/AreaDTO.java @@ -0,0 +1,14 @@ +package com.conveyal.gtfs.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * DTO used to model expected {@link com.conveyal.gtfs.model.Area} JSON structure for the editor. NOTE: reference types + * (e.g., Integer and Double) are used here in order to model null/empty values in JSON object. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class AreaDTO { + public Integer id; + public String area_id; + public String area_name; +} diff --git a/src/test/java/com/conveyal/gtfs/dto/FareLegRuleDTO.java b/src/test/java/com/conveyal/gtfs/dto/FareLegRuleDTO.java new file mode 100644 index 000000000..457d375ab --- /dev/null +++ b/src/test/java/com/conveyal/gtfs/dto/FareLegRuleDTO.java @@ -0,0 +1,20 @@ +package com.conveyal.gtfs.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * DTO used to model expected {@link com.conveyal.gtfs.model.FareLegRule} JSON structure for the editor. NOTE: reference types + * (e.g., Integer and Double) are used here in order to model null/empty values in JSON object. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class FareLegRuleDTO { + public Integer id; + public String leg_group_id; + public String network_id; + public String from_area_id; + public String to_area_id; + public String from_timeframe_group_id; + public String to_timeframe_group_id; + public String fare_product_id; + public int rule_priority; +} diff --git a/src/test/java/com/conveyal/gtfs/dto/FareMediaDTO.java b/src/test/java/com/conveyal/gtfs/dto/FareMediaDTO.java new file mode 100644 index 000000000..23b47d51b --- /dev/null +++ b/src/test/java/com/conveyal/gtfs/dto/FareMediaDTO.java @@ -0,0 +1,15 @@ +package com.conveyal.gtfs.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * DTO used to model expected {@link com.conveyal.gtfs.model.FareMedia} JSON structure for the editor. NOTE: reference types + * (e.g., Integer and Double) are used here in order to model null/empty values in JSON object. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class FareMediaDTO { + public Integer id; + public String fare_media_id; + public String fare_media_name; + public int fare_media_type; +} diff --git a/src/test/java/com/conveyal/gtfs/dto/FareProductDTO.java b/src/test/java/com/conveyal/gtfs/dto/FareProductDTO.java new file mode 100644 index 000000000..57cecd2ee --- /dev/null +++ b/src/test/java/com/conveyal/gtfs/dto/FareProductDTO.java @@ -0,0 +1,17 @@ +package com.conveyal.gtfs.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * DTO used to model expected {@link com.conveyal.gtfs.model.FareProduct} JSON structure for the editor. NOTE: reference types + * (e.g., Integer and Double) are used here in order to model null/empty values in JSON object. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class FareProductDTO { + public Integer id; + public String fare_product_id; + public String fare_product_name; + public String fare_media_id; + public double amount; + public String currency; +} diff --git a/src/test/java/com/conveyal/gtfs/dto/FareTransferRuleDTO.java b/src/test/java/com/conveyal/gtfs/dto/FareTransferRuleDTO.java new file mode 100644 index 000000000..738132cb8 --- /dev/null +++ b/src/test/java/com/conveyal/gtfs/dto/FareTransferRuleDTO.java @@ -0,0 +1,19 @@ +package com.conveyal.gtfs.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * DTO used to model expected {@link com.conveyal.gtfs.model.FareTransferRule} JSON structure for the editor. NOTE: reference types + * (e.g., Integer and Double) are used here in order to model null/empty values in JSON object. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class FareTransferRuleDTO { + public Integer id; + public String from_leg_group_id; + public String to_leg_group_id; + public int transfer_count; + public int duration_limit; + public int duration_limit_type; + public int fare_transfer_type; + public String fare_product_id; +} diff --git a/src/test/java/com/conveyal/gtfs/dto/NetworkDTO.java b/src/test/java/com/conveyal/gtfs/dto/NetworkDTO.java new file mode 100644 index 000000000..3e34c7d34 --- /dev/null +++ b/src/test/java/com/conveyal/gtfs/dto/NetworkDTO.java @@ -0,0 +1,14 @@ +package com.conveyal.gtfs.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * DTO used to model expected {@link com.conveyal.gtfs.model.Network} JSON structure for the editor. NOTE: reference types + * (e.g., Integer and Double) are used here in order to model null/empty values in JSON object. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class NetworkDTO { + public Integer id; + public String network_id; + public String network_name; +} diff --git a/src/test/java/com/conveyal/gtfs/dto/RouteNetworkDTO.java b/src/test/java/com/conveyal/gtfs/dto/RouteNetworkDTO.java new file mode 100644 index 000000000..1ad077c5e --- /dev/null +++ b/src/test/java/com/conveyal/gtfs/dto/RouteNetworkDTO.java @@ -0,0 +1,14 @@ +package com.conveyal.gtfs.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * DTO used to model expected {@link com.conveyal.gtfs.model.RouteNetwork} JSON structure for the editor. NOTE: reference types + * (e.g., Integer and Double) are used here in order to model null/empty values in JSON object. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class RouteNetworkDTO { + public Integer id; + public String network_id; + public String route_id; +} diff --git a/src/test/java/com/conveyal/gtfs/dto/StopAreaDTO.java b/src/test/java/com/conveyal/gtfs/dto/StopAreaDTO.java new file mode 100644 index 000000000..66003fca6 --- /dev/null +++ b/src/test/java/com/conveyal/gtfs/dto/StopAreaDTO.java @@ -0,0 +1,14 @@ +package com.conveyal.gtfs.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * DTO used to model expected {@link com.conveyal.gtfs.model.StopArea} JSON structure for the editor. NOTE: reference types + * (e.g., Integer and Double) are used here in order to model null/empty values in JSON object. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class StopAreaDTO { + public Integer id; + public String area_id; + public String stop_id; +} diff --git a/src/test/java/com/conveyal/gtfs/dto/TimeFrameDTO.java b/src/test/java/com/conveyal/gtfs/dto/TimeFrameDTO.java new file mode 100644 index 000000000..c1df7d7da --- /dev/null +++ b/src/test/java/com/conveyal/gtfs/dto/TimeFrameDTO.java @@ -0,0 +1,16 @@ +package com.conveyal.gtfs.dto; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +/** + * DTO used to model expected {@link com.conveyal.gtfs.model.TimeFrame} JSON structure for the editor. NOTE: reference types + * (e.g., Integer and Double) are used here in order to model null/empty values in JSON object. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class TimeFrameDTO { + public Integer id; + public String timeframe_group_id; + public Integer start_time; + public Integer end_time; + public String service_id; +} diff --git a/src/test/java/com/conveyal/gtfs/graphql/GTFSGraphQLTest.java b/src/test/java/com/conveyal/gtfs/graphql/GTFSGraphQLTest.java index 240683306..176e5c030 100644 --- a/src/test/java/com/conveyal/gtfs/graphql/GTFSGraphQLTest.java +++ b/src/test/java/com/conveyal/gtfs/graphql/GTFSGraphQLTest.java @@ -40,6 +40,11 @@ public class GTFSGraphQLTest { private static DataSource testInjectionDataSource; private static String testInjectionNamespace; private static String badCalendarDateNamespace; + + public static String faresDBName; + private static DataSource faresDataSource; + private static String faresNamespace; + private static final int TEST_TIMEOUT = 5000; @BeforeAll @@ -75,12 +80,25 @@ public static void setUpClass() throws IOException { testInjectionNamespace = injectionFeedLoadResult.uniqueIdentifier; // validate feed to create additional tables validate(testInjectionNamespace, testInjectionDataSource); + + String folderName = "fake-agency-with-fares-v2"; + String faresZipFileName = TestUtils.zipFolderFiles(folderName, true); + // create a new database + faresDBName = TestUtils.generateNewDB(); + String faresConnectionUrl = String.format("jdbc:postgresql://localhost/%s", faresDBName); + faresDataSource = TestUtils.createTestDataSource(faresConnectionUrl); + // load feed into db + FeedLoadResult faresFeedLoadResult = load(faresZipFileName, faresDataSource); + faresNamespace = faresFeedLoadResult.uniqueIdentifier; + // validate feed to create additional tables + validate(faresNamespace, faresDataSource); } @AfterAll public static void tearDownClass() { TestUtils.dropDB(testDBName); TestUtils.dropDB(testInjectionDBName); + TestUtils.dropDB(faresDBName); } /** Tests that the graphQL schema can initialize. */ @@ -230,6 +248,69 @@ public void canFetchServices() { }); } + @Test + void canFetchAreas() { + assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { + MatcherAssert.assertThat(queryFaresGraphQL("feedAreas.txt"), matchesSnapshot()); + }); + } + + @Test + void canFetchStopAreas() { + assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { + MatcherAssert.assertThat(queryFaresGraphQL("feedStopAreas.txt"), matchesSnapshot()); + }); + } + + @Test + void canFetchFareTransferRules() { + assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { + MatcherAssert.assertThat(queryFaresGraphQL("feedFareTransferRules.txt"), matchesSnapshot()); + }); + } + + @Test + void canFetchFareProducts() { + assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { + MatcherAssert.assertThat(queryFaresGraphQL("feedFareProducts.txt"), matchesSnapshot()); + }); + } + + @Test + void canFetchFareMedias() { + assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { + MatcherAssert.assertThat(queryFaresGraphQL("feedFareMedias.txt"), matchesSnapshot()); + }); + } + + @Test + void canFetchFareLegRules() { + assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { + MatcherAssert.assertThat(queryFaresGraphQL("feedFareLegRules.txt"), matchesSnapshot()); + }); + } + + @Test + void canFetchTimeFrames() { + assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { + MatcherAssert.assertThat(queryFaresGraphQL("feedTimeFrames.txt"), matchesSnapshot()); + }); + } + + @Test + void canFetchNetworks() { + assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { + MatcherAssert.assertThat(queryFaresGraphQL("feedNetworks.txt"), matchesSnapshot()); + }); + } + + @Test + void canFetchRouteNetworks() { + assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { + MatcherAssert.assertThat(queryFaresGraphQL("feedRouteNetworks.txt"), matchesSnapshot()); + }); + } + /** Tests that the stop times of a feed can be fetched. */ @Test public void canFetchRoutesAndFilterTripsByDateAndTime() { @@ -352,6 +433,18 @@ private Map queryGraphQL(String queryFilename) throws IOExceptio return queryGraphQL(queryFilename, variables, testDataSource); } + /** + * Helper method to make a fares query with default variables. + * + * @param queryFilename the filename that should be used to generate the GraphQL query. This file must be present + * in the `src/test/resources/graphql` folder + */ + private Map queryFaresGraphQL(String queryFilename) throws IOException { + Map variables = new HashMap<>(); + variables.put("namespace", faresNamespace); + return queryGraphQL(queryFilename, variables, faresDataSource); + } + /** * Helper method to execute a GraphQL query and return the result. * diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchTimeFrames-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchTimeFrames-0.json deleted file mode 100644 index 66b4bbc81..000000000 --- a/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchTimeFrames-0.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "data" : { - "feed" : { - "feed_version" : "1.0", - "timeFrame" : [ { - "end_time" : null, - "id" : 2, - "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", - "start_time" : null, - "timeframe_group_id" : "timeframe_regular" - }, { - "end_time" : null, - "id" : 3, - "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", - "start_time" : null, - "timeframe_group_id" : "timeframe_systemwide_free" - }, { - "end_time" : null, - "id" : 4, - "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", - "start_time" : null, - "timeframe_group_id" : "timeframe_sumner_tunnel_closure" - }, { - "end_time" : "9000", - "id" : 5, - "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", - "start_time" : "0", - "timeframe_group_id" : "timeframe_sumner_tunnel_closure" - }, { - "end_time" : "86400", - "id" : 6, - "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", - "start_time" : "9000", - "timeframe_group_id" : "timeframe_regular" - }, { - "end_time" : null, - "id" : 7, - "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", - "start_time" : null, - "timeframe_group_id" : "timeframe_alewife_kendall_surge" - }, { - "end_time" : "9000", - "id" : 8, - "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", - "start_time" : "0", - "timeframe_group_id" : "timeframe_alewife_kendall_surge" - } ] - } - } -} \ No newline at end of file diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchAreas-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchAreas-0.json similarity index 100% rename from src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchAreas-0.json rename to src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchAreas-0.json diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareLegRules-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareLegRules-0.json similarity index 100% rename from src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareLegRules-0.json rename to src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareLegRules-0.json diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareMedias-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareMedias-0.json similarity index 100% rename from src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareMedias-0.json rename to src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareMedias-0.json diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareProducts-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareProducts-0.json similarity index 100% rename from src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareProducts-0.json rename to src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareProducts-0.json diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareTransferRules-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareTransferRules-0.json similarity index 100% rename from src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchFareTransferRules-0.json rename to src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareTransferRules-0.json diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchNetworks-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchNetworks-0.json similarity index 100% rename from src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchNetworks-0.json rename to src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchNetworks-0.json diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchRouteNetworks-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchRouteNetworks-0.json similarity index 100% rename from src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchRouteNetworks-0.json rename to src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchRouteNetworks-0.json diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchStopAreas-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchStopAreas-0.json similarity index 100% rename from src/test/resources/snapshots/com/conveyal/gtfs/GTFSFaresV2Test/canFetchStopAreas-0.json rename to src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchStopAreas-0.json From 5f0c806dc72d0da0f37fcec81a8112bb5a2a9cc4 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Mon, 19 Aug 2024 12:20:47 +0100 Subject: [PATCH 04/40] refactor(Entity.java): Updated zip sub directory file name check Replaced endsWith with equals to avoid unwanted matches e.g. stop_areas.txt being used instead of areas.txt --- src/main/java/com/conveyal/gtfs/model/Entity.java | 2 +- src/test/java/com/conveyal/gtfs/GTFSTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/conveyal/gtfs/model/Entity.java b/src/main/java/com/conveyal/gtfs/model/Entity.java index b4ada462c..c6810ea2a 100644 --- a/src/main/java/com/conveyal/gtfs/model/Entity.java +++ b/src/main/java/com/conveyal/gtfs/model/Entity.java @@ -288,7 +288,7 @@ public void loadTable(ZipFile zip) throws IOException { // check if table is contained within sub-directory while (entries.hasMoreElements()) { ZipEntry e = entries.nextElement(); - if (e.getName().endsWith(tableName + ".txt")) { + if (e.getName().equals(tableName + ".txt")) { entry = e; feed.errors.add(new TableInSubdirectoryError(tableName, entry.getName().replace(tableName + ".txt", ""))); } diff --git a/src/test/java/com/conveyal/gtfs/GTFSTest.java b/src/test/java/com/conveyal/gtfs/GTFSTest.java index 20c2f7c69..3da9a8841 100644 --- a/src/test/java/com/conveyal/gtfs/GTFSTest.java +++ b/src/test/java/com/conveyal/gtfs/GTFSTest.java @@ -344,7 +344,7 @@ void canLoadAndExportFaresV2Feed() throws IOException { } /** - * Persistence expectations for use with the GTFS contained within the "MBTA_GTFS_Fares_v2.zip" feed. + * Persistence expectations for use with the GTFS contained within the "fake-agency-with-fares-v2" feed. */ private final PersistenceExpectation[] faresV2PersistenceExpectations = new PersistenceExpectation[]{ new PersistenceExpectation( From 5dd609bfb5a953c7ffeadc7d157fde42522e8731 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Mon, 19 Aug 2024 12:52:06 +0100 Subject: [PATCH 05/40] refactor(Updated to extract the file name from path): --- src/main/java/com/conveyal/gtfs/loader/Table.java | 3 ++- src/main/java/com/conveyal/gtfs/model/Entity.java | 8 +++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/conveyal/gtfs/loader/Table.java b/src/main/java/com/conveyal/gtfs/loader/Table.java index c131ef5c6..2f778c15e 100644 --- a/src/main/java/com/conveyal/gtfs/loader/Table.java +++ b/src/main/java/com/conveyal/gtfs/loader/Table.java @@ -46,6 +46,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; +import java.nio.file.Paths; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -782,7 +783,7 @@ public CsvReader getCsvReader(ZipFile zipFile, SQLErrorStorage sqlErrorStorage) Enumeration entries = zipFile.entries(); while (entries.hasMoreElements()) { ZipEntry e = entries.nextElement(); - if (e.getName().endsWith(tableFileName)) { + if (Paths.get(e.getName()).getFileName().toString().equals(tableFileName)) { entry = e; if (sqlErrorStorage != null) sqlErrorStorage.storeError(NewGTFSError.forTable(this, TABLE_IN_SUBDIRECTORY)); break; diff --git a/src/main/java/com/conveyal/gtfs/model/Entity.java b/src/main/java/com/conveyal/gtfs/model/Entity.java index c6810ea2a..20ba92488 100644 --- a/src/main/java/com/conveyal/gtfs/model/Entity.java +++ b/src/main/java/com/conveyal/gtfs/model/Entity.java @@ -30,6 +30,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.Charset; +import java.nio.file.Paths; import java.sql.JDBCType; import java.sql.PreparedStatement; import java.sql.SQLException; @@ -282,15 +283,16 @@ protected V getRefField(String column, boolean required, Map target * @param zip the zip file from which to read a table */ public void loadTable(ZipFile zip) throws IOException { - ZipEntry entry = zip.getEntry(tableName + ".txt"); + String fileName = tableName + ".txt"; + ZipEntry entry = zip.getEntry(fileName); if (entry == null) { Enumeration entries = zip.entries(); // check if table is contained within sub-directory while (entries.hasMoreElements()) { ZipEntry e = entries.nextElement(); - if (e.getName().equals(tableName + ".txt")) { + if (Paths.get(e.getName()).getFileName().toString().equals(fileName)) { entry = e; - feed.errors.add(new TableInSubdirectoryError(tableName, entry.getName().replace(tableName + ".txt", ""))); + feed.errors.add(new TableInSubdirectoryError(tableName, entry.getName().replace(fileName, ""))); } } /* This GTFS table did not exist in the zip. */ From 1d82b215c59325be260775aab9f9390f5b262bd9 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Fri, 23 Aug 2024 15:18:43 +0100 Subject: [PATCH 06/40] refactor(Refactored GraphQL classes to accom fares v2): --- .../graphql/GraphQLGtfsFaresV2Schema.java | 161 ++ .../gtfs/graphql/GraphQLGtfsSchema.java | 1469 ++++++----------- .../conveyal/gtfs/graphql/GraphQLUtil.java | 94 ++ .../conveyal/gtfs/loader/EntityPopulator.java | 68 +- .../java/com/conveyal/gtfs/loader/Table.java | 112 +- .../java/com/conveyal/gtfs/model/Area.java | 10 +- .../com/conveyal/gtfs/model/FareLegRule.java | 48 +- .../com/conveyal/gtfs/model/FareMedia.java | 18 +- .../com/conveyal/gtfs/model/FareProduct.java | 32 +- .../conveyal/gtfs/model/FareTransferRule.java | 42 +- .../java/com/conveyal/gtfs/model/Network.java | 10 +- .../com/conveyal/gtfs/model/RouteNetwork.java | 14 +- .../com/conveyal/gtfs/model/StopArea.java | 14 +- .../com/conveyal/gtfs/model/TimeFrame.java | 24 +- .../com/conveyal/gtfs/GTFSFaresV2Test.java | 76 +- 15 files changed, 997 insertions(+), 1195 deletions(-) create mode 100644 src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsFaresV2Schema.java diff --git a/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsFaresV2Schema.java b/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsFaresV2Schema.java new file mode 100644 index 000000000..ea1fe4e68 --- /dev/null +++ b/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsFaresV2Schema.java @@ -0,0 +1,161 @@ +package com.conveyal.gtfs.graphql; + +import com.conveyal.gtfs.graphql.fetchers.MapFetcher; +import com.conveyal.gtfs.model.Area; +import com.conveyal.gtfs.model.FareLegRule; +import com.conveyal.gtfs.model.FareMedia; +import com.conveyal.gtfs.model.FareProduct; +import com.conveyal.gtfs.model.FareTransferRule; +import com.conveyal.gtfs.model.Network; +import com.conveyal.gtfs.model.Route; +import com.conveyal.gtfs.model.RouteNetwork; +import com.conveyal.gtfs.model.StopArea; +import com.conveyal.gtfs.model.TimeFrame; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLObjectType; + +import java.util.Arrays; +import java.util.List; + +import static com.conveyal.gtfs.graphql.GraphQLUtil.createFieldDefinition; +import static graphql.Scalars.GraphQLInt; +import static graphql.schema.GraphQLObjectType.newObject; + +public class GraphQLGtfsFaresV2Schema { + + private GraphQLGtfsFaresV2Schema() {} + + public static final GraphQLObjectType stopAreaType = newObject().name("stopArea") + .description("A GTFS stop area object") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field(StopArea.AREA_ID_NAME)) + .field(MapFetcher.field(StopArea.STOP_ID_NAME)) + .field(createFieldDefinition("stops", GraphQLGtfsSchema.stopType, "stops", StopArea.STOP_ID_NAME)) + .build(); + + public static final GraphQLObjectType areaType = newObject().name("area") + .description("A GTFS area object") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field(Area.AREA_ID_NAME)) + .field(MapFetcher.field(Area.AREA_NAME_NAME)) + .field(createFieldDefinition("stopAreas", stopAreaType, StopArea.TABLE_NAME, Area.AREA_ID_NAME)) + .build(); + + public static final GraphQLObjectType timeFrameType = newObject().name("timeFrame") + .description("A GTFS time frame object") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field(TimeFrame.TIME_FRAME_GROUP_ID_NAME)) + .field(MapFetcher.field(TimeFrame.START_TIME_NAME)) + .field(MapFetcher.field(TimeFrame.END_TIME_NAME)) + .field(MapFetcher.field(TimeFrame.SERVICE_ID_NAME)) + .build(); + + public static final GraphQLObjectType networkType = newObject().name("network") + .description("A GTFS network object") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field(Network.NETWORK_ID_NAME)) + .field(MapFetcher.field(Network.NETWORK_NAME_NAME)) + .build(); + + public static final GraphQLObjectType routeNetworkType = newObject().name("routeNetwork") + .description("A GTFS route network object") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field(RouteNetwork.NETWORK_ID_NAME)) + .field(MapFetcher.field(RouteNetwork.ROUTE_ID_NAME)) + .field(createFieldDefinition("networks", networkType, Network.TABLE_NAME, RouteNetwork.NETWORK_ID_NAME)) + .build(); + + public static final GraphQLObjectType fareMediaType = newObject().name("fareMedia") + .description("A GTFS fare media object") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field(FareMedia.FARE_MEDIA_ID_NAME)) + .field(MapFetcher.field(FareMedia.FARE_MEDIA_NAME_NAME)) + .field(MapFetcher.field(FareMedia.FARE_MEDIA_TYPE_NAME)) + .build(); + + public static final GraphQLObjectType fareProductType = newObject().name("fareProduct") + .description("A GTFS fare product object") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field(FareProduct.FARE_PRODUCT_ID_NAME)) + .field(MapFetcher.field(FareProduct.FARE_PRODUCT_NAME_NAME)) + .field(MapFetcher.field(FareProduct.FARE_MEDIA_ID_NAME)) + .field(MapFetcher.field(FareProduct.AMOUNT_NAME)) + .field(MapFetcher.field(FareProduct.CURRENCY_NAME)) + .field(createFieldDefinition("fareMedia", fareMediaType, FareMedia.TABLE_NAME, FareProduct.FARE_MEDIA_ID_NAME)) + .build(); + + public static final GraphQLObjectType fareLegRuleType = newObject().name("fareLegRule") + .description("A GTFS fare leg rule object") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field(FareLegRule.LEG_GROUP_ID_NAME)) + .field(MapFetcher.field(FareLegRule.NETWORK_ID_NAME)) + .field(MapFetcher.field(FareLegRule.FROM_AREA_ID_NAME)) + .field(MapFetcher.field(FareLegRule.TO_AREA_ID_NAME)) + .field(MapFetcher.field(FareLegRule.FROM_TIMEFRAME_GROUP_ID_NAME)) + .field(MapFetcher.field(FareLegRule.TO_TIMEFRAME_GROUP_ID_NAME)) + .field(MapFetcher.field(FareLegRule.FARE_PRODUCT_ID_NAME)) + .field(MapFetcher.field(FareLegRule.RULE_PRIORITY_NAME)) + // Will return either routes or networks, not both. + .field(createFieldDefinition("routes", GraphQLGtfsSchema.routeType, Route.TABLE_NAME, FareLegRule.NETWORK_ID_NAME)) + .field(createFieldDefinition("networks", networkType, Network.TABLE_NAME, Network.NETWORK_ID_NAME)) + .field(createFieldDefinition("fareProducts", fareProductType, FareProduct.TABLE_NAME, FareLegRule.FARE_PRODUCT_ID_NAME)) + // fromTimeFrame and toTimeFrame may return multiple time frames. + .field(createFieldDefinition( + "fromTimeFrame", + timeFrameType, + TimeFrame.TABLE_NAME, + FareLegRule.FROM_TIMEFRAME_GROUP_ID_NAME, + TimeFrame.TIME_FRAME_GROUP_ID_NAME + )) + .field(createFieldDefinition( + "toTimeFrame", + timeFrameType, + TimeFrame.TABLE_NAME, + FareLegRule.TO_TIMEFRAME_GROUP_ID_NAME, + TimeFrame.TIME_FRAME_GROUP_ID_NAME + )) + .field(createFieldDefinition("toArea", areaType, Area.TABLE_NAME, FareLegRule.TO_AREA_ID_NAME, Area.AREA_ID_NAME)) + .field(createFieldDefinition("fromArea", areaType, Area.TABLE_NAME, FareLegRule.FROM_AREA_ID_NAME, Area.AREA_ID_NAME)) + .build(); + + public static final GraphQLObjectType fareTransferRuleType = newObject().name("fareTransferRule") + .description("A GTFS fare transfer rule object") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field(FareTransferRule.FROM_LEG_GROUP_ID_NAME)) + .field(MapFetcher.field(FareTransferRule.TO_LEG_GROUP_ID_NAME)) + .field(MapFetcher.field(FareTransferRule.TRANSFER_COUNT_NAME)) + .field(MapFetcher.field(FareTransferRule.DURATION_LIMIT_NAME)) + .field(MapFetcher.field(FareTransferRule.DURATION_LIMIT_TYPE_NAME)) + .field(MapFetcher.field(FareTransferRule.FARE_PRODUCT_ID_NAME)) + .field(createFieldDefinition("toArea", areaType, Area.TABLE_NAME, FareLegRule.TO_AREA_ID_NAME, Area.AREA_ID_NAME)) + .field(createFieldDefinition("fareProducts", fareProductType, FareProduct.TABLE_NAME, FareProduct.FARE_PRODUCT_ID_NAME)) + .field(createFieldDefinition( + "fromFareLegRule", + fareLegRuleType, + FareLegRule.TABLE_NAME, + FareTransferRule.FROM_LEG_GROUP_ID_NAME, + FareLegRule.LEG_GROUP_ID_NAME + )) + .field(createFieldDefinition( + "toFareLegRule", + fareLegRuleType, + FareLegRule.TABLE_NAME, + FareTransferRule.TO_LEG_GROUP_ID_NAME, + FareLegRule.LEG_GROUP_ID_NAME + )) + .build(); + + public static List getFaresV2FieldDefinitions() { + return Arrays.asList( + createFieldDefinition("area", areaType, Area.TABLE_NAME), + createFieldDefinition("stopArea", stopAreaType, StopArea.TABLE_NAME), + createFieldDefinition("fareTransferRule", fareTransferRuleType, FareTransferRule.TABLE_NAME), + createFieldDefinition("fareProduct", fareProductType, FareProduct.TABLE_NAME), + createFieldDefinition("fareMedia", fareMediaType, FareMedia.TABLE_NAME), + createFieldDefinition("fareLegRule", fareLegRuleType, FareLegRule.TABLE_NAME), + createFieldDefinition("timeFrame", timeFrameType, TimeFrame.TABLE_NAME), + createFieldDefinition("network", networkType, Network.TABLE_NAME), + createFieldDefinition("routeNetwork", routeNetworkType, RouteNetwork.TABLE_NAME) + ); + } +} diff --git a/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsSchema.java b/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsSchema.java index ac5b10531..a9395cdc6 100644 --- a/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsSchema.java +++ b/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsSchema.java @@ -9,17 +9,8 @@ import com.conveyal.gtfs.graphql.fetchers.RowCountFetcher; import com.conveyal.gtfs.graphql.fetchers.SQLColumnFetcher; import com.conveyal.gtfs.graphql.fetchers.SourceObjectFetcher; -import com.conveyal.gtfs.model.Area; -import com.conveyal.gtfs.model.FareLegRule; -import com.conveyal.gtfs.model.FareMedia; -import com.conveyal.gtfs.model.FareProduct; -import com.conveyal.gtfs.model.FareTransferRule; -import com.conveyal.gtfs.model.Network; -import com.conveyal.gtfs.model.Route; -import com.conveyal.gtfs.model.RouteNetwork; -import com.conveyal.gtfs.model.StopArea; -import com.conveyal.gtfs.model.TimeFrame; import graphql.schema.Coercing; +import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLList; import graphql.schema.GraphQLObjectType; import graphql.schema.GraphQLScalarType; @@ -28,8 +19,11 @@ import java.sql.Array; import java.sql.SQLException; +import java.util.List; +import static com.conveyal.gtfs.graphql.GraphQLUtil.createFieldDefinition; import static com.conveyal.gtfs.graphql.GraphQLUtil.floatArg; +import static com.conveyal.gtfs.graphql.GraphQLUtil.buildArgs; import static com.conveyal.gtfs.graphql.GraphQLUtil.intArg; import static com.conveyal.gtfs.graphql.GraphQLUtil.intt; import static com.conveyal.gtfs.graphql.GraphQLUtil.multiStringArg; @@ -68,37 +62,38 @@ public class GraphQLGtfsSchema { // by using static fields to hold these types, backward references are enforced. a few forward references are inserted explicitly. // Represents rows from agency.txt - public static final GraphQLObjectType agencyType = newObject().name("agency") - .description("A GTFS agency object") - .field(MapFetcher.field("id", GraphQLInt)) - .field(MapFetcher.field("agency_id")) - .field(MapFetcher.field("agency_name")) - .field(MapFetcher.field("agency_url")) - .field(MapFetcher.field("agency_branding_url")) - .field(MapFetcher.field("agency_phone")) - .field(MapFetcher.field("agency_email")) - .field(MapFetcher.field("agency_lang")) - .field(MapFetcher.field("agency_fare_url")) - .field(MapFetcher.field("agency_timezone")) - .build(); + public static final GraphQLObjectType agencyType = newObject() + .name("agency") + .description("A GTFS agency object") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field("agency_id")) + .field(MapFetcher.field("agency_name")) + .field(MapFetcher.field("agency_url")) + .field(MapFetcher.field("agency_branding_url")) + .field(MapFetcher.field("agency_phone")) + .field(MapFetcher.field("agency_email")) + .field(MapFetcher.field("agency_lang")) + .field(MapFetcher.field("agency_fare_url")) + .field(MapFetcher.field("agency_timezone")) + .build(); // Represents rows from calendar.txt public static final GraphQLObjectType calendarType = newObject() - .name("calendar") - .field(MapFetcher.field("id", GraphQLInt)) - .field(MapFetcher.field("service_id")) - .field(MapFetcher.field("monday", GraphQLInt)) - .field(MapFetcher.field("tuesday", GraphQLInt)) - .field(MapFetcher.field("wednesday", GraphQLInt)) - .field(MapFetcher.field("thursday", GraphQLInt)) - .field(MapFetcher.field("friday", GraphQLInt)) - .field(MapFetcher.field("saturday", GraphQLInt)) - .field(MapFetcher.field("sunday", GraphQLInt)) - .field(MapFetcher.field("start_date")) - .field(MapFetcher.field("end_date")) - // FIXME: Description is an editor-specific field - .field(MapFetcher.field("description")) - .build(); + .name("calendar") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field("service_id")) + .field(MapFetcher.field("monday", GraphQLInt)) + .field(MapFetcher.field("tuesday", GraphQLInt)) + .field(MapFetcher.field("wednesday", GraphQLInt)) + .field(MapFetcher.field("thursday", GraphQLInt)) + .field(MapFetcher.field("friday", GraphQLInt)) + .field(MapFetcher.field("saturday", GraphQLInt)) + .field(MapFetcher.field("sunday", GraphQLInt)) + .field(MapFetcher.field("start_date")) + .field(MapFetcher.field("end_date")) + // FIXME: Description is an editor-specific field + .field(MapFetcher.field("description")) + .build(); private static final GraphQLScalarType stringList = GraphQLScalarType .newScalar() @@ -108,180 +103,144 @@ public class GraphQLGtfsSchema { .build(); // Represents GTFS Editor service exceptions. - public static final GraphQLObjectType scheduleExceptionType = newObject().name("scheduleException") - .description("A GTFS Editor schedule exception type") - .field(MapFetcher.field("id", GraphQLInt)) - .field(MapFetcher.field("name")) - .field(MapFetcher.field("exemplar", GraphQLInt)) - .field(MapFetcher.field("dates", stringList)) - .field(MapFetcher.field("custom_schedule", stringList)) - .field(MapFetcher.field("added_service", stringList)) - .field(MapFetcher.field("removed_service", stringList)) - .build(); + public static final GraphQLObjectType scheduleExceptionType = newObject() + .name("scheduleException") + .description("A GTFS Editor schedule exception type") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field("name")) + .field(MapFetcher.field("exemplar", GraphQLInt)) + .field(MapFetcher.field("dates", stringList)) + .field(MapFetcher.field("custom_schedule", stringList)) + .field(MapFetcher.field("added_service", stringList)) + .field(MapFetcher.field("removed_service", stringList)) + .build(); // Represents rows from fare_rules.txt - public static final GraphQLObjectType fareRuleType = newObject().name("fareRule") - .description("A GTFS agency object") - .field(MapFetcher.field("id", GraphQLInt)) - .field(MapFetcher.field("fare_id")) - .field(MapFetcher.field("route_id")) - .field(MapFetcher.field("origin_id")) - .field(MapFetcher.field("destination_id")) - .field(MapFetcher.field("contains_id")) - .build(); + public static final GraphQLObjectType fareRuleType = newObject() + .name("fareRule") + .description("A GTFS agency object") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field("fare_id")) + .field(MapFetcher.field("route_id")) + .field(MapFetcher.field("origin_id")) + .field(MapFetcher.field("destination_id")) + .field(MapFetcher.field("contains_id")) + .build(); // Represents rows from fare_attributes.txt - public static final GraphQLObjectType fareType = newObject().name("fare_attributes") - .description("A GTFS agency object") - .field(MapFetcher.field("id", GraphQLInt)) - .field(MapFetcher.field("agency_id")) - .field(MapFetcher.field("fare_id")) - .field(MapFetcher.field("price", GraphQLFloat)) - .field(MapFetcher.field("currency_type")) - .field(MapFetcher.field("payment_method", GraphQLInt)) - .field(MapFetcher.field("transfers", GraphQLInt)) - .field(MapFetcher.field("transfer_duration", GraphQLInt)) - .field(newFieldDefinition() - .name("fare_rules") - .type(new GraphQLList(fareRuleType)) - // FIXME Update JDBCFetcher to have noLimit boolean for fetchers on "naturally" nested types - // (i.e., nested types that typically would only be nested under another entity and only make sense - // with the entire set -- fares -> fare rules, trips -> stop times, patterns -> pattern stops/shapes) - .argument(intArg(LIMIT_ARG)) - .dataFetcher(new JDBCFetcher("fare_rules", "fare_id")) - .build() - ) - .build(); + public static final GraphQLObjectType fareType = newObject() + .name("fare_attributes") + .description("A GTFS agency object") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field("agency_id")) + .field(MapFetcher.field("fare_id")) + .field(MapFetcher.field("price", GraphQLFloat)) + .field(MapFetcher.field("currency_type")) + .field(MapFetcher.field("payment_method", GraphQLInt)) + .field(MapFetcher.field("transfers", GraphQLInt)) + .field(MapFetcher.field("transfer_duration", GraphQLInt)) + .field(createFieldDefinition("fare_rules", fareRuleType, "fare_rules", "fare_id")) + .build(); // Represents feed_info.txt - public static final GraphQLObjectType feedInfoType = newObject().name("feed_info") - .description("A GTFS feed_info object") - .field(MapFetcher.field("id", GraphQLInt)) - .field(MapFetcher.field("feed_id")) - .field(MapFetcher.field("feed_contact_email")) - .field(MapFetcher.field("feed_contact_url")) - .field(MapFetcher.field("feed_publisher_name")) - .field(MapFetcher.field("feed_publisher_url")) - .field(MapFetcher.field("feed_lang")) - .field(MapFetcher.field("default_lang")) - .field(MapFetcher.field("feed_start_date")) - .field(MapFetcher.field("feed_end_date")) - .field(MapFetcher.field("feed_version")) - // Editor-specific fields - .field(MapFetcher.field("default_route_color")) - .field(MapFetcher.field("default_route_type")) - .build(); + public static final GraphQLObjectType feedInfoType = newObject() + .name("feed_info") + .description("A GTFS feed_info object") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field("feed_id")) + .field(MapFetcher.field("feed_contact_email")) + .field(MapFetcher.field("feed_contact_url")) + .field(MapFetcher.field("feed_publisher_name")) + .field(MapFetcher.field("feed_publisher_url")) + .field(MapFetcher.field("feed_lang")) + .field(MapFetcher.field("default_lang")) + .field(MapFetcher.field("feed_start_date")) + .field(MapFetcher.field("feed_end_date")) + .field(MapFetcher.field("feed_version")) + // Editor-specific fields + .field(MapFetcher.field("default_route_color")) + .field(MapFetcher.field("default_route_type")) + .build(); // Represents rows from shapes.txt - public static final GraphQLObjectType shapePointType = newObject().name("shapePoint") - .field(MapFetcher.field("shape_id")) - .field(MapFetcher.field("shape_dist_traveled", GraphQLFloat)) - .field(MapFetcher.field("shape_pt_lat", GraphQLFloat)) - .field(MapFetcher.field("shape_pt_lon", GraphQLFloat)) - .field(MapFetcher.field("shape_pt_sequence", GraphQLInt)) - .field(MapFetcher.field("point_type", GraphQLInt)) - .build(); + public static final GraphQLObjectType shapePointType = newObject() + .name("shapePoint") + .field(MapFetcher.field("shape_id")) + .field(MapFetcher.field("shape_dist_traveled", GraphQLFloat)) + .field(MapFetcher.field("shape_pt_lat", GraphQLFloat)) + .field(MapFetcher.field("shape_pt_lon", GraphQLFloat)) + .field(MapFetcher.field("shape_pt_sequence", GraphQLInt)) + .field(MapFetcher.field("point_type", GraphQLInt)) + .build(); // Represents a set of rows from shapes.txt joined by shape_id - public static final GraphQLObjectType shapeEncodedPolylineType = newObject().name("shapeEncodedPolyline") + public static final GraphQLObjectType shapeEncodedPolylineType = newObject() + .name("shapeEncodedPolyline") .field(string("shape_id")) .field(string("polyline")) .build(); // Represents rows from frequencies.txt - public static final GraphQLObjectType frequencyType = newObject().name("frequency") - .field(MapFetcher.field("trip_id")) - .field(MapFetcher.field("start_time", GraphQLInt)) - .field(MapFetcher.field("end_time", GraphQLInt)) - .field(MapFetcher.field("headway_secs", GraphQLInt)) - .field(MapFetcher.field("exact_times", GraphQLInt)) - .build(); + public static final GraphQLObjectType frequencyType = newObject() + .name("frequency") + .field(MapFetcher.field("trip_id")) + .field(MapFetcher.field("start_time", GraphQLInt)) + .field(MapFetcher.field("end_time", GraphQLInt)) + .field(MapFetcher.field("headway_secs", GraphQLInt)) + .field(MapFetcher.field("exact_times", GraphQLInt)) + .build(); // Represents rows from trips.txt public static final GraphQLObjectType tripType = newObject() - .name("trip") - .field(MapFetcher.field("id", GraphQLInt)) - .field(MapFetcher.field("trip_id")) - .field(MapFetcher.field("trip_headsign")) - .field(MapFetcher.field("trip_short_name")) - .field(MapFetcher.field("block_id")) - .field(MapFetcher.field("direction_id", GraphQLInt)) - .field(MapFetcher.field("route_id")) - .field(MapFetcher.field("service_id")) - .field(MapFetcher.field("wheelchair_accessible", GraphQLInt)) - .field(MapFetcher.field("bikes_allowed", GraphQLInt)) - .field(MapFetcher.field("shape_id")) - .field(MapFetcher.field("pattern_id")) - .field(newFieldDefinition() - .name("stop_times") - // forward reference to the as yet undefined stopTimeType (must be defined - // after tripType) - .type(new GraphQLList(new GraphQLTypeReference("stopTime"))) - // FIXME Update JDBCFetcher to have noLimit boolean for fetchers on "naturally" - // nested types (i.e., nested types that typically would only be nested under - // another entity and only make sense with the entire set -- fares -> fare - // rules, trips -> stop times, patterns -> pattern stops/shapes) - .argument(intArg(LIMIT_ARG)) - .dataFetcher(new JDBCFetcher( - "stop_times", - "trip_id", - "stop_sequence", - false)) - .build() - ) - .field(newFieldDefinition() - .name("frequencies") - // forward reference to the as yet undefined stopTimeType (must be defined after tripType) - .type(new GraphQLList(frequencyType)) - // FIXME Update JDBCFetcher to have noLimit boolean for fetchers on "naturally" nested types - // (i.e., nested types that typically would only be nested under another entity and only make sense - // with the entire set -- fares -> fare rules, trips -> stop times, patterns -> pattern stops/shapes) - .argument(intArg(LIMIT_ARG)) - .dataFetcher(new JDBCFetcher("frequencies", "trip_id")) - .build() - ) - // TODO should this be included in the query? - .field(newFieldDefinition() - .name("shape") - .type(new GraphQLList(shapePointType)) - .dataFetcher(new JDBCFetcher("shapes", "shape_id")) - .build()) -// // some pseudo-fields to reduce the amount of data that has to be fetched over GraphQL to summarize -// .field(newFieldDefinition() -// .name("start_time") -// .type(GraphQLInt) -// .dataFetcher(TripDataFetcher::getStartTime) -// .build() -// ) -// .field(newFieldDefinition() -// .name("duration") -// .type(GraphQLInt) -// .dataFetcher(TripDataFetcher::getDuration) -// .build() -// ) - .build(); + .name("trip") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field("trip_id")) + .field(MapFetcher.field("trip_headsign")) + .field(MapFetcher.field("trip_short_name")) + .field(MapFetcher.field("block_id")) + .field(MapFetcher.field("direction_id", GraphQLInt)) + .field(MapFetcher.field("route_id")) + .field(MapFetcher.field("service_id")) + .field(MapFetcher.field("wheelchair_accessible", GraphQLInt)) + .field(MapFetcher.field("bikes_allowed", GraphQLInt)) + .field(MapFetcher.field("shape_id")) + .field(MapFetcher.field("pattern_id")) + .field(newFieldDefinition() + .name("stop_times") + // forward reference to the as yet undefined stopTimeType (must be defined + // after tripType) + .type(new GraphQLList(new GraphQLTypeReference("stopTime"))) + .argument(intArg(LIMIT_ARG)) + .dataFetcher(new JDBCFetcher("stop_times", "trip_id", "stop_sequence", false)) + .build() + ) + .field(createFieldDefinition("frequencies", frequencyType, "frequencies", "trip_id")) + .field(createFieldDefinition("shape", shapePointType, "shapes", "shape_id")) + .build(); // Represents rows from stop_times.txt - public static final GraphQLObjectType stopTimeType = newObject().name("stopTime") - .field(MapFetcher.field("trip_id")) - .field(MapFetcher.field("stop_id")) - .field(MapFetcher.field("stop_sequence", GraphQLInt)) - .field(MapFetcher.field("arrival_time", GraphQLInt)) - .field(MapFetcher.field("departure_time", GraphQLInt)) - .field(MapFetcher.field("stop_headsign")) - .field(MapFetcher.field("timepoint", GraphQLInt)) - .field(MapFetcher.field("drop_off_type", GraphQLInt)) - .field(MapFetcher.field("pickup_type", GraphQLInt)) - .field(MapFetcher.field("continuous_drop_off", GraphQLInt)) - .field(MapFetcher.field("continuous_pickup", GraphQLInt)) - .field(MapFetcher.field("shape_dist_traveled", GraphQLFloat)) - .build(); + public static final GraphQLObjectType stopTimeType = newObject() + .name("stopTime") + .field(MapFetcher.field("trip_id")) + .field(MapFetcher.field("stop_id")) + .field(MapFetcher.field("stop_sequence", GraphQLInt)) + .field(MapFetcher.field("arrival_time", GraphQLInt)) + .field(MapFetcher.field("departure_time", GraphQLInt)) + .field(MapFetcher.field("stop_headsign")) + .field(MapFetcher.field("timepoint", GraphQLInt)) + .field(MapFetcher.field("drop_off_type", GraphQLInt)) + .field(MapFetcher.field("pickup_type", GraphQLInt)) + .field(MapFetcher.field("continuous_drop_off", GraphQLInt)) + .field(MapFetcher.field("continuous_pickup", GraphQLInt)) + .field(MapFetcher.field("shape_dist_traveled", GraphQLFloat)) + .build(); // Represents rows from attributions.txt - public static final GraphQLObjectType attributionsType = newObject().name("attributions") + public static final GraphQLObjectType attributionsType = newObject() + .name("attributions") .field(MapFetcher.field("attribution_id")) .field(MapFetcher.field("agency_id")) .field(MapFetcher.field("route_id")) @@ -296,7 +255,8 @@ public class GraphQLGtfsSchema { .build(); // Represents rows from translations.txt - public static final GraphQLObjectType translationsType = newObject().name("translations") + public static final GraphQLObjectType translationsType = newObject() + .name("translations") .field(MapFetcher.field("table_name")) .field(MapFetcher.field("field_name")) .field(MapFetcher.field("language")) @@ -307,133 +267,132 @@ public class GraphQLGtfsSchema { .build(); // Represents rows from routes.txt - public static final GraphQLObjectType routeType = newObject().name("route") - .description("A line from a GTFS routes.txt table") - .field(MapFetcher.field("id", GraphQLInt)) - .field(MapFetcher.field("agency_id")) - .field(MapFetcher.field("route_id")) - .field(MapFetcher.field("route_short_name")) - .field(MapFetcher.field("route_long_name")) - .field(MapFetcher.field("route_desc")) - .field(MapFetcher.field("route_url")) - .field(MapFetcher.field("route_branding_url")) - .field(MapFetcher.field("continuous_drop_off", GraphQLInt)) - .field(MapFetcher.field("continuous_pickup", GraphQLInt)) - .field(MapFetcher.field("route_type", GraphQLInt)) - .field(MapFetcher.field("route_color")) - .field(MapFetcher.field("route_text_color")) - // FIXME ˇˇ Editor fields that should perhaps be moved elsewhere. - .field(MapFetcher.field("wheelchair_accessible")) - .field(MapFetcher.field("publicly_visible", GraphQLInt)) - .field(MapFetcher.field("status", GraphQLInt)) - .field(MapFetcher.field("route_sort_order")) - // FIXME ^^ - .field(RowCountFetcher.field("trip_count", "trips", "route_id")) - .field(RowCountFetcher.field("pattern_count", "patterns", "route_id")) - .field(newFieldDefinition() - .name("stops") - .description("GTFS stop entities that the route serves") - // Field type should be equivalent to the final JDBCFetcher table type. - .type(new GraphQLList(new GraphQLTypeReference("stop"))) - // We scope to a single feed namespace, otherwise GTFS entity IDs are ambiguous. - .argument(stringArg("namespace")) - .argument(stringArg(SEARCH_ARG)) - .argument(intArg(LIMIT_ARG)) - // We allow querying only for a single stop, otherwise result processing can take a long time (lots - // of join queries). - .argument(stringArg("route_id")) - .dataFetcher(new NestedJDBCFetcher( - new JDBCFetcher("patterns", "route_id", null, false), - new JDBCFetcher("pattern_stops", "pattern_id", null, false), - new JDBCFetcher("stops", "stop_id"))) - .build()) - .field(newFieldDefinition() - .type(new GraphQLList(tripType)) - .name("trips") - .argument(multiStringArg("trip_id")) - // FIXME Update JDBCFetcher to have noLimit boolean for fetchers on "naturally" nested types - // (i.e., nested types that typically would only be nested under another entity and only make sense - // with the entire set -- fares -> fare rules, trips -> stop times, patterns -> pattern stops/shapes) - .argument(intArg(LIMIT_ARG)) - .argument(stringArg(DATE_ARG)) - .argument(intArg(FROM_ARG)) - .argument(intArg(TO_ARG)) - .dataFetcher(new JDBCFetcher("trips", "route_id")) - .build() - ) - .field(newFieldDefinition() - .type(new GraphQLList(new GraphQLTypeReference("pattern"))) - .name("patterns") - .argument(intArg(LIMIT_ARG)) - .argument(multiStringArg("pattern_id")) - .dataFetcher(new JDBCFetcher("patterns", "route_id")) - .build() - ) - .field(RowCountFetcher.field("count", "routes")) - .build(); + public static final GraphQLObjectType routeType = newObject() + .name("route") + .description("A line from a GTFS routes.txt table") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field("agency_id")) + .field(MapFetcher.field("route_id")) + .field(MapFetcher.field("route_short_name")) + .field(MapFetcher.field("route_long_name")) + .field(MapFetcher.field("route_desc")) + .field(MapFetcher.field("route_url")) + .field(MapFetcher.field("route_branding_url")) + .field(MapFetcher.field("continuous_drop_off", GraphQLInt)) + .field(MapFetcher.field("continuous_pickup", GraphQLInt)) + .field(MapFetcher.field("route_type", GraphQLInt)) + .field(MapFetcher.field("route_color")) + .field(MapFetcher.field("route_text_color")) + // FIXME ˇˇ Editor fields that should perhaps be moved elsewhere. + .field(MapFetcher.field("wheelchair_accessible")) + .field(MapFetcher.field("publicly_visible", GraphQLInt)) + .field(MapFetcher.field("status", GraphQLInt)) + .field(MapFetcher.field("route_sort_order")) + // FIXME ^^ + .field(RowCountFetcher.field("trip_count", "trips", "route_id")) + .field(RowCountFetcher.field("pattern_count", "patterns", "route_id")) + .field(newFieldDefinition() + .name("stops") + .description("GTFS stop entities that the route serves") + // Field type should be equivalent to the final JDBCFetcher table type. + .type(new GraphQLList(new GraphQLTypeReference("stop"))) + // We scope to a single feed namespace, otherwise GTFS entity IDs are ambiguous. + .argument(stringArg("namespace")) + .argument(stringArg(SEARCH_ARG)) + .argument(intArg(LIMIT_ARG)) + // We allow querying only for a single stop, otherwise result processing can take a long time (lots + // of join queries). + .argument(stringArg("route_id")) + .dataFetcher(new NestedJDBCFetcher( + new JDBCFetcher("patterns", "route_id", null, false), + new JDBCFetcher("pattern_stops", "pattern_id", null, false), + new JDBCFetcher("stops", "stop_id"))) + .build()) + .field(newFieldDefinition() + .type(new GraphQLList(tripType)) + .name("trips") + .argument(multiStringArg("trip_id")) + .argument(intArg(LIMIT_ARG)) + .argument(stringArg(DATE_ARG)) + .argument(intArg(FROM_ARG)) + .argument(intArg(TO_ARG)) + .dataFetcher(new JDBCFetcher("trips", "route_id")) + .build() + ) + .field(newFieldDefinition() + .type(new GraphQLList(new GraphQLTypeReference("pattern"))) + .name("patterns") + .argument(intArg(LIMIT_ARG)) + .argument(multiStringArg("pattern_id")) + .dataFetcher(new JDBCFetcher("patterns", "route_id")) + .build() + ) + .field(RowCountFetcher.field("count", "routes")) + .build(); // Represents rows from stops.txt // Contains a reference to stopTimeType and routeType - public static final GraphQLObjectType stopType = newObject().name("stop") - .description("A GTFS stop object") - .field(MapFetcher.field("id", GraphQLInt)) - .field(MapFetcher.field("stop_id")) - .field(MapFetcher.field("stop_name")) - .field(MapFetcher.field("stop_code")) - .field(MapFetcher.field("stop_desc")) - .field(MapFetcher.field("stop_lon", GraphQLFloat)) - .field(MapFetcher.field("stop_lat", GraphQLFloat)) - .field(MapFetcher.field("zone_id")) - .field(MapFetcher.field("stop_url")) - .field(MapFetcher.field("stop_timezone")) - .field(MapFetcher.field("parent_station")) - .field(MapFetcher.field("platform_code")) - .field(MapFetcher.field("location_type", GraphQLInt)) - .field(MapFetcher.field("wheelchair_boarding", GraphQLInt)) - // Returns all stops that reference parent stop's stop_id - .field(newFieldDefinition() - .name("child_stops") - .type(new GraphQLList(new GraphQLTypeReference("stop"))) - .dataFetcher(new JDBCFetcher( - "stops", - "stop_id", - null, - false, - "parent_station" - )) - .build()) - .field(RowCountFetcher.field("stop_time_count", "stop_times", "stop_id")) - .field(newFieldDefinition() - .name("patterns") - // Field type should be equivalent to the final JDBCFetcher table type. - .type(new GraphQLList(new GraphQLTypeReference("pattern"))) - .argument(stringArg("namespace")) - .dataFetcher(new NestedJDBCFetcher( - new JDBCFetcher("pattern_stops", "stop_id", null, false), - new JDBCFetcher("patterns", "pattern_id"))) - .build()) - .field(newFieldDefinition() - .name("routes") - // Field type should be equivalent to the final JDBCFetcher table type. - .type(new GraphQLList(routeType)) - .argument(stringArg("namespace")) - .argument(stringArg(SEARCH_ARG)) - .argument(intArg(LIMIT_ARG)) - .dataFetcher(new NestedJDBCFetcher( - new JDBCFetcher("pattern_stops", "stop_id", null, false), - new JDBCFetcher("patterns", "pattern_id", null, false), - new JDBCFetcher("routes", "route_id"))) - .build()) - .field(newFieldDefinition() - .name("stop_times") - // forward reference to the as yet undefined stopTimeType (must be defined after tripType) - .type(new GraphQLList(new GraphQLTypeReference("stopTime"))) - // FIXME Update JDBCFetcher to have noLimit boolean for fetchers on "naturally" nested types - // (i.e., nested types that typically would only be nested under another entity and only make sense - // with the entire set -- fares -> fare rules, trips -> stop times, patterns -> pattern stops/shapes) - .argument(intArg(LIMIT_ARG)) - .dataFetcher(new JDBCFetcher("stop_times", "stop_id", "stop_sequence", false))) - .build(); + public static final GraphQLObjectType stopType = newObject() + .name("stop") + .description("A GTFS stop object") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field("stop_id")) + .field(MapFetcher.field("stop_name")) + .field(MapFetcher.field("stop_code")) + .field(MapFetcher.field("stop_desc")) + .field(MapFetcher.field("stop_lon", GraphQLFloat)) + .field(MapFetcher.field("stop_lat", GraphQLFloat)) + .field(MapFetcher.field("zone_id")) + .field(MapFetcher.field("stop_url")) + .field(MapFetcher.field("stop_timezone")) + .field(MapFetcher.field("parent_station")) + .field(MapFetcher.field("platform_code")) + .field(MapFetcher.field("location_type", GraphQLInt)) + .field(MapFetcher.field("wheelchair_boarding", GraphQLInt)) + // Returns all stops that reference parent stop's stop_id + .field(newFieldDefinition() + .name("child_stops") + .type(new GraphQLList(new GraphQLTypeReference("stop"))) + .dataFetcher(new JDBCFetcher( + "stops", + "stop_id", + null, + false, + "parent_station" + )) + .build()) + .field(RowCountFetcher.field("stop_time_count", "stop_times", "stop_id")) + .field(newFieldDefinition() + .name("patterns") + // Field type should be equivalent to the final JDBCFetcher table type. + .type(new GraphQLList(new GraphQLTypeReference("pattern"))) + .argument(stringArg("namespace")) + .dataFetcher(new NestedJDBCFetcher( + new JDBCFetcher("pattern_stops", "stop_id", null, false), + new JDBCFetcher("patterns", "pattern_id"))) + .build()) + .field(newFieldDefinition() + .name("routes") + // Field type should be equivalent to the final JDBCFetcher table type. + .type(new GraphQLList(routeType)) + .argument(stringArg("namespace")) + .argument(stringArg(SEARCH_ARG)) + .argument(intArg(LIMIT_ARG)) + .dataFetcher(new NestedJDBCFetcher( + new JDBCFetcher("pattern_stops", "stop_id", null, false), + new JDBCFetcher("patterns", "pattern_id", null, false), + new JDBCFetcher("routes", "route_id"))) + .build()) + .field(newFieldDefinition() + .name("stop_times") + // forward reference to the as yet undefined stopTimeType (must be defined after tripType) + .type(new GraphQLList(new GraphQLTypeReference("stopTime"))) + // FIXME Update JDBCFetcher to have noLimit boolean for fetchers on "naturally" nested types + // (i.e., nested types that typically would only be nested under another entity and only make sense + // with the entire set -- fares -> fare rules, trips -> stop times, patterns -> pattern stops/shapes) + .argument(intArg(LIMIT_ARG)) + .dataFetcher(new JDBCFetcher("stop_times", "stop_id", "stop_sequence", false))) + .build(); /** * Represents each stop in a list of stops within a pattern. @@ -441,389 +400,211 @@ public class GraphQLGtfsSchema { * that structure would prevent us from joining tables and returning additional stop details * like lat and lon, or pickup and dropoff types if we add those to the pattern signature. */ - public static final GraphQLObjectType patternStopType = newObject().name("patternStop") - .field(MapFetcher.field("id", GraphQLInt)) - .field(MapFetcher.field("pattern_id")) - .field(MapFetcher.field("stop_id")) - .field(MapFetcher.field("default_travel_time", GraphQLInt)) - .field(MapFetcher.field("default_dwell_time", GraphQLInt)) - .field(MapFetcher.field("shape_dist_traveled", GraphQLFloat)) - .field(MapFetcher.field("drop_off_type", GraphQLInt)) - .field(MapFetcher.field("pickup_type", GraphQLInt)) - .field(MapFetcher.field("stop_headsign")) - .field(MapFetcher.field("continuous_drop_off", GraphQLInt)) - .field(MapFetcher.field("continuous_pickup", GraphQLInt)) - .field(MapFetcher.field("stop_sequence", GraphQLInt)) - .field(MapFetcher.field("timepoint", GraphQLInt)) - // FIXME: This will only returns a list with one stop entity (unless there is a referential integrity issue) - // Should this be modified to be an object, rather than a list? - .field(newFieldDefinition() - .type(new GraphQLList(stopType)) - .name("stop") - .dataFetcher(new JDBCFetcher("stops", "stop_id")) - .build() - ) - .build(); + public static final GraphQLObjectType patternStopType = newObject() + .name("patternStop") + .field(MapFetcher.field("id", GraphQLInt)) + .field(MapFetcher.field("pattern_id")) + .field(MapFetcher.field("stop_id")) + .field(MapFetcher.field("default_travel_time", GraphQLInt)) + .field(MapFetcher.field("default_dwell_time", GraphQLInt)) + .field(MapFetcher.field("shape_dist_traveled", GraphQLFloat)) + .field(MapFetcher.field("drop_off_type", GraphQLInt)) + .field(MapFetcher.field("pickup_type", GraphQLInt)) + .field(MapFetcher.field("stop_headsign")) + .field(MapFetcher.field("continuous_drop_off", GraphQLInt)) + .field(MapFetcher.field("continuous_pickup", GraphQLInt)) + .field(MapFetcher.field("stop_sequence", GraphQLInt)) + .field(MapFetcher.field("timepoint", GraphQLInt)) + // FIXME: This will only returns a list with one stop entity (unless there is a referential integrity issue) + // Should this be modified to be an object, rather than a list? + .field(newFieldDefinition() + .type(new GraphQLList(stopType)) + .name("stop") + .dataFetcher(new JDBCFetcher("stops", "stop_id")) + .build() + ) + .build(); /** * The GraphQL API type representing entries in the table of errors encountered while loading or validating a feed. */ - public static GraphQLObjectType validationErrorType = newObject().name("validationError") - .description("An error detected when loading or validating a feed.") - .field(MapFetcher.field("error_id", GraphQLInt)) - .field(MapFetcher.field("error_type")) - .field(MapFetcher.field("entity_type")) - // FIXME: change to id? - .field(MapFetcher.field("line_number", GraphQLInt)) - .field(MapFetcher.field("entity_id")) - .field(MapFetcher.field("entity_sequence", GraphQLInt)) - .field(MapFetcher.field("bad_value")) - .build(); + public static GraphQLObjectType validationErrorType = newObject() + .name("validationError") + .description("An error detected when loading or validating a feed.") + .field(MapFetcher.field("error_id", GraphQLInt)) + .field(MapFetcher.field("error_type")) + .field(MapFetcher.field("entity_type")) + // FIXME: change to id? + .field(MapFetcher.field("line_number", GraphQLInt)) + .field(MapFetcher.field("entity_id")) + .field(MapFetcher.field("entity_sequence", GraphQLInt)) + .field(MapFetcher.field("bad_value")) + .build(); /** * The GraphQL API type representing counts of rows in the various GTFS tables. * The context here for fetching subfields is the feedType. A special dataFetcher is used to pass that identical * context down. */ - public static GraphQLObjectType rowCountsType = newObject().name("rowCounts") - .description("Counts of rows in the various GTFS tables.") - .field(RowCountFetcher.field("stops")) - .field(RowCountFetcher.field("trips")) - .field(RowCountFetcher.field("routes")) - .field(RowCountFetcher.field("stop_times")) - .field(RowCountFetcher.field("agency")) - .field(RowCountFetcher.field("calendar")) - .field(RowCountFetcher.field("calendar_dates")) - .field(RowCountFetcher.field("errors")) - .build(); + public static GraphQLObjectType rowCountsType = newObject() + .name("rowCounts") + .description("Counts of rows in the various GTFS tables.") + .field(RowCountFetcher.field("stops")) + .field(RowCountFetcher.field("trips")) + .field(RowCountFetcher.field("routes")) + .field(RowCountFetcher.field("stop_times")) + .field(RowCountFetcher.field("agency")) + .field(RowCountFetcher.field("calendar")) + .field(RowCountFetcher.field("calendar_dates")) + .field(RowCountFetcher.field("errors")) + .build(); - public static GraphQLObjectType tripGroupCountType = newObject().name("tripGroupCount") - .description("") - .field(RowCountFetcher.groupedField("trips", "service_id")) - .field(RowCountFetcher.groupedField("trips", "route_id")) - .field(RowCountFetcher.groupedField("trips", "pattern_id")) - .build(); + public static GraphQLObjectType tripGroupCountType = newObject() + .name("tripGroupCount") + .description("") + .field(RowCountFetcher.groupedField("trips", "service_id")) + .field(RowCountFetcher.groupedField("trips", "route_id")) + .field(RowCountFetcher.groupedField("trips", "pattern_id")) + .build(); /** * GraphQL does not have a type for arbitrary maps (String -> X). Such maps must be expressed as a list of * key-value pairs. This is probably intended to protect us from ourselves (sending untyped data) but it just * leads to silly workarounds like this. */ - public static GraphQLObjectType errorCountType = newObject().name("errorCount") - .description("Quantity of validation errors of a specific type.") - .field(string("type")) - .field(intt("count")) - .field(string("message")) - .field(string("priority")) - .build(); + public static GraphQLObjectType errorCountType = newObject() + .name("errorCount") + .description("Quantity of validation errors of a specific type.") + .field(string("type")) + .field(intt("count")) + .field(string("message")) + .field(string("priority")) + .build(); /** * The GraphQL API type representing a unique sequence of stops on a route. This is used to group trips together. */ - public static final GraphQLObjectType patternType = newObject().name("pattern") - .description("A sequence of stops that characterizes a set of trips on a single route.") - .field(MapFetcher.field("id", GraphQLInt)) - .field(MapFetcher.field("pattern_id")) - .field(MapFetcher.field("shape_id")) - .field(MapFetcher.field("route_id")) - // FIXME: Fields directly below are editor-specific. Move somewhere else? - .field(MapFetcher.field("direction_id", GraphQLInt)) - .field(MapFetcher.field("use_frequency", GraphQLInt)) - .field(MapFetcher.field("name")) - .field(newFieldDefinition() - .name("shape") - .type(new GraphQLList(shapePointType)) - // FIXME Update JDBCFetcher to have noLimit boolean for fetchers on "naturally" nested types - // (i.e., nested types that typically would only be nested under another entity and only make sense - // with the entire set -- fares -> fare rules, trips -> stop times, patterns -> pattern stops/shapes) - .argument(intArg(LIMIT_ARG)) - .dataFetcher(new JDBCFetcher("shapes", - "shape_id", - "shape_pt_sequence", - false)) - .build()) - .field(RowCountFetcher.field("trip_count", "trips", "pattern_id")) - .field(newFieldDefinition() - .name("pattern_stops") - .type(new GraphQLList(patternStopType)) - // FIXME Update JDBCFetcher to have noLimit boolean for fetchers on "naturally" nested types - // (i.e., nested types that typically would only be nested under another entity and only make sense - // with the entire set -- fares -> fare rules, trips -> stop times, patterns -> pattern stops/shapes) - .argument(intArg(LIMIT_ARG)) - .dataFetcher(new JDBCFetcher("pattern_stops", - "pattern_id", - "stop_sequence", - false)) - .build()) - .field(newFieldDefinition() - .name("stops") - .description("GTFS stop entities that the pattern serves") - // Field type should be equivalent to the final JDBCFetcher table type. - .type(new GraphQLList(stopType)) - // We scope to a single feed namespace, otherwise GTFS entity IDs are ambiguous. - .argument(stringArg("namespace")) - .argument(intArg(LIMIT_ARG)) - // We allow querying only for a single stop, otherwise result processing can take a long time (lots - // of join queries). - .argument(stringArg("pattern_id")) - .dataFetcher(new NestedJDBCFetcher( - new JDBCFetcher("pattern_stops", "pattern_id", null, false), - new JDBCFetcher("stops", "stop_id"))) - .build()) - .field(newFieldDefinition() - .name("trips") - .type(new GraphQLList(tripType)) - // FIXME Update JDBCFetcher to have noLimit boolean for fetchers on "naturally" nested types - // (i.e., nested types that typically would only be nested under another entity and only make sense - // with the entire set -- fares -> fare rules, trips -> stop times, patterns -> pattern stops/shapes) - .argument(intArg(LIMIT_ARG)) - .argument(stringArg(DATE_ARG)) - .argument(intArg(FROM_ARG)) - .argument(intArg(TO_ARG)) - .argument(multiStringArg("service_id")) - .dataFetcher(new JDBCFetcher("trips", "pattern_id")) - .build()) - // FIXME This is a singleton array because the JdbcFetcher currently only works with one-to-many joins. - .field(newFieldDefinition() - .name("route") - // Field type should be equivalent to the final JDBCFetcher table type. - .type(new GraphQLList(routeType)) - .argument(stringArg("namespace")) - .dataFetcher(new JDBCFetcher("routes", "route_id")) - .build()) - .build(); - - /** - * Durations that a service runs on each mode of transport (route_type). - */ - public static final GraphQLObjectType serviceDurationType = newObject().name("serviceDuration") - .field(MapFetcher.field("route_type", GraphQLInt)) - .field(MapFetcher.field("duration_seconds", GraphQLInt)) - .build(); - - /** - * The GraphQL API type representing a service (a service_id attached to trips to say they run on certain days). - */ - public static GraphQLObjectType serviceType = newObject().name("service") - .description("A group of trips that all run together on certain days.") - .field(MapFetcher.field("service_id")) - .field(MapFetcher.field("n_days_active")) - .field(MapFetcher.field("duration_seconds")) - .field(newFieldDefinition() - .name("dates") - // FIXME Update JDBCFetcher to have noLimit boolean for fetchers on "naturally" nested types - // (i.e., nested types that typically would only be nested under another entity and only make sense - // with the entire set -- fares -> fare rules, trips -> stop times, patterns -> pattern stops/shapes) - .type(new GraphQLList(GraphQLString)) - .dataFetcher(new SQLColumnFetcher("service_dates", "service_id", "service_date")) - .build()) - .field(newFieldDefinition() - .name("trips") - // FIXME Update JDBCFetcher to have noLimit boolean for fetchers on "naturally" nested types - // (i.e., nested types that typically would only be nested under another entity and only make sense - // with the entire set -- fares -> fare rules, trips -> stop times, patterns -> pattern stops/shapes) - .type(new GraphQLList(tripType)) - .dataFetcher(new JDBCFetcher("trips", "service_id")) - .build()) - .field(newFieldDefinition() - .name("durations") - .type(new GraphQLList(serviceDurationType)) - .dataFetcher(new JDBCFetcher("service_durations", "service_id")) - .build()) - .build(); - - public static final GraphQLObjectType stopAreaType = newObject().name("stopArea") - .description("A GTFS stop area object") - .field(MapFetcher.field("id", GraphQLInt)) - .field(MapFetcher.field(StopArea.AREA_ID_COLUMN_NAME)) - .field(MapFetcher.field(StopArea.STOP_ID_COLUMN_NAME)) - .field(newFieldDefinition() - .name("stops") - .type(new GraphQLList(stopType)) - .dataFetcher(new JDBCFetcher("stops", StopArea.STOP_ID_COLUMN_NAME)) - .build()) - .build(); - - public static final GraphQLObjectType areaType = newObject().name("area") - .description("A GTFS area object") - .field(MapFetcher.field("id", GraphQLInt)) - .field(MapFetcher.field(Area.AREA_ID_COLUMN_NAME)) - .field(MapFetcher.field(Area.AREA_NAME_COLUMN_NAME)) - .field(newFieldDefinition() - .name("stopAreas") - .type(new GraphQLList(stopAreaType)) - .dataFetcher(new JDBCFetcher(StopArea.TABLE_NAME, Area.AREA_ID_COLUMN_NAME)) - .build()) - .build(); - - public static final GraphQLObjectType fareMediaType = newObject().name("fareMedia") - .description("A GTFS fare media object") - .field(MapFetcher.field("id", GraphQLInt)) - .field(MapFetcher.field(FareMedia.FARE_MEDIA_ID_COLUMN_NAME)) - .field(MapFetcher.field(FareMedia.FARE_MEDIA_NAME_COLUMN_NAME)) - .field(MapFetcher.field(FareMedia.FARE_MEDIA_TYPE_COLUMN_NAME)) - .build(); - - public static final GraphQLObjectType fareProductType = newObject().name("fareProduct") - .description("A GTFS fare product object") - .field(MapFetcher.field("id", GraphQLInt)) - .field(MapFetcher.field(FareProduct.FARE_PRODUCT_ID_COLUMN_NAME)) - .field(MapFetcher.field(FareProduct.FARE_PRODUCT_NAME_COLUMN_NAME)) - .field(MapFetcher.field(FareProduct.FARE_MEDIA_ID_COLUMN_NAME)) - .field(MapFetcher.field(FareProduct.AMOUNT_COLUMN_NAME)) - .field(MapFetcher.field(FareProduct.CURRENCY_COLUMN_NAME)) - .field(newFieldDefinition() - .name("fareMedia") - .type(new GraphQLList(fareMediaType)) - .dataFetcher(new JDBCFetcher(FareMedia.TABLE_NAME, FareProduct.FARE_MEDIA_ID_COLUMN_NAME)) - .build()) - .build(); - - public static final GraphQLObjectType timeFrameType = newObject().name("timeFrame") - .description("A GTFS time frame object") - .field(MapFetcher.field("id", GraphQLInt)) - .field(MapFetcher.field(TimeFrame.TIME_FRAME_GROUP_ID_COLUMN_NAME)) - .field(MapFetcher.field(TimeFrame.START_TIME_COLUMN_NAME)) - .field(MapFetcher.field(TimeFrame.END_TIME_COLUMN_NAME)) - .field(MapFetcher.field(TimeFrame.SERVICE_ID_COLUMN_NAME)) - .build(); - - public static final GraphQLObjectType networkType = newObject().name("network") - .description("A GTFS network object") - .field(MapFetcher.field("id", GraphQLInt)) - .field(MapFetcher.field(Network.NETWORK_ID_COLUMN_NAME)) - .field(MapFetcher.field(Network.NETWORK_NAME_COLUMN_NAME)) - .build(); - - public static final GraphQLObjectType fareLegRuleType = newObject().name("fareLegRule") - .description("A GTFS fare leg rule object") + public static final GraphQLObjectType patternType = newObject() + .name("pattern") + .description("A sequence of stops that characterizes a set of trips on a single route.") .field(MapFetcher.field("id", GraphQLInt)) - .field(MapFetcher.field(FareLegRule.LEG_GROUP_ID_COLUMN_NAME)) - .field(MapFetcher.field(FareLegRule.NETWORK_ID_COLUMN_NAME)) - .field(MapFetcher.field(FareLegRule.FROM_AREA_ID_COLUMN_NAME)) - .field(MapFetcher.field(FareLegRule.TO_AREA_ID_COLUMN_NAME)) - .field(MapFetcher.field(FareLegRule.FROM_TIMEFRAME_GROUP_ID_COLUMN_NAME)) - .field(MapFetcher.field(FareLegRule.TO_TIMEFRAME_GROUP_ID_COLUMN_NAME)) - .field(MapFetcher.field(FareLegRule.FARE_PRODUCT_ID_COLUMN_NAME)) - .field(MapFetcher.field(FareLegRule.RULE_PRIORITY_COLUMN_NAME)) - // Will return either routes or networks, not both. - .field(newFieldDefinition() - .name("routes") - .type(new GraphQLList(routeType)) - .dataFetcher(new JDBCFetcher(Route.TABLE_NAME, FareLegRule.NETWORK_ID_COLUMN_NAME)) - .build()) - .field(newFieldDefinition() - .name("networks") - .type(new GraphQLList(networkType)) - .dataFetcher(new JDBCFetcher(Network.TABLE_NAME, Network.NETWORK_ID_COLUMN_NAME)) - .build()) - .field(newFieldDefinition() - .name("fareProducts") - .type(new GraphQLList(fareProductType)) - .dataFetcher(new JDBCFetcher(FareProduct.TABLE_NAME, FareLegRule.FARE_PRODUCT_ID_COLUMN_NAME)) - .build()) - // fromTimeFrame and toTimeFrame may return multiple time frames. + .field(MapFetcher.field("pattern_id")) + .field(MapFetcher.field("shape_id")) + .field(MapFetcher.field("route_id")) + // FIXME: Fields directly below are editor-specific. Move somewhere else? + .field(MapFetcher.field("direction_id", GraphQLInt)) + .field(MapFetcher.field("use_frequency", GraphQLInt)) + .field(MapFetcher.field("name")) .field(newFieldDefinition() - .name("fromTimeFrame") - .type(new GraphQLList(timeFrameType)) + .name("shape") + .type(new GraphQLList(shapePointType)) + // FIXME Update JDBCFetcher to have noLimit boolean for fetchers on "naturally" nested types + // (i.e., nested types that typically would only be nested under another entity and only make sense + // with the entire set -- fares -> fare rules, trips -> stop times, patterns -> pattern stops/shapes) + .argument(intArg(LIMIT_ARG)) .dataFetcher(new JDBCFetcher( - TimeFrame.TABLE_NAME, - FareLegRule.FROM_TIMEFRAME_GROUP_ID_COLUMN_NAME, - null, - false, - TimeFrame.TIME_FRAME_GROUP_ID_COLUMN_NAME) + "shapes", + "shape_id", + "shape_pt_sequence", + false) ) .build()) + .field(RowCountFetcher.field("trip_count", "trips", "pattern_id")) .field(newFieldDefinition() - .name("toTimeFrame") - .type(new GraphQLList(timeFrameType)) + .name("pattern_stops") + .type(new GraphQLList(patternStopType)) + // FIXME Update JDBCFetcher to have noLimit boolean for fetchers on "naturally" nested types + // (i.e., nested types that typically would only be nested under another entity and only make sense + // with the entire set -- fares -> fare rules, trips -> stop times, patterns -> pattern stops/shapes) + .argument(intArg(LIMIT_ARG)) .dataFetcher(new JDBCFetcher( - TimeFrame.TABLE_NAME, - FareLegRule.TO_TIMEFRAME_GROUP_ID_COLUMN_NAME, - null, - false, - TimeFrame.TIME_FRAME_GROUP_ID_COLUMN_NAME) + "pattern_stops", + "pattern_id", + "stop_sequence", + false) ) .build()) .field(newFieldDefinition() - .name("toArea") - .type(new GraphQLList(areaType)) - .dataFetcher(new JDBCFetcher( - Area.TABLE_NAME, - FareLegRule.TO_AREA_ID_COLUMN_NAME, - null, - false, - Area.AREA_ID_COLUMN_NAME) - ) + .name("stops") + .description("GTFS stop entities that the pattern serves") + // Field type should be equivalent to the final JDBCFetcher table type. + .type(new GraphQLList(stopType)) + // We scope to a single feed namespace, otherwise GTFS entity IDs are ambiguous. + .argument(stringArg("namespace")) + .argument(intArg(LIMIT_ARG)) + // We allow querying only for a single stop, otherwise result processing can take a long time (lots + // of join queries). + .argument(stringArg("pattern_id")) + .dataFetcher(new NestedJDBCFetcher( + new JDBCFetcher("pattern_stops", "pattern_id", null, false), + new JDBCFetcher("stops", "stop_id"))) .build()) .field(newFieldDefinition() - .name("fromArea") - .type(new GraphQLList(areaType)) - .dataFetcher(new JDBCFetcher( - Area.TABLE_NAME, - FareLegRule.FROM_AREA_ID_COLUMN_NAME, - null, - false, - Area.AREA_ID_COLUMN_NAME) - ) + .name("trips") + .type(new GraphQLList(tripType)) + // FIXME Update JDBCFetcher to have noLimit boolean for fetchers on "naturally" nested types + // (i.e., nested types that typically would only be nested under another entity and only make sense + // with the entire set -- fares -> fare rules, trips -> stop times, patterns -> pattern stops/shapes) + .argument(intArg(LIMIT_ARG)) + .argument(stringArg(DATE_ARG)) + .argument(intArg(FROM_ARG)) + .argument(intArg(TO_ARG)) + .argument(multiStringArg("service_id")) + .dataFetcher(new JDBCFetcher("trips", "pattern_id")) .build()) - .build(); - - public static final GraphQLObjectType fareTransferRuleType = newObject().name("fareTransferRule") - .description("A GTFS fare transfer rule object") - .field(MapFetcher.field("id", GraphQLInt)) - .field(MapFetcher.field(FareTransferRule.FROM_LEG_GROUP_ID_COLUMN_NAME)) - .field(MapFetcher.field(FareTransferRule.TO_LEG_GROUP_ID_COLUMN_NAME)) - .field(MapFetcher.field(FareTransferRule.TRANSFER_COUNT_COLUMN_NAME)) - .field(MapFetcher.field(FareTransferRule.DURATION_LIMIT_COLUMN_NAME)) - .field(MapFetcher.field(FareTransferRule.DURATION_LIMIT_TYPE_COLUMN_NAME)) - .field(MapFetcher.field(FareTransferRule.FARE_PRODUCT_ID_COLUMN_NAME)) + // FIXME This is a singleton array because the JdbcFetcher currently only works with one-to-many joins. .field(newFieldDefinition() - .name("fareProducts") - .type(new GraphQLList(fareProductType)) - .dataFetcher(new JDBCFetcher(FareProduct.TABLE_NAME, FareProduct.FARE_PRODUCT_ID_COLUMN_NAME)) + .name("route") + // Field type should be equivalent to the final JDBCFetcher table type. + .type(new GraphQLList(routeType)) + .argument(stringArg("namespace")) + .dataFetcher(new JDBCFetcher("routes", "route_id")) .build()) + .build(); + + /** + * Durations that a service runs on each mode of transport (route_type). + */ + public static final GraphQLObjectType serviceDurationType = newObject() + .name("serviceDuration") + .field(MapFetcher.field("route_type", GraphQLInt)) + .field(MapFetcher.field("duration_seconds", GraphQLInt)) + .build(); + + /** + * The GraphQL API type representing a service (a service_id attached to trips to say they run on certain days). + */ + public static GraphQLObjectType serviceType = newObject() + .name("service") + .description("A group of trips that all run together on certain days.") + .field(MapFetcher.field("service_id")) + .field(MapFetcher.field("n_days_active")) + .field(MapFetcher.field("duration_seconds")) .field(newFieldDefinition() - .name("fromFareLegRule") - .type(new GraphQLList(fareLegRuleType)) - .dataFetcher(new JDBCFetcher( - FareLegRule.TABLE_NAME, - FareTransferRule.FROM_LEG_GROUP_ID_COLUMN_NAME, - null, - false, - FareLegRule.LEG_GROUP_ID_COLUMN_NAME) - ) + .name("dates") + // FIXME Update JDBCFetcher to have noLimit boolean for fetchers on "naturally" nested types + // (i.e., nested types that typically would only be nested under another entity and only make sense + // with the entire set -- fares -> fare rules, trips -> stop times, patterns -> pattern stops/shapes) + .type(new GraphQLList(GraphQLString)) + .dataFetcher(new SQLColumnFetcher("service_dates", "service_id", "service_date")) .build()) .field(newFieldDefinition() - .name("toFareLegRule") - .type(new GraphQLList(fareLegRuleType)) - .dataFetcher(new JDBCFetcher( - FareLegRule.TABLE_NAME, - FareTransferRule.TO_LEG_GROUP_ID_COLUMN_NAME, - null, - false, - FareLegRule.LEG_GROUP_ID_COLUMN_NAME) - ) + .name("trips") + // FIXME Update JDBCFetcher to have noLimit boolean for fetchers on "naturally" nested types + // (i.e., nested types that typically would only be nested under another entity and only make sense + // with the entire set -- fares -> fare rules, trips -> stop times, patterns -> pattern stops/shapes) + .type(new GraphQLList(tripType)) + .dataFetcher(new JDBCFetcher("trips", "service_id")) .build()) - .build(); - - public static final GraphQLObjectType routeNetworkType = newObject().name("routeNetwork") - .description("A GTFS route network object") - .field(MapFetcher.field("id", GraphQLInt)) - .field(MapFetcher.field(RouteNetwork.NETWORK_ID_COLUMN_NAME)) - .field(MapFetcher.field(RouteNetwork.ROUTE_ID_COLUMN_NAME)) .field(newFieldDefinition() - .name("networks") - .type(new GraphQLList(networkType)) - .dataFetcher(new JDBCFetcher(Network.TABLE_NAME, RouteNetwork.NETWORK_ID_COLUMN_NAME)) + .name("durations") + .type(new GraphQLList(serviceDurationType)) + .dataFetcher(new JDBCFetcher("service_durations", "service_id")) .build()) .build(); - /** - * The GraphQL API type representing entries in the top-level table listing all the feeds imported into a gtfs-api - * database, and with sub-fields for each table of GTFS entities within a single feed. - */ - public static final GraphQLObjectType feedType = newObject().name("feedVersion") + public static GraphQLObjectType getFeedType(List faresV2FieldDefinitions) { + return newObject().name("feedVersion") // First, the fields present in the top level table. .field(MapFetcher.field("namespace")) .field(MapFetcher.field("feed_id")) @@ -835,59 +616,34 @@ public class GraphQLGtfsSchema { .field(MapFetcher.field("snapshot_of")) // A field containing row counts for every table. .field(newFieldDefinition() - .name("row_counts") - .type(rowCountsType) - .dataFetcher(new SourceObjectFetcher()) - .build()) + .name("row_counts") + .type(rowCountsType) + .dataFetcher(new SourceObjectFetcher()) + .build()) .field(newFieldDefinition() - .name("trip_counts") - .type(tripGroupCountType) - .dataFetcher(new SourceObjectFetcher()) - .build()) + .name("trip_counts") + .type(tripGroupCountType) + .dataFetcher(new SourceObjectFetcher()) + .build()) // A field containing counts for each type of error independently. .field(newFieldDefinition() - .name("error_counts") - .type(new GraphQLList(errorCountType)) - .dataFetcher(new ErrorCountFetcher()) - .build()) + .name("error_counts") + .type(new GraphQLList(errorCountType)) + .dataFetcher(new ErrorCountFetcher()) + .build()) // A field for the errors themselves. - .field(newFieldDefinition() - .name("errors") - .type(new GraphQLList(validationErrorType)) - .argument(stringArg("namespace")) - .argument(multiStringArg("error_type")) - .argument(intArg(LIMIT_ARG)) - .argument(intArg(OFFSET_ARG)) - .dataFetcher(new JDBCFetcher("errors")) - .build() - ) - // A field containing the feed info table. - .field(newFieldDefinition() - .name("feed_info") - .type(new GraphQLList(feedInfoType)) - // FIXME: These arguments really don't make sense for feed info, but in order to create generic - // fetches on the client-side they have been included here. - .argument(intArg(ID_ARG)) - .argument(intArg(LIMIT_ARG)) - .argument(intArg(OFFSET_ARG)) - // DataFetchers can either be class instances implementing the interface, or a static function reference - .dataFetcher(new JDBCFetcher("feed_info")) - .build()) + .field(createFieldDefinition( + "errors", + validationErrorType, + GraphQLUtil.buildArgs(multiStringArg("error_type")) + )) + .field(createFieldDefinition("feed_info", feedInfoType, "feed_info")) // A field containing all the unique stop sequences (patterns) in this feed. - .field(newFieldDefinition() - .name("patterns") - .type(new GraphQLList(patternType)) - .argument(intArg(ID_ARG)) - .argument(intArg(LIMIT_ARG)) - .argument(intArg(OFFSET_ARG)) - .argument(floatArg(MIN_LAT)) - .argument(floatArg(MIN_LON)) - .argument(floatArg(MAX_LAT)) - .argument(floatArg(MAX_LON)) - .argument(multiStringArg("pattern_id")) - // DataFetchers can either be class instances implementing the interface, or a static function reference - .dataFetcher(new JDBCFetcher("patterns")) - .build()) + .field(createFieldDefinition( + "patterns", + patternType, + GraphQLUtil.buildArgs(floatArg(MIN_LAT), floatArg(MIN_LON), floatArg(MAX_LAT), floatArg(MAX_LON)) + )) .field(newFieldDefinition() .name("shapes_as_polylines") .type(new GraphQLList(shapeEncodedPolylineType)) @@ -895,272 +651,80 @@ public class GraphQLGtfsSchema { .dataFetcher(new PolylineFetcher()) .build()) // Then the fields for the sub-tables within the feed (loaded directly from GTFS). - .field(newFieldDefinition() - .name("agency") - .type(new GraphQLList(GraphQLGtfsSchema.agencyType)) - .argument(stringArg("namespace")) // FIXME maybe these nested namespace arguments are not doing anything. - .argument(multiStringArg("agency_id")) - .argument(intArg(ID_ARG)) - .argument(intArg(LIMIT_ARG)) - .argument(intArg(OFFSET_ARG)) - .dataFetcher(new JDBCFetcher("agency")) - .build() - ) - .field(newFieldDefinition() - .name("calendar") - .type(new GraphQLList(GraphQLGtfsSchema.calendarType)) - .argument(stringArg("namespace")) // FIXME maybe these nested namespace arguments are not doing anything. - .argument(multiStringArg("service_id")) - .argument(intArg(ID_ARG)) - .argument(intArg(LIMIT_ARG)) - .argument(intArg(OFFSET_ARG)) - .dataFetcher(new JDBCFetcher("calendar")) - .build() - ) - .field(newFieldDefinition() - .name("fares") - .type(new GraphQLList(GraphQLGtfsSchema.fareType)) - .argument(stringArg("namespace")) // FIXME maybe these nested namespace arguments are not doing anything. - .argument(multiStringArg("fare_id")) - .argument(intArg(ID_ARG)) - .argument(intArg(LIMIT_ARG)) - .argument(intArg(OFFSET_ARG)) - .dataFetcher(new JDBCFetcher("fare_attributes")) - .build() - ) - .field(newFieldDefinition() - .name("routes") - .type(new GraphQLList(GraphQLGtfsSchema.routeType)) - .argument(stringArg("namespace")) - .argument(multiStringArg("route_id")) - .argument(stringArg(SEARCH_ARG)) - .argument(intArg(ID_ARG)) - .argument(intArg(LIMIT_ARG)) - .argument(intArg(OFFSET_ARG)) - .dataFetcher(new JDBCFetcher("routes")) - .build() - ) - .field(newFieldDefinition() - .name("stops") - .type(new GraphQLList(GraphQLGtfsSchema.stopType)) - .argument(stringArg("namespace")) // FIXME maybe these nested namespace arguments are not doing anything. - .argument(multiStringArg("stop_id")) - .argument(multiStringArg("pattern_id")) - .argument(floatArg(MIN_LAT)) - .argument(floatArg(MIN_LON)) - .argument(floatArg(MAX_LAT)) - .argument(floatArg(MAX_LON)) - .argument(stringArg(SEARCH_ARG)) - .argument(intArg(ID_ARG)) - .argument(intArg(LIMIT_ARG)) - .argument(intArg(OFFSET_ARG)) - .dataFetcher(new JDBCFetcher("stops")) - .build() - ) - .field(newFieldDefinition() - .name("trips") - .type(new GraphQLList(GraphQLGtfsSchema.tripType)) - .argument(stringArg("namespace")) - .argument(multiStringArg("trip_id")) - .argument(multiStringArg("route_id")) - .argument(intArg(ID_ARG)) - .argument(intArg(LIMIT_ARG)) - .argument(stringArg(DATE_ARG)) - .argument(intArg(FROM_ARG)) - .argument(intArg(TO_ARG)) - .argument(intArg(OFFSET_ARG)) - .dataFetcher(new JDBCFetcher("trips")) - .build() - ) - .field(newFieldDefinition() - .name("schedule_exceptions") - .type(new GraphQLList(GraphQLGtfsSchema.scheduleExceptionType)) - .argument(stringArg("namespace")) - .argument(intArg(ID_ARG)) - .argument(intArg(LIMIT_ARG)) - .argument(intArg(OFFSET_ARG)) - .dataFetcher(new JDBCFetcher("schedule_exceptions")) - .build() - ) - .field(newFieldDefinition() - .name("stop_times") - .type(new GraphQLList(GraphQLGtfsSchema.stopTimeType)) - .argument(stringArg("namespace")) - .argument(intArg(LIMIT_ARG)) - .argument(intArg(OFFSET_ARG)) - .dataFetcher(new JDBCFetcher("stop_times")) - .build() - ) - .field(newFieldDefinition() - .name("services") - .argument(multiStringArg("service_id")) - .type(new GraphQLList(GraphQLGtfsSchema.serviceType)) - .argument(intArg(LIMIT_ARG)) // Todo somehow autogenerate these JDBCFetcher builders to include standard params. - .argument(intArg(OFFSET_ARG)) - .dataFetcher(new JDBCFetcher("services")) - .build() - ) - .field(newFieldDefinition() - .name("attributions") - .type(new GraphQLList(GraphQLGtfsSchema.attributionsType)) - .argument(stringArg("namespace")) // FIXME maybe these nested namespace arguments are not doing anything. - .argument(intArg(ID_ARG)) - .argument(intArg(LIMIT_ARG)) - .argument(intArg(OFFSET_ARG)) - .dataFetcher(new JDBCFetcher("attributions")) - .build() - ) - .field(newFieldDefinition() - .name("translations") - .type(new GraphQLList(GraphQLGtfsSchema.translationsType)) - .argument(stringArg("namespace")) // FIXME maybe these nested namespace arguments are not doing anything. - .argument(intArg(ID_ARG)) - .argument(intArg(LIMIT_ARG)) - .argument(intArg(OFFSET_ARG)) - .dataFetcher(new JDBCFetcher("translations")) - .build() - ) - .field(newFieldDefinition() - .name("area") - .type(new GraphQLList(GraphQLGtfsSchema.areaType)) - .argument(stringArg("namespace")) - .argument(multiStringArg("area_id")) - .argument(intArg(ID_ARG)) - .argument(intArg(LIMIT_ARG)) - .argument(intArg(OFFSET_ARG)) - .dataFetcher(new JDBCFetcher("areas")) - .build() - ) - .field(newFieldDefinition() - .name("stopArea") - .type(new GraphQLList(GraphQLGtfsSchema.stopAreaType)) - .argument(stringArg("namespace")) - .argument(multiStringArg("area_id")) - .argument(intArg(ID_ARG)) - .argument(intArg(LIMIT_ARG)) - .argument(intArg(OFFSET_ARG)) - .dataFetcher(new JDBCFetcher("stop_areas")) - .build() - ) - .field(newFieldDefinition() - .name("fareTransferRule") - .type(new GraphQLList(GraphQLGtfsSchema.fareTransferRuleType)) - .argument(stringArg("namespace")) - .argument(intArg(ID_ARG)) - .argument(intArg(LIMIT_ARG)) - .argument(intArg(OFFSET_ARG)) - .dataFetcher(new JDBCFetcher("fare_transfer_rules")) - .build() - ) - .field(newFieldDefinition() - .name("fareProduct") - .type(new GraphQLList(GraphQLGtfsSchema.fareProductType)) - .argument(stringArg("namespace")) - .argument(intArg(ID_ARG)) - .argument(intArg(LIMIT_ARG)) - .argument(intArg(OFFSET_ARG)) - .dataFetcher(new JDBCFetcher("fare_products")) - .build() - ) - .field(newFieldDefinition() - .name("fareMedia") - .type(new GraphQLList(GraphQLGtfsSchema.fareMediaType)) - .argument(stringArg("namespace")) - .argument(intArg(ID_ARG)) - .argument(intArg(LIMIT_ARG)) - .argument(intArg(OFFSET_ARG)) - .dataFetcher(new JDBCFetcher("fare_media")) - .build() - ) - .field(newFieldDefinition() - .name("fareLegRule") - .type(new GraphQLList(GraphQLGtfsSchema.fareLegRuleType)) - .argument(stringArg("namespace")) - .argument(intArg(ID_ARG)) - .argument(intArg(LIMIT_ARG)) - .argument(intArg(OFFSET_ARG)) - .dataFetcher(new JDBCFetcher("fare_leg_rules")) - .build() - ) - .field(newFieldDefinition() - .name("timeFrame") - .type(new GraphQLList(GraphQLGtfsSchema.timeFrameType)) - .argument(stringArg("namespace")) - .argument(intArg(ID_ARG)) - .argument(intArg(LIMIT_ARG)) - .argument(intArg(OFFSET_ARG)) - .dataFetcher(new JDBCFetcher(TimeFrame.TABLE_NAME)) - .build() - ) - .field(newFieldDefinition() - .name("network") - .type(new GraphQLList(GraphQLGtfsSchema.networkType)) - .argument(stringArg("namespace")) - .argument(intArg(ID_ARG)) - .argument(intArg(LIMIT_ARG)) - .argument(intArg(OFFSET_ARG)) - .dataFetcher(new JDBCFetcher(Network.TABLE_NAME)) - .build() - ) - .field(newFieldDefinition() - .name("routeNetwork") - .type(new GraphQLList(GraphQLGtfsSchema.routeNetworkType)) - .argument(stringArg("namespace")) - .argument(intArg(ID_ARG)) - .argument(intArg(LIMIT_ARG)) - .argument(intArg(OFFSET_ARG)) - .dataFetcher(new JDBCFetcher(RouteNetwork.TABLE_NAME)) - .build() - ) + .field(createFieldDefinition( + "agency", + agencyType, + GraphQLUtil.buildArgs(multiStringArg("agency_id")) + )) + .field(createFieldDefinition("calendar", calendarType, GraphQLUtil.buildArgs(multiStringArg("service_id")))) + .field(createFieldDefinition( + "fares", + fareType, + "fare_attributes", + GraphQLUtil.buildArgs(multiStringArg("fare_id")) + )) + .field(createFieldDefinition( + "routes", + routeType, + GraphQLUtil.buildArgs(multiStringArg("route_id"), stringArg(SEARCH_ARG)) + )) + .field(createFieldDefinition( + "stops", + stopType, + GraphQLUtil.buildArgs( + multiStringArg("stop_id"), + multiStringArg("pattern_id"), + floatArg(MIN_LAT), + floatArg(MIN_LON), + floatArg(MAX_LAT), + floatArg(MAX_LON), + stringArg(SEARCH_ARG) + ) + )) + .field(createFieldDefinition( + "trips", + tripType, + GraphQLUtil.buildArgs( + multiStringArg("trip_id"), + multiStringArg("route_id"), + stringArg(DATE_ARG), + intArg(FROM_ARG), + intArg(TO_ARG) + ) + )) + .field(createFieldDefinition("schedule_exceptions", scheduleExceptionType, buildArgs())) + .field(createFieldDefinition("stop_times", stopTimeType, buildArgs())) + .field(createFieldDefinition("services", serviceType, buildArgs())) + .field(createFieldDefinition("attributions", attributionsType, buildArgs())) + .field(createFieldDefinition("translations", translationsType, buildArgs())) + .fields(faresV2FieldDefinitions) .build(); + } /** * This is the top-level query - you must always specify a feed to fetch, and then some other things inside that feed. * TODO decide whether to call this feedVersion or feed within gtfs-lib context. */ private static GraphQLObjectType feedQuery = newObject() - .name("feedQuery") - .field(newFieldDefinition() - .name("feed") - .type(feedType) - // We scope to a single feed namespace, otherwise GTFS entity IDs are ambiguous. - .argument(stringArg("namespace")) - .dataFetcher(new FeedFetcher()) - .build() - ) - .build(); - - /** - * A top-level query that returns all of the patterns that serve a given stop ID. This demonstrates the use of - * NestedJDBCFetcher. - */ -// private static GraphQLObjectType patternsForStopQuery = newObject() -// .name("patternsForStopQuery") -// .field(newFieldDefinition() -// .name("patternsForStop") -// // Field type should be equivalent to the final JDBCFetcher table type. -// .type(new GraphQLList(patternType)) -// // We scope to a single feed namespace, otherwise GTFS entity IDs are ambiguous. -// .argument(stringArg("namespace")) -// // We allow querying only for a single stop, otherwise result processing can take a long time (lots -// // of join queries). -// .argument(stringArg("stop_id")) -// .dataFetcher(new NestedJDBCFetcher( -// new JDBCFetcher("pattern_stops", "stop_id"), -// new JDBCFetcher("patterns", "pattern_id"))) -// .build()) -// .build(); - + .name("feedQuery") + .field(newFieldDefinition() + .name("feed") + .type(getFeedType(GraphQLGtfsFaresV2Schema.getFaresV2FieldDefinitions())) + // We scope to a single feed namespace, otherwise GTFS entity IDs are ambiguous. + .argument(stringArg("namespace")) + .dataFetcher(new FeedFetcher()) + .build() + ) + .build(); /** * This is the new schema as of July 2017, where all sub-entities are wrapped in a feed. * Because all of these fields are static (ugh) this must be declared after the feedQuery it references. */ public static final GraphQLSchema feedBasedSchema = GraphQLSchema - .newSchema() - .query(feedQuery) -// .query(patternsForStopQuery) - .build(); + .newSchema() + .query(feedQuery) + .build(); private static class StringCoercing implements Coercing { @@ -1169,7 +733,6 @@ public Object serialize(Object input) { String[] strings = new String[]{}; try { strings = (String[])((Array) input).getArray(); -// if (strings == null) strings = new String[]{}; } catch (SQLException e) { e.printStackTrace(); } diff --git a/src/main/java/com/conveyal/gtfs/graphql/GraphQLUtil.java b/src/main/java/com/conveyal/gtfs/graphql/GraphQLUtil.java index 5002e5ccc..73a1cf6a3 100644 --- a/src/main/java/com/conveyal/gtfs/graphql/GraphQLUtil.java +++ b/src/main/java/com/conveyal/gtfs/graphql/GraphQLUtil.java @@ -1,10 +1,20 @@ package com.conveyal.gtfs.graphql; +import com.conveyal.gtfs.graphql.fetchers.JDBCFetcher; import graphql.schema.GraphQLArgument; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLList; +import graphql.schema.GraphQLType; import graphql.schema.PropertyDataFetcher; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static com.conveyal.gtfs.graphql.fetchers.JDBCFetcher.ID_ARG; +import static com.conveyal.gtfs.graphql.fetchers.JDBCFetcher.LIMIT_ARG; +import static com.conveyal.gtfs.graphql.fetchers.JDBCFetcher.OFFSET_ARG; import static graphql.Scalars.GraphQLFloat; import static graphql.Scalars.GraphQLInt; import static graphql.Scalars.GraphQLString; @@ -57,4 +67,88 @@ public static GraphQLArgument floatArg (String name) { .build(); } + /** + * Standard base arguments. + */ + public static List buildArgs() { + return new ArrayList<>(Arrays.asList(intArg(ID_ARG), intArg(LIMIT_ARG), intArg(OFFSET_ARG))); + } + + /** + * Standard base arguments with additions. + */ + public static List buildArgs(GraphQLArgument... addOns) { + List args = buildArgs(); + Collections.addAll(args, addOns); + return args; + } + + /** + * Standard field definition with base arguments. + */ + public static GraphQLFieldDefinition createFieldDefinition(String name, GraphQLType graphQLType, String tableName) { + return createFieldDefinition(name, graphQLType, tableName, buildArgs()); + } + + /** + * Field definition with bespoke arguments. Name and table name are the same. + */ + public static GraphQLFieldDefinition createFieldDefinition( + String name, + GraphQLType graphQLType, + List arguments + ) { + return createFieldDefinition(name, graphQLType, name, arguments); + } + + /** + * Field definition with bespoke arguments. + */ + public static GraphQLFieldDefinition createFieldDefinition( + String name, + GraphQLType graphQLType, + String tableName, + List arguments + ) { + return newFieldDefinition() + .name(name) + .type(new GraphQLList(graphQLType)) + .argument(stringArg("namespace")) + .arguments(arguments) + .dataFetcher(new JDBCFetcher(tableName)) + .build(); + } + + /** + * Field definition for standard table join. + */ + public static GraphQLFieldDefinition createFieldDefinition( + String name, + GraphQLType graphQLType, + String tableName, + String parentJoinField + ) { + return newFieldDefinition() + .name(name) + .type(new GraphQLList(graphQLType)) + .dataFetcher(new JDBCFetcher(tableName, parentJoinField)) + .build(); + } + + /** + * Field definition for join with child table. + */ + public static GraphQLFieldDefinition createFieldDefinition( + String name, + GraphQLType graphQLType, + String tableName, + String parentJoinField, + String childJoinField + ) { + return newFieldDefinition() + .name(name) + .type(new GraphQLList(graphQLType)) + .dataFetcher(new JDBCFetcher(tableName, parentJoinField, null, false, childJoinField)) + .build(); + } } diff --git a/src/main/java/com/conveyal/gtfs/loader/EntityPopulator.java b/src/main/java/com/conveyal/gtfs/loader/EntityPopulator.java index ca82d69e3..e6ed31fdb 100644 --- a/src/main/java/com/conveyal/gtfs/loader/EntityPopulator.java +++ b/src/main/java/com/conveyal/gtfs/loader/EntityPopulator.java @@ -241,80 +241,80 @@ public interface EntityPopulator { EntityPopulator AREA = (result, columnForName) -> { Area area = new Area(); - area.area_id = getStringIfPresent(result, Area.AREA_ID_COLUMN_NAME, columnForName); - area.area_name = getStringIfPresent(result, Area.AREA_NAME_COLUMN_NAME, columnForName); + area.area_id = getStringIfPresent(result, Area.AREA_ID_NAME, columnForName); + area.area_name = getStringIfPresent(result, Area.AREA_NAME_NAME, columnForName); return area; }; EntityPopulator STOP_AREA = (result, columnForName) -> { StopArea stopArea = new StopArea(); - stopArea.area_id = getStringIfPresent(result, StopArea.AREA_ID_COLUMN_NAME, columnForName); - stopArea.stop_id = getStringIfPresent(result, StopArea.STOP_ID_COLUMN_NAME, columnForName); + stopArea.area_id = getStringIfPresent(result, StopArea.AREA_ID_NAME, columnForName); + stopArea.stop_id = getStringIfPresent(result, StopArea.STOP_ID_NAME, columnForName); return stopArea; }; EntityPopulator FARE_MEDIA = (result, columnForName) -> { FareMedia fareMedia = new FareMedia(); - fareMedia.fare_media_id = getStringIfPresent(result, FareMedia.FARE_MEDIA_ID_COLUMN_NAME, columnForName); - fareMedia.fare_media_name = getStringIfPresent(result, FareMedia.FARE_MEDIA_NAME_COLUMN_NAME, columnForName); - fareMedia.fare_media_type = getIntIfPresent(result, FareMedia.FARE_MEDIA_TYPE_COLUMN_NAME, columnForName); + fareMedia.fare_media_id = getStringIfPresent(result, FareMedia.FARE_MEDIA_ID_NAME, columnForName); + fareMedia.fare_media_name = getStringIfPresent(result, FareMedia.FARE_MEDIA_NAME_NAME, columnForName); + fareMedia.fare_media_type = getIntIfPresent(result, FareMedia.FARE_MEDIA_TYPE_NAME, columnForName); return fareMedia; }; EntityPopulator FARE_PRODUCT = (result, columnForName) -> { FareProduct fareProduct = new FareProduct(); - fareProduct.fare_product_id = getStringIfPresent(result, FareProduct.FARE_PRODUCT_ID_COLUMN_NAME, columnForName); - fareProduct.fare_product_name = getStringIfPresent(result, FareProduct.FARE_PRODUCT_NAME_COLUMN_NAME, columnForName); - fareProduct.fare_media_id = getStringIfPresent(result, FareProduct.FARE_MEDIA_ID_COLUMN_NAME, columnForName); - fareProduct.amount = getDoubleIfPresent(result, FareProduct.AMOUNT_COLUMN_NAME, columnForName); - fareProduct.currency = getStringIfPresent(result, FareProduct.CURRENCY_COLUMN_NAME, columnForName); + fareProduct.fare_product_id = getStringIfPresent(result, FareProduct.FARE_PRODUCT_ID_NAME, columnForName); + fareProduct.fare_product_name = getStringIfPresent(result, FareProduct.FARE_PRODUCT_NAME_NAME, columnForName); + fareProduct.fare_media_id = getStringIfPresent(result, FareProduct.FARE_MEDIA_ID_NAME, columnForName); + fareProduct.amount = getDoubleIfPresent(result, FareProduct.AMOUNT_NAME, columnForName); + fareProduct.currency = getStringIfPresent(result, FareProduct.CURRENCY_NAME, columnForName); return fareProduct; }; EntityPopulator TIME_FRAME = (result, columnForName) -> { TimeFrame timeFrame = new TimeFrame(); - timeFrame.timeframe_group_id = getStringIfPresent(result, TimeFrame.TIME_FRAME_GROUP_ID_COLUMN_NAME, columnForName); - timeFrame.start_time = getIntIfPresent(result, TimeFrame.START_TIME_COLUMN_NAME, columnForName); - timeFrame.end_time = getIntIfPresent(result, TimeFrame.END_TIME_COLUMN_NAME, columnForName); - timeFrame.service_id = getStringIfPresent(result, FareProduct.FARE_MEDIA_ID_COLUMN_NAME, columnForName); + timeFrame.timeframe_group_id = getStringIfPresent(result, TimeFrame.TIME_FRAME_GROUP_ID_NAME, columnForName); + timeFrame.start_time = getIntIfPresent(result, TimeFrame.START_TIME_NAME, columnForName); + timeFrame.end_time = getIntIfPresent(result, TimeFrame.END_TIME_NAME, columnForName); + timeFrame.service_id = getStringIfPresent(result, FareProduct.FARE_MEDIA_ID_NAME, columnForName); return timeFrame; }; EntityPopulator FARE_LEG_RULE = (result, columnForName) -> { FareLegRule fareLegRule = new FareLegRule(); - fareLegRule.leg_group_id = getStringIfPresent(result, FareLegRule.LEG_GROUP_ID_COLUMN_NAME, columnForName); - fareLegRule.from_timeframe_group_id = getStringIfPresent(result, FareLegRule.FROM_AREA_ID_COLUMN_NAME, columnForName); - fareLegRule.to_timeframe_group_id = getStringIfPresent(result, FareLegRule.TO_AREA_ID_COLUMN_NAME, columnForName); - fareLegRule.from_timeframe_group_id = getStringIfPresent(result, FareLegRule.FROM_TIMEFRAME_GROUP_ID_COLUMN_NAME, columnForName); - fareLegRule.to_timeframe_group_id = getStringIfPresent(result, FareLegRule.TO_TIMEFRAME_GROUP_ID_COLUMN_NAME, columnForName); - fareLegRule.fare_product_id = getStringIfPresent(result, FareLegRule.FARE_PRODUCT_ID_COLUMN_NAME, columnForName); - fareLegRule.rule_priority = getIntIfPresent(result, FareLegRule.RULE_PRIORITY_COLUMN_NAME, columnForName); + fareLegRule.leg_group_id = getStringIfPresent(result, FareLegRule.LEG_GROUP_ID_NAME, columnForName); + fareLegRule.from_timeframe_group_id = getStringIfPresent(result, FareLegRule.FROM_AREA_ID_NAME, columnForName); + fareLegRule.to_timeframe_group_id = getStringIfPresent(result, FareLegRule.TO_AREA_ID_NAME, columnForName); + fareLegRule.from_timeframe_group_id = getStringIfPresent(result, FareLegRule.FROM_TIMEFRAME_GROUP_ID_NAME, columnForName); + fareLegRule.to_timeframe_group_id = getStringIfPresent(result, FareLegRule.TO_TIMEFRAME_GROUP_ID_NAME, columnForName); + fareLegRule.fare_product_id = getStringIfPresent(result, FareLegRule.FARE_PRODUCT_ID_NAME, columnForName); + fareLegRule.rule_priority = getIntIfPresent(result, FareLegRule.RULE_PRIORITY_NAME, columnForName); return fareLegRule; }; EntityPopulator FARE_TRANSFER_RULE = (result, columnForName) -> { FareTransferRule fareTransferRule = new FareTransferRule(); - fareTransferRule.from_leg_group_id = getStringIfPresent(result, FareTransferRule.FROM_LEG_GROUP_ID_COLUMN_NAME, columnForName); - fareTransferRule.to_leg_group_id = getStringIfPresent(result, FareTransferRule.TO_LEG_GROUP_ID_COLUMN_NAME, columnForName); - fareTransferRule.transfer_count = getIntIfPresent(result, FareTransferRule.TRANSFER_COUNT_COLUMN_NAME, columnForName); - fareTransferRule.duration_limit = getIntIfPresent(result, FareTransferRule.DURATION_LIMIT_COLUMN_NAME, columnForName); - fareTransferRule.duration_limit_type = getIntIfPresent(result, FareTransferRule.DURATION_LIMIT_TYPE_COLUMN_NAME, columnForName); - fareTransferRule.fare_transfer_type = getIntIfPresent(result, FareTransferRule.FARE_TRANSFER_TYPE_COLUMN_NAME, columnForName); - fareTransferRule.fare_product_id = getStringIfPresent(result, FareTransferRule.FARE_PRODUCT_ID_COLUMN_NAME, columnForName); + fareTransferRule.from_leg_group_id = getStringIfPresent(result, FareTransferRule.FROM_LEG_GROUP_ID_NAME, columnForName); + fareTransferRule.to_leg_group_id = getStringIfPresent(result, FareTransferRule.TO_LEG_GROUP_ID_NAME, columnForName); + fareTransferRule.transfer_count = getIntIfPresent(result, FareTransferRule.TRANSFER_COUNT_NAME, columnForName); + fareTransferRule.duration_limit = getIntIfPresent(result, FareTransferRule.DURATION_LIMIT_NAME, columnForName); + fareTransferRule.duration_limit_type = getIntIfPresent(result, FareTransferRule.DURATION_LIMIT_TYPE_NAME, columnForName); + fareTransferRule.fare_transfer_type = getIntIfPresent(result, FareTransferRule.FARE_TRANSFER_TYPE_NAME, columnForName); + fareTransferRule.fare_product_id = getStringIfPresent(result, FareTransferRule.FARE_PRODUCT_ID_NAME, columnForName); return fareTransferRule; }; EntityPopulator NETWORK = (result, columnForName) -> { Network network = new Network(); - network.network_id = getStringIfPresent(result, Network.NETWORK_ID_COLUMN_NAME, columnForName); - network.network_name = getStringIfPresent(result, Network.NETWORK_NAME_COLUMN_NAME, columnForName); + network.network_id = getStringIfPresent(result, Network.NETWORK_ID_NAME, columnForName); + network.network_name = getStringIfPresent(result, Network.NETWORK_NAME_NAME, columnForName); return network; }; EntityPopulator ROUTE_NETWORK = (result, columnForName) -> { RouteNetwork routeNetwork = new RouteNetwork(); - routeNetwork.network_id = getStringIfPresent(result, RouteNetwork.NETWORK_ID_COLUMN_NAME, columnForName); - routeNetwork.route_id = getStringIfPresent(result, RouteNetwork.ROUTE_ID_COLUMN_NAME, columnForName); + routeNetwork.network_id = getStringIfPresent(result, RouteNetwork.NETWORK_ID_NAME, columnForName); + routeNetwork.route_id = getStringIfPresent(result, RouteNetwork.ROUTE_ID_NAME, columnForName); return routeNetwork; }; diff --git a/src/main/java/com/conveyal/gtfs/loader/Table.java b/src/main/java/com/conveyal/gtfs/loader/Table.java index 2f778c15e..1c4954c9b 100644 --- a/src/main/java/com/conveyal/gtfs/loader/Table.java +++ b/src/main/java/com/conveyal/gtfs/loader/Table.java @@ -300,106 +300,106 @@ public Table (String name, Class entityClass, Requirement requ .addPrimaryKeyNames("stop_id"); public static final Table AREAS = new Table(Area.TABLE_NAME, Area.class, OPTIONAL, - new StringField(Area.AREA_ID_COLUMN_NAME, REQUIRED), - new StringField(Area.AREA_NAME_COLUMN_NAME, OPTIONAL) + new StringField(Area.AREA_ID_NAME, REQUIRED), + new StringField(Area.AREA_NAME_NAME, OPTIONAL) ) .restrictDelete() - .addPrimaryKeyNames(Area.AREA_ID_COLUMN_NAME); + .addPrimaryKeyNames(Area.AREA_ID_NAME); public static final Table STOP_AREAS = new Table(StopArea.TABLE_NAME, StopArea.class, OPTIONAL, - new StringField(StopArea.AREA_ID_COLUMN_NAME, REQUIRED).isReferenceTo(AREAS), - new StringField(StopArea.STOP_ID_COLUMN_NAME, REQUIRED).isReferenceTo(STOPS) + new StringField(StopArea.AREA_ID_NAME, REQUIRED).isReferenceTo(AREAS), + new StringField(StopArea.STOP_ID_NAME, REQUIRED).isReferenceTo(STOPS) ) .keyFieldIsNotUnique() - .addPrimaryKeyNames(StopArea.AREA_ID_COLUMN_NAME, StopArea.STOP_ID_COLUMN_NAME); + .addPrimaryKeyNames(StopArea.AREA_ID_NAME, StopArea.STOP_ID_NAME); public static final Table FARE_MEDIAS = new Table(FareMedia.TABLE_NAME, FareMedia.class, OPTIONAL, - new StringField(FareMedia.FARE_MEDIA_ID_COLUMN_NAME, REQUIRED), - new StringField(FareMedia.FARE_MEDIA_NAME_COLUMN_NAME, OPTIONAL), - new IntegerField(FareMedia.FARE_MEDIA_TYPE_COLUMN_NAME, REQUIRED) + new StringField(FareMedia.FARE_MEDIA_ID_NAME, REQUIRED), + new StringField(FareMedia.FARE_MEDIA_NAME_NAME, OPTIONAL), + new IntegerField(FareMedia.FARE_MEDIA_TYPE_NAME, REQUIRED) ) .restrictDelete() - .addPrimaryKeyNames(FareMedia.FARE_MEDIA_ID_COLUMN_NAME); + .addPrimaryKeyNames(FareMedia.FARE_MEDIA_ID_NAME); public static final Table FARE_PRODUCTS = new Table(FareProduct.TABLE_NAME, FareProduct.class, OPTIONAL, - new StringField(FareProduct.FARE_PRODUCT_ID_COLUMN_NAME, REQUIRED), - new StringField(FareProduct.FARE_PRODUCT_NAME_COLUMN_NAME, OPTIONAL), - new StringField(FareProduct.FARE_MEDIA_ID_COLUMN_NAME, OPTIONAL).isReferenceTo(FARE_MEDIAS), - new DoubleField(FareProduct.AMOUNT_COLUMN_NAME, REQUIRED, 0.0, Double.MAX_VALUE, 2), - new StringField(FareProduct.CURRENCY_COLUMN_NAME, REQUIRED) + new StringField(FareProduct.FARE_PRODUCT_ID_NAME, REQUIRED), + new StringField(FareProduct.FARE_PRODUCT_NAME_NAME, OPTIONAL), + new StringField(FareProduct.FARE_MEDIA_ID_NAME, OPTIONAL).isReferenceTo(FARE_MEDIAS), + new DoubleField(FareProduct.AMOUNT_NAME, REQUIRED, 0.0, Double.MAX_VALUE, 2), + new StringField(FareProduct.CURRENCY_NAME, REQUIRED) ) .restrictDelete() .keyFieldIsNotUnique() - .addPrimaryKeyNames(FareProduct.FARE_PRODUCT_ID_COLUMN_NAME, FareProduct.FARE_MEDIA_ID_COLUMN_NAME); + .addPrimaryKeyNames(FareProduct.FARE_PRODUCT_ID_NAME, FareProduct.FARE_MEDIA_ID_NAME); public static final Table TIME_FRAMES = new Table(TimeFrame.TABLE_NAME, TimeFrame.class, OPTIONAL, - new StringField(TimeFrame.TIME_FRAME_GROUP_ID_COLUMN_NAME, REQUIRED), - new TimeField(TimeFrame.START_TIME_COLUMN_NAME, OPTIONAL), - new TimeField(TimeFrame.END_TIME_COLUMN_NAME, OPTIONAL), - new StringField(TimeFrame.SERVICE_ID_COLUMN_NAME, OPTIONAL).isReferenceTo(CALENDAR).isReferenceTo(CALENDAR_DATES) + new StringField(TimeFrame.TIME_FRAME_GROUP_ID_NAME, REQUIRED), + new TimeField(TimeFrame.START_TIME_NAME, OPTIONAL), + new TimeField(TimeFrame.END_TIME_NAME, OPTIONAL), + new StringField(TimeFrame.SERVICE_ID_NAME, OPTIONAL).isReferenceTo(CALENDAR).isReferenceTo(CALENDAR_DATES) ) .restrictDelete() .keyFieldIsNotUnique() .addPrimaryKeyNames( - TimeFrame.TIME_FRAME_GROUP_ID_COLUMN_NAME, - TimeFrame.START_TIME_COLUMN_NAME, - TimeFrame.END_TIME_COLUMN_NAME, - TimeFrame.SERVICE_ID_COLUMN_NAME + TimeFrame.TIME_FRAME_GROUP_ID_NAME, + TimeFrame.START_TIME_NAME, + TimeFrame.END_TIME_NAME, + TimeFrame.SERVICE_ID_NAME ); public static final Table FARE_LEG_RULES = new Table(FareLegRule.TABLE_NAME, FareLegRule.class, OPTIONAL, - new StringField(FareLegRule.LEG_GROUP_ID_COLUMN_NAME, OPTIONAL), - new StringField(FareLegRule.NETWORK_ID_COLUMN_NAME, OPTIONAL), - new StringField(FareLegRule.FROM_AREA_ID_COLUMN_NAME, OPTIONAL).isReferenceTo(AREAS), - new StringField(FareLegRule.TO_AREA_ID_COLUMN_NAME, OPTIONAL).isReferenceTo(AREAS), - new StringField(FareLegRule.FROM_TIMEFRAME_GROUP_ID_COLUMN_NAME, OPTIONAL).isReferenceTo(TIME_FRAMES), - new StringField(FareLegRule.TO_TIMEFRAME_GROUP_ID_COLUMN_NAME, OPTIONAL).isReferenceTo(TIME_FRAMES), - new StringField(FareLegRule.FARE_PRODUCT_ID_COLUMN_NAME, REQUIRED).isReferenceTo(FARE_PRODUCTS), - new IntegerField(FareLegRule.RULE_PRIORITY_COLUMN_NAME, OPTIONAL) + new StringField(FareLegRule.LEG_GROUP_ID_NAME, OPTIONAL), + new StringField(FareLegRule.NETWORK_ID_NAME, OPTIONAL), + new StringField(FareLegRule.FROM_AREA_ID_NAME, OPTIONAL).isReferenceTo(AREAS), + new StringField(FareLegRule.TO_AREA_ID_NAME, OPTIONAL).isReferenceTo(AREAS), + new StringField(FareLegRule.FROM_TIMEFRAME_GROUP_ID_NAME, OPTIONAL).isReferenceTo(TIME_FRAMES), + new StringField(FareLegRule.TO_TIMEFRAME_GROUP_ID_NAME, OPTIONAL).isReferenceTo(TIME_FRAMES), + new StringField(FareLegRule.FARE_PRODUCT_ID_NAME, REQUIRED).isReferenceTo(FARE_PRODUCTS), + new IntegerField(FareLegRule.RULE_PRIORITY_NAME, OPTIONAL) ) .restrictDelete() .keyFieldIsNotUnique() .addPrimaryKeyNames( - FareLegRule.NETWORK_ID_COLUMN_NAME, - FareLegRule.FROM_AREA_ID_COLUMN_NAME, - FareLegRule.TO_AREA_ID_COLUMN_NAME, - FareLegRule.FROM_TIMEFRAME_GROUP_ID_COLUMN_NAME, - FareLegRule.TO_TIMEFRAME_GROUP_ID_COLUMN_NAME, - FareLegRule.FARE_PRODUCT_ID_COLUMN_NAME + FareLegRule.NETWORK_ID_NAME, + FareLegRule.FROM_AREA_ID_NAME, + FareLegRule.TO_AREA_ID_NAME, + FareLegRule.FROM_TIMEFRAME_GROUP_ID_NAME, + FareLegRule.TO_TIMEFRAME_GROUP_ID_NAME, + FareLegRule.FARE_PRODUCT_ID_NAME ); public static final Table FARE_TRANSFER_RULES = new Table(FareTransferRule.TABLE_NAME, FareTransferRule.class, OPTIONAL, - new StringField(FareTransferRule.FROM_LEG_GROUP_ID_COLUMN_NAME, OPTIONAL).isReferenceTo(FARE_LEG_RULES), - new StringField(FareTransferRule.TO_LEG_GROUP_ID_COLUMN_NAME, OPTIONAL).isReferenceTo(FARE_LEG_RULES), - new IntegerField(FareTransferRule.TRANSFER_COUNT_COLUMN_NAME, OPTIONAL, -1, Integer.MAX_VALUE), - new IntegerField(FareTransferRule.DURATION_LIMIT_COLUMN_NAME, OPTIONAL), - new IntegerField(FareTransferRule.DURATION_LIMIT_TYPE_COLUMN_NAME, OPTIONAL), - new IntegerField(FareTransferRule.FARE_TRANSFER_TYPE_COLUMN_NAME, REQUIRED), - new StringField(FareTransferRule.FARE_PRODUCT_ID_COLUMN_NAME, OPTIONAL).isReferenceTo(FARE_PRODUCTS) + new StringField(FareTransferRule.FROM_LEG_GROUP_ID_NAME, OPTIONAL).isReferenceTo(FARE_LEG_RULES), + new StringField(FareTransferRule.TO_LEG_GROUP_ID_NAME, OPTIONAL).isReferenceTo(FARE_LEG_RULES), + new IntegerField(FareTransferRule.TRANSFER_COUNT_NAME, OPTIONAL, -1, Integer.MAX_VALUE), + new IntegerField(FareTransferRule.DURATION_LIMIT_NAME, OPTIONAL), + new IntegerField(FareTransferRule.DURATION_LIMIT_TYPE_NAME, OPTIONAL), + new IntegerField(FareTransferRule.FARE_TRANSFER_TYPE_NAME, REQUIRED), + new StringField(FareTransferRule.FARE_PRODUCT_ID_NAME, OPTIONAL).isReferenceTo(FARE_PRODUCTS) ) .restrictDelete() .keyFieldIsNotUnique() .addPrimaryKeyNames( - FareTransferRule.FROM_LEG_GROUP_ID_COLUMN_NAME, - FareTransferRule.TO_LEG_GROUP_ID_COLUMN_NAME, - FareTransferRule.FARE_PRODUCT_ID_COLUMN_NAME, - FareTransferRule.TRANSFER_COUNT_COLUMN_NAME, - FareTransferRule.DURATION_LIMIT_COLUMN_NAME + FareTransferRule.FROM_LEG_GROUP_ID_NAME, + FareTransferRule.TO_LEG_GROUP_ID_NAME, + FareTransferRule.FARE_PRODUCT_ID_NAME, + FareTransferRule.TRANSFER_COUNT_NAME, + FareTransferRule.DURATION_LIMIT_NAME ); public static final Table NETWORKS = new Table(Network.TABLE_NAME, Network.class, OPTIONAL, - new StringField(Network.NETWORK_ID_COLUMN_NAME, REQUIRED), - new StringField(Network.NETWORK_NAME_COLUMN_NAME, OPTIONAL) + new StringField(Network.NETWORK_ID_NAME, REQUIRED), + new StringField(Network.NETWORK_NAME_NAME, OPTIONAL) ) .restrictDelete() - .addPrimaryKeyNames(Network.NETWORK_ID_COLUMN_NAME); + .addPrimaryKeyNames(Network.NETWORK_ID_NAME); public static final Table ROUTE_NETWORKS = new Table(RouteNetwork.TABLE_NAME, RouteNetwork.class, OPTIONAL, - new StringField(RouteNetwork.NETWORK_ID_COLUMN_NAME, REQUIRED).isReferenceTo(NETWORKS), - new StringField(RouteNetwork.ROUTE_ID_COLUMN_NAME, REQUIRED).isReferenceTo(ROUTES) + new StringField(RouteNetwork.NETWORK_ID_NAME, REQUIRED).isReferenceTo(NETWORKS), + new StringField(RouteNetwork.ROUTE_ID_NAME, REQUIRED).isReferenceTo(ROUTES) ) .keyFieldIsNotUnique() - .addPrimaryKeyNames(RouteNetwork.ROUTE_ID_COLUMN_NAME); + .addPrimaryKeyNames(RouteNetwork.ROUTE_ID_NAME); // GTFS reference: https://developers.google.com/transit/gtfs/reference#fare_rulestxt diff --git a/src/main/java/com/conveyal/gtfs/model/Area.java b/src/main/java/com/conveyal/gtfs/model/Area.java index 983864eb1..c8e1f7467 100644 --- a/src/main/java/com/conveyal/gtfs/model/Area.java +++ b/src/main/java/com/conveyal/gtfs/model/Area.java @@ -15,8 +15,8 @@ public class Area extends Entity { public String feed_id; public static final String TABLE_NAME = "areas"; - public static final String AREA_ID_COLUMN_NAME = "area_id"; - public static final String AREA_NAME_COLUMN_NAME = "area_name"; + public static final String AREA_ID_NAME = "area_id"; + public static final String AREA_NAME_NAME = "area_name"; @Override @@ -51,8 +51,8 @@ protected boolean isRequired() { public void loadOneRow() throws IOException { Area area = new Area(); area.id = row + 1; // offset line number by 1 to account for 0-based row index - area.area_id = getStringField(AREA_ID_COLUMN_NAME, true); - area.area_name = getStringField(AREA_NAME_COLUMN_NAME, false); + area.area_id = getStringField(AREA_ID_NAME, true); + area.area_name = getStringField(AREA_NAME_NAME, false); area.feed = feed; area.feed_id = feed.feedId; feed.areas.put(area.getId(), area); @@ -67,7 +67,7 @@ public Writer(GTFSFeed feed) { @Override public void writeHeaders() throws IOException { - writer.writeRecord(new String[] {AREA_ID_COLUMN_NAME, AREA_NAME_COLUMN_NAME}); + writer.writeRecord(new String[] {AREA_ID_NAME, AREA_NAME_NAME}); } @Override diff --git a/src/main/java/com/conveyal/gtfs/model/FareLegRule.java b/src/main/java/com/conveyal/gtfs/model/FareLegRule.java index 4b3d75ae3..d1c417d20 100644 --- a/src/main/java/com/conveyal/gtfs/model/FareLegRule.java +++ b/src/main/java/com/conveyal/gtfs/model/FareLegRule.java @@ -23,14 +23,14 @@ public class FareLegRule extends Entity { public String feed_id; public static final String TABLE_NAME = "fare_leg_rules"; - public static final String LEG_GROUP_ID_COLUMN_NAME = "leg_group_id"; - public static final String NETWORK_ID_COLUMN_NAME = "network_id"; - public static final String FROM_AREA_ID_COLUMN_NAME = "from_area_id"; - public static final String TO_AREA_ID_COLUMN_NAME = "to_area_id"; - public static final String FROM_TIMEFRAME_GROUP_ID_COLUMN_NAME = "from_timeframe_group_id"; - public static final String TO_TIMEFRAME_GROUP_ID_COLUMN_NAME = "to_timeframe_group_id"; - public static final String FARE_PRODUCT_ID_COLUMN_NAME = "fare_product_id"; - public static final String RULE_PRIORITY_COLUMN_NAME = "rule_priority"; + public static final String LEG_GROUP_ID_NAME = "leg_group_id"; + public static final String NETWORK_ID_NAME = "network_id"; + public static final String FROM_AREA_ID_NAME = "from_area_id"; + public static final String TO_AREA_ID_NAME = "to_area_id"; + public static final String FROM_TIMEFRAME_GROUP_ID_NAME = "from_timeframe_group_id"; + public static final String TO_TIMEFRAME_GROUP_ID_NAME = "to_timeframe_group_id"; + public static final String FARE_PRODUCT_ID_NAME = "fare_product_id"; + public static final String RULE_PRIORITY_NAME = "rule_priority"; @Override public String getId () { @@ -77,14 +77,14 @@ protected boolean isRequired() { public void loadOneRow() throws IOException { FareLegRule fareLegRule = new FareLegRule(); fareLegRule.id = row + 1; // offset line number by 1 to account for 0-based row index - fareLegRule.leg_group_id = getStringField(LEG_GROUP_ID_COLUMN_NAME, false); - fareLegRule.network_id = getStringField(NETWORK_ID_COLUMN_NAME, false); - fareLegRule.from_area_id = getStringField(FROM_AREA_ID_COLUMN_NAME, false); - fareLegRule.to_area_id = getStringField(TO_AREA_ID_COLUMN_NAME, false); - fareLegRule.from_timeframe_group_id = getStringField(FROM_TIMEFRAME_GROUP_ID_COLUMN_NAME, false); - fareLegRule.to_timeframe_group_id = getStringField(TO_TIMEFRAME_GROUP_ID_COLUMN_NAME, false); - fareLegRule.fare_product_id = getStringField(FARE_PRODUCT_ID_COLUMN_NAME, true); - fareLegRule.rule_priority = getIntField(RULE_PRIORITY_COLUMN_NAME, false, 0, Integer.MAX_VALUE); + fareLegRule.leg_group_id = getStringField(LEG_GROUP_ID_NAME, false); + fareLegRule.network_id = getStringField(NETWORK_ID_NAME, false); + fareLegRule.from_area_id = getStringField(FROM_AREA_ID_NAME, false); + fareLegRule.to_area_id = getStringField(TO_AREA_ID_NAME, false); + fareLegRule.from_timeframe_group_id = getStringField(FROM_TIMEFRAME_GROUP_ID_NAME, false); + fareLegRule.to_timeframe_group_id = getStringField(TO_TIMEFRAME_GROUP_ID_NAME, false); + fareLegRule.fare_product_id = getStringField(FARE_PRODUCT_ID_NAME, true); + fareLegRule.rule_priority = getIntField(RULE_PRIORITY_NAME, false, 0, Integer.MAX_VALUE); if (fareLegRule.rule_priority == INT_MISSING) { // An empty value for rule_priority is treated as zero. fareLegRule.rule_priority = 0; @@ -104,14 +104,14 @@ public Writer(GTFSFeed feed) { @Override public void writeHeaders() throws IOException { writer.writeRecord(new String[] { - LEG_GROUP_ID_COLUMN_NAME, - NETWORK_ID_COLUMN_NAME, - FROM_AREA_ID_COLUMN_NAME, - TO_AREA_ID_COLUMN_NAME, - FROM_TIMEFRAME_GROUP_ID_COLUMN_NAME, - TO_TIMEFRAME_GROUP_ID_COLUMN_NAME, - FARE_PRODUCT_ID_COLUMN_NAME, - RULE_PRIORITY_COLUMN_NAME + LEG_GROUP_ID_NAME, + NETWORK_ID_NAME, + FROM_AREA_ID_NAME, + TO_AREA_ID_NAME, + FROM_TIMEFRAME_GROUP_ID_NAME, + TO_TIMEFRAME_GROUP_ID_NAME, + FARE_PRODUCT_ID_NAME, + RULE_PRIORITY_NAME }); } diff --git a/src/main/java/com/conveyal/gtfs/model/FareMedia.java b/src/main/java/com/conveyal/gtfs/model/FareMedia.java index d484a4ce3..a7927c014 100644 --- a/src/main/java/com/conveyal/gtfs/model/FareMedia.java +++ b/src/main/java/com/conveyal/gtfs/model/FareMedia.java @@ -17,9 +17,9 @@ public class FareMedia extends Entity { public String feed_id; public static final String TABLE_NAME = "fare_media"; - public static final String FARE_MEDIA_ID_COLUMN_NAME = "fare_media_id"; - public static final String FARE_MEDIA_NAME_COLUMN_NAME = "fare_media_name"; - public static final String FARE_MEDIA_TYPE_COLUMN_NAME = "fare_media_type"; + public static final String FARE_MEDIA_ID_NAME = "fare_media_id"; + public static final String FARE_MEDIA_NAME_NAME = "fare_media_name"; + public static final String FARE_MEDIA_TYPE_NAME = "fare_media_type"; @Override public String getId () { @@ -54,9 +54,9 @@ protected boolean isRequired() { public void loadOneRow() throws IOException { FareMedia fareMedia = new FareMedia(); fareMedia.id = row + 1; // offset line number by 1 to account for 0-based row index - fareMedia.fare_media_id = getStringField(FARE_MEDIA_ID_COLUMN_NAME, true); - fareMedia.fare_media_name = getStringField(FARE_MEDIA_NAME_COLUMN_NAME, false); - fareMedia.fare_media_type = getIntField(FARE_MEDIA_TYPE_COLUMN_NAME, true, 0, 4); + fareMedia.fare_media_id = getStringField(FARE_MEDIA_ID_NAME, true); + fareMedia.fare_media_name = getStringField(FARE_MEDIA_NAME_NAME, false); + fareMedia.fare_media_type = getIntField(FARE_MEDIA_TYPE_NAME, true, 0, 4); fareMedia.feed = feed; fareMedia.feed_id = feed.feedId; feed.fare_medias.put(fareMedia.getId(), fareMedia); @@ -72,9 +72,9 @@ public Writer(GTFSFeed feed) { @Override public void writeHeaders() throws IOException { writer.writeRecord(new String[] { - FARE_MEDIA_ID_COLUMN_NAME, - FARE_MEDIA_NAME_COLUMN_NAME, - FARE_MEDIA_TYPE_COLUMN_NAME + FARE_MEDIA_ID_NAME, + FARE_MEDIA_NAME_NAME, + FARE_MEDIA_TYPE_NAME }); } diff --git a/src/main/java/com/conveyal/gtfs/model/FareProduct.java b/src/main/java/com/conveyal/gtfs/model/FareProduct.java index e7f3599d5..68600b218 100644 --- a/src/main/java/com/conveyal/gtfs/model/FareProduct.java +++ b/src/main/java/com/conveyal/gtfs/model/FareProduct.java @@ -5,9 +5,7 @@ import java.io.IOException; import java.sql.PreparedStatement; import java.sql.SQLException; -import java.util.Arrays; import java.util.Iterator; -import java.util.stream.Collectors; public class FareProduct extends Entity { @@ -21,11 +19,11 @@ public class FareProduct extends Entity { public String feed_id; public static final String TABLE_NAME = "fare_products"; - public static final String FARE_PRODUCT_ID_COLUMN_NAME = "fare_product_id"; - public static final String FARE_PRODUCT_NAME_COLUMN_NAME = "fare_product_name"; - public static final String FARE_MEDIA_ID_COLUMN_NAME = "fare_media_id"; - public static final String AMOUNT_COLUMN_NAME = "amount"; - public static final String CURRENCY_COLUMN_NAME = "currency"; + public static final String FARE_PRODUCT_ID_NAME = "fare_product_id"; + public static final String FARE_PRODUCT_NAME_NAME = "fare_product_name"; + public static final String FARE_MEDIA_ID_NAME = "fare_media_id"; + public static final String AMOUNT_NAME = "amount"; + public static final String CURRENCY_NAME = "currency"; @Override @@ -63,11 +61,11 @@ protected boolean isRequired() { public void loadOneRow() throws IOException { FareProduct fareProduct = new FareProduct(); fareProduct.id = row + 1; // offset line number by 1 to account for 0-based row index - fareProduct.fare_product_id = getStringField(FARE_PRODUCT_ID_COLUMN_NAME, true); - fareProduct.fare_product_name = getStringField(FARE_PRODUCT_NAME_COLUMN_NAME, false); - fareProduct.fare_media_id = getStringField(FARE_MEDIA_ID_COLUMN_NAME, false); - fareProduct.amount = getDoubleField(AMOUNT_COLUMN_NAME, true, 0.0, Double.MAX_VALUE); - fareProduct.currency = getStringField(CURRENCY_COLUMN_NAME, true); + fareProduct.fare_product_id = getStringField(FARE_PRODUCT_ID_NAME, true); + fareProduct.fare_product_name = getStringField(FARE_PRODUCT_NAME_NAME, false); + fareProduct.fare_media_id = getStringField(FARE_MEDIA_ID_NAME, false); + fareProduct.amount = getDoubleField(AMOUNT_NAME, true, 0.0, Double.MAX_VALUE); + fareProduct.currency = getStringField(CURRENCY_NAME, true); fareProduct.feed = feed; fareProduct.feed_id = feed.feedId; feed.fare_products.put(fareProduct.getId(), fareProduct); @@ -75,7 +73,7 @@ public void loadOneRow() throws IOException { /* Check referential integrity without storing references. */ - getRefField(FARE_MEDIA_ID_COLUMN_NAME, false, feed.fare_medias); + getRefField(FARE_MEDIA_ID_NAME, false, feed.fare_medias); } } @@ -88,10 +86,10 @@ public Writer(GTFSFeed feed) { @Override public void writeHeaders() throws IOException { writer.writeRecord(new String[] { - FARE_PRODUCT_ID_COLUMN_NAME, - FARE_PRODUCT_NAME_COLUMN_NAME, - FARE_MEDIA_ID_COLUMN_NAME, - AMOUNT_COLUMN_NAME,CURRENCY_COLUMN_NAME + FARE_PRODUCT_ID_NAME, + FARE_PRODUCT_NAME_NAME, + FARE_MEDIA_ID_NAME, + AMOUNT_NAME, CURRENCY_NAME }); } diff --git a/src/main/java/com/conveyal/gtfs/model/FareTransferRule.java b/src/main/java/com/conveyal/gtfs/model/FareTransferRule.java index bbffb21e8..d22b43fcd 100644 --- a/src/main/java/com/conveyal/gtfs/model/FareTransferRule.java +++ b/src/main/java/com/conveyal/gtfs/model/FareTransferRule.java @@ -22,13 +22,13 @@ public class FareTransferRule extends Entity { public String feed_id; public static final String TABLE_NAME = "fare_transfer_rules"; - public static final String FROM_LEG_GROUP_ID_COLUMN_NAME = "from_leg_group_id"; - public static final String TO_LEG_GROUP_ID_COLUMN_NAME = "to_leg_group_id"; - public static final String TRANSFER_COUNT_COLUMN_NAME = "transfer_count"; - public static final String DURATION_LIMIT_COLUMN_NAME = "duration_limit"; - public static final String DURATION_LIMIT_TYPE_COLUMN_NAME = "duration_limit_type"; - public static final String FARE_TRANSFER_TYPE_COLUMN_NAME = "fare_transfer_type"; - public static final String FARE_PRODUCT_ID_COLUMN_NAME = "fare_product_id"; + public static final String FROM_LEG_GROUP_ID_NAME = "from_leg_group_id"; + public static final String TO_LEG_GROUP_ID_NAME = "to_leg_group_id"; + public static final String TRANSFER_COUNT_NAME = "transfer_count"; + public static final String DURATION_LIMIT_NAME = "duration_limit"; + public static final String DURATION_LIMIT_TYPE_NAME = "duration_limit_type"; + public static final String FARE_TRANSFER_TYPE_NAME = "fare_transfer_type"; + public static final String FARE_PRODUCT_ID_NAME = "fare_product_id"; @Override public String getId () { @@ -73,13 +73,13 @@ protected boolean isRequired() { public void loadOneRow() throws IOException { FareTransferRule fareTransferRule = new FareTransferRule(); fareTransferRule.id = row + 1; // offset line number by 1 to account for 0-based row index - fareTransferRule.from_leg_group_id = getStringField(FROM_LEG_GROUP_ID_COLUMN_NAME, false); - fareTransferRule.to_leg_group_id = getStringField(TO_LEG_GROUP_ID_COLUMN_NAME, false); - fareTransferRule.transfer_count = getIntField(TRANSFER_COUNT_COLUMN_NAME, false, -1, Integer.MAX_VALUE, INT_MISSING); - fareTransferRule.duration_limit = getIntField(DURATION_LIMIT_COLUMN_NAME, false, 0, Integer.MAX_VALUE); - fareTransferRule.duration_limit_type = getIntField(DURATION_LIMIT_TYPE_COLUMN_NAME, false, 0, 3); - fareTransferRule.fare_transfer_type = getIntField(FARE_TRANSFER_TYPE_COLUMN_NAME, true, 0, 2); - fareTransferRule.fare_product_id = getStringField(FARE_PRODUCT_ID_COLUMN_NAME, false); + fareTransferRule.from_leg_group_id = getStringField(FROM_LEG_GROUP_ID_NAME, false); + fareTransferRule.to_leg_group_id = getStringField(TO_LEG_GROUP_ID_NAME, false); + fareTransferRule.transfer_count = getIntField(TRANSFER_COUNT_NAME, false, -1, Integer.MAX_VALUE, INT_MISSING); + fareTransferRule.duration_limit = getIntField(DURATION_LIMIT_NAME, false, 0, Integer.MAX_VALUE); + fareTransferRule.duration_limit_type = getIntField(DURATION_LIMIT_TYPE_NAME, false, 0, 3); + fareTransferRule.fare_transfer_type = getIntField(FARE_TRANSFER_TYPE_NAME, true, 0, 2); + fareTransferRule.fare_product_id = getStringField(FARE_PRODUCT_ID_NAME, false); fareTransferRule.feed = feed; fareTransferRule.feed_id = feed.feedId; feed.fare_transfer_rules.put(fareTransferRule.getId(), fareTransferRule); @@ -95,13 +95,13 @@ public Writer(GTFSFeed feed) { @Override public void writeHeaders() throws IOException { writer.writeRecord(new String[] { - FROM_LEG_GROUP_ID_COLUMN_NAME, - TO_LEG_GROUP_ID_COLUMN_NAME, - TRANSFER_COUNT_COLUMN_NAME, - DURATION_LIMIT_COLUMN_NAME, - DURATION_LIMIT_TYPE_COLUMN_NAME, - FARE_TRANSFER_TYPE_COLUMN_NAME, - FARE_PRODUCT_ID_COLUMN_NAME, + FROM_LEG_GROUP_ID_NAME, + TO_LEG_GROUP_ID_NAME, + TRANSFER_COUNT_NAME, + DURATION_LIMIT_NAME, + DURATION_LIMIT_TYPE_NAME, + FARE_TRANSFER_TYPE_NAME, + FARE_PRODUCT_ID_NAME, }); } diff --git a/src/main/java/com/conveyal/gtfs/model/Network.java b/src/main/java/com/conveyal/gtfs/model/Network.java index 004a0681a..4d509d5df 100644 --- a/src/main/java/com/conveyal/gtfs/model/Network.java +++ b/src/main/java/com/conveyal/gtfs/model/Network.java @@ -15,8 +15,8 @@ public class Network extends Entity { public String feed_id; public static final String TABLE_NAME = "networks"; - public static final String NETWORK_ID_COLUMN_NAME = "network_id"; - public static final String NETWORK_NAME_COLUMN_NAME = "network_name"; + public static final String NETWORK_ID_NAME = "network_id"; + public static final String NETWORK_NAME_NAME = "network_name"; @Override public String getId () { @@ -50,8 +50,8 @@ protected boolean isRequired() { public void loadOneRow() throws IOException { Network network = new Network(); network.id = row + 1; // offset line number by 1 to account for 0-based row index - network.network_id = getStringField(NETWORK_ID_COLUMN_NAME, true); - network.network_name = getStringField(NETWORK_NAME_COLUMN_NAME, false); + network.network_id = getStringField(NETWORK_ID_NAME, true); + network.network_name = getStringField(NETWORK_NAME_NAME, false); network.feed = feed; network.feed_id = feed.feedId; feed.networks.put(network.getId(), network); @@ -66,7 +66,7 @@ public Writer(GTFSFeed feed) { @Override public void writeHeaders() throws IOException { - writer.writeRecord(new String[] {NETWORK_ID_COLUMN_NAME, NETWORK_NAME_COLUMN_NAME}); + writer.writeRecord(new String[] {NETWORK_ID_NAME, NETWORK_NAME_NAME}); } @Override diff --git a/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java b/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java index c39cd1af5..4cdf17025 100644 --- a/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java +++ b/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java @@ -15,8 +15,8 @@ public class RouteNetwork extends Entity { public String feed_id; public static final String TABLE_NAME = "route_networks"; - public static final String NETWORK_ID_COLUMN_NAME = "network_id"; - public static final String ROUTE_ID_COLUMN_NAME = "route_id"; + public static final String NETWORK_ID_NAME = "network_id"; + public static final String ROUTE_ID_NAME = "route_id"; @Override public String getId () { @@ -50,13 +50,13 @@ protected boolean isRequired() { public void loadOneRow() throws IOException { RouteNetwork routeNetwork = new RouteNetwork(); routeNetwork.id = row + 1; // offset line number by 1 to account for 0-based row index - routeNetwork.network_id = getStringField(NETWORK_ID_COLUMN_NAME, true); - routeNetwork.route_id = getStringField(ROUTE_ID_COLUMN_NAME, true); + routeNetwork.network_id = getStringField(NETWORK_ID_NAME, true); + routeNetwork.route_id = getStringField(ROUTE_ID_NAME, true); routeNetwork.feed = feed; routeNetwork.feed_id = feed.feedId; feed.route_networks.put(routeNetwork.getId(), routeNetwork); - getRefField(NETWORK_ID_COLUMN_NAME, true, feed.networks); - getRefField(ROUTE_ID_COLUMN_NAME, true, feed.routes); + getRefField(NETWORK_ID_NAME, true, feed.networks); + getRefField(ROUTE_ID_NAME, true, feed.routes); } } @@ -68,7 +68,7 @@ public Writer(GTFSFeed feed) { @Override public void writeHeaders() throws IOException { - writer.writeRecord(new String[] {NETWORK_ID_COLUMN_NAME, ROUTE_ID_COLUMN_NAME}); + writer.writeRecord(new String[] {NETWORK_ID_NAME, ROUTE_ID_NAME}); } @Override diff --git a/src/main/java/com/conveyal/gtfs/model/StopArea.java b/src/main/java/com/conveyal/gtfs/model/StopArea.java index 8eae495e8..04f0df456 100644 --- a/src/main/java/com/conveyal/gtfs/model/StopArea.java +++ b/src/main/java/com/conveyal/gtfs/model/StopArea.java @@ -15,8 +15,8 @@ public class StopArea extends Entity { public String feed_id; public static final String TABLE_NAME = "stop_areas"; - public static final String AREA_ID_COLUMN_NAME = "area_id"; - public static final String STOP_ID_COLUMN_NAME = "stop_id"; + public static final String AREA_ID_NAME = "area_id"; + public static final String STOP_ID_NAME = "stop_id"; @Override public String getId () { @@ -50,8 +50,8 @@ protected boolean isRequired() { public void loadOneRow() throws IOException { StopArea stopArea = new StopArea(); stopArea.id = row + 1; // offset line number by 1 to account for 0-based row index - stopArea.area_id = getStringField(AREA_ID_COLUMN_NAME, true); - stopArea.stop_id = getStringField(STOP_ID_COLUMN_NAME, true); + stopArea.area_id = getStringField(AREA_ID_NAME, true); + stopArea.stop_id = getStringField(STOP_ID_NAME, true); stopArea.feed = feed; stopArea.feed_id = feed.feedId; feed.stop_areas.put(stopArea.getId(), stopArea); @@ -59,8 +59,8 @@ public void loadOneRow() throws IOException { /* Check referential integrity without storing references. */ - getRefField(AREA_ID_COLUMN_NAME, true, feed.areas); - getRefField(STOP_ID_COLUMN_NAME, true, feed.stops); + getRefField(AREA_ID_NAME, true, feed.areas); + getRefField(STOP_ID_NAME, true, feed.stops); } @@ -73,7 +73,7 @@ public Writer(GTFSFeed feed) { @Override public void writeHeaders() throws IOException { - writer.writeRecord(new String[] {AREA_ID_COLUMN_NAME, STOP_ID_COLUMN_NAME}); + writer.writeRecord(new String[] {AREA_ID_NAME, STOP_ID_NAME}); } @Override diff --git a/src/main/java/com/conveyal/gtfs/model/TimeFrame.java b/src/main/java/com/conveyal/gtfs/model/TimeFrame.java index d065507f3..20a1b7cb2 100644 --- a/src/main/java/com/conveyal/gtfs/model/TimeFrame.java +++ b/src/main/java/com/conveyal/gtfs/model/TimeFrame.java @@ -18,10 +18,10 @@ public class TimeFrame extends Entity { public String feed_id; public static final String TABLE_NAME = "timeframes"; - public static final String TIME_FRAME_GROUP_ID_COLUMN_NAME = "timeframe_group_id"; - public static final String START_TIME_COLUMN_NAME = "start_time"; - public static final String END_TIME_COLUMN_NAME = "end_time"; - public static final String SERVICE_ID_COLUMN_NAME = "service_id"; + public static final String TIME_FRAME_GROUP_ID_NAME = "timeframe_group_id"; + public static final String START_TIME_NAME = "start_time"; + public static final String END_TIME_NAME = "end_time"; + public static final String SERVICE_ID_NAME = "service_id"; @Override public String getId () { @@ -57,18 +57,18 @@ protected boolean isRequired() { public void loadOneRow() throws IOException { TimeFrame timeFrame = new TimeFrame(); timeFrame.id = row + 1; // offset line number by 1 to account for 0-based row index - timeFrame.timeframe_group_id = getStringField(TIME_FRAME_GROUP_ID_COLUMN_NAME, true); - timeFrame.start_time = getTimeField(START_TIME_COLUMN_NAME, false); + timeFrame.timeframe_group_id = getStringField(TIME_FRAME_GROUP_ID_NAME, true); + timeFrame.start_time = getTimeField(START_TIME_NAME, false); if (timeFrame.start_time == INT_MISSING) { // An empty value is considered the start of the day (00:00:00). timeFrame.start_time = 0; } - timeFrame.end_time = getTimeField(END_TIME_COLUMN_NAME, false); + timeFrame.end_time = getTimeField(END_TIME_NAME, false); if (timeFrame.end_time == INT_MISSING) { // An empty value is considered the end of the day (24:00:00). timeFrame.end_time = 86400; } - timeFrame.service_id = getStringField(SERVICE_ID_COLUMN_NAME, true); + timeFrame.service_id = getStringField(SERVICE_ID_NAME, true); timeFrame.feed = feed; timeFrame.feed_id = feed.feedId; feed.time_frames.put(timeFrame.getId(), timeFrame); @@ -84,10 +84,10 @@ public Writer(GTFSFeed feed) { @Override public void writeHeaders() throws IOException { writer.writeRecord(new String[] { - TIME_FRAME_GROUP_ID_COLUMN_NAME, - START_TIME_COLUMN_NAME, - END_TIME_COLUMN_NAME, - SERVICE_ID_COLUMN_NAME + TIME_FRAME_GROUP_ID_NAME, + START_TIME_NAME, + END_TIME_NAME, + SERVICE_ID_NAME }); } diff --git a/src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java b/src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java index 7d2b5d7ff..d24829ae7 100644 --- a/src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java +++ b/src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java @@ -9,7 +9,6 @@ import com.conveyal.gtfs.dto.RouteNetworkDTO; import com.conveyal.gtfs.dto.StopAreaDTO; import com.conveyal.gtfs.dto.TimeFrameDTO; -import com.conveyal.gtfs.graphql.GTFSGraphQL; import com.conveyal.gtfs.loader.FeedLoadResult; import com.conveyal.gtfs.loader.JdbcTableWriter; import com.conveyal.gtfs.loader.Table; @@ -35,7 +34,6 @@ import java.io.IOException; import java.sql.ResultSet; import java.sql.SQLException; -import java.time.Duration; import java.util.zip.ZipFile; import static com.conveyal.gtfs.GTFS.load; @@ -48,7 +46,6 @@ import static com.conveyal.gtfs.TestUtils.getResultSetForId; import static org.hamcrest.Matchers.equalTo; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTimeout; import static org.junit.jupiter.api.Assertions.assertTrue; public class GTFSFaresV2Test { @@ -58,8 +55,6 @@ public class GTFSFaresV2Test { public static String faresDBName; private static DataSource faresDataSource; private static String faresNamespace; - private static final int TEST_TIMEOUT = 5000; - private static final ObjectMapper mapper = new ObjectMapper(); private static JdbcTableWriter createTestTableWriter (Table table) throws InvalidNamespaceException { @@ -89,15 +84,6 @@ public static void tearDownClass() { TestUtils.dropDB(faresDBName); } - /** Tests that the graphQL schema can initialize. */ - @Test - void canInitialize() { - assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { - GTFSGraphQL.initialize(faresDataSource); - GTFSGraphQL.getGraphQl(); - }); - } - /** * Make sure a round-trip of loading fares v2 data and then writing this to another zip file can be performed. */ @@ -218,8 +204,8 @@ void canCreateUpdateAndDeleteAreas() throws IOException, SQLException, InvalidNa ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, updatedAreaDTO.id, Table.AREAS); while (resultSet.next()) { - assertResultValue(resultSet, Area.AREA_ID_COLUMN_NAME, equalTo(createdArea.area_id)); - assertResultValue(resultSet, Area.AREA_NAME_COLUMN_NAME,equalTo(createdArea.area_name)); + assertResultValue(resultSet, Area.AREA_ID_NAME, equalTo(createdArea.area_id)); + assertResultValue(resultSet, Area.AREA_NAME_NAME,equalTo(createdArea.area_name)); } // Delete. @@ -247,8 +233,8 @@ void canCreateUpdateAndDeleteStopAreas() throws IOException, SQLException, Inval ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, updatedStopAreaDTO.id, Table.STOP_AREAS); while (resultSet.next()) { - assertResultValue(resultSet, StopArea.AREA_ID_COLUMN_NAME, equalTo(createdStopArea.area_id)); - assertResultValue(resultSet, StopArea.STOP_ID_COLUMN_NAME,equalTo(createdStopArea.stop_id)); + assertResultValue(resultSet, StopArea.AREA_ID_NAME, equalTo(createdStopArea.area_id)); + assertResultValue(resultSet, StopArea.STOP_ID_NAME,equalTo(createdStopArea.stop_id)); } // Delete. @@ -276,10 +262,10 @@ void canCreateUpdateAndDeleteTimeFrames() throws IOException, SQLException, Inva ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, updatedTimeFrameDTO.id, Table.TIME_FRAMES); while (resultSet.next()) { - assertResultValue(resultSet, TimeFrame.TIME_FRAME_GROUP_ID_COLUMN_NAME, equalTo(createdTimeFrame.timeframe_group_id)); - assertResultValue(resultSet, TimeFrame.START_TIME_COLUMN_NAME, equalTo(createdTimeFrame.start_time)); - assertResultValue(resultSet, TimeFrame.END_TIME_COLUMN_NAME, equalTo(createdTimeFrame.end_time)); - assertResultValue(resultSet, TimeFrame.SERVICE_ID_COLUMN_NAME, equalTo(createdTimeFrame.service_id)); + assertResultValue(resultSet, TimeFrame.TIME_FRAME_GROUP_ID_NAME, equalTo(createdTimeFrame.timeframe_group_id)); + assertResultValue(resultSet, TimeFrame.START_TIME_NAME, equalTo(createdTimeFrame.start_time)); + assertResultValue(resultSet, TimeFrame.END_TIME_NAME, equalTo(createdTimeFrame.end_time)); + assertResultValue(resultSet, TimeFrame.SERVICE_ID_NAME, equalTo(createdTimeFrame.service_id)); } // Delete. @@ -307,8 +293,8 @@ void canCreateUpdateAndDeleteNetworks() throws IOException, SQLException, Invali ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, updatedNetworkDTO.id, Table.NETWORKS); while (resultSet.next()) { - assertResultValue(resultSet, Network.NETWORK_ID_COLUMN_NAME, equalTo(createdNetwork.network_id)); - assertResultValue(resultSet, Network.NETWORK_NAME_COLUMN_NAME, equalTo(createdNetwork.network_name)); + assertResultValue(resultSet, Network.NETWORK_ID_NAME, equalTo(createdNetwork.network_id)); + assertResultValue(resultSet, Network.NETWORK_NAME_NAME, equalTo(createdNetwork.network_name)); } // Delete. @@ -336,8 +322,8 @@ void canCreateUpdateAndDeleteRouteNetworks() throws IOException, SQLException, I ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, updatedNetworkDTO.id, Table.ROUTE_NETWORKS); while (resultSet.next()) { - assertResultValue(resultSet, RouteNetwork.NETWORK_ID_COLUMN_NAME, equalTo(createdRouteNetwork.network_id)); - assertResultValue(resultSet, RouteNetwork.ROUTE_ID_COLUMN_NAME, equalTo(createdRouteNetwork.route_id)); + assertResultValue(resultSet, RouteNetwork.NETWORK_ID_NAME, equalTo(createdRouteNetwork.network_id)); + assertResultValue(resultSet, RouteNetwork.ROUTE_ID_NAME, equalTo(createdRouteNetwork.route_id)); } // Delete. @@ -365,13 +351,13 @@ void canCreateUpdateAndDeleteFareLegRules() throws IOException, SQLException, In ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, updatedFareLegRuleDTO.id, Table.FARE_LEG_RULES); while (resultSet.next()) { - assertResultValue(resultSet, FareLegRule.LEG_GROUP_ID_COLUMN_NAME, equalTo(createdFareLegRule.leg_group_id)); - assertResultValue(resultSet, FareLegRule.NETWORK_ID_COLUMN_NAME, equalTo(createdFareLegRule.network_id)); - assertResultValue(resultSet, FareLegRule.FROM_AREA_ID_COLUMN_NAME, equalTo(createdFareLegRule.from_area_id)); - assertResultValue(resultSet, FareLegRule.TO_AREA_ID_COLUMN_NAME, equalTo(createdFareLegRule.to_area_id)); - assertResultValue(resultSet, FareLegRule.FROM_TIMEFRAME_GROUP_ID_COLUMN_NAME, equalTo(createdFareLegRule.from_timeframe_group_id)); - assertResultValue(resultSet, FareLegRule.FARE_PRODUCT_ID_COLUMN_NAME, equalTo(createdFareLegRule.fare_product_id)); - assertResultValue(resultSet, FareLegRule.RULE_PRIORITY_COLUMN_NAME, equalTo(createdFareLegRule.rule_priority)); + assertResultValue(resultSet, FareLegRule.LEG_GROUP_ID_NAME, equalTo(createdFareLegRule.leg_group_id)); + assertResultValue(resultSet, FareLegRule.NETWORK_ID_NAME, equalTo(createdFareLegRule.network_id)); + assertResultValue(resultSet, FareLegRule.FROM_AREA_ID_NAME, equalTo(createdFareLegRule.from_area_id)); + assertResultValue(resultSet, FareLegRule.TO_AREA_ID_NAME, equalTo(createdFareLegRule.to_area_id)); + assertResultValue(resultSet, FareLegRule.FROM_TIMEFRAME_GROUP_ID_NAME, equalTo(createdFareLegRule.from_timeframe_group_id)); + assertResultValue(resultSet, FareLegRule.FARE_PRODUCT_ID_NAME, equalTo(createdFareLegRule.fare_product_id)); + assertResultValue(resultSet, FareLegRule.RULE_PRIORITY_NAME, equalTo(createdFareLegRule.rule_priority)); } // Delete. @@ -399,9 +385,9 @@ void canCreateUpdateAndDeleteFareMedia() throws IOException, SQLException, Inval ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, updatedFareMediaDTO.id, Table.FARE_MEDIAS); while (resultSet.next()) { - assertResultValue(resultSet, FareMedia.FARE_MEDIA_ID_COLUMN_NAME, equalTo(createdFareMedia.fare_media_id)); - assertResultValue(resultSet, FareMedia.FARE_MEDIA_NAME_COLUMN_NAME, equalTo(createdFareMedia.fare_media_name)); - assertResultValue(resultSet, FareMedia.FARE_MEDIA_TYPE_COLUMN_NAME, equalTo(createdFareMedia.fare_media_type)); + assertResultValue(resultSet, FareMedia.FARE_MEDIA_ID_NAME, equalTo(createdFareMedia.fare_media_id)); + assertResultValue(resultSet, FareMedia.FARE_MEDIA_NAME_NAME, equalTo(createdFareMedia.fare_media_name)); + assertResultValue(resultSet, FareMedia.FARE_MEDIA_TYPE_NAME, equalTo(createdFareMedia.fare_media_type)); } // Delete. @@ -429,9 +415,9 @@ void canCreateUpdateAndDeleteFareProduct() throws IOException, SQLException, Inv ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, fareProductDTO.id, Table.FARE_PRODUCTS); while (resultSet.next()) { - assertResultValue(resultSet, FareProduct.FARE_PRODUCT_ID_COLUMN_NAME, equalTo(createdFareProduct.fare_product_id)); - assertResultValue(resultSet, FareProduct.FARE_PRODUCT_NAME_COLUMN_NAME, equalTo(createdFareProduct.fare_product_name)); - assertResultValue(resultSet, FareProduct.FARE_MEDIA_ID_COLUMN_NAME, equalTo(createdFareProduct.fare_media_id)); + assertResultValue(resultSet, FareProduct.FARE_PRODUCT_ID_NAME, equalTo(createdFareProduct.fare_product_id)); + assertResultValue(resultSet, FareProduct.FARE_PRODUCT_NAME_NAME, equalTo(createdFareProduct.fare_product_name)); + assertResultValue(resultSet, FareProduct.FARE_MEDIA_ID_NAME, equalTo(createdFareProduct.fare_media_id)); } // Delete. @@ -464,12 +450,12 @@ void canCreateUpdateAndDeleteFareTransferRule() throws IOException, SQLException ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, fareTransferRuleDTO.id, Table.FARE_TRANSFER_RULES); while (resultSet.next()) { - assertResultValue(resultSet, FareTransferRule.FROM_LEG_GROUP_ID_COLUMN_NAME, equalTo(fareTransferRules.from_leg_group_id)); - assertResultValue(resultSet, FareTransferRule.TO_LEG_GROUP_ID_COLUMN_NAME, equalTo(fareTransferRules.to_leg_group_id)); - assertResultValue(resultSet, FareTransferRule.TRANSFER_COUNT_COLUMN_NAME, equalTo(fareTransferRules.transfer_count)); - assertResultValue(resultSet, FareTransferRule.DURATION_LIMIT_COLUMN_NAME, equalTo(fareTransferRules.duration_limit)); - assertResultValue(resultSet, FareTransferRule.FARE_TRANSFER_TYPE_COLUMN_NAME, equalTo(fareTransferRules.fare_transfer_type)); - assertResultValue(resultSet, FareTransferRule.FARE_PRODUCT_ID_COLUMN_NAME, equalTo(fareTransferRules.fare_product_id)); + assertResultValue(resultSet, FareTransferRule.FROM_LEG_GROUP_ID_NAME, equalTo(fareTransferRules.from_leg_group_id)); + assertResultValue(resultSet, FareTransferRule.TO_LEG_GROUP_ID_NAME, equalTo(fareTransferRules.to_leg_group_id)); + assertResultValue(resultSet, FareTransferRule.TRANSFER_COUNT_NAME, equalTo(fareTransferRules.transfer_count)); + assertResultValue(resultSet, FareTransferRule.DURATION_LIMIT_NAME, equalTo(fareTransferRules.duration_limit)); + assertResultValue(resultSet, FareTransferRule.FARE_TRANSFER_TYPE_NAME, equalTo(fareTransferRules.fare_transfer_type)); + assertResultValue(resultSet, FareTransferRule.FARE_PRODUCT_ID_NAME, equalTo(fareTransferRules.fare_product_id)); } // Delete. From 1d95cf5dc5eb2907ccf2abd4ede5b55963807195 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Fri, 18 Oct 2024 07:38:02 +0100 Subject: [PATCH 07/40] refactor(Update to include network_id in the route GraphQL obj): --- .../gtfs/graphql/GraphQLGtfsSchema.java | 1 + .../gtfs/graphql/GTFSGraphQLTest.java | 87 +++++++++---------- src/test/resources/graphql/feedRoutes.txt | 1 + .../GTFSGraphQLTest/canFetchRoutes-0.json | 1 + ...nSanitizeSQLInjectionSentAsKeyValue-0.json | 1 + 5 files changed, 47 insertions(+), 44 deletions(-) diff --git a/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsSchema.java b/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsSchema.java index a9395cdc6..af233ed1f 100644 --- a/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsSchema.java +++ b/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsSchema.java @@ -328,6 +328,7 @@ public class GraphQLGtfsSchema { .build() ) .field(RowCountFetcher.field("count", "routes")) + .field(MapFetcher.field("network_id")) .build(); // Represents rows from stops.txt diff --git a/src/test/java/com/conveyal/gtfs/graphql/GTFSGraphQLTest.java b/src/test/java/com/conveyal/gtfs/graphql/GTFSGraphQLTest.java index 176e5c030..d9c0e85c1 100644 --- a/src/test/java/com/conveyal/gtfs/graphql/GTFSGraphQLTest.java +++ b/src/test/java/com/conveyal/gtfs/graphql/GTFSGraphQLTest.java @@ -103,7 +103,7 @@ public static void tearDownClass() { /** Tests that the graphQL schema can initialize. */ @Test - public void canInitialize() { + void canInitialize() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { GTFSGraphQL.initialize(testDataSource); GTFSGraphQL.getGraphQl(); @@ -112,7 +112,7 @@ public void canInitialize() { /** Tests that the root element of a feed can be fetched. */ @Test - public void canFetchFeed() { + void canFetchFeed() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { MatcherAssert.assertThat(queryGraphQL("feed.txt"), matchesSnapshot()); }); @@ -120,7 +120,7 @@ public void canFetchFeed() { /** Tests that the row counts of a feed can be fetched. */ @Test - public void canFetchFeedRowCounts() { + void canFetchFeedRowCounts() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { MatcherAssert.assertThat(queryGraphQL("feedRowCounts.txt"), matchesSnapshot()); }); @@ -128,7 +128,7 @@ public void canFetchFeedRowCounts() { /** Tests that the errors of a feed can be fetched. */ @Test - public void canFetchErrors() { + void canFetchErrors() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { MatcherAssert.assertThat(queryGraphQL("feedErrors.txt"), matchesSnapshot()); }); @@ -136,7 +136,7 @@ public void canFetchErrors() { /** Tests that the feed_info of a feed can be fetched. */ @Test - public void canFetchFeedInfo() { + void canFetchFeedInfo() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { MatcherAssert.assertThat(queryGraphQL("feedFeedInfo.txt"), matchesSnapshot()); }); @@ -144,15 +144,15 @@ public void canFetchFeedInfo() { /** Tests that the patterns of a feed can be fetched. */ @Test - public void canFetchPatterns() { + void canFetchPatterns() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { MatcherAssert.assertThat(queryGraphQL("feedPatterns.txt"), matchesSnapshot()); }); } - /** Tests that the patterns of a feed can be fetched. */ + /** Tests that the poly lines of a feed can be fetched. */ @Test - public void canFetchPolylines() { + void canFetchPolylines() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { MatcherAssert.assertThat(queryGraphQL("feedPolylines.txt"), matchesSnapshot()); }); @@ -160,7 +160,7 @@ public void canFetchPolylines() { /** Tests that the agencies of a feed can be fetched. */ @Test - public void canFetchAgencies() { + void canFetchAgencies() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { MatcherAssert.assertThat(queryGraphQL("feedAgencies.txt"), matchesSnapshot()); }); @@ -168,7 +168,7 @@ public void canFetchAgencies() { /** Tests that the attributions of a feed can be fetched. */ @Test - public void canFetchATtributions() { + void canFetchAttributions() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { MatcherAssert.assertThat(queryGraphQL("feedAttributions.txt"), matchesSnapshot()); }); @@ -176,7 +176,7 @@ public void canFetchATtributions() { /** Tests that the calendars of a feed can be fetched. */ @Test - public void canFetchCalendars() { + void canFetchCalendars() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { MatcherAssert.assertThat(queryGraphQL("feedCalendars.txt"), matchesSnapshot()); }); @@ -184,7 +184,7 @@ public void canFetchCalendars() { /** Tests that the fares of a feed can be fetched. */ @Test - public void canFetchFares() { + void canFetchFares() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { MatcherAssert.assertThat(queryGraphQL("feedFares.txt"), matchesSnapshot()); }); @@ -192,7 +192,7 @@ public void canFetchFares() { /** Tests that the routes of a feed can be fetched. */ @Test - public void canFetchRoutes() { + void canFetchRoutes() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { MatcherAssert.assertThat(queryGraphQL("feedRoutes.txt"), matchesSnapshot()); }); @@ -200,15 +200,15 @@ public void canFetchRoutes() { /** Tests that the stops of a feed can be fetched. */ @Test - public void canFetchStops() { + void canFetchStops() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { MatcherAssert.assertThat(queryGraphQL("feedStops.txt"), matchesSnapshot()); }); } - /** Tests that the stops of a feed can be fetched. */ + /** Tests that stops with children can be fetched. */ @Test - public void canFetchStopWithChildren() { + void canFetchStopWithChildren() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { MatcherAssert.assertThat(queryGraphQL("feedStopWithChildren.txt"), matchesSnapshot()); }); @@ -216,7 +216,7 @@ public void canFetchStopWithChildren() { /** Tests that the trips of a feed can be fetched. */ @Test - public void canFetchTrips() { + void canFetchTrips() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { MatcherAssert.assertThat(queryGraphQL("feedTrips.txt"), matchesSnapshot()); }); @@ -224,7 +224,7 @@ public void canFetchTrips() { /** Tests that the translations of a feed can be fetched. */ @Test - public void canFetchTranslations() { + void canFetchTranslations() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { MatcherAssert.assertThat(queryGraphQL("feedTranslations.txt"), matchesSnapshot()); }); @@ -234,15 +234,15 @@ public void canFetchTranslations() { /** Tests that the stop times of a feed can be fetched. */ @Test - public void canFetchStopTimes() { + void canFetchStopTimes() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { MatcherAssert.assertThat(queryGraphQL("feedStopTimes.txt"), matchesSnapshot()); }); } - /** Tests that the stop times of a feed can be fetched. */ + /** Tests that the services of a feed can be fetched. */ @Test - public void canFetchServices() { + void canFetchServices() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { MatcherAssert.assertThat(queryGraphQL("feedServices.txt"), matchesSnapshot()); }); @@ -251,69 +251,68 @@ public void canFetchServices() { @Test void canFetchAreas() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { - MatcherAssert.assertThat(queryFaresGraphQL("feedAreas.txt"), matchesSnapshot()); + MatcherAssert.assertThat(queryFaresV2GraphQL("feedAreas.txt"), matchesSnapshot()); }); } @Test void canFetchStopAreas() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { - MatcherAssert.assertThat(queryFaresGraphQL("feedStopAreas.txt"), matchesSnapshot()); + MatcherAssert.assertThat(queryFaresV2GraphQL("feedStopAreas.txt"), matchesSnapshot()); }); } @Test void canFetchFareTransferRules() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { - MatcherAssert.assertThat(queryFaresGraphQL("feedFareTransferRules.txt"), matchesSnapshot()); + MatcherAssert.assertThat(queryFaresV2GraphQL("feedFareTransferRules.txt"), matchesSnapshot()); }); } @Test void canFetchFareProducts() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { - MatcherAssert.assertThat(queryFaresGraphQL("feedFareProducts.txt"), matchesSnapshot()); + MatcherAssert.assertThat(queryFaresV2GraphQL("feedFareProducts.txt"), matchesSnapshot()); }); } @Test void canFetchFareMedias() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { - MatcherAssert.assertThat(queryFaresGraphQL("feedFareMedias.txt"), matchesSnapshot()); + MatcherAssert.assertThat(queryFaresV2GraphQL("feedFareMedias.txt"), matchesSnapshot()); }); } @Test void canFetchFareLegRules() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { - MatcherAssert.assertThat(queryFaresGraphQL("feedFareLegRules.txt"), matchesSnapshot()); + MatcherAssert.assertThat(queryFaresV2GraphQL("feedFareLegRules.txt"), matchesSnapshot()); }); } @Test void canFetchTimeFrames() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { - MatcherAssert.assertThat(queryFaresGraphQL("feedTimeFrames.txt"), matchesSnapshot()); + MatcherAssert.assertThat(queryFaresV2GraphQL("feedTimeFrames.txt"), matchesSnapshot()); }); } @Test void canFetchNetworks() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { - MatcherAssert.assertThat(queryFaresGraphQL("feedNetworks.txt"), matchesSnapshot()); + MatcherAssert.assertThat(queryFaresV2GraphQL("feedNetworks.txt"), matchesSnapshot()); }); } @Test void canFetchRouteNetworks() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { - MatcherAssert.assertThat(queryFaresGraphQL("feedRouteNetworks.txt"), matchesSnapshot()); + MatcherAssert.assertThat(queryFaresV2GraphQL("feedRouteNetworks.txt"), matchesSnapshot()); }); } - /** Tests that the stop times of a feed can be fetched. */ @Test - public void canFetchRoutesAndFilterTripsByDateAndTime() { + void canFetchRoutesAndFilterTripsByDateAndTime() { Map variables = new HashMap<>(); variables.put("namespace", testNamespace); variables.put("date", "20170915"); @@ -327,30 +326,30 @@ public void canFetchRoutesAndFilterTripsByDateAndTime() { }); } - /** Tests that the limit argument applies properly to a fetcher defined with autolimit set to false. */ + /** Tests that the limit argument applies properly to a fetcher defined with auto limit set to false. */ @Test - public void canFetchNestedEntityWithLimit() { + void canFetchNestedEntityWithLimit() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { MatcherAssert.assertThat(queryGraphQL("feedStopsStopTimeLimit.txt"), matchesSnapshot()); }); } - /** Tests whether a graphQL query that has superflous and redundant nesting can find the right result. */ + /** Tests whether a graphQL query that has superfluous and redundant nesting can find the right result. */ // if the graphQL dataloader is enabled correctly, there will not be any repeating sql queries in the logs @Test - public void canFetchMultiNestedEntities() { + void canFetchMultiNestedEntities() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { MatcherAssert.assertThat(queryGraphQL("superNested.txt"), matchesSnapshot()); }); } /** - * Tests whether a graphQL query that has superflous and redundant nesting can find the right result. + * Tests whether a graphQL query that has superfluous and redundant nesting can find the right result. * If the graphQL dataloader is enabled correctly, there will not be any repeating sql queries in the logs. - * Furthermore, some queries should have been combined together. + * Furthermore, some queries should have been combined. */ @Test - public void canFetchMultiNestedEntitiesWithoutLimits() { + void canFetchMultiNestedEntitiesWithoutLimits() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { MatcherAssert.assertThat(queryGraphQL("superNestedNoLimits.txt"), matchesSnapshot()); }); @@ -360,7 +359,7 @@ public void canFetchMultiNestedEntitiesWithoutLimits() { * parent_station column in the imported stops table. */ @Test - public void canFetchStopsWithoutParentStationColumn() { + void canFetchStopsWithoutParentStationColumn() { Map variables = new HashMap<>(); variables.put("namespace", badCalendarDateNamespace); assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { @@ -380,7 +379,7 @@ public void canFetchStopsWithoutParentStationColumn() { * The graphql library should properly escape the string and return 0 results for stops. */ @Test - public void canSanitizeSQLInjectionSentAsInput() { + void canSanitizeSQLInjectionSentAsInput() { Map variables = new HashMap<>(); variables.put("namespace", testInjectionNamespace); variables.put("stop_id", Arrays.asList("' OR 1=1;")); @@ -401,7 +400,7 @@ public void canSanitizeSQLInjectionSentAsInput() { * The graphql library should properly escape the string and complete the queries. */ @Test - public void canSanitizeSQLInjectionSentAsKeyValue() { + void canSanitizeSQLInjectionSentAsKeyValue() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { // manually update the route_id key in routes and patterns String injection = "'' OR 1=1; Select ''99"; @@ -434,12 +433,12 @@ private Map queryGraphQL(String queryFilename) throws IOExceptio } /** - * Helper method to make a fares query with default variables. + * Helper method to make a fares V2 query with default variables. * * @param queryFilename the filename that should be used to generate the GraphQL query. This file must be present * in the `src/test/resources/graphql` folder */ - private Map queryFaresGraphQL(String queryFilename) throws IOException { + private Map queryFaresV2GraphQL(String queryFilename) throws IOException { Map variables = new HashMap<>(); variables.put("namespace", faresNamespace); return queryGraphQL(queryFilename, variables, faresDataSource); diff --git a/src/test/resources/graphql/feedRoutes.txt b/src/test/resources/graphql/feedRoutes.txt index f2b74dfb6..053960dd1 100644 --- a/src/test/resources/graphql/feedRoutes.txt +++ b/src/test/resources/graphql/feedRoutes.txt @@ -28,6 +28,7 @@ query ($namespace: String) { trip_id } wheelchair_accessible + network_id } } } \ No newline at end of file diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchRoutes-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchRoutes-0.json index 1c1a10c26..b308a1582 100644 --- a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchRoutes-0.json +++ b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchRoutes-0.json @@ -6,6 +6,7 @@ "agency_id" : "1", "count" : 1, "id" : 2, + "network_id" : null, "pattern_count" : 2, "patterns" : [ { "pattern_id" : "1" diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canSanitizeSQLInjectionSentAsKeyValue-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canSanitizeSQLInjectionSentAsKeyValue-0.json index 04c98ecdc..b541c33a8 100644 --- a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canSanitizeSQLInjectionSentAsKeyValue-0.json +++ b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canSanitizeSQLInjectionSentAsKeyValue-0.json @@ -6,6 +6,7 @@ "agency_id" : "1", "count" : 1, "id" : 2, + "network_id" : null, "pattern_count" : 2, "patterns" : [ { "pattern_id" : "1" From fbf52822497ff58f572ea96c2d1fc26b8a4a9c7e Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Tue, 28 Jan 2025 10:37:40 +0000 Subject: [PATCH 08/40] refactor(Replaced camel case with snake case for GrpahQL objs): --- .../graphql/GraphQLGtfsFaresV2Schema.java | 68 ++++++---- src/test/resources/graphql/feedAreas.txt | 2 +- .../resources/graphql/feedFareLegRules.txt | 12 +- src/test/resources/graphql/feedFareMedias.txt | 2 +- .../resources/graphql/feedFareProducts.txt | 4 +- .../graphql/feedFareTransferRules.txt | 10 +- .../resources/graphql/feedRouteNetworks.txt | 2 +- src/test/resources/graphql/feedStopAreas.txt | 2 +- src/test/resources/graphql/feedTimeFrames.txt | 2 +- .../GTFSGraphQLTest/canFetchAreas-0.json | 4 +- .../canFetchFareLegRules-0.json | 124 +++++++++--------- .../GTFSGraphQLTest/canFetchFareMedias-0.json | 2 +- .../canFetchFareProducts-0.json | 22 ++-- .../canFetchFareTransferRules-0.json | 22 ++-- .../canFetchRouteNetworks-0.json | 2 +- .../GTFSGraphQLTest/canFetchStopAreas-0.json | 2 +- .../GTFSGraphQLTest/canFetchTimeFrames-0.json | 50 +++++++ 17 files changed, 196 insertions(+), 136 deletions(-) create mode 100644 src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchTimeFrames-0.json diff --git a/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsFaresV2Schema.java b/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsFaresV2Schema.java index ea1fe4e68..f1092a3d5 100644 --- a/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsFaresV2Schema.java +++ b/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsFaresV2Schema.java @@ -23,9 +23,19 @@ public class GraphQLGtfsFaresV2Schema { + private static final String AREA_TYPE_NAME = "area"; + private static final String STOP_AREA_TYPE_NAME = "stop_area"; + private static final String TIME_FRAME_TYPE_NAME = "time_frame"; + private static final String NETWORK_TYPE_NAME = "network"; + private static final String ROUTE_NETWORK_TYPE_NAME = "route_network"; + private static final String FARE_MEDIA_TYPE_NAME = "fare_media"; + private static final String FARE_PRODUCT_TYPE_NAME = "fare_product"; + private static final String FARE_LEG_RULE_TYPE_NAME = "fare_leg_rule"; + private static final String FARE_TRANSFER_RULE_TYPE_NAME = "fare_transfer_rule"; + private GraphQLGtfsFaresV2Schema() {} - public static final GraphQLObjectType stopAreaType = newObject().name("stopArea") + public static final GraphQLObjectType stopAreaType = newObject().name(STOP_AREA_TYPE_NAME) .description("A GTFS stop area object") .field(MapFetcher.field("id", GraphQLInt)) .field(MapFetcher.field(StopArea.AREA_ID_NAME)) @@ -33,15 +43,15 @@ private GraphQLGtfsFaresV2Schema() {} .field(createFieldDefinition("stops", GraphQLGtfsSchema.stopType, "stops", StopArea.STOP_ID_NAME)) .build(); - public static final GraphQLObjectType areaType = newObject().name("area") + public static final GraphQLObjectType areaType = newObject().name(AREA_TYPE_NAME) .description("A GTFS area object") .field(MapFetcher.field("id", GraphQLInt)) .field(MapFetcher.field(Area.AREA_ID_NAME)) .field(MapFetcher.field(Area.AREA_NAME_NAME)) - .field(createFieldDefinition("stopAreas", stopAreaType, StopArea.TABLE_NAME, Area.AREA_ID_NAME)) + .field(createFieldDefinition("stop_areas", stopAreaType, StopArea.TABLE_NAME, Area.AREA_ID_NAME)) .build(); - public static final GraphQLObjectType timeFrameType = newObject().name("timeFrame") + public static final GraphQLObjectType timeFrameType = newObject().name(TIME_FRAME_TYPE_NAME) .description("A GTFS time frame object") .field(MapFetcher.field("id", GraphQLInt)) .field(MapFetcher.field(TimeFrame.TIME_FRAME_GROUP_ID_NAME)) @@ -50,14 +60,14 @@ private GraphQLGtfsFaresV2Schema() {} .field(MapFetcher.field(TimeFrame.SERVICE_ID_NAME)) .build(); - public static final GraphQLObjectType networkType = newObject().name("network") + public static final GraphQLObjectType networkType = newObject().name(NETWORK_TYPE_NAME) .description("A GTFS network object") .field(MapFetcher.field("id", GraphQLInt)) .field(MapFetcher.field(Network.NETWORK_ID_NAME)) .field(MapFetcher.field(Network.NETWORK_NAME_NAME)) .build(); - public static final GraphQLObjectType routeNetworkType = newObject().name("routeNetwork") + public static final GraphQLObjectType routeNetworkType = newObject().name(ROUTE_NETWORK_TYPE_NAME) .description("A GTFS route network object") .field(MapFetcher.field("id", GraphQLInt)) .field(MapFetcher.field(RouteNetwork.NETWORK_ID_NAME)) @@ -65,7 +75,7 @@ private GraphQLGtfsFaresV2Schema() {} .field(createFieldDefinition("networks", networkType, Network.TABLE_NAME, RouteNetwork.NETWORK_ID_NAME)) .build(); - public static final GraphQLObjectType fareMediaType = newObject().name("fareMedia") + public static final GraphQLObjectType fareMediaType = newObject().name(FARE_MEDIA_TYPE_NAME) .description("A GTFS fare media object") .field(MapFetcher.field("id", GraphQLInt)) .field(MapFetcher.field(FareMedia.FARE_MEDIA_ID_NAME)) @@ -73,7 +83,7 @@ private GraphQLGtfsFaresV2Schema() {} .field(MapFetcher.field(FareMedia.FARE_MEDIA_TYPE_NAME)) .build(); - public static final GraphQLObjectType fareProductType = newObject().name("fareProduct") + public static final GraphQLObjectType fareProductType = newObject().name(FARE_PRODUCT_TYPE_NAME) .description("A GTFS fare product object") .field(MapFetcher.field("id", GraphQLInt)) .field(MapFetcher.field(FareProduct.FARE_PRODUCT_ID_NAME)) @@ -81,10 +91,10 @@ private GraphQLGtfsFaresV2Schema() {} .field(MapFetcher.field(FareProduct.FARE_MEDIA_ID_NAME)) .field(MapFetcher.field(FareProduct.AMOUNT_NAME)) .field(MapFetcher.field(FareProduct.CURRENCY_NAME)) - .field(createFieldDefinition("fareMedia", fareMediaType, FareMedia.TABLE_NAME, FareProduct.FARE_MEDIA_ID_NAME)) + .field(createFieldDefinition(FARE_MEDIA_TYPE_NAME, fareMediaType, FareMedia.TABLE_NAME, FareProduct.FARE_MEDIA_ID_NAME)) .build(); - public static final GraphQLObjectType fareLegRuleType = newObject().name("fareLegRule") + public static final GraphQLObjectType fareLegRuleType = newObject().name(FARE_LEG_RULE_TYPE_NAME) .description("A GTFS fare leg rule object") .field(MapFetcher.field("id", GraphQLInt)) .field(MapFetcher.field(FareLegRule.LEG_GROUP_ID_NAME)) @@ -98,27 +108,27 @@ private GraphQLGtfsFaresV2Schema() {} // Will return either routes or networks, not both. .field(createFieldDefinition("routes", GraphQLGtfsSchema.routeType, Route.TABLE_NAME, FareLegRule.NETWORK_ID_NAME)) .field(createFieldDefinition("networks", networkType, Network.TABLE_NAME, Network.NETWORK_ID_NAME)) - .field(createFieldDefinition("fareProducts", fareProductType, FareProduct.TABLE_NAME, FareLegRule.FARE_PRODUCT_ID_NAME)) + .field(createFieldDefinition("fare_products", fareProductType, FareProduct.TABLE_NAME, FareLegRule.FARE_PRODUCT_ID_NAME)) // fromTimeFrame and toTimeFrame may return multiple time frames. .field(createFieldDefinition( - "fromTimeFrame", + "from_time_frame", timeFrameType, TimeFrame.TABLE_NAME, FareLegRule.FROM_TIMEFRAME_GROUP_ID_NAME, TimeFrame.TIME_FRAME_GROUP_ID_NAME )) .field(createFieldDefinition( - "toTimeFrame", + "to_time_frame", timeFrameType, TimeFrame.TABLE_NAME, FareLegRule.TO_TIMEFRAME_GROUP_ID_NAME, TimeFrame.TIME_FRAME_GROUP_ID_NAME )) - .field(createFieldDefinition("toArea", areaType, Area.TABLE_NAME, FareLegRule.TO_AREA_ID_NAME, Area.AREA_ID_NAME)) - .field(createFieldDefinition("fromArea", areaType, Area.TABLE_NAME, FareLegRule.FROM_AREA_ID_NAME, Area.AREA_ID_NAME)) + .field(createFieldDefinition("to_area", areaType, Area.TABLE_NAME, FareLegRule.TO_AREA_ID_NAME, Area.AREA_ID_NAME)) + .field(createFieldDefinition("from_area", areaType, Area.TABLE_NAME, FareLegRule.FROM_AREA_ID_NAME, Area.AREA_ID_NAME)) .build(); - public static final GraphQLObjectType fareTransferRuleType = newObject().name("fareTransferRule") + public static final GraphQLObjectType fareTransferRuleType = newObject().name(FARE_TRANSFER_RULE_TYPE_NAME) .description("A GTFS fare transfer rule object") .field(MapFetcher.field("id", GraphQLInt)) .field(MapFetcher.field(FareTransferRule.FROM_LEG_GROUP_ID_NAME)) @@ -127,17 +137,17 @@ private GraphQLGtfsFaresV2Schema() {} .field(MapFetcher.field(FareTransferRule.DURATION_LIMIT_NAME)) .field(MapFetcher.field(FareTransferRule.DURATION_LIMIT_TYPE_NAME)) .field(MapFetcher.field(FareTransferRule.FARE_PRODUCT_ID_NAME)) - .field(createFieldDefinition("toArea", areaType, Area.TABLE_NAME, FareLegRule.TO_AREA_ID_NAME, Area.AREA_ID_NAME)) - .field(createFieldDefinition("fareProducts", fareProductType, FareProduct.TABLE_NAME, FareProduct.FARE_PRODUCT_ID_NAME)) + .field(createFieldDefinition("to_area", areaType, Area.TABLE_NAME, FareLegRule.TO_AREA_ID_NAME, Area.AREA_ID_NAME)) + .field(createFieldDefinition("fare_products", fareProductType, FareProduct.TABLE_NAME, FareProduct.FARE_PRODUCT_ID_NAME)) .field(createFieldDefinition( - "fromFareLegRule", + "from_fare_leg_rule", fareLegRuleType, FareLegRule.TABLE_NAME, FareTransferRule.FROM_LEG_GROUP_ID_NAME, FareLegRule.LEG_GROUP_ID_NAME )) .field(createFieldDefinition( - "toFareLegRule", + "to_fare_leg_rule", fareLegRuleType, FareLegRule.TABLE_NAME, FareTransferRule.TO_LEG_GROUP_ID_NAME, @@ -147,15 +157,15 @@ private GraphQLGtfsFaresV2Schema() {} public static List getFaresV2FieldDefinitions() { return Arrays.asList( - createFieldDefinition("area", areaType, Area.TABLE_NAME), - createFieldDefinition("stopArea", stopAreaType, StopArea.TABLE_NAME), - createFieldDefinition("fareTransferRule", fareTransferRuleType, FareTransferRule.TABLE_NAME), - createFieldDefinition("fareProduct", fareProductType, FareProduct.TABLE_NAME), - createFieldDefinition("fareMedia", fareMediaType, FareMedia.TABLE_NAME), - createFieldDefinition("fareLegRule", fareLegRuleType, FareLegRule.TABLE_NAME), - createFieldDefinition("timeFrame", timeFrameType, TimeFrame.TABLE_NAME), - createFieldDefinition("network", networkType, Network.TABLE_NAME), - createFieldDefinition("routeNetwork", routeNetworkType, RouteNetwork.TABLE_NAME) + createFieldDefinition(AREA_TYPE_NAME, areaType, Area.TABLE_NAME), + createFieldDefinition(FARE_LEG_RULE_TYPE_NAME, fareLegRuleType, FareLegRule.TABLE_NAME), + createFieldDefinition(FARE_MEDIA_TYPE_NAME, fareMediaType, FareMedia.TABLE_NAME), + createFieldDefinition(FARE_PRODUCT_TYPE_NAME, fareProductType, FareProduct.TABLE_NAME), + createFieldDefinition(FARE_TRANSFER_RULE_TYPE_NAME, fareTransferRuleType, FareTransferRule.TABLE_NAME), + createFieldDefinition(NETWORK_TYPE_NAME, networkType, Network.TABLE_NAME), + createFieldDefinition(ROUTE_NETWORK_TYPE_NAME, routeNetworkType, RouteNetwork.TABLE_NAME), + createFieldDefinition(STOP_AREA_TYPE_NAME, stopAreaType, StopArea.TABLE_NAME), + createFieldDefinition(TIME_FRAME_TYPE_NAME, timeFrameType, TimeFrame.TABLE_NAME) ); } } diff --git a/src/test/resources/graphql/feedAreas.txt b/src/test/resources/graphql/feedAreas.txt index 0cbe237a4..b2a19a710 100644 --- a/src/test/resources/graphql/feedAreas.txt +++ b/src/test/resources/graphql/feedAreas.txt @@ -5,7 +5,7 @@ query ($namespace: String) { area_id area_name id - stopAreas { + stop_areas { area_id stop_id id diff --git a/src/test/resources/graphql/feedFareLegRules.txt b/src/test/resources/graphql/feedFareLegRules.txt index 021c9f754..237782680 100644 --- a/src/test/resources/graphql/feedFareLegRules.txt +++ b/src/test/resources/graphql/feedFareLegRules.txt @@ -1,7 +1,7 @@ query ($namespace: String) { feed(namespace: $namespace) { feed_version - fareLegRule(limit: 10) { + fare_leg_rule(limit: 10) { leg_group_id network_id from_area_id @@ -19,30 +19,30 @@ query ($namespace: String) { network_id network_name } - fareProducts { + fare_products { fare_product_id fare_product_name fare_media_id amount currency } - fromTimeFrame { + from_time_frame { timeframe_group_id start_time end_time service_id } - toTimeFrame { + to_time_frame { timeframe_group_id start_time end_time service_id } - toArea { + to_area { area_id area_name } - fromArea { + from_area { area_id area_name } diff --git a/src/test/resources/graphql/feedFareMedias.txt b/src/test/resources/graphql/feedFareMedias.txt index d35b2b057..9d4d08e08 100644 --- a/src/test/resources/graphql/feedFareMedias.txt +++ b/src/test/resources/graphql/feedFareMedias.txt @@ -1,7 +1,7 @@ query ($namespace: String) { feed(namespace: $namespace) { feed_version - fareMedia { + fare_media { fare_media_id fare_media_name fare_media_type diff --git a/src/test/resources/graphql/feedFareProducts.txt b/src/test/resources/graphql/feedFareProducts.txt index bb550d475..536ae1813 100644 --- a/src/test/resources/graphql/feedFareProducts.txt +++ b/src/test/resources/graphql/feedFareProducts.txt @@ -1,13 +1,13 @@ query ($namespace: String) { feed(namespace: $namespace) { feed_version - fareProduct(limit: 10) { + fare_product(limit: 10) { fare_product_id fare_product_name fare_media_id amount currency - fareMedia { + fare_media { fare_media_id fare_media_name fare_media_type diff --git a/src/test/resources/graphql/feedFareTransferRules.txt b/src/test/resources/graphql/feedFareTransferRules.txt index b407b7026..b8d5fc762 100644 --- a/src/test/resources/graphql/feedFareTransferRules.txt +++ b/src/test/resources/graphql/feedFareTransferRules.txt @@ -1,27 +1,27 @@ query ($namespace: String) { feed(namespace: $namespace) { feed_version - fareTransferRule (limit: 2) { + fare_transfer_rule (limit: 2) { from_leg_group_id to_leg_group_id transfer_count duration_limit duration_limit_type fare_product_id - fareProducts { + fare_products { fare_product_id fare_product_name fare_media_id amount currency - fareMedia { + fare_media { fare_media_id fare_media_name fare_media_type id } } - fromFareLegRule { + from_fare_leg_rule { leg_group_id network_id from_area_id @@ -32,7 +32,7 @@ query ($namespace: String) { rule_priority id } - toFareLegRule { + to_fare_leg_rule { leg_group_id network_id from_area_id diff --git a/src/test/resources/graphql/feedRouteNetworks.txt b/src/test/resources/graphql/feedRouteNetworks.txt index 24a303848..19fb4eef0 100644 --- a/src/test/resources/graphql/feedRouteNetworks.txt +++ b/src/test/resources/graphql/feedRouteNetworks.txt @@ -1,7 +1,7 @@ query ($namespace: String) { feed(namespace: $namespace) { feed_version - routeNetwork { + route_network { network_id route_id } diff --git a/src/test/resources/graphql/feedStopAreas.txt b/src/test/resources/graphql/feedStopAreas.txt index 91999bfd7..5e455f11d 100644 --- a/src/test/resources/graphql/feedStopAreas.txt +++ b/src/test/resources/graphql/feedStopAreas.txt @@ -1,7 +1,7 @@ query ($namespace: String) { feed(namespace: $namespace) { feed_version - stopArea { + stop_area { area_id stop_id id diff --git a/src/test/resources/graphql/feedTimeFrames.txt b/src/test/resources/graphql/feedTimeFrames.txt index 6a259e893..80331f449 100644 --- a/src/test/resources/graphql/feedTimeFrames.txt +++ b/src/test/resources/graphql/feedTimeFrames.txt @@ -1,7 +1,7 @@ query ($namespace: String) { feed(namespace: $namespace) { feed_version - timeFrame { + time_frame { timeframe_group_id start_time end_time diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchAreas-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchAreas-0.json index a40f5d459..25b2ec25f 100644 --- a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchAreas-0.json +++ b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchAreas-0.json @@ -5,7 +5,7 @@ "area_id" : "area_bl", "area_name" : "Blue Line", "id" : 2, - "stopAreas" : [ { + "stop_areas" : [ { "area_id" : "area_bl", "id" : 229, "stop_id" : "4u6g", @@ -186,7 +186,7 @@ "area_id" : "area_bl_airport", "area_name" : "Blue Line - Airport Station", "id" : 3, - "stopAreas" : [ { + "stop_areas" : [ { "area_id" : "area_bl_airport", "id" : 238, "stop_id" : "4u6g", diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareLegRules-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareLegRules-0.json index 0403cdb2c..db5a0cc32 100644 --- a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareLegRules-0.json +++ b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareLegRules-0.json @@ -1,20 +1,21 @@ { "data" : { "feed" : { - "fareLegRule" : [ { - "fareProducts" : [ { + "fare_leg_rule" : [ { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "fare_products" : [ { "amount" : "2.4", "currency" : "USD", "fare_media_id" : "charlieticket", "fare_product_id" : "prod_rapid_transit_quick_subway", "fare_product_name" : "Subway Quick Ticket" } ], - "fare_product_id" : "prod_rapid_transit_quick_subway", - "fromArea" : [ { + "from_area" : [ { "area_id" : "area_bl_airport", "area_name" : "Blue Line - Airport Station" } ], - "fromTimeFrame" : [ { + "from_area_id" : "area_bl_airport", + "from_time_frame" : [ { "end_time" : null, "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", "start_time" : null, @@ -25,7 +26,6 @@ "start_time" : "9000", "timeframe_group_id" : "timeframe_regular" } ], - "from_area_id" : "area_bl_airport", "from_timeframe_group_id" : "timeframe_regular", "id" : 2, "leg_group_id" : "leg_airport_rapid_transit_quick_subway", @@ -36,12 +36,13 @@ "route_long_name" : "RL" } ], "rule_priority" : null, - "toArea" : [ ], - "toTimeFrame" : [ ], + "to_area" : [ ], "to_area_id" : null, + "to_time_frame" : [ ], "to_timeframe_group_id" : null }, { - "fareProducts" : [ { + "fare_product_id" : "prod_cape_buzzards_hyannis_fare", + "fare_products" : [ { "amount" : "5.0", "currency" : "USD", "fare_media_id" : "cash", @@ -60,13 +61,12 @@ "fare_product_id" : "prod_cape_buzzards_hyannis_fare", "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" } ], - "fare_product_id" : "prod_cape_buzzards_hyannis_fare", - "fromArea" : [ { + "from_area" : [ { "area_id" : "area_cf_zone_buzzards", "area_name" : "CapeFLYER - Wareham/Buzzards Bay/Bourne" } ], - "fromTimeFrame" : [ ], "from_area_id" : "area_cf_zone_buzzards", + "from_time_frame" : [ ], "from_timeframe_group_id" : null, "id" : 3, "leg_group_id" : "leg_cape_buzzards_hyannis_cash", @@ -74,15 +74,16 @@ "networks" : [ ], "routes" : [ ], "rule_priority" : null, - "toArea" : [ { + "to_area" : [ { "area_id" : "area_cf_zone_hyannis", "area_name" : "CapeFLYER - Hyannis" } ], - "toTimeFrame" : [ ], "to_area_id" : "area_cf_zone_hyannis", + "to_time_frame" : [ ], "to_timeframe_group_id" : null }, { - "fareProducts" : [ { + "fare_product_id" : "prod_cape_buzzards_hyannis_fare", + "fare_products" : [ { "amount" : "5.0", "currency" : "USD", "fare_media_id" : "cash", @@ -101,13 +102,12 @@ "fare_product_id" : "prod_cape_buzzards_hyannis_fare", "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" } ], - "fare_product_id" : "prod_cape_buzzards_hyannis_fare", - "fromArea" : [ { + "from_area" : [ { "area_id" : "area_cf_zone_hyannis", "area_name" : "CapeFLYER - Hyannis" } ], - "fromTimeFrame" : [ ], "from_area_id" : "area_cf_zone_hyannis", + "from_time_frame" : [ ], "from_timeframe_group_id" : null, "id" : 4, "leg_group_id" : "leg_cape_buzzards_hyannis_cash", @@ -115,15 +115,16 @@ "networks" : [ ], "routes" : [ ], "rule_priority" : null, - "toArea" : [ { + "to_area" : [ { "area_id" : "area_cf_zone_buzzards", "area_name" : "CapeFLYER - Wareham/Buzzards Bay/Bourne" } ], - "toTimeFrame" : [ ], "to_area_id" : "area_cf_zone_buzzards", + "to_time_frame" : [ ], "to_timeframe_group_id" : null }, { - "fareProducts" : [ { + "fare_product_id" : "prod_cape_buzzards_hyannis_fare", + "fare_products" : [ { "amount" : "5.0", "currency" : "USD", "fare_media_id" : "cash", @@ -142,13 +143,12 @@ "fare_product_id" : "prod_cape_buzzards_hyannis_fare", "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" } ], - "fare_product_id" : "prod_cape_buzzards_hyannis_fare", - "fromArea" : [ { + "from_area" : [ { "area_id" : "area_cf_zone_hyannis", "area_name" : "CapeFLYER - Hyannis" } ], - "fromTimeFrame" : [ ], "from_area_id" : "area_cf_zone_hyannis", + "from_time_frame" : [ ], "from_timeframe_group_id" : null, "id" : 5, "leg_group_id" : "leg_cape_buzzards_hyannis_cash", @@ -156,15 +156,16 @@ "networks" : [ ], "routes" : [ ], "rule_priority" : null, - "toArea" : [ { + "to_area" : [ { "area_id" : "area_commuter_rail_zone_8", "area_name" : "Commuter Rail Zone 8" } ], - "toTimeFrame" : [ ], "to_area_id" : "area_commuter_rail_zone_8", + "to_time_frame" : [ ], "to_timeframe_group_id" : null }, { - "fareProducts" : [ { + "fare_product_id" : "prod_cape_buzzards_hyannis_fare", + "fare_products" : [ { "amount" : "5.0", "currency" : "USD", "fare_media_id" : "cash", @@ -183,13 +184,12 @@ "fare_product_id" : "prod_cape_buzzards_hyannis_fare", "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" } ], - "fare_product_id" : "prod_cape_buzzards_hyannis_fare", - "fromArea" : [ { + "from_area" : [ { "area_id" : "area_commuter_rail_zone_8", "area_name" : "Commuter Rail Zone 8" } ], - "fromTimeFrame" : [ ], "from_area_id" : "area_commuter_rail_zone_8", + "from_time_frame" : [ ], "from_timeframe_group_id" : null, "id" : 6, "leg_group_id" : "leg_cape_buzzards_hyannis_cash", @@ -197,15 +197,16 @@ "networks" : [ ], "routes" : [ ], "rule_priority" : null, - "toArea" : [ { + "to_area" : [ { "area_id" : "area_cf_zone_hyannis", "area_name" : "CapeFLYER - Hyannis" } ], - "toTimeFrame" : [ ], "to_area_id" : "area_cf_zone_hyannis", + "to_time_frame" : [ ], "to_timeframe_group_id" : null }, { - "fareProducts" : [ { + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fare_products" : [ { "amount" : "20.0", "currency" : "USD", "fare_media_id" : "cash", @@ -224,13 +225,12 @@ "fare_product_id" : "prod_cape_sbb_buzzards_fare", "fare_product_name" : "CapeFLYER Bourne one-way fare" } ], - "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fromArea" : [ { + "from_area" : [ { "area_id" : "area_cf_zone_buzzards", "area_name" : "CapeFLYER - Wareham/Buzzards Bay/Bourne" } ], - "fromTimeFrame" : [ ], "from_area_id" : "area_cf_zone_buzzards", + "from_time_frame" : [ ], "from_timeframe_group_id" : null, "id" : 7, "leg_group_id" : "leg_cape_sbb_buzzards_cash", @@ -238,15 +238,16 @@ "networks" : [ ], "routes" : [ ], "rule_priority" : null, - "toArea" : [ { + "to_area" : [ { "area_id" : "area_commuter_rail_zone_1a", "area_name" : "Commuter Rail Zone 1A" } ], - "toTimeFrame" : [ ], "to_area_id" : "area_commuter_rail_zone_1a", + "to_time_frame" : [ ], "to_timeframe_group_id" : null }, { - "fareProducts" : [ { + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fare_products" : [ { "amount" : "20.0", "currency" : "USD", "fare_media_id" : "cash", @@ -265,13 +266,12 @@ "fare_product_id" : "prod_cape_sbb_buzzards_fare", "fare_product_name" : "CapeFLYER Bourne one-way fare" } ], - "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fromArea" : [ { + "from_area" : [ { "area_id" : "area_cf_zone_buzzards", "area_name" : "CapeFLYER - Wareham/Buzzards Bay/Bourne" } ], - "fromTimeFrame" : [ ], "from_area_id" : "area_cf_zone_buzzards", + "from_time_frame" : [ ], "from_timeframe_group_id" : null, "id" : 8, "leg_group_id" : "leg_cape_sbb_buzzards_cash", @@ -279,15 +279,16 @@ "networks" : [ ], "routes" : [ ], "rule_priority" : null, - "toArea" : [ { + "to_area" : [ { "area_id" : "area_commuter_rail_zone_2", "area_name" : "Commuter Rail Zone 2" } ], - "toTimeFrame" : [ ], "to_area_id" : "area_commuter_rail_zone_2", + "to_time_frame" : [ ], "to_timeframe_group_id" : null }, { - "fareProducts" : [ { + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fare_products" : [ { "amount" : "20.0", "currency" : "USD", "fare_media_id" : "cash", @@ -306,13 +307,12 @@ "fare_product_id" : "prod_cape_sbb_buzzards_fare", "fare_product_name" : "CapeFLYER Bourne one-way fare" } ], - "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fromArea" : [ { + "from_area" : [ { "area_id" : "area_cf_zone_buzzards", "area_name" : "CapeFLYER - Wareham/Buzzards Bay/Bourne" } ], - "fromTimeFrame" : [ ], "from_area_id" : "area_cf_zone_buzzards", + "from_time_frame" : [ ], "from_timeframe_group_id" : null, "id" : 9, "leg_group_id" : "leg_cape_sbb_buzzards_cash", @@ -320,15 +320,16 @@ "networks" : [ ], "routes" : [ ], "rule_priority" : null, - "toArea" : [ { + "to_area" : [ { "area_id" : "area_commuter_rail_zone_4", "area_name" : "Commuter Rail Zone 4" } ], - "toTimeFrame" : [ ], "to_area_id" : "area_commuter_rail_zone_4", + "to_time_frame" : [ ], "to_timeframe_group_id" : null }, { - "fareProducts" : [ { + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fare_products" : [ { "amount" : "20.0", "currency" : "USD", "fare_media_id" : "cash", @@ -347,13 +348,12 @@ "fare_product_id" : "prod_cape_sbb_buzzards_fare", "fare_product_name" : "CapeFLYER Bourne one-way fare" } ], - "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fromArea" : [ { + "from_area" : [ { "area_id" : "area_commuter_rail_zone_1a", "area_name" : "Commuter Rail Zone 1A" } ], - "fromTimeFrame" : [ ], "from_area_id" : "area_commuter_rail_zone_1a", + "from_time_frame" : [ ], "from_timeframe_group_id" : null, "id" : 10, "leg_group_id" : "leg_cape_sbb_buzzards_cash", @@ -361,15 +361,16 @@ "networks" : [ ], "routes" : [ ], "rule_priority" : null, - "toArea" : [ { + "to_area" : [ { "area_id" : "area_cf_zone_buzzards", "area_name" : "CapeFLYER - Wareham/Buzzards Bay/Bourne" } ], - "toTimeFrame" : [ ], "to_area_id" : "area_cf_zone_buzzards", + "to_time_frame" : [ ], "to_timeframe_group_id" : null }, { - "fareProducts" : [ { + "fare_product_id" : "prod_cape_sbb_buzzards_fare", + "fare_products" : [ { "amount" : "20.0", "currency" : "USD", "fare_media_id" : "cash", @@ -388,13 +389,12 @@ "fare_product_id" : "prod_cape_sbb_buzzards_fare", "fare_product_name" : "CapeFLYER Bourne one-way fare" } ], - "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fromArea" : [ { + "from_area" : [ { "area_id" : "area_commuter_rail_zone_2", "area_name" : "Commuter Rail Zone 2" } ], - "fromTimeFrame" : [ ], "from_area_id" : "area_commuter_rail_zone_2", + "from_time_frame" : [ ], "from_timeframe_group_id" : null, "id" : 11, "leg_group_id" : "leg_cape_sbb_buzzards_cash", @@ -402,12 +402,12 @@ "networks" : [ ], "routes" : [ ], "rule_priority" : null, - "toArea" : [ { + "to_area" : [ { "area_id" : "area_cf_zone_buzzards", "area_name" : "CapeFLYER - Wareham/Buzzards Bay/Bourne" } ], - "toTimeFrame" : [ ], "to_area_id" : "area_cf_zone_buzzards", + "to_time_frame" : [ ], "to_timeframe_group_id" : null } ], "feed_version" : "1.0" diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareMedias-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareMedias-0.json index 86d7c8d3d..9995d7f81 100644 --- a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareMedias-0.json +++ b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareMedias-0.json @@ -1,7 +1,7 @@ { "data" : { "feed" : { - "fareMedia" : [ { + "fare_media" : [ { "fare_media_id" : "cash", "fare_media_name" : "Cash", "fare_media_type" : "0", diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareProducts-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareProducts-0.json index aeb5b0f9a..c9607e342 100644 --- a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareProducts-0.json +++ b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareProducts-0.json @@ -1,10 +1,10 @@ { "data" : { "feed" : { - "fareProduct" : [ { + "fare_product" : [ { "amount" : "6.5", "currency" : "USD", - "fareMedia" : [ { + "fare_media" : [ { "fare_media_id" : "cash", "fare_media_name" : "Cash", "fare_media_type" : "0", @@ -16,7 +16,7 @@ }, { "amount" : "6.5", "currency" : "USD", - "fareMedia" : [ { + "fare_media" : [ { "fare_media_id" : "credit_debit", "fare_media_name" : "Credit/debit card", "fare_media_type" : "0", @@ -28,7 +28,7 @@ }, { "amount" : "6.5", "currency" : "USD", - "fareMedia" : [ { + "fare_media" : [ { "fare_media_id" : "mticket", "fare_media_name" : "mTicket app", "fare_media_type" : "4", @@ -40,7 +40,7 @@ }, { "amount" : "2.4", "currency" : "USD", - "fareMedia" : [ { + "fare_media" : [ { "fare_media_id" : "cash", "fare_media_name" : "Cash", "fare_media_type" : "0", @@ -52,7 +52,7 @@ }, { "amount" : "2.4", "currency" : "USD", - "fareMedia" : [ { + "fare_media" : [ { "fare_media_id" : "credit_debit", "fare_media_name" : "Credit/debit card", "fare_media_type" : "0", @@ -64,7 +64,7 @@ }, { "amount" : "2.4", "currency" : "USD", - "fareMedia" : [ { + "fare_media" : [ { "fare_media_id" : "mticket", "fare_media_name" : "mTicket app", "fare_media_type" : "4", @@ -76,7 +76,7 @@ }, { "amount" : "7.0", "currency" : "USD", - "fareMedia" : [ { + "fare_media" : [ { "fare_media_id" : "cash", "fare_media_name" : "Cash", "fare_media_type" : "0", @@ -88,7 +88,7 @@ }, { "amount" : "7.0", "currency" : "USD", - "fareMedia" : [ { + "fare_media" : [ { "fare_media_id" : "credit_debit", "fare_media_name" : "Credit/debit card", "fare_media_type" : "0", @@ -100,7 +100,7 @@ }, { "amount" : "7.0", "currency" : "USD", - "fareMedia" : [ { + "fare_media" : [ { "fare_media_id" : "mticket", "fare_media_name" : "mTicket app", "fare_media_type" : "4", @@ -112,7 +112,7 @@ }, { "amount" : "5.0", "currency" : "USD", - "fareMedia" : [ { + "fare_media" : [ { "fare_media_id" : "cash", "fare_media_name" : "Cash", "fare_media_type" : "0", diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareTransferRules-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareTransferRules-0.json index 964d368c6..d7bd4d3ba 100644 --- a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareTransferRules-0.json +++ b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareTransferRules-0.json @@ -1,13 +1,14 @@ { "data" : { "feed" : { - "fareTransferRule" : [ { + "fare_transfer_rule" : [ { "duration_limit" : "7200", "duration_limit_type" : "1", - "fareProducts" : [ { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "fare_products" : [ { "amount" : "2.4", "currency" : "USD", - "fareMedia" : [ { + "fare_media" : [ { "fare_media_id" : "charlieticket", "fare_media_name" : "CharlieTicket", "fare_media_type" : "1", @@ -17,8 +18,7 @@ "fare_product_id" : "prod_rapid_transit_quick_subway", "fare_product_name" : "Subway Quick Ticket" } ], - "fare_product_id" : "prod_rapid_transit_quick_subway", - "fromFareLegRule" : [ { + "from_fare_leg_rule" : [ { "fare_product_id" : "prod_rapid_transit_quick_subway", "from_area_id" : "area_bl_airport", "from_timeframe_group_id" : "timeframe_regular", @@ -30,7 +30,7 @@ "to_timeframe_group_id" : null } ], "from_leg_group_id" : "leg_airport_rapid_transit_quick_subway", - "toFareLegRule" : [ { + "to_fare_leg_rule" : [ { "fare_product_id" : "prod_rapid_transit_quick_subway", "from_area_id" : "area_route_354_downtown", "from_timeframe_group_id" : null, @@ -586,10 +586,11 @@ }, { "duration_limit" : null, "duration_limit_type" : null, - "fareProducts" : [ { + "fare_product_id" : "prod_rapid_transit_quick_subway", + "fare_products" : [ { "amount" : "2.4", "currency" : "USD", - "fareMedia" : [ { + "fare_media" : [ { "fare_media_id" : "charlieticket", "fare_media_name" : "CharlieTicket", "fare_media_type" : "1", @@ -599,8 +600,7 @@ "fare_product_id" : "prod_rapid_transit_quick_subway", "fare_product_name" : "Subway Quick Ticket" } ], - "fare_product_id" : "prod_rapid_transit_quick_subway", - "fromFareLegRule" : [ { + "from_fare_leg_rule" : [ { "fare_product_id" : "prod_rapid_transit_quick_subway", "from_area_id" : "area_bl_airport", "from_timeframe_group_id" : "timeframe_regular", @@ -612,7 +612,7 @@ "to_timeframe_group_id" : null } ], "from_leg_group_id" : "leg_airport_rapid_transit_quick_subway", - "toFareLegRule" : [ { + "to_fare_leg_rule" : [ { "fare_product_id" : "prod_rapid_transit_quick_subway", "from_area_id" : "area_bl", "from_timeframe_group_id" : "timeframe_regular", diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchRouteNetworks-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchRouteNetworks-0.json index 1a234d07e..a444e1bab 100644 --- a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchRouteNetworks-0.json +++ b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchRouteNetworks-0.json @@ -2,7 +2,7 @@ "data" : { "feed" : { "feed_version" : "1.0", - "routeNetwork" : [ { + "route_network" : [ { "network_id" : "1", "route_id" : "1" } ] diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchStopAreas-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchStopAreas-0.json index a9fab4197..1b2454ee1 100644 --- a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchStopAreas-0.json +++ b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchStopAreas-0.json @@ -2,7 +2,7 @@ "data" : { "feed" : { "feed_version" : "1.0", - "stopArea" : [ { + "stop_area" : [ { "area_id" : "area_route_426_downtown", "id" : 2, "stop_id" : "4u6g", diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchTimeFrames-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchTimeFrames-0.json new file mode 100644 index 000000000..ee6ae00bc --- /dev/null +++ b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchTimeFrames-0.json @@ -0,0 +1,50 @@ +{ + "data" : { + "feed" : { + "feed_version" : "1.0", + "time_frame" : [ { + "end_time" : null, + "id" : 2, + "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", + "start_time" : null, + "timeframe_group_id" : "timeframe_regular" + }, { + "end_time" : null, + "id" : 3, + "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", + "start_time" : null, + "timeframe_group_id" : "timeframe_systemwide_free" + }, { + "end_time" : null, + "id" : 4, + "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", + "start_time" : null, + "timeframe_group_id" : "timeframe_sumner_tunnel_closure" + }, { + "end_time" : "9000", + "id" : 5, + "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", + "start_time" : "0", + "timeframe_group_id" : "timeframe_sumner_tunnel_closure" + }, { + "end_time" : "86400", + "id" : 6, + "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", + "start_time" : "9000", + "timeframe_group_id" : "timeframe_regular" + }, { + "end_time" : null, + "id" : 7, + "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", + "start_time" : null, + "timeframe_group_id" : "timeframe_alewife_kendall_surge" + }, { + "end_time" : "9000", + "id" : 8, + "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", + "start_time" : "0", + "timeframe_group_id" : "timeframe_alewife_kendall_surge" + } ] + } + } +} \ No newline at end of file From 7d41e0043ad8efd3f6fcf3a52c4f5e5d6ba9c717 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Fri, 31 Jan 2025 15:34:07 +0000 Subject: [PATCH 09/40] improvement(Added JDBC table writer tests for fares v2 entities): New generic unit tests which uses --- .../java/com/conveyal/gtfs/TestUtils.java | 17 +- .../loader/JDBCTableWriterFaresV2Test.java | 165 ++++++++++++++++++ .../fares-v2-json-entities/areas.json | 5 + .../fares-v2-json-entities/areas_updated.json | 5 + .../fare_leg_rules.json | 12 ++ .../fare_leg_rules_updated.json | 12 ++ .../fares-v2-json-entities/fare_media.json | 6 + .../fare_media_updated.json | 6 + .../fares-v2-json-entities/fare_product.json | 8 + .../fare_product_updated.json | 8 + .../fare_transfer_rules.json | 13 ++ .../fare_transfer_rules_updated.json | 13 ++ .../fares-v2-json-entities/networks.json | 5 + .../networks_updated.json | 5 + .../route_networks.json | 5 + .../route_networks_updated.json | 5 + .../fares-v2-json-entities/stop_areas.json | 5 + .../stop_areas_updated.json | 5 + .../fares-v2-json-entities/time_frames.json | 7 + .../time_frames_updated.json | 7 + 20 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/conveyal/gtfs/loader/JDBCTableWriterFaresV2Test.java create mode 100644 src/test/resources/fares-v2-json-entities/areas.json create mode 100644 src/test/resources/fares-v2-json-entities/areas_updated.json create mode 100644 src/test/resources/fares-v2-json-entities/fare_leg_rules.json create mode 100644 src/test/resources/fares-v2-json-entities/fare_leg_rules_updated.json create mode 100644 src/test/resources/fares-v2-json-entities/fare_media.json create mode 100644 src/test/resources/fares-v2-json-entities/fare_media_updated.json create mode 100644 src/test/resources/fares-v2-json-entities/fare_product.json create mode 100644 src/test/resources/fares-v2-json-entities/fare_product_updated.json create mode 100644 src/test/resources/fares-v2-json-entities/fare_transfer_rules.json create mode 100644 src/test/resources/fares-v2-json-entities/fare_transfer_rules_updated.json create mode 100644 src/test/resources/fares-v2-json-entities/networks.json create mode 100644 src/test/resources/fares-v2-json-entities/networks_updated.json create mode 100644 src/test/resources/fares-v2-json-entities/route_networks.json create mode 100644 src/test/resources/fares-v2-json-entities/route_networks_updated.json create mode 100644 src/test/resources/fares-v2-json-entities/stop_areas.json create mode 100644 src/test/resources/fares-v2-json-entities/stop_areas_updated.json create mode 100644 src/test/resources/fares-v2-json-entities/time_frames.json create mode 100644 src/test/resources/fares-v2-json-entities/time_frames_updated.json diff --git a/src/test/java/com/conveyal/gtfs/TestUtils.java b/src/test/java/com/conveyal/gtfs/TestUtils.java index a378f5492..ff18e59e8 100644 --- a/src/test/java/com/conveyal/gtfs/TestUtils.java +++ b/src/test/java/com/conveyal/gtfs/TestUtils.java @@ -39,6 +39,8 @@ public class TestUtils { private static final String JDBC_URL = "jdbc:postgresql://localhost"; + static final String TEST_RESOURCE_PATH = "src/test/resources/"; + /** * Forcefully drops a database even if other users are connected to it. * @@ -115,7 +117,7 @@ public static String generateNewDB() { } /** - * Helper to return the relative path to a test resource file + * Helper to return the relative path to a test resource file. * * @param fileName * @return @@ -124,6 +126,19 @@ public static String getResourceFileName(String fileName) { return String.format("./src/test/resources/%s", fileName); } + public static String getTestResourceAsString(String resourcePathName) throws IOException { + return getFileContents(TEST_RESOURCE_PATH + resourcePathName); + } + + /** + * Extract the file contents from the provided path and file name. + */ + public static String getFileContents(String pathAndFileName) throws IOException { + FileInputStream fileInputStream = new FileInputStream(pathAndFileName); + return IOUtils.toString(fileInputStream, StandardCharsets.UTF_8); + } + + /** * Zip files in a folder into a temporary zip file */ diff --git a/src/test/java/com/conveyal/gtfs/loader/JDBCTableWriterFaresV2Test.java b/src/test/java/com/conveyal/gtfs/loader/JDBCTableWriterFaresV2Test.java new file mode 100644 index 000000000..1f270680f --- /dev/null +++ b/src/test/java/com/conveyal/gtfs/loader/JDBCTableWriterFaresV2Test.java @@ -0,0 +1,165 @@ +package com.conveyal.gtfs.loader; + +import com.conveyal.gtfs.TestUtils; +import com.conveyal.gtfs.util.InvalidNamespaceException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.sql.DataSource; +import java.io.IOException; +import java.sql.Connection; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.stream.Stream; + +import static com.conveyal.gtfs.GTFS.makeSnapshot; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * This class contains CRUD tests for {@link JdbcTableWriter} (i.e., editing GTFS entities in the RDBMS). Set up + * consists of creating a scratch database and an empty feed snapshot, which is the necessary starting condition + * for building a GTFS feed from scratch. It then runs the various CRUD tests and finishes by dropping the database + * (even if tests fail). + */ +public class JDBCTableWriterFaresV2Test { + + private static final Logger LOG = LoggerFactory.getLogger(JDBCTableWriterFaresV2Test.class); + + private static String testDBName; + private static DataSource testDataSource; + private static String testNamespace; + + private static JdbcTableWriter createTestTableWriter(Table table) throws InvalidNamespaceException { + return new JdbcTableWriter(table, testDataSource, testNamespace); + } + + @BeforeAll + public static void setUpClass() throws SQLException { + testDBName = TestUtils.generateNewDB(); + String dbConnectionUrl = String.format("jdbc:postgresql://localhost/%s", testDBName); + testDataSource = TestUtils.createTestDataSource(dbConnectionUrl); + LOG.info("creating feeds table because it isn't automatically generated unless you import a feed"); + Connection connection = testDataSource.getConnection(); + connection.createStatement().execute(JdbcGtfsLoader.getCreateFeedRegistrySQL()); + connection.commit(); + LOG.info("feeds table created"); + // Create an empty snapshot to create a new namespace and all the tables. + FeedLoadResult result = makeSnapshot(null, testDataSource, false); + testNamespace = result.uniqueIdentifier; + } + + @AfterAll + public static void tearDownClass() { + TestUtils.dropDB(testDBName); + } + + @ParameterizedTest + @MethodSource("createEntityInput") + void canCreateUpdateAndDeleteEntity( + Table table, + String entity, + String entityUpdated + ) throws IOException, SQLException, InvalidNamespaceException { + assertEquals(entity, createTestTableWriter(table).create(entity, true)); + + JdbcTableWriter updateTableWriter = createTestTableWriter(table); + assertEquals(entityUpdated, updateTableWriter.update(1, entityUpdated, true)); + + deleteEntity(table); + } + + /** + * Define JSON payload. ID must be set as 1. + */ + private static Stream createEntityInput() throws IOException { + return Stream.of( + Arguments.of( + Table.FARE_PRODUCTS, + getEntityFromFile("fare_product.json"), + getEntityFromFile("fare_product_updated.json") + ), + Arguments.of( + Table.FARE_MEDIAS, + getEntityFromFile("fare_media.json"), + getEntityFromFile("fare_media_updated.json") + ), + Arguments.of( + Table.FARE_LEG_RULES, + getEntityFromFile("fare_leg_rules.json"), + getEntityFromFile("fare_leg_rules_updated.json") + ), + Arguments.of( + Table.FARE_TRANSFER_RULES, + getEntityFromFile("fare_transfer_rules.json"), + getEntityFromFile("fare_transfer_rules_updated.json") + ), + Arguments.of( + Table.ROUTE_NETWORKS, + getEntityFromFile("route_networks.json"), + getEntityFromFile("route_networks_updated.json") + ), + Arguments.of( + Table.NETWORKS, + getEntityFromFile("networks.json"), + getEntityFromFile("networks_updated.json") + ), + Arguments.of( + Table.AREAS, + getEntityFromFile("areas.json"), + getEntityFromFile("areas_updated.json") + ), + Arguments.of( + Table.STOP_AREAS, + getEntityFromFile("stop_areas.json"), + getEntityFromFile("stop_areas_updated.json") + ), + Arguments.of( + Table.TIME_FRAMES, + getEntityFromFile("time_frames.json"), + getEntityFromFile("time_frames_updated.json") + ) + ); + } + + /** + * Get an entity from file which is expected to be in JSON and remove any formatting. + */ + private static String getEntityFromFile(String fileName) throws IOException { + return TestUtils + .getTestResourceAsString("fares-v2-json-entities/" + fileName) + .replace("\r\n", "") + .replace(" ", "") + .replace("\"\"", "null"); + } + + private static void deleteEntity(Table table) throws InvalidNamespaceException, SQLException { + JdbcTableWriter deleteTableWriter = createTestTableWriter(table); + deleteTableWriter.delete(1, true); + assertThatSqlQueryYieldsRowCount(getColumnsForId(1, table)); + } + + /** + * Constructs SQL query for the specified ID and columns and returns the resulting result set. + */ + private static String getColumnsForId(int id, Table table, String... columns) { + return String.format( + "select %s from %s.%s where id=%d", + columns.length > 0 ? String.join(", ", columns) : "*", + testNamespace, + table.name, + id + ); + } + + private static void assertThatSqlQueryYieldsRowCount(String sql) throws SQLException { + int recordCount = 0; + ResultSet rs = testDataSource.getConnection().prepareStatement(sql).executeQuery(); + while (rs.next()) recordCount++; + assertEquals(0, recordCount, "Records matching query should equal expected count."); + } +} diff --git a/src/test/resources/fares-v2-json-entities/areas.json b/src/test/resources/fares-v2-json-entities/areas.json new file mode 100644 index 000000000..07fdcbac5 --- /dev/null +++ b/src/test/resources/fares-v2-json-entities/areas.json @@ -0,0 +1,5 @@ +{ + "id": 1, + "area_id": "area_bl", + "area_name": "Blue Line" +} \ No newline at end of file diff --git a/src/test/resources/fares-v2-json-entities/areas_updated.json b/src/test/resources/fares-v2-json-entities/areas_updated.json new file mode 100644 index 000000000..9d0c38432 --- /dev/null +++ b/src/test/resources/fares-v2-json-entities/areas_updated.json @@ -0,0 +1,5 @@ +{ + "id": 1, + "area_id": "area_b2", + "area_name": "Blue Line updated" +} \ No newline at end of file diff --git a/src/test/resources/fares-v2-json-entities/fare_leg_rules.json b/src/test/resources/fares-v2-json-entities/fare_leg_rules.json new file mode 100644 index 000000000..ce0d2ff7a --- /dev/null +++ b/src/test/resources/fares-v2-json-entities/fare_leg_rules.json @@ -0,0 +1,12 @@ +{ + "id": 1, + "leg_group_id": "leg_airport_rapid_transit_quick_subway", + "network_id": "rapid_transit", + "from_area_id": "area_bl_airport", + "to_area_id": "", + "fare_product_id": "prod_rapid_transit_quick_subway", + "from_timeframe_group_id": "timeframe_regular", + "to_timeframe_group_id": "", + "transfer_only": "", + "rule_priority": "" +} \ No newline at end of file diff --git a/src/test/resources/fares-v2-json-entities/fare_leg_rules_updated.json b/src/test/resources/fares-v2-json-entities/fare_leg_rules_updated.json new file mode 100644 index 000000000..16b3c3d36 --- /dev/null +++ b/src/test/resources/fares-v2-json-entities/fare_leg_rules_updated.json @@ -0,0 +1,12 @@ +{ + "id": 1, + "leg_group_id": "leg_airport_rapid_transit_quick_subway updated", + "network_id": "rapid_transit updated", + "from_area_id": "area_bl_airport updated", + "to_area_id": "", + "fare_product_id": "prod_rapid_transit_quick_subway updated", + "from_timeframe_group_id": "timeframe_regular updated", + "to_timeframe_group_id": "", + "transfer_only": "", + "rule_priority": "" +} \ No newline at end of file diff --git a/src/test/resources/fares-v2-json-entities/fare_media.json b/src/test/resources/fares-v2-json-entities/fare_media.json new file mode 100644 index 000000000..701b05698 --- /dev/null +++ b/src/test/resources/fares-v2-json-entities/fare_media.json @@ -0,0 +1,6 @@ +{ + "id": 1, + "fare_media_id": "cash", + "fare_media_name": "Cash", + "fare_media_type": "0" +} \ No newline at end of file diff --git a/src/test/resources/fares-v2-json-entities/fare_media_updated.json b/src/test/resources/fares-v2-json-entities/fare_media_updated.json new file mode 100644 index 000000000..bac11d0f7 --- /dev/null +++ b/src/test/resources/fares-v2-json-entities/fare_media_updated.json @@ -0,0 +1,6 @@ +{ + "id": 1, + "fare_media_id": "cash updated", + "fare_media_name": "Cash updated", + "fare_media_type": "1" +} \ No newline at end of file diff --git a/src/test/resources/fares-v2-json-entities/fare_product.json b/src/test/resources/fares-v2-json-entities/fare_product.json new file mode 100644 index 000000000..60fb64c8b --- /dev/null +++ b/src/test/resources/fares-v2-json-entities/fare_product.json @@ -0,0 +1,8 @@ +{ + "id": 1, + "fare_product_id": "AERIAL_TRAM_ROUND_TRIP", + "fare_product_name": "Portland Aerial Tram Single Round Trip", + "fare_media_id": "1", + "amount": "13.5", + "currency": "USD" +} \ No newline at end of file diff --git a/src/test/resources/fares-v2-json-entities/fare_product_updated.json b/src/test/resources/fares-v2-json-entities/fare_product_updated.json new file mode 100644 index 000000000..19510f60b --- /dev/null +++ b/src/test/resources/fares-v2-json-entities/fare_product_updated.json @@ -0,0 +1,8 @@ +{ + "id": 1, + "fare_product_id": "AERIAL_TRAM_ROUND_TRIP_UPDATED", + "fare_product_name": "Portland Aerial Tram Single Round Trip", + "fare_media_id": "1", + "amount": "14.5", + "currency": "USD" +} \ No newline at end of file diff --git a/src/test/resources/fares-v2-json-entities/fare_transfer_rules.json b/src/test/resources/fares-v2-json-entities/fare_transfer_rules.json new file mode 100644 index 000000000..fb4a94f1c --- /dev/null +++ b/src/test/resources/fares-v2-json-entities/fare_transfer_rules.json @@ -0,0 +1,13 @@ +{ + "id": 1, + "from_leg_group_id": "leg_airport_rapid_transit_quick_subway", + "to_leg_group_id": "leg_local_bus_quick_subway", + "transfer_count": "", + "duration_limit": "7200", + "duration_limit_type": "1", + "fare_transfer_type": "0", + "fare_product_id": "prod_rapid_transit_quick_subway", + "filter_fare_product_id": "prod_rapid_transit_quick_subway", + "fare_media_behavior": "0", + "fare_product_behavior": "1" +} \ No newline at end of file diff --git a/src/test/resources/fares-v2-json-entities/fare_transfer_rules_updated.json b/src/test/resources/fares-v2-json-entities/fare_transfer_rules_updated.json new file mode 100644 index 000000000..5fcf6fc09 --- /dev/null +++ b/src/test/resources/fares-v2-json-entities/fare_transfer_rules_updated.json @@ -0,0 +1,13 @@ +{ + "id": 1, + "from_leg_group_id": "leg_airport_rapid_transit_quick_subway updated", + "to_leg_group_id": "leg_local_bus_quick_subway updated", + "transfer_count": "", + "duration_limit": "7300", + "duration_limit_type": "1", + "fare_transfer_type": "0", + "fare_product_id": "prod_rapid_transit_quick_subway updated", + "filter_fare_product_id": "prod_rapid_transit_quick_subway updated", + "fare_media_behavior": "0", + "fare_product_behavior": "1" +} \ No newline at end of file diff --git a/src/test/resources/fares-v2-json-entities/networks.json b/src/test/resources/fares-v2-json-entities/networks.json new file mode 100644 index 000000000..a43f227a1 --- /dev/null +++ b/src/test/resources/fares-v2-json-entities/networks.json @@ -0,0 +1,5 @@ +{ + "id": 1, + "network_id": 1, + "network_name": "This is the network name" +} \ No newline at end of file diff --git a/src/test/resources/fares-v2-json-entities/networks_updated.json b/src/test/resources/fares-v2-json-entities/networks_updated.json new file mode 100644 index 000000000..fcef0ec98 --- /dev/null +++ b/src/test/resources/fares-v2-json-entities/networks_updated.json @@ -0,0 +1,5 @@ +{ + "id": 1, + "network_id": 1, + "network_name": "This is the network name updated" +} \ No newline at end of file diff --git a/src/test/resources/fares-v2-json-entities/route_networks.json b/src/test/resources/fares-v2-json-entities/route_networks.json new file mode 100644 index 000000000..d39e03a67 --- /dev/null +++ b/src/test/resources/fares-v2-json-entities/route_networks.json @@ -0,0 +1,5 @@ +{ + "id": 1, + "network_id": 1, + "route_id": 1 +} \ No newline at end of file diff --git a/src/test/resources/fares-v2-json-entities/route_networks_updated.json b/src/test/resources/fares-v2-json-entities/route_networks_updated.json new file mode 100644 index 000000000..eb884d24d --- /dev/null +++ b/src/test/resources/fares-v2-json-entities/route_networks_updated.json @@ -0,0 +1,5 @@ +{ + "id": 1, + "network_id": "2", + "route_id": "2" +} \ No newline at end of file diff --git a/src/test/resources/fares-v2-json-entities/stop_areas.json b/src/test/resources/fares-v2-json-entities/stop_areas.json new file mode 100644 index 000000000..c34f7538a --- /dev/null +++ b/src/test/resources/fares-v2-json-entities/stop_areas.json @@ -0,0 +1,5 @@ +{ + "id": 1, + "stop_id": "4u6g", + "area_id": "area_route_426_downtown" +} \ No newline at end of file diff --git a/src/test/resources/fares-v2-json-entities/stop_areas_updated.json b/src/test/resources/fares-v2-json-entities/stop_areas_updated.json new file mode 100644 index 000000000..f84eb6955 --- /dev/null +++ b/src/test/resources/fares-v2-json-entities/stop_areas_updated.json @@ -0,0 +1,5 @@ +{ + "id": 1, + "stop_id": "4u6g updated", + "area_id": "area_route_426_downtown updated" +} \ No newline at end of file diff --git a/src/test/resources/fares-v2-json-entities/time_frames.json b/src/test/resources/fares-v2-json-entities/time_frames.json new file mode 100644 index 000000000..157c1e2c5 --- /dev/null +++ b/src/test/resources/fares-v2-json-entities/time_frames.json @@ -0,0 +1,7 @@ +{ + "id": 1, + "timeframe_group_id": "timeframe_sumner_tunnel_closure", + "start_time": "00:00:00", + "end_time": "02:30:00", + "service_id": "04100312-8fe1-46a5-a9f2-556f39478f57" +} \ No newline at end of file diff --git a/src/test/resources/fares-v2-json-entities/time_frames_updated.json b/src/test/resources/fares-v2-json-entities/time_frames_updated.json new file mode 100644 index 000000000..3ab456b36 --- /dev/null +++ b/src/test/resources/fares-v2-json-entities/time_frames_updated.json @@ -0,0 +1,7 @@ +{ + "id": 1, + "timeframe_group_id": "timeframe_sumner_tunnel_closure_updated", + "start_time": "01:30:00", + "end_time": "03:45:00", + "service_id": "04100312-8fe1-46a5-a9f2-556f39478f57updated" +} \ No newline at end of file From 62a7ed2efba69abfdf9bd08db03f3fe029e2b0a8 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Fri, 31 Jan 2025 15:45:30 +0000 Subject: [PATCH 10/40] fix(JDBCTableWriterFaresV2Test.java): Replaced Windows new line separator with OS derived reference Test should now work on Linux --- .../com/conveyal/gtfs/loader/JDBCTableWriterFaresV2Test.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/conveyal/gtfs/loader/JDBCTableWriterFaresV2Test.java b/src/test/java/com/conveyal/gtfs/loader/JDBCTableWriterFaresV2Test.java index 1f270680f..80d328be9 100644 --- a/src/test/java/com/conveyal/gtfs/loader/JDBCTableWriterFaresV2Test.java +++ b/src/test/java/com/conveyal/gtfs/loader/JDBCTableWriterFaresV2Test.java @@ -132,7 +132,7 @@ private static Stream createEntityInput() throws IOException { private static String getEntityFromFile(String fileName) throws IOException { return TestUtils .getTestResourceAsString("fares-v2-json-entities/" + fileName) - .replace("\r\n", "") + .replace(System.lineSeparator(), "") .replace(" ", "") .replace("\"\"", "null"); } From a2db1a7c664539a4bd32b438d5d886e45cd0b2d0 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Wed, 5 Feb 2025 10:15:22 +0000 Subject: [PATCH 11/40] improvement(GTFSFaresV2Test): Removed superceded JDBC table writer tests --- .../com/conveyal/gtfs/GTFSFaresV2Test.java | 451 +----------------- 1 file changed, 1 insertion(+), 450 deletions(-) diff --git a/src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java b/src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java index d24829ae7..225e09c58 100644 --- a/src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java +++ b/src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java @@ -1,65 +1,27 @@ package com.conveyal.gtfs; -import com.conveyal.gtfs.dto.AreaDTO; -import com.conveyal.gtfs.dto.FareLegRuleDTO; -import com.conveyal.gtfs.dto.FareMediaDTO; -import com.conveyal.gtfs.dto.FareProductDTO; -import com.conveyal.gtfs.dto.FareTransferRuleDTO; -import com.conveyal.gtfs.dto.NetworkDTO; -import com.conveyal.gtfs.dto.RouteNetworkDTO; -import com.conveyal.gtfs.dto.StopAreaDTO; -import com.conveyal.gtfs.dto.TimeFrameDTO; import com.conveyal.gtfs.loader.FeedLoadResult; -import com.conveyal.gtfs.loader.JdbcTableWriter; -import com.conveyal.gtfs.loader.Table; -import com.conveyal.gtfs.model.Area; -import com.conveyal.gtfs.model.FareLegRule; -import com.conveyal.gtfs.model.FareMedia; -import com.conveyal.gtfs.model.FareProduct; -import com.conveyal.gtfs.model.FareTransferRule; -import com.conveyal.gtfs.model.Network; -import com.conveyal.gtfs.model.RouteNetwork; -import com.conveyal.gtfs.model.StopArea; -import com.conveyal.gtfs.model.TimeFrame; -import com.conveyal.gtfs.util.InvalidNamespaceException; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import javax.sql.DataSource; import java.io.File; import java.io.IOException; -import java.sql.ResultSet; -import java.sql.SQLException; import java.util.zip.ZipFile; import static com.conveyal.gtfs.GTFS.load; import static com.conveyal.gtfs.GTFS.makeSnapshot; import static com.conveyal.gtfs.GTFS.validate; -import static com.conveyal.gtfs.TestUtils.assertResultValue; -import static com.conveyal.gtfs.TestUtils.assertThatSqlQueryYieldsZeroRows; import static com.conveyal.gtfs.TestUtils.checkFileTestCases; -import static com.conveyal.gtfs.TestUtils.getColumnsForId; -import static com.conveyal.gtfs.TestUtils.getResultSetForId; -import static org.hamcrest.Matchers.equalTo; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; public class GTFSFaresV2Test { - private static final Logger LOG = LoggerFactory.getLogger(GTFSFaresV2Test.class); private static String faresZipFileName; public static String faresDBName; private static DataSource faresDataSource; private static String faresNamespace; - private static final ObjectMapper mapper = new ObjectMapper(); - - private static JdbcTableWriter createTestTableWriter (Table table) throws InvalidNamespaceException { - return new JdbcTableWriter(table, faresDataSource, faresNamespace); - } @BeforeAll public static void setUpClass() throws IOException { @@ -182,415 +144,4 @@ void canDoRoundTripLoadAndWriteToZipFile() throws IOException { }; checkFileTestCases(zip, fileTestCases); } - - @Test - void canCreateUpdateAndDeleteAreas() throws IOException, SQLException, InvalidNamespaceException { - // Create. - String areaId = "area-id-1"; - AreaDTO createdArea = createArea(areaId); - assertEquals(createdArea.area_id, areaId); - - // Update. - String updatedAreaId = "area-id-2"; - createdArea.area_id = updatedAreaId; - JdbcTableWriter updateTableWriter = createTestTableWriter(Table.AREAS); - String updateOutput = updateTableWriter.update( - createdArea.id, - mapper.writeValueAsString(createdArea), - true - ); - AreaDTO updatedAreaDTO = mapper.readValue(updateOutput, AreaDTO.class); - assertEquals(updatedAreaDTO.area_id, updatedAreaId); - - ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, updatedAreaDTO.id, Table.AREAS); - while (resultSet.next()) { - assertResultValue(resultSet, Area.AREA_ID_NAME, equalTo(createdArea.area_id)); - assertResultValue(resultSet, Area.AREA_NAME_NAME,equalTo(createdArea.area_name)); - } - - // Delete. - deleteRecord(Table.AREAS, createdArea.id); - } - - @Test - void canCreateUpdateAndDeleteStopAreas() throws IOException, SQLException, InvalidNamespaceException { - // Create. - String areaId = "area-id-1"; - StopAreaDTO createdStopArea = createStopArea(areaId); - assertEquals(createdStopArea.area_id, areaId); - - // Update. - String updatedAreaId = "area-id-2"; - createdStopArea.area_id = updatedAreaId; - JdbcTableWriter updateTableWriter = createTestTableWriter(Table.STOP_AREAS); - String updateOutput = updateTableWriter.update( - createdStopArea.id, - mapper.writeValueAsString(createdStopArea), - true - ); - StopAreaDTO updatedStopAreaDTO = mapper.readValue(updateOutput, StopAreaDTO.class); - assertEquals(updatedStopAreaDTO.area_id, updatedAreaId); - - ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, updatedStopAreaDTO.id, Table.STOP_AREAS); - while (resultSet.next()) { - assertResultValue(resultSet, StopArea.AREA_ID_NAME, equalTo(createdStopArea.area_id)); - assertResultValue(resultSet, StopArea.STOP_ID_NAME,equalTo(createdStopArea.stop_id)); - } - - // Delete. - deleteRecord(Table.STOP_AREAS, createdStopArea.id); - } - - @Test - void canCreateUpdateAndDeleteTimeFrames() throws IOException, SQLException, InvalidNamespaceException { - // Create. - String timeFrameGroupId = "time-frame-group-id-1"; - TimeFrameDTO createdTimeFrame = createTimeFrame(timeFrameGroupId); - assertEquals(createdTimeFrame.timeframe_group_id, timeFrameGroupId); - - // Update. - String updatedTimeFrameGroupId = "time-frame-group-id-2"; - createdTimeFrame.timeframe_group_id = updatedTimeFrameGroupId; - JdbcTableWriter updateTableWriter = createTestTableWriter(Table.TIME_FRAMES); - String updateOutput = updateTableWriter.update( - createdTimeFrame.id, - mapper.writeValueAsString(createdTimeFrame), - true - ); - TimeFrameDTO updatedTimeFrameDTO = mapper.readValue(updateOutput, TimeFrameDTO.class); - assertEquals(updatedTimeFrameDTO.timeframe_group_id, updatedTimeFrameGroupId); - - ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, updatedTimeFrameDTO.id, Table.TIME_FRAMES); - while (resultSet.next()) { - assertResultValue(resultSet, TimeFrame.TIME_FRAME_GROUP_ID_NAME, equalTo(createdTimeFrame.timeframe_group_id)); - assertResultValue(resultSet, TimeFrame.START_TIME_NAME, equalTo(createdTimeFrame.start_time)); - assertResultValue(resultSet, TimeFrame.END_TIME_NAME, equalTo(createdTimeFrame.end_time)); - assertResultValue(resultSet, TimeFrame.SERVICE_ID_NAME, equalTo(createdTimeFrame.service_id)); - } - - // Delete. - deleteRecord(Table.TIME_FRAMES, createdTimeFrame.id); - } - - @Test - void canCreateUpdateAndDeleteNetworks() throws IOException, SQLException, InvalidNamespaceException { - // Create. - String networkId = "network-id-1"; - NetworkDTO createdNetwork = createNetwork(networkId); - assertEquals(createdNetwork.network_id, networkId); - - // Update. - String updatedNetworkId = "network-id-2"; - createdNetwork.network_id = updatedNetworkId; - JdbcTableWriter updateTableWriter = createTestTableWriter(Table.NETWORKS); - String updateOutput = updateTableWriter.update( - createdNetwork.id, - mapper.writeValueAsString(createdNetwork), - true - ); - NetworkDTO updatedNetworkDTO = mapper.readValue(updateOutput, NetworkDTO.class); - assertEquals(updatedNetworkDTO.network_id, updatedNetworkId); - - ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, updatedNetworkDTO.id, Table.NETWORKS); - while (resultSet.next()) { - assertResultValue(resultSet, Network.NETWORK_ID_NAME, equalTo(createdNetwork.network_id)); - assertResultValue(resultSet, Network.NETWORK_NAME_NAME, equalTo(createdNetwork.network_name)); - } - - // Delete. - deleteRecord(Table.NETWORKS, createdNetwork.id); - } - - @Test - void canCreateUpdateAndDeleteRouteNetworks() throws IOException, SQLException, InvalidNamespaceException { - // Create. - String networkId = "network-id-1"; - RouteNetworkDTO createdRouteNetwork = createRouteNetwork(networkId); - assertEquals(createdRouteNetwork.network_id, networkId); - - // Update. - String updatedRouteNetworkId = "network-id-2"; - createdRouteNetwork.network_id = updatedRouteNetworkId; - JdbcTableWriter updateTableWriter = createTestTableWriter(Table.ROUTE_NETWORKS); - String updateOutput = updateTableWriter.update( - createdRouteNetwork.id, - mapper.writeValueAsString(createdRouteNetwork), - true - ); - RouteNetworkDTO updatedNetworkDTO = mapper.readValue(updateOutput, RouteNetworkDTO.class); - assertEquals(updatedNetworkDTO.network_id, updatedRouteNetworkId); - - ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, updatedNetworkDTO.id, Table.ROUTE_NETWORKS); - while (resultSet.next()) { - assertResultValue(resultSet, RouteNetwork.NETWORK_ID_NAME, equalTo(createdRouteNetwork.network_id)); - assertResultValue(resultSet, RouteNetwork.ROUTE_ID_NAME, equalTo(createdRouteNetwork.route_id)); - } - - // Delete. - deleteRecord(Table.ROUTE_NETWORKS, createdRouteNetwork.id); - } - - @Test - void canCreateUpdateAndDeleteFareLegRules() throws IOException, SQLException, InvalidNamespaceException { - // Create. - String legGroupId = "leg-group-id-1"; - FareLegRuleDTO createdFareLegRule = createFareLegRule(legGroupId); - assertEquals(createdFareLegRule.leg_group_id, legGroupId); - - // Update. - String updatedLegGroupId = "leg-group-id-2"; - createdFareLegRule.leg_group_id = updatedLegGroupId; - JdbcTableWriter updateTableWriter = createTestTableWriter(Table.FARE_LEG_RULES); - String updateOutput = updateTableWriter.update( - createdFareLegRule.id, - mapper.writeValueAsString(createdFareLegRule), - true - ); - FareLegRuleDTO updatedFareLegRuleDTO = mapper.readValue(updateOutput, FareLegRuleDTO.class); - assertEquals(updatedFareLegRuleDTO.leg_group_id, updatedLegGroupId); - - ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, updatedFareLegRuleDTO.id, Table.FARE_LEG_RULES); - while (resultSet.next()) { - assertResultValue(resultSet, FareLegRule.LEG_GROUP_ID_NAME, equalTo(createdFareLegRule.leg_group_id)); - assertResultValue(resultSet, FareLegRule.NETWORK_ID_NAME, equalTo(createdFareLegRule.network_id)); - assertResultValue(resultSet, FareLegRule.FROM_AREA_ID_NAME, equalTo(createdFareLegRule.from_area_id)); - assertResultValue(resultSet, FareLegRule.TO_AREA_ID_NAME, equalTo(createdFareLegRule.to_area_id)); - assertResultValue(resultSet, FareLegRule.FROM_TIMEFRAME_GROUP_ID_NAME, equalTo(createdFareLegRule.from_timeframe_group_id)); - assertResultValue(resultSet, FareLegRule.FARE_PRODUCT_ID_NAME, equalTo(createdFareLegRule.fare_product_id)); - assertResultValue(resultSet, FareLegRule.RULE_PRIORITY_NAME, equalTo(createdFareLegRule.rule_priority)); - } - - // Delete. - deleteRecord(Table.FARE_LEG_RULES, createdFareLegRule.id); - } - - @Test - void canCreateUpdateAndDeleteFareMedia() throws IOException, SQLException, InvalidNamespaceException { - // Create. - String fareMediaId = "fare-media-id-1"; - FareMediaDTO createdFareMedia = createFareMedia(fareMediaId); - assertEquals(createdFareMedia.fare_media_id, fareMediaId); - - // Update. - String updatedFareMediaId = "fare-media-id-2"; - createdFareMedia.fare_media_id = updatedFareMediaId; - JdbcTableWriter updateTableWriter = createTestTableWriter(Table.FARE_MEDIAS); - String updateOutput = updateTableWriter.update( - createdFareMedia.id, - mapper.writeValueAsString(createdFareMedia), - true - ); - FareMediaDTO updatedFareMediaDTO = mapper.readValue(updateOutput, FareMediaDTO.class); - assertEquals(updatedFareMediaDTO.fare_media_id, updatedFareMediaId); - - ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, updatedFareMediaDTO.id, Table.FARE_MEDIAS); - while (resultSet.next()) { - assertResultValue(resultSet, FareMedia.FARE_MEDIA_ID_NAME, equalTo(createdFareMedia.fare_media_id)); - assertResultValue(resultSet, FareMedia.FARE_MEDIA_NAME_NAME, equalTo(createdFareMedia.fare_media_name)); - assertResultValue(resultSet, FareMedia.FARE_MEDIA_TYPE_NAME, equalTo(createdFareMedia.fare_media_type)); - } - - // Delete. - deleteRecord(Table.FARE_MEDIAS, createdFareMedia.id); - } - - @Test - void canCreateUpdateAndDeleteFareProduct() throws IOException, SQLException, InvalidNamespaceException { - // Create. - String fareProductId = "fare-product-id-1"; - FareProductDTO createdFareProduct = createFareProduct(fareProductId); - assertEquals(createdFareProduct.fare_product_id, fareProductId); - - // Update. - String updatedFareProductId = "fare-product-id-2"; - createdFareProduct.fare_product_id = updatedFareProductId; - JdbcTableWriter updateTableWriter = createTestTableWriter(Table.FARE_PRODUCTS); - String updateOutput = updateTableWriter.update( - createdFareProduct.id, - mapper.writeValueAsString(createdFareProduct), - true - ); - FareProductDTO fareProductDTO = mapper.readValue(updateOutput, FareProductDTO.class); - assertEquals(fareProductDTO.fare_product_id, updatedFareProductId); - - ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, fareProductDTO.id, Table.FARE_PRODUCTS); - while (resultSet.next()) { - assertResultValue(resultSet, FareProduct.FARE_PRODUCT_ID_NAME, equalTo(createdFareProduct.fare_product_id)); - assertResultValue(resultSet, FareProduct.FARE_PRODUCT_NAME_NAME, equalTo(createdFareProduct.fare_product_name)); - assertResultValue(resultSet, FareProduct.FARE_MEDIA_ID_NAME, equalTo(createdFareProduct.fare_media_id)); - } - - // Delete. - deleteRecord(Table.FARE_PRODUCTS, createdFareProduct.id); - } - - @Test - void canCreateUpdateAndDeleteFareTransferRule() throws IOException, SQLException, InvalidNamespaceException { - // Create. - String fromLegGroupId = "from-leg-group-id-1"; - String toLegGroupId = "to-leg-group-id-1"; - FareTransferRuleDTO fareTransferRules = createFareTransferRules(fromLegGroupId, toLegGroupId); - assertEquals(fareTransferRules.from_leg_group_id, fromLegGroupId); - assertEquals(fareTransferRules.to_leg_group_id, toLegGroupId); - - // Update. - String updatedFromLegGroupId = "from-leg-group-id-2"; - String updatedToLegGroupId = "to-leg-group-id-2"; - fareTransferRules.from_leg_group_id = updatedFromLegGroupId; - fareTransferRules.to_leg_group_id = updatedToLegGroupId; - JdbcTableWriter updateTableWriter = createTestTableWriter(Table.FARE_TRANSFER_RULES); - String updateOutput = updateTableWriter.update( - fareTransferRules.id, - mapper.writeValueAsString(fareTransferRules), - true - ); - FareTransferRuleDTO fareTransferRuleDTO = mapper.readValue(updateOutput, FareTransferRuleDTO.class); - assertEquals(fareTransferRuleDTO.from_leg_group_id, updatedFromLegGroupId); - assertEquals(fareTransferRuleDTO.to_leg_group_id, updatedToLegGroupId); - - ResultSet resultSet = getResultSetForId(faresDataSource, faresNamespace, fareTransferRuleDTO.id, Table.FARE_TRANSFER_RULES); - while (resultSet.next()) { - assertResultValue(resultSet, FareTransferRule.FROM_LEG_GROUP_ID_NAME, equalTo(fareTransferRules.from_leg_group_id)); - assertResultValue(resultSet, FareTransferRule.TO_LEG_GROUP_ID_NAME, equalTo(fareTransferRules.to_leg_group_id)); - assertResultValue(resultSet, FareTransferRule.TRANSFER_COUNT_NAME, equalTo(fareTransferRules.transfer_count)); - assertResultValue(resultSet, FareTransferRule.DURATION_LIMIT_NAME, equalTo(fareTransferRules.duration_limit)); - assertResultValue(resultSet, FareTransferRule.FARE_TRANSFER_TYPE_NAME, equalTo(fareTransferRules.fare_transfer_type)); - assertResultValue(resultSet, FareTransferRule.FARE_PRODUCT_ID_NAME, equalTo(fareTransferRules.fare_product_id)); - } - - // Delete. - deleteRecord(Table.FARE_TRANSFER_RULES, fareTransferRules.id); - } - - private void deleteRecord(Table table, Integer id) throws InvalidNamespaceException, SQLException { - JdbcTableWriter deleteTableWriter = createTestTableWriter(table); - int deleteOutput = deleteTableWriter.delete(id, true); - LOG.info("deleted {} records from {}", deleteOutput, table.name); - assertThatSqlQueryYieldsZeroRows(faresDataSource, getColumnsForId(faresNamespace, id, table)); - } - - /** - * Create and store an area for testing. - */ - private static AreaDTO createArea(String areaId) throws InvalidNamespaceException, IOException, SQLException { - AreaDTO input = new AreaDTO(); - input.area_id = areaId; - input.area_name = "area-name"; - JdbcTableWriter createTableWriter = createTestTableWriter(Table.AREAS); - String output = createTableWriter.create(mapper.writeValueAsString(input), true); - return mapper.readValue(output, AreaDTO.class); - } - - /** - * Create and store a stop area for testing. - */ - private static StopAreaDTO createStopArea(String areaId) throws InvalidNamespaceException, IOException, SQLException { - StopAreaDTO input = new StopAreaDTO(); - input.area_id = areaId; - input.stop_id = "stop-id-1"; - JdbcTableWriter createTableWriter = createTestTableWriter(Table.STOP_AREAS); - String output = createTableWriter.create(mapper.writeValueAsString(input), true); - return mapper.readValue(output, StopAreaDTO.class); - } - - /** - * Create and store a time frame for testing. - */ - private static TimeFrameDTO createTimeFrame(String timeframeGroupId) throws InvalidNamespaceException, IOException, SQLException { - TimeFrameDTO input = new TimeFrameDTO(); - input.timeframe_group_id = timeframeGroupId; - input.start_time = 0; - input.end_time = 2600; - input.service_id = "service-id-1"; - JdbcTableWriter createTableWriter = createTestTableWriter(Table.TIME_FRAMES); - String output = createTableWriter.create(mapper.writeValueAsString(input), true); - return mapper.readValue(output, TimeFrameDTO.class); - } - - /** - * Create and store a network for testing. - */ - private static NetworkDTO createNetwork(String networkId) throws InvalidNamespaceException, IOException, SQLException { - NetworkDTO input = new NetworkDTO(); - input.network_id = networkId; - input.network_name = "network-name-1"; - JdbcTableWriter createTableWriter = createTestTableWriter(Table.NETWORKS); - String output = createTableWriter.create(mapper.writeValueAsString(input), true); - return mapper.readValue(output, NetworkDTO.class); - } - - /** - * Create and store a route network for testing. - */ - private static RouteNetworkDTO createRouteNetwork(String networkId) throws InvalidNamespaceException, IOException, SQLException { - RouteNetworkDTO input = new RouteNetworkDTO(); - input.network_id = networkId; - input.route_id = "route-id-1"; - JdbcTableWriter createTableWriter = createTestTableWriter(Table.ROUTE_NETWORKS); - String output = createTableWriter.create(mapper.writeValueAsString(input), true); - return mapper.readValue(output, RouteNetworkDTO.class); - } - - /** - * Create and store a fare leg rule for testing. - */ - private static FareLegRuleDTO createFareLegRule(String legGroupId) throws InvalidNamespaceException, IOException, SQLException { - FareLegRuleDTO input = new FareLegRuleDTO(); - input.leg_group_id = legGroupId; - input.network_id = "network-id-1"; - input.from_area_id = "from-area-id-1"; - input.to_area_id = "to-area-id-1"; - input.from_timeframe_group_id = "from-timeframe-group-id-1"; - input.to_timeframe_group_id = "to-timeframe-group-id-1"; - input.fare_product_id = "fare-product-id-1"; - input.rule_priority = 1; - JdbcTableWriter createTableWriter = createTestTableWriter(Table.FARE_LEG_RULES); - String output = createTableWriter.create(mapper.writeValueAsString(input), true); - return mapper.readValue(output, FareLegRuleDTO.class); - } - - /** - * Create and store a fare media for testing. - */ - private static FareMediaDTO createFareMedia(String fareMediaId) throws InvalidNamespaceException, IOException, SQLException { - FareMediaDTO input = new FareMediaDTO(); - input.fare_media_id = fareMediaId; - input.fare_media_name = "fare-media-name"; - input.fare_media_type = 1; - JdbcTableWriter createTableWriter = createTestTableWriter(Table.FARE_MEDIAS); - String output = createTableWriter.create(mapper.writeValueAsString(input), true); - return mapper.readValue(output, FareMediaDTO.class); - } - - /** - * Create and store a fare product for testing. - */ - private static FareProductDTO createFareProduct(String fareProductId) throws InvalidNamespaceException, IOException, SQLException { - FareProductDTO input = new FareProductDTO(); - input.fare_product_id = fareProductId; - input.fare_product_name = "fare-product-name"; - input.fare_media_id = "fare-media-id"; - input.amount = 6.25; - input.currency = "USD"; - JdbcTableWriter createTableWriter = createTestTableWriter(Table.FARE_PRODUCTS); - String output = createTableWriter.create(mapper.writeValueAsString(input), true); - return mapper.readValue(output, FareProductDTO.class); - } - - /** - * Create and store a fare transfer rules for testing. - */ - private static FareTransferRuleDTO createFareTransferRules(String fromLegGroupId, String toLegGroupId) throws InvalidNamespaceException, IOException, SQLException { - FareTransferRuleDTO input = new FareTransferRuleDTO(); - input.from_leg_group_id = fromLegGroupId; - input.to_leg_group_id = toLegGroupId; - input.transfer_count = -1; - input.duration_limit = 1; - input.duration_limit_type = 1; - input.fare_transfer_type = 2; - input.fare_product_id = "fare-product-id-1"; - JdbcTableWriter createTableWriter = createTestTableWriter(Table.FARE_TRANSFER_RULES); - String output = createTableWriter.create(mapper.writeValueAsString(input), true); - return mapper.readValue(output, FareTransferRuleDTO.class); - } -} +} \ No newline at end of file From e96e8342699d06cc3219815c652f256980271c86 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Wed, 5 Feb 2025 10:45:41 +0000 Subject: [PATCH 12/40] improvement(Removed the id param from the create entities to closer match what will be provided): --- .../gtfs/loader/JDBCTableWriterFaresV2Test.java | 12 ++++++++++-- src/test/resources/fares-v2-json-entities/areas.json | 1 - .../fares-v2-json-entities/fare_leg_rules.json | 1 - .../resources/fares-v2-json-entities/fare_media.json | 1 - .../fares-v2-json-entities/fare_product.json | 1 - .../fares-v2-json-entities/fare_transfer_rules.json | 1 - .../resources/fares-v2-json-entities/networks.json | 1 - .../fares-v2-json-entities/route_networks.json | 1 - .../resources/fares-v2-json-entities/stop_areas.json | 1 - .../fares-v2-json-entities/time_frames.json | 1 - 10 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/test/java/com/conveyal/gtfs/loader/JDBCTableWriterFaresV2Test.java b/src/test/java/com/conveyal/gtfs/loader/JDBCTableWriterFaresV2Test.java index 80d328be9..470bf7334 100644 --- a/src/test/java/com/conveyal/gtfs/loader/JDBCTableWriterFaresV2Test.java +++ b/src/test/java/com/conveyal/gtfs/loader/JDBCTableWriterFaresV2Test.java @@ -65,7 +65,12 @@ void canCreateUpdateAndDeleteEntity( String entity, String entityUpdated ) throws IOException, SQLException, InvalidNamespaceException { - assertEquals(entity, createTestTableWriter(table).create(entity, true)); + assertEquals( + entity, + createTestTableWriter(table) + .create(entity, true) + .replace(",\"id\":1", "") + ); JdbcTableWriter updateTableWriter = createTestTableWriter(table); assertEquals(entityUpdated, updateTableWriter.update(1, entityUpdated, true)); @@ -74,7 +79,7 @@ void canCreateUpdateAndDeleteEntity( } /** - * Define JSON payload. ID must be set as 1. + * Define JSON payloads. ID must be set to 1 in the updated payloads. */ private static Stream createEntityInput() throws IOException { return Stream.of( @@ -137,6 +142,9 @@ private static String getEntityFromFile(String fileName) throws IOException { .replace("\"\"", "null"); } + /** + * Delete entity with id of one and confirm no rows are returned matching this. + */ private static void deleteEntity(Table table) throws InvalidNamespaceException, SQLException { JdbcTableWriter deleteTableWriter = createTestTableWriter(table); deleteTableWriter.delete(1, true); diff --git a/src/test/resources/fares-v2-json-entities/areas.json b/src/test/resources/fares-v2-json-entities/areas.json index 07fdcbac5..d1626de79 100644 --- a/src/test/resources/fares-v2-json-entities/areas.json +++ b/src/test/resources/fares-v2-json-entities/areas.json @@ -1,5 +1,4 @@ { - "id": 1, "area_id": "area_bl", "area_name": "Blue Line" } \ No newline at end of file diff --git a/src/test/resources/fares-v2-json-entities/fare_leg_rules.json b/src/test/resources/fares-v2-json-entities/fare_leg_rules.json index ce0d2ff7a..a06a4e1c7 100644 --- a/src/test/resources/fares-v2-json-entities/fare_leg_rules.json +++ b/src/test/resources/fares-v2-json-entities/fare_leg_rules.json @@ -1,5 +1,4 @@ { - "id": 1, "leg_group_id": "leg_airport_rapid_transit_quick_subway", "network_id": "rapid_transit", "from_area_id": "area_bl_airport", diff --git a/src/test/resources/fares-v2-json-entities/fare_media.json b/src/test/resources/fares-v2-json-entities/fare_media.json index 701b05698..2d0bdd5c9 100644 --- a/src/test/resources/fares-v2-json-entities/fare_media.json +++ b/src/test/resources/fares-v2-json-entities/fare_media.json @@ -1,5 +1,4 @@ { - "id": 1, "fare_media_id": "cash", "fare_media_name": "Cash", "fare_media_type": "0" diff --git a/src/test/resources/fares-v2-json-entities/fare_product.json b/src/test/resources/fares-v2-json-entities/fare_product.json index 60fb64c8b..2ce2d72a3 100644 --- a/src/test/resources/fares-v2-json-entities/fare_product.json +++ b/src/test/resources/fares-v2-json-entities/fare_product.json @@ -1,5 +1,4 @@ { - "id": 1, "fare_product_id": "AERIAL_TRAM_ROUND_TRIP", "fare_product_name": "Portland Aerial Tram Single Round Trip", "fare_media_id": "1", diff --git a/src/test/resources/fares-v2-json-entities/fare_transfer_rules.json b/src/test/resources/fares-v2-json-entities/fare_transfer_rules.json index fb4a94f1c..bef7e2ec6 100644 --- a/src/test/resources/fares-v2-json-entities/fare_transfer_rules.json +++ b/src/test/resources/fares-v2-json-entities/fare_transfer_rules.json @@ -1,5 +1,4 @@ { - "id": 1, "from_leg_group_id": "leg_airport_rapid_transit_quick_subway", "to_leg_group_id": "leg_local_bus_quick_subway", "transfer_count": "", diff --git a/src/test/resources/fares-v2-json-entities/networks.json b/src/test/resources/fares-v2-json-entities/networks.json index a43f227a1..ce15bf112 100644 --- a/src/test/resources/fares-v2-json-entities/networks.json +++ b/src/test/resources/fares-v2-json-entities/networks.json @@ -1,5 +1,4 @@ { - "id": 1, "network_id": 1, "network_name": "This is the network name" } \ No newline at end of file diff --git a/src/test/resources/fares-v2-json-entities/route_networks.json b/src/test/resources/fares-v2-json-entities/route_networks.json index d39e03a67..7feeabe3b 100644 --- a/src/test/resources/fares-v2-json-entities/route_networks.json +++ b/src/test/resources/fares-v2-json-entities/route_networks.json @@ -1,5 +1,4 @@ { - "id": 1, "network_id": 1, "route_id": 1 } \ No newline at end of file diff --git a/src/test/resources/fares-v2-json-entities/stop_areas.json b/src/test/resources/fares-v2-json-entities/stop_areas.json index c34f7538a..1713118ce 100644 --- a/src/test/resources/fares-v2-json-entities/stop_areas.json +++ b/src/test/resources/fares-v2-json-entities/stop_areas.json @@ -1,5 +1,4 @@ { - "id": 1, "stop_id": "4u6g", "area_id": "area_route_426_downtown" } \ No newline at end of file diff --git a/src/test/resources/fares-v2-json-entities/time_frames.json b/src/test/resources/fares-v2-json-entities/time_frames.json index 157c1e2c5..d61225b16 100644 --- a/src/test/resources/fares-v2-json-entities/time_frames.json +++ b/src/test/resources/fares-v2-json-entities/time_frames.json @@ -1,5 +1,4 @@ { - "id": 1, "timeframe_group_id": "timeframe_sumner_tunnel_closure", "start_time": "00:00:00", "end_time": "02:30:00", From 239013046f75becc955f0729fe01ee93d4f74c48 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Wed, 5 Feb 2025 13:41:31 +0000 Subject: [PATCH 13/40] improvement(Altered GraphQL test data): Similified fare product query and included id --- .../resources/graphql/feedFareProducts.txt | 7 +- .../canFetchFareProducts-0.json | 90 +++++-------------- 2 files changed, 21 insertions(+), 76 deletions(-) diff --git a/src/test/resources/graphql/feedFareProducts.txt b/src/test/resources/graphql/feedFareProducts.txt index 536ae1813..daa8ae1cb 100644 --- a/src/test/resources/graphql/feedFareProducts.txt +++ b/src/test/resources/graphql/feedFareProducts.txt @@ -2,17 +2,12 @@ query ($namespace: String) { feed(namespace: $namespace) { feed_version fare_product(limit: 10) { + id fare_product_id fare_product_name fare_media_id amount currency - fare_media { - fare_media_id - fare_media_name - fare_media_type - id - } } } } \ No newline at end of file diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareProducts-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareProducts-0.json index c9607e342..394e87a13 100644 --- a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareProducts-0.json +++ b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareProducts-0.json @@ -4,123 +4,73 @@ "fare_product" : [ { "amount" : "6.5", "currency" : "USD", - "fare_media" : [ { - "fare_media_id" : "cash", - "fare_media_name" : "Cash", - "fare_media_type" : "0", - "id" : 2 - } ], "fare_media_id" : "cash", "fare_product_id" : "prod_boat_zone_1", - "fare_product_name" : "Ferry Zone 1 one-way fare" + "fare_product_name" : "Ferry Zone 1 one-way fare", + "id" : 2 }, { "amount" : "6.5", "currency" : "USD", - "fare_media" : [ { - "fare_media_id" : "credit_debit", - "fare_media_name" : "Credit/debit card", - "fare_media_type" : "0", - "id" : 3 - } ], "fare_media_id" : "credit_debit", "fare_product_id" : "prod_boat_zone_1", - "fare_product_name" : "Ferry Zone 1 one-way fare" + "fare_product_name" : "Ferry Zone 1 one-way fare", + "id" : 3 }, { "amount" : "6.5", "currency" : "USD", - "fare_media" : [ { - "fare_media_id" : "mticket", - "fare_media_name" : "mTicket app", - "fare_media_type" : "4", - "id" : 5 - } ], "fare_media_id" : "mticket", "fare_product_id" : "prod_boat_zone_1", - "fare_product_name" : "Ferry Zone 1 one-way fare" + "fare_product_name" : "Ferry Zone 1 one-way fare", + "id" : 4 }, { "amount" : "2.4", "currency" : "USD", - "fare_media" : [ { - "fare_media_id" : "cash", - "fare_media_name" : "Cash", - "fare_media_type" : "0", - "id" : 2 - } ], "fare_media_id" : "cash", "fare_product_id" : "prod_boat_zone_1a", - "fare_product_name" : "Ferry Zone 1A one-way fare" + "fare_product_name" : "Ferry Zone 1A one-way fare", + "id" : 5 }, { "amount" : "2.4", "currency" : "USD", - "fare_media" : [ { - "fare_media_id" : "credit_debit", - "fare_media_name" : "Credit/debit card", - "fare_media_type" : "0", - "id" : 3 - } ], "fare_media_id" : "credit_debit", "fare_product_id" : "prod_boat_zone_1a", - "fare_product_name" : "Ferry Zone 1A one-way fare" + "fare_product_name" : "Ferry Zone 1A one-way fare", + "id" : 6 }, { "amount" : "2.4", "currency" : "USD", - "fare_media" : [ { - "fare_media_id" : "mticket", - "fare_media_name" : "mTicket app", - "fare_media_type" : "4", - "id" : 5 - } ], "fare_media_id" : "mticket", "fare_product_id" : "prod_boat_zone_1a", - "fare_product_name" : "Ferry Zone 1A one-way fare" + "fare_product_name" : "Ferry Zone 1A one-way fare", + "id" : 7 }, { "amount" : "7.0", "currency" : "USD", - "fare_media" : [ { - "fare_media_id" : "cash", - "fare_media_name" : "Cash", - "fare_media_type" : "0", - "id" : 2 - } ], "fare_media_id" : "cash", "fare_product_id" : "prod_boat_zone_2", - "fare_product_name" : "Ferry Zone 2 one-way fare" + "fare_product_name" : "Ferry Zone 2 one-way fare", + "id" : 8 }, { "amount" : "7.0", "currency" : "USD", - "fare_media" : [ { - "fare_media_id" : "credit_debit", - "fare_media_name" : "Credit/debit card", - "fare_media_type" : "0", - "id" : 3 - } ], "fare_media_id" : "credit_debit", "fare_product_id" : "prod_boat_zone_2", - "fare_product_name" : "Ferry Zone 2 one-way fare" + "fare_product_name" : "Ferry Zone 2 one-way fare", + "id" : 9 }, { "amount" : "7.0", "currency" : "USD", - "fare_media" : [ { - "fare_media_id" : "mticket", - "fare_media_name" : "mTicket app", - "fare_media_type" : "4", - "id" : 5 - } ], "fare_media_id" : "mticket", "fare_product_id" : "prod_boat_zone_2", - "fare_product_name" : "Ferry Zone 2 one-way fare" + "fare_product_name" : "Ferry Zone 2 one-way fare", + "id" : 10 }, { "amount" : "5.0", "currency" : "USD", - "fare_media" : [ { - "fare_media_id" : "cash", - "fare_media_name" : "Cash", - "fare_media_type" : "0", - "id" : 2 - } ], "fare_media_id" : "cash", "fare_product_id" : "prod_cape_buzzards_hyannis_fare", - "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" + "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare", + "id" : 11 } ], "feed_version" : "1.0" } From 2c256c9e8db261d46bd52f140c98a94bf0b9a159 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Thu, 6 Feb 2025 10:27:31 +0000 Subject: [PATCH 14/40] improvement(Removed child entities from GraphQL schema): Only parent entities will be returned --- .../graphql/GraphQLGtfsFaresV2Schema.java | 43 +- .../java/com/conveyal/gtfs/loader/Table.java | 6 +- src/test/resources/graphql/feedAreas.txt | 9 - .../resources/graphql/feedFareLegRules.txt | 35 - .../graphql/feedFareTransferRules.txt | 37 +- src/test/resources/graphql/feedStopAreas.txt | 4 - .../GTFSGraphQLTest/canFetchAreas-0.json | 198 +----- .../canFetchFareLegRules-0.json | 308 --------- .../canFetchFareTransferRules-0.json | 644 +----------------- .../GTFSGraphQLTest/canFetchStopAreas-0.json | 300 ++------ 10 files changed, 61 insertions(+), 1523 deletions(-) diff --git a/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsFaresV2Schema.java b/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsFaresV2Schema.java index f1092a3d5..63b275549 100644 --- a/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsFaresV2Schema.java +++ b/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsFaresV2Schema.java @@ -7,7 +7,6 @@ import com.conveyal.gtfs.model.FareProduct; import com.conveyal.gtfs.model.FareTransferRule; import com.conveyal.gtfs.model.Network; -import com.conveyal.gtfs.model.Route; import com.conveyal.gtfs.model.RouteNetwork; import com.conveyal.gtfs.model.StopArea; import com.conveyal.gtfs.model.TimeFrame; @@ -40,7 +39,6 @@ private GraphQLGtfsFaresV2Schema() {} .field(MapFetcher.field("id", GraphQLInt)) .field(MapFetcher.field(StopArea.AREA_ID_NAME)) .field(MapFetcher.field(StopArea.STOP_ID_NAME)) - .field(createFieldDefinition("stops", GraphQLGtfsSchema.stopType, "stops", StopArea.STOP_ID_NAME)) .build(); public static final GraphQLObjectType areaType = newObject().name(AREA_TYPE_NAME) @@ -48,7 +46,6 @@ private GraphQLGtfsFaresV2Schema() {} .field(MapFetcher.field("id", GraphQLInt)) .field(MapFetcher.field(Area.AREA_ID_NAME)) .field(MapFetcher.field(Area.AREA_NAME_NAME)) - .field(createFieldDefinition("stop_areas", stopAreaType, StopArea.TABLE_NAME, Area.AREA_ID_NAME)) .build(); public static final GraphQLObjectType timeFrameType = newObject().name(TIME_FRAME_TYPE_NAME) @@ -72,7 +69,6 @@ private GraphQLGtfsFaresV2Schema() {} .field(MapFetcher.field("id", GraphQLInt)) .field(MapFetcher.field(RouteNetwork.NETWORK_ID_NAME)) .field(MapFetcher.field(RouteNetwork.ROUTE_ID_NAME)) - .field(createFieldDefinition("networks", networkType, Network.TABLE_NAME, RouteNetwork.NETWORK_ID_NAME)) .build(); public static final GraphQLObjectType fareMediaType = newObject().name(FARE_MEDIA_TYPE_NAME) @@ -91,7 +87,6 @@ private GraphQLGtfsFaresV2Schema() {} .field(MapFetcher.field(FareProduct.FARE_MEDIA_ID_NAME)) .field(MapFetcher.field(FareProduct.AMOUNT_NAME)) .field(MapFetcher.field(FareProduct.CURRENCY_NAME)) - .field(createFieldDefinition(FARE_MEDIA_TYPE_NAME, fareMediaType, FareMedia.TABLE_NAME, FareProduct.FARE_MEDIA_ID_NAME)) .build(); public static final GraphQLObjectType fareLegRuleType = newObject().name(FARE_LEG_RULE_TYPE_NAME) @@ -105,27 +100,6 @@ private GraphQLGtfsFaresV2Schema() {} .field(MapFetcher.field(FareLegRule.TO_TIMEFRAME_GROUP_ID_NAME)) .field(MapFetcher.field(FareLegRule.FARE_PRODUCT_ID_NAME)) .field(MapFetcher.field(FareLegRule.RULE_PRIORITY_NAME)) - // Will return either routes or networks, not both. - .field(createFieldDefinition("routes", GraphQLGtfsSchema.routeType, Route.TABLE_NAME, FareLegRule.NETWORK_ID_NAME)) - .field(createFieldDefinition("networks", networkType, Network.TABLE_NAME, Network.NETWORK_ID_NAME)) - .field(createFieldDefinition("fare_products", fareProductType, FareProduct.TABLE_NAME, FareLegRule.FARE_PRODUCT_ID_NAME)) - // fromTimeFrame and toTimeFrame may return multiple time frames. - .field(createFieldDefinition( - "from_time_frame", - timeFrameType, - TimeFrame.TABLE_NAME, - FareLegRule.FROM_TIMEFRAME_GROUP_ID_NAME, - TimeFrame.TIME_FRAME_GROUP_ID_NAME - )) - .field(createFieldDefinition( - "to_time_frame", - timeFrameType, - TimeFrame.TABLE_NAME, - FareLegRule.TO_TIMEFRAME_GROUP_ID_NAME, - TimeFrame.TIME_FRAME_GROUP_ID_NAME - )) - .field(createFieldDefinition("to_area", areaType, Area.TABLE_NAME, FareLegRule.TO_AREA_ID_NAME, Area.AREA_ID_NAME)) - .field(createFieldDefinition("from_area", areaType, Area.TABLE_NAME, FareLegRule.FROM_AREA_ID_NAME, Area.AREA_ID_NAME)) .build(); public static final GraphQLObjectType fareTransferRuleType = newObject().name(FARE_TRANSFER_RULE_TYPE_NAME) @@ -136,23 +110,8 @@ private GraphQLGtfsFaresV2Schema() {} .field(MapFetcher.field(FareTransferRule.TRANSFER_COUNT_NAME)) .field(MapFetcher.field(FareTransferRule.DURATION_LIMIT_NAME)) .field(MapFetcher.field(FareTransferRule.DURATION_LIMIT_TYPE_NAME)) + .field(MapFetcher.field(FareTransferRule.FARE_TRANSFER_TYPE_NAME)) .field(MapFetcher.field(FareTransferRule.FARE_PRODUCT_ID_NAME)) - .field(createFieldDefinition("to_area", areaType, Area.TABLE_NAME, FareLegRule.TO_AREA_ID_NAME, Area.AREA_ID_NAME)) - .field(createFieldDefinition("fare_products", fareProductType, FareProduct.TABLE_NAME, FareProduct.FARE_PRODUCT_ID_NAME)) - .field(createFieldDefinition( - "from_fare_leg_rule", - fareLegRuleType, - FareLegRule.TABLE_NAME, - FareTransferRule.FROM_LEG_GROUP_ID_NAME, - FareLegRule.LEG_GROUP_ID_NAME - )) - .field(createFieldDefinition( - "to_fare_leg_rule", - fareLegRuleType, - FareLegRule.TABLE_NAME, - FareTransferRule.TO_LEG_GROUP_ID_NAME, - FareLegRule.LEG_GROUP_ID_NAME - )) .build(); public static List getFaresV2FieldDefinitions() { diff --git a/src/main/java/com/conveyal/gtfs/loader/Table.java b/src/main/java/com/conveyal/gtfs/loader/Table.java index 1c4954c9b..02ab683fb 100644 --- a/src/main/java/com/conveyal/gtfs/loader/Table.java +++ b/src/main/java/com/conveyal/gtfs/loader/Table.java @@ -395,13 +395,11 @@ public Table (String name, Class entityClass, Requirement requ .addPrimaryKeyNames(Network.NETWORK_ID_NAME); public static final Table ROUTE_NETWORKS = new Table(RouteNetwork.TABLE_NAME, RouteNetwork.class, OPTIONAL, - new StringField(RouteNetwork.NETWORK_ID_NAME, REQUIRED).isReferenceTo(NETWORKS), - new StringField(RouteNetwork.ROUTE_ID_NAME, REQUIRED).isReferenceTo(ROUTES) + new StringField(RouteNetwork.ROUTE_ID_NAME, REQUIRED).isReferenceTo(ROUTES), + new StringField(RouteNetwork.NETWORK_ID_NAME, REQUIRED).isReferenceTo(NETWORKS) ) - .keyFieldIsNotUnique() .addPrimaryKeyNames(RouteNetwork.ROUTE_ID_NAME); - // GTFS reference: https://developers.google.com/transit/gtfs/reference#fare_rulestxt public static final Table FARE_RULES = new Table("fare_rules", FareRule.class, OPTIONAL, new StringField("fare_id", REQUIRED).isReferenceTo(FARE_ATTRIBUTES), diff --git a/src/test/resources/graphql/feedAreas.txt b/src/test/resources/graphql/feedAreas.txt index b2a19a710..ca1a196c5 100644 --- a/src/test/resources/graphql/feedAreas.txt +++ b/src/test/resources/graphql/feedAreas.txt @@ -5,15 +5,6 @@ query ($namespace: String) { area_id area_name id - stop_areas { - area_id - stop_id - id - stops { - stop_id - stop_name - } - } } } } \ No newline at end of file diff --git a/src/test/resources/graphql/feedFareLegRules.txt b/src/test/resources/graphql/feedFareLegRules.txt index 237782680..813626f25 100644 --- a/src/test/resources/graphql/feedFareLegRules.txt +++ b/src/test/resources/graphql/feedFareLegRules.txt @@ -11,41 +11,6 @@ query ($namespace: String) { fare_product_id rule_priority id - routes { - route_id - route_long_name - } - networks { - network_id - network_name - } - fare_products { - fare_product_id - fare_product_name - fare_media_id - amount - currency - } - from_time_frame { - timeframe_group_id - start_time - end_time - service_id - } - to_time_frame { - timeframe_group_id - start_time - end_time - service_id - } - to_area { - area_id - area_name - } - from_area { - area_id - area_name - } } } } \ No newline at end of file diff --git a/src/test/resources/graphql/feedFareTransferRules.txt b/src/test/resources/graphql/feedFareTransferRules.txt index b8d5fc762..624ec8aa4 100644 --- a/src/test/resources/graphql/feedFareTransferRules.txt +++ b/src/test/resources/graphql/feedFareTransferRules.txt @@ -2,47 +2,14 @@ query ($namespace: String) { feed(namespace: $namespace) { feed_version fare_transfer_rule (limit: 2) { + id from_leg_group_id to_leg_group_id transfer_count duration_limit duration_limit_type + fare_transfer_type fare_product_id - fare_products { - fare_product_id - fare_product_name - fare_media_id - amount - currency - fare_media { - fare_media_id - fare_media_name - fare_media_type - id - } - } - from_fare_leg_rule { - leg_group_id - network_id - from_area_id - to_area_id - from_timeframe_group_id - to_timeframe_group_id - fare_product_id - rule_priority - id - } - to_fare_leg_rule { - leg_group_id - network_id - from_area_id - to_area_id - from_timeframe_group_id - to_timeframe_group_id - fare_product_id - rule_priority - id - } } } } \ No newline at end of file diff --git a/src/test/resources/graphql/feedStopAreas.txt b/src/test/resources/graphql/feedStopAreas.txt index 5e455f11d..4f56c7d86 100644 --- a/src/test/resources/graphql/feedStopAreas.txt +++ b/src/test/resources/graphql/feedStopAreas.txt @@ -5,10 +5,6 @@ query ($namespace: String) { area_id stop_id id - stops { - stop_id - stop_name - } } } } \ No newline at end of file diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchAreas-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchAreas-0.json index 25b2ec25f..9d01175d8 100644 --- a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchAreas-0.json +++ b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchAreas-0.json @@ -4,205 +4,11 @@ "area" : [ { "area_id" : "area_bl", "area_name" : "Blue Line", - "id" : 2, - "stop_areas" : [ { - "area_id" : "area_bl", - "id" : 229, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - }, { - "area_id" : "area_bl", - "id" : 230, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - }, { - "area_id" : "area_bl", - "id" : 231, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - }, { - "area_id" : "area_bl", - "id" : 232, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - }, { - "area_id" : "area_bl", - "id" : 233, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - }, { - "area_id" : "area_bl", - "id" : 234, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - }, { - "area_id" : "area_bl", - "id" : 235, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - }, { - "area_id" : "area_bl", - "id" : 236, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - }, { - "area_id" : "area_bl", - "id" : 237, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - }, { - "area_id" : "area_bl", - "id" : 240, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - }, { - "area_id" : "area_bl", - "id" : 241, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - }, { - "area_id" : "area_bl", - "id" : 242, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - }, { - "area_id" : "area_bl", - "id" : 243, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - }, { - "area_id" : "area_bl", - "id" : 244, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - }, { - "area_id" : "area_bl", - "id" : 245, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - }, { - "area_id" : "area_bl", - "id" : 246, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - }, { - "area_id" : "area_bl", - "id" : 247, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - }, { - "area_id" : "area_bl", - "id" : 248, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - }, { - "area_id" : "area_bl", - "id" : 249, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - }, { - "area_id" : "area_bl", - "id" : 250, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - }, { - "area_id" : "area_bl", - "id" : 251, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - }, { - "area_id" : "area_bl", - "id" : 330, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - } ] + "id" : 2 }, { "area_id" : "area_bl_airport", "area_name" : "Blue Line - Airport Station", - "id" : 3, - "stop_areas" : [ { - "area_id" : "area_bl_airport", - "id" : 238, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - }, { - "area_id" : "area_bl_airport", - "id" : 239, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] - } ] + "id" : 3 } ], "feed_version" : "1.0" } diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareLegRules-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareLegRules-0.json index db5a0cc32..853e1076b 100644 --- a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareLegRules-0.json +++ b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareLegRules-0.json @@ -3,411 +3,103 @@ "feed" : { "fare_leg_rule" : [ { "fare_product_id" : "prod_rapid_transit_quick_subway", - "fare_products" : [ { - "amount" : "2.4", - "currency" : "USD", - "fare_media_id" : "charlieticket", - "fare_product_id" : "prod_rapid_transit_quick_subway", - "fare_product_name" : "Subway Quick Ticket" - } ], - "from_area" : [ { - "area_id" : "area_bl_airport", - "area_name" : "Blue Line - Airport Station" - } ], "from_area_id" : "area_bl_airport", - "from_time_frame" : [ { - "end_time" : null, - "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", - "start_time" : null, - "timeframe_group_id" : "timeframe_regular" - }, { - "end_time" : "86400", - "service_id" : "04100312-8fe1-46a5-a9f2-556f39478f57", - "start_time" : "9000", - "timeframe_group_id" : "timeframe_regular" - } ], "from_timeframe_group_id" : "timeframe_regular", "id" : 2, "leg_group_id" : "leg_airport_rapid_transit_quick_subway", "network_id" : "rapid_transit", - "networks" : [ ], - "routes" : [ { - "route_id" : "1", - "route_long_name" : "RL" - } ], "rule_priority" : null, - "to_area" : [ ], "to_area_id" : null, - "to_time_frame" : [ ], "to_timeframe_group_id" : null }, { "fare_product_id" : "prod_cape_buzzards_hyannis_fare", - "fare_products" : [ { - "amount" : "5.0", - "currency" : "USD", - "fare_media_id" : "cash", - "fare_product_id" : "prod_cape_buzzards_hyannis_fare", - "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" - }, { - "amount" : "5.0", - "currency" : "USD", - "fare_media_id" : "credit_debit", - "fare_product_id" : "prod_cape_buzzards_hyannis_fare", - "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" - }, { - "amount" : "5.0", - "currency" : "USD", - "fare_media_id" : "mticket", - "fare_product_id" : "prod_cape_buzzards_hyannis_fare", - "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" - } ], - "from_area" : [ { - "area_id" : "area_cf_zone_buzzards", - "area_name" : "CapeFLYER - Wareham/Buzzards Bay/Bourne" - } ], "from_area_id" : "area_cf_zone_buzzards", - "from_time_frame" : [ ], "from_timeframe_group_id" : null, "id" : 3, "leg_group_id" : "leg_cape_buzzards_hyannis_cash", "network_id" : "cape_flyer", - "networks" : [ ], - "routes" : [ ], "rule_priority" : null, - "to_area" : [ { - "area_id" : "area_cf_zone_hyannis", - "area_name" : "CapeFLYER - Hyannis" - } ], "to_area_id" : "area_cf_zone_hyannis", - "to_time_frame" : [ ], "to_timeframe_group_id" : null }, { "fare_product_id" : "prod_cape_buzzards_hyannis_fare", - "fare_products" : [ { - "amount" : "5.0", - "currency" : "USD", - "fare_media_id" : "cash", - "fare_product_id" : "prod_cape_buzzards_hyannis_fare", - "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" - }, { - "amount" : "5.0", - "currency" : "USD", - "fare_media_id" : "credit_debit", - "fare_product_id" : "prod_cape_buzzards_hyannis_fare", - "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" - }, { - "amount" : "5.0", - "currency" : "USD", - "fare_media_id" : "mticket", - "fare_product_id" : "prod_cape_buzzards_hyannis_fare", - "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" - } ], - "from_area" : [ { - "area_id" : "area_cf_zone_hyannis", - "area_name" : "CapeFLYER - Hyannis" - } ], "from_area_id" : "area_cf_zone_hyannis", - "from_time_frame" : [ ], "from_timeframe_group_id" : null, "id" : 4, "leg_group_id" : "leg_cape_buzzards_hyannis_cash", "network_id" : "cape_flyer", - "networks" : [ ], - "routes" : [ ], "rule_priority" : null, - "to_area" : [ { - "area_id" : "area_cf_zone_buzzards", - "area_name" : "CapeFLYER - Wareham/Buzzards Bay/Bourne" - } ], "to_area_id" : "area_cf_zone_buzzards", - "to_time_frame" : [ ], "to_timeframe_group_id" : null }, { "fare_product_id" : "prod_cape_buzzards_hyannis_fare", - "fare_products" : [ { - "amount" : "5.0", - "currency" : "USD", - "fare_media_id" : "cash", - "fare_product_id" : "prod_cape_buzzards_hyannis_fare", - "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" - }, { - "amount" : "5.0", - "currency" : "USD", - "fare_media_id" : "credit_debit", - "fare_product_id" : "prod_cape_buzzards_hyannis_fare", - "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" - }, { - "amount" : "5.0", - "currency" : "USD", - "fare_media_id" : "mticket", - "fare_product_id" : "prod_cape_buzzards_hyannis_fare", - "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" - } ], - "from_area" : [ { - "area_id" : "area_cf_zone_hyannis", - "area_name" : "CapeFLYER - Hyannis" - } ], "from_area_id" : "area_cf_zone_hyannis", - "from_time_frame" : [ ], "from_timeframe_group_id" : null, "id" : 5, "leg_group_id" : "leg_cape_buzzards_hyannis_cash", "network_id" : "cape_flyer", - "networks" : [ ], - "routes" : [ ], "rule_priority" : null, - "to_area" : [ { - "area_id" : "area_commuter_rail_zone_8", - "area_name" : "Commuter Rail Zone 8" - } ], "to_area_id" : "area_commuter_rail_zone_8", - "to_time_frame" : [ ], "to_timeframe_group_id" : null }, { "fare_product_id" : "prod_cape_buzzards_hyannis_fare", - "fare_products" : [ { - "amount" : "5.0", - "currency" : "USD", - "fare_media_id" : "cash", - "fare_product_id" : "prod_cape_buzzards_hyannis_fare", - "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" - }, { - "amount" : "5.0", - "currency" : "USD", - "fare_media_id" : "credit_debit", - "fare_product_id" : "prod_cape_buzzards_hyannis_fare", - "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" - }, { - "amount" : "5.0", - "currency" : "USD", - "fare_media_id" : "mticket", - "fare_product_id" : "prod_cape_buzzards_hyannis_fare", - "fare_product_name" : "CapeFLYER Middleborough/Lakeville one-way fare" - } ], - "from_area" : [ { - "area_id" : "area_commuter_rail_zone_8", - "area_name" : "Commuter Rail Zone 8" - } ], "from_area_id" : "area_commuter_rail_zone_8", - "from_time_frame" : [ ], "from_timeframe_group_id" : null, "id" : 6, "leg_group_id" : "leg_cape_buzzards_hyannis_cash", "network_id" : "cape_flyer", - "networks" : [ ], - "routes" : [ ], "rule_priority" : null, - "to_area" : [ { - "area_id" : "area_cf_zone_hyannis", - "area_name" : "CapeFLYER - Hyannis" - } ], "to_area_id" : "area_cf_zone_hyannis", - "to_time_frame" : [ ], "to_timeframe_group_id" : null }, { "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fare_products" : [ { - "amount" : "20.0", - "currency" : "USD", - "fare_media_id" : "cash", - "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fare_product_name" : "CapeFLYER Bourne one-way fare" - }, { - "amount" : "20.0", - "currency" : "USD", - "fare_media_id" : "credit_debit", - "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fare_product_name" : "CapeFLYER Bourne one-way fare" - }, { - "amount" : "20.0", - "currency" : "USD", - "fare_media_id" : "mticket", - "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fare_product_name" : "CapeFLYER Bourne one-way fare" - } ], - "from_area" : [ { - "area_id" : "area_cf_zone_buzzards", - "area_name" : "CapeFLYER - Wareham/Buzzards Bay/Bourne" - } ], "from_area_id" : "area_cf_zone_buzzards", - "from_time_frame" : [ ], "from_timeframe_group_id" : null, "id" : 7, "leg_group_id" : "leg_cape_sbb_buzzards_cash", "network_id" : "cape_flyer", - "networks" : [ ], - "routes" : [ ], "rule_priority" : null, - "to_area" : [ { - "area_id" : "area_commuter_rail_zone_1a", - "area_name" : "Commuter Rail Zone 1A" - } ], "to_area_id" : "area_commuter_rail_zone_1a", - "to_time_frame" : [ ], "to_timeframe_group_id" : null }, { "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fare_products" : [ { - "amount" : "20.0", - "currency" : "USD", - "fare_media_id" : "cash", - "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fare_product_name" : "CapeFLYER Bourne one-way fare" - }, { - "amount" : "20.0", - "currency" : "USD", - "fare_media_id" : "credit_debit", - "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fare_product_name" : "CapeFLYER Bourne one-way fare" - }, { - "amount" : "20.0", - "currency" : "USD", - "fare_media_id" : "mticket", - "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fare_product_name" : "CapeFLYER Bourne one-way fare" - } ], - "from_area" : [ { - "area_id" : "area_cf_zone_buzzards", - "area_name" : "CapeFLYER - Wareham/Buzzards Bay/Bourne" - } ], "from_area_id" : "area_cf_zone_buzzards", - "from_time_frame" : [ ], "from_timeframe_group_id" : null, "id" : 8, "leg_group_id" : "leg_cape_sbb_buzzards_cash", "network_id" : "cape_flyer", - "networks" : [ ], - "routes" : [ ], "rule_priority" : null, - "to_area" : [ { - "area_id" : "area_commuter_rail_zone_2", - "area_name" : "Commuter Rail Zone 2" - } ], "to_area_id" : "area_commuter_rail_zone_2", - "to_time_frame" : [ ], "to_timeframe_group_id" : null }, { "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fare_products" : [ { - "amount" : "20.0", - "currency" : "USD", - "fare_media_id" : "cash", - "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fare_product_name" : "CapeFLYER Bourne one-way fare" - }, { - "amount" : "20.0", - "currency" : "USD", - "fare_media_id" : "credit_debit", - "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fare_product_name" : "CapeFLYER Bourne one-way fare" - }, { - "amount" : "20.0", - "currency" : "USD", - "fare_media_id" : "mticket", - "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fare_product_name" : "CapeFLYER Bourne one-way fare" - } ], - "from_area" : [ { - "area_id" : "area_cf_zone_buzzards", - "area_name" : "CapeFLYER - Wareham/Buzzards Bay/Bourne" - } ], "from_area_id" : "area_cf_zone_buzzards", - "from_time_frame" : [ ], "from_timeframe_group_id" : null, "id" : 9, "leg_group_id" : "leg_cape_sbb_buzzards_cash", "network_id" : "cape_flyer", - "networks" : [ ], - "routes" : [ ], "rule_priority" : null, - "to_area" : [ { - "area_id" : "area_commuter_rail_zone_4", - "area_name" : "Commuter Rail Zone 4" - } ], "to_area_id" : "area_commuter_rail_zone_4", - "to_time_frame" : [ ], "to_timeframe_group_id" : null }, { "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fare_products" : [ { - "amount" : "20.0", - "currency" : "USD", - "fare_media_id" : "cash", - "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fare_product_name" : "CapeFLYER Bourne one-way fare" - }, { - "amount" : "20.0", - "currency" : "USD", - "fare_media_id" : "credit_debit", - "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fare_product_name" : "CapeFLYER Bourne one-way fare" - }, { - "amount" : "20.0", - "currency" : "USD", - "fare_media_id" : "mticket", - "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fare_product_name" : "CapeFLYER Bourne one-way fare" - } ], - "from_area" : [ { - "area_id" : "area_commuter_rail_zone_1a", - "area_name" : "Commuter Rail Zone 1A" - } ], "from_area_id" : "area_commuter_rail_zone_1a", - "from_time_frame" : [ ], "from_timeframe_group_id" : null, "id" : 10, "leg_group_id" : "leg_cape_sbb_buzzards_cash", "network_id" : "cape_flyer", - "networks" : [ ], - "routes" : [ ], "rule_priority" : null, - "to_area" : [ { - "area_id" : "area_cf_zone_buzzards", - "area_name" : "CapeFLYER - Wareham/Buzzards Bay/Bourne" - } ], "to_area_id" : "area_cf_zone_buzzards", - "to_time_frame" : [ ], "to_timeframe_group_id" : null }, { "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fare_products" : [ { - "amount" : "20.0", - "currency" : "USD", - "fare_media_id" : "cash", - "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fare_product_name" : "CapeFLYER Bourne one-way fare" - }, { - "amount" : "20.0", - "currency" : "USD", - "fare_media_id" : "credit_debit", - "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fare_product_name" : "CapeFLYER Bourne one-way fare" - }, { - "amount" : "20.0", - "currency" : "USD", - "fare_media_id" : "mticket", - "fare_product_id" : "prod_cape_sbb_buzzards_fare", - "fare_product_name" : "CapeFLYER Bourne one-way fare" - } ], - "from_area" : [ { - "area_id" : "area_commuter_rail_zone_2", - "area_name" : "Commuter Rail Zone 2" - } ], "from_area_id" : "area_commuter_rail_zone_2", - "from_time_frame" : [ ], "from_timeframe_group_id" : null, "id" : 11, "leg_group_id" : "leg_cape_sbb_buzzards_cash", "network_id" : "cape_flyer", - "networks" : [ ], - "routes" : [ ], "rule_priority" : null, - "to_area" : [ { - "area_id" : "area_cf_zone_buzzards", - "area_name" : "CapeFLYER - Wareham/Buzzards Bay/Bourne" - } ], "to_area_id" : "area_cf_zone_buzzards", - "to_time_frame" : [ ], "to_timeframe_group_id" : null } ], "feed_version" : "1.0" diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareTransferRules-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareTransferRules-0.json index d7bd4d3ba..ac4de0f10 100644 --- a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareTransferRules-0.json +++ b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchFareTransferRules-0.json @@ -5,654 +5,18 @@ "duration_limit" : "7200", "duration_limit_type" : "1", "fare_product_id" : "prod_rapid_transit_quick_subway", - "fare_products" : [ { - "amount" : "2.4", - "currency" : "USD", - "fare_media" : [ { - "fare_media_id" : "charlieticket", - "fare_media_name" : "CharlieTicket", - "fare_media_type" : "1", - "id" : 4 - } ], - "fare_media_id" : "charlieticket", - "fare_product_id" : "prod_rapid_transit_quick_subway", - "fare_product_name" : "Subway Quick Ticket" - } ], - "from_fare_leg_rule" : [ { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_bl_airport", - "from_timeframe_group_id" : "timeframe_regular", - "id" : 2, - "leg_group_id" : "leg_airport_rapid_transit_quick_subway", - "network_id" : "rapid_transit", - "rule_priority" : null, - "to_area_id" : null, - "to_timeframe_group_id" : null - } ], + "fare_transfer_type" : "0", "from_leg_group_id" : "leg_airport_rapid_transit_quick_subway", - "to_fare_leg_rule" : [ { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_354_downtown", - "from_timeframe_group_id" : null, - "id" : 311, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "express_bus", - "rule_priority" : null, - "to_area_id" : "area_route_354_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_354_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 312, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "express_bus", - "rule_priority" : null, - "to_area_id" : "area_route_354_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_426_downtown", - "from_timeframe_group_id" : null, - "id" : 313, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "express_bus", - "rule_priority" : null, - "to_area_id" : "area_route_426_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_426_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 314, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "express_bus", - "rule_priority" : null, - "to_area_id" : "area_route_426_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_450_downtown", - "from_timeframe_group_id" : null, - "id" : 315, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "express_bus", - "rule_priority" : null, - "to_area_id" : "area_route_450_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_450_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 316, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "express_bus", - "rule_priority" : null, - "to_area_id" : "area_route_450_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_354_downtown", - "from_timeframe_group_id" : null, - "id" : 317, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_354_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_354_downtown", - "from_timeframe_group_id" : null, - "id" : 318, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_354_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_354_downtown", - "from_timeframe_group_id" : null, - "id" : 319, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_426_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_354_downtown", - "from_timeframe_group_id" : null, - "id" : 320, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_426_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_354_downtown", - "from_timeframe_group_id" : null, - "id" : 321, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_450_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_354_downtown", - "from_timeframe_group_id" : null, - "id" : 322, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_450_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_354_downtown", - "from_timeframe_group_id" : null, - "id" : 323, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : null, - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_354_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 324, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_354_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_354_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 325, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_354_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_354_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 326, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_426_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_354_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 327, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_426_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_354_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 328, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_450_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_354_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 329, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_450_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_354_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 330, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : null, - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_426_downtown", - "from_timeframe_group_id" : null, - "id" : 331, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_354_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_426_downtown", - "from_timeframe_group_id" : null, - "id" : 332, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_354_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_426_downtown", - "from_timeframe_group_id" : null, - "id" : 333, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_426_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_426_downtown", - "from_timeframe_group_id" : null, - "id" : 334, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_426_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_426_downtown", - "from_timeframe_group_id" : null, - "id" : 335, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_450_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_426_downtown", - "from_timeframe_group_id" : null, - "id" : 336, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_450_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_426_downtown", - "from_timeframe_group_id" : null, - "id" : 337, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : null, - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_426_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 338, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_354_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_426_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 339, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_354_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_426_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 340, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_426_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_426_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 341, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_426_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_426_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 342, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_450_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_426_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 343, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_450_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_426_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 344, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : null, - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_450_downtown", - "from_timeframe_group_id" : null, - "id" : 345, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_354_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_450_downtown", - "from_timeframe_group_id" : null, - "id" : 346, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_354_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_450_downtown", - "from_timeframe_group_id" : null, - "id" : 347, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_426_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_450_downtown", - "from_timeframe_group_id" : null, - "id" : 348, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_426_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_450_downtown", - "from_timeframe_group_id" : null, - "id" : 349, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_450_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_450_downtown", - "from_timeframe_group_id" : null, - "id" : 350, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_450_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_450_downtown", - "from_timeframe_group_id" : null, - "id" : 351, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : null, - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_450_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 352, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_354_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_450_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 353, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_354_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_450_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 354, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_426_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_450_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 355, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_426_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_450_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 356, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_450_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_450_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 357, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_450_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_route_450_outside_downtown", - "from_timeframe_group_id" : null, - "id" : 358, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : null, - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : null, - "from_timeframe_group_id" : null, - "id" : 359, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_354_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : null, - "from_timeframe_group_id" : null, - "id" : 360, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_354_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : null, - "from_timeframe_group_id" : null, - "id" : 361, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_426_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : null, - "from_timeframe_group_id" : null, - "id" : 362, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_426_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : null, - "from_timeframe_group_id" : null, - "id" : 363, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_450_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : null, - "from_timeframe_group_id" : null, - "id" : 364, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : "area_route_450_outside_downtown", - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : null, - "from_timeframe_group_id" : null, - "id" : 365, - "leg_group_id" : "leg_local_bus_quick_subway", - "network_id" : "local_bus", - "rule_priority" : null, - "to_area_id" : null, - "to_timeframe_group_id" : null - } ], + "id" : 2, "to_leg_group_id" : "leg_local_bus_quick_subway", "transfer_count" : null }, { "duration_limit" : null, "duration_limit_type" : null, "fare_product_id" : "prod_rapid_transit_quick_subway", - "fare_products" : [ { - "amount" : "2.4", - "currency" : "USD", - "fare_media" : [ { - "fare_media_id" : "charlieticket", - "fare_media_name" : "CharlieTicket", - "fare_media_type" : "1", - "id" : 4 - } ], - "fare_media_id" : "charlieticket", - "fare_product_id" : "prod_rapid_transit_quick_subway", - "fare_product_name" : "Subway Quick Ticket" - } ], - "from_fare_leg_rule" : [ { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_bl_airport", - "from_timeframe_group_id" : "timeframe_regular", - "id" : 2, - "leg_group_id" : "leg_airport_rapid_transit_quick_subway", - "network_id" : "rapid_transit", - "rule_priority" : null, - "to_area_id" : null, - "to_timeframe_group_id" : null - } ], + "fare_transfer_type" : "0", "from_leg_group_id" : "leg_airport_rapid_transit_quick_subway", - "to_fare_leg_rule" : [ { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_bl", - "from_timeframe_group_id" : "timeframe_regular", - "id" : 476, - "leg_group_id" : "leg_rapid_transit_quick_subway", - "network_id" : "rapid_transit", - "rule_priority" : null, - "to_area_id" : null, - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_gl_govt_ctr", - "from_timeframe_group_id" : "timeframe_regular", - "id" : 477, - "leg_group_id" : "leg_rapid_transit_quick_subway", - "network_id" : "rapid_transit", - "rule_priority" : null, - "to_area_id" : null, - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : "area_ol_state", - "from_timeframe_group_id" : "timeframe_regular", - "id" : 478, - "leg_group_id" : "leg_rapid_transit_quick_subway", - "network_id" : "rapid_transit", - "rule_priority" : null, - "to_area_id" : null, - "to_timeframe_group_id" : null - }, { - "fare_product_id" : "prod_rapid_transit_quick_subway", - "from_area_id" : null, - "from_timeframe_group_id" : "timeframe_regular", - "id" : 479, - "leg_group_id" : "leg_rapid_transit_quick_subway", - "network_id" : "rapid_transit", - "rule_priority" : null, - "to_area_id" : null, - "to_timeframe_group_id" : null - } ], + "id" : 3, "to_leg_group_id" : "leg_rapid_transit_quick_subway", "transfer_count" : null } ], diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchStopAreas-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchStopAreas-0.json index 1b2454ee1..92b7250bd 100644 --- a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchStopAreas-0.json +++ b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchStopAreas-0.json @@ -5,403 +5,203 @@ "stop_area" : [ { "area_id" : "area_route_426_downtown", "id" : 2, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_450_downtown", "id" : 3, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_450_outside_downtown", "id" : 4, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_450_outside_downtown", "id" : 5, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_450_outside_downtown", "id" : 6, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_450_outside_downtown", "id" : 7, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_450_outside_downtown", "id" : 8, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_450_outside_downtown", "id" : 9, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_450_outside_downtown", "id" : 10, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_426_outside_downtown", "id" : 11, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_450_outside_downtown", "id" : 12, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_426_outside_downtown", "id" : 13, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_426_outside_downtown", "id" : 14, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_450_outside_downtown", "id" : 15, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_426_outside_downtown", "id" : 16, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_354_outside_downtown", "id" : 17, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_354_outside_downtown", "id" : 18, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_354_outside_downtown", "id" : 19, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_354_outside_downtown", "id" : 20, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_354_outside_downtown", "id" : 21, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_354_outside_downtown", "id" : 22, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_354_outside_downtown", "id" : 23, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_354_outside_downtown", "id" : 24, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_354_outside_downtown", "id" : 25, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_354_outside_downtown", "id" : 26, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_354_outside_downtown", "id" : 27, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_354_outside_downtown", "id" : 28, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_354_outside_downtown", "id" : 29, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_354_outside_downtown", "id" : 30, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_354_outside_downtown", "id" : 31, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_354_outside_downtown", "id" : 32, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_354_outside_downtown", "id" : 33, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_green_b_west_of_kenmore", "id" : 34, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_green_b_west_of_kenmore", "id" : 35, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_green_b_west_of_kenmore", "id" : 36, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_green_b_west_of_kenmore", "id" : 37, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_sl_logan_terminal", "id" : 38, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_sl_logan_terminal", "id" : 39, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_sl_logan_terminal", "id" : 40, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_sl_logan_terminal", "id" : 41, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_426_outside_downtown", "id" : 42, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_426_outside_downtown", "id" : 43, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_426_outside_downtown", "id" : 44, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_426_outside_downtown", "id" : 45, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_354_outside_downtown", "id" : 46, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_354_outside_downtown", "id" : 47, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_426_outside_downtown", "id" : 48, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_450_outside_downtown", "id" : 49, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_426_outside_downtown", "id" : 50, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" }, { "area_id" : "area_route_450_outside_downtown", "id" : 51, - "stop_id" : "4u6g", - "stops" : [ { - "stop_id" : "4u6g", - "stop_name" : "Butler Ln" - } ] + "stop_id" : "4u6g" } ] } } From 518fcc2f494e89ee378d5c5a5f691208d53f7f2d Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Thu, 6 Feb 2025 11:32:21 +0000 Subject: [PATCH 15/40] improvement(Table.java): Reinstated key field is not unique check for the route network table The route id is unique to the route table has well. This prevents duplicate key failures --- src/main/java/com/conveyal/gtfs/loader/Table.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/conveyal/gtfs/loader/Table.java b/src/main/java/com/conveyal/gtfs/loader/Table.java index 02ab683fb..ac64660d3 100644 --- a/src/main/java/com/conveyal/gtfs/loader/Table.java +++ b/src/main/java/com/conveyal/gtfs/loader/Table.java @@ -398,6 +398,9 @@ public Table (String name, Class entityClass, Requirement requ new StringField(RouteNetwork.ROUTE_ID_NAME, REQUIRED).isReferenceTo(ROUTES), new StringField(RouteNetwork.NETWORK_ID_NAME, REQUIRED).isReferenceTo(NETWORKS) ) + // Although within the context of this table the route id is unique, the unique value e.g. route_id:1 has already + // been flagged as unique when the route table is loaded! + .keyFieldIsNotUnique() .addPrimaryKeyNames(RouteNetwork.ROUTE_ID_NAME); // GTFS reference: https://developers.google.com/transit/gtfs/reference#fare_rulestxt From 731dcf0e4608752cf9ae3b5788da503856e5f64c Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Thu, 31 Jul 2025 14:16:05 +0100 Subject: [PATCH 16/40] feat(Merging stop areas into stops): import stop areas into stops and export to separate files --- src/main/java/com/conveyal/gtfs/GTFSFeed.java | 14 +- .../graphql/GraphQLGtfsFaresV2Schema.java | 10 - .../gtfs/graphql/GraphQLGtfsSchema.java | 1 + .../conveyal/gtfs/loader/EntityPopulator.java | 35 ++- .../java/com/conveyal/gtfs/loader/Feed.java | 2 - .../gtfs/loader/JdbcGTFSFeedConverter.java | 1 - .../gtfs/loader/JdbcGtfsExporter.java | 1 - .../conveyal/gtfs/loader/JdbcGtfsLoader.java | 7 +- .../gtfs/loader/JdbcGtfsSnapshotter.java | 1 - .../java/com/conveyal/gtfs/loader/Table.java | 62 +---- .../java/com/conveyal/gtfs/model/Entity.java | 13 +- .../java/com/conveyal/gtfs/model/Stop.java | 223 ++++++++++++++++-- .../com/conveyal/gtfs/model/StopArea.java | 14 +- .../com/conveyal/gtfs/util/CsvReaderUtil.java | 193 +++++++++++++++ src/test/java/com/conveyal/gtfs/GTFSTest.java | 12 - .../com/conveyal/gtfs/dto/StopAreaDTO.java | 14 -- .../java/com/conveyal/gtfs/dto/StopDTO.java | 1 + .../loader/JDBCTableWriterFaresV2Test.java | 5 - 18 files changed, 442 insertions(+), 167 deletions(-) create mode 100644 src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java delete mode 100644 src/test/java/com/conveyal/gtfs/dto/StopAreaDTO.java diff --git a/src/main/java/com/conveyal/gtfs/GTFSFeed.java b/src/main/java/com/conveyal/gtfs/GTFSFeed.java index b468e9afb..a87f7bf92 100644 --- a/src/main/java/com/conveyal/gtfs/GTFSFeed.java +++ b/src/main/java/com/conveyal/gtfs/GTFSFeed.java @@ -62,6 +62,7 @@ public class GTFSFeed implements Cloneable, Closeable { // This is how you do a multimap in mapdb: https://github.com/jankotek/MapDB/blob/release-1.0/src/test/java/examples/MultiMap.java public final NavigableSet> frequencies; public final Map routes; + public final Map stop_areas; public final Map stops; public final Map transfers; public final BTreeMap trips; @@ -111,7 +112,6 @@ public class GTFSFeed implements Cloneable, Closeable { public transient EventBus eventBus; public final Map areas; - public final Map stop_areas; public final Map fare_products; public final Map fare_medias; public final Map time_frames; @@ -182,7 +182,11 @@ else if (feedId == null || feedId.isEmpty()) { new Pattern.Loader(this).loadTable(zip); new Route.Loader(this).loadTable(zip); new ShapePoint.Loader(this).loadTable(zip); + new StopArea.Loader(this).loadTable(zip); new Stop.Loader(this).loadTable(zip); + if (!stop_areas.isEmpty()) { + Stop.updateStopAreas(stops, stop_areas); + } new Transfer.Loader(this).loadTable(zip); new Trip.Loader(this).loadTable(zip); new Frequency.Loader(this).loadTable(zip); @@ -190,7 +194,6 @@ else if (feedId == null || feedId.isEmpty()) { // Fares v2. new Area.Loader(this).loadTable(zip); - new StopArea.Loader(this).loadTable(zip); new TimeFrame.Loader(this).loadTable(zip); new Network.Loader(this).loadTable(zip); new RouteNetwork.Loader(this).loadTable(zip); @@ -235,6 +238,10 @@ public void toFile (String file) { new Frequency.Writer(this).writeTable(zip); new Route.Writer(this).writeTable(zip); new Stop.Writer(this).writeTable(zip); + if (!stops.isEmpty()) { + // Export stop areas. + Stop.writeStopAreasToFile(zip, new ArrayList<>(stops.values())); + } new ShapePoint.Writer(this).writeTable(zip); new Transfer.Writer(this).writeTable(zip); new Trip.Writer(this).writeTable(zip); @@ -243,7 +250,6 @@ public void toFile (String file) { // Fares v2. new Area.Writer(this).writeTable(zip); - new StopArea.Writer(this).writeTable(zip); new TimeFrame.Writer(this).writeTable(zip); new Network.Writer(this).writeTable(zip); new RouteNetwork.Writer(this).writeTable(zip); @@ -654,11 +660,11 @@ private GTFSFeed (DB db) { stop_times = db.getTreeMap("stop_times"); frequencies = db.getTreeSet("frequencies"); transfers = db.getTreeMap("transfers"); + stop_areas = db.getTreeMap("stop_areas"); stops = db.getTreeMap("stops"); fares = db.getTreeMap("fares"); services = db.getTreeMap("services"); shape_points = db.getTreeMap("shape_points"); - stop_areas = db.getTreeMap("stop_areas"); time_frames = db.getTreeMap("time_frames"); translations = db.getTreeMap("translations"); attributions = db.getTreeMap("attributions"); diff --git a/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsFaresV2Schema.java b/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsFaresV2Schema.java index 63b275549..8b87d818a 100644 --- a/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsFaresV2Schema.java +++ b/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsFaresV2Schema.java @@ -8,7 +8,6 @@ import com.conveyal.gtfs.model.FareTransferRule; import com.conveyal.gtfs.model.Network; import com.conveyal.gtfs.model.RouteNetwork; -import com.conveyal.gtfs.model.StopArea; import com.conveyal.gtfs.model.TimeFrame; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLObjectType; @@ -23,7 +22,6 @@ public class GraphQLGtfsFaresV2Schema { private static final String AREA_TYPE_NAME = "area"; - private static final String STOP_AREA_TYPE_NAME = "stop_area"; private static final String TIME_FRAME_TYPE_NAME = "time_frame"; private static final String NETWORK_TYPE_NAME = "network"; private static final String ROUTE_NETWORK_TYPE_NAME = "route_network"; @@ -34,13 +32,6 @@ public class GraphQLGtfsFaresV2Schema { private GraphQLGtfsFaresV2Schema() {} - public static final GraphQLObjectType stopAreaType = newObject().name(STOP_AREA_TYPE_NAME) - .description("A GTFS stop area object") - .field(MapFetcher.field("id", GraphQLInt)) - .field(MapFetcher.field(StopArea.AREA_ID_NAME)) - .field(MapFetcher.field(StopArea.STOP_ID_NAME)) - .build(); - public static final GraphQLObjectType areaType = newObject().name(AREA_TYPE_NAME) .description("A GTFS area object") .field(MapFetcher.field("id", GraphQLInt)) @@ -123,7 +114,6 @@ public static List getFaresV2FieldDefinitions() { createFieldDefinition(FARE_TRANSFER_RULE_TYPE_NAME, fareTransferRuleType, FareTransferRule.TABLE_NAME), createFieldDefinition(NETWORK_TYPE_NAME, networkType, Network.TABLE_NAME), createFieldDefinition(ROUTE_NETWORK_TYPE_NAME, routeNetworkType, RouteNetwork.TABLE_NAME), - createFieldDefinition(STOP_AREA_TYPE_NAME, stopAreaType, StopArea.TABLE_NAME), createFieldDefinition(TIME_FRAME_TYPE_NAME, timeFrameType, TimeFrame.TABLE_NAME) ); } diff --git a/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsSchema.java b/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsSchema.java index af233ed1f..5dd611a81 100644 --- a/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsSchema.java +++ b/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsSchema.java @@ -350,6 +350,7 @@ public class GraphQLGtfsSchema { .field(MapFetcher.field("platform_code")) .field(MapFetcher.field("location_type", GraphQLInt)) .field(MapFetcher.field("wheelchair_boarding", GraphQLInt)) + .field(MapFetcher.field("stop_area_ids")) // Returns all stops that reference parent stop's stop_id .field(newFieldDefinition() .name("child_stops") diff --git a/src/main/java/com/conveyal/gtfs/loader/EntityPopulator.java b/src/main/java/com/conveyal/gtfs/loader/EntityPopulator.java index e6ed31fdb..95d344aa8 100644 --- a/src/main/java/com/conveyal/gtfs/loader/EntityPopulator.java +++ b/src/main/java/com/conveyal/gtfs/loader/EntityPopulator.java @@ -19,7 +19,6 @@ import com.conveyal.gtfs.model.ScheduleException; import com.conveyal.gtfs.model.ShapePoint; import com.conveyal.gtfs.model.Stop; -import com.conveyal.gtfs.model.StopArea; import com.conveyal.gtfs.model.StopTime; import com.conveyal.gtfs.model.TimeFrame; import com.conveyal.gtfs.model.Trip; @@ -181,19 +180,20 @@ public interface EntityPopulator { EntityPopulator STOP = (result, columnForName) -> { Stop stop = new Stop(); - stop.stop_id = getStringIfPresent(result, "stop_id", columnForName); - stop.stop_code = getStringIfPresent(result, "stop_code", columnForName); - stop.stop_name = getStringIfPresent(result, "stop_name", columnForName); - stop.stop_desc = getStringIfPresent(result, "stop_desc", columnForName); - stop.stop_lat = getDoubleIfPresent(result, "stop_lat", columnForName); - stop.stop_lon = getDoubleIfPresent(result, "stop_lon", columnForName); - stop.zone_id = getStringIfPresent(result, "zone_id", columnForName); - stop.parent_station = getStringIfPresent(result, "parent_station", columnForName); - stop.stop_timezone = getStringIfPresent(result, "stop_timezone", columnForName); - stop.stop_url = getUrlIfPresent (result, "stop_url", columnForName); - stop.location_type = getIntIfPresent (result, "location_type", columnForName); - stop.wheelchair_boarding = getIntIfPresent(result, "wheelchair_boarding", columnForName); - stop.platform_code = getStringIfPresent(result, "platform_code", columnForName); + stop.stop_id = getStringIfPresent(result, Stop.STOP_ID_FIELD, columnForName); + stop.stop_code = getStringIfPresent(result, Stop.STOP_CODE_FIELD, columnForName); + stop.stop_name = getStringIfPresent(result, Stop.STOP_NAME_FIELD, columnForName); + stop.stop_desc = getStringIfPresent(result, Stop.STOP_DESC_FIELD, columnForName); + stop.stop_lat = getDoubleIfPresent(result, Stop.STOP_LAT_FIELD, columnForName); + stop.stop_lon = getDoubleIfPresent(result, Stop.STOP_LON_FIELD, columnForName); + stop.zone_id = getStringIfPresent(result, Stop.ZONE_ID_FIELD, columnForName); + stop.parent_station = getStringIfPresent(result, Stop.PARENT_STATION_FIELD, columnForName); + stop.stop_timezone = getStringIfPresent(result, Stop.STOP_TIMEZONE_FIELD, columnForName); + stop.stop_url = getUrlIfPresent(result, Stop.STOP_URL_FIELD, columnForName); + stop.location_type = getIntIfPresent(result, Stop.LOCATION_TYPE_FIELD, columnForName); + stop.wheelchair_boarding = getIntIfPresent(result, Stop.WHEELCHAIR_BOARDING_FIELD, columnForName); + stop.platform_code = getStringIfPresent(result, Stop.PLATFORM_CODE_FIELD, columnForName); + stop.stop_area_ids = getStringIfPresent(result, Stop.STOP_AREA_IDS_FIELD, columnForName); return stop; }; @@ -246,13 +246,6 @@ public interface EntityPopulator { return area; }; - EntityPopulator STOP_AREA = (result, columnForName) -> { - StopArea stopArea = new StopArea(); - stopArea.area_id = getStringIfPresent(result, StopArea.AREA_ID_NAME, columnForName); - stopArea.stop_id = getStringIfPresent(result, StopArea.STOP_ID_NAME, columnForName); - return stopArea; - }; - EntityPopulator FARE_MEDIA = (result, columnForName) -> { FareMedia fareMedia = new FareMedia(); fareMedia.fare_media_id = getStringIfPresent(result, FareMedia.FARE_MEDIA_ID_NAME, columnForName); diff --git a/src/main/java/com/conveyal/gtfs/loader/Feed.java b/src/main/java/com/conveyal/gtfs/loader/Feed.java index 0711429e8..f1a6d6263 100644 --- a/src/main/java/com/conveyal/gtfs/loader/Feed.java +++ b/src/main/java/com/conveyal/gtfs/loader/Feed.java @@ -41,7 +41,6 @@ public class Feed { public final TableReader stopTimes; public final TableReader patterns; public final TableReader areas; - public final TableReader stopAreas; public final TableReader fareMedia; public final TableReader fareProducts; public final TableReader timeFrames; @@ -71,7 +70,6 @@ public Feed (DataSource dataSource, String databaseSchemaPrefix) { stopTimes = new JDBCTableReader(Table.STOP_TIMES, dataSource, databaseSchemaPrefix, EntityPopulator.STOP_TIME); patterns = new JDBCTableReader(Table.PATTERNS, dataSource, databaseSchemaPrefix, EntityPopulator.PATTERN); areas = new JDBCTableReader(Table.AREAS, dataSource, databaseSchemaPrefix, EntityPopulator.AREA); - stopAreas = new JDBCTableReader(Table.STOP_AREAS, dataSource, databaseSchemaPrefix, EntityPopulator.STOP_AREA); fareMedia = new JDBCTableReader(Table.FARE_MEDIAS, dataSource, databaseSchemaPrefix, EntityPopulator.FARE_MEDIA); fareProducts = new JDBCTableReader(Table.FARE_PRODUCTS, dataSource, databaseSchemaPrefix, EntityPopulator.FARE_PRODUCT); timeFrames = new JDBCTableReader(Table.TIME_FRAMES, dataSource, databaseSchemaPrefix, EntityPopulator.TIME_FRAME); diff --git a/src/main/java/com/conveyal/gtfs/loader/JdbcGTFSFeedConverter.java b/src/main/java/com/conveyal/gtfs/loader/JdbcGTFSFeedConverter.java index 0734dae37..06b328538 100644 --- a/src/main/java/com/conveyal/gtfs/loader/JdbcGTFSFeedConverter.java +++ b/src/main/java/com/conveyal/gtfs/loader/JdbcGTFSFeedConverter.java @@ -125,7 +125,6 @@ public FeedLoadResult loadTables () { // Fares v2. copyEntityToSql(gtfsFeed.areas.values(), Table.AREAS); - copyEntityToSql(gtfsFeed.stop_areas.values(), Table.STOP_AREAS); copyEntityToSql(gtfsFeed.fare_medias.values(), Table.FARE_MEDIAS); copyEntityToSql(gtfsFeed.fare_products.values(), Table.FARE_PRODUCTS); copyEntityToSql(gtfsFeed.time_frames.values(), Table.TIME_FRAMES); diff --git a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java index 86172a72f..ffd7e7dd3 100644 --- a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java +++ b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java @@ -345,7 +345,6 @@ public FeedLoadResult exportTables() { } result.areas = export(Table.AREAS, connection); - result.stopAreas = export(Table.STOP_AREAS, connection); result.fareMedias = export(Table.FARE_MEDIAS, connection); result.fareProducts = export(Table.FARE_PRODUCTS, connection); result.timeFrames = export(Table.TIME_FRAMES, connection); diff --git a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsLoader.java b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsLoader.java index 299af9484..4e5d2465c 100644 --- a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsLoader.java +++ b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsLoader.java @@ -4,6 +4,7 @@ import com.conveyal.gtfs.error.NewGTFSErrorType; import com.conveyal.gtfs.error.SQLErrorStorage; import com.conveyal.gtfs.storage.StorageException; +import com.conveyal.gtfs.util.CsvReaderUtil; import com.csvreader.CsvReader; import com.google.common.hash.HashCode; import com.google.common.hash.Hashing; @@ -170,7 +171,6 @@ public FeedLoadResult loadTables() { result.shapes = load(Table.SHAPES); result.patterns = load(Table.PATTERNS); // refs shapes and routes. result.stops = load(Table.STOPS); - result.stopAreas = load(Table.STOP_AREAS); result.fareRules = load(Table.FARE_RULES); result.trips = load(Table.TRIPS); // refs routes result.transfers = load(Table.TRANSFERS); // refs trips. @@ -232,7 +232,7 @@ private void registerFeed(File gtfsFile) { // FIXME is this extra CSV reader used anymore? Check comment below. // First, inspect feed_info.txt to extract the ID and version. // We could get this with SQL after loading, but feed_info, feed_id and feed_version are all optional. - CsvReader csvReader = Table.FEED_INFO.getCsvReader(zip, errorStorage); + CsvReader csvReader = CsvReaderUtil.getCsvReaderAccordingToFileName(Table.FEED_INFO, zip, errorStorage); String feedId = "", feedVersion = ""; if (csvReader != null) { // feed_info.txt has been found and opened. @@ -331,7 +331,7 @@ private int getTableSize(Table table) { * @return number of rows that were loaded. */ private int loadInternal(Table table) throws Exception { - CsvReader csvReader = table.getCsvReader(zip, errorStorage); + CsvReader csvReader = CsvReaderUtil.getCsvReaderAccordingToFileName(table, zip, errorStorage); if (csvReader == null) { LOG.info("File {} not found in gtfs zip file.", Table.getTableFileNameWithExtension(table.name)); // This GTFS table could not be opened in the zip, even in a subdirectory. @@ -400,6 +400,7 @@ private int loadInternal(Table table) throws Exception { int lineNumber = ((int) csvReader.getCurrentRecord()) + 2; if (lineNumber % 500_000 == 0) LOG.info("Processed {}", human(lineNumber)); if (csvReader.getColumnCount() != fields.length) { + System.out.println(Arrays.toString(csvReader.getHeaders())); String badValues = String.format("expected=%d; found=%d", fields.length, csvReader.getColumnCount()); errorStorage.storeError(NewGTFSError.forLine(table, lineNumber, WRONG_NUMBER_OF_FIELDS, badValues)); continue; diff --git a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsSnapshotter.java b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsSnapshotter.java index 0f3bb12a0..c04fab949 100644 --- a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsSnapshotter.java +++ b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsSnapshotter.java @@ -117,7 +117,6 @@ public SnapshotResult copyTables() { result.translations = copy(Table.TRANSLATIONS, true); // Fares v2. result.areas = copy(Table.AREAS, true); - result.stopAreas = copy(Table.STOP_AREAS, true); result.fareMedias = copy(Table.FARE_MEDIAS, true); result.fareProducts = copy(Table.FARE_PRODUCTS, true); result.timeFrames = copy(Table.TIME_FRAMES, true); diff --git a/src/main/java/com/conveyal/gtfs/loader/Table.java b/src/main/java/com/conveyal/gtfs/loader/Table.java index ac64660d3..609272835 100644 --- a/src/main/java/com/conveyal/gtfs/loader/Table.java +++ b/src/main/java/com/conveyal/gtfs/loader/Table.java @@ -31,22 +31,15 @@ import com.conveyal.gtfs.model.ScheduleException; import com.conveyal.gtfs.model.ShapePoint; import com.conveyal.gtfs.model.Stop; -import com.conveyal.gtfs.model.StopArea; import com.conveyal.gtfs.model.StopTime; import com.conveyal.gtfs.model.TimeFrame; import com.conveyal.gtfs.model.Transfer; import com.conveyal.gtfs.model.Translation; import com.conveyal.gtfs.model.Trip; import com.conveyal.gtfs.storage.StorageException; -import com.csvreader.CsvReader; -import org.apache.commons.io.input.BOMInputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.nio.file.Paths; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -56,18 +49,14 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; import static com.conveyal.gtfs.error.NewGTFSErrorType.DUPLICATE_HEADER; -import static com.conveyal.gtfs.error.NewGTFSErrorType.TABLE_IN_SUBDIRECTORY; import static com.conveyal.gtfs.loader.JdbcGtfsLoader.sanitize; import static com.conveyal.gtfs.loader.Requirement.EDITOR; import static com.conveyal.gtfs.loader.Requirement.EXTENSION; @@ -294,7 +283,8 @@ public Table (String name, Class entityClass, Requirement requ new StringField("parent_station", OPTIONAL).requireConditions(), new StringField("stop_timezone", OPTIONAL), new ShortField("wheelchair_boarding", OPTIONAL, 2), - new StringField("platform_code", OPTIONAL) + new StringField("platform_code", OPTIONAL), + new StringField("stop_area_ids", OPTIONAL) ).restrictDelete() .addPrimaryKey() .addPrimaryKeyNames("stop_id"); @@ -306,13 +296,6 @@ public Table (String name, Class entityClass, Requirement requ .restrictDelete() .addPrimaryKeyNames(Area.AREA_ID_NAME); - public static final Table STOP_AREAS = new Table(StopArea.TABLE_NAME, StopArea.class, OPTIONAL, - new StringField(StopArea.AREA_ID_NAME, REQUIRED).isReferenceTo(AREAS), - new StringField(StopArea.STOP_ID_NAME, REQUIRED).isReferenceTo(STOPS) - ) - .keyFieldIsNotUnique() - .addPrimaryKeyNames(StopArea.AREA_ID_NAME, StopArea.STOP_ID_NAME); - public static final Table FARE_MEDIAS = new Table(FareMedia.TABLE_NAME, FareMedia.class, OPTIONAL, new StringField(FareMedia.FARE_MEDIA_ID_NAME, REQUIRED), new StringField(FareMedia.FARE_MEDIA_NAME_NAME, OPTIONAL), @@ -567,7 +550,6 @@ public Table (String name, Class entityClass, Requirement requ SHAPES, STOPS, AREAS, - STOP_AREAS, FARE_RULES, PATTERN_STOP, TRANSFERS, @@ -770,46 +752,6 @@ private static String getTableFileName(String tableName, String fileExtension) { : String.format("%s%s", tableName, fileExtension); } - /** - * In GTFS feeds, all files are supposed to be in the root of the zip file, but feed producers often put them - * in a subdirectory. This function will search subdirectories if the entry is not found in the root. - * It records an error if the entry is in a subdirectory (as long as errorStorage is not null). - * It then creates a CSV reader for that table if it's found. - */ - public CsvReader getCsvReader(ZipFile zipFile, SQLErrorStorage sqlErrorStorage) { - final String tableFileName = getTableFileNameWithExtension(this.name); - ZipEntry entry = zipFile.getEntry(tableFileName); - if (entry == null) { - // Table was not found, check if it is in a subdirectory. - Enumeration entries = zipFile.entries(); - while (entries.hasMoreElements()) { - ZipEntry e = entries.nextElement(); - if (Paths.get(e.getName()).getFileName().toString().equals(tableFileName)) { - entry = e; - if (sqlErrorStorage != null) sqlErrorStorage.storeError(NewGTFSError.forTable(this, TABLE_IN_SUBDIRECTORY)); - break; - } - } - } - if (entry == null) return null; - try { - InputStream zipInputStream = zipFile.getInputStream(entry); - // Skip any byte order mark that may be present. Files must be UTF-8, - // but the GTFS spec says that "files that include the UTF byte order mark are acceptable". - InputStream bomInputStream = new BOMInputStream(zipInputStream); - CsvReader csvReader = new CsvReader(bomInputStream, ',', Charset.forName("UTF8")); - // Don't skip empty records (this is set to true by default on CsvReader. We want to check for empty records - // during table load, so that they are logged as validation issues (WRONG_NUMBER_OF_FIELDS). - csvReader.setSkipEmptyRecords(false); - csvReader.readHeaders(); - return csvReader; - } catch (IOException e) { - LOG.error("Exception while opening zip entry: {}", e); - e.printStackTrace(); - return null; - } - } - /** * Join a list of fields with a comma + space separator. */ diff --git a/src/main/java/com/conveyal/gtfs/model/Entity.java b/src/main/java/com/conveyal/gtfs/model/Entity.java index 20ba92488..fe99c8cdc 100644 --- a/src/main/java/com/conveyal/gtfs/model/Entity.java +++ b/src/main/java/com/conveyal/gtfs/model/Entity.java @@ -14,17 +14,16 @@ import com.conveyal.gtfs.error.TimeParseError; import com.conveyal.gtfs.error.URLParseError; import com.conveyal.gtfs.loader.DateField; +import com.conveyal.gtfs.util.CsvReaderUtil; import com.conveyal.gtfs.util.Deduplicator; import com.csvreader.CsvReader; import com.csvreader.CsvWriter; -import org.apache.commons.io.input.BOMInputStream; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.FilterOutputStream; import java.io.IOException; -import java.io.InputStream; import java.io.OutputStream; import java.io.Serializable; import java.net.MalformedURLException; @@ -37,9 +36,11 @@ import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; import java.util.Iterator; +import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; @@ -305,12 +306,8 @@ public void loadTable(ZipFile zip) throws IOException { if (entry == null) return; } LOG.info("Loading GTFS table {} from {}", tableName, entry); - InputStream zis = zip.getInputStream(entry); - // skip any byte order mark that may be present. Files must be UTF-8, - // but the GTFS spec says that "files that include the UTF byte order mark are acceptable" - InputStream bis = new BOMInputStream(zis); - CsvReader reader = new CsvReader(bis, ',', Charset.forName("UTF8")); - this.reader = reader; + List errors = new ArrayList<>(); + this.reader = CsvReaderUtil.getCsvReaderAccordingToFileName(tableName, zip, entry, errors); boolean hasHeaders = reader.readHeaders(); if (!hasHeaders) { feed.errors.add(new EmptyTableError(tableName)); diff --git a/src/main/java/com/conveyal/gtfs/model/Stop.java b/src/main/java/com/conveyal/gtfs/model/Stop.java index 24923380e..9c431dfd9 100644 --- a/src/main/java/com/conveyal/gtfs/model/Stop.java +++ b/src/main/java/com/conveyal/gtfs/model/Stop.java @@ -1,15 +1,34 @@ package com.conveyal.gtfs.model; import com.conveyal.gtfs.GTFSFeed; +import com.csvreader.CsvReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringReader; import java.net.URL; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import static com.conveyal.gtfs.util.CsvReaderUtil.hasExpectedNumberOfColumns; public class Stop extends Entity { + private static final Logger LOG = LoggerFactory.getLogger(Stop.class); + private static final long serialVersionUID = 464065335273514677L; public String stop_id; public String stop_code; @@ -18,13 +37,55 @@ public class Stop extends Entity { public double stop_lat; public double stop_lon; public String zone_id; - public URL stop_url; - public int location_type; + public URL stop_url; + public int location_type; public String parent_station; public String stop_timezone; public int wheelchair_boarding; public String feed_id; public String platform_code; + public String stop_area_ids; + + public static final String STOP_ID_FIELD = "stop_id"; + public static final String STOP_CODE_FIELD = "stop_code"; + public static final String STOP_NAME_FIELD = "stop_name"; + public static final String STOP_DESC_FIELD = "stop_desc"; + public static final String STOP_LAT_FIELD = "stop_lat"; + public static final String STOP_LON_FIELD = "stop_lon"; + public static final String ZONE_ID_FIELD = "zone_id"; + public static final String STOP_URL_FIELD = "stop_url"; + public static final String LOCATION_TYPE_FIELD = "location_type"; + public static final String PARENT_STATION_FIELD = "parent_station"; + public static final String STOP_TIMEZONE_FIELD = "stop_timezone"; + public static final String WHEELCHAIR_BOARDING_FIELD = "wheelchair_boarding"; + public static final String PLATFORM_CODE_FIELD = "platform_code"; + public static final String STOP_AREA_IDS_FIELD = "stop_area_ids"; + + public static final String STOPS_FILE_NAME = "stops.txt"; + public static final String AREA_ID_FIELD = "area_id"; + public static final String STOP_AREAS_FILE_NAME = "stop_areas.txt"; + public static final int STOP_AREAS_NUMBER_OF_HEADERS = 2; + private static final String[] CSV_HEADER_FOR_WRITE = new String[] { + STOP_ID_FIELD, + STOP_CODE_FIELD, + STOP_NAME_FIELD, + STOP_DESC_FIELD, + STOP_LAT_FIELD, + STOP_LON_FIELD, + ZONE_ID_FIELD, + STOP_URL_FIELD, + LOCATION_TYPE_FIELD, + PARENT_STATION_FIELD, + STOP_TIMEZONE_FIELD, + WHEELCHAIR_BOARDING_FIELD, + PLATFORM_CODE_FIELD + }; + private static final String CSV_HEADER_FOR_MERGE = String.format( + "%s,%s%s", + String.join(",", CSV_HEADER_FOR_WRITE), + STOP_AREA_IDS_FIELD, + System.lineSeparator() + ); @Override public String getId () { @@ -52,6 +113,7 @@ public void setStatementParameters(PreparedStatement statement, boolean setDefau statement.setString(oneBasedIndex++, stop_timezone); setIntParameter(statement, oneBasedIndex++, wheelchair_boarding); statement.setString(oneBasedIndex++, platform_code); + statement.setString(oneBasedIndex, stop_area_ids); } public static class Loader extends Entity.Loader { @@ -69,21 +131,22 @@ protected boolean isRequired() { public void loadOneRow() throws IOException { Stop s = new Stop(); s.id = row + 1; // offset line number by 1 to account for 0-based row index - s.stop_id = getStringField("stop_id", true); - s.stop_code = getStringField("stop_code", false); - s.stop_name = getStringField("stop_name", true); - s.stop_desc = getStringField("stop_desc", false); - s.stop_lat = getDoubleField("stop_lat", true, -90D, 90D); - s.stop_lon = getDoubleField("stop_lon", true, -180D, 180D); - s.zone_id = getStringField("zone_id", false); - s.stop_url = getUrlField("stop_url", false); - s.location_type = getIntField("location_type", false, 0, 1); - s.parent_station = getStringField("parent_station", false); - s.stop_timezone = getStringField("stop_timezone", false); - s.wheelchair_boarding = getIntField("wheelchair_boarding", false, 0, 2); + s.stop_id = getStringField(STOP_ID_FIELD, true); + s.stop_code = getStringField(STOP_CODE_FIELD, false); + s.stop_name = getStringField(STOP_NAME_FIELD, true); + s.stop_desc = getStringField(STOP_DESC_FIELD, false); + s.stop_lat = getDoubleField(STOP_LAT_FIELD, true, -90D, 90D); + s.stop_lon = getDoubleField(STOP_LON_FIELD, true, -180D, 180D); + s.zone_id = getStringField(ZONE_ID_FIELD, false); + s.stop_url = getUrlField(STOP_URL_FIELD, false); + s.location_type = getIntField(LOCATION_TYPE_FIELD, false, 0, 1); + s.parent_station = getStringField(PARENT_STATION_FIELD, false); + s.stop_timezone = getStringField(STOP_TIMEZONE_FIELD, false); + s.wheelchair_boarding = getIntField(WHEELCHAIR_BOARDING_FIELD, false, 0, 2); s.feed = feed; s.feed_id = feed.feedId; - s.platform_code = getStringField("platform_code", false); + s.platform_code = getStringField(PLATFORM_CODE_FIELD, false); + s.stop_area_ids = getStringField(STOP_AREA_IDS_FIELD, false); /* TODO check ref integrity later, this table self-references via parent_station */ // Attempting to put a null key or value will cause an NPE in BTreeMap if (s.stop_id != null) feed.stops.put(s.stop_id, s); @@ -98,8 +161,7 @@ public Writer (GTFSFeed feed) { @Override public void writeHeaders() throws IOException { - writer.writeRecord(new String[] {"stop_id", "stop_code", "stop_name", "stop_desc", "stop_lat", "stop_lon", "zone_id", - "stop_url", "location_type", "parent_station", "stop_timezone", "wheelchair_boarding", "platform_code"}); + writer.writeRecord(CSV_HEADER_FOR_WRITE); } @Override @@ -125,4 +187,131 @@ public Iterator iterator() { return feed.stops.values().iterator(); } } + + public static void updateStopAreas(Map stops, Map stopAreas) { + // Group StopArea IDs by Stop ID + Map> stopAreasByStopId = new HashMap<>(); + + stopAreas.values().forEach(stopArea -> + stopAreasByStopId + .computeIfAbsent(stopArea.stop_id, id -> new HashSet<>()) + .add(stopArea.area_id) + ); + + // Update each Stop with corresponding StopArea IDs + stops.values().forEach(stop -> stop.stop_area_ids = Optional.ofNullable(stopAreasByStopId.get(stop.stop_id)) + .map(areaIds -> String.join(";", areaIds)) + .orElse("")); + } + + public static CsvReader mergeStopAreasIntoStops( + CsvReader stopsReader, + Map> stopAreasGroupedByStopId + ) { + List rows = new ArrayList<>(); + try { + while (stopsReader.readRecord()) { + String stopId = stopsReader.get(STOP_ID_FIELD); + String stopAreaIds = Optional.ofNullable(stopAreasGroupedByStopId.get(stopId)) + .map(areas -> String.join(";", areas)) + .orElse(""); + rows.add(createRow(stopsReader, stopId, stopAreaIds)); + } + return (rows.isEmpty()) + ? stopsReader + : produceCsvPayload(rows); + } catch (Exception e) { + LOG.error("Error while merging stops", e); + // Any issues, return the original stops reader (minus stop areas). + return stopsReader; + } + } + + private static String createRow(CsvReader stopsReader, String stopId, String stopAreaIds) throws IOException { + StringBuilder row = new StringBuilder(); + row.append(stopId).append(","); + row.append(stopsReader.get(STOP_CODE_FIELD)).append(","); + row.append(stopsReader.get(STOP_NAME_FIELD)).append(","); + row.append(stopsReader.get(STOP_DESC_FIELD)).append(","); + row.append(stopsReader.get(STOP_LAT_FIELD)).append(","); + row.append(stopsReader.get(STOP_LON_FIELD)).append(","); + row.append(stopsReader.get(ZONE_ID_FIELD)).append(","); + row.append(stopsReader.get(STOP_URL_FIELD)).append(","); + row.append(stopsReader.get(LOCATION_TYPE_FIELD)).append(","); + row.append(stopsReader.get(PARENT_STATION_FIELD)).append(","); + row.append(stopsReader.get(STOP_TIMEZONE_FIELD)).append(","); + row.append(stopsReader.get(WHEELCHAIR_BOARDING_FIELD)).append(","); + row.append(stopsReader.get(PLATFORM_CODE_FIELD)).append(","); + row.append(stopAreaIds); + return row.toString(); + } + + /** + * Convert the multiple stops (with stop areas) back into CSV, with header and return a {@link CsvReader} + * representation. + */ + private static CsvReader produceCsvPayload(List rows) { + StringBuilder csvContent = new StringBuilder(); + csvContent.append(CSV_HEADER_FOR_MERGE); + rows.forEach(row -> csvContent.append(row).append(System.lineSeparator())); + return new CsvReader(new StringReader(csvContent.toString())); + } + + /** + * Extract the stop areas from file and group by stop id. This is to allow for easier CRUD by the DT UI. + */ + public static Map> groupStopAreaIds(CsvReader csvReader, List errors) { + Map> stopAreasGroupedByStopId = new HashMap<>(); + + try { + while (csvReader.readRecord()) { + if (!hasExpectedNumberOfColumns(csvReader, errors, 2)) { + continue; + } + String stopAreaId = csvReader.get(AREA_ID_FIELD); + String stopId = csvReader.get(STOP_ID_FIELD); + stopAreasGroupedByStopId.computeIfAbsent(stopId, k -> new HashSet<>()).add(stopAreaId); + } + return stopAreasGroupedByStopId; + } catch (IOException e) { + return Collections.emptyMap(); + } + } + + /** + * Expand the stop area ids and write to zip file. + */ + public static void writeStopAreasToFile(ZipOutputStream zipOutputStream, List stops) throws IOException { + // Create entry for table. + zipOutputStream.putNextEntry(new ZipEntry(STOP_AREAS_FILE_NAME)); + // Create and use PrintWriter, but don't close. This is done when the zip entry is closed. + PrintWriter p = new PrintWriter(zipOutputStream); + p.print(packStopAreas(stops)); + p.flush(); + zipOutputStream.closeEntry(); + } + + /** + * Expand all stop area ids into a single row for each area id. This is to conform with the GTFS Fares v2 standard. + */ + public static String packStopAreas(List stops) { + StringBuilder csvContent = new StringBuilder(AREA_ID_FIELD) + .append(",") + .append(STOP_ID_FIELD) + .append(System.lineSeparator()); + + stops.stream() + .filter(stop -> stop.stop_area_ids != null) + .forEach(stop -> { + String[] areaIds = stop.stop_area_ids.split(";"); + for (String areaId : areaIds) { + csvContent + .append(areaId) + .append(",") + .append(stop.stop_id) + .append(System.lineSeparator()); + } + }); + return csvContent.toString(); + } } diff --git a/src/main/java/com/conveyal/gtfs/model/StopArea.java b/src/main/java/com/conveyal/gtfs/model/StopArea.java index 04f0df456..9190c2b02 100644 --- a/src/main/java/com/conveyal/gtfs/model/StopArea.java +++ b/src/main/java/com/conveyal/gtfs/model/StopArea.java @@ -7,6 +7,10 @@ import java.sql.SQLException; import java.util.Iterator; +/** + * This is only included to read the values from file. No stop areas are saved to the database. Instead, they are merged + * with the appropriate stop. + */ public class StopArea extends Entity { private static final long serialVersionUID = -2825890165823575940L; @@ -23,10 +27,6 @@ public String getId () { return createPrimaryKey(area_id, stop_id); } - /** - * Sets the parameters for a prepared statement following the parameter order defined in - * {@link com.conveyal.gtfs.loader.Table#STOP_AREAS}. JDBC prepared statement parameters use a one-based index. - */ @Override public void setStatementParameters(PreparedStatement statement, boolean setDefaultId) throws SQLException { int oneBasedIndex = 1; @@ -35,7 +35,7 @@ public void setStatementParameters(PreparedStatement statement, boolean setDefau statement.setString(oneBasedIndex, stop_id); } - public static class Loader extends Entity.Loader { + public static class Loader extends Entity.Loader { public Loader(GTFSFeed feed) { super(feed, TABLE_NAME); @@ -88,6 +88,4 @@ public Iterator iterator() { return this.feed.stop_areas.values().iterator(); } } -} - - +} \ No newline at end of file diff --git a/src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java b/src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java new file mode 100644 index 000000000..e62836932 --- /dev/null +++ b/src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java @@ -0,0 +1,193 @@ +package com.conveyal.gtfs.util; + +import com.conveyal.gtfs.error.NewGTFSError; +import com.conveyal.gtfs.error.SQLErrorStorage; +import com.conveyal.gtfs.loader.Table; +import com.conveyal.gtfs.model.Stop; +import com.csvreader.CsvReader; +import org.apache.commons.io.input.BOMInputStream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; + +import static com.conveyal.gtfs.error.NewGTFSErrorType.TABLE_IN_SUBDIRECTORY; +import static com.conveyal.gtfs.loader.Table.getTableFileNameWithExtension; +import static com.conveyal.gtfs.model.Stop.STOPS_FILE_NAME; + +public class CsvReaderUtil { + + private static final Logger LOG = LoggerFactory.getLogger(CsvReaderUtil.class); + + private CsvReaderUtil() { + throw new IllegalStateException("Utility class"); + } + + /** + * In GTFS feeds, all files are supposed to be in the root of the zip file, but feed producers often put them + * in a subdirectory. This function will search subdirectories if the entry is not found in the root. + * It records an error if the entry is in a subdirectory (as long as errorStorage is not null). + * It then creates a {@link CsvReader} for that table if it's found. + */ + public static CsvReader getCsvReaderAccordingToFileName(Table table, ZipFile zipFile, SQLErrorStorage sqlErrorStorage) { + final String tableFileName = getTableFileNameWithExtension(table.name); + ZipEntry entry = zipFile.getEntry(tableFileName); + if (entry == null) { + // Table was not found, check if it is in a subdirectory. + Enumeration entries = zipFile.entries(); + while (entries.hasMoreElements()) { + ZipEntry e = entries.nextElement(); + // Include the file separator prefix to force the complete file name to be considered. + // This prevents stop_areas.txt from being loaded instead of areas.txt. + if (e.getName().endsWith(String.format("%s%s", File.separator, tableFileName))) { + entry = e; + if (sqlErrorStorage != null) { + sqlErrorStorage.storeError(NewGTFSError.forTable(table, TABLE_IN_SUBDIRECTORY)); + } + break; + } + } + } + if (entry == null) { + return null; + } + + try { + List errors = new ArrayList<>(); + CsvReader csvReader = getCsvReaderAccordingToFileName(tableFileName, zipFile, entry, errors); + if (csvReader == null) { + return null; + } + if (!errors.isEmpty() && sqlErrorStorage != null) { + errors.forEach(error -> sqlErrorStorage.storeError(NewGTFSError.forFeed(null, error))); + } + // Don't skip empty records. This is set to true by default on CsvReader. We want to check for empty records + // during table load, so that they are logged as validation issues (WRONG_NUMBER_OF_FIELDS). + csvReader.setSkipEmptyRecords(false); + csvReader.readHeaders(); + return csvReader; + } catch (IOException e) { + LOG.error("Exception while opening zip entry: {}", entry, e); + e.printStackTrace(); + return null; + } + } + + /** + * Create a {@link CsvReader} depending on the table to be loaded. If the table is location related unpack the data + * first according to each individual case and load into a CSV reader, else, read the table contents directly into + * the CSV reader. + */ + public static CsvReader getCsvReaderAccordingToFileName( + String tableFileName, + ZipFile zipFile, + ZipEntry entry, + List errors + ) throws IOException { + CsvReader csvReader; + if (tableFileName.equals(STOPS_FILE_NAME)) { + csvReader = processStops(zipFile, entry, errors); + } else { + csvReader = getCsvReaderFromFile(zipFile, entry); + } + return csvReader; + } + + /** + * If the feed contains stop areas extract and merge with stops. If not, just return stops. + */ + private static CsvReader processStops( + ZipFile zipFile, + ZipEntry stopsEntry, + List errors + ) throws IOException { + ZipEntry stopAreasEntry = zipFile.getEntry(Stop.STOP_AREAS_FILE_NAME); + CsvReader stopsReader = getCsvReaderFromFile(zipFile, stopsEntry); + if (stopAreasEntry != null) { + stopsReader.setSkipEmptyRecords(false); + stopsReader.readHeaders(); + // Stop areas present in zip file. + CsvReader stopAreasReader = getCsvReaderForFile(zipFile, stopAreasEntry, errors, Stop.STOP_AREAS_NUMBER_OF_HEADERS); + if (stopAreasReader != null) { + Map> stopAreas = Stop.groupStopAreaIds(stopAreasReader, errors); + if (!stopAreas.isEmpty()) { + // Merge stop areas into stops. + return Stop.mergeStopAreasIntoStops(stopsReader, stopAreas); + } + } + } + // No stop areas, provide just stops as defined in the feed. + return stopsReader; + } + + /** + * Create a {@link CsvReader} from file and check that the number of headers meets the expected number of headers. + */ + private static CsvReader getCsvReaderForFile( + ZipFile zipFile, + ZipEntry entry, + List errors, + int numOfHeaders + ) throws IOException { + CsvReader csvReader = getCsvReaderFromFile(zipFile, entry); + csvReader.setSkipEmptyRecords(false); + csvReader.readHeaders(); + String[] headers = csvReader.getHeaders(); + if (headers.length != numOfHeaders) { + String message = String.format( + "Wrong number of headers, expected=%d; found=%d in %s.", + numOfHeaders, + headers.length, + entry.getName() + ); + LOG.warn(message); + if (errors != null) { + errors.add(message); + } + return null; + } + return csvReader; + } + + /** + * Create a {@link CsvReader} from the provided file. Warning: Both input streams have to remain open so + * downstream processing using the {@link CsvReader} will continue to function. + */ + public static CsvReader getCsvReaderFromFile(ZipFile zipFile, ZipEntry entry) throws IOException { + InputStream zipInputStream = zipFile.getInputStream(entry); + // Skip any byte order mark that may be present. Files must be UTF-8, + // but the GTFS spec says that "files that include the UTF byte order mark are acceptable". + InputStream bomInputStream = new BOMInputStream(zipInputStream); + return new CsvReader(bomInputStream, ',', StandardCharsets.UTF_8); + } + + /** + * Confirm if the current row being processed has the expected number of columns. + */ + public static boolean hasExpectedNumberOfColumns(CsvReader csvReader, List errors, int expectedNumberOfColumns) { + int lineNumber = ((int) csvReader.getCurrentRecord()) + 2; + if (csvReader.getColumnCount() != expectedNumberOfColumns) { + String message = String.format("Wrong number of columns for line number=%d; expected=%d; found=%d.", + lineNumber, + expectedNumberOfColumns, + csvReader.getColumnCount() + ); + LOG.warn(message); + if (errors != null) { + errors.add(message); + } + return false; + } + return true; + } +} \ No newline at end of file diff --git a/src/test/java/com/conveyal/gtfs/GTFSTest.java b/src/test/java/com/conveyal/gtfs/GTFSTest.java index 3da9a8841..8b9b0a52c 100644 --- a/src/test/java/com/conveyal/gtfs/GTFSTest.java +++ b/src/test/java/com/conveyal/gtfs/GTFSTest.java @@ -6,7 +6,6 @@ import com.conveyal.gtfs.loader.JdbcGtfsExporter; import com.conveyal.gtfs.loader.SnapshotResult; import com.conveyal.gtfs.loader.Table; -import com.conveyal.gtfs.loader.TableLoadResult; import com.conveyal.gtfs.storage.ErrorExpectation; import com.conveyal.gtfs.storage.ExpectedFieldType; import com.conveyal.gtfs.storage.PersistenceExpectation; @@ -42,17 +41,14 @@ import java.util.Arrays; import java.util.Collection; import java.util.Iterator; -import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; -import static com.conveyal.gtfs.TestUtils.getResourceFileName; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.core.IsNull.nullValue; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -334,7 +330,6 @@ void canLoadAndExportFaresV2Feed() throws IOException { new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), - new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), new ErrorExpectation(NewGTFSErrorType.FEED_TRAVEL_TIMES_ROUNDED), new ErrorExpectation(NewGTFSErrorType.DATE_NO_SERVICE) ); @@ -411,13 +406,6 @@ void canLoadAndExportFaresV2Feed() throws IOException { new RecordExpectation("route_id", "1") } ), - new PersistenceExpectation( - "stop_areas", - new RecordExpectation[]{ - new RecordExpectation("stop_id", "4u6g"), - new RecordExpectation("area_id", "area_route_426_downtown") - } - ), new PersistenceExpectation( "timeframes", new RecordExpectation[]{ diff --git a/src/test/java/com/conveyal/gtfs/dto/StopAreaDTO.java b/src/test/java/com/conveyal/gtfs/dto/StopAreaDTO.java deleted file mode 100644 index 66003fca6..000000000 --- a/src/test/java/com/conveyal/gtfs/dto/StopAreaDTO.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.conveyal.gtfs.dto; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * DTO used to model expected {@link com.conveyal.gtfs.model.StopArea} JSON structure for the editor. NOTE: reference types - * (e.g., Integer and Double) are used here in order to model null/empty values in JSON object. - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class StopAreaDTO { - public Integer id; - public String area_id; - public String stop_id; -} diff --git a/src/test/java/com/conveyal/gtfs/dto/StopDTO.java b/src/test/java/com/conveyal/gtfs/dto/StopDTO.java index 66a333863..2ada1976c 100644 --- a/src/test/java/com/conveyal/gtfs/dto/StopDTO.java +++ b/src/test/java/com/conveyal/gtfs/dto/StopDTO.java @@ -15,4 +15,5 @@ public class StopDTO { public Integer location_type; public Integer wheelchair_boarding; public String platform_code; + public String stop_area_ids; } diff --git a/src/test/java/com/conveyal/gtfs/loader/JDBCTableWriterFaresV2Test.java b/src/test/java/com/conveyal/gtfs/loader/JDBCTableWriterFaresV2Test.java index 470bf7334..0da891926 100644 --- a/src/test/java/com/conveyal/gtfs/loader/JDBCTableWriterFaresV2Test.java +++ b/src/test/java/com/conveyal/gtfs/loader/JDBCTableWriterFaresV2Test.java @@ -118,11 +118,6 @@ private static Stream createEntityInput() throws IOException { getEntityFromFile("areas.json"), getEntityFromFile("areas_updated.json") ), - Arguments.of( - Table.STOP_AREAS, - getEntityFromFile("stop_areas.json"), - getEntityFromFile("stop_areas_updated.json") - ), Arguments.of( Table.TIME_FRAMES, getEntityFromFile("time_frames.json"), From a3481b0660acf998c6179be89033e6d45b94583f Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Thu, 31 Jul 2025 15:14:14 +0100 Subject: [PATCH 17/40] improvement(A few minor changes with comments): --- src/main/java/com/conveyal/gtfs/GTFSFeed.java | 2 +- .../java/com/conveyal/gtfs/model/Entity.java | 2 +- .../java/com/conveyal/gtfs/model/Stop.java | 64 +++++++++++-------- 3 files changed, 39 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/conveyal/gtfs/GTFSFeed.java b/src/main/java/com/conveyal/gtfs/GTFSFeed.java index a87f7bf92..4a6ff23c6 100644 --- a/src/main/java/com/conveyal/gtfs/GTFSFeed.java +++ b/src/main/java/com/conveyal/gtfs/GTFSFeed.java @@ -185,7 +185,7 @@ else if (feedId == null || feedId.isEmpty()) { new StopArea.Loader(this).loadTable(zip); new Stop.Loader(this).loadTable(zip); if (!stop_areas.isEmpty()) { - Stop.updateStopAreas(stops, stop_areas); + Stop.mergeStopAreasIntoStops(stops, stop_areas); } new Transfer.Loader(this).loadTable(zip); new Trip.Loader(this).loadTable(zip); diff --git a/src/main/java/com/conveyal/gtfs/model/Entity.java b/src/main/java/com/conveyal/gtfs/model/Entity.java index fe99c8cdc..7c2a82589 100644 --- a/src/main/java/com/conveyal/gtfs/model/Entity.java +++ b/src/main/java/com/conveyal/gtfs/model/Entity.java @@ -278,7 +278,7 @@ protected V getRefField(String column, boolean required, Map target protected abstract void loadOneRow() throws IOException; /** - * The main entry point into an Entity.Loader. Interprets each row of a CSV file within a zip file as a sinle + * The main entry point into an Entity.Loader. Interprets each row of a CSV file within a zip file as a single * GTFS entity, and loads them into a table. * * @param zip the zip file from which to read a table diff --git a/src/main/java/com/conveyal/gtfs/model/Stop.java b/src/main/java/com/conveyal/gtfs/model/Stop.java index 9c431dfd9..1fb2c74c8 100644 --- a/src/main/java/com/conveyal/gtfs/model/Stop.java +++ b/src/main/java/com/conveyal/gtfs/model/Stop.java @@ -188,8 +188,10 @@ public Iterator iterator() { } } - public static void updateStopAreas(Map stops, Map stopAreas) { - // Group StopArea IDs by Stop ID + /** + * Merge stop areas into stops when loading from file. + */ + public static void mergeStopAreasIntoStops(Map stops, Map stopAreas) { Map> stopAreasByStopId = new HashMap<>(); stopAreas.values().forEach(stopArea -> @@ -198,24 +200,21 @@ public static void updateStopAreas(Map stops, Map stop.stop_area_ids = Optional.ofNullable(stopAreasByStopId.get(stop.stop_id)) - .map(areaIds -> String.join(";", areaIds)) - .orElse("")); + stops.values().forEach(stop -> stop.stop_area_ids = getStopAreaIds(stopAreasByStopId, stop.stop_id)); } + /** + * Merge stop areas into stops when loading into DB. + */ public static CsvReader mergeStopAreasIntoStops( CsvReader stopsReader, - Map> stopAreasGroupedByStopId + Map> stopAreasByStopId ) { List rows = new ArrayList<>(); try { while (stopsReader.readRecord()) { String stopId = stopsReader.get(STOP_ID_FIELD); - String stopAreaIds = Optional.ofNullable(stopAreasGroupedByStopId.get(stopId)) - .map(areas -> String.join(";", areas)) - .orElse(""); - rows.add(createRow(stopsReader, stopId, stopAreaIds)); + rows.add(createRow(stopsReader, stopId, getStopAreaIds(stopAreasByStopId, stopId))); } return (rows.isEmpty()) ? stopsReader @@ -227,23 +226,34 @@ public static CsvReader mergeStopAreasIntoStops( } } + /** + * Get all stop areas matching provided stop id. + */ + private static String getStopAreaIds(Map> stopAreasByStopId, String stopId) { + return Optional.ofNullable(stopAreasByStopId.get(stopId)) + .map(areas -> String.join(";", areas)) + .orElse(""); + } + + /** + * Create a CSV row of original stop fields plus the stop area ids. + */ private static String createRow(CsvReader stopsReader, String stopId, String stopAreaIds) throws IOException { - StringBuilder row = new StringBuilder(); - row.append(stopId).append(","); - row.append(stopsReader.get(STOP_CODE_FIELD)).append(","); - row.append(stopsReader.get(STOP_NAME_FIELD)).append(","); - row.append(stopsReader.get(STOP_DESC_FIELD)).append(","); - row.append(stopsReader.get(STOP_LAT_FIELD)).append(","); - row.append(stopsReader.get(STOP_LON_FIELD)).append(","); - row.append(stopsReader.get(ZONE_ID_FIELD)).append(","); - row.append(stopsReader.get(STOP_URL_FIELD)).append(","); - row.append(stopsReader.get(LOCATION_TYPE_FIELD)).append(","); - row.append(stopsReader.get(PARENT_STATION_FIELD)).append(","); - row.append(stopsReader.get(STOP_TIMEZONE_FIELD)).append(","); - row.append(stopsReader.get(WHEELCHAIR_BOARDING_FIELD)).append(","); - row.append(stopsReader.get(PLATFORM_CODE_FIELD)).append(","); - row.append(stopAreaIds); - return row.toString(); + return + stopId + "," + + stopsReader.get(STOP_CODE_FIELD) + "," + + stopsReader.get(STOP_NAME_FIELD) + "," + + stopsReader.get(STOP_DESC_FIELD) + "," + + stopsReader.get(STOP_LAT_FIELD) + "," + + stopsReader.get(STOP_LON_FIELD) + "," + + stopsReader.get(ZONE_ID_FIELD) + "," + + stopsReader.get(STOP_URL_FIELD) + "," + + stopsReader.get(LOCATION_TYPE_FIELD) + "," + + stopsReader.get(PARENT_STATION_FIELD) + "," + + stopsReader.get(STOP_TIMEZONE_FIELD) + "," + + stopsReader.get(WHEELCHAIR_BOARDING_FIELD) + "," + + stopsReader.get(PLATFORM_CODE_FIELD) + "," + + stopAreaIds; } /** From 2b1302c799344ecb95ece57f407a15dad4f3d286 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Tue, 5 Aug 2025 08:54:17 +0100 Subject: [PATCH 18/40] improvement(GraphQL updates): Removed stop areas. Added fares v2 stop test to include stop area ids --- .../gtfs/graphql/GTFSGraphQLTest.java | 15 +- src/test/resources/graphql/feedStopAreas.txt | 10 - .../resources/graphql/feedStopsFaresV2.txt | 33 +++ .../GTFSGraphQLTest/canFetchStopAreas-0.json | 208 ------------------ .../canFetchStopsFaresV2-0.json | 122 ++++++++++ 5 files changed, 163 insertions(+), 225 deletions(-) delete mode 100644 src/test/resources/graphql/feedStopAreas.txt create mode 100644 src/test/resources/graphql/feedStopsFaresV2.txt delete mode 100644 src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchStopAreas-0.json create mode 100644 src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchStopsFaresV2-0.json diff --git a/src/test/java/com/conveyal/gtfs/graphql/GTFSGraphQLTest.java b/src/test/java/com/conveyal/gtfs/graphql/GTFSGraphQLTest.java index d9c0e85c1..33afeaa9c 100644 --- a/src/test/java/com/conveyal/gtfs/graphql/GTFSGraphQLTest.java +++ b/src/test/java/com/conveyal/gtfs/graphql/GTFSGraphQLTest.java @@ -206,6 +206,14 @@ void canFetchStops() { }); } + /** Tests that the stops of a feed can be fetched. */ + @Test + void canFetchStopsFaresV2() { + assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { + MatcherAssert.assertThat(queryFaresV2GraphQL("feedStopsFaresV2.txt"), matchesSnapshot()); + }); + } + /** Tests that stops with children can be fetched. */ @Test void canFetchStopWithChildren() { @@ -255,13 +263,6 @@ void canFetchAreas() { }); } - @Test - void canFetchStopAreas() { - assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { - MatcherAssert.assertThat(queryFaresV2GraphQL("feedStopAreas.txt"), matchesSnapshot()); - }); - } - @Test void canFetchFareTransferRules() { assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { diff --git a/src/test/resources/graphql/feedStopAreas.txt b/src/test/resources/graphql/feedStopAreas.txt deleted file mode 100644 index 4f56c7d86..000000000 --- a/src/test/resources/graphql/feedStopAreas.txt +++ /dev/null @@ -1,10 +0,0 @@ -query ($namespace: String) { - feed(namespace: $namespace) { - feed_version - stop_area { - area_id - stop_id - id - } - } -} \ No newline at end of file diff --git a/src/test/resources/graphql/feedStopsFaresV2.txt b/src/test/resources/graphql/feedStopsFaresV2.txt new file mode 100644 index 000000000..c44ec8dab --- /dev/null +++ b/src/test/resources/graphql/feedStopsFaresV2.txt @@ -0,0 +1,33 @@ +query ($namespace: String) { + feed(namespace: $namespace) { + feed_version + stops { + id + location_type + parent_station + patterns { + pattern_id + } + routes { + route_id + } + stop_code + stop_desc + stop_id + stop_lat + stop_lon + stop_name + stop_time_count + stop_times { + stop_id + stop_sequence + trip_id + } + stop_timezone + stop_url + wheelchair_boarding + zone_id + stop_area_ids + } + } +} \ No newline at end of file diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchStopAreas-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchStopAreas-0.json deleted file mode 100644 index 92b7250bd..000000000 --- a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchStopAreas-0.json +++ /dev/null @@ -1,208 +0,0 @@ -{ - "data" : { - "feed" : { - "feed_version" : "1.0", - "stop_area" : [ { - "area_id" : "area_route_426_downtown", - "id" : 2, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_450_downtown", - "id" : 3, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_450_outside_downtown", - "id" : 4, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_450_outside_downtown", - "id" : 5, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_450_outside_downtown", - "id" : 6, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_450_outside_downtown", - "id" : 7, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_450_outside_downtown", - "id" : 8, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_450_outside_downtown", - "id" : 9, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_450_outside_downtown", - "id" : 10, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_426_outside_downtown", - "id" : 11, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_450_outside_downtown", - "id" : 12, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_426_outside_downtown", - "id" : 13, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_426_outside_downtown", - "id" : 14, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_450_outside_downtown", - "id" : 15, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_426_outside_downtown", - "id" : 16, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_354_outside_downtown", - "id" : 17, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_354_outside_downtown", - "id" : 18, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_354_outside_downtown", - "id" : 19, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_354_outside_downtown", - "id" : 20, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_354_outside_downtown", - "id" : 21, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_354_outside_downtown", - "id" : 22, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_354_outside_downtown", - "id" : 23, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_354_outside_downtown", - "id" : 24, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_354_outside_downtown", - "id" : 25, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_354_outside_downtown", - "id" : 26, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_354_outside_downtown", - "id" : 27, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_354_outside_downtown", - "id" : 28, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_354_outside_downtown", - "id" : 29, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_354_outside_downtown", - "id" : 30, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_354_outside_downtown", - "id" : 31, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_354_outside_downtown", - "id" : 32, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_354_outside_downtown", - "id" : 33, - "stop_id" : "4u6g" - }, { - "area_id" : "area_green_b_west_of_kenmore", - "id" : 34, - "stop_id" : "4u6g" - }, { - "area_id" : "area_green_b_west_of_kenmore", - "id" : 35, - "stop_id" : "4u6g" - }, { - "area_id" : "area_green_b_west_of_kenmore", - "id" : 36, - "stop_id" : "4u6g" - }, { - "area_id" : "area_green_b_west_of_kenmore", - "id" : 37, - "stop_id" : "4u6g" - }, { - "area_id" : "area_sl_logan_terminal", - "id" : 38, - "stop_id" : "4u6g" - }, { - "area_id" : "area_sl_logan_terminal", - "id" : 39, - "stop_id" : "4u6g" - }, { - "area_id" : "area_sl_logan_terminal", - "id" : 40, - "stop_id" : "4u6g" - }, { - "area_id" : "area_sl_logan_terminal", - "id" : 41, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_426_outside_downtown", - "id" : 42, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_426_outside_downtown", - "id" : 43, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_426_outside_downtown", - "id" : 44, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_426_outside_downtown", - "id" : 45, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_354_outside_downtown", - "id" : 46, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_354_outside_downtown", - "id" : 47, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_426_outside_downtown", - "id" : 48, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_450_outside_downtown", - "id" : 49, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_426_outside_downtown", - "id" : 50, - "stop_id" : "4u6g" - }, { - "area_id" : "area_route_450_outside_downtown", - "id" : 51, - "stop_id" : "4u6g" - } ] - } - } -} \ No newline at end of file diff --git a/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchStopsFaresV2-0.json b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchStopsFaresV2-0.json new file mode 100644 index 000000000..28e44d869 --- /dev/null +++ b/src/test/resources/snapshots/com/conveyal/gtfs/graphql/GTFSGraphQLTest/canFetchStopsFaresV2-0.json @@ -0,0 +1,122 @@ +{ + "data" : { + "feed" : { + "feed_version" : "1.0", + "stops" : [ { + "id" : 2, + "location_type" : 0, + "parent_station" : null, + "patterns" : [ { + "pattern_id" : "1" + }, { + "pattern_id" : "2" + } ], + "routes" : [ { + "route_id" : "1" + } ], + "stop_area_ids" : "area_sl_airport;area_route_426_outside_downtown;area_m_ashmont_mattapan;area_green_c_west_of_kenmore;area_route_450_downtown;area_commuter_rail_zone_10;area_green_e_west_of_symphony;area_route_450_outside_downtown;area_sl3_north_of_airport;area_route_426_downtown;area_commuter_rail_zone_8;area_commuter_rail_zone_7;area_cf_zone_buzzards;area_commuter_rail_sumner_tunnel_zone_1a;area_commuter_rail_zone_9;area_route_354_downtown;area_commuter_rail_zone_1a;area_commuter_rail_zone_4;area_commuter_rail_zone_3;area_route_354_outside_downtown;area_commuter_rail_zone_6;area_sl_silver_line_way;area_commuter_rail_zone_5;area_sl_logan_terminal;area_commuter_rail_zone_2;area_commuter_rail_zone_1;area_green_b_west_of_kenmore;area_commuter_rail_porter_zone_1a;area_sl_courthouse;area_sl_south_station;area_cf_zone_hyannis;area_ss_commuter_rail_zone_1a;area_red_south_station;area_sl_world_trade_center;area_fairmount_line_zone_1a;area_gl_govt_ctr;area_bl;area_ol_state;area_bl_airport", + "stop_code" : null, + "stop_desc" : null, + "stop_id" : "4u6g", + "stop_lat" : 37.0612132, + "stop_lon" : -122.0074332, + "stop_name" : "Butler Ln", + "stop_time_count" : 3, + "stop_times" : [ { + "stop_id" : "4u6g", + "stop_sequence" : 1, + "trip_id" : "a30277f8-e50a-4a85-9141-b1e0da9d429d" + }, { + "stop_id" : "4u6g", + "stop_sequence" : 1, + "trip_id" : "frequency-trip" + }, { + "stop_id" : "4u6g", + "stop_sequence" : 1, + "trip_id" : "calendar-date-trip" + } ], + "stop_timezone" : null, + "stop_url" : null, + "wheelchair_boarding" : null, + "zone_id" : null + }, { + "id" : 3, + "location_type" : 0, + "parent_station" : null, + "patterns" : [ { + "pattern_id" : "1" + } ], + "routes" : [ { + "route_id" : "1" + } ], + "stop_area_ids" : null, + "stop_code" : null, + "stop_desc" : null, + "stop_id" : "johv", + "stop_lat" : 37.0590172, + "stop_lon" : -122.0096058, + "stop_name" : "Scotts Valley Dr & Victor Sq", + "stop_time_count" : 1, + "stop_times" : [ { + "stop_id" : "johv", + "stop_sequence" : 2, + "trip_id" : "a30277f8-e50a-4a85-9141-b1e0da9d429d" + } ], + "stop_timezone" : null, + "stop_url" : null, + "wheelchair_boarding" : null, + "zone_id" : null + }, { + "id" : 4, + "location_type" : 1, + "parent_station" : null, + "patterns" : [ ], + "routes" : [ ], + "stop_area_ids" : null, + "stop_code" : null, + "stop_desc" : null, + "stop_id" : "123", + "stop_lat" : 37.0666, + "stop_lon" : -122.0777, + "stop_name" : "Parent Station", + "stop_time_count" : 0, + "stop_times" : [ ], + "stop_timezone" : null, + "stop_url" : null, + "wheelchair_boarding" : null, + "zone_id" : null + }, { + "id" : 5, + "location_type" : 0, + "parent_station" : "123", + "patterns" : [ { + "pattern_id" : "2" + } ], + "routes" : [ { + "route_id" : "1" + } ], + "stop_area_ids" : null, + "stop_code" : null, + "stop_desc" : null, + "stop_id" : "1234", + "stop_lat" : 37.06662, + "stop_lon" : -122.07772, + "stop_name" : "Child Stop", + "stop_time_count" : 2, + "stop_times" : [ { + "stop_id" : "1234", + "stop_sequence" : 2, + "trip_id" : "frequency-trip" + }, { + "stop_id" : "1234", + "stop_sequence" : 2, + "trip_id" : "calendar-date-trip" + } ], + "stop_timezone" : null, + "stop_url" : null, + "wheelchair_boarding" : null, + "zone_id" : null + } ] + } + } +} \ No newline at end of file From 00b9d657270673713fa454d8d0414c201c16b8df Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Mon, 11 Aug 2025 13:07:24 +0100 Subject: [PATCH 19/40] improvement(Entry in zip subdirecotry): Allow for stop areas being in a sub directory --- .../java/com/conveyal/gtfs/model/Entity.java | 44 ++++++++++++------- .../com/conveyal/gtfs/util/CsvReaderUtil.java | 3 +- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/conveyal/gtfs/model/Entity.java b/src/main/java/com/conveyal/gtfs/model/Entity.java index 7c2a82589..6b4914a11 100644 --- a/src/main/java/com/conveyal/gtfs/model/Entity.java +++ b/src/main/java/com/conveyal/gtfs/model/Entity.java @@ -287,23 +287,18 @@ public void loadTable(ZipFile zip) throws IOException { String fileName = tableName + ".txt"; ZipEntry entry = zip.getEntry(fileName); if (entry == null) { - Enumeration entries = zip.entries(); - // check if table is contained within sub-directory - while (entries.hasMoreElements()) { - ZipEntry e = entries.nextElement(); - if (Paths.get(e.getName()).getFileName().toString().equals(fileName)) { - entry = e; - feed.errors.add(new TableInSubdirectoryError(tableName, entry.getName().replace(fileName, ""))); - } - } - /* This GTFS table did not exist in the zip. */ - if (this.isRequired()) { - feed.errors.add(new MissingTableError(tableName)); + entry = getEntryFromZipFile(zip, fileName); + if (entry != null) { + feed.errors.add(new TableInSubdirectoryError(tableName, entry.getName().replace(fileName, ""))); } else { - LOG.info("Table {} was missing but it is not required.", tableName); + /* This GTFS table did not exist in the zip. */ + if (this.isRequired()) { + feed.errors.add(new MissingTableError(tableName)); + } else { + LOG.info("Table {} was missing but it is not required.", tableName); + } + return; } - - if (entry == null) return; } LOG.info("Loading GTFS table {} from {}", tableName, entry); List errors = new ArrayList<>(); @@ -326,6 +321,25 @@ public void loadTable(ZipFile zip) throws IOException { } + /** + * Get entry and allow for the file being in a subdirectory. + */ + public static ZipEntry getEntryFromZipFile(ZipFile zipFile, String fileName) { + ZipEntry entry = zipFile.getEntry(fileName); + if (entry == null) { + Enumeration entries = zipFile.entries(); + // check if table is contained within sub-directory + while (entries.hasMoreElements()) { + ZipEntry e = entries.nextElement(); + if (Paths.get(e.getName()).getFileName().toString().equals(fileName)) { + entry = e; + } + } + } + return entry; + } + + /** * An output stream that cannot be closed. CSVWriters try to close their output streams when they are garbage-collected, * which breaks if another CSV writer is still writing to the ZIP file. diff --git a/src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java b/src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java index e62836932..37ea05b9b 100644 --- a/src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java +++ b/src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java @@ -23,6 +23,7 @@ import static com.conveyal.gtfs.error.NewGTFSErrorType.TABLE_IN_SUBDIRECTORY; import static com.conveyal.gtfs.loader.Table.getTableFileNameWithExtension; +import static com.conveyal.gtfs.model.Entity.getEntryFromZipFile; import static com.conveyal.gtfs.model.Stop.STOPS_FILE_NAME; public class CsvReaderUtil { @@ -111,7 +112,7 @@ private static CsvReader processStops( ZipEntry stopsEntry, List errors ) throws IOException { - ZipEntry stopAreasEntry = zipFile.getEntry(Stop.STOP_AREAS_FILE_NAME); + ZipEntry stopAreasEntry = getEntryFromZipFile(zipFile, Stop.STOP_AREAS_FILE_NAME); CsvReader stopsReader = getCsvReaderFromFile(zipFile, stopsEntry); if (stopAreasEntry != null) { stopsReader.setSkipEmptyRecords(false); From dd972520c3100776e489bdc7e09d38eba9e4a1fd Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Mon, 11 Aug 2025 13:21:23 +0100 Subject: [PATCH 20/40] improvement(Stop.java): Updated stop areas separator to a special unicode character --- src/main/java/com/conveyal/gtfs/model/Stop.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/conveyal/gtfs/model/Stop.java b/src/main/java/com/conveyal/gtfs/model/Stop.java index 1fb2c74c8..285d8b5c3 100644 --- a/src/main/java/com/conveyal/gtfs/model/Stop.java +++ b/src/main/java/com/conveyal/gtfs/model/Stop.java @@ -87,6 +87,9 @@ public class Stop extends Entity { System.lineSeparator() ); + // "§" (section sign, U+00A7) + private static final String STOP_AREAS_SEPARATOR = "§"; + @Override public String getId () { return stop_id; @@ -313,7 +316,7 @@ public static String packStopAreas(List stops) { stops.stream() .filter(stop -> stop.stop_area_ids != null) .forEach(stop -> { - String[] areaIds = stop.stop_area_ids.split(";"); + String[] areaIds = stop.stop_area_ids.split(STOP_AREAS_SEPARATOR); for (String areaId : areaIds) { csvContent .append(areaId) From 6117108af7d265cc62fb272d047ec229ce68573b Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Mon, 11 Aug 2025 13:31:02 +0100 Subject: [PATCH 21/40] improvement(Stop.java): Corrected separator char --- src/main/java/com/conveyal/gtfs/model/Stop.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/conveyal/gtfs/model/Stop.java b/src/main/java/com/conveyal/gtfs/model/Stop.java index 285d8b5c3..7b322d88e 100644 --- a/src/main/java/com/conveyal/gtfs/model/Stop.java +++ b/src/main/java/com/conveyal/gtfs/model/Stop.java @@ -234,7 +234,7 @@ public static CsvReader mergeStopAreasIntoStops( */ private static String getStopAreaIds(Map> stopAreasByStopId, String stopId) { return Optional.ofNullable(stopAreasByStopId.get(stopId)) - .map(areas -> String.join(";", areas)) + .map(areas -> String.join(STOP_AREAS_SEPARATOR, areas)) .orElse(""); } From f3ef2c4270ecc2c47fc1a16df75eec85aa147828 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Mon, 11 Aug 2025 15:17:43 +0100 Subject: [PATCH 22/40] improvement(Checking for files in zip subdirectories): Updated all locations to check subdirectories --- .../java/com/conveyal/gtfs/model/Entity.java | 23 +--------- .../com/conveyal/gtfs/util/CsvReaderUtil.java | 46 +++++++++++-------- 2 files changed, 30 insertions(+), 39 deletions(-) diff --git a/src/main/java/com/conveyal/gtfs/model/Entity.java b/src/main/java/com/conveyal/gtfs/model/Entity.java index 6b4914a11..f7825b385 100644 --- a/src/main/java/com/conveyal/gtfs/model/Entity.java +++ b/src/main/java/com/conveyal/gtfs/model/Entity.java @@ -29,7 +29,6 @@ import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.Charset; -import java.nio.file.Paths; import java.sql.JDBCType; import java.sql.PreparedStatement; import java.sql.SQLException; @@ -38,7 +37,6 @@ import java.time.format.DateTimeParseException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.Locale; @@ -49,6 +47,8 @@ import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; +import static com.conveyal.gtfs.util.CsvReaderUtil.getEntryFromZipFile; + /** * An abstract base class that represents a row in a GTFS table, e.g. a Stop, Trip, or Agency. * One concrete subclass is defined for each table in a GTFS feed. @@ -321,25 +321,6 @@ public void loadTable(ZipFile zip) throws IOException { } - /** - * Get entry and allow for the file being in a subdirectory. - */ - public static ZipEntry getEntryFromZipFile(ZipFile zipFile, String fileName) { - ZipEntry entry = zipFile.getEntry(fileName); - if (entry == null) { - Enumeration entries = zipFile.entries(); - // check if table is contained within sub-directory - while (entries.hasMoreElements()) { - ZipEntry e = entries.nextElement(); - if (Paths.get(e.getName()).getFileName().toString().equals(fileName)) { - entry = e; - } - } - } - return entry; - } - - /** * An output stream that cannot be closed. CSVWriters try to close their output streams when they are garbage-collected, * which breaks if another CSV writer is still writing to the ZIP file. diff --git a/src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java b/src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java index 37ea05b9b..7879f14fa 100644 --- a/src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java +++ b/src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java @@ -9,10 +9,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; @@ -23,7 +23,6 @@ import static com.conveyal.gtfs.error.NewGTFSErrorType.TABLE_IN_SUBDIRECTORY; import static com.conveyal.gtfs.loader.Table.getTableFileNameWithExtension; -import static com.conveyal.gtfs.model.Entity.getEntryFromZipFile; import static com.conveyal.gtfs.model.Stop.STOPS_FILE_NAME; public class CsvReaderUtil { @@ -43,24 +42,16 @@ private CsvReaderUtil() { public static CsvReader getCsvReaderAccordingToFileName(Table table, ZipFile zipFile, SQLErrorStorage sqlErrorStorage) { final String tableFileName = getTableFileNameWithExtension(table.name); ZipEntry entry = zipFile.getEntry(tableFileName); + if (entry == null) { - // Table was not found, check if it is in a subdirectory. - Enumeration entries = zipFile.entries(); - while (entries.hasMoreElements()) { - ZipEntry e = entries.nextElement(); - // Include the file separator prefix to force the complete file name to be considered. - // This prevents stop_areas.txt from being loaded instead of areas.txt. - if (e.getName().endsWith(String.format("%s%s", File.separator, tableFileName))) { - entry = e; - if (sqlErrorStorage != null) { - sqlErrorStorage.storeError(NewGTFSError.forTable(table, TABLE_IN_SUBDIRECTORY)); - } - break; - } + entry = getEntryFromZipFile(zipFile, tableFileName); + + if (entry != null && sqlErrorStorage != null) { + sqlErrorStorage.storeError(NewGTFSError.forTable(table, TABLE_IN_SUBDIRECTORY)); + } + if (entry == null) { + return null; } - } - if (entry == null) { - return null; } try { @@ -191,4 +182,23 @@ public static boolean hasExpectedNumberOfColumns(CsvReader csvReader, List entries = zipFile.entries(); + // check if table is contained within subdirectory + while (entries.hasMoreElements()) { + ZipEntry e = entries.nextElement(); + if (Paths.get(e.getName()).getFileName().toString().equals(fileName)) { + entry = e; + break; + } + } + } + return entry; + } } \ No newline at end of file From 83533349ed58a8ea043439dabae36b9d0bf2cb3c Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Thu, 14 Aug 2025 15:39:59 +0100 Subject: [PATCH 23/40] improvement(Exporting): Added the functionality to export stop areas --- .../gtfs/loader/JdbcGtfsExporter.java | 2 + .../com/conveyal/gtfs/model/RouteNetwork.java | 1 + .../java/com/conveyal/gtfs/model/Stop.java | 52 +++++++++++++++++++ .../com/conveyal/gtfs/GTFSFaresV2Test.java | 40 +++++++++++++- src/test/java/com/conveyal/gtfs/GTFSTest.java | 10 +--- .../java/com/conveyal/gtfs/TestUtils.java | 15 ++++++ 6 files changed, 110 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java index ffd7e7dd3..199d01d51 100644 --- a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java +++ b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java @@ -5,6 +5,7 @@ import com.conveyal.gtfs.model.CalendarDate; import com.conveyal.gtfs.model.ScheduleException; import com.conveyal.gtfs.model.Service; +import com.conveyal.gtfs.model.Stop; import com.google.common.collect.Lists; import org.apache.commons.dbutils.DbUtils; import org.postgresql.copy.CopyManager; @@ -298,6 +299,7 @@ public FeedLoadResult exportTables() { result.shapes = export(Table.SHAPES, connection); } result.stops = export(Table.STOPS, connection); + result.stopAreas = Stop.exportStopAreas(dataSource, feedIdToExport, zipOutputStream); // Only write stop times for "approved" routes using COPY TO with results of select query if (fromEditor) { // Generate filter SQL for trips if exporting a feed/schema that represents an editor snapshot. diff --git a/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java b/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java index 4cdf17025..0f346495f 100644 --- a/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java +++ b/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java @@ -17,6 +17,7 @@ public class RouteNetwork extends Entity { public static final String TABLE_NAME = "route_networks"; public static final String NETWORK_ID_NAME = "network_id"; public static final String ROUTE_ID_NAME = "route_id"; + public static final String ROUTE_NETWORK_FILE_NAME = "route_networks.txt"; @Override public String getId () { diff --git a/src/main/java/com/conveyal/gtfs/model/Stop.java b/src/main/java/com/conveyal/gtfs/model/Stop.java index 7b322d88e..d24b68baa 100644 --- a/src/main/java/com/conveyal/gtfs/model/Stop.java +++ b/src/main/java/com/conveyal/gtfs/model/Stop.java @@ -1,10 +1,16 @@ package com.conveyal.gtfs.model; import com.conveyal.gtfs.GTFSFeed; +import com.conveyal.gtfs.loader.EntityPopulator; +import com.conveyal.gtfs.loader.JDBCTableReader; +import com.conveyal.gtfs.loader.Table; +import com.conveyal.gtfs.loader.TableLoadResult; +import com.conveyal.gtfs.loader.TableReader; import com.csvreader.CsvReader; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.sql.DataSource; import java.io.IOException; import java.io.PrintWriter; import java.io.StringReader; @@ -20,6 +26,8 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @@ -327,4 +335,48 @@ public static String packStopAreas(List stops) { }); return csvContent.toString(); } + + /** + * Export stop areas. + */ + public static TableLoadResult exportStopAreas( + DataSource dataSource, + String feedIdToExport, + ZipOutputStream zipOutputStream + ) { + long startTime = System.currentTimeMillis(); + TableLoadResult tableLoadResult = new TableLoadResult(); + + try { + final TableReader stopIterator = new JDBCTableReader<>( + Table.STOPS, + dataSource, + feedIdToExport + ".", + EntityPopulator.STOP + ); + + List stopsWithStopAreas = StreamSupport + .stream(stopIterator.spliterator(), false) + .filter(stop -> stop.stop_area_ids != null && !stop.stop_area_ids.isEmpty()) + .collect(Collectors.toList()); + + if (stopsWithStopAreas.isEmpty()) { + LOG.warn("No stop areas exported as none have been defined!"); + return tableLoadResult; + } + + // Only export if data is available. + tableLoadResult.rowCount = stopsWithStopAreas.size(); + writeStopAreasToFile(zipOutputStream, stopsWithStopAreas); + + long duration = System.currentTimeMillis() - startTime; + LOG.info("Copied {} {} in {} ms.", tableLoadResult.rowCount, STOP_AREAS_FILE_NAME, duration); + + } catch (IOException e) { + tableLoadResult.fatalException = e.toString(); + LOG.error("Exception while exporting {}", STOP_AREAS_FILE_NAME, e); + } + + return tableLoadResult; + } } diff --git a/src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java b/src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java index 225e09c58..3229d76af 100644 --- a/src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java +++ b/src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java @@ -1,27 +1,40 @@ package com.conveyal.gtfs; import com.conveyal.gtfs.loader.FeedLoadResult; +import graphql.Assert; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.sql.DataSource; import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; import java.util.zip.ZipFile; import static com.conveyal.gtfs.GTFS.load; import static com.conveyal.gtfs.GTFS.makeSnapshot; import static com.conveyal.gtfs.GTFS.validate; import static com.conveyal.gtfs.TestUtils.checkFileTestCases; +import static com.conveyal.gtfs.model.RouteNetwork.ROUTE_NETWORK_FILE_NAME; +import static com.conveyal.gtfs.model.Stop.STOP_AREAS_FILE_NAME; import static org.junit.jupiter.api.Assertions.assertTrue; public class GTFSFaresV2Test { + private static final Logger LOG = LoggerFactory.getLogger(GTFSFaresV2Test.class); + private static String faresZipFileName; public static String faresDBName; private static DataSource faresDataSource; private static String faresNamespace; + private static final String JDBC_URL = "jdbc:postgresql://localhost"; @BeforeAll public static void setUpClass() throws IOException { @@ -29,7 +42,7 @@ public static void setUpClass() throws IOException { faresZipFileName = TestUtils.zipFolderFiles(folderName, true); // create a new database faresDBName = TestUtils.generateNewDB(); - String dbConnectionUrl = String.format("jdbc:postgresql://localhost/%s", faresDBName); + String dbConnectionUrl = String.format("%s/%s", JDBC_URL, faresDBName); faresDataSource = TestUtils.createTestDataSource(dbConnectionUrl); // load feed into db FeedLoadResult feedLoadResult = load(faresZipFileName, faresDataSource); @@ -144,4 +157,29 @@ void canDoRoundTripLoadAndWriteToZipFile() throws IOException { }; checkFileTestCases(zip, fileTestCases); } + + /** + * Confirm that the export contains the fares v2 files which have been merged into parent entities and then + * extracted for writing to file. + */ + @Test + void canExportFaresV2Files() throws IOException { + String testDBName = TestUtils.generateNewDB(); + String zipFileName = TestUtils.zipFolderFiles("fake-agency-with-fares-v2", true); + DataSource dataSource = TestUtils.createTestDataSource(String.join("/", JDBC_URL, testDBName)); + FeedLoadResult loadResult = GTFS.load(zipFileName, dataSource); + String namespace = loadResult.uniqueIdentifier; + File tempFile = TestUtils.exportGtfs(namespace, dataSource, false, true); + try (ZipFile gtfsZipFile = new ZipFile(tempFile.getAbsolutePath())) { + tempFile = TestUtils.exportGtfs(namespace, dataSource, false, true); + Stream + .of(STOP_AREAS_FILE_NAME, ROUTE_NETWORK_FILE_NAME) + .forEach(fileName -> Assert.assertNotNull(gtfsZipFile.getEntry(fileName))); + } catch (IOException e) { + LOG.error("An error occurred while attempting to test exporting of mandatory files.", e); + } finally { + TestUtils.dropDB(testDBName); + tempFile.deleteOnExit(); + } + } } \ No newline at end of file diff --git a/src/test/java/com/conveyal/gtfs/GTFSTest.java b/src/test/java/com/conveyal/gtfs/GTFSTest.java index 8b9b0a52c..577bca2d0 100644 --- a/src/test/java/com/conveyal/gtfs/GTFSTest.java +++ b/src/test/java/com/conveyal/gtfs/GTFSTest.java @@ -44,6 +44,7 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipFile; +import static com.conveyal.gtfs.TestUtils.exportGtfs; import static org.hamcrest.CoreMatchers.containsString; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @@ -980,15 +981,6 @@ private void assertThatSnapshotIsErrorFree(SnapshotResult snapshotResult) { assertThat(snapshotResult.scheduleExceptions.fatalException, is(nullValue())); } - /** - * Helper function to export a GTFS from the database to a temporary zip file. - */ - private File exportGtfs(String namespace, DataSource dataSource, boolean fromEditor, boolean publishProprietaryFiles) throws IOException { - File tempFile = File.createTempFile("snapshot", ".zip"); - GTFS.export(namespace, tempFile.getAbsolutePath(), dataSource, fromEditor, publishProprietaryFiles); - return tempFile; - } - private class ValuePair { private final Object expected; private final Object found; diff --git a/src/test/java/com/conveyal/gtfs/TestUtils.java b/src/test/java/com/conveyal/gtfs/TestUtils.java index ff18e59e8..1aac285b8 100644 --- a/src/test/java/com/conveyal/gtfs/TestUtils.java +++ b/src/test/java/com/conveyal/gtfs/TestUtils.java @@ -340,4 +340,19 @@ public static void assertThatSqlQueryYieldsRowCount(DataSource dataSource, Strin public static void assertThatSqlQueryYieldsZeroRows(DataSource dataSource, String sql) throws SQLException { assertThatSqlQueryYieldsRowCount(dataSource, sql, 0); } + + /** + * Helper function to export a GTFS from the database to a temporary zip file. + */ + public static File exportGtfs( + String namespace, + DataSource dataSource, + boolean fromEditor, + boolean publishProprietaryFiles + ) throws IOException { + File tempFile = File.createTempFile("snapshot", ".zip"); + GTFS.export(namespace, tempFile.getAbsolutePath(), dataSource, fromEditor, publishProprietaryFiles); + return tempFile; + } + } From 7299ed43c1afc0b78adee3ad174918c6d0ae67cf Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Mon, 18 Aug 2025 16:54:10 +0100 Subject: [PATCH 24/40] improvement(Addressed PR feedback): --- src/main/java/com/conveyal/gtfs/GTFSFeed.java | 2 +- .../conveyal/gtfs/loader/JdbcGtfsLoader.java | 1 - .../java/com/conveyal/gtfs/model/Entity.java | 34 ++++++---- .../java/com/conveyal/gtfs/model/Stop.java | 67 ++++++++----------- .../com/conveyal/gtfs/util/CsvReaderUtil.java | 6 +- 5 files changed, 53 insertions(+), 57 deletions(-) diff --git a/src/main/java/com/conveyal/gtfs/GTFSFeed.java b/src/main/java/com/conveyal/gtfs/GTFSFeed.java index 4a6ff23c6..617482c58 100644 --- a/src/main/java/com/conveyal/gtfs/GTFSFeed.java +++ b/src/main/java/com/conveyal/gtfs/GTFSFeed.java @@ -185,7 +185,7 @@ else if (feedId == null || feedId.isEmpty()) { new StopArea.Loader(this).loadTable(zip); new Stop.Loader(this).loadTable(zip); if (!stop_areas.isEmpty()) { - Stop.mergeStopAreasIntoStops(stops, stop_areas); + Stop.getCsvReaderForStopsWithStopAreas(stops, stop_areas); } new Transfer.Loader(this).loadTable(zip); new Trip.Loader(this).loadTable(zip); diff --git a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsLoader.java b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsLoader.java index 4e5d2465c..df60e2e02 100644 --- a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsLoader.java +++ b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsLoader.java @@ -400,7 +400,6 @@ private int loadInternal(Table table) throws Exception { int lineNumber = ((int) csvReader.getCurrentRecord()) + 2; if (lineNumber % 500_000 == 0) LOG.info("Processed {}", human(lineNumber)); if (csvReader.getColumnCount() != fields.length) { - System.out.println(Arrays.toString(csvReader.getHeaders())); String badValues = String.format("expected=%d; found=%d", fields.length, csvReader.getColumnCount()); errorStorage.storeError(NewGTFSError.forLine(table, lineNumber, WRONG_NUMBER_OF_FIELDS, badValues)); continue; diff --git a/src/main/java/com/conveyal/gtfs/model/Entity.java b/src/main/java/com/conveyal/gtfs/model/Entity.java index f7825b385..b918f057d 100644 --- a/src/main/java/com/conveyal/gtfs/model/Entity.java +++ b/src/main/java/com/conveyal/gtfs/model/Entity.java @@ -283,7 +283,7 @@ protected V getRefField(String column, boolean required, Map target * * @param zip the zip file from which to read a table */ - public void loadTable(ZipFile zip) throws IOException { + public void loadTable(ZipFile zip) throws IOException{ String fileName = tableName + ".txt"; ZipEntry entry = zip.getEntry(fileName); if (entry == null) { @@ -302,20 +302,26 @@ public void loadTable(ZipFile zip) throws IOException { } LOG.info("Loading GTFS table {} from {}", tableName, entry); List errors = new ArrayList<>(); - this.reader = CsvReaderUtil.getCsvReaderAccordingToFileName(tableName, zip, entry, errors); - boolean hasHeaders = reader.readHeaders(); - if (!hasHeaders) { - feed.errors.add(new EmptyTableError(tableName)); - } - while (reader.readRecord()) { - // reader.getCurrentRecord() is zero-based and does not include the header line, keep our own row count - if (++row % 500000 == 0) { - LOG.info("Record number {}", human(row)); + try { + reader = CsvReaderUtil.getCsvReaderAccordingToFileName(tableName, zip, entry, errors); + boolean hasHeaders = reader.readHeaders(); + if (!hasHeaders) { + feed.errors.add(new EmptyTableError(tableName)); + } + while (reader.readRecord()) { + // reader.getCurrentRecord() is zero-based and does not include the header line, keep our own row count + if (++row % 500000 == 0) { + LOG.info("Record number {}", human(row)); + } + loadOneRow(); // Call subclass method to produce an entity from the current row. + } + if (row == 0) { + feed.errors.add(new EmptyTableError(tableName)); + } + } finally { + if (reader != null) { + reader.close(); } - loadOneRow(); // Call subclass method to produce an entity from the current row. - } - if (row == 0) { - feed.errors.add(new EmptyTableError(tableName)); } } diff --git a/src/main/java/com/conveyal/gtfs/model/Stop.java b/src/main/java/com/conveyal/gtfs/model/Stop.java index d24b68baa..1fee0d56b 100644 --- a/src/main/java/com/conveyal/gtfs/model/Stop.java +++ b/src/main/java/com/conveyal/gtfs/model/Stop.java @@ -7,6 +7,7 @@ import com.conveyal.gtfs.loader.TableLoadResult; import com.conveyal.gtfs.loader.TableReader; import com.csvreader.CsvReader; +import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -73,7 +74,7 @@ public class Stop extends Entity { public static final String AREA_ID_FIELD = "area_id"; public static final String STOP_AREAS_FILE_NAME = "stop_areas.txt"; public static final int STOP_AREAS_NUMBER_OF_HEADERS = 2; - private static final String[] CSV_HEADER_FOR_WRITE = new String[] { + private static final String[] CSV_FIELDS_FOR_MERGE = new String[] { STOP_ID_FIELD, STOP_CODE_FIELD, STOP_NAME_FIELD, @@ -89,10 +90,9 @@ public class Stop extends Entity { PLATFORM_CODE_FIELD }; private static final String CSV_HEADER_FOR_MERGE = String.format( - "%s,%s%s", - String.join(",", CSV_HEADER_FOR_WRITE), - STOP_AREA_IDS_FIELD, - System.lineSeparator() + "%s,%s%n", + String.join(",", CSV_FIELDS_FOR_MERGE), + STOP_AREA_IDS_FIELD ); // "§" (section sign, U+00A7) @@ -172,7 +172,7 @@ public Writer (GTFSFeed feed) { @Override public void writeHeaders() throws IOException { - writer.writeRecord(CSV_HEADER_FOR_WRITE); + writer.writeRecord(CSV_FIELDS_FOR_MERGE); } @Override @@ -202,7 +202,7 @@ public Iterator iterator() { /** * Merge stop areas into stops when loading from file. */ - public static void mergeStopAreasIntoStops(Map stops, Map stopAreas) { + public static void getCsvReaderForStopsWithStopAreas(Map stops, Map stopAreas) { Map> stopAreasByStopId = new HashMap<>(); stopAreas.values().forEach(stopArea -> @@ -217,7 +217,7 @@ public static void mergeStopAreasIntoStops(Map stops, Map> stopAreasByStopId ) { @@ -225,7 +225,7 @@ public static CsvReader mergeStopAreasIntoStops( try { while (stopsReader.readRecord()) { String stopId = stopsReader.get(STOP_ID_FIELD); - rows.add(createRow(stopsReader, stopId, getStopAreaIds(stopAreasByStopId, stopId))); + rows.add(createRow(stopsReader, getStopAreaIds(stopAreasByStopId, stopId))); } return (rows.isEmpty()) ? stopsReader @@ -249,24 +249,15 @@ private static String getStopAreaIds(Map> stopAreasByStopId, /** * Create a CSV row of original stop fields plus the stop area ids. */ - private static String createRow(CsvReader stopsReader, String stopId, String stopAreaIds) throws IOException { - return - stopId + "," + - stopsReader.get(STOP_CODE_FIELD) + "," + - stopsReader.get(STOP_NAME_FIELD) + "," + - stopsReader.get(STOP_DESC_FIELD) + "," + - stopsReader.get(STOP_LAT_FIELD) + "," + - stopsReader.get(STOP_LON_FIELD) + "," + - stopsReader.get(ZONE_ID_FIELD) + "," + - stopsReader.get(STOP_URL_FIELD) + "," + - stopsReader.get(LOCATION_TYPE_FIELD) + "," + - stopsReader.get(PARENT_STATION_FIELD) + "," + - stopsReader.get(STOP_TIMEZONE_FIELD) + "," + - stopsReader.get(WHEELCHAIR_BOARDING_FIELD) + "," + - stopsReader.get(PLATFORM_CODE_FIELD) + "," + - stopAreaIds; + private static String createRow(CsvReader stopsReader, String stopAreaIds) throws IOException { + String[] fields = new String[CSV_FIELDS_FOR_MERGE.length]; + for (int i = 0; i < CSV_FIELDS_FOR_MERGE.length; i++) { + fields[i] = stopsReader.get(CSV_FIELDS_FOR_MERGE[i]); + } + return String.join(",", fields) + "," + stopAreaIds; } + /** * Convert the multiple stops (with stop areas) back into CSV, with header and return a {@link CsvReader} * representation. @@ -316,26 +307,26 @@ public static void writeStopAreasToFile(ZipOutputStream zipOutputStream, List stops) { - StringBuilder csvContent = new StringBuilder(AREA_ID_FIELD) - .append(",") - .append(STOP_ID_FIELD) - .append(System.lineSeparator()); - - stops.stream() + StringBuilder csvContent = new StringBuilder(createRow(AREA_ID_FIELD, STOP_ID_FIELD)); + stops + .stream() .filter(stop -> stop.stop_area_ids != null) .forEach(stop -> { String[] areaIds = stop.stop_area_ids.split(STOP_AREAS_SEPARATOR); for (String areaId : areaIds) { - csvContent - .append(areaId) - .append(",") - .append(stop.stop_id) - .append(System.lineSeparator()); + csvContent.append(createRow(areaId, stop.stop_id)); } }); return csvContent.toString(); } + /** + * Create a row from the column values provided. + */ + private static String createRow(String... columnValues) { + return String.join(",", columnValues) + System.lineSeparator(); + } + /** * Export stop areas. */ @@ -357,15 +348,15 @@ public static TableLoadResult exportStopAreas( List stopsWithStopAreas = StreamSupport .stream(stopIterator.spliterator(), false) - .filter(stop -> stop.stop_area_ids != null && !stop.stop_area_ids.isEmpty()) + .filter(stop -> !StringUtils.isBlank(stop.stop_area_ids)) .collect(Collectors.toList()); + // Only export if data is available. if (stopsWithStopAreas.isEmpty()) { LOG.warn("No stop areas exported as none have been defined!"); return tableLoadResult; } - // Only export if data is available. tableLoadResult.rowCount = stopsWithStopAreas.size(); writeStopAreasToFile(zipOutputStream, stopsWithStopAreas); diff --git a/src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java b/src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java index 7879f14fa..6f43fc751 100644 --- a/src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java +++ b/src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java @@ -88,7 +88,7 @@ public static CsvReader getCsvReaderAccordingToFileName( ) throws IOException { CsvReader csvReader; if (tableFileName.equals(STOPS_FILE_NAME)) { - csvReader = processStops(zipFile, entry, errors); + csvReader = getCsvReaderFromStopsFile(zipFile, entry, errors); } else { csvReader = getCsvReaderFromFile(zipFile, entry); } @@ -98,7 +98,7 @@ public static CsvReader getCsvReaderAccordingToFileName( /** * If the feed contains stop areas extract and merge with stops. If not, just return stops. */ - private static CsvReader processStops( + private static CsvReader getCsvReaderFromStopsFile( ZipFile zipFile, ZipEntry stopsEntry, List errors @@ -114,7 +114,7 @@ private static CsvReader processStops( Map> stopAreas = Stop.groupStopAreaIds(stopAreasReader, errors); if (!stopAreas.isEmpty()) { // Merge stop areas into stops. - return Stop.mergeStopAreasIntoStops(stopsReader, stopAreas); + return Stop.getCsvReaderForStopsWithStopAreas(stopsReader, stopAreas); } } } From 943698441afbbc48ae8f1639f18dc26c44b089a9 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Wed, 20 Aug 2025 13:00:34 +0100 Subject: [PATCH 25/40] improvement(Stop export): Remove stop areas from stop export --- .../gtfs/loader/JdbcGtfsExporter.java | 2 +- .../java/com/conveyal/gtfs/model/Entity.java | 35 ++++++ .../java/com/conveyal/gtfs/model/Stop.java | 101 ++++++++++++++---- .../com/conveyal/gtfs/GTFSFaresV2Test.java | 5 +- 4 files changed, 115 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java index 199d01d51..8fdd161b0 100644 --- a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java +++ b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java @@ -298,7 +298,7 @@ public FeedLoadResult exportTables() { } else { result.shapes = export(Table.SHAPES, connection); } - result.stops = export(Table.STOPS, connection); + result.stops = Stop.exportStops(dataSource, feedIdToExport, zipOutputStream); result.stopAreas = Stop.exportStopAreas(dataSource, feedIdToExport, zipOutputStream); // Only write stop times for "approved" routes using COPY TO with results of select query if (fromEditor) { diff --git a/src/main/java/com/conveyal/gtfs/model/Entity.java b/src/main/java/com/conveyal/gtfs/model/Entity.java index b918f057d..f6c2bce50 100644 --- a/src/main/java/com/conveyal/gtfs/model/Entity.java +++ b/src/main/java/com/conveyal/gtfs/model/Entity.java @@ -26,6 +26,7 @@ import java.io.IOException; import java.io.OutputStream; import java.io.Serializable; +import java.io.StringReader; import java.net.MalformedURLException; import java.net.URL; import java.nio.charset.Charset; @@ -511,4 +512,38 @@ protected static String createPrimaryKey(Object... fields) { .map(id -> id == null ? "empty" : id.toString()) .collect(Collectors.joining("_")); } + + /** + * Convert multiple rows of data back into CSV, with header and return a {@link CsvReader} + * representation. + */ + protected static CsvReader produceCsvPayload(List rows, String header) { + StringBuilder csvContent = new StringBuilder(); + csvContent.append(header); + rows.forEach(row -> csvContent.append(row).append(System.lineSeparator())); + return new CsvReader(new StringReader(csvContent.toString())); + } + + /** + * Create a row from the column values provided. + */ + protected static String createRow(String... columnValues) { + return String.join(",", columnValues) + System.lineSeparator(); + } + + protected static String computeCsvValue(String value) { + return value != null ? value : ""; + } + + protected static String computeCsvValue(URL value) { + return value != null ? value.toString() : ""; + } + + protected static String computeCsvValue(int value) { + return value != INT_MISSING ? String.valueOf(value) : ""; + } + + protected static String computeCsvValue(double value) { + return value != DOUBLE_MISSING ? String.valueOf(value) : ""; + } } diff --git a/src/main/java/com/conveyal/gtfs/model/Stop.java b/src/main/java/com/conveyal/gtfs/model/Stop.java index 1fee0d56b..116930041 100644 --- a/src/main/java/com/conveyal/gtfs/model/Stop.java +++ b/src/main/java/com/conveyal/gtfs/model/Stop.java @@ -14,7 +14,6 @@ import javax.sql.DataSource; import java.io.IOException; import java.io.PrintWriter; -import java.io.StringReader; import java.net.URL; import java.sql.PreparedStatement; import java.sql.SQLException; @@ -74,7 +73,7 @@ public class Stop extends Entity { public static final String AREA_ID_FIELD = "area_id"; public static final String STOP_AREAS_FILE_NAME = "stop_areas.txt"; public static final int STOP_AREAS_NUMBER_OF_HEADERS = 2; - private static final String[] CSV_FIELDS_FOR_MERGE = new String[] { + private static final String[] CSV_FIELDS = new String[] { STOP_ID_FIELD, STOP_CODE_FIELD, STOP_NAME_FIELD, @@ -91,10 +90,15 @@ public class Stop extends Entity { }; private static final String CSV_HEADER_FOR_MERGE = String.format( "%s,%s%n", - String.join(",", CSV_FIELDS_FOR_MERGE), + String.join(",", CSV_FIELDS), STOP_AREA_IDS_FIELD ); + private static final String CSV_HEADER_FOR_EXPORT = String.format( + "%s", + String.join(",", CSV_FIELDS) + ); + // "§" (section sign, U+00A7) private static final String STOP_AREAS_SEPARATOR = "§"; @@ -172,7 +176,7 @@ public Writer (GTFSFeed feed) { @Override public void writeHeaders() throws IOException { - writer.writeRecord(CSV_FIELDS_FOR_MERGE); + writer.writeRecord(CSV_FIELDS); } @Override @@ -229,7 +233,7 @@ public static CsvReader getCsvReaderForStopsWithStopAreas( } return (rows.isEmpty()) ? stopsReader - : produceCsvPayload(rows); + : produceCsvPayload(rows, CSV_HEADER_FOR_MERGE); } catch (Exception e) { LOG.error("Error while merging stops", e); // Any issues, return the original stops reader (minus stop areas). @@ -250,25 +254,13 @@ private static String getStopAreaIds(Map> stopAreasByStopId, * Create a CSV row of original stop fields plus the stop area ids. */ private static String createRow(CsvReader stopsReader, String stopAreaIds) throws IOException { - String[] fields = new String[CSV_FIELDS_FOR_MERGE.length]; - for (int i = 0; i < CSV_FIELDS_FOR_MERGE.length; i++) { - fields[i] = stopsReader.get(CSV_FIELDS_FOR_MERGE[i]); + String[] fields = new String[CSV_FIELDS.length]; + for (int i = 0; i < CSV_FIELDS.length; i++) { + fields[i] = stopsReader.get(CSV_FIELDS[i]); } return String.join(",", fields) + "," + stopAreaIds; } - - /** - * Convert the multiple stops (with stop areas) back into CSV, with header and return a {@link CsvReader} - * representation. - */ - private static CsvReader produceCsvPayload(List rows) { - StringBuilder csvContent = new StringBuilder(); - csvContent.append(CSV_HEADER_FOR_MERGE); - rows.forEach(row -> csvContent.append(row).append(System.lineSeparator())); - return new CsvReader(new StringReader(csvContent.toString())); - } - /** * Extract the stop areas from file and group by stop id. This is to allow for easier CRUD by the DT UI. */ @@ -303,6 +295,19 @@ public static void writeStopAreasToFile(ZipOutputStream zipOutputStream, List stops) throws IOException { + // Create entry for table. + zipOutputStream.putNextEntry(new ZipEntry(STOPS_FILE_NAME)); + // Create and use PrintWriter, but don't close. This is done when the zip entry is closed. + PrintWriter p = new PrintWriter(zipOutputStream); + p.print(packStops(stops)); + p.flush(); + zipOutputStream.closeEntry(); + } + /** * Expand all stop area ids into a single row for each area id. This is to conform with the GTFS Fares v2 standard. */ @@ -321,10 +326,60 @@ public static String packStopAreas(List stops) { } /** - * Create a row from the column values provided. + * Expand all stops into a single row. */ - private static String createRow(String... columnValues) { - return String.join(",", columnValues) + System.lineSeparator(); + public static String packStops(List stops) { + StringBuilder csvContent = new StringBuilder(createRow(CSV_HEADER_FOR_EXPORT)); + stops.forEach(stop -> csvContent.append(createRow( + computeCsvValue(stop.stop_id), + computeCsvValue(stop.stop_code), + computeCsvValue(stop.stop_name), + computeCsvValue(stop.stop_desc), + computeCsvValue(stop.stop_lat), + computeCsvValue(stop.stop_lon), + computeCsvValue(stop.zone_id), + computeCsvValue(stop.stop_url), + computeCsvValue(stop.location_type), + computeCsvValue(stop.parent_station), + computeCsvValue(stop.stop_timezone), + computeCsvValue(stop.wheelchair_boarding), + computeCsvValue(stop.platform_code) + ))); + return csvContent.toString(); + } + + /** + * Export stops, minus stop areas. + */ + public static TableLoadResult exportStops( + DataSource dataSource, + String feedIdToExport, + ZipOutputStream zipOutputStream + ) { + long startTime = System.currentTimeMillis(); + TableLoadResult tableLoadResult = new TableLoadResult(); + + try { + final TableReader stopIterator = new JDBCTableReader<>( + Table.STOPS, + dataSource, + feedIdToExport + ".", + EntityPopulator.STOP + ); + + List stops = new ArrayList<>(); + stopIterator.forEach(stops::add); + writeStopsToFile(zipOutputStream, stops); + + long duration = System.currentTimeMillis() - startTime; + LOG.info("Copied {} {} in {} ms.", tableLoadResult.rowCount, STOPS_FILE_NAME, duration); + + } catch (IOException e) { + tableLoadResult.fatalException = e.toString(); + LOG.error("Exception while exporting {}", STOPS_FILE_NAME, e); + } + + return tableLoadResult; } /** diff --git a/src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java b/src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java index 3229d76af..6af56d4f3 100644 --- a/src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java +++ b/src/test/java/com/conveyal/gtfs/GTFSFaresV2Test.java @@ -11,10 +11,6 @@ import javax.sql.DataSource; import java.io.File; import java.io.IOException; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; import java.util.stream.Stream; import java.util.zip.ZipFile; @@ -176,6 +172,7 @@ void canExportFaresV2Files() throws IOException { .of(STOP_AREAS_FILE_NAME, ROUTE_NETWORK_FILE_NAME) .forEach(fileName -> Assert.assertNotNull(gtfsZipFile.getEntry(fileName))); } catch (IOException e) { + Assert.assertShouldNeverHappen(); LOG.error("An error occurred while attempting to test exporting of mandatory files.", e); } finally { TestUtils.dropDB(testDBName); From ff93df694cbe1689a22c402c08a385ff51b0b361 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Wed, 20 Aug 2025 14:17:33 +0100 Subject: [PATCH 26/40] improvement(Entity.java): Reinstated space --- src/main/java/com/conveyal/gtfs/model/Entity.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/conveyal/gtfs/model/Entity.java b/src/main/java/com/conveyal/gtfs/model/Entity.java index f6c2bce50..c02333d8f 100644 --- a/src/main/java/com/conveyal/gtfs/model/Entity.java +++ b/src/main/java/com/conveyal/gtfs/model/Entity.java @@ -284,7 +284,7 @@ protected V getRefField(String column, boolean required, Map target * * @param zip the zip file from which to read a table */ - public void loadTable(ZipFile zip) throws IOException{ + public void loadTable(ZipFile zip) throws IOException { String fileName = tableName + ".txt"; ZipEntry entry = zip.getEntry(fileName); if (entry == null) { From 77e3b6152ec343ee7460fbfadfa714b0a7ff869a Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Thu, 21 Aug 2025 10:19:24 +0100 Subject: [PATCH 27/40] improvement(Addressed PR feedback): Streamlined writting to zip --- src/main/java/com/conveyal/gtfs/GTFSFeed.java | 2 +- .../java/com/conveyal/gtfs/model/Entity.java | 2 +- .../java/com/conveyal/gtfs/model/Stop.java | 35 +++++++------------ 3 files changed, 15 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/conveyal/gtfs/GTFSFeed.java b/src/main/java/com/conveyal/gtfs/GTFSFeed.java index 617482c58..8eb5055bb 100644 --- a/src/main/java/com/conveyal/gtfs/GTFSFeed.java +++ b/src/main/java/com/conveyal/gtfs/GTFSFeed.java @@ -240,7 +240,7 @@ public void toFile (String file) { new Stop.Writer(this).writeTable(zip); if (!stops.isEmpty()) { // Export stop areas. - Stop.writeStopAreasToFile(zip, new ArrayList<>(stops.values())); + Stop.writeEntityToFile(zip, new ArrayList<>(stops.values()), Stop.STOP_AREAS_FILE_NAME); } new ShapePoint.Writer(this).writeTable(zip); new Transfer.Writer(this).writeTable(zip); diff --git a/src/main/java/com/conveyal/gtfs/model/Entity.java b/src/main/java/com/conveyal/gtfs/model/Entity.java index c02333d8f..b2d20744f 100644 --- a/src/main/java/com/conveyal/gtfs/model/Entity.java +++ b/src/main/java/com/conveyal/gtfs/model/Entity.java @@ -520,7 +520,7 @@ protected static String createPrimaryKey(Object... fields) { protected static CsvReader produceCsvPayload(List rows, String header) { StringBuilder csvContent = new StringBuilder(); csvContent.append(header); - rows.forEach(row -> csvContent.append(row).append(System.lineSeparator())); + rows.forEach(csvContent::append); return new CsvReader(new StringReader(csvContent.toString())); } diff --git a/src/main/java/com/conveyal/gtfs/model/Stop.java b/src/main/java/com/conveyal/gtfs/model/Stop.java index 116930041..643537f3c 100644 --- a/src/main/java/com/conveyal/gtfs/model/Stop.java +++ b/src/main/java/com/conveyal/gtfs/model/Stop.java @@ -7,6 +7,7 @@ import com.conveyal.gtfs.loader.TableLoadResult; import com.conveyal.gtfs.loader.TableReader; import com.csvreader.CsvReader; +import com.google.common.collect.Lists; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -258,7 +259,7 @@ private static String createRow(CsvReader stopsReader, String stopAreaIds) throw for (int i = 0; i < CSV_FIELDS.length; i++) { fields[i] = stopsReader.get(CSV_FIELDS[i]); } - return String.join(",", fields) + "," + stopAreaIds; + return String.format("%s,%s%n", String.join(",", fields), stopAreaIds); } /** @@ -283,27 +284,18 @@ public static Map> groupStopAreaIds(CsvReader csvReader, Lis } /** - * Expand the stop area ids and write to zip file. + * Write stops or stop areas to zip file. */ - public static void writeStopAreasToFile(ZipOutputStream zipOutputStream, List stops) throws IOException { + public static void writeEntityToFile(ZipOutputStream zipOutputStream, List stops, String fileName) throws IOException { // Create entry for table. - zipOutputStream.putNextEntry(new ZipEntry(STOP_AREAS_FILE_NAME)); + zipOutputStream.putNextEntry(new ZipEntry(fileName)); // Create and use PrintWriter, but don't close. This is done when the zip entry is closed. PrintWriter p = new PrintWriter(zipOutputStream); - p.print(packStopAreas(stops)); - p.flush(); - zipOutputStream.closeEntry(); - } - - /** - * Write stops to zip file. - */ - public static void writeStopsToFile(ZipOutputStream zipOutputStream, List stops) throws IOException { - // Create entry for table. - zipOutputStream.putNextEntry(new ZipEntry(STOPS_FILE_NAME)); - // Create and use PrintWriter, but don't close. This is done when the zip entry is closed. - PrintWriter p = new PrintWriter(zipOutputStream); - p.print(packStops(stops)); + if (fileName.equals(STOPS_FILE_NAME)) { + p.print(packStops(stops)); + } else { + p.print(packStopAreas(stops)); + } p.flush(); zipOutputStream.closeEntry(); } @@ -367,9 +359,8 @@ public static TableLoadResult exportStops( EntityPopulator.STOP ); - List stops = new ArrayList<>(); - stopIterator.forEach(stops::add); - writeStopsToFile(zipOutputStream, stops); + List stops = Lists.newArrayList(stopIterator); + writeEntityToFile(zipOutputStream, stops, STOPS_FILE_NAME); long duration = System.currentTimeMillis() - startTime; LOG.info("Copied {} {} in {} ms.", tableLoadResult.rowCount, STOPS_FILE_NAME, duration); @@ -413,7 +404,7 @@ public static TableLoadResult exportStopAreas( } tableLoadResult.rowCount = stopsWithStopAreas.size(); - writeStopAreasToFile(zipOutputStream, stopsWithStopAreas); + writeEntityToFile(zipOutputStream, stopsWithStopAreas, STOP_AREAS_FILE_NAME); long duration = System.currentTimeMillis() - startTime; LOG.info("Copied {} {} in {} ms.", tableLoadResult.rowCount, STOP_AREAS_FILE_NAME, duration); From b091a913d41f86b6adbdbaadf077665d3d69eaa7 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Mon, 18 Aug 2025 14:33:15 +0100 Subject: [PATCH 28/40] feat(route network merge): Started on merging rout networks into routes --- .../java/com/conveyal/gtfs/model/Entity.java | 3 ++ .../java/com/conveyal/gtfs/model/Route.java | 40 ++++++++++++++++++- .../com/conveyal/gtfs/model/RouteNetwork.java | 5 +++ .../java/com/conveyal/gtfs/model/Stop.java | 7 +--- 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/conveyal/gtfs/model/Entity.java b/src/main/java/com/conveyal/gtfs/model/Entity.java index b2d20744f..e1e4b05f0 100644 --- a/src/main/java/com/conveyal/gtfs/model/Entity.java +++ b/src/main/java/com/conveyal/gtfs/model/Entity.java @@ -66,6 +66,9 @@ public abstract class Entity implements Serializable { /* The feed from which this entity was loaded. TODO is this really necessary in every entity? */ transient GTFSFeed feed; + // "§" (section sign, U+00A7) + public static final String SEPARATOR = "§"; + /** * This method should be overridden by each Entity subtype to return the proper key field for that subtype. * @return a key that according to the GTFS spec should uniquely identify this entity, either alone or together diff --git a/src/main/java/com/conveyal/gtfs/model/Route.java b/src/main/java/com/conveyal/gtfs/model/Route.java index a55ea29fc..3689b869d 100644 --- a/src/main/java/com/conveyal/gtfs/model/Route.java +++ b/src/main/java/com/conveyal/gtfs/model/Route.java @@ -37,9 +37,47 @@ public class Route extends Entity { // implements Entity.Factory public int continuous_pickup = INT_MISSING; public int continuous_drop_off = INT_MISSING; public String network_id; + public String route_network_ids; + + public static final String ROUTE_ID_FIELD = "route_id"; + public static final String AGENCY_ID_FIELD = "agency_id"; + public static final String ROUTE_SHORT_NAME_FIELD = "route_short_name"; + public static final String ROUTE_LONG_NAME_FIELD = "route_long_name"; + public static final String ROUTE_DESC_FIELD = "route_desc"; + public static final String ROUTE_TYPE_FIELD = "route_type"; + public static final String ROUTE_URL_FIELD = "route_url"; + public static final String ROUTE_COLOR_FIELD = "route_color"; + public static final String ROUTE_SORT_ORDER_FIELD = "route_sort_order"; + public static final String ROUTE_TEXT_COLOR_FIELD = "route_text_color"; + public static final String ROUTE_BRANDING_URL_FIELD = "route_branding_url"; + public static final String CONTINUOUS_PICKUP_FIELD = "continuous_pickup"; + public static final String CONTINUOUS_DROP_OFF_FIELD = "continuous_drop_off"; + public static final String NETWORK_ID_FIELD = "network_id"; + public static final String ROUTE_NETWORK_IDS_FIELD = "route_network_ids"; public static final String TABLE_NAME = "routes"; - + private static final String[] CSV_HEADER_FOR_WRITE = new String[] { + ROUTE_ID_FIELD, + AGENCY_ID_FIELD, + ROUTE_SHORT_NAME_FIELD, + ROUTE_LONG_NAME_FIELD, + ROUTE_DESC_FIELD, + ROUTE_TYPE_FIELD, + ROUTE_URL_FIELD, + ROUTE_COLOR_FIELD, + ROUTE_SORT_ORDER_FIELD, + ROUTE_TEXT_COLOR_FIELD, + ROUTE_BRANDING_URL_FIELD, + CONTINUOUS_PICKUP_FIELD, + CONTINUOUS_DROP_OFF_FIELD, + NETWORK_ID_FIELD + }; + private static final String CSV_HEADER_FOR_MERGE = String.format( + "%s,%s%s", + String.join(",", CSV_HEADER_FOR_WRITE), + ROUTE_NETWORK_IDS_FIELD, + System.lineSeparator() + ); @Override public String getId () { return route_id; diff --git a/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java b/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java index 0f346495f..7afefb898 100644 --- a/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java +++ b/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java @@ -7,6 +7,10 @@ import java.sql.SQLException; import java.util.Iterator; +/** + * This is only included to read the values from file. No route networks are saved directly to the database. Instead, + * they are merged with the appropriate route (and then saved to the database). + */ public class RouteNetwork extends Entity { private static final long serialVersionUID = -4739475958736362940L; @@ -18,6 +22,7 @@ public class RouteNetwork extends Entity { public static final String NETWORK_ID_NAME = "network_id"; public static final String ROUTE_ID_NAME = "route_id"; public static final String ROUTE_NETWORK_FILE_NAME = "route_networks.txt"; + public static final int ROUTE_NETWORK_NUMBER_OF_HEADERS = 2; @Override public String getId () { diff --git a/src/main/java/com/conveyal/gtfs/model/Stop.java b/src/main/java/com/conveyal/gtfs/model/Stop.java index 643537f3c..1ffedec72 100644 --- a/src/main/java/com/conveyal/gtfs/model/Stop.java +++ b/src/main/java/com/conveyal/gtfs/model/Stop.java @@ -100,9 +100,6 @@ public class Stop extends Entity { String.join(",", CSV_FIELDS) ); - // "§" (section sign, U+00A7) - private static final String STOP_AREAS_SEPARATOR = "§"; - @Override public String getId () { return stop_id; @@ -247,7 +244,7 @@ public static CsvReader getCsvReaderForStopsWithStopAreas( */ private static String getStopAreaIds(Map> stopAreasByStopId, String stopId) { return Optional.ofNullable(stopAreasByStopId.get(stopId)) - .map(areas -> String.join(STOP_AREAS_SEPARATOR, areas)) + .map(areas -> String.join(SEPARATOR, areas)) .orElse(""); } @@ -309,7 +306,7 @@ public static String packStopAreas(List stops) { .stream() .filter(stop -> stop.stop_area_ids != null) .forEach(stop -> { - String[] areaIds = stop.stop_area_ids.split(STOP_AREAS_SEPARATOR); + String[] areaIds = stop.stop_area_ids.split(SEPARATOR); for (String areaId : areaIds) { csvContent.append(createRow(areaId, stop.stop_id)); } From 98241804e1d140b3a15b38ad7a81f80f2d4423cf Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Wed, 20 Aug 2025 10:02:40 +0100 Subject: [PATCH 29/40] improvement(Various updates to accom route networks in routes): --- src/main/java/com/conveyal/gtfs/GTFSFeed.java | 9 +- .../graphql/GraphQLGtfsFaresV2Schema.java | 9 - .../gtfs/graphql/GraphQLGtfsSchema.java | 1 + .../conveyal/gtfs/loader/EntityPopulator.java | 32 ++- .../java/com/conveyal/gtfs/loader/Feed.java | 2 - .../gtfs/loader/JdbcGTFSFeedConverter.java | 1 - .../gtfs/loader/JdbcGtfsExporter.java | 3 +- .../conveyal/gtfs/loader/JdbcGtfsLoader.java | 1 - .../gtfs/loader/JdbcGtfsSnapshotter.java | 1 - .../java/com/conveyal/gtfs/loader/Table.java | 13 +- .../java/com/conveyal/gtfs/model/Route.java | 211 +++++++++++++++++- .../com/conveyal/gtfs/model/RouteNetwork.java | 14 +- .../java/com/conveyal/gtfs/model/Stop.java | 2 +- .../com/conveyal/gtfs/util/CsvReaderUtil.java | 31 +++ .../java/com/conveyal/gtfs/dto/RouteDTO.java | 1 + .../conveyal/gtfs/dto/RouteNetworkDTO.java | 14 -- .../gtfs/graphql/GTFSGraphQLTest.java | 7 - .../loader/JDBCTableWriterFaresV2Test.java | 5 - 18 files changed, 274 insertions(+), 83 deletions(-) delete mode 100644 src/test/java/com/conveyal/gtfs/dto/RouteNetworkDTO.java diff --git a/src/main/java/com/conveyal/gtfs/GTFSFeed.java b/src/main/java/com/conveyal/gtfs/GTFSFeed.java index 8eb5055bb..3be39b33d 100644 --- a/src/main/java/com/conveyal/gtfs/GTFSFeed.java +++ b/src/main/java/com/conveyal/gtfs/GTFSFeed.java @@ -180,7 +180,11 @@ else if (feedId == null || feedId.isEmpty()) { fares = null; // free memory new Pattern.Loader(this).loadTable(zip); + new RouteNetwork.Loader(this).loadTable(zip); new Route.Loader(this).loadTable(zip); + if (!route_networks.isEmpty()) { + Route.getCsvReaderForRoutesWithRouteNetworks(routes, route_networks); + } new ShapePoint.Loader(this).loadTable(zip); new StopArea.Loader(this).loadTable(zip); new Stop.Loader(this).loadTable(zip); @@ -196,7 +200,6 @@ else if (feedId == null || feedId.isEmpty()) { new Area.Loader(this).loadTable(zip); new TimeFrame.Loader(this).loadTable(zip); new Network.Loader(this).loadTable(zip); - new RouteNetwork.Loader(this).loadTable(zip); new FareMedia.Loader(this).loadTable(zip); new FareProduct.Loader(this).loadTable(zip); new FareLegRule.Loader(this).loadTable(zip); @@ -237,6 +240,10 @@ public void toFile (String file) { new FareRule.Writer(this).writeTable(zip); new Frequency.Writer(this).writeTable(zip); new Route.Writer(this).writeTable(zip); + if (!routes.isEmpty()) { + // Export route networks. + Route.writeRouteNetworksToFile(zip, new ArrayList<>(routes.values())); + } new Stop.Writer(this).writeTable(zip); if (!stops.isEmpty()) { // Export stop areas. diff --git a/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsFaresV2Schema.java b/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsFaresV2Schema.java index 8b87d818a..e7088eb78 100644 --- a/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsFaresV2Schema.java +++ b/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsFaresV2Schema.java @@ -24,7 +24,6 @@ public class GraphQLGtfsFaresV2Schema { private static final String AREA_TYPE_NAME = "area"; private static final String TIME_FRAME_TYPE_NAME = "time_frame"; private static final String NETWORK_TYPE_NAME = "network"; - private static final String ROUTE_NETWORK_TYPE_NAME = "route_network"; private static final String FARE_MEDIA_TYPE_NAME = "fare_media"; private static final String FARE_PRODUCT_TYPE_NAME = "fare_product"; private static final String FARE_LEG_RULE_TYPE_NAME = "fare_leg_rule"; @@ -55,13 +54,6 @@ private GraphQLGtfsFaresV2Schema() {} .field(MapFetcher.field(Network.NETWORK_NAME_NAME)) .build(); - public static final GraphQLObjectType routeNetworkType = newObject().name(ROUTE_NETWORK_TYPE_NAME) - .description("A GTFS route network object") - .field(MapFetcher.field("id", GraphQLInt)) - .field(MapFetcher.field(RouteNetwork.NETWORK_ID_NAME)) - .field(MapFetcher.field(RouteNetwork.ROUTE_ID_NAME)) - .build(); - public static final GraphQLObjectType fareMediaType = newObject().name(FARE_MEDIA_TYPE_NAME) .description("A GTFS fare media object") .field(MapFetcher.field("id", GraphQLInt)) @@ -113,7 +105,6 @@ public static List getFaresV2FieldDefinitions() { createFieldDefinition(FARE_PRODUCT_TYPE_NAME, fareProductType, FareProduct.TABLE_NAME), createFieldDefinition(FARE_TRANSFER_RULE_TYPE_NAME, fareTransferRuleType, FareTransferRule.TABLE_NAME), createFieldDefinition(NETWORK_TYPE_NAME, networkType, Network.TABLE_NAME), - createFieldDefinition(ROUTE_NETWORK_TYPE_NAME, routeNetworkType, RouteNetwork.TABLE_NAME), createFieldDefinition(TIME_FRAME_TYPE_NAME, timeFrameType, TimeFrame.TABLE_NAME) ); } diff --git a/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsSchema.java b/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsSchema.java index 5dd611a81..00382d9f6 100644 --- a/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsSchema.java +++ b/src/main/java/com/conveyal/gtfs/graphql/GraphQLGtfsSchema.java @@ -291,6 +291,7 @@ public class GraphQLGtfsSchema { // FIXME ^^ .field(RowCountFetcher.field("trip_count", "trips", "route_id")) .field(RowCountFetcher.field("pattern_count", "patterns", "route_id")) + .field(MapFetcher.field("route_network_ids")) .field(newFieldDefinition() .name("stops") .description("GTFS stop entities that the route serves") diff --git a/src/main/java/com/conveyal/gtfs/loader/EntityPopulator.java b/src/main/java/com/conveyal/gtfs/loader/EntityPopulator.java index 95d344aa8..a947ed3d7 100644 --- a/src/main/java/com/conveyal/gtfs/loader/EntityPopulator.java +++ b/src/main/java/com/conveyal/gtfs/loader/EntityPopulator.java @@ -163,18 +163,19 @@ public interface EntityPopulator { EntityPopulator ROUTE = (result, columnForName) -> { Route route = new Route(); - route.route_id = getStringIfPresent(result, "route_id", columnForName); - route.agency_id = getStringIfPresent(result, "agency_id", columnForName); - route.route_short_name = getStringIfPresent(result, "route_short_name", columnForName); - route.route_long_name = getStringIfPresent(result, "route_long_name", columnForName); - route.route_desc = getStringIfPresent(result, "route_desc", columnForName); - route.route_type = getIntIfPresent (result, "route_type", columnForName); - route.route_color = getStringIfPresent(result, "route_color", columnForName); - route.route_text_color = getStringIfPresent(result, "route_text_color", columnForName); - route.route_url = getUrlIfPresent (result, "route_url", columnForName); - route.route_branding_url = getUrlIfPresent (result, "route_branding_url", columnForName); - route.continuous_pickup = getIntIfPresent (result, "continuous_pickup", columnForName); - route.continuous_drop_off = getIntIfPresent (result, "continuous_drop_off", columnForName); + route.route_id = getStringIfPresent(result, Route.ROUTE_ID_FIELD, columnForName); + route.agency_id = getStringIfPresent(result, Route.AGENCY_ID_FIELD, columnForName); + route.route_short_name = getStringIfPresent(result, Route.ROUTE_SHORT_NAME_FIELD, columnForName); + route.route_long_name = getStringIfPresent(result, Route.ROUTE_LONG_NAME_FIELD, columnForName); + route.route_desc = getStringIfPresent(result, Route.ROUTE_DESC_FIELD, columnForName); + route.route_type = getIntIfPresent(result, Route.ROUTE_TYPE_FIELD, columnForName); + route.route_color = getStringIfPresent(result, Route.ROUTE_COLOR_FIELD, columnForName); + route.route_text_color = getStringIfPresent(result, Route.ROUTE_TEXT_COLOR_FIELD, columnForName); + route.route_url = getUrlIfPresent(result, Route.ROUTE_URL_FIELD, columnForName); + route.route_branding_url = getUrlIfPresent(result, Route.ROUTE_BRANDING_URL_FIELD, columnForName); + route.continuous_pickup = getIntIfPresent(result, Route.CONTINUOUS_PICKUP_FIELD, columnForName); + route.continuous_drop_off = getIntIfPresent(result, Route.CONTINUOUS_DROP_OFF_FIELD, columnForName); + route.route_network_ids = getStringIfPresent(result, Route.ROUTE_NETWORK_IDS_FIELD, columnForName); return route; }; @@ -304,13 +305,6 @@ public interface EntityPopulator { return network; }; - EntityPopulator ROUTE_NETWORK = (result, columnForName) -> { - RouteNetwork routeNetwork = new RouteNetwork(); - routeNetwork.network_id = getStringIfPresent(result, RouteNetwork.NETWORK_ID_NAME, columnForName); - routeNetwork.route_id = getStringIfPresent(result, RouteNetwork.ROUTE_ID_NAME, columnForName); - return routeNetwork; - }; - // The reason we're passing in the columnForName map is that resultSet.getX(columnName) throws an exception // when the column is not present. // Exceptions should only be used in exceptional circumstances (ones that should be logged as errors). diff --git a/src/main/java/com/conveyal/gtfs/loader/Feed.java b/src/main/java/com/conveyal/gtfs/loader/Feed.java index f1a6d6263..5130552cf 100644 --- a/src/main/java/com/conveyal/gtfs/loader/Feed.java +++ b/src/main/java/com/conveyal/gtfs/loader/Feed.java @@ -47,7 +47,6 @@ public class Feed { public final TableReader fareLegRules; public final TableReader fareTransferRules; public final TableReader networks; - public final TableReader routeNetworks; /** * Create a feed that reads tables over a JDBC connection. The connection should already be set to the right @@ -76,7 +75,6 @@ public Feed (DataSource dataSource, String databaseSchemaPrefix) { fareLegRules = new JDBCTableReader(Table.FARE_LEG_RULES, dataSource, databaseSchemaPrefix, EntityPopulator.FARE_LEG_RULE); fareTransferRules = new JDBCTableReader(Table.FARE_TRANSFER_RULES, dataSource, databaseSchemaPrefix, EntityPopulator.FARE_TRANSFER_RULE); networks = new JDBCTableReader(Table.NETWORKS, dataSource, databaseSchemaPrefix, EntityPopulator.NETWORK); - routeNetworks = new JDBCTableReader(Table.ROUTE_NETWORKS, dataSource, databaseSchemaPrefix, EntityPopulator.ROUTE_NETWORK); } /** diff --git a/src/main/java/com/conveyal/gtfs/loader/JdbcGTFSFeedConverter.java b/src/main/java/com/conveyal/gtfs/loader/JdbcGTFSFeedConverter.java index 06b328538..ada60e709 100644 --- a/src/main/java/com/conveyal/gtfs/loader/JdbcGTFSFeedConverter.java +++ b/src/main/java/com/conveyal/gtfs/loader/JdbcGTFSFeedConverter.java @@ -131,7 +131,6 @@ public FeedLoadResult loadTables () { copyEntityToSql(gtfsFeed.fare_leg_rules.values(), Table.FARE_LEG_RULES); copyEntityToSql(gtfsFeed.fare_transfer_rules.values(), Table.FARE_TRANSFER_RULES); copyEntityToSql(gtfsFeed.networks.values(), Table.NETWORKS); - copyEntityToSql(gtfsFeed.route_networks.values(), Table.ROUTE_NETWORKS); // result.errorCount = errorStorage.getErrorCount(); // This will commit and close the single connection that has been shared between all preceding load steps. diff --git a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java index 8fdd161b0..0ead1464d 100644 --- a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java +++ b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java @@ -3,6 +3,7 @@ import com.conveyal.gtfs.GTFSFeed; import com.conveyal.gtfs.model.Calendar; import com.conveyal.gtfs.model.CalendarDate; +import com.conveyal.gtfs.model.Route; import com.conveyal.gtfs.model.ScheduleException; import com.conveyal.gtfs.model.Service; import com.conveyal.gtfs.model.Stop; @@ -353,7 +354,7 @@ public FeedLoadResult exportTables() { result.fareLegRules = export(Table.FARE_LEG_RULES, connection); result.fareTransferRules = export(Table.FARE_TRANSFER_RULES, connection); result.networks = export(Table.NETWORKS, connection); - result.routeNetworks = export(Table.ROUTE_NETWORKS, connection); + result.routeNetworks = Route.exportRouteNetworks(dataSource, feedIdToExport, zipOutputStream); exportProprietaryFiles(result); diff --git a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsLoader.java b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsLoader.java index df60e2e02..bd6788086 100644 --- a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsLoader.java +++ b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsLoader.java @@ -163,7 +163,6 @@ public FeedLoadResult loadTables() { result.fareMedias = load(Table.FARE_MEDIAS); result.fareProducts = load(Table.FARE_PRODUCTS); result.networks = load(Table.NETWORKS); - result.routeNetworks = load(Table.ROUTE_NETWORKS); // refs networks. result.areas = load(Table.AREAS); result.fareLegRules = load(Table.FARE_LEG_RULES); // ref areas result.fareTransferRules = load(Table.FARE_TRANSFER_RULES); diff --git a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsSnapshotter.java b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsSnapshotter.java index c04fab949..1b28b1706 100644 --- a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsSnapshotter.java +++ b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsSnapshotter.java @@ -123,7 +123,6 @@ public SnapshotResult copyTables() { result.fareLegRules = copy(Table.FARE_LEG_RULES, true); result.fareTransferRules = copy(Table.FARE_TRANSFER_RULES, true); result.networks = copy(Table.NETWORKS, true); - result.routeNetworks = copy(Table.ROUTE_NETWORKS, true); result.completionTime = System.currentTimeMillis(); result.loadTimeMillis = result.completionTime - startTime; diff --git a/src/main/java/com/conveyal/gtfs/loader/Table.java b/src/main/java/com/conveyal/gtfs/loader/Table.java index 609272835..0f88764fa 100644 --- a/src/main/java/com/conveyal/gtfs/loader/Table.java +++ b/src/main/java/com/conveyal/gtfs/loader/Table.java @@ -229,7 +229,8 @@ public Table (String name, Class entityClass, Requirement requ new ShortField("status", EDITOR, 2), new ShortField("continuous_pickup", OPTIONAL,3), new ShortField("continuous_drop_off", OPTIONAL,3), - new StringField("network_id", OPTIONAL) + new StringField("network_id", OPTIONAL), + new StringField("route_network_ids", OPTIONAL) ).addPrimaryKey() .addPrimaryKeyNames("route_id"); @@ -377,15 +378,6 @@ public Table (String name, Class entityClass, Requirement requ .restrictDelete() .addPrimaryKeyNames(Network.NETWORK_ID_NAME); - public static final Table ROUTE_NETWORKS = new Table(RouteNetwork.TABLE_NAME, RouteNetwork.class, OPTIONAL, - new StringField(RouteNetwork.ROUTE_ID_NAME, REQUIRED).isReferenceTo(ROUTES), - new StringField(RouteNetwork.NETWORK_ID_NAME, REQUIRED).isReferenceTo(NETWORKS) - ) - // Although within the context of this table the route id is unique, the unique value e.g. route_id:1 has already - // been flagged as unique when the route table is loaded! - .keyFieldIsNotUnique() - .addPrimaryKeyNames(RouteNetwork.ROUTE_ID_NAME); - // GTFS reference: https://developers.google.com/transit/gtfs/reference#fare_rulestxt public static final Table FARE_RULES = new Table("fare_rules", FareRule.class, OPTIONAL, new StringField("fare_id", REQUIRED).isReferenceTo(FARE_ATTRIBUTES), @@ -545,7 +537,6 @@ public Table (String name, Class entityClass, Requirement requ FARE_TRANSFER_RULES, FEED_INFO, ROUTES, - ROUTE_NETWORKS, PATTERNS, SHAPES, STOPS, diff --git a/src/main/java/com/conveyal/gtfs/model/Route.java b/src/main/java/com/conveyal/gtfs/model/Route.java index 3689b869d..7a71e54ff 100644 --- a/src/main/java/com/conveyal/gtfs/model/Route.java +++ b/src/main/java/com/conveyal/gtfs/model/Route.java @@ -2,14 +2,43 @@ import com.conveyal.gtfs.GTFSFeed; import com.conveyal.gtfs.error.NoAgencyInFeedError; +import com.conveyal.gtfs.loader.EntityPopulator; +import com.conveyal.gtfs.loader.JDBCTableReader; +import com.conveyal.gtfs.loader.Table; +import com.conveyal.gtfs.loader.TableLoadResult; +import com.conveyal.gtfs.loader.TableReader; +import com.csvreader.CsvReader; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import javax.sql.DataSource; import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringReader; import java.net.URL; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; -public class Route extends Entity { // implements Entity.Factory +import static com.conveyal.gtfs.model.RouteNetwork.ROUTE_NETWORK_FILE_NAME; +import static com.conveyal.gtfs.util.CsvReaderUtil.hasExpectedNumberOfColumns; + +public class Route extends Entity { + + private static final Logger LOG = LoggerFactory.getLogger(Route.class); private static final long serialVersionUID = -819444896818029068L; @@ -56,7 +85,7 @@ public class Route extends Entity { // implements Entity.Factory public static final String ROUTE_NETWORK_IDS_FIELD = "route_network_ids"; public static final String TABLE_NAME = "routes"; - private static final String[] CSV_HEADER_FOR_WRITE = new String[] { + private static final String[] CSV_FIELDS_FOR_MERGE = new String[] { ROUTE_ID_FIELD, AGENCY_ID_FIELD, ROUTE_SHORT_NAME_FIELD, @@ -74,7 +103,7 @@ public class Route extends Entity { // implements Entity.Factory }; private static final String CSV_HEADER_FOR_MERGE = String.format( "%s,%s%s", - String.join(",", CSV_HEADER_FOR_WRITE), + String.join(",", CSV_FIELDS_FOR_MERGE), ROUTE_NETWORK_IDS_FIELD, System.lineSeparator() ); @@ -110,6 +139,8 @@ public void setStatementParameters(PreparedStatement statement, boolean setDefau setIntParameter(statement, oneBasedIndex++, continuous_pickup); setIntParameter(statement, oneBasedIndex++, continuous_drop_off); statement.setString(oneBasedIndex, network_id); + statement.setString(oneBasedIndex, route_network_ids); + } public static class Loader extends Entity.Loader { @@ -153,6 +184,7 @@ public void loadOneRow() throws IOException { r.continuous_pickup = getIntField("continuous_pickup", false, 0, 3, INT_MISSING); r.continuous_drop_off = getIntField("continuous_drop_off", false, 0, 3, INT_MISSING); r.network_id = getStringField("network_id", false); + r.route_network_ids = getStringField(ROUTE_NETWORK_IDS_FIELD, false); r.feed = feed; r.feed_id = feed.feedId; // Attempting to put a null key or value will cause an NPE in BTreeMap @@ -209,4 +241,177 @@ public Iterator iterator() { return feed.routes.values().iterator(); } } + + /** + * Merge route networks into routes when loading from file. + */ + public static void getCsvReaderForRoutesWithRouteNetworks(Map routes, Map routeNetworks) { + Map> routeNetworksByRouteId = new HashMap<>(); + + routeNetworks.values().forEach(routeNetwork -> + routeNetworksByRouteId + .computeIfAbsent(routeNetwork.route_id, id -> new HashSet<>()) + .add(routeNetwork.network_id) + ); + + routes.values().forEach(route -> route.route_network_ids = getRouteNetworkIds(routeNetworksByRouteId, route.route_id)); + } + + /** + * Merge route networks into routes when loading into DB. + */ + public static CsvReader getCsvReaderForRoutesWithRouteNetworks( + CsvReader routesReader, + Map> routeNetworksByRouteId + ) { + List rows = new ArrayList<>(); + try { + while (routesReader.readRecord()) { + String routeId = routesReader.get(ROUTE_ID_FIELD); + rows.add(createRow(routesReader, getRouteNetworkIds(routeNetworksByRouteId, routeId))); + } + return (rows.isEmpty()) + ? routesReader + : produceCsvPayload(rows); + } catch (Exception e) { + LOG.error("Error while merging routes", e); + // Any issues, return the original routes reader (minus route networks). + return routesReader; + } + } + + /** + * Get all route networks matching provided route id. + */ + public static String getRouteNetworkIds(Map> routeNetworksByRouteId, String routeId) { + return Optional.ofNullable(routeNetworksByRouteId.get(routeId)) + .map(routeNetworkIds -> String.join(SEPARATOR, routeNetworkIds)) + .orElse(""); + } + + /** + * Create a CSV row of original route fields plus the route networks ids. + */ + private static String createRow(CsvReader routesReader, String routeNetworkIds) throws IOException { + String[] fields = new String[CSV_FIELDS_FOR_MERGE.length]; + for (int i = 0; i < CSV_FIELDS_FOR_MERGE.length; i++) { + fields[i] = routesReader.get(CSV_FIELDS_FOR_MERGE[i]); + } + return String.join(",", fields) + "," + routeNetworkIds; + } + + + /** + * Convert the multiple routes (with route networks) back into CSV, with header and return a {@link CsvReader} + * representation. + */ + private static CsvReader produceCsvPayload(List rows) { + StringBuilder csvContent = new StringBuilder(); + csvContent.append(CSV_HEADER_FOR_MERGE); + rows.forEach(row -> csvContent.append(row).append(System.lineSeparator())); + return new CsvReader(new StringReader(csvContent.toString())); + } + + /** + * Extract the route networks from file and group by route id. This is to allow for easier CRUD by the DT UI. + */ + public static Map> groupRouteNetworkIds(CsvReader csvReader, List errors) { + Map> routeNetworksGroupedByRouteId = new HashMap<>(); + + try { + while (csvReader.readRecord()) { + if (!hasExpectedNumberOfColumns(csvReader, errors, 2)) { + continue; + } + String routeNetworkId = csvReader.get(RouteNetwork.NETWORK_ID_FIELD); + String routeId = csvReader.get(RouteNetwork.ROUTE_ID_FIELD); + routeNetworksGroupedByRouteId.computeIfAbsent(routeId, k -> new HashSet<>()).add(routeNetworkId); + } + return routeNetworksGroupedByRouteId; + } catch (IOException e) { + return Collections.emptyMap(); + } + } + + /** + * Expand the route network ids and write to zip file. + */ + public static void writeRouteNetworksToFile(ZipOutputStream zipOutputStream, List routes) throws IOException { + // Create entry for table. + zipOutputStream.putNextEntry(new ZipEntry(ROUTE_NETWORK_FILE_NAME)); + // Create and use PrintWriter, but don't close. This is done when the zip entry is closed. + PrintWriter p = new PrintWriter(zipOutputStream); + p.print(packRouteNetworks(routes)); + p.flush(); + zipOutputStream.closeEntry(); + } + + /** + * Expand all route network ids into a single row for each route id. This is to conform with the GTFS Fares v2 standard. + */ + public static String packRouteNetworks(List routes) { + StringBuilder csvContent = new StringBuilder(createRow(RouteNetwork.ROUTE_ID_FIELD, RouteNetwork.NETWORK_ID_FIELD)); + routes + .stream() + .filter(route -> route.route_network_ids != null) + .forEach(route -> { + String[] routeNetworkIds = route.route_network_ids.split(SEPARATOR); + for (String routeNetworkId : routeNetworkIds) { + csvContent.append(createRow(routeNetworkId, route.route_id)); + } + }); + return csvContent.toString(); + } + + /** + * Create a row from the column values provided. + */ + private static String createRow(String... columnValues) { + return String.join(",", columnValues) + System.lineSeparator(); + } + + /** + * Export route networks. + */ + public static TableLoadResult exportRouteNetworks( + DataSource dataSource, + String feedIdToExport, + ZipOutputStream zipOutputStream + ) { + long startTime = System.currentTimeMillis(); + TableLoadResult tableLoadResult = new TableLoadResult(); + + try { + final TableReader routeIterator = new JDBCTableReader<>( + Table.ROUTES, + dataSource, + feedIdToExport + ".", + EntityPopulator.ROUTE + ); + + List routesWithRouteNetworks = StreamSupport + .stream(routeIterator.spliterator(), false) + .filter(route -> !StringUtils.isBlank(route.route_network_ids)) + .collect(Collectors.toList()); + + // Only export if data is available. + if (routesWithRouteNetworks.isEmpty()) { + LOG.warn("No route networks exported as none have been defined!"); + return tableLoadResult; + } + + tableLoadResult.rowCount = routesWithRouteNetworks.size(); + writeRouteNetworksToFile(zipOutputStream, routesWithRouteNetworks); + + long duration = System.currentTimeMillis() - startTime; + LOG.info("Copied {} {} in {} ms.", tableLoadResult.rowCount, ROUTE_NETWORK_FILE_NAME, duration); + + } catch (IOException e) { + tableLoadResult.fatalException = e.toString(); + LOG.error("Exception while exporting {}", ROUTE_NETWORK_FILE_NAME, e); + } + + return tableLoadResult; + } + } diff --git a/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java b/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java index 7afefb898..15c12ef4a 100644 --- a/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java +++ b/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java @@ -19,8 +19,8 @@ public class RouteNetwork extends Entity { public String feed_id; public static final String TABLE_NAME = "route_networks"; - public static final String NETWORK_ID_NAME = "network_id"; - public static final String ROUTE_ID_NAME = "route_id"; + public static final String NETWORK_ID_FIELD = "network_id"; + public static final String ROUTE_ID_FIELD = "route_id"; public static final String ROUTE_NETWORK_FILE_NAME = "route_networks.txt"; public static final int ROUTE_NETWORK_NUMBER_OF_HEADERS = 2; @@ -56,13 +56,13 @@ protected boolean isRequired() { public void loadOneRow() throws IOException { RouteNetwork routeNetwork = new RouteNetwork(); routeNetwork.id = row + 1; // offset line number by 1 to account for 0-based row index - routeNetwork.network_id = getStringField(NETWORK_ID_NAME, true); - routeNetwork.route_id = getStringField(ROUTE_ID_NAME, true); + routeNetwork.network_id = getStringField(NETWORK_ID_FIELD, true); + routeNetwork.route_id = getStringField(ROUTE_ID_FIELD, true); routeNetwork.feed = feed; routeNetwork.feed_id = feed.feedId; feed.route_networks.put(routeNetwork.getId(), routeNetwork); - getRefField(NETWORK_ID_NAME, true, feed.networks); - getRefField(ROUTE_ID_NAME, true, feed.routes); + getRefField(NETWORK_ID_FIELD, true, feed.networks); + getRefField(ROUTE_ID_FIELD, true, feed.routes); } } @@ -74,7 +74,7 @@ public Writer(GTFSFeed feed) { @Override public void writeHeaders() throws IOException { - writer.writeRecord(new String[] {NETWORK_ID_NAME, ROUTE_ID_NAME}); + writer.writeRecord(new String[] {NETWORK_ID_FIELD, ROUTE_ID_FIELD}); } @Override diff --git a/src/main/java/com/conveyal/gtfs/model/Stop.java b/src/main/java/com/conveyal/gtfs/model/Stop.java index 1ffedec72..84248b3f9 100644 --- a/src/main/java/com/conveyal/gtfs/model/Stop.java +++ b/src/main/java/com/conveyal/gtfs/model/Stop.java @@ -242,7 +242,7 @@ public static CsvReader getCsvReaderForStopsWithStopAreas( /** * Get all stop areas matching provided stop id. */ - private static String getStopAreaIds(Map> stopAreasByStopId, String stopId) { + public static String getStopAreaIds(Map> stopAreasByStopId, String stopId) { return Optional.ofNullable(stopAreasByStopId.get(stopId)) .map(areas -> String.join(SEPARATOR, areas)) .orElse(""); diff --git a/src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java b/src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java index 6f43fc751..6e019ff2e 100644 --- a/src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java +++ b/src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java @@ -3,6 +3,8 @@ import com.conveyal.gtfs.error.NewGTFSError; import com.conveyal.gtfs.error.SQLErrorStorage; import com.conveyal.gtfs.loader.Table; +import com.conveyal.gtfs.model.Route; +import com.conveyal.gtfs.model.RouteNetwork; import com.conveyal.gtfs.model.Stop; import com.csvreader.CsvReader; import org.apache.commons.io.input.BOMInputStream; @@ -89,6 +91,8 @@ public static CsvReader getCsvReaderAccordingToFileName( CsvReader csvReader; if (tableFileName.equals(STOPS_FILE_NAME)) { csvReader = getCsvReaderFromStopsFile(zipFile, entry, errors); + } else if (tableFileName.equals(RouteNetwork.ROUTE_NETWORK_FILE_NAME)) { + csvReader = getCsvReaderFromRoutesFile(zipFile, entry, errors); } else { csvReader = getCsvReaderFromFile(zipFile, entry); } @@ -122,6 +126,33 @@ private static CsvReader getCsvReaderFromStopsFile( return stopsReader; } + /** + * If the feed contains route networks extract and merge with routes. If not, just return routes. + */ + private static CsvReader getCsvReaderFromRoutesFile( + ZipFile zipFile, + ZipEntry routesEntry, + List errors + ) throws IOException { + ZipEntry routeNetworksEntry = getEntryFromZipFile(zipFile, RouteNetwork.ROUTE_NETWORK_FILE_NAME); + CsvReader routesReader = getCsvReaderFromFile(zipFile, routesEntry); + if (routeNetworksEntry != null) { + routesReader.setSkipEmptyRecords(false); + routesReader.readHeaders(); + // Route networks present in zip file. + CsvReader routeNetworksReader = getCsvReaderForFile(zipFile, routeNetworksEntry, errors, RouteNetwork.ROUTE_NETWORK_NUMBER_OF_HEADERS); + if (routeNetworksReader != null) { + Map> routeNetworks = Route.groupRouteNetworkIds(routeNetworksReader, errors); + if (!routeNetworks.isEmpty()) { + // Merge route networks into routes. + return Route.getCsvReaderForRoutesWithRouteNetworks(routesReader, routeNetworks); + } + } + } + // No route networks, provide just routes as defined in the feed. + return routesReader; + } + /** * Create a {@link CsvReader} from file and check that the number of headers meets the expected number of headers. */ diff --git a/src/test/java/com/conveyal/gtfs/dto/RouteDTO.java b/src/test/java/com/conveyal/gtfs/dto/RouteDTO.java index 6b8cf8444..53ba624b2 100644 --- a/src/test/java/com/conveyal/gtfs/dto/RouteDTO.java +++ b/src/test/java/com/conveyal/gtfs/dto/RouteDTO.java @@ -27,5 +27,6 @@ public class RouteDTO { public int continuous_pickup; public int continuous_drop_off; public String network_id; + public String route_network_ids; } diff --git a/src/test/java/com/conveyal/gtfs/dto/RouteNetworkDTO.java b/src/test/java/com/conveyal/gtfs/dto/RouteNetworkDTO.java deleted file mode 100644 index 1ad077c5e..000000000 --- a/src/test/java/com/conveyal/gtfs/dto/RouteNetworkDTO.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.conveyal.gtfs.dto; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -/** - * DTO used to model expected {@link com.conveyal.gtfs.model.RouteNetwork} JSON structure for the editor. NOTE: reference types - * (e.g., Integer and Double) are used here in order to model null/empty values in JSON object. - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class RouteNetworkDTO { - public Integer id; - public String network_id; - public String route_id; -} diff --git a/src/test/java/com/conveyal/gtfs/graphql/GTFSGraphQLTest.java b/src/test/java/com/conveyal/gtfs/graphql/GTFSGraphQLTest.java index 33afeaa9c..2aeaa7dbe 100644 --- a/src/test/java/com/conveyal/gtfs/graphql/GTFSGraphQLTest.java +++ b/src/test/java/com/conveyal/gtfs/graphql/GTFSGraphQLTest.java @@ -305,13 +305,6 @@ void canFetchNetworks() { }); } - @Test - void canFetchRouteNetworks() { - assertTimeout(Duration.ofMillis(TEST_TIMEOUT), () -> { - MatcherAssert.assertThat(queryFaresV2GraphQL("feedRouteNetworks.txt"), matchesSnapshot()); - }); - } - @Test void canFetchRoutesAndFilterTripsByDateAndTime() { Map variables = new HashMap<>(); diff --git a/src/test/java/com/conveyal/gtfs/loader/JDBCTableWriterFaresV2Test.java b/src/test/java/com/conveyal/gtfs/loader/JDBCTableWriterFaresV2Test.java index 0da891926..73c856a72 100644 --- a/src/test/java/com/conveyal/gtfs/loader/JDBCTableWriterFaresV2Test.java +++ b/src/test/java/com/conveyal/gtfs/loader/JDBCTableWriterFaresV2Test.java @@ -103,11 +103,6 @@ private static Stream createEntityInput() throws IOException { getEntityFromFile("fare_transfer_rules.json"), getEntityFromFile("fare_transfer_rules_updated.json") ), - Arguments.of( - Table.ROUTE_NETWORKS, - getEntityFromFile("route_networks.json"), - getEntityFromFile("route_networks_updated.json") - ), Arguments.of( Table.NETWORKS, getEntityFromFile("networks.json"), From a93b5bc7ad941c8e2dff649f847f6cc2e2596418 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Thu, 21 Aug 2025 09:39:07 +0100 Subject: [PATCH 30/40] improvement(Further dev work): --- .../gtfs/loader/JdbcGtfsExporter.java | 2 +- .../java/com/conveyal/gtfs/model/Route.java | 112 ++++++++++++++---- .../com/conveyal/gtfs/model/RouteNetwork.java | 4 - 3 files changed, 87 insertions(+), 31 deletions(-) diff --git a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java index 0ead1464d..484051394 100644 --- a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java +++ b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java @@ -266,7 +266,7 @@ public FeedLoadResult exportTables() { ) ); } else { - result.routes = export(Table.ROUTES, connection); + result.routes = Route.exportRoutes(dataSource, feedIdToExport, zipOutputStream); } // Only write shapes for "approved" routes using COPY TO with results of select query diff --git a/src/main/java/com/conveyal/gtfs/model/Route.java b/src/main/java/com/conveyal/gtfs/model/Route.java index 7a71e54ff..74888b433 100644 --- a/src/main/java/com/conveyal/gtfs/model/Route.java +++ b/src/main/java/com/conveyal/gtfs/model/Route.java @@ -15,7 +15,6 @@ import javax.sql.DataSource; import java.io.IOException; import java.io.PrintWriter; -import java.io.StringReader; import java.net.URL; import java.sql.PreparedStatement; import java.sql.SQLException; @@ -84,8 +83,11 @@ public class Route extends Entity { public static final String NETWORK_ID_FIELD = "network_id"; public static final String ROUTE_NETWORK_IDS_FIELD = "route_network_ids"; + public static final String ROUTE_FILE_NAME = "routes.txt"; + public static final String TABLE_NAME = "routes"; - private static final String[] CSV_FIELDS_FOR_MERGE = new String[] { + + private static final String[] CSV_FIELDS = new String[] { ROUTE_ID_FIELD, AGENCY_ID_FIELD, ROUTE_SHORT_NAME_FIELD, @@ -101,12 +103,18 @@ public class Route extends Entity { CONTINUOUS_DROP_OFF_FIELD, NETWORK_ID_FIELD }; + private static final String CSV_HEADER_FOR_MERGE = String.format( - "%s,%s%s", - String.join(",", CSV_FIELDS_FOR_MERGE), - ROUTE_NETWORK_IDS_FIELD, - System.lineSeparator() + "%s,%s%n", + String.join(",", CSV_FIELDS), + ROUTE_NETWORK_IDS_FIELD ); + + private static final String CSV_HEADER_FOR_EXPORT = String.format( + "%s", + String.join(",", CSV_FIELDS) + ); + @Override public String getId () { return route_id; @@ -138,7 +146,7 @@ public void setStatementParameters(PreparedStatement statement, boolean setDefau setIntParameter(statement, oneBasedIndex++, 0); setIntParameter(statement, oneBasedIndex++, continuous_pickup); setIntParameter(statement, oneBasedIndex++, continuous_drop_off); - statement.setString(oneBasedIndex, network_id); + statement.setString(oneBasedIndex++, network_id); statement.setString(oneBasedIndex, route_network_ids); } @@ -272,7 +280,7 @@ public static CsvReader getCsvReaderForRoutesWithRouteNetworks( } return (rows.isEmpty()) ? routesReader - : produceCsvPayload(rows); + : produceCsvPayload(rows, CSV_HEADER_FOR_MERGE); } catch (Exception e) { LOG.error("Error while merging routes", e); // Any issues, return the original routes reader (minus route networks). @@ -293,25 +301,13 @@ public static String getRouteNetworkIds(Map> routeNetworksBy * Create a CSV row of original route fields plus the route networks ids. */ private static String createRow(CsvReader routesReader, String routeNetworkIds) throws IOException { - String[] fields = new String[CSV_FIELDS_FOR_MERGE.length]; - for (int i = 0; i < CSV_FIELDS_FOR_MERGE.length; i++) { - fields[i] = routesReader.get(CSV_FIELDS_FOR_MERGE[i]); + String[] fields = new String[CSV_FIELDS.length]; + for (int i = 0; i < CSV_FIELDS.length; i++) { + fields[i] = routesReader.get(CSV_FIELDS[i]); } return String.join(",", fields) + "," + routeNetworkIds; } - - /** - * Convert the multiple routes (with route networks) back into CSV, with header and return a {@link CsvReader} - * representation. - */ - private static CsvReader produceCsvPayload(List rows) { - StringBuilder csvContent = new StringBuilder(); - csvContent.append(CSV_HEADER_FOR_MERGE); - rows.forEach(row -> csvContent.append(row).append(System.lineSeparator())); - return new CsvReader(new StringReader(csvContent.toString())); - } - /** * Extract the route networks from file and group by route id. This is to allow for easier CRUD by the DT UI. */ @@ -364,10 +360,74 @@ public static String packRouteNetworks(List routes) { } /** - * Create a row from the column values provided. + * Export routes, minus route networks. + */ + public static TableLoadResult exportRoutes( + DataSource dataSource, + String feedIdToExport, + ZipOutputStream zipOutputStream + ) { + long startTime = System.currentTimeMillis(); + TableLoadResult tableLoadResult = new TableLoadResult(); + + try { + final TableReader routeIterator = new JDBCTableReader<>( + Table.ROUTES, + dataSource, + feedIdToExport + ".", + EntityPopulator.ROUTE + ); + + List routes = new ArrayList<>(); + routeIterator.forEach(routes::add); + writeRoutesToFile(zipOutputStream, routes); + + long duration = System.currentTimeMillis() - startTime; + LOG.info("Copied {} {} in {} ms.", tableLoadResult.rowCount, ROUTE_FILE_NAME, duration); + + } catch (IOException e) { + tableLoadResult.fatalException = e.toString(); + LOG.error("Exception while exporting {}", ROUTE_FILE_NAME, e); + } + + return tableLoadResult; + } + + /** + * Write routes to zip file. + */ + public static void writeRoutesToFile(ZipOutputStream zipOutputStream, List routes) throws IOException { + // Create entry for table. + zipOutputStream.putNextEntry(new ZipEntry(ROUTE_FILE_NAME)); + // Create and use PrintWriter, but don't close. This is done when the zip entry is closed. + PrintWriter p = new PrintWriter(zipOutputStream); + p.print(packRoutes(routes)); + p.flush(); + zipOutputStream.closeEntry(); + } + + /** + * Expand all stops into a single row. */ - private static String createRow(String... columnValues) { - return String.join(",", columnValues) + System.lineSeparator(); + public static String packRoutes(List routes) { + StringBuilder csvContent = new StringBuilder(createRow(CSV_HEADER_FOR_EXPORT)); + routes.forEach(route -> csvContent.append(createRow( + computeCsvValue(route.route_id), + computeCsvValue(route.agency_id), + computeCsvValue(route.route_short_name), + computeCsvValue(route.route_long_name), + computeCsvValue(route.route_desc), + computeCsvValue(route.route_type), + computeCsvValue(route.route_url), + computeCsvValue(route.route_color), + computeCsvValue(route.route_sort_order), + computeCsvValue(route.route_text_color), + computeCsvValue(route.route_branding_url), + computeCsvValue(route.continuous_pickup), + computeCsvValue(route.continuous_drop_off), + computeCsvValue(route.network_id) + ))); + return csvContent.toString(); } /** diff --git a/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java b/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java index 15c12ef4a..92decb904 100644 --- a/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java +++ b/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java @@ -29,10 +29,6 @@ public String getId () { return route_id; } - /** - * Sets the parameters for a prepared statement following the parameter order defined in - * {@link com.conveyal.gtfs.loader.Table#ROUTE_NETWORKS}. JDBC prepared statement parameters use a one-based index. - */ @Override public void setStatementParameters(PreparedStatement statement, boolean setDefaultId) throws SQLException { int oneBasedIndex = 1; From d04597954439cbe69d58d7db6940e4c4857d6033 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Thu, 21 Aug 2025 14:56:59 +0100 Subject: [PATCH 31/40] improvement(Minor changes and address bug): --- src/main/java/com/conveyal/gtfs/GTFSFeed.java | 5 +- .../java/com/conveyal/gtfs/model/Route.java | 72 +++++++------------ .../com/conveyal/gtfs/util/CsvReaderUtil.java | 2 +- 3 files changed, 28 insertions(+), 51 deletions(-) diff --git a/src/main/java/com/conveyal/gtfs/GTFSFeed.java b/src/main/java/com/conveyal/gtfs/GTFSFeed.java index 3be39b33d..f7f1a5e25 100644 --- a/src/main/java/com/conveyal/gtfs/GTFSFeed.java +++ b/src/main/java/com/conveyal/gtfs/GTFSFeed.java @@ -41,6 +41,8 @@ import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; +import static com.conveyal.gtfs.model.RouteNetwork.ROUTE_NETWORK_FILE_NAME; + /** * All entities must be from a single feed namespace. * Composed of several GTFSTables. @@ -242,7 +244,7 @@ public void toFile (String file) { new Route.Writer(this).writeTable(zip); if (!routes.isEmpty()) { // Export route networks. - Route.writeRouteNetworksToFile(zip, new ArrayList<>(routes.values())); + Route.writeEntityToFile(zip, new ArrayList<>(routes.values()), ROUTE_NETWORK_FILE_NAME); } new Stop.Writer(this).writeTable(zip); if (!stops.isEmpty()) { @@ -259,7 +261,6 @@ public void toFile (String file) { new Area.Writer(this).writeTable(zip); new TimeFrame.Writer(this).writeTable(zip); new Network.Writer(this).writeTable(zip); - new RouteNetwork.Writer(this).writeTable(zip); new FareMedia.Writer(this).writeTable(zip); new FareProduct.Writer(this).writeTable(zip); new FareLegRule.Writer(this).writeTable(zip); diff --git a/src/main/java/com/conveyal/gtfs/model/Route.java b/src/main/java/com/conveyal/gtfs/model/Route.java index 74888b433..9ead2677b 100644 --- a/src/main/java/com/conveyal/gtfs/model/Route.java +++ b/src/main/java/com/conveyal/gtfs/model/Route.java @@ -166,7 +166,7 @@ protected boolean isRequired() { public void loadOneRow() throws IOException { Route r = new Route(); r.id = row + 1; // offset line number by 1 to account for 0-based row index - r.route_id = getStringField("route_id", true); + r.route_id = getStringField(ROUTE_ID_FIELD, true); Agency agency = getRefField("agency_id", false, feed.agency); if (agency == null) { @@ -180,18 +180,18 @@ public void loadOneRow() throws IOException { r.agency_id = agency.agency_id; } - r.route_short_name = getStringField("route_short_name", false); // one or the other required, needs a special validator - r.route_long_name = getStringField("route_long_name", false); - r.route_desc = getStringField("route_desc", false); - r.route_type = getIntField("route_type", true, 0, 7); - r.route_sort_order = getIntField("route_sort_order", false, 0, Integer.MAX_VALUE); - r.route_url = getUrlField("route_url", false); - r.route_color = getStringField("route_color", false); - r.route_text_color = getStringField("route_text_color", false); - r.route_branding_url = getUrlField("route_branding_url", false); - r.continuous_pickup = getIntField("continuous_pickup", false, 0, 3, INT_MISSING); - r.continuous_drop_off = getIntField("continuous_drop_off", false, 0, 3, INT_MISSING); - r.network_id = getStringField("network_id", false); + r.route_short_name = getStringField(ROUTE_SHORT_NAME_FIELD, false); // one or the other required, needs a special validator + r.route_long_name = getStringField(ROUTE_LONG_NAME_FIELD, false); + r.route_desc = getStringField(ROUTE_DESC_FIELD, false); + r.route_type = getIntField(ROUTE_TYPE_FIELD, true, 0, 7); + r.route_sort_order = getIntField(ROUTE_SORT_ORDER_FIELD, false, 0, Integer.MAX_VALUE); + r.route_url = getUrlField(ROUTE_URL_FIELD, false); + r.route_color = getStringField(ROUTE_COLOR_FIELD, false); + r.route_text_color = getStringField(ROUTE_TEXT_COLOR_FIELD, false); + r.route_branding_url = getUrlField(ROUTE_BRANDING_URL_FIELD, false); + r.continuous_pickup = getIntField(CONTINUOUS_PICKUP_FIELD, false, 0, 3, INT_MISSING); + r.continuous_drop_off = getIntField(CONTINUOUS_DROP_OFF_FIELD, false, 0, 3, INT_MISSING); + r.network_id = getStringField(NETWORK_ID_FIELD, false); r.route_network_ids = getStringField(ROUTE_NETWORK_IDS_FIELD, false); r.feed = feed; r.feed_id = feed.feedId; @@ -208,21 +208,7 @@ public Writer (GTFSFeed feed) { @Override public void writeHeaders() throws IOException { - writeStringField("agency_id"); - writeStringField("route_id"); - writeStringField("route_short_name"); - writeStringField("route_long_name"); - writeStringField("route_desc"); - writeStringField("route_type"); - writeStringField("route_url"); - writeStringField("route_color"); - writeStringField("route_text_color"); - writeStringField("route_branding_url"); - writeStringField("route_sort_order"); - writeStringField("continuous_pickup"); - writeStringField("continuous_drop_off"); - writeStringField("network_id"); - endRecord(); + writer.writeRecord(CSV_FIELDS); } @Override @@ -329,19 +315,6 @@ public static Map> groupRouteNetworkIds(CsvReader csvReader, } } - /** - * Expand the route network ids and write to zip file. - */ - public static void writeRouteNetworksToFile(ZipOutputStream zipOutputStream, List routes) throws IOException { - // Create entry for table. - zipOutputStream.putNextEntry(new ZipEntry(ROUTE_NETWORK_FILE_NAME)); - // Create and use PrintWriter, but don't close. This is done when the zip entry is closed. - PrintWriter p = new PrintWriter(zipOutputStream); - p.print(packRouteNetworks(routes)); - p.flush(); - zipOutputStream.closeEntry(); - } - /** * Expand all route network ids into a single row for each route id. This is to conform with the GTFS Fares v2 standard. */ @@ -380,7 +353,7 @@ public static TableLoadResult exportRoutes( List routes = new ArrayList<>(); routeIterator.forEach(routes::add); - writeRoutesToFile(zipOutputStream, routes); + writeEntityToFile(zipOutputStream, routes, ROUTE_FILE_NAME); long duration = System.currentTimeMillis() - startTime; LOG.info("Copied {} {} in {} ms.", tableLoadResult.rowCount, ROUTE_FILE_NAME, duration); @@ -394,14 +367,18 @@ public static TableLoadResult exportRoutes( } /** - * Write routes to zip file. + * Write routes or route networks to zip file. */ - public static void writeRoutesToFile(ZipOutputStream zipOutputStream, List routes) throws IOException { + public static void writeEntityToFile(ZipOutputStream zipOutputStream, List routes, String fileName) throws IOException { // Create entry for table. - zipOutputStream.putNextEntry(new ZipEntry(ROUTE_FILE_NAME)); + zipOutputStream.putNextEntry(new ZipEntry(fileName)); // Create and use PrintWriter, but don't close. This is done when the zip entry is closed. PrintWriter p = new PrintWriter(zipOutputStream); - p.print(packRoutes(routes)); + if (fileName.equalsIgnoreCase(ROUTE_FILE_NAME)) { + p.print(packRoutes(routes)); + } else { + p.print(packRouteNetworks(routes)); + } p.flush(); zipOutputStream.closeEntry(); } @@ -461,7 +438,7 @@ public static TableLoadResult exportRouteNetworks( } tableLoadResult.rowCount = routesWithRouteNetworks.size(); - writeRouteNetworksToFile(zipOutputStream, routesWithRouteNetworks); + writeEntityToFile(zipOutputStream, routesWithRouteNetworks, ROUTE_NETWORK_FILE_NAME); long duration = System.currentTimeMillis() - startTime; LOG.info("Copied {} {} in {} ms.", tableLoadResult.rowCount, ROUTE_NETWORK_FILE_NAME, duration); @@ -473,5 +450,4 @@ public static TableLoadResult exportRouteNetworks( return tableLoadResult; } - } diff --git a/src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java b/src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java index 6e019ff2e..a1a4dbdaa 100644 --- a/src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java +++ b/src/main/java/com/conveyal/gtfs/util/CsvReaderUtil.java @@ -91,7 +91,7 @@ public static CsvReader getCsvReaderAccordingToFileName( CsvReader csvReader; if (tableFileName.equals(STOPS_FILE_NAME)) { csvReader = getCsvReaderFromStopsFile(zipFile, entry, errors); - } else if (tableFileName.equals(RouteNetwork.ROUTE_NETWORK_FILE_NAME)) { + } else if (tableFileName.equals(Route.ROUTE_FILE_NAME)) { csvReader = getCsvReaderFromRoutesFile(zipFile, entry, errors); } else { csvReader = getCsvReaderFromFile(zipFile, entry); From f787aa27d5431b244796e4aa8d642935700d5448 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Thu, 21 Aug 2025 15:26:00 +0100 Subject: [PATCH 32/40] improvement(GTFSTest.java): Updated test to allow for now route networks table --- src/test/java/com/conveyal/gtfs/GTFSTest.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/test/java/com/conveyal/gtfs/GTFSTest.java b/src/test/java/com/conveyal/gtfs/GTFSTest.java index 577bca2d0..f946d9281 100644 --- a/src/test/java/com/conveyal/gtfs/GTFSTest.java +++ b/src/test/java/com/conveyal/gtfs/GTFSTest.java @@ -330,7 +330,6 @@ void canLoadAndExportFaresV2Feed() throws IOException { new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), - new ErrorExpectation(NewGTFSErrorType.TABLE_IN_SUBDIRECTORY), new ErrorExpectation(NewGTFSErrorType.FEED_TRAVEL_TIMES_ROUNDED), new ErrorExpectation(NewGTFSErrorType.DATE_NO_SERVICE) ); @@ -400,13 +399,6 @@ void canLoadAndExportFaresV2Feed() throws IOException { new RecordExpectation("network_name", "Forbidden because network id is defined in routes") } ), - new PersistenceExpectation( - "route_networks", - new RecordExpectation[]{ - new RecordExpectation("network_id", "1"), - new RecordExpectation("route_id", "1") - } - ), new PersistenceExpectation( "timeframes", new RecordExpectation[]{ From 519371315c4c084237e93f9d193635ded81d13ca Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Thu, 21 Aug 2025 16:40:43 +0100 Subject: [PATCH 33/40] improvement(Consolidating methods): Moved methods from stops and route to entity --- src/main/java/com/conveyal/gtfs/GTFSFeed.java | 4 +- .../java/com/conveyal/gtfs/model/Entity.java | 56 +++++++++++++++++++ .../java/com/conveyal/gtfs/model/Route.java | 37 ++---------- .../java/com/conveyal/gtfs/model/Stop.java | 44 +-------------- 4 files changed, 64 insertions(+), 77 deletions(-) diff --git a/src/main/java/com/conveyal/gtfs/GTFSFeed.java b/src/main/java/com/conveyal/gtfs/GTFSFeed.java index f7f1a5e25..7410252e2 100644 --- a/src/main/java/com/conveyal/gtfs/GTFSFeed.java +++ b/src/main/java/com/conveyal/gtfs/GTFSFeed.java @@ -244,12 +244,12 @@ public void toFile (String file) { new Route.Writer(this).writeTable(zip); if (!routes.isEmpty()) { // Export route networks. - Route.writeEntityToFile(zip, new ArrayList<>(routes.values()), ROUTE_NETWORK_FILE_NAME); + Entity.writeEntityToFile(zip, new ArrayList<>(routes.values()), ROUTE_NETWORK_FILE_NAME); } new Stop.Writer(this).writeTable(zip); if (!stops.isEmpty()) { // Export stop areas. - Stop.writeEntityToFile(zip, new ArrayList<>(stops.values()), Stop.STOP_AREAS_FILE_NAME); + Entity.writeEntityToFile(zip, new ArrayList<>(stops.values()), Stop.STOP_AREAS_FILE_NAME); } new ShapePoint.Writer(this).writeTable(zip); new Transfer.Writer(this).writeTable(zip); diff --git a/src/main/java/com/conveyal/gtfs/model/Entity.java b/src/main/java/com/conveyal/gtfs/model/Entity.java index e1e4b05f0..d98fb5097 100644 --- a/src/main/java/com/conveyal/gtfs/model/Entity.java +++ b/src/main/java/com/conveyal/gtfs/model/Entity.java @@ -25,6 +25,7 @@ import java.io.FilterOutputStream; import java.io.IOException; import java.io.OutputStream; +import java.io.PrintWriter; import java.io.Serializable; import java.io.StringReader; import java.net.MalformedURLException; @@ -42,12 +43,21 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; +import static com.conveyal.gtfs.model.Route.ROUTE_FILE_NAME; +import static com.conveyal.gtfs.model.Route.packRouteNetworks; +import static com.conveyal.gtfs.model.Route.packRoutes; +import static com.conveyal.gtfs.model.RouteNetwork.ROUTE_NETWORK_FILE_NAME; +import static com.conveyal.gtfs.model.Stop.STOPS_FILE_NAME; +import static com.conveyal.gtfs.model.Stop.STOP_AREAS_FILE_NAME; +import static com.conveyal.gtfs.model.Stop.packStopAreas; +import static com.conveyal.gtfs.model.Stop.packStops; import static com.conveyal.gtfs.util.CsvReaderUtil.getEntryFromZipFile; /** @@ -534,6 +544,17 @@ protected static String createRow(String... columnValues) { return String.join(",", columnValues) + System.lineSeparator(); } + /** + * Create a CSV row from original fields plus grouped (stop areas or route network) ids. + */ + protected static String createRow(CsvReader stopsReader, String ids, String[] csvFields) throws IOException { + String[] fields = new String[csvFields.length]; + for (int i = 0; i < csvFields.length; i++) { + fields[i] = stopsReader.get(csvFields[i]); + } + return String.format("%s,%s%n", String.join(",", fields), ids); + } + protected static String computeCsvValue(String value) { return value != null ? value : ""; } @@ -549,4 +570,39 @@ protected static String computeCsvValue(int value) { protected static String computeCsvValue(double value) { return value != DOUBLE_MISSING ? String.valueOf(value) : ""; } + + /** + * Write routes or route networks to zip file. + */ + public static void writeEntityToFile(ZipOutputStream zipOutputStream, List entities, String fileName) throws IOException { + // Create entry for table. + zipOutputStream.putNextEntry(new ZipEntry(fileName)); + // Create and use PrintWriter, but don't close. This is done when the zip entry is closed. + PrintWriter p = new PrintWriter(zipOutputStream); + switch (fileName) { + case STOPS_FILE_NAME: + p.print(packStops((List) entities)); + break; + case STOP_AREAS_FILE_NAME: + p.print(packStopAreas((List) entities)); + break; + case ROUTE_FILE_NAME: + p.print(packRoutes((List) entities)); + break; + case ROUTE_NETWORK_FILE_NAME: + p.print(packRouteNetworks((List) entities)); + break; + } + p.flush(); + zipOutputStream.closeEntry(); + } + + /** + * Get all child ids matching provided parent id. + */ + protected static String getChildIdsMatchingParentId(Map> groupedChildIds, String parentId) { + return Optional.ofNullable(groupedChildIds.get(parentId)) + .map(id -> String.join(SEPARATOR, id)) + .orElse(""); + } } diff --git a/src/main/java/com/conveyal/gtfs/model/Route.java b/src/main/java/com/conveyal/gtfs/model/Route.java index 9ead2677b..63d0db40d 100644 --- a/src/main/java/com/conveyal/gtfs/model/Route.java +++ b/src/main/java/com/conveyal/gtfs/model/Route.java @@ -8,13 +8,13 @@ import com.conveyal.gtfs.loader.TableLoadResult; import com.conveyal.gtfs.loader.TableReader; import com.csvreader.CsvReader; +import com.google.common.collect.Lists; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.sql.DataSource; import java.io.IOException; -import java.io.PrintWriter; import java.net.URL; import java.sql.PreparedStatement; import java.sql.SQLException; @@ -25,11 +25,9 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.StreamSupport; -import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import static com.conveyal.gtfs.model.RouteNetwork.ROUTE_NETWORK_FILE_NAME; @@ -248,7 +246,7 @@ public static void getCsvReaderForRoutesWithRouteNetworks(Map rou .add(routeNetwork.network_id) ); - routes.values().forEach(route -> route.route_network_ids = getRouteNetworkIds(routeNetworksByRouteId, route.route_id)); + routes.values().forEach(route -> route.route_network_ids = getChildIdsMatchingParentId(routeNetworksByRouteId, route.route_id)); } /** @@ -262,7 +260,7 @@ public static CsvReader getCsvReaderForRoutesWithRouteNetworks( try { while (routesReader.readRecord()) { String routeId = routesReader.get(ROUTE_ID_FIELD); - rows.add(createRow(routesReader, getRouteNetworkIds(routeNetworksByRouteId, routeId))); + rows.add(createRow(routesReader, getChildIdsMatchingParentId(routeNetworksByRouteId, routeId), CSV_FIELDS)); } return (rows.isEmpty()) ? routesReader @@ -274,15 +272,6 @@ public static CsvReader getCsvReaderForRoutesWithRouteNetworks( } } - /** - * Get all route networks matching provided route id. - */ - public static String getRouteNetworkIds(Map> routeNetworksByRouteId, String routeId) { - return Optional.ofNullable(routeNetworksByRouteId.get(routeId)) - .map(routeNetworkIds -> String.join(SEPARATOR, routeNetworkIds)) - .orElse(""); - } - /** * Create a CSV row of original route fields plus the route networks ids. */ @@ -351,8 +340,7 @@ public static TableLoadResult exportRoutes( EntityPopulator.ROUTE ); - List routes = new ArrayList<>(); - routeIterator.forEach(routes::add); + List routes = Lists.newArrayList(routeIterator); writeEntityToFile(zipOutputStream, routes, ROUTE_FILE_NAME); long duration = System.currentTimeMillis() - startTime; @@ -366,23 +354,6 @@ public static TableLoadResult exportRoutes( return tableLoadResult; } - /** - * Write routes or route networks to zip file. - */ - public static void writeEntityToFile(ZipOutputStream zipOutputStream, List routes, String fileName) throws IOException { - // Create entry for table. - zipOutputStream.putNextEntry(new ZipEntry(fileName)); - // Create and use PrintWriter, but don't close. This is done when the zip entry is closed. - PrintWriter p = new PrintWriter(zipOutputStream); - if (fileName.equalsIgnoreCase(ROUTE_FILE_NAME)) { - p.print(packRoutes(routes)); - } else { - p.print(packRouteNetworks(routes)); - } - p.flush(); - zipOutputStream.closeEntry(); - } - /** * Expand all stops into a single row. */ diff --git a/src/main/java/com/conveyal/gtfs/model/Stop.java b/src/main/java/com/conveyal/gtfs/model/Stop.java index 84248b3f9..34de9f5cc 100644 --- a/src/main/java/com/conveyal/gtfs/model/Stop.java +++ b/src/main/java/com/conveyal/gtfs/model/Stop.java @@ -14,7 +14,6 @@ import javax.sql.DataSource; import java.io.IOException; -import java.io.PrintWriter; import java.net.URL; import java.sql.PreparedStatement; import java.sql.SQLException; @@ -25,11 +24,9 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.StreamSupport; -import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import static com.conveyal.gtfs.util.CsvReaderUtil.hasExpectedNumberOfColumns; @@ -213,7 +210,7 @@ public static void getCsvReaderForStopsWithStopAreas(Map stops, Ma .add(stopArea.area_id) ); - stops.values().forEach(stop -> stop.stop_area_ids = getStopAreaIds(stopAreasByStopId, stop.stop_id)); + stops.values().forEach(stop -> stop.stop_area_ids = getChildIdsMatchingParentId(stopAreasByStopId, stop.stop_id)); } /** @@ -227,7 +224,7 @@ public static CsvReader getCsvReaderForStopsWithStopAreas( try { while (stopsReader.readRecord()) { String stopId = stopsReader.get(STOP_ID_FIELD); - rows.add(createRow(stopsReader, getStopAreaIds(stopAreasByStopId, stopId))); + rows.add(createRow(stopsReader, getChildIdsMatchingParentId(stopAreasByStopId, stopId), CSV_FIELDS)); } return (rows.isEmpty()) ? stopsReader @@ -239,26 +236,6 @@ public static CsvReader getCsvReaderForStopsWithStopAreas( } } - /** - * Get all stop areas matching provided stop id. - */ - public static String getStopAreaIds(Map> stopAreasByStopId, String stopId) { - return Optional.ofNullable(stopAreasByStopId.get(stopId)) - .map(areas -> String.join(SEPARATOR, areas)) - .orElse(""); - } - - /** - * Create a CSV row of original stop fields plus the stop area ids. - */ - private static String createRow(CsvReader stopsReader, String stopAreaIds) throws IOException { - String[] fields = new String[CSV_FIELDS.length]; - for (int i = 0; i < CSV_FIELDS.length; i++) { - fields[i] = stopsReader.get(CSV_FIELDS[i]); - } - return String.format("%s,%s%n", String.join(",", fields), stopAreaIds); - } - /** * Extract the stop areas from file and group by stop id. This is to allow for easier CRUD by the DT UI. */ @@ -280,23 +257,6 @@ public static Map> groupStopAreaIds(CsvReader csvReader, Lis } } - /** - * Write stops or stop areas to zip file. - */ - public static void writeEntityToFile(ZipOutputStream zipOutputStream, List stops, String fileName) throws IOException { - // Create entry for table. - zipOutputStream.putNextEntry(new ZipEntry(fileName)); - // Create and use PrintWriter, but don't close. This is done when the zip entry is closed. - PrintWriter p = new PrintWriter(zipOutputStream); - if (fileName.equals(STOPS_FILE_NAME)) { - p.print(packStops(stops)); - } else { - p.print(packStopAreas(stops)); - } - p.flush(); - zipOutputStream.closeEntry(); - } - /** * Expand all stop area ids into a single row for each area id. This is to conform with the GTFS Fares v2 standard. */ From 116a8dd24d1c310e58b46e25e32526e56333d270 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Fri, 22 Aug 2025 12:31:37 +0100 Subject: [PATCH 34/40] improvement(Route from editor export): Update to export only approved routes --- .../conveyal/gtfs/loader/JDBCTableReader.java | 17 ++++++++++------- .../conveyal/gtfs/loader/JdbcGtfsExporter.java | 9 +-------- .../java/com/conveyal/gtfs/model/Route.java | 13 +++++++++++-- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/conveyal/gtfs/loader/JDBCTableReader.java b/src/main/java/com/conveyal/gtfs/loader/JDBCTableReader.java index 0eaa80331..40b893eed 100644 --- a/src/main/java/com/conveyal/gtfs/loader/JDBCTableReader.java +++ b/src/main/java/com/conveyal/gtfs/loader/JDBCTableReader.java @@ -10,12 +10,8 @@ import org.slf4j.LoggerFactory; import javax.sql.DataSource; -import java.lang.reflect.Field; import java.sql.*; -import java.util.Arrays; import java.util.Iterator; -import java.util.List; -import java.util.stream.Collectors; import static java.sql.ResultSet.TYPE_FORWARD_ONLY; import static java.sql.ResultSet.CONCUR_READ_ONLY; @@ -44,7 +40,7 @@ public class JDBCTableReader implements TableReader { /** * @param tablePrefix must not be null, can be empty string, should include any separator character (dot) */ - public JDBCTableReader(Table specTable, DataSource dataSource, String tablePrefix, EntityPopulator entityPopulator) { + public JDBCTableReader(Table specTable, DataSource dataSource, String tablePrefix, EntityPopulator entityPopulator, String whereClause) { qualifiedTableName = tablePrefix + specTable.name; this.dataSource = dataSource; this.entityPopulator = entityPopulator; @@ -53,12 +49,14 @@ public JDBCTableReader(Table specTable, DataSource dataSource, String tablePrefi // We do this in the constructor to avoid rebuilding the mapping every time we fetch a single entity from the table. // No entry value defaults to zero, and SQL columns are 1-based. columnForName = new TObjectIntHashMap<>(); - selectClause = "select * from " + qualifiedTableName; + selectClause = whereClause != null + ? String.format("select * from %s %s", qualifiedTableName, whereClause) + : "select * from " + qualifiedTableName; // Try-with-resources will automatically close the connection when the try block exits. try (Connection connection = dataSource.getConnection()) { LOG.info("Connected to {}", qualifiedTableName); PreparedStatement selectAll = connection.prepareStatement( - selectClause, TYPE_FORWARD_ONLY, CONCUR_READ_ONLY, CLOSE_CURSORS_AT_COMMIT); + selectClause, TYPE_FORWARD_ONLY, CONCUR_READ_ONLY, CLOSE_CURSORS_AT_COMMIT); ResultSetMetaData metaData = selectAll.getMetaData(); int nColumns = metaData.getColumnCount(); for (int c = 1; c <= nColumns; c++) { @@ -69,6 +67,11 @@ public JDBCTableReader(Table specTable, DataSource dataSource, String tablePrefi LOG.warn("Could not connect to required table " + qualifiedTableName); } } + + } + + public JDBCTableReader(Table specTable, DataSource dataSource, String tablePrefix, EntityPopulator entityPopulator) { + this(specTable, dataSource, tablePrefix, entityPopulator, null); } /** diff --git a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java index 484051394..0ef9b4618 100644 --- a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java +++ b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java @@ -257,14 +257,7 @@ public FeedLoadResult exportTables() { // Only write "approved" routes using COPY TO with results of select query if (fromEditor) { // The filter clause for routes is simple. We're just checking that the route is APPROVED. - result.routes = export( - Table.ROUTES, - String.join( - " ", - Table.ROUTES.generateSelectSql(feedIdToExport, Requirement.OPTIONAL), - whereRouteIsApproved - ) - ); + result.routes = Route.exportRoutes(dataSource, feedIdToExport, zipOutputStream, whereRouteIsApproved); } else { result.routes = Route.exportRoutes(dataSource, feedIdToExport, zipOutputStream); } diff --git a/src/main/java/com/conveyal/gtfs/model/Route.java b/src/main/java/com/conveyal/gtfs/model/Route.java index 63d0db40d..95420da55 100644 --- a/src/main/java/com/conveyal/gtfs/model/Route.java +++ b/src/main/java/com/conveyal/gtfs/model/Route.java @@ -321,13 +321,21 @@ public static String packRouteNetworks(List routes) { return csvContent.toString(); } + public static TableLoadResult exportRoutes( + DataSource dataSource, + String feedIdToExport, + ZipOutputStream zipOutputStream + ) { + return exportRoutes(dataSource, feedIdToExport, zipOutputStream, null); + } /** * Export routes, minus route networks. */ public static TableLoadResult exportRoutes( DataSource dataSource, String feedIdToExport, - ZipOutputStream zipOutputStream + ZipOutputStream zipOutputStream, + String whereRouteIsApproved ) { long startTime = System.currentTimeMillis(); TableLoadResult tableLoadResult = new TableLoadResult(); @@ -337,7 +345,8 @@ public static TableLoadResult exportRoutes( Table.ROUTES, dataSource, feedIdToExport + ".", - EntityPopulator.ROUTE + EntityPopulator.ROUTE, + whereRouteIsApproved ); List routes = Lists.newArrayList(routeIterator); From f43fa022d17dacbbe0af4e130ada6caf0baab3f6 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Fri, 22 Aug 2025 12:44:44 +0100 Subject: [PATCH 35/40] improvement(Route.java): Added route export count and removed redundant method --- src/main/java/com/conveyal/gtfs/model/Route.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/main/java/com/conveyal/gtfs/model/Route.java b/src/main/java/com/conveyal/gtfs/model/Route.java index 95420da55..512f541b5 100644 --- a/src/main/java/com/conveyal/gtfs/model/Route.java +++ b/src/main/java/com/conveyal/gtfs/model/Route.java @@ -272,17 +272,6 @@ public static CsvReader getCsvReaderForRoutesWithRouteNetworks( } } - /** - * Create a CSV row of original route fields plus the route networks ids. - */ - private static String createRow(CsvReader routesReader, String routeNetworkIds) throws IOException { - String[] fields = new String[CSV_FIELDS.length]; - for (int i = 0; i < CSV_FIELDS.length; i++) { - fields[i] = routesReader.get(CSV_FIELDS[i]); - } - return String.join(",", fields) + "," + routeNetworkIds; - } - /** * Extract the route networks from file and group by route id. This is to allow for easier CRUD by the DT UI. */ @@ -350,6 +339,7 @@ public static TableLoadResult exportRoutes( ); List routes = Lists.newArrayList(routeIterator); + tableLoadResult.rowCount = routes.size(); writeEntityToFile(zipOutputStream, routes, ROUTE_FILE_NAME); long duration = System.currentTimeMillis() - startTime; From 6739bc6f50295f930b8f0d0c2ee999066c6f4cc5 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Fri, 22 Aug 2025 13:02:30 +0100 Subject: [PATCH 36/40] improvement(Efficiency updates and row count outputs): --- .../java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java | 8 ++------ src/main/java/com/conveyal/gtfs/model/Route.java | 7 ------- src/main/java/com/conveyal/gtfs/model/Stop.java | 1 + 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java index 0ef9b4618..3004480b6 100644 --- a/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java +++ b/src/main/java/com/conveyal/gtfs/loader/JdbcGtfsExporter.java @@ -255,12 +255,8 @@ public FeedLoadResult exportTables() { } // Only write "approved" routes using COPY TO with results of select query - if (fromEditor) { - // The filter clause for routes is simple. We're just checking that the route is APPROVED. - result.routes = Route.exportRoutes(dataSource, feedIdToExport, zipOutputStream, whereRouteIsApproved); - } else { - result.routes = Route.exportRoutes(dataSource, feedIdToExport, zipOutputStream); - } + // The filter clause for routes is simple. We're just checking that the route is APPROVED. + result.routes = Route.exportRoutes(dataSource, feedIdToExport, zipOutputStream, fromEditor ? whereRouteIsApproved : null); // Only write shapes for "approved" routes using COPY TO with results of select query if (fromEditor) { diff --git a/src/main/java/com/conveyal/gtfs/model/Route.java b/src/main/java/com/conveyal/gtfs/model/Route.java index 512f541b5..307f06fe8 100644 --- a/src/main/java/com/conveyal/gtfs/model/Route.java +++ b/src/main/java/com/conveyal/gtfs/model/Route.java @@ -310,13 +310,6 @@ public static String packRouteNetworks(List routes) { return csvContent.toString(); } - public static TableLoadResult exportRoutes( - DataSource dataSource, - String feedIdToExport, - ZipOutputStream zipOutputStream - ) { - return exportRoutes(dataSource, feedIdToExport, zipOutputStream, null); - } /** * Export routes, minus route networks. */ diff --git a/src/main/java/com/conveyal/gtfs/model/Stop.java b/src/main/java/com/conveyal/gtfs/model/Stop.java index 34de9f5cc..dd9f03707 100644 --- a/src/main/java/com/conveyal/gtfs/model/Stop.java +++ b/src/main/java/com/conveyal/gtfs/model/Stop.java @@ -317,6 +317,7 @@ public static TableLoadResult exportStops( ); List stops = Lists.newArrayList(stopIterator); + tableLoadResult.rowCount = stops.size(); writeEntityToFile(zipOutputStream, stops, STOPS_FILE_NAME); long duration = System.currentTimeMillis() - startTime; From 82684ac3a10245fd73a993a76471ab408648cb57 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Tue, 26 Aug 2025 12:15:56 +0100 Subject: [PATCH 37/40] improvement(Addressed PR feedback): --- src/main/java/com/conveyal/gtfs/GTFSFeed.java | 4 +-- .../conveyal/gtfs/loader/JDBCTableReader.java | 4 +-- .../java/com/conveyal/gtfs/model/Entity.java | 13 +++++----- .../java/com/conveyal/gtfs/model/Route.java | 24 +++++++++-------- .../com/conveyal/gtfs/model/RouteNetwork.java | 26 ------------------- .../java/com/conveyal/gtfs/model/Stop.java | 2 +- .../com/conveyal/gtfs/model/StopArea.java | 26 ------------------- 7 files changed, 24 insertions(+), 75 deletions(-) diff --git a/src/main/java/com/conveyal/gtfs/GTFSFeed.java b/src/main/java/com/conveyal/gtfs/GTFSFeed.java index 7410252e2..ddc2f0ed0 100644 --- a/src/main/java/com/conveyal/gtfs/GTFSFeed.java +++ b/src/main/java/com/conveyal/gtfs/GTFSFeed.java @@ -185,13 +185,13 @@ else if (feedId == null || feedId.isEmpty()) { new RouteNetwork.Loader(this).loadTable(zip); new Route.Loader(this).loadTable(zip); if (!route_networks.isEmpty()) { - Route.getCsvReaderForRoutesWithRouteNetworks(routes, route_networks); + Route.mergeRouteNetworks(routes, route_networks); } new ShapePoint.Loader(this).loadTable(zip); new StopArea.Loader(this).loadTable(zip); new Stop.Loader(this).loadTable(zip); if (!stop_areas.isEmpty()) { - Stop.getCsvReaderForStopsWithStopAreas(stops, stop_areas); + Stop.mergeStopAreas(stops, stop_areas); } new Transfer.Loader(this).loadTable(zip); new Trip.Loader(this).loadTable(zip); diff --git a/src/main/java/com/conveyal/gtfs/loader/JDBCTableReader.java b/src/main/java/com/conveyal/gtfs/loader/JDBCTableReader.java index 40b893eed..8927ad8c8 100644 --- a/src/main/java/com/conveyal/gtfs/loader/JDBCTableReader.java +++ b/src/main/java/com/conveyal/gtfs/loader/JDBCTableReader.java @@ -49,9 +49,7 @@ public JDBCTableReader(Table specTable, DataSource dataSource, String tablePrefi // We do this in the constructor to avoid rebuilding the mapping every time we fetch a single entity from the table. // No entry value defaults to zero, and SQL columns are 1-based. columnForName = new TObjectIntHashMap<>(); - selectClause = whereClause != null - ? String.format("select * from %s %s", qualifiedTableName, whereClause) - : "select * from " + qualifiedTableName; + selectClause = String.format("select * from %s %s", qualifiedTableName, whereClause != null ? whereClause : ""); // Try-with-resources will automatically close the connection when the try block exits. try (Connection connection = dataSource.getConnection()) { LOG.info("Connected to {}", qualifiedTableName); diff --git a/src/main/java/com/conveyal/gtfs/model/Entity.java b/src/main/java/com/conveyal/gtfs/model/Entity.java index d98fb5097..4ff89be04 100644 --- a/src/main/java/com/conveyal/gtfs/model/Entity.java +++ b/src/main/java/com/conveyal/gtfs/model/Entity.java @@ -547,12 +547,13 @@ protected static String createRow(String... columnValues) { /** * Create a CSV row from original fields plus grouped (stop areas or route network) ids. */ - protected static String createRow(CsvReader stopsReader, String ids, String[] csvFields) throws IOException { - String[] fields = new String[csvFields.length]; - for (int i = 0; i < csvFields.length; i++) { - fields[i] = stopsReader.get(csvFields[i]); + protected static String createRow(CsvReader csvReader, String ids, String[] csvFields) throws IOException { + List fields = new ArrayList<>(); + for (String csvField : csvFields) { + fields.add(csvReader.get(csvField)); } - return String.format("%s,%s%n", String.join(",", fields), ids); + fields.add(ids); + return String.join(",", fields) + System.lineSeparator(); } protected static String computeCsvValue(String value) { @@ -572,7 +573,7 @@ protected static String computeCsvValue(double value) { } /** - * Write routes or route networks to zip file. + * Write stops, stop areas, routes or route networks to zip file. */ public static void writeEntityToFile(ZipOutputStream zipOutputStream, List entities, String fileName) throws IOException { // Create entry for table. diff --git a/src/main/java/com/conveyal/gtfs/model/Route.java b/src/main/java/com/conveyal/gtfs/model/Route.java index 307f06fe8..c1b10aac2 100644 --- a/src/main/java/com/conveyal/gtfs/model/Route.java +++ b/src/main/java/com/conveyal/gtfs/model/Route.java @@ -19,6 +19,7 @@ import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -27,6 +28,7 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import java.util.stream.StreamSupport; import java.util.zip.ZipOutputStream; @@ -62,6 +64,9 @@ public class Route extends Entity { public String feed_id; public int continuous_pickup = INT_MISSING; public int continuous_drop_off = INT_MISSING; + + /** Used to directly link a route to a network. Multiple routes can have the same network id. This is forbidden if + * route network ids are defined. */ public String network_id; public String route_network_ids; @@ -102,16 +107,13 @@ public class Route extends Entity { NETWORK_ID_FIELD }; - private static final String CSV_HEADER_FOR_MERGE = String.format( - "%s,%s%n", - String.join(",", CSV_FIELDS), - ROUTE_NETWORK_IDS_FIELD - ); + private static final String CSV_HEADER_FOR_MERGE = + Stream.concat( + Arrays.stream(CSV_FIELDS), + Stream.of(ROUTE_NETWORK_IDS_FIELD) + ).collect(Collectors.joining(",")) + System.lineSeparator(); - private static final String CSV_HEADER_FOR_EXPORT = String.format( - "%s", - String.join(",", CSV_FIELDS) - ); + private static final String CSV_HEADER_FOR_EXPORT = String.join(",", CSV_FIELDS); @Override public String getId () { @@ -237,7 +239,7 @@ public Iterator iterator() { /** * Merge route networks into routes when loading from file. */ - public static void getCsvReaderForRoutesWithRouteNetworks(Map routes, Map routeNetworks) { + public static void mergeRouteNetworks(Map routes, Map routeNetworks) { Map> routeNetworksByRouteId = new HashMap<>(); routeNetworks.values().forEach(routeNetwork -> @@ -297,7 +299,7 @@ public static Map> groupRouteNetworkIds(CsvReader csvReader, * Expand all route network ids into a single row for each route id. This is to conform with the GTFS Fares v2 standard. */ public static String packRouteNetworks(List routes) { - StringBuilder csvContent = new StringBuilder(createRow(RouteNetwork.ROUTE_ID_FIELD, RouteNetwork.NETWORK_ID_FIELD)); + StringBuilder csvContent = new StringBuilder(createRow(RouteNetwork.NETWORK_ID_FIELD, RouteNetwork.ROUTE_ID_FIELD)); routes .stream() .filter(route -> route.route_network_ids != null) diff --git a/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java b/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java index 92decb904..f15fe3cf0 100644 --- a/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java +++ b/src/main/java/com/conveyal/gtfs/model/RouteNetwork.java @@ -5,7 +5,6 @@ import java.io.IOException; import java.sql.PreparedStatement; import java.sql.SQLException; -import java.util.Iterator; /** * This is only included to read the values from file. No route networks are saved directly to the database. Instead, @@ -60,31 +59,6 @@ public void loadOneRow() throws IOException { getRefField(NETWORK_ID_FIELD, true, feed.networks); getRefField(ROUTE_ID_FIELD, true, feed.routes); } - } - - public static class Writer extends Entity.Writer { - public Writer(GTFSFeed feed) { - super(feed, TABLE_NAME); - } - - @Override - public void writeHeaders() throws IOException { - writer.writeRecord(new String[] {NETWORK_ID_FIELD, ROUTE_ID_FIELD}); - } - - @Override - public void writeOneRow(RouteNetwork routeNetwork) throws IOException { - writeStringField(routeNetwork.network_id); - writeStringField(routeNetwork.route_id); - endRecord(); - } - - @Override - public Iterator iterator() { - return this.feed.route_networks.values().iterator(); - } - } - } diff --git a/src/main/java/com/conveyal/gtfs/model/Stop.java b/src/main/java/com/conveyal/gtfs/model/Stop.java index dd9f03707..59be5ee17 100644 --- a/src/main/java/com/conveyal/gtfs/model/Stop.java +++ b/src/main/java/com/conveyal/gtfs/model/Stop.java @@ -201,7 +201,7 @@ public Iterator iterator() { /** * Merge stop areas into stops when loading from file. */ - public static void getCsvReaderForStopsWithStopAreas(Map stops, Map stopAreas) { + public static void mergeStopAreas(Map stops, Map stopAreas) { Map> stopAreasByStopId = new HashMap<>(); stopAreas.values().forEach(stopArea -> diff --git a/src/main/java/com/conveyal/gtfs/model/StopArea.java b/src/main/java/com/conveyal/gtfs/model/StopArea.java index 9190c2b02..daf15da2e 100644 --- a/src/main/java/com/conveyal/gtfs/model/StopArea.java +++ b/src/main/java/com/conveyal/gtfs/model/StopArea.java @@ -5,7 +5,6 @@ import java.io.IOException; import java.sql.PreparedStatement; import java.sql.SQLException; -import java.util.Iterator; /** * This is only included to read the values from file. No stop areas are saved to the database. Instead, they are merged @@ -61,31 +60,6 @@ public void loadOneRow() throws IOException { */ getRefField(AREA_ID_NAME, true, feed.areas); getRefField(STOP_ID_NAME, true, feed.stops); - - } - - } - - public static class Writer extends Entity.Writer { - public Writer(GTFSFeed feed) { - super(feed, TABLE_NAME); - } - - @Override - public void writeHeaders() throws IOException { - writer.writeRecord(new String[] {AREA_ID_NAME, STOP_ID_NAME}); - } - - @Override - public void writeOneRow(StopArea stopArea) throws IOException { - writeStringField(stopArea.area_id); - writeStringField(stopArea.stop_id); - endRecord(); - } - - @Override - public Iterator iterator() { - return this.feed.stop_areas.values().iterator(); } } } \ No newline at end of file From d63eacae7169ea8376a27953c8d2ec7c0c97495b Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Tue, 26 Aug 2025 15:29:39 +0100 Subject: [PATCH 38/40] improvement(Reverted concat changes): --- src/main/java/com/conveyal/gtfs/model/Entity.java | 11 +++++------ src/main/java/com/conveyal/gtfs/model/Route.java | 10 +++++----- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/conveyal/gtfs/model/Entity.java b/src/main/java/com/conveyal/gtfs/model/Entity.java index 4ff89be04..ef4a104be 100644 --- a/src/main/java/com/conveyal/gtfs/model/Entity.java +++ b/src/main/java/com/conveyal/gtfs/model/Entity.java @@ -547,13 +547,12 @@ protected static String createRow(String... columnValues) { /** * Create a CSV row from original fields plus grouped (stop areas or route network) ids. */ - protected static String createRow(CsvReader csvReader, String ids, String[] csvFields) throws IOException { - List fields = new ArrayList<>(); - for (String csvField : csvFields) { - fields.add(csvReader.get(csvField)); + protected static String createRow(CsvReader stopsReader, String ids, String[] csvFields) throws IOException { + String[] fields = new String[csvFields.length]; + for (int i = 0; i < csvFields.length; i++) { + fields[i] = stopsReader.get(csvFields[i]); } - fields.add(ids); - return String.join(",", fields) + System.lineSeparator(); + return String.format("%s,%s%n", String.join(",", fields), ids); } protected static String computeCsvValue(String value) { diff --git a/src/main/java/com/conveyal/gtfs/model/Route.java b/src/main/java/com/conveyal/gtfs/model/Route.java index c1b10aac2..cfe3aeb96 100644 --- a/src/main/java/com/conveyal/gtfs/model/Route.java +++ b/src/main/java/com/conveyal/gtfs/model/Route.java @@ -107,11 +107,11 @@ public class Route extends Entity { NETWORK_ID_FIELD }; - private static final String CSV_HEADER_FOR_MERGE = - Stream.concat( - Arrays.stream(CSV_FIELDS), - Stream.of(ROUTE_NETWORK_IDS_FIELD) - ).collect(Collectors.joining(",")) + System.lineSeparator(); + private static final String CSV_HEADER_FOR_MERGE = String.format( + "%s,%s%n", + String.join(",", CSV_FIELDS), + ROUTE_NETWORK_IDS_FIELD + ); private static final String CSV_HEADER_FOR_EXPORT = String.join(",", CSV_FIELDS); From a65a86a30d60276c82d1de7f6c8791cb94eeaa62 Mon Sep 17 00:00:00 2001 From: Robin Beer Date: Tue, 26 Aug 2025 16:12:42 +0100 Subject: [PATCH 39/40] improvement(Entity.java): Corrected param name change --- src/main/java/com/conveyal/gtfs/model/Entity.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/conveyal/gtfs/model/Entity.java b/src/main/java/com/conveyal/gtfs/model/Entity.java index ef4a104be..9d1791418 100644 --- a/src/main/java/com/conveyal/gtfs/model/Entity.java +++ b/src/main/java/com/conveyal/gtfs/model/Entity.java @@ -547,10 +547,10 @@ protected static String createRow(String... columnValues) { /** * Create a CSV row from original fields plus grouped (stop areas or route network) ids. */ - protected static String createRow(CsvReader stopsReader, String ids, String[] csvFields) throws IOException { + protected static String createRow(CsvReader csvReader, String ids, String[] csvFields) throws IOException { String[] fields = new String[csvFields.length]; for (int i = 0; i < csvFields.length; i++) { - fields[i] = stopsReader.get(csvFields[i]); + fields[i] = csvReader.get(csvFields[i]); } return String.format("%s,%s%n", String.join(",", fields), ids); } From f0bd480268062abe5dec38defff614cb4af25217 Mon Sep 17 00:00:00 2001 From: binh-dam-ibigroup Date: Tue, 26 Aug 2025 13:24:53 -0400 Subject: [PATCH 40/40] refactor(Entity): Extract logic for printing col after existing ones. --- src/main/java/com/conveyal/gtfs/model/Entity.java | 9 ++++++++- src/main/java/com/conveyal/gtfs/model/Route.java | 14 ++------------ src/main/java/com/conveyal/gtfs/model/Stop.java | 14 ++------------ 3 files changed, 12 insertions(+), 25 deletions(-) diff --git a/src/main/java/com/conveyal/gtfs/model/Entity.java b/src/main/java/com/conveyal/gtfs/model/Entity.java index 9d1791418..f7ccd51c9 100644 --- a/src/main/java/com/conveyal/gtfs/model/Entity.java +++ b/src/main/java/com/conveyal/gtfs/model/Entity.java @@ -544,6 +544,13 @@ protected static String createRow(String... columnValues) { return String.join(",", columnValues) + System.lineSeparator(); } + /** + * Create a CSV row by combining an array of values plus another one. + */ + protected static String createRow(String[] values, String extraValue) { + return String.format("%s,%s%n", String.join(",", values), extraValue); + } + /** * Create a CSV row from original fields plus grouped (stop areas or route network) ids. */ @@ -552,7 +559,7 @@ protected static String createRow(CsvReader csvReader, String ids, String[] csvF for (int i = 0; i < csvFields.length; i++) { fields[i] = csvReader.get(csvFields[i]); } - return String.format("%s,%s%n", String.join(",", fields), ids); + return createRow(fields, ids); } protected static String computeCsvValue(String value) { diff --git a/src/main/java/com/conveyal/gtfs/model/Route.java b/src/main/java/com/conveyal/gtfs/model/Route.java index cfe3aeb96..70eea222d 100644 --- a/src/main/java/com/conveyal/gtfs/model/Route.java +++ b/src/main/java/com/conveyal/gtfs/model/Route.java @@ -19,7 +19,6 @@ import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -28,7 +27,6 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; -import java.util.stream.Stream; import java.util.stream.StreamSupport; import java.util.zip.ZipOutputStream; @@ -107,14 +105,6 @@ public class Route extends Entity { NETWORK_ID_FIELD }; - private static final String CSV_HEADER_FOR_MERGE = String.format( - "%s,%s%n", - String.join(",", CSV_FIELDS), - ROUTE_NETWORK_IDS_FIELD - ); - - private static final String CSV_HEADER_FOR_EXPORT = String.join(",", CSV_FIELDS); - @Override public String getId () { return route_id; @@ -266,7 +256,7 @@ public static CsvReader getCsvReaderForRoutesWithRouteNetworks( } return (rows.isEmpty()) ? routesReader - : produceCsvPayload(rows, CSV_HEADER_FOR_MERGE); + : produceCsvPayload(rows, createRow(CSV_FIELDS, ROUTE_NETWORK_IDS_FIELD)); } catch (Exception e) { LOG.error("Error while merging routes", e); // Any issues, return the original routes reader (minus route networks). @@ -352,7 +342,7 @@ public static TableLoadResult exportRoutes( * Expand all stops into a single row. */ public static String packRoutes(List routes) { - StringBuilder csvContent = new StringBuilder(createRow(CSV_HEADER_FOR_EXPORT)); + StringBuilder csvContent = new StringBuilder(createRow(CSV_FIELDS)); routes.forEach(route -> csvContent.append(createRow( computeCsvValue(route.route_id), computeCsvValue(route.agency_id), diff --git a/src/main/java/com/conveyal/gtfs/model/Stop.java b/src/main/java/com/conveyal/gtfs/model/Stop.java index 59be5ee17..38d2d038a 100644 --- a/src/main/java/com/conveyal/gtfs/model/Stop.java +++ b/src/main/java/com/conveyal/gtfs/model/Stop.java @@ -86,16 +86,6 @@ public class Stop extends Entity { WHEELCHAIR_BOARDING_FIELD, PLATFORM_CODE_FIELD }; - private static final String CSV_HEADER_FOR_MERGE = String.format( - "%s,%s%n", - String.join(",", CSV_FIELDS), - STOP_AREA_IDS_FIELD - ); - - private static final String CSV_HEADER_FOR_EXPORT = String.format( - "%s", - String.join(",", CSV_FIELDS) - ); @Override public String getId () { @@ -228,7 +218,7 @@ public static CsvReader getCsvReaderForStopsWithStopAreas( } return (rows.isEmpty()) ? stopsReader - : produceCsvPayload(rows, CSV_HEADER_FOR_MERGE); + : produceCsvPayload(rows, createRow(CSV_FIELDS, STOP_AREA_IDS_FIELD)); } catch (Exception e) { LOG.error("Error while merging stops", e); // Any issues, return the original stops reader (minus stop areas). @@ -278,7 +268,7 @@ public static String packStopAreas(List stops) { * Expand all stops into a single row. */ public static String packStops(List stops) { - StringBuilder csvContent = new StringBuilder(createRow(CSV_HEADER_FOR_EXPORT)); + StringBuilder csvContent = new StringBuilder(createRow(CSV_FIELDS)); stops.forEach(stop -> csvContent.append(createRow( computeCsvValue(stop.stop_id), computeCsvValue(stop.stop_code),