Skip to content

Commit 199c559

Browse files
authored
feat: Add shapes.txt validator (#2123)
1 parent bd5120b commit 199c559

3 files changed

Lines changed: 177 additions & 3 deletions

File tree

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ private <E extends GtfsEntity> int loadUniqueCount(
183183
* "Zone-Based Demand Responsive Transit" feature.
184184
* @return true if at least one trip with only location_id is found, false otherwise.
185185
*/
186-
private boolean hasAtLeastOneTripWithOnlyLocationId(GtfsFeedContainer feedContainer) {
186+
public static boolean hasAtLeastOneTripWithOnlyLocationId(GtfsFeedContainer feedContainer) {
187187
var optionalStopTimeTable = feedContainer.getTableForFilename(GtfsStopTime.FILENAME);
188188
if (optionalStopTimeTable.isPresent()) {
189189
for (GtfsEntity entity : optionalStopTimeTable.get().getEntities()) {
@@ -206,7 +206,7 @@ private boolean hasAtLeastOneTripWithOnlyLocationId(GtfsFeedContainer feedContai
206206
* "Fixed-Stops Demand Responsive Transit" feature.
207207
* @return true if at least one trip with only location_group_id is found, false otherwise.
208208
*/
209-
private boolean hasAtLeastOneTripWithOnlyLocationGroupId(GtfsFeedContainer feedContainer) {
209+
public static boolean hasAtLeastOneTripWithOnlyLocationGroupId(GtfsFeedContainer feedContainer) {
210210
var optionalStopTimeTable = feedContainer.getTableForFilename(GtfsStopTime.FILENAME);
211211
if (optionalStopTimeTable.isPresent()) {
212212
for (GtfsEntity entity : optionalStopTimeTable.get().getEntities()) {
@@ -791,7 +791,7 @@ public void loadServiceWindow(
791791
}
792792
}
793793

794-
private boolean hasAtLeastOneRecordInFile(
794+
public static boolean hasAtLeastOneRecordInFile(
795795
GtfsFeedContainer feedContainer, String featureFilename) {
796796
var table = feedContainer.getTableForFilename(featureFilename);
797797
return table.isPresent() && table.get().entityCount() > 0;
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package org.mobilitydata.gtfsvalidator.validator;
2+
3+
import javax.inject.Inject;
4+
import org.mobilitydata.gtfsvalidator.annotation.GtfsValidator;
5+
import org.mobilitydata.gtfsvalidator.notice.MissingRecommendedFileNotice;
6+
import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
7+
import org.mobilitydata.gtfsvalidator.reportsummary.model.FeedMetadata;
8+
import org.mobilitydata.gtfsvalidator.table.GtfsFeedContainer;
9+
import org.mobilitydata.gtfsvalidator.table.GtfsLocationGroupsTableContainer;
10+
import org.mobilitydata.gtfsvalidator.table.GtfsShapeTableContainer;
11+
12+
/**
13+
* Validates that the feed has either a `shapes.txt` file, or uses zone-based DRT or fixed-stops
14+
* DRT.
15+
*
16+
* <p>Generated notice: {@link MissingRecommendedFileNotice}.
17+
*/
18+
@GtfsValidator
19+
public class MissingShapesFileValidator extends FileValidator {
20+
private final GtfsShapeTableContainer shapeTable;
21+
private final GtfsLocationGroupsTableContainer locationGroups;
22+
private final GtfsFeedContainer feedContainer;
23+
24+
@Inject
25+
MissingShapesFileValidator(
26+
GtfsShapeTableContainer shapeTable,
27+
GtfsLocationGroupsTableContainer locationGroups,
28+
GtfsFeedContainer feedContainer) {
29+
this.shapeTable = shapeTable;
30+
this.locationGroups = locationGroups;
31+
this.feedContainer = feedContainer;
32+
}
33+
34+
@Override
35+
public void validate(NoticeContainer noticeContainer) {
36+
37+
Boolean missingShapes =
38+
shapeTable == null || shapeTable.isMissingFile() || shapeTable.getEntities().isEmpty();
39+
boolean hasZoneBasedDrt = FeedMetadata.hasAtLeastOneTripWithOnlyLocationId(feedContainer);
40+
boolean hasFixedStopsDrt =
41+
FeedMetadata.hasAtLeastOneRecordInFile(feedContainer, "location_groups.txt")
42+
&& FeedMetadata.hasAtLeastOneTripWithOnlyLocationGroupId(feedContainer);
43+
44+
// Do we NOT have: a shapes.txt file and the required fields for Zone-Based DRT,
45+
// and also the required fields for Fixed-Stop DRT?
46+
if (missingShapes && !hasZoneBasedDrt && !hasFixedStopsDrt) {
47+
noticeContainer.addValidationNotice(new MissingRecommendedFileNotice("shapes.txt"));
48+
// This is a feed-level warning; emit it at most once.
49+
return;
50+
}
51+
}
52+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package org.mobilitydata.gtfsvalidator.validator;
2+
3+
import static com.google.common.truth.Truth.assertThat;
4+
5+
import com.google.common.collect.ImmutableList;
6+
import java.util.ArrayList;
7+
import java.util.List;
8+
import org.junit.Test;
9+
import org.mobilitydata.gtfsvalidator.notice.MissingRecommendedFileNotice;
10+
import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
11+
import org.mobilitydata.gtfsvalidator.notice.ValidationNotice;
12+
import org.mobilitydata.gtfsvalidator.table.GtfsFeedContainer;
13+
import org.mobilitydata.gtfsvalidator.table.GtfsLocationGroups;
14+
import org.mobilitydata.gtfsvalidator.table.GtfsLocationGroupsTableContainer;
15+
import org.mobilitydata.gtfsvalidator.table.GtfsShape;
16+
import org.mobilitydata.gtfsvalidator.table.GtfsShapeTableContainer;
17+
import org.mobilitydata.gtfsvalidator.table.TableStatus;
18+
19+
public class MissingShapesFileValidatorTest {
20+
21+
private static GtfsFeedContainer createFeedContainer(
22+
List<GtfsShape> shapes, List<GtfsLocationGroups> locationGroups) {
23+
NoticeContainer noticeContainer = new NoticeContainer();
24+
return new GtfsFeedContainer(
25+
ImmutableList.of(
26+
GtfsShapeTableContainer.forEntities(shapes, noticeContainer),
27+
GtfsLocationGroupsTableContainer.forEntities(locationGroups, noticeContainer)));
28+
}
29+
30+
private static List<GtfsShape> createShapeTable(int rows) {
31+
ArrayList<GtfsShape> shapes = new ArrayList<>();
32+
for (int i = 0; i < rows; i++) {
33+
shapes.add(new GtfsShape.Builder().setCsvRowNumber(i + 1).setShapeId("s" + i).build());
34+
}
35+
return shapes;
36+
}
37+
38+
private static List<GtfsLocationGroups> createLocationGroupsTable(
39+
int rows, String groupId, String groupName) {
40+
ArrayList<GtfsLocationGroups> locationGroups = new ArrayList<>();
41+
for (int i = 0; i < rows; i++) {
42+
locationGroups.add(
43+
new GtfsLocationGroups.Builder()
44+
.setCsvRowNumber(i + 1)
45+
.setLocationGroupId(groupId)
46+
.setLocationGroupName(groupName)
47+
.build());
48+
}
49+
return locationGroups;
50+
}
51+
52+
@Test
53+
public void testShapesFileAndFixedDrtPresent() {
54+
List<ValidationNotice> notices =
55+
generateNotices(
56+
createShapeTable(1),
57+
createLocationGroupsTable(1, "b", "testgroup"),
58+
createFeedContainer(
59+
createShapeTable(1), createLocationGroupsTable(1, "b", "testgroup")));
60+
boolean found =
61+
notices.stream().anyMatch(notice -> notice instanceof MissingRecommendedFileNotice);
62+
assertThat(found).isFalse();
63+
}
64+
65+
@Test
66+
public void testShapesFileAndZoneBasedDrtPresent() {
67+
List<ValidationNotice> notices =
68+
generateNotices(
69+
createShapeTable(1),
70+
createLocationGroupsTable(1, "d", "t3stgroup"),
71+
createFeedContainer(
72+
createShapeTable(1), createLocationGroupsTable(1, "d", "t3stgroup")));
73+
boolean found =
74+
notices.stream().anyMatch(notice -> notice instanceof MissingRecommendedFileNotice);
75+
assertThat(found).isFalse();
76+
}
77+
78+
@Test
79+
public void testNoShapesFileAndNoDrtPresent() {
80+
// Create containers where shapes.txt is missing and location_groups is empty
81+
var shapeContainer = GtfsShapeTableContainer.forStatus(TableStatus.MISSING_FILE);
82+
var locationGroupsContainer =
83+
GtfsLocationGroupsTableContainer.forEntities(
84+
createLocationGroupsTable(0, null, null), new NoticeContainer());
85+
GtfsFeedContainer feedContainer = createFeedContainer(shapeContainer, locationGroupsContainer);
86+
87+
List<ValidationNotice> notices =
88+
generateNotices(shapeContainer, locationGroupsContainer, feedContainer);
89+
long missingRecommendedFileNoticesCount =
90+
notices.stream().filter(notice -> notice instanceof MissingRecommendedFileNotice).count();
91+
assertThat(missingRecommendedFileNoticesCount).isAtLeast(1);
92+
}
93+
94+
private static GtfsFeedContainer createFeedContainer(
95+
GtfsShapeTableContainer shapeContainer,
96+
GtfsLocationGroupsTableContainer locationGroupsContainer) {
97+
return new GtfsFeedContainer(ImmutableList.of(shapeContainer, locationGroupsContainer));
98+
}
99+
100+
private static List<ValidationNotice> generateNotices(
101+
GtfsShapeTableContainer shapeTable,
102+
GtfsLocationGroupsTableContainer locationGroups,
103+
GtfsFeedContainer feedContainer) {
104+
NoticeContainer noticeContainer = new NoticeContainer();
105+
new MissingShapesFileValidator(shapeTable, locationGroups, feedContainer)
106+
.validate(noticeContainer);
107+
return noticeContainer.getValidationNotices();
108+
}
109+
110+
private static List<ValidationNotice> generateNotices(
111+
List<GtfsShape> shapes,
112+
List<GtfsLocationGroups> locationGroups,
113+
GtfsFeedContainer feedContainer) {
114+
NoticeContainer noticeContainer = new NoticeContainer();
115+
new MissingShapesFileValidator(
116+
GtfsShapeTableContainer.forEntities(shapes, noticeContainer),
117+
GtfsLocationGroupsTableContainer.forEntities(locationGroups, noticeContainer),
118+
feedContainer)
119+
.validate(noticeContainer);
120+
return noticeContainer.getValidationNotices();
121+
}
122+
}

0 commit comments

Comments
 (0)