diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/reportsummary/model/FeedMetadata.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/reportsummary/model/FeedMetadata.java index 26f102215a..e6c7db5ecf 100644 --- a/main/src/main/java/org/mobilitydata/gtfsvalidator/reportsummary/model/FeedMetadata.java +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/reportsummary/model/FeedMetadata.java @@ -183,7 +183,7 @@ private int loadUniqueCount( * "Zone-Based Demand Responsive Transit" feature. * @return true if at least one trip with only location_id is found, false otherwise. */ - private boolean hasAtLeastOneTripWithOnlyLocationId(GtfsFeedContainer feedContainer) { + public static boolean hasAtLeastOneTripWithOnlyLocationId(GtfsFeedContainer feedContainer) { var optionalStopTimeTable = feedContainer.getTableForFilename(GtfsStopTime.FILENAME); if (optionalStopTimeTable.isPresent()) { for (GtfsEntity entity : optionalStopTimeTable.get().getEntities()) { @@ -206,7 +206,7 @@ private boolean hasAtLeastOneTripWithOnlyLocationId(GtfsFeedContainer feedContai * "Fixed-Stops Demand Responsive Transit" feature. * @return true if at least one trip with only location_group_id is found, false otherwise. */ - private boolean hasAtLeastOneTripWithOnlyLocationGroupId(GtfsFeedContainer feedContainer) { + public static boolean hasAtLeastOneTripWithOnlyLocationGroupId(GtfsFeedContainer feedContainer) { var optionalStopTimeTable = feedContainer.getTableForFilename(GtfsStopTime.FILENAME); if (optionalStopTimeTable.isPresent()) { for (GtfsEntity entity : optionalStopTimeTable.get().getEntities()) { @@ -791,7 +791,7 @@ public void loadServiceWindow( } } - private boolean hasAtLeastOneRecordInFile( + public static boolean hasAtLeastOneRecordInFile( GtfsFeedContainer feedContainer, String featureFilename) { var table = feedContainer.getTableForFilename(featureFilename); return table.isPresent() && table.get().entityCount() > 0; diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/MissingShapesFileValidator.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/MissingShapesFileValidator.java new file mode 100644 index 0000000000..39d9f35389 --- /dev/null +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/MissingShapesFileValidator.java @@ -0,0 +1,51 @@ +package org.mobilitydata.gtfsvalidator.validator; + +import javax.inject.Inject; +import org.mobilitydata.gtfsvalidator.annotation.GtfsValidator; +import org.mobilitydata.gtfsvalidator.notice.MissingRecommendedFileNotice; +import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; +import org.mobilitydata.gtfsvalidator.reportsummary.model.FeedMetadata; +import org.mobilitydata.gtfsvalidator.table.GtfsFeedContainer; +import org.mobilitydata.gtfsvalidator.table.GtfsLocationGroupsTableContainer; +import org.mobilitydata.gtfsvalidator.table.GtfsShapeTableContainer; + +/** + * Validates that the feed has either a `shapes.txt` file, or uses zone-based DRT or fixed-stops + * DRT. + * + *

Generated notice: {@link MissingRecommendedFileNotice}. + */ +@GtfsValidator +public class MissingShapesFileValidator extends FileValidator { + private final GtfsShapeTableContainer shapeTable; + private final GtfsLocationGroupsTableContainer locationGroups; + private final GtfsFeedContainer feedContainer; + + @Inject + MissingShapesFileValidator( + GtfsShapeTableContainer shapeTable, + GtfsLocationGroupsTableContainer locationGroups, + GtfsFeedContainer feedContainer) { + this.shapeTable = shapeTable; + this.locationGroups = locationGroups; + this.feedContainer = feedContainer; + } + + @Override + public void validate(NoticeContainer noticeContainer) { + + Boolean missingShapes = shapeTable == null || shapeTable.isMissingFile(); + boolean hasZoneBasedDrt = FeedMetadata.hasAtLeastOneTripWithOnlyLocationId(feedContainer); + boolean hasFixedStopsDrt = + FeedMetadata.hasAtLeastOneRecordInFile(feedContainer, "location_groups.txt") + && FeedMetadata.hasAtLeastOneTripWithOnlyLocationGroupId(feedContainer); + + // Do we NOT have: a shapes.txt file and the required fields for Zone-Based DRT, + // and also the required fields for Fixed-Stop DRT? + if (missingShapes && !hasZoneBasedDrt && !hasFixedStopsDrt) { + noticeContainer.addValidationNotice(new MissingRecommendedFileNotice("shapes.txt")); + // This is a feed-level warning; emit it at most once. + return; + } + } +} diff --git a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/MissingShapesFileValidatorTest.java b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/MissingShapesFileValidatorTest.java new file mode 100644 index 0000000000..20285a205e --- /dev/null +++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/MissingShapesFileValidatorTest.java @@ -0,0 +1,122 @@ +package org.mobilitydata.gtfsvalidator.validator; + +import static com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.List; +import org.junit.Test; +import org.mobilitydata.gtfsvalidator.notice.MissingRecommendedFileNotice; +import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; +import org.mobilitydata.gtfsvalidator.notice.ValidationNotice; +import org.mobilitydata.gtfsvalidator.table.GtfsFeedContainer; +import org.mobilitydata.gtfsvalidator.table.GtfsLocationGroups; +import org.mobilitydata.gtfsvalidator.table.GtfsLocationGroupsTableContainer; +import org.mobilitydata.gtfsvalidator.table.GtfsShape; +import org.mobilitydata.gtfsvalidator.table.GtfsShapeTableContainer; +import org.mobilitydata.gtfsvalidator.table.TableStatus; + +public class MissingShapesFileValidatorTest { + + private static GtfsFeedContainer createFeedContainer( + List shapes, List locationGroups) { + NoticeContainer noticeContainer = new NoticeContainer(); + return new GtfsFeedContainer( + ImmutableList.of( + GtfsShapeTableContainer.forEntities(shapes, noticeContainer), + GtfsLocationGroupsTableContainer.forEntities(locationGroups, noticeContainer))); + } + + private static List createShapeTable(int rows) { + ArrayList shapes = new ArrayList<>(); + for (int i = 0; i < rows; i++) { + shapes.add(new GtfsShape.Builder().setCsvRowNumber(i + 1).setShapeId("s" + i).build()); + } + return shapes; + } + + private static List createLocationGroupsTable( + int rows, String groupId, String groupName) { + ArrayList locationGroups = new ArrayList<>(); + for (int i = 0; i < rows; i++) { + locationGroups.add( + new GtfsLocationGroups.Builder() + .setCsvRowNumber(i + 1) + .setLocationGroupId(groupId) + .setLocationGroupName(groupName) + .build()); + } + return locationGroups; + } + + @Test + public void testShapesFileAndFixedDrtPresent() { + List notices = + generateNotices( + createShapeTable(1), + createLocationGroupsTable(1, "b", "testgroup"), + createFeedContainer( + createShapeTable(1), createLocationGroupsTable(1, "b", "testgroup"))); + boolean found = + notices.stream().anyMatch(notice -> notice instanceof MissingRecommendedFileNotice); + assertThat(found).isFalse(); + } + + @Test + public void testShapesFileAndZoneBasedDrtPresent() { + List notices = + generateNotices( + createShapeTable(1), + createLocationGroupsTable(1, "d", "t3stgroup"), + createFeedContainer( + createShapeTable(1), createLocationGroupsTable(1, "d", "t3stgroup"))); + boolean found = + notices.stream().anyMatch(notice -> notice instanceof MissingRecommendedFileNotice); + assertThat(found).isFalse(); + } + + @Test + public void testNoShapesFileAndNoDrtPresent() { + // Create containers where shapes.txt is missing and location_groups is empty + var shapeContainer = GtfsShapeTableContainer.forStatus(TableStatus.MISSING_FILE); + var locationGroupsContainer = + GtfsLocationGroupsTableContainer.forEntities( + createLocationGroupsTable(0, null, null), new NoticeContainer()); + GtfsFeedContainer feedContainer = createFeedContainer(shapeContainer, locationGroupsContainer); + + List notices = + generateNotices(shapeContainer, locationGroupsContainer, feedContainer); + long missingRecommendedFileNoticesCount = + notices.stream().filter(notice -> notice instanceof MissingRecommendedFileNotice).count(); + assertThat(missingRecommendedFileNoticesCount).isAtLeast(1); + } + + private static GtfsFeedContainer createFeedContainer( + GtfsShapeTableContainer shapeContainer, + GtfsLocationGroupsTableContainer locationGroupsContainer) { + return new GtfsFeedContainer(ImmutableList.of(shapeContainer, locationGroupsContainer)); + } + + private static List generateNotices( + GtfsShapeTableContainer shapeTable, + GtfsLocationGroupsTableContainer locationGroups, + GtfsFeedContainer feedContainer) { + NoticeContainer noticeContainer = new NoticeContainer(); + new MissingShapesFileValidator(shapeTable, locationGroups, feedContainer) + .validate(noticeContainer); + return noticeContainer.getValidationNotices(); + } + + private static List generateNotices( + List shapes, + List locationGroups, + GtfsFeedContainer feedContainer) { + NoticeContainer noticeContainer = new NoticeContainer(); + new MissingShapesFileValidator( + GtfsShapeTableContainer.forEntities(shapes, noticeContainer), + GtfsLocationGroupsTableContainer.forEntities(locationGroups, noticeContainer), + feedContainer) + .validate(noticeContainer); + return noticeContainer.getValidationNotices(); + } +}