diff --git a/core/src/main/java/org/mobilitydata/gtfsvalidator/notice/ForbiddenGeographyIdNotice.java b/core/src/main/java/org/mobilitydata/gtfsvalidator/notice/ForbiddenGeographyIdNotice.java index 41647bbbee..0a823a648e 100644 --- a/core/src/main/java/org/mobilitydata/gtfsvalidator/notice/ForbiddenGeographyIdNotice.java +++ b/core/src/main/java/org/mobilitydata/gtfsvalidator/notice/ForbiddenGeographyIdNotice.java @@ -33,7 +33,7 @@ public class ForbiddenGeographyIdNotice extends ValidationNotice { /** The row of the faulty record. */ private final int csvRowNumber; - /** The sThe id that already exists. */ + /** The id that already exists. */ private final String stopId; /** The id that already exists. */ diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/StopTimesShapeDistTraveledPresenceValidator.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/StopTimesShapeDistTraveledPresenceValidator.java new file mode 100644 index 0000000000..9d13045246 --- /dev/null +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/StopTimesShapeDistTraveledPresenceValidator.java @@ -0,0 +1,103 @@ +/* + * Copyright 2024 MobilityData + * + * 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.annotation.GtfsValidationNotice.SectionRef.FILE_REQUIREMENTS; +import static org.mobilitydata.gtfsvalidator.notice.SeverityLevel.ERROR; + +import org.mobilitydata.gtfsvalidator.annotation.GtfsValidationNotice; +import org.mobilitydata.gtfsvalidator.annotation.GtfsValidator; +import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; +import org.mobilitydata.gtfsvalidator.notice.ValidationNotice; +import org.mobilitydata.gtfsvalidator.table.GtfsStopTime; + +/** + * Check that only entries with stop_id have shape_dist_traveled. A GeoJSON location or location + * group is forbidden to have an associated shape_dist_traveled field in stop_times.txt. + * + *

Generated notice: {@link ForbiddenShapeDistTraveledNotice}. + */ +@GtfsValidator +public class StopTimesShapeDistTraveledPresenceValidator + extends SingleEntityValidator { + + @Override + public void validate(GtfsStopTime stopTime, NoticeContainer noticeContainer) { + if (stopTime.hasStopId()) { + return; + } + if (stopTime.hasLocationGroupId() || stopTime.hasLocationId()) { + if (stopTime.hasShapeDistTraveled()) { + noticeContainer.addValidationNotice( + new ForbiddenShapeDistTraveledNotice( + stopTime.csvRowNumber(), + stopTime.tripId(), + stopTime.locationGroupId(), + stopTime.locationId(), + stopTime.shapeDistTraveled())); + } + } + } + + @Override + public boolean shouldCallValidate(ColumnInspector header) { + // No point in validating if there is no shape_dist_traveled column + // And we need to have either location_id or location_group_id for this validator to make sense + return header.hasColumn(GtfsStopTime.SHAPE_DIST_TRAVELED_FIELD_NAME) + && (header.hasColumn(GtfsStopTime.LOCATION_ID_FIELD_NAME) + || header.hasColumn(GtfsStopTime.LOCATION_GROUP_ID_FIELD_NAME)); + } + + /** + * A stop_time entry has a `shape_dist_traveled` without a `stop_id` value. + * + *

A GeoJSON location or location group has an associated shape_dist_traveled field in + * stop_times.txt. shape_dist_traveled values should only be provided for stops. + */ + @GtfsValidationNotice( + severity = ERROR, + sections = @GtfsValidationNotice.SectionRefs(FILE_REQUIREMENTS)) + public static class ForbiddenShapeDistTraveledNotice extends ValidationNotice { + + /** The row of the faulty record. */ + private final int csvRowNumber; + + /** The trip_id for which the shape_dist_traveled is defined */ + private final String tripId; + + /** The location_grpup_id for which the shape_dist_traveled is defined */ + private final String locationGroupId; + + /** The location_id for which the shape_dist_traveled is defined */ + private final String locationId; + + /** The shape_dist_traveled value */ + private final double shapeDistTraveled; + + public ForbiddenShapeDistTraveledNotice( + int csvRowNumber, + String tripId, + String locationGroupId, + String locationId, + double shapeDistTraveled) { + this.csvRowNumber = csvRowNumber; + this.tripId = tripId; + this.locationGroupId = locationGroupId; + this.locationId = locationId; + this.shapeDistTraveled = shapeDistTraveled; + } + } +} diff --git a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/StopTimesShapeDistTraveledPresenceValidatorTest.java b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/StopTimesShapeDistTraveledPresenceValidatorTest.java new file mode 100644 index 0000000000..2c7d17baf0 --- /dev/null +++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/StopTimesShapeDistTraveledPresenceValidatorTest.java @@ -0,0 +1,80 @@ +/* + * 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 com.google.common.truth.Truth.assertThat; + +import com.google.common.collect.ImmutableList; +import java.util.List; +import org.junit.Test; +import org.mobilitydata.gtfsvalidator.notice.NoticeContainer; +import org.mobilitydata.gtfsvalidator.notice.ValidationNotice; +import org.mobilitydata.gtfsvalidator.table.GtfsStopTime; + +public class StopTimesShapeDistTraveledPresenceValidatorTest { + public static GtfsStopTime createStopTime( + int csvRowNumber, + String tripId, + String stopId, + String locationGroupId, + String locationId, + int stopSequence, + double shapeDistTraveled) { + var builder = + new GtfsStopTime.Builder() + .setCsvRowNumber(csvRowNumber) + .setTripId(tripId) + .setStopSequence(stopSequence) + .setShapeDistTraveled(shapeDistTraveled); + if (stopId != null) { + builder.setStopId(stopId); + } + if (locationGroupId != null) { + builder.setLocationGroupId(locationGroupId); + } + if (locationId != null) { + builder.setLocationId(locationId); + } + + return builder.build(); + } + + private static List generateNotices(List stopTimes) { + NoticeContainer noticeContainer = new NoticeContainer(); + + var validator = new StopTimesShapeDistTraveledPresenceValidator(); + for (var stopTime : stopTimes) { + validator.validate(stopTime, noticeContainer); + } + return noticeContainer.getValidationNotices(); + } + + @Test + public void locationWithShapeDistanceShouldGenerateNotice() { + assertThat( + generateNotices( + ImmutableList.of( + createStopTime(1, "first trip", null, "loc1", null, 2, 10.0d), + createStopTime(2, "first trip", null, null, "loc2", 42, 45.0d), + createStopTime(3, "first trip", "stop1", null, null, 46, 64.0d)))) + .containsExactly( + new StopTimesShapeDistTraveledPresenceValidator.ForbiddenShapeDistTraveledNotice( + 1, "first trip", "loc1", "", 10.0d), + new StopTimesShapeDistTraveledPresenceValidator.ForbiddenShapeDistTraveledNotice( + 2, "first trip", "", "loc2", 45.0d)); + } +}