Skip to content

Commit 22e62eb

Browse files
authored
fix: false positives for features (#2096)
1 parent 2919b7c commit 22e62eb

File tree

2 files changed

+723
-42
lines changed

2 files changed

+723
-42
lines changed

main/src/main/java/org/mobilitydata/gtfsvalidator/reportsummary/model/FeedMetadata.java

Lines changed: 260 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)