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(); + } +}