Skip to content

Commit

Permalink
Merge 7fb7b3d into 9f06d68
Browse files Browse the repository at this point in the history
  • Loading branch information
cka-y authored Dec 24, 2024
2 parents 9f06d68 + 7fb7b3d commit 7e68a93
Show file tree
Hide file tree
Showing 5 changed files with 446 additions and 4 deletions.
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

0 comments on commit 7e68a93

Please sign in to comment.