Skip to content

Commit

Permalink
feat: flex - overlapping_zone_and_pickup_drop_off_window
Browse files Browse the repository at this point in the history
  • Loading branch information
cka-y committed Dec 23, 2024
1 parent 9f06d68 commit 24f225c
Show file tree
Hide file tree
Showing 4 changed files with 151 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,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.

Expand All @@ -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;
}

Expand Down
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,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<String, Collection<GtfsStopTime>> entry :
stopTimeTableContainer.byTripIdMap().asMap().entrySet()) {
Collection<GtfsStopTime> 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.
*
* <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 `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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ public void testNoticeClassFieldNames() {
"departureTime1",
"distanceKm",
"endFieldName",
"endPickupDropOffWindowA",
"endPickupDropOffWindowB",
"endValue",
"entityCount",
"entityId",
Expand Down Expand Up @@ -120,6 +122,8 @@ public void testNoticeClassFieldNames() {
"lineIndex",
"locationGroupId",
"locationId",
"locationIdA",
"locationIdB",
"locationType",
"locationTypeName",
"locationTypeValue",
Expand Down Expand Up @@ -182,6 +186,8 @@ public void testNoticeClassFieldNames() {
"specifiedField",
"speedKph",
"startFieldName",
"startPickupDropOffWindowA",
"startPickupDropOffWindowB",
"startValue",
"stopCsvRowNumber",
"stopDesc",
Expand Down

0 comments on commit 24f225c

Please sign in to comment.