Skip to content

Commit e9258a2

Browse files
authored
feat: flex - overlapping_zone_and_pickup_drop_off_window (#1934)
1 parent 7c671d8 commit e9258a2

File tree

5 files changed

+460
-4
lines changed

5 files changed

+460
-4
lines changed

main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsGeoJsonFeature.java

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import javax.annotation.Nonnull;
44
import javax.annotation.Nullable;
5-
import org.locationtech.jts.geom.Polygonal;
5+
import org.locationtech.jts.geom.Geometry;
66
import org.mobilitydata.gtfsvalidator.util.geojson.GeometryType;
77

88
/** This class contains the information from one feature in the GeoJSON file. */
@@ -25,12 +25,20 @@ public final class GtfsGeoJsonFeature implements GtfsEntity {
2525

2626
private String featureId; // The id of a feature in the GeoJSON file.
2727
private GeometryType geometryType; // The type of the geometry.
28-
private Polygonal geometryDefinition; // The geometry of the feature.
28+
private Geometry geometryDefinition; // The geometry of the feature.
2929
private String stopName; // The name of the location as displayed to the riders.
3030
private String stopDesc; // A description of the location.
3131

3232
public GtfsGeoJsonFeature() {}
3333

34+
private GtfsGeoJsonFeature(Builder builder) {
35+
this.featureId = builder.featureId;
36+
this.geometryType = builder.geometryType;
37+
this.geometryDefinition = builder.geometryDefinition;
38+
this.stopName = builder.stopName;
39+
this.stopDesc = builder.stopDesc;
40+
}
41+
3442
// TODO: Change the interface hierarchy so we dont need this. It's not relevant for geojson
3543
@Override
3644
public int csvRowNumber() {
@@ -50,15 +58,22 @@ public void setFeatureId(@Nullable String featureId) {
5058
this.featureId = featureId;
5159
}
5260

53-
public Polygonal geometryDefinition() {
61+
public Geometry geometryDefinition() {
5462
return geometryDefinition;
5563
}
5664

65+
public Boolean geometryOverlaps(GtfsGeoJsonFeature other) {
66+
if (geometryDefinition == null || other.geometryDefinition == null) {
67+
return false;
68+
}
69+
return geometryDefinition.overlaps(other.geometryDefinition);
70+
}
71+
5772
public Boolean hasGeometryDefinition() {
5873
return geometryDefinition != null;
5974
}
6075

61-
public void setGeometryDefinition(Polygonal polygon) {
76+
public void setGeometryDefinition(Geometry polygon) {
6277
this.geometryDefinition = polygon;
6378
}
6479

@@ -97,4 +112,42 @@ public Boolean hasStopDesc() {
97112
public void setStopDesc(@Nullable String stopDesc) {
98113
this.stopDesc = stopDesc;
99114
}
115+
116+
/** Builder class for GtfsGeoJsonFeature. */
117+
public static class Builder {
118+
private String featureId;
119+
private GeometryType geometryType;
120+
private Geometry geometryDefinition;
121+
private String stopName;
122+
private String stopDesc;
123+
124+
public Builder featureId(String featureId) {
125+
this.featureId = featureId;
126+
return this;
127+
}
128+
129+
public Builder geometryType(GeometryType geometryType) {
130+
this.geometryType = geometryType;
131+
return this;
132+
}
133+
134+
public Builder geometryDefinition(Geometry geometryDefinition) {
135+
this.geometryDefinition = geometryDefinition;
136+
return this;
137+
}
138+
139+
public Builder stopName(String stopName) {
140+
this.stopName = stopName;
141+
return this;
142+
}
143+
144+
public Builder stopDesc(String stopDesc) {
145+
this.stopDesc = stopDesc;
146+
return this;
147+
}
148+
149+
public GtfsGeoJsonFeature build() {
150+
return new GtfsGeoJsonFeature(this);
151+
}
152+
}
100153
}

main/src/main/java/org/mobilitydata/gtfsvalidator/table/GtfsGeoJsonFeaturesContainer.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,12 @@ private void setupIndices(NoticeContainer noticeContainer) {
8686
// }
8787
}
8888
}
89+
90+
public Map<String, GtfsGeoJsonFeature> byLocationIdMap() {
91+
return byLocationIdMap;
92+
}
93+
94+
public GtfsGeoJsonFeature byLocationId(String locationId) {
95+
return byLocationIdMap.get(locationId);
96+
}
8997
}
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package org.mobilitydata.gtfsvalidator.validator;
2+
3+
import static org.mobilitydata.gtfsvalidator.notice.SeverityLevel.ERROR;
4+
5+
import java.util.ArrayList;
6+
import java.util.Collection;
7+
import java.util.List;
8+
import java.util.Map;
9+
import javax.inject.Inject;
10+
import org.mobilitydata.gtfsvalidator.annotation.GtfsValidationNotice;
11+
import org.mobilitydata.gtfsvalidator.annotation.GtfsValidator;
12+
import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
13+
import org.mobilitydata.gtfsvalidator.notice.ValidationNotice;
14+
import org.mobilitydata.gtfsvalidator.table.*;
15+
import org.mobilitydata.gtfsvalidator.type.GtfsTime;
16+
17+
@GtfsValidator
18+
public class OverlappingPickupDropOffZoneValidator extends FileValidator {
19+
20+
private final GtfsStopTimeTableContainer stopTimeTableContainer;
21+
private final GtfsGeoJsonFeaturesContainer geoJsonFeaturesContainer;
22+
23+
@Inject
24+
OverlappingPickupDropOffZoneValidator(
25+
GtfsStopTimeTableContainer table, GtfsGeoJsonFeaturesContainer geoJsonFeaturesContainer) {
26+
this.stopTimeTableContainer = table;
27+
this.geoJsonFeaturesContainer = geoJsonFeaturesContainer;
28+
}
29+
30+
@Override
31+
public void validate(NoticeContainer noticeContainer) {
32+
// If either the stop_times file or GeoJSON file is missing, skip validation.
33+
if (stopTimeTableContainer.isMissingFile() || geoJsonFeaturesContainer.isMissingFile()) {
34+
return;
35+
}
36+
37+
// Iterate through all stop times grouped by trip ID.
38+
for (Map.Entry<String, Collection<GtfsStopTime>> entry :
39+
stopTimeTableContainer.byTripIdMap().asMap().entrySet()) {
40+
List<GtfsStopTime> stopTimesForTrip = new ArrayList<>(entry.getValue());
41+
42+
// Compare each pair of stop times within the same trip.
43+
for (int i = 0; i < stopTimesForTrip.size(); i++) {
44+
GtfsStopTime stopTime1 = stopTimesForTrip.get(i);
45+
for (int j = i + 1; j < stopTimesForTrip.size(); j++) {
46+
GtfsStopTime stopTime2 = stopTimesForTrip.get(j);
47+
48+
// Skip validation if the two stop times have different pickup/drop-off types or if
49+
// the types are UNRECOGNIZED.
50+
if ((stopTime1.pickupType() != stopTime2.pickupType()
51+
&& stopTime1.dropOffType() != stopTime2.dropOffType())
52+
|| (stopTime1.pickupType() == GtfsPickupDropOff.UNRECOGNIZED
53+
|| stopTime1.dropOffType() == GtfsPickupDropOff.UNRECOGNIZED
54+
|| stopTime2.pickupType() == GtfsPickupDropOff.UNRECOGNIZED
55+
|| stopTime2.dropOffType() == GtfsPickupDropOff.UNRECOGNIZED)) {
56+
continue;
57+
}
58+
59+
// Skip validation if any required fields are missing in either stop time.
60+
if (!(stopTime1.hasEndPickupDropOffWindow()
61+
&& stopTime1.hasStartPickupDropOffWindow()
62+
&& stopTime2.hasEndPickupDropOffWindow()
63+
&& stopTime2.hasStartPickupDropOffWindow()
64+
&& stopTime1.hasLocationId()
65+
&& stopTime2.hasLocationId())) {
66+
continue;
67+
}
68+
69+
// Skip validation if both stop times reference the same location.
70+
if (stopTime1.locationId().equals(stopTime2.locationId())) {
71+
continue;
72+
}
73+
74+
// Skip validation if the pickup/drop-off windows of the two stop times do not overlap.
75+
if (stopTime1.startPickupDropOffWindow().isAfter(stopTime2.endPickupDropOffWindow())
76+
|| stopTime1.endPickupDropOffWindow().isBefore(stopTime2.startPickupDropOffWindow())
77+
|| stopTime1.endPickupDropOffWindow().equals(stopTime2.startPickupDropOffWindow())
78+
|| stopTime1.startPickupDropOffWindow().equals(stopTime2.endPickupDropOffWindow())) {
79+
continue;
80+
}
81+
82+
// Retrieve GeoJSON features for the locations referenced by the two stop times.
83+
GtfsGeoJsonFeature stop1GeoJsonFeature =
84+
geoJsonFeaturesContainer.byLocationId(stopTime1.locationId());
85+
GtfsGeoJsonFeature stop2GeoJsonFeature =
86+
geoJsonFeaturesContainer.byLocationId(stopTime2.locationId());
87+
88+
// Skip validation if either location has no corresponding GeoJSON feature.
89+
if (stop1GeoJsonFeature == null || stop2GeoJsonFeature == null) {
90+
continue;
91+
}
92+
93+
// If the geometries of the two locations overlap, generate a validation notice.
94+
if (stop1GeoJsonFeature.geometryOverlaps(stop2GeoJsonFeature)) {
95+
noticeContainer.addValidationNotice(
96+
new OverlappingZoneAndPickupDropOffWindowNotice(
97+
stopTime1.tripId(),
98+
stopTime1.stopSequence(),
99+
stopTime1.locationId(),
100+
stopTime1.startPickupDropOffWindow(),
101+
stopTime1.endPickupDropOffWindow(),
102+
stopTime2.stopSequence(),
103+
stopTime2.locationId(),
104+
stopTime2.startPickupDropOffWindow(),
105+
stopTime2.endPickupDropOffWindow()));
106+
}
107+
}
108+
}
109+
}
110+
}
111+
112+
/**
113+
* Two entities have overlapping pickup/drop-off windows and zones.
114+
*
115+
* <p>Two entities in `stop_times.txt` with the same `trip_id` have the same `pickup_type` or
116+
* `drop_off_type`, overlapping pickup/drop-off windows and overlapping zones in
117+
* `locations.geojson`.
118+
*/
119+
@GtfsValidationNotice(
120+
severity = ERROR,
121+
files = @GtfsValidationNotice.FileRefs({GtfsGeoJsonFeature.class, GtfsStopTime.class}))
122+
static class OverlappingZoneAndPickupDropOffWindowNotice extends ValidationNotice {
123+
/** The `trip_id` of the entities. */
124+
private final String tripId;
125+
126+
/** The `stop_sequence` of the first entity in `stop_times.txt`. */
127+
private final Integer stopSequence1;
128+
129+
/** The `location_id` of the first entity. */
130+
private final String locationId1;
131+
132+
/** The `start_pickup_drop_off_window` of the first entity in `stop_times.txt`. */
133+
private final GtfsTime startPickupDropOffWindow1;
134+
135+
/** The `end_pickup_drop_off_window` of the first entity in `stop_times.txt`. */
136+
private final GtfsTime endPickupDropOffWindow1;
137+
138+
/** The `stop_sequence` of the second entity in `stop_times.txt`. */
139+
private final Integer stopSequence2;
140+
141+
/** The `location_id` of the second entity. */
142+
private final String locationId2;
143+
144+
/** The `start_pickup_drop_off_window` of the second entity in `stop_times.txt`. */
145+
private final GtfsTime startPickupDropOffWindow2;
146+
147+
/** The `end_pickup_drop_off_window` of the second entity in `stop_times.txt`. */
148+
private final GtfsTime endPickupDropOffWindow2;
149+
150+
OverlappingZoneAndPickupDropOffWindowNotice(
151+
String tripId,
152+
Integer stopSequence1,
153+
String locationId1,
154+
GtfsTime startPickupDropOffWindow1,
155+
GtfsTime endPickupDropOffWindow1,
156+
Integer stopSequence2,
157+
String locationId2,
158+
GtfsTime startPickupDropOffWindow2,
159+
GtfsTime endPickupDropOffWindow2) {
160+
this.tripId = tripId;
161+
this.stopSequence1 = stopSequence1;
162+
this.locationId1 = locationId1;
163+
this.startPickupDropOffWindow1 = startPickupDropOffWindow1;
164+
this.endPickupDropOffWindow1 = endPickupDropOffWindow1;
165+
this.stopSequence2 = stopSequence2;
166+
this.locationId2 = locationId2;
167+
this.startPickupDropOffWindow2 = startPickupDropOffWindow2;
168+
this.endPickupDropOffWindow2 = endPickupDropOffWindow2;
169+
}
170+
}
171+
}

main/src/test/java/org/mobilitydata/gtfsvalidator/validator/NoticeFieldsTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ public void testNoticeClassFieldNames() {
8181
"distanceKm",
8282
"endFieldName",
8383
"endPickupDropOffWindow",
84+
"endPickupDropOffWindow1",
85+
"endPickupDropOffWindow2",
8486
"endValue",
8587
"entityCount",
8688
"entityId",
@@ -121,6 +123,8 @@ public void testNoticeClassFieldNames() {
121123
"lineIndex",
122124
"locationGroupId",
123125
"locationId",
126+
"locationId1",
127+
"locationId2",
124128
"locationType",
125129
"locationTypeName",
126130
"locationTypeValue",
@@ -184,6 +188,8 @@ public void testNoticeClassFieldNames() {
184188
"speedKph",
185189
"startFieldName",
186190
"startPickupDropOffWindow",
191+
"startPickupDropOffWindow1",
192+
"startPickupDropOffWindow2",
187193
"startValue",
188194
"stopCsvRowNumber",
189195
"stopDesc",

0 commit comments

Comments
 (0)