diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TripAndShapeDistanceValidator.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TripAndShapeDistanceValidator.java
new file mode 100644
index 0000000000..13196dd68b
--- /dev/null
+++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/TripAndShapeDistanceValidator.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2021 MobilityData IO
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.mobilitydata.gtfsvalidator.validator;
+
+import static org.mobilitydata.gtfsvalidator.notice.SeverityLevel.ERROR;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import org.mobilitydata.gtfsvalidator.annotation.GtfsValidationNotice;
+import org.mobilitydata.gtfsvalidator.annotation.GtfsValidationNotice.FileRefs;
+import org.mobilitydata.gtfsvalidator.annotation.GtfsValidator;
+import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
+import org.mobilitydata.gtfsvalidator.notice.ValidationNotice;
+import org.mobilitydata.gtfsvalidator.table.*;
+
+/**
+ * Validates that the distance traveled by a trip is lesser or equals the max length of its shape.
+ *
+ *
Generated notice: {@link TripDistanceExceedsShapeDistanceNotice}.
+ */
+@GtfsValidator
+public class TripAndShapeDistanceValidator extends FileValidator {
+
+ private final GtfsTripTableContainer tripTable;
+
+ private final GtfsStopTimeTableContainer stopTimeTable;
+
+ private final GtfsShapeTableContainer shapeTable;
+
+ @Inject
+ TripAndShapeDistanceValidator(
+ GtfsTripTableContainer tripTable,
+ GtfsStopTimeTableContainer stopTimeTable,
+ GtfsShapeTableContainer shapeTable) {
+ this.tripTable = tripTable;
+ this.stopTimeTable = stopTimeTable;
+ this.shapeTable = shapeTable;
+ }
+
+ @Override
+ public void validate(NoticeContainer noticeContainer) {
+ List uniqueShapeIds =
+ shapeTable.getEntities().stream()
+ .map(GtfsShape::shapeId)
+ .distinct()
+ .collect(Collectors.toList());
+
+ uniqueShapeIds.forEach(
+ shapeId -> {
+ double maxShapeDist =
+ shapeTable.getEntities().stream()
+ .filter(s -> s.shapeId().equals(shapeId))
+ .mapToDouble(GtfsShape::shapeDistTraveled)
+ .max()
+ .orElse(Double.NEGATIVE_INFINITY);
+
+ tripTable
+ .byShapeId(shapeId)
+ .forEach(
+ trip -> {
+ double maxStopTimeDist =
+ stopTimeTable.byTripId(trip.tripId()).stream()
+ .mapToDouble(GtfsStopTime::shapeDistTraveled)
+ .max()
+ .orElse(Double.NEGATIVE_INFINITY);
+
+ if (maxStopTimeDist > maxShapeDist) {
+ noticeContainer.addValidationNotice(
+ new TripDistanceExceedsShapeDistanceNotice(
+ trip.tripId(), shapeId, maxStopTimeDist, maxShapeDist));
+ }
+ });
+ });
+ }
+
+ /** The distance traveled by a trip should be less or equal to the max length of its shape. */
+ @GtfsValidationNotice(
+ severity = ERROR,
+ files = @FileRefs({GtfsTrip.class, GtfsStopTime.class, GtfsShape.class}))
+ static class TripDistanceExceedsShapeDistanceNotice extends ValidationNotice {
+
+ /** The faulty record's trip id. */
+ private final String tripId;
+
+ /** The faulty record's shape id. */
+ private final String shapeId;
+
+ /** The faulty record's trip max distance traveled. */
+ private final double maxTripDistanceTraveled;
+
+ /** The faulty record's shape max distance traveled. */
+ private final double maxShapeDistanceTraveled;
+
+ TripDistanceExceedsShapeDistanceNotice(
+ String tripId,
+ String shapeId,
+ double maxTripDistanceTraveled,
+ double maxShapeDistanceTraveled) {
+ this.tripId = tripId;
+ this.shapeId = shapeId;
+ this.maxShapeDistanceTraveled = maxShapeDistanceTraveled;
+ this.maxTripDistanceTraveled = maxTripDistanceTraveled;
+ }
+ }
+}
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 939269a0c7..a67b558575 100644
--- a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java
+++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java
@@ -192,7 +192,9 @@ public void testNoticeClassFieldNames() {
"tripIdB",
"tripIdFieldName",
"validator",
- "value");
+ "value",
+ "maxShapeDistanceTraveled",
+ "maxTripDistanceTraveled");
}
private static List discoverValidationNoticeFieldNames() {
diff --git a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TripAndShapeDistanceValidatorTest.java b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TripAndShapeDistanceValidatorTest.java
new file mode 100644
index 0000000000..03d786f92e
--- /dev/null
+++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/TripAndShapeDistanceValidatorTest.java
@@ -0,0 +1,97 @@
+package org.mobilitydata.gtfsvalidator.validator;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
+import org.mobilitydata.gtfsvalidator.notice.ValidationNotice;
+import org.mobilitydata.gtfsvalidator.table.*;
+
+@RunWith(JUnit4.class)
+public class TripAndShapeDistanceValidatorTest {
+
+ private static List createTripTable(int rows) {
+ ArrayList trips = new ArrayList<>();
+ for (int i = 0; i < rows; i++) {
+ trips.add(
+ new GtfsTrip.Builder()
+ .setCsvRowNumber(i + 1)
+ .setTripId("t" + i)
+ .setServiceId("sr" + i)
+ .setRouteId("r" + i)
+ .setShapeId("s" + i)
+ .build());
+ }
+ return trips;
+ }
+
+ private static List createShapeTable(int rows, double shapeDistTraveled) {
+ ArrayList shapes = new ArrayList<>();
+ for (int i = 0; i < rows; i++) {
+ shapes.add(
+ new GtfsShape.Builder()
+ .setCsvRowNumber(i + 1)
+ .setShapeId("s" + i)
+ .setShapePtLat(1.0)
+ .setShapePtLon(1.0)
+ .setShapePtSequence(0)
+ .setShapeDistTraveled(shapeDistTraveled + i)
+ .build());
+ }
+ return shapes;
+ }
+
+ private static List createStopTimesTable(int rows, double shapeDistTraveled) {
+ ArrayList stopTimes = new ArrayList<>();
+ for (int i = 0; i < rows; i++) {
+ stopTimes.add(
+ new GtfsStopTime.Builder()
+ .setCsvRowNumber(i + 1)
+ .setTripId("t" + i)
+ .setStopSequence(0)
+ .setStopId("st" + i)
+ .setShapeDistTraveled(shapeDistTraveled + i)
+ .build());
+ }
+ return stopTimes;
+ }
+
+ private static List generateNotices(
+ List trips, List stopTimes, List shapes) {
+ NoticeContainer noticeContainer = new NoticeContainer();
+ new TripAndShapeDistanceValidator(
+ GtfsTripTableContainer.forEntities(trips, noticeContainer),
+ GtfsStopTimeTableContainer.forEntities(stopTimes, noticeContainer),
+ GtfsShapeTableContainer.forEntities(shapes, noticeContainer))
+ .validate(noticeContainer);
+ return noticeContainer.getValidationNotices();
+ }
+
+ @Test
+ public void testTripDistanceExceedsShapeDistance() {
+ assertThat(
+ generateNotices(
+ createTripTable(1), createStopTimesTable(1, 10.0), createShapeTable(1, 9.0)))
+ .isNotEmpty();
+ }
+
+ @Test
+ public void testValidTripVsShapeDistance1() {
+ assertThat(
+ generateNotices(
+ createTripTable(1), createStopTimesTable(1, 10.0), createShapeTable(1, 10.0)))
+ .isEmpty();
+ }
+
+ @Test
+ public void testValidTripVsShapeDistance2() {
+ assertThat(
+ generateNotices(
+ createTripTable(1), createStopTimesTable(1, 9.0), createShapeTable(1, 10.0)))
+ .isEmpty();
+ }
+}