diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/PathwayModeFieldsValidator.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/PathwayModeFieldsValidator.java new file mode 100644 index 0000000000..ff4a062ab5 --- /dev/null +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/PathwayModeFieldsValidator.java @@ -0,0 +1,130 @@ +package org.mobilitydata.gtfsvalidator.validator; + +import static org.mobilitydata.gtfsvalidator.notice.SeverityLevel.WARNING; + +import org.mobilitydata.gtfsvalidator.annotation.GtfsValidationNotice; +import org.mobilitydata.gtfsvalidator.annotation.GtfsValidationNotice.FileRefs; +import org.mobilitydata.gtfsvalidator.annotation.GtfsValidator; +import org.mobilitydata.gtfsvalidator.notice.MissingRecommendedFieldNotice; +import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; +import org.mobilitydata.gtfsvalidator.notice.ValidationNotice; +import org.mobilitydata.gtfsvalidator.table.GtfsPathway; +import org.mobilitydata.gtfsvalidator.table.GtfsPathwayMode; +import org.mobilitydata.gtfsvalidator.table.GtfsPathwaySchema; + +/** + * Validates the pathway fields whose expectations depend on {@code pathway_mode}. + * + *
Generated notices: {@link MissingRecommendedFieldNotice}, {@link
+ * IrrelevantMaxSlopeSetForPathwayModeNotice}.
+ */
+@GtfsValidator
+public class PathwayModeFieldsValidator extends SingleEntityValidator The GTFS specification states that `max_slope` should only be used with walkways
+ * (`pathway_mode=1`) and moving sidewalks (`pathway_mode=3`). A `max_slope` of `0` means no slope
+ * and is not reported.
+ */
+ @GtfsValidationNotice(severity = WARNING, files = @FileRefs({GtfsPathwaySchema.class}))
+ static class IrrelevantMaxSlopeSetForPathwayModeNotice extends ValidationNotice {
+ /** The row number of the faulty record. */
+ private final int csvRowNumber;
+
+ /** The `pathway_id` of the faulty record. */
+ private final String pathwayId;
+
+ /** The `pathway_mode` of the faulty record. */
+ private final int pathwayMode;
+
+ /** The `max_slope` defined on the faulty record. */
+ private final double maxSlope;
+
+ IrrelevantMaxSlopeSetForPathwayModeNotice(GtfsPathway pathway) {
+ this.csvRowNumber = pathway.csvRowNumber();
+ this.pathwayId = pathway.pathwayId();
+ this.pathwayMode = pathway.pathwayMode().getNumber();
+ this.maxSlope = pathway.maxSlope();
+ }
+ }
+}
diff --git a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java
index 7a6840a1f0..5a8315b4af 100644
--- a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java
+++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java
@@ -144,6 +144,7 @@ public void testNoticeClassFieldNames() {
"match2",
"matchCount",
"maxShapeDistanceTraveled",
+ "maxSlope",
"maxTripDistanceTraveled",
"message",
"minServiceStartDate",
diff --git a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/PathwayModeFieldsValidatorTest.java b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/PathwayModeFieldsValidatorTest.java
new file mode 100644
index 0000000000..419fd148e5
--- /dev/null
+++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/PathwayModeFieldsValidatorTest.java
@@ -0,0 +1,197 @@
+package org.mobilitydata.gtfsvalidator.validator;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mobilitydata.gtfsvalidator.notice.MissingRecommendedFieldNotice;
+import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
+import org.mobilitydata.gtfsvalidator.notice.ValidationNotice;
+import org.mobilitydata.gtfsvalidator.table.GtfsPathway;
+import org.mobilitydata.gtfsvalidator.table.GtfsPathwayMode;
+
+@RunWith(JUnit4.class)
+public class PathwayModeFieldsValidatorTest {
+
+ // stair_count is recommended for stairs (pathway_mode=2).
+
+ @Test
+ public void stairsWithoutStairCount_yieldsNotice() {
+ assertThat(validationNoticesFor(pathway(GtfsPathwayMode.STAIRS).build()))
+ .containsExactly(new MissingRecommendedFieldNotice("pathways.txt", 2, "stair_count"));
+ }
+
+ @Test
+ public void stairsWithStairCount_yieldsNoNotice() {
+ assertThat(validationNoticesFor(pathway(GtfsPathwayMode.STAIRS).setStairCount(5).build()))
+ .isEmpty();
+ }
+
+ @Test
+ public void stairsWithNegativeStairCount_yieldsNoNotice() {
+ // The spec says a negative stair_count describes walking down, so it is still defined.
+ assertThat(validationNoticesFor(pathway(GtfsPathwayMode.STAIRS).setStairCount(-5).build()))
+ .isEmpty();
+ }
+
+ @Test
+ public void nonStairsWithoutStairCount_yieldsNoStairCountNotice() {
+ for (GtfsPathwayMode mode : GtfsPathwayMode.values()) {
+ if (mode == GtfsPathwayMode.STAIRS) {
+ continue;
+ }
+ assertThat(validationNoticesFor(pathway(mode).setTraversalTime(30).build()))
+ .doesNotContain(new MissingRecommendedFieldNotice("pathways.txt", 2, "stair_count"));
+ }
+ }
+
+ // traversal_time is recommended for moving sidewalks, escalators and elevators.
+
+ @Test
+ public void mechanicalPathwaysWithoutTraversalTime_yieldNotice() {
+ for (GtfsPathwayMode mode :
+ List.of(
+ GtfsPathwayMode.MOVING_SIDEWALK, GtfsPathwayMode.ESCALATOR, GtfsPathwayMode.ELEVATOR)) {
+ assertThat(validationNoticesFor(pathway(mode).build()))
+ .containsExactly(new MissingRecommendedFieldNotice("pathways.txt", 2, "traversal_time"));
+ }
+ }
+
+ @Test
+ public void mechanicalPathwaysWithTraversalTime_yieldNoNotice() {
+ for (GtfsPathwayMode mode :
+ List.of(
+ GtfsPathwayMode.MOVING_SIDEWALK, GtfsPathwayMode.ESCALATOR, GtfsPathwayMode.ELEVATOR)) {
+ assertThat(validationNoticesFor(pathway(mode).setTraversalTime(45).build())).isEmpty();
+ }
+ }
+
+ @Test
+ public void walkwayWithoutTraversalTime_yieldsNoNotice() {
+ assertThat(validationNoticesFor(pathway(GtfsPathwayMode.WALKWAY).setLength(12.0).build()))
+ .isEmpty();
+ }
+
+ // length is recommended for walkways, fare gates and exit gates.
+
+ @Test
+ public void pathwaysRecommendingLengthWithoutLength_yieldNotice() {
+ for (GtfsPathwayMode mode :
+ List.of(GtfsPathwayMode.WALKWAY, GtfsPathwayMode.FARE_GATE, GtfsPathwayMode.EXIT_GATE)) {
+ assertThat(validationNoticesFor(pathway(mode).build()))
+ .containsExactly(new MissingRecommendedFieldNotice("pathways.txt", 2, "length"));
+ }
+ }
+
+ @Test
+ public void pathwaysRecommendingLengthWithLength_yieldNoNotice() {
+ for (GtfsPathwayMode mode :
+ List.of(GtfsPathwayMode.WALKWAY, GtfsPathwayMode.FARE_GATE, GtfsPathwayMode.EXIT_GATE)) {
+ assertThat(validationNoticesFor(pathway(mode).setLength(12.0).build())).isEmpty();
+ }
+ }
+
+ @Test
+ public void zeroLength_yieldsNoNotice() {
+ // Unlike max_slope, the spec gives no special meaning to a length of 0, so it counts as
+ // defined.
+ assertThat(validationNoticesFor(pathway(GtfsPathwayMode.WALKWAY).setLength(0.0).build()))
+ .isEmpty();
+ }
+
+ @Test
+ public void stairsWithoutLength_yieldsNoLengthNotice() {
+ assertThat(validationNoticesFor(pathway(GtfsPathwayMode.STAIRS).setStairCount(5).build()))
+ .doesNotContain(new MissingRecommendedFieldNotice("pathways.txt", 2, "length"));
+ }
+
+ // max_slope should only be used with walkways and moving sidewalks.
+
+ @Test
+ public void maxSlopeOnDisallowedMode_yieldsNotice() {
+ GtfsPathway entity =
+ pathway(GtfsPathwayMode.ELEVATOR).setMaxSlope(0.083).setTraversalTime(30).build();
+ assertThat(validationNoticesFor(entity))
+ .containsExactly(
+ new PathwayModeFieldsValidator.IrrelevantMaxSlopeSetForPathwayModeNotice(entity));
+ }
+
+ @Test
+ public void maxSlopeOnWalkwayOrMovingSidewalk_yieldsNoNotice() {
+ assertThat(
+ validationNoticesFor(
+ pathway(GtfsPathwayMode.WALKWAY).setMaxSlope(0.083).setLength(12.0).build()))
+ .isEmpty();
+ assertThat(
+ validationNoticesFor(
+ pathway(GtfsPathwayMode.MOVING_SIDEWALK)
+ .setMaxSlope(0.083)
+ .setTraversalTime(30)
+ .build()))
+ .isEmpty();
+ }
+
+ @Test
+ public void zeroMaxSlopeOnDisallowedMode_yieldsNoNotice() {
+ // The spec treats an empty max_slope and a max_slope of 0 alike, both meaning no slope.
+ GtfsPathway entity =
+ pathway(GtfsPathwayMode.ELEVATOR).setMaxSlope(0.0).setTraversalTime(30).build();
+ assertThat(validationNoticesFor(entity)).isEmpty();
+ }
+
+ @Test
+ public void negativeMaxSlopeOnDisallowedMode_yieldsNotice() {
+ // A negative slope describes a downward pathway, so it is a real value.
+ GtfsPathway entity =
+ pathway(GtfsPathwayMode.EXIT_GATE).setMaxSlope(-0.05).setLength(12.0).build();
+ assertThat(validationNoticesFor(entity))
+ .containsExactly(
+ new PathwayModeFieldsValidator.IrrelevantMaxSlopeSetForPathwayModeNotice(entity));
+ }
+
+ // An unusable pathway_mode is reported by the parser, so this validator stays quiet.
+
+ @Test
+ public void maxSlopeOnUnrecognizedMode_yieldsNoNotice() {
+ GtfsPathway entity =
+ new GtfsPathway.Builder()
+ .setCsvRowNumber(2)
+ .setPathwayId("pathway1")
+ .setFromStopId("stop1")
+ .setToStopId("stop2")
+ .setPathwayMode(9)
+ .setMaxSlope(0.083)
+ .build();
+ assertThat(validationNoticesFor(entity)).isEmpty();
+ }
+
+ @Test
+ public void maxSlopeWithoutPathwayMode_yieldsNoNotice() {
+ GtfsPathway entity =
+ new GtfsPathway.Builder()
+ .setCsvRowNumber(2)
+ .setPathwayId("pathway1")
+ .setFromStopId("stop1")
+ .setToStopId("stop2")
+ .setMaxSlope(0.083)
+ .build();
+ assertThat(validationNoticesFor(entity)).isEmpty();
+ }
+
+ private static GtfsPathway.Builder pathway(GtfsPathwayMode pathwayMode) {
+ return new GtfsPathway.Builder()
+ .setCsvRowNumber(2)
+ .setPathwayId("pathway1")
+ .setFromStopId("stop1")
+ .setToStopId("stop2")
+ .setPathwayMode(pathwayMode);
+ }
+
+ private static List