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

Corrections to Boston fare router #655

Draft
wants to merge 46 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
c0ad73e
refactor(point-to-point): remove unnecessary CORS headers, all use of…
mattwigway Nov 13, 2020
f4e2823
feat(gtfs): read some components of gtfs-fares v2 (#124)
mattwigway Jul 11, 2020
b5af23e
refactor(gtfs): remove * import
mattwigway Jul 11, 2020
20ae03d
feat(fares-v2): load subset of GTFS fares-v2 into transport network (…
mattwigway Jul 12, 2020
0db2b87
feat(fares-v2): first implementation of fares v2 router; too slow (#124)
mattwigway Jul 12, 2020
87e09e0
feat(fares-v2): use RoaringBitSets for tractability (#124).
mattwigway Jul 13, 2020
4818578
fix(fares-v2): better log statements
mattwigway Jul 21, 2020
1eab3b5
fix(fares-v2): allow is_symmetrical fare leg rules correctly (#124)
mattwigway Jul 21, 2020
87e5e80
feat(fares-v2): improve performance with caching (#124)
mattwigway Jul 21, 2020
65eab0a
feat(fares): properly handle as_route fares in transfer allowance
mattwigway Jul 21, 2020
e4d570f
feat(fares-v2): improve performance by precalculating fare transfer r…
mattwigway Jul 25, 2020
33b75aa
fix(fares-v2): use new fareTransferRuleForLegGroupId in transfer allo…
mattwigway Jul 26, 2020
a5e4766
feat(fares-v2): add LRU cache for better performance finding transfer…
mattwigway Jul 26, 2020
78db523
feat(fares-v2): simplify fare network finding
mattwigway Jul 26, 2020
629a3e1
feat(fares-v2): add hack to compute as_route fare based on all statio…
mattwigway Jul 26, 2020
0931547
feat(fares-v2): improve performance for useAllStopsWhenCalculatingAsR…
mattwigway Jul 28, 2020
f810c77
docs(gtfs-fares-v2): update comment in faresv2 transfer allowance
mattwigway Aug 3, 2020
25d4517
docs(fares-v2): additional comment updates.
mattwigway Aug 3, 2020
ca2414c
docs(fares-v2): additional comments
mattwigway Aug 3, 2020
30aa94d
fix(fares-v2): round currency instead of flooring, workaround for #131
mattwigway Aug 3, 2020
fb23694
feat(gtfs-fares-v2): make the search process a lot faster by replacin…
mattwigway Aug 5, 2020
98745fa
docs(fares-v2): add docs for fares-v2 calculator
mattwigway Aug 5, 2020
e4b0e84
feat(fares-v2): lower max transfer allowance to better support ttc fares
mattwigway Aug 6, 2020
65d0cbd
fix(fares-v2): bounds check, fixes #135
mattwigway Aug 7, 2020
97dc47f
docs(fares): update fares index
mattwigway Aug 7, 2020
413e356
refactor: rm unused imports and stray comment
ansoncfit Aug 5, 2020
94d1c77
refactor(fares-v2): rm unused variable
mattwigway Aug 28, 2020
7538dcd
fix(boston-fares): allow behind-the-gates transfers even after a tran…
mattwigway Apr 12, 2020
07d1ac8
fix(boston-fares): don't allow out-of-system subway transfers (fixes …
mattwigway Apr 13, 2020
16e5a03
fix(boston-fares): correctly compare out-of-subway fares (fixes #593)…
mattwigway Apr 13, 2020
b437360
fix(fares): throw UnsupportedOperationException when tightenExpiratio…
mattwigway May 11, 2020
85eb964
feat(boston-fares): mark fields in BostonTransferAllowance public so …
mattwigway Jun 18, 2020
3d0b471
fix(fares): throw UnsupportedOperationException when superclass tight…
mattwigway Jun 18, 2020
fc7b0c8
fix(boston-fares): allow same-direction transfers on commuter rail.
mattwigway Jun 18, 2020
7e3dd62
Revert "fix(boston-fares): allow same-direction transfers on commuter…
mattwigway Jul 2, 2020
59c73a8
fix(boston-fares): avoid floating point roundoff
mattwigway Aug 4, 2020
a6e34af
fix(bos-fares): make sure that max transfer allowance is set correctl…
mattwigway Aug 28, 2020
5680700
fix(boston-fares): treat SL3 to Blue Line transfer at Airport like a …
mattwigway Sep 3, 2020
640d547
fix(boston-fares): do not allow virtual behind gates transfer at airp…
mattwigway Sep 3, 2020
6061e38
fix(boston-fares): correctly handle transfers to SL_FREE
mattwigway Sep 3, 2020
48a17ac
fix(boston-fares): kep track of whether rider entered the subway by p…
mattwigway Sep 3, 2020
adc2d1e
feat(transitlayer): allow retaining shapes when building transportnet…
mattwigway Nov 17, 2020
0536add
docs(boston-fares): incomplete boston fare docs
mattwigway Nov 20, 2020
5e8a656
docs(boston-fares): finish boston fare docs
mattwigway Nov 24, 2020
29affbb
fix(http-api): remove cors in local mode (see proxy-backend branch of…
mattwigway Nov 25, 2020
6c13a80
fix(cors): proxy requests to grids through frontend
mattwigway Nov 25, 2020
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
107 changes: 107 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)
25 changes: 16 additions & 9 deletions src/main/java/com/conveyal/analysis/components/HttpApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,11 @@ private spark.Service configureSparkService () {
// Or now with non-static Spark we can run two HTTP servers on different ports.

// Set CORS headers, to allow requests to this API server from any page.
res.header("Access-Control-Allow-Origin", "*");
// but do not do this when running offline with no auth, as this may allow a malicious website to use a local
// browser to access the analysis server.
if (!config.offline()) {
res.header("Access-Control-Allow-Origin", "*");
}

// The default MIME type is JSON. This will be overridden by the few controllers that do not return JSON.
res.type("application/json");
Expand Down Expand Up @@ -120,14 +124,17 @@ private spark.Service configureSparkService () {
});

// Handle CORS preflight requests (which are OPTIONS requests).
sparkService.options("/*", (req, res) -> {
res.header("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,OPTIONS");
res.header("Access-Control-Allow-Credentials", "true");
res.header("Access-Control-Allow-Headers", "Accept,Authorization,Content-Type,Origin," +
"X-Requested-With,Content-Length,X-Conveyal-Access-Group"
);
return "OK";
});
// except when running in offline mode (see above comment about auth and CORS)
if (!config.offline()) {
sparkService.options("/*", (req, res) -> {
res.header("Access-Control-Allow-Methods", "GET,PUT,POST,DELETE,OPTIONS");
res.header("Access-Control-Allow-Credentials", "true");
res.header("Access-Control-Allow-Headers", "Accept,Authorization,Content-Type,Origin," +
"X-Requested-With,Content-Length,X-Conveyal-Access-Group"
);
return "OK";
});
}

// Allow client to fetch information about the backend build version.
sparkService.get(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public LocalComponents () {
taskScheduler = new TaskScheduler(config);
fileStorage = new LocalFileStorage(
config.localCacheDirectory(),
String.format("http://localhost:%s/files", config.serverPort())
"/api/backend/files" // proxied by analysis-ui
);
gtfsCache = new GTFSCache(fileStorage, config);
osmCache = new OSMCache(fileStorage, config);
Expand Down
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 @@ -6,8 +6,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 @@ -144,6 +148,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 @@ -228,6 +244,24 @@ else if (feedId == null || feedId.isEmpty()) {
// Joined Fares have been persisted to MapDB. Release in-memory HashMap for garbage collection.
fares = null;

// 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 @@ -812,6 +846,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));
}
}
}
Loading