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

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

There is a newer version: 2.5.0
Show newest version
package org.opentripplanner.routing.fares.impl;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Currency;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.opentripplanner.model.FeedScopedId;
import org.opentripplanner.model.Route;
import org.opentripplanner.model.plan.Itinerary;
import org.opentripplanner.model.plan.Leg;
import org.opentripplanner.routing.core.Fare;
import org.opentripplanner.routing.core.Fare.FareType;
import org.opentripplanner.routing.core.WrappedCurrency;
import org.opentripplanner.routing.fares.FareService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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(Itinerary itinerary) {

		// Use custom ride-categorizing method instead of the usual mapper from default fare service.
		List rides = createRides(itinerary);

		// 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(Itinerary itinerary) {
		return itinerary.legs.stream()
				.map(leg -> mapToRide(itinerary, leg))
				.filter(Objects::nonNull)
				.collect(Collectors.toList());
	}

	private static Ride mapToRide(Itinerary itinerary, Leg leg) {
		// It seems like we should do something more sophisticated than just ignore
		// agency IDs we don't recognize.
		if (!AGENCIES.contains(leg.getAgency().getId().getFeedId())) {
			return null;
		} else if (isTransferLeg(leg, itinerary)) {
			Ride ride = new Ride();
			ride.classifier = NycRideClassifier.WALK;
			return ride;
		} else if (leg.isTransitLeg()) {

			Ride ride = RideMapper.rideForTransitPathLeg(leg);
			Route route = leg.getRoute();
			int routeType = route.getGtfsType();

			// 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 ride;
		}
		return null;
	}

	private static boolean isTransferLeg(Leg leg, Itinerary itinerary) {
		return !itinerary.firstLeg().equals(leg) && !itinerary.lastLeg().equals(leg) && leg.isWalkingLeg();
	}

	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