Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: trip vs shape distance validation #1553

Merged
merged 5 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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<String> 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,9 @@ public void testNoticeClassFieldNames() {
"tripIdB",
"tripIdFieldName",
"validator",
"value");
"value",
"maxShapeDistanceTraveled",
"maxTripDistanceTraveled");
}

private static List<String> discoverValidationNoticeFieldNames() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great addition!


private static List<GtfsTrip> createTripTable(int rows) {
ArrayList<GtfsTrip> 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<GtfsShape> createShapeTable(int rows, double shapeDistTraveled) {
ArrayList<GtfsShape> 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<GtfsStopTime> createStopTimesTable(int rows, double shapeDistTraveled) {
ArrayList<GtfsStopTime> 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<ValidationNotice> generateNotices(
List<GtfsTrip> trips, List<GtfsStopTime> stopTimes, List<GtfsShape> 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();
}
}