From 24f225c11f671069ab428fefeaa00d3f2c3cd74b Mon Sep 17 00:00:00 2001 From: cka-y Date: Mon, 23 Dec 2024 11:48:25 -0500 Subject: [PATCH] feat: flex - overlapping_zone_and_pickup_drop_off_window --- .../table/GtfsGeoJsonFeature.java | 15 ++- .../table/GtfsGeoJsonFeaturesContainer.java | 8 ++ ...OverlappingPickupDropOffZoneValidator.java | 126 ++++++++++++++++++ .../validator/NoticeFieldsTest.java | 6 + 4 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 main/src/main/java/org/mobilitydata/gtfsvalidator/validator/OverlappingPickupDropOffZoneValidator.java diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsGeoJsonFeature.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsGeoJsonFeature.java index a7e75d4ab5..4b97405c57 100644 --- a/main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsGeoJsonFeature.java +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsGeoJsonFeature.java @@ -2,7 +2,7 @@ import javax.annotation.Nonnull; import javax.annotation.Nullable; -import org.locationtech.jts.geom.Polygonal; +import org.locationtech.jts.geom.Geometry; import org.mobilitydata.gtfsvalidator.util.geojson.GeometryType; /** This class contains the information from one feature in the GeoJSON file. */ @@ -25,7 +25,7 @@ public final class GtfsGeoJsonFeature implements GtfsEntity { private String featureId; // The id of a feature in the GeoJSON file. private GeometryType geometryType; // The type of the geometry. - private Polygonal geometryDefinition; // The geometry of the feature. + private Geometry geometryDefinition; // The geometry of the feature. private String stopName; // The name of the location as displayed to the riders. private String stopDesc; // A description of the location. @@ -50,15 +50,22 @@ public void setFeatureId(@Nullable String featureId) { this.featureId = featureId; } - public Polygonal geometryDefinition() { + public Geometry geometryDefinition() { return geometryDefinition; } + public Boolean geometryOverlaps(GtfsGeoJsonFeature other) { + if (geometryDefinition == null || other.geometryDefinition == null) { + return false; + } + return geometryDefinition.overlaps(other.geometryDefinition); + } + public Boolean hasGeometryDefinition() { return geometryDefinition != null; } - public void setGeometryDefinition(Polygonal polygon) { + public void setGeometryDefinition(Geometry polygon) { this.geometryDefinition = polygon; } diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsGeoJsonFeaturesContainer.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsGeoJsonFeaturesContainer.java index bdf9cea7cb..4e9d4fc36d 100644 --- a/main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsGeoJsonFeaturesContainer.java +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsGeoJsonFeaturesContainer.java @@ -86,4 +86,12 @@ private void setupIndices(NoticeContainer noticeContainer) { // } } } + + public Map byLocationIdMap() { + return byLocationIdMap; + } + + public GtfsGeoJsonFeature byLocationId(String locationId) { + return byLocationIdMap.get(locationId); + } } diff --git a/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/OverlappingPickupDropOffZoneValidator.java b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/OverlappingPickupDropOffZoneValidator.java new file mode 100644 index 0000000000..ac3252315e --- /dev/null +++ b/main/src/main/java/org/mobilitydata/gtfsvalidator/validator/OverlappingPickupDropOffZoneValidator.java @@ -0,0 +1,126 @@ +package org.mobilitydata.gtfsvalidator.validator; + +import static org.mobilitydata.gtfsvalidator.notice.SeverityLevel.ERROR; + +import java.util.Collection; +import java.util.Map; +import javax.inject.Inject; +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.*; +import org.mobilitydata.gtfsvalidator.type.GtfsTime; + +@GtfsValidator +public class OverlappingPickupDropOffZoneValidator extends FileValidator { + + private final GtfsStopTimeTableContainer stopTimeTableContainer; + private final GtfsGeoJsonFeaturesContainer geoJsonFeaturesContainer; + + @Inject + OverlappingPickupDropOffZoneValidator( + GtfsStopTimeTableContainer table, GtfsGeoJsonFeaturesContainer geoJsonFeaturesContainer) { + this.stopTimeTableContainer = table; + this.geoJsonFeaturesContainer = geoJsonFeaturesContainer; + } + + @Override + public void validate(NoticeContainer noticeContainer) { + if (stopTimeTableContainer.isMissingFile() || geoJsonFeaturesContainer.isMissingFile()) { + return; + } + // For all entities with the same trip id + for (Map.Entry> entry : + stopTimeTableContainer.byTripIdMap().asMap().entrySet()) { + Collection stopTimesForTrip = entry.getValue(); + // Checking entities two by two + for (GtfsStopTime stopTime1 : stopTimesForTrip) { + for (GtfsStopTime stopTime2 : stopTimesForTrip) { + // If the two entities are the same, skip + if (stopTime1.equals(stopTime2)) { + continue; + } + // If the two entities have overlapping pickup/drop-off windows + if (!(stopTime1.hasEndPickupDropOffWindow() + && stopTime1.hasStartPickupDropOffWindow() + && stopTime2.hasEndPickupDropOffWindow() + && stopTime2.hasStartPickupDropOffWindow() + && stopTime1.hasLocationId() + && stopTime2.hasLocationId())) { + continue; + } + if (stopTime1.startPickupDropOffWindow().isAfter(stopTime2.endPickupDropOffWindow()) + || stopTime1 + .endPickupDropOffWindow() + .isBefore(stopTime2.startPickupDropOffWindow())) { + continue; + } + // If the two entities have overlapping pickup/drop-off zones + GtfsGeoJsonFeature stop1GeoJsonFeature = + geoJsonFeaturesContainer.byLocationId(stopTime1.locationId()); + GtfsGeoJsonFeature stop2GeoJsonFeature = + geoJsonFeaturesContainer.byLocationId(stopTime2.locationId()); + if (stop1GeoJsonFeature == null || stop2GeoJsonFeature == null) { + continue; + } + if (stop1GeoJsonFeature.geometryOverlaps(stop2GeoJsonFeature)) { + noticeContainer.addValidationNotice( + new OverlappingZoneAndPickupDropOffWindowNotice( + stopTime1.locationId(), + stopTime1.startPickupDropOffWindow(), + stopTime1.endPickupDropOffWindow(), + stopTime2.locationId(), + stopTime2.startPickupDropOffWindow(), + stopTime2.endPickupDropOffWindow())); + } + } + } + } + } + + /** + * Two entities have overlapping pickup/drop-off windows and zones. + * + *

Two entities in `stop_times.txt` with the same `trip_id` have overlapping pickup/drop-off + * windows and have overlapping zones in `locations.geojson`. + */ + @GtfsValidationNotice( + severity = ERROR, + files = @GtfsValidationNotice.FileRefs({GtfsGeoJsonFeature.class, GtfsStopTime.class})) + static class OverlappingZoneAndPickupDropOffWindowNotice extends ValidationNotice { + + /** The `location_id` of the first entity. */ + private final String locationIdA; + + /** The `start_pickup_drop_off_window` of the first entity in `stop_times.txt`. */ + private final GtfsTime startPickupDropOffWindowA; + + /** The `end_pickup_drop_off_window` of the first entity in `stop_times.txt`. */ + private final GtfsTime endPickupDropOffWindowA; + + /** The `location_id` of the second entity. */ + private final String locationIdB; + + /** The `start_pickup_drop_off_window` of the second entity in `stop_times.txt`. */ + private final GtfsTime startPickupDropOffWindowB; + + /** The `end_pickup_drop_off_window` of the second entity in `stop_times.txt`. */ + private final GtfsTime endPickupDropOffWindowB; + + OverlappingZoneAndPickupDropOffWindowNotice( + String locationIdA, + GtfsTime startPickupDropOffWindowA, + GtfsTime endPickupDropOffWindowA, + String locationIdB, + GtfsTime startPickupDropOffWindowB, + GtfsTime endPickupDropOffWindowB) { + this.locationIdA = locationIdA; + this.startPickupDropOffWindowA = startPickupDropOffWindowA; + this.endPickupDropOffWindowA = endPickupDropOffWindowA; + this.locationIdB = locationIdB; + this.startPickupDropOffWindowB = startPickupDropOffWindowB; + this.endPickupDropOffWindowB = endPickupDropOffWindowB; + } + } +} 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 6a1a55ae91..c8b3896bb4 100644 --- a/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java +++ b/main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java @@ -80,6 +80,8 @@ public void testNoticeClassFieldNames() { "departureTime1", "distanceKm", "endFieldName", + "endPickupDropOffWindowA", + "endPickupDropOffWindowB", "endValue", "entityCount", "entityId", @@ -120,6 +122,8 @@ public void testNoticeClassFieldNames() { "lineIndex", "locationGroupId", "locationId", + "locationIdA", + "locationIdB", "locationType", "locationTypeName", "locationTypeValue", @@ -182,6 +186,8 @@ public void testNoticeClassFieldNames() { "specifiedField", "speedKph", "startFieldName", + "startPickupDropOffWindowA", + "startPickupDropOffWindowB", "startValue", "stopCsvRowNumber", "stopDesc",