org.opentripplanner.routing.impl.NycFareServiceImpl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of otp Show documentation
Show all versions of otp Show documentation
The OpenTripPlanner multimodal journey planning system
package org.opentripplanner.routing.impl;
import org.opentripplanner.model.FeedScopedId;
import org.opentripplanner.model.Route;
import org.opentripplanner.model.TripPattern;
import org.opentripplanner.routing.algorithm.raptor.transit.TransitLayer;
import org.opentripplanner.routing.algorithm.raptor.transit.TripSchedule;
import org.opentripplanner.routing.core.Fare;
import org.opentripplanner.routing.core.Fare.FareType;
import org.opentripplanner.routing.core.WrappedCurrency;
import org.opentripplanner.routing.services.FareService;
import org.opentripplanner.transit.raptor.api.path.Path;
import org.opentripplanner.transit.raptor.api.path.PathLeg;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Currency;
import java.util.List;
enum NycFareState {
INIT,
SUBWAY_PRE_TRANSFER,
SUBWAY_PRE_TRANSFER_WALKED,
SUBWAY_POST_TRANSFER,
SIR_PRE_TRANSFER,
SIR_POST_TRANSFER_FROM_SUBWAY,
SIR_POST_TRANSFER_FROM_BUS,
EXPENSIVE_EXPRESS_BUS,
BUS_PRE_TRANSFER, CANARSIE,
}
enum NycRideClassifier {
SUBWAY,
SIR,
LOCAL_BUS,
EXPRESS_BUS,
EXPENSIVE_EXPRESS_BUS,
WALK
}
/**
* This handles the New York City MTA's baroque fare rules for subways and buses
* with the following limitations:
* (1) the two hour limit on transfers is not enforced
* (2) the b61/b62 special case is not handled
* (3) MNR, LIRR, and LI Bus are not supported -- only subways and buses
*
* I have not yet tested this on NY data since we switched to OTP2 (Raptor). It may need to be fixed.
* The only thing I've changed is how we produce rides from PathLegs instead of AStar states.
* The actual fare calculation logic remains exactly the same except for one thing: thanks to
* switching to typesafe enums, I fixed one bug where we were adding the enum value instead of the
* fare to the total cost.
*/
public class NycFareServiceImpl implements FareService {
private static final Logger LOG = LoggerFactory.getLogger(NycFareServiceImpl.class);
private static final long serialVersionUID = 1L;
private static final float ORDINARY_FARE = 2.75f;
private static final float EXPRESS_FARE = 6.50f;
private static final float EXPENSIVE_EXPRESS_FARE = 7.50f; // BxM4C only
private static final List SIR_PAID_STOPS = makeMtaStopList("S31", "S30");
private static final List SUBWAY_FREE_TRANSFER_STOPS = makeMtaStopList(
"R11", "B08", "629");
private static final List SIR_BONUS_STOPS = makeMtaStopList("140", "420",
"419", "418", "M22", "M23", "R27", "R26");
private static final List SIR_BONUS_ROUTES = makeMtaStopList("M5", "M20",
"M15-SBS");
private static final List CANARSIE = makeMtaStopList("L29", "303345");
// List of NYC agencies to set fares for
private static final List AGENCIES = Arrays.asList("MTABC", "MTA NYCT");
public NycFareServiceImpl() { }
@Override
public Fare getCost(Path path, TransitLayer transitLayer) {
// Use custom ride-categorizing method instead of the usual mapper from default fare service.
List rides = createRides(path, transitLayer);
// There are no rides, so there's no fare.
if (rides.size() == 0) {
return null;
}
NycFareState state = NycFareState.INIT;
boolean lexFreeTransfer = false;
boolean canarsieFreeTransfer = false;
boolean siLocalBus = false;
boolean sirBonusTransfer = false;
float totalFare = 0;
for (Ride ride : rides) {
FeedScopedId firstStopId = null;
FeedScopedId lastStopId = null;
if (ride.firstStop != null) {
firstStopId = ride.firstStop.getId();
lastStopId = ride.lastStop.getId();
}
switch (state) {
case INIT:
lexFreeTransfer = siLocalBus = canarsieFreeTransfer = false;
if (ride.classifier.equals(NycRideClassifier.WALK)) {
// walking keeps you in init
} else if (ride.classifier.equals(NycRideClassifier.SUBWAY)) {
state = NycFareState.SUBWAY_PRE_TRANSFER;
totalFare += ORDINARY_FARE;
if (SUBWAY_FREE_TRANSFER_STOPS.contains(ride.lastStop.getId())) {
lexFreeTransfer = true;
}
if (CANARSIE.contains(ride.lastStop.getId())) {
canarsieFreeTransfer = true;
}
} else if (ride.classifier.equals(NycRideClassifier.SIR)) {
state = NycFareState.SIR_PRE_TRANSFER;
if (SIR_PAID_STOPS.contains(firstStopId)
|| SIR_PAID_STOPS.contains(lastStopId)) {
totalFare += ORDINARY_FARE;
}
} else if (ride.classifier.equals(NycRideClassifier.LOCAL_BUS)) {
state = NycFareState.BUS_PRE_TRANSFER;
totalFare += ORDINARY_FARE;
if (CANARSIE.contains(ride.lastStop.getId())) {
canarsieFreeTransfer = true;
}
siLocalBus = ride.route.getId().startsWith("S");
} else if (ride.classifier.equals(NycRideClassifier.EXPRESS_BUS)) {
state = NycFareState.BUS_PRE_TRANSFER;
totalFare += EXPRESS_FARE;
} else if (ride.classifier.equals(NycRideClassifier.EXPENSIVE_EXPRESS_BUS)) {
state = NycFareState.EXPENSIVE_EXPRESS_BUS;
totalFare += EXPENSIVE_EXPRESS_FARE;
}
break;
case SUBWAY_PRE_TRANSFER_WALKED:
if (ride.classifier.equals(NycRideClassifier.SUBWAY)) {
// subway-to-subway transfers are verbotten except at
// lex and 59/63
if (!(lexFreeTransfer && SUBWAY_FREE_TRANSFER_STOPS
.contains(ride.firstStop.getId()))) {
totalFare += ORDINARY_FARE;
}
lexFreeTransfer = canarsieFreeTransfer = false;
if (SUBWAY_FREE_TRANSFER_STOPS.contains(ride.lastStop.getId())) {
lexFreeTransfer = true;
}
if (CANARSIE.contains(ride.lastStop.getId())) {
canarsieFreeTransfer = true;
}
}
/* FALL THROUGH */
case SUBWAY_PRE_TRANSFER:
// it will always be possible to transfer from the first subway
// trip to anywhere,
// since no sequence of subway trips takes greater than two
// hours (if only just)
if (ride.classifier.equals(NycRideClassifier.WALK)) {
state = NycFareState.SUBWAY_PRE_TRANSFER_WALKED;
} else if (ride.classifier.equals(NycRideClassifier.SIR)) {
state = NycFareState.SIR_POST_TRANSFER_FROM_SUBWAY;
} else if (ride.classifier.equals(NycRideClassifier.LOCAL_BUS)) {
if (CANARSIE.contains(ride.firstStop.getId())
&& canarsieFreeTransfer) {
state = NycFareState.BUS_PRE_TRANSFER;
} else {
state = NycFareState.INIT;
}
} else if (ride.classifier.equals(NycRideClassifier.EXPRESS_BUS)) {
// need to pay the upgrade cost
totalFare += EXPRESS_FARE - ORDINARY_FARE;
} else if (ride.classifier.equals(NycRideClassifier.EXPENSIVE_EXPRESS_BUS)) {
totalFare += EXPENSIVE_EXPRESS_FARE; // no transfers to the
// BxMM4C
}
break;
case BUS_PRE_TRANSFER:
if (ride.classifier.equals(NycRideClassifier.SUBWAY)) {
if (CANARSIE.contains(ride.firstStop.getId())
&& canarsieFreeTransfer) {
state = NycFareState.SUBWAY_PRE_TRANSFER;
} else {
state = NycFareState.INIT;
}
} else if (ride.classifier.equals(NycRideClassifier.SIR)) {
if (siLocalBus) {
// SI local bus to SIR, so it is as if we started on the
// SIR (except that when we enter the bus or subway system we need to do
// so at certain places)
sirBonusTransfer = true;
state = NycFareState.SIR_PRE_TRANSFER;
} else {
//transfers exhausted
state = NycFareState.INIT;
}
} else if (ride.classifier.equals(NycRideClassifier.LOCAL_BUS)) {
state = NycFareState.INIT;
} else if (ride.classifier.equals(NycRideClassifier.EXPRESS_BUS)) {
// need to pay the upgrade cost
totalFare += EXPRESS_FARE - ORDINARY_FARE;
state = NycFareState.INIT;
} else if (ride.classifier.equals(NycRideClassifier.EXPENSIVE_EXPRESS_BUS)) {
totalFare += EXPENSIVE_EXPRESS_FARE;
// no transfers to the BxMM4C
}
break;
case SIR_PRE_TRANSFER:
if (ride.classifier.equals(NycRideClassifier.SUBWAY)) {
if (sirBonusTransfer && !SIR_BONUS_STOPS.contains(ride.firstStop.getId())) {
//we were relying on the bonus transfer to be in the "pre-transfer state",
//but the bonus transfer does not apply here
totalFare += ORDINARY_FARE;
}
if (CANARSIE.contains(ride.lastStop.getId())) {
canarsieFreeTransfer = true;
}
state = NycFareState.SUBWAY_POST_TRANSFER;
} else if (ride.classifier.equals(NycRideClassifier.SIR)) {
/* should not happen, and unhandled */
LOG.warn("Should not transfer from SIR to SIR");
} else if (ride.classifier.equals(NycRideClassifier.LOCAL_BUS)) {
if (!SIR_BONUS_ROUTES.contains(ride.route)) {
totalFare += ORDINARY_FARE;
}
state = NycFareState.BUS_PRE_TRANSFER;
} else if (ride.classifier.equals(NycRideClassifier.EXPRESS_BUS)) {
totalFare += EXPRESS_FARE;
state = NycFareState.BUS_PRE_TRANSFER;
} else if (ride.classifier.equals(NycRideClassifier.EXPENSIVE_EXPRESS_BUS)) {
totalFare += EXPENSIVE_EXPRESS_FARE;
state = NycFareState.BUS_PRE_TRANSFER;
}
break;
case SIR_POST_TRANSFER_FROM_SUBWAY:
if (ride.classifier.equals(NycRideClassifier.SUBWAY)) {
/* should not happen */
totalFare += ORDINARY_FARE;
state = NycFareState.SUBWAY_PRE_TRANSFER;
} else if (ride.classifier.equals(NycRideClassifier.SIR)) {
/* should not happen, and unhandled */
LOG.warn("Should not transfer from SIR to SIR");
} else if (ride.classifier.equals(NycRideClassifier.LOCAL_BUS)) {
if (!ride.route.getId().startsWith("S")) {
totalFare += ORDINARY_FARE;
state = NycFareState.BUS_PRE_TRANSFER;
} else {
state = NycFareState.INIT;
}
} else if (ride.classifier.equals(NycRideClassifier.EXPRESS_BUS)) {
// need to pay the full cost
totalFare += EXPRESS_FARE;
state = NycFareState.INIT;
} else if (ride.classifier.equals(NycRideClassifier.EXPENSIVE_EXPRESS_BUS)) {
/* should not happen */
// no transfers to the BxMM4C
totalFare += EXPENSIVE_EXPRESS_FARE;
state = NycFareState.BUS_PRE_TRANSFER;
}
break;
case SUBWAY_POST_TRANSFER:
if (ride.classifier.equals(NycRideClassifier.WALK)) {
if (!canarsieFreeTransfer) {
/* note: if we end up walking to another subway after alighting
* at Canarsie, we will mistakenly not be charged, but nobody
* would ever do this */
state = NycFareState.INIT;
}
} else if (ride.classifier.equals(NycRideClassifier.SIR)) {
totalFare += ORDINARY_FARE;
state = NycFareState.SIR_PRE_TRANSFER;
} else if (ride.classifier.equals(NycRideClassifier.LOCAL_BUS)) {
if (!(CANARSIE.contains(ride.firstStop.getId())
&& canarsieFreeTransfer)) {
totalFare += ORDINARY_FARE;
}
state = NycFareState.INIT;
} else if (ride.classifier.equals(NycRideClassifier.SUBWAY)) {
//walking transfer
totalFare += ORDINARY_FARE;
state = NycFareState.SUBWAY_PRE_TRANSFER;
} else if (ride.classifier.equals(NycRideClassifier.EXPRESS_BUS)) {
totalFare += EXPRESS_FARE;
state = NycFareState.BUS_PRE_TRANSFER;
} else if (ride.classifier.equals(NycRideClassifier.EXPENSIVE_EXPRESS_BUS)) {
totalFare += EXPENSIVE_EXPRESS_FARE;
state = NycFareState.BUS_PRE_TRANSFER;
}
}
}
Currency currency = Currency.getInstance("USD");
Fare fare = new Fare();
fare.addFare(FareType.regular, new WrappedCurrency(currency),
(int) Math.round(totalFare
* Math.pow(10, currency.getDefaultFractionDigits())));
return fare;
}
private static List createRides (Path path, TransitLayer transitLayer) {
List rides = new ArrayList<>();
for (PathLeg leg = path.accessLeg(); ! leg.isEgressLeg(); leg = leg.nextLeg()) {
if (leg.isTransferLeg()) {
Ride ride = new Ride();
ride.classifier = NycRideClassifier.WALK;
rides.add(ride);
continue;
} else if (leg.isTransitLeg()) {
Ride ride = RideMapper.rideForTransitPathLeg(leg.asTransitLeg(), transitLayer);
// It seems like we should do something more sophisticated than just ignore
// agency IDs we don't recognize.
if (!AGENCIES.contains(ride.agency)) {
continue;
}
TripPattern tripPattern = leg.asTransitLeg().trip().getOriginalTripPattern();
Route route = tripPattern.route;
int routeType = route.getType();
// Note the old implementation directly used the ints as classifiers here.
if (routeType == 1) {
ride.classifier = NycRideClassifier.SUBWAY;
} else if (routeType == 2) {
// All rail is Staten Island Railway? This won't work for LIRR and MNRR.
ride.classifier = NycRideClassifier.SIR;
} else if (routeType == 3) {
ride.classifier = NycRideClassifier.LOCAL_BUS;
}
String shortName = route.getShortName();
if (shortName == null ) {
ride.classifier = NycRideClassifier.SUBWAY;
} else if (shortName.equals("BxM4C")) {
ride.classifier = NycRideClassifier.EXPENSIVE_EXPRESS_BUS;
} else if (shortName.startsWith("X")
|| shortName.startsWith("BxM")
|| shortName.startsWith("QM")
|| shortName.startsWith("BM")) {
ride.classifier = NycRideClassifier.EXPRESS_BUS;
}
}
}
return rides;
}
private static List makeMtaStopList(String... stops) {
ArrayList out = new ArrayList();
for (String stop : stops) {
out.add(new FeedScopedId("MTA NYCT", stop));
out.add(new FeedScopedId("MTA NYCT", stop + "N"));
out.add(new FeedScopedId("MTA NYCT", stop + "S"));
}
return out;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy