Skip to content

Commit

Permalink
Merge branch 'matt/boston-fare-corrections' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
mattwigway committed Sep 7, 2023
2 parents fe6ac7d + b0df0fc commit 1644cf7
Show file tree
Hide file tree
Showing 25 changed files with 1,976 additions and 82 deletions.
108 changes: 108 additions & 0 deletions docs/fares/boston.md

Large diffs are not rendered by default.

99 changes: 99 additions & 0 deletions docs/fares/gtfs-fares-v2.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion docs/fares/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ Finding cheapest paths is implemented in the McRAPTOR (multi-criteria RAPTOR) ro

Unfortunately, while there is a common data format for transit timetables (GTFS), no such format exists for fares. GTFS does include two different fare specifications (GTFS-fares and GTFS-fares v2), but they are not able to represent complex fare systems. As such, unless and until such a specification becomes available, Conveyal Analysis includes location-specific fare calculators for a number of locations around the world. They have their own documentation:

- [New York](newyork.html)
- [New York](newyork.md)
- Boston (documentation coming soon)
- [GTFS-Fares v2](gtfs-fares-v2.md)
38 changes: 38 additions & 0 deletions src/main/java/com/conveyal/gtfs/GTFSFeed.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
import com.conveyal.gtfs.model.CalendarDate;
import com.conveyal.gtfs.model.Entity;
import com.conveyal.gtfs.model.Fare;
import com.conveyal.gtfs.model.FareArea;
import com.conveyal.gtfs.model.FareAttribute;
import com.conveyal.gtfs.model.FareLegRule;
import com.conveyal.gtfs.model.FareNetwork;
import com.conveyal.gtfs.model.FareRule;
import com.conveyal.gtfs.model.FareTransferRule;
import com.conveyal.gtfs.model.FeedInfo;
import com.conveyal.gtfs.model.Frequency;
import com.conveyal.gtfs.model.Pattern;
Expand Down Expand Up @@ -145,6 +149,18 @@ public class GTFSFeed implements Cloneable, Closeable {
/** A fare is a fare_attribute and all fare_rules that reference that fare_attribute. TODO what is the path? */
public final Map<String, Fare> fares;

/** GTFS-Fares V2: One entry per fare area, containing all the rows for that fare area */
public final Map<String, FareArea> fare_areas;

/** GTFS-Fares V2: One entry per fare network, containing all members of that network */
public final Map<String, FareNetwork> fare_networks;

/** GTFS Fares V2: Fare leg rules */
public final NavigableSet<FareLegRule> fare_leg_rules;

/** GTFS Fares V2: Fare transfer rules */
public final NavigableSet<FareTransferRule> fare_transfer_rules;

/** A service is a calendar entry and all calendar_dates that modify that calendar entry. TODO what is the path? */
public final BTreeMap<String, Service> services;

Expand Down Expand Up @@ -242,6 +258,24 @@ public void loadFromFile(ZipFile zip, String fid) throws Exception {
// Joined Fares have been persisted to MapDB. In-memory HashMap goes out of scope for garbage collection.
}

// Read GTFS-Fares V2

// FareAreas are joined into a single object for each FareArea. Use an in-memory map since
// there will be a lot of changing of values that are immutable once placed in MapDB.
Map<String, FareArea> fare_areas = new HashMap<>();
new FareArea.Loader(this, fare_areas).loadTable(zip);
this.fare_areas.putAll(fare_areas);
fare_areas = null; // allow gc

// FareNetworks are likewise joined into single objects
Map<String, FareNetwork> fare_networks = new HashMap<>();
new FareNetwork.Loader(this, fare_networks).loadTable(zip);
this.fare_networks.putAll(fare_networks);
fare_networks = null; // allow gc

new FareLegRule.Loader(this).loadTable(zip);
new FareTransferRule.Loader(this).loadTable(zip);

// Comment out the StopTime and/or ShapePoint loaders for quick testing on large feeds.
new Route.Loader(this).loadTable(zip);
new ShapePoint.Loader(this).loadTable(zip);
Expand Down Expand Up @@ -773,6 +807,10 @@ private GTFSFeed (DB db) {
fares = db.getTreeMap("fares");
services = db.getTreeMap("services");
shape_points = db.getTreeMap("shape_points");
fare_areas = db.getTreeMap("fare_areas");
fare_networks = db.getTreeMap("fare_networks");
fare_leg_rules = db.getTreeSet("fare_leg_rules");
fare_transfer_rules = db.getTreeSet("fare_transfer_rules");

// Note that the feedId and checksum fields are manually read in and out of entries in the MapDB, rather than
// the class fields themselves being of type Atomic.String and Atomic.Long. This avoids any locking and
Expand Down
67 changes: 67 additions & 0 deletions src/main/java/com/conveyal/gtfs/model/FareArea.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.conveyal.gtfs.model;

import com.conveyal.gtfs.GTFSFeed;

import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;

/** A FareArea represents a group of stops in the GTFS Fares V2 specification */
public class FareArea extends Entity {
private static final long serialVersionUID = 1L;

public String fare_area_id;
public String fare_area_name;
public String ticketing_fare_area_id;
public Collection<FareAreaMember> members = new ArrayList<>();

public static class Loader extends Entity.Loader<FareArea> {
private Map<String, FareArea> fareAreas;

public Loader (GTFSFeed feed, Map<String, FareArea> fareAreas) {
super(feed, "fare_areas");
this.fareAreas = fareAreas;
}

@Override
protected boolean isRequired() {
return false;
}

@Override
protected void loadOneRow() throws IOException {
// Fare areas are composed of members that refer to specific stops or trip/stop combos
FareAreaMember member = new FareAreaMember();
member.stop_id = getStringField("stop_id", false);
member.trip_id = getStringField("trip_id", false);
member.stop_sequence = getIntField("stop_sequence", false, 0, Integer.MAX_VALUE, INT_MISSING);
member.sourceFileLine = row + 1;

String fareAreaId = getStringField("fare_area_id", true);

FareArea fareArea;
if (fareAreas.containsKey(fareAreaId)) {
fareArea = fareAreas.get(fareAreaId);
// TODO make sure that fare_area_name, etc all match
} else {
fareArea = new FareArea();
fareArea.fare_area_id = fareAreaId;
fareArea.fare_area_name = getStringField("fare_area_name", false);
fareArea.ticketing_fare_area_id = getStringField("ticketing_fare_area_id", false);
fareAreas.put(fareAreaId, fareArea);
}
fareArea.members.add(member);
}
}

/** What are the members of this FareArea? */
public static class FareAreaMember implements Serializable {
private static final long serialVersionUID = 1L;
public String stop_id;
public String trip_id;
public int stop_sequence;
public int sourceFileLine;
}
}
121 changes: 121 additions & 0 deletions src/main/java/com/conveyal/gtfs/model/FareLegRule.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package com.conveyal.gtfs.model;

import com.conveyal.gtfs.GTFSFeed;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Ordering;
import org.apache.commons.lang3.builder.CompareToBuilder;

import java.io.IOException;
import java.util.Objects;

/**
* A GTFS-Fares V2 FareLegRule
*/
public class FareLegRule extends Entity implements Comparable {
public static final long serialVersionUID = 1L;

public int order;
public String fare_network_id;
public String from_area_id;
public String contains_area_id;
public String to_area_id;
public int is_symmetrical;
public String from_timeframe_id;
public String to_timeframe_id;
public double min_fare_distance;
public double max_fare_distance;
public String service_id;
public double amount;
public double min_amount;
public double max_amount;
public String currency;
public String leg_group_id;

@Override
public int compareTo(Object other) {
FareLegRule o = (FareLegRule) other;
return ComparisonChain.start()
.compare(order, o.order)
.compare(fare_network_id, o.fare_network_id, Ordering.natural().nullsFirst())
.compare(from_area_id, o.from_area_id, Ordering.natural().nullsFirst())
.compare(contains_area_id, o.contains_area_id, Ordering.natural().nullsFirst())
.compare(to_area_id, o.to_area_id, Ordering.natural().nullsFirst())
.compare(is_symmetrical, o.is_symmetrical)
.compare(from_timeframe_id, o.from_timeframe_id, Ordering.natural().nullsFirst())
.compare(to_timeframe_id, o.to_timeframe_id, Ordering.natural().nullsFirst())
.compare(min_fare_distance, o.min_fare_distance)
.compare(max_fare_distance, o.max_fare_distance)
.compare(service_id, o.service_id, Ordering.natural().nullsFirst())
.compare(amount, o.amount)
.compare(min_amount, o.min_amount)
.compare(max_amount, o.max_amount)
.compare(currency, o.currency, Ordering.natural().nullsFirst())
.compare(leg_group_id, o.leg_group_id, Ordering.natural().nullsFirst())
.result();
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FareLegRule that = (FareLegRule) o;
return order == that.order &&
is_symmetrical == that.is_symmetrical &&
Double.compare(that.min_fare_distance, min_fare_distance) == 0 &&
Double.compare(that.max_fare_distance, max_fare_distance) == 0 &&
Double.compare(that.amount, amount) == 0 &&
Double.compare(that.min_amount, min_amount) == 0 &&
Double.compare(that.max_amount, max_amount) == 0 &&
Objects.equals(fare_network_id, that.fare_network_id) &&
Objects.equals(from_area_id, that.from_area_id) &&
Objects.equals(contains_area_id, that.contains_area_id) &&
Objects.equals(to_area_id, that.to_area_id) &&
Objects.equals(from_timeframe_id, that.from_timeframe_id) &&
Objects.equals(to_timeframe_id, that.to_timeframe_id) &&
Objects.equals(service_id, that.service_id) &&
Objects.equals(currency, that.currency) &&
Objects.equals(leg_group_id, that.leg_group_id);
}

@Override
public int hashCode() {
return Objects.hash(order, fare_network_id, from_area_id, contains_area_id, to_area_id, is_symmetrical,
from_timeframe_id, to_timeframe_id, min_fare_distance, max_fare_distance, service_id, amount,
min_amount, max_amount, currency, leg_group_id);
}

public static class Loader extends Entity.Loader<FareLegRule> {
public Loader (GTFSFeed feed) {
super(feed, "fare_leg_rules");
}

@Override
protected boolean isRequired() {
return false;
}

@Override
protected void loadOneRow() throws IOException {
FareLegRule rule = new FareLegRule();
rule.sourceFileLine = row + 1;
rule.order = getIntField("order", true, 0, Integer.MAX_VALUE);
rule.fare_network_id = getStringField("fare_network_id", false);
rule.from_area_id = getStringField("from_area_id", false);
rule.to_area_id = getStringField("to_area_id", false);
rule.contains_area_id = getStringField("contains_area_id", false);
rule.is_symmetrical = getIntField("is_symmetrical", false, 0, 1, 0);
rule.from_timeframe_id = getStringField("from_timeframe_id", false);
rule.to_timeframe_id = getStringField("to_timeframe_id", false);
rule.min_fare_distance = getDoubleField("min_fare_distance", false, 0, Double.MAX_VALUE);
rule.max_fare_distance = getDoubleField("max_fare_distance", false, 0, Double.MAX_VALUE);
rule.service_id = getStringField("service_id", false);
rule.amount = getDoubleField("amount", false, 0, Double.MAX_VALUE);
rule.min_amount = getDoubleField("min_amount", false, 0, Double.MAX_VALUE);
rule.max_amount = getDoubleField("max_amount", false, 0, Double.MAX_VALUE);
rule.currency = getStringField("currency", true);
rule.leg_group_id = getStringField("leg_group_id", true);

feed.fare_leg_rules.add(rule);
}
}
}
49 changes: 49 additions & 0 deletions src/main/java/com/conveyal/gtfs/model/FareNetwork.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.conveyal.gtfs.model;

import com.conveyal.gtfs.GTFSFeed;

import java.io.IOException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/** GTFS-Fares V2 FareNetwork. Not represented exactly in GTFS, but a single entry for each FareNetwork */
public class FareNetwork extends Entity {
public static final long serialVersionUID = 1L;

public String fare_network_id;
public int as_route;
public Set<String> route_ids = new HashSet<>();

public static class Loader extends Entity.Loader<FareNetwork> {
private Map<String, FareNetwork> fareNetworks;

public Loader (GTFSFeed feed, Map<String, FareNetwork> fareNetworks) {
super(feed, "fare_networks");
this.fareNetworks = fareNetworks;
}

@Override
protected boolean isRequired() {
return false;
}

@Override
protected void loadOneRow() throws IOException {
String fareNetworkId = getStringField("fare_network_id", true);

FareNetwork fareNetwork;
if (fareNetworks.containsKey(fareNetworkId)) {
fareNetwork = fareNetworks.get(fareNetworkId);
// TODO confirm as_route is consistent
} else {
fareNetwork = new FareNetwork();
fareNetwork.fare_network_id = fareNetworkId;
fareNetwork.as_route = getIntField("as_route", false, 0, 1, 0);
fareNetworks.put(fareNetworkId, fareNetwork);
}

fareNetwork.route_ids.add(getStringField("route_id", true));
}
}
}
81 changes: 81 additions & 0 deletions src/main/java/com/conveyal/gtfs/model/FareTransferRule.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.conveyal.gtfs.model;

import com.conveyal.gtfs.GTFSFeed;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Ordering;

import java.io.IOException;

public class FareTransferRule extends Entity implements Comparable {
public static final long serialVersionUID = 1L;

public int order;
public String from_leg_group_id;
public String to_leg_group_id;
public int is_symmetrical; // is_symetrical in the spec
public int spanning_limit;
public int duration_limit_type;
public int duration_limit;
public int fare_transfer_type;
public double amount;
public double min_amount;
public double max_amount;
public String currency;

@Override
public int compareTo(Object other) {
FareTransferRule o = (FareTransferRule) other;
return ComparisonChain.start()
.compare(order, o.order)
.compare(from_leg_group_id, o.from_leg_group_id, Ordering.natural().nullsFirst())
.compare(to_leg_group_id, o.to_leg_group_id, Ordering.natural().nullsFirst())
.compare(is_symmetrical, o.is_symmetrical)
.compare(spanning_limit, o.spanning_limit)
.compare(duration_limit_type, o.duration_limit_type)
.compare(duration_limit, o.duration_limit)
.compare(fare_transfer_type, o.fare_transfer_type)
.compare(amount, o.amount)
.compare(min_amount, o.min_amount)
.compare(max_amount, o.max_amount)
.compare(currency, o.currency, Ordering.natural().nullsFirst())
.result();
}

public static class Loader extends Entity.Loader<FareTransferRule> {
public Loader (GTFSFeed feed) {
super(feed, "fare_transfer_rules");
}

@Override
protected boolean isRequired() {
return false;
}

@Override
protected void loadOneRow() throws IOException {
FareTransferRule rule = new FareTransferRule();
rule.sourceFileLine = row + 1;
rule.order = getIntField("order", true, 0, Integer.MAX_VALUE);
rule.from_leg_group_id = getStringField("from_leg_group_id", false);
rule.to_leg_group_id = getStringField("to_leg_group_id", false);

// allow is_symmetrical to be misspelled is_symetrical due to typo in original spec
rule.is_symmetrical = getIntField("is_symmetrical", false, 0, 1, INT_MISSING);
if (rule.is_symmetrical == INT_MISSING) {
rule.is_symmetrical = getIntField("is_symetrical", false, 0, 1, 0);
}

rule.spanning_limit = getIntField("spanning_limit", false, 0, 1, 0);
rule.duration_limit = getIntField("duration_limit", false, 0, Integer.MAX_VALUE);
rule.duration_limit_type = getIntField("duration_limit_type", false, 0, 2, 0);
rule.fare_transfer_type = getIntField("fare_transfer_type", false, 0, 2, INT_MISSING);
// can be less than zero to represent a discount (in fact, often will be)
rule.amount = getDoubleField("amount", false, -Double.MAX_VALUE, Double.MAX_VALUE);
rule.min_amount = getDoubleField("min_amount", false, -Double.MAX_VALUE, Double.MAX_VALUE);
rule.max_amount = getDoubleField("max_amount", false, -Double.MAX_VALUE, Double.MAX_VALUE);
rule.currency = getStringField("currency", false);

feed.fare_transfer_rules.add(rule);
}
}
}
Loading

0 comments on commit 1644cf7

Please sign in to comment.