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: flex - overlapping_zone_and_pickup_drop_off_window #1934

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 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
Expand Up @@ -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. */
Expand All @@ -25,12 +25,20 @@ 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.

public GtfsGeoJsonFeature() {}

private GtfsGeoJsonFeature(Builder builder) {
this.featureId = builder.featureId;
this.geometryType = builder.geometryType;
this.geometryDefinition = builder.geometryDefinition;
this.stopName = builder.stopName;
this.stopDesc = builder.stopDesc;
}

// TODO: Change the interface hierarchy so we dont need this. It's not relevant for geojson
@Override
public int csvRowNumber() {
Expand All @@ -50,15 +58,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;
}

Expand Down Expand Up @@ -97,4 +112,42 @@ public Boolean hasStopDesc() {
public void setStopDesc(@Nullable String stopDesc) {
this.stopDesc = stopDesc;
}

/** Builder class for GtfsGeoJsonFeature. */
public static class Builder {
private String featureId;
private GeometryType geometryType;
private Geometry geometryDefinition;
private String stopName;
private String stopDesc;

public Builder featureId(String featureId) {
this.featureId = featureId;
return this;
}

public Builder geometryType(GeometryType geometryType) {
this.geometryType = geometryType;
return this;
}

public Builder geometryDefinition(Geometry geometryDefinition) {
this.geometryDefinition = geometryDefinition;
return this;
}

public Builder stopName(String stopName) {
this.stopName = stopName;
return this;
}

public Builder stopDesc(String stopDesc) {
this.stopDesc = stopDesc;
return this;
}

public GtfsGeoJsonFeature build() {
return new GtfsGeoJsonFeature(this);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,12 @@ private void setupIndices(NoticeContainer noticeContainer) {
// }
}
}

public Map<String, GtfsGeoJsonFeature> byLocationIdMap() {
return byLocationIdMap;
}

public GtfsGeoJsonFeature byLocationId(String locationId) {
return byLocationIdMap.get(locationId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package org.mobilitydata.gtfsvalidator.validator;

import static org.mobilitydata.gtfsvalidator.notice.SeverityLevel.ERROR;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
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 either the stop_times file or GeoJSON file is missing, skip validation.
if (stopTimeTableContainer.isMissingFile() || geoJsonFeaturesContainer.isMissingFile()) {
return;
}

// Iterate through all stop times grouped by trip ID.
for (Map.Entry<String, Collection<GtfsStopTime>> entry :
stopTimeTableContainer.byTripIdMap().asMap().entrySet()) {
List<GtfsStopTime> stopTimesForTrip = new ArrayList<>(entry.getValue());

// Compare each pair of stop times within the same trip.
for (int i = 0; i < stopTimesForTrip.size(); i++) {
GtfsStopTime stopTime1 = stopTimesForTrip.get(i);
for (int j = i + 1; j < stopTimesForTrip.size(); j++) {
GtfsStopTime stopTime2 = stopTimesForTrip.get(j);

// Skip validation if any required fields are missing in either stop time.
if (!(stopTime1.hasEndPickupDropOffWindow()
&& stopTime1.hasStartPickupDropOffWindow()
&& stopTime2.hasEndPickupDropOffWindow()
&& stopTime2.hasStartPickupDropOffWindow()
&& stopTime1.hasLocationId()
&& stopTime2.hasLocationId())) {
continue;
}

// Skip validation if both stop times reference the same location.
if (stopTime1.locationId().equals(stopTime2.locationId())) {
continue;
}

// Skip validation if the pickup/drop-off windows of the two stop times do not overlap.
if (stopTime1.startPickupDropOffWindow().isAfter(stopTime2.endPickupDropOffWindow())
|| stopTime1.endPickupDropOffWindow().isBefore(stopTime2.startPickupDropOffWindow())
|| stopTime1.endPickupDropOffWindow().equals(stopTime2.startPickupDropOffWindow())
|| stopTime1.startPickupDropOffWindow().equals(stopTime2.endPickupDropOffWindow())) {
continue;
}

// Retrieve GeoJSON features for the locations referenced by the two stop times.
GtfsGeoJsonFeature stop1GeoJsonFeature =
geoJsonFeaturesContainer.byLocationId(stopTime1.locationId());
GtfsGeoJsonFeature stop2GeoJsonFeature =
geoJsonFeaturesContainer.byLocationId(stopTime2.locationId());

// Skip validation if either location has no corresponding GeoJSON feature.
if (stop1GeoJsonFeature == null || stop2GeoJsonFeature == null) {
continue;
}

// If the geometries of the two locations overlap, generate a validation notice.
if (stop1GeoJsonFeature.geometryOverlaps(stop2GeoJsonFeature)) {
noticeContainer.addValidationNotice(
new OverlappingZoneAndPickupDropOffWindowNotice(
stopTime1.tripId(),
stopTime1.stopSequence(),
stopTime1.locationId(),
stopTime1.startPickupDropOffWindow(),
stopTime1.endPickupDropOffWindow(),
stopTime2.stopSequence(),
stopTime2.locationId(),
stopTime2.startPickupDropOffWindow(),
stopTime2.endPickupDropOffWindow()));
}
}
}
}
}

/**
* Two entities have overlapping pickup/drop-off windows and zones.
*
* <p>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 `trip_id` of the entities. */
private final String tripId;

/** The `stop_sequence` of the first entity in `stop_times.txt`. */
private final Integer stopSequence1;

/** The `location_id` of the first entity. */
private final String locationId1;

/** The `start_pickup_drop_off_window` of the first entity in `stop_times.txt`. */
private final GtfsTime startPickupDropOffWindow1;

/** The `end_pickup_drop_off_window` of the first entity in `stop_times.txt`. */
private final GtfsTime endPickupDropOffWindow1;

/** The `stop_sequence` of the second entity in `stop_times.txt`. */
private final Integer stopSequence2;

/** The `location_id` of the second entity. */
private final String locationId2;

/** The `start_pickup_drop_off_window` of the second entity in `stop_times.txt`. */
private final GtfsTime startPickupDropOffWindow2;

/** The `end_pickup_drop_off_window` of the second entity in `stop_times.txt`. */
private final GtfsTime endPickupDropOffWindow2;

OverlappingZoneAndPickupDropOffWindowNotice(
String tripId,
Integer stopSequence1,
String locationId1,
GtfsTime startPickupDropOffWindow1,
GtfsTime endPickupDropOffWindow1,
Integer stopSequence2,
String locationId2,
GtfsTime startPickupDropOffWindow2,
GtfsTime endPickupDropOffWindow2) {
this.tripId = tripId;
this.stopSequence1 = stopSequence1;
this.locationId1 = locationId1;
this.startPickupDropOffWindow1 = startPickupDropOffWindow1;
this.endPickupDropOffWindow1 = endPickupDropOffWindow1;
this.stopSequence2 = stopSequence2;
this.locationId2 = locationId2;
this.startPickupDropOffWindow2 = startPickupDropOffWindow2;
this.endPickupDropOffWindow2 = endPickupDropOffWindow2;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ public void testNoticeClassFieldNames() {
"departureTime1",
"distanceKm",
"endFieldName",
"endPickupDropOffWindow1",
"endPickupDropOffWindow2",
"endValue",
"entityCount",
"entityId",
Expand Down Expand Up @@ -120,6 +122,8 @@ public void testNoticeClassFieldNames() {
"lineIndex",
"locationGroupId",
"locationId",
"locationId1",
"locationId2",
"locationType",
"locationTypeName",
"locationTypeValue",
Expand Down Expand Up @@ -182,6 +186,8 @@ public void testNoticeClassFieldNames() {
"specifiedField",
"speedKph",
"startFieldName",
"startPickupDropOffWindow1",
"startPickupDropOffWindow2",
"startValue",
"stopCsvRowNumber",
"stopDesc",
Expand Down
Loading
Loading