@@ -50,17 +50,10 @@ public class FeedMetadata {
5050 new Pair <>(new FeatureMetadata ("Translations" , null ), GtfsTranslation .FILENAME ),
5151 new Pair <>(new FeatureMetadata ("Fares V1" , "Fares" ), GtfsFareAttribute .FILENAME ),
5252 new Pair <>(new FeatureMetadata ("Fare Products" , "Fares" ), GtfsFareProduct .FILENAME ),
53- new Pair <>(new FeatureMetadata ("Fare Media" , "Fares" ), GtfsFareMedia .FILENAME ),
54- new Pair <>(new FeatureMetadata ("Zone-Based Fares" , "Fares" ), GtfsArea .FILENAME ),
5553 new Pair <>(new FeatureMetadata ("Fare Transfers" , "Fares" ), GtfsFareTransferRule .FILENAME ),
56- new Pair <>(new FeatureMetadata ("Time-Based Fares" , "Fares" ), GtfsTimeframe .FILENAME ),
5754 new Pair <>(
58- new FeatureMetadata ("Rider Categories" , "Fares" ), GtfsRiderCategories .FILENAME ),
59- new Pair <>(
60- new FeatureMetadata ("Booking Rules" , "Flexible Services" ), GtfsBookingRules .FILENAME ),
61- new Pair <>(
62- new FeatureMetadata ("Fixed-Stops Demand Responsive Transit" , "Flexible Services" ),
63- GtfsLocationGroups .FILENAME ));
55+ new FeatureMetadata ("Booking Rules" , "Flexible Services" ),
56+ GtfsBookingRules .FILENAME ));
6457
6558 protected FeedMetadata () {}
6659
@@ -172,17 +165,84 @@ private <E extends GtfsEntity> int loadUniqueCount(
172165 return uniqueIds .size ();
173166 }
174167
168+ /**
169+ * Returns true if a record within least one trip in `trips.txt` with defined values for `trip_id`
170+ * and `location_id` fields and no value for `stop_id` field in `stop_times.txt`.
171+ *
172+ * @param feedContainer Feed container to check for the presence of the required fields for the
173+ * "Zone-Based Demand Responsive Transit" feature.
174+ * @return true if at least one trip with only location_id is found, false otherwise.
175+ */
176+ private boolean hasAtLeastOneTripWithOnlyLocationId (GtfsFeedContainer feedContainer ) {
177+ var optionalStopTimeTable = feedContainer .getTableForFilename (GtfsStopTime .FILENAME );
178+ if (optionalStopTimeTable .isPresent ()) {
179+ for (GtfsEntity entity : optionalStopTimeTable .get ().getEntities ()) {
180+ if (entity instanceof GtfsStopTime ) {
181+ GtfsStopTime stopTime = (GtfsStopTime ) entity ;
182+ if (stopTime .hasTripId () && stopTime .hasLocationId () && (!stopTime .hasStopId ())) {
183+ return true ;
184+ }
185+ }
186+ }
187+ }
188+ return false ;
189+ }
190+
191+ /**
192+ * Returns true if least one trip in `trips.txt` with defined values for `trip_id` and
193+ * `location_group_id` fields and no value for `stop_id` field in `stop_times.txt`.
194+ *
195+ * @param feedContainer Feed container to check for the presence of the required fields for the
196+ * "Fixed-Stops Demand Responsive Transit" feature.
197+ * @return true if at least one trip with only location_group_id is found, false otherwise.
198+ */
199+ private boolean hasAtLeastOneTripWithOnlyLocationGroupId (GtfsFeedContainer feedContainer ) {
200+ var optionalStopTimeTable = feedContainer .getTableForFilename (GtfsStopTime .FILENAME );
201+ if (optionalStopTimeTable .isPresent ()) {
202+ for (GtfsEntity entity : optionalStopTimeTable .get ().getEntities ()) {
203+ if (entity instanceof GtfsStopTime ) {
204+ GtfsStopTime stopTime = (GtfsStopTime ) entity ;
205+ if (stopTime .hasTripId () && stopTime .hasLocationGroupId () && !stopTime .hasStopId ()) {
206+ return true ;
207+ }
208+ }
209+ }
210+ }
211+ return false ;
212+ }
213+
214+ /**
215+ * Loads the presence of GTFS features based on the GTFS specification. For each feature, it
216+ * checks the presence of required files and fields in the feed container to determine if the
217+ * feature is present in the feed.
218+ *
219+ * @param feedContainer the container for the GTFS feed data
220+ */
175221 private void loadSpecFeatures (GtfsFeedContainer feedContainer ) {
176222 loadSpecFeaturesBasedOnFilePresence (feedContainer );
177223 loadSpecFeaturesBasedOnFieldPresence (feedContainer );
178224 }
179225
226+ /**
227+ * Loads features that can be determined based solely on the presence of at least one record in a
228+ * specific file. For each feature in the FILE_BASED_FEATURES list, it checks if there is at least
229+ * one record in the corresponding file and updates the specFeatures map accordingly.
230+ *
231+ * @param feedContainer the container for the GTFS feed data
232+ */
180233 private void loadSpecFeaturesBasedOnFilePresence (GtfsFeedContainer feedContainer ) {
181234 for (Pair <FeatureMetadata , String > entry : FILE_BASED_FEATURES ) {
182235 specFeatures .put (entry .getKey (), hasAtLeastOneRecordInFile (feedContainer , entry .getValue ()));
183236 }
184237 }
185238
239+ /**
240+ * Loads features that require checking the presence of specific fields in the GTFS feed. For each
241+ * feature, it checks if there is at least one record with the required fields and updates the
242+ * specFeatures map accordingly.
243+ *
244+ * @param feedContainer the container for the GTFS feed data
245+ */
186246 private void loadSpecFeaturesBasedOnFieldPresence (GtfsFeedContainer feedContainer ) {
187247 loadRouteColorsFeature (feedContainer );
188248 loadHeadsignsFeature (feedContainer );
@@ -198,14 +258,148 @@ private void loadSpecFeaturesBasedOnFieldPresence(GtfsFeedContainer feedContaine
198258 loadContinuousStopsFeature (feedContainer );
199259 loadZoneBasedDemandResponsiveTransitFeature (feedContainer );
200260 loadDeviatedFixedRouteFeature (feedContainer );
261+ loadFareMediaFeature (feedContainer );
262+ loadRiderCategoriesFeature (feedContainer );
263+ loadTimeBasedFaresFeature (feedContainer );
264+ loadZoneBasedFaresFeature (feedContainer );
265+ loadFixedStopsDemandResponseTransit (feedContainer );
266+ }
267+
268+ /**
269+ * Determines the presence of the "Fixed-Stops Demand Responsive Transit" feature, which requires
270+ * one record in location_groups.txt AND at least one trip in stop_times.txt references only
271+ * location_group_id
272+ *
273+ * @param feedContainer Feed container to check for the presence of the required files and fields
274+ * for the "Fixed-Stops Demand Responsive Transit" feature.
275+ */
276+ private void loadFixedStopsDemandResponseTransit (GtfsFeedContainer feedContainer ) {
277+ specFeatures .put (
278+ new FeatureMetadata ("Fixed-Stops Demand Responsive Transit" , "Flexible Services" ),
279+ hasAtLeastOneRecordInFile (feedContainer , GtfsLocationGroups .FILENAME )
280+ && hasAtLeastOneTripWithOnlyLocationGroupId (feedContainer ));
281+ }
282+
283+ /**
284+ * Determines the presence of the "Zone-Based Fares" feature, which requires one line of data in
285+ * areas.txt AND One line of data in fare_products.txt AND One line of data in fare_leg_rules.txt
286+ * AND (One from_area_id value in fare_leg_rules.txt OR One to_area_id value in
287+ * fare_leg_rules.txt)
288+ *
289+ * @param feedContainer Feed container to check for the presence of the required files and fields
290+ * for the "Zone-Based Fares" feature.
291+ */
292+ private void loadZoneBasedFaresFeature (GtfsFeedContainer feedContainer ) {
293+ specFeatures .put (
294+ new FeatureMetadata ("Zone-Based Fares" , "Fares" ),
295+ hasAtLeastOneRecordInFile (feedContainer , GtfsArea .FILENAME )
296+ && hasAtLeastOneRecordInFile (feedContainer , GtfsFareProduct .FILENAME )
297+ && hasAtLeastOneRecordInFile (feedContainer , GtfsFareLegRule .FILENAME )
298+ && (hasAtLeastOneRecordForFields (
299+ feedContainer ,
300+ GtfsFareLegRule .FILENAME ,
301+ List .of ((Function <GtfsFareLegRule , Boolean >) GtfsFareLegRule ::hasFromAreaId ))
302+ || hasAtLeastOneRecordForFields (
303+ feedContainer ,
304+ GtfsFareLegRule .FILENAME ,
305+ List .of ((Function <GtfsFareLegRule , Boolean >) GtfsFareLegRule ::hasToAreaId ))));
306+ }
307+
308+ /**
309+ * Determines the presence of the "Time-Based Fares" feature, which requires one line of data in
310+ * timeframes.txt AND One line of data in fare_products.txt AND One line of data in
311+ * fare_leg_rules.txt AND (One from_timeframe_id value in fare_leg_rules.txt OR One
312+ * to_timeframe_id value in fare_leg_rules.txt)
313+ *
314+ * @param feedContainer Feed container to check for the presence of the required files and fields
315+ * for the "Time-Based Fares" feature.
316+ */
317+ private void loadTimeBasedFaresFeature (GtfsFeedContainer feedContainer ) {
318+ specFeatures .put (
319+ new FeatureMetadata ("Time-Based Fares" , "Fares" ),
320+ hasAtLeastOneRecordInFile (feedContainer , GtfsTimeframe .FILENAME )
321+ && hasAtLeastOneRecordInFile (feedContainer , GtfsFareProduct .FILENAME )
322+ && hasAtLeastOneRecordInFile (feedContainer , GtfsFareLegRule .FILENAME )
323+ && (hasAtLeastOneRecordForFields (
324+ feedContainer ,
325+ GtfsFareLegRule .FILENAME ,
326+ List .of (
327+ (Function <GtfsFareLegRule , Boolean >)
328+ GtfsFareLegRule ::hasFromTimeframeGroupId ))
329+ || hasAtLeastOneRecordForFields (
330+ feedContainer ,
331+ GtfsFareLegRule .FILENAME ,
332+ List .of (
333+ (Function <GtfsFareLegRule , Boolean >)
334+ GtfsFareLegRule ::hasToTimeframeGroupId ))));
335+ }
336+
337+ /**
338+ * Determines the presence of the "Rider Categories" feature, which requires one line of data in
339+ * rider_categories.txt AND One line of data in fare_products.txt AND One rider_category_id value
340+ * in fare_products.txt
341+ *
342+ * @param feedContainer Feed container to check for the presence of the required files and fields
343+ * for the "Rider Categories" feature.
344+ */
345+ private void loadRiderCategoriesFeature (GtfsFeedContainer feedContainer ) {
346+ if (!hasAtLeastOneRecordInFile (feedContainer , GtfsRiderCategories .FILENAME )
347+ || !hasAtLeastOneRecordInFile (feedContainer , GtfsFareProduct .FILENAME )) {
348+ specFeatures .put (new FeatureMetadata ("Rider Categories" , "Fares" ), false );
349+ return ;
350+ }
351+ specFeatures .put (
352+ new FeatureMetadata ("Rider Categories" , "Fares" ),
353+ hasAtLeastOneRecordForFields (
354+ feedContainer ,
355+ GtfsFareProduct .FILENAME ,
356+ List .of ((Function <GtfsFareProduct , Boolean >) GtfsFareProduct ::hasRiderCategoryId )));
201357 }
202358
359+ /**
360+ * Determines the presence of the "Fare Media" feature, which requires one line of data in
361+ * fare_media.txt AND One line of data in fare_products.txt AND One fare_media_id value in
362+ * fare_products.txt
363+ *
364+ * @param feedContainer Feed container to check for the presence of the required files and fields
365+ * for the "Fare Media" feature.
366+ */
367+ private void loadFareMediaFeature (GtfsFeedContainer feedContainer ) {
368+ if (!hasAtLeastOneRecordInFile (feedContainer , GtfsFareMedia .FILENAME )
369+ || !hasAtLeastOneRecordInFile (feedContainer , GtfsFareProduct .FILENAME )) {
370+ specFeatures .put (new FeatureMetadata ("Fare Media" , "Fares" ), false );
371+ return ;
372+ }
373+ specFeatures .put (
374+ new FeatureMetadata ("Fare Media" , "Fares" ),
375+ hasAtLeastOneRecordForFields (
376+ feedContainer ,
377+ GtfsFareProduct .FILENAME ,
378+ List .of ((Function <GtfsFareProduct , Boolean >) GtfsFareProduct ::hasFareMediaId )));
379+ }
380+
381+ /**
382+ * Determines the presence of the "Predefined Routes with Deviation" feature, which requires at
383+ * least one trip in `trips.txt` with defined values for `trip_id`, `location_id`, `stop_id`,
384+ * `arrival_time`, and `departure_time` fields in `stop_times.txt`.
385+ *
386+ * @param feedContainer Feed container to check for the presence of the required fields for the
387+ * "Predefined Routes with Deviation" feature.
388+ */
203389 private void loadDeviatedFixedRouteFeature (GtfsFeedContainer feedContainer ) {
204390 specFeatures .put (
205391 new FeatureMetadata ("Predefined Routes with Deviation" , "Flexible Services" ),
206392 hasAtLeastOneTripWithAllFields (feedContainer ));
207393 }
208394
395+ /**
396+ * Returns true if least one trip in `trips.txt` with defined values for `trip_id`, `location_id`,
397+ * `stop_id`, `arrival_time`, and `departure_time` fields in `stop_times.txt`.
398+ *
399+ * @param feedContainer Feed container to check for the presence of the required fields for the
400+ * "Predefined Routes with Deviation" feature.
401+ * @return true if at least one trip with all required fields is found, false otherwise.
402+ */
209403 private boolean hasAtLeastOneTripWithAllFields (GtfsFeedContainer feedContainer ) {
210404 return feedContainer
211405 .getTableForFilename (GtfsStopTime .FILENAME )
@@ -244,27 +438,20 @@ private boolean hasAtLeastOneTripWithAllFields(GtfsFeedContainer feedContainer)
244438 .orElse (false );
245439 }
246440
441+ /**
442+ * Determines the presence of the "Zone-Based Demand Responsive Transit" feature, which requires
443+ * at least one trip in `trips.txt` with defined values for `trip_id` and `location_id` fields and
444+ * no value for `stop_id` field in `stop_times.txt`.
445+ *
446+ * @param feedContainer Feed container to check for the presence of the required fields for the
447+ * "Zone-Based Demand Responsive Transit" feature.
448+ */
247449 private void loadZoneBasedDemandResponsiveTransitFeature (GtfsFeedContainer feedContainer ) {
248450 specFeatures .put (
249451 new FeatureMetadata ("Zone-Based Demand Responsive Services" , "Flexible Services" ),
250452 hasAtLeastOneTripWithOnlyLocationId (feedContainer ));
251453 }
252454
253- private boolean hasAtLeastOneTripWithOnlyLocationId (GtfsFeedContainer feedContainer ) {
254- var optionalStopTimeTable = feedContainer .getTableForFilename (GtfsStopTime .FILENAME );
255- if (optionalStopTimeTable .isPresent ()) {
256- for (GtfsEntity entity : optionalStopTimeTable .get ().getEntities ()) {
257- if (entity instanceof GtfsStopTime ) {
258- GtfsStopTime stopTime = (GtfsStopTime ) entity ;
259- if (stopTime .hasTripId () && stopTime .hasLocationId () && (!stopTime .hasStopId ())) {
260- return true ;
261- }
262- }
263- }
264- }
265- return false ;
266- }
267-
268455 private void loadContinuousStopsFeature (GtfsFeedContainer feedContainer ) {
269456 specFeatures .put (
270457 new FeatureMetadata ("Continuous Stops" , "Flexible Services" ),
@@ -286,16 +473,38 @@ private void loadContinuousStopsFeature(GtfsFeedContainer feedContainer) {
286473 List .of ((Function <GtfsStopTime , Boolean >) GtfsStopTime ::hasContinuousPickup )));
287474 }
288475
476+ /**
477+ * Determines the presence of the "Route-Based Fares" feature, which requires at least one record
478+ * in `routes.txt` with a reference to `network_id` or at least one record in `networks.txt`, and
479+ * at least one record in `fare_products.txt` and `fare_leg_rules.txt`, and at least one record in
480+ * `fare_leg_rules.txt` with a reference to `network_id`.
481+ *
482+ * @param feedContainer
483+ */
289484 private void loadRouteBasedFaresFeature (GtfsFeedContainer feedContainer ) {
290485 specFeatures .put (
291486 new FeatureMetadata ("Route-Based Fares" , "Fares" ),
292- hasAtLeastOneRecordForFields (
487+ (hasAtLeastOneRecordForFields (
488+ feedContainer ,
489+ GtfsRoute .FILENAME ,
490+ List .of ((Function <GtfsRoute , Boolean >) GtfsRoute ::hasNetworkId ))
491+ || hasAtLeastOneRecordInFile (feedContainer , GtfsNetwork .FILENAME ))
492+ && hasAtLeastOneRecordInFile (feedContainer , GtfsFareProduct .FILENAME )
493+ && hasAtLeastOneRecordInFile (feedContainer , GtfsFareLegRule .FILENAME )
494+ && hasAtLeastOneRecordForFields (
293495 feedContainer ,
294- GtfsRoute .FILENAME ,
295- List .of ((Function <GtfsRoute , Boolean >) GtfsRoute ::hasNetworkId ))
296- || hasAtLeastOneRecordInFile (feedContainer , GtfsNetwork .FILENAME ));
496+ GtfsFareLegRule .FILENAME ,
497+ List .of ((Function <GtfsFareLegRule , Boolean >) GtfsFareLegRule ::hasNetworkId )));
297498 }
298499
500+ /**
501+ * Determines the presence of the "Pathway Signs" feature, which requires at least one record in
502+ * `pathways.txt` with a defined value for `signposted_as` field or at least one record in
503+ * `pathways.txt` with a defined value for `reversed_signposted_as` field.
504+ *
505+ * @param feedContainer Feed container to check for the presence of the required file and field
506+ * for the "Levels" feature.
507+ */
299508 private void loadPathwaySignsFeature (GtfsFeedContainer feedContainer ) {
300509 specFeatures .put (
301510 new FeatureMetadata ("Pathway Signs" , "Pathways" ),
@@ -309,6 +518,16 @@ private void loadPathwaySignsFeature(GtfsFeedContainer feedContainer) {
309518 List .of ((Function <GtfsPathway , Boolean >) GtfsPathway ::hasReversedSignpostedAs )));
310519 }
311520
521+ /**
522+ * Determines the presence of the "Pathway Details" feature, which requires at least one record in
523+ * `pathways.txt` with a defined value for `max_slope` field or at least one record in
524+ * `pathways.txt` with a defined value for `min_width` field or at least one record in
525+ * `pathways.txt` with a defined value for `length` field or at least one record in `pathways.txt`
526+ * with a defined value for `stair_count` field.
527+ *
528+ * @param feedContainer Feed container to check for the presence of the required file and fields
529+ * for the "Pathway Details" feature.
530+ */
312531 private void loadPathwayDetailsFeature (GtfsFeedContainer feedContainer ) {
313532 specFeatures .put (
314533 new FeatureMetadata ("Pathway Details" , "Pathways" ),
@@ -330,12 +549,25 @@ private void loadPathwayDetailsFeature(GtfsFeedContainer feedContainer) {
330549 List .of ((Function <GtfsPathway , Boolean >) GtfsPathway ::hasStairCount )));
331550 }
332551
552+ /**
553+ * Determines the presence of the "Pathway Connections" feature, which requires at least one
554+ * record in `pathways.txt`.
555+ *
556+ * @param feedContainer Feed container to check for the presence of the required file for the
557+ * "Pathway Connections" feature.
558+ */
333559 private void loadPathwayConnectionsFeature (GtfsFeedContainer feedContainer ) {
334560 specFeatures .put (
335561 new FeatureMetadata ("Pathway Connections" , "Pathways" ),
336562 hasAtLeastOneRecordInFile (feedContainer , GtfsPathway .FILENAME ));
337563 }
338564
565+ /**
566+ * Determines the presence of the "In-station Traversal Time" feature, which requires at least one
567+ * record in `pathways.txt` with a defined value for `traversal_time` field.
568+ *
569+ * @param feedContainer
570+ */
339571 private void loadTraversalTimeFeature (GtfsFeedContainer feedContainer ) {
340572 specFeatures .put (
341573 new FeatureMetadata ("In-station Traversal Time" , "Pathways" ),
0 commit comments