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

fix: make stop_without_zone_id conditional on fare rule type (#1663) #1682

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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 @@ -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 @@ -17,44 +17,64 @@

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

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,39 +85,69 @@ 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revised notice description text (open to suggestions for improvement):

A fare rule in fare_rules.txt is provided and uses at least one column among origin_id, destination_id and contains_id, but does not assign stops.zone_id for all associated stops and platforms (location_type = 0).

* `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,
Expand Down
Loading
Loading