Skip to content

Commit

Permalink
fix: make stop_without_zone_id conditional on fare rule type (#1663) (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelandrewkearney authored Mar 12, 2024
1 parent 16d20e0 commit f91e524
Show file tree
Hide file tree
Showing 3 changed files with 416 additions and 128 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.mobilitydata.gtfsvalidator.annotation.FieldTypeEnum;
import org.mobilitydata.gtfsvalidator.annotation.ForeignKey;
import org.mobilitydata.gtfsvalidator.annotation.GtfsTable;
import org.mobilitydata.gtfsvalidator.annotation.Index;
import org.mobilitydata.gtfsvalidator.annotation.PrimaryKey;
import org.mobilitydata.gtfsvalidator.annotation.Required;

Expand All @@ -31,11 +32,13 @@ public interface GtfsFareRuleSchema extends GtfsEntity {
@Required
@ForeignKey(table = "fare_attributes.txt", field = "fare_id")
@PrimaryKey(translationRecordIdType = UNSUPPORTED)
@Index
String fareId();

@FieldType(FieldTypeEnum.ID)
@ForeignKey(table = "routes.txt", field = "route_id")
@PrimaryKey(translationRecordIdType = UNSUPPORTED)
@Index
String routeId();

@FieldType(FieldTypeEnum.ID)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,46 +15,66 @@
*/
package org.mobilitydata.gtfsvalidator.validator;

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

import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import java.util.HashSet;
import java.util.Set;
import javax.inject.Inject;
import org.mobilitydata.gtfsvalidator.annotation.GtfsValidationNotice;
import org.mobilitydata.gtfsvalidator.annotation.GtfsValidationNotice.FileRefs;
import org.mobilitydata.gtfsvalidator.annotation.GtfsValidator;
import org.mobilitydata.gtfsvalidator.notice.NoticeContainer;
import org.mobilitydata.gtfsvalidator.notice.ValidationNotice;
import org.mobilitydata.gtfsvalidator.table.GtfsFareRule;
import org.mobilitydata.gtfsvalidator.table.GtfsFareRuleSchema;
import org.mobilitydata.gtfsvalidator.table.GtfsFareRuleTableContainer;
import org.mobilitydata.gtfsvalidator.table.GtfsLocationType;
import org.mobilitydata.gtfsvalidator.table.GtfsStop;
import org.mobilitydata.gtfsvalidator.table.GtfsStopSchema;
import org.mobilitydata.gtfsvalidator.table.GtfsStopTableContainer;
import org.mobilitydata.gtfsvalidator.table.*;

/**
* Checks that all stops and platforms (location_type = 0) have {@code stops.zone_id} assigned.
* assigned if {@code fare_rules.txt} is provided and at least one of the following fields is
* provided:
*
* <ul>
* <li>{@code fare_rules.origin_id}
* <li>{@code fare_rules.contains_id}
* <li>{@code fare_rules.destination_id}
* </ul>
* If {@code fare_rules.txt} is provided, checks that all stops and platforms (location_type = 0)
* have {@code stops.zone_id} defined if the stop is defined as part of a {@code trip_id} in {@code
* stop_times.txt} whose {@code route_id} defines an {@code origin_id}, {@code destination_id}, or
* {@code contains_id} in {@code fare_rules.txt}.
*
* <p>Generated notice: {@link StopWithoutZoneIdNotice}.
*/
@GtfsValidator
public class StopZoneIdValidator extends FileValidator {

private final GtfsStopTableContainer stopTable;

private final GtfsFareRuleTableContainer fareRuleTable;
private final GtfsStopTimeTableContainer stopTimeTable;
private final GtfsTripTableContainer tripTable;
private final GtfsRouteTableContainer routeTable;

@Inject
StopZoneIdValidator(GtfsStopTableContainer stopTable, GtfsFareRuleTableContainer fareRuleTable) {
StopZoneIdValidator(
GtfsStopTableContainer stopTable,
GtfsFareRuleTableContainer fareRuleTable,
GtfsStopTimeTableContainer stopTimeTable,
GtfsTripTableContainer tripTable,
GtfsRouteTableContainer routeTable) {
this.stopTable = stopTable;
this.fareRuleTable = fareRuleTable;
this.stopTimeTable = stopTimeTable;
this.tripTable = tripTable;
this.routeTable = routeTable;
}

/**
* Checks if the {@code GtfsFareRuleTableContainer} provided as parameter has a fare structure
* that uses zones.
*
* @param fareRuleTable the {@code GtfsFareRuleTableContainer} to be checked
* @return true if the {@code GtfsFareRuleTableContainer} provided as parameter has a fare
* structure that uses zones; false otherwise.
*/
private static boolean hasFareZoneStructure(GtfsFareRuleTableContainer fareRuleTable) {
for (GtfsFareRule fareRule : fareRuleTable.getEntities()) {
if (fareRule.hasContainsId() || fareRule.hasDestinationId() || fareRule.hasOriginId()) {
return true;
}
}
return false;
}

@Override
Expand All @@ -65,43 +85,80 @@ public void validate(NoticeContainer noticeContainer) {
if (!hasFareZoneStructure(fareRuleTable)) {
return;
}

Multimap<String, GtfsFareRule> routesWithZoneFieldsDefined =
Multimaps.filterValues(
fareRuleTable.byRouteIdMap(),
fareRule ->
fareRule.hasOriginId() || fareRule.hasDestinationId() || fareRule.hasContainsId());
for (GtfsStop stop : stopTable.getEntities()) {
if (!stop.locationType().equals(GtfsLocationType.STOP)) {
continue;
}
if (!stop.hasZoneId()) {
noticeContainer.addValidationNotice(new StopWithoutZoneIdNotice(stop));
if (stop.hasZoneId()) {
continue;
}

// check that a stop without zone_id does not have a route_id in a fare_rule with
// zone-dependent fields
for (GtfsRoute route : getRoutesIncludingStop(stop)) {
if (routesWithZoneFieldsDefined.containsKey(route.routeId())) {
noticeContainer.addValidationNotice(new StopWithoutZoneIdNotice(stop));
break;
}
}
}
}

/**
* Checks if the {@code GtfsFareRuleTableContainer} provided as parameter has a fare structure
* that uses zones.
* Gets a deduplicated set of all trips which contain a stop. A trip "contains" a stop if an entry
* in {@code stop_times.txt} defines the stop and the trip.
*
* @param fareRuleTable the {@code GtfsFareRuleTableContainer} to be checked
* @return true if the {@code GtfsFareRuleTableContainer} provided as parameter has a fare
* structure that uses zones; false otherwise.
* @param stop the {@code GtfsStop} for which to get containing {@code GtfsTrip}s
* @return a {@code Set} of {@code GtfsTrip}s containing {@code stop}
*/
private static boolean hasFareZoneStructure(GtfsFareRuleTableContainer fareRuleTable) {
for (GtfsFareRule fareRule : fareRuleTable.getEntities()) {
if (fareRule.hasContainsId() || fareRule.hasDestinationId() || fareRule.hasOriginId()) {
return true;
}
private Set<GtfsTrip> getTripsIncludingStop(GtfsStop stop) {
Set<GtfsTrip> trips = new HashSet<>();
for (GtfsStopTime stopTime : stopTimeTable.byStopId(stop.stopId())) {
tripTable.byTripId(stopTime.tripId()).ifPresent(trips::add);
}
return false;
return trips;
}

/**
* Gets a deduplicated set of all routes which contain a stop. A route "contains" a stop if an
* entry in {@code trips.txt} defines the route and a trip which contains the stop.
*
* @param stop the {@code GtfsStop} for which to get containing {@code GtfsRoute}s
* @return a {@code Set} of {@code GtfsRoute}s containing {@code stop}
*/
private Set<GtfsRoute> getRoutesIncludingStop(GtfsStop stop) {
Set<GtfsRoute> routes = new HashSet<>();
for (GtfsTrip trip : getTripsIncludingStop(stop)) {
routeTable.byRouteId(trip.routeId()).ifPresent(routes::add);
}
return routes;
}

/**
* Stop without value for `stops.zone_id`.
* Stop without value for `stops.zone_id` contained in a route with a zone-dependent fare rule.
*
* <p>If `fare_rules.txt` is provided, and `fare_rules.txt` uses at least one column among
* `origin_id`, `destination_id`, and `contains_id`, then all stops and platforms (location_type =
* 0) must have `stops.zone_id` assigned.
* 0) must have `stops.zone_id` assigned if they are defined in a trip defined in a route defined
* in a fare rule which also defines at least one of `origin_id`, `destination_id`, or
* `contains_id`.
*/
@GtfsValidationNotice(
severity = ERROR,
files = @FileRefs({GtfsStopSchema.class, GtfsFareRuleSchema.class}))
severity = INFO,
files =
@FileRefs({
GtfsStopSchema.class,
GtfsStopTimeSchema.class,
GtfsTripSchema.class,
GtfsRouteSchema.class,
GtfsFareRuleSchema.class
}))
static class StopWithoutZoneIdNotice extends ValidationNotice {

/** The faulty record's id. */
Expand Down
Loading

0 comments on commit f91e524

Please sign in to comment.