All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opentripplanner.routing.impl.NycFareServiceImpl Maven / Gradle / Ivy

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 - 2024 Weber Informatics LLC | Privacy Policy