package org.opentripplanner.profile;
import com.google.common.collect.Lists;
import org.onebusaway.gtfs.model.Route;
import org.onebusaway.gtfs.model.Stop;
import org.opentripplanner.profile.fares.FareTable;
import java.awt.geom.Rectangle2D;
import java.util.Arrays;
import java.util.List;
/**
*/
public class DCFareCalculator {
private static final FareTable METRORAIL = new FareTable("org/opentripplanner/profile/fares/metrorail.csv");
private static final FareTable MARC = new FareTable("org/opentripplanner/profile/fares/marc.csv");
private static final FareTable VRE = new FareTable("org/opentripplanner/profile/fares/vre.csv");
private static final String[] metroExpress = { "J7", "J9", "P17", "P19", "W13", "W19", "11Y", "17A", "17B", "17G",
"17H", "17K", "17L", "17M", "18E", "18G", "18H", "18P", "29E", "29G", "29H", "29X" };
// geographic fare areas for MTA (Maryland) commuter buses
private static final FareArea MTA_DC = new FareArea(-77.066139,38.868166,-76.986868,38.926);
private static final FareArea MTA_SILVER_SPRING = new FareArea(-76.943069,38.838617,-76.916848,38.855997);
private static final FareArea MTA_SUITLAND = new FareArea(-77.04039,38.985653,-77.02017,39.002778);
private static final FareArea MTA_KENT_ISLAND = new FareArea(-76.360503,38.939413,-76.170122,39.012155);
private static final FareArea MTA_DAVIDSONVILLE = new FareArea(-76.669367,38.952661,-76.637353,38.970023);
private static final FareArea MTA_ANNAPOLIS = new FareArea(-76.574267,38.960267,-76.469125,39.001783);
private static final FareArea MTA_OWINGS_NORTH_BEACH = new FareArea(-76.623115,38.687874,-76.519002,38.732147);
private static final FareArea MTA_PINDELL = new FareArea(-76.772156,38.754121,-76.639891,38.830921);
private static final FareArea MTA_CHARLOTTE_HALL_WALDORF = new FareArea(-76.97628,38.435241,-76.71225,38.656712);
private static final FareArea MTA_CALIFORNIA = new FareArea(-76.571502,38.273603,-76.483254,38.3446);
private static final FareArea MTA_DUNKIRK = new FareArea(-76.677069, 38.692187, -76.635025, 38.742091);
private static final FareArea MTA_SUNDERLAND = new FareArea(-76.627974, 38.530956, -76.564987, 38.678062);
private static final FareArea MTA_ST_LEONARD = new FareArea(-76.527123, 38.458123, -76.48802, 38.495911);
private static RideType classify (Route route) {
// NOTE the agencyId string of the route's agencyAndId is not the same as the one given by route.getAgency.
// The former is the same for all routes in the feed. The latter is the true agency of the feed.
String agency = route.getAgency().getId();
String agency_url = route.getAgency().getUrl(); // this is used in single-agency feeds so it should work
String short_name = route.getShortName();
String long_name = route.getLongName();
if ("MET".equals(agency)) {
if (route.getType() == 1) return RideType.METRO_RAIL;
if ("5A".equals(short_name) || "B30".equals(short_name)) return RideType.METRO_BUS_AIRPORT;
for (String sn : metroExpress) if (sn.equals(short_name)) return RideType.METRO_BUS_EXPRESS;
return RideType.METRO_BUS_LOCAL;
} else if ("DC".equals(agency)) {
return RideType.DC_CIRCULATOR_BUS;
} else if ("MCRO".equals(agency)) {
if (short_name.equals("70")) return RideType.MCRO_BUS_EXPRESS;
else return RideType.MCRO_BUS_LOCAL;
} else if (agency_url != null) {
if (agency_url.contains("fairfaxconnector.com")) {
return RideType.FAIRFAX_CONNECTOR_BUS;
}
if (agency_url.contains("prtctransit.org")) {
return RideType.PRTC_BUS;
}
if (agency_url.contains("arlingtontransit.com")) {
return RideType.ART_BUS;
}
if (agency_url.contains("vre.org")) {
return RideType.VRE_RAIL;
}
if (agency_url.contains("mtamaryland.com")) {
if (route.getType() == 2) return RideType.MARC_RAIL;
int shortName;
try {
shortName = Integer.parseInt(route.getShortName());
}
catch(NumberFormatException ex) {
// assume a local bus if route number cannot be parsed
return RideType.MTA_BUS_LOCAL;
}
if(shortName < 100) { // local routes are 0 - 99
return RideType.MTA_BUS_LOCAL;
}
else if(shortName < 200) { // express routes are 100 - 199
return RideType.MTA_BUS_EXPRESS;
}
// commuter routes are 200+
return RideType.MTA_BUS_COMMUTER;
}
}
return null;
}
/**
* Should we have exactly one fare per ride, where some fares may have zero cost if they are transfers from the same operator?
* ...except that this doesn't work for MetroRail, where two legs combine into one.
*/
public static List<Fare> calculateFares (List<Ride> rides) {
List<FareRide> fareRides = Lists.newArrayList();
FareRide prev = null;
for (Ride ride : rides) {
// Calculate the fare for a ride based on only one of its patterns. This is usually right as long as the
// patterns are all the same mode and operator. That happens less naturally now that stops are being grouped.
// FIXME cluster stops considering mode and operator
PatternRide exemplarPatternRide = ride.patternRides.get(0);
FareRide fareRide = new FareRide(exemplarPatternRide, prev);
if (prev != null && prev.type == fareRide.type) {
prev.to = fareRide.to;
prev.calcFare(); // recalculate existing fare using new destination
} else {
fareRides.add(fareRide);
prev = fareRide;
}
}
List<Fare> fares = Lists.newArrayList();
for (FareRide fareRide : fareRides) {
fares.add(fareRide.fare);
}
return fares;
}
public static Rectangle2D.Double createFareZone(double min_lon, double max_lat, double max_lon, double min_lat) {
return new Rectangle2D.Double(min_lon, max_lat, max_lon - min_lon, max_lat - min_lat);
}
static class FareArea extends Rectangle2D.Double {
public FareArea(double min_lon, double min_lat, double max_lon, double max_lat) {
super(min_lon, min_lat, max_lon - min_lon, max_lat - min_lat);
}
public boolean containsStop(Stop stop) {
return super.contains(stop.getLon(), stop.getLat());
}
}
static class FareRide {
Stop from;
Stop to;
Route route;
RideType type;
Fare fare;
FareRide prev;
public FareRide (PatternRide pRide, FareRide prevRide) {
from = pRide.getFromStop();
to = pRide.getToStop();
// Problem: Rides no longer have a single fare because they may be on multiple routes.
// TODO: make sure Patterns in Rides are all the same mode and operator.
// This seems to happen naturally because different operators generally do not share stops.
route = pRide.pattern.route;
type = classify(route);
prev = prevRide;
calcFare();
}
private void setFare(double base, boolean transferReduction) {
fare = new Fare(base);
fare.transferReduction = transferReduction;
}
private void setFare(double low, double peak, double senior, boolean transferReduction) {
fare = new Fare(peak);
fare.low = low;
fare.senior = senior;
fare.transferReduction = transferReduction;
}
// TODO store rule-based Fares in a table keyed on (type, prevtype) instead of doing on the fly
// automatically compose string using 'free' or 'discounted' and route name
private void calcFare() {
RideType prevType = (prev == null) ? null : prev.type;
if (type == null)
return;
switch (type) {
case METRO_RAIL:
fare = METRORAIL.lookup(from, to);
if (prevType == RideType.METRO_BUS_LOCAL || prevType == RideType.METRO_BUS_EXPRESS || // TODO merge local and express categories
prevType == RideType.MCRO_BUS_LOCAL || prevType == RideType.MCRO_BUS_EXPRESS) { // TODO merge local and express categories
fare.discount(0.50);
}
break;
case METRO_BUS_LOCAL:
if (prevType == RideType.DASH_BUS) {
setFare(0.00, true);
} else if (prevType == RideType.METRO_BUS_EXPRESS || prevType == RideType.METRO_BUS_AIRPORT) {
setFare(0.00, true);
} else if (prevType == RideType.MCRO_BUS_LOCAL || prevType == RideType.MCRO_BUS_EXPRESS) {
setFare(0.00, true);
} else if (prevType == RideType.METRO_RAIL) {
setFare(1.10, true);
} else if (prevType == RideType.ART_BUS) {
setFare(0.10, true);
} else {
setFare(1.60, false);
}
break;
case METRO_BUS_EXPRESS:
if (prevType == RideType.METRO_BUS_LOCAL) {
setFare(2.05, true);
} else {
setFare(3.65, false);
}
break;
case METRO_BUS_AIRPORT:
setFare(6.00, false);
break;
case DC_CIRCULATOR_BUS :
if (prevType == RideType.METRO_BUS_LOCAL || prevType == RideType.METRO_BUS_EXPRESS ||
prevType == RideType.METRO_BUS_AIRPORT || prevType == RideType.ART_BUS) {
setFare(0.00, true);
} else if (prevType == RideType.METRO_RAIL) {
setFare(0.50, true);
} else {
setFare(1.00, false);
}
break;
case ART_BUS:
if (prevType == RideType.METRO_BUS_LOCAL || prevType == RideType.METRO_BUS_EXPRESS) {
setFare(0.00, true);
} else if (prevType == RideType.METRO_RAIL) {
setFare(1.00, true);
} else {
setFare(1.50, false);
}
break;
case DASH_BUS:
if (prevType == RideType.METRO_BUS_LOCAL || prevType == RideType.METRO_BUS_EXPRESS) {
setFare(0.00, true);
} else {
setFare(1.60, false);
}
break;
case MARC_RAIL:
fare = MARC.lookup(from, to);
break;
case VRE_RAIL:
fare = VRE.lookup(from, to);
break;
case MCRO_BUS_LOCAL:
if (prevType == RideType.MCRO_BUS_EXPRESS) {
setFare(0.00, true);
} else if (prevType == RideType.METRO_RAIL) {
setFare(1.10, true);
} else {
setFare(1.60, false);
}
break;
case MCRO_BUS_EXPRESS:
if (prevType == RideType.MCRO_BUS_LOCAL) {
setFare(2.05, true);
} else if (prevType == RideType.METRO_RAIL) {
setFare(3.15, true);
} else {
setFare(3.65, false);
}
break;
case FAIRFAX_CONNECTOR_BUS:
String routeName = route.getShortName();
if (routeName.equals("394") || routeName.equals("395")) {
setFare(3.65, false);
} else if (routeName.equals("480")) {
setFare(5.00, false);
} else if (routeName.equals("595") || routeName.equals("597")) {
setFare(7.50, false);
}
break;
case PRTC_BUS:
routeName = route.getLongName();
if (prevType == RideType.VRE_RAIL) {
setFare(0.00, true);
} else if (routeName.contains("omniride")) {
setFare(5.75, false);
} else if (routeName.contains("omnilink") || routeName.contains("connector")) {
setFare(1.30, false);
} else if (routeName.contains("metro direct")) {
setFare(2.90, false);
}
break;
case MTA_BUS_LOCAL:
setFare(1.60, false);
break;
case MTA_BUS_EXPRESS:
setFare(2.00, false);
break;
case MTA_BUS_COMMUTER:
String shortName = route.getShortName();
double mtaDefault = 0;
switch(shortName) {
// $5.00 flat-fare routes
case "201":
case "202":
case "203":
case "204":
case "240":
setFare(5.00, false);
break;
// $4.25 flat-fare routes
case "230":
case "260":
case "335":
case "345":
case "610":
case "620":
case "630":
case "640":
case "650":
setFare(4.25, false);
break;
// $3.50 flat-fare routes
case "310":
case "410":
setFare(3.50, false);
break;
// variable-fare routes
case "220":
if(MTA_KENT_ISLAND.containsStop(from) || MTA_KENT_ISLAND.containsStop(to)) {
setFare(5.00, false);
}
else if(MTA_ANNAPOLIS.containsStop(from) || MTA_ANNAPOLIS.containsStop(to)) {
setFare(4.25, false);
}
else {
setFare(mtaDefault, false);
}
break;
case "250":
if(MTA_KENT_ISLAND.containsStop(from) || MTA_KENT_ISLAND.containsStop(to)) {
setFare(5.00, false);
}
else if(MTA_DAVIDSONVILLE.containsStop(from) || MTA_DAVIDSONVILLE.containsStop(to)) {
setFare(4.25, false);
}
else {
setFare(mtaDefault, false);
}
break;
case "305":
case "315":
case "325":
if(MTA_DC.containsStop(from) || MTA_DC.containsStop(to)) {
setFare(4.25, false);
}
else if(MTA_SILVER_SPRING.containsStop(from) || MTA_SILVER_SPRING.containsStop(to)) {
setFare(3.50, false);
}
else {
setFare(mtaDefault, false);
}
break;
case "902":
if(MTA_DUNKIRK.containsStop(from) || MTA_DUNKIRK.containsStop(to)) {
setFare(3.50, false);
}
else if(MTA_SUNDERLAND.containsStop(from) || MTA_SUNDERLAND.containsStop(to)) {
setFare(4.25, false);
}
else if(MTA_ST_LEONARD.containsStop(from) || MTA_ST_LEONARD.containsStop(to)) {
setFare(5.00, false);
}
else {
setFare(mtaDefault, false);
}
break;
case "903":
if(MTA_DC.containsStop(from) || MTA_DC.containsStop(to)) {
setFare(4.25, false);
}
else if(MTA_SUITLAND.containsStop(from) || MTA_SUITLAND.containsStop(to)) {
setFare(3.50, false);
}
else {
setFare(mtaDefault, false);
}
break;
case "904":
if(MTA_PINDELL.containsStop(from) || MTA_PINDELL.containsStop(to)) {
setFare(3.50, false);
}
else if(MTA_OWINGS_NORTH_BEACH.containsStop(from) || MTA_OWINGS_NORTH_BEACH.containsStop(to)) {
setFare(4.25, false);
}
else {
setFare(mtaDefault, false);
}
break;
case "905":
case "909":
if(MTA_CHARLOTTE_HALL_WALDORF.containsStop(from) || MTA_CHARLOTTE_HALL_WALDORF.containsStop(to)) {
setFare(4.25, false);
}
else if(MTA_CALIFORNIA.containsStop(from) || MTA_CALIFORNIA.containsStop(to)) {
setFare(5.75, false);
}
else {
setFare(mtaDefault, false);
}
break;
default:
setFare(mtaDefault, false);
}
break;
default:
setFare(0.00, false);
}
if (fare != null) fare.type = type;
}
}
public static class Fare {
public RideType type;
public double low;
public double peak;
public double senior;
public boolean transferReduction;
public Fare (Fare other) {
this.accumulate(other);
}
public Fare (double base) {
low = peak = senior = base;
}
public Fare (double low, double peak, double senior) {
this.low = low;
this.peak = peak;
this.senior = senior;
}
public void accumulate (Fare other) {
if (other != null) {
low += other.low;
peak += other.peak;
senior += other.senior;
}
}
public void discount(double amount) {
low -= amount;
peak -= amount;
senior -= amount;
transferReduction = true;
}
}
enum RideType {
METRO_RAIL,
METRO_BUS_LOCAL,
METRO_BUS_EXPRESS,
METRO_BUS_AIRPORT,
DC_CIRCULATOR_BUS,
ART_BUS,
DASH_BUS,
MARC_RAIL,
MTA_BUS_LOCAL,
MTA_BUS_EXPRESS,
MTA_BUS_COMMUTER,
VRE_RAIL,
MCRO_BUS_LOCAL,
MCRO_BUS_EXPRESS,
FAIRFAX_CONNECTOR_BUS,
PRTC_BUS
}
}